Compare commits

...

216 commits

Author SHA1 Message Date
Linus Heckemann c95b73d8a1 Merge "libstore: report all differing outputs rather than just the first" into main 2024-10-30 19:04:57 +00:00
V. 56ead73fda Merge "chore: remove monolithic coreutils requirement" into main 2024-10-30 16:53:43 +00:00
Dusk Banks 22eb47f0fd tests/functional/flakes: test with UTF-8 bullets
using UTF-8 bullets in the sample avoids locale confusion where Bash
doesn't know to treat `•` as a single character.

Signed-off-by: Dusk Banks <me@bb010g.com>
Change-Id: I829019b66e93e6d33ac3a6641df07d0dd2332a5a
2024-10-30 08:21:58 -07:00
Dusk Banks 8b2f8d538b Merge "libstore: restore mode after changing xattrs" into main 2024-10-30 14:56:43 +00:00
V. fb1b211037 chore: remove monolithic coreutils requirement
It's only used in a couple of tests, and only in such a way that
replacing it with a random command suffices.
I also removed a few pointless uses of the variable.

Fixes: lix-project/lix#376
Change-Id: I90aedb61d64b02f7c9b007e72f9d614cc1b37a2e
2024-10-30 15:12:35 +04:00
Linus Heckemann 8b0ac51f12 libstore: report all differing outputs rather than just the first
Before:

error: derivation '/nix/store/4spy3nz1661zm15gkybsy1h5f36aliwx-python3.11-test-1.0.0.drv' may not be deterministic: output '/nix/store/ccqcp01zg18wp9iadzmzimqzdi3ll08d-python3.11-test-1.0.0-dist' differs from '/nix/store/ccqcp01zg18wp9iadzmzimqzdi3ll08d-python3.11-test-1.0.0-dist.check'

After:

error: derivation '4spy3nz1661zm15gkybsy1h5f36aliwx-python3.11-test-1.0.0.drv' may not be deterministic: outputs differ
         output differs: output '/nix/store/ccqcp01zg18wp9iadzmzimqzdi3ll08d-python3.11-test-1.0.0-dist' differs from '/nix/store/ccqcp01zg18wp9iadzmzimqzdi3ll08d-python3.11-test-1.0.0-dist.check'
         output differs: output '/nix/store/yl59v08356i841c560alb0zmk7q16klb-python3.11-test-1.0.0' differs from '/nix/store/yl59v08356i841c560alb0zmk7q16klb-python3.11-test-1.0.0.check'

Change-Id: Ib2871fa602bf1fa9c00e2565b3a2e1b26f908152
2024-10-29 18:34:54 +01:00
eldritch horrors 9c22a4d31b libstore: don't use curl for file:// downloads
curl can't pause downloads of file:// urls, which is very much in the
way of making the curl wrapper fully asynchronous. we can emulate all
the things curl does, but unfortunately curl is *rather extensive* in
its support of frankly weird shit. hopefully this subset of features,
which notably does not include curl readdir support, is enough for us

Change-Id: I5f67768c4b512565655b94b0421270c7dbbd8d11
2024-10-28 18:52:49 +00:00
eldritch horrors c83b13eafd libstore: reunify all file transfer methods again
with the api cleaned up we can suddenly reunify uploads, downloads, and
existence checks through curl in the same wrapper function. uploads and
existence checks simply don't use the result source, and given that all
transfers (or at least *most* transfers to date) go through the network
the few extra allocations do not hurt us at all. even for file:// calls
the overhead won't much matter as going to disk and back *is* expensive

Change-Id: I4f9ca6681a8fc303377b4cf4c63e3363ae32c18b
2024-10-28 18:52:49 +00:00
eldritch horrors d65838a900 libstore: remove FileTransfer::enqueueDownload
it's no longer needed. `download` can do everything `enqueueDownload`
did, and a lot more. e.g. not block the calling thread, for instance.

Change-Id: I4b36235ed707c92d117b4c33efa3db50d26f9a84
2024-10-28 18:52:49 +00:00
eldritch horrors c68f0cdf00 libstore: return transfer metadata from download
as promised earlier. nothing uses it yet, but just you wait.

Change-Id: I77d185578d96c2134b756d20f2fcf1c02de0da6f
2024-10-28 18:52:49 +00:00
eldritch horrors 14eff10fe4 libstore: split callback into metadata and finished parts
this will let us return metadata from FileTransfer::download, which in
turn is necessary to remove enqueueDownload. it also opens avenues for
streaming downloads that keep download metadata instead of dropping it

Change-Id: If0fc6af5eb2aeb689fc866c345c9d7bce4d59f2d
2024-10-28 18:52:49 +00:00
eldritch horrors 923abe347c libstore: use data callback for simple downloads too
this is an intermediate step towards removing enqueueDownload entirely.

Change-Id: I05ec0c7f4a234fdc966e5005308b37f6f905d433
2024-10-28 18:52:49 +00:00
eldritch horrors 64864c3730 libstore: pass only data to TransferItem data callback
with encoding being handled by curl the reference is no longer needed.

Change-Id: Ibfaf5f55e5314e81ce45ba4523b960c401dd2e1c
2024-10-28 18:52:49 +00:00
eldritch horrors 10488f7431 libstore: use curl content-encoding support, not our own
let's use the automatic decoding functions curl provides instead of
implementing them ourselves for the dubious ability to support both
xz and bzip2 encodings as well, neither of which anything will send

Change-Id: I3edfebeb596a0e9d5c986efca9270501c996f2dd
2024-10-28 18:52:49 +00:00
Justin ! 8c567c0424 Merge "libutil: implement PathsSetting<PathSet>" into main 2024-10-28 14:22:59 +00:00
eldritch horrors 61146c73ce Merge changes I0220cedd,Ide0c0512,I6fcd920e,I85ec62ff,I35853a91, ... into main
* changes:
  libstore: check that transfer headers don't change during retries
  libstore: use effective transfer url for retries
  libstore: collect effective url and cachedness earlier
  libstore: remove TransferItem::active
  libstore: always allocate TransferItem::req
  libstore: remove FileTransferResult::data
  libstore: de-future-ize FileTransfer::enqueueUpload
  libstore: remove FileTransferRequest
  libstore: remove FileTransferRequest::expectedETag
  libstore: remove FileTransferResult::bodySize
  libstore: remove FileTransferRequest::verifyTLS
  libstore: remove FiletransferRequest::head
2024-10-28 01:36:45 +00:00
Linus Heckemann f55ed83991 Merge "libutil: handle json builder log messages with unexpected format" into main 2024-10-27 22:54:11 +00:00
eldritch horrors 212a14bb1f libstore: check that transfer headers don't change during retries
etag changing implies with high probability that the content of the
resource changed. immutable url changing implies that the immutable
url we got previously was wrong, which is probably a server bug. if
the encoding changes our decoding will break completely, so that is
also very illegal. one notable change we still allow is etags going
away completely, mostly since this does not imply any data changes.

Change-Id: I0220ceddc3fd732cd1b3bb39b40021cc631baadc
2024-10-27 21:44:38 +00:00
eldritch horrors 7c716b9716 libstore: use effective transfer url for retries
do not retread the entire redirection path if we've seen the end of the
road. this avoids silently downloading wrong data, and notifies us when
a url we've received data from turns into a redirect when retrying. for
reasons of simplicity we don't turn of libcurl redirects on retries. if
we did that we'd have to conditionally process http status codes, which
sounds annoying and would make the header callback even more of a mess.

