Skip to content

Build-time flake inputs #49

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9071d83
Allow dynamic registration of builtin builders
edolstra May 1, 2025
7762dd2
Put the builder context in a struct
edolstra May 1, 2025
4d485f3
Add builtin:fetch-tree
edolstra May 1, 2025
464f408
Pass tmpDirInSandbox to the builtin builders
edolstra May 1, 2025
d3ff470
Move fetchSettings back to libfetchers
edolstra May 1, 2025
94facc9
Hack to disable the fetcher cache in forked processes
edolstra May 1, 2025
961b3a1
builtin:fetch-tree: Propagate access tokens, set cache directory
edolstra May 1, 2025
99f35e1
Allow flake inputs to be fetched at build time
edolstra May 1, 2025
febe4de
Formatting
edolstra May 1, 2025
c3270b9
Always add a NAR hash for build-time inputs
edolstra May 14, 2025
655b26c
Revert "Hack to disable the fetcher cache in forked processes"
edolstra May 14, 2025
38b45aa
Sync: Support moving out of another Sync
edolstra May 14, 2025
0d440c9
Remove global fetcher cache
edolstra May 14, 2025
06c44ce
builtin:fetch-tree: Hack to avoid touching the parent's FileTransfer …
edolstra May 14, 2025
c75cab6
Move getTarballCache() into fetchers::Settings
edolstra May 14, 2025
16bd9a8
Formatting
edolstra May 14, 2025
3df518b
Add test
edolstra May 14, 2025
66382fe
Merge commit '47281531e' into build-time-fetch-tree
edolstra Jul 23, 2025
43e7a7a
Merge commit '09fbe1569430ca561c461b9b4ece3428785a53d6' into build-ti…
edolstra Jul 23, 2025
24fc713
Merge remote-tracking branch 'detsys/detsys-main' into build-time-fet…
edolstra Aug 18, 2025
943aaa4
Fix test
edolstra Aug 19, 2025
0159911
Add build-time-fetch-tree experimental feature
edolstra Aug 19, 2025
7e50ba7
Provide downloadFile() with a writable store
edolstra Aug 20, 2025
6f272c5
Fix segfault destroying prevFileTransfer
edolstra Aug 20, 2025
486c48a
Add tests for build-time fetching of GitHub flakes
edolstra Aug 20, 2025
1201c72
GitRepo::fetch(): Fall back to using libgit2 for fetching
edolstra Aug 20, 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
4 changes: 0 additions & 4 deletions src/libcmd/common-eval-args.cc
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,6 @@

