Making Nixpkgs less dependent on Bash

Fair warning

I am not super familiar with the architecture of Nixpkgs, and am mostly basing the following statements on what information I was able to gather from similar threads, as well as on my own assumptions. Feel free to point out any potential nonsense.

I was looking through many different shells recently, and have come to realize that NixOS (or more specifically Nixpkgs) seems to be very reliant on Bash via “bashisms”.

Now personally, I don’t have an agenda against Bash, but making the build systems portable to other POSIX-compatible shells could bring a meaningful benefit to evaluation times when running them against faster shells such as Dash, as well as possibly allowing for toying with the system’s forbidden fruit, better known as environment.binsh.

Ridding the build phases of Nix packages of bashisms would likely prove to be a tall order, so I’ve been thinking whether it would be worthwhile to instead try and automate the process with a dedicated tool, through the means of analysis, change suggestions, and maybe even autofixing. Worst case scenario, an actual LSP/formatter would have to give the altered snippets a second opinion, post mortem (and hey, that could also help make the styling more consistent).

And who knows, we may even see the day when someone writes a speed-optimised shell-like evaluator, designed specifically for speeding up preexisting build pipelines, potentially shaving even more miliseconds off build times. Having a POSIX-compliant build stack could certainly come in handy then.

So my questions towards the well-versed are:

  • do you know of any packages with particularly convoluted build phases?
  • what approach do you think would be best when designing such a tool?
  • are there things other than Nix packages that would require “unbashifying”?
  • what would be the best way of testing how preexisting packages would fare
    against being built with their phases being evaluated in alternate shells?
11 Likes

I don’t think that could possibly be true. When IFD isn’t in play (as it isn’t in Nixpkgs), evaluation happens entirely prior to anything being executed in a shell. (Unless you meant the evaluation that the shell does and not Nix evaluation, which is what folks around here usually mean when they use the word unqualified.)

This could, as you say, shave some milliseconds off of build times. It’s a rare package whose build time is remotely close to being dominated by what we do in shell.

