Compare commits

..

58 Commits

Author SHA1 Message Date
Sergei Zimmerman
6dfeff04c1 local-binary-cache-store: Check that paths don't escape the binary cache directory
Previously arguments to getFile, upsertFile weren't checked
to be inside the root directory. It's not a very big issue since
substituters/stores are already a trusted component and can't be
specified without being a trusted user. Still, it's nice to validate
the necessary preconditions. Also changes the binaryCacheDir to be a
std::filesystem::path. Note the gotcha with absolute paths and operator/.
2026-02-24 19:53:09 +03:00
Sergei Zimmerman
3d9174cec5 libutil: Accept std::filesystem::path in readFile 2026-02-24 19:53:08 +03:00
John Ericson
5ce241cbfd Merge pull request #15307 from nix-windows/windows-local-shorthand
windows: add a separate local_shorthand_path test
2026-02-23 19:42:48 +00:00
John Ericson
3bfd64c3cd Merge pull request #15323 from obsidiansystems/fix-eio-readline
libutil: treat EIO as EOF in `readLine`
2026-02-23 19:30:19 +00:00
Brian McKenna
3f419cfa4e ParsedURL::path <-> std::filesystem::path, use in StoreReference
This missing URL functionality allow us to properly fix the path
shorthand for local store URLs test case.
2026-02-23 13:32:07 -05:00
Amaan Qureshi
994137dcf7 libutil: treat EIO as EOF in readLine
Reading from a pty master returns `EIO` once the slave side closes, however, `readLine` lets it propagate as an uncaught `SysError`, which causes spurious build failures in gvisor and similar sandboxed environments where pty teardown races differently. This commit catches `EIO` inside the read lambda and maps it to a zero-length read, reusing the existing EOF path.
2026-02-23 13:30:05 -05:00
Sergei Zimmerman
9242d74bc1 Merge pull request #15321 from obsidiansystems/writefull-interrupt-test
tests: add `writeFull` interrupt-handling regression test
2026-02-23 18:29:27 +00:00
Amaan Qureshi
6cddf03b5a tests: add writeFull interrupt-handling regression test
This commit verifies that `writeFull` with `allowInterrupts=false` completes
successfully when the interrupt flag is set. This prevents regressions
like the one fixed by #15255 where `write()` called `checkInterrupt()`
unconditionally.
2026-02-23 12:20:54 -05:00
Bernardo Meurer
afccf1d2d3 Merge pull request #15256 from NixOS/dependabot/github_actions/cachix/install-nix-action-31.9.1
build(deps): bump cachix/install-nix-action from 31.9.0 to 31.9.1
2026-02-23 14:09:16 +00:00
Bernardo Meurer
de16ef8be6 Merge pull request #15257 from NixOS/dependabot/github_actions/korthout/backport-action-4.1.0
build(deps): bump korthout/backport-action from 4.0.1 to 4.1.0
2026-02-23 14:08:51 +00:00
Sergei Zimmerman
7cd7930344 Merge pull request #15319 from xokdvium/fix-interrupts-write-full
libutil: Fix writeFull to respect allowInterrupts
2026-02-23 14:03:30 +00:00
Sergei Zimmerman
658c775f01 libutil: Fix writeFull to respect allowInterrupts
c0e849b696 broke interrupt handling since
writeFull started throwing Interrupted even when allowInterrupts was false.
This would lead to exceptions leaking out when they must not (for example during
progress bar shutdown).
2026-02-23 15:34:18 +03:00
Sergei Zimmerman
b1ad42e6d5 Merge pull request #15242 from obsidiansystems/fix-prefetch-segfault
libstore: guard against empty archive in unpack paths
2026-02-22 19:14:11 +00:00
John Ericson
761139f31c Merge pull request #15314 from obsidiansystems/fix-registry-symlink
registry: fix symlinked flake registry files broken by convert-end-exes
2026-02-21 20:06:31 +00:00
Amaan Qureshi
c6d93828bd registry: fix symlinked flake registry files broken by convert-end-exes
This commit reverts to using `getFSSourceAccessor()` so absolute symlink targets resolve correctly, since `makeFSSourceAccessor(path)` roots the accessor at `path` and can't follow symlinks that escape it.
2026-02-21 12:14:42 -05:00
Sergei Zimmerman
614072adcb Merge pull request #15286 from NixOS/failed-values-v2
Introduce a "failed" value type (v2)
2026-02-21 09:05:15 +00:00
John Ericson
42f6e9933d Merge pull request #15311 from obsidiansystems/convert-end-exes
libutil: return `std::filesystem::path` from XDG directory helpers
2026-02-21 04:22:26 +00:00
siddhantCodes
6808bfab92 libutil: return std::filesystem::path from XDG directory helpers
`getCacheDir`, `getConfigDir`, `getDataDir`, `getStateDir`, and related functions now return `std::filesystem::path` and use `getEnvOs` for native OS string handling, letting callsites replace string concatenation with `operator/` and drop the ad-hoc `namespace nix::fs` alias from six CLI files. `expandTilde` is fixed to strip both `~` and `/` before joining with `operator/` (an absolute right-hand operand silently replaces the left-hand side), and `ExecutablePath` gains `parseAppend` for incremental `PATH` construction.

Co-authored-by: Amaan Qureshi <git@amaanq.com>
2026-02-20 22:35:47 -05:00
John Ericson
92942071c7 Merge pull request #15310 from obsidiansystems/pathlocks-wine
`PathLocks` allow to fail on Wine
2026-02-21 03:32:18 +00:00
John Ericson
8899af09c1 Merge pull request #15308 from obsidiansystems/fix-ioport-init
Fix initialization of `ioport` in `Worker` on Windows
2026-02-21 02:48:26 +00:00
John Ericson
6363c1bf00 PathLocks allow to fail on Wine
It needs functionality Wine doesn't implement yet. So let's just have a
wine-only warn-and-keep-going.
2026-02-20 21:45:04 -05:00
John Ericson
5adb6a36b6 Fix initialization of ioport in Worker on Windows 2026-02-20 18:56:00 -05:00
John Ericson
b10cb6596e Merge pull request #15305 from obsidiansystems/file-system-prep
Centralize I/O error handling and make read/write functions portable
2026-02-20 17:31:21 +00:00
John Ericson
c0e849b696 Centralize I/O error handling and make read/write functions portable
- Improve existing `read` and `readOffset` wrappers:
  - Unix: Add `EINTR` retry handling and `checkInterrupt`
  - Windows: Handle `ERROR_BROKEN_PIPE` as EOF, add `checkInterrupt`

- Add `write` wrapper with same treatment (`EINTR` on Unix, `checkInterrupt`)

- Improve many `windows/file-descriptor.cc` error messages with
  `descriptorToPath`

- Move `readFull`, `readLine`, `writeFull` to common file, using the
  platform wrappers instead of duplicating platform-specific logic.
  These ones don't need any `EINTR` or interrupt checking either. They
  only have `EGAIN` checking as Unix-specific code, but this is a temp
  hack to be removed per #12688, so its fine that it goes in the
  platform-agnostic file for now.