Change-Id: Ide0c0512ef9b2579350101246d654a2375541a39
2024-10-27 21:44:38 +00:00
eldritch horrors 2b3bdda027 libstore: collect effective url and cachedness earlier
this will let us return metadata for a transfer without having to wait
for the entire transfer to complete. more importantly for current uses
though is that we could now send retries to the effective url directly
instead of retreading the entire redirect path. this improves latency,
and in such cases where redirects change while we're downloading it'll
also improve correctness (previously we'd silently download bad data).

Change-Id: I6fcd920eb96fbdb2e960b73773c0b854e0300e99
2024-10-27 21:44:38 +00:00
eldritch horrors 97c76c4655 libstore: remove TransferItem::active
it's always legal to call curl_multi_remove_handle on a valid pair of
multi and easy handles. removing an easy handle that is not currently
attached to a multi handle is a no-op, and removing an easy handle of
a different multi handle is something we can't reasonably trigger. if
we *did* ever manage it would result in an error we'd ignore, and the
handles in question would not be changed at all. this is just simpler

Change-Id: I85ec62ff89385981ca49d243376b9c32586bd128
2024-10-27 21:44:38 +00:00
eldritch horrors c82407fc1e libstore: always allocate TransferItem::req
there's no reason not to do this. also improve error handling a bit.

Change-Id: I35853a919fa58a9a34ad47ffab6de77ba6f7fb86
2024-10-27 21:44:38 +00:00
eldritch horrors 982d049d3b libstore: remove FileTransferResult::data
return it as a separate item in a pair instead. this will let us remove
enqueueDownload() in favor of returning metadata from download() itself

Change-Id: I74fad2ca15f920da1eefabc950c2baa2c360f2ba
2024-10-27 21:44:38 +00:00
eldritch horrors 5cd7055044 libstore: de-future-ize FileTransfer::enqueueUpload
it's only used once, and synchronously at that.

Change-Id: Ife9db15dd97bc0de8de59a25d27f3f7afeb8791b
2024-10-27 21:44:38 +00:00
eldritch horrors 6f18e1ebde libstore: remove FileTransferRequest
it's just a uri and some headers now. those can be function arguments
with no loss of clarity. *actual* additional arguments, for example a
TLS context with additional certificates, could be added on a new and
improved FileTransfer class that carries not just a backend reference
but some real, visible context for its transfers. curl not being very
multi-threading-friendly when using multi handles will make sharing a
bit hard anyway once we drop the single global download worker thread

Change-Id: Id2112c95cbd118c6d920488f38d272d7da926460
2024-10-27 21:44:38 +00:00
eldritch horrors a839c31e6c libstore: remove FileTransferRequest::expectedETag
just another http specific used in only one place.

Change-Id: I99361a7226f4e6cd8f18170d3683c0025657bcb3
2024-10-27 21:44:38 +00:00
eldritch horrors 30bec83fa4 libstore: remove FileTransferResult::bodySize
it's only used for internal bookkeeping in TransferItem.

Change-Id: I467c5be023488be4a8a76e5f98a4ef25762df6f3
2024-10-27 21:44:38 +00:00
eldritch horrors d82b212d33 libstore: remove FileTransferRequest::verifyTLS
it's never set to false.

Change-Id: I1e436c82f1097091a08faa1dfada75e51bd5edf9
2024-10-27 21:44:38 +00:00
eldritch horrors 220251ba51 libstore: remove FiletransferRequest::head
add a method to FileTransfer that provides this functionality instead.

Change-Id: Ic1933a5df76a109c248c9c5efea065356b20a6f9
2024-10-27 21:44:38 +00:00
raito 9f682204b5 Merge changes I85b6075a,Iee41b055 into main
* changes:
  libstore: ban unpacking case hacked filenames from NARs
  testsuite: add a NAR generator with some evil NARs
2024-10-27 18:12:18 +00:00
raito f9e7df01f3 Merge "daemon: stop eating SIGINTs" into main 2024-10-27 18:11:20 +00:00
raito f7edee7c14 Merge changes I8e11ddbe,Idb8d9a00 into main
* changes:
  nix-shell: stop using dynamic format strings!!
  tests: move nix-shell related tests to subdir
2024-10-27 18:10:17 +00:00
eldritch horrors 6c2609c5f9 libstore: remove FileTransferRequest::tries
it's never set, and then only used internally. *once*.

Change-Id: I32585b1821e979f3ebb53b794ba0d1f576126b92
2024-10-26 21:42:35 +00:00
eldritch horrors af27d1ecd8 libstore: make baseRetryTimeMs a FileTransfer property
we don't even need this outside of tests. maybe we should not do
automatic retries at this level at all and use retrying wrappers
instead? at some point we may have to do this, but not just yet.

Change-Id: If0088aa55215be81f1770c25b3bb1b5268c65cf8
2024-10-26 21:42:35 +00:00
eldritch horrors 1e3b45546c libstore: remove FileTransferRequest::parentAct
never set explicitly, and transfers are never instantiated with one
current activity but submitted with a *different* current activity.

Change-Id: I1a3ec57c02013565aeb9e9398ea42d0c4279095e
2024-10-26 21:42:35 +00:00
eldritch horrors ce3e1d1e7a libstore: remove FileTransferRequests::data
use separate upload and download methods instead.

Change-Id: I5baa2177c8ddd70268c75ff074e361b2f17dddbd
2024-10-26 21:42:35 +00:00
eldritch horrors 2d49efaa2e libstore: remove Filetransfer::transfer
just use enqueueFileTransfer().get() insteaad.

Change-Id: I67a43c9d3d5f68ac3f9e8ba7973c243dd78b86a3
2024-10-26 21:42:35 +00:00
eldritch horrors 98b55c3a1d libstore: move FileTransferRequest::verb to TransferItem
this function is only used internally by curl wrapper.

Change-Id: I71d4c430cb069e2c949be769c17fede8dd04d480
2024-10-26 21:42:35 +00:00
eldritch horrors a83bf24281 libstore: remove FileTransferRequest::mimeType
it's only used by HttpBinaryCacheStore, and even there used in only on
place. this one place can set the header explicitly, which it now does

Change-Id: Id89228150669e25e7f59a3d6bd939e46059ce29e
2024-10-26 21:42:35 +00:00
eldritch horrors a8d6577bf0 libstore: HttpBinaryCacheStore::{makeRequest -> makeURI}
it only sets the one field anyway (and the parent activity as a side
effect that does not depend on the exact location of the constructor
call). when FileTransferRequest goes away we would need this anyway.

Change-Id: I35cf2ed3533239181449a62cf34cd282b395e5db
2024-10-26 20:35:16 +00:00
eldritch horrors 59e364c2a8 tests: stop using OCR for nix copy tests
this is fragile, slow as fuck, breaks constantly under high concurrency,
and completely unnecessary since ssh bypasses the stdio file descriptors
*anyway*. we do still check that we see ssh messages to ensure that none
of our subprocess handling messes with ssh's /dev/tty, but that's it now

Change-Id: Ib8e31e1999f813d07a27efc63a9d3454a9e4fcdd
2024-10-26 17:50:54 +00:00
Yureka b020d1fc27 Merge "fix build for 32-bit platforms" into main 2024-10-26 15:46:05 +00:00
alois31 2734a9cf94 Merge changes I29e66ad8,I77ea62cd,I7cd58d92 into main
* changes:
  treewide: make more settings conditionally available
  libstore/build: only send overridden settings to the build hook
  treewide: consistently mark overridden settings as such
2024-10-23 15:20:51 +00:00
eldritch horrors 5f1344dd8a libstore: turn Worker::run into a promise
a first little step into pushing the event loops up, up and away.
eventually we will want them to be instantiated only at the roots
of every thread (since kj binds loops to threads), but not today.

Change-Id: Ic97f1debba382a5a3f46daeaf2d6d434ee42569f
2024-10-23 11:55:12 +00:00
eldritch horrors faee771b30 libstore: hide Worker and goals where possible
goals should be considered internal to the worker architecture due to
the tight coupling of the two, and we can finally do that. doing this
is also a prerequisite for turning Worker::run() into a real promise.

Change-Id: I7cf273d4a6fdb75b8d192fce1af07c6265ff6980
2024-10-23 11:55:12 +00:00
eldritch horrors b8cc54df0a libstore: return relevant store path in goal result
we now do not need the goal pointers any more to process worker results.

Change-Id: I1a021b862ca666bcd23fee9f38973e90e6f94a72
2024-10-23 11:55:12 +00:00
eldritch horrors 67f1aafd61 libstore: restrict curl protocols
previously it was possible to fetchurl a dict server, or an ldap server,
or an imap server. this is a bit of a problem, both because rare schemes
may not be available on all systems, and because some schemes (e.g. scp)
are inherently insecure in potentially surprising ways we needn't allow.

Change-Id: I18fc567c6f58c3221b5ea8ce927f4da780057828
2024-10-23 11:32:14 +00:00
eldritch horrors 1d9d40b2a6 libstore: move failingExitStatus into worker results
we already have a results type for the entire worker call, we may as
well put this bit of info in there. we do keep the assumption of the
old code that the field will only be read if some goals have failed;
fixing that is a very different mess, and not immediately necessary.

Change-Id: If3fc32649dcd88e1987cdd1758c6c5743e3b35ac
2024-10-22 15:32:36 +00:00
eldritch horrors 343aca3a27 libstore: clear derivation build activities when done
without this derivations do not show as completely processed in the
internal-json logs (or the newer multiline output). the former also
breaks external tools like nix-output-monitor which, like multiline
output, grow vertically until at least some goals are finally freed

Change-Id: I55758daf526ba29ae15fb82e0d88da8afb45bf5c
2024-10-22 14:55:55 +00:00
Lily Ballard 5ce1d8463a Merge "libstore: add missing #include on darwin" into main 2024-10-22 02:05:44 +00:00
Yureka b77687945e fix build for 32-bit platforms
Change-Id: I113e131eb5c66c42c0bbc60181a7faafc02ca02e
2024-10-21 08:52:29 +02:00
Lily Ballard 4308ec1ae4 gitignore: ignore *.pyc files
Running our installcheck test suite creates these files.

Change-Id: I97ac8f1aa165a491c55dff6b48486db17b75443b
2024-10-20 22:08:35 -07:00
Lily Ballard 65551175e3 libstore: add missing #include on darwin
optimise-store.cc used std::regex on darwin, but forgot to include the
header. This probably compiled due to the precompiled headers file, but
it caused errors in the editor.

Change-Id: I23297c08cb66d44e4d4f303560f46e4adc7d5a43
2024-10-20 22:08:35 -07:00
Lily Ballard 068f4b147d libstore: fix sign comparison warnings in darwin platform
It's not clear to me if `proc_pidinfo()` or `proc_pidfdinfo()` can
actually return negative values, the syscall wrappers convert `-1` into
zero and the semantics suggest that negative values don't make sense,
but just to be safe we'll preserve the int type until we've checked that
it's a positive value.

Fixes: lix-project/lix#548
Change-Id: If575aec6b1e27dba63091c7a0316c7b3788747cd
2024-10-20 13:13:11 -07:00
eldritch horrors 0ff8f91325 libutil: disallow AsyncCollect relocations
some promises capture `this`.  we could also allocate a shared state,
but this thing doesn't really need to ever be moved anyway. so there.

Change-Id: I50b5c44684a8ab4e984b1323de21f97ace4a864a
2024-10-19 19:47:46 +00:00
eldritch horrors b0e619b8bd Merge "libstore: always release build/substitution slot tokens" into main 2024-10-19 18:32:30 +00:00
eldritch horrors 564d931134 libstore: always release build/substitution slot tokens
not doing this can freeze slots until the goal that occupied them is
freed (rather than simply complete), and then can freeze the system.

fixes #549

Change-Id: I042df04222f8ffbaa18ef4a4eae6cbd6f89b679e
2024-10-19 16:17:58 +02:00
jade dccde94369 daemon: stop eating SIGINTs
Daemon client handler processes are forked off of the main nix process
and thus will not have a signal handler thread anymore. This leads to a
high likelihood of bustage, since the Worker infrastructure expects the
interrupt infrastructure to actually, you know, work, to be able to get
interrupted.

The expected behaviour after fork is either:
- Start a signal handler thread if you expect to do complicated things
  that need ReceiveInterrupts.
- Call restoreProcessContext and don't handle signals specially
  otherwise.

Change-Id: I73d36b5bbf96dddd21d5e1c3bd0484d715c00e8b
2024-10-18 21:34:18 -07:00
Dusk Banks 60b89c63db libstore: restore mode after changing xattrs
this was missed in 3f07c65510. if mode is
not restored, the tree will have mutability where it shouldn't.

fixes test `functional-repair`, which fails from the output path
directory being unnecessarily writable:

```
++(repair.sh:12) nix-hash /tmp/nix-shell.IIUJAq/nix-test/repair/store/j3kmp9zhc5y7gqs591l0nrscm5hw4145-dependencies-input-2
+(repair.sh:12) hash=d790f49fc89cb6f384b6dbe450790d07
+(repair.sh:15) chmod u+w /tmp/nix-shell.IIUJAq/nix-test/repair/store/j3kmp9zhc5y7gqs591l0nrscm5hw4145-dependencies-input-2
+(repair.sh:16) touch /tmp/nix-shell.IIUJAq/nix-test/repair/store/j3kmp9zhc5y7gqs591l0nrscm5hw4145-dependencies-input-2/bad
+(repair.sh:18) nix-store --verify --check-contents -v
reading the Nix store...
checking path existence...
checking link hashes...
checking store hashes...
checking contents of '/tmp/nix-shell.IIUJAq/nix-test/repair/store/20z19rjkwmwpb2ba4x29kac6xnslai38-dependencies-top.drv'
checking contents of '/tmp/nix-shell.IIUJAq/nix-test/repair/store/3mmawi9lj33xz96cf6kw9989xc8v5i96-fod-input'
checking contents of '/tmp/nix-shell.IIUJAq/nix-test/repair/store/5fp4r2kh1fcv6mv9dv6rywhhr1am9hhm-builder-dependencies-input-0.sh'
checking contents of '/tmp/nix-shell.IIUJAq/nix-test/repair/store/9kryn4ihv6b7bjswv2rsjq4533n2w5zk-fod-input.drv'
checking contents of '/tmp/nix-shell.IIUJAq/nix-test/repair/store/ap8s0fim8s3ilzj8aqwlwk7gv1crjp4j-dependencies-top'
checking contents of '/tmp/nix-shell.IIUJAq/nix-test/repair/store/i2ipd9p282zkqr1zb4glqiqigv8ybsyk-dependencies.builder0.sh'
checking contents of '/tmp/nix-shell.IIUJAq/nix-test/repair/store/i3xyw46h0lsx7ad6bczwi9sqjjx5f0j0-dependencies-input-0'
checking contents of '/tmp/nix-shell.IIUJAq/nix-test/repair/store/j3kmp9zhc5y7gqs591l0nrscm5hw4145-dependencies-input-2'
path '/tmp/nix-shell.IIUJAq/nix-test/repair/store/j3kmp9zhc5y7gqs591l0nrscm5hw4145-dependencies-input-2' was modified! expected hash 'sha256:1rhaylnjs5lbp089lnk7qvsypqmbm5vvyvxvv7i68b4x33pncqgs', got 'sha256:0nkjrmdc6ixf935chj3zhpqph5i15p306ffdsa850qh8mncpnsmc'
checking contents of '/tmp/nix-shell.IIUJAq/nix-test/repair/store/pfaxqiw8zf3bw0w8w8gazswh76d729yy-builder-dependencies-input-2.sh'
checking contents of '/tmp/nix-shell.IIUJAq/nix-test/repair/store/pzzms3k5wl8d3wszv3maw29zylfkiiw0-dependencies-input-1'
checking contents of '/tmp/nix-shell.IIUJAq/nix-test/repair/store/riwx84p57ckfvgli9nwhx88z6zh1c8ss-builder-fod-input.sh'
checking contents of '/tmp/nix-shell.IIUJAq/nix-test/repair/store/rjsphx7xvnqh2qafdrr7fiyxqc1rljhw-dependencies-input-2.drv'
checking contents of '/tmp/nix-shell.IIUJAq/nix-test/repair/store/scywzq87dvm0c5f1h16hww6mvzhcsx3f-builder-dependencies-input-1.sh'
checking contents of '/tmp/nix-shell.IIUJAq/nix-test/repair/store/v65j4w033i05nyfd2h0g8h41ijmfglp7-dependencies-input-0.drv'
checking contents of '/tmp/nix-shell.IIUJAq/nix-test/repair/store/ykwibh62xyp81k75vdw9c7y21kg4ibzf-dependencies-input-1.drv'
warning: not all store errors were fixed
+(repair.sh:21) nix-store --verify --check-contents --repair
reading the Nix store...
checking path existence...
checking link hashes...
checking store hashes...
path '/tmp/nix-shell.IIUJAq/nix-test/repair/store/j3kmp9zhc5y7gqs591l0nrscm5hw4145-dependencies-input-2' was modified! expected hash 'sha256:1rhaylnjs5lbp089lnk7qvsypqmbm5vvyvxvv7i68b4x33pncqgs', got 'sha256:0nkjrmdc6ixf935chj3zhpqph5i15p306ffdsa850qh8mncpnsmc'
checking path '/tmp/nix-shell.IIUJAq/nix-test/repair/store/j3kmp9zhc5y7gqs591l0nrscm5hw4145-dependencies-input-2'...
path '/tmp/nix-shell.IIUJAq/nix-test/repair/store/j3kmp9zhc5y7gqs591l0nrscm5hw4145-dependencies-input-2' is corrupted or missing!
checking path '/tmp/nix-shell.IIUJAq/nix-test/repair/store/i3xyw46h0lsx7ad6bczwi9sqjjx5f0j0-dependencies-input-0'...
repairing outputs of '/tmp/nix-shell.IIUJAq/nix-test/repair/store/rjsphx7xvnqh2qafdrr7fiyxqc1rljhw-dependencies-input-2.drv'...
+(repair.sh:23) '[' -e /tmp/nix-shell.IIUJAq/nix-test/repair/store/j3kmp9zhc5y7gqs591l0nrscm5hw4145-dependencies-input-2/bad ']'
+(repair.sh:24) '[' -w /tmp/nix-shell.IIUJAq/nix-test/repair/store/j3kmp9zhc5y7gqs591l0nrscm5hw4145-dependencies-input-2 ']'
++(repair.sh:24) onError
++(/var/home/bb010g/Sources/Nix/lix/build/tests/functional/common/vars-and-functions.sh:244) set +x
repair.sh: test failed at:
  main in repair.sh:24
```

fixes test `functional-simple`, which fails from the output path file
being unnecessarily writable:

```
+(simple.sh:11) echo 'output path is /tmp/nix-shell.IIUJAq/nix-test/simple/store/9fqn0rs99ymn1r09yip7bsifcdh3ra0y-simple'
+(simple.sh:13) '[' -w /tmp/nix-shell.IIUJAq/nix-test/simple/store/9fqn0rs99ymn1r09yip7bsifcdh3ra0y-simple ']'
++(simple.sh:13) onError
++(/var/home/bb010g/Sources/Nix/lix/build/tests/functional/common/vars-and-functions.sh:244) set +x
simple.sh: test failed at:
  main in simple.sh:13
```

fixes test `functional-optimise-store`, which fails from the unnecessary
mutability making Lix avoid hard linking:

```
++(optimise-store.sh:5) nix-build - --no-out-link --auto-optimise-store
this derivation will be built:
  /tmp/nix-shell.IIUJAq/nix-test/optimise-store/store/2ql4kjxhnzdard8d6n3h9hc1m3lawr2b-foo1.drv
building '/tmp/nix-shell.IIUJAq/nix-test/optimise-store/store/2ql4kjxhnzdard8d6n3h9hc1m3lawr2b-foo1.drv'...
warning: skipping suspicious writable file '/tmp/nix-shell.IIUJAq/nix-test/optimise-store/store/h7i1pp848f9a8452y0s18kgsnis77vjn-foo1/foo'
+(optimise-store.sh:5) outPath1=/tmp/nix-shell.IIUJAq/nix-test/optimise-store/store/h7i1pp848f9a8452y0s18kgsnis77vjn-foo1
++(optimise-store.sh:6) echo 'with import ./config.nix; mkDerivation { name = "foo2"; builder = builtins.toFile "builder" "mkdir $out; echo hello > $out/foo"; }'
++(optimise-store.sh:6) nix-build - --no-out-link --auto-optimise-store
this derivation will be built:
  /tmp/nix-shell.IIUJAq/nix-test/optimise-store/store/wjgvfhfsp14w06im8bbp1kqzz7smdkcy-foo2.drv
building '/tmp/nix-shell.IIUJAq/nix-test/optimise-store/store/wjgvfhfsp14w06im8bbp1kqzz7smdkcy-foo2.drv'...
warning: skipping suspicious writable file '/tmp/nix-shell.IIUJAq/nix-test/optimise-store/store/gjg6gayj2f6x3h53sp5i21nbnsd7b4i3-foo2/foo'
+(optimise-store.sh:6) outPath2=/tmp/nix-shell.IIUJAq/nix-test/optimise-store/store/gjg6gayj2f6x3h53sp5i21nbnsd7b4i3-foo2
++(optimise-store.sh:8) stat --format=%i /tmp/nix-shell.IIUJAq/nix-test/optimise-store/store/h7i1pp848f9a8452y0s18kgsnis77vjn-foo1/foo
+(optimise-store.sh:8) inode1=328316
++(optimise-store.sh:9) stat --format=%i /tmp/nix-shell.IIUJAq/nix-test/optimise-store/store/gjg6gayj2f6x3h53sp5i21nbnsd7b4i3-foo2/foo
+(optimise-store.sh:9) inode2=328404
+(optimise-store.sh:10) '[' 328316 '!=' 328404 ']'
+(optimise-store.sh:11) echo 'inodes do not match'
+(optimise-store.sh:12) exit 1
```

Signed-off-by: Dusk Banks <me@bb010g.com>
Change-Id: I87eeb74e718746a587be2ac52bcc9b5b1e5529db
2024-10-18 20:57:22 -07:00
jade 60578b4d7d Merge "flake.lock: update everything" into main 2024-10-19 00:03:42 +00:00
jade eadce58a90 Merge "testsuite: Fix tests on systems with a non-master defaultBranch" into main 2024-10-19 00:00:30 +00:00
jade ca55060ac6 packaging: use in-tree capnproto derivation
This is done because the one in nixpkgs has several problems and we
don't want to conflict with them fixing those problems:
- not building shared libs
- not building debuginfo (more critical for us due to
  lix-project/lix#549)
- not setting the correct cmake build type
- not setting the correct cxxflags to build the coroutine library
- not building with clang (required for the coroutine library since gcc
  is known to miscompile coroutines *in kj*).

CC: lix-project/lix#551

Fixes: lix-project/lix#550
Change-Id: Ia5b78dc1809963fdd1a8203b127a216cb575d751
2024-10-18 15:40:22 -07:00
jade 77b6f6734f packaging: remove workaround for clang stdenv asserts
This workaround was for the stdenv not being set when callPackage'ing
package.nix for some of the stranger CI outputs.

Change-Id: I2acdd6efa721b90dd3cb04358544d25d591ff084
2024-10-18 15:40:22 -07:00
piegames e2d00ac3a8 libexpr: Fix typo in error message
Closes #523

Change-Id: Ib5705e405b74d07a8fcf0163847405e9c791c3e3
2024-10-18 19:37:23 +02:00
piegames 3ba5ef91bc benchmarks: Fix purity
Some stuff wasn't working on my machine because it depended on the host
environment in some ways, fixed those.

Change-Id: Iff4931a9a26c6827978f1ee6434710f406d67a96
2024-10-18 19:37:23 +02:00
piegames e5de1d13c4 libexpr: Optimize complex indented strings
The old behavior results in lots of concatenations happening for no good
reason and is an artifact of the technical limitations of the old parser
(combined with some lack of care for such details).

Change-Id: I0d78d6220ca6aeaa10bc437e48e08bf7922e0bb3
2024-10-18 19:37:23 +02:00
piegames 878e181882 libexpr: Print interpolations more accurately in show
This is only a minor semantical distinction, but we should be able to
properly test it, and the parser tests rely on show for that.

Change-Id: I25e868cf9544e30cdff17deb5fd50a434e0f367e
2024-10-18 11:40:04 +00:00
piegames c852ae60da libexpr: Rewrite stripIndentation for indented strings
This commit should faithfully reproduce the old behavior down to the
bugs. The new code is a lot more readable, all quirks are well
documented, and it is overall much more maintainable.

Change-Id: I629585918e4f2b7d296b6b8330235cdc90b7bade
2024-10-18 11:40:04 +00:00
piegames 765771a355 tests/functional-lang: Add test cases for indented strings
Accidentally came across them elsewhere, wanted them covered in the
ind-strings test as well.

Change-Id: Iba418a687388ba85516d13d4c9d815744297dc5c
2024-10-18 11:40:04 +00:00
piegames 14291856e4 tests/functional-lang: Test indented string parsing
There is already an eval test for it, but it doesn't test the parser's
output

Change-Id: I94809080f90ace8aa3f610bcd315e9df0cf5d12f
2024-10-18 11:40:04 +00:00
piegames e7d6212f77 libexpr: move parser semantics into separate file
Another preparation for forking off and versioning the parser

Change-Id: I7b1225a44a3b81486414c1d37bd3e76a3ab307f9
2024-10-18 11:40:04 +00:00
piegames f98ee07573 libexpr: rename grammar to grammar::v1
Let's make some space in the namespace for a v2

https://wiki.lix.systems/books/lix-contributors/page/nix-lang-v2
Change-Id: If56e6dbf680d931233aa822ef91c8832464471e4
2024-10-18 11:40:04 +00:00
piegames 580df9bdb3 tests/functional-lang: Small cleanup of flagfiles handling
Flag files now properly work for all tests.

Change-Id: If652235960e8cf4120270a1b974c17586a31a157
2024-10-18 11:40:04 +00:00
jade 52b64662a4 Merge "fix: macOS build broken by fatal lowdown CLI sandbox setup" into main 2024-10-17 21:17:49 +00:00
alois31 689eb45630
treewide: make more settings conditionally available
Some settings only make sense on particular platforms, or only when a certain
experimental feature is enabled. Several of those were already conditionally
available. Do the same for a bunch more instead of silently ignoring them.
Exceptionally, the use-case-hack setting is not made conditional because it is
included in the test suite.

Change-Id: I29e66ad8ee6178a7c0eff9efb55c3410fae32514
2024-10-15 19:55:50 +02:00
alois31 ece99fee23
libstore/build: only send overridden settings to the build hook
The build hook is still running locally, so it will run with the same default
settings. Hence, just as with the daemon, it is enough to send it only the
overridden settings. This will prevent warnings like

   warning: Ignoring setting 'auto-allocate-uids' because experimental feature 'auto-allocate-uids' is not enabled

when the user didn't actually set those settings.

This is inspired by and an alternative to [0].
[0] https://github.com/NixOS/nix/pull/10049

Change-Id: I77ea62cd017614b16b55979dd30e75f09f860d21
2024-10-15 19:55:50 +02:00
alois31 4dbbd721eb
treewide: consistently mark overridden settings as such
Only overridden settings are sent to the daemon, and we're going to do the same
for the build hook to. It needs to be ensured that overridden settings are in
fact consistently marked as such, so that they actually get sent.

Change-Id: I7cd58d925702f86cf2c35ad121eb191ceb62a355
2024-10-15 19:55:50 +02:00
Linus Heckemann e55cd3beea libutil: handle json builder log messages with unexpected format
Before this change, expressions like:

with import <nixpkgs> {};
runCommand "foo" {} ''
  echo '@nix {}' >&$NIX_LOG_FD
''

would result in Lix crashing, because accessing nonexistent fields of
a JSON object throws an exception.

Rather than handling each field individually, we just catch JSON
exceptions wholesale. Since these log messages are an unusual
circumstance, log a warning when this happens.

Fixes #544.

Change-Id: Idc2d8acf6e37046b3ec212f42e29269163dca893
2024-10-15 11:15:42 +02:00
Justin ! 5a06b17b91
libutil: implement PathsSetting<PathSet>
By implementing the `PathSet` specialization for `PathsSetting`, we'll
be able to use `PathsSetting` for the `sandboxPaths` setting in
`src/libstore/globals.hh`.

Fixes: lix-project/lix#498

Change-Id: I8bf7dfff98609d1774fdb36d63e57d787bcc829f
2024-10-14 20:31:04 -04:00
jade f6077314fa Merge "Fix std::terminate call in thread pool" into main 2024-10-15 00:11:59 +00:00
jade a020f5f6cb fix: macOS build broken by fatal lowdown CLI sandbox setup
This failed due to https://github.com/NixOS/nixpkgs/pull/346945, which
makes a second lowdown-unsandboxed that works in nix builds, and the
regular lowdown has executables that fail closed when the sandbox setup
fails.

The actual failure here is only visible on nixos-unstable at the moment,
not 24.05, but this commit should fix it up for all versions.

Fixes: lix-project/lix#547
Change-Id: I50c0ecb59518ef01a7c0181114c1b4c5a7c6b78b
2024-10-14 16:49:16 -07:00
V. fbf7a8b440 Merge "Remove ancient let from 2 test files" into main 2024-10-14 22:06:48 +00:00
V. 31ff77b3f9 Remove ancient let from 2 test files
Change-Id: I992bc7f9e1cfcb1e4038fbe6ee04178bbf938556
2024-10-14 17:19:22 +04:00
jade c1f4c60bc2 nix-shell: stop using dynamic format strings!!
This was always a terrible idea independently of whether it crashes.
Stop doing it!

This commit was verified by running nix-shell on a trivial derivation
with --debug --verbose to get the vomit-level output of the shell rc
file and then diffing it before/after this change. I have reasonable
confidence it did not regress anything, though this code is genuinely
really hard to follow (which is a second reason that I split it into two
fmt calls).

Fixes: lix-project/lix#533
Change-Id: I8e11ddbece2b12749fda13efe0b587a71b00bfe5
2024-10-13 23:12:45 -07:00
jade 8497f0fe19 tests: move nix-shell related tests to subdir
This change feels kind of gross and reveals a fair bit about the
disorganization of our tests, but I think it makes parts of it a bit
better.

Change-Id: Idb8d9a00cbd75d5c156678c6b408b42b59d5e4d7
2024-10-13 23:12:45 -07:00
Maximilian Bosch 4682e40183 ssh-ng: better way to keep SSH errors visible
A better fix than in 104448e75d, hence a
revert + the fix.

It turns out that this commit has the side-effect that when having e.g.
`StrictHostKeyChecking=accept-new` for a remote builder, the warnings à la

    Warning: Permanently added 'builder' (ED25519) to the list of known hosts.

actually end up in the derivation's log whereas hostkey verification
errors don't, but only in the stderr of the `nix-build` invocation
(which was the motivation for the patch).

This change writes the stderr from the build-hook to

* the daemon's stderr, so that the SSH errors appear in the journal
  (which was the case before 104448e75d)
* the client's stderr, as a log message
* NOT to the drv log (this is handled via `handleJSONLogMessage`)

I tried to fix the issue for legacy-ssh as well, but failed and
ultimately decided to not bother.

I know that we'll sooner or later replace the entire component, however
this is the part of the patch I have working for a while, so I figured I
might still submit it for the time being.

Change-Id: I21ca1aa0d8ae281d2eacddf26e0aa825272707e5
2024-10-14 06:01:18 +00:00
Zebreus d726236e27 testsuite: Fix tests on systems with a non-master defaultBranch
When the git default branch is not set to master the installcheck
test suite fails. This patch adjusts the test setup scripts to
ignore the system and user git config files.

GIT_CONFIG_SYSTEM is set to /dev/null to ignore /etc/gitconfig

GIT_CONFIG_GLOBAL is not set because the global config files
are loaded from $HOME or $XDG_CONFIG_HOME which we already
reset.

git documentation: https://git-scm.com/docs/git#Documentation/git.txt-codeGITCONFIGGLOBALcode

Change-Id: Ie73bbed1db9419c9885b9d57e4edb7a4047d5cce
2024-10-14 05:56:02 +00:00
jade 326cbecb61 Merge changes I327db40f,If762efce into main
* changes:
  testsuite: use xdist for parallel test running
  testsuite: add a functional2 test suite based on pytest
2024-10-14 05:53:44 +00:00
Maximilian Bosch a322fcea4a
worker: respect C-c on sudo nix-build
While debugging something else I observed that latest `main` ignores
`Control-C` on `sudo nix-build`.

After reading through the capnproto docs, it seems as if the promise
must be fulfilled to actually terminate the `promise.wait()` below.

This also applies to scenarios such as stopping the client
(`nix-build`), but the builders on the daemon-side are still running,
i.e. closes #540

Co-authored-by: eldritch horrors <pennae@lix.systems>

Change-Id: I9634d14df4909fc1b65d05654aad0309bcca8a0a
2024-10-12 21:16:30 +02:00
jade a0fb52c0af Fix std::terminate call in thread pool
So we received a report that the thread pool crashed due to an
Interrupted exception.

Relevant log tail:

copying path '/nix/store/0kal2k73inviikxv9f1ciaj39lkl9a87-etc-os-release' to 'ssh://192.168.0.27'...
Lix crashed. This is a bug. We would appreciate if you report it along with what caused it at https://git.lix.systems/lix-project/lix/issues with the following information included:

error (ignored): error: interrupted by the user
Exception: nix::Interrupted: error: interrupted by the user

Relevant stack trace:

 4# __cxa_rethrow in /nix/store/22nxhmsfcv2q2rpkmfvzwg2w5z1l231z-gcc-13.3.0-lib/lib/libstdc++.so.6
 5# nix::ignoreExceptionExceptInterrupt(nix::Verbosity) in /nix/store/ghxr2ykqc3rrfcy8rzdys0rzx9ah5fqj-lix-2.92.0-dev-pre20241005-ed9b7f4/lib/liblixutil.so
 6# nix::ThreadPool::doWork(bool) in /nix/store/ghxr2ykqc3rrfcy8rzdys0rzx9ah5fqj-lix-2.92.0-dev-pre20241005-ed9b7f4/lib/liblixutil.so
 7# 0x00007FA7A00E86D3 in /nix/store/22nxhmsfcv2q2rpkmfvzwg2w5z1l231z-gcc-13.3.0-lib/lib/libstdc++.so.6
 8# 0x00007FA79FE99A42 in /nix/store/3dyw8dzj9ab4m8hv5dpyx7zii8d0w6fi-glibc-2.39-52/lib/libc.so.6
 9# 0x00007FA79FF1905C in /nix/store/3dyw8dzj9ab4m8hv5dpyx7zii8d0w6fi-glibc-2.39-52/lib/libc.so.6

Notably, this is *not* in the main thread, so this implies that the
thread didn't get joined properly before their destructors got called.
That, in turn, should have only possibly happened because join() threw
on a previous iteration of the loop joining threads, I think. Or if it
threw while in the ThreadPool destructor. Either way we had better stop
letting Interrupted fall out of our child threads!

If:
- Interrupted was thrown inside the action in the main thread: it would
  have fallen out of doWork if state->exception was already set and got
  caught by ThreadPool::process, calling shutdown() and the join loop
  which would crash the process entirely.
- Interrupted was thrown inside the action on a secondary thread: it
  would have been caught and put into the exception field and then
  possibly rethrown to fall out of the thread (since it was previously
  ignoreExceptionExceptInterrupt).

The one possible hole in this hypothesis is that there is an "error
(ignored)" line in there implying that at least one Interrupted got
eaten by an ignoreExceptionInDestructor. It's also unclear whether this
got reordered because of stderr buffering.

Fixes: lix-project/lix#542
Change-Id: I322cf050da660af78f5cb0e08ec6e6d27d09ac76
2024-10-09 15:38:40 -07:00
jade 822997bd34 libstore: ban unpacking case hacked filenames from NARs
There is absolutely no good reason these should show up in NARs besides
misconfigured systems and as long as the case hack exists, unpacking
such a NAR will cause its repacking to be wrong on systems with case
hack enabled.

This should not have any security impact on Lix to fix, but it was one
of the vectors for CVE-2024-45593:
https://github.com/NixOS/nix/security/advisories/GHSA-h4vv-h3jq-v493

Change-Id: I85b6075aacc069ee7039240b0f525804a2d8edcb
2024-10-09 14:47:39 -07:00
jade 4180b84a67 testsuite: use xdist for parallel test running
This is capped at 12 because 3.7 seconds of startup is painful enough
and 5.5 seconds with 24 was more annoying.

Change-Id: I327db40fd98deaa5330cd9cf6de99fb07b2c1cb0
2024-10-09 14:47:39 -07:00
jade 3571817e3a testsuite: add a NAR generator with some evil NARs
This also rewrites a lot of the command handling in the fixtures
library, since we want to more precisely control which way that the nix
store is set up in the tests, rather than the previous method of
renaming /nix/store to some temp dir (which allows builds but does not
allow any /nix/store paths or stability across runs, which is a
significant issue for snapshot testing).

It uses a builder to reduce the amount of state carelessly thrown
around.

The evil NARs are inspired by CVE-2024-45593
(https://github.com/NixOS/nix/security/advisories/GHSA-h4vv-h3jq-v493).

No bugs were found in this endeavor.

Change-Id: Iee41b055fa96529c5a3c761f680ed1d0667ba5da
2024-10-09 14:47:39 -07:00
jade 3caf3e1e08 testsuite: add a functional2 test suite based on pytest
I am tired of bad shell scripts, let me write bad python quickly
instead. It's definitely, $100%, better.

This is not planned as an immediate replacement of the old test suite,
but we::jade would not oppose tests getting ported.

What is here is a mere starting point and there is a lot more
functionality that we need.

Fixes: lix-project/lix#488

Change-Id: If762efce69030bb667491b263b874c36024bf7b6
2024-10-09 14:47:39 -07:00
jade 9865ebaaa6 Merge "Remove static initializers for RegisterLegacyCommand" into main 2024-10-09 20:37:58 +00:00
jade 7f7a38f278 Merge changes Ib27cb43d,I03687b8b into main
* changes:
  testsuite: override NIX_CONF_DIR and NIX_USER_CONF_FILES
  Remove some outdated `make test` invocation suggestions
2024-10-09 20:37:16 +00:00
Rebecca Turner 0012887310 Merge "Add release note for CTRL-C improvements" into main 2024-10-08 22:15:56 +00:00
Lulu 4ea8c9d643 Set c++ version to c++23
I followed @pennae's advice and moved the constructor definition of
`AttrName` from the header file `nixexpr.hh` to `nixexpr.cc`.

Change-Id: I733f56c25635b366b11ba332ccec38dd7444e793
2024-10-08 20:05:28 +02:00
Lulu 43e79f4434 Fix gcc warning -Wmissing-field-initializers
The approach that was taken here was to add default values to the type
definitions rather than specify them whenever they are missing.

Now the only remaining warning is '-Wunused-parameter' which @jade said
is usually counterproductive and that we can just disable it:
lix-project/lix#456 (comment)

So this change adds the flags '-Wall', '-Wextra' and
'-Wno-unused-parameter', so that all warnings are enabled except for
'-Wunused-parameter'.

Change-Id: Ic223a964d67ab429e8da804c0721ba5e25d53012
2024-10-08 01:44:38 +00:00
Lulu 299813f324 Merge "Avoid calling memcpy when len == 0 in filetransfer.cc" into main 2024-10-08 01:41:41 +00:00
Lulu d6e1b11d3e Fix gcc warning -Wsign-compare
Add the compile flag '-Wsign-compare' and adapt the code to fix all
cases of this warning.

Change-Id: I26b08fa5a03e4ac294daf697d32cf9140d84350d
2024-10-08 01:32:12 +02:00
Lulu 51a5025913 Avoid calling memcpy when len == 0 in filetransfer.cc
There was a bug report about a potential call to `memcpy` with a null
pointer which is not reproducible:
lix-project/lix#492

This occurred in `src/libstore/filetransfer.cc` in `InnerSource::read`.

To ensure that this doesn't happen, an early return is added before
calling `memcpy` if the length of the data to be copied is 0.

This change also adds a test that ensures that when `InnerSource::read`
is called with an empty file, it throws an `EndOfFile` exception.

Change-Id: Ia18149bee9a3488576c864f28475a3a0c9eadfbb
2024-10-08 01:26:30 +02:00
eldritch horrors ed9b7f4f84 libstore: remove Worker::{childStarted, goalFinished}
these two functions are now nearly trivial and much better inline into
makeGoalCommon. keeping them separate also separates information about
goal completion flows and how failure information ends up in `Worker`.

Change-Id: I6af86996e4a2346583371186595e3013c88fb082
2024-10-05 21:19:51 +00:00
eldritch horrors 649d8cd08f libstore: remove Worker::removeGoal
we can use our newfound powers of Goal::work Is A Real Promise to remove
completed goals from continuation promises. apart from being much easier
to follow it's also a lot more efficient because we have the iterator to
the item we are trying to remove, skipping a linear search of the cache.

Change-Id: Ie0190d051c5f4b81304d98db478348b20c209df5
2024-10-05 21:19:51 +00:00
eldritch horrors 9adf6f4568 libstore: remove Goal::notify
Goal::work() is a fully usable promise that does not rely on the worker
to report completion conditions. as such we no longer need the `notify`
field that enabled this interplay. we do have to clear goal caches when
destroying the worker though, otherwise goal promises may (incorrectly)
keep goals alive due to strong shared pointers created by childStarted.

Change-Id: Ie607209aafec064dbdf3464fe207d70ba9ee158a
2024-10-05 21:19:51 +00:00
eldritch horrors 03cbc0ecb9 libstore: move Goal::ex to WorkResult
yet another duplicated field. it's the last one though.

Change-Id: I352df8d306794d262d8c9066f3be78acd40e82cf
2024-10-05 21:19:51 +00:00
eldritch horrors 1caf2afb1d libstore: move Goal::buildResult to WorkResult
derivation goals still hold a BuildResult member variable since parts of
these results of accumulated in different places, but the Goal class now
no longer has such a field. substitution goals don't need it at all, and
derivation goals should also be refactored to not drop their buildResult

Change-Id: Ic6d3d471cdbe790a6e09a43445e25bedec6ed446
2024-10-05 20:53:39 +00:00
eldritch horrors 7ff60b7445 libstore: move Goal::exitCode to WorkResult
the field is simply duplicated between the two, and now that we can
return WorkResults from Worker::run we no longer need both of them.

Change-Id: I82fc47d050b39b7bb7d1656445630d271f6c9830
2024-10-05 20:17:20 +00:00
eldritch horrors fc6291e46d libstore: return goal results from Worker::run()
this will be needed to move all interesting result fields out of Goal
proper and into WorkResult. once that is done we can treat goals as a
totally internal construct of the worker mechanism, which also allows
us to fully stop exposing unclear intermediate state to Worker users.

Change-Id: I98d7778a4b5b2590b7b070bdfc164a22a0ef7190
2024-10-05 20:12:13 +00:00
eldritch horrors 40f154c0ed libstore: remove Worker::topGoals
since we now propagate goal exceptions properly we no longer need to
check topGoals for a reason to abort early. any early abort reasons,
whether by exception or a clean top goal failure, can now be handled
by inspecting the goal result in the main loop. this greatly reduces
goal-to-goal interactions that do not happen at the main loop level.

since the underscore-free name is now available for use as variables
we'll migrate to that where we currently use `_topGoals` for locals.

Change-Id: I5727c5ea7799647c0a69ab76975b1a03a6558aa6
2024-10-05 19:53:30 +00:00
eldritch horrors f389a54079 libstore: propagate goal exceptions using promises
drop childException since it's no longer needed. also makes
waitForInput, childFinished, and childTerminated redundant.

Change-Id: I05d88ffd323c5b5c909ac21056162f69ffb0eb9f
2024-10-05 19:44:47 +00:00
eldritch horrors 7ef4466018 libstore: have goals promise WorkResults, not void
Change-Id: Idd218ec1572eda84dc47accc0dcd8a954d36f098
2024-10-05 19:06:59 +00:00
eldritch horrors a9f2aab226 libstore: extract Worker::goalFinished specifics
there's no reason to have the worker set information on goals that the
goals themselves return from their entry point. doing this in the goal
`work()` function is much cleaner, and a prerequisite to removing more
implicit strong shared references to goals that are currently running.

Change-Id: Ibb3e953ab8482a6a21ce2ed659d5023a991e7923
2024-10-05 19:06:59 +00:00
eldritch horrors 99edc2ae38 libstore: check for interrupts in parallel promise
this simplifies the worker loop, and lets us remove it entirely later.
note that ideally only one promise waiting for interrupts should exist
in the entire system. not one per event loop, one per *process*. extra
interrupt waiters make interrupt response nondeterministic and as such
aren't great for user experience. if anything wants to react to aborts
caused by explicit interruptions, or anything else, those things would
be much better served using RAII guards such as Finally (or KJ_DEFER).

Change-Id: I41d035ff40172d536e098153c7375b0972110d51
2024-10-05 19:06:59 +00:00
eldritch horrors 896a123605 libstore: remove Goal::StillAlive
this was a triumph. i'm making a note here: huge success. it's hard to
overstate my satisfaction! i'm not even angry. i'm being so sincere ri

actually, no. we *are* angry. this was one dumbass odyssey. nobody has
asked for this. but not doing it would have locked us into old, broken
protocols forever or (possibly worse) forced us to write our own async
framework building on the old did-you-mean-continuations in Worker. if
we had done that we'd be locked into ever more, and ever more complex,
manual state management all over the place. this just could not stand.

Change-Id: I43a6de1035febff59d2eff83be9ad52af4659871
2024-10-05 18:21:02 +00:00
Rebecca Turner 0d484aa498
Add release note for CTRL-C improvements
I'm very excited for cl/2016, so others will probably be excited also!
Let's add a release note.

Change-Id: Ic84a4444241aafce4cb6d5a6d1dddb47e7a7dd7b
2024-10-05 10:40:51 -07:00
Rebecca Turner 86b213e632 Merge "Split ignoreException to avoid suppressing CTRL-C" into main 2024-10-05 17:33:00 +00:00
eldritch horrors a3dd07535c fix build test error count checks
with async runtime scheduling we can no longer guarantee exact error
counts for builds that do not set keepGoing. the old behavior can be
recovered with a number of hacks that affect scheduling, but none of
those are very easy to follow now advisable. exact error counts will
like not be needed for almost all uses except tests, and *those* had
better check the actual messages rather than how many they got. more
messages can even help to avoid unnecessary rebuilds for most users.

Change-Id: I1c9aa7a401227dcaf2e19975b8cb83c5d4f85d64
2024-10-05 16:21:19 +00:00
alois31 5df2cccc49
doc: install the HTML manual again
In 0e6b3435a1, installation of the HTML manual
was accidentally dropped: setting install_dir on a custom_target only sets the
directory where something is going to be installed if it is installed at all,
but does not itself trigger installation. The latter has to be explicitly
requested, which is just what we do here to get the manual back.

Change-Id: Iff8b791de7e7cb4c8d747c2a9b1154b5fcc32fe0
2024-10-05 10:49:34 +02:00
jade 345e3d068a testsuite: override NIX_CONF_DIR and NIX_USER_CONF_FILES
The test suite can load the global configuration files under certain
circumstances, and, though we would really rather it didn't ever do that
at all, we should at least break the mechanism.

Fixes: lix-project/lix#474
Change-Id: Ib27cb43dd5dfaa70ac491c395b5ba308fd7bd289
2024-10-04 19:17:08 -07:00
jade 19edaed81b Remove some outdated make test invocation suggestions
These should be meson.

Change-Id: I03687b8b03f50fb1684e7ffcd487be855052d6c2
2024-10-04 18:55:52 -07:00
eldritch horrors 5b1715e633 libstore: forbid addWantedGoals when finished
due to event loop scheduling behavior it's possible for a derivation
goal to fully finish (having seen all paths it was asked to create),
but to not notify the worker of this in time to prevent another goal
asking the recently-finished goal for more outputs. if this happened
the finished goal would ignore the request for more outputs since it
considered itself fully done, and the delayed result reporting would
cause the requesting goal to assume its request had been honored. if
the requested goal had finished *properly* the worker would recreate
it instead of asking for more outputs, and this would succeed. it is
thus safe to always recreate goals once they are done, so we now do.

Change-Id: Ifedd69ca153372c623abe9a9b49cd1523588814f
2024-10-04 17:49:57 +00:00
Rebecca Turner 0b29859cfe Merge "editorconfig: Add meson.build" into main 2024-10-04 16:36:20 +00:00
Olivia Crain 1bfc37fea5 Merge "internal-api-docs: allow Doxygen to build regardless of workdir" into main 2024-10-04 09:59:01 +00:00
Olivia Crain 8f300fbd82 Merge "build: let meson add compiler flags for libstdc++ assertions" into main 2024-10-04 09:58:32 +00:00
Rebecca Turner 36073781fb
editorconfig: Add meson.build
Change-Id: Ibb59ddc21f5d3ef7fb4c900e3413e426c201334d
2024-10-01 16:09:47 -07:00
Rebecca Turner b63d4a0c62
Remove static initializers for RegisterLegacyCommand
This moves the "legacy"/"nix2" commands under a new `src/legacy/`
directory, instead of being scattered around in a bunch of different
directories.

A new `liblegacy` build target is defined, and the `nix` binary is
linked against it.

Then, `RegisterLegacyCommand` is replaced with `LegacyCommand::add`
calls in functions like `registerNixCollectGarbage()`. These
registration functions are called explicitly in `src/nix/main.cc`.

See: lix-project/lix#359

Change-Id: Id450ffc3f793374907599cfcc121863b792aac1a
2024-10-01 16:08:58 -07:00
Robert Hensing ee0c195eba
Split ignoreException to avoid suppressing CTRL-C
This splits `ignoreException` into `ignoreExceptionExceptInterrupt`
(which ignores all exceptions except `Interrupt`, which indicates a
SIGINT/CTRL-C) and `ignoreExceptionInDestructor` (which ignores all
exceptions, so that destructors do not throw exceptions).

This prevents many cases where Nix ignores CTRL-C entirely.
See: https://github.com/NixOS/nix/issues/7245

Upstream-PR: https://github.com/NixOS/nix/pull/11618
Change-Id: Ie7d2467eedbe840d1b9fa2e88a4e88e4ab26a87b
2024-10-01 15:49:56 -07:00
eldritch horrors 7752927660 libstore: turn DerivationGoal::work into *one* promise
Change-Id: Ic2f7bc2bd6a1879ad614e4be81a7214f64eb0e85
2024-10-01 11:55:47 +00:00
eldritch horrors 3edc272341 libstore: turn DrvOutputSubstitutionGoal::work into *one* promise
Change-Id: I2d4dcedff0a278d2d8f3d264a9186dfb399275e2
2024-10-01 11:55:42 +00:00
eldritch horrors 9b05636937 libstore: make PathSubstitutionGoal::work *one* promise
Change-Id: I38cfe8c7059251b581f1013c4213804f36b985ea
2024-10-01 11:55:36 +00:00
eldritch horrors 9889c79fe3 libstore: turn Worker::updateStatistics into a promise
we'll now loop to update displayed statistics, and use this loop to
limit the update rate to 50 times per second. we could have updated
much more frequently before this (once per iteration of `runImpl`),
much faster than would ever be useful in practice. aggressive stats
updates can even impede progress due to terminal or network delays.

Change-Id: Ifba755a2569f73c919b1fbb06a142c0951395d6d
2024-10-01 11:55:29 +00:00
eldritch horrors 732de75f67 libstore: remove Worker::wakeUp()
Worker::run() is now entirely based on the kj event loop and promises,
so we need not handle awakeness of goals manually any more. every goal
can instead, once it has finished a partial work call, defer itself to
being called again in the next iteration of the loop. same end effect.

Change-Id: I320eee2fa60bcebaabd74d1323fa96d1402c1d15
2024-10-01 13:55:03 +02:00
eldritch horrors d5db0b1abc libstore: turn periodic gc attempt into a promise
notably we will check whether we want to do GC at all only once during
startup, and we'll only attempt GC every ten seconds rather than every
time a goal has finished a partial work call. this shouldn't cause any
problems in practice since relying on auto-gc is not deterministic and
stores in which builds can fill all remaining free space in merely ten
seconds are severely troubled even when gargage collection runs a lot.

Change-Id: I1175a56bf7f4e531f8be90157ad88750ff2ddec4
2024-10-01 11:36:45 +00:00
eldritch horrors b0c7c1ec66 libstore: turn Worker::run() main loop into a promise
Change-Id: Ib112ea9a3e67d5cb3d7d0ded30bbd25c96262470
2024-10-01 11:36:45 +00:00
eldritch horrors d31310bf59 libstore: turn waitForInput into a promise
Change-Id: I8355d8d3f6c43a812990c1912b048e5735b07f7b
2024-10-01 11:36:45 +00:00
raito 8e05cc1e6c Revert "libstore: remove worker removeGoal"
Revert submission 1946

Reason for revert: regression in building (found via bisection)

Reported by users:
> error: path '/nix/store/04ca5xwvasz6s3jg0k7njz6rzi0d225w-jq-1.7.1-dev' does not exist in the store

Reverted changes: /q/submissionid:1946

Change-Id: I6f1a4b2f7d7ef5ca430e477fc32bca62fd97036b
2024-10-01 11:07:57 +00:00
Jonas Chevalier a16ceb9411 Merge "fix(nix fmt): remove the default "." argument" into main 2024-09-30 16:10:32 +00:00
eldritch horrors aa33c34c9b libstore: merge ContinueImmediately and StillAlive
nothing needs to signal being still active but not actively pollable,
only that immediate polling for the next goal work phase is in order.

Change-Id: Ia43c1015e94ba4f5f6b9cb92943da608c4a01555
2024-09-29 15:29:56 +00:00
eldritch horrors ccd2862666 libstore: remove worker removeGoal
this was immensely inefficient on large caches, as can exist when many
derivations are buildable simultaneously. since we have smart pointers
to goals we can do cache maintenance in goal deleters instead, and use
the exact iterators instead of doing a linear search. this *does* rely
on goals being deleted to remove them from the cache, which isn't true
for toplevel goals. those would have previously been removed when done
in all cases, removing the cache entry when keep-going is set. this is
arguably incorrect since it might result in those goals being retried,
although that could only happen with dynamic derivations or the likes.
(luckily dynamic derivations not complete enough to allow this at all)

Change-Id: I8e750b868393588c33e4829333d370f2c509ce99
2024-09-29 15:29:56 +00:00
eldritch horrors 47ddd11933 libstore: extract a real makeGoalCommon
makeDerivationGoalCommon had the right idea, but it didn't quite go far
enough. let's do the rest and remove the remaining factory duplication.

Change-Id: I1fe32446bdfb501e81df56226fd962f85720725b
2024-09-29 15:07:30 +00:00
eldritch horrors 7f4f86795c libstore: remove Goal::key
this was a debugging aid from day one that should not have any impact on
build semantics, and if it *does* have an impact on build semantics then
build semantics are seriously broken. keeping the order imposed by these
keys will be impossible once we let a real event loop schedule our jobs.

Change-Id: I5c313324e1f213ab6453d82f41ae5e59de809a5b
2024-09-29 14:29:14 +00:00
eldritch horrors a5240b23ab libstore: make non-cache goal pointers strong
without circular references we do not need weak goal pointers except for
caches, which should not prevent goal destructors running. caches though
cannot create circular references even when they keep strong references.
if we removed goals from caches when their work() is fully finished, not
when their destructors are run, we could keep strong pointers in caches.
since we do not gain much from this we keep those pointers weak for now.

Change-Id: I1d4a6850ff5e264443c90eb4531da89f5e97a3a0
2024-09-29 14:29:14 +00:00
eldritch horrors 8fb642b6e0 libstore: remove Goal::WaitForWorld
have DerivationGoal and its subclasses produce a wrapper promise for
their intermediate results instead, and return this wrapper promise.
Worker already handles promises that do not complete immediately, so
we do not have to duplicate this into an entire result type variant.

Change-Id: Iae8dbf63cfc742afda4d415922a29ac5a3f39348
2024-09-29 14:29:14 +00:00
eldritch horrors 1a52e4f755 libstore: fix build tests
the new event loop could very occasionally notice that a dependency of
some goal has failed, process the failure, cause the depending goal to
fail accordingly, and in the doing of the latter two steps let further
dependencies that previously have not been reported as failed do their
reporting anyway. in such cases a goal could fail with "1 dependencies
failed", but more than one dependency failure message was shown. we'll
now report the correct number of failed dependency goals in all cases.

Change-Id: I5aa95dcb2db4de4fd5fee8acbf5db833531d81a8
2024-09-29 13:17:15 +00:00
eldritch horrors 3f7519526f libstore: have makeLocalDerivationGoal return unique_ptrs
these can be unique rather than shared because shared_ptr has a
converting constructor. preparatory refactor for something else
and not necessary on its own, and the extra allocations we must
do for shared_ptr control blocks isn't usually relevant anyway.

Change-Id: I5391715545240c6ec8e83a031206edafdfc6462f
2024-09-29 12:09:24 +00:00
Maximilian Bosch 289e7a6b5a Merge "libfetchers/git: restore compat with builtins.fetchGit from 2.3" into main 2024-09-29 08:56:16 +00:00
Olivia Crain f12b60273b Merge changes I5c640824,I09ffc92e,I259583b7 into main
* changes:
  build: require meson 1.4.0 or newer
  build: fix deprecated uses of configure_file
  build: install html manual without using install_subdir
2024-09-28 23:41:30 +00:00
Maximilian Bosch 04daff94e3
libfetchers/git: restore compat with builtins.fetchGit from 2.3
Since fb38459d6e, each `ref` is appended
with `refs/heads` unless it starts with `refs/` already. This regressed
two use-cases that worked fine before:

* Specifying a commit hash as `ref`: now, if `ref` looks like a commit
  hash it will be directly passed to `git fetch`.

* Specifying a tag without `refs/tags` as prefix: now, the fetcher prepends
  `refs/*` to a ref that doesn't start with `refs/` and doesn't look
  like a commit hash. That way, both a branch and a tag specified in
  `ref` can be fetched.

  The order of preference in git is

  * file in `refs/` (e.g. `HEAD`)
  * file in `refs/tags/`
  * file in `refs/heads` (i.e. a branch)

  After fetching `refs/*`, ref is resolved the same way as git does.

Change-Id: Idd49b97cbdc8c6fdc8faa5a48bef3dec25e4ccc3
2024-09-28 14:52:06 +02:00
Olivia Crain 4780dd6bc4
build: let meson add compiler flags for libstdc++ assertions
We have manually enabled libstdc++ assertions since cl/797. Meson 1.4.0
(the minimum version we mandate) enables this by default, so we can
remove the enabling compiler flag from the list of project arguments.

With this patch, `-D_GLIBCXX_ASSERTIONS=1` is still present in the
compile command logs when building with both gccStdenv and clangStdenv.

See: https://gerrit.lix.systems/c/lix/+/797
See: https://mesonbuild.com/Release-notes-for-1-4-0.html#ndebug-setting-now-controls-c-stdlib-assertions
Change-Id: I53483fadfe5cbd11ba35544b437d3a9ee8031631
2024-09-27 12:26:07 -05:00
Olivia Crain b86863d935
build: require meson 1.4.0 or newer
This was already the de facto requirement, we use the method `full_path`
on a file object (introduced in Meson 1.4.0) in the functional test
suite's build.

This version of Meson is in NixOS 24.05, so there should be no
compatibility issues should this make it into a backported release of
Lix.

CC: lix-project/lix#247
Change-Id: I5c640824807353b6eb4287e7ed09c4e89a4bdde2
2024-09-27 11:57:53 -05:00
Olivia Crain 624f44bf25
build: fix deprecated uses of configure_file
Using `configure_file` to copy files has been deprecated since Meson 0.64.0.
The intended replacement is the `fs.copyfile` method.

This removes the following deprecation warning that arises when a minimum
Meson version is specified:

``
Project [...] uses feature deprecated since '0.64.0': copy arg in configure_file. Use fs.copyfile instead
``

Change-Id: I09ffc92e96311ef9ed594343a0a16d51e74b114a
2024-09-27 11:55:32 -05:00
Olivia Crain 0e6b3435a1
build: install html manual without using install_subdir
In Meson, `install_subdir` is meant to be used with directories in the source
directory. When using it to install the HTML manual, we provide it with a path
under the build directory.

We should instead specify an install directory for the HTML manual as part of
the custom target that builds it.

What we do currently isn't broken, just semantically incorrect. Changing it does
get rid of the following deprecation warning, though:

``
Project [...] uses feature deprecated since '0.60.0': install_subdir with empty directory. It worked by accident and is buggy. Use install_emptydir instead.
``

Change-Id: I259583b7bdff8ecbb3b342653d70dc5f034c7fad
2024-09-27 11:55:28 -05:00
eldritch horrors ae5d8dae1b libstore: turn Goal::WaitForGoals into a promise
also gets rid of explicit strong references to dependencies of any goal,
and weak references to dependers as well. those are now only held within
promises representing goal completion and thus independent of the goal's
relation to each other. the weak references to dependers was only needed
for notifications, and that's much better handled entirely by kj itself.

Change-Id: I00d06df9090f8d6336ee4bb0c1313a7052fb016b
2024-09-27 16:40:27 +02:00
eldritch horrors 852da07b67 libstore: replace Goal::WaitForSlot with semaphores
now that we have an event loop in the worker we can use it and its
magical execution suspending properties to replace the slot counts
we managed explicitly with semaphores and raii tokens. technically
this would not have needed an event loop base to be doable, but it
is a whole lot easier to wait for a token to be available if there
is a callback mechanism ready for use that doesn't require a whole
damn dedicated abstract method in Goal to work, and specific calls
to that dedicated method strewn all over the worker implementation

Change-Id: I1da7cf386d94e2bbf2dba9b53ff51dbce6a0cff7
2024-09-27 16:40:27 +02:00
eldritch horrors bf32085d63 libstore: simplify Worker::waitForInput
with waitForAWhile turned into promised the core functionality of
waitForInput is now merely to let gc run every so often if needed

Change-Id: I68da342bbc1d67653901cf4502dabfa5bc947628
2024-09-27 16:40:26 +02:00
eldritch horrors cd1ceffb0e libstore: make waiting for a while a promise
this simplifies waitForInput quite a lot, and at the same time makes
polling less thundering-herd-y. it even fixes early polling wakeups!

Change-Id: I6dfa62ce91729b8880342117d71af5ae33366414
2024-09-27 16:39:33 +02:00
eldritch horrors 0478949c72 libstore: turn builder output processing into event loop
this removes the rather janky did-you-mean-async poll loop we had so
far. sadly kj does not play well with pty file descriptors, so we do
have to add our own async input stream that does not eat pty EIO and
turns it into an exception. that's still a *lot* better than the old
code, and using a real even loop makes everything else easier later.

Change-Id: Idd7e0428c59758602cc530bcad224cd2fed4c15e
2024-09-27 16:38:16 +02:00
Jonas Chevalier 2265536e85 fix(nix fmt): remove the default "." argument
When `nix fmt` is called without an argument, Nix appends the "." argument before calling the formatter. The comment in the code is:
> Format the current flake out of the box

This also happens when formatting sub-folders.

This means that the formatter is now unable to distinguish, as an interface, whether the "." argument is coming from the flake or the user's intent to format the current folder. This decision should be up to the formatter.

Treefmt, for example, will automatically look up the project's root and format all the files. This is the desired behaviour. But because the "." argument is passed, it cannot function as expected.

Upstream-PR: https://github.com/nixos/nix/pull/11438

Change-Id: I60fb6b3ed4ec1b24f81b5f0d76c0be98470817ce
2024-09-26 14:32:29 -07:00
jade 14dc84ed03 Merge changes Iaa2e0e9d,Ia973420f into main
* changes:
  Fix passing custom CA files into the builtin:fetchurl sandbox
  [security] builtin:fetchurl: Enable TLS verification
2024-09-26 20:53:46 +00:00
eldritch horrors 619a93bd54 Merge "libutil: add async collection mechanism" into main 2024-09-26 17:23:52 +00:00
jade 5dc7671d81 Merge "fmt: fail hard on bad format strings going into nix::fmt too" into main 2024-09-26 17:07:29 +00:00
jade b6038e988d Merge "main: log stack traces for std::terminate" into main 2024-09-26 17:06:01 +00:00
eldritch horrors 531d040e8c libutil: add async collection mechanism
like kj::joinPromisesFailFast this allows waiting for the results of
multiple promises at once, but unlike it not all input promises must
be complete (or any of them failed) for results to become available.

Change-Id: I0e4a37e7bd90651d56b33d0bc5afbadc56cde70c
2024-09-26 16:56:08 +00:00
eldritch horrors ca9256a789 libutil: add an async semaphore implementation
like a normal semaphore, but with awaitable acquire actions. this is
primarily intended as an intermediate concurrency limiting device in
the Worker code, but it may find other uses over time. we do not use
std::counting_semaphore as a base because the counter of that is not
inspectable as will be needed for Worker. we also do not need atomic
operations for cross-thread consistency since we don't have multiple
threads (thanks to kj event loops being confined to a single thread)

Change-Id: Ie2bcb107f3a2c0185138330f7cbba4cec6cbdd95
2024-09-26 16:32:02 +00:00
eldritch horrors 4b66e1e24f fix internal-api-docs build
this one is also run from a gcc stdenv.

Change-Id: I91ff6915c6689ece15224f348f54367cff5d2b5a
2024-09-26 16:11:43 +00:00
puck 37b22dae04 Fix passing custom CA files into the builtin:fetchurl sandbox
Without this, verifying TLS certificates would fail on macOS, as well
as any system that doesn't have a certificate file at /etc/ssl/certs/ca-certificates.crt,
which includes e.g. Fedora.

Change-Id: Iaa2e0e9db3747645b5482c82e3e0e4e8f229f5f9
2024-09-26 15:25:28 +00:00
Maximilian Bosch 31954b5136 Merge "flake: use clangStdenv for overlays.default" into main 2024-09-26 07:13:41 +00:00
jade acf963468f flake.lock: update everything
Periodic updating of everything.

Change-Id: Ie006fd7c2dc0725309dea831685ea7b24b569df5
2024-09-25 18:42:16 -07:00
Eelco Dolstra c1631b0a39 [security] builtin:fetchurl: Enable TLS verification
This is better for privacy and to avoid leaking netrc credentials in a
MITM attack, but also the assumption that we check the hash no longer
holds in some cases (in particular for impure derivations).

Partially reverts 5db358d4d7.

(cherry picked from commit c04bc17a5a0fdcb725a11ef6541f94730112e7b6)
(cherry picked from commit f2f47fa725fc87bfb536de171a2ea81f2789c9fb)
(cherry picked from commit 7b39cd631e0d3c3d238015c6f450c59bbc9cbc5b)

Upstream-PR: https://github.com/NixOS/nix/pull/11585

Change-Id: Ia973420f6098113da05a594d48394ce1fe41fbb9
2024-09-25 18:40:58 -07:00
jade aca19187d0 fmt: fail hard on bad format strings going into nix::fmt too
Previously we would only crash the program for bad HintFmt calls.
nix::fmt should also crash.

Change-Id: I4ba0abeb8557b208bd9c0be624c022a60446ef7e
2024-09-25 15:20:48 -07:00
jade 19e0ce2c03 main: log stack traces for std::terminate
These stack traces kind of suck for the reasons mentioned on the
CppTrace page here (no symbols for inline functions is a major one):
https://github.com/jeremy-rifkin/cpptrace

I would consider using CppTrace if it were packaged, but to be honest, I
think that the more reasonable option is actually to move entirely to
out-of-process crash handling and symbolization.

The reason for this is that if you want to generate anything of
substance on SIGSEGV or really any deadly signal, you are stuck in
async-signal-safe land, which is not a place to be trying to run a
symbolizer. LLVM does it anyway, probably carefully, and chromium *can*
do it on debug builds but in general uses crashpad:
https://source.chromium.org/chromium/chromium/src/+/main:base/debug/stack_trace_posix.cc;l=974;drc=82dff63dbf9db05e9274e11d9128af7b9f51ceaa;bpv=1;bpt=1

However, some stack traces are better than *no* stack traces when we get
mystery exceptions falling out the bottom of the program. I've also
promoted the path for "mystery exceptions falling out the bottom of the
program" to hard crash and generate a core dump because although there's
been some months since the last one of these, these are nonetheless
always *atrociously* diagnosed.

We can't improve the crash handling further until either we use Crashpad
(which involves more C++ deps, no thanks) or we put in the ostensibly
work in progress Rust minidump infrastructure, in which case we need to
finish full support for Rust in libutil first.

Sample report:

Lix crashed. This is a bug. We would appreciate if you report it at https://git.lix.systems/lix-project/lix/issues with the following information included:

Exception: std::runtime_error: lol
Stack trace:
 0# nix::printStackTrace() in /home/jade/lix/lix3/build/src/nix/../libutil/liblixutil.so
 1# 0x000073C9862331F2 in /home/jade/lix/lix3/build/src/nix/../libmain/liblixmain.so
 2# 0x000073C985F2E21A in /nix/store/p44qan69linp3ii0xrviypsw2j4qdcp2-gcc-13.2.0-lib/lib/libstdc++.so.6
 3# 0x000073C985F2E285 in /nix/store/p44qan69linp3ii0xrviypsw2j4qdcp2-gcc-13.2.0-lib/lib/libstdc++.so.6
 4# nix::handleExceptions(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::function<void ()>) in /home/jade/lix/lix3/build/src/nix/../libmain/liblixmain.so
 5# 0x00005CF65B6B048B in /home/jade/lix/lix3/build/src/nix/nix
 6# 0x000073C985C8810E in /nix/store/dbcw19dshdwnxdv5q2g6wldj6syyvq7l-glibc-2.39-52/lib/libc.so.6
 7# __libc_start_main in /nix/store/dbcw19dshdwnxdv5q2g6wldj6syyvq7l-glibc-2.39-52/lib/libc.so.6
 8# 0x00005CF65B610335 in /home/jade/lix/lix3/build/src/nix/nix

Change-Id: I1a9f6d349b617fd7145a37159b78ecb9382cb4e9
2024-09-25 14:03:45 -07:00
jade 8a6b84df14 Merge "package.nix: fix cross for editline" into main 2024-09-25 20:23:39 +00:00
Maximilian Bosch eccbe9586a
flake: use clangStdenv for overlays.default
We don't support GCC anymore for building, so the overlay currently
fails to evaluate with

    error: assertion '((stdenv).cc.isClang || lintInsteadOfBuild)' failed

`clangStdenv` seems like a reasonable default now.

Noticed while upgrading Lix for our Hydra fork.

Change-Id: I948a7c03b3e5648fc7c596f96e1b8053a9e7f92f
2024-09-25 18:31:34 +02:00
Olivia Crain 2f794733b2
internal-api-docs: allow Doxygen to build regardless of workdir
Previously, Doxygen needed to be ran from the project's source root dir
due to the relative paths in the config's `INPUT` tag. We now preprocess
the relative paths by prefixing them with the absolute path of the
project's source root dir. The HTML output remains unchanged.

Fixes: lix-project/lix#240
Change-Id: I85f099c22bfc5fdbf26be27c2db7dcbc8155c8b2
2024-09-24 13:26:22 -05:00
alois31 5f298f74c9 Merge "local-store: make extended attribute handling more robust" into main 2024-09-21 07:55:13 +00:00
jade 79246a3733 Merge "util: fix brotli decompression of empty input" into main 2024-09-18 23:36:25 +00:00
jade 789b19a0cf util: fix brotli decompression of empty input
This caused an infinite loop before since it would just keep asking the
underlying source for more data.

In practice this happened because an HTTP server served a
response to a HEAD request (for which curl will not retrieve any body or
call our write callback function) with Content-Encoding: br, leading to
decompressing nothing at all and going into an infinite loop.

This adds a test to make sure none of our compression methods do that
again, as well as just patching the HTTP client to never feed empty data
into a compression algorithm (since they absolutely have the right to
throw CompressionError on unexpectedly-short streams!).

Reported on Matrix: https://matrix.to/#/!lymvtcwDJ7ZA9Npq:lix.systems/$8BWQR_zKxCQDJ40C5NnDo4bQPId3pZ_aoDj2ANP7Itc?via=lix.systems&via=matrix.org&via=tchncs.de

Change-Id: I027566e280f0f569fdb8df40e5ecbf46c211dad1
2024-09-18 15:37:29 -07:00
Rebecca Turner 0943b214c9 Merge "tests/compression: rewrite" into main 2024-09-18 20:38:18 +00:00
alois31 2afdf1ed66
path-info: wipe the progress bar before printing
The legitimate output of `nix path-info` may visually interfere with the
progress bar, by appending to stale progress output before the latter has been
erased. Conveniently, all expensive operations (evaluation or building) have
already been performed before, so we can simply wipe the progress bar at this
point to fix the issue.

Fixes: lix-project/lix#343
Change-Id: Id9a807a5c882295b3e6fbf841f9c15dc96f67f6e
2024-09-18 19:26:40 +02:00
jade ed381cd58a package.nix: fix cross for editline
editline's dep on ncurses is a runtime one, so it should be in
buildInputs, not nativeBuildInputs.

CC: lix-project/lix#527
Change-Id: I631c192a55677b0cc77faa7511986f1fa2205e91
2024-09-17 20:43:21 -07:00
jade 4046e019ca tests/compression: rewrite
This test suite was in desperate need of using the parameterization
available with gtest, and was a bunch of useless duplicated code. At
least now it's not duplicated code, though it still probably should be
more full of property tests.

Change-Id: Ia8ccee7ef4f02b2fa40417b79aa8c8f0626ea479
2024-09-17 19:07:48 -07:00
Rebecca Turner 8ab5743904 Merge "Remove readline support" into main 2024-09-17 16:36:34 +00:00
Rebecca Turner 7ae0409989
Remove readline support
Lix cannot be built with GNU readline, and we would "rather not" be GPL.

Change-Id: I0e86f0f10dab966ab1d1d467fb61fd2de50c00de
2024-09-16 10:48:20 -07:00
Maximilian Bosch 80202e3ca3
common-eval-args: raise warning if --arg isn't a valid Nix identifier
See lix-project/lix#496.

The core idea is to be able to do e.g.

    nix-instantiate -A some-nonfree-thing --arg config.allowUnfree true

which is currently not possible since `config.allowUnfree` is
interpreted as attribute name with a dot in it.

In order to change that (probably), Jade suggested to find out if there
are any folks out there relying on this behavior.

For such a use-case, it may still be possible to accept strings, i.e.
`--arg '"config.allowUnfree"'.

Change-Id: I986c73619fbd87a95b55e2f0ac03feaed3de2d2d
2024-09-15 16:52:30 +02:00
jade 727258241f fix: docs issue template was busted
Apparently forgejo has a more creative interpretation of \(\) than I was
hoping in their markdown parser and thought it was maths. I have no idea
then how you put a link in parens next to another square-bracket link,
but I am not going to worry about it.

There were several more typos, which I also fixed.

Fixes: lix-project/lix#517
Change-Id: I6b144c6881f92ca60ba72a304ce7a0bcb9c6659a
2024-09-14 19:28:46 +00:00
jade 5246cea6c8 Merge "store: add a hint on how to fix Lix installs broken by macOS Sequoia" into main 2024-09-14 19:28:24 +00:00
jade 8f88590d13 Merge changes Ia1481da4,Ifca1d74d into main
* changes:
  archive: refactor bad mutable-state API in the NAR parse listener
  archive: rename ParseSink to NARParseVisitor
2024-09-14 19:26:08 +00:00
alois31 3f07c65510
local-store: make extended attribute handling more robust
* Move the extended attribute deletion after the hardlink sanity check. We
  shouldn't be removing extended attributes on random files.
* Make the entity owner-writable before attempting to remove extended
  attributes, since this operation usually requires write access on the file,
  and we shouldn't fail xattr deletion on a file that has been made unwritable
  by the builder or a previous canonicalisation pass.

Fixes: lix-project/lix#507
Change-Id: I7e6ccb71649185764cd5210f4a4794ee174afea6
2024-09-14 10:36:22 +02:00
jade b7fc37b015 store: add a hint on how to fix Lix installs broken by macOS Sequoia
This is not a detailed diagnosis, and it's not worth writing one, tbh.
This error basically never happens in normal operation, so diagnosing it
by changing the error on macOS is good enough.

Relevant: lix-project/lix-installer#24
Relevant: lix-project/lix-installer#18
Relevant: lix-project/lix#521

Change-Id: I03701f917d116575c72a97502b8e1617679447f2
2024-09-14 07:31:30 +00:00
jade ca1dc3f70b archive: refactor bad mutable-state API in the NAR parse listener
Remove the mutable state stuff that assumes that one file is being
written a time. It's true that we don't write multiple files
interleaved, but that mutable state is evil.

Change-Id: Ia1481da48255d901e4b09a9b783e7af44fae8cff
2024-09-13 17:11:43 -07:00
alois31 b2fc007811 Merge "fish-completion: leave the shell prompt intact" into main 2024-09-12 06:14:22 +00:00
alois31 82aa1ccab4
fish-completion: leave the shell prompt intact
When generating shell completions, no logging output should be visible because
it would destroy the shell prompt. Originally this was attempted to be done by
simply disabling the progress bar (ca946860ce),
since the situation is particularly bad there (the screen clearing required for
the rendering ends up erasing the shell prompt). Due to overlooking the
implementation of this hack, it was accidentally undone during a later change
(0dd1d8ca1c).
Since even with the hack correctly in place, it is still possible to mess up
the prompt by logging output (for example warnings for disabled experimental
features, or messages generated by `builtins.trace`), simply send it to the bit
bucket where it belongs. This was already done for bash and zsh
(9d840758a8), and it seems that fish was simply
missed at that time. The last trace of the no-longer-working and obsolete hack
is deleted too.

Fixes: lix-project/lix#513
Change-Id: I59f1ebf90903034e2059298fa8d76bf970bc3315
2024-09-11 19:03:11 +02:00
Rebecca Turner df0137226d
editline: Vendor cl/1883 patch to recognize Alt+Left/Alt+Right
This vendors the patch added in cl/1883 to avoid GitHub
garbage-collecting the commits we're referring to.

As @emilazy pointed out on GitHub:

> GitHub can garbage‐collect unmerged PR commits if they are later
> force‐pushed, which means that code review in upstreams can cause
> Nixpkgs builds to fail to reproduce in future.

See: https://github.com/NixOS/nixpkgs/pull/341131#discussion_r1753046220
See: https://github.com/troglobit/editline/pull/70
See: https://gerrit.lix.systems/c/lix/+/1883

Change-Id: Ifff522f7f23310d6dbe9efc72fd40be5500ae872
2024-09-11 09:35:00 -07:00
jade 81c2e0ac8e archive: rename ParseSink to NARParseVisitor
- Rename the listener to not be called a "sink". If it were a "sink" it
  would be eating bytes and conform with any of the Nix sink stuff
  (maybe FileHandle should be a Sink itself! but that's a later CL's
  problem). This is a parser listener.
- Move the RetrieveRegularNARSink thing into store-api.cc, which is its
  only usage, and fix it to actually do what it is stated to do: crash
  if its invariants are violated.

  It's, of course, used to erm, unpack single-file NAR files, generated
  via a horrible contraption of sources and sinks that looks like a
  plumbing blueprint. Refactoring that is a future task.
- Add a description of the invariants of NARParseVisitor in preparation
  of refactoring it.

Change-Id: Ifca1d74d2947204a1f66349772e54dad0743e944
2024-09-11 01:10:49 -07:00
Rebecca Turner 24db81eaf2 Merge "repl: Patch editline to recognize Meta-Left & Meta-Right" into main 2024-09-11 01:02:27 +00:00
Rebecca Turner cc183fdbc1 Merge "repl-overlays: Provide an elaborate example" into main 2024-09-10 00:17:01 +00:00
Rebecca Turner f5ae72d445 Merge "Add getCwd" into main 2024-09-10 00:10:40 +00:00
Rebecca Turner 6de6cae3e7
repl: Patch editline to recognize Meta-Left & Meta-Right
This applies https://github.com/troglobit/editline/pull/70 to our build
of editline, which translates `meta-left` and `meta-right` into
`fd_word` and `bk_word`. This makes `nix repl` soooo much nicer to use!

Note: My terminal renders `meta-left` as `\e\e[C` and `meta-right` as
`\e\e[D`.

Closes lix-project/lix#501

Change-Id: I048b10cf17231bbf4e6bf38e1d1d8572cedaa194
2024-09-09 15:34:50 -07:00
alois31 8f7ab26f96 Merge changes If8ec210f,I6e2851b2 into main
* changes:
  libfetchers: serialise accept-flake-config properly
  libstore: declare SandboxMode JSON serialisation in the header
2024-09-09 16:14:23 +00:00
eldritch horrors c14486ae8d forbid gcc for compilation, only allow clang
while gcc 12 and older miscompile our generators, gcc 13 and older
outright crash on kj coroutines. (newer gcc versions may fix this)

Change-Id: I19f12c8c147239680eb0fa5a84ef5c7de38c9263
2024-09-09 01:48:20 +00:00
alois31 e9505dcc5a Merge "libmain/progress-bar: erase all lines of the multi-line format" into main 2024-09-08 13:42:17 +00:00
eldritch horrors f2a49032a6 libstore: turn Worker in a kj event loop user
using a proper event loop basis we no longer have to worry about most of
the intricacies of poll(), or platform-dependent replacements for it. we
may even be able to use the event loop and its promise system for all of
our scheduling in the future. we don't do any real async processing yet,
this is just preparation to separate the first such change from the huge
api design difference with the async framework we chose (kj from capnp):

kj::Promise, unlike std::future, doesn't return exceptions unmangled. it
instead wraps any non-kj exception into a kj exception, erasing all type
information and preserving mostly the what() string in the process. this
makes sense in the capnp rpc use case where unrestricted exception types
can't be transferred, and since it moves error handling styles closer to
a world we'd actually like there's no harm in doing it only here for now

Change-Id: I20f888de74d525fb2db36ca30ebba4bcfe9cc838
2024-09-08 01:57:48 +00:00
eldritch horrors 92eccfbd68 libutil: add a result type using boost outcome
we're using boost::outcome rather than leaf or stl types because stl
types are not available everywhere and leaf does not provide its own
storage for error values, relying on thread-locals and the stack. if
we want to use promises we won't have a stack and would have to wrap
everything into leaf-specific allocating wrappers, so outcome it is.

Change-Id: I35111a1f9ed517e7f12a839e2162b1ba6a993f8f
2024-09-08 01:57:48 +00:00
alois31 4715d557ef
libmain/progress-bar: erase all lines of the multi-line format
When the multi-line log format is enabled, the progress bar usually occupies
multiple lines on the screen. When stopping the progress bar, only the last
line was wiped, leaving all others visible on the screen. Erase all lines
belonging to the progress bar to prevent these leftovers.
Asking the user for input is theoretically affected by a similar issue, but
this is not observed in practice since the only place where the user is asked
(whether configuration options coming from flakes should be accepted) does not
actually have multiple lines on the progress bar. However, there is no real
reason to not fix this either, so let's do it anyway.

Change-Id: Iaa5a701874fca32e6f06d85912835d86b8fa7a16
2024-09-07 10:37:12 +02:00
alois31 991d8ce275 Merge "Stop the logger in legacy commands again" into main 2024-09-06 17:07:16 +00:00
Rebecca Turner 72589e7032 Merge "Test including relative paths in configuration" into main 2024-09-03 18:48:47 +00:00
Rebecca Turner 644176a631 Merge "Expand comment on std::string operator+" into main 2024-09-03 18:48:37 +00:00
alois31 63ee2cdda3
libfetchers: serialise accept-flake-config properly
The AcceptFlakeConfig type used was missing its JSON serialisation definition,
so it was incorrectly serialised as an integer, ending up that way for example
in the nix.conf manual page. Declare a proper serialisation.

Change-Id: If8ec210f9d4dd42fe480c4e97d0a4920eb66a01e
2024-09-02 18:50:15 +02:00
alois31 d7c37324bb
libstore: declare SandboxMode JSON serialisation in the header
The JSON serialisation should be declared in the header so that all translation
units can see it when needed, even though it seems that it has not been used
anywhere else so far. Unfortunately, this means we cannot use the
NLOHMANN_JSON_SERIALIZE_ENUM convenience macro, since it uses a slightly
different signature, but the code is not too bad either.

Change-Id: I6e2851b250e0b53114d2fecb8011ff1ea9379d0f
2024-09-02 18:50:14 +02:00
Rebecca Turner 75c0de3e3c
Test including relative paths in configuration
Change-Id: If6c69a5e16d1ccd223fba392890f08f0032fb754
2024-09-01 15:52:48 -07:00
Rebecca Turner fc4a160878
repl-overlays: Provide an elaborate example
This is the repl overlay from my dotfiles, which I think provides a
reasonable and ergonomic set of variables. We can iterate on this over
time, or (perhaps?) provide a sentinel value like `repl-overlays =
<DEFAULT>` to include a "suggested default" overlay like this one.

Change-Id: I8eba3934c50fbac8367111103e66c7375b8d134e
2024-09-01 15:30:58 -07:00
Rebecca Turner b7b1b9723f
Clarify that diff-hook no longer needs to be an absolute path
See: https://gerrit.lix.systems/c/lix/+/1864
Change-Id: Ic70bfe42b261a83f2cb68b8f102833b739b8e03a
2024-09-01 15:20:09 -07:00
Rebecca Turner 9d8f433246
Expand comment on std::string operator+
Nuts!

Change-Id: Ib5bc0606d7c86e57ef76dd7bcc89dce91bd3d50a
2024-09-01 15:10:31 -07:00
Rebecca Turner 742303dc3a
Add getCwd
It's nice for this to be a separate function and not just inline in
`absPath`.

Prepared as part of cl/1865, though I don't think I actually ended up
using it there.

Change-Id: I24d9d4a984cee0af587010baf04b3939a1c147ec
2024-08-26 11:22:07 -07:00
alois31 de552c42cb
Stop the logger in legacy commands again
Commit 0dd1d8ca1c included an accidental revert
of 1461e6cdda (actually slightly worse), leading
to the progress bar not being stopped properly when a legacy command was
invoked with `--log-format bar` (or similar options that show a progress bar).
Move the progress bar stopping code to its proper place again to fix this
regression.

Change-Id: I676333da096d5990b717a387924bb988c9b73fab
2024-08-21 17:28:42 +02:00
233 changed files with 5898 additions and 3369 deletions

View file

@ -29,3 +29,7 @@ trim_trailing_whitespace = false
indent_style = space
indent_size = 2
max_line_length = 0
[meson.build]
indent_style = space
indent_size = 2

View file

@ -2,7 +2,7 @@
name: Missing or incorrect documentation
about: Help us improve the reference manual
title: ''
labels: documentation
labels: docs
assignees: ''
---
@ -19,10 +19,10 @@ assignees: ''
<!-- make sure this issue is not redundant or obsolete -->
- [ ] checked [latest Lix manual] \([source]\)
- [ ] checked [latest Lix manual] or its [source code]
- [ ] checked [documentation issues] and [recent documentation changes] for possible duplicates
[latest Nix manual]: https://docs.lix.systems/manual/lix/nightly
[source]: https://git.lix.systems/lix-project/lix/src/main/doc/manual/src
[latest Lix manual]: https://docs.lix.systems/manual/lix/nightly
[source code]: https://git.lix.systems/lix-project/lix/src/main/doc/manual/src
[documentation issues]: https://git.lix.systems/lix-project/lix/issues?labels=151&state=all
[recent documentation changes]: https://gerrit.lix.systems/q/p:lix+path:%22%5Edoc/manual/.*%22

3
.gitignore vendored
View file

@ -36,3 +36,6 @@ buildtime.bin
# Rust build files when using Cargo (not actually supported for building but it spews the files anyway)
/target/
# Python compiled files from the test suite
*.pyc

View file

@ -1,4 +1,5 @@
#!/usr/bin/env bash
#!/usr/bin/env nix-shell
#!nix-shell -i bash -p bash -p hyperfine
set -euo pipefail
shopt -s inherit_errexit
@ -21,16 +22,21 @@ fi
_exit=""
trap "$_exit" EXIT
# XXX: yes this is very silly. flakes~!!
nix build --impure --expr '(builtins.getFlake "git+file:.").inputs.nixpkgs.outPath' -o bench/nixpkgs
flake_args=("--extra-experimental-features" "nix-command flakes")
# XXX: yes this is very silly. flakes~!!
nix build "${flake_args[@]}" --impure --expr '(builtins.getFlake "git+file:.").inputs.nixpkgs.outPath' -o bench/nixpkgs
# We must ignore the global config, or else NIX_PATH won't work reliably.
# See https://github.com/NixOS/nix/issues/9574
export NIX_CONF_DIR='/var/empty'
export NIX_REMOTE="$(mktemp -d)"
_exit='rm -rfv "$NIX_REMOTE"; $_exit'
export NIX_PATH="nixpkgs=bench/nixpkgs:nixos-config=bench/configuration.nix"
builds=("$@")
flake_args="--extra-experimental-features 'nix-command flakes'"
flake_args="${flake_args[*]@Q}"
hyperfineArgs=(
--parameter-list BUILD "$(IFS=,; echo "${builds[*]}")"

View file

@ -33,32 +33,7 @@ GENERATE_LATEX = NO
# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING
# Note: If this tag is empty the current directory is searched.
# FIXME Make this list more maintainable somehow. We could maybe generate this
# in the Makefile, but we would need to change how `.in` files are preprocessed
# so they can expand variables despite configure variables.
INPUT = \
src/libcmd \
src/libexpr \
src/libexpr/flake \
tests/unit/libexpr \
tests/unit/libexpr/value \
tests/unit/libexpr/test \
tests/unit/libexpr/test/value \
src/libexpr/value \
src/libfetchers \
src/libmain \
src/libstore \
src/libstore/build \
src/libstore/builtins \
tests/unit/libstore \
tests/unit/libstore/test \
src/libutil \
tests/unit/libutil \
tests/unit/libutil/test \
src/nix \
src/nix-env \
src/nix-store
INPUT = @INPUT_PATHS@
# If the MACRO_EXPANSION tag is set to YES, doxygen will expand all macro names
# in the source code. If set to NO, only conditional compilation will be
@ -97,3 +72,15 @@ EXPAND_AS_DEFINED = \
DECLARE_WORKER_SERIALISER \
DECLARE_SERVE_SERIALISER \
LENGTH_PREFIXED_PROTO_HELPER
# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path.
# Stripping is only done if one of the specified strings matches the left-hand
# part of the path. The tag can be used to show relative paths in the file list.
# If left blank the directory from which doxygen is run is used as the path to
# strip.
#
# Note that you can specify absolute paths here, but also relative paths, which
# will be relative from the directory where doxygen is started.
# This tag requires that the tag FULL_PATH_NAMES is set to YES.
STRIP_FROM_PATH = "@PROJECT_SOURCE_ROOT@"

View file

@ -1,3 +1,35 @@
internal_api_sources = [
'src/libcmd',
'src/libexpr',
'src/libexpr/flake',
'tests/unit/libexpr',
'tests/unit/libexpr/value',
'tests/unit/libexpr/test',
'tests/unit/libexpr/test/value',
'src/libexpr/value',
'src/libfetchers',
'src/libmain',
'src/libstore',
'src/libstore/build',
'src/libstore/builtins',
'tests/unit/libstore',
'tests/unit/libstore/test',
'src/libutil',
'tests/unit/libutil',
'tests/unit/libutil/test',
'src/nix',
'src/nix-env',
'src/nix-store',
]
# We feed Doxygen absolute paths so it can be invoked from any working directory.
internal_api_sources_absolute = []
foreach src : internal_api_sources
internal_api_sources_absolute += '"' + (meson.project_source_root() / src) + '"'
endforeach
internal_api_sources_oneline = ' \\\n '.join(internal_api_sources_absolute)
doxygen_cfg = configure_file(
input : 'doxygen.cfg.in',
output : 'doxygen.cfg',
@ -5,22 +37,16 @@ doxygen_cfg = configure_file(
'PACKAGE_VERSION': meson.project_version(),
'RAPIDCHECK_HEADERS': rapidcheck_meson.get_variable('includedir'),
'docdir' : meson.current_build_dir(),
'INPUT_PATHS' : internal_api_sources_oneline,
'PROJECT_SOURCE_ROOT' : meson.project_source_root(),
},
)
internal_api_docs = custom_target(
'internal-api-docs',
command : [
bash,
# Meson can you please just give us a `workdir` argument to custom targets...
'-c',
# We have to prefix the doxygen_cfg path with the project build root
# because of the cd in front.
'cd @0@ && @1@ @2@/@INPUT0@'.format(
meson.project_source_root(),
doxygen.full_path(),
meson.project_build_root(),
),
doxygen.full_path(),
'@INPUT0@',
],
input : [
doxygen_cfg,

View file

@ -85,6 +85,10 @@ kloenk:
forgejo: kloenk
github: kloenk
lheckemann:
forgejo: lheckemann
github: lheckemann
lovesegfault:
github: lovesegfault
@ -141,9 +145,17 @@ valentin:
display_name: Valentin Gagarin
github: fricklerhandwerk
vigress8:
display_name: Vigress
forgejo: vigress8
github: vigress8
winter:
forgejo: winter
github: winterqt
yshui:
github: yshui
zimbatm:
github: zimbatm

View file

@ -126,20 +126,19 @@ manual = custom_target(
'manual',
'markdown',
],
install : true,
install_dir : [
datadir / 'doc/nix',
false,
],
depfile : 'manual.d',
env : {
'RUST_LOG': 'info',
'MDBOOK_SUBSTITUTE_SEARCH': meson.current_build_dir() / 'src',
},
)
manual_html = manual[0]
manual_md = manual[1]
install_subdir(
manual_html.full_path(),
install_dir : datadir / 'doc/nix',
)
nix_nested_manpages = [
[ 'nix-env',
[

View file

@ -0,0 +1,10 @@
---
synopsis: "`Alt+Left` and `Alt+Right` go back/forwards by words in `nix repl`"
issues: [fj#501]
cls: [1883]
category: Fixes
credits: 9999years
---
`nix repl` now recognizes `Alt+Left` and `Alt+Right` for navigating by words
when entering input in `nix repl` on more terminals/platforms.

View file

@ -0,0 +1,15 @@
---
synopsis: "Drop support for `xz` and `bzip2` Content-Encoding"
category: Miscellany
cls: [2134]
credits: horrors
---
Lix no longer supports the non-standard HTTP Content-Encoding values `xz` and `bzip2`.
We do not expect this to cause any problems in practice since these encodings *aren't*
standard, and any server delivering them anyway without being asked to is already well
and truly set on the path of causing inexplicable client breakages.
Lix's ability to decompress files compressed with `xz` or `bzip2` is unaffected. We're
only bringing Lix more in line with the HTTP standard; all post-transfer data handling
remains as it was before.

View file

@ -0,0 +1,13 @@
---
synopsis: Ctrl-C stops Nix commands much more reliably and responsively
issues: [7245, fj#393]
cls: [2016]
prs: [11618]
category: Fixes
credits: [roberth, 9999years]
---
CTRL-C will now stop Nix commands much more reliably and responsively. While
there are still some cases where a Nix command can be slow or unresponsive
following a `SIGINT` (please report these as issues!), the vast majority of
signals will now cause the Nix command to quit quickly and consistently.

View file

@ -0,0 +1,10 @@
---
synopsis: "transfers no longer allow arbitrary url schemas"
category: Breaking Changes
cls: [2106]
credits: horrors
---
Lix no longer allows transfers using arbitrary url schemas. Only `http://`, `https://`, `ftp://`, `ftps://`, and `file://` urls are supported going forward. This affects `builtins.fetchurl`, `<nix/fetchurl.nix>`, transfers to and from binary caches, and all other uses of the internal file transfer code. Flake inputs using multi-protocol schemas (e.g. `git+ssh`) are not affected as those use external utilities to transfer data.
The `s3://` scheme is not affected at all by this change and continues to work if S3 support is built into Lix.

View file

@ -0,0 +1,23 @@
---
synopsis: restore backwards-compatibility of `builtins.fetchGit` with Nix 2.3
issues: [5291, 5128]
credits: [ma27]
category: Fixes
---
Compatibility with `builtins.fetchGit` from Nix 2.3 has been restored as follows:
* Until now, each `ref` was prefixed with `refs/heads` unless it starts with `refs/` itself.
Now, this is not done if the `ref` looks like a commit hash.
* Specifying `builtins.fetchGit { ref = "a-tag"; /* … */ }` was broken because `refs/heads` was appended.
Now, the fetcher doesn't turn a ref into `refs/heads/ref`, but into `refs/*/ref`. That way,
the value in `ref` can be either a tag or a branch.
* The ref resolution happens the same way as in git:
* If `refs/ref` exists, it's used.
* If a tag `refs/tags/ref` exists, it's used.
* If a branch `refs/heads/ref` exists, it's used.

View file

@ -0,0 +1,38 @@
---
synopsis: Removing the `.` default argument passed to the `nix fmt` formatter
issues: []
prs: [11438]
cls: [1902]
category: Breaking Changes
credits: zimbatm
---
The underlying formatter no longer receives the ". " default argument when `nix fmt` is called with no arguments.
This change was necessary as the formatter wasn't able to distinguish between
a user wanting to format the current folder with `nix fmt .` or the generic
`nix fmt`.
The default behaviour is now the responsibility of the formatter itself, and
allows tools such as treefmt to format the whole tree instead of only the
current directory and below.
This may cause issues with some formatters: nixfmt, nixpkgs-fmt and alejandra currently format stdin when no arguments are passed.
Here is a small wrapper example that will restore the previous behaviour for such a formatter:
```nix
{
outputs = { self, nixpkgs, systems }:
let
eachSystem = nixpkgs.lib.genAttrs (import systems) (system: nixpkgs.legacyPackages.${system});
in
{
formatter = eachSystem (pkgs:
pkgs.writeShellScriptBin "formatter" ''
if [[ $# = 0 ]]; set -- .; fi
exec "${pkgs.nixfmt-rfc-style}/bin/nixfmt "$@"
'');
};
}
```

View file

@ -0,0 +1,10 @@
---
synopsis: "The beginnings of a new pytest-based functional test suite"
category: Development
cls: [2036, 2037]
credits: jade
---
The existing integration/functional test suite is based on a large volume of shell scripts.
This often makes it somewhat challenging to debug at the best of times.
The goal of the pytest test suite is to make tests have more obvious dependencies on files and to make tests more concise and easier to write, as well as making new testing methods like snapshot testing easy.

View file

@ -0,0 +1,17 @@
---
synopsis: readline support removed
cls: [1885]
category: Packaging
credits: [9999years]
---
Support for building Lix with [`readline`][readline] instead of
[`editline`][editline] has been removed. `readline` support hasn't worked for a
long time (attempting to use it would lead to build errors) and would make Lix
subject to the GPL if it did work. In the future, we're hoping to replace
`editline` with [`rustyline`][rustyline] for improved ergonomics in the `nix
repl`.
[readline]: https://en.wikipedia.org/wiki/GNU_Readline
[editline]: https://github.com/troglobit/editline
[rustyline]: https://github.com/kkawakam/rustyline

View file

@ -0,0 +1,8 @@
---
synopsis: "Dependency on monolithic coreutils removed"
category: Development
cls: [2108]
credits: vigress8
---
Previously, the build erroneously depended on a `coreutils` binary, which requires `coreutils` to be built with a specific configuration. This was only used in one test and was not required to be a single binary. This dependency is removed now.

View file

@ -0,0 +1,22 @@
---
synopsis: "Reproducibility check builds now report all differing outputs"
cls: [2069]
category: Improvements
credits: [lheckemann]
---
`nix-build --check` allows rerunning the build of an already-built derivation to check that it produces the same output again.
If a multiple-output derivation with impure behaviour is built with `--check`, only the first output would be shown in the resulting error message (and kept for comparison):
```
error: derivation '/nix/store/4spy3nz1661zm15gkybsy1h5f36aliwx-python3.11-test-1.0.0.drv' may not be deterministic: output '/nix/store/ccqcp01zg18wp9iadzmzimqzdi3ll08d-python3.11-test
-1.0.0-dist' differs from '/nix/store/ccqcp01zg18wp9iadzmzimqzdi3ll08d-python3.11-test-1.0.0-dist.check'
```
Now, all differing outputs are kept and reported:
```
error: derivation '4spy3nz1661zm15gkybsy1h5f36aliwx-python3.11-test-1.0.0.drv' may not be deterministic: outputs differ
output differs: output '/nix/store/ccqcp01zg18wp9iadzmzimqzdi3ll08d-python3.11-test-1.0.0-dist' differs from '/nix/store/ccqcp01zg18wp9iadzmzimqzdi3ll08d-python3.11-test-1.0.0-dist.check'
output differs: output '/nix/store/yl59v08356i841c560alb0zmk7q16klb-python3.11-test-1.0.0' differs from '/nix/store/yl59v08356i841c560alb0zmk7q16klb-python3.11-test-1.0.0.check'
```

View file

@ -0,0 +1,26 @@
---
synopsis: "Some Lix crashes now produce reporting instructions and a stack trace, then abort"
cls: [1854]
category: Improvements
credits: jade
---
Lix, being a C++ program, can crash in a few kinds of ways.
It can obviously do a memory access violation, which will generate a core dump and thus be relatively debuggable.
But, worse, it could throw an unhandled exception, and, in the past, we would just show the message but not where it comes from, in spite of this always being a bug, since we expect all such errors to be translated to a Lix specific error.
Now the latter kind of bug should print reporting instructions, a rudimentary stack trace and (depending on system configuration) generate a core dump.
Sample output:
```
Lix crashed. This is a bug. We would appreciate if you report it along with what caused it at https://git.lix.systems/lix-project/lix/issues with the following information included:
Exception: std::runtime_error: test exception
Stack trace:
0# nix::printStackTrace() in /home/jade/lix/lix3/build/src/nix/../libutil/liblixutil.so
1# 0x000073C9862331F2 in /home/jade/lix/lix3/build/src/nix/../libmain/liblixmain.so
2# 0x000073C985F2E21A in /nix/store/p44qan69linp3ii0xrviypsw2j4qdcp2-gcc-13.2.0-lib/lib/libstdc++.so.6
3# 0x000073C985F2E285 in /nix/store/p44qan69linp3ii0xrviypsw2j4qdcp2-gcc-13.2.0-lib/lib/libstdc++.so.6
4# nix::handleExceptions(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::function<void ()>) in /home/jade/lix/lix3/build/src/nix/../libmain/liblixmain.so
...
```

View file

@ -0,0 +1,10 @@
---
synopsis: "`<nix/fetchurl.nix>` now uses TLS verification"
category: Fixes
prs: [11585]
credits: edolstra
---
Previously `<nix/fetchurl.nix>` did not do TLS verification. This was because the Nix sandbox in the past did not have access to TLS certificates, and Nix checks the hash of the fetched file anyway. However, this can expose authentication data from `netrc` and URLs to man-in-the-middle attackers. In addition, Nix now in some cases (such as when using impure derivations) does *not* check the hash. Therefore we have now enabled TLS verification. This means that downloads by `<nix/fetchurl.nix>` will now fail if you're fetching from a HTTPS server that does not have a valid certificate.
`<nix/fetchurl.nix>` is also known as the builtin derivation builder `builtin:fetchurl`. It's not to be confused with the evaluation-time function `builtins.fetchurl`, which was not affected by this issue.

View file

@ -19,11 +19,11 @@
"nix2container": {
"flake": false,
"locked": {
"lastModified": 1720642556,
"narHash": "sha256-qsnqk13UmREKmRT7c8hEnz26X3GFFyIQrqx4EaRc1Is=",
"lastModified": 1724996935,
"narHash": "sha256-njRK9vvZ1JJsP8oV2OgkBrpJhgQezI03S7gzskCcHos=",
"owner": "nlewo",
"repo": "nix2container",
"rev": "3853e5caf9ad24103b13aa6e0e8bcebb47649fe4",
"rev": "fa6bb0a1159f55d071ba99331355955ae30b3401",
"type": "github"
},
"original": {
@ -34,11 +34,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1721931987,
"narHash": "sha256-1Zg8LY0T5EfXtv0Kf4M6SFnjH7Eto4VV+EKJ/YSnhiI=",
"lastModified": 1727184566,
"narHash": "sha256-mgdK8BcFsLSNhe780+cHbEUbZ3OruLa1T/xgQlL4Aj4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e21630230c77140bc6478a21cd71e8bb73706fce",
"rev": "48c3030083c46042584531bc9d931020f1975677",
"type": "github"
},
"original": {
@ -67,11 +67,11 @@
"pre-commit-hooks": {
"flake": false,
"locked": {
"lastModified": 1721042469,
"narHash": "sha256-6FPUl7HVtvRHCCBQne7Ylp4p+dpP3P/OYuzjztZ4s70=",
"lastModified": 1726745158,
"narHash": "sha256-D5AegvGoEjt4rkKedmxlSEmC+nNLMBPWFxvmYnVLhjk=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "f451c19376071a90d8c58ab1a953c6e9840527fd",
"rev": "4e743a6920eab45e8ba0fbe49dc459f1423a4b74",
"type": "github"
},
"original": {

View file

@ -99,9 +99,10 @@
];
stdenvs = [
"gccStdenv"
# see assertion in package.nix why these two are disabled
# "stdenv"
# "gccStdenv"
"clangStdenv"
"stdenv"
"libcxxStdenv"
"ccacheStdenv"
];
@ -121,7 +122,11 @@
name = "${stdenvName}Packages";
value = f stdenvName;
}) stdenvs
);
)
// {
# TODO delete this and reënable gcc stdenvs once gcc compiles kj coros correctly
stdenvPackages = f "clangStdenv";
};
# Memoize nixpkgs for different platforms for efficiency.
nixpkgsFor = forAllSystems (
@ -212,7 +217,7 @@
# A Nixpkgs overlay that overrides the 'nix' and
# 'nix.perl-bindings' packages.
overlays.default = overlayFor (p: p.stdenv);
overlays.default = overlayFor (p: p.clangStdenv);
hydraJobs = {
# Binary package for various platforms.
@ -264,6 +269,8 @@
nix = pkgs.callPackage ./package.nix {
inherit versionSuffix officialRelease buildUnreleasedNotes;
inherit (pkgs) build-release-notes;
# Required since we don't support gcc stdenv
stdenv = pkgs.clangStdenv;
internalApiDocs = true;
busybox-sandbox-shell = pkgs.busybox-sandbox-shell;
};
@ -321,6 +328,8 @@
inherit (nixpkgs) pkgs;
in
pkgs.callPackage ./package.nix {
# Required since we don't support gcc stdenv
stdenv = pkgs.clangStdenv;
versionSuffix = "";
lintInsteadOfBuild = true;
};

View file

@ -47,12 +47,12 @@
# in the build directory.
project('lix', 'cpp', 'rust',
meson_version : '>=1.4.0',
version : run_command('bash', '-c', 'echo -n $(jq -r .version < ./version.json)$VERSION_SUFFIX', check : true).stdout().strip(),
default_options : [
'cpp_std=c++2a',
'cpp_std=c++23',
'rust_std=2021',
# TODO(Qyriad): increase the warning level
'warning_level=1',
'warning_level=2',
'debug=true',
'optimization=2',
'errorlogs=true', # Please print logs for tests that fail
@ -167,10 +167,18 @@ endif
# frees one would expect when the objects are unique_ptrs. these problems
# often show up as memory corruption when nesting generators (since we do
# treat generators like owned memory) and will cause inexplicable crashs.
#
# gcc 13 does not compile capnp coroutine code correctly. a newer version
# may fix this. (cf. https://gcc.gnu.org/bugzilla/show_bug.cgi?id=102051)
# we allow gcc 13 here anyway because CI uses it for clang-tidy, and when
# the compiler crashes outright if won't produce any bad binaries either.
assert(
cxx.get_id() != 'gcc' or cxx.version().version_compare('>=13'),
'GCC 12 and earlier are known to miscompile lix coroutines, use GCC 13 or clang.'
'GCC is known to miscompile coroutines, use clang.'
)
if cxx.get_id() == 'gcc'
warning('GCC is known to crash while building coroutines, use clang.')
endif
# Translate some historical and Mesony CPU names to Lixy CPU names.
@ -229,6 +237,7 @@ configdata += {
}
boost = dependency('boost', required : true, modules : ['container'], include_type : 'system')
kj = dependency('kj-async', required : true, include_type : 'system')
# cpuid only makes sense on x86_64
cpuid_required = is_x64 ? get_option('cpuid') : false
@ -349,7 +358,6 @@ endif
#
# Build-time tools
#
coreutils = find_program('coreutils', native : true)
dot = find_program('dot', required : false, native : true)
pymod = import('python')
python = pymod.find_installation('python3')
@ -475,6 +483,7 @@ add_project_arguments(
# TODO(Qyriad): Yes this is how the autoconf+Make system did it.
# It would be nice for our headers to be idempotent instead.
'-include', 'config.h',
'-Wno-unused-parameter',
'-Wno-deprecated-declarations',
'-Wimplicit-fallthrough',
'-Werror=switch',
@ -483,12 +492,6 @@ add_project_arguments(
'-Wdeprecated-copy',
'-Wignored-qualifiers',
'-Werror=suggest-override',
# Enable assertions in libstdc++ by default. Harmless on libc++. Benchmarked
# at ~1% overhead in `nix search`.
#
# FIXME: remove when we get meson 1.4.0 which will default this to on for us:
# https://mesonbuild.com/Release-notes-for-1-4-0.html#ndebug-setting-now-controls-c-stdlib-assertions
'-D_GLIBCXX_ASSERTIONS=1',
language : 'cpp',
)
@ -584,10 +587,10 @@ run_command(
)
if is_darwin
configure_file(
input : 'misc/launchd/org.nixos.nix-daemon.plist.in',
output : 'org.nixos.nix-daemon.plist',
copy : true,
fs.copyfile(
'misc/launchd/org.nixos.nix-daemon.plist.in',
'org.nixos.nix-daemon.plist',
install : true,
install_dir : prefix / 'Library/LaunchDaemons',
)
endif
@ -603,6 +606,7 @@ endif
if enable_tests
subdir('tests/unit')
subdir('tests/functional')
subdir('tests/functional2')
endif
subdir('meson/clang-tidy')

View file

@ -1,8 +1,7 @@
configure_file(
input : 'completion.sh',
output : 'nix',
fs.copyfile(
'completion.sh',
'nix',
install : true,
install_dir : datadir / 'bash-completion/completions',
install_mode : 'rw-r--r--',
copy : true,
)

60
misc/capnproto.nix Normal file
View file

@ -0,0 +1,60 @@
# FIXME: upstream to nixpkgs (do NOT build with gcc due to gcc coroutine bugs)
{
lib,
stdenv,
fetchFromGitHub,
cmake,
openssl,
zlib,
}:
assert stdenv.cc.isClang;
stdenv.mkDerivation rec {
pname = "capnproto";
version = "1.0.2";
# release tarballs are missing some ekam rules
src = fetchFromGitHub {
owner = "capnproto";
repo = "capnproto";
rev = "v${version}";
sha256 = "sha256-LVdkqVBTeh8JZ1McdVNtRcnFVwEJRNjt0JV2l7RkuO8=";
};
nativeBuildInputs = [ cmake ];
propagatedBuildInputs = [
openssl
zlib
];
# FIXME: separate the binaries from the stuff that user systems actually use
# This runs into a terrible UX issue in Lix and I just don't want to debug it
# right now for the couple MB of closure size:
# https://git.lix.systems/lix-project/lix/issues/551
# outputs = [ "bin" "dev" "out" ];
cmakeFlags = [
(lib.cmakeBool "BUILD_SHARED_LIBS" true)
# Take optimization flags from CXXFLAGS rather than cmake injecting them
(lib.cmakeFeature "CMAKE_BUILD_TYPE" "None")
];
env = {
# Required to build the coroutine library
CXXFLAGS = "-std=c++20";
};
separateDebugInfo = true;
meta = with lib; {
homepage = "https://capnproto.org/";
description = "Cap'n Proto cerealization protocol";
longDescription = ''
Capn Proto is an insanely fast data interchange format and
capability-based RPC system. Think JSON, except binary. Or think Protocol
Buffers, except faster.
'';
license = licenses.mit;
platforms = platforms.all;
maintainers = lib.teams.lix.members;
};
}

View file

@ -14,7 +14,7 @@ function _nix_complete
# But the variable also misses the current token so it cancels out.
set -l nix_arg_to_complete (count $nix_args)
env NIX_GET_COMPLETIONS=$nix_arg_to_complete $nix_args $current_token
env NIX_GET_COMPLETIONS=$nix_arg_to_complete $nix_args $current_token 2>/dev/null
end
function _nix_accepts_files

View file

@ -1,8 +1,7 @@
configure_file(
input : 'completion.fish',
output : 'nix.fish',
fs.copyfile(
'completion.fish',
'nix.fish',
install : true,
install_dir : datadir / 'fish/vendor_completions.d',
install_mode : 'rw-r--r--',
copy : true,
)

View file

@ -5,8 +5,4 @@ subdir('zsh')
subdir('systemd')
subdir('flake-registry')
runinpty = configure_file(
copy : true,
input : meson.current_source_dir() / 'runinpty.py',
output : 'runinpty.py',
)
runinpty = fs.copyfile('runinpty.py')

View file

@ -1,10 +1,9 @@
foreach script : [ [ 'completion.zsh', '_nix' ], [ 'run-help-nix' ] ]
configure_file(
input : script[0],
output : script.get(1, script[0]),
fs.copyfile(
script[0],
script.get(1, script[0]),
install : true,
install_dir : datadir / 'zsh/site-functions',
install_mode : 'rw-r--r--',
copy : true,
)
endforeach

106
nix-support/editline.patch Normal file
View file

@ -0,0 +1,106 @@
From d0f2a5bc2300b96b2434c7838184c1dfd6a639f5 Mon Sep 17 00:00:00 2001
From: Rebecca Turner <rbt@sent.as>
Date: Sun, 8 Sep 2024 15:42:42 -0700
Subject: [PATCH 1/2] Recognize Meta+Left and Meta+Right
Recognize `Alt-Left` and `Alt-Right` for navigating by words in more
terminals/shells/platforms.
I'm not sure exactly where to find canonical documentation for these
codes, but this seems to match what my terminal produces (macOS + iTerm2
+ Fish + Tmux).
It might also be nice to have some more support for editing the bindings
for these characters; sequences of more than one character are not
supported by `el_bind_key` and similar.
Originally from: https://github.com/troglobit/editline/pull/70
This patch is applied upstream: https://gerrit.lix.systems/c/lix/+/1883
---
src/editline.c | 29 +++++++++++++++++++++++++++--
1 file changed, 27 insertions(+), 2 deletions(-)
diff --git a/src/editline.c b/src/editline.c
index 5ec9afb..d1cfbbc 100644
--- a/src/editline.c
+++ b/src/editline.c
@@ -1034,6 +1034,30 @@ static el_status_t meta(void)
return CSeof;
#ifdef CONFIG_ANSI_ARROWS
+ /* See: https://en.wikipedia.org/wiki/ANSI_escape_code */
+ /* Recognize ANSI escapes for `Meta+Left` and `Meta+Right`. */
+ if (c == '\e') {
+ switch (tty_get()) {
+ case '[':
+ {
+ switch (tty_get()) {
+ /* \e\e[C = Meta+Left */
+ case 'C': return fd_word();
+ /* \e\e[D = Meta+Right */
+ case 'D': return bk_word();
+ default:
+ break;
+ }
+
+ return el_ring_bell();
+ }
+ default:
+ break;
+ }
+
+ return el_ring_bell();
+ }
+
/* Also include VT-100 arrows. */
if (c == '[' || c == 'O') {
switch (tty_get()) {
@@ -1043,6 +1067,7 @@ static el_status_t meta(void)
char seq[4] = { 0 };
seq[0] = tty_get();
+ /* \e[1~ */
if (seq[0] == '~')
return beg_line(); /* Home */
@@ -1050,9 +1075,9 @@ static el_status_t meta(void)
seq[c] = tty_get();
if (!strncmp(seq, ";5C", 3))
- return fd_word(); /* Ctrl+Right */
+ return fd_word(); /* \e[1;5C = Ctrl+Right */
if (!strncmp(seq, ";5D", 3))
- return bk_word(); /* Ctrl+Left */
+ return bk_word(); /* \e[1;5D = Ctrl+Left */
break;
}
From 4c4455353a0a88bee09d5f27c28f81f747682fed Mon Sep 17 00:00:00 2001
From: Rebecca Turner <rbt@sent.as>
Date: Mon, 9 Sep 2024 09:44:44 -0700
Subject: [PATCH 2/2] Add support for \e[1;3C and \e[1;3D
---
src/editline.c | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/src/editline.c b/src/editline.c
index d1cfbbc..350b5cb 100644
--- a/src/editline.c
+++ b/src/editline.c
@@ -1074,9 +1074,11 @@ static el_status_t meta(void)
for (c = 1; c < 3; c++)
seq[c] = tty_get();
- if (!strncmp(seq, ";5C", 3))
+ if (!strncmp(seq, ";5C", 3)
+ || !strncmp(seq, ";3C", 3))
return fd_word(); /* \e[1;5C = Ctrl+Right */
- if (!strncmp(seq, ";5D", 3))
+ if (!strncmp(seq, ";5D", 3)
+ || !strncmp(seq, ";3D", 3))
return bk_word(); /* \e[1;5D = Ctrl+Left */
break;

View file

@ -15,6 +15,7 @@
brotli,
bzip2,
callPackage,
capnproto-lix ? __forDefaults.capnproto-lix,
cmake,
curl,
doxygen,
@ -30,12 +31,15 @@
lix-clang-tidy ? null,
llvmPackages,
lsof,
# FIXME: remove default after dropping NixOS 24.05
lowdown-unsandboxed ? lowdown,
lowdown,
mdbook,
mdbook-linkcheck,
mercurial,
meson,
ninja,
ncurses,
openssl,
pegtl,
pkg-config,
@ -79,12 +83,37 @@
boehmgc-nix = boehmgc.override { enableLargeConfig = true; };
editline-lix = editline.overrideAttrs (prev: {
configureFlags = prev.configureFlags or [ ] ++ [ (lib.enableFeature true "sigstop") ];
patches = (prev.patches or [ ]) ++ [
# Recognize `Alt-Left` and `Alt-Right` for navigating by words in more
# terminals/shells/platforms.
#
# See: https://github.com/troglobit/editline/pull/70
./nix-support/editline.patch
];
configureFlags = (prev.configureFlags or [ ]) ++ [
# Enable SIGSTOP (Ctrl-Z) behavior.
(lib.enableFeature true "sigstop")
# Enable ANSI arrow keys.
(lib.enableFeature true "arrow-keys")
# Use termcap library to query terminal size.
(lib.enableFeature (ncurses != null) "termcap")
];
buildInputs = (prev.buildInputs or [ ]) ++ [ ncurses ];
});
build-release-notes = callPackage ./maintainers/build-release-notes.nix { };
# needs derivation patching to add debuginfo and coroutine library support
# !! must build this with clang as it is affected by the gcc coroutine bugs
capnproto-lix = callPackage ./misc/capnproto.nix { inherit stdenv; };
},
}:
# gcc miscompiles coroutines at least until 13.2, possibly longer
assert stdenv.cc.isClang;
let
inherit (__forDefaults) canRunInstalled;
inherit (lib) fileset;
@ -143,6 +172,7 @@ let
functionalTestFiles = fileset.unions [
./tests/functional
./tests/functional2
./tests/unit
(fileset.fileFilter (f: lib.strings.hasPrefix "nix-profile" f.name) ./scripts)
];
@ -216,13 +246,16 @@ stdenv.mkDerivation (finalAttrs: {
nativeBuildInputs =
[
python3
python3.pkgs.pytest
python3.pkgs.pytest-xdist
meson
ninja
cmake
rustc
capnproto-lix
]
++ [
(lib.getBin lowdown)
(lib.getBin lowdown-unsandboxed)
mdbook
mdbook-linkcheck
]
@ -241,6 +274,10 @@ stdenv.mkDerivation (finalAttrs: {
++ lib.optionals lintInsteadOfBuild [
# required for a wrapped clang-tidy
llvmPackages.clang-tools
# load-bearing order (just as below); the actual stdenv wrapped clang
# needs to precede the unwrapped clang in PATH such that calling `clang`
# can compile things.
stdenv.cc
# required for run-clang-tidy
llvmPackages.clang-unwrapped
];
@ -260,6 +297,7 @@ stdenv.mkDerivation (finalAttrs: {
libsodium
toml11
pegtl
capnproto-lix
]
++ lib.optionals hostPlatform.isLinux [
libseccomp
@ -411,6 +449,7 @@ stdenv.mkDerivation (finalAttrs: {
editline-lix
build-release-notes
pegtl
capnproto-lix
;
# The collection of dependency logic for this derivation is complicated enough that
@ -445,6 +484,11 @@ stdenv.mkDerivation (finalAttrs: {
pythonPackages = (
p: [
# FIXME: these have to be added twice due to the nix shell using a
# wrapped python instead of build inputs for its python inputs
p.pytest
p.pytest-xdist
p.yapf
p.python-frontmatter
p.requests

View file

@ -8,12 +8,7 @@ configure_file(
}
)
# https://github.com/mesonbuild/meson/issues/860
configure_file(
input : 'nix-profile.sh.in',
output : 'nix-profile.sh.in',
copy : true,
)
fs.copyfile('nix-profile.sh.in')
foreach rc : [ '.sh', '.fish', '-daemon.sh', '-daemon.fish' ]
configure_file(

View file

@ -19,8 +19,9 @@
#include "legacy.hh"
#include "experimental-features.hh"
#include "hash.hh"
#include "build-remote.hh"
using namespace nix;
namespace nix {
static void handleAlarm(int sig) {
}
@ -388,4 +389,8 @@ connected:
}
}
static RegisterLegacyCommand r_build_remote("build-remote", main_build_remote);
void registerBuildRemote() {
LegacyCommands::add("build-remote", main_build_remote);
}
}

View file

@ -0,0 +1,8 @@
#pragma once
/// @file
namespace nix {
void registerBuildRemote();
}

35
src/legacy/meson.build Normal file
View file

@ -0,0 +1,35 @@
legacy_include_directories = include_directories('.')
legacy_sources = files(
# `build-remote` is not really legacy (it powers all remote builds), but it's
# not a `nix3` command.
'build-remote.cc',
'dotgraph.cc',
'graphml.cc',
'nix-build.cc',
'nix-channel.cc',
'nix-collect-garbage.cc',
'nix-copy-closure.cc',
'nix-env.cc',
'nix-env.hh',
'nix-instantiate.cc',
'nix-store.cc',
'user-env.cc',
)
legacy_headers = files(
'build-remote.hh',
'nix-build.hh',
'nix-channel.hh',
'nix-collect-garbage.hh',
'nix-copy-closure.hh',
'nix-instantiate.hh',
'nix-store.hh',
)
legacy_generated_headers = [
gen_header.process('buildenv.nix', preserve_path_from: meson.current_source_dir()),
gen_header.process('unpack-channel.nix', preserve_path_from: meson.current_source_dir()),
]
fs.copyfile('unpack-channel.nix')

View file

@ -24,12 +24,14 @@
#include "attr-path.hh"
#include "legacy.hh"
#include "shlex.hh"
#include "nix-build.hh"
extern char * * environ __attribute__((weak)); // Man what even is this
namespace nix {
using namespace nix;
using namespace std::string_literals;
extern char * * environ __attribute__((weak));
static void main_nix_build(int argc, char * * argv)
{
auto dryRun = false;
@ -488,42 +490,51 @@ static void main_nix_build(int argc, char * * argv)
environment variables and shell functions. Also don't
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%; }; )"s +
(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;") +
"%2%"
// always clear PATH.
// when nix-shell is run impure, we rehydrate it with the `p=$PATH` above
"unset PATH;"
"dontAddDisableDepTrack=1;\n"
+ structuredAttrsRC +
"\n[ -e $stdenv/setup ] && source $stdenv/setup; "
"%3%"
"PATH=%4%:\"$PATH\"; "
"SHELL=%5%; "
"BASH=%5%; "
"set +e; "
R"s([ -n "$PS1" -a -z "$NIX_SHELL_PRESERVE_PROMPT" ] && )s" +
(getuid() == 0 ? R"s(PS1='\n\[\033[1;31m\][nix-shell:\w]\$\[\033[0m\] '; )s"
: R"s(PS1='\n\[\033[1;32m\][nix-shell:\w]\$\[\033[0m\] '; )s") +
"if [ \"$(type -t runHook)\" = function ]; then runHook shellHook; fi; "
"unset NIX_ENFORCE_PURITY; "
"shopt -u nullglob; "
"unset TZ; %6%"
"shopt -s execfail;"
"%7%",
shellEscape(tmpDir),
(pure ? "" : "p=$PATH; "),
(pure ? "" : "PATH=$PATH:$p; unset p; "),
shellEscape(dirOf(*shell)),
shellEscape(*shell),
(getenv("TZ") ? (std::string("export TZ=") + shellEscape(getenv("TZ")) + "; ") : ""),
envCommand);
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; ")
);
rc += structuredAttrsRC;
rc += fmt(
"\n[ -e $stdenv/setup ] && source $stdenv/setup; "
"%1%"
"PATH=%2%:\"$PATH\"; "
"SHELL=%3%; "
"BASH=%3%; "
"set +e; "
R"s([ -n "$PS1" -a -z "$NIX_SHELL_PRESERVE_PROMPT" ] && )s"
"%4%"
"if [ \"$(type -t runHook)\" = function ]; then runHook shellHook; fi; "
"unset NIX_ENFORCE_PURITY; "
"shopt -u nullglob; "
"unset TZ; %5%"
"shopt -s execfail;"
"%6%",
(pure ? "" : "PATH=$PATH:$p; unset p; "),
shellEscape(dirOf(*shell)),
shellEscape(*shell),
(getuid() == 0 ? R"s(PS1='\n\[\033[1;31m\][nix-shell:\w]\$\[\033[0m\] '; )s"
: R"s(PS1='\n\[\033[1;32m\][nix-shell:\w]\$\[\033[0m\] '; )s"),
(tz.has_value()
? (std::string("export TZ=") + shellEscape(*tz) + "; ")
: ""),
envCommand
);
vomit("Sourcing nix-shell with file %s and contents:\n%s", rcfile, rc);
writeFile(rcfile, rc);
@ -613,5 +624,9 @@ static void main_nix_build(int argc, char * * argv)
}
}
static RegisterLegacyCommand r_nix_build("nix-build", main_nix_build);
static RegisterLegacyCommand r_nix_shell("nix-shell", main_nix_build);
void registerNixBuildAndNixShell() {
LegacyCommands::add("nix-build", main_nix_build);
LegacyCommands::add("nix-shell", main_nix_build);
}
}

8
src/legacy/nix-build.hh Normal file
View file

@ -0,0 +1,8 @@
#pragma once
/// @file
namespace nix {
void registerNixBuildAndNixShell();
}

View file

@ -7,12 +7,13 @@
#include "fetchers.hh"
#include "eval-settings.hh" // for defexpr
#include "users.hh"
#include "nix-channel.hh"
#include <fcntl.h>
#include <regex>
#include <pwd.h>
using namespace nix;
namespace nix {
typedef std::map<std::string, std::string> Channels;
@ -264,4 +265,8 @@ static int main_nix_channel(int argc, char ** argv)
}
}
static RegisterLegacyCommand r_nix_channel("nix-channel", main_nix_channel);
void registerNixChannel() {
LegacyCommands::add("nix-channel", main_nix_channel);
}
}

View file

@ -0,0 +1,8 @@
#pragma once
/// @file
namespace nix {
void registerNixChannel();
}

View file

@ -7,11 +7,12 @@
#include "globals.hh"
#include "legacy.hh"
#include "signals.hh"
#include "nix-collect-garbage.hh"
#include <iostream>
#include <cerrno>
using namespace nix;
namespace nix {
std::string deleteOlderThan;
bool dryRun = false;
@ -110,4 +111,8 @@ static int main_nix_collect_garbage(int argc, char * * argv)
}
}
static RegisterLegacyCommand r_nix_collect_garbage("nix-collect-garbage", main_nix_collect_garbage);
void registerNixCollectGarbage() {
LegacyCommands::add("nix-collect-garbage", main_nix_collect_garbage);
}
}

View file

@ -0,0 +1,8 @@
#pragma once
/// @file
namespace nix {
void registerNixCollectGarbage();
}

View file

@ -1,8 +1,9 @@
#include "shared.hh"
#include "store-api.hh"
#include "legacy.hh"
#include "nix-copy-closure.hh"
using namespace nix;
namespace nix {
static int main_nix_copy_closure(int argc, char ** argv)
{
@ -60,4 +61,8 @@ static int main_nix_copy_closure(int argc, char ** argv)
}
}
static RegisterLegacyCommand r_nix_copy_closure("nix-copy-closure", main_nix_copy_closure);
void registerNixCopyClosure() {
LegacyCommands::add("nix-copy-closure", main_nix_copy_closure);
}
}

View file

@ -0,0 +1,8 @@
#pragma once
/// @file
namespace nix {
void registerNixCopyClosure();
}

View file

@ -17,6 +17,7 @@
#include "xml-writer.hh"
#include "legacy.hh"
#include "eval-settings.hh" // for defexpr
#include "nix-env.hh"
#include <ctime>
#include <algorithm>
@ -28,9 +29,10 @@
#include <unistd.h>
#include <nlohmann/json.hpp>
using namespace nix;
using std::cout;
namespace nix {
typedef enum {
srcNixExprDrvs,
@ -1544,4 +1546,8 @@ static int main_nix_env(int argc, char * * argv)
}
}
static RegisterLegacyCommand r_nix_env("nix-env", main_nix_env);
void registerNixEnv() {
LegacyCommands::add("nix-env", main_nix_env);
}
}

8
src/legacy/nix-env.hh Normal file
View file

@ -0,0 +1,8 @@
#pragma once
/// @file
namespace nix {
void registerNixEnv();
}

View file

@ -11,12 +11,13 @@
#include "local-fs-store.hh"
#include "common-eval-args.hh"
#include "legacy.hh"
#include "nix-instantiate.hh"
#include <map>
#include <iostream>
using namespace nix;
namespace nix {
static Path gcRoot;
@ -195,4 +196,8 @@ static int main_nix_instantiate(int argc, char * * argv)
}
}
static RegisterLegacyCommand r_nix_instantiate("nix-instantiate", main_nix_instantiate);
void registerNixInstantiate() {
LegacyCommands::add("nix-instantiate", main_nix_instantiate);
}
}

View file

@ -0,0 +1,8 @@
#pragma once
/// @file
namespace nix {
void registerNixInstantiate();
}

View file

@ -15,6 +15,7 @@
#include "graphml.hh"
#include "legacy.hh"
#include "path-with-outputs.hh"
#include "nix-store.hh"
#include <iostream>
#include <algorithm>
@ -24,10 +25,9 @@
#include <fcntl.h>
namespace nix_store {
namespace nix {
using namespace nix;
using std::cin;
using std::cout;
@ -831,12 +831,12 @@ static void opServe(Strings opFlags, Strings opArgs)
// FIXME: changing options here doesn't work if we're
// building through the daemon.
verbosity = lvlError;
settings.keepLog = false;
settings.useSubstitutes = false;
settings.maxSilentTime = readInt(in);
settings.buildTimeout = readInt(in);
settings.keepLog.override(false);
settings.useSubstitutes.override(false);
settings.maxSilentTime.override(readInt(in));
settings.buildTimeout.override(readInt(in));
if (GET_PROTOCOL_MINOR(clientVersion) >= 2)
settings.maxLogSize = readNum<unsigned long>(in);
settings.maxLogSize.override(readNum<unsigned long>(in));
if (GET_PROTOCOL_MINOR(clientVersion) >= 3) {
auto nrRepeats = readInt(in);
if (nrRepeats != 0) {
@ -850,10 +850,10 @@ static void opServe(Strings opFlags, Strings opArgs)
// asked for.
readInt(in);
settings.runDiffHook = true;
settings.runDiffHook.override(true);
}
if (GET_PROTOCOL_MINOR(clientVersion) >= 7) {
settings.keepFailed = (bool) readInt(in);
settings.keepFailed.override((bool) readInt(in));
}
};
@ -1176,6 +1176,8 @@ static int main_nix_store(int argc, char * * argv)
}
}
static RegisterLegacyCommand r_nix_store("nix-store", main_nix_store);
void registerNixStore() {
LegacyCommands::add("nix-store", main_nix_store);
}
}

8
src/legacy/nix-store.hh Normal file
View file

@ -0,0 +1,8 @@
#pragma once
/// @file
namespace nix {
void registerNixStore();
}

View file

@ -9,8 +9,24 @@
#include "store-api.hh"
#include "command.hh"
#include <regex>
namespace nix {
static std::regex const identifierRegex("^[A-Za-z_][A-Za-z0-9_'-]*$");
static void warnInvalidNixIdentifier(const std::string & name)
{
std::smatch match;
if (!std::regex_match(name, match, identifierRegex)) {
warn("This Nix invocation specifies a value for argument '%s' which isn't a valid \
Nix identifier. The project is considering to drop support for this \
or to require quotes around args that aren't valid Nix identifiers. \
If you depend on this behvior, please reach out in \
https://git.lix.systems/lix-project/lix/issues/496 so we can discuss \
your use-case.", name);
}
}
MixEvalArgs::MixEvalArgs()
{
addFlag({
@ -18,7 +34,10 @@ MixEvalArgs::MixEvalArgs()
.description = "Pass the value *expr* as the argument *name* to Nix functions.",
.category = category,
.labels = {"name", "expr"},
.handler = {[&](std::string name, std::string expr) { autoArgs[name] = 'E' + expr; }}
.handler = {[&](std::string name, std::string expr) {
warnInvalidNixIdentifier(name);
autoArgs[name] = 'E' + expr;
}}
});
addFlag({
@ -26,7 +45,10 @@ MixEvalArgs::MixEvalArgs()
.description = "Pass the string *string* as the argument *name* to Nix functions.",
.category = category,
.labels = {"name", "string"},
.handler = {[&](std::string name, std::string s) { autoArgs[name] = 'S' + s; }},
.handler = {[&](std::string name, std::string s) {
warnInvalidNixIdentifier(name);
autoArgs[name] = 'S' + s;
}},
});
addFlag({
@ -115,7 +137,7 @@ MixEvalArgs::MixEvalArgs()
.description = "Allow access to mutable paths and repositories.",
.category = category,
.handler = {[&]() {
evalSettings.pureEval = false;
evalSettings.pureEval.override(false);
}},
});

View file

@ -212,7 +212,7 @@ void SourceExprCommand::completeInstallable(AddCompletions & completions, std::s
if (file) {
completions.setType(AddCompletions::Type::Attrs);
evalSettings.pureEval = false;
evalSettings.pureEval.override(false);
auto state = getEvalState();
Expr & e = state->parseExprFromFile(
resolveExprPath(state->checkSourcePath(lookupFileArg(*state, *file)))
@ -435,7 +435,7 @@ Installables SourceExprCommand::parseInstallables(
throw UsageError("'--file' and '--expr' are exclusive");
// FIXME: backward compatibility hack
if (file) evalSettings.pureEval = false;
if (file) evalSettings.pureEval.override(false);
auto state = getEvalState();
auto vFile = state->allocValue();

View file

@ -2,6 +2,6 @@
namespace nix {
RegisterLegacyCommand::Commands * RegisterLegacyCommand::commands = 0;
LegacyCommands::Commands * LegacyCommands::commands = 0;
}

View file

@ -9,12 +9,12 @@ namespace nix {
typedef std::function<void(int, char * *)> MainFunction;
struct RegisterLegacyCommand
struct LegacyCommands
{
typedef std::map<std::string, MainFunction> Commands;
static Commands * commands;
RegisterLegacyCommand(const std::string & name, MainFunction fun)
static void add(const std::string & name, MainFunction fun)
{
if (!commands) commands = new Commands;
(*commands)[name] = fun;

View file

@ -8,10 +8,6 @@
#include <string_view>
#include <cerrno>
#ifdef READLINE
#include <readline/history.h>
#include <readline/readline.h>
#else
// editline < 1.15.2 don't wrap their API for C++ usage
// (added in https://github.com/troglobit/editline/commit/91398ceb3427b730995357e9d120539fb9bb7461).
// This results in linker errors due to to name-mangling of editline C symbols.
@ -20,7 +16,6 @@
extern "C" {
#include <editline.h>
}
#endif
#include "finally.hh"
#include "repl-interacter.hh"
@ -115,17 +110,13 @@ ReadlineLikeInteracter::Guard ReadlineLikeInteracter::init(detail::ReplCompleter
} catch (SysError & e) {
logWarning(e.info());
}
#ifndef READLINE
el_hist_size = 1000;
#endif
read_history(historyFile.c_str());
auto oldRepl = curRepl;
curRepl = repl;
Guard restoreRepl([oldRepl] { curRepl = oldRepl; });
#ifndef READLINE
rl_set_complete_func(completionCallback);
rl_set_list_possib_func(listPossibleCallback);
#endif
return restoreRepl;
}

View file

@ -817,10 +817,10 @@ ProcessLineResult NixRepl::processLine(std::string line)
else if (command == ":te" || command == ":trace-enable") {
if (arg == "false" || (arg == "" && loggerSettings.showTrace)) {
std::cout << "not showing error traces\n";
loggerSettings.showTrace = false;
loggerSettings.showTrace.override(false);
} else if (arg == "true" || (arg == "" && !loggerSettings.showTrace)) {
std::cout << "showing error traces\n";
loggerSettings.showTrace = true;
loggerSettings.showTrace.override(true);
} else {
throw Error("unexpected argument '%s' to %s", arg, command);
};

View file

@ -79,7 +79,7 @@ struct AttrDb
state->txn->commit();
state->txn.reset();
} catch (...) {
ignoreException();
ignoreExceptionInDestructor();
}
}
@ -90,7 +90,7 @@ struct AttrDb
try {
return fun();
} catch (SQLiteError &) {
ignoreException();
ignoreExceptionExceptInterrupt();
failed = true;
return 0;
}
@ -329,7 +329,7 @@ static std::shared_ptr<AttrDb> makeAttrDb(
try {
return std::make_shared<AttrDb>(cfg, fingerprint, symbols);
} catch (SQLiteError &) {
ignoreException();
ignoreExceptionExceptInterrupt();
return nullptr;
}
}

View file

@ -47,7 +47,7 @@ static Strings parseNixPath(const std::string & s)
EvalSettings::EvalSettings()
{
auto var = getEnv("NIX_PATH");
if (var) nixPath = parseNixPath(*var);
if (var) nixPath.setDefault(parseNixPath(*var));
}
Strings EvalSettings::getDefaultNixPath()

View file

@ -185,6 +185,54 @@ struct EvalSettings : Config
else
{ }
```
Here's a more elaborate `repl-overlay`, which provides the following
variables:
- The original, unmodified variables are aliased to `original`.
- `legacyPackages.${system}` (if it exists) or `packages.${system}`
(otherwise) is aliased to `pkgs`.
- All attribute set variables with a `${system}` attribute are
abbreviated in the same manner; e.g. `devShells.${system}` is
shortened to `devShells`.
For example, the following attribute set:
```nix
info: final: attrs: let
# Equivalent to nixpkgs `lib.optionalAttrs`.
optionalAttrs = predicate: attrs:
if predicate
then attrs
else {};
# If `attrs.${oldName}.${info.currentSystem}` exists, alias `${newName}` to
# it.
collapseRenamed = oldName: newName:
optionalAttrs (builtins.hasAttr oldName attrs
&& builtins.hasAttr info.currentSystem attrs.${oldName})
{
${newName} = attrs.${oldName}.${info.currentSystem};
};
# Alias `attrs.${oldName}.${info.currentSystem} to `${newName}`.
collapse = name: collapseRenamed name name;
# Alias all `attrs` keys with an `${info.currentSystem}` attribute.
collapseAll =
builtins.foldl'
(prev: name: prev // collapse name)
{}
(builtins.attrNames attrs);
in
# Preserve the original bindings as `original`.
(optionalAttrs (! attrs ? original)
{
original = attrs;
})
// (collapseRenamed "packages" "pkgs")
// (collapseRenamed "legacyPackages" "pkgs")
// collapseAll
```
)"};
};

View file

@ -21,6 +21,14 @@ std::ostream & operator <<(std::ostream & str, const SymbolStr & symbol)
return printIdentifier(str, s);
}
AttrName::AttrName(Symbol s) : symbol(s)
{
}
AttrName::AttrName(std::unique_ptr<Expr> e) : expr(std::move(e))
{
}
void Expr::show(const SymbolTable & symbols, std::ostream & str) const
{
abort();
@ -239,9 +247,24 @@ void ExprConcatStrings::show(const SymbolTable & symbols, std::ostream & str) co
{
bool first = true;
str << "(";
for (auto & i : es) {
if (first) first = false; else str << " + ";
i.second->show(symbols, str);
for (auto & [_pos, part] : es) {
if (first)
first = false;
else
str << " + ";
if (forceString && !dynamic_cast<ExprString *>(part.get())) {
/* Print as a string with an interpolation, to preserve the
* semantics of the value having to be a string.
* Interpolations are weird and someone should eventually
* move them out into their own AST node please.
*/
str << "\"${";
part->show(symbols, str);
str << "}\"";
} else {
part->show(symbols, str);
}
}
str << ")";
}

View file

@ -30,8 +30,8 @@ struct AttrName
{
Symbol symbol;
std::unique_ptr<Expr> expr;
AttrName(Symbol s) : symbol(s) {};
AttrName(std::unique_ptr<Expr> e) : expr(std::move(e)) {};
AttrName(Symbol s);
AttrName(std::unique_ptr<Expr> e);
};
typedef std::vector<AttrName> AttrPath;

View file

@ -12,7 +12,7 @@
// eolf rules in favor of reproducing the old flex lexer as faithfully as
// possible, and deferring calculation of positions to downstream users.
namespace nix::parser::grammar {
namespace nix::parser::grammar::v1 {
using namespace tao::pegtl;
namespace p = tao::pegtl;
@ -225,7 +225,8 @@ struct string : _string, seq<
> {};
struct _ind_string {
template<bool Indented, typename... Inner>
struct line_start : semantic, star<one<' '>> {};
template<typename... Inner>
struct literal : semantic, seq<Inner...> {};
struct interpolation : semantic, seq<
p::string<'$', '{'>, seps,
@ -233,34 +234,53 @@ struct _ind_string {
must<one<'}'>>
> {};
struct escape : semantic, must<any> {};
/* Marker for non-empty lines */
struct has_content : semantic, seq<> {};
};
struct ind_string : _ind_string, seq<
TAO_PEGTL_STRING("''"),
// Strip first line completely if empty
opt<star<one<' '>>, one<'\n'>>,
star<
sor<
_ind_string::literal<
true,
list<
seq<
// Start a line with some indentation
// (we always match even the empty string if no indentation, as this creates the line)
_ind_string::line_start,
// The actual line
opt<
plus<
sor<
not_one<'$', '\''>,
seq<one<'$'>, not_one<'{', '\''>>,
seq<one<'\''>, not_one<'\'', '$'>>
>
>
>,
_ind_string::interpolation,
_ind_string::literal<false, one<'$'>>,
_ind_string::literal<false, one<'\''>, not_at<one<'\''>>>,
seq<one<'\''>, _ind_string::literal<false, p::string<'\'', '\''>>>,
seq<
p::string<'\'', '\''>,
sor<
_ind_string::literal<false, one<'$'>>,
seq<one<'\\'>, _ind_string::escape>
_ind_string::literal<
plus<
sor<
not_one<'$', '\'', '\n'>,
// TODO probably factor this out like the others for performance
seq<one<'$'>, not_one<'{', '\'', '\n'>>,
seq<one<'$'>, at<one<'\n'>>>,
seq<one<'\''>, not_one<'\'', '$', '\n'>>,
seq<one<'\''>, at<one<'\n'>>>
>
>
>,
_ind_string::interpolation,
_ind_string::literal<one<'$'>>,
_ind_string::literal<one<'\''>, not_at<one<'\''>>>,
seq<one<'\''>, _ind_string::literal<p::string<'\'', '\''>>>,
seq<
p::string<'\'', '\''>,
sor<
_ind_string::literal<one<'$'>>,
seq<one<'\\'>, _ind_string::escape>
>
>
>,
_ind_string::has_content
>
>
>
>,
// End of line, LF. CR is just ignored and not treated as ending a line
// (for the purpose of indentation stripping)
_ind_string::literal<one<'\n'>>
>,
must<TAO_PEGTL_STRING("''")>
> {};
@ -352,10 +372,10 @@ struct formals : semantic, _formals, seq<
struct _attr {
struct simple : semantic, sor<t::identifier, t::kw_or> {};
struct string : semantic, seq<grammar::string> {};
struct string : semantic, seq<grammar::v1::string> {};
struct expr : semantic, seq<
TAO_PEGTL_STRING("${"), seps,
must<grammar::expr>, seps,
must<grammar::v1::expr>, seps,
must<one<'}'>>
> {};
};
@ -452,9 +472,9 @@ struct _expr {
struct id : semantic, t::identifier {};
struct int_ : semantic, t::integer {};
struct float_ : semantic, t::floating {};
struct string : semantic, seq<grammar::string> {};
struct ind_string : semantic, seq<grammar::ind_string> {};
struct path : semantic, seq<grammar::path> {};
struct string : semantic, seq<grammar::v1::string> {};
struct ind_string : semantic, seq<grammar::v1::ind_string> {};
struct path : semantic, seq<grammar::v1::path> {};
struct uri : semantic, t::uri {};
struct ancient_let : semantic, _attrset<must, t::kw_let, seps> {};
struct rec_set : semantic, _attrset<must, t::kw_rec, seps> {};
@ -628,34 +648,34 @@ struct nothing : p::nothing<Rule> {
template<typename Self, typename OpCtx, typename AttrPathT, typename ExprT>
struct operator_semantics {
struct has_attr : grammar::op::has_attr {
struct has_attr : grammar::v1::op::has_attr {
AttrPathT path;
};
struct OpEntry {
OpCtx ctx;
uint8_t prec;
grammar::op::kind assoc;
grammar::v1::op::kind assoc;
std::variant<
grammar::op::not_,
grammar::op::unary_minus,
grammar::op::implies,
grammar::op::or_,
grammar::op::and_,
grammar::op::equals,
grammar::op::not_equals,
grammar::op::less_eq,
grammar::op::greater_eq,
grammar::op::update,
grammar::op::concat,
grammar::op::less,
grammar::op::greater,
grammar::op::plus,
grammar::op::minus,
grammar::op::mul,
grammar::op::div,
grammar::op::pipe_right,
grammar::op::pipe_left,
grammar::v1::op::not_,
grammar::v1::op::unary_minus,
grammar::v1::op::implies,
grammar::v1::op::or_,
grammar::v1::op::and_,
grammar::v1::op::equals,
grammar::v1::op::not_equals,
grammar::v1::op::less_eq,
grammar::v1::op::greater_eq,
grammar::v1::op::update,
grammar::v1::op::concat,
grammar::v1::op::less,
grammar::v1::op::greater,
grammar::v1::op::plus,
grammar::v1::op::minus,
grammar::v1::op::mul,
grammar::v1::op::div,
grammar::v1::op::pipe_right,
grammar::v1::op::pipe_left,
has_attr
> op;
};
@ -676,7 +696,7 @@ struct operator_semantics {
auto & [ctx, precedence, kind, op] = ops.back();
// NOTE this relies on associativity not being mixed within a precedence level.
if ((precedence > toPrecedence)
|| (kind != grammar::op::kind::leftAssoc && precedence == toPrecedence))
|| (kind != grammar::v1::op::kind::leftAssoc && precedence == toPrecedence))
break;
std::visit([&, ctx=std::move(ctx)] (auto & op) {
exprs.push_back(static_cast<Self &>(*this).applyOp(ctx, op, args...));
@ -694,9 +714,9 @@ struct operator_semantics {
void pushOp(OpCtx ctx, auto o, auto &... args)
{
if (o.kind != grammar::op::kind::unary)
if (o.kind != grammar::v1::op::kind::unary)
reduce(o.precedence, args...);
if (!ops.empty() && o.kind == grammar::op::kind::nonAssoc) {
if (!ops.empty() && o.kind == grammar::v1::op::kind::nonAssoc) {
auto & [_pos, _prec, _kind, _o] = ops.back();
if (_kind == o.kind && _prec == o.precedence)
Self::badOperator(ctx, args...);

View file

@ -0,0 +1,863 @@
// flip this define when doing parser development to enable some g checks.
#if 0
#include <tao/pegtl/contrib/analyze.hpp>
#define ANALYZE_GRAMMAR \
([] { \
const std::size_t issues = tao::pegtl::analyze<grammar::v1::root>(); \
assert(issues == 0); \
})()
#else
#define ANALYZE_GRAMMAR ((void) 0)
#endif
namespace p = tao::pegtl;
namespace nix::parser::v1 {
namespace {
template<typename>
inline constexpr const char * error_message = nullptr;
#define error_message_for(...) \
template<> inline constexpr auto error_message<__VA_ARGS__>
error_message_for(p::one<'{'>) = "expecting '{'";
error_message_for(p::one<'}'>) = "expecting '}'";
error_message_for(p::one<'"'>) = "expecting '\"'";
error_message_for(p::one<';'>) = "expecting ';'";
error_message_for(p::one<')'>) = "expecting ')'";
error_message_for(p::one<']'>) = "expecting ']'";
error_message_for(p::one<':'>) = "expecting ':'";
error_message_for(p::string<'\'', '\''>) = "expecting \"''\"";
error_message_for(p::any) = "expecting any character";
error_message_for(grammar::v1::eof) = "expecting end of file";
error_message_for(grammar::v1::seps) = "expecting separators";
error_message_for(grammar::v1::path::forbid_prefix_triple_slash) = "too many slashes in path";
error_message_for(grammar::v1::path::forbid_prefix_double_slash_no_interp) = "path has a trailing slash";
error_message_for(grammar::v1::expr) = "expecting expression";
error_message_for(grammar::v1::expr::unary) = "expecting expression";
error_message_for(grammar::v1::binding::equal) = "expecting '='";
error_message_for(grammar::v1::expr::lambda::arg) = "expecting identifier";
error_message_for(grammar::v1::formals) = "expecting formals";
error_message_for(grammar::v1::attrpath) = "expecting attribute path";
error_message_for(grammar::v1::expr::select) = "expecting selection expression";
error_message_for(grammar::v1::t::kw_then) = "expecting 'then'";
error_message_for(grammar::v1::t::kw_else) = "expecting 'else'";
error_message_for(grammar::v1::t::kw_in) = "expecting 'in'";
struct SyntaxErrors
{
template<typename Rule>
static constexpr auto message = error_message<Rule>;
template<typename Rule>
static constexpr bool raise_on_failure = false;
};
template<typename Rule>
struct Control : p::must_if<SyntaxErrors>::control<Rule>
{
template<typename ParseInput, typename... States>
[[noreturn]] static void raise(const ParseInput & in, States &&... st)
{
if (in.empty()) {
std::string expected;
if constexpr (constexpr auto msg = error_message<Rule>)
expected = fmt(", %s", msg);
throw p::parse_error("unexpected end of file" + expected, in);
}
p::must_if<SyntaxErrors>::control<Rule>::raise(in, st...);
}
};
struct ExprState
: grammar::v1::
operator_semantics<ExprState, PosIdx, AttrPath, std::pair<PosIdx, std::unique_ptr<Expr>>>
{
std::unique_ptr<Expr> popExprOnly() {
return std::move(popExpr().second);
}
template<typename Op, typename... Args>
std::unique_ptr<Expr> applyUnary(Args &&... args) {
return std::make_unique<Op>(popExprOnly(), std::forward<Args>(args)...);
}
template<typename Op>
std::unique_ptr<Expr> applyBinary(PosIdx pos) {
auto right = popExprOnly(), left = popExprOnly();
return std::make_unique<Op>(pos, std::move(left), std::move(right));
}
std::unique_ptr<Expr> call(PosIdx pos, Symbol fn, bool flip = false)
{
std::vector<std::unique_ptr<Expr>> args(2);
args[flip ? 0 : 1] = popExprOnly();
args[flip ? 1 : 0] = popExprOnly();
return std::make_unique<ExprCall>(pos, std::make_unique<ExprVar>(fn), std::move(args));
}
std::unique_ptr<Expr> pipe(PosIdx pos, State & state, bool flip = false)
{
if (!state.featureSettings.isEnabled(Xp::PipeOperator))
throw ParseError({
.msg = HintFmt("Pipe operator is disabled"),
.pos = state.positions[pos]
});
// Reverse the order compared to normal function application: arg |> fn
std::unique_ptr<Expr> fn, arg;
if (flip) {
fn = popExprOnly();
arg = popExprOnly();
} else {
arg = popExprOnly();
fn = popExprOnly();
}
std::vector<std::unique_ptr<Expr>> args{1};
args[0] = std::move(arg);
return std::make_unique<ExprCall>(pos, std::move(fn), std::move(args));
}
std::unique_ptr<Expr> order(PosIdx pos, bool less, State & state)
{
return call(pos, state.s.lessThan, !less);
}
std::unique_ptr<Expr> concatStrings(PosIdx pos)
{
std::vector<std::pair<PosIdx, std::unique_ptr<Expr>>> args(2);
args[1] = popExpr();
args[0] = popExpr();
return std::make_unique<ExprConcatStrings>(pos, false, std::move(args));
}
std::unique_ptr<Expr> negate(PosIdx pos, State & state)
{
std::vector<std::unique_ptr<Expr>> args(2);
args[0] = std::make_unique<ExprInt>(0);
args[1] = popExprOnly();
return std::make_unique<ExprCall>(pos, std::make_unique<ExprVar>(state.s.sub), std::move(args));
}
std::pair<PosIdx, std::unique_ptr<Expr>> applyOp(PosIdx pos, auto & op, State & state) {
using Op = grammar::v1::op;
auto not_ = [] (auto e) {
return std::make_unique<ExprOpNot>(std::move(e));
};
return {
pos,
(overloaded {
[&] (Op::implies) { return applyBinary<ExprOpImpl>(pos); },
[&] (Op::or_) { return applyBinary<ExprOpOr>(pos); },
[&] (Op::and_) { return applyBinary<ExprOpAnd>(pos); },
[&] (Op::equals) { return applyBinary<ExprOpEq>(pos); },
[&] (Op::not_equals) { return applyBinary<ExprOpNEq>(pos); },
[&] (Op::less) { return order(pos, true, state); },
[&] (Op::greater_eq) { return not_(order(pos, true, state)); },
[&] (Op::greater) { return order(pos, false, state); },
[&] (Op::less_eq) { return not_(order(pos, false, state)); },
[&] (Op::update) { return applyBinary<ExprOpUpdate>(pos); },
[&] (Op::not_) { return applyUnary<ExprOpNot>(); },
[&] (Op::plus) { return concatStrings(pos); },
[&] (Op::minus) { return call(pos, state.s.sub); },
[&] (Op::mul) { return call(pos, state.s.mul); },
[&] (Op::div) { return call(pos, state.s.div); },
[&] (Op::concat) { return applyBinary<ExprOpConcatLists>(pos); },
[&] (has_attr & a) { return applyUnary<ExprOpHasAttr>(std::move(a.path)); },
[&] (Op::unary_minus) { return negate(pos, state); },
[&] (Op::pipe_right) { return pipe(pos, state, true); },
[&] (Op::pipe_left) { return pipe(pos, state); },
})(op)
};
}
// always_inline is needed, otherwise pushOp slows down considerably
[[noreturn, gnu::always_inline]]
static void badOperator(PosIdx pos, State & state)
{
throw ParseError({
.msg = HintFmt("syntax error, unexpected operator"),
.pos = state.positions[pos]
});
}
template<typename Expr, typename... Args>
Expr & pushExpr(PosIdx pos, Args && ... args)
{
auto p = std::make_unique<Expr>(std::forward<Args>(args)...);
auto & result = *p;
exprs.emplace_back(pos, std::move(p));
return result;
}
};
struct SubexprState {
private:
ExprState * up;
public:
explicit SubexprState(ExprState & up, auto &...) : up(&up) {}
operator ExprState &() { return *up; }
ExprState * operator->() { return up; }
};
template<typename Rule>
struct BuildAST : grammar::v1::nothing<Rule> {};
struct LambdaState : SubexprState {
using SubexprState::SubexprState;
Symbol arg;
std::unique_ptr<Formals> formals;
};
struct FormalsState : SubexprState {
using SubexprState::SubexprState;
Formals formals{};
Formal formal{};
};
template<> struct BuildAST<grammar::v1::formal::name> {
static void apply(const auto & in, FormalsState & s, State & ps) {
s.formal = {
.pos = ps.at(in),
.name = ps.symbols.create(in.string_view()),
};
}
};
template<> struct BuildAST<grammar::v1::formal> {
static void apply0(FormalsState & s, State &) {
s.formals.formals.emplace_back(std::move(s.formal));
}
};
template<> struct BuildAST<grammar::v1::formal::default_value> {
static void apply0(FormalsState & s, State & ps) {
s.formal.def = s->popExprOnly();
}
};
template<> struct BuildAST<grammar::v1::formals::ellipsis> {
static void apply0(FormalsState & s, State &) {
s.formals.ellipsis = true;
}
};
template<> struct BuildAST<grammar::v1::formals> : change_head<FormalsState> {
static void success0(FormalsState & f, LambdaState & s, State &) {
s.formals = std::make_unique<Formals>(std::move(f.formals));
}
};
struct AttrState : SubexprState {
using SubexprState::SubexprState;
std::vector<AttrName> attrs;
template <typename T>
void pushAttr(T && attr, PosIdx) { attrs.emplace_back(std::forward<T>(attr)); }
};
template<> struct BuildAST<grammar::v1::attr::simple> {
static void apply(const auto & in, auto & s, State & ps) {
s.pushAttr(ps.symbols.create(in.string_view()), ps.at(in));
}
};
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
s.pushAttr(std::move(e), ps.at(in));
}
};
template<> struct BuildAST<grammar::v1::attr::expr> : BuildAST<grammar::v1::attr::string> {};
struct BindingsState : SubexprState {
using SubexprState::SubexprState;
ExprAttrs attrs;
AttrPath path;
std::unique_ptr<Expr> value;
};
struct InheritState : SubexprState {
using SubexprState::SubexprState;
std::vector<std::pair<AttrName, PosIdx>> attrs;
std::unique_ptr<Expr> from;
PosIdx fromPos;
template <typename T>
void pushAttr(T && attr, PosIdx pos) { attrs.emplace_back(std::forward<T>(attr), pos); }
};
template<> struct BuildAST<grammar::v1::inherit::from> {
static void apply(const auto & in, InheritState & s, State & ps) {
s.from = s->popExprOnly();
s.fromPos = ps.at(in);
}
};
template<> struct BuildAST<grammar::v1::inherit> : change_head<InheritState> {
static void success0(InheritState & s, BindingsState & b, State & ps) {
auto & attrs = b.attrs.attrs;
// TODO this should not reuse generic attrpath rules.
for (auto & [i, iPos] : s.attrs) {
if (i.symbol)
continue;
if (auto str = dynamic_cast<ExprString *>(i.expr.get()))
i = AttrName(ps.symbols.create(str->s));
else {
throw ParseError({
.msg = HintFmt("dynamic attributes not allowed in inherit"),
.pos = ps.positions[iPos]
});
}
}
if (s.from != nullptr) {
if (!b.attrs.inheritFromExprs)
b.attrs.inheritFromExprs = std::make_unique<std::vector<ref<Expr>>>();
auto fromExpr = ref<Expr>(std::move(s.from));
b.attrs.inheritFromExprs->push_back(fromExpr);
for (auto & [i, iPos] : s.attrs) {
if (attrs.find(i.symbol) != attrs.end())
ps.dupAttr(i.symbol, iPos, attrs[i.symbol].pos);
auto inheritFrom = std::make_unique<ExprInheritFrom>(
s.fromPos,
b.attrs.inheritFromExprs->size() - 1,
fromExpr
);
attrs.emplace(
i.symbol,
ExprAttrs::AttrDef(
std::make_unique<ExprSelect>(iPos, std::move(inheritFrom), i.symbol),
iPos,
ExprAttrs::AttrDef::Kind::InheritedFrom));
}
} else {
for (auto & [i, iPos] : s.attrs) {
if (attrs.find(i.symbol) != attrs.end())
ps.dupAttr(i.symbol, iPos, attrs[i.symbol].pos);
attrs.emplace(
i.symbol,
ExprAttrs::AttrDef(
std::make_unique<ExprVar>(iPos, i.symbol),
iPos,
ExprAttrs::AttrDef::Kind::Inherited));
}
}
}
};
template<> struct BuildAST<grammar::v1::binding::path> : change_head<AttrState> {
static void success0(AttrState & a, BindingsState & s, State & ps) {
s.path = std::move(a.attrs);
}
};
template<> struct BuildAST<grammar::v1::binding::value> {
static void apply0(BindingsState & s, State & ps) {
s.value = s->popExprOnly();
}
};
template<> struct BuildAST<grammar::v1::binding> {
static void apply(const auto & in, BindingsState & s, State & ps) {
ps.addAttr(&s.attrs, std::move(s.path), std::move(s.value), ps.at(in));
}
};
template<> struct BuildAST<grammar::v1::expr::id> {
static void apply(const auto & in, ExprState & s, State & ps) {
if (in.string_view() == "__curPos")
s.pushExpr<ExprPos>(ps.at(in), ps.at(in));
else
s.pushExpr<ExprVar>(ps.at(in), ps.at(in), ps.symbols.create(in.string_view()));
}
};
template<> struct BuildAST<grammar::v1::expr::int_> {
static void apply(const auto & in, ExprState & s, State & ps) {
int64_t v;
if (std::from_chars(in.begin(), in.end(), v).ec != std::errc{}) {
throw ParseError({
.msg = HintFmt("invalid integer '%1%'", in.string_view()),
.pos = ps.positions[ps.at(in)],
});
}
s.pushExpr<ExprInt>(noPos, v);
}
};
template<> struct BuildAST<grammar::v1::expr::float_> {
static void apply(const auto & in, ExprState & s, State & ps) {
// copy the input into a temporary string so we can call stod.
// can't use from_chars because libc++ (thus darwin) does not have it,
// and floats are not performance-sensitive anyway. if they were you'd
// be in much bigger trouble than this.
//
// we also get to do a locale-save dance because stod is locale-aware and
// something (a plugin?) may have called setlocale or uselocale.
static struct locale_hack {
locale_t posix;
locale_hack(): posix(newlocale(LC_ALL_MASK, "POSIX", 0))
{
if (posix == 0)
throw SysError("could not get POSIX locale");
}
} locale;
auto tmp = in.string();
double v = [&] {
auto oldLocale = uselocale(locale.posix);
Finally resetLocale([=] { uselocale(oldLocale); });
try {
return std::stod(tmp);
} catch (...) {
throw ParseError({
.msg = HintFmt("invalid float '%1%'", in.string_view()),
.pos = ps.positions[ps.at(in)],
});
}
}();
s.pushExpr<ExprFloat>(noPos, v);
}
};
struct StringState : SubexprState {
using SubexprState::SubexprState;
std::string currentLiteral;
PosIdx currentPos;
std::vector<std::pair<nix::PosIdx, std::unique_ptr<Expr>>> parts;
void append(PosIdx pos, std::string_view s)
{
if (currentLiteral.empty())
currentPos = pos;
currentLiteral += s;
}
// FIXME this truncates strings on NUL for compat with the old parser. ideally
// we should use the decomposition the g gives us instead of iterating over
// the entire string again.
static void unescapeStr(std::string & str)
{
char * s = str.data();
char * t = s;
char c;
while ((c = *s++)) {
if (c == '\\') {
c = *s++;
if (c == 'n') *t = '\n';
else if (c == 'r') *t = '\r';
else if (c == 't') *t = '\t';
else *t = c;
}
else if (c == '\r') {
/* Normalise CR and CR/LF into LF. */
*t = '\n';
if (*s == '\n') s++; /* cr/lf */
}
else *t = c;
t++;
}
str.resize(t - str.data());
}
void endLiteral()
{
if (!currentLiteral.empty()) {
unescapeStr(currentLiteral);
parts.emplace_back(currentPos, std::make_unique<ExprString>(std::move(currentLiteral)));
}
}
std::unique_ptr<Expr> finish()
{
if (parts.empty()) {
unescapeStr(currentLiteral);
return std::make_unique<ExprString>(std::move(currentLiteral));
} else {
endLiteral();
auto pos = parts[0].first;
return std::make_unique<ExprConcatStrings>(pos, true, std::move(parts));
}
}
};
template<typename... Content> struct BuildAST<grammar::v1::string::literal<Content...>> {
static void apply(const auto & in, StringState & s, State & ps) {
s.append(ps.at(in), in.string_view());
}
};
template<> struct BuildAST<grammar::v1::string::cr_lf> {
static void apply(const auto & in, StringState & s, State & ps) {
s.append(ps.at(in), in.string_view()); // FIXME compat with old parser
}
};
template<> struct BuildAST<grammar::v1::string::interpolation> {
static void apply(const auto & in, StringState & s, State & ps) {
s.endLiteral();
s.parts.emplace_back(ps.at(in), s->popExprOnly());
}
};
template<> struct BuildAST<grammar::v1::string::escape> {
static void apply(const auto & in, StringState & s, State & ps) {
s.append(ps.at(in), "\\"); // FIXME compat with old parser
s.append(ps.at(in), in.string_view());
}
};
template<> struct BuildAST<grammar::v1::string> : change_head<StringState> {
static void success0(StringState & s, ExprState & e, State &) {
e.exprs.emplace_back(noPos, s.finish());
}
};
struct IndStringState : SubexprState {
using SubexprState::SubexprState;
std::vector<IndStringLine> lines;
};
template<> struct BuildAST<grammar::v1::ind_string::line_start> {
static void apply(const auto & in, IndStringState & s, State & ps) {
s.lines.push_back(IndStringLine { in.string_view(), ps.at(in) });
}
};
template<typename... Content>
struct BuildAST<grammar::v1::ind_string::literal<Content...>> {
static void apply(const auto & in, IndStringState & s, State & ps) {
s.lines.back().parts.emplace_back(ps.at(in), in.string_view());
}
};
template<> struct BuildAST<grammar::v1::ind_string::interpolation> {
static void apply(const auto & in, IndStringState & s, State & ps) {
s.lines.back().parts.emplace_back(ps.at(in), s->popExprOnly());
}
};
template<> struct BuildAST<grammar::v1::ind_string::escape> {
static void apply(const auto & in, IndStringState & s, State & ps) {
switch (*in.begin()) {
case 'n': s.lines.back().parts.emplace_back(ps.at(in), "\n"); break;
case 'r': s.lines.back().parts.emplace_back(ps.at(in), "\r"); break;
case 't': s.lines.back().parts.emplace_back(ps.at(in), "\t"); break;
default: s.lines.back().parts.emplace_back(ps.at(in), in.string_view()); break;
}
}
};
template<> struct BuildAST<grammar::v1::ind_string::has_content> {
static void apply(const auto & in, IndStringState & s, State & ps) {
s.lines.back().hasContent = true;
}
};
template<> struct BuildAST<grammar::v1::ind_string> : change_head<IndStringState> {
static void success(const auto & in, IndStringState & s, ExprState & e, State & ps) {
e.exprs.emplace_back(noPos, ps.stripIndentation(ps.at(in), std::move(s.lines)));
}
};
template<typename... Content> struct BuildAST<grammar::v1::path::literal<Content...>> {
static void apply(const auto & in, StringState & s, State & ps) {
s.append(ps.at(in), in.string_view());
s.endLiteral();
}
};
template<> struct BuildAST<grammar::v1::path::interpolation> : BuildAST<grammar::v1::string::interpolation> {};
template<> struct BuildAST<grammar::v1::path::anchor> {
static void apply(const auto & in, StringState & s, State & ps) {
Path path(absPath(in.string(), ps.basePath.path.abs()));
/* add back in the trailing '/' to the first segment */
if (in.string_view().ends_with('/') && in.size() > 1)
path += "/";
s.parts.emplace_back(ps.at(in), new ExprPath(std::move(path)));
}
};
template<> struct BuildAST<grammar::v1::path::home_anchor> {
static void apply(const auto & in, StringState & s, State & ps) {
if (evalSettings.pureEval)
throw Error("the path '%s' can not be resolved in pure mode", in.string_view());
Path path(getHome() + in.string_view().substr(1));
s.parts.emplace_back(ps.at(in), new ExprPath(std::move(path)));
}
};
template<> struct BuildAST<grammar::v1::path::searched_path> {
static void apply(const auto & in, StringState & s, State & ps) {
std::vector<std::unique_ptr<Expr>> args{2};
args[0] = std::make_unique<ExprVar>(ps.s.nixPath);
args[1] = std::make_unique<ExprString>(in.string());
s.parts.emplace_back(
ps.at(in),
std::make_unique<ExprCall>(
ps.at(in),
std::make_unique<ExprVar>(ps.s.findFile),
std::move(args)));
}
};
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('/'))
return;
if (s.parts.size() > 1 || e->s != "/")
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) {
s.endLiteral();
check_slash<ExprPath>(ps.atEnd(in), s, ps);
check_slash<ExprString>(ps.atEnd(in), s, ps);
if (s.parts.size() == 1) {
e.exprs.emplace_back(noPos, std::move(s.parts.back().second));
} else {
e.pushExpr<ExprConcatStrings>(ps.at(in), ps.at(in), false, std::move(s.parts));
}
}
};
// strings and paths sare handled fully by the grammar-level rule for now
template<> struct BuildAST<grammar::v1::expr::string> : p::maybe_nothing {};
template<> struct BuildAST<grammar::v1::expr::ind_string> : p::maybe_nothing {};
template<> struct BuildAST<grammar::v1::expr::path> : p::maybe_nothing {};
template<> struct BuildAST<grammar::v1::expr::uri> {
static void apply(const auto & in, ExprState & s, State & ps) {
bool URLLiterals = ps.featureSettings.isEnabled(Dep::UrlLiterals);
if (!URLLiterals)
throw ParseError({
.msg = HintFmt("URL literals are deprecated, allow using them with %s", "--extra-deprecated-features url-literals"),
.pos = ps.positions[ps.at(in)]
});
s.pushExpr<ExprString>(ps.at(in), in.string());
}
};
template<> struct BuildAST<grammar::v1::expr::ancient_let> : change_head<BindingsState> {
static void success(const auto & in, BindingsState & b, ExprState & s, State & ps) {
// Added 2024-09-18. Turn into an error at some point in the future.
// See the documentation on deprecated features for more details.
if (!ps.featureSettings.isEnabled(Dep::AncientLet))
warn(
"%s found at %s. This feature is deprecated and will be removed in the future. Use %s to silence this warning.",
"let {",
ps.positions[ps.at(in)],
"--extra-deprecated-features ancient-let"
);
b.attrs.pos = ps.at(in);
b.attrs.recursive = true;
s.pushExpr<ExprSelect>(b.attrs.pos, b.attrs.pos, std::make_unique<ExprAttrs>(std::move(b.attrs)), ps.s.body);
}
};
template<> struct BuildAST<grammar::v1::expr::rec_set> : change_head<BindingsState> {
static void success(const auto & in, BindingsState & b, ExprState & s, State & ps) {
// Before inserting new attrs, check for __override and throw an error
// (the error will initially be a warning to ease migration)
if (!featureSettings.isEnabled(Dep::RecSetOverrides) && b.attrs.attrs.contains(ps.s.overrides)) {
ps.overridesFound(ps.at(in));
}
b.attrs.pos = ps.at(in);
b.attrs.recursive = true;
s.pushExpr<ExprAttrs>(b.attrs.pos, std::move(b.attrs));
}
};
template<> struct BuildAST<grammar::v1::expr::set> : change_head<BindingsState> {
static void success(const auto & in, BindingsState & b, ExprState & s, State & ps) {
b.attrs.pos = ps.at(in);
s.pushExpr<ExprAttrs>(b.attrs.pos, std::move(b.attrs));
}
};
using ListState = std::vector<std::unique_ptr<Expr>>;
template<> struct BuildAST<grammar::v1::expr::list> : change_head<ListState> {
static void success(const auto & in, ListState & ls, ExprState & s, State & ps) {
auto e = std::make_unique<ExprList>();
e->elems = std::move(ls);
s.exprs.emplace_back(ps.at(in), std::move(e));
}
};
template<> struct BuildAST<grammar::v1::expr::list::entry> : change_head<ExprState> {
static void success0(ExprState & e, ListState & s, State & ps) {
s.emplace_back(e.finish(ps).second);
}
};
struct SelectState : SubexprState {
using SubexprState::SubexprState;
PosIdx pos;
ExprSelect * e = nullptr;
};
template<> struct BuildAST<grammar::v1::expr::select::head> {
static void apply(const auto & in, SelectState & s, State & ps) {
s.pos = ps.at(in);
}
};
template<> struct BuildAST<grammar::v1::expr::select::attr> : change_head<AttrState> {
static void success0(AttrState & a, SelectState & s, State &) {
s.e = &s->pushExpr<ExprSelect>(s.pos, s.pos, s->popExprOnly(), std::move(a.attrs), nullptr);
}
};
template<> struct BuildAST<grammar::v1::expr::select::attr_or> {
static void apply0(SelectState & s, State &) {
s.e->def = s->popExprOnly();
}
};
template<> struct BuildAST<grammar::v1::expr::select::as_app_or> {
static void apply(const auto & in, SelectState & s, State & ps) {
std::vector<std::unique_ptr<Expr>> args(1);
args[0] = std::make_unique<ExprVar>(ps.at(in), ps.s.or_);
s->pushExpr<ExprCall>(s.pos, s.pos, s->popExprOnly(), std::move(args));
}
};
template<> struct BuildAST<grammar::v1::expr::select> : change_head<SelectState> {
static void success0(const auto &...) {}
};
struct AppState : SubexprState {
using SubexprState::SubexprState;
PosIdx pos;
ExprCall * e = nullptr;
};
template<> struct BuildAST<grammar::v1::expr::app::select_or_fn> {
static void apply(const auto & in, AppState & s, State & ps) {
s.pos = ps.at(in);
}
};
template<> struct BuildAST<grammar::v1::expr::app::first_arg> {
static void apply(auto & in, AppState & s, State & ps) {
auto arg = s->popExprOnly(), fn = s->popExprOnly();
if ((s.e = dynamic_cast<ExprCall *>(fn.get()))) {
// TODO remove.
// AST compat with old parser, semantics are the same.
// this can happen on occasions such as `<p> <p>` or `a or b or`,
// neither of which are super worth optimizing.
s.e->args.push_back(std::move(arg));
s->exprs.emplace_back(noPos, std::move(fn));
} else {
std::vector<std::unique_ptr<Expr>> args{1};
args[0] = std::move(arg);
s.e = &s->pushExpr<ExprCall>(s.pos, s.pos, std::move(fn), std::move(args));
}
}
};
template<> struct BuildAST<grammar::v1::expr::app::another_arg> {
static void apply0(AppState & s, State & ps) {
s.e->args.push_back(s->popExprOnly());
}
};
template<> struct BuildAST<grammar::v1::expr::app> : change_head<AppState> {
static void success0(const auto &...) {}
};
template<typename Op> struct BuildAST<grammar::v1::expr::operator_<Op>> {
static void apply(const auto & in, ExprState & s, State & ps) {
s.pushOp(ps.at(in), Op{}, ps);
}
};
template<> struct BuildAST<grammar::v1::expr::operator_<grammar::v1::op::has_attr>> : change_head<AttrState> {
static void success(const auto & in, AttrState & a, ExprState & s, State & ps) {
s.pushOp(ps.at(in), ExprState::has_attr{{}, std::move(a.attrs)}, ps);
}
};
template<> struct BuildAST<grammar::v1::expr::lambda::arg> {
static void apply(const auto & in, LambdaState & s, State & ps) {
s.arg = ps.symbols.create(in.string_view());
}
};
template<> struct BuildAST<grammar::v1::expr::lambda> : change_head<LambdaState> {
static void success(const auto & in, LambdaState & l, ExprState & s, State & ps) {
if (l.formals)
l.formals = ps.validateFormals(std::move(l.formals), ps.at(in), l.arg);
s.pushExpr<ExprLambda>(ps.at(in), ps.at(in), l.arg, std::move(l.formals), l->popExprOnly());
}
};
template<> struct BuildAST<grammar::v1::expr::assert_> {
static void apply(const auto & in, ExprState & s, State & ps) {
auto body = s.popExprOnly(), cond = s.popExprOnly();
s.pushExpr<ExprAssert>(ps.at(in), ps.at(in), std::move(cond), std::move(body));
}
};
template<> struct BuildAST<grammar::v1::expr::with> {
static void apply(const auto & in, ExprState & s, State & ps) {
auto body = s.popExprOnly(), scope = s.popExprOnly();
s.pushExpr<ExprWith>(ps.at(in), ps.at(in), std::move(scope), std::move(body));
}
};
template<> struct BuildAST<grammar::v1::expr::let> : change_head<BindingsState> {
static void success(const auto & in, BindingsState & b, ExprState & s, State & ps) {
if (!b.attrs.dynamicAttrs.empty())
throw ParseError({
.msg = HintFmt("dynamic attributes not allowed in let"),
.pos = ps.positions[ps.at(in)]
});
s.pushExpr<ExprLet>(ps.at(in), std::make_unique<ExprAttrs>(std::move(b.attrs)), b->popExprOnly());
}
};
template<> struct BuildAST<grammar::v1::expr::if_> {
static void apply(const auto & in, ExprState & s, State & ps) {
auto else_ = s.popExprOnly(), then = s.popExprOnly(), cond = s.popExprOnly();
s.pushExpr<ExprIf>(ps.at(in), ps.at(in), std::move(cond), std::move(then), std::move(else_));
}
};
template<> struct BuildAST<grammar::v1::expr> : change_head<ExprState> {
static void success0(ExprState & inner, ExprState & outer, State & ps) {
outer.exprs.push_back(inner.finish(ps));
}
};
}
}

View file

@ -14,857 +14,11 @@
#include <charconv>
#include <memory>
// flip this define when doing parser development to enable some g checks.
#if 0
#include <tao/pegtl/contrib/analyze.hpp>
#define ANALYZE_GRAMMAR \
([] { \
const std::size_t issues = tao::pegtl::analyze<grammar::root>(); \
assert(issues == 0); \
})()
#else
#define ANALYZE_GRAMMAR ((void) 0)
#endif
namespace p = tao::pegtl;
namespace nix::parser {
namespace {
template<typename>
inline constexpr const char * error_message = nullptr;
#define error_message_for(...) \
template<> inline constexpr auto error_message<__VA_ARGS__>
error_message_for(p::one<'{'>) = "expecting '{'";
error_message_for(p::one<'}'>) = "expecting '}'";
error_message_for(p::one<'"'>) = "expecting '\"'";
error_message_for(p::one<';'>) = "expecting ';'";
error_message_for(p::one<')'>) = "expecting ')'";
error_message_for(p::one<']'>) = "expecting ']'";
error_message_for(p::one<':'>) = "expecting ':'";
error_message_for(p::string<'\'', '\''>) = "expecting \"''\"";
error_message_for(p::any) = "expecting any character";
error_message_for(grammar::eof) = "expecting end of file";
error_message_for(grammar::seps) = "expecting separators";
error_message_for(grammar::path::forbid_prefix_triple_slash) = "too many slashes in path";
error_message_for(grammar::path::forbid_prefix_double_slash_no_interp) = "path has a trailing slash";
error_message_for(grammar::expr) = "expecting expression";
error_message_for(grammar::expr::unary) = "expecting expression";
error_message_for(grammar::binding::equal) = "expecting '='";
error_message_for(grammar::expr::lambda::arg) = "expecting identifier";
error_message_for(grammar::formals) = "expecting formals";
error_message_for(grammar::attrpath) = "expecting attribute path";
error_message_for(grammar::expr::select) = "expecting selection expression";
error_message_for(grammar::t::kw_then) = "expecting 'then'";
error_message_for(grammar::t::kw_else) = "expecting 'else'";
error_message_for(grammar::t::kw_in) = "expecting 'in'";
struct SyntaxErrors
{
template<typename Rule>
static constexpr auto message = error_message<Rule>;
template<typename Rule>
static constexpr bool raise_on_failure = false;
};
template<typename Rule>
struct Control : p::must_if<SyntaxErrors>::control<Rule>
{
template<typename ParseInput, typename... States>
[[noreturn]] static void raise(const ParseInput & in, States &&... st)
{
if (in.empty()) {
std::string expected;
if constexpr (constexpr auto msg = error_message<Rule>)
expected = fmt(", %s", msg);
throw p::parse_error("unexpected end of file" + expected, in);
}
p::must_if<SyntaxErrors>::control<Rule>::raise(in, st...);
}
};
struct ExprState
: grammar::
operator_semantics<ExprState, PosIdx, AttrPath, std::pair<PosIdx, std::unique_ptr<Expr>>>
{
std::unique_ptr<Expr> popExprOnly() {
return std::move(popExpr().second);
}
template<typename Op, typename... Args>
std::unique_ptr<Expr> applyUnary(Args &&... args) {
return std::make_unique<Op>(popExprOnly(), std::forward<Args>(args)...);
}
template<typename Op>
std::unique_ptr<Expr> applyBinary(PosIdx pos) {
auto right = popExprOnly(), left = popExprOnly();
return std::make_unique<Op>(pos, std::move(left), std::move(right));
}
std::unique_ptr<Expr> call(PosIdx pos, Symbol fn, bool flip = false)
{
std::vector<std::unique_ptr<Expr>> args(2);
args[flip ? 0 : 1] = popExprOnly();
args[flip ? 1 : 0] = popExprOnly();
return std::make_unique<ExprCall>(pos, std::make_unique<ExprVar>(fn), std::move(args));
}
std::unique_ptr<Expr> pipe(PosIdx pos, State & state, bool flip = false)
{
if (!state.featureSettings.isEnabled(Xp::PipeOperator))
throw ParseError({
.msg = HintFmt("Pipe operator is disabled"),
.pos = state.positions[pos]
});
// Reverse the order compared to normal function application: arg |> fn
std::unique_ptr<Expr> fn, arg;
if (flip) {
fn = popExprOnly();
arg = popExprOnly();
} else {
arg = popExprOnly();
fn = popExprOnly();
}
std::vector<std::unique_ptr<Expr>> args{1};
args[0] = std::move(arg);
return std::make_unique<ExprCall>(pos, std::move(fn), std::move(args));
}
std::unique_ptr<Expr> order(PosIdx pos, bool less, State & state)
{
return call(pos, state.s.lessThan, !less);
}
std::unique_ptr<Expr> concatStrings(PosIdx pos)
{
std::vector<std::pair<PosIdx, std::unique_ptr<Expr>>> args(2);
args[1] = popExpr();
args[0] = popExpr();
return std::make_unique<ExprConcatStrings>(pos, false, std::move(args));
}
std::unique_ptr<Expr> negate(PosIdx pos, State & state)
{
std::vector<std::unique_ptr<Expr>> args(2);
args[0] = std::make_unique<ExprInt>(0);
args[1] = popExprOnly();
return std::make_unique<ExprCall>(pos, std::make_unique<ExprVar>(state.s.sub), std::move(args));
}
std::pair<PosIdx, std::unique_ptr<Expr>> applyOp(PosIdx pos, auto & op, State & state) {
using Op = grammar::op;
auto not_ = [] (auto e) {
return std::make_unique<ExprOpNot>(std::move(e));
};
return {
pos,
(overloaded {
[&] (Op::implies) { return applyBinary<ExprOpImpl>(pos); },
[&] (Op::or_) { return applyBinary<ExprOpOr>(pos); },
[&] (Op::and_) { return applyBinary<ExprOpAnd>(pos); },
[&] (Op::equals) { return applyBinary<ExprOpEq>(pos); },
[&] (Op::not_equals) { return applyBinary<ExprOpNEq>(pos); },
[&] (Op::less) { return order(pos, true, state); },
[&] (Op::greater_eq) { return not_(order(pos, true, state)); },
[&] (Op::greater) { return order(pos, false, state); },
[&] (Op::less_eq) { return not_(order(pos, false, state)); },
[&] (Op::update) { return applyBinary<ExprOpUpdate>(pos); },
[&] (Op::not_) { return applyUnary<ExprOpNot>(); },
[&] (Op::plus) { return concatStrings(pos); },
[&] (Op::minus) { return call(pos, state.s.sub); },
[&] (Op::mul) { return call(pos, state.s.mul); },
[&] (Op::div) { return call(pos, state.s.div); },
[&] (Op::concat) { return applyBinary<ExprOpConcatLists>(pos); },
[&] (has_attr & a) { return applyUnary<ExprOpHasAttr>(std::move(a.path)); },
[&] (Op::unary_minus) { return negate(pos, state); },
[&] (Op::pipe_right) { return pipe(pos, state, true); },
[&] (Op::pipe_left) { return pipe(pos, state); },
})(op)
};
}
// always_inline is needed, otherwise pushOp slows down considerably
[[noreturn, gnu::always_inline]]
static void badOperator(PosIdx pos, State & state)
{
throw ParseError({
.msg = HintFmt("syntax error, unexpected operator"),
.pos = state.positions[pos]
});
}
template<typename Expr, typename... Args>
Expr & pushExpr(PosIdx pos, Args && ... args)
{
auto p = std::make_unique<Expr>(std::forward<Args>(args)...);
auto & result = *p;
exprs.emplace_back(pos, std::move(p));
return result;
}
};
struct SubexprState {
private:
ExprState * up;
public:
explicit SubexprState(ExprState & up, auto &...) : up(&up) {}
operator ExprState &() { return *up; }
ExprState * operator->() { return up; }
};
template<typename Rule>
struct BuildAST : grammar::nothing<Rule> {};
struct LambdaState : SubexprState {
using SubexprState::SubexprState;
Symbol arg;
std::unique_ptr<Formals> formals;
};
struct FormalsState : SubexprState {
using SubexprState::SubexprState;
Formals formals{};
Formal formal{};
};
template<> struct BuildAST<grammar::formal::name> {
static void apply(const auto & in, FormalsState & s, State & ps) {
s.formal = {
.pos = ps.at(in),
.name = ps.symbols.create(in.string_view()),
};
}
};
template<> struct BuildAST<grammar::formal> {
static void apply0(FormalsState & s, State &) {
s.formals.formals.emplace_back(std::move(s.formal));
}
};
template<> struct BuildAST<grammar::formal::default_value> {
static void apply0(FormalsState & s, State & ps) {
s.formal.def = s->popExprOnly();
}
};
template<> struct BuildAST<grammar::formals::ellipsis> {
static void apply0(FormalsState & s, State &) {
s.formals.ellipsis = true;
}
};
template<> struct BuildAST<grammar::formals> : change_head<FormalsState> {
static void success0(FormalsState & f, LambdaState & s, State &) {
s.formals = std::make_unique<Formals>(std::move(f.formals));
}
};
struct AttrState : SubexprState {
using SubexprState::SubexprState;
std::vector<AttrName> attrs;
template <typename T>
void pushAttr(T && attr, PosIdx) { attrs.emplace_back(std::forward<T>(attr)); }
};
template<> struct BuildAST<grammar::attr::simple> {
static void apply(const auto & in, auto & s, State & ps) {
s.pushAttr(ps.symbols.create(in.string_view()), ps.at(in));
}
};
template<> struct BuildAST<grammar::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
s.pushAttr(std::move(e), ps.at(in));
}
};
template<> struct BuildAST<grammar::attr::expr> : BuildAST<grammar::attr::string> {};
struct BindingsState : SubexprState {
using SubexprState::SubexprState;
ExprAttrs attrs;
AttrPath path;
std::unique_ptr<Expr> value;
};
struct InheritState : SubexprState {
using SubexprState::SubexprState;
std::vector<std::pair<AttrName, PosIdx>> attrs;
std::unique_ptr<Expr> from;
PosIdx fromPos;
template <typename T>
void pushAttr(T && attr, PosIdx pos) { attrs.emplace_back(std::forward<T>(attr), pos); }
};
template<> struct BuildAST<grammar::inherit::from> {
static void apply(const auto & in, InheritState & s, State & ps) {
s.from = s->popExprOnly();
s.fromPos = ps.at(in);
}
};
template<> struct BuildAST<grammar::inherit> : change_head<InheritState> {
static void success0(InheritState & s, BindingsState & b, State & ps) {
auto & attrs = b.attrs.attrs;
// TODO this should not reuse generic attrpath rules.
for (auto & [i, iPos] : s.attrs) {
if (i.symbol)
continue;
if (auto str = dynamic_cast<ExprString *>(i.expr.get()))
i = AttrName(ps.symbols.create(str->s));
else {
throw ParseError({
.msg = HintFmt("dynamic attributes not allowed in inherit"),
.pos = ps.positions[iPos]
});
}
}
if (s.from != nullptr) {
if (!b.attrs.inheritFromExprs)
b.attrs.inheritFromExprs = std::make_unique<std::vector<ref<Expr>>>();
auto fromExpr = ref<Expr>(std::move(s.from));
b.attrs.inheritFromExprs->push_back(fromExpr);
for (auto & [i, iPos] : s.attrs) {
if (attrs.find(i.symbol) != attrs.end())
ps.dupAttr(i.symbol, iPos, attrs[i.symbol].pos);
auto inheritFrom = std::make_unique<ExprInheritFrom>(
s.fromPos,
b.attrs.inheritFromExprs->size() - 1,
fromExpr
);
attrs.emplace(
i.symbol,
ExprAttrs::AttrDef(
std::make_unique<ExprSelect>(iPos, std::move(inheritFrom), i.symbol),
iPos,
ExprAttrs::AttrDef::Kind::InheritedFrom));
}
} else {
for (auto & [i, iPos] : s.attrs) {
if (attrs.find(i.symbol) != attrs.end())
ps.dupAttr(i.symbol, iPos, attrs[i.symbol].pos);
attrs.emplace(
i.symbol,
ExprAttrs::AttrDef(
std::make_unique<ExprVar>(iPos, i.symbol),
iPos,
ExprAttrs::AttrDef::Kind::Inherited));
}
}
}
};
template<> struct BuildAST<grammar::binding::path> : change_head<AttrState> {
static void success0(AttrState & a, BindingsState & s, State & ps) {
s.path = std::move(a.attrs);
}
};
template<> struct BuildAST<grammar::binding::value> {
static void apply0(BindingsState & s, State & ps) {
s.value = s->popExprOnly();
}
};
template<> struct BuildAST<grammar::binding> {
static void apply(const auto & in, BindingsState & s, State & ps) {
ps.addAttr(&s.attrs, std::move(s.path), std::move(s.value), ps.at(in));
}
};
template<> struct BuildAST<grammar::expr::id> {
static void apply(const auto & in, ExprState & s, State & ps) {
if (in.string_view() == "__curPos")
s.pushExpr<ExprPos>(ps.at(in), ps.at(in));
else
s.pushExpr<ExprVar>(ps.at(in), ps.at(in), ps.symbols.create(in.string_view()));
}
};
template<> struct BuildAST<grammar::expr::int_> {
static void apply(const auto & in, ExprState & s, State & ps) {
int64_t v;
if (std::from_chars(in.begin(), in.end(), v).ec != std::errc{}) {
throw ParseError({
.msg = HintFmt("invalid integer '%1%'", in.string_view()),
.pos = ps.positions[ps.at(in)],
});
}
s.pushExpr<ExprInt>(noPos, v);
}
};
template<> struct BuildAST<grammar::expr::float_> {
static void apply(const auto & in, ExprState & s, State & ps) {
// copy the input into a temporary string so we can call stod.
// can't use from_chars because libc++ (thus darwin) does not have it,
// and floats are not performance-sensitive anyway. if they were you'd
// be in much bigger trouble than this.
//
// we also get to do a locale-save dance because stod is locale-aware and
// something (a plugin?) may have called setlocale or uselocale.
static struct locale_hack {
locale_t posix;
locale_hack(): posix(newlocale(LC_ALL_MASK, "POSIX", 0))
{
if (posix == 0)
throw SysError("could not get POSIX locale");
}
} locale;
auto tmp = in.string();
double v = [&] {
auto oldLocale = uselocale(locale.posix);
Finally resetLocale([=] { uselocale(oldLocale); });
try {
return std::stod(tmp);
} catch (...) {
throw ParseError({
.msg = HintFmt("invalid float '%1%'", in.string_view()),
.pos = ps.positions[ps.at(in)],
});
}
}();
s.pushExpr<ExprFloat>(noPos, v);
}
};
struct StringState : SubexprState {
using SubexprState::SubexprState;
std::string currentLiteral;
PosIdx currentPos;
std::vector<std::pair<nix::PosIdx, std::unique_ptr<Expr>>> parts;
void append(PosIdx pos, std::string_view s)
{
if (currentLiteral.empty())
currentPos = pos;
currentLiteral += s;
}
// FIXME this truncates strings on NUL for compat with the old parser. ideally
// we should use the decomposition the g gives us instead of iterating over
// the entire string again.
static void unescapeStr(std::string & str)
{
char * s = str.data();
char * t = s;
char c;
while ((c = *s++)) {
if (c == '\\') {
c = *s++;
if (c == 'n') *t = '\n';
else if (c == 'r') *t = '\r';
else if (c == 't') *t = '\t';
else *t = c;
}
else if (c == '\r') {
/* Normalise CR and CR/LF into LF. */
*t = '\n';
if (*s == '\n') s++; /* cr/lf */
}
else *t = c;
t++;
}
str.resize(t - str.data());
}
void endLiteral()
{
if (!currentLiteral.empty()) {
unescapeStr(currentLiteral);
parts.emplace_back(currentPos, std::make_unique<ExprString>(std::move(currentLiteral)));
}
}
std::unique_ptr<Expr> finish()
{
if (parts.empty()) {
unescapeStr(currentLiteral);
return std::make_unique<ExprString>(std::move(currentLiteral));
} else {
endLiteral();
auto pos = parts[0].first;
return std::make_unique<ExprConcatStrings>(pos, true, std::move(parts));
}
}
};
template<typename... Content> struct BuildAST<grammar::string::literal<Content...>> {
static void apply(const auto & in, StringState & s, State & ps) {
s.append(ps.at(in), in.string_view());
}
};
template<> struct BuildAST<grammar::string::cr_lf> {
static void apply(const auto & in, StringState & s, State & ps) {
s.append(ps.at(in), in.string_view()); // FIXME compat with old parser
}
};
template<> struct BuildAST<grammar::string::interpolation> {
static void apply(const auto & in, StringState & s, State & ps) {
s.endLiteral();
s.parts.emplace_back(ps.at(in), s->popExprOnly());
}
};
template<> struct BuildAST<grammar::string::escape> {
static void apply(const auto & in, StringState & s, State & ps) {
s.append(ps.at(in), "\\"); // FIXME compat with old parser
s.append(ps.at(in), in.string_view());
}
};
template<> struct BuildAST<grammar::string> : change_head<StringState> {
static void success0(StringState & s, ExprState & e, State &) {
e.exprs.emplace_back(noPos, s.finish());
}
};
struct IndStringState : SubexprState {
using SubexprState::SubexprState;
std::vector<std::pair<PosIdx, std::variant<std::unique_ptr<Expr>, StringToken>>> parts;
};
template<bool Indented, typename... Content>
struct BuildAST<grammar::ind_string::literal<Indented, Content...>> {
static void apply(const auto & in, IndStringState & s, State & ps) {
s.parts.emplace_back(ps.at(in), StringToken{in.string_view(), Indented});
}
};
template<> struct BuildAST<grammar::ind_string::interpolation> {
static void apply(const auto & in, IndStringState & s, State & ps) {
s.parts.emplace_back(ps.at(in), s->popExprOnly());
}
};
template<> struct BuildAST<grammar::ind_string::escape> {
static void apply(const auto & in, IndStringState & s, State & ps) {
switch (*in.begin()) {
case 'n': s.parts.emplace_back(ps.at(in), StringToken{"\n"}); break;
case 'r': s.parts.emplace_back(ps.at(in), StringToken{"\r"}); break;
case 't': s.parts.emplace_back(ps.at(in), StringToken{"\t"}); break;
default: s.parts.emplace_back(ps.at(in), StringToken{in.string_view()}); break;
}
}
};
template<> struct BuildAST<grammar::ind_string> : change_head<IndStringState> {
static void success(const auto & in, IndStringState & s, ExprState & e, State & ps) {
e.exprs.emplace_back(noPos, ps.stripIndentation(ps.at(in), std::move(s.parts)));
}
};
template<typename... Content> struct BuildAST<grammar::path::literal<Content...>> {
static void apply(const auto & in, StringState & s, State & ps) {
s.append(ps.at(in), in.string_view());
s.endLiteral();
}
};
template<> struct BuildAST<grammar::path::interpolation> : BuildAST<grammar::string::interpolation> {};
template<> struct BuildAST<grammar::path::anchor> {
static void apply(const auto & in, StringState & s, State & ps) {
Path path(absPath(in.string(), ps.basePath.path.abs()));
/* add back in the trailing '/' to the first segment */
if (in.string_view().ends_with('/') && in.size() > 1)
path += "/";
s.parts.emplace_back(ps.at(in), new ExprPath(std::move(path)));
}
};
template<> struct BuildAST<grammar::path::home_anchor> {
static void apply(const auto & in, StringState & s, State & ps) {
if (evalSettings.pureEval)
throw Error("the path '%s' can not be resolved in pure mode", in.string_view());
Path path(getHome() + in.string_view().substr(1));
s.parts.emplace_back(ps.at(in), new ExprPath(std::move(path)));
}
};
template<> struct BuildAST<grammar::path::searched_path> {
static void apply(const auto & in, StringState & s, State & ps) {
std::vector<std::unique_ptr<Expr>> args{2};
args[0] = std::make_unique<ExprVar>(ps.s.nixPath);
args[1] = std::make_unique<ExprString>(in.string());
s.parts.emplace_back(
ps.at(in),
std::make_unique<ExprCall>(
ps.at(in),
std::make_unique<ExprVar>(ps.s.findFile),
std::move(args)));
}
};
template<> struct BuildAST<grammar::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('/'))
return;
if (s.parts.size() > 1 || e->s != "/")
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) {
s.endLiteral();
check_slash<ExprPath>(ps.atEnd(in), s, ps);
check_slash<ExprString>(ps.atEnd(in), s, ps);
if (s.parts.size() == 1) {
e.exprs.emplace_back(noPos, std::move(s.parts.back().second));
} else {
e.pushExpr<ExprConcatStrings>(ps.at(in), ps.at(in), false, std::move(s.parts));
}
}
};
// strings and paths sare handled fully by the grammar-level rule for now
template<> struct BuildAST<grammar::expr::string> : p::maybe_nothing {};
template<> struct BuildAST<grammar::expr::ind_string> : p::maybe_nothing {};
template<> struct BuildAST<grammar::expr::path> : p::maybe_nothing {};
template<> struct BuildAST<grammar::expr::uri> {
static void apply(const auto & in, ExprState & s, State & ps) {
bool URLLiterals = ps.featureSettings.isEnabled(Dep::UrlLiterals);
if (!URLLiterals)
throw ParseError({
.msg = HintFmt("URL literals are deprecated, allow using them with --extra-deprecated-features=url-literals"),
.pos = ps.positions[ps.at(in)]
});
s.pushExpr<ExprString>(ps.at(in), in.string());
}
};
template<> struct BuildAST<grammar::expr::ancient_let> : change_head<BindingsState> {
static void success(const auto & in, BindingsState & b, ExprState & s, State & ps) {
// Added 2024-09-18. Turn into an error at some point in the future.
// See the documentation on deprecated features for more details.
if (!ps.featureSettings.isEnabled(Dep::AncientLet))
warn(
"%s found at %s. This feature is deprecated and will be removed in the future. Use %s to silence this warning.",
"let {",
ps.positions[ps.at(in)],
"--extra-deprecated-features ancient-let"
);
b.attrs.pos = ps.at(in);
b.attrs.recursive = true;
s.pushExpr<ExprSelect>(b.attrs.pos, b.attrs.pos, std::make_unique<ExprAttrs>(std::move(b.attrs)), ps.s.body);
}
};
template<> struct BuildAST<grammar::expr::rec_set> : change_head<BindingsState> {
static void success(const auto & in, BindingsState & b, ExprState & s, State & ps) {
// Before inserting new attrs, check for __override and throw an error
// (the error will initially be a warning to ease migration)
if (!featureSettings.isEnabled(Dep::RecSetOverrides) && b.attrs.attrs.contains(ps.s.overrides)) {
ps.overridesFound(ps.at(in));
}
b.attrs.pos = ps.at(in);
b.attrs.recursive = true;
s.pushExpr<ExprAttrs>(b.attrs.pos, std::move(b.attrs));
}
};
template<> struct BuildAST<grammar::expr::set> : change_head<BindingsState> {
static void success(const auto & in, BindingsState & b, ExprState & s, State & ps) {
b.attrs.pos = ps.at(in);
s.pushExpr<ExprAttrs>(b.attrs.pos, std::move(b.attrs));
}
};
using ListState = std::vector<std::unique_ptr<Expr>>;
template<> struct BuildAST<grammar::expr::list> : change_head<ListState> {
static void success(const auto & in, ListState & ls, ExprState & s, State & ps) {
auto e = std::make_unique<ExprList>();
e->elems = std::move(ls);
s.exprs.emplace_back(ps.at(in), std::move(e));
}
};
template<> struct BuildAST<grammar::expr::list::entry> : change_head<ExprState> {
static void success0(ExprState & e, ListState & s, State & ps) {
s.emplace_back(e.finish(ps).second);
}
};
struct SelectState : SubexprState {
using SubexprState::SubexprState;
PosIdx pos;
ExprSelect * e = nullptr;
};
template<> struct BuildAST<grammar::expr::select::head> {
static void apply(const auto & in, SelectState & s, State & ps) {
s.pos = ps.at(in);
}
};
template<> struct BuildAST<grammar::expr::select::attr> : change_head<AttrState> {
static void success0(AttrState & a, SelectState & s, State &) {
s.e = &s->pushExpr<ExprSelect>(s.pos, s.pos, s->popExprOnly(), std::move(a.attrs), nullptr);
}
};
template<> struct BuildAST<grammar::expr::select::attr_or> {
static void apply0(SelectState & s, State &) {
s.e->def = s->popExprOnly();
}
};
template<> struct BuildAST<grammar::expr::select::as_app_or> {
static void apply(const auto & in, SelectState & s, State & ps) {
std::vector<std::unique_ptr<Expr>> args(1);
args[0] = std::make_unique<ExprVar>(ps.at(in), ps.s.or_);
s->pushExpr<ExprCall>(s.pos, s.pos, s->popExprOnly(), std::move(args));
}
};
template<> struct BuildAST<grammar::expr::select> : change_head<SelectState> {
static void success0(const auto &...) {}
};
struct AppState : SubexprState {
using SubexprState::SubexprState;
PosIdx pos;
ExprCall * e = nullptr;
};
template<> struct BuildAST<grammar::expr::app::select_or_fn> {
static void apply(const auto & in, AppState & s, State & ps) {
s.pos = ps.at(in);
}
};
template<> struct BuildAST<grammar::expr::app::first_arg> {
static void apply(auto & in, AppState & s, State & ps) {
auto arg = s->popExprOnly(), fn = s->popExprOnly();
if ((s.e = dynamic_cast<ExprCall *>(fn.get()))) {
// TODO remove.
// AST compat with old parser, semantics are the same.
// this can happen on occasions such as `<p> <p>` or `a or b or`,
// neither of which are super worth optimizing.
s.e->args.push_back(std::move(arg));
s->exprs.emplace_back(noPos, std::move(fn));
} else {
std::vector<std::unique_ptr<Expr>> args{1};
args[0] = std::move(arg);
s.e = &s->pushExpr<ExprCall>(s.pos, s.pos, std::move(fn), std::move(args));
}
}
};
template<> struct BuildAST<grammar::expr::app::another_arg> {
static void apply0(AppState & s, State & ps) {
s.e->args.push_back(s->popExprOnly());
}
};
template<> struct BuildAST<grammar::expr::app> : change_head<AppState> {
static void success0(const auto &...) {}
};
template<typename Op> struct BuildAST<grammar::expr::operator_<Op>> {
static void apply(const auto & in, ExprState & s, State & ps) {
s.pushOp(ps.at(in), Op{}, ps);
}
};
template<> struct BuildAST<grammar::expr::operator_<grammar::op::has_attr>> : change_head<AttrState> {
static void success(const auto & in, AttrState & a, ExprState & s, State & ps) {
s.pushOp(ps.at(in), ExprState::has_attr{{}, std::move(a.attrs)}, ps);
}
};
template<> struct BuildAST<grammar::expr::lambda::arg> {
static void apply(const auto & in, LambdaState & s, State & ps) {
s.arg = ps.symbols.create(in.string_view());
}
};
template<> struct BuildAST<grammar::expr::lambda> : change_head<LambdaState> {
static void success(const auto & in, LambdaState & l, ExprState & s, State & ps) {
if (l.formals)
l.formals = ps.validateFormals(std::move(l.formals), ps.at(in), l.arg);
s.pushExpr<ExprLambda>(ps.at(in), ps.at(in), l.arg, std::move(l.formals), l->popExprOnly());
}
};
template<> struct BuildAST<grammar::expr::assert_> {
static void apply(const auto & in, ExprState & s, State & ps) {
auto body = s.popExprOnly(), cond = s.popExprOnly();
s.pushExpr<ExprAssert>(ps.at(in), ps.at(in), std::move(cond), std::move(body));
}
};
template<> struct BuildAST<grammar::expr::with> {
static void apply(const auto & in, ExprState & s, State & ps) {
auto body = s.popExprOnly(), scope = s.popExprOnly();
s.pushExpr<ExprWith>(ps.at(in), ps.at(in), std::move(scope), std::move(body));
}
};
template<> struct BuildAST<grammar::expr::let> : change_head<BindingsState> {
static void success(const auto & in, BindingsState & b, ExprState & s, State & ps) {
if (!b.attrs.dynamicAttrs.empty())
throw ParseError({
.msg = HintFmt("dynamic attributes not allowed in let"),
.pos = ps.positions[ps.at(in)]
});
s.pushExpr<ExprLet>(ps.at(in), std::make_unique<ExprAttrs>(std::move(b.attrs)), b->popExprOnly());
}
};
template<> struct BuildAST<grammar::expr::if_> {
static void apply(const auto & in, ExprState & s, State & ps) {
auto else_ = s.popExprOnly(), then = s.popExprOnly(), cond = s.popExprOnly();
s.pushExpr<ExprIf>(ps.at(in), ps.at(in), std::move(cond), std::move(then), std::move(else_));
}
};
template<> struct BuildAST<grammar::expr> : change_head<ExprState> {
static void success0(ExprState & inner, ExprState & outer, State & ps) {
outer.exprs.push_back(inner.finish(ps));
}
};
}
}
// Linter complains that this is a "suspicious include of file with '.cc' extension".
// While that is correct and generally not great, it is one of the less bad options to pick
// in terms of diff noise.
// NOLINTNEXTLINE(bugprone-suspicious-include)
#include "parser-impl1.inc.cc"
namespace nix {
@ -884,7 +38,6 @@ Expr * EvalState::parse(
exprSymbols,
featureSettings,
};
parser::ExprState x;
assert(length >= 2);
assert(text[length - 1] == 0);
@ -893,7 +46,12 @@ Expr * EvalState::parse(
p::string_input<p::tracking_mode::lazy> inp{std::string_view{text, length}, "input"};
try {
p::parse<parser::grammar::root, parser::BuildAST, parser::Control>(inp, x, s);
parser::v1::ExprState x;
p::parse<parser::grammar::v1::root, parser::v1::BuildAST, parser::v1::Control>(inp, x, s);
auto [_pos, result] = x.finish(s);
result->bindVars(*this, staticEnv);
return result.release();
} catch (p::parse_error & e) {
auto pos = e.positions().back();
throw ParseError({
@ -901,10 +59,6 @@ Expr * EvalState::parse(
.pos = positions[s.positions.add(s.origin, pos.byte)]
});
}
auto [_pos, result] = x.finish(s);
result->bindVars(*this, staticEnv);
return result.release();
}
}

View file

@ -6,11 +6,21 @@
namespace nix::parser {
struct StringToken
{
std::string_view s;
bool hasIndentation;
operator std::string_view() const { return s; }
struct IndStringLine {
// String containing only the leading whitespace of the line. May be empty.
std::string_view indentation;
// Position of the line start (before the indentation)
PosIdx pos;
// Whether the line contains anything besides indentation and line break
bool hasContent = false;
std::vector<
std::pair<
PosIdx,
std::variant<std::unique_ptr<Expr>, std::string_view>
>
> parts = {};
};
struct State
@ -27,8 +37,7 @@ struct State
void overridesFound(const PosIdx pos);
void addAttr(ExprAttrs * attrs, AttrPath && attrPath, std::unique_ptr<Expr> e, const PosIdx pos);
std::unique_ptr<Formals> validateFormals(std::unique_ptr<Formals> formals, PosIdx pos = noPos, Symbol arg = {});
std::unique_ptr<Expr> stripIndentation(const PosIdx pos,
std::vector<std::pair<PosIdx, std::variant<std::unique_ptr<Expr>, StringToken>>> && es);
std::unique_ptr<Expr> stripIndentation(const PosIdx pos, std::vector<IndStringLine> && line);
// lazy positioning means we don't get byte offsets directly, in.position() would work
// but also requires line and column (which is expensive)
@ -182,98 +191,87 @@ inline std::unique_ptr<Formals> State::validateFormals(std::unique_ptr<Formals>
return formals;
}
inline std::unique_ptr<Expr> State::stripIndentation(const PosIdx pos,
std::vector<std::pair<PosIdx, std::variant<std::unique_ptr<Expr>, StringToken>>> && es)
inline std::unique_ptr<Expr> State::stripIndentation(
const PosIdx pos,
std::vector<IndStringLine> && lines)
{
if (es.empty()) return std::make_unique<ExprString>("");
/* If the only line is whitespace-only, directly return empty string.
* The rest of the code relies on the final string not being empty.
*/
if (lines.size() == 1 && lines.front().parts.empty()) {
return std::make_unique<ExprString>("");
}
/* Figure out the minimum indentation. Note that by design
whitespace-only final lines are not taken into account. (So
the " " in "\n ''" is ignored, but the " " in "\n foo''" is.) */
bool atStartOfLine = true; /* = seen only whitespace in the current line */
/* If the last line only contains whitespace, trim it to not cause excessive whitespace.
* (Other whitespace-only lines get stripped only of the common indentation, and excess
* whitespace becomes part of the string.)
*/
if (lines.back().parts.empty()) {
lines.back().indentation = {};
}
/* Figure out the minimum indentation. Note that by design
whitespace-only lines are not taken into account. */
size_t minIndent = 1000000;
size_t curIndent = 0;
for (auto & [i_pos, i] : es) {
auto * str = std::get_if<StringToken>(&i);
if (!str || !str->hasIndentation) {
/* Anti-quotations and escaped characters end the current start-of-line whitespace. */
if (atStartOfLine) {
atStartOfLine = false;
if (curIndent < minIndent) minIndent = curIndent;
}
continue;
}
for (size_t j = 0; j < str->s.size(); ++j) {
if (atStartOfLine) {
if (str->s[j] == ' ')
curIndent++;
else if (str->s[j] == '\n') {
/* Empty line, doesn't influence minimum
indentation. */
curIndent = 0;
} else {
atStartOfLine = false;
if (curIndent < minIndent) minIndent = curIndent;
}
} else if (str->s[j] == '\n') {
atStartOfLine = true;
curIndent = 0;
}
for (auto & line : lines) {
if (line.hasContent) {
minIndent = std::min(minIndent, line.indentation.size());
}
}
/* Strip spaces from each line. */
std::vector<std::pair<PosIdx, std::unique_ptr<Expr>>> es2;
atStartOfLine = true;
size_t curDropped = 0;
size_t n = es.size();
auto i = es.begin();
const auto trimExpr = [&] (std::unique_ptr<Expr> e) {
atStartOfLine = false;
curDropped = 0;
es2.emplace_back(i->first, std::move(e));
};
const auto trimString = [&] (const StringToken t) {
std::string s2;
for (size_t j = 0; j < t.s.size(); ++j) {
if (atStartOfLine) {
if (t.s[j] == ' ') {
if (curDropped++ >= minIndent)
s2 += t.s[j];
}
else if (t.s[j] == '\n') {
curDropped = 0;
s2 += t.s[j];
} else {
atStartOfLine = false;
curDropped = 0;
s2 += t.s[j];
}
} else {
s2 += t.s[j];
if (t.s[j] == '\n') atStartOfLine = true;
}
}
/* Remove the last line if it is empty and consists only of
spaces. */
if (n == 1) {
std::string::size_type p = s2.find_last_of('\n');
if (p != std::string::npos && s2.find_first_not_of(' ', p + 1) == std::string::npos)
s2 = std::string(s2, 0, p + 1);
}
es2.emplace_back(i->first, std::make_unique<ExprString>(std::move(s2)));
};
for (; i != es.end(); ++i, --n) {
std::visit(overloaded { trimExpr, trimString }, std::move(i->second));
for (auto & line : lines) {
line.indentation.remove_prefix(std::min(minIndent, line.indentation.size()));
}
/* If this is a single string, then don't do a concatenation. */
if (es2.size() == 1 && dynamic_cast<ExprString *>(es2[0].second.get())) {
return std::move(es2[0].second);
/* Concat the parts together again */
std::vector<std::pair<PosIdx, std::unique_ptr<Expr>>> parts;
/* Accumulator for merging intermediates */
PosIdx merged_pos;
std::string merged = "";
auto push_merged = [&] (PosIdx i_pos, std::string_view str) {
if (merged.empty()) {
merged_pos = i_pos;
}
merged += str;
};
auto flush_merged = [&] () {
if (!merged.empty()) {
parts.emplace_back(merged_pos, std::make_unique<ExprString>(std::string(merged)));
merged.clear();
}
};
for (auto && [li, line] : enumerate(lines)) {
push_merged(line.pos, line.indentation);
for (auto & val : line.parts) {
auto &[i_pos, item] = val;
std::visit(overloaded{
[&](std::string_view str) {
push_merged(i_pos, str);
},
[&](std::unique_ptr<Expr> expr) {
flush_merged();
parts.emplace_back(i_pos, std::move(expr));
},
}, std::move(item));
}
}
return std::make_unique<ExprConcatStrings>(pos, true, std::move(es2));
flush_merged();
/* If this is a single string, then don't do a concatenation.
* (If it's a single expression, still do the ConcatStrings to properly force it being a string.)
*/
if (parts.size() == 1 && dynamic_cast<ExprString *>(parts[0].second.get())) {
return std::move(parts[0].second);
}
return std::make_unique<ExprConcatStrings>(pos, true, std::move(parts));
}
}

View file

@ -394,7 +394,8 @@ static RegisterPrimOp primop_fetchGit({
[Git reference]: https://git-scm.com/book/en/v2/Git-Internals-Git-References
By default, the `ref` value is prefixed with `refs/heads/`.
As of 2.3.0, Nix will not prefix `refs/heads/` if `ref` starts with `refs/`.
As of 2.3.0, Nix will not prefix `refs/heads/` if `ref` starts with `refs/` or
if `ref` looks like a commit hash for backwards compatibility with CppNix 2.3.
- `submodules` (default: `false`)

View file

@ -7,6 +7,32 @@
namespace nix {
void to_json(nlohmann::json & j, const AcceptFlakeConfig & e)
{
if (e == AcceptFlakeConfig::False) {
j = false;
} else if (e == AcceptFlakeConfig::Ask) {
j = "ask";
} else if (e == AcceptFlakeConfig::True) {
j = true;
} else {
abort();
}
}
void from_json(const nlohmann::json & j, AcceptFlakeConfig & e)
{
if (j == false) {
e = AcceptFlakeConfig::False;
} else if (j == "ask") {
e = AcceptFlakeConfig::Ask;
} else if (j == true) {
e = AcceptFlakeConfig::True;
} else {
throw Error("Invalid accept-flake-config value '%s'", std::string(j));
}
}
template<> AcceptFlakeConfig BaseSetting<AcceptFlakeConfig>::parse(const std::string & str, const ApplyConfigOptions & options) const
{
if (str == "true") return AcceptFlakeConfig::True;

View file

@ -13,6 +13,9 @@ namespace nix {
enum class AcceptFlakeConfig { False, Ask, True };
void to_json(nlohmann::json & j, const AcceptFlakeConfig & e);
void from_json(const nlohmann::json & j, AcceptFlakeConfig & e);
struct FetchSettings : public Config
{
FetchSettings();

View file

@ -209,7 +209,7 @@ DownloadFileResult downloadFile(
const std::string & url,
const std::string & name,
bool locked,
const Headers & headers = {});
Headers headers = {});
struct DownloadTarballResult
{

View file

@ -1,3 +1,4 @@
#include "error.hh"
#include "fetchers.hh"
#include "cache.hh"
#include "globals.hh"
@ -257,6 +258,28 @@ std::pair<StorePath, Input> fetchFromWorkdir(ref<Store> store, Input & input, co
}
} // end namespace
static std::optional<Path> resolveRefToCachePath(
Input & input,
const Path & cacheDir,
std::vector<Path> & gitRefFileCandidates,
std::function<bool(const Path&)> condition)
{
if (input.getRef()->starts_with("refs/")) {
Path fullpath = cacheDir + "/" + *input.getRef();
if (condition(fullpath)) {
return fullpath;
}
}
for (auto & candidate : gitRefFileCandidates) {
if (condition(candidate)) {
return candidate;
}
}
return std::nullopt;
}
struct GitInputScheme : InputScheme
{
std::optional<Input> inputFromURL(const ParsedURL & url, bool requireTree) const override
@ -539,10 +562,13 @@ struct GitInputScheme : InputScheme
runProgram("git", true, { "-c", "init.defaultBranch=" + gitInitialBranch, "init", "--bare", repoDir });
}
Path localRefFile =
input.getRef()->compare(0, 5, "refs/") == 0
? cacheDir + "/" + *input.getRef()
: cacheDir + "/refs/heads/" + *input.getRef();
std::vector<Path> gitRefFileCandidates;
for (auto & infix : {"", "tags/", "heads/"}) {
Path p = cacheDir + "/refs/" + infix + *input.getRef();
gitRefFileCandidates.push_back(p);
}
Path localRefFile;
bool doFetch;
time_t now = time(0);
@ -564,29 +590,70 @@ struct GitInputScheme : InputScheme
if (allRefs) {
doFetch = true;
} else {
/* 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;
doFetch = stat(localRefFile.c_str(), &st) != 0 ||
!isCacheFileWithinTtl(now, st);
std::function<bool(const Path&)> condition;
condition = [&now](const Path & path) {
/* 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);
};
if (auto result = resolveRefToCachePath(
input,
cacheDir,
gitRefFileCandidates,
condition
)) {
localRefFile = *result;
doFetch = false;
} else {
doFetch = true;
}
}
}
// When having to fetch, we don't know `localRefFile` yet.
// 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));
// FIXME: git stderr messes up our progress indicator, so
// we're using --quiet for now. Should process its stderr.
auto ref = input.getRef();
std::string fetchRef;
if (allRefs) {
fetchRef = "refs/*";
} else if (
ref->starts_with("refs/")
|| *ref == "HEAD"
|| std::regex_match(*ref, revRegex))
{
fetchRef = *ref;
} else {
fetchRef = "refs/*/" + *ref;
}
try {
auto ref = input.getRef();
auto fetchRef = allRefs
? "refs/*"
: ref->compare(0, 5, "refs/") == 0
? *ref
: ref == "HEAD"
? *ref
: "refs/heads/" + *ref;
runProgram("git", true, { "-C", repoDir, "--git-dir", gitDir, "fetch", "--quiet", "--force", "--", actualUrl, fmt("%s:%s", fetchRef, fetchRef) }, true);
Finally finally([&]() {
if (auto p = resolveRefToCachePath(
input,
cacheDir,
gitRefFileCandidates,
pathExists
)) {
localRefFile = *p;
}
});
// FIXME: git stderr messes up our progress indicator, so
// we're using --quiet for now. Should process its stderr.
runProgram("git", true, {
"-C", repoDir,
"--git-dir", gitDir,
"fetch",
"--quiet",
"--force",
"--", actualUrl, fmt("%s:%s", fetchRef, fetchRef)
}, true);
} catch (Error & e) {
if (!pathExists(localRefFile)) throw;
warn("could not update local clone of Git repository '%s'; continuing with the most recent version", actualUrl);

View file

@ -15,7 +15,7 @@ DownloadFileResult downloadFile(
const std::string & url,
const std::string & name,
bool locked,
const Headers & headers)
Headers headers)
{
// FIXME: check store
@ -40,13 +40,14 @@ DownloadFileResult downloadFile(
if (cached && !cached->expired)
return useCached();
FileTransferRequest request(url);
request.headers = headers;
if (cached)
request.expectedETag = getStrAttr(cached->infoAttrs, "etag");
headers.emplace_back("If-None-Match", getStrAttr(cached->infoAttrs, "etag"));
FileTransferResult res;
std::string data;
try {
res = getFileTransfer()->transfer(request);
auto [meta, content] = getFileTransfer()->download(url, headers);
res = std::move(meta);
data = content->drain();
} catch (FileTransferError & e) {
if (cached) {
warn("%s; using cached version", e.msg());
@ -71,8 +72,8 @@ DownloadFileResult downloadFile(
storePath = std::move(cached->storePath);
} else {
StringSink sink;
sink << dumpString(res.data);
auto hash = hashString(HashType::SHA256, res.data);
sink << dumpString(data);
auto hash = hashString(HashType::SHA256, data);
ValidPathInfo info {
*store,
name,

View file

@ -0,0 +1,41 @@
#include "crash-handler.hh"
#include "fmt.hh"
#include <boost/core/demangle.hpp>
#include <exception>
namespace nix {
namespace {
void onTerminate()
{
std::cerr << "Lix crashed. This is a bug. We would appreciate if you report it along with what caused it at https://git.lix.systems/lix-project/lix/issues with the following information included:\n\n";
try {
std::exception_ptr eptr = std::current_exception();
if (eptr) {
std::rethrow_exception(eptr);
} else {
std::cerr << "std::terminate() called without exception\n";
}
} catch (const std::exception & ex) {
std::cerr << "Exception: " << boost::core::demangle(typeid(ex).name()) << ": " << ex.what() << "\n";
} catch (...) {
std::cerr << "Unknown exception! Spooky.\n";
}
std::cerr << "Stack trace:\n";
nix::printStackTrace();
std::abort();
}
}
void registerCrashHandler()
{
// DO NOT use this for signals. Boost stacktrace is very much not
// async-signal-safe, and in a world with ASLR, addr2line is pointless.
//
// If you want signals, set up a minidump system and do it out-of-process.
std::set_terminate(onTerminate);
}
}

View file

@ -0,0 +1,21 @@
#pragma once
/// @file Crash handler for Lix that prints back traces (hopefully in instances where it is not just going to crash the process itself).
/*
* Author's note: This will probably be partially/fully supplanted by a
* minidump writer like the following once we get our act together on crashes a
* little bit more:
* https://github.com/rust-minidump/minidump-writer
* https://github.com/EmbarkStudios/crash-handling
* (out of process implementation *should* be able to be done on-demand)
*
* Such an out-of-process implementation could then both make minidumps and
* print stack traces for arbitrarily messed-up process states such that we can
* safely give out backtraces for SIGSEGV and other deadly signals.
*/
namespace nix {
/** Registers the Lix crash handler for std::terminate (currently; will support more crashes later). See also detectStackOverflow(). */
void registerCrashHandler();
}

View file

@ -7,7 +7,7 @@ namespace nix {
LogFormat defaultLogFormat = LogFormat::raw;
LogFormat parseLogFormat(const std::string & logFormatStr) {
if (logFormatStr == "raw" || getEnv("NIX_GET_COMPLETIONS"))
if (logFormatStr == "raw")
return LogFormat::raw;
else if (logFormatStr == "raw-with-logs")
return LogFormat::rawWithLogs;

View file

@ -1,5 +1,6 @@
libmain_sources = files(
'common-args.cc',
'crash-handler.cc',
'loggers.cc',
'progress-bar.cc',
'shared.cc',
@ -8,6 +9,7 @@ libmain_sources = files(
libmain_headers = files(
'common-args.hh',
'crash-handler.hh',
'loggers.hh',
'progress-bar.hh',
'shared.hh',

View file

@ -92,7 +92,7 @@ void ProgressBar::resume()
nextWakeup = draw(*state, {});
state.wait_for(quitCV, std::chrono::milliseconds(50));
}
writeLogsToStderr("\r\e[K");
eraseProgressDisplay(*state);
});
}
@ -558,7 +558,8 @@ std::optional<char> ProgressBar::ask(std::string_view msg)
{
auto state(state_.lock());
if (state->paused > 0 || !isatty(STDIN_FILENO)) return {};
std::cerr << fmt("\r\e[K%s ", msg);
eraseProgressDisplay(*state);
std::cerr << msg;
auto s = trim(readLine(STDIN_FILENO));
if (s.size() != 1) return {};
draw(*state, {});

View file

@ -1,3 +1,4 @@
#include "crash-handler.hh"
#include "globals.hh"
#include "shared.hh"
#include "store-api.hh"
@ -118,6 +119,8 @@ static void sigHandler(int signo) { }
void initNix()
{
registerCrashHandler();
/* Turn on buffering for cerr. */
static char buf[1024];
std::cerr.rdbuf()->pubsetbuf(buf, sizeof(buf));
@ -193,20 +196,20 @@ LegacyArgs::LegacyArgs(const std::string & programName,
.longName = "keep-failed",
.shortName ='K',
.description = "Keep temporary directories of failed builds.",
.handler = {&(bool&) settings.keepFailed, true},
.handler = {[&]() { settings.keepFailed.override(true); }},
});
addFlag({
.longName = "keep-going",
.shortName ='k',
.description = "Keep going after a build fails.",
.handler = {&(bool&) settings.keepGoing, true},
.handler = {[&]() { settings.keepGoing.override(true); }},
});
addFlag({
.longName = "fallback",
.description = "Build from source if substitution fails.",
.handler = {&(bool&) settings.tryFallback, true},
.handler = {[&]() { settings.tryFallback.override(true); }},
});
auto intSettingAlias = [&](char shortName, const std::string & longName,
@ -244,7 +247,7 @@ LegacyArgs::LegacyArgs(const std::string & programName,
.longName = "store",
.description = "The URL of the Nix store to use.",
.labels = {"store-uri"},
.handler = {&(std::string&) settings.storeUri},
.handler = {[&](std::string storeUri) { settings.storeUri.override(storeUri); }},
});
}
@ -335,12 +338,15 @@ int handleExceptions(const std::string & programName, std::function<void()> fun)
} catch (BaseError & e) {
logError(e.info());
return e.info().status;
} catch (std::bad_alloc & e) {
} catch (const std::bad_alloc & e) {
printError(error + "out of memory");
return 1;
} catch (std::exception & e) {
printError(error + e.what());
return 1;
} catch (const std::exception & e) {
// Random exceptions bubbling into main are cause for bug reports, crash
std::terminate();
} catch (...) {
// Explicitly do not tolerate non-std exceptions escaping.
std::terminate();
}
return 0;
@ -389,7 +395,7 @@ RunPager::~RunPager()
pid.wait();
}
} catch (...) {
ignoreException();
ignoreExceptionInDestructor();
}
}

View file

@ -111,7 +111,7 @@ struct PrintFreed
/**
* Install a SIGSEGV handler to detect stack overflows.
* Install a SIGSEGV handler to detect stack overflows. See also registerCrashHandler().
*/
void detectStackOverflow();

View file

@ -47,7 +47,7 @@ struct BuildResult
* @todo This should be an entire ErrorInfo object, not just a
* string, for richer information.
*/
std::string errorMsg;
std::string errorMsg = {};
std::string toString() const {
auto strStatus = [&]() {
@ -90,7 +90,7 @@ struct BuildResult
* For derivations, a mapping from the names of the wanted outputs
* to actual paths.
*/
SingleDrvOutputs builtOutputs;
SingleDrvOutputs builtOutputs = {};
/**
* The start/stop times of the build (or one of the rounds, if it

View file

@ -3,7 +3,7 @@
namespace nix {
void commonChildInit()
void commonExecveingChildInit()
{
logger = makeSimpleLogger();

View file

@ -4,8 +4,12 @@
namespace nix {
/**
* Common initialisation performed in child processes.
* Common initialisation performed in child processes that are just going to
* execve.
*
* These processes may not use ReceiveInterrupts as they do not have an
* interrupt receiving thread.
*/
void commonChildInit();
void commonExecveingChildInit();
}

View file

@ -11,7 +11,13 @@
#include "drv-output-substitution-goal.hh"
#include "strings.hh"
#include <boost/outcome/try.hpp>
#include <fstream>
#include <kj/array.h>
#include <kj/async-unix.h>
#include <kj/async.h>
#include <kj/debug.h>
#include <kj/vector.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
@ -65,7 +71,6 @@ DerivationGoal::DerivationGoal(const StorePath & drvPath,
, wantedOutputs(wantedOutputs)
, buildMode(buildMode)
{
state = &DerivationGoal::getDerivation;
name = fmt(
"building of '%s' from .drv file",
DerivedPath::Built { makeConstantStorePathRef(drvPath), wantedOutputs }.to_string(worker.store));
@ -85,7 +90,6 @@ DerivationGoal::DerivationGoal(const StorePath & drvPath, const BasicDerivation
{
this->drv = std::make_unique<Derivation>(drv);
state = &DerivationGoal::haveDerivation;
name = fmt(
"building of '%s' from in-memory derivation",
DerivedPath::Built { makeConstantStorePathRef(drvPath), drv.outputNames() }.to_string(worker.store));
@ -103,17 +107,7 @@ DerivationGoal::~DerivationGoal() noexcept(false)
{
/* Careful: we should never ever throw an exception from a
destructor. */
try { closeLogFile(); } catch (...) { ignoreException(); }
}
std::string DerivationGoal::key()
{
/* Ensure that derivations get built in order of their name,
i.e. a derivation named "aardvark" always comes before
"baboon". And substitution goals always happen before
derivation goals (due to "b$"). */
return "b$" + std::string(drvPath.name()) + "$" + worker.store.printStorePath(drvPath);
try { closeLogFile(); } catch (...) { ignoreExceptionInDestructor(); }
}
@ -124,20 +118,32 @@ void DerivationGoal::killChild()
}
Goal::Finished DerivationGoal::timedOut(Error && ex)
Goal::WorkResult DerivationGoal::timedOut(Error && ex)
{
killChild();
return done(BuildResult::TimedOut, {}, std::move(ex));
}
Goal::WorkResult DerivationGoal::work(bool inBuildSlot)
kj::Promise<Result<Goal::WorkResult>> DerivationGoal::workImpl() noexcept
{
return (this->*state)(inBuildSlot);
KJ_DEFER({
act.reset();
actLock.reset();
builderActivities.clear();
});
BOOST_OUTCOME_CO_TRY(auto result, co_await (useDerivation ? getDerivation() : haveDerivation()));
result.storePath = drvPath;
co_return result;
}
void DerivationGoal::addWantedOutputs(const OutputsSpec & outputs)
bool DerivationGoal::addWantedOutputs(const OutputsSpec & outputs)
{
if (isDone) {
return false;
}
auto newWanted = wantedOutputs.union_(outputs);
switch (needRestart) {
case NeedRestartForMoreOutputs::OutputsUnmodifedDontNeed:
@ -154,32 +160,38 @@ void DerivationGoal::addWantedOutputs(const OutputsSpec & outputs)
break;
};
wantedOutputs = newWanted;
return true;
}
Goal::WorkResult DerivationGoal::getDerivation(bool inBuildSlot)
{
kj::Promise<Result<Goal::WorkResult>> DerivationGoal::getDerivation() noexcept
try {
trace("init");
/* The first thing to do is to make sure that the derivation
exists. If it doesn't, it may be created through a
substitute. */
if (buildMode == bmNormal && worker.evalStore.isValidPath(drvPath)) {
return loadDerivation(inBuildSlot);
co_return co_await loadDerivation();
}
state = &DerivationGoal::loadDerivation;
return WaitForGoals{{worker.goalFactory().makePathSubstitutionGoal(drvPath)}};
(co_await waitForGoals(worker.goalFactory().makePathSubstitutionGoal(drvPath))).value();
co_return co_await loadDerivation();
} catch (...) {
co_return result::failure(std::current_exception());
}
Goal::WorkResult DerivationGoal::loadDerivation(bool inBuildSlot)
{
kj::Promise<Result<Goal::WorkResult>> DerivationGoal::loadDerivation() noexcept
try {
trace("loading derivation");
if (nrFailed != 0) {
return done(BuildResult::MiscFailure, {}, Error("cannot build missing derivation '%s'", worker.store.printStorePath(drvPath)));
return {done(
BuildResult::MiscFailure,
{},
Error("cannot build missing derivation '%s'", worker.store.printStorePath(drvPath))
)};
}
/* `drvPath' should already be a root, but let's be on the safe
@ -201,12 +213,14 @@ Goal::WorkResult DerivationGoal::loadDerivation(bool inBuildSlot)
}
assert(drv);
return haveDerivation(inBuildSlot);
return haveDerivation();
} catch (...) {
return {std::current_exception()};
}
Goal::WorkResult DerivationGoal::haveDerivation(bool inBuildSlot)
{
kj::Promise<Result<Goal::WorkResult>> DerivationGoal::haveDerivation() noexcept
try {
trace("have derivation");
parsedDrv = std::make_unique<ParsedDerivation>(drvPath, *drv);
@ -233,7 +247,7 @@ Goal::WorkResult DerivationGoal::haveDerivation(bool inBuildSlot)
});
}
return gaveUpOnSubstitution(inBuildSlot);
co_return co_await gaveUpOnSubstitution();
}
for (auto & i : drv->outputsAndOptPaths(worker.store))
@ -255,19 +269,19 @@ Goal::WorkResult DerivationGoal::haveDerivation(bool inBuildSlot)
/* If they are all valid, then we're done. */
if (allValid && buildMode == bmNormal) {
return done(BuildResult::AlreadyValid, std::move(validOutputs));
co_return done(BuildResult::AlreadyValid, std::move(validOutputs));
}
/* We are first going to try to create the invalid output paths
through substitutes. If that doesn't work, we'll build
them. */
WaitForGoals result;
kj::Vector<std::pair<GoalPtr, kj::Promise<Result<WorkResult>>>> dependencies;
if (settings.useSubstitutes) {
if (parsedDrv->substitutesAllowed()) {
for (auto & [outputName, status] : initialOutputs) {
if (!status.wanted) continue;
if (!status.known)
result.goals.insert(
dependencies.add(
worker.goalFactory().makeDrvOutputSubstitutionGoal(
DrvOutput{status.outputHash, outputName},
buildMode == bmRepair ? Repair : NoRepair
@ -275,7 +289,7 @@ Goal::WorkResult DerivationGoal::haveDerivation(bool inBuildSlot)
);
else {
auto * cap = getDerivationCA(*drv);
result.goals.insert(worker.goalFactory().makePathSubstitutionGoal(
dependencies.add(worker.goalFactory().makePathSubstitutionGoal(
status.known->path,
buildMode == bmRepair ? Repair : NoRepair,
cap ? std::optional { *cap } : std::nullopt));
@ -286,24 +300,31 @@ Goal::WorkResult DerivationGoal::haveDerivation(bool inBuildSlot)
}
}
if (result.goals.empty()) { /* to prevent hang (no wake-up event) */
return outputsSubstitutionTried(inBuildSlot);
} else {
state = &DerivationGoal::outputsSubstitutionTried;
return result;
if (!dependencies.empty()) { /* to prevent hang (no wake-up event) */
(co_await waitForGoals(dependencies.releaseAsArray())).value();
}
co_return co_await outputsSubstitutionTried();
} catch (...) {
co_return result::failure(std::current_exception());
}
Goal::WorkResult DerivationGoal::outputsSubstitutionTried(bool inBuildSlot)
{
kj::Promise<Result<Goal::WorkResult>> DerivationGoal::outputsSubstitutionTried() noexcept
try {
trace("all outputs substituted (maybe)");
assert(drv->type().isPure());
if (nrFailed > 0 && nrFailed > nrNoSubstituters + nrIncompleteClosure && !settings.tryFallback) {
return done(BuildResult::TransientFailure, {},
Error("some substitutes for the outputs of derivation '%s' failed (usually happens due to networking issues); try '--fallback' to build derivation from source ",
worker.store.printStorePath(drvPath)));
if (nrFailed > 0 && nrFailed > nrNoSubstituters + nrIncompleteClosure && !settings.tryFallback)
{
return {done(
BuildResult::TransientFailure,
{},
Error(
"some substitutes for the outputs of derivation '%s' failed (usually happens due "
"to networking issues); try '--fallback' to build derivation from source ",
worker.store.printStorePath(drvPath)
)
)};
}
/* If the substitutes form an incomplete closure, then we should
@ -337,13 +358,13 @@ Goal::WorkResult DerivationGoal::outputsSubstitutionTried(bool inBuildSlot)
if (needRestart == NeedRestartForMoreOutputs::OutputsAddedDoNeed) {
needRestart = NeedRestartForMoreOutputs::OutputsUnmodifedDontNeed;
return haveDerivation(inBuildSlot);
return haveDerivation();
}
auto [allValid, validOutputs] = checkPathValidity();
if (buildMode == bmNormal && allValid) {
return done(BuildResult::Substituted, std::move(validOutputs));
return {done(BuildResult::Substituted, std::move(validOutputs))};
}
if (buildMode == bmRepair && allValid) {
return repairClosure();
@ -353,15 +374,17 @@ Goal::WorkResult DerivationGoal::outputsSubstitutionTried(bool inBuildSlot)
worker.store.printStorePath(drvPath));
/* Nothing to wait for; tail call */
return gaveUpOnSubstitution(inBuildSlot);
return gaveUpOnSubstitution();
} catch (...) {
return {std::current_exception()};
}
/* At least one of the output paths could not be
produced using a substitute. So we have to build instead. */
Goal::WorkResult DerivationGoal::gaveUpOnSubstitution(bool inBuildSlot)
{
WaitForGoals result;
kj::Promise<Result<Goal::WorkResult>> DerivationGoal::gaveUpOnSubstitution() noexcept
try {
kj::Vector<std::pair<GoalPtr, kj::Promise<Result<WorkResult>>>> dependencies;
/* At this point we are building all outputs, so if more are wanted there
is no need to restart. */
@ -374,7 +397,7 @@ Goal::WorkResult DerivationGoal::gaveUpOnSubstitution(bool inBuildSlot)
addWaiteeDerivedPath = [&](ref<SingleDerivedPath> inputDrv, const DerivedPathMap<StringSet>::ChildNode & inputNode) {
if (!inputNode.value.empty())
result.goals.insert(worker.goalFactory().makeGoal(
dependencies.add(worker.goalFactory().makeGoal(
DerivedPath::Built {
.drvPath = inputDrv,
.outputs = inputNode.value,
@ -419,20 +442,20 @@ Goal::WorkResult DerivationGoal::gaveUpOnSubstitution(bool inBuildSlot)
if (!settings.useSubstitutes)
throw Error("dependency '%s' of '%s' does not exist, and substitution is disabled",
worker.store.printStorePath(i), worker.store.printStorePath(drvPath));
result.goals.insert(worker.goalFactory().makePathSubstitutionGoal(i));
dependencies.add(worker.goalFactory().makePathSubstitutionGoal(i));
}
if (result.goals.empty()) {/* to prevent hang (no wake-up event) */
return inputsRealised(inBuildSlot);
} else {
state = &DerivationGoal::inputsRealised;
return result;
if (!dependencies.empty()) {/* to prevent hang (no wake-up event) */
(co_await waitForGoals(dependencies.releaseAsArray())).value();
}
co_return co_await inputsRealised();
} catch (...) {
co_return result::failure(std::current_exception());
}
Goal::WorkResult DerivationGoal::repairClosure()
{
kj::Promise<Result<Goal::WorkResult>> DerivationGoal::repairClosure() noexcept
try {
assert(drv->type().isPure());
/* If we're repairing, we now know that our own outputs are valid.
@ -467,7 +490,7 @@ Goal::WorkResult DerivationGoal::repairClosure()
}
/* Check each path (slow!). */
WaitForGoals result;
kj::Vector<std::pair<GoalPtr, kj::Promise<Result<WorkResult>>>> dependencies;
for (auto & i : outputClosure) {
if (worker.pathContentsGood(i)) continue;
printError(
@ -475,9 +498,9 @@ Goal::WorkResult DerivationGoal::repairClosure()
worker.store.printStorePath(i), worker.store.printStorePath(drvPath));
auto drvPath2 = outputsToDrv.find(i);
if (drvPath2 == outputsToDrv.end())
result.goals.insert(worker.goalFactory().makePathSubstitutionGoal(i, Repair));
dependencies.add(worker.goalFactory().makePathSubstitutionGoal(i, Repair));
else
result.goals.insert(worker.goalFactory().makeGoal(
dependencies.add(worker.goalFactory().makeGoal(
DerivedPath::Built {
.drvPath = makeConstantStorePathRef(drvPath2->second),
.outputs = OutputsSpec::All { },
@ -485,40 +508,50 @@ Goal::WorkResult DerivationGoal::repairClosure()
bmRepair));
}
if (result.goals.empty()) {
return done(BuildResult::AlreadyValid, assertPathValidity());
if (dependencies.empty()) {
co_return done(BuildResult::AlreadyValid, assertPathValidity());
}
state = &DerivationGoal::closureRepaired;
return result;
(co_await waitForGoals(dependencies.releaseAsArray())).value();
co_return co_await closureRepaired();
} catch (...) {
co_return result::failure(std::current_exception());
}
Goal::WorkResult DerivationGoal::closureRepaired(bool inBuildSlot)
{
kj::Promise<Result<Goal::WorkResult>> DerivationGoal::closureRepaired() noexcept
try {
trace("closure repaired");
if (nrFailed > 0)
throw Error("some paths in the output closure of derivation '%s' could not be repaired",
worker.store.printStorePath(drvPath));
return done(BuildResult::AlreadyValid, assertPathValidity());
return {done(BuildResult::AlreadyValid, assertPathValidity())};
} catch (...) {
return {std::current_exception()};
}
Goal::WorkResult DerivationGoal::inputsRealised(bool inBuildSlot)
{
kj::Promise<Result<Goal::WorkResult>> DerivationGoal::inputsRealised() noexcept
try {
trace("all inputs realised");
if (nrFailed != 0) {
if (!useDerivation)
throw Error("some dependencies of '%s' are missing", worker.store.printStorePath(drvPath));
return done(BuildResult::DependencyFailed, {}, Error(
co_return done(
BuildResult::DependencyFailed,
{},
Error(
"%s dependencies of derivation '%s' failed to build",
nrFailed, worker.store.printStorePath(drvPath)));
nrFailed,
worker.store.printStorePath(drvPath)
)
);
}
if (retrySubstitution == RetrySubstitution::YesNeed) {
retrySubstitution = RetrySubstitution::AlreadyRetried;
return haveDerivation(inBuildSlot);
co_return co_await haveDerivation();
}
/* Gather information necessary for computing the closure and/or
@ -580,11 +613,12 @@ Goal::WorkResult DerivationGoal::inputsRealised(bool inBuildSlot)
worker.store.printStorePath(pathResolved),
});
resolvedDrvGoal = worker.goalFactory().makeDerivationGoal(
auto dependency = worker.goalFactory().makeDerivationGoal(
pathResolved, wantedOutputs, buildMode);
resolvedDrvGoal = dependency.first;
state = &DerivationGoal::resolvedFinished;
return WaitForGoals{{resolvedDrvGoal}};
(co_await waitForGoals(std::move(dependency))).value();
co_return co_await resolvedFinished();
}
std::function<void(const StorePath &, const DerivedPathMap<StringSet>::ChildNode &)> accumInputPaths;
@ -648,8 +682,9 @@ Goal::WorkResult DerivationGoal::inputsRealised(bool inBuildSlot)
/* Okay, try to build. Note that here we don't wait for a build
slot to become available, since we don't need one if there is a
build hook. */
state = &DerivationGoal::tryToBuild;
return tryToBuild(inBuildSlot);
co_return co_await tryToBuild();
} catch (...) {
co_return result::failure(std::current_exception());
}
void DerivationGoal::started()
@ -665,8 +700,9 @@ void DerivationGoal::started()
mcRunningBuilds = worker.runningBuilds.addTemporarily(1);
}
Goal::WorkResult DerivationGoal::tryToBuild(bool inBuildSlot)
{
kj::Promise<Result<Goal::WorkResult>> DerivationGoal::tryToBuild() noexcept
try {
retry:
trace("trying to build");
/* Obtain locks on all output paths, if the paths are known a priori.
@ -700,7 +736,9 @@ Goal::WorkResult DerivationGoal::tryToBuild(bool inBuildSlot)
if (!actLock)
actLock = std::make_unique<Activity>(*logger, lvlWarn, actBuildWaiting,
fmt("waiting for lock on %s", Magenta(showPaths(lockFiles))));
return WaitForAWhile{};
co_await waitForAWhile();
// we can loop very often, and `co_return co_await` always allocates a new frame
goto retry;
}
actLock.reset();
@ -717,7 +755,7 @@ Goal::WorkResult DerivationGoal::tryToBuild(bool inBuildSlot)
if (buildMode != bmCheck && allValid) {
debug("skipping build of derivation '%s', someone beat us to it", worker.store.printStorePath(drvPath));
outputLocks.setDeletion(true);
return done(BuildResult::AlreadyValid, std::move(validOutputs));
co_return done(BuildResult::AlreadyValid, std::move(validOutputs));
}
/* If any of the outputs already exist but are not valid, delete
@ -737,49 +775,63 @@ Goal::WorkResult DerivationGoal::tryToBuild(bool inBuildSlot)
&& settings.maxBuildJobs.get() != 0;
if (!buildLocally) {
auto hookReply = tryBuildHook(inBuildSlot);
auto result = std::visit(
overloaded{
[&](HookReply::Accept & a) -> std::optional<WorkResult> {
/* Yes, it has started doing so. Wait until we get
EOF from the hook. */
actLock.reset();
buildResult.startTime = time(0); // inexact
state = &DerivationGoal::buildDone;
started();
return WaitForWorld{std::move(a.fds), false};
},
[&](HookReply::Postpone) -> std::optional<WorkResult> {
/* 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))));
outputLocks.unlock();
return WaitForAWhile{};
},
[&](HookReply::Decline) -> std::optional<WorkResult> {
/* We should do it ourselves. */
return std::nullopt;
},
},
hookReply);
if (result) {
return std::move(*result);
auto hookReply = tryBuildHook();
switch (hookReply.index()) {
case 0: {
HookReply::Accept & a = std::get<0>(hookReply);
/* Yes, it has started doing so. Wait until we get
EOF from the hook. */
actLock.reset();
buildResult.startTime = time(0); // inexact
started();
auto r = co_await a.promise;
if (r.has_value()) {
co_return co_await buildDone();
} else if (r.has_error()) {
co_return r.assume_error();
} else {
co_return r.assume_exception();
}
}
case 1: {
HookReply::Decline _ [[gnu::unused]] = std::get<1>(hookReply);
break;
}
case 2: {
HookReply::Postpone _ [[gnu::unused]] = std::get<2>(hookReply);
/* 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))));
outputLocks.unlock();
co_await waitForAWhile();
goto retry;
}
default:
// can't static_assert this because HookReply *subclasses* variant and std::variant_size breaks
assert(false && "unexpected hook reply");
}
}
actLock.reset();
state = &DerivationGoal::tryLocalBuild;
return tryLocalBuild(inBuildSlot);
co_return co_await tryLocalBuild();
} catch (...) {
co_return result::failure(std::current_exception());
}
Goal::WorkResult DerivationGoal::tryLocalBuild(bool inBuildSlot) {
kj::Promise<Result<Goal::WorkResult>> DerivationGoal::tryLocalBuild() noexcept
try {
throw Error(
"unable to build with a primary store that isn't a local store; "
"either pass a different '--store' or enable remote builds."
"\nhttps://docs.lix.systems/manual/lix/stable/advanced-topics/distributed-builds.html");
} catch (...) {
return {std::current_exception()};
}
@ -819,7 +871,7 @@ void replaceValidPath(const Path & storePath, const Path & tmpPath)
// attempt to recover
movePath(oldPath, storePath);
} catch (...) {
ignoreException();
ignoreExceptionExceptInterrupt();
}
throw;
}
@ -935,10 +987,11 @@ void runPostBuildHook(
proc.getStdout()->drainInto(sink);
}
Goal::WorkResult DerivationGoal::buildDone(bool inBuildSlot)
{
kj::Promise<Result<Goal::WorkResult>> DerivationGoal::buildDone() noexcept
try {
trace("build done");
slotToken = {};
Finally releaseBuildUser([&](){ this->cleanupHookFinally(); });
cleanupPreChildKill();
@ -954,9 +1007,6 @@ Goal::WorkResult DerivationGoal::buildDone(bool inBuildSlot)
buildResult.timesBuilt++;
buildResult.stopTime = time(0);
/* So the child is gone now. */
worker.childTerminated(this);
/* Close the read side of the logger pipe. */
closeReadPipes();
@ -1030,7 +1080,7 @@ Goal::WorkResult DerivationGoal::buildDone(bool inBuildSlot)
outputLocks.setDeletion(true);
outputLocks.unlock();
return done(BuildResult::Built, std::move(builtOutputs));
return {done(BuildResult::Built, std::move(builtOutputs))};
} catch (BuildError & e) {
outputLocks.unlock();
@ -1051,12 +1101,14 @@ Goal::WorkResult DerivationGoal::buildDone(bool inBuildSlot)
BuildResult::PermanentFailure;
}
return done(st, {}, std::move(e));
return {done(st, {}, std::move(e))};
}
} catch (...) {
return {std::current_exception()};
}
Goal::WorkResult DerivationGoal::resolvedFinished(bool inBuildSlot)
{
kj::Promise<Result<Goal::WorkResult>> DerivationGoal::resolvedFinished() noexcept
try {
trace("resolved derivation finished");
assert(resolvedDrvGoal);
@ -1123,10 +1175,12 @@ Goal::WorkResult DerivationGoal::resolvedFinished(bool inBuildSlot)
if (status == BuildResult::AlreadyValid)
status = BuildResult::ResolvesToAlreadyValid;
return done(status, std::move(builtOutputs));
return {done(status, std::move(builtOutputs))};
} catch (...) {
return {std::current_exception()};
}
HookReply DerivationGoal::tryBuildHook(bool inBuildSlot)
HookReply DerivationGoal::tryBuildHook()
{
if (!worker.hook.available || !useDerivation) return HookReply::Decline{};
@ -1138,7 +1192,7 @@ HookReply DerivationGoal::tryBuildHook(bool inBuildSlot)
/* Send the request to the hook. */
worker.hook.instance->sink
<< "try"
<< (inBuildSlot ? 1 : 0)
<< (slotToken.valid() ? 1 : 0)
<< drv->platform
<< worker.store.printStorePath(drvPath)
<< parsedDrv->getRequiredSystemFeatures();
@ -1165,6 +1219,7 @@ HookReply DerivationGoal::tryBuildHook(bool inBuildSlot)
else {
s += "\n";
writeLogsToStderr(s);
logger->log(lvlInfo, s);
}
}
@ -1224,12 +1279,8 @@ HookReply DerivationGoal::tryBuildHook(bool inBuildSlot)
/* Create the log file and pipe. */
Path logFile = openLogFile();
std::set<int> fds;
fds.insert(hook->fromHook.get());
fds.insert(hook->builderOut.get());
builderOutFD = &hook->builderOut;
return HookReply::Accept{std::move(fds)};
return HookReply::Accept{handleChildOutput()};
}
@ -1289,23 +1340,69 @@ void DerivationGoal::closeLogFile()
}
Goal::WorkResult DerivationGoal::handleChildOutput(int fd, std::string_view data)
Goal::WorkResult DerivationGoal::tooMuchLogs()
{
assert(builderOutFD);
killChild();
return done(
BuildResult::LogLimitExceeded, {},
Error("%s killed after writing more than %d bytes of log output",
getName(), settings.maxLogSize));
}
auto tooMuchLogs = [&] {
killChild();
return done(
BuildResult::LogLimitExceeded, {},
Error("%s killed after writing more than %d bytes of log output",
getName(), settings.maxLogSize));
};
struct DerivationGoal::InputStream final : private kj::AsyncObject
{
int fd;
kj::UnixEventPort::FdObserver observer;
InputStream(kj::UnixEventPort & ep, int fd)
: fd(fd)
, observer(ep, fd, kj::UnixEventPort::FdObserver::OBSERVE_READ)
{
int flags = fcntl(fd, F_GETFL);
if (flags < 0) {
throw SysError("fcntl(F_GETFL) failed on fd %i", fd);
}
if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) {
throw SysError("fcntl(F_SETFL) failed on fd %i", fd);
}
}
kj::Promise<std::string_view> read(kj::ArrayPtr<char> buffer)
{
const auto res = ::read(fd, buffer.begin(), buffer.size());
// closing a pty endpoint causes EIO on the other endpoint. stock kj streams
// do not handle this and throw exceptions we can't ask for errno instead :(
// (we can't use `errno` either because kj may well have mangled it by now.)
if (res == 0 || (res == -1 && errno == EIO)) {
return std::string_view{};
}
KJ_NONBLOCKING_SYSCALL(res) {}
if (res > 0) {
return std::string_view{buffer.begin(), static_cast<size_t>(res)};
}
return observer.whenBecomesReadable().then([this, buffer] {
return read(buffer);
});
}
};
kj::Promise<Outcome<void, Goal::WorkResult>> DerivationGoal::handleBuilderOutput(InputStream & in) noexcept
try {
auto buf = kj::heapArray<char>(4096);
while (true) {
auto data = co_await in.read(buf);
lastChildActivity = worker.aio.provider->getTimer().now();
if (data.empty()) {
co_return result::success();
}
// local & `ssh://`-builds are dealt with here.
if (fd == builderOutFD->get()) {
logSize += data.size();
if (settings.maxLogSize && logSize > settings.maxLogSize) {
return tooMuchLogs();
co_return tooMuchLogs();
}
for (auto c : data)
@ -1320,10 +1417,22 @@ Goal::WorkResult DerivationGoal::handleChildOutput(int fd, std::string_view data
}
if (logSink) (*logSink)(data);
return StillAlive{};
}
} catch (...) {
co_return std::current_exception();
}
kj::Promise<Outcome<void, Goal::WorkResult>> DerivationGoal::handleHookOutput(InputStream & in) noexcept
try {
auto buf = kj::heapArray<char>(4096);
while (true) {
auto data = co_await in.read(buf);
lastChildActivity = worker.aio.provider->getTimer().now();
if (data.empty()) {
co_return result::success();
}
if (hook && fd == hook->fromHook.get()) {
for (auto c : data)
if (c == '\n') {
auto json = parseJSONMessage(currentHookLine);
@ -1339,7 +1448,7 @@ Goal::WorkResult DerivationGoal::handleChildOutput(int fd, std::string_view data
(fields.size() > 0 ? fields[0].get<std::string>() : "") + "\n";
logSize += logLine.size();
if (settings.maxLogSize && logSize > settings.maxLogSize) {
return tooMuchLogs();
co_return tooMuchLogs();
}
(*logSink)(logLine);
} else if (type == resSetPhase && ! fields.is_null()) {
@ -1363,16 +1472,83 @@ Goal::WorkResult DerivationGoal::handleChildOutput(int fd, std::string_view data
} else
currentHookLine += c;
}
return StillAlive{};
} catch (...) {
co_return std::current_exception();
}
kj::Promise<Outcome<void, Goal::WorkResult>> DerivationGoal::handleChildOutput() noexcept
try {
assert(builderOutFD);
void DerivationGoal::handleEOF(int fd)
auto builderIn = kj::heap<InputStream>(worker.aio.unixEventPort, builderOutFD->get());
kj::Own<InputStream> hookIn;
if (hook) {
hookIn = kj::heap<InputStream>(worker.aio.unixEventPort, hook->fromHook.get());
}
auto handlers = handleChildStreams(*builderIn, hookIn.get()).attach(std::move(builderIn), std::move(hookIn));
if (respectsTimeouts() && settings.buildTimeout != 0) {
handlers = handlers.exclusiveJoin(
worker.aio.provider->getTimer()
.afterDelay(settings.buildTimeout.get() * kj::SECONDS)
.then([this]() -> Outcome<void, WorkResult> {
return timedOut(
Error("%1% timed out after %2% seconds", name, settings.buildTimeout)
);
})
);
}
return handlers.then([this](auto r) -> Outcome<void, WorkResult> {
if (!currentLogLine.empty()) flushLine();
return r;
});
} catch (...) {
return {std::current_exception()};
}
kj::Promise<Outcome<void, Goal::WorkResult>> DerivationGoal::monitorForSilence() noexcept
{
if (!currentLogLine.empty()) flushLine();
while (true) {
const auto stash = lastChildActivity;
auto waitUntil = lastChildActivity + settings.maxSilentTime.get() * kj::SECONDS;
co_await worker.aio.provider->getTimer().atTime(waitUntil);
if (lastChildActivity == stash) {
co_return timedOut(
Error("%1% timed out after %2% seconds of silence", name, settings.maxSilentTime)
);
}
}
}
kj::Promise<Outcome<void, Goal::WorkResult>>
DerivationGoal::handleChildStreams(InputStream & builderIn, InputStream * hookIn) noexcept
{
lastChildActivity = worker.aio.provider->getTimer().now();
auto handlers = kj::joinPromisesFailFast([&] {
kj::Vector<kj::Promise<Outcome<void, WorkResult>>> parts{2};
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) {
BOOST_OUTCOME_CO_TRYV(r);
}
co_return result::success();
}
void DerivationGoal::flushLine()
{
@ -1513,11 +1689,13 @@ SingleDrvOutputs DerivationGoal::assertPathValidity()
}
Goal::Finished DerivationGoal::done(
Goal::WorkResult DerivationGoal::done(
BuildResult::Status status,
SingleDrvOutputs builtOutputs,
std::optional<Error> ex)
{
isDone = true;
outputLocks.unlock();
buildResult.status = status;
if (ex)
@ -1548,7 +1726,7 @@ Goal::Finished DerivationGoal::done(
logError(ex->info());
}
return Finished{
return WorkResult{
.exitCode = buildResult.success() ? ecSuccess : ecFailed,
.result = buildResult,
.ex = ex ? std::make_shared<Error>(std::move(*ex)) : nullptr,
@ -1587,5 +1765,4 @@ void DerivationGoal::waiteeDone(GoalPtr waitee)
}
}
}
}

View file

@ -8,6 +8,7 @@
#include "store-api.hh"
#include "pathlocks.hh"
#include "goal.hh"
#include <kj/time.h>
namespace nix {
@ -17,7 +18,7 @@ struct HookInstance;
struct HookReplyBase {
struct [[nodiscard]] Accept {
std::set<int> fds;
kj::Promise<Outcome<void, Goal::WorkResult>> promise;
};
struct [[nodiscard]] Decline {};
struct [[nodiscard]] Postpone {};
@ -62,7 +63,7 @@ struct InitialOutputStatus {
struct InitialOutput {
bool wanted;
Hash outputHash;
std::optional<InitialOutputStatus> known;
std::optional<InitialOutputStatus> known = {};
};
/**
@ -70,6 +71,14 @@ struct InitialOutput {
*/
struct DerivationGoal : public Goal
{
struct InputStream;
/**
* Whether this goal has completed. Completed goals can not be
* asked for more outputs, a new goal must be created instead.
*/
bool isDone = false;
/**
* Whether to use an on-disk .drv file.
*/
@ -175,6 +184,11 @@ struct DerivationGoal : public Goal
std::map<std::string, InitialOutput> initialOutputs;
/**
* Build result.
*/
BuildResult buildResult;
/**
* File descriptor for the log file.
*/
@ -213,9 +227,6 @@ struct DerivationGoal : public Goal
*/
std::optional<DerivationType> derivationType;
typedef WorkResult (DerivationGoal::*GoalState)(bool inBuildSlot);
GoalState state;
BuildMode buildMode;
NotifyingCounter<uint64_t>::Bump mcExpectedBuilds, mcRunningBuilds;
@ -242,37 +253,35 @@ struct DerivationGoal : public Goal
BuildMode buildMode = bmNormal);
virtual ~DerivationGoal() noexcept(false);
Finished timedOut(Error && ex) override;
WorkResult timedOut(Error && ex);
std::string key() override;
WorkResult work(bool inBuildSlot) override;
kj::Promise<Result<WorkResult>> workImpl() noexcept override;
/**
* Add wanted outputs to an already existing derivation goal.
*/
void addWantedOutputs(const OutputsSpec & outputs);
bool addWantedOutputs(const OutputsSpec & outputs);
/**
* The states.
*/
WorkResult getDerivation(bool inBuildSlot);
WorkResult loadDerivation(bool inBuildSlot);
WorkResult haveDerivation(bool inBuildSlot);
WorkResult outputsSubstitutionTried(bool inBuildSlot);
WorkResult gaveUpOnSubstitution(bool inBuildSlot);
WorkResult closureRepaired(bool inBuildSlot);
WorkResult inputsRealised(bool inBuildSlot);
WorkResult tryToBuild(bool inBuildSlot);
virtual WorkResult tryLocalBuild(bool inBuildSlot);
WorkResult buildDone(bool inBuildSlot);
kj::Promise<Result<WorkResult>> getDerivation() noexcept;
kj::Promise<Result<WorkResult>> loadDerivation() noexcept;
kj::Promise<Result<WorkResult>> haveDerivation() noexcept;
kj::Promise<Result<WorkResult>> outputsSubstitutionTried() noexcept;
kj::Promise<Result<WorkResult>> gaveUpOnSubstitution() noexcept;
kj::Promise<Result<WorkResult>> closureRepaired() noexcept;
kj::Promise<Result<WorkResult>> inputsRealised() noexcept;
kj::Promise<Result<WorkResult>> tryToBuild() noexcept;
virtual kj::Promise<Result<WorkResult>> tryLocalBuild() noexcept;
kj::Promise<Result<WorkResult>> buildDone() noexcept;
WorkResult resolvedFinished(bool inBuildSlot);
kj::Promise<Result<WorkResult>> resolvedFinished() noexcept;
/**
* Is the build hook willing to perform the build?
*/
HookReply tryBuildHook(bool inBuildSlot);
HookReply tryBuildHook();
virtual int getChildStatus();
@ -312,13 +321,19 @@ struct DerivationGoal : public Goal
virtual void cleanupPostOutputsRegisteredModeCheck();
virtual void cleanupPostOutputsRegisteredModeNonCheck();
/**
* Callback used by the worker to write to the log.
*/
WorkResult handleChildOutput(int fd, std::string_view data) override;
void handleEOF(int fd) override;
protected:
kj::TimePoint lastChildActivity = kj::minValue;
kj::Promise<Outcome<void, WorkResult>> handleChildOutput() noexcept;
kj::Promise<Outcome<void, WorkResult>>
handleChildStreams(InputStream & builderIn, InputStream * hookIn) noexcept;
kj::Promise<Outcome<void, WorkResult>> handleBuilderOutput(InputStream & in) noexcept;
kj::Promise<Outcome<void, WorkResult>> handleHookOutput(InputStream & in) noexcept;
kj::Promise<Outcome<void, WorkResult>> monitorForSilence() noexcept;
WorkResult tooMuchLogs();
void flushLine();
public:
/**
* Wrappers around the corresponding Store methods that first consult the
* derivation. This is currently needed because when there is no drv file
@ -346,17 +361,22 @@ struct DerivationGoal : public Goal
*/
virtual void killChild();
WorkResult repairClosure();
kj::Promise<Result<WorkResult>> repairClosure() noexcept;
void started();
Finished done(
WorkResult done(
BuildResult::Status status,
SingleDrvOutputs builtOutputs = {},
std::optional<Error> ex = {});
void waiteeDone(GoalPtr waitee) override;
virtual bool respectsTimeouts()
{
return false;
}
StorePathSet exportReferences(const StorePathSet & storePaths);
JobCategory jobCategory() const override {

View file

@ -4,6 +4,9 @@
#include "worker.hh"
#include "substitution-goal.hh"
#include "signals.hh"
#include <kj/array.h>
#include <kj/async.h>
#include <kj/vector.h>
namespace nix {
@ -16,31 +19,32 @@ DrvOutputSubstitutionGoal::DrvOutputSubstitutionGoal(
: Goal(worker, isDependency)
, id(id)
{
state = &DrvOutputSubstitutionGoal::init;
name = fmt("substitution of '%s'", id.to_string());
trace("created");
}
Goal::WorkResult DrvOutputSubstitutionGoal::init(bool inBuildSlot)
{
kj::Promise<Result<Goal::WorkResult>> DrvOutputSubstitutionGoal::workImpl() noexcept
try {
trace("init");
/* If the derivation already exists, were done */
if (worker.store.queryRealisation(id)) {
return Finished{ecSuccess, std::move(buildResult)};
co_return WorkResult{ecSuccess};
}
subs = settings.useSubstitutes ? getDefaultSubstituters() : std::list<ref<Store>>();
return tryNext(inBuildSlot);
co_return co_await tryNext();
} catch (...) {
co_return result::failure(std::current_exception());
}
Goal::WorkResult DrvOutputSubstitutionGoal::tryNext(bool inBuildSlot)
{
kj::Promise<Result<Goal::WorkResult>> DrvOutputSubstitutionGoal::tryNext() noexcept
try {
trace("trying next substituter");
if (!inBuildSlot) {
return WaitForSlot{};
if (!slotToken.valid()) {
slotToken = co_await worker.substitutions.acquire();
}
maintainRunningSubstitutions = worker.runningSubstitutions.addTemporarily(1);
@ -57,7 +61,7 @@ Goal::WorkResult DrvOutputSubstitutionGoal::tryNext(bool inBuildSlot)
/* Hack: don't indicate failure if there were no substituters.
In that case the calling derivation should just do a
build. */
return Finished{substituterFailed ? ecFailed : ecNoSubstituters, std::move(buildResult)};
co_return WorkResult{substituterFailed ? ecFailed : ecNoSubstituters};
}
sub = subs.front();
@ -67,23 +71,26 @@ Goal::WorkResult DrvOutputSubstitutionGoal::tryNext(bool inBuildSlot)
some other error occurs), so it must not touch `this`. So put
the shared state in a separate refcounted object. */
downloadState = std::make_shared<DownloadState>();
downloadState->outPipe.create();
auto pipe = kj::newPromiseAndCrossThreadFulfiller<void>();
downloadState->outPipe = kj::mv(pipe.fulfiller);
downloadState->result =
std::async(std::launch::async, [downloadState{downloadState}, id{id}, sub{sub}] {
Finally updateStats([&]() { downloadState->outPipe->fulfill(); });
ReceiveInterrupts receiveInterrupts;
Finally updateStats([&]() { downloadState->outPipe.writeSide.close(); });
return sub->queryRealisation(id);
});
state = &DrvOutputSubstitutionGoal::realisationFetched;
return WaitForWorld{{downloadState->outPipe.readSide.get()}, true};
co_await pipe.promise;
co_return co_await realisationFetched();
} catch (...) {
co_return result::failure(std::current_exception());
}
Goal::WorkResult DrvOutputSubstitutionGoal::realisationFetched(bool inBuildSlot)
{
worker.childTerminated(this);
kj::Promise<Result<Goal::WorkResult>> DrvOutputSubstitutionGoal::realisationFetched() noexcept
try {
maintainRunningSubstitutions.reset();
slotToken = {};
try {
outputInfo = downloadState->result.get();
@ -93,10 +100,10 @@ Goal::WorkResult DrvOutputSubstitutionGoal::realisationFetched(bool inBuildSlot)
}
if (!outputInfo) {
return tryNext(inBuildSlot);
co_return co_await tryNext();
}
WaitForGoals result;
kj::Vector<std::pair<GoalPtr, kj::Promise<Result<WorkResult>>>> dependencies;
for (const auto & [depId, depPath] : outputInfo->dependentRealisations) {
if (depId != id) {
if (auto localOutputInfo = worker.store.queryRealisation(depId);
@ -110,56 +117,46 @@ Goal::WorkResult DrvOutputSubstitutionGoal::realisationFetched(bool inBuildSlot)
worker.store.printStorePath(localOutputInfo->outPath),
worker.store.printStorePath(depPath)
);
return tryNext(inBuildSlot);
co_return co_await tryNext();
}
result.goals.insert(worker.goalFactory().makeDrvOutputSubstitutionGoal(depId));
dependencies.add(worker.goalFactory().makeDrvOutputSubstitutionGoal(depId));
}
}
result.goals.insert(worker.goalFactory().makePathSubstitutionGoal(outputInfo->outPath));
dependencies.add(worker.goalFactory().makePathSubstitutionGoal(outputInfo->outPath));
if (result.goals.empty()) {
return outPathValid(inBuildSlot);
} else {
state = &DrvOutputSubstitutionGoal::outPathValid;
return result;
if (!dependencies.empty()) {
(co_await waitForGoals(dependencies.releaseAsArray())).value();
}
co_return co_await outPathValid();
} catch (...) {
co_return result::failure(std::current_exception());
}
Goal::WorkResult DrvOutputSubstitutionGoal::outPathValid(bool inBuildSlot)
{
kj::Promise<Result<Goal::WorkResult>> DrvOutputSubstitutionGoal::outPathValid() noexcept
try {
assert(outputInfo);
trace("output path substituted");
if (nrFailed > 0) {
debug("The output path of the derivation output '%s' could not be substituted", id.to_string());
return Finished{
return {WorkResult{
nrNoSubstituters > 0 || nrIncompleteClosure > 0 ? ecIncompleteClosure : ecFailed,
std::move(buildResult),
};
}};
}
worker.store.registerDrvOutput(*outputInfo);
return finished();
} catch (...) {
return {std::current_exception()};
}
Goal::WorkResult DrvOutputSubstitutionGoal::finished()
{
kj::Promise<Result<Goal::WorkResult>> DrvOutputSubstitutionGoal::finished() noexcept
try {
trace("finished");
return Finished{ecSuccess, std::move(buildResult)};
return {WorkResult{ecSuccess}};
} catch (...) {
return {std::current_exception()};
}
std::string DrvOutputSubstitutionGoal::key()
{
/* "a$" ensures substitution goals happen before derivation
goals. */
return "a$" + std::string(id.to_string());
}
Goal::WorkResult DrvOutputSubstitutionGoal::work(bool inBuildSlot)
{
return (this->*state)(inBuildSlot);
}
}

View file

@ -45,7 +45,7 @@ class DrvOutputSubstitutionGoal : public Goal {
struct DownloadState
{
Pipe outPipe;
kj::Own<kj::CrossThreadPromiseFulfiller<void>> outPipe;
std::future<std::shared_ptr<const Realisation>> result;
};
@ -65,20 +65,12 @@ public:
std::optional<ContentAddress> ca = std::nullopt
);
typedef WorkResult (DrvOutputSubstitutionGoal::*GoalState)(bool inBuildSlot);
GoalState state;
kj::Promise<Result<WorkResult>> tryNext() noexcept;
kj::Promise<Result<WorkResult>> realisationFetched() noexcept;
kj::Promise<Result<WorkResult>> outPathValid() noexcept;
kj::Promise<Result<WorkResult>> finished() noexcept;
WorkResult init(bool inBuildSlot);
WorkResult tryNext(bool inBuildSlot);
WorkResult realisationFetched(bool inBuildSlot);
WorkResult outPathValid(bool inBuildSlot);
WorkResult finished();
Finished timedOut(Error && ex) override { abort(); };
std::string key() override;
WorkResult work(bool inBuildSlot) override;
kj::Promise<Result<WorkResult>> workImpl() noexcept override;
JobCategory jobCategory() const override {
return JobCategory::Substitution;

View file

@ -8,38 +8,36 @@ namespace nix {
void Store::buildPaths(const std::vector<DerivedPath> & reqs, BuildMode buildMode, std::shared_ptr<Store> evalStore)
{
Worker worker(*this, evalStore ? *evalStore : *this);
auto aio = kj::setupAsyncIo();
auto goals = worker.run([&](GoalFactory & gf) {
Goals goals;
auto results = processGoals(*this, evalStore ? *evalStore : *this, aio, [&](GoalFactory & gf) {
Worker::Targets goals;
for (auto & br : reqs)
goals.insert(gf.makeGoal(br, buildMode));
goals.emplace_back(gf.makeGoal(br, buildMode));
return goals;
});
}).wait(aio.waitScope).value();
StringSet failed;
std::shared_ptr<Error> ex;
for (auto & i : goals) {
if (i->ex) {
for (auto & [i, result] : results.goals) {
if (result.ex) {
if (ex)
logError(i->ex->info());
logError(result.ex->info());
else
ex = i->ex;
ex = result.ex;
}
if (i->exitCode != Goal::ecSuccess) {
if (auto i2 = dynamic_cast<DerivationGoal *>(i.get()))
failed.insert(printStorePath(i2->drvPath));
else if (auto i2 = dynamic_cast<PathSubstitutionGoal *>(i.get()))
failed.insert(printStorePath(i2->storePath));
if (result.exitCode != Goal::ecSuccess) {
if (result.storePath)
failed.insert(printStorePath(*result.storePath));
}
}
if (failed.size() == 1 && ex) {
ex->withExitStatus(worker.failingExitStatus());
ex->withExitStatus(results.failingExitStatus);
throw std::move(*ex);
} else if (!failed.empty()) {
if (ex) logError(ex->info());
throw Error(worker.failingExitStatus(), "build of %s failed", concatStringsSep(", ", quoteStrings(failed)));
throw Error(results.failingExitStatus, "build of %s failed", concatStringsSep(", ", quoteStrings(failed)));
}
}
@ -48,23 +46,20 @@ std::vector<KeyedBuildResult> Store::buildPathsWithResults(
BuildMode buildMode,
std::shared_ptr<Store> evalStore)
{
Worker worker(*this, evalStore ? *evalStore : *this);
std::vector<std::pair<const DerivedPath &, GoalPtr>> state;
auto aio = kj::setupAsyncIo();
auto goals = worker.run([&](GoalFactory & gf) {
Goals goals;
auto goals = processGoals(*this, evalStore ? *evalStore : *this, aio, [&](GoalFactory & gf) {
Worker::Targets goals;
for (const auto & req : reqs) {
auto goal = gf.makeGoal(req, buildMode);
goals.insert(goal);
state.push_back({req, goal});
goals.emplace_back(gf.makeGoal(req, buildMode));
}
return goals;
});
}).wait(aio.waitScope).value().goals;
std::vector<KeyedBuildResult> results;
for (auto & [req, goalPtr] : state)
results.emplace_back(goalPtr->buildResult.restrictTo(req));
for (auto && [goalIdx, req] : enumerate(reqs))
results.emplace_back(goals[goalIdx].result.restrictTo(req));
return results;
}
@ -72,14 +67,16 @@ std::vector<KeyedBuildResult> Store::buildPathsWithResults(
BuildResult Store::buildDerivation(const StorePath & drvPath, const BasicDerivation & drv,
BuildMode buildMode)
{
Worker worker(*this, *this);
auto aio = kj::setupAsyncIo();
try {
auto goals = worker.run([&](GoalFactory & gf) -> Goals {
return Goals{gf.makeBasicDerivationGoal(drvPath, drv, OutputsSpec::All{}, buildMode)};
});
auto goal = *goals.begin();
return goal->buildResult.restrictTo(DerivedPath::Built {
auto results = processGoals(*this, *this, aio, [&](GoalFactory & gf) {
Worker::Targets goals;
goals.emplace_back(gf.makeBasicDerivationGoal(drvPath, drv, OutputsSpec::All{}, buildMode));
return goals;
}).wait(aio.waitScope).value();
auto & result = results.goals.begin()->second;
return result.result.restrictTo(DerivedPath::Built {
.drvPath = makeConstantStorePathRef(drvPath),
.outputs = OutputsSpec::All {},
});
@ -97,48 +94,55 @@ void Store::ensurePath(const StorePath & path)
/* If the path is already valid, we're done. */
if (isValidPath(path)) return;
Worker worker(*this, *this);
auto aio = kj::setupAsyncIo();
auto goals =
worker.run([&](GoalFactory & gf) { return Goals{gf.makePathSubstitutionGoal(path)}; });
auto goal = *goals.begin();
auto results = processGoals(*this, *this, aio, [&](GoalFactory & gf) {
Worker::Targets goals;
goals.emplace_back(gf.makePathSubstitutionGoal(path));
return goals;
}).wait(aio.waitScope).value();
auto & result = results.goals.begin()->second;
if (goal->exitCode != Goal::ecSuccess) {
if (goal->ex) {
goal->ex->withExitStatus(worker.failingExitStatus());
throw std::move(*goal->ex);
if (result.exitCode != Goal::ecSuccess) {
if (result.ex) {
result.ex->withExitStatus(results.failingExitStatus);
throw std::move(*result.ex);
} else
throw Error(worker.failingExitStatus(), "path '%s' does not exist and cannot be created", printStorePath(path));
throw Error(results.failingExitStatus, "path '%s' does not exist and cannot be created", printStorePath(path));
}
}
void Store::repairPath(const StorePath & path)
{
Worker worker(*this, *this);
auto aio = kj::setupAsyncIo();
auto goals = worker.run([&](GoalFactory & gf) {
return Goals{gf.makePathSubstitutionGoal(path, Repair)};
});
auto goal = *goals.begin();
auto results = processGoals(*this, *this, aio, [&](GoalFactory & gf) {
Worker::Targets goals;
goals.emplace_back(gf.makePathSubstitutionGoal(path, Repair));
return goals;
}).wait(aio.waitScope).value();
auto & result = results.goals.begin()->second;
if (goal->exitCode != Goal::ecSuccess) {
if (result.exitCode != Goal::ecSuccess) {
/* Since substituting the path didn't work, if we have a valid
deriver, then rebuild the deriver. */
auto info = queryPathInfo(path);
if (info->deriver && isValidPath(*info->deriver)) {
worker.run([&](GoalFactory & gf) {
return Goals{gf.makeGoal(
processGoals(*this, *this, aio, [&](GoalFactory & gf) {
Worker::Targets goals;
goals.emplace_back(gf.makeGoal(
DerivedPath::Built{
.drvPath = makeConstantStorePathRef(*info->deriver),
// FIXME: Should just build the specific output we need.
.outputs = OutputsSpec::All{},
},
bmRepair
)};
});
));
return goals;
}).wait(aio.waitScope).value();
} else
throw Error(worker.failingExitStatus(), "cannot repair path '%s'", printStorePath(path));
throw Error(results.failingExitStatus, "cannot repair path '%s'", printStorePath(path));
}
}

View file

@ -1,18 +1,79 @@
#include "goal.hh"
#include "async-collect.hh"
#include "worker.hh"
#include <boost/outcome/try.hpp>
#include <kj/time.h>
namespace nix {
bool CompareGoalPtrs::operator() (const GoalPtr & a, const GoalPtr & b) const {
std::string s1 = a->key();
std::string s2 = b->key();
return s1 < s2;
}
void Goal::trace(std::string_view s)
{
debug("%1%: %2%", name, s);
}
kj::Promise<void> Goal::waitForAWhile()
{
trace("wait for a while");
/* If we are polling goals that are waiting for a lock, then wake
up after a few seconds at most. */
return worker.aio.provider->getTimer().afterDelay(settings.pollInterval.get() * kj::SECONDS);
}
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");
cleanup();
co_return std::move(result);
} catch (...) {
co_return result::failure(std::current_exception());
}
kj::Promise<Result<void>>
Goal::waitForGoals(kj::Array<std::pair<GoalPtr, kj::Promise<Result<WorkResult>>>> dependencies) noexcept
try {
auto left = dependencies.size();
for (auto & [dep, p] : dependencies) {
p = p.then([this, dep, &left](auto _result) -> Result<WorkResult> {
BOOST_OUTCOME_TRY(auto result, _result);
left--;
trace(fmt("waitee '%s' done; %d left", dep->name, left));
if (result.exitCode != Goal::ecSuccess) ++nrFailed;
if (result.exitCode == Goal::ecNoSubstituters) ++nrNoSubstituters;
if (result.exitCode == Goal::ecIncompleteClosure) ++nrIncompleteClosure;
return std::move(result);
}).eagerlyEvaluate(nullptr);
}
auto collectDeps = asyncCollect(std::move(dependencies));
while (auto item = co_await collectDeps.next()) {
auto & [dep, _result] = *item;
BOOST_OUTCOME_CO_TRY(auto result, _result);
waiteeDone(dep);
if (result.exitCode == ecFailed && !settings.keepGoing) {
co_return result::success();
}
}
co_return result::success();
} catch (...) {
co_return result::failure(std::current_exception());
}
}

View file

@ -1,9 +1,13 @@
#pragma once
///@file
#include "async-semaphore.hh"
#include "result.hh"
#include "types.hh"
#include "store-api.hh"
#include "build-result.hh"
#include <concepts> // IWYU pragma: keep
#include <kj/async.h>
namespace nix {
@ -17,22 +21,11 @@ class Worker;
* A pointer to a goal.
*/
typedef std::shared_ptr<Goal> GoalPtr;
typedef std::weak_ptr<Goal> WeakGoalPtr;
struct CompareGoalPtrs {
bool operator() (const GoalPtr & a, const GoalPtr & b) const;
};
/**
* Set of goals.
*/
typedef std::set<GoalPtr, CompareGoalPtrs> Goals;
typedef std::set<WeakGoalPtr, std::owner_less<WeakGoalPtr>> WeakGoals;
/**
* A map of paths to goals (and the other way around).
*/
typedef std::map<StorePath, WeakGoalPtr> WeakGoalMap;
typedef std::set<GoalPtr> Goals;
/**
* Used as a hint to the worker on how to schedule a particular goal. For example,
@ -67,17 +60,6 @@ struct Goal
*/
const bool isDependency;
/**
* Goals that this goal is waiting for.
*/
Goals waitees;
/**
* Goals waiting for this one to finish. Must use weak pointers
* here to prevent cycles.
*/
WeakGoals waiters;
/**
* Number of goals we are/were waiting for that have failed.
*/
@ -100,57 +82,40 @@ struct Goal
*/
std::string name;
/**
* Whether the goal is finished.
*/
std::optional<ExitCode> exitCode;
/**
* Build result.
*/
BuildResult buildResult;
protected:
AsyncSemaphore::Token slotToken;
public:
struct [[nodiscard]] StillAlive {};
struct [[nodiscard]] WaitForSlot {};
struct [[nodiscard]] WaitForAWhile {};
struct [[nodiscard]] ContinueImmediately {};
struct [[nodiscard]] WaitForGoals {
Goals goals;
};
struct [[nodiscard]] WaitForWorld {
std::set<int> fds;
bool inBuildSlot;
};
struct [[nodiscard]] Finished {
struct [[nodiscard]] WorkResult {
ExitCode exitCode;
BuildResult result;
std::shared_ptr<Error> ex;
BuildResult result = {};
std::shared_ptr<Error> ex = {};
bool permanentFailure = false;
bool timedOut = false;
bool hashMismatch = false;
bool checkMismatch = false;
/// Store path this goal relates to. Will be set to drvPath for
/// derivations, or the substituted store path for substitions.
std::optional<StorePath> storePath = {};
};
struct [[nodiscard]] WorkResult : std::variant<
StillAlive,
WaitForSlot,
WaitForAWhile,
ContinueImmediately,
WaitForGoals,
WaitForWorld,
Finished>
protected:
kj::Promise<void> waitForAWhile();
kj::Promise<Result<void>>
waitForGoals(kj::Array<std::pair<GoalPtr, kj::Promise<Result<WorkResult>>>> dependencies) noexcept;
template<std::derived_from<Goal>... G>
kj::Promise<Result<void>>
waitForGoals(std::pair<std::shared_ptr<G>, kj::Promise<Result<WorkResult>>>... goals) noexcept
{
WorkResult() = delete;
using variant::variant;
};
return waitForGoals(
kj::arrOf<std::pair<GoalPtr, kj::Promise<Result<WorkResult>>>>(std::move(goals)...)
);
}
/**
* Exception containing an error message, if any.
*/
std::shared_ptr<Error> ex;
virtual kj::Promise<Result<WorkResult>> workImpl() noexcept = 0;
public:
explicit Goal(Worker & worker, bool isDependency)
: worker(worker)
, isDependency(isDependency)
@ -161,24 +126,10 @@ public:
trace("goal destroyed");
}
virtual WorkResult work(bool inBuildSlot) = 0;
kj::Promise<Result<WorkResult>> work() noexcept;
virtual void waiteeDone(GoalPtr waitee) { }
virtual WorkResult handleChildOutput(int fd, std::string_view data)
{
abort();
}
virtual void handleEOF(int fd)
{
}
virtual bool respectsTimeouts()
{
return false;
}
void trace(std::string_view s);
std::string getName() const
@ -186,15 +137,6 @@ public:
return name;
}
/**
* Callback in case of a timeout. It should wake up its waiters,
* get rid of any running child processes that are being monitored
* by the worker (important!), etc.
*/
virtual Finished timedOut(Error && ex) = 0;
virtual std::string key() = 0;
virtual void cleanup() { }
/**

View file

@ -1,4 +1,5 @@
#include "child.hh"
#include "error.hh"
#include "file-system.hh"
#include "globals.hh"
#include "hook-instance.hh"
@ -44,7 +45,7 @@ HookInstance::HookInstance()
if (dup2(fromHook_.writeSide.get(), STDERR_FILENO) == -1)
throw SysError("cannot pipe standard error into log file");
commonChildInit();
commonExecveingChildInit();
if (chdir("/") == -1) throw SysError("changing into /");
@ -73,7 +74,7 @@ HookInstance::HookInstance()
sink = FdSink(toHook.get());
std::map<std::string, Config::SettingInfo> settings;
globalConfig.getSettings(settings);
globalConfig.getSettings(settings, true);
for (auto & setting : settings)
sink << 1 << setting.first << setting.second.value;
sink << 0;
@ -86,7 +87,7 @@ HookInstance::~HookInstance()
toHook.reset();
if (pid) pid.kill();
} catch (...) {
ignoreException();
ignoreExceptionInDestructor();
}
}

View file

@ -1,4 +1,5 @@
#include "local-derivation-goal.hh"
#include "error.hh"
#include "indirect-root-store.hh"
#include "machines.hh"
#include "store-api.hh"
@ -98,9 +99,9 @@ LocalDerivationGoal::~LocalDerivationGoal() noexcept(false)
{
/* Careful: we should never ever throw an exception from a
destructor. */
try { deleteTmpDir(false); } catch (...) { ignoreException(); }
try { killChild(); } catch (...) { ignoreException(); }
try { stopDaemon(); } catch (...) { ignoreException(); }
try { deleteTmpDir(false); } catch (...) { ignoreExceptionInDestructor(); }
try { killChild(); } catch (...) { ignoreExceptionInDestructor(); }
try { stopDaemon(); } catch (...) { ignoreExceptionInDestructor(); }
}
@ -121,8 +122,6 @@ LocalStore & LocalDerivationGoal::getLocalStore()
void LocalDerivationGoal::killChild()
{
if (pid) {
worker.childTerminated(this);
/* If we're using a build user, then there is a tricky race
condition: if we kill the build user before the child has
done its setuid() to the build user uid, then it won't be
@ -149,17 +148,18 @@ void LocalDerivationGoal::killSandbox(bool getStats)
}
Goal::WorkResult LocalDerivationGoal::tryLocalBuild(bool inBuildSlot)
{
kj::Promise<Result<Goal::WorkResult>> LocalDerivationGoal::tryLocalBuild() noexcept
try {
retry:
#if __APPLE__
additionalSandboxProfile = parsedDrv->getStringAttr("__sandboxProfile").value_or("");
#endif
if (!inBuildSlot) {
state = &DerivationGoal::tryToBuild;
if (!slotToken.valid()) {
outputLocks.unlock();
if (0U != settings.maxBuildJobs) {
return WaitForSlot{};
if (worker.localBuilds.capacity() > 0) {
slotToken = co_await worker.localBuilds.acquire();
co_return co_await tryToBuild();
}
if (getMachines().empty()) {
throw Error(
@ -214,7 +214,9 @@ Goal::WorkResult LocalDerivationGoal::tryLocalBuild(bool inBuildSlot)
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))));
return WaitForAWhile{};
co_await waitForAWhile();
// we can loop very often, and `co_return co_await` always allocates a new frame
goto retry;
}
}
@ -243,22 +245,29 @@ Goal::WorkResult LocalDerivationGoal::tryLocalBuild(bool inBuildSlot)
try {
/* Okay, we have to build. */
auto fds = startBuilder();
/* This state will be reached when we get EOF on the child's
log pipe. */
state = &DerivationGoal::buildDone;
auto promise = startBuilder();
started();
return WaitForWorld{std::move(fds), true};
auto r = co_await promise;
if (r.has_value()) {
// all good so far
} else if (r.has_error()) {
co_return r.assume_error();
} else {
co_return r.assume_exception();
}
} catch (BuildError & e) {
outputLocks.unlock();
buildUser.reset();
auto report = done(BuildResult::InputRejected, {}, std::move(e));
report.permanentFailure = true;
return report;
co_return report;
}
co_return co_await buildDone();
} catch (...) {
co_return result::failure(std::current_exception());
}
@ -388,7 +397,9 @@ void LocalDerivationGoal::cleanupPostOutputsRegisteredModeNonCheck()
cleanupPostOutputsRegisteredModeCheck();
}
std::set<int> LocalDerivationGoal::startBuilder()
// NOTE this one isn't noexcept because it's called from places that expect
// exceptions to signal failure to launch. we should change this some time.
kj::Promise<Outcome<void, Goal::WorkResult>> LocalDerivationGoal::startBuilder()
{
if ((buildUser && buildUser->getUIDCount() != 1)
#if __linux__
@ -777,7 +788,7 @@ std::set<int> LocalDerivationGoal::startBuilder()
msgs.push_back(std::move(msg));
}
return {builderOutPTY.get()};
return handleChildOutput();
}
@ -1239,7 +1250,7 @@ void LocalDerivationGoal::startDaemon()
NotTrusted, daemon::Recursive);
debug("terminated daemon connection");
} catch (SysError &) {
ignoreException();
ignoreExceptionExceptInterrupt();
}
});
@ -1353,19 +1364,26 @@ void LocalDerivationGoal::runChild()
try { /* child */
commonChildInit();
commonExecveingChildInit();
setupSyscallFilter();
bool setUser = true;
/* Make the contents of netrc available to builtin:fetchurl
(which may run under a different uid and/or in a sandbox). */
/* Make the contents of netrc and the CA certificate bundle
available to builtin:fetchurl (which may run under a
different uid and/or in a sandbox). */
std::string netrcData;
try {
if (drv->isBuiltin() && drv->builder == "builtin:fetchurl" && !derivationType->isSandboxed())
std::string caFileData;
if (drv->isBuiltin() && drv->builder == "builtin:fetchurl" && !derivationType->isSandboxed()) {
try {
netrcData = readFile(settings.netrcFile);
} catch (SysError &) { }
} catch (SysError &) { }
try {
caFileData = readFile(settings.caFile);
} catch (SysError &) { }
}
#if __linux__
if (useChroot) {
@ -1800,7 +1818,7 @@ void LocalDerivationGoal::runChild()
e.second = rewriteStrings(e.second, inputRewrites);
if (drv->builder == "builtin:fetchurl")
builtinFetchurl(drv2, netrcData);
builtinFetchurl(drv2, netrcData, caFileData);
else if (drv->builder == "builtin:buildenv")
builtinBuildenv(drv2);
else if (drv->builder == "builtin:unpack-channel")
@ -2003,6 +2021,8 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs()
OutputPathMap finalOutputs;
std::vector<std::pair<Path, std::optional<Path>>> nondeterministic;
for (auto & outputName : sortedOutputNames) {
auto output = get(drv->outputs, outputName);
auto scratchPath = get(scratchOutputs, outputName);
@ -2289,20 +2309,20 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs()
buildUser ? buildUser->getGID() : getgid(),
finalDestPath, dst, worker.store.printStorePath(drvPath), tmpDir);
throw NotDeterministic("derivation '%s' may not be deterministic: output '%s' differs from '%s'",
worker.store.printStorePath(drvPath), worker.store.toRealPath(finalDestPath), dst);
nondeterministic.push_back(std::make_pair(worker.store.toRealPath(finalDestPath), dst));
} else
throw NotDeterministic("derivation '%s' may not be deterministic: output '%s' differs",
worker.store.printStorePath(drvPath), worker.store.toRealPath(finalDestPath));
nondeterministic.push_back(std::make_pair(worker.store.toRealPath(finalDestPath), std::nullopt));
}
/* Since we verified the build, it's now ultimately trusted. */
if (!oldInfo.ultimate) {
else if (!oldInfo.ultimate) {
oldInfo.ultimate = true;
localStore.signPathInfo(oldInfo);
localStore.registerValidPaths({{oldInfo.path, oldInfo}});
}
/* Don't register anything, since we already have the
previous versions which we're comparing. */
continue;
}
@ -2333,6 +2353,18 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs()
}
if (buildMode == bmCheck) {
if (!nondeterministic.empty()) {
std::ostringstream msg;
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);
} else {
msg << HintFmt("\n output '%s' differs", oldPath.c_str());
}
}
throw NotDeterministic(msg.str());
}
/* In case of fixed-output derivations, if there are
mismatches on `--check` an error must be thrown as this is
also a source for non-determinism. */

View file

@ -182,7 +182,7 @@ struct LocalDerivationGoal : public DerivationGoal
* Create a LocalDerivationGoal without an on-disk .drv file,
* possibly a platform-specific subclass
*/
static std::shared_ptr<LocalDerivationGoal> makeLocalDerivationGoal(
static std::unique_ptr<LocalDerivationGoal> makeLocalDerivationGoal(
const StorePath & drvPath,
const OutputsSpec & wantedOutputs,
Worker & worker,
@ -194,7 +194,7 @@ struct LocalDerivationGoal : public DerivationGoal
* Create a LocalDerivationGoal for an on-disk .drv file,
* possibly a platform-specific subclass
*/
static std::shared_ptr<LocalDerivationGoal> makeLocalDerivationGoal(
static std::unique_ptr<LocalDerivationGoal> makeLocalDerivationGoal(
const StorePath & drvPath,
const BasicDerivation & drv,
const OutputsSpec & wantedOutputs,
@ -213,12 +213,12 @@ struct LocalDerivationGoal : public DerivationGoal
/**
* The additional states.
*/
WorkResult tryLocalBuild(bool inBuildSlot) override;
kj::Promise<Result<WorkResult>> tryLocalBuild() noexcept override;
/**
* Start building a derivation.
*/
std::set<int> startBuilder();
kj::Promise<Outcome<void, WorkResult>> startBuilder();
/**
* Fill in the environment for the builder.

View file

@ -3,6 +3,9 @@
#include "nar-info.hh"
#include "signals.hh"
#include "finally.hh"
#include <boost/outcome/try.hpp>
#include <kj/array.h>
#include <kj/vector.h>
namespace nix {
@ -18,7 +21,6 @@ PathSubstitutionGoal::PathSubstitutionGoal(
, repair(repair)
, ca(ca)
{
state = &PathSubstitutionGoal::init;
name = fmt("substitution of '%s'", worker.store.printStorePath(this->storePath));
trace("created");
maintainExpectedSubstitutions = worker.expectedSubstitutions.addTemporarily(1);
@ -31,35 +33,29 @@ PathSubstitutionGoal::~PathSubstitutionGoal()
}
Goal::Finished PathSubstitutionGoal::done(
Goal::WorkResult PathSubstitutionGoal::done(
ExitCode result,
BuildResult::Status status,
std::optional<std::string> errorMsg)
{
buildResult.status = status;
BuildResult buildResult{.status = status};
if (errorMsg) {
debug(*errorMsg);
buildResult.errorMsg = *errorMsg;
}
return Finished{result, std::move(buildResult)};
return WorkResult{result, std::move(buildResult)};
}
Goal::WorkResult PathSubstitutionGoal::work(bool inBuildSlot)
{
return (this->*state)(inBuildSlot);
}
Goal::WorkResult PathSubstitutionGoal::init(bool inBuildSlot)
{
kj::Promise<Result<Goal::WorkResult>> PathSubstitutionGoal::workImpl() noexcept
try {
trace("init");
worker.store.addTempRoot(storePath);
/* If the path already exists we're done. */
if (!repair && worker.store.isValidPath(storePath)) {
return done(ecSuccess, BuildResult::AlreadyValid);
co_return done(ecSuccess, BuildResult::AlreadyValid);
}
if (settings.readOnlyMode)
@ -67,12 +63,16 @@ Goal::WorkResult PathSubstitutionGoal::init(bool inBuildSlot)
subs = settings.useSubstitutes ? getDefaultSubstituters() : std::list<ref<Store>>();
return tryNext(inBuildSlot);
BOOST_OUTCOME_CO_TRY(auto result, co_await tryNext());
result.storePath = storePath;
co_return result;
} catch (...) {
co_return result::failure(std::current_exception());
}
Goal::WorkResult PathSubstitutionGoal::tryNext(bool inBuildSlot)
{
kj::Promise<Result<Goal::WorkResult>> PathSubstitutionGoal::tryNext() noexcept
try {
trace("trying next substituter");
cleanup();
@ -87,7 +87,7 @@ Goal::WorkResult PathSubstitutionGoal::tryNext(bool inBuildSlot)
/* Hack: don't indicate failure if there were no substituters.
In that case the calling derivation should just do a
build. */
return done(
co_return done(
substituterFailed ? ecFailed : ecNoSubstituters,
BuildResult::NoSubstituters,
fmt("path '%s' is required, but there is no substituter that can build it", worker.store.printStorePath(storePath)));
@ -103,26 +103,28 @@ Goal::WorkResult PathSubstitutionGoal::tryNext(bool inBuildSlot)
if (sub->storeDir == worker.store.storeDir)
assert(subPath == storePath);
} else if (sub->storeDir != worker.store.storeDir) {
return tryNext(inBuildSlot);
co_return co_await tryNext();
}
try {
// FIXME: make async
info = sub->queryPathInfo(subPath ? *subPath : storePath);
} catch (InvalidPath &) {
return tryNext(inBuildSlot);
} catch (SubstituterDisabled &) {
if (settings.tryFallback) {
return tryNext(inBuildSlot);
do {
try {
// FIXME: make async
info = sub->queryPathInfo(subPath ? *subPath : storePath);
break;
} catch (InvalidPath &) {
} catch (SubstituterDisabled &) {
if (!settings.tryFallback) {
throw;
}
} catch (Error & e) {
if (settings.tryFallback) {
logError(e.info());
} else {
throw;
}
}
throw;
} catch (Error & e) {
if (settings.tryFallback) {
logError(e.info());
return tryNext(inBuildSlot);
}
throw;
}
co_return co_await tryNext();
} while (false);
if (info->path != storePath) {
if (info->isContentAddressed(*sub) && info->references.empty()) {
@ -132,7 +134,7 @@ Goal::WorkResult PathSubstitutionGoal::tryNext(bool inBuildSlot)
} else {
printError("asked '%s' for '%s' but got '%s'",
sub->getUri(), worker.store.printStorePath(storePath), sub->printStorePath(info->path));
return tryNext(inBuildSlot);
co_return co_await tryNext();
}
}
@ -153,65 +155,67 @@ Goal::WorkResult PathSubstitutionGoal::tryNext(bool inBuildSlot)
{
warn("ignoring substitute for '%s' from '%s', as it's not signed by any of the keys in 'trusted-public-keys'",
worker.store.printStorePath(storePath), sub->getUri());
return tryNext(inBuildSlot);
co_return co_await tryNext();
}
/* To maintain the closure invariant, we first have to realise the
paths referenced by this one. */
WaitForGoals result;
kj::Vector<std::pair<GoalPtr, kj::Promise<Result<WorkResult>>>> dependencies;
for (auto & i : info->references)
if (i != storePath) /* ignore self-references */
result.goals.insert(worker.goalFactory().makePathSubstitutionGoal(i));
dependencies.add(worker.goalFactory().makePathSubstitutionGoal(i));
if (result.goals.empty()) {/* to prevent hang (no wake-up event) */
return referencesValid(inBuildSlot);
} else {
state = &PathSubstitutionGoal::referencesValid;
return result;
if (!dependencies.empty()) {/* to prevent hang (no wake-up event) */
(co_await waitForGoals(dependencies.releaseAsArray())).value();
}
co_return co_await referencesValid();
} catch (...) {
co_return result::failure(std::current_exception());
}
Goal::WorkResult PathSubstitutionGoal::referencesValid(bool inBuildSlot)
{
kj::Promise<Result<Goal::WorkResult>> PathSubstitutionGoal::referencesValid() noexcept
try {
trace("all references realised");
if (nrFailed > 0) {
return done(
return {done(
nrNoSubstituters > 0 || nrIncompleteClosure > 0 ? ecIncompleteClosure : ecFailed,
BuildResult::DependencyFailed,
fmt("some references of path '%s' could not be realised", worker.store.printStorePath(storePath)));
fmt("some references of path '%s' could not be realised", worker.store.printStorePath(storePath)))};
}
for (auto & i : info->references)
if (i != storePath) /* ignore self-references */
assert(worker.store.isValidPath(i));
state = &PathSubstitutionGoal::tryToRun;
return tryToRun(inBuildSlot);
return tryToRun();
} catch (...) {
return {std::current_exception()};
}
Goal::WorkResult PathSubstitutionGoal::tryToRun(bool inBuildSlot)
{
kj::Promise<Result<Goal::WorkResult>> PathSubstitutionGoal::tryToRun() noexcept
try {
trace("trying to run");
if (!inBuildSlot) {
return WaitForSlot{};
if (!slotToken.valid()) {
slotToken = co_await worker.substitutions.acquire();
}
maintainRunningSubstitutions = worker.runningSubstitutions.addTemporarily(1);
outPipe.create();
auto pipe = kj::newPromiseAndCrossThreadFulfiller<void>();
outPipe = kj::mv(pipe.fulfiller);
thr = std::async(std::launch::async, [this]() {
/* Wake up the worker loop when we're done. */
Finally updateStats([this]() { outPipe->fulfill(); });
auto & fetchPath = subPath ? *subPath : storePath;
try {
ReceiveInterrupts receiveInterrupts;
/* Wake up the worker loop when we're done. */
Finally updateStats([this]() { outPipe.writeSide.close(); });
Activity act(*logger, actSubstitute, Logger::Fields{worker.store.printStorePath(storePath), sub->getUri()});
PushActivity pact(act.id);
@ -227,37 +231,39 @@ Goal::WorkResult PathSubstitutionGoal::tryToRun(bool inBuildSlot)
}
});
state = &PathSubstitutionGoal::finished;
return WaitForWorld{{outPipe.readSide.get()}, true};
co_await pipe.promise;
co_return co_await finished();
} catch (...) {
co_return result::failure(std::current_exception());
}
Goal::WorkResult PathSubstitutionGoal::finished(bool inBuildSlot)
{
kj::Promise<Result<Goal::WorkResult>> PathSubstitutionGoal::finished() noexcept
try {
trace("substitute finished");
worker.childTerminated(this);
try {
thr.get();
} catch (std::exception & e) {
printError(e.what());
/* Cause the parent build to fail unless --fallback is given,
or the substitute has disappeared. The latter case behaves
the same as the substitute never having existed in the
first place. */
do {
try {
throw;
} catch (SubstituteGone &) {
} catch (...) {
substituterFailed = true;
}
slotToken = {};
thr.get();
break;
} catch (std::exception & e) {
printError(e.what());
/* Cause the parent build to fail unless --fallback is given,
or the substitute has disappeared. The latter case behaves
the same as the substitute never having existed in the
first place. */
try {
throw;
} catch (SubstituteGone &) {
} catch (...) {
substituterFailed = true;
}
}
/* Try the next substitute. */
state = &PathSubstitutionGoal::tryNext;
return tryNext(inBuildSlot);
}
co_return co_await tryNext();
} while (false);
worker.markContentsGood(storePath);
@ -274,13 +280,9 @@ Goal::WorkResult PathSubstitutionGoal::finished(bool inBuildSlot)
worker.doneNarSize += maintainExpectedNar.delta();
maintainExpectedNar.reset();
return done(ecSuccess, BuildResult::Substituted);
}
Goal::WorkResult PathSubstitutionGoal::handleChildOutput(int fd, std::string_view data)
{
return StillAlive{};
co_return done(ecSuccess, BuildResult::Substituted);
} catch (...) {
co_return result::failure(std::current_exception());
}
@ -290,12 +292,9 @@ void PathSubstitutionGoal::cleanup()
if (thr.valid()) {
// FIXME: signal worker thread to quit.
thr.get();
worker.childTerminated(this);
}
outPipe.close();
} catch (...) {
ignoreException();
ignoreExceptionInDestructor();
}
}

View file

@ -46,7 +46,7 @@ struct PathSubstitutionGoal : public Goal
/**
* Pipe for the substituter's standard output.
*/
Pipe outPipe;
kj::Own<kj::CrossThreadPromiseFulfiller<void>> outPipe;
/**
* The substituter thread.
@ -67,15 +67,12 @@ struct PathSubstitutionGoal : public Goal
NotifyingCounter<uint64_t>::Bump maintainExpectedSubstitutions,
maintainRunningSubstitutions, maintainExpectedNar, maintainExpectedDownload;
typedef WorkResult (PathSubstitutionGoal::*GoalState)(bool inBuildSlot);
GoalState state;
/**
* Content address for recomputing store path
*/
std::optional<ContentAddress> ca;
Finished done(
WorkResult done(
ExitCode result,
BuildResult::Status status,
std::optional<std::string> errorMsg = {});
@ -90,32 +87,15 @@ public:
);
~PathSubstitutionGoal();
Finished timedOut(Error && ex) override { abort(); };
/**
* We prepend "a$" to the key name to ensure substitution goals
* happen before derivation goals.
*/
std::string key() override
{
return "a$" + std::string(storePath.name()) + "$" + worker.store.printStorePath(storePath);
}
WorkResult work(bool inBuildSlot) override;
kj::Promise<Result<WorkResult>> workImpl() noexcept override;
/**
* The states.
*/
WorkResult init(bool inBuildSlot);
WorkResult tryNext(bool inBuildSlot);
WorkResult referencesValid(bool inBuildSlot);
WorkResult tryToRun(bool inBuildSlot);
WorkResult finished(bool inBuildSlot);
/**
* Callback used by the worker to write to the log.
*/
WorkResult handleChildOutput(int fd, std::string_view data) override;
kj::Promise<Result<WorkResult>> tryNext() noexcept;
kj::Promise<Result<WorkResult>> referencesValid() noexcept;
kj::Promise<Result<WorkResult>> tryToRun() noexcept;
kj::Promise<Result<WorkResult>> finished() noexcept;
/* Called by destructor, can't be overridden */
void cleanup() override final;

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