Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
3751c06
coerceToStorePath(): Improve error message
edolstra Feb 6, 2025
f24ff05
Make `nix flake metadata|update|lock` lazy
edolstra Feb 6, 2025
9e6b89c
lockFlake(): Always compute a NAR hash for inputs
edolstra Feb 7, 2025
2890a2e
Typo
edolstra Feb 7, 2025
3432184
Compute NAR hash for Git archive flakes if --no-trust-tarballs-from-g…
edolstra Feb 10, 2025
307ce9b
Add NAR hash mismatch test
edolstra Feb 10, 2025
228dac4
Merge remote-tracking branch 'origin/master' into lazy-flake-commands
edolstra Feb 13, 2025
c891554
Merge branch 'lazy-flake-commands' into lazy-trees-v2
edolstra Apr 8, 2025
febd28d
Lazily copy trees to the store
edolstra Apr 8, 2025
fa5cb62
Revert unneeded test change
edolstra Apr 8, 2025
f058567
Fix printAmbiguous() / printValueAsJSON()
edolstra Apr 8, 2025
f45db85
Actually ignore system/user registries during locking
edolstra Apr 9, 2025
0cb06d7
Rename FlakeCache -> InputCache and key it on Inputs instead of Flake…
edolstra Apr 9, 2025
3bbf917
Move the input cache into libfetchers
edolstra Apr 9, 2025
dd15c8a
Move getAccessorCached() to InputCache
edolstra Apr 9, 2025
62565ce
Remove unused variable
edolstra Apr 10, 2025
e099a5b
Move the InputCache to EvalState
edolstra Apr 14, 2025
0c0dda3
Devirtualize double-copied paths
edolstra Apr 15, 2025
43a2691
unsafeGetAttrPos: Set string context on store paths
edolstra Apr 18, 2025
a6faa69
Merge remote-tracking branch 'detsys/detsys-main' into lazy-trees-tmp
edolstra Apr 23, 2025
ff85b34
Temporarily run all flake regression tests
edolstra Apr 23, 2025
182edb4
Move mountInput into EvalState
edolstra Apr 23, 2025
9d87ab1
Add a setting to enable lazy trees
edolstra Apr 24, 2025
2aa3655
computeBaseName(): Respect the original store path name
edolstra Apr 24, 2025
88cd822
Fix the nix-community/patsh/0.2.1 flake regression test (again)
edolstra Apr 25, 2025
ae5ac8a
Limit parallelism
edolstra Apr 25, 2025
fef193f
Try namespace runner
edolstra May 6, 2025
577b331
Merge remote-tracking branch 'detsys/detsys-main' into lazy-trees-v2
edolstra May 6, 2025
6f5cfaf
Run flake-regressions with --lazy-trees
edolstra May 6, 2025
630bdff
Re-enable _NIX_TEST_FAIL_ON_LARGE_PATH tests
edolstra May 7, 2025
91cde8c
EvalState::mountInput(): Throw an error if there is a NAR hash mismatch
edolstra May 7, 2025
9bab483
Improve error message
edolstra May 7, 2025
d0a89fa
Put flake_regressions back in the merge queue
edolstra May 7, 2025
f6ad629
nix flake metadata: Show store path if available
edolstra May 7, 2025
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
27 changes: 25 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,30 @@ jobs:
flake_regressions:
if: github.event_name == 'merge_group'
needs: build_x86_64-linux
runs-on: blacksmith-32vcpu-ubuntu-2204
runs-on: namespace-profile-x86-32cpu-64gb
steps:
- name: Checkout nix
uses: actions/checkout@v4
- name: Checkout flake-regressions
uses: actions/checkout@v4
with:
repository: DeterminateSystems/flake-regressions
path: flake-regressions
- name: Checkout flake-regressions-data
uses: actions/checkout@v4
with:
repository: DeterminateSystems/flake-regressions-data
path: flake-regressions/tests
- uses: DeterminateSystems/nix-installer-action@main
with:
determinate: true
- uses: DeterminateSystems/flakehub-cache-action@main
- run: nix build -L --out-link ./new-nix && PATH=$(pwd)/new-nix/bin:$PATH PARALLEL="-P 50%" flake-regressions/eval-all.sh

flake_regressions_lazy:
if: github.event_name == 'merge_group'
needs: build_x86_64-linux
runs-on: namespace-profile-x86-32cpu-64gb
steps:
- name: Checkout nix
uses: actions/checkout@v4
Expand All @@ -109,7 +132,7 @@ jobs:
with:
determinate: true
- uses: DeterminateSystems/flakehub-cache-action@main
- run: nix build -L --out-link ./new-nix && PATH=$(pwd)/new-nix/bin:$PATH MAX_FLAKES=50 flake-regressions/eval-all.sh
- run: nix build -L --out-link ./new-nix && PATH=$(pwd)/new-nix/bin:$PATH PARALLEL="-P 50%" NIX_CONFIG="lazy-trees = true" flake-regressions/eval-all.sh

