Compare commits

..
Author SHA1 Message Date
Raito Bezarius e3ac0d9c19 release: merge release 2.94.2 back to mainline
This merge commit returns to the previous state prior to the release but leaves the tag in the branch history.
Release created with releng/create_release.xsh

Change-Id: I01c4b3caeaf2c180d69b406b0dbd663ed1e4392d
2026-05-04 19:02:23 +02:00
Raito Bezarius c8a447db88 release: 2.94.2 "Açaí na tigela"
Release produced with releng/create_release.xsh

Change-Id: I7cfa601006515be2710bc01111c6adc34ee5c048
2026-05-04 19:02:22 +02:00
Raito Bezarius ec912eda76 release: release notes for 2.94.2
Release created with releng/create_release.xsh

Change-Id: I117f10a79a674d262446555206e4976bb330f491
2026-05-04 19:02:20 +02:00
eldritch horrorsandRaito Bezarius 667b40f1ff libutil: fix nar parser buffer overflow
string data shares a buffer with the binary string length field. size
calculations for string read buffers always include the length field;
sufficiently large length fields can cause these calculations to wrap.
a malicious nar could use this for OOB writes in the daemon (as root).

since we use strings only as tags for archive members and for symlinks
with their OS-dependent length limits we can simply limit string size.
1 MiB should be sufficient for all symlinks, and tags are always tiny.

Change-Id: I89fb05f73c1dbeda45d91244aba4cd526a3d83e1
2026-05-04 19:01:44 +02:00
Raito Bezarius 64599c010c releng/keys: update the way to receive the ephemeral key
I don't understand how `ssh -l lix-releng` is supposed to work if it
doesn't say which host to target.

Change-Id: I791f3f3f49ecd5884c9e86b5d3b617fc139e031f
Signed-off-by: Raito Bezarius <raito@lix.systems>
2026-05-04 19:01:44 +02:00
Raito Bezarius 1953c4ae4b releng/environment: update staging parameters
These parameters are now created on https://s3.afnix.fr.

Change-Id: I96b6fd913429ee46d04c412cb141edd288665ced
Signed-off-by: Raito Bezarius <raito@lix.systems>
2026-05-04 19:01:44 +02:00
YurekaandRaito Bezarius 7e56afa0b1 releng: Adapt for AFNix S3
Change-Id: I29dbd62dcc70595ba3f2ac2a466a5c26a28aea99
(cherry picked from commit 0c63036c7d)
2026-05-04 19:01:44 +02:00
Florian KlinkandRaito Bezarius 5d6134064e libcstore: Fix null deref in writeDebugInfo for non-directory NARs
When index-debug-info is enabled and the store path being copied is a
regular file (not a directory), std::get_if<nar_index::Directory>
returns nullptr since the NAR root is a File variant. The loop then
immediately dereferences buildIdDir->contents on the null pointer,
causing a segfault.

Add a null check at the top of the loop to break early when the NAR
root is not a directory.

Change-Id: I3a6e792b84cc12c837ecaddf4fee889e1bcb6397
(cherry picked from commit 6c7ccc2588)
2026-05-04 16:33:04 +00:00
sterni 1d36c544be libcmd: add support for lowdown >= 3.0.0
lowdown 3.0.0 merged some flags into one to save on bits and did not add
any aliases for backward compatibility.

As with the changes for lowdown >= 1.4, we define a preprocessor flag to
gate the changes on and add a job to CI to ensure that lowdown < 3.0
keeps working (which is used by NixOS 25.11).

The channel version this Lix branch uses doesn't have bmake with support
for all tested platforms, so we can't properly build lowdown 3.0 (which
depends on it) for tests in CI. Given this is a relatively simple
backport tested on other Lix branches, it's probably fine.

Change-Id: I20a3e2fdaa05906f032ff66911c42867557fdd11
(cherry picked from commit e839708839aa132c8694abfdf83409a619f72c5a)
2026-04-10 16:04:52 +02:00
Jade Lovelace 7b210490aa version.json: 2.94.2, if we do backport more stuff
Change-Id: Ide1ce2a59a80a524d62c77681c2ea45e859a77e2
2026-03-13 12:17:56 -07:00
Jade Lovelace fba0fa5767 release: merge release 2.94.1 back to mainline
This merge commit returns to the previous state prior to the release but leaves the tag in the branch history.
Release created with releng/create_release.xsh

Change-Id: I2c10392b0b80bff519501c8018ed51aa6a151ca1
2026-03-13 09:39:07 -07:00
Jade Lovelace 533429d89a release: 2.94.1 "Açaí na tigela"
Release produced with releng/create_release.xsh

Change-Id: Ie38032fe8dfebed83f9d24762ce48f73b23149b5
2026-03-13 09:39:06 -07:00
Jade Lovelace c135090468 release: release notes for 2.94.1
Release created with releng/create_release.xsh

Change-Id: I970e8f5422e3705917b04c09d28498fd7b849d27
2026-03-13 09:38:52 -07:00
Jade Lovelace 72f7965679 2.94.1: version
Change-Id: I92eeeebfd6b966ce77833785482db989962f8c9e
2026-03-13 09:38:44 -07:00
Raito Bezarius b7be40c785 libstore/build: fix starvation during substitution
When the destructor of PathSubstitutionGoal is run, this happens in a
sync context and can cause starvation of all ongoing IO w.r.t. to other
substitutions, including our own substitution.

While there's only a decompressor thread per stream, the other side of
the IO runs on the event loop.

In order to fix this, it is sufficient to remove the thread indirection
and inline the async code.

Fixes #1126. Great thanks to horrors' patience.

Co-authored-by: eldritch horrors <pennae@lix.systems>
Change-Id: I3eb37bc37d156f0f5528364e568fdaa2ced58011
Signed-off-by: Raito Bezarius <raito@lix.systems>
(cherry picked from commit 505d0669dc)
2026-02-11 15:39:06 +00:00
Qyriad 6350f51458 libutil: include LIX_MAJOR, LIX_MINOR, and LIX_PATCH macros
Backport of I7d8a4648890fce7ff15695876c9b9d3a6a6a6964 to 2.94 branch.

Change-Id: Ia6b8d89007974c8cf873fa1f536ce8416a6a6964
2026-01-15 11:48:26 +00:00
eldritch horrorsandRaito Bezarius c6f3f3a0d3 libexpr: fix app chain extension
during the value rewrite we accidentally broke extension of incomplete
primop application. this only shows up when binding on incomplete call
to a primop to a name, binding an incomplete call to *that* to another
name, and then finally calling the second binding with enough args for
a complete primop application. since this only shows up when calling a
primop with three or more args it took a while to surface. we have few
builtins that match this: foldl', replaceStrings, and substring. these
are not used incompletely in this manner very often, so this lingered.

fixes #1102

Change-Id: I218dffc14ae876efc86a86c7eb6c895e2405201c
2026-01-14 22:09:56 +00:00
Maximilian Bosch de4cfec46a libstore: fix reporting output cycles on drvs with references to other drvs
Closes #1064

The culprit here is that `genGraphString` is only invoked with the
store-paths associated with the outputs of the derivation, so when
filling `dependents`, the `graph_data.find(p)` call would return the end
of the iterator when doing this for references to other store-paths.

As a result, the code wrote information behind the graph data-structure
causing a corruption. For me, this resulted in a SIGSEGV most of the
time and in a few cases in an uncaught `map::at`-exception as reported
by Niklas.

This patch changes two aspects of the original implementation:

* When filling `dependents` in the node-set, use `map.at()` instead of
  `map.find()->second`. The latter doesn't make any sense and was the
  cause of corrupting memory. The `at` would've made it far easier to
  spot this in the first place.

* Filter out store-paths that don't belong to a different output of the
  derivation when creating `outputGraph`. This variable is used on two
  places, `genGraphString` and for topological sorting.

  The latter already filters out store-paths from a different drv, so
  this is happening now when creating the variable in the first place
  such that `genGraphString` never ends up with corrupt data in the
  first place. This is the actual bugfix.

Implemented a regression-test for this case to be sure.

Change-Id: Ie02144d89c32b0a776cb1ece0601d0229315ebc3
(cherry picked from commit 0a5f474a25)
2025-12-08 07:20:51 +01:00
Raito Bezarius 0873bed39d nix/upgrade-nix: disallow daemon connections for the store
Prior to I6a6a6964d2b5ad47ae5ea9eb11af9b6373ce2141 — `sudo nix
upgrade-nix` would perform direct store access.

This ensured a certain number of desireable properties for upgrading the
Lix binary itself.

We re-introduce direct store access for upgrading Lix binaries.

Fixes #1060.

Change-Id: I523c4d3023ed5fe9eff8fde9a266c56a0de47d8c
Signed-off-by: Raito Bezarius <raito@lix.systems>
(cherry picked from commit d2ca1810b1)
2025-12-06 22:20:18 +00:00
Zoe ZuserandRaito Bezarius 5dcb90548f meson: fix libstore.pc
typo of aws-cpp-sdk-transfer as aws-cpp-std-transfer prevents linking
against lix

Change-Id: Id847eab2601698696030d31fcd51288aa5f3d274
(cherry picked from commit 06f987fb0c)
2025-12-05 16:58:28 +00:00
Jade Lovelace 54118efaf6 release: merge release 2.94.0 back to mainline
This merge commit returns to the previous state prior to the release but leaves the tag in the branch history.
Release created with releng/create_release.xsh

Change-Id: Ida5959473ec469dd5e5e977ebdc8aab6e694b0de
2025-11-17 11:42:20 -08:00
Jade Lovelace fd38f625e6 release: 2.94.0 "Açaí na tigela"
Release produced with releng/create_release.xsh

Change-Id: If2ac30e569759c9035a6fc999c631ec545d190fa
2025-11-17 11:42:19 -08:00
Jade Lovelace 49a5495cbc release: release notes for 2.94.0
Release created with releng/create_release.xsh

Change-Id: Id7c776dc0ed46b1f6f09db2c77e04166469f5710
2025-11-17 11:42:12 -08:00
Jade Lovelace b6a8e312e7 2.94: release name
Change-Id: I18f5abe00130b53053e3a04074617071c0f884a2
2025-11-17 11:41:57 -08:00
WeetHet 7d46e9db7e libstore/build: extend DNS configuration access in network sandbox
https://github.com/NixOS/nixpkgs/pull/451579 PR
enabled c-aresSupport for curl on darwin which ended up breaking
DNS resolution in FOD sandboxes:

```
nix-run> exporting https://tangled.org/@weethet.bsky.social/nix-run (rev 73d7bf6b58848fb8f42e3a69816e0847f041c689) into /nix/store/m4m951648wmipxgwrgsml9gzjwfpfhm7-nix-run-73d7bf6
nix-run> Initialized empty Git repository in /nix/store/m4m951648wmipxgwrgsml9gzjwfpfhm7-nix-run-73d7bf6/.git/
nix-run> fatal: unable to access 'https://tangled.org/@weethet.bsky.social/nix-run/': Could not resolve host: tangled.org (Could not contact DNS servers)
nix-run> fatal: unable to access 'https://tangled.org/@weethet.bsky.social/nix-run/': Could not resolve host: tangled.org (Could not contact DNS servers)
nix-run> fatal: unable to access 'https://tangled.org/@weethet.bsky.social/nix-run/': Could not resolve host: tangled.org (Could not contact DNS servers)
nix-run> Unable to checkout 73d7bf6b58848fb8f42e3a69816e0847f041c689 from https://tangled.org/@weethet.bsky.social/nix-run.
```

with these sandbox failures:
```
deny mach-lookup com.apple.SystemConfiguration.DNSConfiguration
deny file-read-metadata /private/etc/hosts
deny file-read-data /private/etc/hosts
```

We allow those so that DNS resolution work fine

Change-Id: I9102293691972feb085adf8e9b1ad915bb3a36ab
2025-11-17 19:37:08 +00:00
Raito Bezarius 7e193f962e libstore/filetransfer: support HTTP/3 transfers if the user requests it
This adds the enablement code to support HTTP/3 if the user requests it.

We leave it disabled because h3 is not onpar with h2 performance.

Change-Id: I1fd3d4c97b972dcf36bccacc6c9a8290e22b31e0
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-11-17 19:11:32 +00:00
adam fb4cc15f39 chore: update nixos channel URLs
Channels have moved from `nixos.org/channels` to `cnannels.nixos.org`.
This udates all relevant links (excluding release notes) to use the new
canonical URLs and replaces HTTP with HTTPS.

Fixes #1031.

Change-Id: I212821c44ac5e482c8e9eaa415c7d8ee17ff8341
Signed-off-by: adam <me@adamperkowski.dev>
2025-11-15 18:21:47 +00:00
Alois Wohlschlager 243836ed0a libutil: remove the no-url-literals experimental feature
URL literals were deprecated in 278fddc317,
effectively stabilizing the experimental feature. Enabling it has no effect any
more. Remove it.

Change-Id: I8fbca03b1a2be6a8cddfce644043ac0a6a6a6964
2025-11-15 15:54:39 +01:00
Raito Bezariusandeldritch horrors 5fd9c65a8f doc/manual/known-issues: init
We start this section with shortcomings of unsandboxed builds.

Fixes #1018.

Co-authored-by: eldritch horrors <pennae@lix.systems>
Change-Id: Ieb17e4340beab0c1197951813ae602de453a3fd9
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-11-14 21:36:48 +01:00
EmilyandQyriad 82463616f9 libstore: skip the nested build directory on Darwin
This is unnecessary because Darwin builds already cannot create
`set{u,g}id` files due to the minimal sandbox policy, because we can
forbid messing with the top‐level build directory directly in the
sandbox rules, and because Darwin builds can trivially avoid isolation
through temporary directories right now anyway.

This does regress the build directory isolation for builds with
`sandbox = false`, but I can’t imagine that mattering given the
above. The sandbox change prepares us for a world where we close
off shared temporary directories for `sandbox = true` builds and try
harder to achieve proper isolation on Darwin, but probably doesn’t
have a meaningful security impact one way or another for now.

With this change, we get down to 41 byte build directory paths on
Darwin, a ~2–3½ byte improvement over the old status quo. We can
also restore the 0710 permissions on Linux.

Change-Id: I6a6a6964a681c0365241fe7234831db656b76799
2025-11-14 14:58:15 +01:00
EmilyandQyriad e87e547a36 libstore: default to /nix/var/nix/b for build-dir
The minimum build directory length on Darwin with default settings
when we were still using `/tmp` was 22 bytes. Deriving build directory
names from my local store, the median and mode were 43, the mean was
around 44½, and the maximum was 127.

The switch to `/nix/var/nix/builds` over `/tmp` added a 15 byte
penalty, and the additional `/b` directory added another 2.

Now that we use opaque build directory names, the length is 48, so
we’re still at a ~3½–5 byte penalty over the previous status
quo. This change brings us down to 43, matching the previous median.

Note that these calculations do not take into account the fact that
`/tmp` is a symbolic link to `/private/tmp` on Darwin. Anything
that was canonicalizing paths would have had an additional 8 byte
penalty in the previous status quo that is not applicable here,
so we may already be ahead even without this change. If the more
opaque directory name here is undesirable, then that factor could
potentially help us squeeze by without. Alternatively, in combination
with dropping the `/b` on Darwin we could use `/nix/var/nix/bld` or
similar, but I feel that the paths in general are sufficiently opaque
that it should be okay to go with the shorter option here. Given that
some projects already had to reduce filename lengths to avoid this
limit even before the recent changes, I think it is best to try and
improve on the previous status quo.

Note that `/nix/var/nix/builds` will unfortunately not be cleaned up
on Darwin. However, we don’t clean up the directories inside it on
Darwin anyway, so hopefully that’s okay for now?

Closes: #913
Change-Id: I6a6a6964bffce7194bcddcaefb4c4a37569c7df5
2025-11-14 14:58:15 +01:00
EmilyandQyriad 13a37f1246 libstore: don’t include derivation names in build directories
They have variable size, which is bad for #913.

Change-Id: I6a6a6964870e984c66277c7556ff3c2bc34ddca1
2025-11-14 14:58:15 +01:00
EmilyandQyriad 1102cc180a tests: remove obsolete code to create custom build directories
We now do in fact do this and decide this.

Change-Id: I6a6a696493b288ed76d809122bda259dc0225846
2025-11-14 14:58:15 +01:00
EmilyandQyriad ea0a2c8e74 libstore: make temporary path prefixes optional
This is not the same thing as passing an empty string, because it
avoids the `-` separator.

Change-Id: I6a6a696451667cbf500914e2dfbca2a4646ff20b
2025-11-14 14:58:15 +01:00
EmilyandQyriad c5221e508f libstore: simplify createTempDir interface
We always use the default temporary directory, because
`createUniqueDir` has an interface nice enough to use directly for
the few bespoke uses.

Change-Id: I6a6a696450b7c0a0bd76655632fb14d7c5e38199
2025-11-14 14:58:15 +01:00
EmilyandQyriad b1cfb58187 libutil: use makeTempPath in createTempSubdir
This makes the paths more nondeterministic, but more reliably unique,
and lets us remove the retry loop.

Note that this adds random entropy to the build directory visible
inside derivations on Darwin and unsandboxed Linux. It was already
non‐deterministic in the presence of concurrent builds and similar,
but now we can reliably expect it to be different every time. On the
whole I think that’s a good thing, as it is impossible to ensure
a single consistent build directory and derivation outputs should
not depend on it.

Package reproducibility isn’t great on Darwin to begin with,
though, and the reproducibility bugs this will turn up in packages
will be more urgent to fix than when the build directory was mostly
consistent. A quick survey of my local store shows that many C, C++,
and Rust binaries contain build directory references, likely due to
use of `__FILE__` and its equivalents; non‐binary offenders include:

* Install logs included in the Rust and Cargo bootstrap compilers
* Example errors in the Rust documentation referencing build paths
* Configuration information installed with CPython itself
* Python 2 metadata from resholve’s closure
* Cython metadata
* Generated headers in Facebook libraries referencing source paths
* Generated CMake files in Facebook libraries referencing source paths

I haven’t built that much in this store since the last GC, so this is
probably only a small sample of the problems across the tree. These are
all instances of <https://reproducible-builds.org/docs/build-path/>,
though, and should probably just be treated as general reproducibility
bugs outside of contexts like the Linux sandbox where we can normalize
them away entirely.

I have implemented away build directory paths for C/C++, applied some
additional fixes for non‐`__FILE__`‐related issues in binaries
from ATF and LLVM, and fixed the derivation bug causing the CPython
3 issue, and will work on upstreaming these changes. Rust is working
on the problem upstream, with some temporary workarounds we can
potentially apply in Nixpkgs for now. The rest will require some
distributed effort.

Change-Id: I6a6a69645b4915c56c0fdef904aa81684e4136c6
2025-11-14 14:58:15 +01:00
EmilyandQyriad 6e7212bad7 libstore: simplify fallback build directory logic
This does change the behaviour when the global temporary directory
does not exist, but other uses of the global temporary directory are
already broken in that circumstance, and it should be fixed centrally
if the use case is considered desirable. The logic was not present
before the recent churn around build directories – it was added now
that Lix is taking ownership of the build directory in the store –
so this should not be a meaningful regression.

Change-Id: I6a6a6964e345ea6803226c5ad759e836de7cb0ed
2025-11-14 14:58:15 +01:00
EmilyandQyriad d23bf515a7 libstore: use makeTemp{,Sibling}Path more
Change-Id: I6a6a6964c885be6dea0a69ee3162fbf4b812471f
2025-11-14 14:58:15 +01:00
EmilyandQyriad 0b2fcd1bcd libstore: simplify makeTemp{,Sibling}Path callers
There is now no risk of race conditions on a system with a functioning
entropy source, and the bespoke prefixes are either redundant to the
default or unnecessary.

Change-Id: I6a6a69641211c6bb979ea48ad30aecb1a53d03f0
2025-11-14 14:58:15 +01:00
EmilyandQyriad d03b1eca57 libutil: use OS‐provided entropy for temporary filenames
Relax the constraints on keeping the exact same filename format to
provide a more robust source of entropy with a simpler interface
(as previously suggested by eldritch horrors). Using 128 bits of
OS‐provided entropy ensures global uniqueness and allows us to
skip any thought of gracefully handling the case where these files
already exist.

My microbenchmark that repeatedly constructed paths like this and
printed them out showed that this takes about 1.23× the time of
the previous implementation, both taking on the order of a couple
microseconds for one iteration. Since everything that uses it is doing
things more expensive than printing to standard output, the actual
performance delta is likely to be lost in the noise. If it somehow
becomes a bottleneck, it can be optimized without sacrificing the
guarantees by reading from the system RNG only to seed a thread‐local
CSPRNG like [ChaCha8Rand], but I think that’s very unlikely.

We also tweak the recommended way of creating a temporary file inside
a directory in anticipation of later changes, and rename the `suffix`
parameter to `prefix` (it’s a prefix to the random characters and
a suffix to the root, but this way is more consistent).

[ChaCha8Rand]: https://c2sp.org/chacha8rand

Change-Id: I6a6a69648502c746d13d8c3bd2768cbbf1b90466
2025-11-14 14:58:15 +01:00
EmilyandQyriad 9f3ba30783 libutil: extract Base32 helpers from Hash
base32Encode now takes std::span<std::byte>, with a base32EncodeStr
convenience wrapper which takes std::string_view.

Co-authored-by: Qyriad <qyriad@qyriad.me>

Change-Id: I6a6a6964f799dc84ecbfb55c7ca03a064cff71d9
2025-11-14 14:58:15 +01:00
eldritch horrors 2fa40c9de4 libstore: bring back some pointer equality nonsense
the change to shareable thunks also removed a few cases of pointer
equality checks that allowed structures containing functions to be
considered equal to other sets containing the same functions, even
if the sets themselves were pointer-equal themselves. *so* busted.

Change-Id: If87fdab658f9037ce2a654f69a9e3da6ae2f53e5
2025-11-14 02:56:53 +00:00
Qyriad 78bdc60c11 libutil: refactor Pos::getSource to Origin
Change-Id: I1501fceb9ff1e768c8f5c45b65d5638568babf63
2025-11-12 10:40:21 +01:00
Qyriad 3ad1af8a98 libexpr: add trace for if 'foo' errors in 'or foo'
Change-Id: I59365e4c1bf4aed71c008554eca0c9e6d085219d
2025-11-07 14:54:02 +01:00
Qyriad 2806613954 libexpr: refactor ExprSelect::eval
Change-Id: Ibb21dbd3c25649b3b301bc7baaa52a8aa938ebe0
2025-11-07 14:54:02 +01:00
Qyriad 2e7bfa4474 libutil: impl <=> for Pos::{Stdin,String}
Change-Id: Ieb5016a261f5a90bbf08b2afb69bb1c288d07ca7
2025-11-07 12:07:23 +01:00
Qyriad f39cbc5d60 libutil: replace Pos operator< with <=> and constexpr ==
Change-Id: I60080c416b2ee2b0efce4584cf13f37c0596d73d
2025-11-07 12:07:23 +01:00
Qyriad f00d720d83 libutil: refactor trace types and functions into their own file
Change-Id: I00fba6b382991322a083bedeaf169eb5a86f5aeb
2025-11-06 19:19:36 +01:00
eldritch horrors 1e386c3780 libstore: add usage hints to repairPath on daemons
we can't change the protocol to allow daemons to do this, and we should
not try to guess what the `auto` store uri means depending on whether a
command was run by root or not due to copious side effects and not even
being able to tell whether the `auto` store uri was given explicitly or
not. while `auto` may *technically* allow this via its naming we should
resist the urge to add a hack and fix the underlying protocols instead,
especially since repairPath should be a rare, superuser only operation.

fixes #888

Change-Id: I1b53245db226199f827a89a237a2ab9907c3f766
2025-11-06 15:10:34 +01:00
eldritch horrors 24054c1107 libutil: restore process context before subcommand exec
fixes #1028

Change-Id: Ic50b9cc0c65607cd96dd81fd770cda34b4caf9d5
2025-11-05 13:56:09 +00:00
eldritch horrors 0d24aee673 testing: drop compression from truncated nar test
some versions of zstd enable checksumming, some don't. don't use it.

fixes #1027

Change-Id: I06811072d9e56a682a792f084cf76a81c7b5b4ce
2025-11-05 00:02:10 +01:00
eldritch horrors f369c711dd libutil: prefer reporting libarchive source errors
if curl fails during a transfer and said transfer is piped through
libarchive we see very strange errors (#1009). in such cases we'll
want to prioritize reporting the source errors to allow debugging.

Change-Id: I8cc231257eca5b749471ecd38df15071170decc2
2025-11-03 23:35:42 +00:00
hexchen 57658b2146 tests/functional/daemon-trust: fallback to uid
The test would fail if the identity cannot be determined
(as is the case e.g. when using auto-allocate-uids under darwin).
Instead we use the uid when the user name is not available.

Fixes: #1025

Signed-off-by: hexchen <hexchen@lilwit.ch>
Change-Id: I12f35840b78f989b5fd09bbd44c833dc6a6a6964
2025-11-03 18:24:07 +01:00
Raito BezariusandQyriad d4f404ded3 nix/doctor: test whether the current profile generation points somewhere
This helps users to debug whether their current profile symlinks are
correctly set.

Expected outputs look like this:
```
❯ sudo ./outputs/out/bin/nix doctor
[snip]

[PASS] All profiles are gcroots.
[PASS] Client protocol matches store protocol.
[INFO] You are trusted by store uri: local
[FAIL] Error: current generation cannot be discovered for profile:
'/nix/var/nix/profiles/default'
```

```
❯ ./outputs/out/bin/nix doctor
[snip]

[PASS] All profiles are gcroots.
[PASS] Client protocol matches store protocol.
[INFO] You are trusted by store uri: daemon
[PASS] You have 28 generations for profile
'/nix/var/nix/profiles/per-user/raito/profile'
The current generation number is '290'
```

Change-Id: I50c69cbeac3291d668f4c2332803411579adc944
Signed-off-by: Raito Bezarius <raito@lix.systems>
Co-authored-by: Qyriad <qyriad@qyriad.me>
2025-11-03 14:41:47 +00:00
Qyriad a30d87eadc nix/doctor: skip inaccessible PATH elems for duplicate nix
Inaccessible-but-extant PATH elements can't be executed anyway, so we
can skip them as if they didn't exist, instead of erroring like before.

Change-Id: I9288c3ecb6768171e62a3088122d98421558eb03
2025-11-03 14:41:47 +00:00
Qyriad d15b99f489 libstore/static-init: don't crash if nixBinDir exists but isn't accessible
In d2696cdd¹, libstore was made to prioritize NIX_BIN_DIR/nix (over
/proc/self/exe) if it exists. But `pathExists()` throws an exception if
lstat() returns any error other than non-existence.

Because this check happens in libstore's Settings constructor for the
static-initialized global `nix::Settings`, this case would abort Lix
well before main().

[1]: d2696cdd1e

Change-Id: I62e44c8a33b28c234900df3e4790c1badefee358
2025-11-03 14:41:47 +00:00
Commentator2.0 acf6e1c6b7 tests/functional2/file_helper: add a way to interpolate environment variables into files
So far it was impossible to put absolute paths into files, as one
wouldn't know whre the test would be placed.
This commit adds a new Fileish variant called `EnvTemplate` which uses
the given string as a template and replaces the `@ENV_VARIABLE_NAME@`
placeholders with the according value of the environment variable.
This way one can use `@HOME@` or `@TEST_ROOT@` to build absolute paths

Change-Id: I425cb6408dceb8d7f26d136ace4ac98b1ca31ec3
2025-11-03 12:58:37 +00:00
Raito Bezarius 85db5e3bf4 libstore/store-api: rate-limit concurrent copies based on system limits
Since we have async, we are unleashed and therefore we started to cause
people to run into system limits like number of open files.

We introduce an async semaphore which uses max(25 % of max open files,
max number of cores) which should lead to 256 maximum concurrent copies.

We believe that the copy operation is the only one that can cause issues
given that the daemon already runs with obscene fd limits.

Fixes #1022.

Change-Id: Iec433d10d5c5003962ea749cf8e32bafb314f0d9
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-11-01 23:47:13 +00:00
eldritch horrors f5ae0d1eff libutil: fix EBADF when cancelling decompression streams
relying on the sink/source code is a mistake. no exceptions.

Change-Id: Ied4ffc4065ce628ed7f8397526cdb68c8ac652d8
2025-11-01 20:42:12 +00:00
Raito Bezarius df862c1655 libstore/s3: attach more information to error messages
In case of empty messages, it is good to print the raw error code.

Additionally, we print request IDs which can help users to reconcile
what happened with the service provider.

Change-Id: I4d83c011c1b7a5514e3d1b21123df38308279044
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-11-01 21:17:26 +01:00
Raito Bezarius ccf196d7f4 libstore/s3: resolve completion status via the transfer status callback
Completion status can be updated in two ways:

- progress callback
- transfer status update callback (esp. important in multipart
transfers)

It's guaranteed that one of the two will provide progress for our
promise.

This fixes #945.

Change-Id: Iac9e92df34f82fbc1facae7a411ccabf1688da52
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-11-01 21:17:26 +01:00
Raito Bezarius 91867941fa libstore/s3: sign payloads if the request demands it
This fixes problems with S3 implementations that have mandatory payload
signing, e.g. Garage v2, AWS S3 itself, etc.

The problem manifested itself when the AWS SDK threw some error codes 99
(NETWORK_CONNECTION) with no messages and retried until it finally came
to a moment where the state machinery decided to send a
`Transfer-Encoding` header in a `Content-Encoding`/`Content-Length`-set
request with signed headers (even though payload signing is disabled),
causing the server to reject the transfer and crash the copy.

I did not debug super far what went wrong in AWS SDK, but I can confirm
this change makes transfers possible to finish with Garage v2.

Change-Id: Icc9e6a9f2afb0d760cf2d1e27816decd385a1d85
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-10-31 20:01:14 +01:00
Commentator2.0 9ed75192ec tests/functional2: fix crash on empty nix.nix([]) calls on darwin
Calling `nix.nix` with an empty argument list, would crash on darwin due
to a Index Out Of Range Error, as the build="auto" functionallity would
try to access the first element of the provided arguments (second
element at the place of call) resulting in a crash

Change-Id: Ia678109808b3f75a30182114baa421cedaa8759b
2025-10-30 18:53:11 +00:00
Raito Bezarius 95f15cf94f libstore/binary-cache: default to zstd for compression
libarchive's xz offers single threaded xz compression which is very slow
and provides ~10-20Mbps compression speed in addition to maxing a core.

In exchange, it achieves optimal compression ratios among all our
compression methods.

Nonetheless, xz prevent the saturation of 1Gbps+ connections and slow
down significantly decompression for end users. As these connections and
faster hardware is becoming prevalent for cache servers and clients, we
offer to default to zstd.

Lix is a "compress once, decompress many times" application. To avoid
incurring a high penalty to end users very sensitive to compress ratio
(very slow Internet connections), we dampen the consequences of
switching to zstd by increasing the default zstd level to 12.

On one example, xz will compress a 4.4GB file to 632MB, zstd on 12 will
compress it to 775MB, that is a ~18 % increase over the optimal xz
compression. zstd took 18 seconds to produce this file.

Increasing to level 14 leads to a 773MB file while taking 37s.
Increasing to level 16 leads to 735MB file while taking 66s.

Finally, xz took 77s, so a 50 % reduction in time taken to compress in
exchange of an increase of 18 % of the compressed size.

This change will reduce issues encountered in #945 but is probably not
the root cause.

References:

- https://discourse.nixos.org/t/switch-cache-nixos-org-to-zstd-to-fix-slow-nixos-updates-nix-downloads/23961

Change-Id: I7beda2bf2c1fed146dcb797b8f85dc290c486ab2
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-10-30 15:10:10 +01:00
eldritch horrors 047aad9992 libstore: fix s3 promise fulfiller lifetimes
if a promise for an async s3 operation is cancelled is inner fulfiller
will be destroyed, but the s3 operation may still be running. once the
operation finished and attempted to fulfill the promise we'd segfault.

Change-Id: I603c75d6cc39a1aee465b3ceee06b638f7f7f596
2025-10-30 13:57:15 +01:00
Raito Bezarius 1f08eaeb55 doc/manual: provide more information about Pasta and its shortcomings
As we saw actual users running into them throughout the debugging of
#920 and #1014.

It's best to document and point end users to this so they can take
measures for themselves.

Change-Id: I7d08407f4354055bf65fc6dd7d1624c5a9304402
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-10-30 00:04:21 +01:00
Raito Bezarius 480c8e52b6 libstore/linux/pasta: always list an IPv4 & IPv6 nameserver
Fixes #1014.

Address family detection is performed by Pasta, we influence Pasta into
enabling dual stack by listing both an IPv4 & IPv6.

Change-Id: I4121e0eae7d5185c287957106adc36edf3f82a40
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-10-29 21:44:12 +00:00
Raito Bezariusandeldritch horrors 12b87538e3 lix/nix-shell: cleanup of shell directories happens in Lix itself
Instead of letting inner shell perform the cleanup, we will fork/execvp
and finish the work ourselves. Once an interruption arrives, we wait
again and return the exit status code of the inner shell.

This idea was suggested by eldritch horrors.

Fixes #1020.

Co-authored-by: eldritch horrors <pennae@lix.systems>
Change-Id: Id38af7ac33874aa1c4b7ca9c6123e3cbd8c3cd8c
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-10-29 20:13:23 +01:00
Raito Bezarius f6d723b22f lix/main: return the legacy command statuses
This was forgotten which should have been part of 64b9247f.

Change-Id: I3cf83d8699f2c1963ce530bbea6a511075202a79
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-10-29 18:53:00 +01:00
Raito Bezarius 64b9427f98 legacy/nix-build: return exit codes
Legacy commands "main function" will now return an exit code.
Fun fact: `main_nix_build` was the only one not returning integers
already.

Change-Id: Ia43a16c3c3fb9a670e8889aefc4ee9b6528a7df4
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-10-28 20:38:39 +01:00
Raito Bezarius 56b4ed3908 libmain/main: propagate status codes via return values for mainWrapped
This also prevent `legacy` to return nothing as it's void.

Change-Id: Ic797544a59b04b41ad9e1c46af4cad3a1cb6fdc8
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-10-28 20:38:39 +01:00
eldritch horrors 2c176afa7a libutil: don't allow interactive runProgram2
realistically only runProgram is ever called for interactive reasons,
and even those calls seem to be rather ill-advised in many cases. the
chance of multiple interactive processes interfering with each other,
whether for input or for output, must be very low to make this in any
way reasonable: if e.g. git calls ssh for multiple fetched inputs and
ssh requests passphrases for both we can otherwise not guarantee that
*any* input is routed correctly. misrouted output is merely annoying.

Change-Id: I794e3fdf0a3238cb9292003a89ac267f0de7a939
2025-10-28 11:53:33 +00:00
Qyriad 9523446901 libexpr/libutil: summarize derivations in stack traces regardless of --show-trace
Eval errors will now print a simple, no frills chain of involved
derivations at the bottom. For example, trying to evaluate
`pkgs.xonsh.override { python3 = pkgs.python2; }` has the usual

Package ‘python-2.7.18.8’ in /nix/store/9v6qa656sq3xc58vkxslqy646p0ajj61-source/pkgs/development/interpreters/python/cpython/2.7/default.nix:398 is marked as insecure, refusing to evaluate.

message, but now also includes the following:

       note: trace involved the following derivations:
       derivation 'xonsh-0.19.9'
       derivation 'python2.7-xonsh-0.19.9'
       derivation 'python2.7-setuptools-44.0.0'

To give the user information about why the erroring derivation was
involved in the first place.

We would like more structured information in the future, but this should
still be a significant improvement.

Change-Id: Icf6da52abd0a043cfb63943bf0b0c160c21ee59e
2025-10-27 20:28:27 +01:00
Commentator2.0 73c019c57b tests/functional2/lang: allow adding requiring of global assets like config.nix
Change-Id: Ia740b91fb3d18f7d01c0d551196b71e7f3e46384
2025-10-27 17:07:47 +01:00
Qyriad 836caf1173 libutil: add concepts for constraining iterables ("ranges")
Change-Id: I5d75abb9d3713a3fab863666786aa8d38cc93dbd
2025-10-27 13:16:29 +01:00
Qyriad b8462562f2 libutil: allow moving Trace into Error
`pushTrace` now move-constructs or copy-constructs, instead of always
copy-constructing.

Change-Id: Ie5f02643e98b7a6c1adbe58b0d46d9e116eba28e
2025-10-27 13:14:50 +01:00
eldritch horrors 18efc848fe libstore: move curl-multi wrapper into own class
the wrapper is needed by transfer streams to restart a failed transfer
if desired. curlFileTransfer itself is more of a fancy handler for the
thread we're dedicating to curl io handling. the thread will stay with
the multi handle for now because quit handling needs to stay there. we
could have CurlMulti keep only a flag, but that does not help us much.

Change-Id: I99550f0bbb635b75898ca7260f08275df86050e3
2025-10-23 22:52:09 +00:00
eldritch horrors b460a7a38b libstore: move curlFileTransfer::TransferItem up
we have anonymous namespaces and no other scoping requirements, we can
move it out. this will also make it easier to move *other* things out,
such as the transport streams and all the shared multi state handling.

Change-Id: Iafa1eaeb69e096e98c4667f0531b17d5fe7c78a6
2025-10-24 00:16:15 +02:00
eldritch horrors 253ed1cc05 libstore: abort all transfers when shutting down curl thread
waiting for them to finish leaves us prone to deadlocking, e.g. if some
active transfers is blocked and hasn't been destroyed before our thread
was asked to exit. aborting transfers is the safer option here; even if
we cause use after frees we'll at least have a crash, not hang forever.

Change-Id: I333dcced810e8a85e2ded1f377962a0176798098
2025-10-22 17:36:21 +00:00
Raito Bezarius e0d2fa89d3 libstore/build: resolve symlinks beneath settings.caFile
This behavior existed before I32f8b5ce20fe9b6a911768114c92f95fc886cc07
was introduced.

It was remarked in
https://gerrit.lix.systems/c/lix/+/3765/comment/1ff0b2b7_ae67ab1b/.

Change-Id: I7a72e7c36c6043a6c2cb8cab57ad45a552d5e2b6
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-10-22 13:23:48 +00:00
Alois Wohlschlager 7a0b8834a4 Reapply "functional2: fix in the development shell"
This reverts commit 3ef7e0711b.

Change-Id: I5b76d2c2e9b0dd669818c7218493d3e76a6a6964
2025-10-22 07:41:46 +02:00
Alois Wohlschlager 0e0853cc70 functional2: handle unset or empty BUILD_TEST_SHELL properly
If BUILD_TEST_SHELL is not set or empty, None or the empty string respectively
should not make it into the PATH. Ensure this property.

Change-Id: I4ce9b0c06c407b465308b63b9cb64e7d6a6a6964
2025-10-22 07:41:46 +02:00
eldritch horrors 5346b2bc68 libstore: simplify worker child handling a bit
we no longer need the optional wrapping.

Change-Id: I742cca23753c6e0a97ada0b301bb42f442803ea8
2025-10-20 12:43:22 +00:00
eldritch horrors 31b6eb2786 libstore: convert build-hook logging to rpc
Change-Id: I0c20f89de113dce6032c93a32e9fcd43b4478f55
2025-10-20 12:43:22 +00:00
eldritch horrors 5b7ed433d6 libutil: add logging rpc definitions
this is still somewhat experimental and should be considered in flux. we
will have to nail down a logger interface once we start moving the store
protocol to rpc, but until we do that we can use build hooks to test it.

Change-Id: Id20cd346c9520f45871799c31b0af040adde56ef
2025-10-20 12:43:22 +00:00
eldritch horrors 2d2cd7ac03 libutil: always rethrow kj::CanceledException
the fiber runtime requires this for correctness.

Change-Id: I5ebc6a046596403e86cebaa95962053ff7813877
2025-10-20 12:43:22 +00:00
eldritch horrors bab44b870e libstore: handle log size limiting in only one place
extracting the size-specific log handling into a new sink deduplicates
the size-limiting code and lets us clean up the derivation goal a bit:
a number of log-related fields we kept only because previous ownership
semantics were a lot more ad-hoc can be moved into the new sink class.

Change-Id: Ie770a276597c84f9d928a9624c0f0de86adfe815
2025-10-20 12:43:22 +00:00
eldritch horrors 4b9723a23f libstore: DerivationGoal::{handleChildOutput -> wrapChildHandler}
we'll want to remove handleRawChildStream from DerivationGoal, but the
timeout handling is still important. the wrapper is thus useful, but a
more generic way to pass it a promise to do the handling is necessary.

Change-Id: I55bf041d51e4f8a8b8291deae053423d06724329
2025-10-20 12:43:22 +00:00
eldritch horrors b02a66d13e libstore: simplify hook instance handling
we never need the connection or the rpc client directly after the hook
is constructed, only the hook instance rpc object. wrapping them using
kj primitives makes this more explicit and prevents destruction errors

Change-Id: I7e0b14d7e365c2e001273e04c0e741ba4afb12f2
2025-10-20 12:43:08 +00:00
eldritch horrors 6c416ff2ca libstore: make build hook exit status reusable
this makes it possible to wait for the hook to exit in one place,
process its exit status for some purpose, and later process it in
another place for a different purpose. rpc will need this to shut
down a hook cleanly after the hook has completed its assignments.

Change-Id: I5af2d5aac4b02a0a200d720d0e6f0b5df5496aaf
2025-10-19 16:09:32 +00:00
eldritch horrors 7e171a4abe libstore: localize act, builderActivities
only LocalDerivationGoal needs these, and once the child is done
running (or is begin torn down) we can just destroy all of them.

Change-Id: Id4d9f0105aaf33d6736c435bb891d3f9af8e7894
2025-10-18 21:33:54 +00:00
eldritch horrors 0365d17d7e libutil: disallow implicit ref downcasts
wtf?

Change-Id: I3dbce7052e72525c9430efb0f5090b168db04750
2025-10-18 21:33:54 +00:00
eldritch horrors 641769414b libutil: add a timed wait to async mutex
wrapping the wait in a timeout at the call site is insufficient because
a timeout would not relock the mutex and leave the raii object invalid.

Change-Id: I0be2aaf6b3c9966070fb0f1deaab048765fd7af3
2025-10-18 21:33:54 +00:00
Commentator2.0 2541db5321 tests/functional2/lang: fix expected skip message
it seems like pytest is updating the message printed when skipping a set
due to no parametrization
which leads to breakage on staging-next.
this commit changes the expected message to allow both old and new skip
message

Change-Id: I34d11ad84d7075911d8229fbc89889bb880a27c9
2025-10-18 17:19:01 +02:00
Commentator2.0 0ac71ea1cc tests/functional2/lang: fix wrong unused file error for generic test group collection
Change-Id: I1c548fcebe45d2a6621987091e3a852b8a9b5765
2025-10-18 11:24:00 +00:00
Commentator2.0 4fcfdc66fe tests/functional2: migrate debugger.sh
Change-Id: I82c5b739db09530ff216c91608be2f70a2036e11
2025-10-18 11:23:43 +00:00
helle e8d281eac6 doc: information at the use-cgroups option on conflicts it can give
as discussed on Zulip and with Lunaphied

Change-Id: I7c6b3e7994c1e7961033e3daf509c81348927baf
2025-10-18 09:51:36 +00:00
K900 6e2edbff93 flake: only apply the monotonic clocks patch to capnp<1.2.0
1.2.0 already includes it so building on unstable fails.

Change-Id: Iefa49203c371e5bab164c06b9da6f89a89ce7cfe
2025-10-18 12:04:06 +03:00
eldritch horrors a55fe35dac apply capnp patches again
This partially reverts commit f68233ec43.

while more modern capnp in *does* have the build system changes, it
does *not* have the patches. and those patches are rather important
to us; the clock patch because our CI machiens don't behave the way
capnp expect and the nodiscard patch because it *is* a bugsquasher.

Change-Id: Ic273777f09fd8e5e7001f815db8998dae2dd7b88
2025-10-17 19:21:17 +00:00
EmilyandQyriad 6ea1d8e37e libstore: use makeTempSiblingPath in replaceValidPath
Change-Id: I6a6a69641a3b4e6fdd076faac44dc314e6cc057e
2025-10-17 18:41:58 +00:00
EmilyandQyriad 39ebb982ea libutil: add makeTempSiblingPath helper
The prospective callers of this should probably be doing something
smarter or more abstracted to begin with, but this is useful as an
incremental improvement for call sites with existing `makeTempPath`
logic in the face of filename length limits.

Change-Id: I6a6a69644292f5bbf984a1df90192e06c6022b53
2025-10-17 18:41:58 +00:00
EmilyandQyriad d9eead0eac tests: add fetchTarball test for symlink temporary directories
Change-Id: I6a6a6964dfe85b5ee4f66b8c442fe448c7c5c87c
2025-10-17 18:41:58 +00:00
Qyriad 013d5e54e9 libstore: fix non-awaited calls to optimisePath()
LocalStore::optimisePath() was made asynchronous in 38b75b736¹, but we
lost warnings for not awaiting kj::Promise in f68233ec4², since our
capnproto no longer has the [[nodiscard]] patch.

[1]: 38b75b7367
[2]: f68233ec43

Change-Id: I2363396949f40dd43eae08cc76be9ede6b488cf7
2025-10-17 18:41:43 +00:00
eldritch horrors 20ba2b49fb testing: mock ssh during nix-copy prompt test
we don't need full ssh, we only need to know something makes it onto the terminal.

Change-Id: Ic31808e10e1b876b35d0dbdeee0b5ede7b5b9883
2025-10-17 15:13:37 +02:00
eldritch horrors ab9dddfb7a libutil: wrap async kj exceptions as errors
this is important for rpc error reporting: if the peer disconnects we
want to throw a useful error that can e.g. let pending builds finish,
not kill the entire process because kj errors are non-lix exceptions.

Change-Id: I41ebe10ad7fd65d886238ce52eeca1fa243b1bb2
2025-10-17 11:33:00 +00:00
eldritch horrors 21d7cb8e1c libstore: initialize build hooks via rpc
this removes another file descriptor from the build hook. we are now
using only rpc to communicate between build hook and daemon, setting
the stage for a future world in which we don't even need build hooks

Change-Id: I706d9d3f2a420abd42fc7762c205931a45d3c8c5
2025-10-17 11:33:00 +00:00
eldritch horrors 9b886e2acf libstore: remove unused DerivationGoal::machineName
interpolation of the remote store name into activity text is now done in
the build hook, so we no longer need to send it back to the daemon. this
also reduces the dependency of the daemon on how *exactly* remote builds
are done a little bit, and maybe in time we can make these fully opaque.

Change-Id: Ie51c4f2cd107350fec4ad0a8ef83621ed86db6a2
2025-10-17 11:33:00 +00:00
eldritch horrors 711ad52776 libstore: move logSize into each log handler
there's no need for this to be an instance variable of the goals.

Change-Id: Iaa315e8b5d8649d8eb3c3fedc387c0f0c48945fc
2025-10-17 11:33:00 +00:00
eldritch horrors 645a0533eb libstore: move sandbox log handling to LocalDerivationGoal
only local builds can now have a builderOutFD, remote builds only log
via json streams. these two kinds of logs have different requirements
and each is only needed by its respective build kind. splitting these
apart thus makes sense, though ideally we'd also split DerivationGoal
into a RemoteDerivationGoal to clean up the rest, but that will wait.

Change-Id: Ib577537266d1160355ab9c44b4604ebda87a7d04
2025-10-17 11:33:00 +00:00
eldritch horrors a824877d45 libstore: don't treat legacy ssh like a sandbox
treat it like ssh-ng instead, and have the build hook do the translation
of ssh stdout to the json log message steam the derivaiton goal can use.
since the ssh-ng path also handles all store urls that aren't legacy ssh
we now have a single logging system that handles every remote build kind
equally, without requiring fd passing into the build hook. this is later
required to rpc-ify the log stream emitted by build hooks to the daemon.

Change-Id: Ifb522eb8a9745029050f16b1b3b3601a6ddac748
2025-10-17 11:33:00 +00:00
Alois Wohlschlager ac8209404c nix-eval-jobs: estimate memory usage using the heap size
When the maximum memory size is chosen too large (relative to the free memory
usage), so that the evaluation workers don't fit into the free memory, they
will start being swapped out. Further increase in memory usage will not be
reflected in the RSS, so they will not exit, and instead fill up all swap too
until the system runs out of memory.

The BDW-GC keeps track of its heap size, including any parts of it that may be
swapped out. For this reason it does not suffer from a similar problem. While
not all memory usage by the evaluation worker is accounted for by the GC, a
simple affine model for the overhead works reasonably well in practice. The
specific parameter values were determined using nixpkgs evaluation, and appear
to work quite well also for somewhat different workloads (like the `hydraJobs`
of Lix, which include a bunch of NixOS system configurations). So this is what
gets used.

When Lix is configured without the BDW-GC, of course this approach cannot be
used. In this case the old strategy is retained.

Change-Id: I6cb5f0a9b4ceda9dd14be165dda108cd6a6a6964
2025-10-16 15:22:54 +00:00
Alois Wohlschlager ed9cc5f448 nix-eval-jobs: move more worker initialization code to its file
Change-Id: Ief8b16220c153e402ecbdeabd576344c6a6a6964
2025-10-16 15:22:54 +00:00
Alois Wohlschlager e36ac58f93 nix-eval-jobs: hide hidden flags in the --help
Right now there are no hidden flags so this does not matter, but this will
change very soon.

Change-Id: I6d65ce9c32170b6d4f4a691ff9f8806a6a6a6964
2025-10-16 15:22:54 +00:00
eldritch horrors a85115bd33 libutil: add a log line splitter
we'll need this in other places too soon.

Change-Id: I8ddd57aefa4cbd1f8f7232f35e30a204ace77e58
2025-10-16 12:31:08 +00:00
eldritch horrors daff1b148f build-remote: de-optionalize an always-set parameter
Change-Id: Ifd79229cf5f608e3cfe5532ab24b2a4efb547e15
2025-10-16 11:51:27 +00:00
eldritch horrors cac395ffbc nej: fix some clang-tidy lints
these are really annoying when running `just lint` with nej enabled.

Change-Id: I8d35e77fe9905fdd6c28f95361f7431459079b96
2025-10-15 20:27:28 +02:00
eldritch horrors 59b583b46d n-e-j: disable darwin ci
it's a disaster. cf #1015

Change-Id: I4671c1ea0f1b6b2750b97cf3c42497460ef33259
2025-10-15 13:37:18 +00:00
helle 30a5f8ec15 doc: Mention Buildkite in the hacking document
Includes link to the Buildkite SSO login, which is currently not visible
anywhere else in documentation.

Change-Id: Ic3ec6412127b2ac0df7e6efc4ec6020cc9d8b34f
2025-10-15 09:21:49 +00:00
Jade LovelaceandCommentator2.0 faa0b4f291 functional2/http_server: add some example misbehaviour routes
Part of https://git.lix.systems/lix-project/lix/issues/949.

This also fixes a bug where our SO_REUSEADDR wasn't done before bind was
called and thus didn't work as intended.

Co-authored-by: Commentator2.0 <lix@crystal-cavern.systems>

Change-Id: Ie69fd594a32d7a6f538fb66d9d2cac6fe117d563
2025-10-13 18:30:51 +02:00
Jade LovelaceandCommentator2.0 556012e409 functional2: forbid chdir and setting environment
These are a footgun and are not acceptable in functional2 due to thread
safety, effects on other tests, etc.

Co-authored-by: Commentator2.0 <lix@crystal-cavern.systems>

Change-Id: I8d7285061eaa9bab27edd52f3646024c8cf605e5
2025-10-13 18:30:51 +02:00
Commentator2.0 844feb17b5 tests/functional2/lang: fix duplicate collection when both err and out are provided
When providing both a `.err.exp` and a `.out.exp` for a lang test
**not** containing a toml, the test was collected twice and hence threw
a duplicate id error.
This commit adresses this issue

Change-Id: Ia781d69e0f1db2809a30192d6cd0a98861e6cc32
2025-10-13 14:33:25 +00:00
eldritch horrors 2e3d97eb37 libstore: move currentHookLine into handleHookOutput
Change-Id: I00cdc7fae1b953f2d541bc966d15afa00c04b06a
2025-10-13 11:26:49 +00:00
eldritch horrors 26e8e3caac libutil: allow builders to create all activity types
this restriction was added in 4af2611bd1
and extended later. it makes little sense to allow *one* activity type
specifically and drop all others on the floor, especially since broken
builders can create transfer activities with ids that would be used by
other, "real" activities. the only thing achieved here is to drop logs
written in json format in the build sandbox, which does not help much.

we may revisit this restriction later during other log-related rework.

Change-Id: I8bda494083877b71a2f958470fa52380f6ec4968
2025-10-13 11:26:49 +00:00
eldritch horrors b04521e4ec libstore: move log handling fully into handleBuilderOutput
it was split up like this only because the old worker system had no
promises. since we have them now we can use them for encapsulation.

Change-Id: Idbd523eff617d4c53c14b125ab3dfce4979cdab1
2025-10-13 11:26:49 +00:00
eldritch horrors 1210a4e025 libstore: flush log lines only from handleBuilderOutput
also change the tests to ensure that we handle all lines correctly.

Change-Id: I13de286bda8eee57acd53af1ab5c081d3048b500
2025-10-13 11:26:49 +00:00
eldritch horrors f29dfb3d3c libutil: add buffer state management to loggers
currently all loggers can always accept messages and never suggest
flushing buffers. in the future this may change, and at that point
we're already fully set up for it. local loggers should never keep
asynchronous (i.e. network-backed) buffers, disk buffers are fine.
networked loggers will require buffers and periodic flushes later.

Change-Id: Ide2114f5bc17f4a1d289c92ed4f9381a1d59dacf
2025-10-13 11:26:49 +00:00
eldritch horrors 38b75b7367 libstore: make store optimization non-recursive
this keeps the call stack flat even for very deep store directories.
moving progress reporting out of the method optimizing one path also
lets us move the inevitable IO wait and associated promise overhead.

Change-Id: I5083d88b05c247c7e19a21a646bcf3f62bfa3200
2025-10-13 11:26:49 +00:00
eldritch horrors 34b1f362cd libutil: wrap activity updates in macros
we'll eventually want to yield to flush buffers from the non-sync versions.

Change-Id: I8bcbb1c4c8af39c7d7ebf5a76e1ce1dc98067e00
2025-10-12 22:37:31 +00:00
eldritch horrors b73a7f1815 libutil: clean up the Activity interface a bit
create activities from loggers themselves instead of passing the logger
as a constructor argument and allow direct construction of children, no
direct logger access needed. most call sites are not changed because we
still need to handle the "no parent" case, and the logger method can do
that more cleanly than a ternary at each site that creates an activity.
we may eventually want to create a root activity, which is cleaner too.

Change-Id: I295e056228dabb08a1316eba7973874784baa113
2025-10-12 22:37:31 +00:00
eldritch horrors 47000b658e libutil: don't trust RemoteStore activity ids
remotes can generate the same activity ids we might generate locally,
especially if the remote is accessed over the network. in that case a
pid collision is possible, and since activity ids are sequential it's
very possible to create colliding activity ids on both sides as well.

Change-Id: Id58074a41f5f7a59171b52818d1fb5a1beb4bf40
2025-10-12 22:37:31 +00:00
eldritch horrors ed034d0526 libutil: make Activity a real, move-only class
activities are scopes, and there's no reason scopes should not be able
to move around. this also means we can *create* them elsewhere without
also having to box them in some way, making parent relationships clear

Change-Id: I5df036e12ebd8270feb4dca1f23b6bee1f08e906
2025-10-12 22:37:31 +00:00
Lunaphied e9f6baedd1 libstore: add an informative message about disabling cgroups
Change-Id: I0271eff2f7a357af18ddd6ae0571d548a9adbe3e
2025-10-12 20:12:33 +02:00
Commentator2.0 d9c1c0925c unify version string between lix and nixos-module
Currenlty `lix` and `nixos-module` have slightly diverging version
numbering. This commit fixes the lix side of things, in accordance to
the format proposed in
https://git.lix.systems/lix-project/lix/issues/585#issuecomment-7336
which allows for better use of `builtins.compareVersions`

fixes #585

Change-Id: I2072b701f9fadf780e7aab67b62fbcd2ec5df76d
2025-10-11 18:47:46 +00:00
eldritch horrors b88a6e6f11 libexpr: use pascal strings for eval
this has no performance impact in any benchmarks we've run. nul bytes
are still used as implicit truncation points in many places all over:
rejecting them in all locations that treat them as a string end point
requires large changes such as using a proper path library everywhere

Change-Id: I936158bd435f6abf009a689adfbc24496262c578
2025-10-11 12:57:57 +02:00
eldritch horrors 53d172a308 libutil: remove unused CanonPath methods
Change-Id: Id593e0d03373af88e03a9da473935e5c00f86b33
2025-10-10 16:55:34 +00:00
eldritch horrors 2f16cf6014 treewide: lint syscalls for c string safety
Change-Id: I3caf476e59dcb7899ac5a3d83dfa3fb7ceaaabf0
2025-10-10 16:13:54 +00:00
eldritch horrors 3ef7e0711b Revert "functional2: fix in the development shell"
This reverts commit 42691f0d94.

Reason for revert: darwin builds time out now, apparently due to newly appearing infinite loops in the test suite

Change-Id: I81a09efd53636a287f3095ac87ed8854b6e57922
2025-10-10 12:49:45 +00:00
Commentator2.0 5e2412ea7e tests/functional2: migrate add.sh
Change-Id: I699929b61db19a4ab7bea8281df830de9fea6d04
2025-10-08 13:09:17 +02:00
Alois Wohlschlager 42691f0d94 functional2: fix in the development shell
The functional2 test suite was broken in the development shell for two mostly
separate reasons leading to "no such file or directory" errors:

* The `BUILD_TEST_SHELL` (already containing the correct path) would always be
  set from the Meson option `build_test_shell`, even to the empty string if
  that option is not set. Skip the overwrite in this case to make the
  environment variable work again.
* Sandboxed builds would fail to find their builder `/bin/sh`, since in the
  development shell busybox is not found, so no sandbox shell gets configured.
  Use the shell from `BUILD_TEST_SHELL` instead. (How this does not break the
  old functional tests remains a mystery.)

Change-Id: I6a6a696424e8caaef3f9b68e3738bfd58ea0b056
2025-10-07 20:14:48 +00:00
Raito Bezarius 41fab8a31c legacy/nix-store: do not crash on cache unsoundness
Local store make use of SQLite as a cache for output maps, they can
diverge with actual store reality.

Causing a crash at realization time.

We can just throw an error instead.

Change-Id: I943a4bddfd2461a34933e9ec079a6638ab58e5b1
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-10-07 13:35:42 +00:00
Raito Bezarius feab75bde0 legacy/nix-build: create various temporary directories into a known tempdir
Fixes fj#940.

When running `nix-shell`, the `$NIX_BUILD_TOP` environment variable is
set to `$TMPDIR` or `/tmp`.

nixpkgs stdenv uses $NIX_BUILD_TOP to create `$NIX_BUILD_TOP/env-vars`
which contains all the environment variables set by stdenv. This is used
for debugging purposes in combination with `--keep-failed` to reload the
bash environment of a derivation.

`$TMPDIR` is often unset, therefore, `/tmp/env-vars` was constantly
being created. On a multi-user system or, when you run Lix as root, you
might create a `/tmp/env-vars` with different permission bits.

As a result, `nix-shell` can cease to function because that file will
fail creation for an unprivileged user for example.

fj#940 rightfully remark that the code is not consistent between
nix3-develop and nix-shell and it should be reworked.

Change-Id: Iddf15945385d8bd497b2800b37fee5e1f97689b7
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-10-07 03:28:45 +00:00
eldritch horrorsandRaito Bezarius 7e665feae6 libexpr: make .debug_gdb_scripts aligned
This avoids mangling debug gdb scripts and breaking them resulting in no
script loading at all.

Change-Id: Ib5e7034c37a1e41d1a1c9bd8442b8b8b8758e350
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-10-06 23:00:40 +02:00
Raito Bezariusandeldritch horrors 7b6bcffe8b libexpr/genericClosure: de-ptr-ize UnsafeValueList
UnsafeValueList held pointers from Value which were not necessarily GC
allocated, causing mayhem when evaluating something with genericClosure
(texlive environments).

We get rid of storing pointers and we let comparison take places on
const references.

We keep `gc_allocator<Value>` so that GC can scan things it allocates
inside the `res` list.

Co-authored-by: eldritch horrors <pennae@lix.systems>
Reported-by: qbit
Change-Id: I4fed3a3d9e18a2ef2d751f32d81801540e196f92
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-10-06 23:00:40 +02:00
Raito Bezarius b5d31b7780 libexpr: CompareValues can take const references
Change-Id: I5ab1e9f71c2f066eb9bb39683ff00c5190c1fcd7
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-10-06 22:27:36 +02:00
eldritch horrors 14737888e0 libexpr: add a gdb script with value pretty printers
Change-Id: Ia4a6feb194c73dddf468c78287962713bef582f7
2025-10-05 16:23:05 +02:00
eldritch horrors 80654b84b6 libexpr: de-ptr-ize Value references
thunk values are shareable, and we can represent invalid/uninitialized
values with a special bit pattern that makes no sense otherwise. there
is no need to keep allocating values on the heap, instead we can treat
values like reference-counted smart pointers to heap objects, which in
turn lets us save a lot of allocations and, ultimately, gc heap space.

compared to our baseline (main of 2025-09-27) we save 15%+ memory on a
system rebuild and 17% on nix search. eval time regresses by ~3% for a
system rebuild, while nix search is 7% faster. further optimization is
probably possible (but for now this will just have to be good enough).

Change-Id: Ib6c47acdbe2fac4f76a83c2269f16f30ef66b2e1
2025-10-05 16:23:05 +02:00
eldritch horrors 28c4431c69 libexpr: reformat stuff we'll change soon
mainly to keep the next diff smaller. it'll be large enough as is.

Change-Id: Ib8a34520f03539cbf6aa2f0e66cbed05fe1225eb
2025-10-05 16:22:35 +02:00
eldritch horrors db35aa753b libexpr: de-ptr-ize many Value uses
with thunk state being shareable we no longer need to worry about value
uniqueness, only about value lifetime. this means we can liberally drop
indirections and allocations, passing references instead of pointers or
using stack memory instead of gc-managed memory for some intermediates.

Change-Id: I2d48a6fd57a376d544bd9bd2d05e5420611986d1
2025-10-05 16:22:35 +02:00
eldritch horrors 23df732ddb libexpr: remove ValueVector{,Map}
these typedefs were convenient in the past, but now they're not really.

Change-Id: I7522f7582ead545148af8e6444295d620ee0872a
2025-10-05 16:22:35 +02:00
eldritch horrors ad4c6b6eb1 libexpr: remove unused return types
Change-Id: Ief898b70f9781bd3dbe66734710f388daa2f2fed
2025-10-05 16:22:34 +02:00
eldritch horrors f4dee18693 libexpr: remove type punning in primt_attrValues
there's no measurable performance gain in real-world testing to abusing
our list value storage like this. we haven't tested how much storage we
actually need on the stack to offset most of the temporary storage cost
and used 128 as a "good enough" value instead, reserving 1 kiB of stack
space on 64 bit platforms in a leaf function. this should do for a bit.

Change-Id: Ie98519b1da3e6fe685da88d1c44ffb4580fb592a
2025-10-05 14:56:56 +02:00
eldritch horrors 8108e8a760 libexpr: tag Value::Acb
Value is already tagged. Value::Acb blocks are allocated for lambdas (so
we can fit the value tag into the three bits we have available), but the
current layout is rather wasteful for this purpose. the type bits can be
stored together with parts of pointers, which in the lambda case will be
the scope the lambda captures. the expr could also be used, but Env is a
gc-allocated item and thus guaranteed to be aligned properly for tagging

Change-Id: Ia685875387c7795bc4a00d73d1ce3cfea84e7297
2025-10-05 14:56:56 +02:00
eldritch horrors b19bbdfee1 libexpr: make thunk state shareable
this is a strong prerequisite for making values themselves copyable
without duplicating evaluation side effects. with this we can treat
`Value` the way we treated `Value *` to date and drop indirections.

Change-Id: I08f30d12697614a3ae7149615f6f1da83b13f52b
2025-10-05 14:56:56 +02:00
eldritch horrorsandRaito Bezarius 57333a0e60 libutil: preserve ForeignException type info
preserve type information of wrapped exception when constructing a
ForeignException. this requires using `std::current_exception` for
construction in all cases as passing the exception by reference to
any function erases dynamic type information: `throw` may copy the
thrown exception from its source expression; this only works as we
need it to if static and dynamic type of the expression are equal.

fixes #1000

Change-Id: I7167f298540de7c43c2566592bdb8d7a93c34d31
2025-10-05 00:40:22 +00:00
eldritch horrors dc99a5ea1f libexpr: reduce Value size to one pointer
Change-Id: Id6e4a2f68eaa4afdd006379ebcf839c4a126b819
2025-10-04 21:06:52 +02:00
eldritch horrors 0e9e9fd917 libexpr: heap-allocate "large" integers
Change-Id: Ic391f2f1bf87f044d7a688196ba9e0ad766d65aa
2025-10-04 16:31:45 +02:00
eldritch horrors 79586575c5 libexpr: heap-allocate app nodes
Change-Id: I9a39dcf0be7589cedf494757e21665c5d50e446b
2025-10-04 16:31:45 +02:00
eldritch horrors 94a866d44f libexpr: heap-allocate thunk control state
Change-Id: I20d8ab1d6f683c0a2f3b77edf9bdad147d62c8fa
2025-10-04 16:31:45 +02:00
eldritch horrors d84f3003cf libexpr: move lambdas to auxiliary storage
Change-Id: Ibe4885f17c0ba1634ed6dbca0a45f8bd4619d69b
2025-10-04 16:25:05 +02:00
eldritch horrors 1b5f4eb5eb libexpr: move primops to auxiliary storage
same as for null: we have few of them, they're statically allocated,
and they're not the largest contributor to the value population. not
storing them in Value itself frees up resources we *will* use later.

Change-Id: I521f9f243f48f56a78f7bffdf1dc1f0bc40a5c2d
2025-10-04 16:25:04 +02:00
eldritch horrors 05b80a5ba1 libexpr: move null to auxiliary storage
we only need the one object for it. there's no need to waste precious
bits of the value internal type enum for this simple singleton datum.

Change-Id: Ie314b5bf429015e518798d9d65ad8ab2bb84a38e
2025-10-04 16:20:07 +02:00
eldritch horrors 1d998a343b libexpr: move floats to auxiliary storage
floats are used very rarely, and our float support is bad enough to
strongly discourage using them on reproducibility grounds alone. we
can thus move them to more expensive storage without hurting folks.

Change-Id: I1086f612f85e294dd3fae4a2d334e09f52bbe4a8
2025-10-04 16:20:07 +02:00
eldritch horrors 4d7f468c87 libexpr: move external value refs to aux storage
external values very rarely appear during eval "normal" eval, and
creating them is pretty expensive. does *anything* even use them?

Change-Id: Id50fa3f76b7e1f551d550d99996a1ed5880b2531
2025-10-04 16:20:07 +02:00
eldritch horrors 90be3e34c9 libexpr: heap-alloc string control blocks
despite not using allocation caches this does not have a statistically
significant performance impact, with less than 1% extra memory needed.

Change-Id: Ibe51a55ba986e471f217f3724977af17880fafff
2025-10-04 16:20:07 +02:00
f1ef994f12 libexpr: enable parallel marking in boehm-gc
Using parallel marking in GC speeds up evaluation a fair bit

```
Benchmark 1: ./lix-main/bin/nix search nixpkgs hello --no-eval-cache
  Time (mean ± σ):     20.740 s ±  0.046 s    [User: 17.583 s, System: 3.062 s]
  Range (min … max):   20.678 s … 20.794 s    10 runs

Benchmark 2: ./lix-parallel-marking/bin/nix search nixpkgs hello --no-eval-cache
  Time (mean ± σ):     15.037 s ±  0.080 s    [User: 19.602 s, System: 3.125 s]
  Range (min … max):   14.960 s … 15.227 s    10 runs

Summary
  ./lix-parallel-marking/bin/nix search nixpkgs hello --no-eval-cache ran
    1.38 ± 0.01 times faster than ./lix-main/bin/nix search nixpkgs hello --no-eval-cache
```

Based-on: https://github.com/NixOS/nix/pull/13708
Co-authored-by: Eelco Dolstra <edolstra@gmail.com>
Change-Id: Ibc7625f21e0ee7c8ad66203eeb3aca5d83977731
2025-10-01 23:07:28 +00:00
eldritch horrors 1c4e77387a libexpr: remove tPrimOpApp
using the same nodes as tApp is possible, and thanks to multi-arg app
nodes it can even be a bit faster than the linked lists used to date.

Change-Id: Idccb7c0b54c808e62da85d1c42ee09e6e92c4f7b
2025-09-29 17:56:05 +02:00
eldritch horrors 0b5b14ddc7 libexpr: add multi-arg app nodes
these behave like the old chains of app nodes, but they can store more
than one argument per node. for tApp values themselves this is not all
that useful, but if we could share tApp and tPrimOpApp backing storage
we could avoid creating and traversing the linked lists of values that
are currently needed to represent partially applied builtin functions.

Change-Id: I5a2a02d9733e1e0be5443459e2998d62fd3b9a5b
2025-09-29 17:56:05 +02:00
eldritch horrors 3350ab8a56 libexpr: use std::span for callFunction
Change-Id: I8c94bafabdb2416c85d9721d3d6424f52fbd45e0
2025-09-29 17:56:05 +02:00
eldritch horrors 0d69da184c libexpr: unify strings and paths
paths already are just strings with slightly magic semantics. the type
can mirror that at no perf cost, letting us drop one internalType tag.

Change-Id: I98acaa4fe3bedd28fc0841e1b81184d8dcddacc5
2025-09-29 17:56:05 +02:00
eldritch horrors d23e3a09a4 libexpr: alloc list storage as a (length, vla) type
memory overhead is minimal and performance impact not measurable. once
we've done something like this for all value types that don't fit in a
single machine word we can cut a word from Value, offsetting the cost.

Change-Id: I9813bacd7e851957ad3426aed8f74033179a4212
2025-09-29 17:55:54 +02:00
eldritch horrors 01f1852360 libexpr: don't inline small lists into values
this has no measurable performance impact thanks to the new caches.

Change-Id: Ib403a9a567161675f78e8c5d314d6340183d181d
2025-09-29 15:22:41 +02:00
eldritch horrors 739624ddcf libexpr: cache more allocation sizes
we now use a single cache set for a number of sizes from one to eight
words. this also matches small attrsets, but perf impact seems small.

Change-Id: Icf16b329b98a20fcc9fe75e6395e148f0852c798
2025-09-29 15:22:41 +02:00
eldritch horrors c7cc7d6c31 libexpr: "hide" Value union members
on its own this is not very useful, but having accessors for every value
kind is a prerequisite for doing smart things with Value than the union.
the net effect for now is only to add a few parentheses across the tree.

Change-Id: I88688ac09eb08495dad1eb221034ca540f094950
2025-09-29 15:22:41 +02:00
eldritch horrors 2dae1141d9 bench: add memory benchmark
Change-Id: I4b2aa305b452a3a0c73e37953a5c332219d43d2c
2025-09-29 15:22:41 +02:00
eldritch horrors f31e8e2b55 bench: allow benchmarking a single build
Change-Id: Ie77247f9348eaeea740b0f894819ee2383d92312
2025-09-28 13:35:37 +00:00
eldritch horrors e4ee5e2eb5 testing: disable substitution in f2
mirrors f1. much faster.

Change-Id: I38bbbd5b26220f480afe76ef9302e5c90f21507a
2025-09-28 13:19:13 +00:00
273 changed files with 7236 additions and 3682 deletions
+11
View File
@@ -73,6 +73,9 @@ detroyejr:
display_name: Jonathan De Troye
github: detroyejr
edef:
github: edef1c
edolstra:
display_name: Eelco Dolstra
github: edolstra
@@ -86,6 +89,11 @@ ericson:
display_name: John Ericson
github: ericson2314
getchoo:
display_name: Seth Flynn
forgejo: getchoo
github: getchoo
gilice:
forgejo: gilice
@@ -224,6 +232,9 @@ roberth:
display_name: Robert Hensing
github: roberth
sandydoo:
github: sandydoo
seppel3210:
github: Seppel3210
-16
View File
@@ -1,16 +0,0 @@
---
synopsis: First argument to `--arg`/`--argstr` must be a valid Nix identifier
issues: [fj#496]
category: "Breaking Changes"
credits: [ma27]
---
The first argument to `--arg`/`--argstr` must be a valid Nix identifier, i.e.
`nix-build --arg config.allowUnfree true` is now rejected.
This is because that invocation is a false friend since it doesn't set
`{ config = { allowUnfree = true; }; }`, but `{ "config.allowUnfree" = true; }`.
The idea is to change the behavior to the latter in the long-term. For that,
non-identifiers started giving a warning since 2.92 and are now rejected to give people
who depend on that a chance to notice and potentially weigh in on the discussion.
-20
View File
@@ -1,20 +0,0 @@
---
synopsis: "Improved susbtituter query speed"
issues: []
cls: []
category: Improvements
credits: [horrors]
---
The code used to query substituters for derivations has been rewritten slightly
to take advantage of our asynchronous runtime. Such queries run for every build
that could download from substituters and processes every derivation that isn't
yet present on the local system. Previously Lix would use `http-connections` to
limit query concurrency, even for modern caches that support HTTP/2 and have no
limit on how many queries can be run concurrently on one single connection. Lix
no longer does this, resulting in approximately 60% reduction in query time for
medium-sized closures (e.g. NixOS system closures) during testing, although the
exact number depends greatly on local network latency and generally improves as
latency increases. Unlike previously setting `http-connections` to `1` or other
low values no longer brings a massive penalty in query performance if the cache
in use by the querying system supports HTTP/2 (as e.g. `cache.nixos.org` does).
-12
View File
@@ -1,12 +0,0 @@
---
synopsis: "`build-dir` no longer defaults to `temp-dir`"
cls: [3453]
category: "Fixes"
credits: [horrors]
---
The directory in which temporary build directories are created no longer defaults
to the value of the `temp-dir` setting to avoid builders making their directories
world-accessible. This behavior has been used to escape the build sandbox and can
cause build impurities even when not used maliciously. We now default to `builds`
in `NIX_STATE_DIR` (which is `/nix/var/nix/builds` in the default configuration).
@@ -1,60 +0,0 @@
---
synopsis: "Global certificate authorities are copied inside the builder's environment"
issues: [gh#12698, fj#885]
cls: [3765]
category: Fixes
credits: [raito, emilazy]
---
Previously, CA certificates were only installed at
`/etc/ssl/certs/ca-certificates.crt` for sandboxed builds on Linux.
This setup was insufficient in light of recent changes in `nixpkgs`, which now
enforce HTTPS usage for `fetchurl`, even for fixed-output derivations, to
mitigate confidentiality risks such as `netrc` or credentials leakage.
`nixpkgs` still make use of a special package called `cacerts` which contains a
copy of the CA certificates maintained by Nixpkgs and added as a reference for
TLS-enabled fetchers.
As a result, having a consistent and trusted certificate authority in all
builder environments is becoming more essential.
On `nix-darwin`, the `NIX_SSL_CERT_FILE` environment variable is always
explicitly defined, but it is ignored by the sandbox setup.
Simultaneously, Nix evaluates and propagates impure environment variables via
`lib.proxyImpureEnvVars`, meaning that if `NIX_SSL_CERT_FILE` is set (which
influences the default value for `ssl-cert-file`), it will be forwarded
unchanged into the builder environment.
However, on Linux, Nix also *copies* the CA file into the sandbox, creating a
discrepancy between the value of `NIX_SSL_CERT_FILE` and the actual trusted
certificate path used during the build.
This divergence caused confusion and was partially addressed by attempts to
whitelist the CA path in the Darwin sandbox (see cl/2906), but that approach
involved a non-trivial path canonicalization step and is not as general as this one.
To address this properly, we now emit a warning and override
`NIX_SSL_CERT_FILE` inside the builder, explicitly pointing it to the CA file
copied into the sandbox.
This eliminates ambiguity between `NIX_SSL_CERT_FILE`
and `ssl-cert-file`, ensuring consistent trust anchors across platforms.
This warning might become a hard error as we figure out what to do regarding
`lib.proxyImpureEnvVars` in nixpkgs.
The behavior has been verified across sandboxed and unsandboxed builds on both
Linux and Darwin.
As a consequence of this change, approximately 500KB of CA certificate data is
now unconditionally copied into the build directory for fixed-output
derivations.
While this ensures consistent trust verification without having to restart the
daemon after system upgrades, it may introduce a slight overhead in build
performance. At present, no optimizations have been implemented to avoid this
copy, but if this overhead proves noticeable in your workflows, please open an
issue so we can evaluate and possibly implement different strategies to render
trust anchors visible.
-69
View File
@@ -1,69 +0,0 @@
---
synopsis: New cgroup delegation model
issues: [fj#537, fj#77]
cls: [3230]
category: "Breaking Changes"
credits: [raito, horrors, lheckemann]
---
Builds using cgroups (i.e. `use-cgroups = true` and the experimental feature
`cgroups`) now always delegate a cgroup tree to the sandbox.
Compared to the original C++ Nix project, our delegation includes the
`subtree_control` file as well, which means that the sandbox can disable
certain controllers in its own cgroup tree.
This is a breaking change because this requires the Nix daemon to run with an
already delegated cgroup tree by the service manager.
## How to setup the cgroup tree with systemd?
systemd offers knobs to perform the required setup using:
```
[Service]
Delegate=yes
DelegateSubtree=supervisor
```
These directives are now included in our systemd packaging.
## What about using Nix as root without connecting to the daemon?
Builds run as `root` without connecting to the daemon relying on the cgroup
feature are now broken, i.e.
```console
# nix-build --use-cgroups --sandbox ... # will not work
```
Consider doing instead:
```console
# systemd-run --same-dir --wait -p Delegate=yes -p DelegateSubgroup=supervisor nix-build --use-cgroups ...
```
If you need to disable cgroups temporarily, remember that you can do
`NIX_CONF='include /etc/nix/nix.conf\nuse-cgroups = false' nix-build ...` or
`nix-build --no-use-cgroups ...`.
## What about other service managers than systemd?
systemd has a [documentation](https://systemd.io/CGROUP_DELEGATION/) on how to
handle cgroup delegation from service management perspective.
If your service manager adheres to systemd semantics, e.g. writing an extended
attribute `user.delegate=1` on the delegated cgroup tree directory and moving
the `nix-daemon` process inside a cgroup tree to respect the inner process
rule, then, the feature will work as well.
## Why is the cgroup feature still experimental?
While the cgroup feature unlocks many use cases, its behavior and integration (e.g. user experience), especially at scale on build farms or in multi-tenant environments, are not yet fully matured. Theres also potential for deeper systemd integration (e.g. using slices and scopes) that has not been fully explored.
To avoid locking in an unstable interface, were keeping the experimental flag until we have validated the feature across a broader range of scenarios, including but not limited to:
* Nix as root
* Hydra-style build farms
* Forgejo CI runners
* Shared remote builders
@@ -1,16 +0,0 @@
---
synopsis: Deprecation of CA derivations, dynamic derivations, and impure derivations
issues: [fj#815]
cls: []
significance: significant
category: Miscellany
credits: []
---
Content-addressed derivations are now deprecated and slated for removal in Lix 2.94.
We're doing this because the CA derivation system has been a known cause of problems
and inconsistencies, is unmaintained, habitually makes improving the store code very
difficult (or blocks such improvements outright), and is beset by a number of design
flaws that in our opinion cannot be fixed without a full reimplementation from zero.
Dynamic derivations and impure derivations are built on the CA derivation framework,
and owing to this they too are deprecated and slated for removal in another release.
-25
View File
@@ -1,25 +0,0 @@
---
synopsis: "Hitting Control-C twice always terminates Lix"
cls: [3574]
issues: []
category: "Improvements"
credits: [horrors]
---
Hitting Control-C or sending `SIGINT` to Lix now prints an informational message
if it is still running after on second, the second Control-C/`SIGINT` terminates
Lix immediately without waiting for any shutdown code to finish running. Lix did
not treat the second such event differently from first in the past; this made it
impossible to easily terminate running Lix processes that got stuck in e.g. very
expensive Nixlang code that never interacted with the store. We now terminate as
soon as the user hits Control-C again without waiting any more, to much the same
effect as putting Lix into the background and killing it immediately afterwards.
This means you can now more conveniently break out of stuck Nixlang evaluations:
```
nix-instantiate --eval --expr 'let f = n: if n == 0 then 0 else f (n - 1) + f (n - 1); in f 32'
^CStill shutting down. Press ^C again to abort all operations immediately.
^C
❌130
```
-12
View File
@@ -1,12 +0,0 @@
---
synopsis: "libstore: exponential backoff for downloads"
issues: [lix#932]
cls: [3856]
category: Fixes
credits: [ma27]
---
The connection timeout when downloading from e.g. a binary cache is exponentially
increased per failure. The option `connect-timeout` is now an alias to `max-connect-timeout`
which is the maximum value for a timeout. The start value is controlled
by `initial-connect-timeout` which is `5` by default.
-9
View File
@@ -1,9 +0,0 @@
---
synopsis: Fix develop shells for derivations with escape codes
issues: [fj#991]
cls: [4154, 4155]
category: Fixes
credits: [Qyriad]
---
ASCII control characters (including `\e`, used for ANSI escape codes) in derivation variables are now correctly escaped for `nix develop` and `nix print-dev-env`, instead of erroring.
-13
View File
@@ -1,13 +0,0 @@
---
synopsis: "nix-store --delete: always remove obsolete hardlinks"
issues: []
cls: [3188]
category: Fixes
credits: [lheckemann]
---
Deleting specific paths using `nix-store --delete` or `nix store
delete` previously did not delete hard links created by `nix-store
--optimise` even if they became obsolete, unless _all_ of the given
paths were deleted successfully. Now, hard links are always cleaned
up, even if some of the given paths could not be deleted.
-24
View File
@@ -1,24 +0,0 @@
---
synopsis: "Report GC statistics correctly"
issues: []
cls: [3188]
category: Fixes
credits: [lheckemann]
---
Deleting specific paths using `nix-store --delete` or `nix store delete` previously did
not report statistics correctly when some of the paths could not be deleted, even if
others were deleted:
```
$ nix store delete /nix/store/9bwryidal9q3g91cjm6xschfn4ikd82q-hello-2.12.1 --delete-closure -v
finding garbage collector roots...
deleting '/nix/store/9bwryidal9q3g91cjm6xschfn4ikd82q-hello-2.12.1'
0 store paths deleted, 0.00 MiB freed
error: Cannot delete some of the given paths because they are still alive. Paths not deleted:
k9bxzr1l92r5y6mihrkbpbr3fmc8qszx-libidn2-2.3.8
mbx9ii53lzjlrsnlrfmzpwm33ynljwdn-libunistring-1.3
rf8hcy6bldxdqc0g6q1dcka1vh47x69s-xgcc-14.2.1.20250322-libgcc
vbrdc5wgzn0w1zdp10xd2favkjn5fk7y-glibc-2.40-66
To find out why, use nix-store --query --roots and nix-store --query --referrers.
```
-16
View File
@@ -1,16 +0,0 @@
---
synopsis: Add `inputs.self.submodules` flake attribute
issues: [fj#942]
cls: [3839]
category: Features
credits: [edolstra, kasimeka]
---
A port of <https://github.com/NixOS/nix/pull/12421> to Lix, which:
- adds a general `inputs.self` flake attribute that retroactively applies
configurations to a flake after it's been fetched, then triggers a refetch of
the flake with the new config.
- implements `inputs.self.submodules` that allows a flake to declare its need
for submodules, which are then fetched automatically with no need to pass
`?submodules=1` anywhere.
@@ -1,16 +0,0 @@
---
synopsis: Add hyperlinks in attr set printing
issues: []
cls: [3790]
category: Features
credits: [jade]
---
The attribute set printer, such as is seen in `nix repl` or in type errors, now prints hyperlinks on each attribute name to its definition site if it is known.
Example: all of the attributes shown here are hyperlinks to the exact definition site of the attribute in question:
```
$ nix eval -f '<nixpkgs>' lib.licenses.mit
{ deprecated = false; free = true; fullName = "MIT License"; redistributable = true; shortName = "mit"; spdxId = "MIT"; url = "https://spdx.org/licenses/MIT.html"; }
```
-24
View File
@@ -1,24 +0,0 @@
---
synopsis: Repl debugger uses `--ignore-try` by default
issues: [lix#666]
cls: [3488]
category: Breaking Changes
credits: [jade]
---
Previously, using the debugger meant that exceptions thrown in `builtins.tryEval` would trigger the debugger.
However, this caught nixpkgs initialization code, which is unhelpful in the majority of cases, so we changed the default.
To get the old behaviour, use `--no-ignore-try`.
```
$ nix repl --debugger --expr 'with import <nixpkgs> {}; pkgs.hello'
Lix 2.94.0-dev-pre20250625-9a59106
Type :? for help.
error: file 'nixpkgs-overlays' was not found in the Nix search path (add it using $NIX_PATH or -I)
This exception occurred in a 'tryEval' call. Use --ignore-try to skip these.
Added 13 variables.
nix-repl>
```
@@ -1,25 +0,0 @@
---
synopsis: "Fallback to safe temp dir when build-dir is unwritable"
issues: [fj#876]
cls: [3501]
category: "Fixes"
credits: ["raito", "horrors"]
---
Non-daemon builds started failing with a permission error after introducing the `build-dir` option:
```
$ nix build --store ~/scratch nixpkgs#hello --rebuild
error: creating directory '/nix/var/nix/builds/nix-build-hello-2.12.2.drv-0': Permission denied
```
This happens because:
1. These builds are not run via the daemon, which owns `/nix/var/nix/builds`.
2. The user lacks permissions for that path.
We considered making `build-dir` a store-level option and defaulting it to `<chroot-root>/nix/var/nix/builds` for chroot stores, but opted instead for a fallback: if the default fails, Nix now creates a safe build directory under `/tmp`.
To avoid CVE-2025-52991, the fallback uses an extra path component between `/tmp` and the build dir.
**Note**: this fallback clutters `/tmp` with build directories that are not cleaned up. To prevent this, explicitly set `build-dir` to a path managed by Lix, even for local workloads.
-47
View File
@@ -1,47 +0,0 @@
---
synopsis: Experimental integer coercion in interpolated strings
issues: []
cls: [3198]
category: "Features"
credits: [raito, delroth, horrors, winter]
---
Ever tried interpolating a port number in Lix and ended up with something like this?
```nix
"http://${config.network.host}:${builtins.toString config.network.port}/"
```
You're not alone. Thousands of Lix users suffer every day from excessive `builtins.toString` syndrome. Its 2025, and we still have to cast integers to use them in strings.
To address this, Lix introduces the **`coerce-integers`** experimental feature. When enabled, interpolated integers within `"${...}"` are automatically coerced to strings. This allows writing:
```nix
"http://${config.network.host}:${config.network.port}/"
```
without additional conversion.
To enable the feature, you need to add `coerce-integers` to your set of experimental features.
### Stabilization criteria
The `coerce-integers` feature is experimental and limited strictly to string interpolation (`"${...}"`). Before stabilization, the following must hold:
1. **Interpolation-only**
Coercion must not occur outside interpolation. Expressions like `"" + 42` must continue to fail.
2. **Expectation that no explicit cast are being observed**
Cases observing explicit coercion (e.g., via `tryEval` gadget or similar) are expected not to be load-bearing in actual production code.
### Timeline for stabilization
If the feature proves safe and is widely adopted across typical usage (e.g., actual configurations in the wild turning on the flag, non-trivial out-of-tree projects using it), the experimental flag will be removed **after six months of active use or two Lix releases**, whichever is longer.
This avoids locking the feature in experimental status indefinitely, as happened with Flakes, while allowing time for validation and ecosystem integration.
### What about coercing floats or more?
Coercion beyond integers -- such as for floats or other types -- is **not planned**, even under an experimental flag. Questions like "what is the canonical string representation of a float?" involve subtle and context-dependent trade-offs. Without a robust and principled mechanism to define and audit such behavior, introducing broader coercion risks setting unintended and hard-to-reverse precedents. The scope of `coerce-integers` is intentionally narrow and will remain so.
In terms of outlook, a proposal like https://git.lix.systems/lix-project/lix/issues/835 could pave the way for a better solution.
@@ -1,16 +0,0 @@
---
synopsis: Parse overflowing JSON number literals as floatingpoint
issues: []
cls: [3919]
category: "Fixes"
credits: [emilazy]
---
Previously, `builtins.fromJSON "-9223372036854775809"` would
return a floatingpoint number, while `builtins.fromJSON
"9223372036854775808"` would cause an evaluation error. This was
introduced with the banning of integer overflow in Lix 2.91; previously
the latter would result in C++ undefined behaviour. These cases are
now treated consistently with JSONs model of a single numeric type,
and JSON number literals that do not fit in a Nixlanguage integer
will be parsed as floatingpoint numbers.
-13
View File
@@ -1,13 +0,0 @@
---
synopsis: "`--keep-failed` chowns the build directory to the user that request the build"
issues: []
cls: []
category: Improvements
credits: [horrors]
---
Running a build with `--keep-failed` now chowns the temporary directory from the
builder user and group to the user that request the build if the build came from
a local user connected to the daemon. This makes inspecting failed derivations a
lot easier. On Linux the build directory made visible to the user will not be in
the same path as it was in the sandbox and continuing builds will usually break.
@@ -1,11 +0,0 @@
---
synopsis: Fix handling of OSC codes in terminal output
issues: [fj#160]
cls: [3143]
category: Fixes
credits: [lilyball]
---
OSC codes in terminal output are now handled correctly, where OSC 8 (hyperlink) is preserved any
time color codes are allowed and all other OSC codes are stripped out. This applies not only to
output from build commands but also to rendered documentation in the REPL.
-14
View File
@@ -1,14 +0,0 @@
---
synopsis: Better debuggability on fixed-output hash mismatches
issues: []
cls: []
category: Improvements
credits: [lheckemann]
---
Fixed-output derivation hash mismatch error messages will now include the path that was
produced unexpectedly, and this path will be registered as valid even if `--check`
(`nix-store`, `nix-build`) or `--rebuild` (`nix build`) was passed. This makes comparing
the expected path with the obtained path easier, and is useful for debugging when
upstreams modify previously-published releases or when changes in fixed-output
derivations' dependencies affect their output unexpectedly.
-10
View File
@@ -1,10 +0,0 @@
---
synopsis: "nix-eval-jobs: support `--no-instantiate` flag"
issues: [fj#987]
category: Features
credits: [mic92,ma27]
---
`nix-eval-jobs` now supports a flag called `--no-instantiate`. With this enabled,
no write operations on the eval store are performed. That means, only evaluation is
performed, but derivations (and their gcroots) aren't created.
@@ -1,29 +0,0 @@
---
synopsis: "Fix nix develop for derivations that rejects dependencies with structured attrs"
issues: [fj#997]
cls: [4182]
category: Fixes
credits: [raito]
---
For the sake of concision, we refer to `disallowedReferences` in what follows,
but all output checks were equally fixed:
`{dis,}allowed{References,Requisites}`.
Derivations can define *output checks* to reject unwanted dependencies, such as
interpreters like `bash` or compilers like `gcc`. This can be done in two ways:
* **Legacy style**: `disallowedReferences = [ ... ]` in the environment.
* **Structured attrs**: `outputChecks.<output>.disallowedReferences = [ ... ]`,
typically used in `__json`.
Only the structured form supports derivations with multiple outputs.
`nix develop` internally rewrites derivations to create development shells. It
relied on the legacy `disallowedReferences`, and failed to honor the structured
variant. This led to broken shells in cases where `bashInteractive` was
explicitly disallowed using structured output checks, e.g. `nix develop
nixpkgs#systemd` after the "bash-less NixOS" changes.
This fix teaches `nix develop` to respect structured output checks, restoring
support for such derivations.
-12
View File
@@ -1,12 +0,0 @@
---
synopsis: "Add --raw flag to `nix-instantiate --eval` for unescaped output"
issues: []
prs: [gh#12119]
cls: [2886]
category: Improvements
credits: [not-my-profile, infinisil, raito]
---
The `nix-instantiate --eval` command now supports a `--raw` flag. When used,
the result must be coercible to a string (as with `${...}`) and is printed
verbatim, without quotes or escaping.
@@ -1,12 +0,0 @@
---
synopsis: Allow `nix store ls` to read nar listings from binary cache stores.
issues: []
cls: [3225]
category: Improvements
credits: [vlinkz]
---
The `nix store ls` command now supports reading `.ls` nar listings from binary cache stores.
If a listing is detected for the store path being queried, the nar is no longer downloaded.
These nar listings are available in binary cache stores where the `write-nar-listing` option is
enabled, such as cache.nixos.org.
@@ -1,10 +0,0 @@
---
synopsis: "nix-eval-jobs: retain NIX_PATH"
issues: []
cls: [3859]
category: Fixes
credits: [ma27,mic92]
---
`nix-eval-jobs` doesn't clear the `NIX_PATH` from the environment anymore. This matches the behavior
of [upstream version `2.30`](https://github.com/nix-community/nix-eval-jobs/releases/tag/v2.30.0).
-25
View File
@@ -1,25 +0,0 @@
---
synopsis: "show tree with references that lead to an output cycle"
issues: [fj#551]
category: Improvements
credits: [ma27]
---
When Lix determines a cyclic dependency between several outputs of a derivation,
it now displays which files in which outputs lead to an output cycle:
```
error: cycle detected in build of '/nix/store/gc5h2whz3rylpf34n99nswvqgkjkigmy-demo.drv' in the references of output 'bar' from output 'foo'.
Shown below are the files inside the outputs leading to the cycle:
/nix/store/3lrgm74j85nzpnkz127rkwbx3fz5320q-demo-bar
└───lib/libfoo: …stuffbefore /nix/store/h680k7k53rjl9p15g6h7kpym33250w0y-demo-baz andafter.…
→ /nix/store/h680k7k53rjl9p15g6h7kpym33250w0y-demo-baz
└───share/snenskek: …???? /nix/store/dm24c76p9y2mrvmwgpmi64rryw6x5qmm-demo-foo ....…
→ /nix/store/dm24c76p9y2mrvmwgpmi64rryw6x5qmm-demo-foo
└───bin/alarm: …textexttext/nix/store/3lrgm74j85nzpnkz127rkwbx3fz5320q-demo-bar abcabcabc.…
→ /nix/store/3lrgm74j85nzpnkz127rkwbx3fz5320q-demo-bar
```
Please note that showing the files and its contents while displaying the cycles only works
on Linux.
-20
View File
@@ -1,20 +0,0 @@
---
synopsis: "Fixed output derivations can be run using `pasta` network isolation"
cls: [3452]
issues: [fj#285]
category: "Breaking Changes"
credits: [horrors, puck]
---
Fixed output derivations traditionally run in the host network namespace.
On Linux this allows such derivations to communicate with other sandboxes
or the host using the abstract Unix domains socket namespace; this hasn't
been unproblematic in the past and has been used in two distinct exploits
to break out of the sandbox. For this reason fixed output derivations can
now run in a network namespace (provided by [`pasta`]), restricted to TCP
and UDP communication with the rest of the world. When enabled this could
be a breaking change and we classify it as such, even though we don't yet
enable or require such isolation by default. We may enforce this in later
releases of Lix once we have sufficient confidence that breakage is rare.
[`pasta`]: https://passt.top/
@@ -1,21 +0,0 @@
---
synopsis: Remove reliance on Bash for remote stores via SSH
issues: [fj#830, fj#805, fj#304]
cls: [3159]
category: "Fixes"
credits: [raito]
---
The pre-flight `echo started` handshake -- added years ago to catch race conditions -- has been removed.
After removal of connection sharing in Lix 2.93, it required a Bash-compatible shell and a standard `echo`, so it failed on:
* builders protected by `ForceCommand` wrappers (e.g. `nix-remote-build`),
* BusyBox / initrd images with no Bash,
* hosts using non-POSIX shells such as Nushell.
The race the probe once addressed was tied to SSH connection-sharing -- since connection-sharing code has already been removed, the probe is now pointless.
Real connection or protocol errors are now left to SSH/Nix to report directly.
This is technically a breaking change if you had scripts that relied on the literal "started" which needs to be updated to rely on other signals, e.g., exit codes.
-20
View File
@@ -1,20 +0,0 @@
---
synopsis: Remove support for daemon protocols before 2.18
issues: [fj#510]
cls: [3249]
significance: significant
category: "Breaking Changes"
credits: [horrors]
---
Support for daemon wire protocols belonging to Nix 2.17 or older have been
removed. This impacts clients connecting to the local daemon socket or any
remote builder configured using the `ssh-ng` protocol. Builders configured
with the `ssh` protocol are still accessible from clients such as Nix 2.3.
Additionally Lix will not be able to connect to an old daemon locally, and
remote build connections to old daemons is likewise limited to `ssh` urls.
We have decided to take this step because the old protocols are very badly
tested (if at all), maintenance overhead is high, and a number of problems
with their design makes it infeasible to remain backwards compatible while
we move Lix to a more modern RPC mechanism with better versioning support.
-14
View File
@@ -1,14 +0,0 @@
---
synopsis: "`nix eval --write-to` has been removed"
cls: [4045]
issues: [fj#974, fj#227]
category: "Breaking Changes"
credits: [horrors]
---
`nix eval --write-to` has been removed since it was underspecified, not widely
useful, and prone to security-sensitive misbehaviors. The feature was added in
Nix 2.4 purely for internal use in the build system. According to our research
it hasn't found any use outside of some distribution packaging scripts. Please
use structured outputs formats (such as JSON) instead as they have better type
fidelity, don't conflate attributes with paths, and are useful to other tools.
@@ -1,17 +0,0 @@
---
synopsis: Remove impure derivations and dynamic derivations
issues: [fj#815]
cls: [3210]
significance: significant
category: "Breaking Changes"
credits: [horrors]
---
The `impure-derivations` and `dynamic-derivations` experimental feature have
been removed.
New impure or dynamic derivations cannot be created from this point forward, and
any such pre-existing store derivations canot be read or built any more.
Derivation outputs created by building such a derivation are still valid
until garbage collected; existing store derivations can only be garbage
collected.
@@ -1,17 +0,0 @@
---
synopsis: Remove the `parse-toml-timestamps` experimental feature
category: "Breaking Changes"
credits: [emilazy]
---
The `parse-toml-timestamps` experimental feature has been removed.
This feature used inband signalling to mark timestamps, making it
impossible to unambiguously parse TOML documents. It also exposed
implementationdefined behaviour in the TOML specification that
changed in the toml11 parser library.
Any interface for parsing TOML timestamps suitable for future
stabilization would necessarily involve breaking changes, and there
is no evidence this experimental feature is being relied upon in the
wild, so it has been removed.
-20
View File
@@ -1,20 +0,0 @@
---
synopsis: "repl-overlays now work in the debugger for flakes"
issues: [fj#777]
cls: [3398]
category: Fixes
credits: [jade]
---
Due to a bug, it was previously not possible to use the debugger on flakes with repl-overlays, or with pure evaluation in general:
```
$ nix repl --pure-eval
Lix 2.94.0-dev-pre20250617-87d99da
Type :? for help.
Loading 'repl-overlays'...
error: access to absolute path '/Users/jade/.config/nix/repl.nix' is forbidden in pure eval mode (use '--impure' to override)
```
This is now fixed.
The contents of the repl-overlays file itself (i.e. most typically the top level lambda in it) will be evaluated in impure mode.
It may be necessary to use `builtins.seq` to force the impure operations to happen first if one wants to do impure operations inside a repl-overlays file in pure evaluation mode.
@@ -1,19 +0,0 @@
---
synopsis: "`disallowedRequisites` now reports chains of disallowed requisites"
issues: [fj#334,fj#626,gh#10877]
category: Improvements
credits: [ma27,roberth]
---
When a build fails because of [`disallowedRequisites`](@docroot@/language/advanced-attributes.md#adv-attr-disallowedRequisites), the error message now includes the chain of references that led to the failure. This makes it easier to see in which derivations the chain can be broken, to resolve the problem.
Example:
```
$ nix-build -A hello
error: output '/nix/store/0b7k85gg5r28gb54px9nq7iv5986mns9-hello-2.12.2' is not allowed to refer to the following paths:
/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-2.40-66
Shown below are chains that lead to the forbidden path(s).
/nix/store/0b7k85gg5r28gb54px9nq7iv5986mns9-hello-2.12.2
└───/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-2.40-66
```
@@ -1,17 +0,0 @@
---
synopsis: "libstore/binary-cache-store: don't cache narinfo on nix copy, remove negative entry"
issues: []
cls: [3789]
category: Fixes
credits: [ma27]
---
When using e.g. [Snix's nar-bridge](https://snix.dev/docs/components/overview/#nar-bridge) via
an `http`-store, Lix would create cache entries with a wrong URL to the NAR when uploading
a store-path.
This caused hard build failures for Hydra.
Lix doesn't create these entries on upload anymore. Instead, it only removes negative cache entries.
The cache entry for a narinfo is now created the first time, Lix queries the cache
for the previously uploaded store-path again.
-10
View File
@@ -1,10 +0,0 @@
---
synopsis: "Lix libraries can now be linked statically"
issues: [fj#789]
cls: [3775, 3778]
category: Fixes
credits: [alois31]
---
Previously the pkg-config files distributed with Lix were only suitable for dynamic linkage, causing "undefined reference to…" linker errors when trying to link statically.
Private dependency information has now been added to make static linkage work as expected without user intervention.
In addition, relevant static libraries are now prelinked to avoid strange failures due to missing static initializers.
-34
View File
@@ -1,34 +0,0 @@
---
synopsis: Symbols reuses once-allocated Value to reduce garbage collected allocations
issues: []
cls: [3308, 3300, 3314, 3310, 3312, 3313]
category: Improvements
credits: [raito, horrors, thubrecht, xokdvium, nan-git]
---
In the Lix evaluator, **symbols** represent immutable strings, like those used
for attribute names.
In evaluator design, such strings are typically [**interned**](https://en.wikipedia.org/wiki/String_interning), stored uniquely
to save memory, and Lix inherits this approach from the original C++ codebase.
However, some builtins, like `builtins.attrNames`, must return a `Value` type
that can represent any Nix value (strings, integers, lists, etc.).
Before this change, these builtins would create lists of `Value` objects by
allocating them through the garbage collector, copying the symbols string
content each time.
This allocation is unnecessary if the interned symbols themselves also hold a
`Value` representation allocated outside the garbage collector, since these
live for the full duration of evaluation.
As a result, this reduces the number of allocations, leading to:
* A significant drop in maximum [resident set memory](https://en.wikipedia.org/wiki/Resident_set_size) (RSS), with some large-scale
tests showing up to 11% (about 500 MiB) savings in large colmena deployments.
* A slight decrease in CPU usage during Nix evaluations.
This change is inspired by https://github.com/NixOS/nix/pull/13258 but the approach is different.
**Note** : [`xokdvium`](https://github.com/xokdvium) is the rightful author of https://gerrit.lix.systems/c/lix/+/3300 and the credit was missed on our end during the development process. We are deeply sorry for this mistake.
@@ -1,14 +0,0 @@
---
synopsis: Reject overflowing TOML integer literals
issues: []
cls: [3916]
category: "Breaking Changes"
credits: [emilazy]
---
The toml11 library used by Lix was updated. The new
version aligns with the [TOML v1.0.0 specifications
requirement](https://toml.io/en/v1.0.0#integer) to reject integer
literals that cannot be losslessly parsed. This means that code like
`builtins.fromTOML "v=0x8000000000000000"` will now produce an error
rather than silently saturating the integer result.
@@ -1,15 +0,0 @@
---
synopsis: uid-range depends on cgroups
issues: []
cls: [3230]
category: "Breaking Changes"
credits: [raito, horrors]
---
`uid-range` builds now depends on `cgroups`, an experimental feature.
`uid-range` builds already depended upon `auto-allocate-uids`, another experimental feature.
The rationale for doing so is that `uid-range` provides a sandbox with many
UIDs, this is useful for re-mapping them into a nested namespace, e.g. a
container.
@@ -1,10 +0,0 @@
---
synopsis: add description to zsh completions
issues: [fj#910]
cls: [3632]
category: "Fixes"
credits: [matthewbauer]
---
Emit descriptions when completing args in zsh completions. This uses the descriptions we already
provided in NIX\_GET\_COMPLETIONS.
+4
View File
@@ -39,6 +39,9 @@
- [Tuning Cores and Jobs](advanced-topics/cores-vs-jobs.md)
- [Verifying Build Reproducibility](advanced-topics/diff-hook.md)
- [Using the `post-build-hook`](advanced-topics/post-build-hook.md)
- [Pasta](advanced-topics/pasta.md)
- [Known Issues](known-issues/known-issues.md)
- [Limitations around non-isolated builds](known-issues/non-isolated-build-limits.md)
- [Command Reference](command-ref/command-ref.md)
- [Common Options](command-ref/opt-common.md)
- [Common Environment Variables](command-ref/env-common.md)
@@ -197,6 +200,7 @@
- [Release Notes](release-notes/release-notes.md)
- [Upcoming release](release-notes/rl-next.md)
<!-- RELENG-AUTO-INSERTION-MARKER (see releng/release_notes.py) -->
- [Lix 2.94 (2025-11-17)](release-notes/rl-2.94.md)
- [Lix 2.93 (2025-05-09)](release-notes/rl-2.93.md)
- [Lix 2.92 (2025-01-18)](release-notes/rl-2.92.md)
- [Lix 2.91 (2024-08-12)](release-notes/rl-2.91.md)
+19
View File
@@ -0,0 +1,19 @@
# [Pasta](https://passt.top/passt/about/): a network sandbox for fixed-output derivations
## Introduction
This section only applies to **Linux systems** as Pasta is a Linux-only measure.
Since [CVE-2025-46416](https://lix.systems/blog/2025-06-24-lix-cves/), the Lix project decided to adopt [Pasta](https://passt.top/passt/about/) for all fixed-output derivations, protecting against various attack vectors such as UNIX abstract domain sockets or more manipulation at the network layer from a malicious fixed-output derivation code.
Pasta acts as a translation layer between a layer-2 network interface and layer-4 sockets (TCP, UDP, ICMP/ICMPv6 echo) on the host. It requires no special privileges and can serve as a alternative to [SLiRP](https://en.wikipedia.org/wiki/Slirp) which was used [by Guix to mitigate the same problem](https://codeberg.org/guix/guix/commit/fb42611b8f27960304db5a1c0d33b8371dcde2a8).
## How to disable Pasta?
It's sufficient to pass `pasta-path = ""` in your `/etc/nix/nix.conf` or on the command line `--pasta-path ""` of a Lix invocation.
## Known issues surrounding Pasta
- Only the first DNS server in `/etc/resolv.conf` is considered: failover is not possible.
- [Reduced feature set compared to the Linux kernel](https://passt.top/passt/about/#features)
- [Performance overhead in multi-gigabits contexts and IMIX MTUs](https://passt.top/passt/about/#performance_1)
+1 -1
View File
@@ -58,7 +58,7 @@ $ nix-build flake:nixpkgs -A firefox
$ nix-build flake:github:NixOS/nixpkgs/release-23.11 -A firefox
```
Finally, for legacy reasons, if a path starts with `channel:`, the rest of the argument is interpreted as the name of a *nixpkgs* channel tarball to fetch from `https://nixos.org/channels/$CHANNEL_NAME/nixexprs.tar.xz`.
Finally, for legacy reasons, if a path starts with `channel:`, the rest of the argument is interpreted as the name of a *nixpkgs* channel tarball to fetch from `https://channels.nixos.org/$CHANNEL_NAME/nixexprs.tar.xz`.
This is a **hard coded URL** pattern and is *not* related to the subscribed channels managed by the [nix-channel](./nix-channel.md) command.
> **Note**: any of the special syntaxes may always be disambiguated by prefixing the path.
+3 -3
View File
@@ -11,7 +11,7 @@
Channels are a mechanism for referencing remote Nix expressions and conveniently retrieving their latest version.
The moving parts of channels are:
- The official channels listed at <https://nixos.org/channels>
- The official channels listed at <https://channels.nixos.org>
- The user-specific list of [subscribed channels](#subscribed-channels)
- The [downloaded channel contents](#channels)
- The [Nix expression search path](@docroot@/command-ref/conf-file.md#conf-nix-path), set with the [`-I` option](#opt-I) or the [`NIX_PATH` environment variable](#env-NIX_PATH)
@@ -77,9 +77,9 @@ This command has the following operations:
Subscribe to the Nixpkgs channel and run `hello` from the GNU Hello package:
```console
$ nix-channel --add https://nixos.org/channels/nixpkgs-unstable
$ nix-channel --add https://channels.nixos.org/nixpkgs-unstable
$ nix-channel --list
nixpkgs https://nixos.org/channels/nixpkgs
nixpkgs https://channels.nixos.org/nixpkgs
$ nix-channel --update
$ nix-shell -p hello --run hello
hello
+8
View File
@@ -170,6 +170,14 @@ Once you get a `Code-Review+2` vote on your change, it's rebased on `main` and C
If the change appears as "Rebase Required", you need to rebase it on `main` locally or via the Gerrit UI and wait for `Verified+1` before the Submit button is made active
The `Code-Review+2` from before will stick around through trivial rebases so no need to re-request review for a mere rebase.
## Interacting with the CI, Buildkite
We use Buildkite for our CI, usually you will not have to interact directly with it other than reviewing any errors it produces, which are linked from Gerrit.
However in certain cases a CI run will fail due to transient issues not related to your code and you will need to rerun it by hand.
You can log in to the CI via [SSO](https://buildkite.com/sso/lix-project). On your job you can then hit the "Retry failed" button to rerun it, normally you will not have a repeat of the transient issue.
If the build still fails on CI issues or all builds are failing this should be reported via [Zulip on #T-infra](https://zulip.lix.systems/#narrow/channel/7-T-infra) or [Matrix on #dev](https://matrix.to/#/%23dev%3Alix.systems?via=lix.systems).
## Building Lix with `nix`
To build a release version of Lix for the current operating system and CPU architecture:
+6
View File
@@ -62,6 +62,12 @@ For `installcheck` specifically, first run `just install` before running the tes
Finer-grained filtering within a test suite is also possible using the [--gtest_filter](https://google.github.io/googletest/advanced.html#running-a-subset-of-the-tests) command-line option to a test suite executable, or the `GTEST_FILTER` environment variable.
### Inspecting failures
The test suite emits logs in `build/meson-logs/`; the full textual failure logs are in `build/meson-logs/testlog.txt`.
If you want a much nicer experience of viewing the logs in a structured manner, use `xunit-viewer --results build/meson-logs/testlog.junit.xml --server` to view them in a web browser.
### Unit test support libraries
There are headers and code which are not just used to test the library in question, but also downstream libraries.
@@ -60,3 +60,10 @@ Then:
```console
$ docker run -ti lix
```
# Known issues
Lix in Docker is very sensitive to **functional** DNS resolution if you are running with [Pasta protections](../advanced-topics/pasta.md) which are enabled by default since Lix 2.93.0 on most distributions.
If you notice failure to download things, double check whether your **first** DNS entry in `/etc/resolv.conf` is functional.
Lix with [Pasta protections](../advanced-topics/pasta.md) does not support failing over the next entries.
@@ -0,0 +1 @@
This section lists known issues around Lix.
@@ -0,0 +1,21 @@
# Limitations of non-isolated builds
## What are non-isolated builds?
In Lix, only builds done on Linux with `sandbox = true` and a functioning
`pasta-path` are isolated from the rest of the system, all other builds are
considered non-isolated to some degree.
For example, running Lix with [Pasta](@docroot@/advanced-topics/pasta.md)
disabled makes the host network visible to fixed-output derivations, reducing
isolation somewhat.
## Clean termination of non-isolated builds
Non-isolated builds may not terminate cleanly in all cases due to limitations in Lix's process management.
This occurs when a build keeps the build log file descriptor open past the end of the actual build. A common cause of this are background tasks that aren't properly terminated before the main build process exits, for example: HTTP servers run as part of a test suite.
See [issue #1018](https://git.lix.systems/lix-project/lix/issues/1018) for an example.
The only solution is to manually terminate leftover processes in your derivation, including during failure scenarios.
@@ -41,7 +41,7 @@ install Lix. If this is not the case for some reason, you can add it
as follows:
```console
$ nix-channel --add https://nixos.org/channels/nixpkgs-unstable
$ nix-channel --add https://channels.nixos.org/nixpkgs-unstable
$ nix-channel --update
```
@@ -49,7 +49,7 @@ $ nix-channel --update
>
> On NixOS, youre automatically subscribed to a NixOS channel
> corresponding to your NixOS major release (e.g.
> <http://nixos.org/channels/nixos-21.11>). A NixOS channel is identical
> <https://channels.nixos.org/nixos-21.11>). A NixOS channel is identical
> to the Nixpkgs channel, except that it contains only Linux binaries
> and is updated only if a set of regression tests succeed.
+989
View File
@@ -0,0 +1,989 @@
# Lix 2.94 "Açaí na tigela" (2025-11-17)
# Lix 2.94.2 (2026-05-04)
## Fixes
- Fix unsigned overflow leading to out-of-band write in the NAR parser [cl/5553](https://gerrit.lix.systems/c/lix/+/5553)
The NAR parser contained an unsigned integer overflow that could be used by an
attacker to write arbitrary data to an unknown memory location and possibly
achieve code execution. A successful attack on the system-wide Lix daemon
could lead to privilege escalation to root. Any process that involves NAR
serialization could trigger this issue, including (but not limited to)
- local user interaction, whether the users are trusted or untrusted
- malicious substituters sending malformed NARs
- remote builders sending malformed build results
- remote daemons sending malformed inputs when requesting remote builds
Successful attacks using this bug require ASLR weakening of some sort, whether
by architecture constraints (e.g. on 32 bit systems, where little randomization
is possible) or system configuration (e.g. low ASLR entropy when loading
libraries), and millions of attempts. Local attacks can be mounted in less than
an hour. Remote builds typically require a fresh SSH connection for each build
and are thus less susceptible. Only one attempt can be made by substituters for
every build using substituters, they are thus not a likely vector for attacks.
At the time of writing, MITRE has not assigned this a CVE yet.
Many thanks to [eldritch horrors](https://git.lix.systems/pennae), [Raito Bezarius](https://git.lix.systems/raito), [edef](https://github.com/edef1c), and [sandydoo](https://github.com/sandydoo) for this.
# Lix 2.94.1 (2026-03-13)
# Lix 2.94.0 (2025-11-17)
## Breaking Changes
- Remove support for daemon protocols before 2.18 [fj#510](https://git.lix.systems/lix-project/lix/issues/510) [cl/3249](https://gerrit.lix.systems/c/lix/+/3249)
Support for daemon wire protocols belonging to Nix 2.17 or older have been
removed. This impacts clients connecting to the local daemon socket or any
remote builder configured using the `ssh-ng` protocol. Builders configured
with the `ssh` protocol are still accessible from clients such as Nix 2.3.
Additionally Lix will not be able to connect to an old daemon locally, and
remote build connections to old daemons is likewise limited to `ssh` urls.
We have decided to take this step because the old protocols are very badly
tested (if at all), maintenance overhead is high, and a number of problems
with their design makes it infeasible to remain backwards compatible while
we move Lix to a more modern RPC mechanism with better versioning support.
Many thanks to [eldritch horrors](https://git.lix.systems/pennae) for this.
- Remove impure derivations and dynamic derivations [fj#815](https://git.lix.systems/lix-project/lix/issues/815) [cl/3210](https://gerrit.lix.systems/c/lix/+/3210)
The `impure-derivations` and `dynamic-derivations` experimental feature have
been removed.
New impure or dynamic derivations cannot be created from this point forward, and
any such pre-existing store derivations canot be read or built any more.
Derivation outputs created by building such a derivation are still valid
until garbage collected; existing store derivations can only be garbage
collected.
Many thanks to [eldritch horrors](https://git.lix.systems/pennae) for this.
- First argument to `--arg`/`--argstr` must be a valid Nix identifier [fj#496](https://git.lix.systems/lix-project/lix/issues/496)
The first argument to `--arg`/`--argstr` must be a valid Nix identifier, i.e.
`nix-build --arg config.allowUnfree true` is now rejected.
This is because that invocation is a false friend since it doesn't set
`{ config = { allowUnfree = true; }; }`, but `{ "config.allowUnfree" = true; }`.
The idea is to change the behavior to the latter in the long-term. For that,
non-identifiers started giving a warning since 2.92 and are now rejected to give people
who depend on that a chance to notice and potentially weigh in on the discussion.
Many thanks to [ma27](https://git.lix.systems/ma27) for this.
- New cgroup delegation model [fj#537](https://git.lix.systems/lix-project/lix/issues/537) [fj#77](https://git.lix.systems/lix-project/lix/issues/77) [cl/3230](https://gerrit.lix.systems/c/lix/+/3230)
Builds using cgroups (i.e. `use-cgroups = true` and the experimental feature
`cgroups`) now always delegate a cgroup tree to the sandbox.
Compared to the original C++ Nix project, our delegation includes the
`subtree_control` file as well, which means that the sandbox can disable
certain controllers in its own cgroup tree.
This is a breaking change because this requires the Nix daemon to run with an
already delegated cgroup tree by the service manager.
## How to setup the cgroup tree with systemd?
systemd offers knobs to perform the required setup using:
```
[Service]
Delegate=yes
DelegateSubtree=supervisor
```
These directives are now included in our systemd packaging.
## What about using Nix as root without connecting to the daemon?
Builds run as `root` without connecting to the daemon relying on the cgroup
feature are now broken, i.e.
```console
# nix-build --use-cgroups --sandbox ... # will not work
```
Consider doing instead:
```console
# systemd-run --same-dir --wait -p Delegate=yes -p DelegateSubgroup=supervisor nix-build --use-cgroups ...
```
If you need to disable cgroups temporarily, remember that you can do
`NIX_CONF='include /etc/nix/nix.conf\nuse-cgroups = false' nix-build ...` or
`nix-build --no-use-cgroups ...`.
## What about other service managers than systemd?
systemd has a [documentation](https://systemd.io/CGROUP_DELEGATION/) on how to
handle cgroup delegation from service management perspective.
If your service manager adheres to systemd semantics, e.g. writing an extended
attribute `user.delegate=1` on the delegated cgroup tree directory and moving
the `nix-daemon` process inside a cgroup tree to respect the inner process
rule, then, the feature will work as well.
## Why is the cgroup feature still experimental?
While the cgroup feature unlocks many use cases, its behavior and integration (e.g. user experience), especially at scale on build farms or in multi-tenant environments, are not yet fully matured. Theres also potential for deeper systemd integration (e.g. using slices and scopes) that has not been fully explored.
To avoid locking in an unstable interface, were keeping the experimental flag until we have validated the feature across a broader range of scenarios, including but not limited to:
* Nix as root
* Hydra-style build farms
* Forgejo CI runners
* Shared remote builders
Many thanks to [Raito Bezarius](https://git.lix.systems/raito), [eldritch horrors](https://git.lix.systems/pennae), and [lheckemann](https://git.lix.systems/lheckemann) for this.
- Enable high compress ratio zstd compression by default for binary caches uploads [fj#945](https://git.lix.systems/lix-project/lix/issues/945) [cl/4503](https://gerrit.lix.systems/c/lix/+/4503)
The default compression method for binary cache uploads has been switched from
[`xz`](https://github.com/tukaani-project/xz) to
[`zstd`](https://github.com/facebook/zstd) to address performance and usability
issues related to modern hardware and high-speed connections.
## Why?
`xz` offers compression ratios but is single-threaded in our implementation and
very slow (~10-20 Mbps in our test), preventing full utilization of 100Mbps+
connections and significantly slowing decompression for end users.
Lix is a "compress once, decompress many" application: build farms can afford
to spend more time compressing to achieve a faster download transfer for the
end user. More importantly, it matters that all end users spend the least
amount of time decompressing.
## What about compression ratios?
`zstd` cannot achieve the same peaks as `xz`, nonetheless, `zstd` compression
level has been increased to level 12 by default to balance compression ratio
and performance.
## Synthetic test case data
* **xz** (default compression level) on a 4.4GB file: ~632MB (77s)
* **zstd** (level 12) on the same file: ~775MB (18s), 18% larger but 50% faster
* **zstd** (level 14): ~773MB (37s)
* **zstd** (level 16): ~735MB (66s)
Many thanks to [eldritch horrors](https://git.lix.systems/pennae) and [Raito Bezarius](https://git.lix.systems/raito) for this.
- Repl debugger uses `--ignore-try` by default [lix#666](https://git.lix.systems/lix-project/lix/issues/666) [cl/3488](https://gerrit.lix.systems/c/lix/+/3488)
Previously, using the debugger meant that exceptions thrown in `builtins.tryEval` would trigger the debugger.
However, this caught nixpkgs initialization code, which is unhelpful in the majority of cases, so we changed the default.
To get the old behaviour, use `--no-ignore-try`.
```
$ nix repl --debugger --expr 'with import <nixpkgs> {}; pkgs.hello'
Lix 2.94.0-dev-pre20250625-9a59106
Type :? for help.
error: file 'nixpkgs-overlays' was not found in the Nix search path (add it using $NIX_PATH or -I)
This exception occurred in a 'tryEval' call. Use --ignore-try to skip these.
Added 13 variables.
nix-repl>
```
Many thanks to [jade](https://git.lix.systems/jade) for this.
- Strings may now contain NUL bytes [cl/3968](https://gerrit.lix.systems/c/lix/+/3968)
Lix now allows strings to contain NUL bytes instead of silently truncating the
string before the first such byte. Notably NUL-bearing strings were allowed as
attribute names—even though the corresponding strings were not representable!—
leading to very surprising and incorrect behavior in corner cases, for example
```
nix-repl> builtins.fromJSON ''{"a": 1, "a\u0000b": 2}''
{
a = 1;
"ab" = 2;
}
nix-repl> builtins.attrNames (builtins.fromJSON ''{"a": 1, "a\u0000b": 2}'')
[
"a"
"a"
]
```
rather than the more correct but still with the terminal eating NUL on display
```
nix-repl> builtins.fromJSON ''{"a": 1, "a\u0000b": 2}''
{
a = 1;
"ab" = 2;
}
nix-repl> builtins.attrNames (builtins.fromJSON ''{"a": 1, "a\u0000b": 2}'')
[
"a"
"ab"
]
```
We consider this a breaking change since eval results *will* change if strings
with embedded NUL bytes were used, but we also consider the old behavior to be
not intentional (seeing how inconsistent it was) but merely fallout from a old
and misguided implementation decision to be worked around, not actually fixed.
Many thanks to [eldritch horrors](https://git.lix.systems/pennae) for this.
- Fixed output derivations can be run using `pasta` network isolation [fj#285](https://git.lix.systems/lix-project/lix/issues/285) [cl/3452](https://gerrit.lix.systems/c/lix/+/3452)
Fixed output derivations traditionally run in the host network namespace.
On Linux this allows such derivations to communicate with other sandboxes
or the host using the abstract Unix domains socket namespace; this hasn't
been unproblematic in the past and has been used in two distinct exploits
to break out of the sandbox. For this reason fixed output derivations can
now run in a network namespace (provided by [`pasta`]), restricted to TCP
and UDP communication with the rest of the world. When enabled this could
be a breaking change and we classify it as such, even though we don't yet
enable or require such isolation by default. We may enforce this in later
releases of Lix once we have sufficient confidence that breakage is rare.
[`pasta`]: https://passt.top/
Many thanks to [eldritch horrors](https://git.lix.systems/pennae) and [puck](https://git.lix.systems/puck) for this.
- Function equality semantics are more consistent, but still bad [cl/4556](https://gerrit.lix.systems/c/lix/+/4556) [cl/4244](https://gerrit.lix.systems/c/lix/+/4244)
Lix has inherited a historic misfeature from CppNix in the form of pointer
equality checks built into the `==` operator. These checks were originally
meant to optimize comparison for large sets, but they have the unfortunate
side effect of producing unexpected results when sets containing functions
are compared. **Lix 2.93 and earlier** behave as shown in the repl session
```
Lix 2.93.3
Type :? for help.
nix-repl> f = x: x
Added f.
nix-repl> f == f
false
nix-repl> let s.f = f; in s.f == s.f
false
nix-repl> # however!
{ inherit f; } == { inherit f; }
true
nix-repl> [ f ] == [ f ]
true
nix-repl> # and, in another twist:
[ f ] == map f [ f ]
false
```
Nixpkgs relies on sets containing functions being comparable, so we cannot
simply deprecate this behavior. Due to changes to the object model used by
Lix ***all* comparisons above now evaluate to `true`**. This is considered
a breaking change because eval results may differ, but we also consider it
minor because the optimization is unsound (c.f. `let l = [NaN]; in l == l`
evaluates to `true` even though floating point `NaN` is incomparable). Lix
intends to remove this optimization altogether in the future, but until we
can do that we instead make it slightly less broken to allow other, *real*
optimizations. Function equality comparison remains **undefined behavior**
and should not be relied upon in Nixlang code that intends to be portable.
Many thanks to [eldritch horrors](https://git.lix.systems/pennae) for this.
- `nix eval --write-to` has been removed [fj#974](https://git.lix.systems/lix-project/lix/issues/974) [fj#227](https://git.lix.systems/lix-project/lix/issues/227) [cl/4045](https://gerrit.lix.systems/c/lix/+/4045)
`nix eval --write-to` has been removed since it was underspecified, not widely
useful, and prone to security-sensitive misbehaviors. The feature was added in
Nix 2.4 purely for internal use in the build system. According to our research
it hasn't found any use outside of some distribution packaging scripts. Please
use structured outputs formats (such as JSON) instead as they have better type
fidelity, don't conflate attributes with paths, and are useful to other tools.
Many thanks to [eldritch horrors](https://git.lix.systems/pennae) for this.
- Remove the `parse-toml-timestamps` experimental feature
The `parse-toml-timestamps` experimental feature has been removed.
This feature used inband signalling to mark timestamps, making it
impossible to unambiguously parse TOML documents. It also exposed
implementationdefined behaviour in the TOML specification that
changed in the toml11 parser library.
Any interface for parsing TOML timestamps suitable for future
stabilization would necessarily involve breaking changes, and there
is no evidence this experimental feature is being relied upon in the
wild, so it has been removed.
Many thanks to [Emily](https://git.lix.systems/emilazy) for this.
- Reject overflowing TOML integer literals [cl/3916](https://gerrit.lix.systems/c/lix/+/3916)
The toml11 library used by Lix was updated. The new
version aligns with the [TOML v1.0.0 specifications
requirement](https://toml.io/en/v1.0.0#integer) to reject integer
literals that cannot be losslessly parsed. This means that code like
`builtins.fromTOML "v=0x8000000000000000"` will now produce an error
rather than silently saturating the integer result.
Many thanks to [Emily](https://git.lix.systems/emilazy) for this.
- uid-range depends on cgroups [cl/3230](https://gerrit.lix.systems/c/lix/+/3230)
`uid-range` builds now depends on `cgroups`, an experimental feature.
`uid-range` builds already depended upon `auto-allocate-uids`, another experimental feature.
The rationale for doing so is that `uid-range` provides a sandbox with many
UIDs, this is useful for re-mapping them into a nested namespace, e.g. a
container.
Many thanks to [Raito Bezarius](https://git.lix.systems/raito) and [eldritch horrors](https://git.lix.systems/pennae) for this.
## Features
- Add `inputs.self.submodules` flake attribute [fj#942](https://git.lix.systems/lix-project/lix/issues/942) [cl/3839](https://gerrit.lix.systems/c/lix/+/3839)
A port of <https://github.com/NixOS/nix/pull/12421> to Lix, which:
- adds a general `inputs.self` flake attribute that retroactively applies
configurations to a flake after it's been fetched, then triggers a refetch of
the flake with the new config.
- implements `inputs.self.submodules` that allows a flake to declare its need
for submodules, which are then fetched automatically with no need to pass
`?submodules=1` anywhere.
Many thanks to [Eelco Dolstra](https://github.com/edolstra) and [ورد](https://git.lix.systems/janw4ld) for this.
- Lix supports HTTP/3 behind `--http3` [fj#1033](https://git.lix.systems/lix-project/lix/issues/1033)
Lix now supports HTTP/3 for file transfers when the linked curl version
supports it.
By default, HTTP/3 is disabled notably due to performance issues reported in
mid-2024. [More details
here](https://daniel.haxx.se/blog/2024/06/10/http-3-in-curl-mid-2024/).
As of 2025-11-14, [NixOS official cache](https://cache.nixos.org) supports
HTTP/3 via Fastly. [More info
here](https://github.com/NixOS/infra/commit/157fa70e46afbd6338a32407be461fce05c57bf8).
To enable HTTP/3:
* Use `--http3` for individual transfers.
* Add `http3 = true` in your Nix configuration for permanent activation.
To disable it, use `--no-http3`.
**Note**:
* `--no-http2 --http3` will still enable both HTTP/2 and HTTP/3.
* `--http2 --http3` will prioritize HTTP/3 and fall back to HTTP/2 (and then
HTTP/1.1).
These are current CLI limitations. In the future, we plan to replace `--httpX`
options with `--max-http-version [1,2,3]` for easier version selection in Lix
transfers.
Many thanks to [Raito Bezarius](https://git.lix.systems/raito) and [eldritch horrors](https://git.lix.systems/pennae) for this.
- Add hyperlinks in attr set printing [cl/3790](https://gerrit.lix.systems/c/lix/+/3790)
The attribute set printer, such as is seen in `nix repl` or in type errors, now prints hyperlinks on each attribute name to its definition site if it is known.
Example: all of the attributes shown here are hyperlinks to the exact definition site of the attribute in question:
```
$ nix eval -f '<nixpkgs>' lib.licenses.mit
{ deprecated = false; free = true; fullName = "MIT License"; redistributable = true; shortName = "mit"; spdxId = "MIT"; url = "https://spdx.org/licenses/MIT.html"; }
```
Many thanks to [jade](https://git.lix.systems/jade) for this.
- Experimental integer coercion in interpolated strings [cl/3198](https://gerrit.lix.systems/c/lix/+/3198)
Ever tried interpolating a port number in Lix and ended up with something like this?
```nix
"http://${config.network.host}:${builtins.toString config.network.port}/"
```
You're not alone. Thousands of Lix users suffer every day from excessive `builtins.toString` syndrome. Its 2025, and we still have to cast integers to use them in strings.
To address this, Lix introduces the **`coerce-integers`** experimental feature. When enabled, interpolated integers within `"${...}"` are automatically coerced to strings. This allows writing:
```nix
"http://${config.network.host}:${config.network.port}/"
```
without additional conversion.
To enable the feature, you need to add `coerce-integers` to your set of experimental features.
### Stabilization criteria
The `coerce-integers` feature is experimental and limited strictly to string interpolation (`"${...}"`). Before stabilization, the following must hold:
1. **Interpolation-only**
Coercion must not occur outside interpolation. Expressions like `"" + 42` must continue to fail.
2. **Expectation that no explicit cast are being observed**
Cases observing explicit coercion (e.g., via `tryEval` gadget or similar) are expected not to be load-bearing in actual production code.
### Timeline for stabilization
If the feature proves safe and is widely adopted across typical usage (e.g., actual configurations in the wild turning on the flag, non-trivial out-of-tree projects using it), the experimental flag will be removed **after six months of active use or two Lix releases**, whichever is longer.
This avoids locking the feature in experimental status indefinitely, as happened with Flakes, while allowing time for validation and ecosystem integration.
### What about coercing floats or more?
Coercion beyond integers -- such as for floats or other types -- is **not planned**, even under an experimental flag. Questions like "what is the canonical string representation of a float?" involve subtle and context-dependent trade-offs. Without a robust and principled mechanism to define and audit such behavior, introducing broader coercion risks setting unintended and hard-to-reverse precedents. The scope of `coerce-integers` is intentionally narrow and will remain so.
In terms of outlook, a proposal like https://git.lix.systems/lix-project/lix/issues/835 could pave the way for a better solution.
Many thanks to [Raito Bezarius](https://git.lix.systems/raito), [delroth](https://github.com/delroth), [eldritch horrors](https://git.lix.systems/pennae), and [winter](https://git.lix.systems/winter) for this.
- nix-eval-jobs: support `--no-instantiate` flag [fj#987](https://git.lix.systems/lix-project/lix/issues/987)
`nix-eval-jobs` now supports a flag called `--no-instantiate`. With this enabled,
no write operations on the eval store are performed. That means, only evaluation is
performed, but derivations (and their gcroots) aren't created.
Many thanks to [mic92](https://github.com/mic92) and [ma27](https://git.lix.systems/ma27) for this.
## Improvements
- Assess current profile generations pointers in `nix doctor` [cl/3108](https://gerrit.lix.systems/c/lix/+/3108)
Added a new check to `nix doctor` that verifies whether the current generation of
a Nix profile can be resolved. This helps users diagnose issues with broken or
misconfigured profile symlinks.
This helps determining if you have broken symlinks or misconfigured packaging.
Many thanks to [Raito Bezarius](https://git.lix.systems/raito) for this.
- Improved susbtituter query speed
The code used to query substituters for derivations has been rewritten slightly
to take advantage of our asynchronous runtime. Such queries run for every build
that could download from substituters and processes every derivation that isn't
yet present on the local system. Previously Lix would use `http-connections` to
limit query concurrency, even for modern caches that support HTTP/2 and have no
limit on how many queries can be run concurrently on one single connection. Lix
no longer does this, resulting in approximately 60% reduction in query time for
medium-sized closures (e.g. NixOS system closures) during testing, although the
exact number depends greatly on local network latency and generally improves as
latency increases. Unlike previously setting `http-connections` to `1` or other
low values no longer brings a massive penalty in query performance if the cache
in use by the querying system supports HTTP/2 (as e.g. `cache.nixos.org` does).
Many thanks to [eldritch horrors](https://git.lix.systems/pennae) for this.
- Hitting Control-C twice always terminates Lix [cl/3574](https://gerrit.lix.systems/c/lix/+/3574)
Hitting Control-C or sending `SIGINT` to Lix now prints an informational message
if it is still running after on second, the second Control-C/`SIGINT` terminates
Lix immediately without waiting for any shutdown code to finish running. Lix did
not treat the second such event differently from first in the past; this made it
impossible to easily terminate running Lix processes that got stuck in e.g. very
expensive Nixlang code that never interacted with the store. We now terminate as
soon as the user hits Control-C again without waiting any more, to much the same
effect as putting Lix into the background and killing it immediately afterwards.
This means you can now more conveniently break out of stuck Nixlang evaluations:
```
nix-instantiate --eval --expr 'let f = n: if n == 0 then 0 else f (n - 1) + f (n - 1); in f 32'
^CStill shutting down. Press ^C again to abort all operations immediately.
^C
❌130
```
Many thanks to [eldritch horrors](https://git.lix.systems/pennae) for this.
- `--keep-failed` chowns the build directory to the user that request the build
Running a build with `--keep-failed` now chowns the temporary directory from the
builder user and group to the user that request the build if the build came from
a local user connected to the daemon. This makes inspecting failed derivations a
lot easier. On Linux the build directory made visible to the user will not be in
the same path as it was in the sandbox and continuing builds will usually break.
Many thanks to [eldritch horrors](https://git.lix.systems/pennae) for this.
- Better debuggability on fixed-output hash mismatches
Fixed-output derivation hash mismatch error messages will now include the path that was
produced unexpectedly, and this path will be registered as valid even if `--check`
(`nix-store`, `nix-build`) or `--rebuild` (`nix build`) was passed. This makes comparing
the expected path with the obtained path easier, and is useful for debugging when
upstreams modify previously-published releases or when changes in fixed-output
derivations' dependencies affect their output unexpectedly.
Many thanks to [lheckemann](https://git.lix.systems/lheckemann) for this.
- Add --raw flag to `nix-instantiate --eval` for unescaped output [gh#12119](https://github.com/NixOS/nix/pull/12119) [cl/2886](https://gerrit.lix.systems/c/lix/+/2886)
The `nix-instantiate --eval` command now supports a `--raw` flag. When used,
the result must be coercible to a string (as with `${...}`) and is printed
verbatim, without quotes or escaping.
Many thanks to [Martin Fischer](https://github.com/not-my-profile), [infinisil](https://github.com/infinisil), and [Raito Bezarius](https://git.lix.systems/raito) for this.
- Allow `nix store ls` to read nar listings from binary cache stores. [cl/3225](https://gerrit.lix.systems/c/lix/+/3225)
The `nix store ls` command now supports reading `.ls` nar listings from binary cache stores.
If a listing is detected for the store path being queried, the nar is no longer downloaded.
These nar listings are available in binary cache stores where the `write-nar-listing` option is
enabled, such as cache.nixos.org.
Many thanks to [Victor Fuentes](https://git.lix.systems/vlinkz) for this.
- show tree with references that lead to an output cycle [fj#551](https://git.lix.systems/lix-project/lix/issues/551)
When Lix determines a cyclic dependency between several outputs of a derivation,
it now displays which files in which outputs lead to an output cycle:
```
error: cycle detected in build of '/nix/store/gc5h2whz3rylpf34n99nswvqgkjkigmy-demo.drv' in the references of output 'bar' from output 'foo'.
Shown below are the files inside the outputs leading to the cycle:
/nix/store/3lrgm74j85nzpnkz127rkwbx3fz5320q-demo-bar
└───lib/libfoo: …stuffbefore /nix/store/h680k7k53rjl9p15g6h7kpym33250w0y-demo-baz andafter.…
→ /nix/store/h680k7k53rjl9p15g6h7kpym33250w0y-demo-baz
└───share/snenskek: …???? /nix/store/dm24c76p9y2mrvmwgpmi64rryw6x5qmm-demo-foo ....…
→ /nix/store/dm24c76p9y2mrvmwgpmi64rryw6x5qmm-demo-foo
└───bin/alarm: …textexttext/nix/store/3lrgm74j85nzpnkz127rkwbx3fz5320q-demo-bar abcabcabc.…
→ /nix/store/3lrgm74j85nzpnkz127rkwbx3fz5320q-demo-bar
```
Please note that showing the files and its contents while displaying the cycles only works
on Linux.
Many thanks to [ma27](https://git.lix.systems/ma27) for this.
- Lix now enables parallel marking in boehm-gc [fj#983](https://git.lix.systems/lix-project/lix/issues/983) [cl/3880](https://gerrit.lix.systems/c/lix/+/3880)
This brings a fairly modest performance improvement (~38% for `nixpkgs search hello`) to evaluation, especially in scenarios that necessitate larger heap sizes.
Many thanks to [Eelco Dolstra](https://github.com/edolstra) and [Seth Flynn](https://git.lix.systems/getchoo) for this.
- `disallowedRequisites` now reports chains of disallowed requisites [fj#334](https://git.lix.systems/lix-project/lix/issues/334) [fj#626](https://git.lix.systems/lix-project/lix/issues/626) [gh#10877](https://github.com/NixOS/nix/issues/10877)
When a build fails because of [`disallowedRequisites`](@docroot@/language/advanced-attributes.md#adv-attr-disallowedRequisites), the error message now includes the chain of references that led to the failure. This makes it easier to see in which derivations the chain can be broken, to resolve the problem.
Example:
```
$ nix-build -A hello
error: output '/nix/store/0b7k85gg5r28gb54px9nq7iv5986mns9-hello-2.12.2' is not allowed to refer to the following paths:
/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-2.40-66
Shown below are chains that lead to the forbidden path(s).
/nix/store/0b7k85gg5r28gb54px9nq7iv5986mns9-hello-2.12.2
└───/nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-glibc-2.40-66
```
Many thanks to [ma27](https://git.lix.systems/ma27) and [Robert Hensing](https://github.com/roberth) for this.
- Stack traces now summarize involved derivations at the bottom [cl/4493](https://gerrit.lix.systems/c/lix/+/4493)
When evaluation errors and a stack trace is printed,
For example, if I add Nheko to a NixOS `environment.systemPackages` without adding `olm-3.2.16` `nixpkgs.config.permittedInsecurePackages`, then without `--show-trace`, I previously got this:
```
error:
… while calling the 'head' builtin
at /nix/store/9v6qa656sq3xc58vkxslqy646p0ajj61-source/lib/attrsets.nix:1701:13:
1700| if length values == 1 || pred here (elemAt values 1) (head values) then
1701| head values
| ^
1702| else
… while evaluating the attribute 'value'
at /nix/store/9v6qa656sq3xc58vkxslqy646p0ajj61-source/lib/modules.nix:1118:7:
1117| // {
1118| value = addErrorContext "while evaluating the option `${showOption loc}':" value;
| ^
1119| inherit (res.defsFinal') highestPrio;
(stack trace truncated; use '--show-trace' to show the full trace)
error: Package olm-3.2.16 in /nix/store/9v6qa656sq3xc58vkxslqy646p0ajj61-source/pkgs/by-name/ol/olm/package.nix:37 is marked as insecure, refusing to evaluate.
< -snip the whole explanation about olm's CVEs- >
```
This doesn't tell me anything about where `olm-3.2.16` came from.
With `--show-trace`, there's 1155 lines to sift through, but does contain lines like "while evaluating derivation 'nheko-0.12.1'".
With this change, those lines are summarized and collected at the bottom, regardless of `--show-trace`:
```
error:
… while calling the 'head' builtin
at /nix/store/9v6qa656sq3xc58vkxslqy646p0ajj61-source/lib/attrsets.nix:1701:13:
1700| if length values == 1 || pred here (elemAt values 1) (head values) then
1701| head values
| ^
1702| else
… while evaluating the attribute 'value'
at /nix/store/9v6qa656sq3xc58vkxslqy646p0ajj61-source/lib/modules.nix:1118:7:
1117| // {
1118| value = addErrorContext "while evaluating the option `${showOption loc}':" value;
| ^
1119| inherit (res.defsFinal') highestPrio;
(stack trace truncated; use '--show-trace' to show the full trace)
error: Package olm-3.2.16 in /nix/store/9v6qa656sq3xc58vkxslqy646p0ajj61-source/pkgs/by-name/ol/olm/package.nix:37 is marked as insecure, refusing to evaluate.
< -snip the whole explanation about olm's CVEs- >
note: trace involved the following derivations:
derivation 'etc'
derivation 'dbus-1'
derivation 'system-path'
derivation 'nheko-0.12.1'
derivation 'mtxclient-0.10.1'
```
Now we finally know that olm was evaluated because of Nheko, without sifting through *thousands* of lines of error message.
Many thanks to [Qyriad](https://git.lix.systems/Qyriad) for this.
- Symbols reuses once-allocated Value to reduce garbage collected allocations [cl/3308](https://gerrit.lix.systems/c/lix/+/3308) [cl/3300](https://gerrit.lix.systems/c/lix/+/3300) [cl/3314](https://gerrit.lix.systems/c/lix/+/3314) [cl/3310](https://gerrit.lix.systems/c/lix/+/3310) [cl/3312](https://gerrit.lix.systems/c/lix/+/3312) [cl/3313](https://gerrit.lix.systems/c/lix/+/3313)
In the Lix evaluator, **symbols** represent immutable strings, like those used
for attribute names.
In evaluator design, such strings are typically [**interned**](https://en.wikipedia.org/wiki/String_interning), stored uniquely
to save memory, and Lix inherits this approach from the original C++ codebase.
However, some builtins, like `builtins.attrNames`, must return a `Value` type
that can represent any Nix value (strings, integers, lists, etc.).
Before this change, these builtins would create lists of `Value` objects by
allocating them through the garbage collector, copying the symbols string
content each time.
This allocation is unnecessary if the interned symbols themselves also hold a
`Value` representation allocated outside the garbage collector, since these
live for the full duration of evaluation.
As a result, this reduces the number of allocations, leading to:
* A significant drop in maximum [resident set memory](https://en.wikipedia.org/wiki/Resident_set_size) (RSS), with some large-scale
tests showing up to 11% (about 500 MiB) savings in large colmena deployments.
* A slight decrease in CPU usage during Nix evaluations.
This change is inspired by https://github.com/NixOS/nix/pull/13258 but the approach is different.
**Note** : [`xokdvium`](https://github.com/xokdvium) is the rightful author of https://gerrit.lix.systems/c/lix/+/3300 and the credit was missed on our end during the development process. We are deeply sorry for this mistake.
Many thanks to [Raito Bezarius](https://git.lix.systems/raito), [eldritch horrors](https://git.lix.systems/pennae), [Tom Hubrecht](https://git.lix.systems/tom-hubrecht), [xokdvium](https://github.com/xokdvium), and [NaN-git](https://github.com/NaN-git) for this.
## Fixes
- `build-dir` no longer defaults to `temp-dir` [cl/3453](https://gerrit.lix.systems/c/lix/+/3453)
The directory in which temporary build directories are created no longer defaults
to the value of the `temp-dir` setting to avoid builders making their directories
world-accessible. This behavior has been used to escape the build sandbox and can
cause build impurities even when not used maliciously. We now default to `builds`
in `NIX_STATE_DIR` (which is `/nix/var/nix/b` in the default configuration).
Many thanks to [eldritch horrors](https://git.lix.systems/pennae) for this.
- Global certificate authorities are copied inside the builder's environment [gh#12698](https://github.com/NixOS/nix/issues/12698) [fj#885](https://git.lix.systems/lix-project/lix/issues/885) [cl/3765](https://gerrit.lix.systems/c/lix/+/3765)
Previously, CA certificates were only installed at
`/etc/ssl/certs/ca-certificates.crt` for sandboxed builds on Linux.
This setup was insufficient in light of recent changes in `nixpkgs`, which now
enforce HTTPS usage for `fetchurl`, even for fixed-output derivations, to
mitigate confidentiality risks such as `netrc` or credentials leakage.
`nixpkgs` still make use of a special package called `cacerts` which contains a
copy of the CA certificates maintained by Nixpkgs and added as a reference for
TLS-enabled fetchers.
As a result, having a consistent and trusted certificate authority in all
builder environments is becoming more essential.
On `nix-darwin`, the `NIX_SSL_CERT_FILE` environment variable is always
explicitly defined, but it is ignored by the sandbox setup.
Simultaneously, Nix evaluates and propagates impure environment variables via
`lib.proxyImpureEnvVars`, meaning that if `NIX_SSL_CERT_FILE` is set (which
influences the default value for `ssl-cert-file`), it will be forwarded
unchanged into the builder environment.
However, on Linux, Nix also *copies* the CA file into the sandbox, creating a
discrepancy between the value of `NIX_SSL_CERT_FILE` and the actual trusted
certificate path used during the build.
This divergence caused confusion and was partially addressed by attempts to
whitelist the CA path in the Darwin sandbox (see cl/2906), but that approach
involved a non-trivial path canonicalization step and is not as general as this one.
To address this properly, we now emit a warning and override
`NIX_SSL_CERT_FILE` inside the builder, explicitly pointing it to the CA file
copied into the sandbox.
This eliminates ambiguity between `NIX_SSL_CERT_FILE`
and `ssl-cert-file`, ensuring consistent trust anchors across platforms.
This warning might become a hard error as we figure out what to do regarding
`lib.proxyImpureEnvVars` in nixpkgs.
The behavior has been verified across sandboxed and unsandboxed builds on both
Linux and Darwin.
As a consequence of this change, approximately 500KB of CA certificate data is
now unconditionally copied into the build directory for fixed-output
derivations.
While this ensures consistent trust verification without having to restart the
daemon after system upgrades, it may introduce a slight overhead in build
performance. At present, no optimizations have been implemented to avoid this
copy, but if this overhead proves noticeable in your workflows, please open an
issue so we can evaluate and possibly implement different strategies to render
trust anchors visible.
Many thanks to [Raito Bezarius](https://git.lix.systems/raito) and [Emily](https://git.lix.systems/emilazy) for this.
- libstore: exponential backoff for downloads [lix#932](https://git.lix.systems/lix-project/lix/issues/932) [cl/3856](https://gerrit.lix.systems/c/lix/+/3856)
The connection timeout when downloading from e.g. a binary cache is exponentially
increased per failure. The option `connect-timeout` is now an alias to `max-connect-timeout`
which is the maximum value for a timeout. The start value is controlled
by `initial-connect-timeout` which is `5` by default.
Many thanks to [ma27](https://git.lix.systems/ma27) for this.
- Fix develop shells for derivations with escape codes [fj#991](https://git.lix.systems/lix-project/lix/issues/991) [cl/4154](https://gerrit.lix.systems/c/lix/+/4154) [cl/4155](https://gerrit.lix.systems/c/lix/+/4155)
ASCII control characters (including `\e`, used for ANSI escape codes) in derivation variables are now correctly escaped for `nix develop` and `nix print-dev-env`, instead of erroring.
Many thanks to [Qyriad](https://git.lix.systems/Qyriad) for this.
- nix-store --delete: always remove obsolete hardlinks [cl/3188](https://gerrit.lix.systems/c/lix/+/3188)
Deleting specific paths using `nix-store --delete` or `nix store
delete` previously did not delete hard links created by `nix-store
--optimise` even if they became obsolete, unless _all_ of the given
paths were deleted successfully. Now, hard links are always cleaned
up, even if some of the given paths could not be deleted.
Many thanks to [lheckemann](https://git.lix.systems/lheckemann) for this.
- Report GC statistics correctly [cl/3188](https://gerrit.lix.systems/c/lix/+/3188)
Deleting specific paths using `nix-store --delete` or `nix store delete` previously did
not report statistics correctly when some of the paths could not be deleted, even if
others were deleted:
```
$ nix store delete /nix/store/9bwryidal9q3g91cjm6xschfn4ikd82q-hello-2.12.1 --delete-closure -v
finding garbage collector roots...
deleting '/nix/store/9bwryidal9q3g91cjm6xschfn4ikd82q-hello-2.12.1'
0 store paths deleted, 0.00 MiB freed
error: Cannot delete some of the given paths because they are still alive. Paths not deleted:
k9bxzr1l92r5y6mihrkbpbr3fmc8qszx-libidn2-2.3.8
mbx9ii53lzjlrsnlrfmzpwm33ynljwdn-libunistring-1.3
rf8hcy6bldxdqc0g6q1dcka1vh47x69s-xgcc-14.2.1.20250322-libgcc
vbrdc5wgzn0w1zdp10xd2favkjn5fk7y-glibc-2.40-66
To find out why, use nix-store --query --roots and nix-store --query --referrers.
```
Many thanks to [lheckemann](https://git.lix.systems/lheckemann) for this.
- Fallback to safe temp dir when build-dir is unwritable [fj#876](https://git.lix.systems/lix-project/lix/issues/876) [cl/3501](https://gerrit.lix.systems/c/lix/+/3501)
Non-daemon builds started failing with a permission error after introducing the `build-dir` option:
```
$ nix build --store ~/scratch nixpkgs#hello --rebuild
error: creating directory '/nix/var/nix/builds/nix-build-hello-2.12.2.drv-0': Permission denied
```
This happens because:
1. These builds are not run via the daemon, which owns `/nix/var/nix/builds`.
2. The user lacks permissions for that path.
We considered making `build-dir` a store-level option and defaulting it to `<chroot-root>/nix/var/nix/builds` for chroot stores, but opted instead for a fallback: if the default fails, Nix now creates a safe build directory under `/tmp`.
To avoid CVE-2025-52991, the fallback uses an extra path component between `/tmp` and the build dir.
**Note**: this fallback clutters `/tmp` with build directories that are not cleaned up. To prevent this, explicitly set `build-dir` to a path managed by Lix, even for local workloads.
Many thanks to [Raito Bezarius](https://git.lix.systems/raito) and [eldritch horrors](https://git.lix.systems/pennae) for this.
- Parse overflowing JSON number literals as floatingpoint [cl/3919](https://gerrit.lix.systems/c/lix/+/3919)
Previously, `builtins.fromJSON "-9223372036854775809"` would
return a floatingpoint number, while `builtins.fromJSON
"9223372036854775808"` would cause an evaluation error. This was
introduced with the banning of integer overflow in Lix 2.91; previously
the latter would result in C++ undefined behaviour. These cases are
now treated consistently with JSONs model of a single numeric type,
and JSON number literals that do not fit in a Nixlanguage integer
will be parsed as floatingpoint numbers.
Many thanks to [Emily](https://git.lix.systems/emilazy) for this.
- Fix handling of OSC codes in terminal output [fj#160](https://git.lix.systems/lix-project/lix/issues/160) [cl/3143](https://gerrit.lix.systems/c/lix/+/3143)
OSC codes in terminal output are now handled correctly, where OSC 8 (hyperlink) is preserved any
time color codes are allowed and all other OSC codes are stripped out. This applies not only to
output from build commands but also to rendered documentation in the REPL.
Many thanks to [lilyball](https://git.lix.systems/lilyball) for this.
- Fix nix develop for derivations that rejects dependencies with structured attrs [fj#997](https://git.lix.systems/lix-project/lix/issues/997) [cl/4182](https://gerrit.lix.systems/c/lix/+/4182)
For the sake of concision, we refer to `disallowedReferences` in what follows,
but all output checks were equally fixed:
`{dis,}allowed{References,Requisites}`.
Derivations can define *output checks* to reject unwanted dependencies, such as
interpreters like `bash` or compilers like `gcc`. This can be done in two ways:
* **Legacy style**: `disallowedReferences = [ ... ]` in the environment.
* **Structured attrs**: `outputChecks.<output>.disallowedReferences = [ ... ]`,
typically used in `__json`.
Only the structured form supports derivations with multiple outputs.
`nix develop` internally rewrites derivations to create development shells. It
relied on the legacy `disallowedReferences`, and failed to honor the structured
variant. This led to broken shells in cases where `bashInteractive` was
explicitly disallowed using structured output checks, e.g. `nix develop
nixpkgs#systemd` after the "bash-less NixOS" changes.
This fix teaches `nix develop` to respect structured output checks, restoring
support for such derivations.
Many thanks to [Raito Bezarius](https://git.lix.systems/raito) for this.
- nix-eval-jobs: retain NIX_PATH [cl/3859](https://gerrit.lix.systems/c/lix/+/3859)
`nix-eval-jobs` doesn't clear the `NIX_PATH` from the environment anymore. This matches the behavior
of [upstream version `2.30`](https://github.com/nix-community/nix-eval-jobs/releases/tag/v2.30.0).
Many thanks to [ma27](https://git.lix.systems/ma27) and [mic92](https://github.com/mic92) for this.
- Remove reliance on Bash for remote stores via SSH [fj#830](https://git.lix.systems/lix-project/lix/issues/830) [fj#805](https://git.lix.systems/lix-project/lix/issues/805) [fj#304](https://git.lix.systems/lix-project/lix/issues/304) [cl/3159](https://gerrit.lix.systems/c/lix/+/3159)
The pre-flight `echo started` handshake -- added years ago to catch race conditions -- has been removed.
After removal of connection sharing in Lix 2.93, it required a Bash-compatible shell and a standard `echo`, so it failed on:
* builders protected by `ForceCommand` wrappers (e.g. `nix-remote-build`),
* BusyBox / initrd images with no Bash,
* hosts using non-POSIX shells such as Nushell.
The race the probe once addressed was tied to SSH connection-sharing -- since connection-sharing code has already been removed, the probe is now pointless.
Real connection or protocol errors are now left to SSH/Nix to report directly.
This is technically a breaking change if you had scripts that relied on the literal "started" which needs to be updated to rely on other signals, e.g., exit codes.
Many thanks to [Raito Bezarius](https://git.lix.systems/raito) for this.
- repl-overlays now work in the debugger for flakes [fj#777](https://git.lix.systems/lix-project/lix/issues/777) [cl/3398](https://gerrit.lix.systems/c/lix/+/3398)
Due to a bug, it was previously not possible to use the debugger on flakes with repl-overlays, or with pure evaluation in general:
```
$ nix repl --pure-eval
Lix 2.94.0-dev-pre20250617-87d99da
Type :? for help.
Loading 'repl-overlays'...
error: access to absolute path '/Users/jade/.config/nix/repl.nix' is forbidden in pure eval mode (use '--impure' to override)
```
This is now fixed.
The contents of the repl-overlays file itself (i.e. most typically the top level lambda in it) will be evaluated in impure mode.
It may be necessary to use `builtins.seq` to force the impure operations to happen first if one wants to do impure operations inside a repl-overlays file in pure evaluation mode.
Many thanks to [jade](https://git.lix.systems/jade) for this.
- `nix-shell` default shell directory is not `/tmp` anymore for `$NIX_BUILD_TOP` [fj#940](https://git.lix.systems/lix-project/lix/issues/940)
Previously, Lix `nix-shell`s could exit non-zero status when `stdenv`'s `dumpVars` phase failed to write to `$NIX_BUILD_TOP/env-vars`, despite `dumpVars` being intended as a debugging aid.
This happens when `TMPDIR` is not set and defaults therefore to `/tmp`, resulting in a `/tmp/env-vars` global file that every `nix-shell` wants to write.
We fix this issue by reusing a pre-created, unique, and writable location, as the build top directory, avoiding shell exiting from write failures silently.
Many thanks to [Raito Bezarius](https://git.lix.systems/raito) for this.
- libstore/binary-cache-store: don't cache narinfo on nix copy, remove negative entry [cl/3789](https://gerrit.lix.systems/c/lix/+/3789)
When using e.g. [Snix's nar-bridge](https://snix.dev/docs/components/overview/#nar-bridge) via
an `http`-store, Lix would create cache entries with a wrong URL to the NAR when uploading
a store-path.
This caused hard build failures for Hydra.
Lix doesn't create these entries on upload anymore. Instead, it only removes negative cache entries.
The cache entry for a narinfo is now created the first time, Lix queries the cache
for the previously uploaded store-path again.
Many thanks to [ma27](https://git.lix.systems/ma27) for this.
- Lix libraries can now be linked statically [fj#789](https://git.lix.systems/lix-project/lix/issues/789) [cl/3775](https://gerrit.lix.systems/c/lix/+/3775) [cl/3778](https://gerrit.lix.systems/c/lix/+/3778)
Previously the pkg-config files distributed with Lix were only suitable for dynamic linkage, causing "undefined reference to…" linker errors when trying to link statically.
Private dependency information has now been added to make static linkage work as expected without user intervention.
In addition, relevant static libraries are now prelinked to avoid strange failures due to missing static initializers.
Many thanks to [alois31](https://git.lix.systems/alois31) for this.
- add description to zsh completions [fj#910](https://git.lix.systems/lix-project/lix/issues/910) [cl/3632](https://gerrit.lix.systems/c/lix/+/3632)
Emit descriptions when completing args in zsh completions. This uses the descriptions we already
provided in NIX\_GET\_COMPLETIONS.
Many thanks to [matthewbauer](https://github.com/matthewbauer) for this.
## Miscellany
- Deprecation of CA derivations, dynamic derivations, and impure derivations [fj#815](https://git.lix.systems/lix-project/lix/issues/815)
Content-addressed derivations are now deprecated and slated for removal in Lix 2.94.
We're doing this because the CA derivation system has been a known cause of problems
and inconsistencies, is unmaintained, habitually makes improving the store code very
difficult (or blocks such improvements outright), and is beset by a number of design
flaws that in our opinion cannot be fixed without a full reimplementation from zero.
Dynamic derivations and impure derivations are built on the CA derivation framework,
and owing to this they too are deprecated and slated for removal in another release.
+1 -1
View File
@@ -8,7 +8,7 @@
tag ? "latest",
bundleNixpkgs ? true,
channelName ? "nixpkgs",
channelURL ? "https://nixos.org/channels/nixpkgs-unstable",
channelURL ? "https://channels.nixos.org/nixpkgs-unstable",
extraPkgs ? [ ],
maxLayers ? 100,
nixConf ? { },
Generated
+3 -3
View File
@@ -108,11 +108,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1758391731,
"narHash": "sha256-UuwQoPWv13DVKMveeev+F0OC/N95AOmAz6SzCuGhxjQ=",
"lastModified": 1757198069,
"narHash": "sha256-m3VUcOD4rTs8J7S+3dOjWMrAjw6RcITC3XYQ98zhEFs=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "3f00d36f15e16e0471d9ca1e8f88958941fa970a",
"rev": "0747026fc57ecb9c28901c7f7a2b5dc40e8af43c",
"type": "github"
},
"original": {
+19 -2
View File
@@ -88,7 +88,7 @@
else
"pre${
builtins.substring 0 8 (self.lastModifiedDate or self.lastModified or "19700101")
}_${self.shortRev or "dirty"}";
}-dev_${self.shortRev or "dirty"}";
linux32BitSystems = [ "i686-linux" ];
linux64BitSystems = [
@@ -100,6 +100,7 @@
"x86_64-darwin"
"aarch64-darwin"
];
nonDarwinSystems = linuxSystems;
systems = linuxSystems ++ darwinSystems;
# If you add something here, please update the list in doc/manual/src/contributing/hacking.md.
@@ -259,6 +260,19 @@
nativeBuildInputs = prevAttrs.nativeBuildInputs ++ [ final.buildPackages.bmake ];
postInstall = lib.replaceStrings [ "lowdown.so.1" ] [ "lowdown.so.2" ] prevAttrs.postInstall;
});
capnproto = prev.capnproto.overrideAttrs (old: {
patches =
old.patches or [ ]
++ [
# backport of https://github.com/capnproto/capnproto/pull/1810
./misc/capnproto-promise-nodiscard.patch
]
++ lib.optionals (lib.versionOlder old.version "1.2.0") [
# backport of https://github.com/capnproto/capnproto/pull/2296
./misc/capnproto-monotonic-clocks-are-a-lie.patch
];
});
};
in
{
@@ -371,7 +385,10 @@
;
}
// {
nix-eval-jobs = forAllSystems (system: self.packages.${system}.nix-eval-jobs.tests.nix-eval-jobs);
# the n-e-j test suite is unusably slow in darwin ci. disbled until anywho fixes this.
nix-eval-jobs = (lib.genAttrs nonDarwinSystems) (
system: self.packages.${system}.nix-eval-jobs.tests.nix-eval-jobs
);
# This is x86_64-linux only, just because we have significantly
# cheaper x86_64-linux compute in CI.
+195 -110
View File
@@ -1,15 +1,18 @@
#include "lix/libstore/path.hh"
#include "lix/libutil/async.hh"
#include "lix/libutil/c-calls.hh"
#include "lix/libutil/error.hh"
#include "lix/libutil/file-descriptor.hh"
#include "lix/libutil/logging-rpc.hh"
#include "lix/libutil/logging.hh"
#include "lix/libutil/rpc.hh"
#include "lix/libutil/types-rpc.hh"
#include "lix/libutil/types-rpc.hh" // IWYU pragma: keep
#include "lix/libutil/types.hh"
#include <algorithm>
#include <capnp/rpc-twoparty.h>
#include <chrono>
#include <cstring>
#include <future>
#include <exception>
#include <kj/async.h>
#include <kj/time.h>
#include <set>
#include <memory>
@@ -43,9 +46,11 @@ namespace {
struct Instance final : rpc::build_remote::HookInstance::Server
{
unsigned int maxBuildJobs;
bool initialized = false, used = false;
Instance(unsigned int maxBuildJobs) : maxBuildJobs(maxBuildJobs) {}
kj::Promise<void> init(InitContext context) override;
kj::Promise<void> buildImpl(BuildContext context);
kj::Promise<void> build(BuildContext context) override;
};
}
@@ -183,27 +188,62 @@ struct BuilderConnection
// start the thread that reads ssh stderr and turns it into log items.
// this future *must* outlive sshStore, otherwise it will never finish
std::future<void> startLogThread(int intoFD)
{
kj::Promise<Result<void>> startLogThread(std::string buildDescription, std::string drvPath)
try {
if (!logPipe.readSide) {
return {};
co_return result::success();
}
logPipe.writeSide.close();
return std::async(
std::launch::async,
[](int from, int to) {
AsyncIoRoot aio;
auto reader = AIO().lowLevelProvider.wrapInputFd(from);
auto writer = AIO().lowLevelProvider.wrapOutputFd(to);
reader->pumpTo(*writer).wait(aio.kj.waitScope);
},
logPipe.readSide.get(),
intoFD
// NOTE this is very similar to handleBuilderOutput in DerivationGoal, but unlike
// the derivation goal we do not need to handle EIO from a pty here. we also have
// no timeouts or limits to keep track of, which makes deduplication less useful.
auto act = logger->startActivity(
lvlInfo, actBuild, buildDescription, Logger::Fields{drvPath, storeUri, 1, 1}
);
std::map<ActivityId, Activity> activities;
auto reader = AIO().lowLevelProvider.wrapInputFd(logPipe.readSide.get());
LogLineSplitter splitter;
auto flushLine = [&](const std::string & line) {
if (const auto state =
handleJSONLogMessage(line, act, activities, "the derivation builder"))
{
return *state;
} else {
return act.result(resBuildLogLine, line);
}
};
auto buf = kj::heapArray<char>(4096);
while (true) {
const auto got = co_await reader->tryRead(buf.begin(), 1, buf.size());
if (got == 0) {
break;
}
std::string_view data{buf.begin(), got};
while (!data.empty()) {
if (auto line = splitter.feed(data)) {
if (flushLine(*line) == Logger::BufferState::NeedsFlush) {
TRY_AWAIT(act.getLogger().flush());
}
}
}
}
if (auto line = splitter.finish(); !line.empty()) {
(void) flushLine(line);
TRY_AWAIT(act.getLogger().flush());
}
co_return result::success();
} catch (...) {
co_return result::current_exception();
}
};
@@ -212,21 +252,16 @@ struct AcceptedBuild final : rpc::build_remote::HookInstance::AcceptedBuild::Ser
ref<Store> store;
StorePath drvPath;
BuilderConnection builder;
rpc::build_remote::HookInstance::BuildLogger::Client buildLogger;
bool used = false;
AcceptedBuild(
ref<Store> store,
StorePath drvPath,
BuilderConnection builder,
rpc::build_remote::HookInstance::BuildLogger::Client buildLogger
)
AcceptedBuild(ref<Store> store, StorePath drvPath, BuilderConnection builder)
: store(store)
, drvPath(drvPath)
, builder(std::move(builder))
, buildLogger(std::move(buildLogger))
{
}
kj::Promise<void> runImpl(RunContext context);
kj::Promise<void> run(RunContext context) override;
};
@@ -235,7 +270,7 @@ enum class BuildRejected { Temporarily, Permanently };
static kj::Promise<Result<std::variant<BuildRejected, BuilderConnection>>> connectToBuilder(
const ref<Store> & store,
const std::optional<StorePath> & drvPath,
const StorePath & drvPath,
Machines & machines,
const unsigned int maxBuildJobs,
const bool amWilling,
@@ -255,7 +290,7 @@ try {
bool canBuildLocally = amWilling && couldBuildLocally;
/* Error ignored here, will be caught later */
mkdir(currentLoad.c_str(), 0777);
(void) sys::mkdir(currentLoad, 0777);
while (true) {
bestSlotLock.reset();
@@ -272,7 +307,7 @@ try {
} else {
printSelectionFailureMessage(
couldBuildLocally ? lvlChatty : lvlWarn,
drvPath ? drvPath->to_string() : "<unknown>",
drvPath.to_string(),
machines,
neededSystem,
requiredFeatures
@@ -294,8 +329,8 @@ try {
Pipe logPipe;
try {
Activity act(
*logger, lvlTalkative, actUnknown, fmt("connecting to '%s'", bestMachine->storeUri)
auto act = logger->startActivity(
lvlTalkative, actUnknown, fmt("connecting to '%s'", bestMachine->storeUri)
);
std::tie(sshStore, logPipe) = TRY_AWAIT(bestMachine->openStore());
@@ -321,8 +356,6 @@ try {
static int main_build_remote(AsyncIoRoot & aio, std::string programName, Strings argv)
{
{
logger = makeJSONLogger(*logger);
/* Ensure we don't get any SSH passphrase or host key popups. */
unsetenv("DISPLAY");
unsetenv("SSH_ASKPASS");
@@ -335,104 +368,146 @@ static int main_build_remote(AsyncIoRoot & aio, std::string programName, Strings
verbosity = (Verbosity) std::stoll(argv.front());
FdSource source(STDIN_FILENO);
/* Read the parent's settings. */
while (readNum<unsigned>(source)) {
auto name = readString(source);
auto value = readString(source);
settings.set(name, value);
}
auto maxBuildJobs = settings.maxBuildJobs;
settings.maxBuildJobs.set("1"); // hack to make tests with local?root= work
initPlugins();
auto conn = aio.kj.lowLevelProvider->wrapUnixSocketFd(1);
capnp::TwoPartyServer srv(kj::heap<Instance>(maxBuildJobs));
capnp::TwoPartyServer srv(kj::heap<Instance>());
srv.accept(*conn, 1).wait(aio.kj.waitScope);
return 0;
}
}
kj::Promise<void> Instance::build(BuildContext context)
kj::Promise<void> Instance::init(InitContext context)
{
try {
// FIXME this does not open a daemon connection for historical reasons.
// we may create a lot of build hook instances, and having each of them
// also create a daemon instance is inefficient and wasteful. in future
// versions of the build hook (where we don't need one hook process per
// build) we should change this to using a daemon connection, ideally a
// daemon connection provided by the parent via file descriptor passing
auto store = TRY_AWAIT(openStore(settings.storeUri, {}, AllowDaemon::Disallow));
/* It would be more appropriate to use $XDG_RUNTIME_DIR, since
that gets cleared on reboot, but it wouldn't work on macOS. */
auto currentLoadName = "/current-load";
if (auto localStore = store.try_cast_shared<LocalFSStore>())
currentLoad = std::string { localStore->config().stateDir } + currentLoadName;
else
currentLoad = settings.nixStateDir + currentLoadName;
auto machines = getMachines();
debug("got %d remote builders", machines.size());
if (machines.empty()) {
context.getResults().initResult().initGood().setDeclinePermanently();
co_return;
if (initialized) {
throw Error("build hook can only be initialized once");
}
auto amWilling = context.getParams().getAmWilling();
auto neededSystem = rpc::to<std::string>(context.getParams().getNeededSystem());
auto drvPath = from(context.getParams().getDrvPath(), *store);
auto requiredFeatures =
rpc::to<std::set<std::string>>(context.getParams().getRequiredFeatures());
auto buildLogger = context.getParams().getBuildLogger();
logger = rpc::log::makeRpcLoggerClient(context.getParams().getLogger());
auto result = TRY_AWAIT(connectToBuilder(
store, drvPath, machines, maxBuildJobs, amWilling, neededSystem, requiredFeatures
));
if (auto immediateResponse = std::get_if<BuildRejected>(&result)) {
switch (*immediateResponse) {
case BuildRejected::Temporarily:
context.getResults().initResult().initGood().setPostpone();
co_return;
case BuildRejected::Permanently:
context.getResults().initResult().initGood().setDecline();
co_return;
}
/* Read the parent's settings. */
for (const auto & [name, value] : rpc::to<StringMap>(context.getParams().getSettings())) {
settings.set(name, value);
}
auto builder = std::get_if<BuilderConnection>(&result);
assert(builder);
maxBuildJobs = settings.maxBuildJobs;
settings.maxBuildJobs.set("1"); // hack to make tests with local?root= work
auto ac = context.getResults().initResult().initGood().initAccept();
RPC_FILL(ac, setMachineName, builder->storeUri);
ac.setMachine(kj::heap<AcceptedBuild>(store, drvPath, std::move(*builder), buildLogger));
initPlugins();
initialized = true;
context.getResults().initResult().setGood();
} catch (...) {
RPC_FILL(context.getResults(), initResult, std::current_exception());
}
return kj::READY_NOW;
}
kj::Promise<void> Instance::buildImpl(BuildContext context)
{
if (!initialized) {
throw Error("build hook not fully initialized");
}
// FIXME this does not open a daemon connection for historical reasons.
// we may create a lot of build hook instances, and having each of them
// also create a daemon instance is inefficient and wasteful. in future
// versions of the build hook (where we don't need one hook process per
// build) we should change this to using a daemon connection, ideally a
// daemon connection provided by the parent via file descriptor passing
auto store = TRY_AWAIT(openStore(settings.storeUri, {}, AllowDaemon::Disallow));
/* It would be more appropriate to use $XDG_RUNTIME_DIR, since
that gets cleared on reboot, but it wouldn't work on macOS. */
auto currentLoadName = "/current-load";
if (auto localStore = store.try_cast_shared<LocalFSStore>()) {
currentLoad = std::string{localStore->config().stateDir} + currentLoadName;
} else {
currentLoad = settings.nixStateDir + currentLoadName;
}
auto machines = getMachines();
debug("got %d remote builders", machines.size());
if (machines.empty()) {
context.getResults().initResult().initGood().setDeclinePermanently();
co_return;
}
auto amWilling = context.getParams().getAmWilling();
auto neededSystem = rpc::to<std::string>(context.getParams().getNeededSystem());
auto drvPath = from(context.getParams().getDrvPath(), *store);
auto requiredFeatures =
rpc::to<std::set<std::string>>(context.getParams().getRequiredFeatures());
auto result = TRY_AWAIT(connectToBuilder(
store, drvPath, machines, maxBuildJobs, amWilling, neededSystem, requiredFeatures
));
if (auto immediateResponse = std::get_if<BuildRejected>(&result)) {
switch (*immediateResponse) {
case BuildRejected::Temporarily:
context.getResults().initResult().initGood().setPostpone();
co_return;
case BuildRejected::Permanently:
context.getResults().initResult().initGood().setDecline();
co_return;
}
}
auto builder = std::get_if<BuilderConnection>(&result);
assert(builder);
auto ac = context.getResults().initResult().initGood().initAccept();
ac.setMachine(kj::heap<AcceptedBuild>(store, drvPath, std::move(*builder)));
}
kj::Promise<void> Instance::build(BuildContext context)
try {
if (used) {
throw Error("build hooks can only accept a single job");
}
used = true; // lock out other rpc calls during processing
co_await buildImpl(context);
TRY_AWAIT(logger->flush());
used = context.getResults().getResult().getGood().isAccept();
} catch (...) {
RPC_FILL(context.getResults(), getResult, std::current_exception());
}
kj::Promise<void> AcceptedBuild::run(RunContext context)
{
try {
const int logFD = (co_await buildLogger.getFd()).orDefault(-1);
if (logFD < 0) {
throw Error("build-hook needs a logFD from the builder to build");
}
auto logThread = builder.startLogThread(logFD);
auto oldLogger = logger;
logger = rpc::log::makeRpcLoggerClient(context.getParams().getLogger());
TRY_AWAIT(oldLogger->flush());
KJ_DEFER({
// drop any existing ssh connection so the log thread can exit
builder.sshStore = nullptr;
if (logThread.valid()) {
logThread.get();
}
delete logger;
logger = oldLogger;
});
if (used) {
throw Error("build hooks builds are single-use items");
}
used = true;
co_await runImpl(context);
TRY_AWAIT(logger->flush());
} catch (...) {
RPC_FILL(context.getResults(), getResult, std::current_exception());
}
}
kj::Promise<void> AcceptedBuild::runImpl(RunContext context)
{
try {
auto logHandler = builder.startLogThread(
fmt("%s on '%s'",
rpc::to<std::string_view>(context.getParams().getDescription()),
builder.storeUri),
store->printStorePath(drvPath)
);
auto & sshStore = builder.sshStore;
auto & storeUri = builder.storeUri;
@@ -444,7 +519,9 @@ kj::Promise<void> AcceptedBuild::run(RunContext context)
AutoCloseFD uploadLock = openLockFile(lockFileName, true);
{
Activity act(*logger, lvlTalkative, actUnknown, fmt("waiting for the upload lock to '%s'", storeUri));
auto act = logger->startActivity(
lvlTalkative, actUnknown, fmt("waiting for the upload lock to '%s'", storeUri)
);
auto result = TRY_AWAIT(
AIO().timeoutAfter(15 * kj::MINUTES, lockFileAsync(uploadLock.get(), ltWrite))
@@ -457,7 +534,9 @@ kj::Promise<void> AcceptedBuild::run(RunContext context)
auto substitute = settings.buildersUseSubstitutes ? Substitute : NoSubstitute;
{
Activity act(*logger, lvlTalkative, actUnknown, fmt("copying dependencies to '%s'", storeUri));
auto act = logger->startActivity(
lvlTalkative, actUnknown, fmt("copying dependencies to '%s'", storeUri)
);
TRY_AWAIT(copyPaths(*store, *sshStore, inputs, NoRepair, NoCheckSigs, substitute));
}
@@ -527,7 +606,9 @@ kj::Promise<void> AcceptedBuild::run(RunContext context)
}
if (!missingPaths.empty()) {
Activity act(*logger, lvlTalkative, actUnknown, fmt("copying outputs from '%s'", storeUri));
auto act = logger->startActivity(
lvlTalkative, actUnknown, fmt("copying outputs from '%s'", storeUri)
);
if (auto localStore = store.try_cast_shared<LocalStore>())
for (auto & path : missingPaths)
localStore->locksHeld.insert(store->printStorePath(path)); /* FIXME: ugly */
@@ -536,6 +617,10 @@ kj::Promise<void> AcceptedBuild::run(RunContext context)
);
}
// drop store connection, let log handler process any remaining input
builder.sshStore = nullptr;
TRY_AWAIT(logHandler);
context.getResults().initResult().setGood();
} catch (...) {
RPC_FILL(context.getResults(), initResult, std::current_exception());
+54 -46
View File
@@ -9,6 +9,7 @@
#include "lix/libstore/store-api.hh"
#include "lix/libstore/local-fs-store.hh"
#include "lix/libstore/globals.hh"
#include "lix/libutil/c-calls.hh"
#include "lix/libutil/current-process.hh"
#include "lix/libstore/derivations.hh"
#include "lix/libmain/shared.hh"
@@ -18,6 +19,8 @@
#include "lix/libcmd/common-eval-args.hh"
#include "lix/libexpr/attr-path.hh"
#include "lix/libcmd/legacy.hh"
#include "lix/libutil/finally.hh"
#include "lix/libutil/processes.hh"
#include "lix/libutil/regex.hh"
#include "lix/libutil/shlex.hh"
#include "nix-build.hh"
@@ -29,7 +32,7 @@ namespace nix {
using namespace std::string_literals;
static void main_nix_build(AsyncIoRoot & aio, std::string programName, Strings argv)
static int main_nix_build(AsyncIoRoot & aio, std::string programName, Strings argv)
{
auto dryRun = false;
auto runEnv = std::regex_search(programName, regex::parse("nix-shell$"));
@@ -187,7 +190,8 @@ static void main_nix_build(AsyncIoRoot & aio, std::string programName, Strings a
if (packages && fromArgs)
throw UsageError("'-p' and '-E' are mutually exclusive");
AutoDelete tmpDir(createTempDir("", myName));
AutoDelete tmpDir(createTempDir(myName));
AutoDelete buildTopTmpDir(createTempSubdir(tmpDir, "build-top"));
if (outLink.empty())
outLink = (Path) tmpDir + "/result";
@@ -224,8 +228,9 @@ static void main_nix_build(AsyncIoRoot & aio, std::string programName, Strings a
left = {"default.nix"};
}
if (runEnv)
setenv("IN_NIX_SHELL", pure ? "pure" : "impure", 1);
if (runEnv) {
(void) sys::setenv("IN_NIX_SHELL", pure ? "pure" : "impure", 1);
}
DrvInfos drvs;
@@ -397,7 +402,9 @@ static void main_nix_build(AsyncIoRoot & aio, std::string programName, Strings a
buildPaths(pathsToBuild);
if (dryRun) return;
if (dryRun) {
return 0;
}
if (shellDrv) {
auto shellDrvOutputs =
@@ -419,24 +426,24 @@ static void main_nix_build(AsyncIoRoot & aio, std::string programName, Strings a
}
// Don't use defaultTempDir() here! We want to preserve the user's TMPDIR for the shell
env["NIX_BUILD_TOP"] = env["TMPDIR"] = env["TEMPDIR"] = env["TMP"] = env["TEMP"] = getEnvNonEmpty("TMPDIR").value_or("/tmp");
env["NIX_BUILD_TOP"] = env["TMPDIR"] = env["TEMPDIR"] = env["TMP"] = env["TEMP"] =
getEnvNonEmpty("TMPDIR").value_or(buildTopTmpDir);
env["NIX_STORE"] = store->config().storeDir;
env["NIX_BUILD_CORES"] = std::to_string(settings.buildCores);
auto passAsFile = tokenizeString<StringSet>(getOr(drv.env, "passAsFile", ""));
bool keepTmp = false;
int fileNr = 0;
for (auto & var : drv.env)
if (passAsFile.count(var.first)) {
keepTmp = true;
auto fn = ".attr-" + std::to_string(fileNr++);
Path p = (Path) tmpDir + "/" + fn;
writeFile(p, var.second);
env[var.first + "Path"] = p;
} else
} else {
env[var.first] = var.second;
}
std::string structuredAttrsRC;
@@ -469,7 +476,6 @@ static void main_nix_build(AsyncIoRoot & aio, std::string programName, Strings a
env["NIX_ATTRS_SH_FILE"] = attrsSH;
env["NIX_ATTRS_JSON_FILE"] = attrsJSON;
keepTmp = true;
}
}
@@ -479,24 +485,13 @@ static void main_nix_build(AsyncIoRoot & aio, std::string programName, Strings a
lose the current $PATH directories. */
auto rcfile = (Path) tmpDir + "/rc";
auto tz = getEnv("TZ");
std::string rc = fmt(
R"(_nix_shell_clean_tmpdir() { command rm -rf %1%; }; )"
"%2%"
"%3%"
// always clear PATH.
// when nix-shell is run impure, we rehydrate it with the `p=$PATH` above
"unset PATH;"
"dontAddDisableDepTrack=1;\n",
shellEscape(tmpDir),
(keepTmp
? "trap _nix_shell_clean_tmpdir EXIT; "
"exitHooks+=(_nix_shell_clean_tmpdir); "
"failureHooks+=(_nix_shell_clean_tmpdir); "
: "_nix_shell_clean_tmpdir; "),
(pure
? ""
: "[ -n \"$PS1\" ] && [ -e ~/.bashrc ] && source ~/.bashrc; p=$PATH; ")
);
std::string rc =
fmt("%1%"
// always clear PATH.
// when nix-shell is run impure, we rehydrate it with the `p=$PATH` above
"unset PATH;"
"dontAddDisableDepTrack=1;\n",
(pure ? "" : "[ -n \"$PS1\" ] && [ -e ~/.bashrc ] && source ~/.bashrc; p=$PATH; "));
rc += structuredAttrsRC;
rc += fmt(
"\n[ -e $stdenv/setup ] && source $stdenv/setup; "
@@ -526,29 +521,38 @@ static void main_nix_build(AsyncIoRoot & aio, std::string programName, Strings a
vomit("Sourcing nix-shell with file %s and contents:\n%s", rcfile, rc);
writeFile(rcfile, rc);
Strings envStrs;
for (auto & i : env)
envStrs.push_back(i.first + "=" + i.second);
auto args = interactive
? Strings{"bash", "--rcfile", rcfile}
: Strings{"bash", rcfile};
auto envPtrs = stringsToCharPtrs(envStrs);
environ = envPtrs.data();
auto argPtrs = stringsToCharPtrs(args);
restoreProcessContext();
auto args = interactive ? Strings{"--rcfile", rcfile} : Strings{rcfile};
// We are going to run an interactive command, do not let the logger send a line.
logger->pause();
printMsg(lvlChatty, "running shell: %s", concatMapStringsSep(" ", args, shellEscape));
execvp(shell->c_str(), argPtrs.data());
RunningProgram proc = runProgram2(
{.program = *shell,
.searchPath = true,
.args = args,
.environment = env,
.dieWithParent = true}
);
throw SysError("executing shell '%s'", *shell);
// NOTE: we wait and return the status check immediately.
// If there's interruption, we will swallow it and wait again for termination.
auto toExitStatus = [](int waitRes) {
if (WIFEXITED(waitRes)) {
return WEXITSTATUS(waitRes);
} else if (WIFSIGNALED(waitRes)) {
return 128 + WTERMSIG(waitRes);
} else {
return 255;
}
};
try {
return toExitStatus(proc.wait());
} catch (Interrupted &) {
return toExitStatus(proc.wait());
}
}
else {
@@ -582,7 +586,9 @@ static void main_nix_build(AsyncIoRoot & aio, std::string programName, Strings a
buildPaths(pathsToBuild);
if (dryRun) return;
if (dryRun) {
return 0;
}
std::vector<StorePath> outPaths;
@@ -611,6 +617,8 @@ static void main_nix_build(AsyncIoRoot & aio, std::string programName, Strings a
for (auto & path : outPaths)
std::cout << store->printStorePath(path) << '\n';
}
return 0;
}
void registerLegacyNixBuildAndNixShell() {
+5 -3
View File
@@ -8,6 +8,7 @@
#include "lix/libexpr/eval-settings.hh" // for defexpr
#include "lix/libstore/temporary-dir.hh"
#include "lix/libutil/async.hh"
#include "lix/libutil/c-calls.hh"
#include "lix/libutil/regex.hh"
#include "lix/libutil/result.hh"
#include "lix/libutil/users.hh"
@@ -44,7 +45,7 @@ static void readChannels()
// Writes the list of channels.
static void writeChannels()
{
auto channelsFD = AutoCloseFD{open(channelsList.c_str(), O_WRONLY | O_CLOEXEC | O_CREAT | O_TRUNC, 0644)};
auto channelsFD = sys::open(channelsList, O_WRONLY | O_CLOEXEC | O_CREAT | O_TRUNC, 0644);
if (!channelsFD)
throw SysError("opening '%1%' for writing", channelsList);
for (const auto & channel : channels)
@@ -174,11 +175,12 @@ static void update(AsyncIoRoot & aio, const StringSet & channelNames)
// Make the channels appear in nix-env.
struct stat st;
if (lstat(nixDefExpr.c_str(), &st) == 0) {
if (sys::lstat(nixDefExpr, &st) == 0) {
if (S_ISLNK(st.st_mode))
// old-skool ~/.nix-defexpr
if (unlink(nixDefExpr.c_str()) == -1)
if (sys::unlink(nixDefExpr) == -1) {
throw SysError("unlinking %1%", nixDefExpr);
}
} else if (errno != ENOENT) {
throw SysError("getting status of %1%", nixDefExpr);
}
+5 -2
View File
@@ -1,3 +1,4 @@
#include "lix/libutil/c-calls.hh"
#include "lix/libutil/file-system.hh"
#include "lix/libstore/store-api.hh"
#include "lix/libstore/store-cast.hh"
@@ -24,9 +25,11 @@ bool dryRun = false;
static void removeOldGenerations(std::string dir, NeverAsync = {})
{
if (access(dir.c_str(), R_OK) != 0) return;
if (sys::access(dir, R_OK) != 0) {
return;
}
bool canWrite = access(dir.c_str(), W_OK) == 0;
bool canWrite = sys::access(dir, W_OK) == 0;
for (auto & i : readDirectory(dir)) {
checkInterrupt();
+11
View File
@@ -25,6 +25,7 @@
#include <iostream>
#include <algorithm>
#include <ranges>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
@@ -88,6 +89,16 @@ try {
if (i == drv.outputs.end())
throw Error("derivation '%s' does not have an output named '%s'",
store2->printStorePath(path.path), j);
if (!outputPaths.contains(i->first)) {
throw Error(
"Possible SQLite database corruption: derivation '%s' output map contains only "
"outputs '{%s}', not '%s'\n"
"Note: derivation output maps are stored in the SQLite database.",
store2->printStorePath(path.path),
concatStringsSep(", ", std::views::keys(outputPaths)),
i->first
);
}
auto outPath = outputPaths.at(i->first);
auto retPath = store->printStorePath(outPath);
if (store2) {
+7 -3
View File
@@ -5,6 +5,8 @@
#include "lix/libstore/profiles.hh"
#include "lix/libcmd/repl.hh"
#include "lix/libutil/async.hh"
#include "lix/libutil/c-calls.hh"
#include "lix/libutil/error.hh"
extern char * * environ __attribute__((weak));
@@ -303,8 +305,10 @@ void MixEnvironment::setEnviron() {
throw UsageError("--unset does not make sense with --ignore-environment");
for (const auto & var : keep) {
auto val = getenv(var.c_str());
if (val) stringsEnv.emplace_back(fmt("%s=%s", var.c_str(), val));
auto val = sys::getenv(var);
if (val) {
stringsEnv.emplace_back(fmt("%s=%s", var, val));
}
}
vectorEnv = stringsToCharPtrs(stringsEnv);
@@ -314,7 +318,7 @@ void MixEnvironment::setEnviron() {
throw UsageError("--keep does not make sense without --ignore-environment");
for (const auto & var : unset)
unsetenv(var.c_str());
(void) sys::unsetenv(var);
}
}
+1 -1
View File
@@ -113,7 +113,7 @@ MixEvalArgs::MixEvalArgs()
```
-I nixpkgs=channel:nixos-21.05
-I nixpkgs=https://nixos.org/channels/nixos-21.05/nixexprs.tar.xz
-I nixpkgs=https://channels.nixos.org/nixos-21.05/nixexprs.tar.xz
```
You can also fetch source trees using [flake URLs](./nix3-flake.md#url-like-syntax) and add them to the
+2 -1
View File
@@ -60,7 +60,8 @@ InstallableFlake::InstallableFlake(
DerivedPathsWithInfo InstallableFlake::toDerivedPaths(EvalState & state)
{
Activity act(*logger, lvlTalkative, actUnknown, fmt("evaluating derivation '%s'", what()));
auto act =
logger->startActivity(lvlTalkative, actUnknown, fmt("evaluating derivation '%s'", what()));
auto attr = getCursor(state);
+1 -1
View File
@@ -9,7 +9,7 @@
namespace nix {
typedef std::function<void(AsyncIoRoot &, std::string, std::list<std::string>)> MainFunction;
typedef std::function<int(AsyncIoRoot &, std::string, std::list<std::string>)> MainFunction;
struct LegacyCommandRegistry
{
+4
View File
@@ -74,7 +74,11 @@ std::string renderMarkdownToTerminal(std::string_view markdown, StandardOutputSt
.vmargin = 0,
#endif /* LOWDOWN_SEPARATE_TERM_OPTS */
.feat = LOWDOWN_COMMONMARK | LOWDOWN_FENCED | LOWDOWN_DEFLIST | LOWDOWN_TABLES,
#ifdef LOWDOWN_CONSOLIDATED_OFLAGS
.oflags = LOWDOWN_NOLINK,
#else
.oflags = LOWDOWN_TERM_NOLINK,
#endif /* LOWDOWN_CONSOLIDATED_OFLAGS */
};
if (!shouldANSI(fileno)) {
opts.oflags |= LOWDOWN_TERM_NOANSI;
+6 -5
View File
@@ -1,3 +1,4 @@
#include "lix/libutil/c-calls.hh"
#include "lix/libutil/error.hh"
#include "lix/libutil/file-system.hh"
#include "lix/libutil/logging.hh"
@@ -110,7 +111,7 @@ static el_status_t doCompletion() {
if (possible.size() == 1) {
const auto completion = *possible.cbegin();
if (completion.size() > s.size()) {
rl_insert_text(completion.c_str() + s.size());
rl_insert_text(requireCString(completion.substr(s.size())));
return redisplay();
}
@@ -134,7 +135,7 @@ static el_status_t doCompletion() {
}
if (len > 0) {
auto commonPrefix = possible.begin()->substr(start, len);
rl_insert_text(commonPrefix.c_str());
rl_insert_text(requireCString(commonPrefix));
el_ring_bell();
return redisplay();
}
@@ -154,7 +155,7 @@ ReadlineLikeInteracter::Guard ReadlineLikeInteracter::init(detail::ReplCompleter
logWarning(e.info());
}
el_hist_size = 1000;
read_history(historyFile.c_str());
read_history(requireCString(historyFile));
auto oldRepl = curRepl;
curRepl = repl;
Guard restoreRepl([oldRepl] { curRepl = oldRepl; });
@@ -202,7 +203,7 @@ bool ReadlineLikeInteracter::getLine(std::string & input, ReplPromptType promptT
};
setupSignals();
char * s = readline(promptForType(promptType));
char * s = readline(promptForType(promptType)); // NOLINT(lix-unsafe-c-calls)
Finally doFree([&]() { free(s); });
restoreSignals();
@@ -223,7 +224,7 @@ bool ReadlineLikeInteracter::getLine(std::string & input, ReplPromptType promptT
void ReadlineLikeInteracter::writeHistory()
{
int ret = write_history(historyFile.c_str());
int ret = write_history(requireCString(historyFile));
int writeHistErr = errno;
if (ret == 0) {
+4 -3
View File
@@ -56,6 +56,7 @@ using NdString = std::unique_ptr<const char, decltype(&lixdoc_free_string)>;
*/
NdString lambdaDocsForPos(SourcePath const path, nix::Pos const &pos) {
std::string const file = path.to_string();
// NOLINTNEXTLINE(lix-unsafe-c-calls): paths are safe
return NdString{lixdoc_get_function_docs(file.c_str(), pos.line, pos.column), &lixdoc_free_string};
}
@@ -1003,8 +1004,8 @@ Value NixRepl::getReplOverlaysEvalFunction()
Value NixRepl::replOverlays()
{
Value replInits;
auto replInitElems = evaluator.mem.newList(evalSettings.replOverlays.get().size());
replInits = {NewValueAs::list, replInitElems};
auto replInitStorage = evaluator.mem.newList(evalSettings.replOverlays.get().size());
replInits = {NewValueAs::list, replInitStorage};
size_t i = 0;
for (auto path : evalSettings.replOverlays.get()) {
@@ -1045,7 +1046,7 @@ Value NixRepl::replOverlays()
.debugThrow();
}
replInitElems->elems[i] = replInit;
replInitStorage->elems[i] = replInit;
i++;
}
+1 -1
View File
@@ -43,7 +43,7 @@ static_assert(sizeof(Attr) == 2 * sizeof(uint32_t) + sizeof(Value *),
* elements allocated after this structure, while the size corresponds to
* the number of elements already inserted in this structure.
*/
class Bindings
class alignas(Value::TAG_ALIGN) Bindings
{
public:
using Size = uint32_t;
+15 -4
View File
@@ -20,18 +20,28 @@ inline Value::Value(app_t, EvalMemory & mem, Value & lhs, Value & rhs)
}
inline Value::Value(app_t, EvalMemory & mem, Value & lhs, std::span<Value> args)
: Value(app_t{}, mem, lhs, args, {})
{
auto app = static_cast<Value::App *>(mem.allocBytes(sizeof(Value::App) + args.size_bytes()));
}
inline Value::Value(
app_t, EvalMemory & mem, const Value & lhs, std::span<Value> baseArgs, std::span<Value> moreArgs
)
{
auto app = static_cast<Value::App *>(
mem.allocBytes(sizeof(Value::App) + baseArgs.size_bytes() + moreArgs.size_bytes())
);
app->_left = lhs;
app->_n = args.size();
std::copy(args.begin(), args.end(), app->_args);
app->_n = baseArgs.size() + moreArgs.size();
std::copy(baseArgs.begin(), baseArgs.end(), app->_args);
std::copy(moreArgs.begin(), moreArgs.end(), app->_args + baseArgs.size());
raw = tag(tApp, app);
}
inline Value::Value(thunk_t, EvalMemory & mem, Env & env, Expr & expr)
{
auto thunk = mem.allocType<Thunk>();
*thunk = {._env = reinterpret_cast<uintptr_t>(&env), .expr = &expr};
*thunk = {._env = &env, .expr = &expr};
raw = tag(tThunk, thunk);
}
@@ -81,6 +91,7 @@ template<typename T>
[[gnu::always_inline]]
T * EvalMemory::allocType(size_t n)
{
// NOLINTNEXTLINE(bugprone-sizeof-expression)
return static_cast<T *>(allocBytes(checkedArrayAllocSize(sizeof(T), n)));
}
+1 -1
View File
@@ -82,7 +82,7 @@ bool EvalSettings::isPseudoUrl(std::string_view s)
std::string EvalSettings::resolvePseudoUrl(std::string_view url)
{
if (url.starts_with("channel:"))
return "https://nixos.org/channels/" + std::string(url.substr(8)) + "/nixexprs.tar.xz";
return "https://channels.nixos.org/" + std::string(url.substr(8)) + "/nixexprs.tar.xz";
else
return std::string(url);
}
+163 -99
View File
@@ -39,10 +39,26 @@
#include <sys/resource.h>
#include <fstream>
#include <functional>
#include <ranges>
#include <sys/resource.h>
#include <boost/container/small_vector.hpp>
// Ignore all internal signals boehm uses for parallel marking
// FIXME: Find out how to do this with LLDB for macOS!
#ifndef __APPLE__
[[gnu::section(".debug_gdb_scripts"), gnu::used, gnu::aligned(1)]]
// TODO: We should use `SECTION_SCRIPT_ID_PYTHON_TEXT` from
// `<gdb/section-scripts.h>` instead of hardcoding 4.
// But why isn't this header exported by GDB?
static const char printer_script[] =
"\4"
R"(lix-ignore-boehm-signals
import gdb
gdb.execute("handle SIGPWR SIGXCPU ignore")
)";
#endif
#if HAVE_BOEHMGC
#define GC_INCLUDE_NEW
@@ -177,6 +193,9 @@ void initLibExpr()
GC_INIT();
// Enable parallel marking
GC_start_mark_threads();
GC_set_oom_fn(oomHandler);
/* Set the initial heap size to something fairly big (25% of
@@ -573,20 +592,20 @@ std::ostream & operator<<(std::ostream & output, const PrimOp & primOp)
return output;
}
void EvalBuiltins::addPrimOp(PrimOpDetails && primOp)
void EvalBuiltins::addPrimOp(PrimOpDetails primOp)
{
/* Hack to make constants lazy: turn them into a application of
the primop to a dummy value. */
if (primOp.arity == 0) {
primOp.arity = 1;
Value vPrimOp{NewValueAs::primop, *new PrimOp(std::move(primOp))};
Value vPrimOp{NewValueAs::primop, *new PrimOp(primOp)};
Value v{NewValueAs::app, mem, vPrimOp, vPrimOp};
addConstant(
vPrimOp.primOp()->name,
v,
{
.type = nFunction,
.doc = primOp.doc,
.doc = vPrimOp.primOp()->doc,
}
);
}
@@ -1285,113 +1304,149 @@ static std::string showAttrPath(EvalState & state, Env & env, const AttrPath & a
return out.str();
}
/** Returns `nullptr` if we should be using a default instead. */
Attr const * ExprSelect::selectSingleAttr(
EvalState & state, Env & env, AttrName const & attrName, Value & vCurrent
)
{
Symbol const attrSym = getName(attrName, state, env);
try {
state.forceValue(vCurrent, pos);
} catch (Error & e) {
// clang-format off
e.addTrace(state.ctx.positions[attrName.pos], HintFmt(
"while evaluating an expression to select '%s' on it", state.ctx.symbols[attrSym]
));
// clang-format on
throw;
}
if (vCurrent.type() != nAttrs) {
// If we have an `or` provided default, then it doesn't have to be an attrset.
// Let the caller know there's no attr value here.
if (def != nullptr) {
return nullptr;
}
// Otherwise, we must type error.
// clang-format off
state.ctx.errors.make<TypeError>(
"expected a set but found %s: %s",
showType(vCurrent),
ValuePrinter(state, vCurrent, errorPrintOptions)
).addTrace(
attrName.pos,
HintFmt("while selecting '%s'", state.ctx.symbols[attrSym])
).debugThrow();
// clang-format on
}
// Now that we know it's an attrset, we can actually look for the name.
auto const attrIt = vCurrent.attrs()->get(attrSym);
if (!attrIt) {
// Again if we have an `or` provided default, then missing attr is not an error.
if (def != nullptr) {
return nullptr;
}
// Otherwise, we collect all attr names and throw an attr missing error.
std::set<std::string> const allAttrNames = *vCurrent.attrs()
| std::views::transform([&state](auto const & attr) {
return std::string{state.ctx.symbols[attr.name]};
})
| std::ranges::to<std::set>();
auto suggestions = Suggestions::bestMatches(allAttrNames, state.ctx.symbols[attrSym]);
state.ctx.errors.make<EvalError>("attribute '%s' missing", state.ctx.symbols[attrSym])
.atPos(attrName.pos)
.withSuggestions(suggestions)
.withFrame(env, *this)
.debugThrow();
}
// If we made it here, then we successfully found the attribute.
// Return it to our caller!
return attrIt;
}
void ExprSelect::eval(EvalState & state, Env & env, Value & v)
{
Value vFirst;
// Pointer to the current attrset Value in this select chain.
Value * vCurrent = &vFirst;
// Position for the current attrset Value in this select chain.
PosIdx posCurrent;
// Position for the current selector in this select chain.
PosIdx posCurrentSyntax;
Value baseSelectee;
try {
e->eval(state, env, vFirst);
// Evaluate the original thing we're selecting on.
e->eval(state, env, baseSelectee);
} catch (Error & e) {
assert(this->e != nullptr);
e.addTrace(
state.ctx.positions[getPos()],
// clang-format off
e.addTrace(state.ctx.positions[getPos()], HintFmt(
"while evaluating an expression to select '%s' on it",
showAttrPath(state.ctx.symbols, this->attrPath)
);
showAttrPath(state.ctx.symbols, attrPath)
));
// clang-format on
throw;
}
try {
for (auto const & [partIdx, currentAttrName] : enumerate(attrPath)) {
// With the original selectee evaluated, we'll walk the selection path starting
// with the evaluated original selectee.
std::reference_wrapper<Value> curSelectee = std::ref(baseSelectee);
for (AttrName const & attrName : attrPath) {
state.ctx.stats.nrLookups++;
Symbol const name = getName(currentAttrName, state, env);
try {
state.forceValue(*vCurrent, pos);
} catch (Error & e) {
e.addTrace(
state.ctx.positions[currentAttrName.pos],
"while evaluating an expression to select '%s' on it",
state.ctx.symbols[name]
);
throw;
}
if (vCurrent->type() != nAttrs) {
// If we have an `or` provided default,
// then this is allowed to not be an attrset.
if (def != nullptr) {
// Select `attrName` on `curSelectee`.
auto const attr = selectSingleAttr(state, env, attrName, curSelectee.get());
if (!attr) {
// Use default.
try {
this->def->eval(state, env, v);
return;
} catch (Error & err) {
err.addTrace(
state.ctx.positions[this->def->pos],
"while evaluating fallback for missing attribute '%s'",
state.ctx.symbols[getName(attrName, state, env)]
);
throw;
}
// Otherwise, we must type error.
state.ctx.errors.make<TypeError>(
"expected a set but found %s: %s",
showType(*vCurrent),
ValuePrinter(state, *vCurrent, errorPrintOptions)
).addTrace(
currentAttrName.pos,
HintFmt("while selecting '%s'", state.ctx.symbols[name])
).debugThrow();
return;
}
// Now that we know this is actually an attrset, try to find an attr
// with the selected name.
auto attrIt = vCurrent->attrs()->get(name);
if (!attrIt) {
// The selection worked. If we have another iteration, then we use `attr->value`
// as the thing to select on. If this is the last iteration, then `attr->value`
// is the final value this ExprSelect evaluated to.
curSelectee = std::ref(attr->value);
// If we have an `or` provided default, then we'll use that.
if (def != nullptr) {
this->def->eval(state, env, v);
return;
}
// Otherwise, missing attr error.
std::set<std::string> allAttrNames;
for (auto const & attr : *vCurrent->attrs()) {
allAttrNames.emplace(state.ctx.symbols[attr.name]);
}
auto suggestions = Suggestions::bestMatches(allAttrNames, state.ctx.symbols[name]);
state.ctx.errors.make<EvalError>("attribute '%s' missing", state.ctx.symbols[name])
.atPos(currentAttrName.pos)
.withSuggestions(suggestions)
.withFrame(env, *this)
.debugThrow();
posCurrent = attr->pos;
posCurrentSyntax = attrName.pos;
if (state.ctx.stats.countCalls) {
state.ctx.stats.attrSelects[posCurrent]++;
}
// If we're here, then we successfully found the attribute.
// Set our currently operated-on attrset to this one, and keep going.
vCurrent = &attrIt->value;
posCurrent = attrIt->pos;
posCurrentSyntax = currentAttrName.pos;
if (state.ctx.stats.countCalls) state.ctx.stats.attrSelects[posCurrent]++;
}
state.forceValue(*vCurrent, (posCurrent ? posCurrent : posCurrentSyntax));
state.forceValue(curSelectee.get(), posCurrent ? posCurrent : posCurrentSyntax);
v = curSelectee.get();
} catch (Error & err) {
auto const & lastPos = state.ctx.positions[posCurrent];
if (lastPos && !std::get_if<Pos::Hidden>(&lastPos.origin)) {
err.addTrace(
lastPos, "while evaluating the attribute '%s'", showAttrPath(state, env, attrPath)
);
}
} catch (Error & e) {
auto pos2r = state.ctx.positions[posCurrent];
if (pos2r && !std::get_if<Pos::Hidden>(&pos2r.origin))
e.addTrace(pos2r, "while evaluating the attribute '%1%'",
showAttrPath(state, env, attrPath));
throw;
}
v = *vCurrent;
}
void ExprOpHasAttr::eval(EvalState & state, Env & env, Value & v)
{
Value vTmp;
@@ -1584,7 +1639,14 @@ void EvalState::callFunction(Value & fun, std::span<Value> args, Value & vRes, c
Value vCur(fun);
auto makeAppChain = [&]() { vRes = {NewValueAs::app, ctx.mem, vCur, args}; };
auto makeAppChain = [&]() {
if (vCur.isApp()) {
auto & app = vCur.app();
vRes = {NewValueAs::app, ctx.mem, app.left(), app.args(), args};
} else {
vRes = {NewValueAs::app, ctx.mem, vCur, args};
}
};
const Attr * functor;
@@ -1996,17 +2058,16 @@ void ExprConcatStrings::eval(EvalState & state, Env & env, Value & v)
for (const auto & part : s) result += *part;
return result;
};
/* c_str() is not str().c_str() because we want to create a string
Value. allocating a GC'd string directly and moving it into a
Value lets us avoid an allocation and copy. */
const auto c_str = [&] {
char * result = gcAllocString(sSize + 1);
char * tmp = result;
/* build a gc'd value string directly instead of going through str()
and mkString to save an allocation and copy */
const auto gcStr = [&] {
auto result = Value::Str::gcAlloc(sSize);
char * tmp = result->contents;
for (const auto & part : s) {
memcpy(tmp, part->data(), part->size());
tmp += part->size();
}
*tmp = 0;
return result;
};
@@ -2080,7 +2141,7 @@ void ExprConcatStrings::eval(EvalState & state, Env & env, Value & v)
state.ctx.errors.make<EvalError>("a string that refers to a store path cannot be appended to a path").atPos(pos).withFrame(env, *this).debugThrow();
v.mkPath(CanonPath(canonPath(str())));
} else
v.mkStringMove(c_str(), context);
v.mkStringMove(gcStr(), context);
}
@@ -2351,8 +2412,8 @@ BackedStringView EvalState::coerceToString(
if (v.type() == nPath) {
return !canonicalizePath && !copyToStore
? v.string().content // FIXME: hack to preserve path literals that end in a slash, as in
// /foo/${x}.
// FIXME: hack to preserve path literals that end in a slash, as in /foo/${x}.
? std::string(v.string().content->str())
: (copyToStore
? ctx.store->printStorePath(
aio.blockOn(ctx.paths.copyPathToStore(context, v.path(), ctx.repair))
@@ -2558,11 +2619,6 @@ bool EvalState::eqValues(Value & v1, Value & v2, const PosIdx pos, std::string_v
forceValue(v1, pos);
forceValue(v2, pos);
/* !!! Hack to support some old broken code that relies on pointer
equality tests between sets. (Specifically, builderDefs calls
uniqList on a list of sets.) Will remove this eventually. */
if (&v1 == &v2) return true;
// Special case type-compatibility between float and int
if (v1.type() == nInt && v2.type() == nFloat) {
return v1.integer().value == v2.fpoint();
@@ -2574,6 +2630,11 @@ bool EvalState::eqValues(Value & v1, Value & v2, const PosIdx pos, std::string_v
// All other types are not compatible with each other.
if (v1.type() != v2.type()) return false;
/* !!! Hack to support some old broken code that relies on pointer
equality tests between sets. (Specifically, builderDefs calls
uniqList on a list of sets.) Will remove this eventually. */
auto pointerEq = [&] { return v1.pointerEqProxy() == v2.pointerEqProxy(); };
switch (v1.type()) {
case nInt:
return v1.integer() == v2.integer();
@@ -2585,12 +2646,13 @@ bool EvalState::eqValues(Value & v1, Value & v2, const PosIdx pos, std::string_v
return v1.str() == v2.str();
case nPath:
return strcmp(v1.string().content, v2.string().content) == 0;
return v1.string().content->str() == v2.string().content->str();
case nNull:
return true;
case nList:
if (pointerEq()) return true;
if (v1.listSize() != v2.listSize()) return false;
for (size_t n = 0; n < v1.listSize(); ++n) {
if (!eqValues(v1.listElems()[n], v2.listElems()[n], pos, errorCtx)) {
@@ -2600,6 +2662,7 @@ bool EvalState::eqValues(Value & v1, Value & v2, const PosIdx pos, std::string_v
return true;
case nAttrs: {
if (pointerEq()) return true;
/* If both sets denote a derivation (type = "derivation"),
then compare their outPaths. */
if (isDerivation(v1) && isDerivation(v2)) {
@@ -2624,11 +2687,12 @@ bool EvalState::eqValues(Value & v1, Value & v2, const PosIdx pos, std::string_v
return true;
}
/* Functions are incomparable. */
/* Functions are incomparable, except for identity (see note above about this nonsense). */
case nFunction:
return false;
return pointerEq();
case nExternal:
if (pointerEq()) return true;
return *v1.external() == *v2.external();
case nFloat:
+1 -1
View File
@@ -265,7 +265,7 @@ private:
void addConstant(const std::string & name, const Value & v, Constant info);
void addPrimOp(PrimOpDetails && primOp);
void addPrimOp(PrimOpDetails primOp);
Value prepareNixPath(const SearchPath & searchPath);
+2 -76
View File
@@ -10,7 +10,8 @@ namespace nix {
ExprBlackHole eBlackHole;
Value::Thunk Value::blackHole{{0}, &eBlackHole};
static Env nullEnv;
Value::Thunk Value::blackHole{{&nullEnv}, &eBlackHole};
// FIXME: remove, because *symbols* are abstract and do not have a single
// textual representation; see printIdentifier()
@@ -321,74 +322,6 @@ JSON printAttrPathToJson(const SymbolTable & symbols, const AttrPath & attrPath)
/* Computing levels/displacements for variables. */
namespace {
// This is a one-pass static analyzer for
// various topics.
struct StaticAnalyzer : ExprVisitor
{
std::set<Symbol> staticallyUsedVariables;
bool usedDynamicVariables = false;
StaticAnalyzer() {}
using ExprVisitor::visit;
void visit(ExprDebugFrame & e, std::unique_ptr<Expr> & ptr) override
{
visit(e.inner);
}
void visit(ExprLiteral & e, std::unique_ptr<Expr> & ptr) override {}
void visit(ExprVar & e, std::unique_ptr<Expr> & ptr) override
{
staticallyUsedVariables.insert(e.name);
}
void visit(ExprInheritFrom & e, std::unique_ptr<Expr> & ptr) override {}
void visit(ExprSelect & e, std::unique_ptr<Expr> & ptr) override
{
if (e.isDynamic()) {
usedDynamicVariables = true;
}
visit(e.def);
visit(e.e);
}
void visit(ExprOpHasAttr & e, std::unique_ptr<Expr> & ptr) override
{
if (e.isDynamic()) {
usedDynamicVariables = true;
}
visit(e.e);
}
void visit(ExprSet & e, std::unique_ptr<Expr> & ptr) override
{
// TODO: oh bro, we need to analyze dynamic attributes for their value expressions.
}
void visit(ExprList & e, std::unique_ptr<Expr> & ptr) override {}
void visit(ExprLambda & e, std::unique_ptr<Expr> & ptr) override {}
void visit(ExprCall & e, std::unique_ptr<Expr> & ptr) override {}
void visit(ExprLet & e, std::unique_ptr<Expr> & ptr) override {}
void visit(ExprWith & e, std::unique_ptr<Expr> & ptr) override {}
void visit(ExprIf & e, std::unique_ptr<Expr> & ptr) override {}
void visit(ExprAssert & e, std::unique_ptr<Expr> & ptr) override {}
void visit(ExprOpNot & e, std::unique_ptr<Expr> & ptr) override {}
#define BINOP(type) \
/* NOLINTNEXTLINE(bugprone-macro-parentheses) */ \
void visit(type & e, std::unique_ptr<Expr> & ptr) override \
{ \
visit(e.e1); \
visit(e.e2); \
}
BINOP(ExprOpEq)
BINOP(ExprOpNEq)
BINOP(ExprOpAnd)
BINOP(ExprOpOr)
BINOP(ExprOpImpl)
BINOP(ExprOpUpdate)
BINOP(ExprOpConcatLists)
#undef BINOP
void visit(ExprConcatStrings & e, std::unique_ptr<Expr> & ptr) override {}
void visit(ExprPos & e, std::unique_ptr<Expr> & ptr) override {}
void visit(ExprBlackHole & e, std::unique_ptr<Expr> & ptr) override {}
};
struct VarBinder : ExprVisitor
{
Evaluator & es;
@@ -658,13 +591,6 @@ void VarBinder::visit(ExprLambda & e, std::unique_ptr<Expr> & ptr)
{
withEnv(e.pattern->buildEnv(env.get()), [&] {
e.pattern->accept(*this);
/* TODO: If statically, e.body makes only use of some parameters and not the whole scope.
* We shouldn't have to keep around all the environment data which might contain trapped
* pointers. Analyze `e.body` and return its statically known set of used variables.
* */
DirectCallAnalyzer analyzer{es, env};
analyzer.visit(e.body);
e.shortcut = analyzer.shortcut;
visit(e.body);
});
}
+22 -35
View File
@@ -204,21 +204,31 @@ struct ExprFloat : ExprLiteral
struct ExprString : ExprLiteral
{
std::string s;
Value::String strcb{.content = s.c_str(), .context = nullptr};
ExprString(const PosIdx pos, std::string && s) : ExprLiteral(pos), s(std::move(s))
std::unique_ptr<Value::Str, Value::Str::Deleter> contents;
Value::String strcb{.content = contents.get(), .context = nullptr};
ExprString(const PosIdx pos, std::string s) : ExprLiteral(pos), contents(Value::Str::copy(s))
{
v = {NewValueAs::string, &strcb};
}
std::string_view str() const
{
return contents->str();
}
};
struct ExprPath : ExprLiteral
{
std::string s;
Value::String strcb{.content = s.c_str(), .context = Value::String::path};
ExprPath(const PosIdx pos, std::string s) : ExprLiteral(pos), s(std::move(s))
std::unique_ptr<Value::Str, Value::Str::Deleter> contents;
Value::String strcb{.content = contents.get(), .context = Value::String::path};
ExprPath(const PosIdx pos, std::string s) : ExprLiteral(pos), contents(Value::Str::copy(s))
{
v = {NewValueAs::path, &strcb};
v = Value{NewValueAs::path, &strcb};
}
std::string_view str() const
{
return contents->str();
}
};
@@ -279,6 +289,8 @@ struct ExprInheritFrom : Expr
void accept(ExprVisitor & ev, std::unique_ptr<Expr> & ptr) override { ev.visit(*this, ptr); }
};
struct Attr;
struct ExprSelect : Expr
{
/** The expression attributes are being selected on. e.g. `foo` in `foo.bar.baz`. */
@@ -292,38 +304,20 @@ struct ExprSelect : Expr
/** The path of attributes being selected. e.g. `bar.baz` in `foo.bar.baz.` */
AttrPath attrPath;
bool isDynamic() const
{
for (auto & name : attrPath) {
if (name.expr) {
return true;
}
}
return false;
}
ExprSelect(const PosIdx & pos, std::unique_ptr<Expr> e, AttrPath attrPath, std::unique_ptr<Expr> def) : Expr(pos), e(std::move(e)), def(std::move(def)), attrPath(std::move(attrPath)) { };
ExprSelect(const PosIdx & pos, std::unique_ptr<Expr> e, const PosIdx namePos, Symbol name) : Expr(pos), e(std::move(e)) { attrPath.push_back(AttrName(namePos, name)); };
JSON toJSON(const SymbolTable & symbols) const override;
void eval(EvalState & state, Env & env, Value & v) override;
void accept(ExprVisitor & ev, std::unique_ptr<Expr> & ptr) override { ev.visit(*this, ptr); }
private:
Attr const * selectSingleAttr(EvalState & state, Env & env, AttrName const & attrName, Value & vCurrent);
};
struct ExprOpHasAttr : Expr
{
std::unique_ptr<Expr> e;
AttrPath attrPath;
bool isDynamic() const
{
for (auto & name : attrPath) {
if (name.expr) {
return true;
}
}
return false;
}
ExprOpHasAttr(const PosIdx & pos, std::unique_ptr<Expr> e, AttrPath attrPath) : Expr(pos), e(std::move(e)), attrPath(std::move(attrPath)) { };
JSON toJSON(const SymbolTable & symbols) const override;
void eval(EvalState & state, Env & env, Value & v) override;
@@ -500,13 +494,6 @@ struct ExprLambda : Expr
Symbol name;
std::unique_ptr<Pattern> pattern;
std::unique_ptr<Expr> body;
// This is a shortcut variant which
// exhausts the body further lambda constructions
// to transform x1: x2: …: xn: b
// into { x1, …, xn }: b
// This can be used when you know that you are
// passing all the arguments at once.
std::unique_ptr<ExprLambda> shortcut;
ExprLambda(PosIdx pos, std::unique_ptr<Pattern> pattern, std::unique_ptr<Expr> body)
: Expr(pos), pattern(std::move(pattern)), body(std::move(body))
{
+11 -8
View File
@@ -326,10 +326,11 @@ template<> struct BuildAST<grammar::v1::attr::simple> {
template<> struct BuildAST<grammar::v1::attr::string> {
static void apply(const auto & in, auto & s, State & ps) {
auto e = s->popExprOnly();
if (auto str = dynamic_cast<ExprString *>(e.get()))
s.pushAttr(ps.symbols.create(str->s), ps.at(in));
else
if (auto estr = dynamic_cast<ExprString *>(e.get())) {
s.pushAttr(ps.symbols.create(estr->str()), ps.at(in));
} else {
s.pushAttr(std::move(e), ps.at(in));
}
}
};
@@ -387,9 +388,9 @@ template<> struct BuildAST<grammar::v1::inherit> : change_head<InheritState> {
for (auto & i : s.attrs) {
if (i.symbol)
continue;
if (auto str = dynamic_cast<ExprString *>(i.expr.get()))
i = AttrName(i.pos, ps.symbols.create(str->s));
else {
if (auto estr = dynamic_cast<ExprString *>(i.expr.get())) {
i = AttrName(i.pos, ps.symbols.create(estr->str()));
} else {
throw ParseError({
.msg = HintFmt("dynamic attributes not allowed in inherit"),
.pos = ps.positions[i.pos]
@@ -770,13 +771,15 @@ template<> struct BuildAST<grammar::v1::path> : change_head<StringState> {
template<typename E>
static void check_slash(PosIdx end, StringState & s, State & ps) {
auto e = dynamic_cast<E *>(s.parts.back().second.get());
if (!e || !e->s.ends_with('/'))
if (!e || !e->str().ends_with('/')) {
return;
if (s.parts.size() > 1 || e->s != "/")
}
if (s.parts.size() > 1 || e->str() != "/") {
throw ParseError({
.msg = HintFmt("path has a trailing slash"),
.pos = ps.positions[end],
});
}
}
static void success(const auto & in, StringState & s, ExprState & e, State & ps) {
+37 -26
View File
@@ -16,6 +16,7 @@
#include "lix/libexpr/value-to-xml.hh"
#include "lix/libexpr/primops.hh"
#include "lix/libfetchers/fetch-to-store.hh"
#include "lix/libutil/c-calls.hh"
#include "lix/libutil/regex.hh"
#include "lix/libutil/types.hh"
#include "value.hh"
@@ -273,12 +274,12 @@ void prim_importNative(EvalState & state, Value * * args, Value & v)
std::string sym(state.forceStringNoCtx(*args[1], noPos, "while evaluating the second argument passed to builtins.importNative"));
void *handle = dlopen(path.canonical().c_str(), RTLD_LAZY | RTLD_LOCAL);
void * handle = dlopen(requireCString(path.canonical().abs()), RTLD_LAZY | RTLD_LOCAL);
if (!handle)
state.ctx.errors.make<EvalError>("could not open '%1%': %2%", path, dlerror()).debugThrow();
dlerror();
ValueInitializer func = reinterpret_cast<ValueInitializer>(dlsym(handle, sym.c_str()));
ValueInitializer func = reinterpret_cast<ValueInitializer>(dlsym(handle, requireCString(sym)));
if(!func) {
char *message = dlerror();
if (message)
@@ -454,7 +455,12 @@ struct CompareValues : NeverAsync
return (*this)(v1, v2, errorCtx);
}
bool operator()(Value & v1, Value & v2, std::string_view errorCtx) const
bool operator()(Value const & v1, Value const & v2) const
{
return (*this)(v1, v2, errorCtx);
}
bool operator()(Value const & v1, Value const & v2, std::string_view errorCtx) const
{
try {
if (v1.type() == nFloat && v2.type() == nInt) {
@@ -479,7 +485,7 @@ struct CompareValues : NeverAsync
case nString:
return v1.str() < v2.str();
case nPath:
return strcmp(v1.string().content, v2.string().content) < 0;
return v1.string().content->str() < v2.string().content->str();
case nList:
// Lexicographic comparison
for (size_t i = 0;; i++) {
@@ -518,9 +524,9 @@ struct CompareValues : NeverAsync
/// NOTE: this type must NEVER be outside of GC-scanned memory.
#if HAVE_BOEHMGC
using UnsafeValueList = std::list<Value *, gc_allocator<Value *>>;
using UnsafeValueList = std::list<Value, gc_allocator<Value>>;
#else
using UnsafeValueList = std::list<Value *>;
using UnsafeValueList = std::list<Value>;
#endif
static const Attr *
@@ -553,7 +559,7 @@ static void prim_genericClosure(EvalState & state, Value * * args, Value & v)
UnsafeValueList workSet;
for (auto & elem : startSet->value.listItems()) {
workSet.push_back(&elem);
workSet.push_back(elem);
}
if (startSet->value.listSize() == 0) {
@@ -581,36 +587,41 @@ static void prim_genericClosure(EvalState & state, Value * * args, Value & v)
// `doneKeys' doesn't need to be a GC root, because its values are
// reachable from res.
auto cmp = CompareValues(state, "while comparing the `key` attributes of two genericClosure elements");
std::set<Value *, decltype(cmp)> doneKeys(cmp);
std::set<Value, decltype(cmp)> doneKeys(cmp);
while (!workSet.empty()) {
Value * e = *(workSet.begin());
Value e = *(workSet.begin());
workSet.pop_front();
state.forceAttrs(*e, noPos, "while evaluating one of the elements generated by (or initially passed to) builtins.genericClosure");
state.forceAttrs(
e,
noPos,
"while evaluating one of the elements generated by (or initially passed to) "
"builtins.genericClosure"
);
auto key = getAttr(
state,
state.ctx.s.key,
e->attrs(),
e.attrs(),
"in one of the attrsets generated by (or initially passed to) builtins.genericClosure"
);
state.forceValue(key->value, noPos);
if (!doneKeys.insert(&key->value).second) {
if (!doneKeys.insert(key->value).second) {
continue;
}
res.push_back(e);
/* Call the `operator' function with `e' as argument. */
Value newElements;
state.callFunction(op->value, {e, 1}, newElements, noPos);
state.callFunction(op->value, {&e, 1}, newElements, noPos);
state.forceList(newElements, noPos, "while evaluating the return value of the `operator` passed to builtins.genericClosure");
/* Add the values returned by the operator to the work set. */
for (auto & elem : newElements.listItems()) {
state.forceValue(elem, noPos); // "while evaluating one one of the elements returned by
// the `operator` passed to builtins.genericClosure");
workSet.push_back(&elem);
workSet.push_back(elem);
}
}
@@ -619,7 +630,7 @@ static void prim_genericClosure(EvalState & state, Value * * args, Value & v)
v = {NewValueAs::list, result};
unsigned int n = 0;
for (auto & i : res)
result->elems[n++] = *i;
result->elems[n++] = i;
}
@@ -842,10 +853,12 @@ static void prim_derivationStrict(EvalState & state, Value * * args, Value & v)
* often results from the composition of several functions
* (derivationStrict, derivation, mkDerivation, mkPythonModule, etc.)
*/
e.addTrace(nullptr, HintFmt(
"while evaluating derivation '%s'\n"
" whose name attribute is located at %s",
drvName, pos));
e.pushTrace(Trace::fromDrv(
state.ctx.positions[nameAttr->pos],
drvName
));
throw;
}
}
@@ -1071,8 +1084,11 @@ drvName, Bindings * attrs, Value & v)
}
} catch (Error & e) {
e.addTrace(state.ctx.positions[i->pos],
HintFmt("while evaluating attribute '%1%' of derivation '%2%'", key, drvName));
e.pushTrace(Trace::fromDrvAttr(
state.ctx.positions[i->pos],
std::string(drvName),
std::string(key)
));
throw;
}
}
@@ -1344,11 +1360,6 @@ static void prim_readFile(EvalState & state, Value * * args, Value & v)
{
auto path = realisePath(state, *args[0]);
auto s = path.readFile();
if (s.find((char) 0) != std::string::npos)
state.ctx.errors.make<EvalError>(
"the contents of the file '%1%' cannot be represented as a Nix string",
path
).debugThrow();
StorePathSet refs;
if (state.ctx.store->isInStore(path.canonical().abs())) {
try {
+6 -6
View File
@@ -45,7 +45,7 @@ private:
/*
* The type that actually stores the string contained inside of the Value.
*/
std::string contents;
std::unique_ptr<Value::Str, Value::Str::Deleter> contents;
Value::String strcb;
@@ -56,8 +56,8 @@ private:
public:
explicit InternedSymbol(std::string_view s)
: contents(s)
, strcb{.content = contents.c_str(), .context = nullptr}
: contents(Value::Str::copy(s))
, strcb{.content = contents.get(), .context = nullptr}
, underlyingValue(NewValueAs::string, &strcb)
{
}
@@ -69,17 +69,17 @@ public:
operator SymbolStr() const
{
return SymbolStr(contents);
return SymbolStr(contents->str());
}
bool operator==(std::string_view s2) const
{
return contents == s2;
return contents->str() == s2;
}
operator std::string_view() const
{
return contents;
return contents->str();
}
Value toValue() const
+165 -5
View File
@@ -14,6 +14,19 @@ Value Value::EMPTY_LIST{Value::list_t{}, &emptyListData};
const Value::Null Value::NULL_ACB = {{Value::Acb::tNull}};
static_assert(alignof(Value::String) >= Value::TAG_ALIGN);
static_assert(alignof(Bindings) >= Value::TAG_ALIGN);
static_assert(alignof(Value::List) >= Value::TAG_ALIGN);
static_assert(alignof(Value::Thunk) >= Value::TAG_ALIGN);
static_assert(alignof(Value::App) >= Value::TAG_ALIGN);
static_assert(alignof(Value::External) >= Value::Acb::TAG_ALIGN);
static_assert(alignof(Value::Float) >= Value::Acb::TAG_ALIGN);
static_assert(alignof(Value::Null) >= Value::Acb::TAG_ALIGN);
static_assert(alignof(Value::PrimOp) >= Value::Acb::TAG_ALIGN);
static_assert(alignof(Value::Int) >= Value::Acb::TAG_ALIGN);
static_assert(alignof(Value::Lambda) >= Value::Acb::TAG_ALIGN);
static void copyContextToValue(Value::String & s, const NixStringContext & context)
{
if (!context.empty()) {
@@ -46,9 +59,11 @@ void Value::mkPrimOp(PrimOp * p)
*this = {NewValueAs::primop, *p};
}
void Value::mkString(std::string_view s)
void Value::mkString(std::string_view s, const char ** context)
{
mkString(gcCopyStringIfNeeded(s));
auto block = gcAllocType<String>();
*block = {.content = Str::gcCopy(s), .context = context};
raw = tag(tString, block);
}
void Value::mkString(std::string_view s, const NixStringContext & context)
@@ -57,10 +72,12 @@ void Value::mkString(std::string_view s, const NixStringContext & context)
copyContextToValue(*untag<String *>(), context);
}
void Value::mkStringMove(const char * s, const NixStringContext & context)
void Value::mkStringMove(Str * s, const NixStringContext & context)
{
mkString(s);
copyContextToValue(*untag<String *>(), context);
auto block = gcAllocType<String>();
*block = {.content = s, .context = nullptr};
raw = tag(tString, block);
copyContextToValue(*block, context);
}
void Value::mkPath(const SourcePath & path)
@@ -68,4 +85,147 @@ void Value::mkPath(const SourcePath & path)
*this = Value(NewValueAs::path, path);
}
#ifndef __APPLE__
[[gnu::section(".debug_gdb_scripts"), gnu::used, gnu::aligned(1)]]
static const char printer_script[] =
"\4"
R"(lix-value-printer
class ValuePrinter(gdb.ValuePrinter):
def __init__(self, val):
self._val = val
self._t_thunk = gdb.lookup_type('nix::Value::Thunk').pointer()
self._t_app = gdb.lookup_type('nix::Value::App').pointer()
self._t_int = gdb.lookup_type('intptr_t')
self._t_string = gdb.lookup_type('nix::Value::String').pointer()
self._t_attrs = gdb.lookup_type('nix::Bindings').pointer()
self._t_list = gdb.lookup_type('nix::Value::List').pointer()
self._t_aux = gdb.lookup_type('nix::Value::Acb').pointer()
def _addr(self, t = None):
addr = self._val['raw'] & ~self._val['TAG_MASK']
return addr.cast(t).referenced_value() if t else addr
def _tag(self):
return self._val['raw'] & self._val['TAG_MASK']
def _v_int(self):
return self._val['raw'].cast(self._t_int) >> self._val['TAG_BITS']
def to_string(self):
if self._tag() == 7:
return self._addr(self._t_aux)
elif not self.children():
return f'undecoded {self._val['raw'].format_string(format='x')}'
return None
def children(self):
match self._tag():
case 0:
return [('thunk', self._addr(self._t_thunk))]
case 1:
return [('app', self._addr(self._t_app))]
case 2:
return [('int', self._v_int())]
case 3:
return [('bool', (self._val['raw'] >> self._val['TAG_BITS']) != 0)]
case 4:
return [('string', self._addr(self._t_string))]
case 5:
return [('attrs', self._addr(self._t_attrs))]
case 6:
return [('list', self._addr(self._t_list))]
case _:
return []
class ThunkPrinter(gdb.ValuePrinter):
def __init__(self, val):
self._val = val
self._t_value = gdb.lookup_type('nix::Value')
self._t_env = gdb.lookup_type('nix::Env').pointer()
def children(self):
if self._val['expr'] == 0:
return [('result', self._val['_result'].cast(self._t_value))]
else:
return [
('env', self._val['_env'].cast(self._t_env).referenced_value()),
('expr', self._val['expr'].referenced_value()),
]
class AppPrinter(gdb.ValuePrinter):
def __init__(self, val):
self._val = val
self._t_value = gdb.lookup_type('nix::Value')
def children(self):
if self._val['_n'] + 1 == 0:
return [('result', self._val['_left'].cast(self._t_value))]
else:
n = int(self._val['_n'])
arr = self._t_value.array(0, n - 1)
return [
('fn', self._val['_left'].cast(self._t_value)),
('n', n),
('args', self._val['_args'][0].cast(arr)),
]
class AcbPrinter(gdb.ValuePrinter):
def __init__(self, val):
self._val = val
self._t_external = gdb.lookup_type('nix::Value::External').pointer()
self._t_float = gdb.lookup_type('nix::Value::Float').pointer()
self._t_primop = gdb.lookup_type('nix::Value::PrimOp')
self._t_primopdetails = gdb.lookup_type('nix::PrimOpDetails')
self._t_lambda = gdb.lookup_type('nix::Value::Lambda').pointer()
self._t_int = gdb.lookup_type('nix::Value::Int').pointer()
def _addr(self, t = None):
addr = self._val['raw'] & ~self._val['TAG_MASK']
return addr.cast(t).referenced_value() if t else addr
def _tag(self):
return self._val['raw'] & self._val['TAG_MASK']
def to_string(self):
match self._tag():
case 2:
return 'null'
case 3:
op = self._val.cast(self._t_primop).cast(self._t_primopdetails)
return f'primop {op['name']}'
case _:
return None
def children(self):
match self._tag():
case 0:
return [('external', self._addr(self._t_external))]
case 1:
return [('float', self._addr(self._t_float)['value'])]
case 4:
return [('lambda', self._addr(self._t_lambda))]
case 5:
return [('int', self._addr(self._t_int)['value'])]
case _:
return []
def value_lookup_function(val):
lookup_tag = val.type.tag
if lookup_tag is None:
return None
if lookup_tag == 'nix::Value':
return ValuePrinter(val)
elif lookup_tag == 'nix::Value::Thunk':
return ThunkPrinter(val)
elif lookup_tag == 'nix::Value::App':
return AppPrinter(val)
elif lookup_tag == 'nix::Value::Acb':
return AcbPrinter(val)
return None
def register_printers(objfile):
objfile.pretty_printers.append(value_lookup_function)
register_printers(gdb.current_objfile())
)";
#endif
}
+129 -88
View File
@@ -7,8 +7,10 @@
#include <cstring>
#include <functional>
#include <limits>
#include <memory>
#include <ranges>
#include <span>
#include <string_view>
#include <type_traits>
#include "lix/libexpr/gc-alloc.hh"
@@ -72,6 +74,8 @@ struct PrimOpDetails
std::optional<ExperimentalFeature> experimentalFeature;
};
// NOTE value.cc contains alignment assertions for pointers tagged thusly.
// *always* ensure that these assertions match the tag types declared here
typedef enum {
// NOTE: tThunk *must* be 0, otherwise invalid value detection breaks
// since invalid values are encoded as thunks with a null thunk state
@@ -247,10 +251,16 @@ struct Value
private:
mutable uintptr_t raw;
public:
static constexpr size_t TAG_BITS = 3;
static constexpr size_t TAG_ALIGN = 1 << TAG_BITS;
static constexpr uintptr_t TAG_MASK = (1 << TAG_BITS) - 1;
private:
// boehmgc always allocate in two-word chunks, which means 8 bytes on 32 bit architectures.
// ensure that malloc must always use at least 8 byte chunks as well so our tags always fit
static_assert(alignof(std::max_align_t) >= Value::TAG_ALIGN);
static uintptr_t tag(InternalType t, auto v)
{
if constexpr (std::is_null_pointer_v<decltype(v)>) {
@@ -266,7 +276,6 @@ private:
T untag() const
{
if constexpr (std::is_pointer_v<T>) {
static_assert(alignof(T) >= TAG_BITS);
return reinterpret_cast<T>(raw & ~TAG_MASK);
} else {
return static_cast<T>((raw & ~TAG_MASK) >> TAG_BITS);
@@ -282,6 +291,67 @@ private:
public:
/**
* Underlying data storage for stringly values (i.e., strings and paths). Stores
* both the length of the string and its contents in a single GC-allocated block
* of memory to reduce overhead in the most common case. This and `String` could
* be merged into a single struct to decrease memory overhead further, but doing
* so precludes us from using atomic allocations that do not need to be scanned,
* increasing GC runtime overhead. We only use this struct to replace C strings.
*/
struct Str
{
struct Deleter
{
void operator()(Str * s)
{
free(s);
}
};
size_t length;
char contents[0];
std::string_view str() const
{
return {contents, length};
}
static Str * gcAlloc(size_t size)
{
auto result = static_cast<Str *>(LIX_GC_MALLOC_ATOMIC(sizeof(Value::Str) + size));
if (result) {
result->length = size;
return result;
}
throw std::bad_alloc();
}
static std::unique_ptr<Str, Deleter> copy(std::string_view s)
{
auto result = alloc(s.size());
memcpy(result->contents, s.data(), s.size());
return {result, {}};
}
static Str * gcCopy(std::string_view s)
{
auto result = gcAlloc(s.size());
memcpy(result->contents, s.data(), s.size());
return result;
}
private:
static Str * alloc(size_t size)
{
if (auto result = static_cast<Str *>(malloc(sizeof(Value::Str) + size))) {
result->length = size;
return result;
}
throw std::bad_alloc();
}
};
/**
* Empty list constant.
*/
@@ -370,7 +440,7 @@ public:
/// Neither the C-string nor the context array are copied; this constructor
/// assumes suitable memory has already been allocated (with the GC if
/// enabled), and string and context data copied into that memory.
Value(string_t, char const * strPtr, char const ** contextPtr = nullptr)
Value(string_t, const Str * strPtr, char const ** contextPtr = nullptr)
{
auto block = gcAllocType<String>();
*block = {.content = strPtr, .context = contextPtr};
@@ -387,7 +457,7 @@ public:
Value(string_t, std::string_view copyFrom, NixStringContext const & context = {})
{
auto block = gcAllocType<String>();
*block = {.content = gcCopyStringIfNeeded(copyFrom), .context = nullptr};
*block = {.content = Str::gcCopy(copyFrom), .context = nullptr};
raw = tag(tString, block);
if (context.empty()) {
@@ -408,39 +478,6 @@ public:
block->context[n] = nullptr;
}
/// Constructx a nix language value of type "string", with the value of the
/// C-string pointed to by @ref strPtr, and optionally with a set of string
/// context @ref context.
///
/// The C-string is not copied; this constructor assumes suitable memory
/// has already been allocated (with the GC if enabled), and string data
/// has been copied into that memory. The context data *is* copied from
/// @ref context, and this constructor performs a dynamic (GC) allocation
/// to do so.
Value(string_t, char const * strPtr, NixStringContext const & context)
{
auto block = gcAllocType<String>();
*block = {.content = strPtr, .context = nullptr};
raw = tag(tString, block);
if (context.empty()) {
// It stays nullptr
return;
}
// Copy the context.
block->context = gcAllocType<char const *>(context.size() + 1);
size_t n = 0;
for (NixStringContextElem const & contextElem : context) {
block->context[n] = gcCopyStringIfNeeded(contextElem.to_string());
n += 1;
}
// Terminator sentinel.
block->context[n] = nullptr;
}
/// Constructs a nix language value of type "path", with the value of the
/// C-string pointed to by @ref strPtr.
///
@@ -460,7 +497,7 @@ public:
Value(path_t, SourcePath const & path)
{
auto block = gcAllocType<String>();
*block = {.content = gcCopyStringIfNeeded(path.canonical().abs()), .context = String::path};
*block = {.content = Str::gcCopy(path.canonical().abs()), .context = String::path};
raw = tag(tString, block);
}
@@ -507,6 +544,10 @@ public:
/// lazy and/or partial application of a function.
Value(app_t, EvalMemory & mem, Value & lhs, std::span<Value> args);
/// Constructs a nix language value of type "lambda", which represents a
/// lazy and/or partial application of a function.
Value(app_t, EvalMemory & mem, const Value & lhs, std::span<Value> baseArgs, std::span<Value> moreArgs);
/// Constructs a nix language value of type "external", which is only used
/// by plugins. Do any existing plugins even use this mechanism?
Value(external_t, ExternalValueBase & external)
@@ -569,10 +610,7 @@ public:
{
return internalType() == tApp;
}
inline bool isBlackhole() const
{
return internalType() == tThunk && untag<const Thunk *>()->expr == blackHole.expr;
}
inline bool isBlackhole() const;
inline bool isInvalid() const
{
return raw == 0;
@@ -616,7 +654,7 @@ public:
/// marker location for paths, to be used as path context.
static inline const char * path[] = {"\1<path>", nullptr};
const char * content;
const Str * content;
const char ** context; // must be in sorted order
bool isPath() const
@@ -631,6 +669,8 @@ public:
/// these blocks are usually heap-allocated in GC memory space.
struct alignas(TAG_ALIGN) Acb
{
// NOTE value.cc contains alignment assertions for pointers tagged thusly.
// *always* ensure that these assertions match the tag types declared here
enum Type {
tExternal,
tFloat,
@@ -661,7 +701,6 @@ public:
T untag() const
{
if constexpr (std::is_pointer_v<T>) {
static_assert(alignof(T) >= TAG_BITS);
return reinterpret_cast<T>(raw & ~TAG_MASK);
} else {
return static_cast<T>((raw & ~TAG_MASK) >> TAG_BITS);
@@ -685,7 +724,7 @@ public:
{};
struct PrimOp : Acb, PrimOpDetails
{
explicit PrimOp(PrimOpDetails && p) : Acb{tPrimOp}, PrimOpDetails(std::move(p)) {}
explicit PrimOp(PrimOpDetails p) : Acb{tPrimOp}, PrimOpDetails(std::move(p)) {}
};
struct Int : Acb
{
@@ -703,37 +742,6 @@ public:
return untag<Env *>();
}
};
struct alignas(TAG_ALIGN) Thunk
{
union {
uintptr_t _env;
uintptr_t _result;
};
Expr * expr;
bool resolved() const
{
return expr == nullptr;
}
void resolve(Value v)
{
_result = v.raw;
expr = nullptr;
}
Env * env() const
{
return reinterpret_cast<Env *>(_env);
}
Value result() const
{
Value v;
v.raw = _result;
return v;
}
};
/**
* Returns the normal type of a Value. This only returns nThunk if
@@ -759,25 +767,18 @@ public:
raw = tag(tBool, b);
}
inline void mkString(const char * s, const char * * context = 0)
{
auto block = gcAllocType<String>();
*block = {.content = s, .context = context};
raw = tag(tString, block);
}
void mkString(std::string_view s);
void mkString(std::string_view s, const char ** context = 0);
void mkString(std::string_view s, const NixStringContext & context);
void mkStringMove(const char * s, const NixStringContext & context);
void mkStringMove(Str * s, const NixStringContext & context);
void mkPath(const SourcePath & path);
inline void mkPath(const char * path)
{
auto block = gcAllocType<String>();
*block = {.content = path, .context = String::path};
*block = {.content = Str::gcCopy(path), .context = String::path};
raw = tag(tString, block);
}
@@ -838,13 +839,13 @@ public:
SourcePath path() const
{
assert(internalType() == tString && untag<const String *>()->isPath());
return SourcePath{CanonPath(untag<const String *>()->content)};
return SourcePath{CanonPath(untag<const String *>()->content->str())};
}
std::string_view str() const
{
assert(internalType() == tString && !untag<const String *>()->isPath());
return std::string_view(untag<const String *>()->content);
return std::string_view(untag<const String *>()->content->str());
}
NixInt integer() const
@@ -911,6 +912,41 @@ public:
{
return untag<const Acb *>();
}
uintptr_t pointerEqProxy() const
{
return raw;
}
};
struct alignas(Value::TAG_ALIGN) Value::Thunk
{
union {
Env * _env;
Value _result;
};
Expr * expr;
bool resolved() const
{
return expr == nullptr;
}
void resolve(Value v)
{
_result = v;
expr = nullptr;
}
Env * env() const
{
return _env;
}
Value result() const
{
return _result;
}
};
struct alignas(Value::TAG_ALIGN) Value::List
@@ -1003,7 +1039,7 @@ again:
abort();
}
} else if (thunk().resolved()) {
raw = thunk()._env;
raw = thunk().result().raw;
goto again;
}
return nThunk;
@@ -1016,6 +1052,11 @@ again:
}
}
inline bool Value::isBlackhole() const
{
return internalType() == tThunk && untag<const Thunk *>()->expr == blackHole.expr;
}
inline bool Value::isPrimOpApp() const
{
return internalType() == tApp && !app().resolved() && app().target().isPrimOp();
+3 -3
View File
@@ -10,7 +10,7 @@ kj::Promise<Result<StorePath>> fetchToStoreFlat(
std::string_view name,
RepairFlag repair)
try {
Activity act(*logger, lvlChatty, actUnknown, fmt("copying '%s' to the store", path));
auto act = logger->startActivity(lvlChatty, actUnknown, fmt("copying '%s' to the store", path));
auto physicalPath = path.canonical().abs();
co_return settings.readOnlyMode
@@ -26,8 +26,8 @@ kj::Promise<Result<StorePath>> fetchToStoreRecursive(
std::string_view name,
RepairFlag repair)
try {
Activity act(
*logger, lvlChatty, actUnknown, fmt("copying '%s' to the store", contents.rootPath)
auto act = logger->startActivity(
lvlChatty, actUnknown, fmt("copying '%s' to the store", contents.rootPath)
);
co_return settings.readOnlyMode
+25 -16
View File
@@ -1,6 +1,7 @@
#include "lix/libutil/archive.hh"
#include "lix/libutil/async-io.hh"
#include "lix/libutil/async.hh"
#include "lix/libutil/c-calls.hh"
#include "lix/libutil/error.hh"
#include "lix/libfetchers/fetchers.hh"
#include "lix/libfetchers/cache.hh"
@@ -20,6 +21,7 @@
#include "lix/libfetchers/fetch-settings.hh"
#include <optional>
#include <regex>
#include <string.h>
#include <sys/time.h>
@@ -51,7 +53,7 @@ bool touchCacheFile(const Path & path, time_t touch_time)
times[1].tv_sec = touch_time;
times[1].tv_usec = 0;
return lutimes(path.c_str(), times) == 0;
return sys::lutimes(path, times) == 0;
}
Path getCachePath(std::string_view key)
@@ -69,15 +71,13 @@ Path getCachePath(std::string_view key)
// ...
static kj::Promise<Result<std::optional<std::string>>> readHead(const Path & path)
try {
auto [status, output] = TRY_AWAIT(runProgram(RunOptions{
.program = "git",
auto output = TRY_AWAIT(runProgram(
"git",
true,
// FIXME: use 'HEAD' to avoid returning all refs
.args = {"ls-remote", "--symref", path},
.isInteractive = true,
}));
if (status != 0) {
co_return std::nullopt;
}
{"ls-remote", "--symref", path},
true
));
std::string_view line = output;
line = line.substr(0, line.find("\n"));
@@ -93,6 +93,8 @@ try {
co_return parseResult->target;
}
co_return std::nullopt;
} catch (ExecError &) {
co_return std::nullopt;
} catch (...) {
co_return result::current_exception();
}
@@ -128,7 +130,7 @@ try {
time_t now = time(0);
struct stat st;
std::optional<std::string> cachedRef;
if (stat(headRefFile.c_str(), &st) == 0) {
if (sys::stat(headRefFile, &st) == 0) {
cachedRef = TRY_AWAIT(readHead(cacheDir));
if (cachedRef != std::nullopt &&
*cachedRef != gitInitialBranch &&
@@ -737,8 +739,7 @@ struct GitInputScheme : InputScheme
/* If the local ref is older than tarball-ttl seconds, do a
git fetch to update the local ref to the remote ref. */
struct stat st;
return stat(path.c_str(), &st) == 0 &&
isCacheFileWithinTtl(now, st);
return sys::stat(path, &st) == 0 && isCacheFileWithinTtl(now, st);
};
if (auto result = resolveRefToCachePath(
input,
@@ -758,7 +759,9 @@ struct GitInputScheme : InputScheme
// Because git needs to figure out what we're fetching
// (i.e. is it a rev? a branch? a tag?)
if (doFetch) {
Activity act(*logger, lvlTalkative, actUnknown, fmt("fetching Git repository '%s'", actualUrl));
auto act = logger->startActivity(
lvlTalkative, actUnknown, fmt("fetching Git repository '%s'", actualUrl)
);
auto ref = input.getRef();
std::string fetchRef;
@@ -893,7 +896,9 @@ struct GitInputScheme : InputScheme
// TODO: repoDir might lack the ref (it only checks if rev
// exists, see FIXME above) so use a big hammer and fetch
// everything to ensure we get the rev.
Activity act(*logger, lvlTalkative, actUnknown, fmt("making temporary clone of '%s'", repoDir));
auto act = logger->startActivity(
lvlTalkative, actUnknown, fmt("making temporary clone of '%s'", repoDir)
);
TRY_AWAIT(runProgram(
"git",
true,
@@ -932,13 +937,17 @@ struct GitInputScheme : InputScheme
source repo if it exists. */
auto modulesPath = repoDir + "/" + gitDir + "/modules";
if (pathExists(modulesPath)) {
Activity act(*logger, lvlTalkative, actUnknown, fmt("copying submodules of '%s'", actualUrl));
auto act = logger->startActivity(
lvlTalkative, actUnknown, fmt("copying submodules of '%s'", actualUrl)
);
TRY_AWAIT(runProgram("cp", true, {"-R", "--", modulesPath, tmpGitDir + "/modules"})
);
}
{
Activity act(*logger, lvlTalkative, actUnknown, fmt("fetching submodules of '%s'", actualUrl));
auto act = logger->startActivity(
lvlTalkative, actUnknown, fmt("fetching submodules of '%s'", actualUrl)
);
TRY_AWAIT(runProgram(
"git",
true,
+3 -1
View File
@@ -307,7 +307,9 @@ struct MercurialInputScheme : InputScheme
.second
== "1"))
{
Activity act(*logger, lvlTalkative, actUnknown, fmt("fetching Mercurial repository '%s'", actualUrl));
auto act = logger->startActivity(
lvlTalkative, actUnknown, fmt("fetching Mercurial repository '%s'", actualUrl)
);
if (pathExists(cacheDir)) {
try {
+1 -1
View File
@@ -131,7 +131,7 @@ struct PathInputScheme : InputScheme
} else
absPath = path;
Activity act(*logger, lvlTalkative, actUnknown, fmt("copying '%s'", absPath));
auto act = logger->startActivity(lvlTalkative, actUnknown, fmt("copying '%s'", absPath));
// FIXME: check whether access to 'path' is allowed.
auto storePath = store->maybeParseStorePath(absPath);
+23 -11
View File
@@ -104,31 +104,34 @@ bool ProgressBar::isVerbose()
return printBuildLogs;
}
void ProgressBar::log(Verbosity lvl, std::string_view s)
Logger::BufferState ProgressBar::log(Verbosity lvl, std::string_view s)
{
if (lvl > verbosity) return;
if (lvl > verbosity) {
return BufferState::HasSpace;
}
auto state(state_.lock());
log(*state, lvl, s);
return log(*state, lvl, s);
}
void ProgressBar::logEI(const ErrorInfo & ei)
Logger::BufferState ProgressBar::logEI(const ErrorInfo & ei)
{
auto state(state_.lock());
std::stringstream oss;
showErrorInfo(oss, ei, loggerSettings.showTrace.get());
log(*state, ei.level, oss.str());
return log(*state, ei.level, oss.str());
}
void ProgressBar::log(State & state, Verbosity lvl, std::string_view s)
Logger::BufferState ProgressBar::log(State & state, Verbosity lvl, std::string_view s)
{
if (state.paused == 0) eraseProgressDisplay(state);
writeLogsToStderr(filterANSIEscapes(s + ANSI_NORMAL "\n", !isTTY));
restoreProgressDisplay(state);
return BufferState::HasSpace;
}
void ProgressBar::startActivity(
Logger::BufferState ProgressBar::startActivityImpl(
ActivityId act,
Verbosity lvl,
ActivityType type,
@@ -140,7 +143,7 @@ void ProgressBar::startActivity(
auto state(state_.lock());
if (lvl <= verbosity && !s.empty() && type != actBuildWaiting)
log(*state, lvl, s + "...");
(void) log(*state, lvl, s + "...");
state->activities.emplace_back(ActInfo {
.s = s,
@@ -198,6 +201,7 @@ void ProgressBar::startActivity(
i->visible = false;
update(*state);
return BufferState::HasSpace;
}
/* Check whether an activity has an ancestore with the specified
@@ -213,7 +217,7 @@ bool ProgressBar::hasAncestor(State & state, ActivityType type, ActivityId act)
return false;
}
void ProgressBar::stopActivity(ActivityId act)
Logger::BufferState ProgressBar::stopActivityImpl(ActivityId act)
{
auto state(state_.lock());
@@ -233,9 +237,11 @@ void ProgressBar::stopActivity(ActivityId act)
}
update(*state);
return BufferState::HasSpace;
}
void ProgressBar::result(ActivityId act, ResultType type, const std::vector<Field> & fields)
Logger::BufferState
ProgressBar::resultImpl(ActivityId act, ResultType type, const std::vector<Field> & fields)
{
auto state(state_.lock());
@@ -256,7 +262,11 @@ void ProgressBar::result(ActivityId act, ResultType type, const std::vector<Fiel
if (type == resPostBuildLogLine) {
suffix = " (post)> ";
}
log(*state, lvlInfo, ANSI_FAINT + info.name.value_or("unnamed") + suffix + ANSI_NORMAL + lastLine);
(void) log(
*state,
lvlInfo,
ANSI_FAINT + info.name.value_or("unnamed") + suffix + ANSI_NORMAL + lastLine
);
} else {
if (!printMultiline) {
state->activities.erase(i->second);
@@ -310,6 +320,8 @@ void ProgressBar::result(ActivityId act, ResultType type, const std::vector<Fiel
state->activitiesByType[type].expected += j;
update(*state);
}
return BufferState::HasSpace;
}
void ProgressBar::update(State & state)
+7 -6
View File
@@ -75,13 +75,13 @@ struct ProgressBar : public Logger
bool isVerbose() override;
void log(Verbosity lvl, std::string_view s) override;
BufferState log(Verbosity lvl, std::string_view s) override;
void logEI(const ErrorInfo & ei) override;
BufferState logEI(const ErrorInfo & ei) override;
void log(State & state, Verbosity lvl, std::string_view s);
BufferState log(State & state, Verbosity lvl, std::string_view s);
void startActivity(
BufferState startActivityImpl(
ActivityId act,
Verbosity lvl,
ActivityType type,
@@ -92,9 +92,10 @@ struct ProgressBar : public Logger
bool hasAncestor(State & state, ActivityType type, ActivityId act);
void stopActivity(ActivityId act) override;
BufferState stopActivityImpl(ActivityId act) override;
void result(ActivityId act, ResultType type, const std::vector<Field> & fields) override;
BufferState
resultImpl(ActivityId act, ResultType type, const std::vector<Field> & fields) override;
void update(State & state);
+5 -5
View File
@@ -3,6 +3,7 @@
#include "lix/libmain/shared.hh"
#include "lix/libstore/store-api.hh"
#include "lix/libstore/gc-store.hh"
#include "lix/libutil/c-calls.hh"
#include "lix/libutil/result.hh"
#include "lix/libutil/signals.hh"
#include "lix/libmain/loggers.hh"
@@ -307,13 +308,12 @@ void printVersion(const std::string & programName)
void showManPage(const std::string & name)
{
restoreProcessContext();
setenv("MANPATH", settings.nixManDir.c_str(), 1);
execlp("man", "man", name.c_str(), nullptr);
(void) sys::setenv("MANPATH", settings.nixManDir, 1);
execlp("man", "man", requireCString(name).asCStr(), nullptr);
throw SysError("command 'man %1%' failed", name.c_str());
}
int handleExceptions(const std::string & programName, std::function<void()> fun)
int handleExceptions(const std::string & programName, std::function<int()> fun)
{
ReceiveInterrupts receiveInterrupts; // FIXME: need better place for this
@@ -323,7 +323,7 @@ int handleExceptions(const std::string & programName, std::function<void()> fun)
bool onlyForSubcommands = baseNameOf(programName) == "lix";
try {
fun();
return fun();
} catch (Exit & e) {
return e.status;
} catch (UsageError & e) {
+1 -1
View File
@@ -14,7 +14,7 @@
namespace nix {
int handleExceptions(const std::string & programName, std::function<void()> fun);
int handleExceptions(const std::string & programName, std::function<int()> fun);
/**
* Don't forget to call initPlugins() after settings are initialized!
+4 -4
View File
@@ -225,6 +225,7 @@ try {
auto * buildIdDir = std::get_if<nar_index::Directory>(&narIndex);
for (auto subdir : { "lib", "debug", ".build-id" }) {
if (!buildIdDir) break;
// get returns nullptr subdir does not exist, and std::get_if propagates it.
buildIdDir = std::get_if<nar_index::Directory>(get(buildIdDir->contents, subdir));
}
@@ -463,18 +464,17 @@ BinaryCacheStore::queryPathInfoUncached(const StorePath & storePath, const Activ
try {
auto uri = getUri();
auto storePathS = printStorePath(storePath);
auto act = std::make_shared<Activity>(
*logger,
auto act = logger->startActivity(
lvlTalkative,
actQueryPathInfo,
fmt("querying info about '%s' on '%s'", storePathS, uri),
Logger::Fields{storePathS, uri},
context ? context->id : 0
context
);
auto narInfoFile = narInfoFileFor(storePath);
auto data = TRY_AWAIT(getFileContents(narInfoFile, act.get()));
auto data = TRY_AWAIT(getFileContents(narInfoFile, &act));
if (!data) co_return result::success(nullptr);
+15 -4
View File
@@ -19,8 +19,12 @@ struct BinaryCacheStoreConfig : virtual StoreConfig
{
using StoreConfig::StoreConfig;
const Setting<std::string> compression{this, "xz", "compression",
"NAR compression method (`xz`, `bzip2`, `gzip`, `zstd`, or `none`)."};
const Setting<std::string> compression{
this,
"zstd",
"compression",
"NAR compression method (`xz`, `bzip2`, `gzip`, `zstd`, or `none`)."
};
const Setting<bool> writeNARListing{this, false, "write-nar-listing",
"Whether to write a JSON file that lists the files in each NAR."};
@@ -40,12 +44,19 @@ struct BinaryCacheStoreConfig : virtual StoreConfig
const Setting<bool> parallelCompression{this, false, "parallel-compression",
"Enable multi-threaded compression of NARs. This is currently only available for `xz` and `zstd`."};
const Setting<int> compressionLevel{this, -1, "compression-level",
const Setting<int> compressionLevel{
this,
-1,
"compression-level",
R"(
The *preset level* to be used when compressing NARs.
The meaning and accepted values depend on the compression method selected.
`-1` specifies that the default compression level should be used.
)"};
Note: when using zstd `-1` will select level 12 to approximately match xz compression
ratios at default settings rather than the zstd library default of 3.
)"
};
};
+6 -4
View File
@@ -1,3 +1,4 @@
#include "lix/libutil/c-calls.hh"
#include "lix/libutil/current-process.hh"
#include "lix/libutil/logging.hh"
@@ -22,12 +23,13 @@ void commonExecveingChildInit()
throw SysError("cannot dup stderr into stdout");
/* Reroute stdin to /dev/null. */
int fdDevNull = open(pathNullDevice.c_str(), O_RDWR);
if (fdDevNull == -1)
auto fdDevNull = sys::open(pathNullDevice, O_RDWR);
if (!fdDevNull) {
throw SysError("cannot open '%1%'", pathNullDevice);
if (dup2(fdDevNull, STDIN_FILENO) == -1)
}
if (dup2(fdDevNull.get(), STDIN_FILENO) == -1) {
throw SysError("cannot dup null device into stdin");
close(fdDevNull);
}
}
}
+143 -261
View File
@@ -1,6 +1,7 @@
#include "lix/libstore/build/derivation-goal.hh"
#include "lix/libutil/async-io.hh"
#include "lix/libutil/async.hh"
#include "lix/libutil/c-calls.hh"
#include "lix/libutil/file-descriptor.hh"
#include "lix/libutil/file-system.hh"
#include "lix/libstore/build/hook-instance.hh"
@@ -22,12 +23,18 @@
#include <boost/outcome/try.hpp>
#include <capnp/rpc-twoparty.h>
#include <cstdint>
#include <exception>
#include <fstream>
#include <kj/array.h>
#include <kj/async-unix.h>
#include <kj/async.h>
#include <kj/debug.h>
#include <kj/exception.h>
#include <kj/time.h>
#include <kj/vector.h>
#include <limits>
#include <memory>
#include <optional>
#include <ranges>
#include <sys/types.h>
@@ -120,7 +127,6 @@ DerivationGoal::~DerivationGoal() noexcept(false)
void DerivationGoal::killChild()
{
hook.reset();
builderOutFD = nullptr;
}
@@ -133,10 +139,13 @@ Goal::WorkResult DerivationGoal::timedOut(Error && ex)
kj::Promise<Result<Goal::WorkResult>> DerivationGoal::workImpl() noexcept
{
// always clear the slot token, no matter what happens. not doing this
// can cause builds to get stuck on exceptions (or other early exits).
// ideally we'd use scoped slot tokens instead of keeping them in some
// goal member variable, but we cannot do this yet for legacy reasons.
KJ_DEFER({
act.reset();
actLock.reset();
builderActivities.clear();
slotToken = {};
});
BOOST_OUTCOME_CO_TRY(auto result, co_await (useDerivation ? getDerivation() : haveDerivation()));
@@ -607,17 +616,14 @@ try {
co_return result::current_exception();
}
void DerivationGoal::started()
std::string DerivationGoal::buildDescription() const
{
auto msg = fmt(
buildMode == bmRepair ? "repairing outputs of '%s'" :
buildMode == bmCheck ? "checking outputs of '%s'" :
"building '%s'", worker.store.printStorePath(drvPath));
fmt("building '%s'", worker.store.printStorePath(drvPath));
if (hook) msg += fmt(" on '%s'", machineName);
act = std::make_unique<Activity>(*logger, lvlInfo, actBuild, msg,
Logger::Fields{worker.store.printStorePath(drvPath), hook ? machineName : "", 1, 1});
mcRunningBuilds = worker.runningBuilds.addTemporarily(1);
return fmt(
buildMode == bmRepair ? "repairing outputs of '%s'"
: buildMode == bmCheck ? "checking outputs of '%s'"
: "building '%s'",
worker.store.printStorePath(drvPath)
);
}
kj::Promise<Result<Goal::WorkResult>> DerivationGoal::tryToBuild() noexcept
@@ -648,8 +654,11 @@ retry:
outputLocks = tryLockPaths(lockFiles);
if (!outputLocks) {
if (!actLock)
actLock = std::make_unique<Activity>(*logger, lvlWarn, actBuildWaiting,
fmt("waiting for lock on %s", Magenta(showPaths(lockFiles))));
actLock = logger->startActivity(
lvlWarn,
actBuildWaiting,
fmt("waiting for lock on %s", Magenta(showPaths(lockFiles)))
);
co_await waitForAWhile();
// we can loop very often, and `co_return co_await` always allocates a new frame
goto retry;
@@ -705,8 +714,12 @@ retry:
/* Not now; wait until at least one child finishes or
the wake-up timeout expires. */
if (!actLock)
actLock = std::make_unique<Activity>(*logger, lvlTalkative, actBuildWaiting,
fmt("waiting for a machine to build '%s'", Magenta(worker.store.printStorePath(drvPath))));
actLock = logger->startActivity(
lvlTalkative,
actBuildWaiting,
fmt("waiting for a machine to build '%s'",
Magenta(worker.store.printStorePath(drvPath)))
);
outputLocks.reset();
co_await waitForAWhile();
goto retry;
@@ -763,11 +776,7 @@ void replaceValidPath(const Path & storePath, const Path & tmpPath)
we're repairing (say) Glibc, we end up with a broken system. */
Path oldPath;
if (pathExists(storePath)) {
do {
oldPath = makeTempPath(storePath, ".old");
// store paths are often directories so we can't just unlink() it
// let's make sure the path doesn't exist before we try to use it
} while (pathExists(oldPath));
oldPath = makeTempSiblingPath(storePath);
movePath(storePath, oldPath);
}
@@ -791,16 +800,10 @@ void replaceValidPath(const Path & storePath, const Path & tmpPath)
int DerivationGoal::getChildStatus()
{
builderOutFD = nullptr;
return hook->pid.kill();
return hook->kill();
}
void DerivationGoal::closeReadPipes()
{
hook->fromHook.reset();
builderOutFD = nullptr;
}
void DerivationGoal::closeReadPipes() {}
void DerivationGoal::cleanupHookFinally()
{
@@ -841,8 +844,7 @@ try {
co_return result::success();
}
Activity act(
logger,
auto act = logger.startActivity(
lvlTalkative,
actPostBuildHook,
fmt("running post-build-hook '%s'", settings.postBuildHook),
@@ -881,7 +883,7 @@ try {
const std::string_view data{buffer.data(), *got};
for (auto c : data) {
if (c == '\n') {
act.result(resPostBuildLogLine, currentLine);
ACTIVITY_RESULT(act, resPostBuildLogLine, currentLine);
currentLine.clear();
} else {
currentLine += c;
@@ -891,7 +893,7 @@ try {
if (currentLine != "") {
currentLine += '\n';
act.result(resPostBuildLogLine, currentLine);
ACTIVITY_RESULT(act, resPostBuildLogLine, currentLine);
}
wait.run();
@@ -1035,15 +1037,20 @@ try {
}
namespace {
struct BuildHookLogger final : rpc::build_remote::HookInstance::BuildLogger::Server
struct ActivityTrackingHookLogger final : HookInstance::HookLogger
{
AutoCloseFD fd;
kj::TimePoint & tracker;
BuildHookLogger(AutoCloseFD fd) : fd(std::move(fd)) {}
kj::Maybe<int> getFd() override
ActivityTrackingHookLogger(const Activity & act, FinishSink * logSink, kj::TimePoint & tracker)
: HookLogger(act, logSink)
, tracker(tracker)
{
return fd.get();
}
kj::Promise<void> push(PushContext context) override
{
tracker = AIO().provider.getTimer().now();
return HookLogger::push(context);
}
};
}
@@ -1061,24 +1068,16 @@ try {
hook = std::move(worker.hook.instances.front());
worker.hook.instances.pop_front();
} else {
hook = TRY_AWAIT(HookInstance::create());
hook = TRY_AWAIT(HookInstance::create(worker.act));
}
// open a pipe to receive logs directly from the hook
Pipe logPipe;
logPipe.create();
builderOutFD = &logPipe.readSide;
KJ_DEFER(builderOutFD = nullptr);
KJ_DEFER(hook = nullptr);
auto output = handleChildOutput();
auto buildReq = hook->rpc.buildRequest();
auto buildReq = hook->rpc->buildRequest();
RPC_FILL(buildReq, setAmWilling, slotToken.valid());
RPC_FILL(buildReq, setNeededSystem, drv->platform);
RPC_FILL(buildReq, initDrvPath, drvPath, worker.store);
RPC_FILL(buildReq, initRequiredFeatures, parsedDrv->getRequiredSystemFeatures());
buildReq.setBuildLogger(kj::heap<BuildHookLogger>(std::move(logPipe.writeSide)));
auto buildRespPromise = buildReq.send();
auto buildResp = TRY_AWAIT_RPC(buildRespPromise);
@@ -1100,11 +1099,15 @@ try {
// the build was accepted by the hook, we can free the slot for another build now
hookSlot = {};
machineName = rpc::to<std::string>(buildResp.getAccept().getMachineName());
/* Create the log file and pipe. */
openLogFile();
auto runReq = buildResp.getAccept().getMachine().runRequest();
/* Tell the hook all the inputs that have to be copied to the
remote system. */
runReq.setLogger(
kj::heap<ActivityTrackingHookLogger>(worker.act, logSink.get(), lastChildActivity)
);
RPC_FILL(runReq, initInputs, inputPaths, worker.store);
/* Tell the hooks the missing outputs that have to be copied back
@@ -1117,35 +1120,35 @@ try {
missingOutputs.insert(outputName);
}
RPC_FILL(runReq, initWantedOutputs, missingOutputs);
RPC_FILL(runReq, setDescription, buildDescription());
}
/* Create the log file and pipe. */
openLogFile();
auto runPromise = runReq.send();
// build via hook is now properly running. wait for it to finish
actLock.reset();
buildResult.startTime = time(0); // inexact
started();
mcRunningBuilds = worker.runningBuilds.addTemporarily(1);
auto result = co_await runPromise;
auto result = TRY_AWAIT(
wrapChildHandler(runPromise.then([&](auto result) -> kj::Promise<Result<WorkResult>> {
try {
std::shared_ptr<Error> remoteError;
if (result.getResult().isBad()) {
remoteError = std::make_shared<Error>(from(result.getResult().getBad()));
logErrorInfo(remoteError->info().level, remoteError->info());
}
// close the rpc connection to have the hook exit
hook->rpc = nullptr;
hook->wait();
return buildDone(remoteError);
} catch (...) {
return {result::current_exception()};
}
}))
);
// close the rpc connection to have the hook exit
hook->rpc = nullptr;
hook->client = std::nullopt;
hook->conn = nullptr;
if (auto error = TRY_AWAIT(output)) {
co_return HookResult::Accept{std::move(*error)};
}
std::shared_ptr<Error> remoteError;
if (result.getResult().isBad()) {
remoteError = std::make_shared<Error>(from(result.getResult().getBad()));
logErrorInfo(remoteError->info().level, remoteError->info());
}
co_return HookResult::Accept{TRY_AWAIT(buildDone(remoteError))};
co_return HookResult::Accept{std::move(result)};
} catch (...) {
co_return result::current_exception();
}
@@ -1162,10 +1165,39 @@ kj::Promise<Result<SingleDrvOutputs>> DerivationGoal::registerOutputs()
return assertPathValidity();
}
DerivationGoal::LogSink::LogSink(AutoCloseFD fd, ref<BufferedSink> file, bool compress, uint64_t limit)
: fd(std::move(fd))
, file(file)
, target(compress ? makeCompressionSink("bzip2", *file) : file)
, limit(limit)
{
}
DerivationGoal::LogSink::~LogSink() noexcept
{
signal.fulfiller->fulfill(false);
}
void DerivationGoal::LogSink::operator()(std::string_view data)
{
writtenSoFar += data.size();
if (writtenSoFar <= limit) {
(*target)(data);
} else {
signal.fulfiller->fulfill(true);
}
}
void DerivationGoal::LogSink::finish()
{
if (auto inner2 = dynamic_cast<FinishSink *>(&*target)) {
inner2->finish();
}
file->flush();
}
Path DerivationGoal::openLogFile()
{
logSize = 0;
if (!settings.keepLog) return "";
auto baseName = std::string(baseNameOf(worker.store.printStorePath(drvPath)));
@@ -1182,27 +1214,26 @@ Path DerivationGoal::openLogFile()
Path logFileName = fmt("%s/%s%s", dir, baseName.substr(2),
settings.compressLog ? ".bz2" : "");
fdLogFile = AutoCloseFD{open(logFileName.c_str(), O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC, 0666)};
auto fdLogFile = sys::open(logFileName, O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC, 0666);
if (!fdLogFile) throw SysError("creating log file '%1%'", logFileName);
logFileSink = std::make_shared<FdSink>(fdLogFile.get());
auto logFileSink = make_ref<FdSink>(fdLogFile.get());
const auto logLimit =
settings.maxLogSize ? settings.maxLogSize.get() : std::numeric_limits<uint64_t>::max();
if (settings.compressLog)
logSink = std::shared_ptr<CompressionSink>(makeCompressionSink("bzip2", *logFileSink));
else
logSink = logFileSink;
logSink = std::make_unique<LogSink>(
std::move(fdLogFile), logFileSink, settings.compressLog, logLimit
);
return logFileName;
}
void DerivationGoal::closeLogFile()
{
auto logSink2 = std::dynamic_pointer_cast<CompressionSink>(logSink);
if (logSink2) logSink2->finish();
if (logFileSink) logFileSink->flush();
logSink = logFileSink = 0;
fdLogFile.reset();
if (logSink) {
logSink->finish();
}
logSink = 0;
}
@@ -1215,130 +1246,19 @@ Goal::WorkResult DerivationGoal::tooMuchLogs()
getName(), settings.maxLogSize));
}
kj::Promise<Result<std::optional<Goal::WorkResult>>>
DerivationGoal::handleBuilderOutput(AsyncInputStream & in) noexcept
try {
auto buf = kj::heapArray<char>(4096);
while (true) {
std::string_view data;
try {
if (const auto got = TRY_AWAIT(in.read(buf.begin(), buf.size()))) {
data = {buf.begin(), *got};
} else {
co_return std::nullopt;
}
} catch (SysError & e) {
// the builder output stream may be a pty fd, and closing one pty
// endpoint sends EIO to the other endpoint. this is a good exit.
if (e.errNo == EIO) {
data = {};
} else {
throw;
}
}
lastChildActivity = AIO().provider.getTimer().now();
if (data.empty()) {
co_return std::nullopt;
}
logSize += data.size();
if (settings.maxLogSize && logSize > settings.maxLogSize) {
co_return tooMuchLogs();
}
for (auto c : data)
if (c == '\r')
currentLogLinePos = 0;
else if (c == '\n')
flushLine();
else {
if (currentLogLinePos >= currentLogLine.size())
currentLogLine.resize(currentLogLinePos + 1);
currentLogLine[currentLogLinePos++] = c;
}
if (logSink) (*logSink)(data);
kj::Promise<Result<Goal::WorkResult>>
DerivationGoal::wrapChildHandler(kj::Promise<Result<WorkResult>> handler) noexcept
{
if (respectsTimeouts() && settings.maxSilentTime != 0) {
handler = handler.exclusiveJoin(monitorForSilence());
}
} catch (...) {
co_return result::current_exception();
}
kj::Promise<Result<std::optional<Goal::WorkResult>>>
DerivationGoal::handleHookOutput(AsyncInputStream & in) noexcept
try {
auto buf = kj::heapArray<char>(4096);
while (true) {
const auto got = TRY_AWAIT(in.read(buf.begin(), buf.size()));
if (!got) {
co_return std::nullopt;
}
std::string_view data = {buf.begin(), *got};
lastChildActivity = AIO().provider.getTimer().now();
for (auto c : data)
if (c == '\n') {
auto json = parseJSONMessage(currentHookLine, "the derivation builder");
if (json) {
auto s = handleJSONLogMessage(*json, worker.act, hook->activities, "the derivation builder", true);
// ensure that logs from a builder using `ssh-ng://` as protocol
// are also available to `nix log`.
if (s && logSink) {
const auto type = (*json)["type"];
const auto fields = (*json)["fields"];
if (type == resBuildLogLine) {
const std::string logLine =
(fields.size() > 0 ? fields[0].get<std::string>() : "") + "\n";
logSize += logLine.size();
if (settings.maxLogSize && logSize > settings.maxLogSize) {
co_return tooMuchLogs();
}
(*logSink)(logLine);
} else if (type == resSetPhase && ! fields.is_null()) {
const auto phase = fields[0];
if (! phase.is_null()) {
// nixpkgs' stdenv produces lines in the log to signal
// phase changes.
// We want to get the same lines in case of remote builds.
// The format is:
// @nix { "action": "setPhase", "phase": "$curPhase" }
const auto logLine = JSON::object({
{"action", "setPhase"},
{"phase", phase}
});
(*logSink)("@nix " + logLine.dump(-1, ' ', false, JSON::error_handler_t::replace) + "\n");
}
}
}
}
currentHookLine.clear();
} else
currentHookLine += c;
}
} catch (...) {
co_return result::current_exception();
}
kj::Promise<Result<std::optional<Goal::WorkResult>>> DerivationGoal::handleChildOutput() noexcept
try {
kj::Own<AsyncInputStream> builderIn, hookIn;
if (builderOutFD) {
builderIn = kj::heap<AsyncFdIoStream>(AsyncFdIoStream::shared_fd{}, builderOutFD->get());
}
if (hook) {
hookIn = kj::heap<AsyncFdIoStream>(AsyncFdIoStream::shared_fd{}, hook->fromHook.get());
}
auto handlers = handleChildStreams(builderIn.get(), hookIn.get())
.attach(std::move(builderIn), std::move(hookIn));
if (respectsTimeouts() && settings.buildTimeout != 0) {
handlers = handlers.exclusiveJoin(
handler = handler.exclusiveJoin(
AIO()
.provider.getTimer()
.afterDelay(settings.buildTimeout.get() * kj::SECONDS)
.then([this]() -> Result<std::optional<WorkResult>> {
.then([this]() -> Result<WorkResult> {
return timedOut(
Error("%1% timed out after %2% seconds", name, settings.buildTimeout)
);
@@ -1346,16 +1266,29 @@ try {
);
}
return handlers.then([this](auto r) {
if (!currentLogLine.empty()) flushLine();
return r;
});
} catch (...) {
return {result::current_exception()};
if (logSink) {
handler = handler.exclusiveJoin(
logSink->signal.promise.then([&](bool limitReached) -> kj::Promise<Result<WorkResult>> {
try {
if (limitReached) {
return {tooMuchLogs()};
} else {
return kj::NEVER_DONE;
}
} catch (...) {
return {result::current_exception()};
}
})
);
}
return handler;
}
kj::Promise<Result<std::optional<Goal::WorkResult>>> DerivationGoal::monitorForSilence() noexcept
kj::Promise<Result<Goal::WorkResult>> DerivationGoal::monitorForSilence() noexcept
{
lastChildActivity = AIO().provider.getTimer().now();
while (true) {
const auto stash = lastChildActivity;
auto waitUntil = lastChildActivity + settings.maxSilentTime.get() * kj::SECONDS;
@@ -1368,57 +1301,6 @@ kj::Promise<Result<std::optional<Goal::WorkResult>>> DerivationGoal::monitorForS
}
}
kj::Promise<Result<std::optional<Goal::WorkResult>>>
DerivationGoal::handleChildStreams(AsyncInputStream * builderIn, AsyncInputStream * hookIn) noexcept
{
assert(builderIn || hookIn);
lastChildActivity = AIO().provider.getTimer().now();
auto handlers = kj::joinPromisesFailFast([&] {
kj::Vector<kj::Promise<Result<std::optional<WorkResult>>>> parts{2};
if (builderIn) {
parts.add(handleBuilderOutput(*builderIn));
}
if (hookIn) {
parts.add(handleHookOutput(*hookIn));
}
return parts.releaseAsArray();
}());
if (respectsTimeouts() && settings.maxSilentTime != 0) {
handlers = handlers.exclusiveJoin(monitorForSilence().then([](auto r) {
return kj::arr(std::move(r));
}));
}
for (auto r : co_await handlers) {
if (r) {
co_return r;
}
}
co_return std::nullopt;
}
void DerivationGoal::flushLine()
{
if (handleJSONLogMessage(currentLogLine, *act, builderActivities, "the derivation builder", false))
;
else {
logTail.push_back(currentLogLine);
if (logTail.size() > settings.logLines) logTail.pop_front();
act->result(resBuildLogLine, currentLogLine);
}
currentLogLine = "";
currentLogLinePos = 0;
}
kj::Promise<Result<OutputPathMap>> DerivationGoal::queryDerivationOutputMap()
try {
OutputPathMap res;
+28 -39
View File
@@ -74,6 +74,25 @@ struct InitialOutput {
*/
struct DerivationGoal : public Goal
{
/**
* A sink that only passes a limited amount of data and signals exhaustion through a promise.
*/
struct LogSink : FinishSink
{
AutoCloseFD fd;
ref<BufferedSink> file, target;
const uint64_t limit;
uint64_t writtenSoFar = 0;
kj::PromiseFulfillerPair<bool> signal = kj::newPromiseAndFulfiller<bool>();
LogSink(AutoCloseFD fd, ref<BufferedSink> file, bool compress, uint64_t limit);
~LogSink() noexcept;
void operator()(std::string_view data) override;
void finish() override;
};
/**
* Whether this goal has completed. Completed goals can not be
* asked for more outputs, a new goal must be created instead.
@@ -186,38 +205,20 @@ struct DerivationGoal : public Goal
BuildResult buildResult;
/**
* File descriptor for the log file.
* Sink for the log file.
*/
AutoCloseFD fdLogFile;
std::shared_ptr<BufferedSink> logFileSink, logSink;
/**
* Number of bytes received from the builder's stdout/stderr.
*/
unsigned long logSize;
std::shared_ptr<LogSink> logSink;
/**
* The most recent log lines.
*/
std::list<std::string> logTail;
std::string currentLogLine;
size_t currentLogLinePos = 0; // to handle carriage return
std::string currentHookLine;
/**
* The build hook.
*/
std::unique_ptr<HookInstance> hook;
/**
* Builder output is pulled from this file descriptor when not null.
* Owned by the derivation goal or subclass, must not be reset until
* the build has finished and no more output must be processed by us
*/
AutoCloseFD * builderOutFD = nullptr;
/**
* The sort of derivation we are building.
*/
@@ -227,19 +228,10 @@ struct DerivationGoal : public Goal
NotifyingCounter<uint64_t>::Bump mcExpectedBuilds, mcRunningBuilds;
std::unique_ptr<Activity> act;
/**
* Activity that denotes waiting for a lock.
*/
std::unique_ptr<Activity> actLock;
std::map<ActivityId, Activity> builderActivities;
/**
* The remote machine on which we're building.
*/
std::string machineName;
std::optional<Activity> actLock;
/** Witness type to say that the drvPath has already been added as a temp root */
struct DrvHasRoot { explicit DrvHasRoot() = default; };
@@ -254,7 +246,7 @@ struct DerivationGoal : public Goal
WorkResult timedOut(Error && ex);
kj::Promise<Result<WorkResult>> workImpl() noexcept override;
kj::Promise<Result<WorkResult>> workImpl() noexcept override final;
/**
* Add wanted outputs to an already existing derivation goal.
@@ -315,17 +307,14 @@ struct DerivationGoal : public Goal
virtual void cleanupPostOutputsRegisteredModeNonCheck();
protected:
AsyncSemaphore::Token slotToken;
kj::TimePoint lastChildActivity = kj::minValue;
kj::Promise<Result<std::optional<WorkResult>>> handleChildOutput() noexcept;
kj::Promise<Result<std::optional<WorkResult>>>
handleChildStreams(AsyncInputStream * builderIn, AsyncInputStream * hookIn) noexcept;
kj::Promise<Result<std::optional<WorkResult>>> handleBuilderOutput(AsyncInputStream & in
kj::Promise<Result<WorkResult>> wrapChildHandler(kj::Promise<Result<WorkResult>> handler
) noexcept;
kj::Promise<Result<std::optional<WorkResult>>> handleHookOutput(AsyncInputStream & in) noexcept;
kj::Promise<Result<std::optional<WorkResult>>> monitorForSilence() noexcept;
kj::Promise<Result<WorkResult>> monitorForSilence() noexcept;
WorkResult tooMuchLogs();
void flushLine();
virtual std::string buildErrorContents(const std::string & exitMsg, bool diskFull);
@@ -358,7 +347,7 @@ public:
kj::Promise<Result<WorkResult>> repairClosure() noexcept;
void started();
std::string buildDescription() const;
WorkResult done(
BuildResult::Status status,
-6
View File
@@ -22,12 +22,6 @@ kj::Promise<void> Goal::waitForAWhile()
kj::Promise<Result<Goal::WorkResult>> Goal::work() noexcept
try {
// always clear the slot token, no matter what happens. not doing this
// can cause builds to get stuck on exceptions (or other early exist).
// ideally we'd use scoped slot tokens instead of keeping them in some
// goal member variable, but we cannot do this yet for legacy reasons.
KJ_DEFER({ slotToken = {}; });
BOOST_OUTCOME_CO_TRY(auto result, co_await workImpl());
trace("done");
-3
View File
@@ -82,9 +82,6 @@ struct Goal
*/
std::string name;
protected:
AsyncSemaphore::Token slotToken;
public:
struct [[nodiscard]] WorkResult {
ExitCode exitCode;
+11 -10
View File
@@ -5,17 +5,16 @@ $Cxx.namespace("nix::rpc::build_remote");
$Cxx.allowCancellation;
using Types = import "/lix/libutil/types.capnp";
using Log = import "/lix/libutil/logging.capnp";
using StoreTypes = import "/lix/libstore/types.capnp";
interface HookInstance {
interface BuildLogger {
# only used for fd passing
}
interface AcceptedBuild {
run @0 (
logger :Log.LogStream,
inputs :List(StoreTypes.StorePath), # actual a set
wantedOutputs :List(Data), # actually StringSet
description :Text, # root activity description for this build
) -> (result :Types.ResultV);
}
@@ -23,19 +22,21 @@ interface HookInstance {
union {
accept :group {
machine @0 :AcceptedBuild;
machineName @1 :Data;
}
postpone @2 :Void;
decline @3 :Void;
declinePermanently @4 :Void;
postpone @1 :Void;
decline @2 :Void;
declinePermanently @3 :Void;
}
}
build @0 (
init @0 (
logger :Log.LogStream,
settings :Types.Settings,
) -> (result :Types.ResultV);
build @1 (
amWilling :Bool,
neededSystem :Data,
drvPath :StoreTypes.StorePath,
requiredFeatures :List(Data),
buildLogger :BuildLogger,
) -> (result :Types.Result(BuildResponse));
}
+67 -22
View File
@@ -1,13 +1,66 @@
#include "lix/libstore/build/child.hh"
#include "lix/libutil/c-calls.hh"
#include "lix/libutil/error.hh"
#include "lix/libutil/file-system.hh"
#include "lix/libstore/globals.hh"
#include "lix/libstore/build/hook-instance.hh"
#include "lix/libutil/json.hh"
#include "lix/libutil/logging.hh"
#include "lix/libutil/result.hh"
#include "lix/libutil/rpc.hh"
#include "lix/libutil/serialise.hh"
#include "lix/libutil/strings.hh"
#include "lix/libutil/logging-rpc.hh" // IWYU pragma: keep
#include "lix/libutil/types-rpc.hh" // IWYU pragma: keep
#include <kj/memory.h>
#include <memory>
#include <string_view>
namespace nix {
kj::Promise<Result<std::unique_ptr<HookInstance>>> HookInstance::create()
void HookInstance::HookLogger::emitLog(rpc::log::Event::Result::Reader r)
{
auto type = rpc::log::from(r.getType());
auto fields = r.getFields();
if (!type) {
return;
}
// ensure that logs from a builder using `ssh-ng://` as protocol
// are also available to `nix log`.
if (type == resBuildLogLine) {
if (fields.size() > 0 && fields[0].isS()) {
(*logSink)(fmt("%s\n", rpc::to<std::string_view>(fields[0].getS())));
} else {
(*logSink)("\n");
}
} else if (type == resSetPhase && fields.size() > 0 && fields[0].isS()) {
// nixpkgs' stdenv produces lines in the log to signal phase changes.
// We want to get the same lines in case of remote builds.
// The format is:
// @nix { "action": "setPhase", "phase": "$curPhase" }
const auto phase = rpc::to<std::string_view>(fields[0].getS());
const auto logLine = JSON::object({{"action", "setPhase"}, {"phase", phase}});
(*logSink)("@nix " + logLine.dump(-1, ' ', false, JSON::error_handler_t::replace) + "\n");
}
}
kj::Promise<void> HookInstance::HookLogger::push(PushContext context)
{
try {
auto e = context.getParams().getE();
if (logSink && e.isResult()) {
emitLog(e.getResult());
}
} catch (std::exception & e) { // NOLINT(lix-foreign-exceptions)
printError("error in log processor: %s", e.what());
throw; // NOLINT(lix-foreign-exceptions)
}
return RpcLoggerServer::push(context);
}
kj::Promise<Result<std::unique_ptr<HookInstance>>> HookInstance::create(const Activity & act)
try {
debug("starting build hook '%s'", concatStringsSep(" ", settings.buildHook.get()));
@@ -27,36 +80,23 @@ try {
args.push_back(std::to_string(verbosity));
/* Create a pipe to get the output of the child. */
Pipe fromHook_;
fromHook_.create();
/* Create the communication pipes. */
Pipe toHook_;
toHook_.create();
auto [selfRPC, hookRPC] = SocketPair::stream();
printMsg(lvlChatty, "running build hook: %s", concatMapStringsSep(" ", args, shellEscape));
/* Fork the hook. */
auto pid = startProcess([&]() {
if (dup2(fromHook_.writeSide.get(), STDERR_FILENO) == -1)
throw SysError("cannot pipe standard error into log file");
commonExecveingChildInit();
if (chdir("/") == -1) throw SysError("changing into /");
/* Dup the communication pipes. */
if (dup2(toHook_.readSide.get(), STDIN_FILENO) == -1) {
throw SysError("dupping to-hook read side");
}
if (dup2(hookRPC.get(), STDOUT_FILENO) == -1) {
throw SysError("dupping to-hook read side");
}
execv(buildHook.c_str(), stringsToCharPtrs(args).data());
sys::execv(buildHook, args);
throw SysError("executing '%s'", buildHook);
});
@@ -65,15 +105,20 @@ try {
std::map<std::string, Config::SettingInfo> settings;
globalConfig.getSettings(settings, true);
FdSink sink(toHook_.writeSide.get());
for (auto & setting : settings) {
sink << 1 << setting.first << setting.second.value;
auto conn = AIO().lowLevelProvider.wrapUnixSocketFd(kj::AutoCloseFd(selfRPC.release()));
auto client = std::make_unique<capnp::TwoPartyClient>(*conn, 1);
auto rpc = client->bootstrap().castAs<rpc::build_remote::HookInstance>();
{
auto initReq = rpc.initRequest();
initReq.setLogger(kj::heap<HookLogger>(act, nullptr));
RPC_FILL(initReq, initSettings, settings);
TRY_AWAIT_RPC(initReq.send());
}
sink << 0;
sink.flush();
co_return std::make_unique<HookInstance>(
std::move(fromHook_.readSide), std::move(selfRPC), std::move(pid)
kj::heap(std::move(rpc)).attach(std::move(conn), std::move(client)), std::move(pid)
);
} catch (...) {
co_return result::current_exception();
@@ -82,7 +127,7 @@ try {
HookInstance::~HookInstance()
{
try {
if (pid) pid.kill();
kill();
} catch (...) {
ignoreExceptionInDestructor();
}
+51 -19
View File
@@ -2,43 +2,75 @@
///@file
#include "hook-instance.capnp.h"
#include "lix/libutil/logging-rpc.hh"
#include "lix/libutil/logging.hh"
#include "lix/libutil/processes.hh"
#include "lix/libutil/serialise.hh"
#include "logging.capnp.h"
#include <capnp/rpc-twoparty.h>
#include <kj/async-io.h>
#include <memory>
#include <utility>
namespace nix {
struct HookInstance
{
/**
* Pipe for the hook's standard output/error.
*/
AutoCloseFD fromHook;
struct HookLogger : rpc::log::RpcLoggerServer
{
FinishSink * logSink;
kj::Own<kj::AsyncCapabilityStream> conn;
std::optional<capnp::TwoPartyClient> client;
rpc::build_remote::HookInstance::Client rpc;
HookLogger(const Activity & act, FinishSink * logSink)
: rpc::log::RpcLoggerServer(act)
, logSink(logSink)
{
}
/**
* The process ID of the hook.
*/
Pid pid;
void emitLog(rpc::log::Event::Result::Reader r);
kj::Promise<void> push(PushContext context) override;
};
std::map<ActivityId, Activity> activities;
kj::Own<rpc::build_remote::HookInstance::Client> rpc;
static kj::Promise<Result<std::unique_ptr<HookInstance>>> create();
static kj::Promise<Result<std::unique_ptr<HookInstance>>> create(const Activity & act);
HookInstance(AutoCloseFD fromHook, AutoCloseFD rpc, Pid pid)
: fromHook(std::move(fromHook))
, conn(AIO().lowLevelProvider.wrapUnixSocketFd(kj::AutoCloseFd(rpc.release())))
, client(std::in_place, *this->conn, 1)
, rpc(client->bootstrap().castAs<rpc::build_remote::HookInstance>())
, pid(std::move(pid))
HookInstance(kj::Own<rpc::build_remote::HookInstance::Client> rpc, Pid pid)
: rpc(std::move(rpc))
, pidOrStatus(std::move(pid))
{
}
~HookInstance();
int wait()
{
return childStatusOr<&Pid::wait>();
}
int kill()
{
return childStatusOr<&Pid::kill>();
}
private:
/**
* The process ID of the hook if it's running, or its exit status if not.
*/
std::variant<Pid, int> pidOrStatus;
template<int (Pid::*fn)()>
int childStatusOr()
{
return std::visit(
overloaded{
[&](Pid & pid) {
int status = (pid.*fn)();
pidOrStatus = status;
return status;
},
[](int status) { return status; },
},
pidOrStatus
);
}
};
}
+191 -85
View File
@@ -1,4 +1,5 @@
#include "lix/libstore/build/local-derivation-goal.hh"
#include "build/derivation-goal.hh"
#include "lix/libutil/async-io.hh"
#include "lix/libutil/async.hh"
#include "lix/libutil/error.hh"
@@ -12,6 +13,7 @@
#include "lix/libstore/path-references.hh"
#include "lix/libutil/archive.hh"
#include "lix/libstore/daemon.hh"
#include "lix/libutil/c-calls.hh"
#include "lix/libutil/fmt.hh"
#include "lix/libutil/regex.hh"
#include "lix/libutil/file-descriptor.hh"
@@ -239,8 +241,12 @@ retry:
if (!buildUser) {
if (!actLock)
actLock = std::make_unique<Activity>(*logger, lvlWarn, actBuildWaiting,
fmt("waiting for a free build user ID for '%s'", Magenta(worker.store.printStorePath(drvPath))));
actLock = logger->startActivity(
lvlWarn,
actBuildWaiting,
fmt("waiting for a free build user ID for '%s'",
Magenta(worker.store.printStorePath(drvPath)))
);
co_await waitForAWhile();
// we can loop very often, and `co_return co_await` always allocates a new frame
goto retry;
@@ -274,10 +280,8 @@ retry:
/* Okay, we have to build. */
TRY_AWAIT(startBuilder());
started();
if (auto error = TRY_AWAIT(handleChildOutput())) {
co_return std::move(*error);
}
mcRunningBuilds = worker.runningBuilds.addTemporarily(1);
co_return TRY_AWAIT(wrapChildHandler(handleRawChild()));
} catch (BuildError & e) {
outputLocks.reset();
@@ -286,8 +290,6 @@ retry:
report.permanentFailure = true;
co_return report;
}
co_return co_await buildDone();
} catch (...) {
co_return result::current_exception();
}
@@ -326,7 +328,6 @@ void LocalDerivationGoal::closeReadPipes()
DerivationGoal::closeReadPipes();
} else {
builderOutPTY.close();
builderOutFD = nullptr;
}
}
@@ -370,13 +371,14 @@ bool LocalDerivationGoal::cleanupDecideWhetherDiskFull()
auto & localStore = getLocalStore();
uint64_t required = 8ULL * 1024 * 1024; // FIXME: make configurable
struct statvfs st;
if (statvfs(localStore.config().realStoreDir.get().c_str(), &st) == 0 &&
(uint64_t) st.f_bavail * st.f_bsize < required)
diskFull = true;
if (statvfs(tmpDirRoot.c_str(), &st) == 0 && (uint64_t) st.f_bavail * st.f_bsize < required)
if (sys::statvfs(localStore.config().realStoreDir, &st) == 0
&& (uint64_t) st.f_bavail * st.f_bsize < required)
{
diskFull = true;
}
if (sys::statvfs(tmpDirRoot, &st) == 0 && (uint64_t) st.f_bavail * st.f_bsize < required) {
diskFull = true;
}
}
#endif
@@ -454,8 +456,7 @@ try {
/* Create a temporary directory where the build will take
place. */
tmpDirRoot =
createTempDir(buildDir, "nix-build-" + std::string(drvPath.name()), false, false, 0700);
tmpDirRoot = createTempSubdir(buildDir, std::nullopt, 0700);
} catch (SysError & e) {
/*
* Fallback to the global tmpdir and create a safe space there
@@ -465,53 +466,41 @@ try {
throw;
}
auto globalTmp = defaultTempDir();
createDirs(globalTmp);
#if __APPLE__
/* macOS filesystem namespacing does not exist, to avoid breaking builds, we need to weaken
* the mode bits on the top-level directory. This avoids issues like
* https://github.com/NixOS/nix/pull/11031. */
constexpr int toplevelDirMode = 0755;
#else
constexpr int toplevelDirMode = 0700;
#endif
auto nixBuildsTmp = createTempDir(
globalTmp, fmt("nix-builds-%s", geteuid()), false, false, toplevelDirMode
);
auto nixBuildsTmp = createTempDir(fmt("nix-builds-%s", geteuid()), 0700);
printTaggedWarning(
"Failed to use the system-wide build directory '%s', falling back to a temporary "
"directory inside '%s'",
settings.buildDir.get(),
nixBuildsTmp
);
tmpDirRoot = createTempSubdir(nixBuildsTmp, std::nullopt, 0700);
worker.buildDirOverride = nixBuildsTmp;
tmpDirRoot = createTempDir(
nixBuildsTmp, "nix-build-" + std::string(drvPath.name()), false, false, 0700
);
}
/* The TOCTOU between the previous mkdir call and this open call is unavoidable due to
* POSIX semantics.*/
tmpDirRootFd = AutoCloseFD{open(tmpDirRoot.c_str(), O_RDONLY | O_NOFOLLOW | O_DIRECTORY)};
tmpDirRootFd = sys::open(tmpDirRoot, O_RDONLY | O_NOFOLLOW | O_DIRECTORY);
if (!tmpDirRootFd) {
throw SysError("failed to open the build temporary directory descriptor '%1%'", tmpDirRoot);
}
#if __APPLE__
// The Darwin sandbox ensures that builds cannot change the
// permissions of their own build directory. Unsandboxed builds
// disable this, but have no isolation by design anyway. The
// minimal sandbox (applied even when `sandbox = false`, though not
// when `_NIX_TEST_NO_SANDBOX` is set) prevents the creation of
// `set{u,g}id` files regardless.
tmpDir = tmpDirRoot;
tmpDirFd = std::move(tmpDirRootFd);
#else
// place the actual build directory in a subdirectory of tmpDirRoot. if
// we do not do this a build can `chown 777` its build directory and so
// make it accessible to everyone in the system, breaking isolation. we
// also need the intermediate level to be inaccessible to others. build
// processes must be able to at least traverse to the directory though,
// without being able to chmod. this means either mode 0750 or 0710. we
// cannot use 0710 because the libarchive we link with is compiled with
// an old apple sdk that does not have O_SEARCH, which makes libarchive
// try to open tmpDirRoot for *read* and fail because g+r is not set. a
// future update to nixpkgs may fix this. until then we do not lose any
// security by setting mode 0750 because we use only a single subdir in
// tmpDirRoot, so being able to list its parent doesn't break anything.
//
// use a short name to not increase the path length too much on darwin.
// darwin has a severe sockaddr_un path length limitation, so this does
// make a difference over more evocative names. we use `b` for `build`.
// use 0710 just to be extra safe; if we ever add more directories they
// will not be enumerable to other processes in the builder user group.
tmpDir = tmpDirRoot + "/b";
if (mkdirat(tmpDirRootFd.get(), "b", 0700)) {
throw SysError("failed to create the build temporary directory '%1%'", tmpDir);
@@ -520,15 +509,17 @@ try {
if (!tmpDirFd)
throw SysError("failed to open the build temporary directory descriptor '%1%'", tmpDir);
chownToBuilder(tmpDirFd);
if (buildUser) {
if (fchown(tmpDirRootFd.get(), -1, buildUser->getGID()) == -1) {
throw SysError("cannot change ownership of '%1%'", tmpDirRoot);
}
if (fchmod(tmpDirRootFd.get(), 0750) == -1) {
if (fchmod(tmpDirRootFd.get(), 0710) == -1) {
throw SysError("cannot change mode of '%1%'", tmpDirRoot);
}
}
#endif
chownToBuilder(tmpDirFd);
for (auto & [outputName, status] : initialOutputs) {
/* Set scratch path we'll actually use during the build.
@@ -777,19 +768,21 @@ try {
/* Create a pseudoterminal to get the output of the builder. */
builderOutPTY = AutoCloseFD{posix_openpt(O_RDWR | O_NOCTTY)};
if (!builderOutPTY)
if (!builderOutPTY) {
throw SysError("opening pseudoterminal master");
builderOutFD = &builderOutPTY;
}
// FIXME: not thread-safe, use ptsname_r
std::string slaveName = ptsname(builderOutPTY.get());
if (buildUser) {
if (chmod(slaveName.c_str(), 0600))
if (sys::chmod(slaveName, 0600)) {
throw SysError("changing mode of pseudoterminal slave");
}
if (chown(slaveName.c_str(), buildUser->getUID(), 0))
if (sys::chown(slaveName, buildUser->getUID(), 0)) {
throw SysError("changing owner of pseudoterminal slave");
}
}
#if __APPLE__
else {
@@ -802,9 +795,8 @@ try {
throw SysError("unlocking pseudoterminal");
/* Open the slave side of the pseudoterminal and use it as stderr. */
auto openSlave = [&]()
{
AutoCloseFD builderOut{open(slaveName.c_str(), O_RDWR | O_NOCTTY)};
auto openSlave = [&]() {
AutoCloseFD builderOut{sys::open(slaveName, O_RDWR | O_NOCTTY)};
if (!builderOut)
throw SysError("opening pseudoterminal slave");
@@ -896,7 +888,12 @@ void LocalDerivationGoal::initTmpDir() {
std::string fn = ".attr-" + hash.to_string(Base::Base32, false);
Path p = tmpDir + "/" + fn;
/* TODO(jade): we should have BorrowedFD instead of OwnedFD. */
AutoCloseFD passAsFileFd{openat(tmpDirFd.get(), fn.c_str(), O_WRONLY | O_TRUNC | O_CREAT | O_CLOEXEC | O_EXCL | O_NOFOLLOW, 0666)};
AutoCloseFD passAsFileFd{sys::openat(
tmpDirFd.get(),
fn,
O_WRONLY | O_TRUNC | O_CREAT | O_CLOEXEC | O_EXCL | O_NOFOLLOW,
0666
)};
if (!passAsFileFd) {
throw SysError("opening `passAsFile` file in the sandbox '%1%'", p);
}
@@ -925,7 +922,7 @@ void LocalDerivationGoal::initTmpDir() {
void LocalDerivationGoal::setupConfiguredCertificateAuthority()
{
if (settings.caFile != "") {
if (pathAccessible(settings.caFile)) {
if (pathAccessible(settings.caFile, true)) {
auto prefix = useChroot ?
#if __linux__
chrootRootDir
@@ -1083,8 +1080,9 @@ try {
void LocalDerivationGoal::chownToBuilder(const Path & path)
{
if (!buildUser) return;
if (chown(path.c_str(), buildUser->getUID(), buildUser->getGID()) == -1)
if (sys::chown(path, buildUser->getUID(), buildUser->getGID()) == -1) {
throw SysError("cannot change ownership of '%1%'", path);
}
}
void LocalDerivationGoal::chownToBuilder(const AutoCloseFD & fd)
@@ -1173,8 +1171,9 @@ void LocalDerivationGoal::runChild()
/* Bind-mount chroot directory to itself, to treat it as a
different filesystem from /, as needed for pivot_root. */
if (mount(chrootRootDir.c_str(), chrootRootDir.c_str(), 0, MS_BIND, 0) == -1)
if (sys::mount(chrootRootDir, chrootRootDir, "", MS_BIND, 0) == -1) {
throw SysError("unable to bind mount '%1%'", chrootRootDir);
}
/* Bind-mount the sandbox's Nix store onto itself so that
we can mark it as a "shared" subtree, allowing bind
@@ -1186,11 +1185,13 @@ void LocalDerivationGoal::runChild()
to fail with EINVAL. Don't know why. */
Path chrootStoreDir = chrootRootDir + worker.store.config().storeDir;
if (mount(chrootStoreDir.c_str(), chrootStoreDir.c_str(), 0, MS_BIND, 0) == -1)
if (sys::mount(chrootStoreDir, chrootStoreDir, "", MS_BIND, 0) == -1) {
throw SysError("unable to bind mount the Nix store", chrootStoreDir);
}
if (mount(0, chrootStoreDir.c_str(), 0, MS_SHARED, 0) == -1)
if (sys::mount("", chrootStoreDir, "", MS_SHARED, 0) == -1) {
throw SysError("unable to make '%s' shared", chrootStoreDir);
}
/* Set up a nearly empty /dev, unless the user asked to
bind-mount the host /dev. */
@@ -1295,21 +1296,31 @@ void LocalDerivationGoal::runChild()
/* Bind a new instance of procfs on /proc. */
createDirs(chrootRootDir + "/proc");
if (mount("none", (chrootRootDir + "/proc").c_str(), "proc", 0, 0) == -1)
if (sys::mount("none", chrootRootDir + "/proc", "proc", 0, 0) == -1) {
throw SysError("mounting /proc");
}
/* Mount sysfs on /sys. */
if (buildUser && buildUser->getUIDCount() != 1) {
createDirs(chrootRootDir + "/sys");
if (mount("none", (chrootRootDir + "/sys").c_str(), "sysfs", 0, 0) == -1)
if (sys::mount("none", chrootRootDir + "/sys", "sysfs", 0, 0) == -1) {
throw SysError("mounting /sys");
}
}
/* Mount a new tmpfs on /dev/shm to ensure that whatever
the builder puts in /dev/shm is cleaned up automatically. */
if (pathExists("/dev/shm") && mount("none", (chrootRootDir + "/dev/shm").c_str(), "tmpfs", 0,
fmt("size=%s", settings.sandboxShmSize).c_str()) == -1)
if (pathExists("/dev/shm")
&& sys::mount(
"none",
chrootRootDir + "/dev/shm",
"tmpfs",
0,
fmt("size=%s", settings.sandboxShmSize).c_str()
) == -1)
{
throw SysError("mounting /dev/shm");
}
/* Mount a new devpts on /dev/pts. Note that this
requires the kernel to be compiled with
@@ -1319,7 +1330,10 @@ void LocalDerivationGoal::runChild()
!pathExists(chrootRootDir + "/dev/ptmx")
&& !pathsInChroot.count("/dev/pts"))
{
if (mount("none", (chrootRootDir + "/dev/pts").c_str(), "devpts", 0, "newinstance,mode=0620") == 0)
if (sys::mount(
"none", (chrootRootDir + "/dev/pts"), "devpts", 0, "newinstance,mode=0620"
)
== 0)
{
createSymlink("/dev/pts/ptmx", chrootRootDir + "/dev/ptmx");
@@ -1364,8 +1378,9 @@ void LocalDerivationGoal::runChild()
throw SysError("unsharing cgroup namespace");
/* Do the chroot(). */
if (chdir(chrootRootDir.c_str()) == -1)
if (sys::chdir(chrootRootDir) == -1) {
throw SysError("cannot change directory to '%1%'", chrootRootDir);
}
if (mkdir("real-root", 0) == -1)
throw SysError("cannot create real-root directory");
@@ -1424,8 +1439,9 @@ void LocalDerivationGoal::runChild()
}
#endif
if (chdir(tmpDirInSandbox.c_str()) == -1)
if (sys::chdir(tmpDirInSandbox) == -1) {
throw SysError("changing into '%1%'", tmpDir);
}
/* Close all other file descriptors. */
closeExtraFDs();
@@ -1658,7 +1674,7 @@ void LocalDerivationGoal::runChild()
void LocalDerivationGoal::execBuilder(std::string builder, Strings args, Strings envStrs)
{
execve(builder.c_str(), stringsToCharPtrs(args).data(), stringsToCharPtrs(envStrs).data());
sys::execve(builder, args, envStrs);
}
@@ -1738,7 +1754,7 @@ try {
continue;
}
auto optSt = maybeLstat(actualPath.c_str());
auto optSt = maybeLstat(actualPath);
if (!optSt)
throw BuildError(
"builder for '%s' failed to produce output path for output '%s' at '%s'",
@@ -1809,15 +1825,18 @@ try {
);
}
outputGraph[scratchOutputs.at(name)] = StorePathSet{};
std::visit(
overloaded{/* Since we'll use the already installed versions of these, we
can treat them as leaves and ignore any references they
have. */
[&](const AlreadyRegistered &) {
outputGraph[scratchOutputs.at(name)] = StorePathSet{};
},
[&](const AlreadyRegistered &) {},
[&](const PerhapsNeedToRegister & refs) {
outputGraph[scratchOutputs.at(name)] = refs.refs;
for (auto & ref : refs.refs) {
if (inverseOutputMap.find(ref) != inverseOutputMap.end()) {
outputGraph[scratchOutputs.at(name)].insert(ref);
}
}
}
},
*orifu
@@ -1828,10 +1847,8 @@ try {
topoSort(outputsToSort, {[&](const std::string & name) {
StringSet dependencies;
for (auto & path : outputGraph.at(scratchOutputs.at(name))) {
auto outputName = inverseOutputMap.find(path);
if (outputName != inverseOutputMap.end()) {
dependencies.insert(outputName->second);
}
auto outputName = inverseOutputMap.at(path);
dependencies.insert(outputName);
}
return dependencies;
}});
@@ -2192,7 +2209,8 @@ try {
debug("unreferenced input: '%1%'", worker.store.printStorePath(i));
}
localStore.optimisePath(actualPath, NoRepair); // FIXME: combine with scanForReferences()
// FIXME: combine with scanForReferences()
TRY_AWAIT(localStore.optimisePath(actualPath, NoRepair));
worker.markContentsGood(newInfo.path);
newInfo.deriver = drvPath;
@@ -2216,9 +2234,11 @@ try {
msg << HintFmt("derivation '%s' may not be deterministic: outputs differ", drvPath.to_string());
for (auto [oldPath, newPath]: nondeterministic) {
if (newPath) {
msg << HintFmt("\n output differs: output '%s' differs from '%s'", oldPath.c_str(), *newPath);
msg << HintFmt(
"\n output differs: output '%s' differs from '%s'", oldPath, *newPath
);
} else {
msg << HintFmt("\n output '%s' differs", oldPath.c_str());
msg << HintFmt("\n output '%s' differs", oldPath);
}
}
throw NotDeterministic(msg.str());
@@ -2569,36 +2589,39 @@ try {
static void makeVisible(int parentFd, const char * entry, uid_t user, gid_t group)
{
struct stat st;
// NOLINTNEXTLINE(lix-unsafe-c-calls): entry is a dentry name
if (fstatat(parentFd, entry, &st, AT_SYMLINK_NOFOLLOW)) {
throw SysError("fstat(%s)", guessOrInventPathFromFD(parentFd));
}
if (S_ISDIR(st.st_mode)) {
int dirfd = openat(parentFd, entry, O_RDONLY | O_DIRECTORY | O_NOFOLLOW);
if (dirfd < 0) {
auto dirfd = sys::openat(parentFd, entry, O_RDONLY | O_DIRECTORY | O_NOFOLLOW);
if (!dirfd) {
throw SysError("openat(%s/%s)", guessOrInventPathFromFD(parentFd), entry);
}
AutoCloseDir dir(fdopendir(dirfd));
AutoCloseDir dir(fdopendir(dirfd.get()));
if (!dir) {
close(dirfd);
throw SysError("fdopendir(%s/%s)", guessOrInventPathFromFD(parentFd), entry);
}
dirfd.release();
struct dirent * dirent;
while (errno = 0, dirent = readdir(dir.get())) {
if (strcmp(dirent->d_name, ".") == 0 || strcmp(dirent->d_name, "..") == 0) {
continue;
}
makeVisible(dirfd, dirent->d_name, user, group);
makeVisible(::dirfd(dir.get()), dirent->d_name, user, group);
}
}
// ignore permissions errors for symlinks. linux can't chmod them.
// clear special permission bits while we're here, just to be safe
if (fchmodat(parentFd, entry, st.st_mode & 0777, AT_SYMLINK_NOFOLLOW) && !S_ISLNK(st.st_mode)) {
if (sys::fchmodat(parentFd, entry, st.st_mode & 0777, AT_SYMLINK_NOFOLLOW)
&& !S_ISLNK(st.st_mode))
{
throw SysError("fchmod(%s)", guessOrInventPathFromFD(parentFd));
}
if (user != uid_t(-1) && group != gid_t(-1)
&& fchownat(parentFd, entry, user, group, AT_SYMLINK_NOFOLLOW))
&& sys::fchownat(parentFd, entry, user, group, AT_SYMLINK_NOFOLLOW))
{
throw SysError("fchown(%s)", guessOrInventPathFromFD(parentFd));
}
@@ -2621,7 +2644,7 @@ void LocalDerivationGoal::finalizeTmpDir(bool force, bool duringDestruction)
} catch (SysError & e) {
printError("error making '%s' accessible: %s", tmpDir, e.what());
}
chmod(tmpDirRoot.c_str(), 0755);
(void) sys::chmod(tmpDirRoot, 0755);
}
else if (duringDestruction)
deletePathUninterruptible(tmpDirRoot);
@@ -2647,5 +2670,88 @@ StorePath LocalDerivationGoal::makeFallbackPath(const StorePath & path)
Hash(HashType::SHA256), path.name());
}
kj::Promise<Result<Goal::WorkResult>> LocalDerivationGoal::handleRawChild() noexcept
try {
if (auto error = TRY_AWAIT(handleRawChildStream())) {
co_return std::move(*error);
}
co_return TRY_AWAIT(buildDone());
} catch (...) {
co_return result::current_exception();
}
kj::Promise<Result<std::optional<Goal::WorkResult>>>
LocalDerivationGoal::handleRawChildStream() noexcept
try {
AsyncFdIoStream in(AsyncFdIoStream::shared_fd{}, builderOutPTY.get());
std::map<ActivityId, Activity> builderActivities;
LogLineSplitter splitter;
auto act = logger->startActivity(
lvlInfo,
actBuild,
buildDescription(),
Logger::Fields{worker.store.printStorePath(drvPath), "", 1, 1}
);
auto flushLine = [&](const std::string & line) {
if (const auto state =
handleJSONLogMessage(line, act, builderActivities, "the derivation builder"))
{
return *state;
} else {
logTail.push_back(line);
if (logTail.size() > settings.logLines) {
logTail.pop_front();
}
return act.result(resBuildLogLine, line);
}
};
auto buf = kj::heapArray<char>(4096);
while (true) {
std::string_view data;
try {
if (const auto got = TRY_AWAIT(in.read(buf.begin(), buf.size()))) {
data = {buf.begin(), *got};
} else {
co_return std::nullopt;
}
} catch (SysError & e) {
// the builder output stream may be a pty fd, and closing one pty
// endpoint sends EIO to the other endpoint. this is a good exit.
if (e.errNo == EIO) {
data = {};
} else {
throw;
}
}
lastChildActivity = AIO().provider.getTimer().now();
if (data.empty()) {
if (auto left = splitter.finish(); !left.empty()) {
if (flushLine(left) == Logger::BufferState::NeedsFlush) {
TRY_AWAIT(act.getLogger().flush());
}
}
co_return std::nullopt;
}
if (logSink) {
(*logSink)(data);
}
while (!data.empty()) {
if (auto line = splitter.feed(data)) {
if (flushLine(*line) == Logger::BufferState::NeedsFlush) {
TRY_AWAIT(act.getLogger().flush());
}
}
}
}
} catch (...) {
co_return result::current_exception();
}
}
@@ -324,6 +324,9 @@ protected:
*/
virtual Pid startChild(std::function<void()> openSlave);
kj::Promise<Result<WorkResult>> handleRawChild() noexcept;
kj::Promise<Result<std::optional<WorkResult>>> handleRawChildStream() noexcept;
/**
* Set up the system call filtering required for the sandbox.
* This currently only has an effect on Linux.
+14
View File
@@ -26,6 +26,20 @@ R""(
; Allow getpwuid.
(allow mach-lookup (global-name "com.apple.system.opendirectoryd.libinfo"))
; Disallow messing with the toplevel build directory.
(deny file-write-owner file-write-flags file-write-xattr file-write-mount
file-write-unmount
(literal (param "_NIX_BUILD_TOP")))
; Nixpkgs does `chmod -R` on `$NIX_BUILD_TOP/$sourceRoot` by default,
; which results in it trying to set the mode of `$NIX_BUILD_TOP` when
; derivations set `sourceRoot = ".";`. Thankfully, the GNU `chmod(1)`
; treats `ENOTSUP` as a nonfatal, nonreported error in this case, and
; continues to descend into the directory tree.
;
; See: <https://gitweb.git.savannah.gnu.org/gitweb/?p=coreutils.git;a=blob;f=src/chmod.c;hb=refs/tags/v9.7#l312>
(deny file-write-mode (with errno ENOTSUP)
(literal (param "_NIX_BUILD_TOP")))
; Access to /tmp and the build directory.
; The network-outbound/network-inbound ones are for unix domain sockets, which
; we allow access to in TMPDIR (but if we allow them more broadly, you could in

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