Skip to content
Draft
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
2 changes: 2 additions & 0 deletions src/libexpr-c/nix_api_value.cc
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,8 @@ ValueType nix_get_type(nix_c_context * context, const nix_value * value)
return NIX_TYPE_FUNCTION;
case nExternal:
return NIX_TYPE_EXTERNAL;
case nWorldPath:
return NIX_TYPE_WORLDPATH;
}
return NIX_TYPE_NULL;
}
Expand Down
5 changes: 4 additions & 1 deletion src/libexpr-c/nix_api_value.h
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,10 @@ typedef enum {
/** @brief External value from C++ plugins or C API
* @see Externals
*/
NIX_TYPE_EXTERNAL
NIX_TYPE_EXTERNAL,
/** @brief World path (//path/in/world)
*/
NIX_TYPE_WORLDPATH
} ValueType;

// forward declarations
Expand Down
1 change: 1 addition & 0 deletions src/libexpr/eval-cache.cc
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,7 @@ string_t AttrCursor::getStringWithContext()
return b.drvPath->getBaseStorePath();
},
[&](const NixStringContextElem::Opaque & o) -> const StorePath & { return o.path; },
[&](const NixStringContextElem::WorldZone & w) -> const StorePath & { return w.path; },
},
c.raw);
if (!root->state.store->isValidPath(path)) {
Expand Down
195 changes: 195 additions & 0 deletions src/libexpr/eval.cc
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include "nix/fetchers/fetch-to-store.hh"
#include "nix/fetchers/tarball.hh"
#include "nix/fetchers/input-cache.hh"
#include "nix/fetchers/git-utils.hh"
#include "nix/util/current-process.hh"

#include "parser-tab.hh"
Expand Down Expand Up @@ -153,6 +154,8 @@ std::string_view showType(ValueType type, bool withArticle)
return WA("an", "external value");
case nFloat:
return WA("a", "float");
case nWorldPath:
return WA("a", "world path");
case nThunk:
return WA("a", "thunk");
}
Expand Down Expand Up @@ -304,6 +307,8 @@ EvalState::EvalState(
, importResolutionCache(make_ref<decltype(importResolutionCache)::element_type>())
, fileEvalCache(make_ref<decltype(fileEvalCache)::element_type>())
, regexCache(makeRegexCache())
, worldTreeShaCache(make_ref<decltype(worldTreeShaCache)::element_type>())
, worldZoneDirtyCache(make_ref<decltype(worldZoneDirtyCache)::element_type>())
#if NIX_USE_BOEHMGC
, baseEnvP(std::allocate_shared<Env *>(traceable_allocator<Env *>(), &mem.allocEnv(BASE_ENV_SIZE)))
, baseEnv(**baseEnvP)
Expand Down Expand Up @@ -395,6 +400,146 @@ void EvalState::allowAndSetStorePathString(const StorePath & storePath, Value &
mkStorePathString(storePath, v);
}

ref<GitRepo> EvalState::getWorldRepo() const
{
if (!worldRepo) {
auto gitDir = settings.worldGitDir.get();
if (gitDir.empty())
throw Error("--world-git-dir must be specified to use world builtins");

// Expand ~ to home directory
if (hasPrefix(gitDir, "~/"))
gitDir = getHome() + gitDir.substr(1);

worldRepo = GitRepo::openRepo(std::filesystem::path(gitDir), false, true);
}
return *worldRepo;
}

ref<SourceAccessor> EvalState::getWorldGitAccessor() const
{
if (!worldGitAccessor) {
auto sha = settings.worldSha.get();
if (sha.empty())
throw Error("--world-sha must be specified to use world builtins");

auto repo = getWorldRepo();
auto hash = Hash::parseNonSRIUnprefixed(sha, HashAlgorithm::SHA1);

if (!repo->hasObject(hash))
throw Error("world-sha '%s' not found in repository", sha);

GitAccessorOptions opts{.exportIgnore = false, .smudgeLfs = false};
worldGitAccessor = repo->getAccessor(hash, opts, "world");
}
return *worldGitAccessor;
}

std::optional<ref<SourceAccessor>> EvalState::getWorldCheckoutAccessor() const
{
if (!isWorldSourceAvailable())
return std::nullopt;

if (!worldCheckoutAccessor) {
auto checkoutPath = settings.worldCheckoutPath.get();
// Use the global filesystem accessor with the checkout path as root
worldCheckoutAccessor = getFSSourceAccessor();
}
return *worldCheckoutAccessor;
}

bool EvalState::isWorldSourceAvailable() const
{
return !settings.worldCheckoutPath.get().empty();
}

Hash EvalState::getWorldTreeSha(std::string_view worldPath) const
{
// Normalize path (remove leading //)
std::string path(worldPath);
if (hasPrefix(path, "//"))
path = path.substr(2);

// Check cache first
if (auto cached = getConcurrent(*worldTreeShaCache, path))
return *cached;

// Compute by walking from root
auto repo = getWorldRepo();
auto sha = settings.worldSha.get();
auto commitSha = Hash::parseNonSRIUnprefixed(sha, HashAlgorithm::SHA1);

// Get the root tree SHA from the commit
auto rootTreeSha = repo->getCommitTree(commitSha);

// Walk path components, caching intermediate results
Hash currentSha = rootTreeSha;
std::string currentPath;

// Create an accessor for path validation
GitAccessorOptions opts{.exportIgnore = false, .smudgeLfs = false};
auto accessor = repo->getAccessor(commitSha, opts, "world-tree");

for (auto & component : tokenizeString<std::vector<std::string>>(path, "/")) {
if (component.empty()) continue;

std::string nextPath = currentPath.empty() ? component : currentPath + "/" + component;

// Check if this level is cached
if (auto cached = getConcurrent(*worldTreeShaCache, nextPath)) {
currentSha = *cached;
currentPath = nextPath;
continue;
}

// Need to compute: get tree entry for this component
auto fullPath = CanonPath("/" + nextPath);
auto stat = accessor->maybeLstat(fullPath);

if (!stat || stat->type != SourceAccessor::Type::tDirectory)
throw Error("path '%s' does not exist or is not a directory in world", nextPath);

// Get the tree SHA for this subtree
currentSha = repo->getSubtreeSha(currentSha, component);

// Cache this level
worldTreeShaCache->try_emplace(nextPath, currentSha);
currentPath = nextPath;
}

return currentSha;
}

bool EvalState::isZoneDirty(std::string_view zonePath) const
{
if (!isWorldSourceAvailable())
return false;

std::string path(zonePath);
if (hasPrefix(path, "//"))
path = path.substr(2);

// Check cache
if (auto cached = getConcurrent(*worldZoneDirtyCache, path))
return *cached;

// Get workdir info (already cached by GitRepo::getCachedWorkdirInfo)
auto checkoutPath = settings.worldCheckoutPath.get();
auto workdirInfo = GitRepo::getCachedWorkdirInfo(checkoutPath);

// Check if any dirty files are under this zone path
bool dirty = false;
for (const auto & dirtyFile : workdirInfo.dirtyFiles) {
if (hasPrefix(dirtyFile.abs(), "/" + path + "/") || dirtyFile.abs() == "/" + path) {
dirty = true;
break;
}
}

worldZoneDirtyCache->try_emplace(std::string(path), dirty);
return dirty;
}

inline static bool isJustSchemePrefix(std::string_view prefix)
{
return !prefix.empty() && prefix[prefix.size() - 1] == ':'
Expand Down Expand Up @@ -2422,6 +2567,48 @@ BackedStringView EvalState::coerceToString(
}
}

if (v.type() == nWorldPath) {
if (copyToStore) {
// Create a SourcePath from the world accessor and path
auto sourcePath = SourcePath(
ref(v.worldPathAccessor()->shared_from_this()),
CanonPath(v.worldPathStrView()));

// Copy to store (similar to copyPathToStore but with WorldZone context)
if (nix::isDerivation(sourcePath.path.abs()))
error<EvalError>("file names are not allowed to end in '%1%'", drvExtension).debugThrow();

auto dstPathCached = getConcurrent(*srcToStore, sourcePath);
auto dstPath = dstPathCached ? *dstPathCached : [&]() {
auto dstPath = fetchToStore(
fetchSettings,
*store,
sourcePath.resolveSymlinks(SymlinkResolution::Ancestors),
settings.readOnlyMode ? FetchMode::DryRun : FetchMode::Copy,
sourcePath.baseName(),
ContentAddressMethod::Raw::NixArchive,
nullptr,
repair);
allowPath(dstPath);
srcToStore->try_emplace(sourcePath, dstPath);
printMsg(lvlChatty, "copied world source '%1%' -> '%2%'", sourcePath, store->printStorePath(dstPath));
return dstPath;
}();

// Add WorldZone context to track zone origin
// Use the world path string (e.g., "//areas/tools/dev/zone.nix") as zonePath
// TODO: resolve to actual zone prefix from manifest
context.insert(NixStringContextElem::WorldZone{
.path = dstPath,
.zonePath = std::string(v.worldPathStrView()),
});

return store->printStorePath(dstPath);
} else {
return std::string(v.worldPathStrView());
}
}

if (v.type() == nAttrs) {
auto maybeString = tryAttrsToString(pos, v, context, coerceMore, copyToStore);
if (maybeString)
Expand Down Expand Up @@ -2530,6 +2717,10 @@ SourcePath EvalState::coerceToPath(const PosIdx pos, Value & v, NixStringContext
if (v.type() == nPath)
return v.path();

/* Handle world path values directly. */
if (v.type() == nWorldPath)
return SourcePath(ref(v.worldPathAccessor()->shared_from_this()), CanonPath(v.worldPathStrView()));

/* Similarly, handle __toString where the result may be a path
value. */
if (v.type() == nAttrs) {
Expand Down Expand Up @@ -2579,6 +2770,10 @@ std::pair<SingleDerivedPath, std::string_view> EvalState::coerceToSingleDerivedP
.debugThrow();
},
[&](NixStringContextElem::Built && b) -> SingleDerivedPath { return std::move(b); },
[&](NixStringContextElem::WorldZone && w) -> SingleDerivedPath {
// Treat world zone context as an opaque store path
return SingleDerivedPath::Opaque{.path = std::move(w.path)};
},
},
((NixStringContextElem &&) *context.begin()).raw);
return {
Expand Down
35 changes: 35 additions & 0 deletions src/libexpr/include/nix/expr/eval-settings.hh
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,41 @@ struct EvalSettings : Config
The default value is chosen to balance performance and memory usage. On 32 bit systems
where memory is scarce, the default is a large value to reduce the amount of allocations.
)"};

Setting<std::string> worldGitDir{
this,
"",
"world-git-dir",
R"(
Path to the git directory for world builtins (e.g., `~/world/git`).

This enables the world builtins (`builtins.worldTreeSha`, `builtins.worldTree`,
`builtins.worldFile`, `builtins.worldZoneSrc`, `builtins.worldDir`) which provide
native access to files from a git repository during Nix evaluation.
)"};

Setting<std::string> worldSha{
this,
"",
"world-sha",
R"(
Git commit SHA to use for world builtins.

This specifies the commit to read from when using world builtins.
Typically set to HEAD of the world repository.
)"};

Setting<std::string> worldCheckoutPath{
this,
"",
"world-checkout-path",
R"(
Path to checkout directory for source-available mode.

When set, uncommitted files in the checkout are preferred over git content
for world builtins. This enables local development workflows where changes
are visible before committing.
)"};
};