namespace nix {

fetchers::Settings fetchSettings;

static GlobalConfig::Register rFetchSettings(&fetchSettings);

EvalSettings evalSettings{
settings.readOnlyMode,
{
Expand Down
3 changes: 0 additions & 3 deletions src/libcmd/include/nix/cmd/common-eval-args.hh
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,6 @@ namespace flake {
struct Settings;
}

/**
* @todo Get rid of global settings variables
*/
extern fetchers::Settings fetchSettings;

/**
Expand Down
3 changes: 2 additions & 1 deletion src/libexpr/include/nix/expr/eval.hh
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,8 @@ public:
fetchers::Input & input,
const fetchers::Input & originalInput,
ref<SourceAccessor> accessor,
bool requireLockable);
bool requireLockable,
bool forceNarHash = false);

/**
* Parse a Nix expression from the specified file.
Expand Down
10 changes: 8 additions & 2 deletions src/libexpr/paths.cc
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,11 @@ std::string EvalState::computeBaseName(const SourcePath & path, PosIdx pos)
}

StorePath EvalState::mountInput(
fetchers::Input & input, const fetchers::Input & originalInput, ref<SourceAccessor> accessor, bool requireLockable)
fetchers::Input & input,
const fetchers::Input & originalInput,
ref<SourceAccessor> accessor,
bool requireLockable,
bool forceNarHash)
{
auto storePath = settings.lazyTrees
? StorePath::random(input.getName())
Expand All @@ -95,7 +99,9 @@ StorePath EvalState::mountInput(

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

if (requireLockable && (!settings.lazyTrees || !settings.lazyLocks || !input.isLocked()) && !input.getNarHash())
if (forceNarHash
|| (requireLockable && (!settings.lazyTrees || !settings.lazyLocks || !input.isLocked())
&& !input.getNarHash()))
input.attrs.insert_or_assign("narHash", getNarHash()->to_string(HashFormat::SRI, true));

if (originalInput.getNarHash() && *getNarHash() != *originalInput.getNarHash())
Expand Down
60 changes: 60 additions & 0 deletions src/libfetchers/builtin.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#include "nix/store/builtins.hh"
#include "nix/store/parsed-derivations.hh"
#include "nix/fetchers/fetchers.hh"
#include "nix/fetchers/fetch-settings.hh"
#include "nix/util/archive.hh"
#include "nix/store/filetransfer.hh"
#include "nix/store/store-open.hh"

#include <nlohmann/json.hpp>

namespace nix {

static void builtinFetchTree(const BuiltinBuilderContext & ctx)
{
experimentalFeatureSettings.require(Xp::BuildTimeFetchTree);

auto out = get(ctx.drv.outputs, "out");
if (!out)
throw Error("'builtin:fetch-tree' requires an 'out' output");

if (!(ctx.drv.type().isFixed() || ctx.drv.type().isImpure()))
throw Error("'builtin:fetch-tree' must be a fixed-output or impure derivation");

if (!ctx.parsedDrv)
throw Error("'builtin:fetch-tree' must have '__structuredAttrs = true'");

setenv("NIX_CACHE_HOME", ctx.tmpDirInSandbox.c_str(), 1);

using namespace fetchers;

fetchers::Settings myFetchSettings;
myFetchSettings.accessTokens = fetchSettings.accessTokens.get();

// Make sure we don't use the FileTransfer object of the parent
// since it's in a broken state after the fork. We also must not
// delete it, so hang on to the shared_ptr.
// FIXME: move FileTransfer into fetchers::Settings.
static auto prevFileTransfer = resetFileTransfer();

// FIXME: disable use of the git/tarball cache

auto input = Input::fromAttrs(myFetchSettings, jsonToAttrs(ctx.parsedDrv->structuredAttrs["input"]));

std::cerr << fmt("fetching '%s'...\n", input.to_string());

/* Functions like downloadFile() expect a store. We can't use the
real one since we're in a forked process. FIXME: use recursive
Nix's daemon so we can use the real store? */
auto tmpStore = openStore(ctx.tmpDirInSandbox + "/nix");

auto [accessor, lockedInput] = input.getAccessor(tmpStore);

auto source = sinkToSource([&](Sink & sink) { accessor->dumpPath(CanonPath::root, sink); });

restorePath(ctx.outputs.at("out"), *source);
}

static RegisterBuiltinBuilder registerUnpackChannel("fetch-tree", builtinFetchTree);

} // namespace nix
9 changes: 9 additions & 0 deletions src/libfetchers/fetch-settings.cc
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
#include "nix/fetchers/fetch-settings.hh"
#include "nix/util/config-global.hh"

namespace nix::fetchers {

Settings::Settings() {}

} // namespace nix::fetchers

namespace nix {

fetchers::Settings fetchSettings;

static GlobalConfig::Register rFetchSettings(&fetchSettings);

} // namespace nix
67 changes: 48 additions & 19 deletions src/libfetchers/git-utils.cc
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include "nix/util/sync.hh"
#include "nix/util/thread-pool.hh"
#include "nix/util/pool.hh"
#include "nix/util/executable-path.hh"

#include <git2/attr.h>
#include <git2/blob.h>
Expand Down Expand Up @@ -549,21 +550,44 @@ struct GitRepoImpl : GitRepo, std::enable_shared_from_this<GitRepoImpl>
// that)
// then use code that was removed in this commit (see blame)

auto dir = this->path;
Strings gitArgs{"-C", dir.string(), "--git-dir", ".", "fetch", "--quiet", "--force"};
if (shallow)
append(gitArgs, {"--depth", "1"});
append(gitArgs, {std::string("--"), url, refspec});

runProgram(
RunOptions{
.program = "git",
.lookupPath = true,
// FIXME: git stderr messes up our progress indicator, so
// we're using --quiet for now. Should process its stderr.
.args = gitArgs,
.input = {},
.isInteractive = true});
if (ExecutablePath::load().findName("git")) {
auto dir = this->path;
Strings gitArgs{"-C", dir.string(), "--git-dir", ".", "fetch", "--quiet", "--force"};
if (shallow)
append(gitArgs, {"--depth", "1"});
append(gitArgs, {std::string("--"), url, refspec});

runProgram(
RunOptions{
.program = "git",
.lookupPath = true,
// FIXME: git stderr messes up our progress indicator, so
// we're using --quiet for now. Should process its stderr.
.args = gitArgs,
.input = {},
.isInteractive = true});
} else {
// Fall back to using libgit2 for fetching. This does not
// support SSH very well.
Remote remote;

if (git_remote_create_anonymous(Setter(remote), *this, url.c_str()))
throw Error("cannot create Git remote '%s': %s", url, git_error_last()->message);

char * refspecs[] = {(char *) refspec.c_str()};
git_strarray refspecs2{.strings = refspecs, .count = 1};

git_fetch_options opts = GIT_FETCH_OPTIONS_INIT;
// FIXME: for some reason, shallow fetching over ssh barfs
// with "could not read from remote repository".
opts.depth = shallow && parseURL(url).scheme != "ssh" ? 1 : GIT_FETCH_DEPTH_FULL;
opts.callbacks.payload = &act;
opts.callbacks.sideband_progress = sidebandProgressCallback;
opts.callbacks.transfer_progress = transferProgressCallback;

if (git_remote_fetch(remote.get(), &refspecs2, &opts, nullptr))
throw Error("fetching '%s' from '%s': %s", refspec, url, git_error_last()->message);
}
}