manual:
if: github.event_name != 'merge_group'
Expand Down
3 changes: 2 additions & 1 deletion src/libcmd/installable-value.cc
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ std::optional<DerivedPathWithInfo> InstallableValue::trySinglePathToDerivedPaths
else if (v.type() == nString) {
return {{
.path = DerivedPath::fromSingle(
state->coerceToSingleDerivedPath(pos, v, errorCtx)),
state->devirtualize(
state->coerceToSingleDerivedPath(pos, v, errorCtx))),
.info = make_ref<ExtraPathInfo>(),
}};
}
Expand Down
30 changes: 21 additions & 9 deletions src/libexpr/eval.cc
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include "nix/util/url.hh"
#include "nix/fetchers/fetch-to-store.hh"
#include "nix/fetchers/tarball.hh"
#include "nix/fetchers/input-cache.hh"

#include "parser-tab.hh"

Expand Down Expand Up @@ -266,12 +267,9 @@ EvalState::EvalState(
/nix/store while using a chroot store. */
auto accessor = getFSSourceAccessor();

auto realStoreDir = dirOf(store->toRealPath(StorePath::dummy));
if (settings.pureEval || store->storeDir != realStoreDir) {
accessor = settings.pureEval
? storeFS.cast<SourceAccessor>()
: makeUnionSourceAccessor({accessor, storeFS});
}
accessor = settings.pureEval
? storeFS.cast<SourceAccessor>()
: makeUnionSourceAccessor({accessor, storeFS});

/* Apply access control if needed. */
if (settings.restrictEval || settings.pureEval)
Expand All @@ -293,6 +291,7 @@ EvalState::EvalState(
)}
, store(store)
, buildStore(buildStore ? buildStore : store)
, inputCache(fetchers::InputCache::create())
, debugRepl(nullptr)
, debugStop(false)
, trylevel(0)
Expand Down Expand Up @@ -949,7 +948,16 @@ void EvalState::mkPos(Value & v, PosIdx p)
auto origin = positions.originOf(p);
if (auto path = std::get_if<SourcePath>(&origin)) {
auto attrs = buildBindings(3);
attrs.alloc(sFile).mkString(path->path.abs());
if (path->accessor == rootFS && store->isInStore(path->path.abs()))
// FIXME: only do this for virtual store paths?
attrs.alloc(sFile).mkString(path->path.abs(),
{
NixStringContextElem::Opaque{
.path = store->toStorePath(path->path.abs()).first
}
});
else
attrs.alloc(sFile).mkString(path->path.abs());
makePositionThunks(*this, p, attrs.alloc(sLine), attrs.alloc(sColumn));
v.mkAttrs(attrs);
} else
Expand Down Expand Up @@ -1135,6 +1143,7 @@ void EvalState::resetFileCache()
{
fileEvalCache.clear();
fileParseCache.clear();
inputCache->clear();
}


Expand Down Expand Up @@ -2317,6 +2326,9 @@ BackedStringView EvalState::coerceToString(
}

if (v.type() == nPath) {
// FIXME: instead of copying the path to the store, we could
// return a virtual store path that lazily copies the path to
// the store in devirtualize().
return
!canonicalizePath && !copyToStore
? // FIXME: hack to preserve path literals that end in a
Expand Down Expand Up @@ -2406,7 +2418,7 @@ StorePath EvalState::copyPathToStore(NixStringContext & context, const SourcePat
*store,
path.resolveSymlinks(SymlinkResolution::Ancestors),
settings.readOnlyMode ? FetchMode::DryRun : FetchMode::Copy,
path.baseName(),
computeBaseName(path),
ContentAddressMethod::Raw::NixArchive,
nullptr,
repair);
Expand Down Expand Up @@ -2461,7 +2473,7 @@ StorePath EvalState::coerceToStorePath(const PosIdx pos, Value & v, NixStringCon
auto path = coerceToString(pos, v, context, errorCtx, false, false, true).toOwned();
if (auto storePath = store->maybeParseStorePath(path))
return *storePath;
error<EvalError>("path '%1%' is not in the Nix store", path).withTrace(pos, errorCtx).debugThrow();
error<EvalError>("cannot coerce '%s' to a store path because it is not a subpath of the Nix store", path).withTrace(pos, errorCtx).debugThrow();
}


Expand Down
5 changes: 5 additions & 0 deletions src/libexpr/include/nix/expr/eval-settings.hh
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,11 @@ struct EvalSettings : Config

This option can be enabled by setting `NIX_ABORT_ON_WARN=1` in the environment.
)"};

