Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ jobs:
- uses: nixbuild/nix-quick-install-action@v34
with:
nix_conf: |
experimental-features = ca-derivations flakes impure-derivations nix-command no-url-literals
experimental-features = ca-derivations flakes impure-derivations nix-command
extra-substituters = https://ilkecan.cachix.org?priority=41 https://nix-community.cachix.org?priority=42
extra-trusted-public-keys = ilkecan.cachix.org-1:hXb7Vo9EzaXiEb0elvG6Tt5TrP3zrcadyoX8c+lbeCY= nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=
http-connections = 128
lint-url-literals = fatal
max-substitution-jobs = 128

- run: nix run
env:
Expand Down
7 changes: 3 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,13 @@ There are three nixpkgs inputs with distinct purposes:

### Input Patching

`nix/inputs/default.nix` applies upstream PRs to inputs and makes patched versions transparent by overwriting the originals under `inputs.*` (except `nixpkgs-patched`, which is always a separate additive input). Modules consume patched inputs the same way as stock ones.
`nix/inputs.nix` applies upstream PRs to inputs and makes patched versions transparent by overwriting the originals under `inputs.*` (except `nixpkgs-patched`, which is always a separate additive input). Modules consume patched inputs the same way as stock ones.

Transitive flake inputs are rewritten recursively: `replacementMapping` maps each top-level input's `outPath` to its resolved counterpart, so `follows` aliases like `nixpkgs-lib` or `nixpkgs-stable` are transparently replaced with the correct canonical node.
Transitive flake inputs are rewritten selectively using `flake.lock` node names. `nodeDirtiness` is a self-referential lazy map over all lock nodes: a node is dirty if it is directly patched or any of its lock-declared inputs resolves to a dirty node. Only dirty inputs are rebuilt via `mkFlake`; clean inputs are returned as-is, avoiding unnecessary `flake.outputs` re-evaluations. `nodeNameMapping` maps each top-level input's lock node name to its canonical resolved version, ensuring that `follows` aliases pointing to the same node share the same thunk.

Important invariants when editing `nix/inputs/default.nix`:
Important invariants when editing `nix/inputs.nix`:

- **Top-level canonical nodes** — if a dependency is shared in multiple places, it should have a canonical representative at the top level and all repeats should follow that node.
- **Additive patched inputs must not be flake inputs** — `nixpkgs-patched` is excluded from `resolvedTopLevel` and `replacementMapping` because it is not declared in `flake.nix`'s inputs. This is by design: its pre-patch source `outPath` would collide with `nixpkgs-unstable` in `replacementMapping`. It enters the final result via the `patchedInputs // resolvedTopLevel` merge and is only consumed directly by self.
- **`self` is special-cased** — the recursive rewrite applies to external inputs, but `self` itself is not rebuilt through that recursion. Only `self.inputs` is updated with the rewritten/exported input set to avoid recursive self-reimport.

### Custom pkgs Overlays
Expand Down
38 changes: 19 additions & 19 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 1 addition & 3 deletions nix/flake/cachix.nix
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,8 @@ let
;

inherit (lib.my)
storePathName
isPatchedInput
;

isPatchedInput = x: storePathName x != "source";
in
{
flake.cachix = {
Expand Down
2 changes: 1 addition & 1 deletion nix/flake/per-system/apps/ci.nix
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
pkgs.writeShellApplication {
name = "ci";

runtimeInputs = with pkgs; [ unstable.nix-fast-build ];
runtimeInputs = with pkgs; [ patched.nix-fast-build ];

text = ''
nix-fast-build --no-nom --skip-cached --cachix-cache ilkecan "$@"
Expand Down
3 changes: 2 additions & 1 deletion nix/flake/per-system/args/pkgs.nix
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
inputs,
lib,
self,
...
}:
Expand All @@ -8,6 +9,6 @@
perSystem =
{ system, ... }:
{
_module.args.pkgs = import "${self}/nix/packages" { inherit inputs system; };
_module.args.pkgs = import "${self}/nix/packages" { inherit inputs lib system; };
};
}
5 changes: 3 additions & 2 deletions nix/hosts/mephistopheles/nix/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ in
"flakes"
"impure-derivations"
"nix-command"
"no-url-literals"
];
trusted-users = [ userConfig.home.username ];

Expand Down Expand Up @@ -66,8 +65,10 @@ in
http-connections = 128; # default: 25
keep-going = true;
keep-outputs = true;
# lint-absolute-path-literals = "warn";
# lint-short-path-literals = "warn";
lint-url-literals = "fatal";
max-substitution-jobs = 128; # default: 16
# warn-short-path-literals = true;
};

gc = {
Expand Down
130 changes: 92 additions & 38 deletions nix/inputs.nix
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@
}:

let
inherit (builtins)
unsafeDiscardStringContext
;

inherit (lib)
any
attrNames
attrValues
elem
filterAttrs
foldl'
importJSON
intersectAttrs
isString
map
mapAttrs
mapAttrs'
Expand All @@ -18,6 +23,8 @@ let

inherit (lib.my)
importTree
isFlake
isPatchedInput
;

# NOTE: unfortunately there is no way to avoid hard-coded `system` yet
Expand Down Expand Up @@ -83,62 +90,109 @@ let
mkFlake {
srcPath = src';
inherit sourceInfo;
inputs' = mapAttrs (_: resolveInput) src.inputs;
inputs' = mapAttrs (
localName: resolveInput (resolveNodeName nodes.${topLevelNodeNames.${input}}.inputs.${localName})
) src.inputs;
};

inherit (inputs) self;
inputs' = removeAttrs inputs [ "self" ];

