nix shell: Handle output paths that are symlinks

This requires moving resolveSymlinks() into SourceAccessor. Also, it
requires LocalStoreAccessor::maybeLstat() to work on parents of the
store (to avoid an error like "/nix is not in the store").

Fixes #10375.
This commit is contained in:
Eelco Dolstra 2024-04-10 23:46:19 +02:00
parent 021488497d
commit 85b9f4ef4f
8 changed files with 97 additions and 72 deletions

View file

@ -33,6 +33,10 @@ struct LocalStoreAccessor : PosixSourceAccessor
std::optional<Stat> maybeLstat(const CanonPath & path) override std::optional<Stat> maybeLstat(const CanonPath & path) override
{ {
/* Handle the case where `path` is (a parent of) the store. */
if (isDirOrInDir(store->storeDir, path.abs()))
return Stat{ .type = tDirectory };
return PosixSourceAccessor::maybeLstat(toRealPath(path)); return PosixSourceAccessor::maybeLstat(toRealPath(path));
} }

View file

@ -39,9 +39,9 @@ void SourceAccessor::readFile(
} }
Hash SourceAccessor::hashPath( Hash SourceAccessor::hashPath(
const CanonPath & path, const CanonPath & path,
PathFilter & filter, PathFilter & filter,
HashAlgorithm ha) HashAlgorithm ha)
{ {
HashSink sink(ha); HashSink sink(ha);
dumpPath(path, sink, filter); dumpPath(path, sink, filter);
@ -67,4 +67,42 @@ std::string SourceAccessor::showPath(const CanonPath & path)
return displayPrefix + path.abs() + displaySuffix; return displayPrefix + path.abs() + displaySuffix;
} }
CanonPath SourceAccessor::resolveSymlinks(
const CanonPath & path,
SymlinkResolution mode)
{
auto res = CanonPath::root;
int linksAllowed = 1024;
std::list<std::string> todo;
for (auto & c : path)
todo.push_back(std::string(c));
while (!todo.empty()) {
auto c = *todo.begin();
todo.pop_front();
if (c == "" || c == ".")
;
else if (c == "..")
res.pop();
else {
res.push(c);
if (mode == SymlinkResolution::Full || !todo.empty()) {
if (auto st = maybeLstat(res); st && st->type == SourceAccessor::tSymlink) {
if (!linksAllowed--)
throw Error("infinite symlink recursion in path '%s'", showPath(path));
auto target = readLink(res);
res.pop();
if (hasPrefix(target, "/"))
res = CanonPath::root;
todo.splice(todo.begin(), tokenizeString<std::list<std::string>>(target, "/"));
}
}
}
}
return res;
}
} }

View file

@ -9,6 +9,26 @@ namespace nix {
struct Sink; struct Sink;
/**
* Note there is a decent chance this type soon goes away because the problem is solved another way.
* See the discussion in https://github.com/NixOS/nix/pull/9985.
*/
enum class SymlinkResolution {
/**
* Resolve symlinks in the ancestors only.
*
* Only the last component of the result is possibly a symlink.
*/
Ancestors,
/**
* Resolve symlinks fully, realpath(3)-style.
*
* No component of the result will be a symlink.
*/
Full,
};
/** /**
* A read-only filesystem abstraction. This is used by the Nix * A read-only filesystem abstraction. This is used by the Nix
* evaluator and elsewhere for accessing sources in various * evaluator and elsewhere for accessing sources in various
@ -112,9 +132,9 @@ struct SourceAccessor
PathFilter & filter = defaultPathFilter); PathFilter & filter = defaultPathFilter);
Hash hashPath( Hash hashPath(
const CanonPath & path, const CanonPath & path,
PathFilter & filter = defaultPathFilter, PathFilter & filter = defaultPathFilter,
HashAlgorithm ha = HashAlgorithm::SHA256); HashAlgorithm ha = HashAlgorithm::SHA256);
/** /**
* Return a corresponding path in the root filesystem, if * Return a corresponding path in the root filesystem, if
@ -137,6 +157,17 @@ struct SourceAccessor
void setPathDisplay(std::string displayPrefix, std::string displaySuffix = ""); void setPathDisplay(std::string displayPrefix, std::string displaySuffix = "");
virtual std::string showPath(const CanonPath & path); virtual std::string showPath(const CanonPath & path);
/**
* Resolve any symlinks in `path` according to the given
* resolution mode.
*
* @param mode might only be a temporary solution for this.
* See the discussion in https://github.com/NixOS/nix/pull/9985.
*/
CanonPath resolveSymlinks(
const CanonPath & path,
SymlinkResolution mode = SymlinkResolution::Full);
}; };
} }

View file