Setting<bool> lazyTrees{this, false, "lazy-trees",
R"(
If set to true, flakes and trees fetched by [`builtins.fetchTree`](@docroot@/language/builtins.md#builtins-fetchTree) are only copied to the Nix store when they're used as a dependency of a derivation. This avoids copying (potentially large) source trees unnecessarily.
)"};
};

/**
Expand Down
42 changes: 41 additions & 1 deletion src/libexpr/include/nix/expr/eval.hh
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ namespace nix {
constexpr size_t maxPrimOpArity = 8;

class Store;
namespace fetchers { struct Settings; }
namespace fetchers {
struct Settings;
struct InputCache;
struct Input;
}
struct EvalSettings;
class EvalState;
class StorePath;
Expand Down Expand Up @@ -301,6 +305,8 @@ public:

RootValue vImportedDrvToDerivation = nullptr;

ref<fetchers::InputCache> inputCache;

/**
* Debugger
*/
Expand Down Expand Up @@ -445,6 +451,15 @@ public:

void checkURI(const std::string & uri);

/**
* Mount an input on the Nix store.
*/
StorePath mountInput(
fetchers::Input & input,
const fetchers::Input & originalInput,
ref<SourceAccessor> accessor,
bool requireLockable);

/**
* Parse a Nix expression from the specified file.
*/
Expand Down Expand Up @@ -554,6 +569,18 @@ public:
std::optional<std::string> tryAttrsToString(const PosIdx pos, Value & v,
NixStringContext & context, bool coerceMore = false, bool copyToStore = true);

StorePath devirtualize(
const StorePath & path,
StringMap * rewrites = nullptr);

SingleDerivedPath devirtualize(
const SingleDerivedPath & path,
StringMap * rewrites = nullptr);

std::string devirtualize(
std::string_view s,
const NixStringContext & context);

/**
* String coercion.
*
Expand All @@ -569,6 +596,19 @@ public:

StorePath copyPathToStore(NixStringContext & context, const SourcePath & path);


/**
* Compute the base name for a `SourcePath`. For non-store paths,
* this is just `SourcePath::baseName()`. But for store paths, for
* backwards compatibility, it needs to be `<hash>-source`,
* i.e. as if the path were copied to the Nix store. This results
* in a "double-copied" store path like
* `/nix/store/<hash1>-<hash2>-source`. We don't need to
* materialize /nix/store/<hash2>-source though. Still, this
* requires reading/hashing the path twice.
*/
std::string computeBaseName(const SourcePath & path);

/**
* Path coercion.
*
Expand Down
8 changes: 4 additions & 4 deletions src/libexpr/include/nix/expr/print-ambiguous.hh
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ namespace nix {
* See: https://github.com/NixOS/nix/issues/9730
*/
void printAmbiguous(
Value &v,
const SymbolTable &symbols,
std::ostream &str,
std::set<const void *> *seen,
EvalState & state,
Value & v,
std::ostream & str,
std::set<const void *> * seen,
int depth);

}
79 changes: 79 additions & 0 deletions src/libexpr/paths.cc
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#include "nix/store/store-api.hh"
#include "nix/expr/eval.hh"
#include "nix/util/mounted-source-accessor.hh"
#include "nix/fetchers/fetch-to-store.hh"

namespace nix {

Expand All @@ -18,4 +20,81 @@ SourcePath EvalState::storePath(const StorePath & path)
return {rootFS, CanonPath{store->printStorePath(path)}};
}

StorePath EvalState::devirtualize(const StorePath & path, StringMap * rewrites)
{
if (auto mount = storeFS->getMount(CanonPath(store->printStorePath(path)))) {
auto storePath = fetchToStore(
*store, SourcePath{ref(mount)}, settings.readOnlyMode ? FetchMode::DryRun : FetchMode::Copy, path.name());
assert(storePath.name() == path.name());
if (rewrites)
rewrites->emplace(path.hashPart(), storePath.hashPart());
return storePath;
} else
return path;
}

SingleDerivedPath EvalState::devirtualize(const SingleDerivedPath & path, StringMap * rewrites)
{
if (auto o = std::get_if<SingleDerivedPath::Opaque>(&path.raw()))
return SingleDerivedPath::Opaque{devirtualize(o->path, rewrites)};
else
return path;
}

std::string EvalState::devirtualize(std::string_view s, const NixStringContext & context)
{
StringMap rewrites;

for (auto & c : context)
if (auto o = std::get_if<NixStringContextElem::Opaque>(&c.raw))
devirtualize(o->path, &rewrites);

return rewriteStrings(std::string(s), rewrites);
}

