The use of wrapper scripts to set or change environment variables before running the real executable has become very widespread in nixpkgs. While they are important for making programs run out-of-the-box, wrappers have some general problems. Some of these could be fixed but some are perhaps unfixable. I'm writing this to generate some discussion to see if we could solve some of these issues or come up with an alternative solution.
Environment variables are propagated to child processes. I believe this is the most problematic thing. The intent of the wrapper is to make the wrapped program run correctly, and the fact that child processes see the adjustment is unintended leakage of information/configuration. Any child process started by the wrapped program that also needs environment changes will generally have its own wrapper. This leakage could cause another application to run incorrectly or differently than if it was started "cleanly".
Wrappers which add to list-type variables (typically colon-separated) will add the value unconditionally without checking if it is already in the list.
To observe (1) and (2) in action, open Konsole and run echo "$XDG_DATA_DIRS". Then type konsole in the same terminal and run the echo in the new terminal. You will see that the variable contains two entries for <konsole-store-path>/share. You can keep repeating this to get XDG_DATA_DIRS that grows without bounds.
Wrappers obscure the process name. Just look at a process list and find many .something-wrapped processes.
People forget to make wrappers and/or don't know which variables need to be set. For example, probably most GTK apps need to have GDK_PIXBUF_MODULE_FILE set to point to some file in the librsvg package. It is far from obvious to a random person packaging an app that this is necessary (see #60254 and #42562).
Wrappers break software that opens the executable and expects it to be an ELF binary. I have encountered this with Wine but I have not yet filed an issue (it results in very suble but widespread breakage). But this is uncommon.
I have an alternative design in mind, where we get rid of wrappers, instead packages declare in passthru what they need to work, and the environment ensures that variables are set globally (or just in the context of desktop sessions) such that declared requirements of all packages included in the environment are met. In particular, nixos/modules/programs/environment.nix would decide which variables to set to what based on the programs included in environment.systemPackages. Similar would need to be done to ensure variables from user environments are set.
But it does have the problem that when a profile is switched (nixos-rebuild switch or nix-env), associated variable changes would not affect running sessions. Maybe there is a way to address this but it probably cannot be simple (like, instead of setting variables in sessions, add wrappers to programs to source export files from all applicable profiles - but that brings back some of the general problems with wrappers).
I have an alternative design in mind, where we get rid of wrappers, instead packages declare in
passthruwhat they need to work, and the environment ensures that variables are set globally (or just in the context of desktop sessions) such that declared requirements of all packages included in the environment are met. In particular,nixos/modules/programs/environment.nixwould decide which variables to set to what based on the programs included inenvironment.systemPackages. Similar would need to be done to ensure variables from user environments are set.
Remember we also need to support users outside of NixOS. They could be running the binary with any environment.
Remember we also need to support users outside of NixOS. They could be running the binary with any environment.
Yeah, that is indeed one aspect of the problem. Wrappers do directly address this, but at the expense of creating a mess with environment variables, including when running in NixOS where this may be unneeded if suitable stuff is included in the profile. I'm wondering if we could, in the long term, drop support for running GUI apps "directly", outside of an environment, and provide a practical solution to encapsulate them in an environment. Like, the environment could be the one that generates correct wrappers, not the package itself.
the environment could be the one that generates correct wrappers, not the package itself.
This would solve "3. Wrappers obscure the process name". It is also more flexible because all packages in the environment (such as whether you are running a specific desktop) could contribute to the variables set in other wrappers, e.g. including the KDE desktop in the environment could ensure that all other apps get support for SVG icons. Or special configuration options (e.g. support these GVFS plugins) could adjust some variable in all wrappers.
By the way, "Wrappers obscure the process name" can already be solved relatively easily, by putting the executable into a subdirectory, e.g. something -> .wrapped/something.
By the way, "Wrappers obscure the process name" can already be solved relatively easily, by putting the executable into a subdirectory, e.g.
something->.wrapped/something.
Might be worth trying, but there could be some issues with relative paths, symlinks, etc. To be clear, I 100% agree with the problems you are describing, I just know from experience that there's not a good alternative.
I think we could look into using a C-based wrapper instead to prevent the issues with script. This is a little bit less readable, but could solve 3 & 5. This is also an issue on macOS with python shebangs.
I think we could look into using a C-based wrapper instead to prevent the issues with script. This is a little bit less readable, but could solve 3 & 5. This is also an issue on macOS with python shebangs.
Not sure about this. How does it help with (3)? And it also does not solve (5) in the sense that the program opening the executable will open the wrong executable (the wrapper) not the one it wanted to.
However, whatever we do with wrappers, we do not address "(1) Environment variables are propagated to child processes". We could look into more invasive approaches. For example, the wrapper could pass all such "variables" via a single specific environment variable (e.g. NIX_WRAPPER_VARS), and the binary would, very early at startup, read this environment variable, save its contents, and unset the original environment variable (so it will not be passed to child processes). The application code would then use a specific library function like get_nix_wrapper_var to read it (the code would be patched to call that instead of getenv), or getenv in the stdlib could be patched to make it look like these are passed as environment variables.
How does it help with (3)?
Ah you probably mean the ability to specify argv[0] in execve.
The idea for a C-based wrapper seems good. It could also help solving (2) (not adding to list if already present) more elegantly. I will now try to implement this, probably with C++, to be used as an #! interpreter where the wrapper executable reads the remaining content that specifies the wrapped executable and which environment adjustments are needed.
GI_TYPELIB_PATH, LUA_PATH and LUA_CPATH redundantly.I see that the current shell wrappers already set argv[0] to the proper program name (of the wrapper) trying to hide the existence of the wrapper, but that apparently does not affect the process name in the process list (I am guessing that is based on the name of the actual executable).
Maybe fiddling with argv[0] is a bad idea (considering this Awesome problem) and the wrapper should just call the executable with its nix store path (whether or not the wrapper is part of the package or made by the environment)? Is there a reason wrappers are currently setting argv[0]? Is it only so that the command line is preserved, if someone looks at it in the process list?
Another problem with the current wrappers is that since it uses bash it cannot work with setuid. This would be solved by the use of a C-based wrapper.
I've made some progress making a C++ wrapper, but it cannot be a drop-in replacement because it's not bash. For example:
--argv0 '$0' which is used in several places relies on variable expansion. This special case could be handled transparently but it's probably better to have something like --argv0-wrapper (i.e. argv0 is the path to the wrapper script, which is what this does - note that the original argv0 is inaccessible anyway).--run <bash code> cannot be implemented. Some examples: --run "export ANGBAND_PATH=\$HOME/.sil", --run "cd $packagePath", --run "[ -n \"\$DISPLAY\" ] && .... Even if it supported running programs, they couldn't make any environment changes or add to extraFlagsArray. Expansions of variables like $HOME could be implemented, and cd can be supported as specific command (makeWrapper ... --cd <directory> ...).In general it seems like like can be done, but some cases would need to be handled differently, possibly by patching the application code or by sticking with the bash wrapper.
Here's a prototype of a C++ based wrapper. There is one executable which is both the interpreter for wrappers and a tool to generate the wrapper file. The latter is so because escaping strings correctly in bash would be a pain. It works for a few test case packages. Prefix and suffix functionality work such that any previous occurrences of added elements are removed.
I don't think it makes sense to go replacing the current wrappers with this. It would be some work and not much benefit (which is the check for existing occurrences of elements, and perhaps a small performance improvement). Many use cases could not be handled because it doesn't let you run bash code. On the other hand I like this limitation because it enforces some sanity. This wrapper could be a good starting point for a system where buildEnv generates wrappers.
About 5. I have encountered issues with wine due to this on several occasions. The more severe of them is that when you create a 64-bit wine prefix with the package wineWowPackages.full it will create a prefix with ONLY 64-bit dlls. The syswow64 folder is completely empty. This may cause alot of issues and needs to be dealt with.
If you want to see this yourself I have made a nix-shell command that reproduces and logs only 64-bit dlls being copied and the error where wine can't be loaded as an ELF executable:
nix-shell -p wineWowPackages.full --run "WINEPREFIX=$HOME/.problematicwine WINEDEBUG=+wineboot,+setupapi wineboot 2>&1 | grep -e fakedlls -e ELF -A 5 -B 5"
I would like to have a binary that reads a nix-support/wrappers.json file. The file contains a mapping where the keys are paths and the values are arguments. We can then improve our hooks to just update that JSON file during build-time, also improving the situation when dealing with multiple hooks, preventing nested wrappers.
https://discourse.nixos.org/t/wrappers-and-hooks-do-not-invoke-wrapprogram-directly/3551
Sample:
{
"bin/foo": { "executable": "/nix/store/....python", "arguments": [ "PATH=/nix/store.../bin"] },
}
the problem, as usual, is finding the path to the file, although that one could be given as argument to the wrapper binary.
Thank you for your contributions.
This has been automatically marked as stale because it has had no activity for 180 days.
If this is still important to you, we ask that you leave a comment below. Your comment can be as simple as "still important to me". This lets people see that at least one person still cares about this. Someone will have to do this at most twice a year if there is no other activity.
Here are suggestions that might help resolve this more quickly:
This issue has been mentioned on NixOS Discourse. There might be relevant details there:
https://discourse.nixos.org/t/include-custom-environment-variable-into-wrapper/8229/2
Most helpful comment
Here's a prototype of a C++ based wrapper. There is one executable which is both the interpreter for wrappers and a tool to generate the wrapper file. The latter is so because escaping strings correctly in bash would be a pain. It works for a few test case packages. Prefix and suffix functionality work such that any previous occurrences of added elements are removed.
I don't think it makes sense to go replacing the current wrappers with this. It would be some work and not much benefit (which is the check for existing occurrences of elements, and perhaps a small performance improvement). Many use cases could not be handled because it doesn't let you run bash code. On the other hand I like this limitation because it enforces some sanity. This wrapper could be a good starting point for a system where
buildEnvgenerates wrappers.