/**
Expand Down
34 changes: 34 additions & 0 deletions src/libexpr/include/nix/expr/eval.hh
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ struct SingleDerivedPath;
enum RepairFlag : bool;
struct MemorySourceAccessor;
struct MountedSourceAccessor;
struct GitRepo;

namespace eval_cache {
class EvalCache;
Expand Down Expand Up @@ -508,6 +509,21 @@ private:
*/
const ref<RegexCache> regexCache;

/** Lazy-initialized git repository for world builtins */
mutable std::optional<ref<GitRepo>> worldRepo;

/** Lazy-initialized source accessor for world git content */
mutable std::optional<ref<SourceAccessor>> worldGitAccessor;

/** Lazy-initialized source accessor for world checkout (source-available mode) */
mutable std::optional<ref<SourceAccessor>> worldCheckoutAccessor;

/** Cache: world path → tree SHA (lazy computed, cached at each path level) */
const ref<boost::concurrent_flat_map<std::string, Hash>> worldTreeShaCache;

/** Cache: zone path → whether zone has uncommitted changes */
const ref<boost::concurrent_flat_map<std::string, bool>> worldZoneDirtyCache;

public:

/**
Expand Down Expand Up @@ -539,6 +555,24 @@ public:
return lookupPath;
}

/** Get the world git repository, initializing lazily */
ref<GitRepo> getWorldRepo() const;

/** Get accessor for world git content at worldSha */
ref<SourceAccessor> getWorldGitAccessor() const;

/** Get accessor for world checkout (only in source-available mode) */
std::optional<ref<SourceAccessor>> getWorldCheckoutAccessor() const;

/** Get tree SHA for a world path, with lazy caching */
Hash getWorldTreeSha(std::string_view worldPath) const;

/** Check if a zone has uncommitted changes (source-available mode only) */
bool isZoneDirty(std::string_view zonePath) const;

/** Check if we're in source-available mode */
bool isWorldSourceAvailable() const;

/**
* Return a `SourcePath` that refers to `path` in the root
* filesystem.
Expand Down
Loading