diff --git a/README.md b/README.md index 9717043..63282c4 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ with-inputs sources follows outputs The `with-inputs` function takes three arguments: 1. already fetched `.outPath` attrs. -2. a function `inputs: specs` for custom follows, input shims or sources overrides. +2. a `specs` attrs or function `inputs: specs` (or a list of such function/attrs) for custom follows, input shims or sources overrides. 3. a function `inputs: outputs` like in flakes. with-inputs does automatic input follows -- having `x.inputs.y` will automatically lookup for a @@ -72,7 +72,7 @@ Download our `default.nix` into your project `./with-inputs.nix`. curl https://raw.githubusercontent.com/vic/with-inputs/refs/heads/main/default.nix -o with-inputs.nix ``` -Or use npins or `builtins.fetchTarball` with a fixed revision of it. [^output-trick] +Or use npins or `builtins.fetchTarball` with a fixed revision of it. [^output-trick] [^output-trick-2] ```shell npins add github vic with-inputs @@ -90,17 +90,42 @@ let in with-inputs outputs ``` +or if you want to allow overrides of inputs from a consumer project: +```nix +# default.nix +{ + inputsOverrides ? { }, +}: +let + sources = import ./npins; # example with npins. use any other sources. + with-inputs = import sources.with-inputs sources [ { + # keep reading for follows and local inputs + } inputsOverrides ]; + + outputs = inputs: { }; # your flake-like outputs function +in +with-inputs outputs +``` [^output-trick]: To use the experimental `nix` cli commands, create a `flake.nix` containing only ```nix { outputs = _: import ./.; } ``` +[^output-trick-2]: To additionally allow inputs overrides (eg, by a `with-inputs`-based consummer project): + ```nix + { outputs = inputsOverrides: import ./. { inherit inputsOverrides; } + ``` + +### Flake backed by non-flake pins +When `with-inputs` detect a flake dependency which does not declare any inputs, that flake `output` function is still called with the all available inputs, so they could be used as overrides. ### Follows and local checkout overrides The second argument to `with-inputs` is an attribute set that can be used to drive input resolution, for example to use local checkout or to specify flake-like follows. +Alternatively that second argument can also be a function taking `inputs` and returning such an attribute set. +It can also be a list of such functions or attribute sets (which is useful to accept overrides from consumer projects). See [tests.nix](./tests.nix) and [vic/vix:follows.nix](https://github.com/vic/vix/tree/unflake/follows.nix) for usage examples. diff --git a/default.nix b/default.nix index a6e3523..e3cefee 100644 --- a/default.nix +++ b/default.nix @@ -9,11 +9,13 @@ let with-inputs = sources: follows: let - f = import ./with-inputs.nix (may-import sources) (may-import follows); + f = import ./with-inputs.nix (may-import sources) ( + if builtins.isList follows then map may-import follows else may-import follows + ); in f // { - __functor = _: outputs: f (may-import outputs); + __functor = allInputs: outputs: f.__functor allInputs (may-import outputs); }; from.niv = root: with-inputs (root + "/nix/sources.nix"); diff --git a/fixtures/with-inputs-flake/default.nix b/fixtures/with-inputs-flake/default.nix new file mode 100644 index 0000000..f4c651d --- /dev/null +++ b/fixtures/with-inputs-flake/default.nix @@ -0,0 +1,14 @@ +{ + inputsOverrides ? { }, +}: +let + withInputs = import ../../. (import ../npins) [ + (i: { + nixpkgs2.follows = "nixpkgs"; + }) + inputsOverrides + ]; +in +withInputs (inputs: { + usedNixpkgs = inputs.nixpkgs2; +}) diff --git a/fixtures/with-inputs-flake/flake.nix b/fixtures/with-inputs-flake/flake.nix new file mode 100644 index 0000000..3ea9e28 --- /dev/null +++ b/fixtures/with-inputs-flake/flake.nix @@ -0,0 +1 @@ +{ outputs = inputs: import ./. { inputsOverrides = removeAttrs inputs [ "self" ]; }; } diff --git a/templates/lon/lon.lock b/templates/lon/lon.lock index 62a7e68..163ab67 100644 --- a/templates/lon/lon.lock +++ b/templates/lon/lon.lock @@ -7,9 +7,9 @@ "owner": "denful", "repo": "den", "branch": "main", - "revision": "0af82e24be89b9fd400bd0b58b0fed5ea0f269ad", - "url": "https://github.com/denful/den/archive/0af82e24be89b9fd400bd0b58b0fed5ea0f269ad.tar.gz", - "hash": "sha256-q4WXIX2E3w9Ld3MZ1Pl8Lh5SgrEFdEuzvY1Lj/Wo2kY=" + "revision": "4f15fb43dd17a5ace3680cde297e31bc7e017c91", + "url": "https://github.com/denful/den/archive/4f15fb43dd17a5ace3680cde297e31bc7e017c91.tar.gz", + "hash": "sha256-zWghnHLAo5rrOxZZXA2E5rY+aEKx/MwVQzL5eCbexJc=" }, "home-manager": { "type": "GitHub", @@ -17,9 +17,9 @@ "owner": "nix-community", "repo": "home-manager", "branch": "master", - "revision": "master", - "url": "https://github.com/nix-community/home-manager/archive/master.tar.gz", - "hash": "sha256-ax3229dUvNuwTQwo2o68kOQ24dvOlJ/BrVYY4miD1bI=" + "revision": "1dc2d1f720ab17fc7981e087346bf54b26d284b1", + "url": "https://github.com/nix-community/home-manager/archive/1dc2d1f720ab17fc7981e087346bf54b26d284b1.tar.gz", + "hash": "sha256-G0F2rFORVcFkEvVdE/qWQTnYekIc77YP5/vRF9nZlxU=" }, "nixpkgs": { "type": "GitHub", @@ -27,9 +27,9 @@ "owner": "nixos", "repo": "nixpkgs", "branch": "master", - "revision": "nixpkgs-unstable", - "url": "https://github.com/nixos/nixpkgs/archive/nixpkgs-unstable.tar.gz", - "hash": "sha256-WacE23EbHTsBKvr8cu+1DFNbP6Rh1brHUH5SDUI0NQI=" + "revision": "35adf7a938f3189febd497330e2f1fe09b632a69", + "url": "https://github.com/nixos/nixpkgs/archive/35adf7a938f3189febd497330e2f1fe09b632a69.tar.gz", + "hash": "sha256-cZPb6rWoDLkSFBFVXT9iznDfWQwSRdP+YYukEVGPPFY=" }, "with-inputs": { "type": "GitHub", @@ -37,9 +37,9 @@ "owner": "denful", "repo": "with-inputs", "branch": "main", - "revision": "b4cbe858b381c0ee0fe617498549f0562090ad04", - "url": "https://github.com/denful/with-inputs/archive/b4cbe858b381c0ee0fe617498549f0562090ad04.tar.gz", - "hash": "sha256-GgKZ4LyKDS9vd946UeArhYqBKw63LdH4JqfbfFf7qNw=" + "revision": "dae6b8126fe613bfe12185d94d593db426a34daa", + "url": "https://github.com/denful/with-inputs/archive/dae6b8126fe613bfe12185d94d593db426a34daa.tar.gz", + "hash": "sha256-ZPGmgcJX+xBAbA0edQC59ewoH56LhAqkH1Y/5KjV9J0=" } } } diff --git a/templates/lon/lon.nix b/templates/lon/lon.nix index a88d448..3a4671f 100644 --- a/templates/lon/lon.nix +++ b/templates/lon/lon.nix @@ -1,10 +1,11 @@ +# SPDX-FileCopyrightText: 2026 lon contributors +# +# SPDX-License-Identifier: MIT + # Generated by lon. Do not modify! let - lock = builtins.fromJSON (builtins.readFile ./lon.lock); - # Override with a path defined in an environment variable. If no variable is - # set, the original path is used. overrideFromEnv = name: path: let @@ -12,10 +13,7 @@ let in if replacement == "" then path - else - # this turns the string into an actual Nix path (for both absolute and - # relative paths) - if builtins.substring 0 1 replacement == "/" then + else if builtins.substring 0 1 replacement == "/" then /. + replacement else /. + builtins.getEnv "PWD" + "/${replacement}"; @@ -23,31 +21,21 @@ let fetchSource = args@{ fetchType, ... }: if fetchType == "git" then - builtins.fetchGit ( - { - url = args.url; - ref = args.branch; - rev = args.revision; - narHash = args.hash; - submodules = args.submodules; - } - // ( - if args ? lastModified then - { - inherit (args) lastModified; - shallow = true; - } - else - { } - ) - ) + fetchGit { + url = args.url; + ref = args.branch; + rev = args.revision; + narHash = args.hash; + submodules = args.submodules; + lastModified = args.lastModified; + shallow = true; + } else if fetchType == "tarball" then - builtins.fetchTarball { + fetchTarball { url = args.url; sha256 = args.hash; } else - builtins.throw "Unsupported source type ${fetchType}"; - + throw "Unsupported source type ${fetchType}"; in builtins.mapAttrs (name: args: overrideFromEnv name (fetchSource args)) lock.sources diff --git a/tests.nix b/tests.nix index cb25a95..ab4e644 100644 --- a/tests.nix +++ b/tests.nix @@ -316,19 +316,19 @@ in # ── Dependency introspection ──────────────────────────────────────────────── - introsepction.test-access-sub-flake-inputs = { + introspection.test-access-sub-flake-inputs = { # inputs.someFlake.inputs.dep — traverse a dependency's own inputs expr = (with-inputs { } { a = mkFlake { dep = mkSrc "/dep"; } { }; }).a.inputs.dep.outPath; expected = "/dep"; }; - introsepction.test-access-sub-flake-outputs = { + introspection.test-access-sub-flake-outputs = { # inputs.someFlake.outputs.lib — explicit outputs access expr = (with-inputs { } { a = mkFlake { } { lib = "mylib"; }; }).a.outputs.lib; expected = "mylib"; }; - introsepction.test-sub-flake-output-attrs-merged-at-top-level = { + introspection.test-sub-flake-output-attrs-merged-at-top-level = { # inputs.someFlake.lib ≡ inputs.someFlake.outputs.lib expr = let @@ -346,6 +346,21 @@ in expected = "mylib"; }; + introspection.test-follow-sub-npins-with-inputs-input = { + # my-lib.inputs.nixpkgs.follows = "with-inputs-dep/nixpkgs" → traverse native inputs + expr = + (with-inputs + { + my-lib = mkSrc ./fixtures/fake-flake; + with-inputs-dep = mkSrc ./fixtures/with-inputs-flake; + } + { + my-lib.inputs.nixpkgs.follows = "with-inputs-dep/nixpkgs2"; + } + ).my-lib.inputs.nixpkgs.outPath; + expected = npins.nixpkgs.outPath; + }; + real-flakes.test-npins-nix-maid-nixosModules-output-is-readable = { expr = (with-inputs npins { } (inputs: { @@ -378,6 +393,24 @@ in expected = npins.nixpkgs.outPath; }; + real-flakes.test-sub-npins-with-inputs-nested-follows = { + # with-inputs-dep.inputs.nixpkgs.follows = "inputs/nixpkgs" → "blind" override of with-inputs-dep.inputs.nixpkgs + expr = + (with-inputs + { + my-lib = mkFlake { nixpkgs = mkSrc "/nested-nixpkgs"; } { }; + with-inputs-dep = mkSrc ./fixtures/with-inputs-flake; + } + { + with-inputs-dep.inputs.nixpkgs2.follows = "my-lib/nixpkgs"; + } + (inputs: { + check = inputs.with-inputs-dep.usedNixpkgs; + }) + ).check.outPath; + expected = "/nested-nixpkgs"; + }; + non-flakes.test-non-flakes-are-not-evaluated = { expr = (with-inputs @@ -408,14 +441,11 @@ in expr = (with-inputs ./fixtures/npins ./fixtures/auto/follows.nix ./fixtures/auto/outputs.nix).result; expected = [ - "__functor" "foo" "hjem" "home-manager" - "inputs" "nix-maid" "nixpkgs" - "outputs" "self" "smfh" ]; diff --git a/with-inputs.nix b/with-inputs.nix index 3a3587b..6ec90d7 100644 --- a/with-inputs.nix +++ b/with-inputs.nix @@ -4,7 +4,7 @@ # Expects pre-fetched sources (npins, unflake, local paths, anything with .outPath). # # sources: attrset of name -> sourceInfo (e.g. from npins) -# inputs: attrset mirroring the `inputs` block of a flake.nix: +# inputsOverrides: [list of] attrset (or inputs -> attrset) mirroring the `inputs` block of a flake.nix: # someLib.outPath = ./someLib; local checkout (loaded as flake if possible) # b.follows = "a"; alias to allInputs.a # b.follows = "a/x/y"; nested follows @@ -15,7 +15,13 @@ sources: inputsOverrides: let inputs = - if builtins.isAttrs inputsOverrides then inputsOverrides else (__functor allInputs) inputsOverrides; + let + f = io: if builtins.isAttrs io then io else (__functor allInputs io).outputs; + in + if builtins.isList inputsOverrides then + builtins.foldl' (x: y: x // (f y)) { } inputsOverrides + else + f inputsOverrides; splitPath = s: builtins.filter builtins.isString (builtins.split "/" s); @@ -109,19 +115,52 @@ let mkFlakeInput = name: sourceInfo: flake: + let + topLevelInputs = inputs; + in let specs = flake.inputs or { }; - inputs = builtins.mapAttrs (sub: spec: resolveSubInput name sub spec) specs; + direct = builtins.mapAttrs (sub: spec: resolveSubInput name sub spec) specs; indirect = builtins.mapAttrs (sub: _: resolveSubInput name sub { }) ( builtins.functionArgs flake.outputs ); - outputs = flake.outputs (indirect // inputs // { inherit self; }); + nonEmptyInputs = direct != { } || indirect != { }; + inputs = + if nonEmptyInputs then + indirect // direct + else + # Assume inputs are not handled by flake, but output function + # may still accept inputs overrides: we give it allInputs minus + # those that would obviously trigger infinite recursion + # (inputs defined as follows of inputs of the flake we are importing) + # plus inputs overrides declared for this flake. + let + recFollows = + let + follows = + input: + isFollows topLevelInputs.${input} + && ( + let + followRoot = builtins.head (builtins.split "/" topLevelInputs.${input}.follows); + in + followRoot == name || follows followRoot + ); + in + [ name ] ++ builtins.filter follows (builtins.attrNames topLevelInputs); + in + removeAttrs allInputs recFollows + // (builtins.mapAttrs (sub: spec: resolveSubInput name sub spec) ( + topLevelInputs.${name}.inputs or { } + )); + outputs = flake.outputs (inputs // { inherit self; }); self = sourceInfo // outputs // { _type = "flake"; - inherit inputs outputs sourceInfo; + inputs = if nonEmptyInputs then inputs else outputs.inputs or { inherit self; }; + inherit outputs sourceInfo; }; in self; @@ -139,14 +178,15 @@ let let # inputs mirrors a real flake: self is included so modules can access # inputs.self.inputs, inputs.self.outputs, and inputs.self.outPath. - inputs = allInputs // { + inputs = removeAttrs allInputs [ "__functor" ] // { inherit self; }; outputs = outputsFn inputs; - # self exposes .inputs and .outputs like a real flake self, with all + # self exposes .inputs, .outputs and ._type like a real flake self, with all # output attributes merged at top level for direct attribute access. self = outputs // { inherit inputs outputs; + _type = "flake"; }; in self;