Skip to content
Open
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
103 changes: 103 additions & 0 deletions src/libexpr/eval.cc
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ EvalState::EvalState(
, debugStop(false)
, trylevel(0)
, srcToStore(make_ref<decltype(srcToStore)::element_type>())
, storeToSrc(make_ref<decltype(storeToSrc)::element_type>())
, importResolutionCache(make_ref<decltype(importResolutionCache)::element_type>())
, fileEvalCache(make_ref<decltype(fileEvalCache)::element_type>())
, positionToDocComment(make_ref<decltype(positionToDocComment)::element_type>())
Expand Down Expand Up @@ -2577,6 +2578,7 @@ StorePath EvalState::copyPathToStore(NixStringContext & context, const SourcePat
repair);
allowPath(dstPath);
srcToStore->try_emplace(path, dstPath);
storeToSrc->try_emplace(dstPath, path);
printMsg(lvlChatty, "copied source '%1%' -> '%2%'", path, store->printStorePath(dstPath));
return dstPath;
}();
Expand All @@ -2585,6 +2587,107 @@ StorePath EvalState::copyPathToStore(NixStringContext & context, const SourcePat
return dstPath;
}

std::optional<SourcePath> EvalState::getSourceOrigin(const StorePath & storePath) const
{
auto result = getConcurrent(*storeToSrc, storePath);
if (result)
return *result;
return std::nullopt;
}

std::map<StorePath, SourcePath> EvalState::getSourceOrigins() const
{
std::map<StorePath, SourcePath> result;
storeToSrc->cvisit_all([&](const auto & entry) {
result.emplace(entry.first, entry.second);
});
return result;
}

void EvalState::recordPathOrigin(const StorePath & storePath, const SourcePath & srcPath)
{
storeToSrc->try_emplace(storePath, srcPath);

// Try to directly resolve the original filesystem path so that
// source-origins can map this store path back without needing to
// trace through accessor chains.

auto pathStr = srcPath.path.abs();
auto storeDirStr = store->storeDir;

// Strategy 1: the SourcePath's canonical path starts with /nix/store/.
// Parse the store path prefix and look it up in sourceStoreToOriginalPath.
if (hasPrefix(pathStr, storeDirStr + "/")) {
auto afterStore = pathStr.find('/', storeDirStr.size() + 1);
std::string storePathStr = (afterStore == std::string::npos)
? pathStr : pathStr.substr(0, afterStore);
try {
auto sp = store->parseStorePath(storePathStr);
auto it = sourceStoreToOriginalPath.find(sp);
if (it != sourceStoreToOriginalPath.end()) {
auto relPath = (afterStore == std::string::npos)
? "" : pathStr.substr(afterStore + 1);
auto origPath = relPath.empty()
? it->second : (it->second / relPath);
sourceStoreToOriginalPath.try_emplace(storePath, origPath);
return;
}
} catch (...) {}
}

// Strategy 2: the accessor has originalRootPath set directly
// (per-input accessor from mountInput).
if (srcPath.accessor->originalRootPath) {
auto relPath = srcPath.path.rel();
auto origRoot = *srcPath.accessor->originalRootPath;
auto origPath = (relPath.empty() || relPath == ".")
? origRoot : (origRoot / relPath);
sourceStoreToOriginalPath.try_emplace(storePath, origPath);
return;
}

// Strategy 3: the SourcePath is {rootFS, /} which means it accesses
// the root of a source tree mounted in storeFS. This happens with
// cleanSourceWith / builtins.path when the path expression resolves
// to the root of a per-input accessor. Use the storeFS to find which
// mount covers this path by checking each known source store path.
if (srcPath.path.isRoot()) {
// If there's exactly one source store path with an original path
// mapping, use it (common case: single flake with one source tree).
// If there are multiple, we can't disambiguate — skip.
if (sourceStoreToOriginalPath.size() == 1) {
auto & [srcStorePath, origPath] = *sourceStoreToOriginalPath.begin();
sourceStoreToOriginalPath.try_emplace(storePath, origPath);
return;
Comment on lines +2658 to +2661
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Strategy 3 heuristic silently maps unrelated store paths to wrong original path

In recordPathOrigin at src/libexpr/eval.cc:2658-2661, when srcPath.path.isRoot() and sourceStoreToOriginalPath has exactly one entry, the code unconditionally maps the new storePath to that entry's original path without verifying any relationship between them. This means if there's one flake source mounted and any other builtins.path call with a root source path, the resulting store path gets silently mapped to the flake's source directory. The getOriginalPath() query at src/libexpr/eval.cc:2683-2688 then returns a wrong filesystem path, which propagates to the sourcePath field in the command output.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

}

// Multiple source store paths — try to find the one that has a
// mount in storeFS by checking if the mount accessor matches
// srcPath's content fingerprint.
for (auto & [srcStorePath, origPath] : sourceStoreToOriginalPath) {
auto mountPath = CanonPath(store->printStorePath(srcStorePath));
auto mount = storeFS->getMount(mountPath);
if (mount) {
// Check if the srcPath's accessor delegates to this mount.
// We verify by checking if the mount's accessor has the
// same originalRootPath as the origPath we expect.
if (mount->originalRootPath && *mount->originalRootPath == origPath) {
sourceStoreToOriginalPath.try_emplace(storePath, origPath);
return;
}
}
}
}
}

