Mana solves dependency locking and injection
- Its only few lines of bash β‘οΈ nix
- Its better than flakes
nix run github:hsjobeki/mana init
nix run github:hsjobeki/mana updateCreates a lock.json that pins down all dependencies
Now you can build:
nix build -f default.nix hello
Done β‘οΈ
You should take a look at all files that exists. Before reading further
mana.nixentrypoint.nix- ...
Tip
For ergonomics install it on your system
e.g. environment.systemPackages [ mana ]
- Since this tool uses
fetchTree- the fetcher inside flakes - it is limited to fetching sources that are supported by flakes. - Currently verbose lockfile
- Requires the
importer.nixshim. - When using flakes that is hidden inside nix. - nix commands require
-fflag / or a flake.nix compat shim (see nix commands )
# mana.nix
{
entrypoint = ./entrypoint.nix;
dependencies = {
nixpkgs.url = "github:nixos/nixpkgs";
treefmt-nix.url = "github:numtide/treefmt-nix";
};
groups = {
eval = {
nixpkgs = [ ];
};
dev = {
treefmt-nix = [ "eval" "dev" ];
};
};
}groups control which dependencies are fetched. A dependency is only downloaded when it belongs to an enabled group. Without groups, everything goes into eval by default.
Here nixpkgs is in eval (always fetched), while treefmt-nix is in dev (only fetched when requested).
default.nix enables only eval. To also fetch dev dependencies:
# ci.nix
(import ./nix/importer.nix) { groups = [ "eval" "dev" ]; }There is one entrypoint.nix for all groups. Disabled dependencies throw on access:
# entrypoint.nix
{ nixpkgs, treefmt-nix }:
{ system ? builtins.currentSystem }:
let
pkgs = nixpkgs { inherit system; };
in
{
packages.x = pkgs.callPackage ./. { };
checks.formatting = pkgs.callPackage ./. { inherit treefmt-nix; };
}Using default.nix: treefmt-nix throws when accessed.
Using ci.nix: treefmt-nix is available.
graph TD
default.nix -->|eval| entrypoint.nix
dev.nix -->|eval, dev| entrypoint.nix
doc.nix -->|eval, doc| entrypoint.nix
entrypoint.nix --> Packages
entrypoint.nix --> Modules
entrypoint.nix --> Documentation
By default, mana respects upstream manifests but re-locks all dependencies locally.
You often want to reduce nixpkgs downloads by forcing dependencies to use your pinned version.
Use share to list dependencies that should be shared with all transitive dependencies:
# mana.nix
{
entrypoint = ./entrypoint.nix;
dependencies = {
nixpkgs.url = "github:nixos/nixpkgs";
treefmt-nix.url = "github:numtide/treefmt-nix";
};
# treefmt-nix (and any deeper deps) will use YOUR nixpkgs
share = [ "nixpkgs" ];
}This overrides nixpkgs in:
- treefmt-nix's dependencies
- Any transitive dependencies (dependencies of dependencies)
- Does not override your root-level nixpkgs
NOTE: share is syntactic sugar for transitiveOverrides
For granular control over specific dependencies, use local overrides:
# mana.nix
rec {
entrypoint = ./entrypoint.nix;
dependencies = {
nixpkgs.url = "github:nixos/nixpkgs";
treefmt-nix.url = "github:numtide/treefmt-nix";
treefmt-nix.overrides = deps: deps // {
nixpkgs = dependencies.nixpkgs;
};
};
}For advanced cases, transitiveOverrides gives you a function over the full dependency set:
# mana.nix
rec {
entrypoint = ./entrypoint.nix;
dependencies = {
nixpkgs.url = "github:nixos/nixpkgs";
treefmt-nix.url = "github:numtide/treefmt-nix";
};
transitiveOverrides = deps: deps // {
nixpkgs = dependencies.nixpkgs;
};
}share = [ "nixpkgs" ] is equivalent shorthand for the above.
If both share and transitiveOverrides are set, share is applied first, then transitiveOverrides on top.
Mana uses a two-level precedence system:
-
At the root level (lenient mode):
Local
overrideswin overtransitiveOverrides/share(overrides > transitiveOverrides) Lets you customize immediate dependencies while setting defaults for the tree -
For all nested dependencies (strict mode):
transitiveOverrides/sharewin over localoverrides(transitiveOverrides > overrides) Ensures your pins are enforced throughout the dependency tree
Example:
# Root mana.nix
rec {
dependencies = {
nixpkgs.url = "example:v25.05";
utils.url = "example:v1.0";
dep-a.url = "example:dep-a";
dep-a.overrides = deps: deps // {
nixpkgs.url = "example:v-unstable"; # β Takes effect (root level is lenient)
};
};
share = [ "nixpkgs" "utils" ];
}# dep-a's mana.nix
{
dependencies = {
nixpkgs.url = "example:v-old";
utils.url = "example:v-old";
dep-b.url = "example:dep-b";
dep-b.overrides = deps: deps // {
nixpkgs.url = "example:v-unstable"; # β Ignored (strict mode)
utils.url = "example:v2.0"; # β Ignored (strict mode)
};
};
}Results:
- Root's
nixpkgsβexample:v25.05(root's own dependency) - Root's
dep-agetsnixpkgsβexample:v-unstable(local override at root, lenient) dep-a.dep-bgetsnixpkgsβexample:v25.05(root's share enforced, strict)dep-a.dep-bgetsutilsβexample:v1.0(root's share enforced, strict)
By default, mana imports each dependency's entrypoint (from its mana.nix) or falls back to default.nix. You can override this per-dependency:
entrypoint = null disables the import
{
dependencies = {
nixpkgs.url = "github:nixos/nixpkgs";
nixpkgs.entrypoint = null; # raw source path
};
}# entrypoint.nix
{ nixpkgs }:
let
pkgs = import nixpkgs { system = "x86_64-linux"; };
in
pkgs.helloUse entrypoint = "./path/to/file.nix" to import a specific file instead of the default:
{
dependencies = {
some-lib.url = "github:someone/some-lib";
some-lib.entrypoint = "./lib/special.nix";
};
}Often we want our tools to be runnable / buildable by people just entering nix build or nix run.
These experimental commands are only natively compatible with flakes. - They require a flake.nix -
When using other files they require passing -f <filename> attrName
One possible way to get a more native experience is to create a flake.nix shim that re-exposes your runnable packages.
# flake.nix
# shim for nix run compat
{
outputs =
_:
let
systems = [
"aarch64-linux"
"x86_64-linux"
"x86_64-darwin"
"aarch64-darwin"
];
in
{
packages = builtins.listToAttrs (
map (system: {
name = system;
value =
let
self = import ./default.nix { inherit system; };
in
self
// {
# The default package
# for 'nix run'
default = self.hello-world;
};
}) systems
);
};
}