Warning
This is very cursed and scuffed, and completely untested (don't be fooled by
the test/ directory, that's just a sandbox to try things in). I've been
told this will break nixpkgs, and I haven't even tried it before publishing
this! How irresponsible can you get??
This is a utility that works in conjunction with npins
and allows two pretty nice things:
The entire thing is implemented and amply commented in the
inject.nix top. In fact, if you want to use this, all you have
to do is copy that file into your npins/ directory. See the
How do I use it? section for more detailed information.
Using vanilla npins can be frustrating when working on a project with a bunch
of files, since you have to find some way of "smuggling" the pin objects across
each file. One advantage that channels specified in NIX_PATH have is that you
can simply use the <bracket> syntax to refer to things, without worrying about
passing them across files.
This project allows you to use npins while still using the convenient
<bracket> syntax! Just use the name of the pin inside your file and it'll
resolve auto-magically to the path you wanted!
Referring to a pinned nixpkgs using <bracket> syntax
nixpkgs using <bracket> syntaxsimplified npins/sources.json:
{
"nixpkgs": {
"channel": "nixpkgs-unstable",
"version": "26.05pre038...",
"outPath": "/nix/store/...-nixpkgs-26.05pre038..."
}
}main.nix:
let
pkgs = import <nixpkgs> {};
in
"nixpkgs v${pkgs.lib.version} at path ${pkgs.path}"The <nixpkgs> reference will auto-magically refer to the pinned version, so
we'll get the following output:
nixpkgs v26.05pre038... at path /nix/store/...-nixpkgs-26.05pre038...The one thing you can't do with npins is force dependencies to use a certain
version of something, even if they're using npins themselves. With flakes,
you have "follows," which basically allow you to override the inputs
(dependencies) of your own dependencies. This can be used, for example, to
ensure that one of your dependencies uses the same version of nixpkgs as you,
despite the author having originally used a different version.
The whole point of frozenpins is to allow npins to do just that! :D
Following are a few basic examples, which should give you a starting idea. They have had the surrounding "boilerplate" removed for clarity, you can read How do I use it? for more details.
Example 1: Basic override
In this example, our root project has too pins/dependencies: nix-debug and
nixpkgs. This will make nix-debug use the nixpkgs version that we pinned,
instead of the one it might have specified.
pins: {
nix-debug.nixpkgs = pins.nixpkgs;
}Example 2: Multiple independent follows
This example has a similar setup: we directly depend on nix-debug and oestro,
as well as two versions of nixpkgs: nixpkgs-unstable and nixpkgs-25.11. We
want nix-debug to use nixpkgs-unstable, but oestro to use nixpkgs-25.11.
pins: {
nix-debug.nixpkgs = pins.nixpkgs-unstable;
oestro.nixpkgs = pins."nixpkgs-25.11";
}Example 3: Dependent pins
Now, we're using a third project, zpkgs, that itself depends on oestro, and
we want it to use the same version of oestro as us. We are also overriding
oestro's version of nixpkgs, and we want to ensure dependencies are coherent,
so zpkgs.oestro.nixpkgs should be the same as oestro.nixpkgs, since we told
zpkgs to use the same oestro as us. Thankfully, this is a lot easier to
write than to explain:
pins: {
oestro.nixpkgs = pins.nixpkgs;
zpkgs.oestro = pins.oestro;
}As you can see, the zpkgs.oestro.nixpkgs = pins.nixpkgs line is "implicit,"
since it is inherited from the pin we defined for oestro. Similarly, if
nixpkgs itself also had a dependency foo that we overrode, it would "bubble
up" to both oestro.nixpkgs.foo and zspkgs.oestro.nixpkgs.foo.
Of course, if we actually wanted zpkgs's oestro to use a different nixpkgs,
we could still override its specific nixpkgs:
pins: {
oestro.nixpkgs = pins.nixpkgs;
zpkgs.oestro = pins.oestro // {
nixpkgs = pins.nixpkgs-unstable;
}
}[!WARN] Do not use
recfor the attribute set, as that will give you wrong and inconsistent results, since the pins will not have undergone the necessary normalization and merging.
Example 4: Local override
Now, we'd like to override one of the dependencies with a local version of it.
Thankfully, with frozentrone, this is relatively easy:
pins: {
nix-debug = ~/dev/nix-debug;
oestro.nixpkgs = ~/dev/nixpkgs;
}Example 5: Local override + follows
If you want to both set a dependency to a local path while also overriding one
of its own dependencies, you can explicitely use outPath to specify the path:
pins: {
nix-debug = {
outPath = ~/dev/nix-debug;
nixpkgs = pins.nixpkgs;
};
}Sorry that this is kinda hard to discover :(
Note
Overrides for dependencies are inherited from a parent project to its dependencies, and it will override the dependency's follows, when applicable.
In an average project (e.g. a user package repository like
blokyk/packages.nix) uses npins,
you simply need to:
- drop
inject.nixinto yournpins/folder (no, you can't just fetch it, it has be physically next tonpins/default.nixandnpins/sources.json, sorry) - move the code in your
default.nixfile to another file (e.g.main.nix) - replace
default.nixwith the following code:let injectImport = import ./npins/inject.nix (pins: { # todo: add your overrides/follows here! }); in injectImport ./main.nix
That's about it! Any project you depend on using npins will now be available in
the other files of your project that's imported (directly or not) by main.nix.
In particular, if one of your dependencies uses channels/<bracket> syntax, it
will refer to your pins instead of using NIX_PATH.
TODO
(see home-manager section above, exact same reason)
TODO
(it's not quite as simple because home-manager wraps your code up in a module,
but the code for that module hasn't been wrapped in an injector, so it won't
use the correct imports and stuff. huh. i know it's possible though! the
real question is how ugly/invasive will it be ;-;)
Because "frozen" (like flakes) and "npins" form a beautiful portmanteau :D
Oh, you meant why do this project? Because I didn't know if it could be done, and just after giving up on my first try @piegames give me little hint that reinvigorated me. What follows is roughly 3*24h of a mix of hyperfocus and sunk cost fallacy that was really irresponsible and intellectually draining (thinking about lexical scopes and imports and how they compose and recurse and blablabla all day is absolutely impossible for my tiny head), but god damn it it was enjoyable and fun to figure out.
Until I figure out how to use it for home-manager and NixOS, I don't think
I'll actually use it, in part because it's generally cursed, but also because
I don't want to have to think about the intricate details of this code ever
again. When I say it was draining I mean it. So many sheets of paper got
scarred just for this. It took multiple full rewrites to get a working
version, and even after that it took multiple hours to iron out the most obvious
kinks. And most of all, it is nigh-impossible to actually debug, in part because
the nix debugger, god bless its poor soul, is absolutely garbage for debugging
(above and beyond how bad debugging for lazy functional languages usually is).
See the github issue list.
Woah, you okay there buddy? I don't know if that's very wise...
(Of course, contributions, whether it be tiny typo corrections, better docs, bug reports, feature requests, or even PRs are all very welcome, but beware that this project is pretty cursed)
Here are a few projects that might also interest you, or that might be what
you actually wanted from this. They are also are generally much better written
and tested than mine. Really wish I had found unflake before starting this.
-
goldstein/unflake allows non-flake projects to depend on flake inputs while unifying dependencies, with similar override rules and more; notably, it supports
npinsas a dependency pinning tool, instead of the default nix "tarball caching" mechanism. it's really good! -
andir/npins is a lightweight dependency management tool, as well as the reason we're all here today. if you're not using it yet for your projects, you're missing out.
The file inject.nix is licensed under the MIT license, as it is meant
to be freely copy-and-pasted into projects that needs it. An acknowledgment
would be nice, but you are not legally bound to it.
The rest of this work, including but not limited to documentation and tests, is licensed under the European Union Public License (EUPL) v1.2.