std::optional<std::filesystem::path> EvalState::getOriginalPath(const StorePath & storePath) const
{
auto it = sourceStoreToOriginalPath.find(storePath);
if (it != sourceStoreToOriginalPath.end())
return it->second;
return std::nullopt;
}

SourcePath EvalState::coerceToPath(const PosIdx pos, Value & v, NixStringContext & context, std::string_view errorCtx)
{
try {
Expand Down
35 changes: 35 additions & 0 deletions src/libexpr/include/nix/expr/eval.hh
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,15 @@ private:
paths. */
const ref<boost::concurrent_flat_map<SourcePath, StorePath>> srcToStore;

/* Reverse mapping: store paths back to their original source paths.
Populated alongside srcToStore so we can recover provenance. */
const ref<boost::concurrent_flat_map<StorePath, SourcePath>> storeToSrc;

/* Map from flake source store paths (e.g. /nix/store/xxx-source) to
their original filesystem paths (e.g. /home/user/project).
Populated by mountInput() for path: inputs. */
std::map<StorePath, std::filesystem::path> sourceStoreToOriginalPath;

/**
* A cache that maps paths to "resolved" paths for importing Nix
* expressions, i.e. `/foo` to `/foo/default.nix`.
Expand Down Expand Up @@ -571,6 +580,32 @@ public:
*/
void allowAndSetStorePathString(const StorePath & storePath, Value & v);

/**
* Look up the original source path for a store path that was
* copied into the store during this evaluation. Returns
* std::nullopt when the store path wasn't produced by this
* evaluator (e.g. it came from a substituter).
*/
std::optional<SourcePath> getSourceOrigin(const StorePath & storePath) const;

/**
* Return the full store→source mapping built during this evaluation.
*/
std::map<StorePath, SourcePath> getSourceOrigins() const;

/**
* Look up the original filesystem path for a flake source store path.
* Returns std::nullopt if the store path wasn't a path: input.
*/
std::optional<std::filesystem::path> getOriginalPath(const StorePath & storePath) const;

/**
* Record a store path → source path mapping. Used by addPath
* (builtins.path / builtins.filterSource) so that filtered sources
* also appear in the storeToSrc provenance map.
*/
void recordPathOrigin(const StorePath & storePath, const SourcePath & srcPath);

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

/**
Expand Down
13 changes: 13 additions & 0 deletions src/libexpr/paths.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include "nix/expr/eval.hh"
#include "nix/util/mounted-source-accessor.hh"
#include "nix/fetchers/fetch-to-store.hh"
#include "nix/fetchers/fetchers.hh"

namespace nix {

Expand Down Expand Up @@ -33,6 +34,18 @@ EvalState::mountInput(fetchers::Input & input, const fetchers::Input & originalI

input.attrs.insert_or_assign("narHash", narHash.to_string(HashFormat::SRI, true));

// Record the mapping from this source store path to the original
// filesystem path so that source-origins can resolve provenance.
// Prefer the accessor's originalRootPath (set by git/path input schemes
// to the source tree root) over input.getSourcePath() (which returns
// the flake directory, not the git root for git-tracked flakes).
if (accessor->originalRootPath) {
sourceStoreToOriginalPath.try_emplace(storePath, *accessor->originalRootPath);
} else if (auto origPath = input.getSourcePath()) {
sourceStoreToOriginalPath.try_emplace(storePath, *origPath);
accessor->originalRootPath = *origPath;
}

if (originalInput.getNarHash() && narHash != *originalInput.getNarHash())
throw Error(
(unsigned int) 102,
Expand Down
8 changes: 7 additions & 1 deletion src/libexpr/primops.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2860,9 +2860,15 @@ static void addPath(
state.error<EvalError>("store path mismatch in (possibly filtered) path added from '%s'", path)
.atPos(pos)
.debugThrow();
// Record the store→source mapping so that source-origins
// can trace filtered paths (cleanSourceWith / builtins.path)
// back to their original source location.
state.recordPathOrigin(dstPath, path);
state.allowAndSetStorePathString(dstPath, v);
} else
} else {
state.recordPathOrigin(*expectedStorePath, path);
state.allowAndSetStorePathString(*expectedStorePath, v);
}
} catch (Error & e) {
e.addTrace(state.positions[pos], "while adding path '%s'", path);
throw;
Expand Down
4 changes: 4 additions & 0 deletions src/libfetchers/git.cc
Original file line number Diff line number Diff line change
Expand Up @@ -989,6 +989,10 @@ struct GitInputScheme : InputScheme
ref<SourceAccessor> accessor =
repo->getAccessor(repoInfo.workdirInfo, {.exportIgnore = exportIgnore}, makeNotAllowedError(repoPath));

// Record the git repo root as the original filesystem path so that
// source-origins can map store paths back to their original locations.
accessor->originalRootPath = repoPath;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 originalRootPath lost when git repo has submodules

originalRootPath is set on the inner git workdir accessor at src/libfetchers/git.cc:994, but when the repo has submodules, accessor is reassigned to a new MountedSourceAccessor at line 1025. The mounted accessor does not inherit originalRootPath from the inner accessor. Downstream in mountInput() (src/libexpr/paths.cc:42), the check accessor->originalRootPath will be false, falling through to the input.getSourcePath() fallback which may return a different path (the URL path rather than the resolved repo root) or nullopt for non-file:// URLs.

Prompt for agents
In src/libfetchers/git.cc, in getAccessorFromWorkdir(), the originalRootPath is set on the inner accessor at line 994, but after the submodule block (lines 999-1026) when accessor is replaced with a mounted accessor, the originalRootPath is lost. After line 1025 (accessor = makeMountedSourceAccessor(...)), add: accessor->originalRootPath = repoPath; to propagate the original root path to the new mounted accessor.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


/* If the repo has submodules, return a mounted input accessor
consisting of the accessor for the top-level repo and the
accessors for the submodule workdirs. */
Expand Down
4 changes: 4 additions & 0 deletions src/libfetchers/path.cc
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@ struct PathInputScheme : InputScheme

auto accessor = store.requireStoreObjectAccessor(*storePath);

// Record the original filesystem root so that source-origins
// can map store paths back to their original locations.
accessor->originalRootPath = absPath;

// To prevent `fetchToStore()` copying the path again to Nix
// store, pre-create an entry in the fetcher cache.
auto narHash = store.queryPathInfo(*storePath)->narHash.to_string(HashFormat::SRI, true);
Expand Down
8 changes: 8 additions & 0 deletions src/libutil/include/nix/util/source-accessor.hh
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,14 @@ struct SourceAccessor : std::enable_shared_from_this<SourceAccessor>
*/
CanonPath resolveSymlinks(const CanonPath & path, SymlinkResolution mode = SymlinkResolution::Full);

/**
* For accessors backed by store paths that were copied from a
* local filesystem path (e.g. `path:` flake inputs), this records
* the original filesystem root so that we can map store paths back
* to their original locations.
*/
std::optional<std::filesystem::path> originalRootPath;

/**
* A string that uniquely represents the contents of this
* accessor. This is used for caching lookups (see `fetchToStore()`).
Expand Down
Loading