viperML / wrapper-manager

Post-modern configuration management
https://viperml.github.io/wrapper-manager/
European Union Public License 1.2
210 stars 3 forks source link

Feature request: Add support for Bernstein chained wrappers #18

Open jmickelin opened 2 months ago

jmickelin commented 2 months ago

While the common use case for nixkpkgs.makeWrapper is to add arguments to the end of a command line string or change environment variables, I think it would make sense to also provide plumbing for creating "Bernstein chain"-style wrappers. These are the ones where you pass your program as an argument to a wrapping command, which then invokes it for you in some way. Like so:

Instead of

$ my-cool-command --foo bar

you run

$ wrapper my-cool-command --foo bar

Common examples are su, sudo and ssh, but more relevant for Nix and home-manager are programs like nixGL or boxxy. The first of which in particular seems to have a recurring demand within the community for a good way to create wrappers:

While the above threads have many proposed solutions, I think it would be nice to consolidate efforts, especially with the special care needed to ensure things like .desktop files point to the wrapper script (something which is overlooked in at least some of said solutions).


Findings and my suggested solution

It's not at all obvious at first, but Nix's nixpkgs.makeWrapper does allow you to write these sorts of prefix wrappers, if you use the makeWrapper/makeShellWrapper function rather than the more specialized wrapProgram/wrapShellProgram. These versions allow you to specify a different name of the wrapped program than the one you are wrapping.

Here is an example I wrote to wrap programs with boxxy on my machine (though it is really generic enough to factor the actual wrapper program out already):

# wrap the binary for pkg in a boxxy call
{ boxxy
, copyDesktopItems
, lib
, makeWrapper
, stdenv

, pkg
, binaryName ? lib.meta.getExe pkg
}:

let
  binaryBaseName = builtins.baseNameOf binaryName;
in

stdenv.mkDerivation rec {
    pname = "${pkg.pname}-boxxy";

    inherit (pkg) version meta;

    src = pkg;

    nativeBuildInputs = [ makeWrapper copyDesktopItems ];
    desktopItems = [ pkg ];

    postBuild = ''
      mkdir -p $out/bin
      makeShellWrapper ${lib.meta.getExe boxxy} $out/bin/${binaryBaseName} --add-flags ${binaryName}
    '';

    preFixup = ''
      shopt -s nullglob
      for desktopFile in $out/share/applications/*
      do 
        substituteInPlace "$desktopFile" --replace ${binaryName} $out/bin/${binaryBaseName}
      done   
      shopt -u nullglob
     '';
  }

The important line is

      makeShellWrapper ${lib.meta.getExe boxxy} $out/bin/${binaryBaseName} --add-flags ${binaryName}

Let's instantiate it with a concrete program to be wrapped, just to aid in the discussion:

      makeShellWrapper ${lib.meta.getExe boxxy} $out/bin/chromium --add-flags /nix/store/.../bin/chromium

To summarize what it does, since it is a bit backwards compared to normal usage of the library, it creates a wrapper script ($out/bin/chromium) -- not with the original chromium binary as input, but with the wrapper one. Then, it adds the original chromium binary as the first argument to this command.

Since you already use the makeWrapper package in wrapper-manager, I think it would be elegant to reuse its functionality like this. For example by providing a version of the derivation with makeWrapper instead of wrapProgram, plus a convenience function that then pre-fills the output name, argv0 and the first argument like above.

viperML commented 2 months ago

While I don't really use these programs, the usecase is definitely interesting, and would fit nicely in the interface of wrapper-manager.

I think we can already start discussing the api for it, maybe something like this?

  wrappers.my-tmux = {
    basePackage = pkgs.tmux;
    under = pkgs.boxxy;
  };
jmickelin commented 2 months ago

Oh yes! That looks sensible.

Also worth considering is whether the wrapper itself has optional command line flags that the user might want to inject before the wrapped command.

boxxy has a couple of them for special use-cases (e.g. --immutable), and one could envision a wrapper like this:

#!/nix/store/.../env bash

# Runs Chromium in kiosk-mode in an ad-hoc container with 
# write access to only the Chromium-specific config directories 
# specified in the boxxy configuration
/nix/store/.../boxxy --immutable /nix/store/.../chromium --kiosk "$@"

So it would be ergonomic if the API was flexible enough to account for both without needing to first wrap the two input derivations separately. Something like this:

  wrappers.my-chromium = {
    basePackage = pkgs.tmux;
    flags = [ "--kiosk" ];
    # More standard wrapping functionality options here
    ...

    under = pkgs.boxxy;
    underFlags = [ "--immutable" ];
  };

To complicate it further, it's good form to include a -- to separate the wrapper's flags from the invocation of the inner command and prevent accidentally parsing the command name or its flags as options for the wrapper, e.g.

$ boxxy --immutable -- chromium --kiosk "$@"

While most programs of this style do support -- as a separator, I'm not entirely convinced that all of them do. A flag to optionally suppress it could be useful, just in case.

Say we add a new boolean attr noHyphenSeparator, which is used like this internally:

{
  inherit basePackage flags ... under noHyphenSeparator;

  underFlags = underFlags ++ (lib.lists.optional 
  (!noHyphenSeparator) "--");
}

Then again, it's not a huge deal when it comes to nix, since the path of the basePackage is most likely going to be prefixed by a nix-store path, and as such shouldn't ever be misinterpreted as an option by any wrapper program... What do you think?

viperML commented 2 months ago

I feel like we could set the double hyphen unconditionally and wait for somebody to complain about that

jmickelin commented 2 months ago

Good point! Agreed :smile:

viperML commented 2 months ago

If you want to write the implementation in a separate PR I'd be happy to review. But I have other projects on the top of my stack for me to implement this.

poperigby commented 14 hours ago

Would this feature also allow running something like mangohud or gamescope before a program?