std::string EvalState::computeBaseName(const SourcePath & path)
{
if (path.accessor == rootFS) {
if (auto storePath = store->maybeParseStorePath(path.path.abs())) {
warn(
"Performing inefficient double copy of path '%s' to the store. "
"This can typically be avoided by rewriting an attribute like `src = ./.` "
"to `src = builtins.path { path = ./.; name = \"source\"; }`.",
path);
return std::string(fetchToStore(*store, path, FetchMode::DryRun, storePath->name()).to_string());
}
}
return std::string(path.baseName());
}

StorePath EvalState::mountInput(
fetchers::Input & input, const fetchers::Input & originalInput, ref<SourceAccessor> accessor, bool requireLockable)
{
auto storePath = settings.lazyTrees ? StorePath::random(input.getName())
: fetchToStore(*store, accessor, FetchMode::Copy, input.getName());

allowPath(storePath); // FIXME: should just whitelist the entire virtual store

storeFS->mount(CanonPath(store->printStorePath(storePath)), accessor);

if (requireLockable && !input.isLocked() && !input.getNarHash()) {
auto narHash = accessor->hashPath(CanonPath::root);
input.attrs.insert_or_assign("narHash", narHash.to_string(HashFormat::SRI, true));
}

// FIXME: what to do with the NAR hash in lazy mode?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this fixme'd?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, but we can postpone this for now.

if (!settings.lazyTrees && originalInput.getNarHash()) {
auto expected = originalInput.computeStorePath(*store);
if (storePath != expected)
throw Error(
(unsigned int) 102,
"NAR hash mismatch in input '%s', expected '%s' but got '%s'",
originalInput.to_string(),
store->printStorePath(storePath),
store->printStorePath(expected));
}

return storePath;
}

}
15 changes: 12 additions & 3 deletions src/libexpr/primops.cc
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include "nix/expr/value-to-xml.hh"
#include "nix/expr/primops.hh"
#include "nix/fetchers/fetch-to-store.hh"
#include "nix/util/mounted-source-accessor.hh"

#include <boost/container/small_vector.hpp>
#include <nlohmann/json.hpp>
Expand Down Expand Up @@ -75,7 +76,10 @@ StringMap EvalState::realiseContext(const NixStringContext & context, StorePathS
ensureValid(b.drvPath->getBaseStorePath());
},
[&](const NixStringContextElem::Opaque & o) {
ensureValid(o.path);
// We consider virtual store paths valid here. They'll
// be devirtualized if needed elsewhere.
if (!storeFS->getMount(CanonPath(store->printStorePath(o.path))))
ensureValid(o.path);
if (maybePathsOut)
maybePathsOut->emplace(o.path);
},
Expand Down Expand Up @@ -1408,6 +1412,8 @@ static void derivationStrictInternal(
/* Everything in the context of the strings in the derivation
attributes should be added as dependencies of the resulting
derivation. */
StringMap rewrites;

for (auto & c : context) {
std::visit(overloaded {
/* Since this allows the builder to gain access to every
Expand All @@ -1430,11 +1436,13 @@ static void derivationStrictInternal(
drv.inputDrvs.ensureSlot(*b.drvPath).value.insert(b.output);
},
[&](const NixStringContextElem::Opaque & o) {
drv.inputSrcs.insert(o.path);
drv.inputSrcs.insert(state.devirtualize(o.path, &rewrites));
},
}, c.raw);
}

drv.applyRewrites(rewrites);

/* Do we have all required attributes? */
if (drv.builder == "")
state.error<EvalError>("required attribute 'builder' missing")
Expand Down Expand Up @@ -2500,6 +2508,7 @@ static void addPath(
{}));

if (!expectedHash || !state.store->isValidPath(*expectedStorePath)) {
// FIXME: make this lazy?
auto dstPath = fetchToStore(
*state.store,
path.resolveSymlinks(),
Expand Down Expand Up @@ -2530,7 +2539,7 @@ static void prim_filterSource(EvalState & state, const PosIdx pos, Value * * arg
"while evaluating the second argument (the path to filter) passed to 'builtins.filterSource'");
state.forceFunction(*args[0], pos, "while evaluating the first argument passed to builtins.filterSource");

addPath(state, pos, path.baseName(), path, args[0], ContentAddressMethod::Raw::NixArchive, std::nullopt, v, context);
addPath(state, pos, state.computeBaseName(path), path, args[0], ContentAddressMethod::Raw::NixArchive, std::nullopt, v, context);
}

static RegisterPrimOp primop_filterSource({
Expand Down
Loading