diff --git a/tests.nix b/tests.nix index c10eee1..19e42c2 100644 --- a/tests.nix +++ b/tests.nix @@ -413,6 +413,29 @@ in expected = "/our-nixpkgs"; }; + input-overrides.test-follows-cycle-is-refused = { + expr = + (with-inputs + ( + import ./fixtures/npins + // { + test2.outPath = ./fixtures/with-inputs-project2; + } + ) + { + nixpkgs = mkSrc "/our-nixpkgs"; + nixpkgs3.follows = "common-dep/x"; + common-dep.follows = "nixpkgs3/y"; + test3.outPath = ./fixtures/with-inputs-project3; + } + (inputs: { + res = inputs.test3.usedNixpkgs; + }) + ).res.outPath; + expectedError.type = "ThrownError"; + expectedError.msg = "follows cycle"; + }; + # ── Dependency introspection ──────────────────────────────────────────────── introspection.test-access-sub-flake-inputs = { diff --git a/with-inputs.nix b/with-inputs.nix index 37bb577..0be5c3b 100644 --- a/with-inputs.nix +++ b/with-inputs.nix @@ -186,17 +186,29 @@ let let recFollows = let + # `seen`: the inputs already on this chain, so a follows cycle is refused by name, not a stack overflow. follows = - input: + seen: input: isFollows inputs.${input} && ( let followRoot = builtins.head (builtins.split "/" inputs.${input}.follows); + chain = seen ++ [ input ]; + cycle = builtins.concatStringsSep " -> " (chain ++ [ followRoot ]); in - followRoot == name || (inputs ? ${followRoot} && follows followRoot) + followRoot == name + || ( + inputs ? ${followRoot} + && ( + if builtins.elem followRoot chain then + throw "with-inputs: follows cycle: ${cycle}" + else + follows chain followRoot + ) + ) ); in - [ name ] ++ builtins.filter follows (builtins.attrNames inputs); + [ name ] ++ builtins.filter (follows [ ]) (builtins.attrNames inputs); in builtins.mapAttrs (