Compare commits

..

7 Commits

Author SHA1 Message Date
Eelco Dolstra
490cb842cc Add release note 2026-02-18 22:50:37 +01:00
Eelco Dolstra
6992698ac5 builtins.getFlake: Support path values
This allows doing `builtins.getFlake ./subflake` instead of ugly hacks.
2026-02-18 22:12:09 +01:00
Eelco Dolstra
9868310d6f Add test for builtins.getFlake 2026-02-18 21:58:36 +01:00
John Ericson
08ce8dbfba Merge pull request #15283 from obsidiansystems/filesytem-error-improvements
Filesystem error improvements
2026-02-18 18:29:44 +00:00
John Ericson
bbcf2041e1 File system error improvements
- Make `descriptorToPath` cross-platform (renamed from
  `windows::handleToPath`). Uses `/proc/self/fd` on Linux and
  `F_GETPATH` on macOS. Add `HAVE_F_GETPATH` meson check.

  This is based on 7226a116a0, which was
  removed in 479c356510, but is now
  introduced more judiciously.

- Unix error messages in `readFull`, `writeFull`, `readLine` now include
  file paths via `descriptorToPath`.

- Convert `std::filesystem::filesystem_error` to `SystemError`

  Wrappers like `readLink`, `createDirs`, `DirectoryIterator`, etc. now
  catch `std::filesystem::filesystem_error` and rethrow as `SystemError`
  with the error code preserved. This ensures consistent exception types
  throughout the codebase.

  Call sites that previously caught `filesystem_error` and rethrew with
  `throw;` now throw `SystemError(e.code(), ...)` instead.

  Some call sites can stop catching `filesystem_error` at all,
  because they only call the wrapped functions.

- Rework `SystemError` constructors to auto-append error message

  The public `SystemError(std::error_code, ...)` constructor now
  automatically appends `errorCode.message()` to the error message.
  A protected constructor takes an explicit error message string for
  subclasses.

  `SysError` delegates to the protected constructor with `strerror(errNo)`.
  `WinError` delegates with `renderError(lastError)` (now static).

  This removes the need to manually append `e.code().message()` at call
  sites when converting `filesystem_error` to `SystemError`.

- Use perfect forwarding (`Args &&...` with `std::forward`) consistently
  in `BaseError`, `SystemError`, `SysError`, and `WinError` constructors.

Co-authored-by: Sergei Zimmerman <sergei@zimmerman.foo>
2026-02-18 12:29:11 -05:00
John Ericson
96bcf5928f Merge pull request #15273 from NixOS/more-robust-ubsan-macro
libutil: More robust check for NIX_UBSAN_ENABLED
2026-02-18 16:15:26 +00:00
Sergei Zimmerman
db853cf4fb libutil: More robust check for NIX_UBSAN_ENABLED
In 3df91bea62 I forgot that the header
might get included out-of-tree with -Wundef. Let's make this a public
config option for libutil as it can affect function bodies in headers.
2026-02-18 17:33:51 +03:00
56 changed files with 322 additions and 208 deletions

View File

@@ -0,0 +1,6 @@
---
synopsis: "`builtins.getFlake` now supports path values"
prs: [15290]
---
`builtins.getFlake` now accepts path values in addition to flakerefs, allowing you to write `builtins.getFlake ./subflake` instead of having to use ugly workarounds to construct a pure flakeref.

View File

@@ -9,9 +9,3 @@ endif
if 'address' in get_option('b_sanitize')
deps_other += declare_dependency(sources : 'asan-options.cc')
endif
if 'undefined' in get_option('b_sanitize')
add_project_arguments('-DNIX_UBSAN_ENABLED=1', language : 'cpp')
else
add_project_arguments('-DNIX_UBSAN_ENABLED=0', language : 'cpp')
endif

View File

@@ -710,7 +710,7 @@ void NixRepl::loadFlake(const std::string & flakeRefS)
try {
cwd = std::filesystem::current_path();
} catch (std::filesystem::filesystem_error & e) {
throw SysError("cannot determine current working directory");
throw SystemError(e.code(), "cannot determine current working directory");
}
auto flakeRef = parseFlakeRef(fetchSettings, flakeRefS, cwd.string(), true);

View File