Rewriting activation-time components to use faster or non-shell languages is likely to be a far better use of one’s time. (Example: NixOS: Systemd unit-linking script rewrite for 60x speedups by Profpatsch · Pull Request #479442 · NixOS/nixpkgs · GitHub, though that seems to be foundering on a non-technical conflict.)

9 Likes

Yes, I meant shell evaluation.

Maybe I have overemphasized the wrong thing. So to be clear, to me, the actual biggest potential benefit of doing something alike what I have proposed would be making the environment.binsh option user-configurable.

And sure, the upsides of switching out the system shell would likely still be negligible, but some other distros are doing it, and I just find it to be a reasonable thing one might want to customize (albeit some sanity checks would be in order, such as checking whether the selected package is a POSIX-compatible shell).

I’m fine with trying to help out with such a refactor, and am just looking for directions and/or interest.

Hrm… it seems that the title of this post should be more like “Making NixOS less dependant on bash”, would you agree?

If that’s the case, then I think most would agree, as there has been an effort towards that end, such as the one you’ve been (by chance) referred to.

Personally, I would like to see bash scripts generally everywhere replaced with nushell scripts. They’re safer, have incomparably better errors and the developer experience is oil tankers ahead. But nushell itself is compiled by rust, which we currently bootstrap from upstream binaries, which is less desirable than the nice bootstrap from source I guess we have leading to bash.

If you’re interested in pursuing something, consider finding or asking for a concrete task.

4 Likes

I have assumed most of NixOS’ current Bash dependency stems from Nixpkgs, hence the title and category. But yes, that’s entirely fair.

Without a doubt, but my proposed approach could serve as an intermediary solution - one that would likely not spark too much opposition, since at the end of it, the build phases would still work with the current tooling (Bash).

Even in the aforementioned Github issue, there was some discussion regarding which language should be used for the specific reimplementation, which is a topic I’m not qualified enough to give opinions on, but am still able to realize it could be particularly controversial when considering a repository-wide change.

The amount of labor is also a concern. Nixpkgs is large, and there is a reason why I have proposed the creation of some sort of automated tool to help with the process of merely refactoring some of its syntax (a tool that could also be used with scripts completely unrelated to Nix or NixOS). Switching to a different scripting language would require much more upfront labor.

Also, since I imagine this process could require reimplementing a handful of Bash-specific features, I’m curious whether it would be viable/reasonable to reduce code duplication via the creation of some sort of Nix library whose functions could be concatenated within the build phases.

1 Like

I had played around with some basic Dash stuff (which I think Debian/Ubuntu still use as the default shell, no? It is smaller & faster) as builtins.derivation. It works, but what I miss is the ability to prefer local building like pkgs.runCommandLocal as it’s tied to stdenv which is tied to Bash.

Pointing out Bashism in reviews seemed to annoy folks as “the whole system relies on Bash anyways”. Which is true, but the bandwagon isn’t always the best approach; perhaps it could be worthwhile to push back on Bashisms before they would creep further in. checkbashism could theoretically be ran say on new commits assert no new ones are being added.

2 Likes

That option is already “user-configurable”, it’s just hidden, for some reason. The description

Please note that NixOS assumes all over the place that shell to be Bash, so override the default setting only if you know exactly what you’re doing.

is hyperbolic. I have been using environment.binsh = lib.getExe pkgs.dash for years with no issue at all. All scripts generated by NixOS have a shebang with an appropriate shell and never use /bin/sh for purity reasons. The only thing you could reasonably worry about is the libc system() function using /bin/sh, but since many distros (rightly) don’t set it to bash, most software generally assumes it’s a generic POSIX shell.

3 Likes

I have come across an opposite statement from a Nixpkgs contributor.

It’s great to hear you’re not having issues with the option yourself, though.

1 Like

Many nixpkgs packages do unfortunately call /bin/sh directly. I use a script that logs such actions to the journal, haven’t gotten around to fixing many of them though.

My goal is to eventually just null out binsh and usrbinenv.

6 Likes

I’m not sure whether it’s what you mean, but passing --option substituters "" to nixos-rebuild seems to disable the use of non-local caches for the rebuild (aka cache.nixos.org, by default), but I can’t really be sure, as my rebuilds currently fail due to downtime of busybox.net.

Come to think of it, the cache may also be the reason behind @rnhmjoj’s success with the environment.binsh option.

I still don’t know how to attempt building all of Nixpkgs for testing, say 100 packages at a time (because SSD storage is really expensive right now), and I don’t think I’ll be resorting to adding them to my system config manually.

Writing robust shell code without relying in bash-isms is hard. Particularly the lack of array variables is painful. You have to code in circles to get around the limitations. I wouldn’t support removing them from nixpkgs build code if it costs in readability and maintainability of stdenv and nixpkgs in general, which I am convinced it would.

As for /bin/sh, it’s a dependency of a lot of things. I do think we should avoid nix-packaged things depending on it as much as we can, but I believe even statically-compiled binaries for general linux often rely on it to interpret calls to external commands. If you remove this, then many statically-compiled binaries would no longer work on nixos, which was apparently a bridge too far when originally designing the os. There’s an argument to be made for removing it, but it’s really hard to argue it’s worth it.

Also, we need /usr/bin/env. For hashbangs. Not really necessary for anything else, but having hashbangs do PATH-lookups is kind of important.

6 Likes

If you’re implying /bin/sh is used during a build, that’s not true. In the Nix build sandbox /bin/sh is symlinked to ash, regardless of what the builder actual /bin/sh is.

/bin/sh and /usr/bin/env only really matters for software you’re running on your machine, they’re almost immaterial in relation to Nixpkgs infrastructure

I think that /bin/sh could be easily dealt with by patching glibc. See #1424. I bet that most uses are just calling system().

/usr/bin/env can already be removed without much consequences, because there aren’t really scripts built by Nixpkgs that uses it as a shebang.

If you need it for covenience in your own scripts, you can reimplement it with a binfmt interpreter:

environment.usrbinenv = null;
boot.binfmt.registrations.env =
  { interpreter = pkgs.writers.writeDash "env"
      ''
        args=$(head -n1 "$1" | cut -d' ' -f2-)
        env $args $@
      '';
    magicOrExtension = "#!/usr/bin/env";
  };

The real problem with usrbinenv = null is that without /usr a bunch of software, most notably systemd, craps out. For example, services that use sandboxing options like ProtectSystem=true will try to bind-mount /usr and fail, because systemd insists that these paths must exist (see systemd#18867).

5 Likes

It seems somebody has already written a POSIX reimplementation of some Bash array operations.

Depending on the particular variant of the script, the addition could end up being pretty lengthy, so my earlier suggestion of creating a POSIX shorthand library could be necessary.

It is GPL-licensed (unlike Nixpkgs), so I’m guessing I would have to write my own version anyways (for the sake of not cluttering the repository with extra license files, and taking advantage of implementing it as a Nix library).

1 Like

I wouldn’t disagree, but should the shell be something you opt into as most derivations aren’t using arrays? I think I had a look & stdenv, could be Bash-less.

I also have to question what exactly the goal really is here.

Highly questionable. Firstly, using a different shell would affect build times, not evaluation times. For most users this would be very little effect since almost everything they do is cached. For actual compiles that aren’t cached, the shell used is far from a relevant performance impact.

This could have a slight effect on the “glue” derivations that put things together locally, like the derivation that builds the symlink environment from the set of installed packages, but those derivations (aside from the man cache, which is not a shell issue) are already fast enough I doubt I would ever care about the difference.

Should be unrelated. The shell used in the build environment has nothing to do with the setting of environment.binsh.

2 Likes

Why would this ever be configurable? What could possibly be beneficial enough about using a different shell to get the exact same work done that it would be worth losing the entire binary cache? Seriously, what’s the goal that justifies the amount of work and continuing maintenance burden involved in supporting more shells than bash?

This doesn’t affect what shell a user actually uses, what the /bin/sh symlink points to, or even whether bash is installed on a (non-build) machine.

2 Likes

I think a far more interesting discussion would be “making nixpkgs less dependant on POSIX-ish shells”, because seriously stdenv is really hard and counterintuitive to use and also writing *sh code sucks; POSIX simply wasn’t designed for writing maintainable code.

Existing hooks and various subcomponents of the conglomerate, especially if you include the various things that derive from stdenv, also have almost zero indexable documentation, IMO caused by it being a messy pile of shell code; this is worse yet than the general doc situation in nixpkgs, where at least most of it has something resembling an API, even if it’s hard to find. For hooks and build systems you’re mostly in the wild west and just have to read the code, assuming you can even find it.

It’d be cool to completely overhaul this in some language that is better suited for all of this. But of course, that’s also a significantly more involved piece of work.

And yeah, completely separate from the topic here, there isn’t much of a benefit to changing stdenv to a non-bash POSIX-ish shell; the code would barely change (likely to be even less maintainable, not more), and it has next to no impact on users.

The only minor benefit I can imagine is a small performance improvement for builds, but it’d probably be vanishingly small. If you can benchmark this somehow (probably not too difficult to comment out all the actual work-doing parts of nixpkgs builds) and show a meaningful difference @username-generic, maybe people would be more keen on this suggestion.

20 Likes

There are some builds where sequential configure is a large part of the build time on multi-core machines… but what shell configure uses is of course independent from what shell stdenv code uses.

1 Like

@TLATER Yeah, shell is an ugly language littered with footguns that lacks a bunch of nice abstractions. But honestly, what you would replace it with? There aren’t a lot of languages that wouldn’t make the main kind of work we’re currently doing with bash unbearably verbose and hard to understand. I’ve never really found a “better shell language”. For the things it’s best at, it really is best at them. The one thing I can think of that might do the job alright would be nushell, but its build closure is unacceptably large for stdenv, and I don’t think I’d be comfortable making a relatively new project completely vital to nixpkgs architecture.

@7c6f434c Yeah, I could see running configure with a faster shell by default if that could get meaningful improvements. That would be a comparatively compartmentalized change with some meaningful chance of clear benefits.

3 Likes