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
29 changes: 27 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ with-inputs sources follows outputs
The `with-inputs` function takes three arguments:

1. already fetched `<name>.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
Expand Down Expand Up @@ -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
Expand All @@ -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.

Expand Down
6 changes: 4 additions & 2 deletions default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
14 changes: 14 additions & 0 deletions fixtures/with-inputs-flake/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
inputsOverrides ? { },
}:
let
withInputs = import ../../. (import ../npins) [
(i: {
nixpkgs2.follows = "nixpkgs";
})
inputsOverrides
];
in
withInputs (inputs: {
usedNixpkgs = inputs.nixpkgs2;
})
1 change: 1 addition & 0 deletions fixtures/with-inputs-flake/flake.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ outputs = inputs: import ./. { inputsOverrides = removeAttrs inputs [ "self" ]; }; }
24 changes: 12 additions & 12 deletions templates/lon/lon.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7,39 +7,39 @@
"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",
"fetchType": "tarball",
"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",
"fetchType": "tarball",
"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",
"fetchType": "tarball",
"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="
}
}
}
44 changes: 16 additions & 28 deletions templates/lon/lon.nix
Original file line number Diff line number Diff line change
@@ -1,53 +1,41 @@
# 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
replacement = builtins.getEnv "LON_OVERRIDE_${name}";
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}";

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
42 changes: 36 additions & 6 deletions tests.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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: {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
];
Expand Down
54 changes: 47 additions & 7 deletions with-inputs.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);

Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down
Loading