void verifyCommit(const Hash & rev, const std::vector<fetchers::PublicKey> & publicKeys) override
Expand Down Expand Up @@ -1312,13 +1336,18 @@ std::vector<std::tuple<GitRepoImpl::Submodule, Hash>> GitRepoImpl::getSubmodules
return result;
}

ref<GitRepo> getTarballCache()
{
static auto repoDir = std::filesystem::path(getCacheDir()) / "tarball-cache";
namespace fetchers {

return GitRepo::openRepo(repoDir, true, true);
ref<GitRepo> Settings::getTarballCache() const
{
auto tarballCache(_tarballCache.lock());
if (!*tarballCache)
*tarballCache = GitRepo::openRepo(std::filesystem::path(getCacheDir()) / "tarball-cache", true, true);
return ref<GitRepo>(*tarballCache);
}

} // namespace fetchers

GitRepo::WorkdirInfo GitRepo::getCachedWorkdirInfo(const std::filesystem::path & path)
{
static Sync<std::map<std::filesystem::path, WorkdirInfo>> _cache;
Expand Down
7 changes: 4 additions & 3 deletions src/libfetchers/github.cc
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ struct GitArchiveInputScheme : InputScheme
if (auto lastModifiedAttrs = cache->lookup(lastModifiedKey)) {
auto treeHash = getRevAttr(*treeHashAttrs, "treeHash");
auto lastModified = getIntAttr(*lastModifiedAttrs, "lastModified");
if (getTarballCache()->hasObject(treeHash))
if (input.settings->getTarballCache()->hasObject(treeHash))
return {std::move(input), TarballInfo{.treeHash = treeHash, .lastModified = (time_t) lastModified}};
else
debug("Git tree with hash '%s' has disappeared from the cache, refetching...", treeHash.gitRev());
Expand All @@ -289,7 +289,7 @@ struct GitArchiveInputScheme : InputScheme
*logger, lvlInfo, actUnknown, fmt("unpacking '%s' into the Git cache", input.to_string()));

TarArchive archive{*source};
auto tarballCache = getTarballCache();
auto tarballCache = input.settings->getTarballCache();
auto parseSink = tarballCache->getFileSystemObjectSink();
auto lastModified = unpackTarfileToSink(archive, *parseSink);
auto tree = parseSink->flush();
Expand Down Expand Up @@ -323,7 +323,8 @@ struct GitArchiveInputScheme : InputScheme
#endif
input.attrs.insert_or_assign("lastModified", uint64_t(tarballInfo.lastModified));

auto accessor = getTarballCache()->getAccessor(tarballInfo.treeHash, false, "«" + input.to_string() + "»");
auto accessor =
input.settings->getTarballCache()->getAccessor(tarballInfo.treeHash, false, "«" + input.to_string() + "»");

if (!input.settings->trustTarballsFromGitForges)
// FIXME: computing the NAR hash here is wasteful if
Expand Down
19 changes: 19 additions & 0 deletions src/libfetchers/include/nix/fetchers/fetch-settings.hh
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@

#include <sys/types.h>

namespace nix {

struct GitRepo;

}

namespace nix::fetchers {

struct Cache;
Expand Down Expand Up @@ -119,8 +125,21 @@ struct Settings : public Config

ref<Cache> getCache() const;

ref<GitRepo> getTarballCache() const;

private:
mutable Sync<std::shared_ptr<Cache>> _cache;

mutable Sync<std::shared_ptr<GitRepo>> _tarballCache;
};

} // namespace nix::fetchers

namespace nix {

/**
* @todo Get rid of global setttings variables
*/
extern fetchers::Settings fetchSettings;

} // namespace nix
2 changes: 0 additions & 2 deletions src/libfetchers/include/nix/fetchers/git-utils.hh
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,6 @@ struct GitRepo
virtual Hash dereferenceSingletonDirectory(const Hash & oid) = 0;
};

