Nixtamal: Fulfilling, Pure Input Pinning for Nix
31 points by toastal
31 points by toastal
I have been using Nixtamal on a lot of my recent projects & it’s felt pretty good to use (& when it didn’t I tried to fix it). I did a lot of work to get the upstream Darcs & Pijul {,pre}fetchers in a great state & with features like mirrors that other fetchers don’t support. These alternative VCSs are great in theory, but lack the tooling to overcome Git—this is one of my attempts to help out with the tooling to encourage others to try them out as well as mirror their code (self-hosting a plain repo if you have hosting isn’t hard & helps when these CDNs go out as not everything can or will be in cache.nixos.org).
One of the first things I needed to do was sell my stock in flakes as these features will likely never be added (political in-fighting in the Nix community included), which has actually made a lot of projects simpler—pointing just files & exposing overlay(s) for consumption. Pure, classic Nix means anyone can use it.
Any chance of native OCI container image support? That's a major annoying omission from flakes, though npins has acquired support recently I think.
I would need to know more about what that is 😅. If there is a fetcher + nix-prefetch-script, it’s easy to add. If it’s downloadable, then fetchurl works & you can use the fresh-cmd + templating to fetch/pin the version you want.
OCI is the standardized docker image format. nvfetcher for example supports this.
Ah. I mean the easiest way is to add a prefetcher upstream for everyone (preferably with JSON output like the others). If I needed it, I would build the prefetcher like I did for Pijul.
I don’t think this really tries to solve the exact same problems as Nix flakes, and purely as an input lock mechanism, doesn’t have too many unique features. I think it’s still possible to improve flakes significantly, even if that’ll have to be outside the official Nix(OS) organisations.
I personally don’t like having a separate language for the manifest. Though, of course, flake.nix isn’t really Nix, since it prohibits most language features. But I enjoy it.
My main gripe with Nix flakes is that you can’t pass non-input arguments easily. There’s been efforts to emulate this with simple flakes that just represent a value like “true” you can pass as an input and the like, but that won’t work in the long term, I think.
Who claimed it was trying to solve the same problems as the whole of flakes? The header says it’s input pinning without flakes & the FAQs state how it’s different. You can do basically anything you want with classic Nix anyhow…
I am skeptical that flakes are even salvageable at this point with its political gridlock & at least 2 forks trying to “stabilize” in different directions. A missing point is that largely this could have been a plain, community-run schema outside the compiler so it could be iterated, versioned independently. If such a schema existed, Nixtamal could target or provide adapters to that schema & we as users could choose whatever tooling we want. The problems you are running into are real & it’s a part of flakes design where it needs to be in control of everything. Folks, after understanding the pain points of flakes, should give classic Nix another shot (such as using a release.nix for all your builds outputs & exposing overlays to users) as you are freed of flake’s design limitations rather than trying to fight so hard against it.
I will write about why the manifest in the FAQs here now that I think about it since the reason… the thing is I don’t think CLIs for managing adding/modifying the manifest is good experience. At some point you need to change branches/channels or apply patches & with the alternatives without a manifest (mind you, flake.nix is a “manifest”) you now need to dig into a lockfile (& need to be wary of JSON rules when modifying but also now the lock is holding data not relevant to locking too!) as the CLI doesn’t easily allow said modifications unless you remove then re-add. This is why the inputs in flake.nix is nice since in a Git repository input you can just change the ?ref=… which is why the manifest exists: it’s easier to manage & maintain text in the long run. KDL, IMO, offers a really nice manifest/config file experience compared to alternatives—& will get even better with time as LSPs can hook into schema files in the future to help with typing/autocomplete.
Folks, after understanding the pain points of flakes, should give classic Nix another shot (such as using a release.nix for all your builds outputs & exposing overlays to users) as you are freed of flake’s design limitations rather than trying to fight so hard against it.
I absolutely agree. I find the most flexible approach is to put the derivation boilerplate in default.nix, as a function with arguments for each "input"/"dependency", and default values for all of those args, e.g. something like:
{
nixpkgs ? builtins.fetchTarball {
sha256 = "1915r28xc4znrh2vf4rrjnxldw2imysz819gzhk9qlrkqanmfsxd";
url = "https://github.com/nixos/nixpkgs/archive/11cb3517b3af6af300dd6c055aeda73c9bf52c48.tar.gz";
},
pkgs ? import nixpkgs { config = {}; overlays = []; },
python3Packages ? pkgs.python3Packages,
src ? pkgs.lib.cleanSource ./.,
}:
python3Packages.buildPythonApplication {
inherit src;
pname = "my-app";
version = "1.2.3";
propagatedBuildInputs = [ python3Packages.requests ];
}
That's enough to use nix-build, nix-shell, etc. and it's trivial to override (just call it with different arguments; they'll propagate through references too, e.g. overriding pkgs in the above will also cause python3Packages and src to be overridden).
If you want to provide overlays, flakes, shell.nix, release.nix, etc. then their boilerplate can go in separate files which load that default.nix (hint: if you use pkgs.callPackage instead of import, you'll get a .override function added automatically!).
If my default.nix file gets too unwieldy, I'll pull out the parts to separate files (usually in a nix/ subdir); and it's perfectly possible to take default values from a pinning system (I've not used nixtamal; but I have used alternatives like niv).
Many of my projects (work and personal) don't need any of that extra stuff though.
This sort of project also composes well, e.g.
{
pkgs ? import nix/nixpkgs.nix,
foo ? import (fetchGit { url = "http://example.com/foo.git"; rev = "abc"; }) {
inherit pkgs python3Packages;
},
bar ? import (fetchGit { url = "http://example.com/bar.git"; rev = "def"; }) {
inherit foo pkgs python3Packages;
},
python3Packages ? pkgs.python3Packages,
src ? pkgs.lib.cleanSource ./.,
}:
python3Packages.buildPythonApplication {
inherit src;
name = "quux";
version = "99";
buildInputs = [ foo bar python3Packages.baz ];
} // { inherit foo bar; }
Here the quux project is loading the foo and bar projects from separate git repos, and overriding their dependencies with its own for consistency (alternatively it could leave their dependencies at their default values, which are presumably known to work, but may lead to redundancy or mismatched versions in the overall combination). We might imagine that the bar project is doing a similar thing for its foo dependency, but we don't need to care since it's easily overridable to be consistent.
As a bonus, I've added // { inherit foo bar; } so that anyone loading this project can easily get the foo and bar dependencies it was using. If bar were to expose its foo dependency in this way, we could use that as our default value (using a little indirection: foo == null will use bar's default value; foo != null will override bar's default value; we need this indirection to avoid overriding bar's default value with bar's default value, which would be an infinite loop!):
{
pkgs ? import nix/nixpkgs.nix,
foo ? null,
bar ? import (fetchGit { url = "http://example.com/bar.git"; rev = "def"; }) {
inherit pkgs python3Packages;
${if foo == null then null else "foo"} = foo;
},
python3Packages ? pkgs.python3Packages,
src ? pkgs.lib.cleanSource ./.,
}:
with {
resolved.foo = if foo = null then bar.foo else foo;
};
python3Packages.buildPythonApplication {
inherit src;
name = "quux";
version = "99";
buildInputs = [ resolved.foo bar python3Packages.baz ];
} // {
inherit bar;
inherit (resolved) foo;
}
The pattern I use a release.nix which is the main entry point which imports my overlays, where the overlays import checks, packages, & shells. It becomes very easy if someone doesn’t like my setup to say, import a single package since all packages are in normal form so you can use callPackage.
Though, of course, flake.nix isn’t really Nix, since it prohibits most language features.
Note that most of flake.nix supports arbitrary Nix. It is evaluated in pure mode by default, though. The inputs in particular is the limited subset.
The pull request to get it into Nixpkgs is here which means if you have nix-command + flake features enabled, it is possible now to trial run nixtamal from the PR using nix run github:toastal/nixpkgs?ref=nixtamal-init#nixtamal