@@ -11,7 +11,7 @@
namespace nix::eval_cache {
CachedEvalError::CachedEvalError(ref<AttrCursor> cursor, Symbol attr)
: CloneableError(cursor->root->state, "cached failure of attribute '%s'", cursor->getAttrPathStr(attr))
: EvalError(cursor->root->state, "cached failure of attribute '%s'", cursor->getAttrPathStr(attr))
, cursor(cursor)
, attr(attr)
{

View File

@@ -14,7 +14,7 @@ namespace nix::eval_cache {
struct AttrDb;
class AttrCursor;
struct CachedEvalError : CloneableError<CachedEvalError, EvalError>
struct CachedEvalError : EvalError
{
const ref<AttrCursor> cursor;
const Symbol attr;

View File

@@ -18,7 +18,7 @@ class EvalErrorBuilder;
*
* Most subclasses should inherit from `EvalError` instead of this class.
*/
class EvalBaseError : public CloneableError<EvalBaseError, Error>
class EvalBaseError : public Error
{
template<class T>
friend class EvalErrorBuilder;
@@ -26,14 +26,14 @@ public:
EvalState & state;
EvalBaseError(EvalState & state, ErrorInfo && errorInfo)
: CloneableError(errorInfo)
: Error(errorInfo)
, state(state)
{
}
template<typename... Args>
explicit EvalBaseError(EvalState & state, const std::string & formatString, const Args &... formatArgs)
: CloneableError(formatString, formatArgs...)
: Error(formatString, formatArgs...)
, state(state)
{
}
@@ -60,23 +60,23 @@ MakeError(InfiniteRecursionError, EvalError);
* Inherits from EvalBaseError (not EvalError) because resource exhaustion
* should not be cached.
*/
struct StackOverflowError : public CloneableError<StackOverflowError, EvalBaseError>
struct StackOverflowError : public EvalBaseError
{
StackOverflowError(EvalState & state)
: CloneableError(state, "stack overflow; max-call-depth exceeded")
: EvalBaseError(state, "stack overflow; max-call-depth exceeded")
{
}
};
MakeError(IFDError, EvalBaseError);
struct InvalidPathError : public CloneableError<InvalidPathError, EvalError>
struct InvalidPathError : public EvalError
{
public:
Path path;
InvalidPathError(EvalState & state, const Path & path)
: CloneableError(state, "path '%s' is not valid", path)
: EvalError(state, "path '%s' is not valid", path)
{
}
};

View File

@@ -9,14 +9,14 @@
namespace nix {
class BadNixStringContextElem final : public CloneableError<BadNixStringContextElem, Error>
class BadNixStringContextElem : public Error
{
public:
std::string_view raw;
template<typename... Args>
BadNixStringContextElem(std::string_view raw_, const Args &... args)
: CloneableError("")
: Error("")
{
raw = raw_;
auto hf = HintFmt(args...);

View File

@@ -1312,12 +1312,11 @@ static void prim_warn(EvalState & state, const PosIdx pos, Value ** args, Value
state.forceString(*args[0], pos, "while evaluating the first argument; the message passed to builtins.warn");
{
ErrorInfo info{
.level = lvlWarn,
.msg = HintFmt(std::string(msgStr)),
.pos = state.positions[pos],
.isFromExpr = true,
};
BaseError msg(std::string{msgStr});
msg.atPos(state.positions[pos]);
auto info = msg.info();
info.level = lvlWarn;
info.isFromExpr = true;
logWarning(info);
}

View File

@@ -74,11 +74,11 @@ namespace nix {
struct GitSourceAccessor;
struct GitError final : public CloneableError<GitError, Error>
struct GitError : public Error
{
template<typename... Ts>
GitError(const git_error & error, Ts &&... args)
: CloneableError("")
: Error("")
{
auto hf = HintFmt(std::forward<Ts>(args)...);
err.msg = HintFmt("%1%: %2% (libgit2 error code = %3%)", Uncolored(hf.str()), error.message, error.klass);
@@ -247,7 +247,8 @@ static void initRepoAtomically(std::filesystem::path & path, GitRepo::Options op
|| e.code() == std::errc::directory_not_empty) {
return;
} else
throw SysError("moving temporary git repository from %s to %s", PathFmt(tmpDir), PathFmt(path));
throw SystemError(
e.code(), "moving temporary git repository from %s to %s", PathFmt(tmpDir), PathFmt(path));
}
// we successfully moved the repository, so the temporary directory no longer exists.
delTmpDir.cancel();

View File

@@ -35,35 +35,39 @@ namespace nix::flake::primops {
PrimOp getFlake(const Settings & settings)
{
auto prim_getFlake = [&settings](EvalState & state, const PosIdx pos, Value ** args, Value & v) {
std::string flakeRefS(
state.forceStringNoCtx(*args[0], pos, "while evaluating the argument passed to builtins.getFlake"));
auto flakeRef = nix::parseFlakeRef(state.fetchSettings, flakeRefS, {}, true);
if (state.settings.pureEval && !flakeRef.input.isLocked(state.fetchSettings))
throw Error(
"cannot call 'getFlake' on unlocked flake reference '%s', at %s (use --impure to override)",
flakeRefS,
state.positions[pos]);
state.forceValue(*args[0], pos);
callFlake(
state,
lockFlake(
settings,
state,
flakeRef,
LockFlags{
.updateLockFile = false,
.writeLockFile = false,
.useRegistries = !state.settings.pureEval && settings.useRegistries,
.allowUnlocked = !state.settings.pureEval,
}),
v);
LockFlags lockFlags{
.updateLockFile = false,
.writeLockFile = false,
.useRegistries = !state.settings.pureEval && settings.useRegistries,
.allowUnlocked = !state.settings.pureEval,
};
if (args[0]->type() == nPath) {
auto path = state.realisePath(pos, *args[0]);
callFlake(state, lockFlake(settings, state, path, lockFlags), v);
} else {
NixStringContext context;
std::string flakeRefS(
state.forceStringNoCtx(*args[0], pos, "while evaluating the argument passed to builtins.getFlake"));
auto flakeRef = nix::parseFlakeRef(state.fetchSettings, flakeRefS, {}, true);
if (state.settings.pureEval && !flakeRef.input.isLocked(state.fetchSettings))
throw Error(
"cannot call 'getFlake' on unlocked flake reference '%s', at %s (use --impure to override)",
flakeRefS,
state.positions[pos]);
callFlake(state, lockFlake(settings, state, flakeRef, lockFlags), v);
}
};
return PrimOp{
.name = "__getFlake",
.args = {"args"},
.doc = R"(
Fetch a flake from a flake reference, and return its output attributes and some metadata. For example:
Fetch a flake from a flake reference or a path, and return its output attributes and some metadata. For example:
```nix
(builtins.getFlake "nix/55bc52401966fbffa525c574c14f67b00bc4fb3a").packages.x86_64-linux.nix

View File

@@ -416,10 +416,8 @@ static LockFile readLockFile(const fetchers::Settings & fetchSettings, const Sou
: LockFile();
}
/* Compute an in-memory lock file for the specified top-level flake,
and optionally write it to file, if the flake is writable. */
LockedFlake
lockFlake(const Settings & settings, EvalState & state, const FlakeRef & topRef, const LockFlags & lockFlags)
LockedFlake lockFlake(
const Settings & settings, EvalState & state, const FlakeRef & topRef, const LockFlags & lockFlags, Flake flake)
{
experimentalFeatureSettings.require(Xp::Flakes);
@@ -427,8 +425,6 @@ lockFlake(const Settings & settings, EvalState & state, const FlakeRef & topRef,
auto useRegistriesTop = useRegistries ? fetchers::UseRegistries::All : fetchers::UseRegistries::No;
auto useRegistriesInputs = useRegistries ? fetchers::UseRegistries::Limited : fetchers::UseRegistries::No;
auto flake = getFlake(state, topRef, useRegistriesTop, {});
if (lockFlags.applyNixConfig) {
flake.config.apply(settings);
state.store->setOptions();
@@ -908,6 +904,22 @@ lockFlake(const Settings & settings, EvalState & state, const FlakeRef & topRef,
}
}
LockedFlake
lockFlake(const Settings & settings, EvalState & state, const FlakeRef & topRef, const LockFlags & lockFlags)
{
auto useRegistries = lockFlags.useRegistries.value_or(settings.useRegistries);
auto useRegistriesTop = useRegistries ? fetchers::UseRegistries::All : fetchers::UseRegistries::No;
return lockFlake(settings, state, topRef, lockFlags, getFlake(state, topRef, useRegistriesTop, {}));
}
LockedFlake
lockFlake(const Settings & settings, EvalState & state, const SourcePath & flakeDir, const LockFlags & lockFlags)
{
/* We need a fake flakeref to put in the `Flake` struct, but it's not used for anything. */
auto fakeRef = parseFlakeRef(state.fetchSettings, "flake:get-flake");
return lockFlake(settings, state, fakeRef, lockFlags, readFlake(state, fakeRef, fakeRef, fakeRef, flakeDir, {}));
}
static ref<SourceAccessor> makeInternalFS()
{
auto internalFS = make_ref<MemorySourceAccessor>(MemorySourceAccessor{});

View File

@@ -214,9 +214,16 @@ struct LockFlags
std::set<NonEmptyInputAttrPath> inputUpdates;
};
/*
* Compute an in-memory lock file for the specified top-level flake, and optionally write it to file, if the flake is
* writable.
*/
LockedFlake
lockFlake(const Settings & settings, EvalState & state, const FlakeRef & flakeRef, const LockFlags & lockFlags);
LockedFlake
lockFlake(const Settings & settings, EvalState & state, const SourcePath & flakeDir, const LockFlags & lockFlags);
void callFlake(EvalState & state, const LockedFlake & lockedFlake, Value & v);
/**

View File

@@ -28,7 +28,7 @@
namespace nix {
AwsAuthError::AwsAuthError(int errorCode)
: CloneableError("AWS authentication error: '%s' (%d)", aws_error_str(errorCode), errorCode)
: Error("AWS authentication error: '%s' (%d)", aws_error_str(errorCode), errorCode)
, errorCode(errorCode)
{
}

View File

@@ -1,11 +1,11 @@
#include "nix/store/build/goal.hh"
#include "nix/store/build/worker.hh"
#include "nix/store/worker-settings.hh"
#include "nix/store/globals.hh"
namespace nix {
TimedOut::TimedOut(time_t maxDuration)
: CloneableError(BuildResult::Failure::TimedOut, "timed out after %1% seconds", maxDuration)
: BuildError(BuildResult::Failure::TimedOut, "timed out after %1% seconds", maxDuration)
, maxDuration(maxDuration)
{
}

View File

@@ -36,8 +36,8 @@ static void builtinUnpackChannel(const BuiltinBuilderContext & ctx)
auto target = out / channelName;
try {
std::filesystem::rename(fileName, target);
} catch (std::filesystem::filesystem_error &) {
throw SysError("failed to rename %1% to %2%", fileName, target.string());
} catch (std::filesystem::filesystem_error & e) {
throw SystemError(e.code(), "failed to rename %1% to %2%", fileName, target.string());
}
}

View File

@@ -60,12 +60,12 @@ namespace {
using curlSList = std::unique_ptr<::curl_slist, decltype([](::curl_slist * list) { ::curl_slist_free_all(list); })>;
using curlMulti = std::unique_ptr<::CURLM, decltype([](::CURLM * multi) { ::curl_multi_cleanup(multi); })>;
struct curlMultiError final : CloneableError<curlMultiError, Error>
struct curlMultiError : Error
{
::CURLMcode code;
curlMultiError(::CURLMcode code)
: CloneableError{"unexpected curl multi error: %s", ::curl_multi_strerror(code)}
: Error{"unexpected curl multi error: %s", ::curl_multi_strerror(code)}
{
assert(code != CURLM_OK);
}
@@ -1212,7 +1212,7 @@ void FileTransfer::download(
template<typename... Args>
FileTransferError::FileTransferError(
FileTransfer::Error error, std::optional<std::string> response, const Args &... args)
: CloneableError(args...)
: Error(args...)
, error(error)
, response(response)
{

View File

@@ -272,7 +272,7 @@ void LocalStore::findRoots(const Path & path, std::filesystem::file_type type, R
|| e.code() == std::errc::not_a_directory)
printInfo("cannot read potential root '%1%'", path);
else
throw;
throw SystemError(e.code(), "finding GC roots in '%1%'", path);
}
catch (SystemError & e) {

View File

@@ -34,13 +34,12 @@ struct AwsCredentials
}
};
class AwsAuthError final : public CloneableError<AwsAuthError, Error>
class AwsAuthError : public Error
{
std::optional<int> errorCode;
public:
using CloneableError::CloneableError;
using Error::Error;
AwsAuthError(int errorCode);
std::optional<int> getErrorCode() const

View File

@@ -58,7 +58,7 @@ enum struct BuildResultFailureStatus : uint8_t {
* This is both an exception type (inherits from Error) and serves as
* the failure variant in BuildResult::inner.
*/
struct BuildError : public CloneableError<BuildError, Error>
struct BuildError : public Error
{
using Status = BuildResultFailureStatus;
using enum Status;
@@ -80,7 +80,7 @@ public:
*/
template<typename... Args>
BuildError(Status status, const Args &... args)
: CloneableError(args...)
: Error(args...)
, status{status}
{
}
@@ -97,7 +97,7 @@ public:
* Also used for deserialization.
*/
BuildError(Args args)
: CloneableError(std::move(args.msg))
: Error(std::move(args.msg))
, status{args.status}
, isNonDeterministic{args.isNonDeterministic}
@@ -108,7 +108,7 @@ public:
* Default constructor for deserialization.
*/
BuildError()
: CloneableError("")
: Error("")
{
}

View File

@@ -20,14 +20,14 @@ namespace nix {
* Denotes a build failure that stemmed from the builder exiting with a
* failing exist status.
*/
struct BuilderFailureError final : CloneableError<BuilderFailureError, BuildError>
struct BuilderFailureError : BuildError
{
int builderStatus;
std::string extraMsgAfter;
BuilderFailureError(BuildResult::Failure::Status status, int builderStatus, std::string extraMsgAfter)
: CloneableError{
: BuildError{
status,
/* No message for now, because the caller will make for
us, with extra context */

View File

@@ -10,7 +10,7 @@
namespace nix {
struct TimedOut final : CloneableError<TimedOut, BuildError>
struct TimedOut : BuildError
{
time_t maxDuration;

View File

@@ -22,7 +22,7 @@ struct Package
}
};
class BuildEnvFileConflictError final : public CloneableError<BuildEnvFileConflictError, Error>
class BuildEnvFileConflictError : public Error
{
public:
const Path fileA;
@@ -30,7 +30,7 @@ public:
int priority;
BuildEnvFileConflictError(const Path fileA, const Path fileB, int priority)
: CloneableError(
: Error(
"Unable to build profile. There is a conflict for the following files:\n"
"\n"
" %1%\n"

View File

@@ -403,7 +403,7 @@ ref<FileTransfer> getFileTransfer();
*/
ref<FileTransfer> makeFileTransfer(const FileTransferSettings & settings = fileTransferSettings);
class FileTransferError final : public CloneableError<FileTransferError, Error>
class FileTransferError : public Error
{
public:
FileTransfer::Error error;

View File

@@ -146,7 +146,7 @@ struct RealisedPath
auto operator<=>(const RealisedPath &) const = default;
};
class MissingRealisation final : public CloneableError<MissingRealisation, Error>
class MissingRealisation : public Error
{
public:
MissingRealisation(DrvOutput & outputId)
@@ -155,7 +155,7 @@ public:
}
MissingRealisation(std::string_view drv, OutputName outputName)
: CloneableError(
: Error(
"cannot operate on output '%s' of the "
"unbuilt derivation '%s'",
outputName,

View File

@@ -166,7 +166,7 @@ struct SQLiteTxn
~SQLiteTxn();
};
struct SQLiteError : CloneableError<SQLiteError, Error>
struct SQLiteError : Error
{
std::string path;
std::string errMsg;

View File

@@ -35,7 +35,7 @@ static void readProcLink(const std::filesystem::path & file, UncheckedRoots & ro
if (e.code() == std::errc::no_such_file_or_directory || e.code() == std::errc::permission_denied
|| e.code() == std::errc::no_such_process)
return;
throw;
throw SystemError(e.code(), "reading symlink '%s'", PathFmt(file));
}
if (buf.is_absolute())
roots[buf.string()].emplace(file.string());

View File

@@ -111,8 +111,8 @@ static std::vector<std::string> expandBuilderLines(const std::string & builders)
std::string text;
try {
text = readFile(path);
} catch (const SysError & e) {
if (e.errNo != ENOENT)
} catch (const SystemError & e) {
if (!e.is(std::errc::no_such_file_or_directory))
throw;
debug("cannot find machines file '%s'", path);
continue;

View File

@@ -202,14 +202,13 @@ void LocalStore::optimisePath_(
full. When that happens, it's fine to ignore it: we
just effectively disable deduplication of this
file.
TODO: Get rid of errno, use error code.
*/
printInfo("cannot link %s to '%s': %s", PathFmt(linkPath), path, strerror(errno));
printInfo("cannot link %s to '%s': %s", PathFmt(linkPath), path, e.code().message());
return;
}
else
throw;
throw SystemError(e.code(), "creating hard link from %1% to %2%", PathFmt(linkPath), path);
}
}
@@ -250,7 +249,7 @@ void LocalStore::optimisePath_(
printInfo("%1% has maximum number of links", PathFmt(linkPath));
return;
}
throw;
throw SystemError(e.code(), "creating hard link from %1% to %2%", PathFmt(linkPath), PathFmt(tempLink));
}
/* Atomically replace the old file with the new hard link. */
@@ -271,7 +270,7 @@ void LocalStore::optimisePath_(
debug("%s has reached maximum number of links", PathFmt(linkPath));
return;
}
throw;
throw SystemError(e.code(), "renaming %1% to %2%", PathFmt(tempLink), path);
}
stats.filesLinked++;

View File

@@ -103,7 +103,7 @@ static void removeFile(const std::filesystem::path & path)
try {
std::filesystem::remove(path);
} catch (std::filesystem::filesystem_error & e) {
throw SysError("removing file %1%", PathFmt(path));
throw SystemError(e.code(), "removing file %1%", PathFmt(path));
}
}
@@ -324,8 +324,6 @@ std::filesystem::path getDefaultProfile(ProfileDirsOptions settings)
return absPath(readLink(profileLink), &linkDir);
} catch (Error &) {
return profileLink;
} catch (std::filesystem::filesystem_error &) {
return profileLink;
}
}

View File

@@ -17,7 +17,7 @@ namespace nix {
SQLiteError::SQLiteError(
const char * path, const char * errMsg, int errNo, int extendedErrNo, int offset, HintFmt && hf)
: CloneableError("")
: Error("")
, path(path)
, errMsg(errMsg)
, errNo(errNo)

View File

@@ -18,11 +18,11 @@ static std::string parsePublicHostKey(std::string_view host, std::string_view ss
}
}
class InvalidSSHAuthority final : public CloneableError<InvalidSSHAuthority, Error>
class InvalidSSHAuthority : public Error
{
public:
InvalidSSHAuthority(const ParsedURL::Authority & authority, std::string_view reason)
: CloneableError("invalid SSH authority: '%s': %s", authority.to_string(), reason)
: Error("invalid SSH authority: '%s': %s", authority.to_string(), reason)
{
}
};

View File

@@ -55,10 +55,10 @@
namespace nix {
struct NotDeterministic final : CloneableError<NotDeterministic, BuildError>
struct NotDeterministic : BuildError
{
NotDeterministic(auto &&... args)
: CloneableError(BuildResult::Failure::NotDeterministic, args...)
: BuildError(BuildResult::Failure::NotDeterministic, args...)
{
isNonDeterministic = true;
}

View File

@@ -319,7 +319,7 @@ TEST(chmodIfNeeded, works)
TEST(chmodIfNeeded, nonexistent)
{
ASSERT_THROW(chmodIfNeeded("/schnitzel/darmstadt/pommes", 0755), SysError);
ASSERT_THROW(chmodIfNeeded("/schnitzel/darmstadt/pommes", 0755), SystemError);
}
/* ----------------------------------------------------------------------------
@@ -340,7 +340,7 @@ TEST(DirectoryIterator, works)
TEST(DirectoryIterator, nonexistent)
{
ASSERT_THROW(DirectoryIterator("/schnitzel/darmstadt/pommes"), SysError);
ASSERT_THROW(DirectoryIterator("/schnitzel/darmstadt/pommes"), SystemError);
}
/* ----------------------------------------------------------------------------

View File

@@ -378,7 +378,7 @@ std::set<ExperimentalFeature> parseFeatures(const StringSet & rawFeatures)
}
MissingExperimentalFeature::MissingExperimentalFeature(ExperimentalFeature feature, std::string reason)
: CloneableError(
: Error(
"experimental Nix feature '%1%' is disabled%2%; add '--extra-experimental-features %1%' to enable it",
showExperimentalFeature(feature),
Uncolored(optionalBracket(" (", reason, ")")))

View File

@@ -40,9 +40,9 @@ DirectoryIterator::DirectoryIterator(const std::filesystem::path & p)
// **Attempt to create the underlying directory_iterator**
it_ = std::filesystem::directory_iterator(p);
} catch (const std::filesystem::filesystem_error & e) {
// **Catch filesystem_error and throw SysError**
// Adapt the error message as needed for SysError
throw SysError("cannot read directory %s", PathFmt(p));
// **Catch filesystem_error and throw SystemError**
// Adapt the error message as needed for SystemError
throw SystemError(e.code(), "cannot read directory %s", PathFmt(p));
}
}
@@ -280,7 +280,11 @@ bool pathAccessible(const std::filesystem::path & path)
std::filesystem::path readLink(const std::filesystem::path & path)
{
checkInterrupt();
return std::filesystem::read_symlink(path);
try {
return std::filesystem::read_symlink(path);
} catch (std::filesystem::filesystem_error & e) {
throw SystemError(e.code(), "reading symbolic link '%s'", PathFmt(path));
}
}
Path readLink(const Path & path)
@@ -463,7 +467,7 @@ void createDirs(const std::filesystem::path & path)
try {
std::filesystem::create_directories(path);
} catch (std::filesystem::filesystem_error & e) {
throw SysError("creating directory '%1%'", path.string());
throw SystemError(e.code(), "creating directory '%1%'", path.string());
}
}
@@ -664,7 +668,7 @@ void replaceSymlink(const std::filesystem::path & target, const std::filesystem:
} catch (std::filesystem::filesystem_error & e) {
if (e.code() == std::errc::file_exists)
continue;
throw SysError("creating symlink %1% -> %2%", PathFmt(tmp), PathFmt(target));
throw SystemError(e.code(), "creating symlink %1% -> %2%", PathFmt(tmp), PathFmt(target));
}
try {
@@ -672,7 +676,7 @@ void replaceSymlink(const std::filesystem::path & target, const std::filesystem:
} catch (std::filesystem::filesystem_error & e) {
if (e.code() == std::errc::file_exists)
continue;
throw SysError("renaming %1% to %2%", PathFmt(tmp), PathFmt(link));
throw SystemError(e.code(), "renaming %1% to %2%", PathFmt(tmp), PathFmt(link));
}
break;
@@ -773,7 +777,7 @@ std::filesystem::path makeParentCanonical(const std::filesystem::path & rawPath)
}
return std::filesystem::canonical(parent) / path.filename();
} catch (std::filesystem::filesystem_error & e) {
throw SysError("canonicalising parent path of %1%", PathFmt(path));
throw SystemError(e.code(), "canonicalising parent path of %1%", PathFmt(path));
}
}

View File

@@ -17,6 +17,7 @@
#include "nix/util/suggestions.hh"
#include "nix/util/fmt.hh"
#include "nix/util/config.hh"
#include <cstring>
#include <list>
@@ -126,20 +127,20 @@ public:
BaseError & operator=(BaseError &&) = default;
template<typename... Args>
BaseError(unsigned int status, const Args &... args)
: err{.level = lvlError, .msg = HintFmt(args...), .pos = {}, .status = status}
BaseError(unsigned int status, Args &&... args)
: err{.level = lvlError, .msg = HintFmt(std::forward<Args>(args)...), .pos = {}, .status = status}
{
}
template<typename... Args>
explicit BaseError(const std::string & fs, const Args &... args)
: err{.level = lvlError, .msg = HintFmt(fs, args...), .pos = {}}
explicit BaseError(const std::string & fs, Args &&... args)
: err{.level = lvlError, .msg = HintFmt(fs, std::forward<Args>(args)...), .pos = {}}
{
}
template<typename... Args>
BaseError(const Suggestions & sug, const Args &... args)
: err{.level = lvlError, .msg = HintFmt(args...), .pos = {}, .suggestions = sug}
BaseError(const Suggestions & sug, Args &&... args)
: err{.level = lvlError, .msg = HintFmt(std::forward<Args>(args)...), .pos = {}, .suggestions = sug}
{
}
@@ -203,9 +204,9 @@ public:
* @param args... Format string arguments.
*/
template<typename... Args>
void addTrace(std::shared_ptr<const Pos> && pos, std::string_view fs, const Args &... args)
void addTrace(std::shared_ptr<const Pos> && pos, std::string_view fs, Args &&... args)
{
addTrace(std::move(pos), HintFmt(std::string(fs), args...));
addTrace(std::move(pos), HintFmt(std::string(fs), std::forward<Args>(args)...));
}
/**
@@ -226,31 +227,13 @@ public:
{
return err;
};
[[noreturn]] virtual void throwClone() const = 0;
};
template<typename Derived, typename Base>
class CloneableError : public Base
{
public:
using Base::Base;
/**
* Rethrow a copy of this exception. Useful when the exception can get
* modified when appending traces.
*/
[[noreturn]] void throwClone() const override
{
throw Derived(static_cast<const Derived &>(*this));
}
};
#define MakeError(newClass, superClass) \
class newClass : public CloneableError<newClass, superClass> \
{ \
public: \
using CloneableError<newClass, superClass>::CloneableError; \
#define MakeError(newClass, superClass) \
class newClass : public superClass \
{ \
public: \
using superClass::superClass; \
}
MakeError(Error, BaseError);
@@ -262,22 +245,42 @@ MakeError(UnimplementedError, Error);
* std::error_code. Use when you want to catch and check an error condition like
* no_such_file_or_directory (ENOENT) without ifdefs.
*/
class SystemError : public CloneableError<SystemError, Error>
class SystemError : public Error
{
std::error_code errorCode;
std::string errorDetails;
public:
protected:
/**
* Just here to allow derived classes to use the right constructor
* (the protected one).
*/
struct Disambig
{};
/**
* Protected constructor for subclasses that provide their own error message.
* The error message is appended to the formatted hint.
*/
template<typename... Args>
SystemError(std::errc posixErrNo, Args &&... args)
: CloneableError(std::forward<Args>(args)...)
, errorCode(std::make_error_code(posixErrNo))
SystemError(Disambig, std::error_code errorCode, std::string_view errorDetails, Args &&... args)
: Error("")
, errorCode(errorCode)
, errorDetails(errorDetails)
{
auto hf = HintFmt(std::forward<Args>(args)...);
err.msg = HintFmt("%s: %s", Uncolored(hf.str()), errorDetails);
}
public:
/**
* Construct with an error code. The error code's message is automatically
* appended to the error message.
*/
template<typename... Args>
SystemError(std::error_code errorCode, Args &&... args)
: CloneableError(std::forward<Args>(args)...)
, errorCode(errorCode)
: SystemError(Disambig{}, errorCode, errorCode.message(), std::forward<Args>(args)...)
{
}
@@ -308,7 +311,7 @@ public:
* support is too WIP to justify the code churn, but if it is finished
* then a better identifier becomes moe worth it.
*/
class SysError final : public CloneableError<SysError, SystemError>
class SysError : public SystemError
{
public:
int errNo;
@@ -318,12 +321,14 @@ public:
* will be used to try to add additional information to the message.
*/
template<typename... Args>
SysError(int errNo, const Args &... args)
: CloneableError(static_cast<std::errc>(errNo), "")
SysError(int errNo, Args &&... args)
: SystemError(
Disambig{},
std::make_error_code(static_cast<std::errc>(errNo)),
strerror(errNo),
std::forward<Args>(args)...)
, errNo(errNo)
{
auto hf = HintFmt(args...);
err.msg = HintFmt("%1%: %2%", Uncolored(hf.str()), strerror(errNo));
}
/**
@@ -333,8 +338,8 @@ public:
* calling this constructor!
*/
template<typename... Args>
SysError(const Args &... args)
: SysError(errno, args...)
SysError(Args &&... args)
: SysError(errno, std::forward<Args>(args)...)
{
}
};
@@ -368,7 +373,7 @@ int handleExceptions(const std::string & programName, std::function<void()> fun)
*/
[[gnu::noinline, gnu::cold, noreturn]] void unreachable(std::source_location loc = std::source_location::current());
#if NIX_UBSAN_ENABLED == 1
#if NIX_UBSAN_ENABLED
/* When building with sanitizers, also enable expensive unreachable checks. In
optimised builds this explicitly invokes UB with std::unreachable for better
optimisations. */
@@ -387,7 +392,7 @@ namespace windows {
* Unless you need to catch a specific error number, don't catch this in
* portable code. Catch `SystemError` instead.
*/
class WinError : public CloneableError<WinError, SystemError>
class WinError : public SystemError
{
public:
DWORD lastError;
@@ -398,12 +403,14 @@ public:
* information to the message.
*/
template<typename... Args>
WinError(DWORD lastError, const Args &... args)
: CloneableError(std::error_code(lastError, std::system_category()), "")
WinError(DWORD lastError, Args &&... args)
: SystemError(
Disambig{},
std::error_code(lastError, std::system_category()),
renderError(lastError),
std::forward<Args>(args)...)
, lastError(lastError)
{
auto hf = HintFmt(args...);
err.msg = HintFmt("%1%: %2%", Uncolored(hf.str()), renderError(lastError));
}
/**
@@ -413,14 +420,14 @@ public:
* before calling this constructor!
*/
template<typename... Args>
WinError(const Args &... args)
: WinError(GetLastError(), args...)
WinError(Args &&... args)
: WinError(GetLastError(), std::forward<Args>(args)...)
{
}
private:
std::string renderError(DWORD lastError);
static std::string renderError(DWORD lastError);
};
} // namespace windows

View File

@@ -80,7 +80,7 @@ std::set<ExperimentalFeature> parseFeatures(const StringSet &);
* An experimental feature was required for some (experimental)
* operation, but was not enabled.
*/
class MissingExperimentalFeature final : public CloneableError<MissingExperimentalFeature, Error>
class MissingExperimentalFeature : public Error
{
public:
/**

View File

@@ -75,7 +75,7 @@ namespace linux {
*
* @see https://man7.org/linux/man-pages/man2/openat2.2.html
* @see https://man7.org/linux/man-pages/man2/open_how.2type.html
v*
*
* @param flags O_* flags
* @param mode Mode for O_{CREAT,TMPFILE}
* @param resolve RESOLVE_* flags

View File

@@ -189,21 +189,19 @@ Path readLink(const Path & path);
*/
std::filesystem::path readLink(const std::filesystem::path & path);
#ifdef _WIN32
namespace windows {
/**
* Get the path associated with a file handle.
* Get the path associated with a file descriptor.
*
* @note One MUST only use this for error handling, because it creates
* TOCTOU issues. We don't mind if error messages point to out of date
* paths (that is a rather trivial TOCTOU --- the error message is best
* effort) but for anything else we do.
*
* @note this function will clobber `errno` (Unix) / "last error"
* (Windows), so care must be used to get those error codes, then call
* this, then build a `SysError` / `WinError` with the saved error code.
*/
std::filesystem::path handleToPath(Descriptor handle);
} // namespace windows
#endif
std::filesystem::path descriptorToPath(Descriptor fd);
/**
* Open a `Descriptor` with read-only access to the given directory.

View File

@@ -2,7 +2,12 @@
include_dirs = [ include_directories('../..') ]
headers = files(
config_pub_h = configure_file(
configuration : configdata_pub,
output : 'config.hh',
)
headers = [ config_pub_h ] + files(
'abstract-setting-to-json.hh',
'alignment.hh',
'ansicolor.hh',

View File

@@ -133,14 +133,14 @@ std::pair<int, std::string> runProgram(RunOptions && options);
void runProgram2(const RunOptions & options);
class ExecError final : public CloneableError<ExecError, Error>
class ExecError : public Error
{
public:
int status;
template<typename... Args>
ExecError(int status, const Args &... args)
: CloneableError(args...)
: Error(args...)
, status(status)
{
}

View File

@@ -231,19 +231,19 @@ ref<SourceAccessor> makeEmptySourceAccessor();
*/
MakeError(RestrictedPathError, Error);
struct SymlinkNotAllowed final : public CloneableError<SymlinkNotAllowed, Error>
struct SymlinkNotAllowed : public Error
{
CanonPath path;
SymlinkNotAllowed(CanonPath path)
: CloneableError("relative path '%s' points to a symlink, which is not allowed", path.rel())
: Error("relative path '%s' points to a symlink, which is not allowed", path.rel())
, path(std::move(path))
{
}
template<typename... Args>
SymlinkNotAllowed(CanonPath path, const std::string & fs, Args &&... args)
: CloneableError(fs, std::forward<Args>(args)...)
: Error(fs, std::forward<Args>(args)...)
, path(std::move(path))
{
}

View File

@@ -16,7 +16,8 @@ cxx = meson.get_compiler('cpp')
subdir('nix-meson-build-support/deps-lists')
configdata = configuration_data()
configdata_pub = configuration_data()
configdata_priv = configuration_data()
deps_private_maybe_subproject = []
deps_public_maybe_subproject = []
@@ -34,9 +35,15 @@ check_funcs = [
foreach funcspec : check_funcs
define_name = 'HAVE_' + funcspec[0].underscorify().to_upper()
define_value = cxx.has_function(funcspec[0]).to_int()
configdata.set(define_name, define_value, description : funcspec[1])
configdata_priv.set(define_name, define_value, description : funcspec[1])
endforeach
configdata_pub.set(
'NIX_UBSAN_ENABLED',
('undefined' in get_option('b_sanitize')).to_int(),
description : 'Whether nix has been built with UBSan enabled',
)
subdir('nix-meson-build-support/libatomic')
if host_machine.system() == 'windows'
@@ -104,7 +111,7 @@ cpuid = dependency(
version : '>= 0.7.0',
required : cpuid_required,
)
configdata.set('HAVE_LIBCPUID', cpuid.found().to_int())
configdata_priv.set('HAVE_LIBCPUID', cpuid.found().to_int())
deps_private += cpuid
nlohmann_json = dependency('nlohmann_json', version : '>= 3.9')
@@ -113,7 +120,7 @@ deps_public += nlohmann_json
cxx = meson.get_compiler('cpp')
config_priv_h = configure_file(
configuration : configdata,
configuration : configdata_priv,
output : 'util-config-private.hh',
)

View File

@@ -170,7 +170,7 @@ SourceAccessor::DirEntries PosixSourceAccessor::readDirectory(const CanonPath &
if (e.code() == std::errc::permission_denied || e.code() == std::errc::operation_not_permitted)
return std::nullopt;
else
throw;
throw SystemError(e.code(), "getting status of '%s'", PathFmt(entry.path()));
}
}();
res.emplace(entry.path().filename().string(), type);

View File

@@ -49,7 +49,8 @@ void readFull(int fd, char * buf, size_t count)
pollFD(fd, POLLIN);
continue;
}
throw SysError("reading from file");
auto savedErrno = errno;
throw SysError(savedErrno, "reading from file %s", PathFmt(descriptorToPath(fd)));
}
if (res == 0)
throw EndOfFile("unexpected end-of-file");
@@ -72,7 +73,8 @@ void writeFull(int fd, std::string_view s, bool allowInterrupts)
pollFD(fd, POLLOUT);
continue;
}
throw SysError("writing to file");
auto savedErrno = errno;
throw SysError(savedErrno, "writing to file %s", PathFmt(descriptorToPath(fd)));
}
if (res > 0)
s.remove_prefix(res);
@@ -95,8 +97,10 @@ std::string readLine(int fd, bool eofOk, char terminator)
pollFD(fd, POLLIN);
continue;
}
default:
throw SysError("reading a line");
default: {
auto savedErrno = errno;
throw SysError(savedErrno, "reading a line from %s", PathFmt(descriptorToPath(fd)));
}
}
} else if (rd == 0) {
if (eofOk)

View File

@@ -71,8 +71,10 @@ void unix::fchmodatTryNoFollow(Descriptor dirFd, const CanonPath & path, mode_t
if (res < 0) {
if (errno == ENOSYS)
fchmodat2Unsupported.test_and_set();
else
throw SysError("fchmodat2 '%s' relative to parent directory", path.rel());
else {
auto savedErrno = errno;
throw SysError(savedErrno, "fchmodat2 %s", PathFmt(descriptorToPath(dirFd) / path.rel()));
}
} else
return;
}
@@ -80,10 +82,13 @@ void unix::fchmodatTryNoFollow(Descriptor dirFd, const CanonPath & path, mode_t
#ifdef __linux__
AutoCloseFD pathFd = ::openat(dirFd, path.rel_c_str(), O_PATH | O_NOFOLLOW | O_CLOEXEC);
if (!pathFd)
if (!pathFd) {
auto savedErrno = errno;
throw SysError(
"opening '%s' relative to parent directory to get an O_PATH file descriptor (fchmodat2 is unsupported)",
path.rel());
savedErrno,
"opening %s to get an O_PATH file descriptor (fchmodat2 is unsupported)",
PathFmt(descriptorToPath(dirFd) / path.rel()));
}
struct ::stat st;
/* Possible since https://github.com/torvalds/linux/commit/55815f70147dcfa3ead5738fd56d3574e2e3c1c2 (3.6) */
@@ -91,7 +96,7 @@ void unix::fchmodatTryNoFollow(Descriptor dirFd, const CanonPath & path, mode_t
throw SysError("statting '%s' relative to parent directory via O_PATH file descriptor", path.rel());
if (S_ISLNK(st.st_mode))
throw SysError(EOPNOTSUPP, "can't change mode of symlink '%s' relative to parent directory", path.rel());
throw SysError(EOPNOTSUPP, "can't change mode of symlink %s", PathFmt(descriptorToPath(dirFd) / path.rel()));
static std::atomic_flag dontHaveProc{};
if (!dontHaveProc.test()) {
@@ -101,8 +106,11 @@ void unix::fchmodatTryNoFollow(Descriptor dirFd, const CanonPath & path, mode_t
if (int res = ::chmod(selfProcFdPath.c_str(), mode); res == -1) {
if (errno == ENOENT)
dontHaveProc.test_and_set();
else
throw SysError("chmod '%s' ('%s' relative to parent directory)", selfProcFdPath, path);
else {
auto savedErrno = errno;
throw SysError(
savedErrno, "chmod %s (%s)", selfProcFdPath, PathFmt(descriptorToPath(dirFd) / path.rel()));
}
} else
return;
}
@@ -122,8 +130,10 @@ void unix::fchmodatTryNoFollow(Descriptor dirFd, const CanonPath & path, mode_t
#endif
);
if (res == -1)
throw SysError("fchmodat '%s' relative to parent directory", path.rel());
if (res == -1) {
auto savedErrno = errno;
throw SysError(savedErrno, "fchmodat %s", PathFmt(descriptorToPath(dirFd) / path.rel()));
}
}
static Descriptor
@@ -206,9 +216,10 @@ OsString readLinkAt(Descriptor dirFd, const CanonPath & path)
checkInterrupt();
buf.resize(bufSize);
ssize_t rlSize = ::readlinkat(dirFd, path.rel_c_str(), buf.data(), bufSize);
if (rlSize == -1)
throw SysError("reading symbolic link '%1%' relative to parent directory", path.rel());
else if (rlSize < bufSize)
if (rlSize == -1) {
auto savedErrno = errno;
throw SysError(savedErrno, "reading symbolic link %1%", PathFmt(descriptorToPath(dirFd) / path.rel()));
} else if (rlSize < bufSize)
return {buf.data(), static_cast<std::size_t>(rlSize)};
}
}

View File

@@ -46,6 +46,31 @@ Descriptor openNewFileForWrite(const std::filesystem::path & path, mode_t mode,
return open(path.c_str(), flags, mode);
}
std::filesystem::path descriptorToPath(Descriptor fd)
{
if (fd == STDIN_FILENO)
return "<stdin>";
if (fd == STDOUT_FILENO)
return "<stdout>";
if (fd == STDERR_FILENO)
return "<stderr>";
#if defined(__linux__)
try {
return readLink("/proc/self/fd/" + std::to_string(fd));
} catch (SystemError &) {
}
#elif HAVE_F_GETPATH
/* F_GETPATH requires PATH_MAX buffer per POSIX */
char buf[PATH_MAX];
if (fcntl(fd, F_GETPATH, buf) != -1)
return buf;
#endif
/* Fallback for unknown fd or unsupported platform */
return "<fd " + std::to_string(fd) + ">";
}
std::filesystem::path defaultTempDir()
{
return getEnvNonEmpty("TMPDIR").value_or("/tmp");

View File

@@ -8,6 +8,12 @@ configdata_unix.set(
description : 'Optionally used for changing the files and symlinks.',
)
configdata_unix.set(
'HAVE_F_GETPATH',
cxx.has_header_symbol('fcntl.h', 'F_GETPATH').to_int(),
description : 'Optionally used for getting the path of a file descriptor (macOS).',
)
# Check for each of these functions, and create a define like `#define
# HAVE_CLOSE_RANGE 1`.
check_funcs_unix = [

View File

@@ -45,9 +45,9 @@ void writeFull(HANDLE handle, std::string_view s, bool allowInterrupts)
checkInterrupt();
DWORD res;
if (!WriteFile(handle, s.data(), s.size(), &res, NULL)) {
// Do this because `handleToPath` will overwrite the last error.
// Do this because `descriptorToPath` will overwrite the last error.
auto lastError = GetLastError();
auto path = handleToPath(handle);
auto path = descriptorToPath(handle);
throw WinError(lastError, "writing to file %d:%s", handle, PathFmt(path));
}
if (res > 0)

View File

@@ -156,13 +156,13 @@ OsString readSymlinkTarget(HANDLE linkHandle)
size_t path_buf_offset = offsetof(ReparseDataBuffer, SymbolicLinkReparseBuffer.PathBuffer[0]);
if (out < path_buf_offset) {
auto fullPath = handleToPath(linkHandle);
auto fullPath = descriptorToPath(linkHandle);
throw WinError(
DWORD{ERROR_REPARSE_TAG_INVALID}, "invalid reparse data for %d:%s", linkHandle, PathFmt(fullPath));
}
if (reparse->ReparseTag != IO_REPARSE_TAG_SYMLINK) {
auto fullPath = handleToPath(linkHandle);
auto fullPath = descriptorToPath(linkHandle);
throw WinError(DWORD{ERROR_REPARSE_TAG_INVALID}, "not a symlink: %d:%s", linkHandle, PathFmt(fullPath));
}
@@ -179,7 +179,7 @@ OsString readSymlinkTarget(HANDLE linkHandle)
}
if (path_buf_offset + name_offset + name_length > out) {
auto fullPath = handleToPath(linkHandle);
auto fullPath = descriptorToPath(linkHandle);
throw WinError(
DWORD{ERROR_REPARSE_TAG_INVALID}, "invalid symlink data for %d:%s", linkHandle, PathFmt(fullPath));
}

View File

@@ -83,7 +83,7 @@ void deletePath(const std::filesystem::path & path, uint64_t & bytesFreed)
deletePath(path);
}
std::filesystem::path windows::handleToPath(HANDLE handle)
std::filesystem::path descriptorToPath(Descriptor handle)
{
std::vector<wchar_t> buf(0x100);
DWORD dw = GetFinalPathNameByHandleW(handle, buf.data(), buf.size(), FILE_NAME_OPENED);

View File

@@ -43,8 +43,8 @@ void removeOldGenerations(std::filesystem::path dir)
std::string link;
try {
link = readLink(path);
} catch (std::filesystem::filesystem_error & e) {
if (e.code() == std::errc::no_such_file_or_directory)
} catch (SystemError & e) {
if (e.is(std::errc::no_such_file_or_directory))
continue;
throw;
}

View File

@@ -1419,7 +1419,6 @@ static int main_nix_env(int argc, char ** argv)
replaceSymlink(defaultChannelsDir(profilesDirOpts), nixExprPath / "channels");
if (!isRootUser())
replaceSymlink(rootChannelsDir(profilesDirOpts), nixExprPath / "channels_root");
} catch (std::filesystem::filesystem_error &) {
} catch (Error &) {
}
}

View File

@@ -32,6 +32,8 @@ writeSimpleFlake() {
baseName = builtins.baseNameOf ./.;
root = ./.;
number = 123;
};
}
EOF

View File

@@ -0,0 +1,26 @@
#!/usr/bin/env bash
source ./common.sh
createFlake1
mkdir -p "$flake1Dir/subflake"
cat > "$flake1Dir/subflake/flake.nix" <<EOF
{
outputs = { self }:
let
# Bad, legacy way of getting a flake from an input.
parentFlake = builtins.getFlake (builtins.flakeRefToString { type = "path"; path = self.sourceInfo.outPath; narHash = self.narHash; });
# Better way using a path value.
parentFlake2 = builtins.getFlake ./..;
in {
x = parentFlake.number;
y = parentFlake2.number;
};
}
EOF
git -C "$flake1Dir" add subflake/flake.nix
[[ $(nix eval "$flake1Dir/subflake#x") = 123 ]]
[[ $(nix eval "$flake1Dir/subflake#y") = 123 ]]

View File

@@ -34,6 +34,7 @@ suites += {
'source-paths.sh',
'old-lockfiles.sh',
'trace-ifd.sh',
'get-flake.sh',
],
'workdir' : meson.current_source_dir(),
}