- Add `retryOnBlock` helper to abstract `EAGAIN`/`EWOULDBLOCK` poll-and-retry
  logic (Unix only, needed for buildhook workaround #12688)

- Simplify `FdSource::readUnbuffered` to just call `nix::read`

- Remove now-dead `EINTR` handling from `drainFD` and `copyFdRange`

- `writeErr` better impl on Windows
2026-02-20 11:39:05 -05:00
John Ericson
2470b7981a Merge pull request #15277 from puffnfresh/windows/remove-profiles-symlink
LocalStore: stop creating outdated profiles symlink
2026-02-20 05:52:49 +00:00
John Ericson
5c1939e315 Merge pull request #15301 from obsidiansystems/file-system-prep
Misc file system fixes, especially for windows
2026-02-20 01:28:58 +00:00
Brian McKenna
9799023545 LocalStore: stop creating outdated profiles symlink
The gcroots/profiles link became outdated in
aeb810b01e when the GC code started
reading directly from /profiles, and gcroots/profiles was even
partially deleted in that commit.
2026-02-20 11:48:13 +11:00
John Ericson
ae4e229c9f Change writeFile(AutoCloseFD &, ...) to take a Descriptor
The new signature is:
  writeFile(Descriptor fd, std::string_view s, FsSync sync = FsSync::No, const Path * origPath = nullptr)

This uses `descriptorToPath` if `origPath` is not provided.
2026-02-19 19:30:55 -05:00
Amaan Qureshi
b63f5d1914 libutil: refactor AutoCloseFD::fsync into standalone syncDescriptor
`AutoCloseFD::fsync()` contained platform-specific logic behind CPP
guards. This extracts it into a free function `syncDescriptor(Descriptor)`
with separate Unix and Windows implementations, and makes
`AutoCloseFD::fsync()` an inline wrapper that delegates to it. The
Windows implementation uses `FlushFileBuffers` (returns `BOOL`, true on
success) while Unix uses `::fsync` (or `F_FULLFSYNC` on macOS), so
splitting them avoids conflating the two calling conventions.
2026-02-19 19:23:49 -05:00
John Ericson
31d87afc5a openFileEnsureBeneathNoSymlinks now returns AutoCloseFD
This indicates that they are returning an owned handle (i.e. the caller
 should --- and will, thanks to RAII --- close it).

 `ntOpenAt` and friends (internal) also use `AutoCloseFD` for the same
 reason.
2026-02-19 19:08:31 -05:00
John Ericson
204618c9d8 Misc unit test improvements 2026-02-19 17:26:38 -05:00
John Ericson
f0d90d3bdb Create two more FSSourceAccessorTests
Make sure we're testing non-directory roots.
2026-02-19 17:25:54 -05:00
John Ericson
0730dcb4a8 Skip chmodIfNeeded test on Windows
It doesn't do anything on Windows, it appears.
2026-02-19 17:21:46 -05:00
John Ericson
9b363e1e5c libutil-tests: Use FS_ROOT macros consistently in file-system tests
Add FS_ROOT_NO_TRAILING_SLASH macro and update isInDir and isDirOrInDir tests to use FS_ROOT and FS_SEP macros for cross-platform compatibility.
2026-02-19 17:21:12 -05:00
John Ericson
3df67a8347 Merge pull request #15298 from dramforever/parse-derivation-empty-special-msg
Add special error message for empty derivation file
2026-02-19 20:12:38 +00:00
Robert Hensing
dd4b73a44d Add rl-next/c-api-recoverable-errors 2026-02-19 14:23:05 +03:00
Sergei Zimmerman
fd4eee9d62 libexpr-tests: Add ValuePrintingTests.vFailed 2026-02-19 14:23:04 +03:00
Sergei Zimmerman
100e9a4436 Add tests/f/lang/eval-fail-memoised-error-trace-not-mutated.nix 2026-02-19 14:23:03 +03:00
Robert Hensing
17f344cdda libexpr: Add recoverable errors
This provides an explicit API for call-fail-retry-succeed evaluation
flows, such as currently used in NixOps4.

An alternative design would simply reset the `Value` to the original
thunk instead of `tFailed` under the condition of catching a
`RecoverableEvalError`.
That is somewhat simpler, but I believe the presence of `tFailed` is
beneficial for possible use in the repl; being able to show the error
sooner, without re-evaluation.

The hasPos method is required in order to avoid an include loop.
2026-02-19 14:23:02 +03:00
Eelco Dolstra
c33d9e31cc Introduce a "failed" value type
In the multithreaded evaluator, it's possible for multiple threads to
wait on the same thunk. If evaluation of the thunk results in an
exception, the waiting threads shouldn't try to re-force the thunk.
Instead, they should rethrow the same exception, without duplicating
any work.

Therefore, there is now a new value type `tFailed` that stores an
std::exception_ptr. If evaluation of a thunk/app results in an
exception, `forceValue()` overwrites the value with a `tFailed`. If
`forceValue()` encounters a `tFailed`, it rethrows the exception. So
you normally never need to check for failed values (since forcing them
causes a rethrow).

Co-authored-by: Robert Hensing <robert@roberthensing.nl>
2026-02-19 14:23:01 +03:00
Sergei Zimmerman
89158eedb5 treewide: Make exceptions cloneable
This is needed to make it possible to store exceptions in failed values
with each new rethrow getting a fresh copy of the exception object.
2026-02-19 14:22:58 +03:00
dramforever
c33c82f345 Add special error message for empty derivation file
This rather commonly occurs when the computer was uncleanly shut down
and fsync was not enabled. Show the user a more actionable message than
"expected string 'D'".
2026-02-19 17:34:57 +08:00
John Ericson
40abcebbe1 Merge pull request #15295 from NixOS/expr-op-update-dont-mutate-result
libexpr: Use temporary value in ExprOpUpdate::eval
2026-02-19 09:14:44 +00:00
John Ericson
3bf690a407 Merge pull request #15294 from NixOS/less-filesystem-to-string
libutil: Drop superflous .string() in pathExists and pathAccessible
2026-02-19 02:33:55 +00:00
Jörg Thalheim
360ff05e73 Merge pull request #15296 from Mic92/fix-nix-build
Reapply "Use the hash modulo in the derivation outputs"
2026-02-19 01:38:29 +00:00
Jörg Thalheim
100e7cc337 Reapply "Use the hash modulo in the derivation outputs"
This reverts commit 4f91e9599f.

This broke:

when I applied this pr, `--print-out-paths` wouldn't print anything:

```
shell-for-nix-env % nix build --print-out-paths .#legacyPackages.aarch64-darwin.homeConfigurations.macos.activationPackage
```

After dropping the patches from my fork, it does again.
```
shell-for-nix-env % nix build --print-out-paths .#legacyPackages.aarch64-darwin.homeConfigurations.macos.activationPackage
/nix/store/s8mlcalszdml0v8172w4hwqnx0m6477r-home-manager-generation
```
2026-02-19 01:50:17 +01:00
Sergei Zimmerman
247cc7013a libexpr: Use temporary value in ExprOpUpdate::eval 2026-02-19 02:49:10 +03:00
Sergei Zimmerman
9e865ae4ff libutil: Drop superflous .string() in pathExists and pathAccessible 2026-02-19 02:13:12 +03:00
Eelco Dolstra
fef83c9f9c Merge pull request #15287 from NixOS/narinfo-cache-meta-ttl
Add setting narinfo-cache-meta-ttl
2026-02-18 22:13:23 +00:00
Eelco Dolstra
8491e26cd4 Merge pull request #15291 from roberth/fix-evalstate-shared-from-this-usage
fix(EvalState): Use make_shared for enable_shared_from_this compatibi…
2026-02-18 22:06:03 +00:00
Eelco Dolstra
e5fa203d7f Add release note 2026-02-18 22:24:42 +01:00
Robert Hensing
987ecca24a fix(EvalState): Use make_shared for enable_shared_from_this compatibility
EvalState inherits from enable_shared_from_this (added in b4c24a29 for
debugRepl), but was being stack-allocated or created with make_unique
in several places. This causes bad_weak_ptr errors when shared_from_this
is called.

Convert all EvalState allocations to use make_shared.
2026-02-18 22:14:20 +01:00
Jörg Thalheim
a98b43b994 Merge pull request #12464 from obsidiansystems/build-trace-rework
Realisations use regular drv paths again
2026-02-18 20:54:56 +00:00
Eelco Dolstra
6733f2e5ce Add setting narinfo-cache-meta-ttl
This makes the current hard-coded 7-day `nix-cache-info` TTL
configurable, making `--offline` and `--refresh` do the right thing.
2026-02-18 21:30:37 +01:00
dependabot[bot]
6429f2fd6c build(deps): bump korthout/backport-action from 4.0.1 to 4.1.0
Bumps [korthout/backport-action](https://github.com/korthout/backport-action) from 4.0.1 to 4.1.0.
- [Release notes](https://github.com/korthout/backport-action/releases)
- [Commits](c656f5d585...01619ebc9a)

---
updated-dependencies:
- dependency-name: korthout/backport-action
  dependency-version: 4.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-16 22:11:56 +00:00
dependabot[bot]
1a2d73dc2b build(deps): bump cachix/install-nix-action from 31.9.0 to 31.9.1
Bumps [cachix/install-nix-action](https://github.com/cachix/install-nix-action) from 31.9.0 to 31.9.1.
- [Release notes](https://github.com/cachix/install-nix-action/releases)
- [Changelog](https://github.com/cachix/install-nix-action/blob/master/RELEASE.md)
- [Commits](4e002c8ec8...2126ae7fc5)

---
updated-dependencies:
- dependency-name: cachix/install-nix-action
  dependency-version: 31.9.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-16 22:11:52 +00:00
Amaan Qureshi
0e39aa2068 libstore: guard against empty archive in unpack paths
`DirectoryIterator` is dereferenced without an end check, which segfaults
when unpacking an empty or zero-sized archive. This commit adds an
emptiness check before the dereference in both `prefetchFile` and
`builtinUnpackChannel`, throwing a descriptive error instead.
Fixes #15116.
2026-02-15 10:16:54 -05:00
John Ericson
4f91e9599f Revert "Use the hash modulo in the derivation outputs"
Fix #11897

As described in the issue, this makes for a simpler and much more
intuitive notion of a realisation key. This is better for pedagogy, and
interoperability between more tools.

The way the issue was written was that we would switch to only having
shallow realisations first, and then do this. But going to only shallow
realisations is more complex change, and it turns out we weren't even
testing for the benefits that derivation hashes (modulo FODs) provided
in the deep realisation case, so I now just want to do this first.

Doing this gets the binary cache data structures in order, which will
unblock the Hydra fixed-output-derivation tracking work. I don't want to
delay that work while I figure out the changes needed for
shallow-realisations only.

This reverts commit bab1cda0e6.

Co-authored-by: Amaan Qureshi <git@amaanq.com>
2026-02-13 15:20:00 -05:00
129 changed files with 1378 additions and 570 deletions

View File

@@ -26,7 +26,7 @@ jobs:
# required to find all branches
fetch-depth: 0
- name: Create backport PRs
uses: korthout/backport-action@c656f5d5851037b2b38fb5db2691a03fa229e3b2 # v4.0.1
uses: korthout/backport-action@01619ebc9a6e3f6820274221b9956b3e7365000a # v4.1.0
id: backport
with:
# Config README: https://github.com/korthout/backport-action#backport-action

View File

@@ -174,7 +174,7 @@ jobs:
echo "installer-url=file://$GITHUB_WORKSPACE/out" >> "$GITHUB_OUTPUT"
TARBALL_PATH="$(find "$GITHUB_WORKSPACE/out" -name 'nix*.tar.xz' -print | head -n 1)"
echo "tarball-path=file://$TARBALL_PATH" >> "$GITHUB_OUTPUT"
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
if: ${{ !matrix.experimental-installer }}
with:
install_url: ${{ format('{0}/install', steps.installer-tarball-url.outputs.installer-url) }}
@@ -222,7 +222,7 @@ jobs:
id: installer-tarball-url
run: |
echo "installer-url=file://$GITHUB_WORKSPACE/out" >> "$GITHUB_OUTPUT"
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
with:
install_url: ${{ format('{0}/install', steps.installer-tarball-url.outputs.installer-url) }}
install_options: ${{ format('--tarball-url-prefix {0}', steps.installer-tarball-url.outputs.installer-url) }}

View File

@@ -0,0 +1,23 @@
---
synopsis: "C API: Errors returned from your primops are not treated as recoverable by default"
prs: [15286, 13930]
---
Nix 2.34 by default remembers the error in the thunk that triggered it.
Previously the following sequence of events worked:
1. Have a thunk that invokes a primop that's defined through the C API
2. The primop returns an error
3. Force the thunk again
4. The primop returns a value
5. The thunk evaluated successfully
**Resolution**
C API consumers that rely on this must change their recoverable error calls:
```diff
-nix_set_err_msg(context, NIX_ERR_*, msg);
+nix_set_err_msg(context, NIX_ERR_RECOVERABLE, msg);
```

View File

@@ -1,6 +0,0 @@
---
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

@@ -0,0 +1,6 @@
---
synopsis: New setting `narinfo-cache-meta-ttl`
prs: [15287]
---
The new setting `narinfo-cache-meta-ttl` controls how long binary cache metadata (i.e. `/nix-cache-info`) is cached locally, in seconds. This was previously hard-coded to 7 days, which is still the default. As a result, you can now use `nix store info --refresh` to check whether a binary cache is still valid.

View File

@@ -358,7 +358,6 @@ dockerTools.buildLayeredImageWithNixDb {
extraCommands = ''
rm -rf nix-support
ln -s /nix/var/nix/profiles nix/var/nix/gcroots/profiles
'';
fakeRootCommands = ''
chmod 1777 tmp

View File

@@ -185,7 +185,9 @@ EvalState * nix_eval_state_build(nix_c_context * context, nix_eval_state_builder
return EvalState{
.fetchSettings = std::move(builder->fetchSettings),
.settings = std::move(builder->settings),
.state = nix::EvalState(builder->lookupPath, builder->store, self->fetchSettings, self->settings),
.statePtr = std::make_shared<nix::EvalState>(
builder->lookupPath, builder->store, self->fetchSettings, self->settings),
.state = *self->statePtr,
};
});
}

View File

@@ -24,7 +24,8 @@ struct EvalState
{
nix::fetchers::Settings fetchSettings;
nix::EvalSettings settings;
nix::EvalState state;
std::shared_ptr<nix::EvalState> statePtr;
nix::EvalState & state;
};
struct BindingsBuilder

View File

@@ -1,4 +1,5 @@
#include "nix/expr/attr-set.hh"
#include "nix/expr/eval-error.hh"
#include "nix/util/configuration.hh"
#include "nix/expr/eval.hh"
#include "nix/store/globals.hh"
@@ -107,8 +108,13 @@ static void nix_c_primop_wrapper(
f(userdata, &ctx, (EvalState *) &state, external_args.data(), vTmpPtr);
if (ctx.last_err_code != NIX_OK) {
/* TODO: Throw different errors depending on the error code */
state.error<nix::EvalError>("Error from custom function: %s", *ctx.last_err).atPos(pos).debugThrow();
if (ctx.last_err_code == NIX_ERR_RECOVERABLE) {
state.error<nix::RecoverableEvalError>("Recoverable error from custom function: %s", *ctx.last_err)
.atPos(pos)
.debugThrow();
} else {
state.error<nix::EvalError>("Error from custom function: %s", *ctx.last_err).atPos(pos).debugThrow();
}
}
if (!vTmp.isValid()) {
@@ -194,6 +200,8 @@ ValueType nix_get_type(nix_c_context * context, const nix_value * value)
switch (v.type()) {
case nThunk:
return NIX_TYPE_THUNK;
case nFailed:
return NIX_TYPE_FAILED;
case nInt:
return NIX_TYPE_INT;
case nFloat:

View File

@@ -100,7 +100,8 @@ typedef enum {
/** @brief External value from C++ plugins or C API
* @see Externals
*/
NIX_TYPE_EXTERNAL
NIX_TYPE_EXTERNAL,
NIX_TYPE_FAILED,
} ValueType;
// forward declarations

View File

@@ -37,7 +37,8 @@ static void BM_EvalDynamicAttrs(benchmark::State & state)
EvalSettings evalSettings{readOnlyMode};
evalSettings.nixPath = {};
EvalState st({}, store, fetchSettings, evalSettings, nullptr);
auto stPtr = std::make_shared<EvalState>(LookupPath{}, store, fetchSettings, evalSettings, nullptr);
auto & st = *stPtr;
Expr * expr = st.parseExprFromString(exprStr, st.rootPath(CanonPath::root));
Value v;

View File

@@ -16,7 +16,8 @@ struct GetDerivationsEnv
fetchers::Settings fetchSettings{};
bool readOnlyMode = true;
EvalSettings evalSettings{readOnlyMode};
EvalState state;
std::shared_ptr<EvalState> statePtr;
EvalState & state;
Bindings * autoArgs = nullptr;
Value attrsValue;
@@ -27,7 +28,8 @@ struct GetDerivationsEnv
settings.nixPath = {};
return settings;
}())
, state({}, store, fetchSettings, evalSettings, nullptr)
, statePtr(std::make_shared<EvalState>(LookupPath{}, store, fetchSettings, evalSettings, nullptr))
, state(*statePtr)
{
autoArgs = state.buildBindings(0).finish();

View File

@@ -517,4 +517,106 @@ TEST_F(nix_api_expr_test, nix_expr_attrset_update)
assert_ctx_ok();
}
// The following is a test case for retryable thunks. This is a requirement
// for the current way in which NixOps4 evaluates its deployment expressions.
// An alternative strategy could be implemented, but unwinding the stack may
// be a more efficient way to deal with many suspensions/resumptions, compared
// to e.g. using a thread or coroutine stack for each suspended dependency.
// This test models the essential bits of a deployment tool that uses such
// a strategy.
// State for the retryable primop - simulates deployment resource availability
struct DeploymentResourceState
{
bool vm_created = false;
};
static void primop_load_resource_input(
void * user_data, nix_c_context * context, EvalState * state, nix_value ** args, nix_value * ret)
{
assert(context);
assert(state);
auto * resource_state = static_cast<DeploymentResourceState *>(user_data);
// Get the resource input name argument
std::string input_name;
if (nix_get_string(context, args[0], OBSERVE_STRING(input_name)) != NIX_OK)
return;
// Only handle "vm_id" input - throw for anything else
if (input_name != "vm_id") {
std::string error_msg = "unknown resource input: " + input_name;
nix_set_err_msg(context, NIX_ERR_NIX_ERROR, error_msg.c_str());
return;
}
if (resource_state->vm_created) {
// VM has been created, return the ID
nix_init_string(context, ret, "vm-12345");
} else {
// VM not created yet, fail with dependency error
nix_set_err_msg(context, NIX_ERR_RECOVERABLE, "VM not yet created");
}
}
TEST_F(nix_api_expr_test, nix_expr_thunk_re_evaluation_after_deployment)
{
// This test demonstrates NixOps4's requirement: a thunk calling a primop should be
// re-evaluable when deployment resources become available that were not available initially.
DeploymentResourceState resource_state;
PrimOp * primop = nix_alloc_primop(
ctx,
primop_load_resource_input,
1,
"loadResourceInput",
nullptr,
"load a deployment resource input",
&resource_state);
assert_ctx_ok();
nix_value * primopValue = nix_alloc_value(ctx, state);
assert_ctx_ok();
nix_init_primop(ctx, primopValue, primop);
assert_ctx_ok();
nix_value * inputName = nix_alloc_value(ctx, state);
assert_ctx_ok();
nix_init_string(ctx, inputName, "vm_id");
assert_ctx_ok();
// Create a single thunk by using nix_init_apply instead of nix_value_call
// This creates a lazy application that can be forced multiple times
nix_value * thunk = nix_alloc_value(ctx, state);
assert_ctx_ok();
nix_init_apply(ctx, thunk, primopValue, inputName);
assert_ctx_ok();
// First force: VM not created yet, should fail
nix_value_force(ctx, state, thunk);
ASSERT_EQ(NIX_ERR_NIX_ERROR, nix_err_code(ctx));
ASSERT_THAT(nix_err_msg(nullptr, ctx, nullptr), testing::HasSubstr("VM not yet created"));
// Clear the error context for the next attempt
nix_c_context_free(ctx);
ctx = nix_c_context_create();
// Simulate deployment process: VM gets created
resource_state.vm_created = true;
// Second force of the SAME thunk: this is where the "failed" value issue appears
// With failed value caching, this should fail because the thunk is marked as permanently failed
// Without failed value caching (or with retryable failures), this should succeed
nix_value_force(ctx, state, thunk);
// If we get here without error, the thunk was successfully re-evaluated
assert_ctx_ok();
std::string result;
nix_get_string(ctx, thunk, OBSERVE_STRING(result));
assert_ctx_ok();
ASSERT_STREQ("vm-12345", result.c_str());
}
} // namespace nixC

View File

@@ -27,7 +27,8 @@ static void BM_EvalManyBuiltinsMatchSameRegex(benchmark::State & state)
EvalSettings evalSettings{readOnlyMode};
evalSettings.nixPath = {};
EvalState st({}, store, fetchSettings, evalSettings, nullptr);
auto stPtr = std::make_shared<EvalState>(LookupPath{}, store, fetchSettings, evalSettings, nullptr);
auto & st = *stPtr;
Expr * expr = st.parseExprFromString(std::string(exprStr), st.rootPath(CanonPath::root));
Value v;

View File

@@ -188,6 +188,22 @@ TEST_F(ValuePrintingTests, vBlackhole)
test(vBlackhole, "«potential infinite recursion»");
}
TEST_F(ValuePrintingTests, vFailed)
{
Value v;
try {
throw Error("nope");
} catch (...) {
v.mkFailed(std::current_exception(), nullptr);
}
// Historically, a tried and then ignored value (e.g. through tryEval) was
// reverted to the original thunk.
test(v, "«thunk»");
test(v, ANSI_MAGENTA "«thunk»" ANSI_NORMAL, PrintOptions{.ansiColors = true});
}
TEST_F(ValuePrintingTests, depthAttrs)
{
Value vOne;

View File

@@ -11,7 +11,7 @@
namespace nix::eval_cache {
CachedEvalError::CachedEvalError(ref<AttrCursor> cursor, Symbol attr)
: EvalError(cursor->root->state, "cached failure of attribute '%s'", cursor->getAttrPathStr(attr))
: CloneableError(cursor->root->state, "cached failure of attribute '%s'", cursor->getAttrPathStr(attr))
, cursor(cursor)
, attr(attr)
{
@@ -70,7 +70,7 @@ struct AttrDb
{
auto state(_state->lock());
auto cacheDir = std::filesystem::path(getCacheDir()) / "eval-cache-v6";
auto cacheDir = getCacheDir() / "eval-cache-v6";
createDirs(cacheDir);
auto dbPath = cacheDir / (fingerprint.to_string(HashFormat::Base16, false) + ".sqlite");

View File

@@ -114,5 +114,6 @@ template class EvalErrorBuilder<InfiniteRecursionError>;
template class EvalErrorBuilder<StackOverflowError>;
template class EvalErrorBuilder<InvalidPathError>;
template class EvalErrorBuilder<IFDError>;
template class EvalErrorBuilder<RecoverableEvalError>;
} // namespace nix

View File

@@ -70,7 +70,7 @@ Strings EvalSettings::getDefaultNixPath()
}
};
add(std::filesystem::path{getNixDefExpr()} / "channels");
add(getNixDefExpr() / "channels");
auto profilesDirOpts = settings.getProfileDirsOptions();
add(rootChannelsDir(profilesDirOpts) / "nixpkgs", "nixpkgs");
add(rootChannelsDir(profilesDirOpts));

View File

@@ -1,4 +1,5 @@
#include "nix/expr/eval.hh"
#include "nix/expr/eval-error.hh"
#include "nix/expr/eval-settings.hh"
#include "nix/expr/primops.hh"
#include "nix/expr/print-options.hh"
@@ -31,6 +32,7 @@
#include <algorithm>
#include <cstddef>
#include <cstdlib>
#include <exception>
#include <iostream>
#include <sstream>
#include <cstring>
@@ -155,6 +157,8 @@ std::string_view showType(ValueType type, bool withArticle)
return WA("a", "float");
case nThunk:
return WA("a", "thunk");
case nFailed:
return WA("an", "error");
}
unreachable();
}
@@ -1987,11 +1991,15 @@ void ExprOpUpdate::eval(EvalState & state, Env & env, Value & v)
UpdateQueue q;
evalForUpdate(state, env, q);
v.mkAttrs(&Bindings::emptyBindings);
Value vTmp;
vTmp.mkAttrs(&Bindings::emptyBindings);
for (auto & rhs : std::views::reverse(q)) {
/* Remember that queue is sorted rightmost attrset first. */
eval(state, /*v=*/v, /*v1=*/v, /*v2=*/rhs);
eval(state, /*v=*/vTmp, /*v1=*/vTmp, /*v2=*/rhs);
}
v = vTmp;
}
void Expr::evalForUpdate(EvalState & state, Env & env, UpdateQueue & q, std::string_view errorCtx)
@@ -2174,6 +2182,54 @@ void ExprBlackHole::eval(EvalState & state, [[maybe_unused]] Env & env, Value &
// always force this to be separate, otherwise forceValue may inline it and take
// a massive perf hit
[[gnu::noinline]]
void EvalState::handleEvalExceptionForThunk(Env * env, Expr * expr, Value & v, const PosIdx pos)
{
if (!env)
tryFixupBlackHolePos(v, pos);
auto e = std::current_exception();
Value * recovery = nullptr;
try {
std::rethrow_exception(e);
} catch (const RecoverableEvalError & e) {
recovery = allocValue();
} catch (...) {
}
if (recovery) {
recovery->mkThunk(env, expr);
}
v.mkFailed(e, recovery);
}
[[gnu::noinline]]
void EvalState::handleEvalExceptionForApp(Value & v, const Value & savedApp)
{
auto e = std::current_exception();
Value * recovery = nullptr;
try {
std::rethrow_exception(e);
} catch (const RecoverableEvalError & e) {
recovery = allocValue();
} catch (...) {
}
if (recovery) {
*recovery = savedApp;
}
v.mkFailed(e, recovery);
}
[[gnu::noinline]]
void EvalState::handleEvalFailed(Value & v, const PosIdx pos)
{
assert(v.isFailed());
if (auto recoveryValue = v.failed().recoveryValue) {
v = *recoveryValue;
forceValue(v, pos);
} else {
v.failed().rethrow();
}
}
void EvalState::tryFixupBlackHolePos(Value & v, PosIdx pos)
{
if (!v.isBlackhole())
@@ -2182,7 +2238,8 @@ void EvalState::tryFixupBlackHolePos(Value & v, PosIdx pos)
try {
std::rethrow_exception(e);
} catch (InfiniteRecursionError & e) {
e.atPos(positions[pos]);
if (!e.hasPos())
e.atPos(positions[pos]);
} catch (...) {
}
}
@@ -2821,8 +2878,11 @@ void EvalState::assertEqValues(Value & v1, Value & v2, const PosIdx pos, std::st
}
return;
case nThunk: // Must not be left by forceValue
assert(false);
// Cannot be returned by forceValue().
case nThunk:
case nFailed:
unreachable();
default: // Note that we pass compiler flags that should make `default:` unreachable.
// Also note that this probably ran after `eqValues`, which implements
// the same logic more efficiently (without having to unwind stacks),
@@ -2916,8 +2976,11 @@ bool EvalState::eqValues(Value & v1, Value & v2, const PosIdx pos, std::string_v
// !!!
return v1.fpoint() == v2.fpoint();
case nThunk: // Must not be left by forceValue
assert(false);
// Cannot be returned by forceValue().
case nThunk:
case nFailed:
unreachable();
default: // Note that we pass compiler flags that should make `default:` unreachable.
error<EvalError>("eqValues: cannot compare %1% with %2%", showType(v1), showType(v2))
.withTrace(pos, errorCtx)

View File

@@ -14,7 +14,7 @@ namespace nix::eval_cache {
struct AttrDb;
class AttrCursor;
struct CachedEvalError : EvalError
struct CachedEvalError : CloneableError<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 Error
class EvalBaseError : public CloneableError<EvalBaseError, Error>
{
template<class T>
friend class EvalErrorBuilder;
@@ -26,14 +26,14 @@ public:
EvalState & state;
EvalBaseError(EvalState & state, ErrorInfo && errorInfo)
: Error(errorInfo)
: CloneableError(errorInfo)
, state(state)
{
}
template<typename... Args>
explicit EvalBaseError(EvalState & state, const std::string & formatString, const Args &... formatArgs)
: Error(formatString, formatArgs...)
: CloneableError(formatString, formatArgs...)
, state(state)
{
}
@@ -60,23 +60,31 @@ MakeError(InfiniteRecursionError, EvalError);
* Inherits from EvalBaseError (not EvalError) because resource exhaustion
* should not be cached.
*/
struct StackOverflowError : public EvalBaseError
struct StackOverflowError : public CloneableError<StackOverflowError, EvalBaseError>
{
StackOverflowError(EvalState & state)
: EvalBaseError(state, "stack overflow; max-call-depth exceeded")
: CloneableError(state, "stack overflow; max-call-depth exceeded")
{
}
};
MakeError(IFDError, EvalBaseError);
struct InvalidPathError : public EvalError
/**
* An evaluation error which should be retried instead of rethrown.
*
* A RecoverableEvalError is not an EvalError, because we shouldn't cache it in
* the eval cache, as it should be retried anyway.
*/
MakeError(RecoverableEvalError, EvalBaseError);
struct InvalidPathError : public CloneableError<InvalidPathError, EvalError>
{
public:
Path path;
InvalidPathError(EvalState & state, const Path & path)
: EvalError(state, "path '%s' is not valid", path)
: CloneableError(state, "path '%s' is not valid", path)
{
}
};

View File

@@ -33,6 +33,9 @@ using gc_allocator = std::allocator<T>;
struct gc
{};
struct gc_cleanup
{};
#endif
namespace nix {

View File

@@ -5,6 +5,7 @@
#include "nix/expr/eval.hh"
#include "nix/expr/eval-error.hh"
#include "nix/expr/eval-settings.hh"
#include <exception>
namespace nix {
@@ -91,18 +92,25 @@ void EvalState::forceValue(Value & v, const PosIdx pos)
Expr * expr = v.thunk().expr;
try {
v.mkBlackhole();
// checkInterrupt();
if (env) [[likely]]
expr->eval(*this, *env, v);
else
ExprBlackHole::throwInfiniteRecursionError(*this, v);
} catch (...) {
v.mkThunk(env, expr);
tryFixupBlackHolePos(v, pos);
handleEvalExceptionForThunk(env, expr, v, pos);
throw;
}
} else if (v.isApp())
callFunction(*v.app().left, *v.app().right, v, pos);
} else if (v.isApp()) {
Value savedApp = v;
try {
callFunction(*v.app().left, *v.app().right, v, pos);
} catch (...) {
handleEvalExceptionForApp(v, savedApp);
throw;
}
} else if (v.isFailed()) {
handleEvalFailed(v, pos);
}
}
[[gnu::always_inline]]

View File

@@ -652,8 +652,28 @@ public:
*/
inline void forceValue(Value & v, const PosIdx pos);
private:
/**
* Internal support function for forceValue
*
* This code is factored out so that it's not in the heavily inlined hot path.
*/
void handleEvalExceptionForThunk(Env * env, Expr * expr, Value & v, const PosIdx pos);
/**
* Internal support function for forceValue
*
* This code is factored out so that it's not in the heavily inlined hot path.
*/
void handleEvalExceptionForApp(Value & v, const Value & savedApp);
void handleEvalFailed(Value & v, PosIdx pos);
void tryFixupBlackHolePos(Value & v, PosIdx pos);
public:
/**
* Force a value, then recursively force list elements and
* attributes.

View File

@@ -7,6 +7,7 @@
#include <cstring>
#include <memory>
#include <memory_resource>
#include <exception>
#include <span>
#include <string_view>
#include <type_traits>
@@ -42,6 +43,7 @@ enum InternalType {
tBool,
tNull,
tFloat,
tFailed,
tExternal,
tPrimOp,
tAttrs,
@@ -68,6 +70,7 @@ enum InternalType {
*/
typedef enum {
nThunk,
nFailed,
nInt,
nFloat,
nBool,
@@ -424,6 +427,36 @@ struct ValueBase
size_t size;
Value * const * elems;
};
struct Failed : gc_cleanup
{
std::exception_ptr ex;
/**
* Optional value for recovering `RecoverableEvalError`
* Must be set iff `ex` is an instance of `RecoverableEvalError`.
*/
Value * recoveryValue;
Failed(std::exception_ptr ex, Value * recoveryValue)
: ex(ex)
, recoveryValue(recoveryValue)
{
}
[[noreturn]] void rethrow() const
{
try {
std::rethrow_exception(ex);
} catch (BaseError & e) {
/* Rethrow the copy of the exception - not the original one.
Stack tracing mechanisms rely on being able to modify the exceptions
they catch by reference. */
e.throwClone();
} catch (...) {
throw;
}
}
};
};
template<typename T>
@@ -450,6 +483,7 @@ struct PayloadTypeToInternalType
MACRO(PrimOp *, primOp, tPrimOp) \
MACRO(ValueBase::PrimOpApplicationThunk, primOpApp, tPrimOpApp) \
MACRO(ExternalValueBase *, external, tExternal) \
MACRO(ValueBase::Failed *, failed, tFailed) \
MACRO(NixFloat, fpoint, tFloat)
#define NIX_VALUE_PAYLOAD_TYPE(T, FIELD_NAME, DISCRIMINATOR) \
@@ -754,6 +788,11 @@ protected:
path.path = std::bit_cast<const StringData *>(payload[1]);
}
void getStorage(Failed *& failed) const noexcept
{
failed = std::bit_cast<Failed *>(payload[1]);
}
void setStorage(NixInt integer) noexcept
{
setSingleDWordPayload<tInt>(integer.value);
@@ -803,6 +842,11 @@ protected:
{
setUntaggablePayload<pdPath>(path.accessor, path.path);
}
void setStorage(Failed * failed) noexcept
{
setSingleDWordPayload<tFailed>(std::bit_cast<PackedPointer>(failed));
}
};
/**
@@ -1054,12 +1098,12 @@ public:
inline bool isThunk() const
{
return isa<tThunk>();
};
}
inline bool isApp() const
{
return isa<tApp>();
};
}
inline bool isBlackhole() const;
@@ -1067,17 +1111,22 @@ public:
inline bool isLambda() const
{
return isa<tLambda>();
};
}
inline bool isPrimOp() const
{
return isa<tPrimOp>();
};
}
inline bool isPrimOpApp() const
{
return isa<tPrimOpApp>();
};
}
inline bool isFailed() const
{
return isa<tFailed>();
}
/**
* Returns the normal type of a Value. This only returns nThunk if
@@ -1098,6 +1147,7 @@ public:
t[tBool] = nBool;
t[tNull] = nNull;
t[tFloat] = nFloat;
t[tFailed] = nFailed;
t[tExternal] = nExternal;
t[tAttrs] = nAttrs;
t[tPrimOp] = nFunction;
@@ -1235,6 +1285,11 @@ public:
setStorage(n);
}
inline void mkFailed(std::exception_ptr e, Value * recovery) noexcept
{
setStorage(new Value::Failed(e, recovery));
}
bool isList() const noexcept
{
return isa<tListSmall, tListN>();
@@ -1349,6 +1404,13 @@ public:
{
return getStorage<Path>().accessor;
}
Failed & failed() const noexcept
{
auto p = getStorage<Failed *>();
assert(p);
return *p;
}
};
extern ExprBlackHole eBlackHole;

View File

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

View File

@@ -562,6 +562,7 @@ static void prim_typeOf(EvalState & state, const PosIdx pos, Value ** args, Valu
v.mkStringNoCopy("float"_sds);
break;
case nThunk:
case nFailed:
unreachable();
}
}
@@ -1312,11 +1313,12 @@ 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");
{
BaseError msg(std::string{msgStr});
msg.atPos(state.positions[pos]);
auto info = msg.info();
info.level = lvlWarn;
info.isFromExpr = true;
ErrorInfo info{
.level = lvlWarn,
.msg = HintFmt(std::string(msgStr)),
.pos = state.positions[pos],
.isFromExpr = true,
};
logWarning(info);
}

View File

@@ -73,6 +73,11 @@ void printAmbiguous(EvalState & state, Value & v, std::ostream & str, std::set<c
str << "«potential infinite recursion»";
}
break;
case nFailed:
// Historically, a tried and then ignored value (e.g. through tryEval) was
// reverted to the original thunk.
str << "<CODE>";
break;
case nFunction:
if (v.isLambda()) {
str << "<LAMBDA>";

View File

@@ -509,6 +509,17 @@ private:
}
}
void printFailed()
{
if (options.ansiColors)
output << ANSI_MAGENTA;
// Historically, a tried and then ignored value (e.g. through tryEval) was
// reverted to the original thunk.
output << "«thunk»";
if (options.ansiColors)
output << ANSI_NORMAL;
}
void printExternal(Value & v)
{
v.external()->print(output);
@@ -590,6 +601,10 @@ private:
printFunction(v);
break;
case nFailed:
printFailed();
break;
case nThunk:
printThunk(v);
break;

View File

@@ -98,6 +98,7 @@ json printValueAsJSON(
break;
case nThunk:
case nFailed:
case nFunction:
state.error<TypeError>("cannot convert %1% to JSON", showType(v)).atPos(v.determinePos(pos)).debugThrow();
}

View File

@@ -171,7 +171,11 @@ static void printValueAsXML(
break;
case nThunk:
// Historically, a tried and then ignored value (e.g. through tryEval) was
// reverted to the original thunk.
case nFailed:
doc.writeEmptyElement("unevaluated");
break;
}
}

View File

@@ -44,8 +44,8 @@ struct CacheImpl : Cache
{
auto state(_state.lock());
auto dbPath = (getCacheDir() / "fetcher-cache-v4.sqlite").string();
createDirs(dirOf(dbPath));
auto dbPath = getCacheDir() / "fetcher-cache-v4.sqlite";
createDirs(dbPath.parent_path());
state->db = SQLite(dbPath, {.useWAL = nix::settings.useSQLiteWAL});
state->db.isCache();

View File

@@ -268,10 +268,10 @@ void Fetch::fetch(
return;
}
std::filesystem::path cacheDir = getCacheDir() / "git-lfs";
auto cacheDir = getCacheDir() / "git-lfs";
std::string key = hashString(HashAlgorithm::SHA256, pointerFilePath.rel()).to_string(HashFormat::Base16, false)
+ "/" + pointer->oid;
std::filesystem::path cachePath = cacheDir / key;
auto cachePath = cacheDir / key;
if (pathExists(cachePath)) {
debug("using cache entry %s -> %s", key, PathFmt(cachePath));
sink(readFile(cachePath));

View File

@@ -74,11 +74,11 @@ namespace nix {
struct GitSourceAccessor;
struct GitError : public Error
struct GitError final : public CloneableError<GitError, Error>
{
template<typename... Ts>
GitError(const git_error & error, Ts &&... args)
: Error("")
: CloneableError("")
{
auto hf = HintFmt(std::forward<Ts>(args)...);
err.msg = HintFmt("%1%: %2% (libgit2 error code = %3%)", Uncolored(hf.str()), error.message, error.klass);

View File

@@ -44,8 +44,9 @@ static bool isCacheFileWithinTtl(const Settings & settings, time_t now, const Po
std::filesystem::path getCachePath(std::string_view key, bool shallow)
{
return getCacheDir() / "gitv3"
/ (hashString(HashAlgorithm::SHA256, key).to_string(HashFormat::Nix32, false) + (shallow ? "-shallow" : ""));
auto name =
hashString(HashAlgorithm::SHA256, key).to_string(HashFormat::Nix32, false) + (shallow ? "-shallow" : "");
return getCacheDir() / "gitv3" / std::move(name);
}
// Returns the name of the HEAD branch.

View File

@@ -31,7 +31,7 @@ namespace nix::flake {
// setting name -> setting value -> allow or ignore.
typedef std::map<std::string, std::map<std::string, bool>> TrustedList;
std::filesystem::path trustedListPath()
static std::filesystem::path trustedListPath()
{
return getDataDir() / "trusted-settings.json";
}

View File

@@ -35,39 +35,35 @@ namespace nix::flake::primops {
PrimOp getFlake(const Settings & settings)
{
auto prim_getFlake = [&settings](EvalState & state, const PosIdx pos, Value ** args, Value & v) {
state.forceValue(*args[0], pos);
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]);
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);
}
callFlake(
state,
lockFlake(
settings,
state,
flakeRef,
LockFlags{
.updateLockFile = false,
.writeLockFile = false,
.useRegistries = !state.settings.pureEval && settings.useRegistries,
.allowUnlocked = !state.settings.pureEval,
}),
v);
};
return PrimOp{
.name = "__getFlake",
.args = {"args"},
.doc = R"(
Fetch a flake from a flake reference or a path, and return its output attributes and some metadata. For example:
Fetch a flake from a flake reference, and return its output attributes and some metadata. For example:
```nix
(builtins.getFlake "nix/55bc52401966fbffa525c574c14f67b00bc4fb3a").packages.x86_64-linux.nix

View File

@@ -416,8 +416,10 @@ static LockFile readLockFile(const fetchers::Settings & fetchSettings, const Sou
: LockFile();
}
LockedFlake lockFlake(
const Settings & settings, EvalState & state, const FlakeRef & topRef, const LockFlags & lockFlags, Flake flake)
/* 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)
{
experimentalFeatureSettings.require(Xp::Flakes);
@@ -425,6 +427,8 @@ LockedFlake lockFlake(
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();
@@ -904,22 +908,6 @@ LockedFlake lockFlake(
}
}
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,16 +214,9 @@ 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

@@ -73,7 +73,7 @@ std::function<void(siginfo_t * info, void * ctx)> stackOverflowHandler(defaultSt
void defaultStackOverflowHandler(siginfo_t * info, void * ctx)
{
char msg[] = "error: stack overflow (possible infinite recursion)\n";
[[gnu::unused]] auto res = write(2, msg, strlen(msg));
[[gnu::unused]] auto res = ::write(2, msg, strlen(msg));
_exit(1); // maybe abort instead?
}

View File

@@ -0,0 +1 @@
C:\foo\bar\baz?trusted=true

View File

@@ -13,10 +13,6 @@ using testing::Eq;
using testing::Field;
using testing::SizeIs;
namespace nix::fs {
using namespace std::filesystem;
}
using namespace nix;
TEST(machines, getMachinesWithEmptyBuilders)

View File

@@ -85,6 +85,20 @@ static StoreReference localExample_2{
},
};
#ifdef _WIN32
static StoreReference localExample_windows{
.variant =
StoreReference::Specified{
.scheme = "local",
.authority = "/C:/foo/bar/baz",
},
.params =
{
{"trusted", "true"},
},
};
#endif
static StoreReference localExample_3{
.variant =
StoreReference::Specified{
@@ -108,7 +122,11 @@ URI_TEST_READ(local_3_no_percent, localExample_3)
URI_TEST_READ(local_shorthand_1, localExample_1)
URI_TEST_READ(local_shorthand_2, localExample_2)
#ifndef _WIN32
URI_TEST_READ(local_shorthand_path_unix, localExample_2)
#else
URI_TEST_READ(local_shorthand_path_windows, localExample_windows)
#endif
URI_TEST(
local_shorthand_3,

View File

@@ -28,7 +28,7 @@
namespace nix {
AwsAuthError::AwsAuthError(int errorCode)
: Error("AWS authentication error: '%s' (%d)", aws_error_str(errorCode), errorCode)
: CloneableError("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/globals.hh"
#include "nix/store/worker-settings.hh"
namespace nix {
TimedOut::TimedOut(time_t maxDuration)
: BuildError(BuildResult::Failure::TimedOut, "timed out after %1% seconds", maxDuration)
: CloneableError(BuildResult::Failure::TimedOut, "timed out after %1% seconds", maxDuration)
, maxDuration(maxDuration)
{
}

View File

@@ -20,6 +20,9 @@ Worker::Worker(Store & store, Store & evalStore)
: act(*logger, actRealise)
, actDerivations(*logger, actBuilds)
, actSubstitutions(*logger, actCopyPaths)
#ifdef _WIN32
, ioport{CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0)}
#endif
, store(store)
, evalStore(evalStore)
, settings(nix::settings.getWorkerSettings())
@@ -27,6 +30,10 @@ Worker::Worker(Store & store, Store & evalStore)
return nix::settings.getWorkerSettings().useSubstitutes ? getDefaultSubstituters() : std::list<ref<Store>>{};
}}
{
#ifdef _WIN32
if (!ioport)
throw windows::WinError("CreateIoCompletionPort");
#endif
nrLocalBuilds = 0;
nrSubstitutions = 0;
lastWokenUp = steady_time_point::min();

View File

@@ -27,6 +27,8 @@ static void builtinUnpackChannel(const BuiltinBuilderContext & ctx)
size_t fileCount;
std::string fileName;
auto entries = DirectoryIterator{out};
if (entries == DirectoryIterator{})
throw Error("channel tarball '%s' is empty", src);
fileName = entries->path().string();
fileCount = std::distance(entries.begin(), entries.end());

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 : Error
struct curlMultiError final : CloneableError<curlMultiError, Error>
{
::CURLMcode code;
curlMultiError(::CURLMcode code)
: Error{"unexpected curl multi error: %s", ::curl_multi_strerror(code)}
: CloneableError{"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)
: Error(args...)
: CloneableError(args...)
, error(error)
, response(response)
{

View File

@@ -34,12 +34,13 @@ struct AwsCredentials
}
};
class AwsAuthError : public Error
class AwsAuthError final : public CloneableError<AwsAuthError, Error>
{
std::optional<int> errorCode;
public:
using Error::Error;
using CloneableError::CloneableError;
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 Error
struct BuildError : public CloneableError<BuildError, Error>
{
using Status = BuildResultFailureStatus;
using enum Status;
@@ -80,7 +80,7 @@ public:
*/
template<typename... Args>
BuildError(Status status, const Args &... args)
: Error(args...)
: CloneableError(args...)
, status{status}
{
}
@@ -97,7 +97,7 @@ public:
* Also used for deserialization.
*/
BuildError(Args args)
: Error(std::move(args.msg))
: CloneableError(std::move(args.msg))
, status{args.status}
, isNonDeterministic{args.isNonDeterministic}
@@ -108,7 +108,7 @@ public:
* Default constructor for deserialization.
*/
BuildError()
: Error("")
: CloneableError("")
{
}

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 : BuildError
struct BuilderFailureError final : CloneableError<BuilderFailureError, BuildError>
{
int builderStatus;
std::string extraMsgAfter;
BuilderFailureError(BuildResult::Failure::Status status, int builderStatus, std::string extraMsgAfter)
: BuildError{
: CloneableError{
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 : BuildError
struct TimedOut final : CloneableError<TimedOut, BuildError>
{
time_t maxDuration;

View File

@@ -22,7 +22,7 @@ struct Package
}
};
class BuildEnvFileConflictError : public Error
class BuildEnvFileConflictError final : public CloneableError<BuildEnvFileConflictError, Error>
{
public:
const Path fileA;
@@ -30,7 +30,7 @@ public:
int priority;
BuildEnvFileConflictError(const Path fileA, const Path fileB, int priority)
: Error(
: CloneableError(
"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 : public Error
class FileTransferError final : public CloneableError<FileTransferError, Error>
{
public:
FileTransfer::Error error;

View File

@@ -86,6 +86,17 @@ struct NarInfoDiskCacheSettings : public virtual Config
would prevent trying to pull the path again and failing with a hash
mismatch if the build isn't reproducible.
)"};
Setting<unsigned int> ttlMeta{
this,
7 * 24 * 3600,
"narinfo-cache-meta-ttl",
R"(
The TTL in seconds for caching binary cache metadata (i.e.
`/nix-cache-info`). This determines how long information about a
binary cache (such as its store directory, priority, and whether it
wants mass queries) is considered valid before being refreshed.
)"};
};
class Settings : public virtual Config,

View File

@@ -14,7 +14,7 @@ struct LocalBinaryCacheStoreConfig : std::enable_shared_from_this<LocalBinaryCac
*/
LocalBinaryCacheStoreConfig(std::string_view scheme, PathView binaryCacheDir, const Params & params);
Path binaryCacheDir;
std::filesystem::path binaryCacheDir;
static const std::string name()
{

View File

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

View File

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

View File

@@ -3,11 +3,32 @@
#include "nix/store/nar-info-disk-cache.hh"
#include "nix/util/signals.hh"
#include "nix/store/store-registration.hh"
#include "nix/util/url.hh"
#include <atomic>
namespace nix {
static std::filesystem::path checkBinaryCachePath(const std::filesystem::path & root, std::string_view path)
{
/* Note: these checks aren't complete and don't guard against symlink shenanigans. */
auto p = std::filesystem::path(path);
if (p.is_absolute())
/* Never happens unless the caller is messed up. */
throw Error("binary cache path '%s' must be relative", path);
auto result = (root / p).lexically_normal();
/* NB: lexically_normal() only does textual normalization and does
not resolve symlinks. This is acceptable because store/substituter
paths are already trusted, and this check is defense-in-depth
against ".." traversal. */
if (!isInDir(result, root.lexically_normal()))
throw Error("binary cache path '%s' escapes cache directory", path);
return result;
}
LocalBinaryCacheStoreConfig::LocalBinaryCacheStoreConfig(
std::string_view scheme, PathView binaryCacheDir, const StoreReference::Params & params)
: Store::Config{params}
@@ -29,7 +50,7 @@ StoreReference LocalBinaryCacheStoreConfig::getReference() const
.variant =
StoreReference::Specified{
.scheme = "file",
.authority = binaryCacheDir,
.authority = encodeUrlPath(pathToUrlPath(binaryCacheDir)),
},
};
}
@@ -56,7 +77,9 @@ protected:
void upsertFile(
const std::string & path, RestartableSource & source, const std::string & mimeType, uint64_t sizeHint) override
{
auto path2 = std::filesystem::path{config->binaryCacheDir} / path;
/* TODO: Maybe use RestoreSink for writing stuff? It would have to gain the ability to write files
atomically (maybe with O_TMPFILE + linkat + AT_EMPTY_PATH when available or fallback to rename). */
auto path2 = checkBinaryCachePath(config->binaryCacheDir, path);
static std::atomic<int> counter{0};
createDirs(path2.parent_path());
auto tmp = path2;
@@ -70,7 +93,7 @@ protected:
void getFile(const std::string & path, Sink & sink) override
{
try {
readFile(config->binaryCacheDir + "/" + path, sink);
readFile(checkBinaryCachePath(config->binaryCacheDir, path), sink);
} catch (SystemError & e) {
if (e.is(std::errc::no_such_file_or_directory))
throw NoSuchBinaryCacheFile("file '%s' does not exist in binary cache", path);
@@ -101,17 +124,17 @@ protected:
void LocalBinaryCacheStore::init()
{
createDirs(config->binaryCacheDir + "/nar");
createDirs(config->binaryCacheDir + "/" + realisationsPrefix);
createDirs(config->binaryCacheDir / "nar");
createDirs(config->binaryCacheDir / realisationsPrefix);
if (config->writeDebugInfo)
createDirs(config->binaryCacheDir + "/debuginfo");
createDirs(config->binaryCacheDir + "/log");
createDirs(config->binaryCacheDir / "debuginfo");
createDirs(config->binaryCacheDir / "log");
BinaryCacheStore::init();
}
bool LocalBinaryCacheStore::fileExists(const std::string & path)
{
return pathExists(config->binaryCacheDir + "/" + path);
return pathExists(checkBinaryCachePath(config->binaryCacheDir, path));
}
StringSet LocalBinaryCacheStoreConfig::uriSchemes()

View File

@@ -144,10 +144,7 @@ LocalStore::LocalStore(ref<const Config> config)
Path gcRootsDir = config->stateDir + "/gcroots";
const auto & localSettings = config->getLocalSettings();
const auto & gcSettings = localSettings.getGCSettings();
if (!pathExists(gcRootsDir)) {
createDirs(gcRootsDir);
replaceSymlink(profilesDir, gcRootsDir + "/profiles");
}
createDirs(gcRootsDir);
for (auto & perUserDir : {profilesDir + "/per-user", gcRootsDir + "/per-user"}) {
createDirs(perUserDir);

View File

@@ -63,9 +63,6 @@ struct NarInfoDiskCacheImpl : NarInfoDiskCache
/* How often to purge expired entries from the cache. */
const int purgeInterval = 24 * 3600;
/* How long to cache binary cache info (i.e. /nix-cache-info) */
const int cacheInfoTtl = 7 * 24 * 3600;
struct Cache
{
int id;
@@ -184,7 +181,7 @@ private:
{
auto i = state.caches.find(uri);
if (i == state.caches.end()) {
auto queryCache(state.queryCache.use()(uri)(time(0) - cacheInfoTtl));
auto queryCache(state.queryCache.use()(uri)(time(0) - settings.ttlMeta));
if (!queryCache.next())
return std::nullopt;
auto cache = Cache{

View File

@@ -293,8 +293,7 @@ std::string optimisticLockProfile(const std::filesystem::path & profile)
std::filesystem::path profilesDir(ProfileDirsOptions settings)
{
auto profileRoot =
isRootUser() ? rootProfilesDir(settings) : std::filesystem::path{createNixStateDir()} / "profiles";
auto profileRoot = isRootUser() ? rootProfilesDir(settings) : createNixStateDir() / "profiles";
createDirs(profileRoot);
return profileRoot;
}
@@ -306,9 +305,8 @@ std::filesystem::path rootProfilesDir(ProfileDirsOptions settings)
std::filesystem::path getDefaultProfile(ProfileDirsOptions settings)
{
std::filesystem::path profileLink = settings.useXDGBaseDirectories
? std::filesystem::path{createNixStateDir()} / "profile"
: std::filesystem::path{getHome()} / ".nix-profile";
std::filesystem::path profileLink =
settings.useXDGBaseDirectories ? createNixStateDir() / "profile" : getHome() / ".nix-profile";
try {
auto profile = profilesDir(settings) / "profile";
if (!pathExists(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)
: Error("")
: CloneableError("")
, 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 : public Error
class InvalidSSHAuthority final : public CloneableError<InvalidSSHAuthority, Error>
{
public:
InvalidSSHAuthority(const ParsedURL::Authority & authority, std::string_view reason)
: Error("invalid SSH authority: '%s': %s", authority.to_string(), reason)
: CloneableError("invalid SSH authority: '%s': %s", authority.to_string(), reason)
{
}
};
@@ -98,7 +98,7 @@ void SSHMaster::addCommonSSHOpts(Strings & args)
args.insert(args.end(), {"-i", keyFile});
if (!sshPublicHostKey.empty()) {
std::filesystem::path fileName = tmpDir->path() / "host-key";
writeFile(fileName.string(), authority.host + " " + sshPublicHostKey + "\n");
writeFile(fileName, authority.host + " " + sshPublicHostKey + "\n");
args.insert(args.end(), {"-oUserKnownHostsFile=" + fileName.string()});
}
if (compress)

View File

@@ -1190,8 +1190,14 @@ Derivation Store::derivationFromPath(const StorePath & drvPath)
static Derivation readDerivationCommon(Store & store, const StorePath & drvPath, bool requireValidPath)
{
auto accessor = store.requireStoreObjectAccessor(drvPath, requireValidPath);
auto contents = accessor->readFile(CanonPath::root);
try {
return parseDerivation(store, accessor->readFile(CanonPath::root), Derivation::nameFromPath(drvPath));
/* Special case for an empty file to show the user a better message */
if (contents.empty())
throw FormatError("file is empty (possible filesystem corruption)");
return parseDerivation(store, std::move(contents), Derivation::nameFromPath(drvPath));
} catch (FormatError & e) {
throw Error("error parsing derivation '%s': %s", store.printStorePath(drvPath), e.msg());
}

View File

@@ -1,4 +1,5 @@
#include "nix/util/error.hh"
#include "nix/util/file-path-impl.hh"
#include "nix/util/split.hh"
#include "nix/util/url.hh"
#include "nix/store/store-reference.hh"
@@ -17,7 +18,7 @@ static bool isNonUriPath(const std::string & spec)
spec.find("://") == std::string::npos
// Has at least one path separator, and so isn't a single word that
// might be special like "auto"
&& spec.find("/") != std::string::npos;
&& OsPathTrait<char>::findPathSep(spec) != std::string::npos;
}
std::string StoreReference::render(bool withParams) const
@@ -111,7 +112,7 @@ StoreReference StoreReference::parse(const std::string & uri, const StoreReferen
.variant =
Specified{
.scheme = "local",
.authority = absPath(baseURI),
.authority = encodeUrlPath(pathToUrlPath(absPath(std::filesystem::path{baseURI}))),
},
.params = std::move(params),
};

View File

@@ -42,17 +42,18 @@ ref<StoreConfig> resolveStoreConfig(StoreReference && storeURI)
/* If /nix doesn't exist, there is no daemon socket, and
we're not root, then automatically set up a chroot
store in ~/.local/share/nix/root. */
auto chrootStore = getDataDir() + "/root";
auto chrootStore = getDataDir() / "root";
if (!pathExists(chrootStore)) {
try {
createDirs(chrootStore);
} catch (SystemError & e) {
return make_ref<LocalStore::Config>(params);
}
warn("'%s' does not exist, so Nix will use '%s' as a chroot store", stateDir, chrootStore);
warn("%s does not exist, so Nix will use %s as a chroot store", stateDir, PathFmt(chrootStore));
} else
debug("'%s' does not exist, so Nix will use '%s' as a chroot store", stateDir, chrootStore);
return make_ref<LocalStore::Config>("local", chrootStore, params);
debug(
"%s does not exist, so Nix will use %s as a chroot store", stateDir, PathFmt(chrootStore));
return make_ref<LocalStore::Config>("local", chrootStore.string(), params);
}
#endif
else

View File

@@ -55,10 +55,10 @@
namespace nix {
struct NotDeterministic : BuildError
struct NotDeterministic final : CloneableError<NotDeterministic, BuildError>
{
NotDeterministic(auto &&... args)
: BuildError(BuildResult::Failure::NotDeterministic, args...)
: CloneableError(BuildResult::Failure::NotDeterministic, args...)
{
isNonDeterministic = true;
}
@@ -1269,7 +1269,7 @@ void DerivationBuilderImpl::writeBuilderFile(const std::string & name, std::stri
openat(tmpDirFd.get(), name.c_str(), O_WRONLY | O_TRUNC | O_CREAT | O_CLOEXEC | O_EXCL | O_NOFOLLOW, 0666)};
if (!fd)
throw SysError("creating file %s", PathFmt(path));
writeFile(fd, path, contents);
writeFile(fd.get(), contents);
chownToBuilder(fd.get(), path);
}

View File

@@ -1,7 +1,9 @@
#include "nix/util/file-system.hh"
#include "nix/util/logging.hh"
#include "nix/store/pathlocks.hh"
#include "nix/util/signals.hh"
#include "nix/util/util.hh"
#include "nix/util/windows-environment.hh"
#ifdef _WIN32
# include <errhandlingapi.h>
@@ -51,31 +53,43 @@ AutoCloseFD openLockFile(const std::filesystem::path & path, bool create)
return desc;
}
/**
* Throw a WinError, or if running under Wine, just warn and return true.
* Wine has incomplete file locking support, so we degrade gracefully.
*/
template<typename... Args>
static bool warnOrThrowWine(DWORD lastError, const std::string & fs, const Args &... args)
{
if (isWine()) {
warn(fs + ": %s (ignored under Wine)", args..., lastError);
return true;
}
throw WinError(lastError, fs, args...);
}
bool lockFile(Descriptor desc, LockType lockType, bool wait)
{
switch (lockType) {
case ltNone: {
OVERLAPPED ov = {0};
if (!UnlockFileEx(desc, 0, 2, 0, &ov)) {
WinError winError("Failed to unlock file desc %s", desc);
throw winError;
}
if (!UnlockFileEx(desc, 0, 2, 0, &ov))
return warnOrThrowWine(GetLastError(), "Failed to unlock file %s", PathFmt(descriptorToPath(desc)));
return true;
}
case ltRead: {
OVERLAPPED ov = {0};
if (!LockFileEx(desc, wait ? 0 : LOCKFILE_FAIL_IMMEDIATELY, 0, 1, 0, &ov)) {
WinError winError("Failed to lock file desc %s", desc);
if (winError.lastError == ERROR_LOCK_VIOLATION)
auto lastError = GetLastError();
if (lastError == ERROR_LOCK_VIOLATION)
return false;
throw winError;
return warnOrThrowWine(lastError, "Failed to lock file %s", PathFmt(descriptorToPath(desc)));
}
ov.Offset = 1;
if (!UnlockFileEx(desc, 0, 1, 0, &ov)) {
WinError winError("Failed to unlock file desc %s", desc);
if (winError.lastError != ERROR_NOT_LOCKED)
throw winError;
auto lastError = GetLastError();
if (lastError != ERROR_NOT_LOCKED)
return warnOrThrowWine(lastError, "Failed to unlock file %s", PathFmt(descriptorToPath(desc)));
}
return true;
}
@@ -83,17 +97,17 @@ bool lockFile(Descriptor desc, LockType lockType, bool wait)
OVERLAPPED ov = {0};
ov.Offset = 1;
if (!LockFileEx(desc, LOCKFILE_EXCLUSIVE_LOCK | (wait ? 0 : LOCKFILE_FAIL_IMMEDIATELY), 0, 1, 0, &ov)) {
WinError winError("Failed to lock file desc %s", desc);
if (winError.lastError == ERROR_LOCK_VIOLATION)
auto lastError = GetLastError();
if (lastError == ERROR_LOCK_VIOLATION)
return false;
throw winError;
return warnOrThrowWine(lastError, "Failed to lock file %s", PathFmt(descriptorToPath(desc)));
}
ov.Offset = 0;
if (!UnlockFileEx(desc, 0, 1, 0, &ov)) {
WinError winError("Failed to unlock file desc %s", desc);
if (winError.lastError != ERROR_NOT_LOCKED)
throw winError;
auto lastError = GetLastError();
if (lastError != ERROR_NOT_LOCKED)
return warnOrThrowWine(lastError, "Failed to unlock file %s", PathFmt(descriptorToPath(desc)));
}
return true;
}

View File

@@ -109,6 +109,13 @@ enum nix_err {
*/
NIX_ERR_NIX_ERROR = -4,
/**
* @brief A recoverable error occurred.
*
* This is used primarily by C API *consumers* to communicate that a failed
* primop call should be retried on the next evaluation attempt.
*/
NIX_ERR_RECOVERABLE = -5,
};
typedef enum nix_err nix_err;

View File

@@ -3,9 +3,15 @@
#include "nix/util/file-descriptor.hh"
#include "nix/util/serialise.hh"
#include "nix/util/signals.hh"
#include <cstring>
#ifndef _WIN32
# include <fcntl.h>
# include <stdlib.h>
#endif
namespace nix {
// BufferedSource with configurable small buffer for precise boundary testing.
@@ -113,6 +119,55 @@ TEST(ReadLine, LineWithNullBytes)
3));
}
#ifndef _WIN32
TEST(ReadLine, TreatsEioAsEof)
{
// Open a pty master. When the slave side is closed (or never opened),
// reading from the master returns EIO, which readLine should treat as EOF.
int master = posix_openpt(O_RDWR | O_NOCTTY);
ASSERT_NE(master, -1);
ASSERT_EQ(grantpt(master), 0);
ASSERT_EQ(unlockpt(master), 0);
// Open and immediately close the slave to trigger EIO on the master.
int slave = open(ptsname(master), O_RDWR | O_NOCTTY);
ASSERT_NE(slave, -1);
close(slave);
// With eofOk=true, readLine should return empty string (treating EIO as EOF).
EXPECT_EQ(readLine(master, /*eofOk=*/true), "");
// With eofOk=false, readLine should throw EndOfFile.
EXPECT_THROW(readLine(master), EndOfFile);
close(master);
}
// macOS (BSD) discards buffered pty data on slave close and returns normal
// EOF (0) instead of EIO, so partial data never reaches the master.
# ifdef __linux__
TEST(ReadLine, PartialLineBeforeEio)
{
int master = posix_openpt(O_RDWR | O_NOCTTY);
ASSERT_NE(master, -1);
ASSERT_EQ(grantpt(master), 0);
ASSERT_EQ(unlockpt(master), 0);
int slave = open(ptsname(master), O_RDWR | O_NOCTTY);
ASSERT_NE(slave, -1);
// Write a partial line (no terminator) from the slave, then close it.
ASSERT_EQ(::write(slave, "partial", 7), 7);
close(slave);
// readLine should return the partial data when eofOk=true.
EXPECT_EQ(readLine(master, /*eofOk=*/true), "partial");
close(master);
}
# endif
#endif
TEST(BufferedSourceReadLine, ReadsLinesFromPipe)
{
Pipe pipe;
@@ -243,4 +298,25 @@ TEST(BufferedSourceReadLine, BufferExhaustedThenEof)
EXPECT_EQ(source.readLine(/*eofOk=*/true), "");
}
TEST(WriteFull, RespectsAllowInterrupts)
{
Pipe pipe;
pipe.create();
setInterrupted(true);
// Must not throw Interrupted even though the interrupt flag is set.
EXPECT_NO_THROW(writeFull(pipe.writeSide.get(), "hello", /*allowInterrupts=*/false));
// Must throw Interrupted when allowInterrupts is true.
EXPECT_THROW(writeFull(pipe.writeSide.get(), "hello", /*allowInterrupts=*/true), Interrupted);
setInterrupted(false);
pipe.writeSide.close();
// Verify the data from the first write was actually written.
FdSource source(pipe.readSide.get());
EXPECT_EQ(source.readLine(/*eofOk=*/true), "hello");
}
} // namespace nix

View File

@@ -84,14 +84,18 @@ TEST(openFileEnsureBeneathNoSymlinks, works)
dirSink.createDirectory(CanonPath("d"));
dirSink.createSymlink(CanonPath("c"), "./d");
});
#ifdef _WIN32
EXPECT_THROW(sink.createDirectory(CanonPath("a/b/c/e")), SymlinkNotAllowed);
#else
// FIXME: This still follows symlinks on Unix (incorrectly succeeds)
sink.createDirectory(CanonPath("a/b/c/e"));
#endif
// Test that symlinks in intermediate path are detected during nested operations
ASSERT_THROW(
EXPECT_THROW(
sink.createDirectory(
CanonPath("a/b/c/f"), [](FileSystemObjectSink & dirSink, const CanonPath & relPath) {}),
SymlinkNotAllowed);
ASSERT_THROW(
EXPECT_THROW(
sink.createRegularFile(
CanonPath("a/b/c/regular"), [](CreateRegularFileSink & crf) { crf("some contents"); }),
SymlinkNotAllowed);
@@ -100,7 +104,7 @@ TEST(openFileEnsureBeneathNoSymlinks, works)
AutoCloseFD dirFd = openDirectory(tmpDir);
// Helper to open files with platform-specific arguments
auto openRead = [&](std::string_view path) -> Descriptor {
auto openRead = [&](std::string_view path) -> AutoCloseFD {
return openFileEnsureBeneathNoSymlinks(
dirFd.get(),
CanonPath(path),
@@ -114,7 +118,7 @@ TEST(openFileEnsureBeneathNoSymlinks, works)
);
};
auto openReadDir = [&](std::string_view path) -> Descriptor {
auto openReadDir = [&](std::string_view path) -> AutoCloseFD {
return openFileEnsureBeneathNoSymlinks(
dirFd.get(),
CanonPath(path),
@@ -128,7 +132,7 @@ TEST(openFileEnsureBeneathNoSymlinks, works)
);
};
auto openCreateExclusive = [&](std::string_view path) -> Descriptor {
auto openCreateExclusive = [&](std::string_view path) -> AutoCloseFD {
return openFileEnsureBeneathNoSymlinks(
dirFd.get(),
CanonPath(path),
@@ -154,19 +158,19 @@ TEST(openFileEnsureBeneathNoSymlinks, works)
#if !defined(_WIN32) && !defined(__CYGWIN__)
// This returns ELOOP on cygwin when O_NOFOLLOW is used
EXPECT_EQ(openCreateExclusive("a/broken_symlink"), INVALID_DESCRIPTOR);
EXPECT_FALSE(openCreateExclusive("a/broken_symlink"));
/* Sanity check, no symlink shenanigans and behaves the same as regular openat with O_EXCL | O_CREAT. */
EXPECT_EQ(errno, EEXIST);
#endif
EXPECT_THROW(openCreateExclusive("a/absolute_symlink/broken_symlink"), SymlinkNotAllowed);
// Test invalid paths
EXPECT_EQ(openRead("c/d/regular/a"), INVALID_DESCRIPTOR);
EXPECT_EQ(openReadDir("c/d/regular"), INVALID_DESCRIPTOR);
EXPECT_FALSE(openRead("c/d/regular/a"));
EXPECT_FALSE(openReadDir("c/d/regular"));
// Test valid paths work
EXPECT_TRUE(AutoCloseFD{openRead("c/d/regular")});
EXPECT_TRUE(AutoCloseFD{openCreateExclusive("a/regular")});
EXPECT_TRUE(openRead("c/d/regular"));
EXPECT_TRUE(openCreateExclusive("a/regular"));
}
} // namespace nix

View File

@@ -16,10 +16,12 @@ using namespace std::string_view_literals;
#ifdef _WIN32
# define FS_SEP L"\\"
# define FS_ROOT L"C:" FS_SEP // Need a mounted one, C drive is likely
# define FS_ROOT_NO_TRAILING_SLASH L"C:" // Need a mounted one, C drive is likely
# define FS_ROOT FS_ROOT_NO_TRAILING_SLASH FS_SEP
#else
# define FS_SEP "/"
# define FS_ROOT FS_SEP
# define FS_ROOT_NO_TRAILING_SLASH FS_SEP
# define FS_ROOT FS_ROOT_NO_TRAILING_SLASH
#endif
#ifndef PATH_MAX
@@ -44,7 +46,7 @@ TEST(absPath, doesntChangeRoot)
{
auto p = absPath(std::filesystem::path{FS_ROOT});
ASSERT_EQ(p, FS_ROOT);
ASSERT_EQ(p, FS_ROOT_NO_TRAILING_SLASH);
}
TEST(absPath, turnsEmptyPathIntoCWD)
@@ -196,42 +198,42 @@ TEST(baseNameOf, absoluteNothingSlashNothing)
TEST(isInDir, trivialCase)
{
EXPECT_TRUE(isInDir("/foo/bar", "/foo"));
EXPECT_TRUE(isInDir(FS_ROOT "foo" FS_SEP "bar", FS_ROOT "foo"));
}
TEST(isInDir, notInDir)
{
EXPECT_FALSE(isInDir("/zes/foo/bar", "/foo"));
EXPECT_FALSE(isInDir(FS_ROOT "zes" FS_SEP "foo" FS_SEP "bar", FS_ROOT "foo"));
}
TEST(isInDir, emptyDir)
{
EXPECT_FALSE(isInDir("/zes/foo/bar", ""));
EXPECT_FALSE(isInDir(FS_ROOT "zes" FS_SEP "foo" FS_SEP "bar", ""));
}
TEST(isInDir, hiddenSubdirectory)
{
EXPECT_TRUE(isInDir("/foo/.ssh", "/foo"));
EXPECT_TRUE(isInDir(FS_ROOT "foo" FS_SEP ".ssh", FS_ROOT "foo"));
}
TEST(isInDir, ellipsisEntry)
{
EXPECT_TRUE(isInDir("/foo/...", "/foo"));
EXPECT_TRUE(isInDir(FS_ROOT "foo" FS_SEP "...", FS_ROOT "foo"));
}
TEST(isInDir, sameDir)
{
EXPECT_FALSE(isInDir("/foo", "/foo"));
EXPECT_FALSE(isInDir(FS_ROOT "foo", FS_ROOT "foo"));
}
TEST(isInDir, sameDirDot)
{
EXPECT_FALSE(isInDir("/foo/.", "/foo"));
EXPECT_FALSE(isInDir(FS_ROOT "foo" FS_SEP ".", FS_ROOT "foo"));
}
TEST(isInDir, dotDotPrefix)
{
EXPECT_FALSE(isInDir("/foo/../bar", "/foo"));
EXPECT_FALSE(isInDir(FS_ROOT "foo" FS_SEP ".." FS_SEP "bar", FS_ROOT "foo"));
}
/* ----------------------------------------------------------------------------
@@ -240,8 +242,8 @@ TEST(isInDir, dotDotPrefix)
TEST(isDirOrInDir, trueForSameDirectory)
{
ASSERT_EQ(isDirOrInDir("/nix", "/nix"), true);
ASSERT_EQ(isDirOrInDir("/", "/"), true);
ASSERT_EQ(isDirOrInDir(FS_ROOT "nix", FS_ROOT "nix"), true);
ASSERT_EQ(isDirOrInDir(FS_ROOT, FS_ROOT), true);
}
TEST(isDirOrInDir, trueForEmptyPaths)
@@ -251,17 +253,17 @@ TEST(isDirOrInDir, trueForEmptyPaths)
TEST(isDirOrInDir, falseForDisjunctPaths)
{
ASSERT_EQ(isDirOrInDir("/foo", "/bar"), false);
ASSERT_EQ(isDirOrInDir(FS_ROOT "foo", FS_ROOT "bar"), false);
}
TEST(isDirOrInDir, relativePaths)
{
ASSERT_EQ(isDirOrInDir("/foo/..", "/foo"), false);
ASSERT_EQ(isDirOrInDir(FS_ROOT "foo" FS_SEP "..", FS_ROOT "foo"), false);
}
TEST(isDirOrInDir, relativePathsTwice)
{
ASSERT_EQ(isDirOrInDir("/foo/..", "/foo/."), false);
ASSERT_EQ(isDirOrInDir(FS_ROOT "foo" FS_SEP "..", FS_ROOT "foo" FS_SEP "."), false);
}
/* ----------------------------------------------------------------------------
@@ -294,13 +296,16 @@ TEST(makeParentCanonical, noParent)
TEST(makeParentCanonical, root)
{
ASSERT_EQ(makeParentCanonical("/"), "/");
ASSERT_EQ(makeParentCanonical(FS_ROOT), FS_ROOT_NO_TRAILING_SLASH);
}
/* ----------------------------------------------------------------------------
* chmodIfNeeded
* --------------------------------------------------------------------------*/
#ifndef _WIN32
// Windows doesn't support Unix-style permission bits - lstat always
// returns the same mode regardless of what chmod sets.
TEST(chmodIfNeeded, works)
{
auto [autoClose_, tmpFile] = nix::createTempFile();
@@ -316,6 +321,7 @@ TEST(chmodIfNeeded, works)
}
}
}
#endif
TEST(chmodIfNeeded, nonexistent)
{

View File

@@ -137,4 +137,34 @@ TEST_F(FSSourceAccessorTest, works)
}
}
/* ----------------------------------------------------------------------------
* RestoreSink non-directory at root (no dirFd)
* --------------------------------------------------------------------------*/
TEST_F(FSSourceAccessorTest, RestoreSinkRegularFileAtRoot)
{
auto filePath = tmpDir / "rootfile";
{
RestoreSink sink(false);
sink.dstPath = filePath;
// No dirFd set - this tests the !dirFd path
sink.createRegularFile(CanonPath::root, [](CreateRegularFileSink & crf) { crf("root content"); });
}
EXPECT_THAT(makeFSSourceAccessor(filePath), HasContents(CanonPath::root, "root content"));
}
TEST_F(FSSourceAccessorTest, RestoreSinkSymlinkAtRoot)
{
auto linkPath = tmpDir / "rootlink";
{
RestoreSink sink(false);
sink.dstPath = linkPath;
// No dirFd set - this tests the !dirFd path
sink.createSymlink(CanonPath::root, "symlink_target");
}
EXPECT_THAT(makeFSSourceAccessor(linkPath), HasSymlink(CanonPath::root, "symlink_target"));
}
} // namespace nix

View File

@@ -956,4 +956,108 @@ TEST(nix, isValidSchemeName)
ASSERT_FALSE(isValidSchemeName("http "));
}
/* ----------------------------------------------------------------------------
* pathToUrlPath / urlPathToPath
* --------------------------------------------------------------------------*/
struct UrlPathTestCase
{
std::string_view urlString;
ParsedURL urlParsed;
std::filesystem::path path;
std::string description;
};
class UrlPathTest : public ::testing::TestWithParam<UrlPathTestCase>
{};
TEST_P(UrlPathTest, pathToUrlPath)
{
const auto & testCase = GetParam();
auto urlPath = pathToUrlPath(testCase.path);
EXPECT_EQ(urlPath, testCase.urlParsed.path);
}
TEST_P(UrlPathTest, urlPathToPath)
{
const auto & testCase = GetParam();
auto path = urlPathToPath(testCase.urlParsed.path);
EXPECT_EQ(path, testCase.path);
}
TEST_P(UrlPathTest, urlToString)
{
const auto & testCase = GetParam();
EXPECT_EQ(testCase.urlParsed.to_string(), testCase.urlString);
}
TEST_P(UrlPathTest, stringToUrl)
{
const auto & testCase = GetParam();
auto parsed = parseURL(std::string{testCase.urlString});
EXPECT_EQ(parsed, testCase.urlParsed);
}
#ifndef _WIN32
INSTANTIATE_TEST_SUITE_P(
Unix,
UrlPathTest,
::testing::Values(
UrlPathTestCase{
.urlString = "file:///foo/bar/baz",
.urlParsed =
ParsedURL{
.scheme = "file",
.authority = ParsedURL::Authority{},
.path = {"", "foo", "bar", "baz"},
},
.path = "/foo/bar/baz",
.description = "absolute_path",
},
UrlPathTestCase{
.urlString = "file:///",
.urlParsed =
ParsedURL{
.scheme = "file",
.authority = ParsedURL::Authority{},
.path = {"", ""},
},
.path = "/",
.description = "root_path",
}),
[](const auto & info) { return info.param.description; });
#else // _WIN32
INSTANTIATE_TEST_SUITE_P(
Windows,
UrlPathTest,
::testing::Values(
UrlPathTestCase{
.urlString = "file:///C:/foo/bar/baz",
.urlParsed =
ParsedURL{
.scheme = "file",
.authority = ParsedURL::Authority{},
.path = {"", "C:", "foo", "bar", "baz"},
},
.path = L"C:\\foo\\bar\\baz",
.description = "absolute_path",
},
UrlPathTestCase{
.urlString = "file:///C:/",
.urlParsed =
ParsedURL{
.scheme = "file",
.authority = ParsedURL::Authority{},
.path = {"", "C:", ""},
},
.path = L"C:\\",
.description = "drive_root",
}),
[](const auto & info) { return info.param.description; });
#endif // _WIN32
} // namespace nix

View File

@@ -41,6 +41,11 @@ const std::string & BaseError::calcWhat() const
}
}
bool BaseError::hasPos() const
{
return err.pos.get() && *err.pos.get();
}
std::optional<std::string> ErrorInfo::programName = std::nullopt;
std::ostream & operator<<(std::ostream & os, const HintFmt & hf)
@@ -422,13 +427,20 @@ std::ostream & showErrorInfo(std::ostream & out, const ErrorInfo & einfo, bool s
*/
static void writeErr(std::string_view buf)
{
Descriptor fd = getStandardError();
while (!buf.empty()) {
auto n = write(STDERR_FILENO, buf.data(), buf.size());
#ifdef _WIN32
DWORD n;
if (!WriteFile(fd, buf.data(), buf.size(), &n, NULL))
abort();
#else
auto n = ::write(fd, buf.data(), buf.size());
if (n < 0) {
if (errno == EINTR)
continue;
abort();
}
#endif
buf = buf.substr(n);
}
}

View File

@@ -20,17 +20,23 @@ ExecutablePath ExecutablePath::load()
}
ExecutablePath ExecutablePath::parse(const OsString & path)
{
ExecutablePath ret;
ret.parseAppend(path);
return ret;
}
void ExecutablePath::parseAppend(const OsString & path)
{
auto strings = path.empty() ? (std::list<OsString>{})
: basicSplitString<std::list<OsString>, OsChar>(path, path_var_separator);
std::vector<std::filesystem::path> ret;
ret.reserve(strings.size());
directories.reserve(directories.size() + strings.size());
std::transform(
std::make_move_iterator(strings.begin()),
std::make_move_iterator(strings.end()),
std::back_inserter(ret),
std::back_inserter(directories),
[](OsString && str) {
return std::filesystem::path{
str.empty()
@@ -45,8 +51,6 @@ ExecutablePath ExecutablePath::parse(const OsString & path)
: std::move(str),
};
});
return {ret};
}
OsString ExecutablePath::render() const

View File

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

View File

@@ -8,10 +8,112 @@
#ifdef _WIN32
# include <winnt.h>
# include <fileapi.h>
#else
# include <poll.h>
#endif
namespace nix {
namespace {
enum class PollDirection { In, Out };
/**
* Retry an I/O operation if it fails with EAGAIN/EWOULDBLOCK.
*
* On Unix, polls the fd and retries. On Windows, just calls `f` once.
*
* This retry logic is needed to handle non-blocking reads/writes. This
* is needed in the buildhook, because somehow the json logger file
* descriptor ends up being non-blocking and breaks remote-building.
*
* @todo Get rid of buildhook and remove this logic again
* (https://github.com/NixOS/nix/issues/12688)
*/
template<typename F>
auto retryOnBlock([[maybe_unused]] Descriptor fd, [[maybe_unused]] PollDirection dir, F && f) -> decltype(f())
{
#ifndef _WIN32
while (true) {
try {
return std::forward<F>(f)();
} catch (SystemError & e) {
if (e.is(std::errc::resource_unavailable_try_again) || e.is(std::errc::operation_would_block)) {
struct pollfd pfd;
pfd.fd = fd;
pfd.events = dir == PollDirection::In ? POLLIN : POLLOUT;
if (poll(&pfd, 1, -1) == -1)
throw SysError("poll on file descriptor failed");
continue;
}
throw;
}
}
#else
return std::forward<F>(f)();
#endif
}
} // namespace
void readFull(Descriptor fd, char * buf, size_t count)
{
while (count) {
checkInterrupt();
auto res = retryOnBlock(
fd, PollDirection::In, [&]() { return read(fd, {reinterpret_cast<std::byte *>(buf), count}); });
if (res == 0)
throw EndOfFile("unexpected end-of-file");
count -= res;
buf += res;
}
}
std::string readLine(Descriptor fd, bool eofOk, char terminator)
{
std::string s;
while (1) {
checkInterrupt();
char ch;
// FIXME: inefficient
auto rd = retryOnBlock(fd, PollDirection::In, [&]() -> size_t {
try {
return read(fd, {reinterpret_cast<std::byte *>(&ch), 1});
} catch (SystemError & e) {
// On pty masters, EIO signals that the slave side closed,
// which is semantically EOF. Map it to a zero-length read
// so the existing EOF path handles it.
if (e.is(std::errc::io_error))
return 0;
throw;
}
});
if (rd == 0) {
if (eofOk)
return s;
else
throw EndOfFile("unexpected EOF reading a line");
} else {
if (ch == terminator)
return s;
s += ch;
}
}
}
void writeFull(Descriptor fd, std::string_view s, bool allowInterrupts)
{
while (!s.empty()) {
if (allowInterrupts)
checkInterrupt();
auto res = retryOnBlock(fd, PollDirection::Out, [&]() {
return write(fd, {reinterpret_cast<const std::byte *>(s.data()), s.size()}, allowInterrupts);
});
if (res > 0)
s.remove_prefix(res);
}
}
void writeLine(Descriptor fd, std::string s)
{
s += '\n';
@@ -67,8 +169,6 @@ void drainFD(Descriptor fd, Sink & sink, DrainFdSinkOpts opts)
&& (e.is(std::errc::resource_unavailable_try_again) || e.is(std::errc::operation_would_block)))
break;
#endif
if (e.is(std::errc::interrupted))
continue;
throw;
}
@@ -105,18 +205,8 @@ void copyFdRange(Descriptor fd, off_t offset, size_t nbytes, Sink & sink)
std::array<std::byte, 64 * 1024> buf;
while (left) {
checkInterrupt();
auto limit = std::min<size_t>(left, buf.size());
/* Should be initialized before we read, because the `catch`
block either throws or continues. */
size_t n;
try {
n = readOffset(fd, offset, std::span(buf.data(), limit));
} catch (SystemError & e) {
if (e.is(std::errc::interrupted))
continue;
throw;
}
auto n = readOffset(fd, offset, std::span(buf.data(), limit));
if (n == 0)
throw EndOfFile("unexpected end-of-file");
assert(n <= left);
@@ -184,24 +274,6 @@ void AutoCloseFD::close()
}
}
void AutoCloseFD::fsync() const
{
if (fd != INVALID_DESCRIPTOR) {
int result;
result =
#ifdef _WIN32
::FlushFileBuffers(fd)
#elif defined(__APPLE__)
::fcntl(fd, F_FULLFSYNC)
#else
::fsync(fd)
#endif
;
if (result == -1)
throw NativeSysError("fsync file descriptor %1%", fd);
}
}
void AutoCloseFD::startFsync() const
{
#ifdef __linux__

View File

@@ -21,7 +21,9 @@
#include <sys/time.h>
#include <unistd.h>
#include <boost/exception/diagnostic_information.hpp>
#include <boost/iostreams/device/mapped_file.hpp>
#include <boost/filesystem/path.hpp>
#ifdef __FreeBSD__
# include <sys/param.h>
@@ -262,13 +264,13 @@ std::optional<PosixStat> maybeLstat(const std::filesystem::path & path)
bool pathExists(const std::filesystem::path & path)
{
return maybeLstat(path.string()).has_value();
return maybeLstat(path).has_value();
}
bool pathAccessible(const std::filesystem::path & path)
{
try {
return pathExists(path.string());
return pathExists(path);
} catch (SystemError & e) {
// swallow EPERM
if (e.is(std::errc::operation_not_permitted))
@@ -305,25 +307,27 @@ std::string readFile(const std::filesystem::path & path)
return readFile(fd.get());
}
void readFile(const Path & path, Sink & sink, bool memory_map)
void readFile(const std::filesystem::path & path, Sink & sink, bool memory_map)
{
// Memory-map the file for faster processing where possible.
if (memory_map) {
try {
boost::iostreams::mapped_file_source mmap(path);
// mapped_file_source can't be constructed from std::filesystem::path with wide paths. Go
// through boost::filesystem::path.
boost::iostreams::mapped_file_source mmap(boost::filesystem::path{path.native()});
if (mmap.is_open()) {
sink({mmap.data(), mmap.size()});
return;
}
} catch (const boost::exception & e) {
debug("memory-mapping failed for path: %s: %s", PathFmt(path), boost::diagnostic_information(e));
}
debug("memory-mapping failed for path: %s", path);
}
// Stream the file instead if memory-mapping fails or is disabled.
AutoCloseFD fd = openFileReadonly(std::filesystem::path(path));
AutoCloseFD fd = openFileReadonly(path);
if (!fd)
throw NativeSysError("opening file %s", path);
throw NativeSysError("opening file %s", PathFmt(path));
drainFD(fd.get(), sink);
}
@@ -340,23 +344,23 @@ void writeFile(const Path & path, std::string_view s, mode_t mode, FsSync sync)
if (!fd)
throw SysError("opening file '%1%'", path);
writeFile(fd, path, s, mode, sync);
writeFile(fd.get(), s, sync, &path);
/* Close explicitly to propagate the exceptions. */
fd.close();
}
void writeFile(AutoCloseFD & fd, const Path & origPath, std::string_view s, mode_t mode, FsSync sync)
void writeFile(Descriptor fd, std::string_view s, FsSync sync, const Path * origPath)
{
assert(fd);
assert(fd != INVALID_DESCRIPTOR);
try {
writeFull(fd.get(), s);
writeFull(fd, s);
if (sync == FsSync::Yes)
fd.fsync();
syncDescriptor(fd);
} catch (Error & e) {
e.addTrace({}, "writing file '%1%'", origPath);
e.addTrace({}, "writing file '%1%'", origPath ? *origPath : descriptorToPath(fd).string());
throw;
}
}

View File

@@ -181,7 +181,7 @@ void RestoreSink::createRegularFile(const CanonPath & path, std::function<void(C
component are not followed. */
constexpr int flags = O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC;
if (!dirFd)
return ::open(p.c_str(), flags, 0666);
return AutoCloseFD{::open(p.c_str(), flags, 0666)};
return openFileEnsureBeneathNoSymlinks(dirFd.get(), path, flags, 0666);
}()
#endif

View File

@@ -191,6 +191,8 @@ public:
err.pos = pos;
}
bool hasPos() const;
void pushTrace(Trace trace)
{
err.traces.push_front(trace);
@@ -227,13 +229,31 @@ public:
{
return err;
};
[[noreturn]] virtual void throwClone() const = 0;
};
#define MakeError(newClass, superClass) \
class newClass : public superClass \
{ \
public: \
using superClass::superClass; \
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; \
}
MakeError(Error, BaseError);
@@ -245,7 +265,7 @@ 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 Error
class SystemError : public CloneableError<SystemError, Error>
{
std::error_code errorCode;
std::string errorDetails;
@@ -265,7 +285,7 @@ protected:
*/
template<typename... Args>
SystemError(Disambig, std::error_code errorCode, std::string_view errorDetails, Args &&... args)
: Error("")
: CloneableError("")
, errorCode(errorCode)
, errorDetails(errorDetails)
{
@@ -311,7 +331,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 : public SystemError
class SysError final : public CloneableError<SysError, SystemError>
{
public:
int errNo;
@@ -322,7 +342,7 @@ public:
*/
template<typename... Args>
SysError(int errNo, Args &&... args)
: SystemError(
: CloneableError(
Disambig{},
std::make_error_code(static_cast<std::errc>(errNo)),
strerror(errNo),
@@ -392,7 +412,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 SystemError
class WinError : public CloneableError<WinError, SystemError>
{
public:
DWORD lastError;
@@ -404,7 +424,7 @@ public:
*/
template<typename... Args>
WinError(DWORD lastError, Args &&... args)
: SystemError(
: CloneableError(
Disambig{},
std::error_code(lastError, std::system_category()),
renderError(lastError),

View File

@@ -33,6 +33,12 @@ struct ExecutablePath
*/
static ExecutablePath parse(const OsString & path);
/**
* Like `parse` but appends new entries to the end of an existing
* `ExecutablePath`.
*/
void parseAppend(const OsString & path);
/**
* Load the `PATH` environment variable and `parse` it.
*/

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 : public Error
class MissingExperimentalFeature final : public CloneableError<MissingExperimentalFeature, Error>
{
public:
/**

View File

@@ -65,7 +65,7 @@ std::string readFile(Descriptor fd);
* Platform-specific read into a buffer.
*
* Thin wrapper around ::read (Unix) or ReadFile (Windows).
* Does NOT handle EINTR on Unix - caller must catch and retry if needed.
* Handles EINTR on Unix. Treats ERROR_BROKEN_PIPE as EOF on Windows.
*
* @param fd The file descriptor to read from
* @param buffer The buffer to read into
@@ -74,6 +74,19 @@ std::string readFile(Descriptor fd);
*/
size_t read(Descriptor fd, std::span<std::byte> buffer);
/**
* Platform-specific write from a buffer.
*
* Thin wrapper around ::write (Unix) or WriteFile (Windows).
* Handles EINTR on Unix.
*
* @param fd The file descriptor to write to
* @param buffer The buffer to write from
* @return The number of bytes actually written
* @throws SystemError on failure
*/
size_t write(Descriptor fd, std::span<const std::byte> buffer, bool allowInterrupts);
/**
* Get the size of a file.
*
@@ -133,6 +146,11 @@ std::string readLine(Descriptor fd, bool eofOk = false, char terminator = '\n');
*/
void writeLine(Descriptor fd, std::string s);
/**
* Perform a blocking fsync operation on a file descriptor.
*/
void syncDescriptor(Descriptor fd);
/**
* Options for draining a file descriptor to a sink.
*/
@@ -253,7 +271,11 @@ public:
/**
* Perform a blocking fsync operation.
*/
void fsync() const;
void fsync() const
{
if (fd != INVALID_DESCRIPTOR)
nix::syncDescriptor(fd);
}
/**
* Asynchronously flush to disk without blocking, if available on

View File

@@ -54,7 +54,7 @@ OsString readLinkAt(Descriptor dirFd, const CanonPath & path);
* @throws SymlinkNotAllowed if any path components are symlinks
* @throws SystemError on other errors
*/
Descriptor openFileEnsureBeneathNoSymlinks(
AutoCloseFD openFileEnsureBeneathNoSymlinks(
Descriptor dirFd,
const CanonPath & path,
#ifdef _WIN32

View File

@@ -12,6 +12,7 @@
#include "nix/util/file-descriptor.hh"
#include "nix/util/file-path.hh"
#include <filesystem>
#include <sys/types.h>
#include <sys/stat.h>
#include <dirent.h>
@@ -243,7 +244,7 @@ Descriptor openNewFileForWrite(const std::filesystem::path & path, mode_t mode,
*/
std::string readFile(const Path & path);
std::string readFile(const std::filesystem::path & path);
void readFile(const Path & path, Sink & sink, bool memory_map = true);
void readFile(const std::filesystem::path & path, Sink & sink, bool memory_map = true);
enum struct FsSync { Yes, No };
@@ -266,8 +267,7 @@ writeFile(const std::filesystem::path & path, Source & source, mode_t mode = 066
return writeFile(path.string(), source, mode, sync);
}
void writeFile(
AutoCloseFD & fd, const Path & origPath, std::string_view s, mode_t mode = 0666, FsSync sync = FsSync::No);
void writeFile(Descriptor fd, std::string_view s, FsSync sync = FsSync::No, const Path * origPath = nullptr);
/**
* Flush a path's parent directory to disk.

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
#pragma once
///@file
#include <filesystem>
#include <ranges>
#include <span>
@@ -265,7 +266,13 @@ std::string percentEncode(std::string_view s, std::string_view keep = "");
* paths have no escape sequences --- file names cannot contain a
* `/`.
*/
Path renderUrlPathEnsureLegal(const std::vector<std::string> & urlPath);
Path renderUrlPathEnsureLegal(std::span<const std::string> urlPath);
/**
* Render URL path segments to a string by joining with `/`.
* Does not percent-encode the segments.
*/
std::string renderUrlPathNoPctEncoding(std::span<const std::string> urlPath);
/**
* Percent encode path. `%2F` for "interior slashes" is the most
@@ -347,6 +354,22 @@ ParsedURL fixGitURL(std::string url);
*/
bool isValidSchemeName(std::string_view scheme);
/**
* Convert a filesystem path to a URL path vector.
*
* On Windows, converts backslashes to forward slashes and prepends a `/`
* before the drive letter (e.g., `C:\foo\bar` becomes `/C:/foo/bar`).
*/
std::vector<std::string> pathToUrlPath(const std::filesystem::path & path);
/**
* Convert a URL path vector to a native filesystem path.
*
* On Windows, strips the leading `/` before the drive letter and converts
* to native format (e.g., `/C:/foo/bar` becomes `C:\foo\bar`).
*/
std::filesystem::path urlPathToPath(std::span<const std::string> urlPath);
/**
* Either a ParsedURL or a verbatim string. This is necessary because in certain cases URI must be passed
* verbatim (e.g. in builtin fetchers), since those are specified by the user.

View File

@@ -2,13 +2,12 @@
///@file
#include <filesystem>
#include "nix/util/types.hh"
#ifndef _WIN32
# include <sys/types.h>
#endif
#include "nix/util/types.hh"
namespace nix {
std::string getUserName();

View File

@@ -190,24 +190,7 @@ bool BufferedSource::hasData()
size_t FdSource::readUnbuffered(char * data, size_t len)
{
#ifdef _WIN32
DWORD n;
checkInterrupt();
if (!::ReadFile(fd, data, len, &n, NULL)) {
_good = false;
throw windows::WinError("ReadFile when FdSource::readUnbuffered");
}
#else
ssize_t n;
do {
checkInterrupt();
n = ::read(fd, data, len);
} while (n == -1 && errno == EINTR);
if (n == -1) {
_good = false;
throw SysError("reading from file");
}
#endif
auto n = nix::read(fd, {reinterpret_cast<std::byte *>(data), len});
if (n == 0) {
_good = false;
throw EndOfFile(std::string(*endOfFileError));

View File

@@ -5,7 +5,6 @@
#include <fcntl.h>
#include <unistd.h>
#include <poll.h>
#include <span>
#include "util-config-private.hh"
@@ -13,111 +12,19 @@
namespace nix {
namespace {
// This function is needed to handle non-blocking reads/writes. This is needed in the buildhook, because
// somehow the json logger file descriptor ends up being non-blocking and breaks remote-building.
// TODO: get rid of buildhook and remove this function again (https://github.com/NixOS/nix/issues/12688)
void pollFD(int fd, int events)
{
struct pollfd pfd;
pfd.fd = fd;
pfd.events = events;
int ret = poll(&pfd, 1, -1);
if (ret == -1) {
throw SysError("poll on file descriptor failed");
}
}
} // namespace
std::make_unsigned_t<off_t> getFileSize(Descriptor fd)
{
auto st = nix::fstat(fd);
return st.st_size;
}
void readFull(int fd, char * buf, size_t count)
{
while (count) {
checkInterrupt();
ssize_t res = ::read(fd, buf, count);
if (res == -1) {
switch (errno) {
case EINTR:
continue;
case EAGAIN:
pollFD(fd, POLLIN);
continue;
}
auto savedErrno = errno;
throw SysError(savedErrno, "reading from file %s", PathFmt(descriptorToPath(fd)));
}
if (res == 0)
throw EndOfFile("unexpected end-of-file");
count -= res;
buf += res;
}
}
void writeFull(int fd, std::string_view s, bool allowInterrupts)
{
while (!s.empty()) {
if (allowInterrupts)
checkInterrupt();
ssize_t res = write(fd, s.data(), s.size());
if (res == -1) {
switch (errno) {
case EINTR:
continue;
case EAGAIN:
pollFD(fd, POLLOUT);
continue;
}
auto savedErrno = errno;
throw SysError(savedErrno, "writing to file %s", PathFmt(descriptorToPath(fd)));
}
if (res > 0)
s.remove_prefix(res);
}
}
std::string readLine(int fd, bool eofOk, char terminator)
{
std::string s;
while (1) {
checkInterrupt();
char ch;
// FIXME: inefficient
ssize_t rd = ::read(fd, &ch, 1);
if (rd == -1) {
switch (errno) {
case EINTR:
continue;
case EAGAIN: {
pollFD(fd, POLLIN);
continue;
}
default: {
auto savedErrno = errno;
throw SysError(savedErrno, "reading a line from %s", PathFmt(descriptorToPath(fd)));
}
}
} else if (rd == 0) {
if (eofOk)
return s;
else
throw EndOfFile("unexpected EOF reading a line");
} else {
if (ch == terminator)
return s;
s += ch;
}
}
}
size_t read(Descriptor fd, std::span<std::byte> buffer)
{
ssize_t n = ::read(fd, buffer.data(), buffer.size());
ssize_t n;
do {
checkInterrupt();
n = ::read(fd, buffer.data(), buffer.size());
} while (n == -1 && errno == EINTR);
if (n == -1)
throw SysError("read of %1% bytes", buffer.size());
return static_cast<size_t>(n);
@@ -125,12 +32,29 @@ size_t read(Descriptor fd, std::span<std::byte> buffer)
size_t readOffset(Descriptor fd, off_t offset, std::span<std::byte> buffer)
{
ssize_t n = pread(fd, buffer.data(), buffer.size(), offset);
ssize_t n;
do {
checkInterrupt();
n = pread(fd, buffer.data(), buffer.size(), offset);
} while (n == -1 && errno == EINTR);
if (n == -1)
throw SysError("pread of %1% bytes at offset %2%", buffer.size(), offset);
return static_cast<size_t>(n);
}
size_t write(Descriptor fd, std::span<const std::byte> buffer, bool allowInterrupts)
{
ssize_t n;
do {
if (allowInterrupts)
checkInterrupt();
n = ::write(fd, buffer.data(), buffer.size());
} while (n == -1 && errno == EINTR);
if (n == -1)
throw SysError("write of %1% bytes", buffer.size());
return static_cast<size_t>(n);
}
//////////////////////////////////////////////////////////////////////
void Pipe::create()
@@ -207,4 +131,17 @@ void unix::closeOnExec(int fd)
throw SysError("setting close-on-exec flag");
}
void syncDescriptor(Descriptor fd)
{
int result =
#if defined(__APPLE__)
::fcntl(fd, F_FULLFSYNC)
#else
::fsync(fd)
#endif
;
if (result == -1)
throw NativeSysError("fsync file descriptor %1%", fd);
}
} // namespace nix

View File

@@ -136,7 +136,7 @@ void unix::fchmodatTryNoFollow(Descriptor dirFd, const CanonPath & path, mode_t
}
}
static Descriptor
static AutoCloseFD
openFileEnsureBeneathNoSymlinksIterative(Descriptor dirFd, const CanonPath & path, int flags, mode_t mode)
{
AutoCloseFD parentFd;
@@ -179,19 +179,19 @@ openFileEnsureBeneathNoSymlinksIterative(Descriptor dirFd, const CanonPath & pat
throw SymlinkNotAllowed(path2);
}
return INVALID_DESCRIPTOR;
return AutoCloseFD{};
}
parentFd = std::move(parentFd2);
}
auto res = ::openat(getParentFd(), std::string(path.baseName().value()).c_str(), flags | O_NOFOLLOW, mode);
if (res < 0 && errno == ELOOP)
AutoCloseFD res = ::openat(getParentFd(), std::string(path.baseName().value()).c_str(), flags | O_NOFOLLOW, mode);
if (!res && errno == ELOOP)
throw SymlinkNotAllowed(path);
return res;
}
Descriptor openFileEnsureBeneathNoSymlinks(Descriptor dirFd, const CanonPath & path, int flags, mode_t mode)
AutoCloseFD openFileEnsureBeneathNoSymlinks(Descriptor dirFd, const CanonPath & path, int flags, mode_t mode)
{
assert(!path.rel().starts_with('/')); /* Just in case the invariant is somehow broken. */
assert(!path.isRoot());
@@ -201,7 +201,7 @@ Descriptor openFileEnsureBeneathNoSymlinks(Descriptor dirFd, const CanonPath & p
if (maybeFd) {
if (*maybeFd < 0 && errno == ELOOP)
throw SymlinkNotAllowed(path);
return *maybeFd;
return AutoCloseFD{*maybeFd};
}
#endif
return openFileEnsureBeneathNoSymlinksIterative(dirFd, path, flags, mode);

View File

@@ -321,7 +321,7 @@ std::string encodeQuery(const StringMap & ss)
return res;
}
Path renderUrlPathEnsureLegal(const std::vector<std::string> & urlPath)
Path renderUrlPathEnsureLegal(std::span<const std::string> urlPath)
{
for (const auto & comp : urlPath) {
/* This is only really valid for UNIX. Windows has more restrictions. */
@@ -334,6 +334,11 @@ Path renderUrlPathEnsureLegal(const std::vector<std::string> & urlPath)
}
}
return renderUrlPathNoPctEncoding(urlPath);
}
std::string renderUrlPathNoPctEncoding(std::span<const std::string> urlPath)
{
return concatStringsSep("/", urlPath);
}
@@ -341,7 +346,7 @@ std::string ParsedURL::renderPath(bool encode) const
{
if (encode)
return encodeUrlPath(path);
return concatStringsSep("/", path);
return renderUrlPathNoPctEncoding(path);
}
std::string ParsedURL::renderAuthorityAndPath() const
@@ -452,6 +457,61 @@ bool isValidSchemeName(std::string_view s)
return std::regex_match(s.begin(), s.end(), regex, std::regex_constants::match_default);
}
std::vector<std::string> pathToUrlPath(const std::filesystem::path & path)
{
std::vector<std::string> urlPath;
// Prepend empty segment for absolute paths (those with a root directory)
if (path.has_root_directory())
urlPath.push_back("");
// Handle Windows drive letter (root_name like "C:")
if (path.has_root_name())
urlPath.push_back(path.root_name().generic_string());
// Iterate only over the relative path portion
for (const auto & component : path.relative_path())
urlPath.push_back(component.generic_string());
// Add trailing empty segment for paths ending with separator (including root-only paths)
if (path.filename().empty())
urlPath.push_back("");
return urlPath;
}
std::filesystem::path urlPathToPath(std::span<const std::string> urlPath)
{
std::filesystem::path result;
auto it = urlPath.begin();
// URL path must start with empty segment (representing absolute path "/")
if (it == urlPath.end() || !it->empty())
throw Error("only absolute URL paths can be converted to filesystem paths");
++it;
result = "/";
#ifdef _WIN32
// On Windows, check if next segment is a drive letter (e.g., "C:").
// If it isn't then this is something like a UNC path rather than a
// DOS path.
if (it != urlPath.end()) {
std::filesystem::path segment{*it};
if (segment.has_root_name()) {
segment /= "/";
result = std::move(segment);
++it;
}
}
#endif
// Append remaining segments
for (; it != urlPath.end(); ++it)
result /= *it;
return result;
}
std::ostream & operator<<(std::ostream & os, const VerbatimURL & url)
{
os << url.to_string();

View File

@@ -1,6 +1,7 @@
#include "nix/util/util.hh"
#include "nix/util/users.hh"
#include "nix/util/environment-variables.hh"
#include "nix/util/executable-path.hh"
#include "nix/util/file-system.hh"
#ifndef _WIN32
@@ -13,7 +14,7 @@ namespace nix {
std::filesystem::path getCacheDir()
{
auto dir = getEnv("NIX_CACHE_HOME");
auto dir = getEnvOs(OS_STR("NIX_CACHE_HOME"));
if (dir)
return *dir;
#ifndef _WIN32
@@ -25,7 +26,7 @@ std::filesystem::path getCacheDir()
std::filesystem::path getConfigDir()
{
auto dir = getEnv("NIX_CONFIG_HOME");
auto dir = getEnvOs(OS_STR("NIX_CONFIG_HOME"));
if (dir)
return *dir;
#ifndef _WIN32
@@ -51,7 +52,7 @@ std::vector<std::filesystem::path> getConfigDirs()
std::filesystem::path getDataDir()
{
auto dir = getEnv("NIX_DATA_HOME");
auto dir = getEnvOs(OS_STR("NIX_DATA_HOME"));
if (dir)
return *dir;
#ifndef _WIN32
@@ -63,7 +64,7 @@ std::filesystem::path getDataDir()
std::filesystem::path getStateDir()
{
auto dir = getEnv("NIX_STATE_HOME");
auto dir = getEnvOs(OS_STR("NIX_STATE_HOME"));
if (dir)
return *dir;
#ifndef _WIN32
@@ -84,9 +85,10 @@ std::string expandTilde(std::string_view path)
{
// TODO: expand ~user ?
auto tilde = path.substr(0, 2);
if (tilde == "~/" || tilde == "~")
return getHome().string() + std::string(path.substr(1));
else
if (tilde == "~/" || tilde == "~") {
auto suffix = path.size() >= 2 ? std::string(path.substr(2)) : std::string{};
return (getHome() / suffix).string();
} else
return std::string(path);
}

View File

@@ -19,82 +19,56 @@ using namespace nix::windows;
std::make_unsigned_t<off_t> getFileSize(Descriptor fd)
{
LARGE_INTEGER li;
if (!GetFileSizeEx(fd, &li))
throw WinError("GetFileSizeEx");
if (!GetFileSizeEx(fd, &li)) {
auto lastError = GetLastError();
throw WinError(lastError, "getting size of file %s", PathFmt(descriptorToPath(fd)));
}
return li.QuadPart;
}
void readFull(HANDLE handle, char * buf, size_t count)
{
while (count) {
checkInterrupt();
DWORD res;
if (!ReadFile(handle, (char *) buf, count, &res, NULL))
throw WinError("%s:%d reading from file", __FILE__, __LINE__);
if (res == 0)
throw EndOfFile("unexpected end-of-file");
count -= res;
buf += res;
}
}
void writeFull(HANDLE handle, std::string_view s, bool allowInterrupts)
{
while (!s.empty()) {
if (allowInterrupts)
checkInterrupt();
DWORD res;
if (!WriteFile(handle, s.data(), s.size(), &res, NULL)) {
// Do this because `descriptorToPath` will overwrite the last error.
auto lastError = GetLastError();
auto path = descriptorToPath(handle);
throw WinError(lastError, "writing to file %d:%s", handle, PathFmt(path));
}
if (res > 0)
s.remove_prefix(res);
}
}
std::string readLine(HANDLE handle, bool eofOk, char terminator)
{
std::string s;
while (1) {
checkInterrupt();
char ch;
// FIXME: inefficient
DWORD rd;
if (!ReadFile(handle, &ch, 1, &rd, NULL)) {
throw WinError("reading a line");
} else if (rd == 0) {
if (eofOk)
return s;
else
throw EndOfFile("unexpected EOF reading a line");
} else {
if (ch == terminator)
return s;
s += ch;
}
}
}
size_t read(Descriptor fd, std::span<std::byte> buffer)
{
checkInterrupt(); // For consistency with unix, and its EINTR loop
DWORD n;
if (!ReadFile(fd, buffer.data(), static_cast<DWORD>(buffer.size()), &n, NULL))
throw WinError("ReadFile of %1% bytes", buffer.size());
if (!ReadFile(fd, buffer.data(), static_cast<DWORD>(buffer.size()), &n, NULL)) {
auto lastError = GetLastError();
if (lastError == ERROR_BROKEN_PIPE)
n = 0; // Treat as EOF
else
throw WinError(lastError, "reading %1% bytes from %2%", buffer.size(), PathFmt(descriptorToPath(fd)));
}
return static_cast<size_t>(n);
}
size_t readOffset(Descriptor fd, off_t offset, std::span<std::byte> buffer)
{
checkInterrupt(); // For consistency with unix, and its EINTR loop
OVERLAPPED ov = {};
ov.Offset = static_cast<DWORD>(offset);
if constexpr (sizeof(offset) > 4) /* We don't build with 32 bit off_t, but let's be safe. */
ov.OffsetHigh = static_cast<DWORD>(offset >> 32);
DWORD n;
if (!ReadFile(fd, buffer.data(), static_cast<DWORD>(buffer.size()), &n, &ov))
throw WinError("ReadFile of %1% bytes at offset %2%", buffer.size(), offset);
if (!ReadFile(fd, buffer.data(), static_cast<DWORD>(buffer.size()), &n, &ov)) {
auto lastError = GetLastError();
throw WinError(
lastError,
"reading %1% bytes at offset %2% from %3%",
buffer.size(),
offset,
PathFmt(descriptorToPath(fd)));
}
return static_cast<size_t>(n);
}
size_t write(Descriptor fd, std::span<const std::byte> buffer, bool allowInterrupts)
{
if (allowInterrupts)
checkInterrupt(); // For consistency with unix
DWORD n;
if (!WriteFile(fd, buffer.data(), static_cast<DWORD>(buffer.size()), &n, NULL)) {
auto lastError = GetLastError();
throw WinError(lastError, "writing %1% bytes to %2%", buffer.size(), PathFmt(descriptorToPath(fd)));
}
return static_cast<size_t>(n);
}
@@ -148,4 +122,12 @@ off_t lseek(HANDLE h, off_t offset, int whence)
return newPos.QuadPart;
}
void syncDescriptor(Descriptor fd)
{
if (!::FlushFileBuffers(fd)) {
auto lastError = GetLastError();
throw WinError(lastError, "flushing file %s", PathFmt(descriptorToPath(fd)));
}
}
} // namespace nix

View File

@@ -29,7 +29,7 @@ namespace {
* @param createDisposition FILE_OPEN, FILE_CREATE, etc.
* @return Handle to the opened file/directory (caller must close)
*/
HANDLE ntOpenAt(
AutoCloseFD ntOpenAt(
Descriptor dirFd,
std::wstring_view pathComponent,
ACCESS_MASK desiredAccess,
@@ -73,7 +73,7 @@ HANDLE ntOpenAt(
throw WinError(
RtlNtStatusToDosError(status), "opening %s relative to directory handle", PathFmt(pathComponent));
return h;
return AutoCloseFD{h};
}
/**
@@ -81,9 +81,9 @@ HANDLE ntOpenAt(
*
* @param dirFd Directory handle to open relative to
* @param path Relative path to the symlink
* @return Handle to the symlink (caller must close)
* @return Handle to the symlink
*/
HANDLE openSymlinkAt(Descriptor dirFd, const CanonPath & path)
AutoCloseFD openSymlinkAt(Descriptor dirFd, const CanonPath & path)
{
assert(!path.isRoot());
assert(!path.rel().starts_with('/')); /* Just in case the invariant is somehow broken. */
@@ -210,7 +210,7 @@ bool isReparsePoint(HANDLE handle)
} // namespace windows
Descriptor openFileEnsureBeneathNoSymlinks(
AutoCloseFD openFileEnsureBeneathNoSymlinks(
Descriptor dirFd, const CanonPath & path, ACCESS_MASK desiredAccess, ULONG createOptions, ULONG createDisposition)
{
assert(!path.isRoot());
@@ -233,9 +233,8 @@ Descriptor openFileEnsureBeneathNoSymlinks(
/* Helper to check if a component is a symlink and throw SymlinkNotAllowed if so */
auto throwIfSymlink = [&](std::wstring_view component, const CanonPath & pathForError) {
try {
auto testFd =
auto testHandle =
ntOpenAt(getParentFd(), component, FILE_READ_ATTRIBUTES | SYNCHRONIZE, FILE_OPEN_REPARSE_POINT);
AutoCloseFD testHandle(testFd);
if (isReparsePoint(testHandle.get()))
throw SymlinkNotAllowed(pathForError);
} catch (SymlinkNotAllowed &) {
@@ -278,7 +277,7 @@ Descriptor openFileEnsureBeneathNoSymlinks(
/* Now open the final component with requested flags */
std::wstring finalComponent = string_to_os_string(std::string(path.baseName().value()));
HANDLE finalHandle;
AutoCloseFD finalHandle;
try {
finalHandle = ntOpenAt(
getParentFd(),
@@ -295,7 +294,7 @@ Descriptor openFileEnsureBeneathNoSymlinks(
}
/* Final check: did we accidentally open a symlink? */
if (isReparsePoint(finalHandle))
if (isReparsePoint(finalHandle.get()))
throw SymlinkNotAllowed(path);
return finalHandle;

View File

@@ -5,5 +5,6 @@ include_dirs += include_directories('../..')
headers += files(
'signals-impl.hh',
'windows-async-pipe.hh',
'windows-environment.hh',
'windows-known-folders.hh',
)

Some files were not shown because too many files have changed in this diff Show More