ref<GitRepo> getTarballCache();

// A helper to ensure that the `git_*_free` functions get called.
template<auto del>
struct Deleter
Expand Down
1 change: 1 addition & 0 deletions src/libfetchers/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ subdir('nix-meson-build-support/common')

sources = files(
'attrs.cc',
'builtin.cc',
'cache.cc',
'fetch-settings.cc',
'fetch-to-store.cc',
Expand Down
10 changes: 6 additions & 4 deletions src/libfetchers/tarball.cc
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,11 @@ static DownloadTarballResult downloadTarball_(
.treeHash = treeHash,
.lastModified = (time_t) getIntAttr(infoAttrs, "lastModified"),
.immutableUrl = maybeGetStrAttr(infoAttrs, "immutableUrl"),
.accessor = getTarballCache()->getAccessor(treeHash, false, displayPrefix),
.accessor = settings.getTarballCache()->getAccessor(treeHash, false, displayPrefix),
};
};

if (cached && !getTarballCache()->hasObject(getRevAttr(cached->value, "treeHash")))
if (cached && !settings.getTarballCache()->hasObject(getRevAttr(cached->value, "treeHash")))
cached.reset();

if (cached && !cached->expired)
Expand Down Expand Up @@ -162,7 +162,7 @@ static DownloadTarballResult downloadTarball_(
TarArchive{path};
})
: TarArchive{*source};
auto tarballCache = getTarballCache();
auto tarballCache = settings.getTarballCache();
auto parseSink = tarballCache->getFileSystemObjectSink();
auto lastModified = unpackTarfileToSink(archive, *parseSink);
auto tree = parseSink->flush();
Expand Down Expand Up @@ -378,7 +378,9 @@ struct TarballInputScheme : CurlInputScheme

input.attrs.insert_or_assign(
"narHash",
getTarballCache()->treeHashToNarHash(*input.settings, result.treeHash).to_string(HashFormat::SRI, true));
input.settings->getTarballCache()
->treeHashToNarHash(*input.settings, result.treeHash)
.to_string(HashFormat::SRI, true));

return {result.accessor, input};
}
Expand Down
13 changes: 12 additions & 1 deletion src/libflake/call-flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,17 @@ let
parentNode = allNodes.${getInputByPath lockFile.root node.parent};

sourceInfo =
if hasOverride then
if node.buildTime or false then
derivation {
name = "source";
builder = "builtin:fetch-tree";
system = "builtin";
__structuredAttrs = true;
input = node.locked;
outputHashMode = "recursive";
outputHash = node.locked.narHash;
}
Comment on lines +49 to +57

Choose a reason for hiding this comment

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

My memory is that, because of the way derivations are added to the store by way of registerOutputs, they are scanned for references to the Nix store. Since the eval-time fetcher doesn't do that (and so the retrieved sources can contain paths to the Nix store), the build-time fetcher will reject some of the sources which the eval-time fetcher would accept (since fixed-output derivations are not allowed to contain references to the Nix store).

Is that correct or something to worry about?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Nix only scans for references that are part of the input closure of a derivation. It doesn't scan for arbitrary references. For the build-time fetcher, the input closure is empty, so no sources will ever be rejected by the build-time fetcher. This also means Nix won't find references that are "hard-coded" (e.g. part of the tarball), but that's the same for other types of derivations (e.g. fetchurl in Nixpkgs).

else if hasOverride then
overrides.${key}.sourceInfo
else if isRelative then
parentNode.sourceInfo
Expand Down Expand Up @@ -93,6 +103,7 @@ let
result =
if node.flake or true then
assert builtins.isFunction flake.outputs;
assert !(node.buildTime or false);
result
else
sourceInfo // { inherit sourceInfo outPath; };
Expand Down
Loading
Loading