@ -62,44 +62,6 @@ bool SourcePath::operator<(const SourcePath & x) const
return std::tie(*accessor, path) < std::tie(*x.accessor, x.path); return std::tie(*accessor, path) < std::tie(*x.accessor, x.path);
} }
SourcePath SourcePath::resolveSymlinks(SymlinkResolution mode) const
{
auto res = SourcePath(accessor);
int linksAllowed = 1024;
std::list<std::string> todo;
for (auto & c : path)
todo.push_back(std::string(c));
bool resolve_last = mode == SymlinkResolution::Full;
while (!todo.empty()) {
auto c = *todo.begin();
todo.pop_front();
if (c == "" || c == ".")
;
else if (c == "..")
res.path.pop();
else {
res.path.push(c);
if (resolve_last || !todo.empty()) {
if (auto st = res.maybeLstat(); st && st->type == InputAccessor::tSymlink) {
if (!linksAllowed--)
throw Error("infinite symlink recursion in path '%s'", path);
auto target = res.readLink();
res.path.pop();
if (hasPrefix(target, "/"))
res.path = CanonPath::root;
todo.splice(todo.begin(), tokenizeString<std::list<std::string>>(target, "/"));
}
}
}
}
return res;
}
std::ostream & operator<<(std::ostream & str, const SourcePath & path) std::ostream & operator<<(std::ostream & str, const SourcePath & path)
{ {
str << path.to_string(); str << path.to_string();

View file

@ -11,26 +11,6 @@
namespace nix { namespace nix {
/**
* Note there is a decent chance this type soon goes away because the problem is solved another way.
* See the discussion in https://github.com/NixOS/nix/pull/9985.
*/
enum class SymlinkResolution {
/**
* Resolve symlinks in the ancestors only.
*
* Only the last component of the result is possibly a symlink.
*/
Ancestors,
/**
* Resolve symlinks fully, realpath(3)-style.
*
* No component of the result will be a symlink.
*/
Full,
};
/** /**
* An abstraction for accessing source files during * An abstraction for accessing source files during
* evaluation. Currently, it's just a wrapper around `CanonPath` that * evaluation. Currently, it's just a wrapper around `CanonPath` that
@ -123,14 +103,13 @@ struct SourcePath
bool operator<(const SourcePath & x) const; bool operator<(const SourcePath & x) const;
/** /**
* Resolve any symlinks in this `SourcePath` according to the * Convenience wrapper around `SourceAccessor::resolveSymlinks()`.
* given resolution mode.
*
* @param mode might only be a temporary solution for this.
* See the discussion in https://github.com/NixOS/nix/pull/9985.
*/ */
SourcePath resolveSymlinks( SourcePath resolveSymlinks(
SymlinkResolution mode = SymlinkResolution::Full) const; SymlinkResolution mode = SymlinkResolution::Full) const
{
return {accessor, accessor->resolveSymlinks(path, mode)};
}
}; };
std::ostream & operator << (std::ostream & str, const SourcePath & path); std::ostream & operator << (std::ostream & str, const SourcePath & path);

View file

@ -124,7 +124,8 @@ struct CmdShell : InstallablesCommand, MixEnvironment
if (true) if (true)
pathAdditions.push_back(store->printStorePath(path) + "/bin"); pathAdditions.push_back(store->printStorePath(path) + "/bin");
auto propPath = CanonPath(store->printStorePath(path)) / "nix-support" / "propagated-user-env-packages"; auto propPath = accessor->resolveSymlinks(
CanonPath(store->printStorePath(path)) / "nix-support" / "propagated-user-env-packages");
if (auto st = accessor->maybeLstat(propPath); st && st->type == SourceAccessor::tRegular) { if (auto st = accessor->maybeLstat(propPath); st && st->type == SourceAccessor::tRegular) {
for (auto & p : tokenizeString<Paths>(accessor->readFile(propPath))) for (auto & p : tokenizeString<Paths>(accessor->readFile(propPath)))
todo.push(store->parseStorePath(p)); todo.push(store->parseStorePath(p));

View file

@ -1,6 +1,6 @@
with import ./config.nix; with import ./config.nix;
{ rec {
hello = mkDerivation { hello = mkDerivation {
name = "hello"; name = "hello";
outputs = [ "out" "dev" ]; outputs = [ "out" "dev" ];
@ -24,6 +24,14 @@ with import ./config.nix;
''; '';
}; };
hello-symlink = mkDerivation {
name = "hello-symlink";
buildCommand =
''
ln -s ${hello} $out
'';
};
salve-mundi = mkDerivation { salve-mundi = mkDerivation {
name = "salve-mundi"; name = "salve-mundi";
outputs = [ "out" ]; outputs = [ "out" ];

View file

@ -10,6 +10,8 @@ nix shell -f shell-hello.nix hello -c hello NixOS | grep 'Hello NixOS'
nix shell -f shell-hello.nix hello^dev -c hello2 | grep 'Hello2' nix shell -f shell-hello.nix hello^dev -c hello2 | grep 'Hello2'
nix shell -f shell-hello.nix 'hello^*' -c hello2 | grep 'Hello2' nix shell -f shell-hello.nix 'hello^*' -c hello2 | grep 'Hello2'
# Test output paths that are a symlink.
#nix shell -f shell-hello.nix hello-symlink -c hello | grep 'Hello World'
if isDaemonNewer "2.20.0pre20231220"; then if isDaemonNewer "2.20.0pre20231220"; then
# Test that command line attribute ordering is reflected in the PATH # Test that command line attribute ordering is reflected in the PATH