Comment by lloeki

2 years ago

> it's filled with Nix-isms

Which part?

For the commands, update install upgrade uninstall are straight the same.

There's an additional -A on install, but you don't need to understand it. The commands are even handed over to you on https://search.nixos.org/packages.

There's an additional nixpkgs prefix but it's perfectly understandable that it's coming from the channel name:

    # nix-channel --list
    nixpkgs https://nixos.org/channels/nixpkgs-23.05-darwin

It's really not out of this world to mentally map this to homebrew taps or apt repos.

nix-env --query is different from homebrew but its design is much the same as pacman -Q and people have no trouble with that (I won't even dive on apt-get vs dpkg vs apt-cache)

nix-env -f <github archive url> --install nixos-23.05.tar.gz is such because it's the branch name, 88f63d51109.tar.gz is just the commit sha. These are GitHub things. Providing the source of packages as a --file option is hardly inscrutable nor even a nix-only thing.

But if you really want a parallel with homebrew, then homebrew has taps, and nixpkgs has channels: nix-channel --add <url> name is directly equivalent to brew tap user/repo <url>, down to installing with brew install user/repo/package vs nix-env --install -A name.package.

And there it is, you have your 1:1 nixism-free homebrew:nixpkgs replacement, and can manage your packages with nix just like you do with homebrew.

> there's now a large quantity of tooling that makes it as easy to work with as Homebrew

My point is that there is no tool that could "make it as easy to work with as homebrew" because homebrew:nixpkgs is already 1:1, and people confused by using nix this way would equally be confused by using homebrew.

You mentioned Fleek, which seems like a nice tool, but managing home is outside the scope of homebrew so I don't see how it follows that it makes nix "as easy as homebrew". It apparently has some homebrew bits, but it's a third party tool to both homebrew and nix.

Don't get me wrong, tools can be nice, I use nix-darwin myself, not too sure about going home-manager yet.

---

Now that second part where I talk about shell.nix, maybe it's this one you took issue with? I aimed it at being a next step, not addressing the above point, but attempting to demonstrate the immediate value it can give without understanding the slightest bit of nix and especially eschewing all the nix-specific vernacular.

The first time it is encountered, this is probably what a reasonable non-nixer vaguely interested in would see:

    {
      pkgs ? import <nixpkgs> {},
      # ^^^^^^^^^^^^^^^^^^^^^^^^
      # okay so I've seen nixpkgs before, that's my channel, where the packages come from
      # that `?` looks odd but well, whatever, there's a "pkgs" thing and an import of sorts.
    }:
    pkgs.mkShell {
    # ^^^^^^^^^^
    # this is:
    # - a shell.nix file
    # - that has been described to me as being used by a `nix-shell` command
    # - and that thing is name mkShell
    # so probably it's going to tell things how to make that shell
      buildInputs = [
        pkgs.ruby_3_2;
        # here's that pkgs that I've seen above
        # it has a dot plus a package name just like when I did nix-env --install
        # except it had nixpkgs before the dot there
        # oh, I see "pkgs ? import ..." sort of creates that variable or whatever from nixpkgs
        # so this is a list of packages from nixpkgs
      ];
      # and so that make a shell thing uses that list to get its dependencies
      # and it's getting named buildInputs, whatever
    }
    # so that's it, it makes a shell using a list of packages from nixpkgs

And proceed to add whatever package they see fit by looking them up on the search page. Understanding of nixisms doesn't matter.

Second example: "let .. in" isn't a very special nix construct, "let" is extremely frequently used in a number of languages (including JS) to define variables. "whatever .. in" is a very common construct as well. It's quite obvious that the example hoists out a few elements by assigning variables between "let" and "in", possibly creating some form of scope after "in". It also introduces "shellHook", which literally contains a multiline bash string; again I feel like it's quite obvious that the "make shell" thingy is going to call that hook at some point.

Last bits shows that the nixpkgs channel can be replaced with fetchTarball + a GitHub tarball archive URL, and that you can have multiple of these at once with different names, referencing packages from one or the other.

> that most people used to imperative thinking would not grok.

I can hear that the syntax is an oddball, but even then that example is really not that hard to wrap one's head around for anyone who has a passing familiarity with programming, whether functional or imperative.

Doesn't mean that they would fully appreciate that { foo ? "bleh" }: whatevs = 42 is actually a function definition equivalent to JS function(foo = "bleh") { return { "whatevs": 42 }; } but that's immaterial to understanding what is happening here and even being able to add/remove packages, change versions, pin sources, or hack that shell bit and ultimately have it be pragmatically useful.

So I don't think the nixisms are any problem in this case because they can be completely ignored. I'm also wondering if people have an epidermic reaction to the syntax, combined with the constant rumour that nix is hard and hardly usable does not set them up for success.

I mean, would people be as hung up if it were written this way?

   args:
     pkgs: <nixpkgs>
   let:
     ruby: pkgs.ruby_3_2;
     whatevs: pkgs.whatevs42;
   in:
     pkgs.mkShell:
       buildInputs:
         - ruby
         - whatevs
         - pkgs.foobar
       shellHook: |
          export RUBY_VERSION="$(ruby -e 'puts RUBY_VERSION.gsub(/\d+$/, "0")')"
          export GEM_HOME="$(pwd)/vendor/bundle/ruby/$RUBY_VERSION"
          export BUNDLE_PATH="$(pwd)/vendor/bundle"
          export PATH="$GEM_HOME/bin:$PATH"

or this way?

    function(pkgs = import(NixPath.resolve('nixpkgs'))) {
      let ruby = pkgs.ruby_3_2;
      let whatevs = pkgs.whatevs42;
    
      return pkgs.mkShell({
        buildInputs: [
          ruby,
          whatevs,
          pkgs.foobar,
        ],
        shellHook: `
          export RUBY_VERSION="$(ruby -e 'puts RUBY_VERSION.gsub(/\d+$/, "0")')"
          export GEM_HOME="$(pwd)/vendor/bundle/ruby/$RUBY_VERSION"
          export BUNDLE_PATH="$(pwd)/vendor/bundle"
          export PATH="$GEM_HOME/bin:$PATH"
        `
      });
    }

Because at the end of the day, it's not that different in complexity, from, say, a Gemfile, a PKGBUILD, or a homebrew formula.

Regarding your last point, I actually do think that if the syntax to nix were different that it would make it much easier to understand. Though, the fact that it is clearly distinct from, say, Python or JavaScript gives it its own distinct feel as to how 'rigid' it is.

To me the big stumbling block in the language is that it at first appears declarative, like you're writing a data structure that describes your configuration. However, when you start modifying anything about packages, you need to call functions to make changes because you're actually writing a function that's transforming input.

So, you're thinking about what you're trying to do in a declarative, descriptive sense and certain parts of what you're writing are structured as such; but then other parts are structured as a transformation.

Eg you write out a list of packages, but then if you want to change one of those packages, you need to start calling functions. As I mentioned in the Python example below, that can wind up requiring calling `override`, `overrideAttrs`, `overridePythonAttrs`, etc.

Consider this:

  {
    packageOverrides = pkgs: {
      python3 = pkgs.python3.override {
        packageOverrides = python-self: python-super: {
          twitch-python = python-super.twitch-python.overrideAttrs (attrs: {
            patches = (attrs.patches or []) ++ [
              ./twitch-allow-no-token.patch
            ];
          });
        };
      };

      twitch-chat-downloader = pkgs.twitch-chat-downloader.overrideAttrs (attrs: {
        patches = (attrs.patches or []) ++ [
          ./twitch-chat-downloader-no-oauth.patch
        ];
      });
    };
  }

Versus this:

  {
    packageOverrides: pkgs {
      python3: {
        twitch-python: pkgs.python3.twitch-python {
          patches: pkgs.python3.twitch-python.patches or ./twitch-allow-no-token.patch
        }
      },
      twitch-chat-downloader: pkgs.twitch-chat-downloader {
        patches: pkgs.twitch-chat-downloader.patches or ./twitch-chat-downloader-no-oauth.patch
      }
  }

The latter is less "functionally perfect", but there is vastly less cognitive overhead required to lay it out because you are basically just describing your goal, rather than having to keep your goal in mind while implementing it functionally (and keeping exactly how nixpkgs is implemented in mind to correctly use those overrides).

This is just off-the-cuff of what I intuitively expected when I first started using Nix, but it's more what I'd expect of a no-holds-barred purpose-built language for declarative package management. All the override stuff seems like needlessly noisy syntax; you know it's going to be everywhere, you might as well build it into the language.

And it can probably be made even simpler with effort.

  • > To me the big stumbling block in the language is that it at first appears declarative, like you're writing a data structure that describes your configuration. However, when you start modifying anything about packages, you need to call functions to make changes because you're actually writing a function that's transforming input.

    That's a very fair point to make. I have noticed the same and it seems like it's borne out of the way Nix (NixOS more precisely) is built, which is in two layers:

    - a first layer of packages and whatnot, which is by and large functional programming, and has the gritty implementation you mention which get exposed when you want to alter some specific things

    - a second layer of configuration modules, which takes packages and turns them into a declarative interface

    From a Ruby analogy I would compare the first to some form of monkey-patching or otherwise forceful injection and the second one to a nice DSL which exposes clear properties to change some bits

    For example, on modules there's `.package` which allows one to override the package to use fairly easily:

      services.tailscale.enable = true;
      services.tailscale.package = let
        old = import(fetchTarball("https://github.com/NixOS/nixpkgs/archive/a695c109a2c.tar.gz")) {};
      in
        old.tailscale;
    

    (taken from this issue https://github.com/NixOS/nixpkgs/issues/245769)

    Frequently you get additionalPackages or extraConfig or something in this "DSL", which handles the whacky non-descriptive stuff behind the scenes which really is an implementation detail that should not leak through.

    So indeed I feel like Nix packages in general should benefit from a more descriptive interface similar to what NixOS modules expose, so that appending patches or adding configure flags would not be such an ordeal.

    Basically this (pseudocode) should be generalised and hidden away:

          # iterate over patch map => mypackage, mypatches
            packageOverrides = python-self: python-super: {
              ${mypackage} = python-super.${mypackage}.overrideAttrs (attrs: {
                patches = (attrs.patches or []) ++ ${mypatches};
              });
            };
    

    So you then would just descriptively pass python3.packagePatches = { twitch-python = [./twitch-allow-no-token.patch] } or something and be done with it. Not saying it's easy for Nix folks to achieve that but it should be doable one way or another. I mean, there's already:

          python_packages = python-packages: [
            python-packages.pip
          ];
          python = pkgs.python39.withPackages python_packages;
    

    It's not out of this world to think there chould be a generalisable extension of that for patches (and more) to fill in the various overrides that exist around Nix derivations.

    That would certainly make nixpkgs more approachable.