patchedInputs = importTree {
# Patch configurations from ./inputs/ that actually apply changes. importFn
# calls patchInput for every spec file; isPatchedInput filters out specs with
# no patches (where applyPatches returns the original src unchanged).
patchedInputs = filterAttrs (_: isPatchedInput) (importTree {
root = ./inputs;
depth = 1;
importFn = x: patchInput (import x);
normalizeNameFn = removeSuffix ".nix";
};
});

# Resolve a lock node reference to its canonical node name.
# String refs are already node names; array refs are follow-paths from root
# (e.g. ["nixpkgs"] resolves root→nixpkgs, ["bar","foo"] resolves root→bar→foo).
resolveNodeName =
inputSpec:
if isString inputSpec then
inputSpec
else
foldl' (nodeName: inputName: resolveNodeName nodes.${nodeName}.inputs.${inputName}) root inputSpec;

lockFile = importJSON ../flake.lock;
inherit (lockFile) nodes root;

# Use the original realized source path as the replacement identity so
# aliases such as nixpkgs-lib and nixpkgs-stable collapse to one node.
inputKey = x: unsafeDiscardStringContext x.outPath;
rootInputs = nodes.${root}.inputs;
resolveTopLevelNodeName = name: resolveNodeName rootInputs.${name};

# Rebuild a non-patched flake against resolved child inputs.
# Top-level canonical inputs are handled separately through replacementMapping.
inputs' = removeAttrs inputs [ "self" ];

# Node key for each top-level input (resolved via lock, not outPath).
topLevelNodeNames = mapAttrs (name: _: resolveTopLevelNodeName name) inputs';

# Shadowed inputs are those whose name also appears in inputs'. Additive
# patches (e.g. nixpkgs-patched) are excluded — they add a name not present
# in inputs', so they have no corresponding lock node to mark dirty.
shadowedInputNames = attrNames (intersectAttrs inputs' patchedInputs);
shadowedNodeNames = map resolveTopLevelNodeName shadowedInputNames;

# Self-referential lazy map: a lock node is dirty if it is directly patched
# or any of its lock-declared inputs resolves to a dirty node.
# Terminates because flake.lock is a DAG. Pattern validated by Nix's own
# call-flake.nix (allNodes), adapted here with a nodeDirtiness check instead
# of unconditionally rebuilding everything.
nodeDirtiness = mapAttrs (
name: node:
elem name shadowedNodeNames
|| any (inputSpec: nodeDirtiness.${resolveNodeName inputSpec}) (attrValues (node.inputs or { }))
) nodes;

# Rebuild a dirty flake against resolved child inputs. Each child's lock node
# name is used to look up its canonical resolved version via resolveInput.
resolveFlake =
input:
if input ? inputs then
nodeName: input:
if isFlake input then
mkFlake {
srcPath = input.outPath;
sourceInfo = input.sourceInfo;
inputs' = mapAttrs (_: resolveInput) input.inputs;
inputs' = mapAttrs (
localName: resolveInput (resolveNodeName nodes.${nodeName}.inputs.${localName})
) input.inputs;
}
else
input;

# These are the canonical nodes for every raw top-level input. Any transitive
# dependency that is shared across the graph is expected to follow one of
# these exact top-level nodes.
resolvedTopLevel = mapAttrs (name: input: patchedInputs.${name} or (resolveFlake input)) inputs';

# Map each original top-level source identity to its canonical resolved node.
# This only covers raw top-level inputs: additive patched inputs such as
# nixpkgs-patched are intentionally excluded because their pre-patch source
# identity collides with another raw input (for example nixpkgs-unstable),
# and they are only meant to be consumed directly by self.
replacementMapping = mapAttrs' (
name: input: nameValuePair (inputKey input) (resolvedTopLevel.${name})
# Canonical resolved version of every raw top-level input: patched inputs use
# their patched version; dirty inputs are rebuilt via resolveFlake; clean
# inputs are returned as-is (no mkFlake, no flake.outputs re-evaluation).
resolvedTopLevel = mapAttrs (
name: input:
patchedInputs.${name} or (
let
nodeName = topLevelNodeNames.${name};
in
if nodeDirtiness.${nodeName} then resolveFlake nodeName input else input
)
) inputs';

# Repository invariant: if a dependency is shared in multiple places, it
# should have a canonical representative at the top level and all repeats
# should follow that node. In that common case we reuse the canonical thunk
# through replacementMapping; the fallback only rebuilds non-canonical inputs
# that are not expected to be shared transitively.
resolveInput = input: replacementMapping.${inputKey input} or (resolveFlake input);
# Maps each top-level input's lock node name → its canonical resolved version.
# Transitive followers share the same lock node name, so they get the same thunk.
nodeNameMapping = mapAttrs' (
name: _: nameValuePair topLevelNodeNames.${name} resolvedTopLevel.${name}
) inputs';

# to include additive patched inputs like `nixpkgs-patched`
# Resolve any input by its lock node name to its canonical version.
# Top-level canonical nodes are served from nodeNameMapping; transitive
# non-canonical nodes fall back to resolveFlake if dirty, or are returned
# as-is if clean.
resolveInput =
nodeName: input:
nodeNameMapping.${nodeName}
or (if nodeDirtiness.${nodeName} then resolveFlake nodeName input else input);

# Merges additive patched inputs (e.g. nixpkgs-patched) with the resolved
# top-level inputs. Additive inputs are not present in inputs' so they would
# otherwise be dropped; this merge ensures they are exported alongside the rest.
resolvedInputs = patchedInputs // resolvedTopLevel;
in
{
self = self // {
self = inputs.self // {
inputs = resolvedInputs;
};
}
Expand Down
Loading