libexpr/primops: allow storePath in pure evaluation mode
`builtins.storePath` allows you to use "external" knowledge of present
store paths in your store to be used in your expressions.
This is sometimes used to avoid double wrapping store paths, for example
when dealing with nixpkgs sources (`builtins.storePath pkgs.path` is a
frequent pattern).
Unfortunately, using your external knowledge of present store paths is
technically impure because Flakes is about not depending on the *STATE*
of your store, no matter what you know about it or what you do to ensure
that your knowledge is accurate.
That being said, `builtins.storePath` is already possible in the
fragment of "pure Nixlang" via a clever use of `builtins.appendContext`.
Precisely:
```
storePath = path:
let path' = builtins.unsafeDiscardStringContext path;
in
# NOTE: merging the context set in all generality is impossible
because getContext on a pure path doesn't work.
builtins.appendContext path' { ${path'} = { path = true; }; };
```
works in pure contexts.
Because of this, we deem that storePath should be allowed in pure
contexts and users do not have to come up with a `storePath` polyfill.
Fixes #402.
Change-Id: I5fff750a4feed082745431835db451f945249cfb
Signed-off-by: Raito Bezarius <raito@lix.systems>
This commit is contained in:
committed by
eldritch horrors
parent
e8015836e3
commit
37ccd28c3b
@@ -0,0 +1,28 @@
|
||||
---
|
||||
synopsis: "`builtins.storePath` is now allowed in pure contexts"
|
||||
cls: [5285]
|
||||
issues: [fj#402]
|
||||
category: "Features"
|
||||
credits: [raito, horrors]
|
||||
---
|
||||
|
||||
`builtins.storePath` allows you to exploit your "external" knowledge about a store path and reuse it to avoid a (needless) copy to the store.
|
||||
|
||||
In Lix, pure evaluation ensures that you cannot depend on the state of things that were not locked down as part of your inputs to achieve the vision of "outputs are a pure function of inputs".
|
||||
|
||||
In this case, `builtins.storePath` allows you to depend on the state of your store and was therefore deemed an impure built-in.
|
||||
|
||||
Unfortunately, it is possible to replicate `builtins.storePath` functionality in the pure context with a clever application of `appendContext`:
|
||||
|
||||
```nix
|
||||
{
|
||||
storePath = path:
|
||||
let path' = builtins.unsafeDiscardStringContext path;
|
||||
in
|
||||
# NOTE: merging the context set in all generality is impossible
|
||||
because getContext on a pure path doesn't work.
|
||||
builtins.appendContext path' { ${path'} = { path = true; }; };
|
||||
}
|
||||
```
|
||||
|
||||
Out of pragmatism in the context of this possibility, `builtins.storePath` is now useable in pure contexts.
|
||||
@@ -1318,14 +1318,13 @@ static void prim_toPath(EvalState & state, Value * * args, Value & v)
|
||||
corner cases. */
|
||||
static void prim_storePath(EvalState & state, Value * * args, Value & v)
|
||||
{
|
||||
if (evalSettings.pureEval)
|
||||
state.ctx.errors.make<EvalError>(
|
||||
"'%s' is not allowed in pure evaluation mode",
|
||||
"builtins.storePath"
|
||||
).debugThrow();
|
||||
|
||||
NixStringContext context;
|
||||
auto path = state.ctx.paths.checkSourcePath(state.coerceToPath(noPos, *args[0], context, "while evaluating the first argument passed to builtins.storePath")).canonical();
|
||||
auto path =
|
||||
state
|
||||
.coerceToPath(
|
||||
noPos, *args[0], context, "while evaluating the first argument passed to builtins.storePath"
|
||||
)
|
||||
.canonical();
|
||||
/* Resolve symlinks in ‘path’, unless ‘path’ itself is a symlink
|
||||
directly in the store. The latter condition is necessary so
|
||||
e.g. nix-push does the right thing. */
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
from testlib.fixtures.nix import Nix
|
||||
from testlib.utils import get_global_asset
|
||||
from testlib.fixtures.file_helper import File, with_files
|
||||
from testlib.fixtures.env import ManagedEnv
|
||||
from pathlib import Path
|
||||
import pytest
|
||||
|
||||
files = {
|
||||
"config.nix": get_global_asset("config.nix"),
|
||||
"default.nix": File("""
|
||||
with import ./config.nix;
|
||||
|
||||
rec {
|
||||
existing = mkDerivation {
|
||||
name = "existing";
|
||||
buildCommand = ''
|
||||
echo meow > $out
|
||||
'';
|
||||
};
|
||||
reuse-existing = mkDerivation {
|
||||
name = "reuse-existing";
|
||||
buildCommand = ''
|
||||
cat ${builtins.storePath existing.outPath} > $out
|
||||
'';
|
||||
};
|
||||
}
|
||||
"""),
|
||||
"flake.nix": File("""
|
||||
{ outputs = _: (import ./default.nix); }
|
||||
"""),
|
||||
}
|
||||
|
||||
|
||||
@with_files(files)
|
||||
def test_primop_store_path_exists(nix: Nix):
|
||||
# We build the `existing` path, it's in the store now.
|
||||
nix.nix_build(["-A", "existing"]).run().ok()
|
||||
nix.nix_build(["-A", "reuse-existing"]).run().ok()
|
||||
|
||||
|
||||
@with_files(files)
|
||||
@pytest.mark.parametrize("under_pure_eval", [True, False])
|
||||
def test_primop_store_path_missing(nix: Nix, under_pure_eval: bool):
|
||||
extra_options = ["--pure-eval"] if under_pure_eval else []
|
||||
# We did not build `existing` path, this should fail.
|
||||
nix.nix_build(["-A", "reuse-existing", *extra_options]).run().expect(1)
|
||||
|
||||
|
||||
@with_files(files)
|
||||
class TestPrimopStorePathSubstitutes:
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup(self, nix: Nix, env: ManagedEnv, files: Path):
|
||||
# We build `existing` path.
|
||||
self.output = (
|
||||
nix.nix_build([str(files / "default.nix"), "-A", "existing"]).run().ok().stdout_plain
|
||||
)
|
||||
cache_dir = env.dirs.home / "cache"
|
||||
nix.nix(
|
||||
["copy", self.output, "--to", f"file://{cache_dir}", "--no-require-sigs"], flake=True
|
||||
).run().ok()
|
||||
nix.clear_store()
|
||||
|
||||
nix.settings.substituters = f"file://{cache_dir}"
|
||||
nix.settings.require_sigs = False
|
||||
|
||||
def test_restricted_eval(self, nix: Nix, env: ManagedEnv):
|
||||
# Using -I lists it as an allowed path.
|
||||
nix.nix_build(
|
||||
[
|
||||
"-A",
|
||||
"reuse-existing",
|
||||
"--restrict-eval",
|
||||
"-I",
|
||||
f"test_file={env.dirs.home}/default.nix",
|
||||
"-I",
|
||||
f"test_file={env.dirs.home}/config.nix",
|
||||
]
|
||||
).run().ok()
|
||||
|
||||
def test_pure_eval(self, nix: Nix):
|
||||
# nix2 cli can't load expressions from file systems in pure eval mode.
|
||||
# nix3 cli substitute in sandboxes because they have no network, where
|
||||
# it will just hard-disable all substitution. even for file:// caches.
|
||||
expr = f"""
|
||||
derivation {{
|
||||
name = "reuse-existing";
|
||||
system = "f2";
|
||||
builder = "/bin/sh";
|
||||
args = [ "-c" "echo ${{builtins.storePath "{self.output}"}} >$out" ];
|
||||
}}
|
||||
"""
|
||||
nix.nix_build(["--pure-eval", "--system", "f2", "--expr", expr]).run().ok()
|
||||
|
||||
def test_impure(self, nix: Nix):
|
||||
nix.nix_build(["-A", "reuse-existing"]).run().ok()
|
||||
Reference in New Issue
Block a user