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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ This means
- `flake check` ensures files are up to date.
- App for `flake.nix` generator: `nix run .#write-flake`
- Custom do-not-edit header.
- Automatic flake.lock [flattening](#automatic-flakelock-flattening).
- Reduce duplicate inputs using automatic follows powered by [`flake-edit`](https://github.com/a-kenji/flake-edit), or via automatic `flake.lock` [flattening](#automatic-flakelock-flattening).
- Incrementally add [flake-parts-builder](#parts_templates) templates.
- Pick flakeModules for different feature sets.
- [Dendritic](https://vic.github.io/dendrix/Dendritic.html) flake template.
Expand Down
116 changes: 99 additions & 17 deletions dev/modules/_lib/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,28 @@ let
in
lib.pipe { } ops;

nonEmptyInputs = input: {
nonEmptyMerge = {
nonEmptyInputs =
input:
let
inputs = inputsFollow input.inputs;
in
{
nonEmptyMerge = lib.optionalAttrs (inputs != { }) { inherit inputs; };
};
};

inputsFollow = lib.mapAttrs (
_: input:
mergeNonEmptyAttrs input {
follows = {
testEmpty = v: !builtins.isString v;
};
inputs = nonEmptyInputs input;
}
);
inputsFollow =
inputs:
lib.filterAttrs (_: input: input != { }) (
lib.mapAttrs (
_: input:
mergeNonEmptyAttrs input {
follows = {
testEmpty = v: !builtins.isString v;
};
inputs = nonEmptyInputs input;
}
) inputs
);

inputsExpr = lib.mapAttrs (
_name: input:
Expand Down Expand Up @@ -77,14 +84,85 @@ let
}
);

mergeAutoFollows =
configured: expr: existing:
lib.mapAttrs (
name: input:
let
existingInput = existing.${name} or { };
nested = mergeNestedAutoFollows (configured.${name}.inputs or { }) (input.inputs or { }) (
existingInput.inputs or { }
);
in
(removeAttrs input [ "inputs" ]) // lib.optionalAttrs (nested != { }) { inputs = nested; }
) expr;

mergeNestedAutoFollows =
configured: expr: existing:
let
names = lib.unique ((builtins.attrNames expr) ++ (builtins.attrNames existing));
in
lib.listToAttrs (
lib.filter (entry: entry.value != { }) (
map (
name:
let
configuredInput = configured.${name} or { };
exprInput = expr.${name} or { };
existingInput = existing.${name} or { };
automaticallyManaged = configuredInput.autoFollow or true;
nested = mergeNestedAutoFollows (configuredInput.inputs or { }) (exprInput.inputs or { }) (
existingInput.inputs or { }
);
preservedFollow = lib.optionalAttrs (
automaticallyManaged && builtins.isString (existingInput.follows or null)
) { follows = existingInput.follows; };
in
{
inherit name;
value =
(removeAttrs exprInput ([ "inputs" ] ++ lib.optional automaticallyManaged "follows"))
// preservedFollow
// lib.optionalAttrs (nested != { }) { inputs = nested; };
}
) names
)
);

autoFollowIgnores =
configured:
lib.concatLists (
lib.mapAttrsToList (
parent: input: collectAutoFollowIgnores [ parent ] (input.inputs or { })
) configured
);

collectAutoFollowIgnores =
path: inputs:
lib.concatLists (
lib.mapAttrsToList (
name: input:
let
inputPath = path ++ [ name ];
disabled = !(input.autoFollow or true);
configuredFollow = input.follows or null;
in
if disabled && builtins.isString configuredFollow then
throw "auto-follow cannot be disabled for ${lib.concatStringsSep "." inputPath} while follows is configured"
else
lib.optional disabled (lib.concatStringsSep "." inputPath)
++ collectAutoFollowIgnores inputPath (input.inputs or { })
) inputs
);

nixAttr =
name: value:
collapse: name: value:
let
childIsAttr = builtins.isAttrs value;
childIsOne = builtins.length (builtins.attrNames value) == 1;
nested = lib.head (lib.mapAttrsToList nixAttr value);
nested = lib.head (lib.mapAttrsToList (nixAttr collapse) value);
in
if childIsAttr && childIsOne then
if collapse && childIsAttr && childIsOne then
{
name = "${name}.${nested.name}";
value = nested.value;
Expand Down Expand Up @@ -118,6 +196,7 @@ let
{
attrSortPriority = [ ];
attrSep = " ";
collapseAttrs = true;
}
else
lib.pipe styles [
Expand All @@ -126,9 +205,10 @@ let
{
attrSortPriority ? [ ],
attrSep ? " ",
collapseAttrs ? true,
}:
{
inherit attrSortPriority attrSep;
inherit attrSortPriority attrSep collapseAttrs;
}
)
];
Expand All @@ -148,7 +228,7 @@ let
lib.strings.escapeNixString expr
else if lib.isAttrs expr then
lib.pipe expr [
(priorityMapAttrsToList nixAttr style.attrSortPriority)
(priorityMapAttrsToList (nixAttr style.collapseAttrs) style.attrSortPriority)
(map (
{ name, value }:
"${name} = ${
Expand Down Expand Up @@ -181,8 +261,10 @@ let
in
{
inherit
autoFollowIgnores
inputsExpr
isNonEmptyString
mergeAutoFollows
priorityComparator
priorityMapAttrsToList
nixCode
Expand Down
114 changes: 113 additions & 1 deletion dev/modules/unit-tests/inputsExpr.nix
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
{ lib, ... }:
let
inherit (import ./../_lib lib) inputsExpr;
inherit (import ./../_lib lib)
autoFollowIgnores
inputsExpr
mergeAutoFollows
;

tests.inputsExpr."test on empty inputs" = {
expr = inputsExpr { };
Expand Down Expand Up @@ -79,6 +83,114 @@ let
};
};

tests.inputsExpr."test autoFollow metadata is not rendered" = {
expr = inputsExpr {
foo = {
url = "foo";
inputs.bar.autoFollow = false;
};
};
expected.foo.url = "foo";
};

tests.inputsExpr."test preserves existing automatic follows" = {
expr = mergeAutoFollows {
foo = {
inputs = { };
};
} { foo.url = "foo"; } { foo.inputs.bar.follows = "baz"; };
expected = {
foo = {
url = "foo";
inputs.bar.follows = "baz";
};
};
};

tests.inputsExpr."test preserves follows for inputs introduced by preProcess" = {
expr = mergeAutoFollows { } { injected.url = "injected"; } {
injected.inputs.nixpkgs.follows = "nixpkgs";
};
expected = {
injected = {
url = "injected";
inputs.nixpkgs.follows = "nixpkgs";
};
};
};

tests.inputsExpr."test automatic follows override configured follows" = {
expr =
mergeAutoFollows
{
foo.inputs.bar = {
autoFollow = true;
follows = "quux";
inputs = { };
};
}
{
foo = {
url = "foo";
inputs.bar.follows = "quux";
};
}
{ foo.inputs.bar.follows = "baz"; };
expected = {
foo = {
url = "foo";
inputs.bar.follows = "baz";
};
};
};

tests.inputsExpr."test opting out removes an automatic follow" = {
expr = mergeAutoFollows {
foo.inputs.bar = {
autoFollow = false;
follows = null;
inputs = { };
};
} { foo.url = "foo"; } { foo.inputs.bar.follows = "baz"; };
expected.foo.url = "foo";
};

tests.inputsExpr."test collects nested auto-follow exclusions" = {
expr = autoFollowIgnores {
foo.inputs = {
bar = {
autoFollow = false;
inputs = { };
};
baz = {
autoFollow = true;
inputs.quux = {
autoFollow = false;
inputs = { };
};
};
};
};
expected = [
"foo.bar"
"foo.baz.quux"
];
};

tests.inputsExpr."test rejects follows on an auto-follow exclusion" = {
expr =
(builtins.tryEval (
builtins.deepSeq (autoFollowIgnores {
foo.inputs.bar = {
autoFollow = false;
follows = "target";
inputs = { };
};
}) null
)).success;
expected = false;
};

in
{
flake = { inherit tests; };
Expand Down
1 change: 1 addition & 0 deletions docs/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export default defineConfig({
{ label: "Templates", slug: "guides/templates" },
{ label: "The outputs Function", slug: "guides/outputs" },
{ label: "Hooks", slug: "guides/hooks" },
{ label: "Automatic Follows", slug: "guides/auto-follow" },
{ label: "Lock Flattening", slug: "guides/lock-flattening" },
{
label: "flake-parts-builder",
Expand Down
45 changes: 45 additions & 0 deletions docs/src/content/docs/guides/auto-follow.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
title: Automatic Follows
description: Use flake-edit to maintain follows declarations automatically.
---

flake-file can use [`flake-edit follow`](https://github.com/a-kenji/flake-edit) to add and remove `follows` declarations whenever `flake.nix` is generated.

This is an alternative to the [`flake.lock` flattening hook](/guides/lock-flattening). Both approaches reduce duplicate transitive inputs, but they store the result in different places. Generally, choose one rather than enabling both.

```nix
{ inputs, ... }:
{
imports = [ inputs.flake-file.flakeModules.auto-follow ];
}
```

Alternatively, enable the underlying option directly with `flake-file.auto-follow.enable = true`.

`write-flake` updates `flake.lock`, runs `flake-edit follow`, and updates the lock again. The generated-file check runs the same follow operation on a temporary copy, so `nix flake check` also detects missing or stale automatic follows.

Automatic follows require `pkgs.flake-edit` version 0.3.5 or newer. Enabling the feature with an older or missing package produces an evaluation error identifying the required version.

## Automatic follows or lock flattening?

| | Automatic follows | Lock flattening hook |
|---|---|---|
| Source of truth | `follows` declarations in `flake.nix` | Rewritten relationships in `flake.lock` |
| Visibility | Relationships are visible when reviewing `flake.nix` | Keeps `flake.nix` free of generated follows |
| Configuration | Supports per-nested-input opt-outs | Configured for the whole lock file |
| Writes | Updates both `flake.nix` and `flake.lock` | Rewrites only `flake.lock` after generation |
| Best fit | Explicit, reviewable Nix-native follows | Existing lock-flattening workflows or minimal generated flakes |

Use automatic follows when you want deduplication represented as ordinary flake declarations. Use the lock hook when you want deduplication to remain an implementation detail of the generated lock file.

## Opting out an input

When automatic follows are enabled, flake-edit manages nested `follows` declarations by default. Exclude a particular nested input when it should not be followed automatically:

```nix
{
flake-file.inputs.crane.inputs.nixpkgs.autoFollow = false;
}
```

An exclusion cannot also configure `follows`: one requests no automatic follow while the other requests an explicit follow. flake-file reports that conflicting configuration when automatic follows are enabled.
6 changes: 6 additions & 0 deletions docs/src/content/docs/guides/flake-modules.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ Enables automatic `flake.lock` flattening using [fzakaria/nix-auto-follow](https

Source: [`modules/prune-lock/nix-auto-follow.nix`](https://github.com/vic/flake-file/tree/main/modules/prune-lock/nix-auto-follow.nix)

## `flakeModules.auto-follow`

Enables automatic `follows` declarations maintained by [`flake-edit follow`](https://github.com/a-kenji/flake-edit). This is an alternative to the lock-flattening modules above.

Source: [`modules/auto-follow.nix`](https://github.com/vic/flake-file/tree/main/modules/auto-follow.nix)

## `flakeModules.npins`

Defines `flake-file` options for [npins](https://github.com/andir/npins)-based environments. Exposes `write-npins`. Supports `github`, `gitlab`, `channel`, `tarball`, and `git` schemes. Respects `follows` for deduplication. Prunes stale pins automatically.
Expand Down
2 changes: 2 additions & 0 deletions docs/src/content/docs/guides/lock-flattening.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ description: Automatically flatten flake.lock to reduce dependency duplication.

Nix flakes fetch every input's own lock file, which can result in many copies of the same package set (e.g. multiple `nixpkgs` versions) being downloaded and stored. Flattening forces all inputs to share common dependencies.

The lock-flattening hook is an alternative to [automatic `follows` declarations with `flake-edit`](/guides/auto-follow). Flattening keeps generated follows out of `flake.nix` and records the deduplication only in `flake.lock`. See the [comparison table](/guides/auto-follow#automatic-follows-or-lock-flattening) before choosing; generally, enable one approach rather than both.

## Built-in Modules

flake-file ships support for two established flattening tools:
Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ flake-file treats `flake.nix` as a generated artifact. You declare inputs and se

<CardGrid>
<Card title="flakeModules" icon="puzzle">
All built-in flakeModules: `default`, `dendritic`, `import-tree`, `npins`, `unflake`, `tack`, `allfollow`, `nix-auto-follow`, and `flake-parts-builder`.
All built-in flakeModules: `default`, `dendritic`, `import-tree`, `npins`, `unflake`, `tack`, `allfollow`, `nix-auto-follow`, `auto-follow`, and `flake-parts-builder`.
<LinkButton href="/guides/flake-modules" variant="minimal" icon="right-arrow">Learn More</LinkButton>
</Card>
<Card title="Templates" icon="document">
Expand Down
Loading
Loading