Compare commits

..

85 commits

Author SHA1 Message Date
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 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
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
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
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
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
99 changed files with 2371 additions and 1617 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

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

@ -147,3 +147,6 @@ winter:
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,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,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,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

@ -217,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.

View file

@ -47,6 +47,7 @@
# 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',
@ -492,12 +493,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',
)
@ -593,10 +588,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

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,
)

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

View file

@ -22,7 +22,6 @@
doxygen,
editline-lix ? __forDefaults.editline-lix,
editline,
fetchpatch,
git,
gtest,
jq,
@ -100,7 +99,7 @@
(lib.enableFeature (ncurses != null) "termcap")
];
nativeBuildInputs = (prev.nativeBuildInputs or [ ]) ++ [ ncurses ];
buildInputs = (prev.buildInputs or [ ]) ++ [ ncurses ];
});
build-release-notes = callPackage ./maintainers/build-release-notes.nix { };
@ -111,7 +110,7 @@
}:
# gcc miscompiles coroutines at least until 13.2, possibly longer
assert stdenv.cc.isClang || lintInsteadOfBuild;
assert stdenv.cc.isClang || lintInsteadOfBuild || internalApiDocs;
let
inherit (__forDefaults) canRunInstalled;

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

@ -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({

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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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));
@ -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

@ -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,24 @@ void DerivationGoal::killChild()
}
Goal::Finished DerivationGoal::timedOut(Error && ex)
Goal::WorkResult DerivationGoal::timedOut(Error && ex)
{
killChild();
return done(BuildResult::TimedOut, {}, std::move(ex));
}
kj::Promise<Result<Goal::WorkResult>> DerivationGoal::work(bool inBuildSlot) noexcept
kj::Promise<Result<Goal::WorkResult>> DerivationGoal::workImpl() noexcept
{
return (this->*state)(inBuildSlot);
return useDerivation ? getDerivation() : haveDerivation();
}
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,10 +152,11 @@ void DerivationGoal::addWantedOutputs(const OutputsSpec & outputs)
break;
};
wantedOutputs = newWanted;
return true;
}
kj::Promise<Result<Goal::WorkResult>> DerivationGoal::getDerivation(bool inBuildSlot) noexcept
kj::Promise<Result<Goal::WorkResult>> DerivationGoal::getDerivation() noexcept
try {
trace("init");
@ -165,18 +164,17 @@ try {
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 (...) {
return {std::current_exception()};
co_return result::failure(std::current_exception());
}
kj::Promise<Result<Goal::WorkResult>> DerivationGoal::loadDerivation(bool inBuildSlot) noexcept
kj::Promise<Result<Goal::WorkResult>> DerivationGoal::loadDerivation() noexcept
try {
trace("loading derivation");
@ -207,13 +205,13 @@ try {
}
assert(drv);
return haveDerivation(inBuildSlot);
return haveDerivation();
} catch (...) {
return {std::current_exception()};
}
kj::Promise<Result<Goal::WorkResult>> DerivationGoal::haveDerivation(bool inBuildSlot) noexcept
kj::Promise<Result<Goal::WorkResult>> DerivationGoal::haveDerivation() noexcept
try {
trace("have derivation");
@ -241,7 +239,7 @@ try {
});
}
return gaveUpOnSubstitution(inBuildSlot);
co_return co_await gaveUpOnSubstitution();
}
for (auto & i : drv->outputsAndOptPaths(worker.store))
@ -263,19 +261,19 @@ try {
/* 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
@ -283,7 +281,7 @@ try {
);
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));
@ -294,17 +292,15 @@ try {
}
}
if (result.goals.empty()) { /* to prevent hang (no wake-up event) */
return outputsSubstitutionTried(inBuildSlot);
} else {
state = &DerivationGoal::outputsSubstitutionTried;
return {std::move(result)};
if (!dependencies.empty()) { /* to prevent hang (no wake-up event) */
(co_await waitForGoals(dependencies.releaseAsArray())).value();
}
co_return co_await outputsSubstitutionTried();
} catch (...) {
return {std::current_exception()};
co_return result::failure(std::current_exception());
}
kj::Promise<Result<Goal::WorkResult>> DerivationGoal::outputsSubstitutionTried(bool inBuildSlot) noexcept
kj::Promise<Result<Goal::WorkResult>> DerivationGoal::outputsSubstitutionTried() noexcept
try {
trace("all outputs substituted (maybe)");
@ -354,7 +350,7 @@ try {
if (needRestart == NeedRestartForMoreOutputs::OutputsAddedDoNeed) {
needRestart = NeedRestartForMoreOutputs::OutputsUnmodifedDontNeed;
return haveDerivation(inBuildSlot);
return haveDerivation();
}
auto [allValid, validOutputs] = checkPathValidity();
@ -370,7 +366,7 @@ try {
worker.store.printStorePath(drvPath));
/* Nothing to wait for; tail call */
return gaveUpOnSubstitution(inBuildSlot);
return gaveUpOnSubstitution();
} catch (...) {
return {std::current_exception()};
}
@ -378,9 +374,9 @@ try {
/* At least one of the output paths could not be
produced using a substitute. So we have to build instead. */
kj::Promise<Result<Goal::WorkResult>> DerivationGoal::gaveUpOnSubstitution(bool inBuildSlot) noexcept
kj::Promise<Result<Goal::WorkResult>> DerivationGoal::gaveUpOnSubstitution() noexcept
try {
WaitForGoals result;
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. */
@ -393,7 +389,7 @@ try {
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,
@ -438,17 +434,15 @@ try {
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 (...) {
return {std::current_exception()};
co_return result::failure(std::current_exception());
}
@ -488,7 +482,7 @@ try {
}
/* 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(
@ -496,9 +490,9 @@ try {
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 { },
@ -506,18 +500,18 @@ try {
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 (...) {
return {std::current_exception()};
co_return result::failure(std::current_exception());
}
kj::Promise<Result<Goal::WorkResult>> DerivationGoal::closureRepaired(bool inBuildSlot) noexcept
kj::Promise<Result<Goal::WorkResult>> DerivationGoal::closureRepaired() noexcept
try {
trace("closure repaired");
if (nrFailed > 0)
@ -529,14 +523,14 @@ try {
}
kj::Promise<Result<Goal::WorkResult>> DerivationGoal::inputsRealised(bool inBuildSlot) noexcept
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(
co_return done(
BuildResult::DependencyFailed,
{},
Error(
@ -544,12 +538,12 @@ try {
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
@ -611,11 +605,12 @@ try {
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;
@ -679,10 +674,9 @@ try {
/* 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 (...) {
return {std::current_exception()};
co_return result::failure(std::current_exception());
}
void DerivationGoal::started()
@ -698,8 +692,9 @@ void DerivationGoal::started()
mcRunningBuilds = worker.runningBuilds.addTemporarily(1);
}
kj::Promise<Result<Goal::WorkResult>> DerivationGoal::tryToBuild(bool inBuildSlot) noexcept
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.
@ -733,7 +728,9 @@ try {
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();
@ -750,7 +747,7 @@ try {
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
@ -770,47 +767,56 @@ try {
&& 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 (...) {
return {std::current_exception()};
co_return result::failure(std::current_exception());
}
kj::Promise<Result<Goal::WorkResult>> DerivationGoal::tryLocalBuild(bool inBuildSlot) noexcept
kj::Promise<Result<Goal::WorkResult>> DerivationGoal::tryLocalBuild() noexcept
try {
throw Error(
"unable to build with a primary store that isn't a local store; "
@ -857,7 +863,7 @@ void replaceValidPath(const Path & storePath, const Path & tmpPath)
// attempt to recover
movePath(oldPath, storePath);
} catch (...) {
ignoreException();
ignoreExceptionExceptInterrupt();
}
throw;
}
@ -973,10 +979,11 @@ void runPostBuildHook(
proc.getStdout()->drainInto(sink);
}
kj::Promise<Result<Goal::WorkResult>> DerivationGoal::buildDone(bool inBuildSlot) noexcept
kj::Promise<Result<Goal::WorkResult>> DerivationGoal::buildDone() noexcept
try {
trace("build done");
slotToken = {};
Finally releaseBuildUser([&](){ this->cleanupHookFinally(); });
cleanupPreChildKill();
@ -992,9 +999,6 @@ try {
buildResult.timesBuilt++;
buildResult.stopTime = time(0);
/* So the child is gone now. */
worker.childTerminated(this);
/* Close the read side of the logger pipe. */
closeReadPipes();
@ -1095,7 +1099,7 @@ try {
return {std::current_exception()};
}
kj::Promise<Result<Goal::WorkResult>> DerivationGoal::resolvedFinished(bool inBuildSlot) noexcept
kj::Promise<Result<Goal::WorkResult>> DerivationGoal::resolvedFinished() noexcept
try {
trace("resolved derivation finished");
@ -1168,7 +1172,7 @@ try {
return {std::current_exception()};
}
HookReply DerivationGoal::tryBuildHook(bool inBuildSlot)
HookReply DerivationGoal::tryBuildHook()
{
if (!worker.hook.available || !useDerivation) return HookReply::Decline{};
@ -1180,7 +1184,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();
@ -1266,12 +1270,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()};
}
@ -1331,23 +1331,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)
@ -1362,10 +1408,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);
@ -1381,7 +1439,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()) {
@ -1405,16 +1463,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()
{
@ -1555,11 +1680,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)
@ -1590,7 +1717,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,
@ -1629,5 +1756,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 {};
@ -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 kj::Promise<Result<WorkResult>> (DerivationGoal::*GoalState)(bool inBuildSlot) noexcept;
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;
kj::Promise<Result<WorkResult>> work(bool inBuildSlot) noexcept 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.
*/
kj::Promise<Result<WorkResult>> getDerivation(bool inBuildSlot) noexcept;
kj::Promise<Result<WorkResult>> loadDerivation(bool inBuildSlot) noexcept;
kj::Promise<Result<WorkResult>> haveDerivation(bool inBuildSlot) noexcept;
kj::Promise<Result<WorkResult>> outputsSubstitutionTried(bool inBuildSlot) noexcept;
kj::Promise<Result<WorkResult>> gaveUpOnSubstitution(bool inBuildSlot) noexcept;
kj::Promise<Result<WorkResult>> closureRepaired(bool inBuildSlot) noexcept;
kj::Promise<Result<WorkResult>> inputsRealised(bool inBuildSlot) noexcept;
kj::Promise<Result<WorkResult>> tryToBuild(bool inBuildSlot) noexcept;
virtual kj::Promise<Result<WorkResult>> tryLocalBuild(bool inBuildSlot) noexcept;
kj::Promise<Result<WorkResult>> buildDone(bool inBuildSlot) noexcept;
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;
kj::Promise<Result<WorkResult>> resolvedFinished(bool inBuildSlot) noexcept;
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
@ -350,13 +365,18 @@ struct DerivationGoal : public Goal
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,33 +19,32 @@ DrvOutputSubstitutionGoal::DrvOutputSubstitutionGoal(
: Goal(worker, isDependency)
, id(id)
{
state = &DrvOutputSubstitutionGoal::init;
name = fmt("substitution of '%s'", id.to_string());
trace("created");
}
kj::Promise<Result<Goal::WorkResult>> DrvOutputSubstitutionGoal::init(bool inBuildSlot) noexcept
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 (...) {
return {std::current_exception()};
co_return result::failure(std::current_exception());
}
kj::Promise<Result<Goal::WorkResult>> DrvOutputSubstitutionGoal::tryNext(bool inBuildSlot) noexcept
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);
@ -59,7 +61,7 @@ try {
/* 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();
@ -69,25 +71,26 @@ try {
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 (...) {
return {std::current_exception()};
co_return result::failure(std::current_exception());
}
kj::Promise<Result<Goal::WorkResult>> DrvOutputSubstitutionGoal::realisationFetched(bool inBuildSlot) noexcept
kj::Promise<Result<Goal::WorkResult>> DrvOutputSubstitutionGoal::realisationFetched() noexcept
try {
worker.childTerminated(this);
maintainRunningSubstitutions.reset();
slotToken = {};
try {
outputInfo = downloadState->result.get();
@ -97,10 +100,10 @@ try {
}
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);
@ -114,34 +117,31 @@ try {
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 {std::move(result)};
if (!dependencies.empty()) {
(co_await waitForGoals(dependencies.releaseAsArray())).value();
}
co_return co_await outPathValid();
} catch (...) {
return {std::current_exception()};
co_return result::failure(std::current_exception());
}
kj::Promise<Result<Goal::WorkResult>> DrvOutputSubstitutionGoal::outPathValid(bool inBuildSlot) noexcept
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),
}};
}
@ -154,22 +154,9 @@ try {
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());
}
kj::Promise<Result<Goal::WorkResult>> DrvOutputSubstitutionGoal::work(bool inBuildSlot) noexcept
{
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 kj::Promise<Result<WorkResult>> (DrvOutputSubstitutionGoal::*GoalState)(bool inBuildSlot) noexcept;
GoalState state;
kj::Promise<Result<WorkResult>> init(bool inBuildSlot) noexcept;
kj::Promise<Result<WorkResult>> tryNext(bool inBuildSlot) noexcept;
kj::Promise<Result<WorkResult>> realisationFetched(bool inBuildSlot) noexcept;
kj::Promise<Result<WorkResult>> outPathValid(bool inBuildSlot) noexcept;
kj::Promise<Result<WorkResult>> tryNext() noexcept;
kj::Promise<Result<WorkResult>> realisationFetched() noexcept;
kj::Promise<Result<WorkResult>> outPathValid() noexcept;
kj::Promise<Result<WorkResult>> finished() noexcept;
Finished timedOut(Error && ex) override { abort(); };
std::string key() override;
kj::Promise<Result<WorkResult>> work(bool inBuildSlot) noexcept override;
kj::Promise<Result<WorkResult>> workImpl() noexcept override;
JobCategory jobCategory() const override {
return JobCategory::Substitution;

View file

@ -17,22 +17,22 @@ void Store::buildPaths(const std::vector<DerivedPath> & reqs, BuildMode buildMod
Worker worker(*this, evalStore ? *evalStore : *this, aio);
auto goals = runWorker(worker, [&](GoalFactory & gf) {
Goals goals;
Worker::Targets goals;
for (auto & br : reqs)
goals.insert(gf.makeGoal(br, buildMode));
goals.emplace(gf.makeGoal(br, buildMode));
return goals;
});
StringSet failed;
std::shared_ptr<Error> ex;
for (auto & i : goals) {
if (i->ex) {
for (auto & [i, result] : 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 (result.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()))
@ -60,11 +60,11 @@ std::vector<KeyedBuildResult> Store::buildPathsWithResults(
std::vector<std::pair<const DerivedPath &, GoalPtr>> state;
auto goals = runWorker(worker, [&](GoalFactory & gf) {
Goals goals;
Worker::Targets goals;
for (const auto & req : reqs) {
auto goal = gf.makeGoal(req, buildMode);
goals.insert(goal);
state.push_back({req, goal});
state.push_back({req, goal.first});
goals.emplace(std::move(goal));
}
return goals;
});
@ -72,7 +72,7 @@ std::vector<KeyedBuildResult> Store::buildPathsWithResults(
std::vector<KeyedBuildResult> results;
for (auto & [req, goalPtr] : state)
results.emplace_back(goalPtr->buildResult.restrictTo(req));
results.emplace_back(goals[goalPtr].result.restrictTo(req));
return results;
}
@ -84,11 +84,13 @@ BuildResult Store::buildDerivation(const StorePath & drvPath, const BasicDerivat
Worker worker(*this, *this, aio);
try {
auto goals = runWorker(worker, [&](GoalFactory & gf) -> Goals {
return Goals{gf.makeBasicDerivationGoal(drvPath, drv, OutputsSpec::All{}, buildMode)};
auto goals = runWorker(worker, [&](GoalFactory & gf) {
Worker::Targets goals;
goals.emplace(gf.makeBasicDerivationGoal(drvPath, drv, OutputsSpec::All{}, buildMode));
return goals;
});
auto goal = *goals.begin();
return goal->buildResult.restrictTo(DerivedPath::Built {
auto [goal, result] = *goals.begin();
return result.result.restrictTo(DerivedPath::Built {
.drvPath = makeConstantStorePathRef(drvPath),
.outputs = OutputsSpec::All {},
});
@ -110,14 +112,16 @@ void Store::ensurePath(const StorePath & path)
Worker worker(*this, *this, aio);
auto goals = runWorker(worker, [&](GoalFactory & gf) {
return Goals{gf.makePathSubstitutionGoal(path)};
Worker::Targets goals;
goals.emplace(gf.makePathSubstitutionGoal(path));
return goals;
});
auto goal = *goals.begin();
auto [goal, result] = *goals.begin();
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(worker.failingExitStatus());
throw std::move(*result.ex);
} else
throw Error(worker.failingExitStatus(), "path '%s' does not exist and cannot be created", printStorePath(path));
}
@ -130,24 +134,28 @@ void Store::repairPath(const StorePath & path)
Worker worker(*this, *this, aio);
auto goals = runWorker(worker, [&](GoalFactory & gf) {
return Goals{gf.makePathSubstitutionGoal(path, Repair)};
Worker::Targets goals;
goals.emplace(gf.makePathSubstitutionGoal(path, Repair));
return goals;
});
auto goal = *goals.begin();
auto [goal, result] = *goals.begin();
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(
Worker::Targets goals;
goals.emplace(gf.makeGoal(
DerivedPath::Built{
.drvPath = makeConstantStorePathRef(*info->deriver),
// FIXME: Should just build the specific output we need.
.outputs = OutputsSpec::All{},
},
bmRepair
)};
));
return goals;
});
} else
throw Error(worker.failingExitStatus(), "cannot repair path '%s'", printStorePath(path));

View file

@ -1,18 +1,73 @@
#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 {
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,10 +1,12 @@
#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 {
@ -19,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,
@ -69,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.
*/
@ -102,30 +82,11 @@ 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;
@ -135,24 +96,23 @@ public:
bool checkMismatch = false;
};
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)
@ -163,24 +123,10 @@ public:
trace("goal destroyed");
}
virtual kj::Promise<Result<WorkResult>> work(bool inBuildSlot) noexcept = 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
@ -188,15 +134,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"
@ -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)
}
kj::Promise<Result<Goal::WorkResult>> LocalDerivationGoal::tryLocalBuild(bool inBuildSlot) noexcept
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 @@ try {
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,24 +245,29 @@ try {
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 {std::move(report)};
co_return report;
}
co_return co_await buildDone();
} catch (...) {
return {std::current_exception()};
co_return result::failure(std::current_exception());
}
@ -390,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__
@ -779,7 +788,7 @@ std::set<int> LocalDerivationGoal::startBuilder()
msgs.push_back(std::move(msg));
}
return {builderOutPTY.get()};
return handleChildOutput();
}
@ -1241,7 +1250,7 @@ void LocalDerivationGoal::startDaemon()
NotTrusted, daemon::Recursive);
debug("terminated daemon connection");
} catch (SysError &) {
ignoreException();
ignoreExceptionExceptInterrupt();
}
});
@ -1361,13 +1370,20 @@ void LocalDerivationGoal::runChild()
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) {
@ -1802,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")

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.
*/
kj::Promise<Result<WorkResult>> tryLocalBuild(bool inBuildSlot) noexcept 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,8 @@
#include "nar-info.hh"
#include "signals.hh"
#include "finally.hh"
#include <kj/array.h>
#include <kj/vector.h>
namespace nix {
@ -18,7 +20,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,27 +32,21 @@ 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)};
}
kj::Promise<Result<Goal::WorkResult>> PathSubstitutionGoal::work(bool inBuildSlot) noexcept
{
return (this->*state)(inBuildSlot);
}
kj::Promise<Result<Goal::WorkResult>> PathSubstitutionGoal::init(bool inBuildSlot) noexcept
kj::Promise<Result<Goal::WorkResult>> PathSubstitutionGoal::workImpl() noexcept
try {
trace("init");
@ -67,13 +62,13 @@ try {
subs = settings.useSubstitutes ? getDefaultSubstituters() : std::list<ref<Store>>();
return tryNext(inBuildSlot);
return tryNext();
} catch (...) {
return {std::current_exception()};
}
kj::Promise<Result<Goal::WorkResult>> PathSubstitutionGoal::tryNext(bool inBuildSlot) noexcept
kj::Promise<Result<Goal::WorkResult>> PathSubstitutionGoal::tryNext() noexcept
try {
trace("trying next substituter");
@ -89,10 +84,10 @@ try {
/* 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)))};
fmt("path '%s' is required, but there is no substituter that can build it", worker.store.printStorePath(storePath)));
}
sub = subs.front();
@ -105,26 +100,28 @@ try {
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()) {
@ -134,7 +131,7 @@ try {
} 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();
}
}
@ -155,28 +152,26 @@ try {
{
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 {std::move(result)};
if (!dependencies.empty()) {/* to prevent hang (no wake-up event) */
(co_await waitForGoals(dependencies.releaseAsArray())).value();
}
co_return co_await referencesValid();
} catch (...) {
return {std::current_exception()};
co_return result::failure(std::current_exception());
}
kj::Promise<Result<Goal::WorkResult>> PathSubstitutionGoal::referencesValid(bool inBuildSlot) noexcept
kj::Promise<Result<Goal::WorkResult>> PathSubstitutionGoal::referencesValid() noexcept
try {
trace("all references realised");
@ -191,33 +186,33 @@ try {
if (i != storePath) /* ignore self-references */
assert(worker.store.isValidPath(i));
state = &PathSubstitutionGoal::tryToRun;
return tryToRun(inBuildSlot);
return tryToRun();
} catch (...) {
return {std::current_exception()};
}
kj::Promise<Result<Goal::WorkResult>> PathSubstitutionGoal::tryToRun(bool inBuildSlot) noexcept
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);
@ -233,39 +228,39 @@ try {
}
});
state = &PathSubstitutionGoal::finished;
return {WaitForWorld{{outPipe.readSide.get()}, true}};
co_await pipe.promise;
co_return co_await finished();
} catch (...) {
return {std::current_exception()};
co_return result::failure(std::current_exception());
}
kj::Promise<Result<Goal::WorkResult>> PathSubstitutionGoal::finished(bool inBuildSlot) noexcept
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);
@ -282,15 +277,9 @@ try {
worker.doneNarSize += maintainExpectedNar.delta();
maintainExpectedNar.reset();
return {done(ecSuccess, BuildResult::Substituted)};
co_return done(ecSuccess, BuildResult::Substituted);
} catch (...) {
return {std::current_exception()};
}
Goal::WorkResult PathSubstitutionGoal::handleChildOutput(int fd, std::string_view data)
{
return StillAlive{};
co_return result::failure(std::current_exception());
}
@ -300,12 +289,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 kj::Promise<Result<WorkResult>> (PathSubstitutionGoal::*GoalState)(bool inBuildSlot) noexcept;
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);
}
kj::Promise<Result<WorkResult>> work(bool inBuildSlot) noexcept override;
kj::Promise<Result<WorkResult>> workImpl() noexcept override;
/**
* The states.
*/
kj::Promise<Result<WorkResult>> init(bool inBuildSlot) noexcept;
kj::Promise<Result<WorkResult>> tryNext(bool inBuildSlot) noexcept;
kj::Promise<Result<WorkResult>> referencesValid(bool inBuildSlot) noexcept;
kj::Promise<Result<WorkResult>> tryToRun(bool inBuildSlot) noexcept;
kj::Promise<Result<WorkResult>> finished(bool inBuildSlot) noexcept;
/**
* 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;

View file

@ -1,3 +1,4 @@
#include "async-collect.hh"
#include "charptr-cast.hh"
#include "worker.hh"
#include "finally.hh"
@ -6,11 +7,22 @@
#include "local-derivation-goal.hh"
#include "signals.hh"
#include "hook-instance.hh" // IWYU pragma: keep
#include <poll.h>
#include <boost/outcome/try.hpp>
#include <kj/vector.h>
namespace nix {
namespace {
struct ErrorHandler : kj::TaskSet::ErrorHandler
{
void taskFailed(kj::Exception && e) override
{
printError("unexpected async failure in Worker: %s", kj::str(e).cStr());
abort();
}
} errorHandler;
}
Worker::Worker(Store & store, Store & evalStore, kj::AsyncIoContext & aio)
: act(*logger, actRealise)
, actDerivations(*logger, actBuilds)
@ -18,11 +30,13 @@ Worker::Worker(Store & store, Store & evalStore, kj::AsyncIoContext & aio)
, store(store)
, evalStore(evalStore)
, aio(aio)
/* Make sure that we are always allowed to run at least one substitution.
This prevents infinite waiting. */
, substitutions(std::max<unsigned>(1, settings.maxSubstitutionJobs))
, localBuilds(settings.maxBuildJobs)
, children(errorHandler)
{
/* Debugging: prevent recursive workers. */
nrLocalBuilds = 0;
nrSubstitutions = 0;
lastWokenUp = steady_time_point::min();
}
@ -32,7 +46,11 @@ Worker::~Worker()
goals that refer to this worker should be gone. (Otherwise we
are in trouble, since goals may call childTerminated() etc. in
their destructors). */
topGoals.clear();
children.clear();
derivationGoals.clear();
drvOutputSubstitutionGoals.clear();
substitutionGoals.clear();
assert(expectedSubstitutions == 0);
assert(expectedDownloadSize == 0);
@ -40,292 +58,158 @@ Worker::~Worker()
}
std::shared_ptr<DerivationGoal> Worker::makeDerivationGoalCommon(
template<typename ID, std::derived_from<Goal> G>
std::pair<std::shared_ptr<G>, kj::Promise<Result<Goal::WorkResult>>> Worker::makeGoalCommon(
std::map<ID, CachedGoal<G>> & map,
const ID & key,
InvocableR<std::unique_ptr<G>> auto create,
InvocableR<bool, G &> auto modify
)
{
auto [it, _inserted] = map.try_emplace(key);
// try twice to create the goal. we can only loop if we hit the continue,
// and then we only want to recreate the goal *once*. concurrent accesses
// to the worker are not sound, we want to catch them if at all possible.
for ([[maybe_unused]] auto _attempt : {1, 2}) {
auto & cachedGoal = it->second;
auto & goal = cachedGoal.goal;
if (!goal) {
goal = create();
// do not start working immediately. if we are not yet running we
// may create dependencies as though they were toplevel goals, in
// which case the dependencies will not report build errors. when
// we are running we may be called for this same goal more times,
// and then we want to modify rather than recreate when possible.
auto removeWhenDone = [goal, &map, it] {
// c++ lambda coroutine capture semantics are *so* fucked up.
return [](auto goal, auto & map, auto it) -> kj::Promise<Result<Goal::WorkResult>> {
auto result = co_await goal->work();
// a concurrent call to makeGoalCommon may have reset our
// cached goal and replaced it with a new instance. don't
// remove the goal in this case, otherwise we will crash.
if (goal == it->second.goal) {
map.erase(it);
}
co_return result;
}(goal, map, it);
};
cachedGoal.promise = kj::evalLater(std::move(removeWhenDone)).fork();
children.add(cachedGoal.promise.addBranch().then([this](auto _result) {
if (_result.has_value()) {
auto & result = _result.value();
permanentFailure |= result.permanentFailure;
timedOut |= result.timedOut;
hashMismatch |= result.hashMismatch;
checkMismatch |= result.checkMismatch;
}
}));
} else {
if (!modify(*goal)) {
cachedGoal = {};
continue;
}
}
return {goal, cachedGoal.promise.addBranch()};
}
assert(false && "could not make a goal. possible concurrent worker access");
}
std::pair<std::shared_ptr<DerivationGoal>, kj::Promise<Result<Goal::WorkResult>>> Worker::makeDerivationGoal(
const StorePath & drvPath, const OutputsSpec & wantedOutputs, BuildMode buildMode
)
{
return makeGoalCommon(
derivationGoals,
drvPath,
[&]() -> std::unique_ptr<DerivationGoal> {
return !dynamic_cast<LocalStore *>(&store)
? std::make_unique<DerivationGoal>(
drvPath, wantedOutputs, *this, running, buildMode
)
: LocalDerivationGoal::makeLocalDerivationGoal(
drvPath, wantedOutputs, *this, running, buildMode
);
},
[&](DerivationGoal & g) { return g.addWantedOutputs(wantedOutputs); }
);
}
std::pair<std::shared_ptr<DerivationGoal>, kj::Promise<Result<Goal::WorkResult>>> Worker::makeBasicDerivationGoal(
const StorePath & drvPath,
const BasicDerivation & drv,
const OutputsSpec & wantedOutputs,
std::function<std::shared_ptr<DerivationGoal>()> mkDrvGoal)
BuildMode buildMode
)
{
std::weak_ptr<DerivationGoal> & goal_weak = derivationGoals[drvPath];
std::shared_ptr<DerivationGoal> goal = goal_weak.lock();
if (!goal) {
goal = mkDrvGoal();
goal_weak = goal;
wakeUp(goal);
} else {
goal->addWantedOutputs(wantedOutputs);
}
return goal;
}
std::shared_ptr<DerivationGoal> Worker::makeDerivationGoal(const StorePath & drvPath,
const OutputsSpec & wantedOutputs, BuildMode buildMode)
{
return makeDerivationGoalCommon(
return makeGoalCommon(
derivationGoals,
drvPath,
wantedOutputs,
[&]() -> std::shared_ptr<DerivationGoal> {
[&]() -> std::unique_ptr<DerivationGoal> {
return !dynamic_cast<LocalStore *>(&store)
? std::make_shared<DerivationGoal>(
drvPath, wantedOutputs, *this, running, buildMode
)
: LocalDerivationGoal::makeLocalDerivationGoal(
drvPath, wantedOutputs, *this, running, buildMode
);
}
);
}
std::shared_ptr<DerivationGoal> Worker::makeBasicDerivationGoal(const StorePath & drvPath,
const BasicDerivation & drv, const OutputsSpec & wantedOutputs, BuildMode buildMode)
{
return makeDerivationGoalCommon(
drvPath,
wantedOutputs,
[&]() -> std::shared_ptr<DerivationGoal> {
return !dynamic_cast<LocalStore *>(&store)
? std::make_shared<DerivationGoal>(
? std::make_unique<DerivationGoal>(
drvPath, drv, wantedOutputs, *this, running, buildMode
)
: LocalDerivationGoal::makeLocalDerivationGoal(
drvPath, drv, wantedOutputs, *this, running, buildMode
);
}
},
[&](DerivationGoal & g) { return g.addWantedOutputs(wantedOutputs); }
);
}
std::shared_ptr<PathSubstitutionGoal> Worker::makePathSubstitutionGoal(const StorePath & path, RepairFlag repair, std::optional<ContentAddress> ca)
std::pair<std::shared_ptr<PathSubstitutionGoal>, kj::Promise<Result<Goal::WorkResult>>>
Worker::makePathSubstitutionGoal(
const StorePath & path, RepairFlag repair, std::optional<ContentAddress> ca
)
{
std::weak_ptr<PathSubstitutionGoal> & goal_weak = substitutionGoals[path];
auto goal = goal_weak.lock(); // FIXME
if (!goal) {
goal = std::make_shared<PathSubstitutionGoal>(path, *this, running, repair, ca);
goal_weak = goal;
wakeUp(goal);
}
return goal;
return makeGoalCommon(
substitutionGoals,
path,
[&] { return std::make_unique<PathSubstitutionGoal>(path, *this, running, repair, ca); },
[&](auto &) { return true; }
);
}
std::shared_ptr<DrvOutputSubstitutionGoal> Worker::makeDrvOutputSubstitutionGoal(const DrvOutput& id, RepairFlag repair, std::optional<ContentAddress> ca)
std::pair<std::shared_ptr<DrvOutputSubstitutionGoal>, kj::Promise<Result<Goal::WorkResult>>>
Worker::makeDrvOutputSubstitutionGoal(
const DrvOutput & id, RepairFlag repair, std::optional<ContentAddress> ca
)
{
std::weak_ptr<DrvOutputSubstitutionGoal> & goal_weak = drvOutputSubstitutionGoals[id];
auto goal = goal_weak.lock(); // FIXME
if (!goal) {
goal = std::make_shared<DrvOutputSubstitutionGoal>(id, *this, running, repair, ca);
goal_weak = goal;
wakeUp(goal);
}
return goal;
return makeGoalCommon(
drvOutputSubstitutionGoals,
id,
[&] { return std::make_unique<DrvOutputSubstitutionGoal>(id, *this, running, repair, ca); },
[&](auto &) { return true; }
);
}
GoalPtr Worker::makeGoal(const DerivedPath & req, BuildMode buildMode)
std::pair<GoalPtr, kj::Promise<Result<Goal::WorkResult>>> Worker::makeGoal(const DerivedPath & req, BuildMode buildMode)
{
return std::visit(overloaded {
[&](const DerivedPath::Built & bfd) -> GoalPtr {
[&](const DerivedPath::Built & bfd) -> std::pair<GoalPtr, kj::Promise<Result<Goal::WorkResult>>> {
if (auto bop = std::get_if<DerivedPath::Opaque>(&*bfd.drvPath))
return makeDerivationGoal(bop->path, bfd.outputs, buildMode);
else
throw UnimplementedError("Building dynamic derivations in one shot is not yet implemented.");
},
[&](const DerivedPath::Opaque & bo) -> GoalPtr {
[&](const DerivedPath::Opaque & bo) -> std::pair<GoalPtr, kj::Promise<Result<Goal::WorkResult>>> {
return makePathSubstitutionGoal(bo.path, buildMode == bmRepair ? Repair : NoRepair);
},
}, req.raw());
}
kj::Promise<Result<Worker::Results>> Worker::updateStatistics()
try {
while (true) {
statisticsUpdateInhibitor = co_await statisticsUpdateSignal.acquire();
template<typename K, typename G>
static void removeGoal(std::shared_ptr<G> goal, std::map<K, std::weak_ptr<G>> & goalMap)
{
/* !!! inefficient */
for (auto i = goalMap.begin();
i != goalMap.end(); )
if (i->second.lock() == goal) {
auto j = i; ++j;
goalMap.erase(i);
i = j;
}
else ++i;
}
void Worker::goalFinished(GoalPtr goal, Goal::Finished & f)
{
goal->trace("done");
assert(!goal->exitCode.has_value());
goal->exitCode = f.exitCode;
goal->ex = f.ex;
permanentFailure |= f.permanentFailure;
timedOut |= f.timedOut;
hashMismatch |= f.hashMismatch;
checkMismatch |= f.checkMismatch;
for (auto & i : goal->waiters) {
if (GoalPtr waiting = i.lock()) {
assert(waiting->waitees.count(goal));
waiting->waitees.erase(goal);
waiting->trace(fmt("waitee '%s' done; %d left", goal->name, waiting->waitees.size()));
if (f.exitCode != Goal::ecSuccess) ++waiting->nrFailed;
if (f.exitCode == Goal::ecNoSubstituters) ++waiting->nrNoSubstituters;
if (f.exitCode == Goal::ecIncompleteClosure) ++waiting->nrIncompleteClosure;
if (waiting->waitees.empty() || (f.exitCode == Goal::ecFailed && !settings.keepGoing)) {
/* If we failed and keepGoing is not set, we remove all
remaining waitees. */
for (auto & i : waiting->waitees) {
i->waiters.extract(waiting);
}
waiting->waitees.clear();
wakeUp(waiting);
}
waiting->waiteeDone(goal);
}
}
goal->waiters.clear();
removeGoal(goal);
goal->cleanup();
}
void Worker::handleWorkResult(GoalPtr goal, Goal::WorkResult how)
{
std::visit(
overloaded{
[&](Goal::StillAlive) {},
[&](Goal::WaitForSlot) { waitForBuildSlot(goal); },
[&](Goal::WaitForAWhile) { waitForAWhile(goal); },
[&](Goal::ContinueImmediately) { wakeUp(goal); },
[&](Goal::WaitForGoals & w) {
for (auto & dep : w.goals) {
goal->waitees.insert(dep);
dep->waiters.insert(goal);
}
},
[&](Goal::WaitForWorld & w) { childStarted(goal, w.fds, w.inBuildSlot); },
[&](Goal::Finished & f) { goalFinished(goal, f); },
},
how
);
}
void Worker::removeGoal(GoalPtr goal)
{
if (auto drvGoal = std::dynamic_pointer_cast<DerivationGoal>(goal))
nix::removeGoal(drvGoal, derivationGoals);
else if (auto subGoal = std::dynamic_pointer_cast<PathSubstitutionGoal>(goal))
nix::removeGoal(subGoal, substitutionGoals);
else if (auto subGoal = std::dynamic_pointer_cast<DrvOutputSubstitutionGoal>(goal))
nix::removeGoal(subGoal, drvOutputSubstitutionGoals);
else
assert(false);
if (topGoals.find(goal) != topGoals.end()) {
topGoals.erase(goal);
/* If a top-level goal failed, then kill all other goals
(unless keepGoing was set). */
if (goal->exitCode == Goal::ecFailed && !settings.keepGoing)
topGoals.clear();
}
}
void Worker::wakeUp(GoalPtr goal)
{
goal->trace("woken up");
awake.insert(goal);
}
void Worker::childStarted(GoalPtr goal, const std::set<int> & fds,
bool inBuildSlot)
{
Child child;
child.goal = goal;
child.goal2 = goal.get();
child.fds = fds;
child.timeStarted = child.lastOutput = steady_time_point::clock::now();
child.inBuildSlot = inBuildSlot;
children.emplace_back(child);
if (inBuildSlot) {
switch (goal->jobCategory()) {
case JobCategory::Substitution:
nrSubstitutions++;
break;
case JobCategory::Build:
nrLocalBuilds++;
break;
default:
abort();
}
}
}
void Worker::childTerminated(Goal * goal)
{
auto i = std::find_if(children.begin(), children.end(),
[&](const Child & child) { return child.goal2 == goal; });
if (i == children.end()) return;
if (i->inBuildSlot) {
switch (goal->jobCategory()) {
case JobCategory::Substitution:
assert(nrSubstitutions > 0);
nrSubstitutions--;
break;
case JobCategory::Build:
assert(nrLocalBuilds > 0);
nrLocalBuilds--;
break;
default:
abort();
}
}
children.erase(i);
/* Wake up goals waiting for a build slot. */
for (auto & j : wantingToBuild) {
GoalPtr goal = j.lock();
if (goal) wakeUp(goal);
}
wantingToBuild.clear();
}
void Worker::waitForBuildSlot(GoalPtr goal)
{
goal->trace("wait for build slot");
bool isSubstitutionGoal = goal->jobCategory() == JobCategory::Substitution;
if ((!isSubstitutionGoal && nrLocalBuilds < settings.maxBuildJobs) ||
(isSubstitutionGoal && nrSubstitutions < settings.maxSubstitutionJobs))
wakeUp(goal); /* we can do it right away */
else
wantingToBuild.insert(goal);
}
void Worker::waitForAWhile(GoalPtr goal)
{
debug("wait for a while");
waitingForAWhile.insert(goal);
}
void Worker::updateStatistics()
{
// only update progress info while running. this notably excludes updating
// progress info while destroying, which causes the progress bar to assert
if (running && statisticsOutdated) {
// only update progress info while running. this notably excludes updating
// progress info while destroying, which causes the progress bar to assert
actDerivations.progress(
doneBuilds, expectedBuilds + doneBuilds, runningBuilds, failedBuilds
);
@ -338,221 +222,82 @@ void Worker::updateStatistics()
act.setExpected(actFileTransfer, expectedDownloadSize + doneDownloadSize);
act.setExpected(actCopyPath, expectedNarSize + doneNarSize);
statisticsOutdated = false;
// limit to 50fps. that should be more than good enough for anything we do
co_await aio.provider->getTimer().afterDelay(20 * kj::MILLISECONDS);
}
} catch (...) {
co_return result::failure(std::current_exception());
}
Goals Worker::run(std::function<Goals (GoalFactory &)> req)
Worker::Results Worker::run(std::function<Targets (GoalFactory &)> req)
{
auto _topGoals = req(goalFactory());
auto topGoals = req(goalFactory());
assert(!running);
running = true;
Finally const _stop([&] { running = false; });
updateStatistics();
auto onInterrupt = kj::newPromiseAndCrossThreadFulfiller<Result<Results>>();
auto interruptCallback = createInterruptCallback([&] {
return result::failure(std::make_exception_ptr(makeInterrupted()));
});
topGoals = _topGoals;
auto promise = runImpl(std::move(topGoals))
.exclusiveJoin(updateStatistics())
.exclusiveJoin(std::move(onInterrupt.promise));
// TODO GC interface?
if (auto localStore = dynamic_cast<LocalStore *>(&store); localStore && settings.minFree != 0) {
// Periodically wake up to see if we need to run the garbage collector.
promise = promise.exclusiveJoin(boopGC(*localStore));
}
return promise.wait(aio.waitScope).value();
}
kj::Promise<Result<Worker::Results>> Worker::runImpl(Targets topGoals)
try {
debug("entered goal loop");
while (1) {
kj::Vector<Targets::value_type> promises(topGoals.size());
for (auto & gp : topGoals) {
promises.add(std::move(gp));
}
checkInterrupt();
Results results;
// TODO GC interface?
if (auto localStore = dynamic_cast<LocalStore *>(&store))
localStore->autoGC(false);
auto collect = AsyncCollect(promises.releaseAsArray());
while (auto done = co_await collect.next()) {
// propagate goal exceptions outward
BOOST_OUTCOME_CO_TRY(auto result, done->second);
results.emplace(done->first, result);
/* Call every wake goal (in the ordering established by
CompareGoalPtrs). */
while (!awake.empty() && !topGoals.empty()) {
Goals awake2;
for (auto & i : awake) {
GoalPtr goal = i.lock();
if (goal) awake2.insert(goal);
}
awake.clear();
for (auto & goal : awake2) {
checkInterrupt();
/* Make sure that we are always allowed to run at least one substitution.
This prevents infinite waiting. */
const bool inSlot = goal->jobCategory() == JobCategory::Substitution
? nrSubstitutions < std::max(1U, (unsigned int) settings.maxSubstitutionJobs)
: nrLocalBuilds < settings.maxBuildJobs;
handleWorkResult(goal, goal->work(inSlot).wait(aio.waitScope).value());
updateStatistics();
if (topGoals.empty()) break; // stuff may have been cancelled
}
}
if (topGoals.empty()) break;
/* Wait for input. */
if (!children.empty() || !waitingForAWhile.empty())
waitForInput();
else {
assert(!awake.empty());
/* If a top-level goal failed, then kill all other goals
(unless keepGoing was set). */
if (result.exitCode == Goal::ecFailed && !settings.keepGoing) {
children.clear();
break;
}
}
/* If --keep-going is not set, it's possible that the main goal
exited while some of its subgoals were still active. But if
--keep-going *is* set, then they must all be finished now. */
assert(!settings.keepGoing || awake.empty());
assert(!settings.keepGoing || wantingToBuild.empty());
assert(!settings.keepGoing || children.empty());
assert(!settings.keepGoing || children.isEmpty());
return _topGoals;
co_return std::move(results);
} catch (...) {
co_return result::failure(std::current_exception());
}
void Worker::waitForInput()
{
printMsg(lvlVomit, "waiting for children");
/* Process output from the file descriptors attached to the
children, namely log output and output path creation commands.
We also use this to detect child termination: if we get EOF on
the logger pipe of a build, we assume that the builder has
terminated. */
bool useTimeout = false;
long timeout = 0;
auto before = steady_time_point::clock::now();
/* If we're monitoring for silence on stdout/stderr, or if there
is a build timeout, then wait for input until the first
deadline for any child. */
auto nearest = steady_time_point::max(); // nearest deadline
if (settings.minFree.get() != 0)
// Periodicallty wake up to see if we need to run the garbage collector.
nearest = before + std::chrono::seconds(10);
for (auto & i : children) {
if (auto goal = i.goal.lock()) {
if (!goal->respectsTimeouts()) continue;
if (0 != settings.maxSilentTime)
nearest = std::min(nearest, i.lastOutput + std::chrono::seconds(settings.maxSilentTime));
if (0 != settings.buildTimeout)
nearest = std::min(nearest, i.timeStarted + std::chrono::seconds(settings.buildTimeout));
}
}
if (nearest != steady_time_point::max()) {
timeout = std::max(1L, (long) std::chrono::duration_cast<std::chrono::seconds>(nearest - before).count());
useTimeout = true;
}
/* If we are polling goals that are waiting for a lock, then wake
up after a few seconds at most. */
if (!waitingForAWhile.empty()) {
useTimeout = true;
if (lastWokenUp == steady_time_point::min() || lastWokenUp > before) lastWokenUp = before;
timeout = std::max(1L,
(long) std::chrono::duration_cast<std::chrono::seconds>(
lastWokenUp + std::chrono::seconds(settings.pollInterval) - before).count());
} else lastWokenUp = steady_time_point::min();
if (useTimeout)
vomit("sleeping %d seconds", timeout);
/* Use select() to wait for the input side of any logger pipe to
become `available'. Note that `available' (i.e., non-blocking)
includes EOF. */
std::vector<struct pollfd> pollStatus;
std::map<int, size_t> fdToPollStatus;
for (auto & i : children) {
for (auto & j : i.fds) {
pollStatus.push_back((struct pollfd) { .fd = j, .events = POLLIN });
fdToPollStatus[j] = pollStatus.size() - 1;
}
}
if (poll(pollStatus.data(), pollStatus.size(),
useTimeout ? timeout * 1000 : -1) == -1) {
if (errno == EINTR) return;
throw SysError("waiting for input");
}
auto after = steady_time_point::clock::now();
/* Process all available file descriptors. FIXME: this is
O(children * fds). */
decltype(children)::iterator i;
for (auto j = children.begin(); j != children.end(); j = i) {
i = std::next(j);
checkInterrupt();
GoalPtr goal = j->goal.lock();
assert(goal);
if (!goal->exitCode.has_value() &&
0 != settings.maxSilentTime &&
goal->respectsTimeouts() &&
after - j->lastOutput >= std::chrono::seconds(settings.maxSilentTime))
{
handleWorkResult(
goal,
goal->timedOut(Error(
"%1% timed out after %2% seconds of silence",
goal->getName(),
settings.maxSilentTime
))
);
continue;
}
else if (!goal->exitCode.has_value() &&
0 != settings.buildTimeout &&
goal->respectsTimeouts() &&
after - j->timeStarted >= std::chrono::seconds(settings.buildTimeout))
{
handleWorkResult(
goal,
goal->timedOut(
Error("%1% timed out after %2% seconds", goal->getName(), settings.buildTimeout)
)
);
continue;
}
std::set<int> fds2(j->fds);
std::vector<unsigned char> buffer(4096);
for (auto & k : fds2) {
const auto fdPollStatusId = get(fdToPollStatus, k);
assert(fdPollStatusId);
assert(*fdPollStatusId < pollStatus.size());
if (pollStatus.at(*fdPollStatusId).revents) {
ssize_t rd = ::read(k, buffer.data(), buffer.size());
// FIXME: is there a cleaner way to handle pt close
// than EIO? Is this even standard?
if (rd == 0 || (rd == -1 && errno == EIO)) {
debug("%1%: got EOF", goal->getName());
goal->handleEOF(k);
handleWorkResult(goal, Goal::ContinueImmediately{});
j->fds.erase(k);
} else if (rd == -1) {
if (errno != EINTR)
throw SysError("%s: read failed", goal->getName());
} else {
printMsg(lvlVomit, "%1%: read %2% bytes",
goal->getName(), rd);
std::string_view data(charptr_cast<char *>(buffer.data()), rd);
j->lastOutput = after;
handleWorkResult(goal, goal->handleChildOutput(k, data));
}
}
}
}
if (!waitingForAWhile.empty() && lastWokenUp + std::chrono::seconds(settings.pollInterval) <= after) {
lastWokenUp = after;
for (auto & i : waitingForAWhile) {
GoalPtr goal = i.lock();
if (goal) wakeUp(goal);
}
waitingForAWhile.clear();
kj::Promise<Result<Worker::Results>> Worker::boopGC(LocalStore & localStore)
try {
while (true) {
co_await aio.provider->getTimer().afterDelay(10 * kj::SECONDS);
localStore.autoGC(false);
}
} catch (...) {
co_return result::failure(std::current_exception());
}

View file

@ -1,6 +1,8 @@
#pragma once
///@file
#include "async-semaphore.hh"
#include "concepts.hh"
#include "notifying-counter.hh"
#include "types.hh"
#include "lock.hh"
@ -18,37 +20,22 @@ namespace nix {
struct DerivationGoal;
struct PathSubstitutionGoal;
class DrvOutputSubstitutionGoal;
class LocalStore;
typedef std::chrono::time_point<std::chrono::steady_clock> steady_time_point;
/**
* A mapping used to remember for each child process to what goal it
* belongs, and file descriptors for receiving log data and output
* path creation commands.
*/
struct Child
{
WeakGoalPtr goal;
Goal * goal2; // ugly hackery
std::set<int> fds;
bool inBuildSlot;
/**
* Time we last got output on stdout/stderr
*/
steady_time_point lastOutput;
steady_time_point timeStarted;
};
/* Forward definition. */
struct HookInstance;
class GoalFactory
{
public:
virtual std::shared_ptr<DerivationGoal> makeDerivationGoal(
virtual std::pair<std::shared_ptr<DerivationGoal>, kj::Promise<Result<Goal::WorkResult>>>
makeDerivationGoal(
const StorePath & drvPath, const OutputsSpec & wantedOutputs, BuildMode buildMode = bmNormal
) = 0;
virtual std::shared_ptr<DerivationGoal> makeBasicDerivationGoal(
virtual std::pair<std::shared_ptr<DerivationGoal>, kj::Promise<Result<Goal::WorkResult>>>
makeBasicDerivationGoal(
const StorePath & drvPath,
const BasicDerivation & drv,
const OutputsSpec & wantedOutputs,
@ -58,12 +45,14 @@ public:
/**
* @ref SubstitutionGoal "substitution goal"
*/
virtual std::shared_ptr<PathSubstitutionGoal> makePathSubstitutionGoal(
virtual std::pair<std::shared_ptr<PathSubstitutionGoal>, kj::Promise<Result<Goal::WorkResult>>>
makePathSubstitutionGoal(
const StorePath & storePath,
RepairFlag repair = NoRepair,
std::optional<ContentAddress> ca = std::nullopt
) = 0;
virtual std::shared_ptr<DrvOutputSubstitutionGoal> makeDrvOutputSubstitutionGoal(
virtual std::pair<std::shared_ptr<DrvOutputSubstitutionGoal>, kj::Promise<Result<Goal::WorkResult>>>
makeDrvOutputSubstitutionGoal(
const DrvOutput & id,
RepairFlag repair = NoRepair,
std::optional<ContentAddress> ca = std::nullopt
@ -75,7 +64,8 @@ public:
* It will be a `DerivationGoal` for a `DerivedPath::Built` or
* a `SubstitutionGoal` for a `DerivedPath::Opaque`.
*/
virtual GoalPtr makeGoal(const DerivedPath & req, BuildMode buildMode = bmNormal) = 0;
virtual std::pair<GoalPtr, kj::Promise<Result<Goal::WorkResult>>>
makeGoal(const DerivedPath & req, BuildMode buildMode = bmNormal) = 0;
};
// elaborate hoax to let goals access factory methods while hiding them from the public
@ -94,61 +84,27 @@ protected:
*/
class Worker : public WorkerBase
{
public:
using Targets = std::map<GoalPtr, kj::Promise<Result<Goal::WorkResult>>>;
using Results = std::map<GoalPtr, Goal::WorkResult>;
private:
bool running = false;
/* Note: the worker should only have strong pointers to the
top-level goals. */
/**
* The top-level goals of the worker.
*/
Goals topGoals;
/**
* Goals that are ready to do some work.
*/
WeakGoals awake;
/**
* Goals waiting for a build slot.
*/
WeakGoals wantingToBuild;
/**
* Child processes currently running.
*/
std::list<Child> children;
/**
* Number of build slots occupied. This includes local builds but does not
* include substitutions or remote builds via the build hook.
*/
unsigned int nrLocalBuilds;
/**
* Number of substitution slots occupied.
*/
unsigned int nrSubstitutions;
template<typename G>
struct CachedGoal
{
std::shared_ptr<G> goal;
kj::ForkedPromise<Result<Goal::WorkResult>> promise{nullptr};
};
/**
* Maps used to prevent multiple instantiations of a goal for the
* same derivation / path.
*/
std::map<StorePath, std::weak_ptr<DerivationGoal>> derivationGoals;
std::map<StorePath, std::weak_ptr<PathSubstitutionGoal>> substitutionGoals;
std::map<DrvOutput, std::weak_ptr<DrvOutputSubstitutionGoal>> drvOutputSubstitutionGoals;
/**
* Goals sleeping for a few seconds (polling a lock).
*/
WeakGoals waitingForAWhile;
/**
* Last time the goals in `waitingForAWhile` where woken up.
*/
steady_time_point lastWokenUp;
std::map<StorePath, CachedGoal<DerivationGoal>> derivationGoals;
std::map<StorePath, CachedGoal<PathSubstitutionGoal>> substitutionGoals;
std::map<DrvOutput, CachedGoal<DrvOutputSubstitutionGoal>> drvOutputSubstitutionGoals;
/**
* Cache for pathContentsGood().
@ -176,60 +132,25 @@ private:
*/
bool checkMismatch = false;
void goalFinished(GoalPtr goal, Goal::Finished & f);
void handleWorkResult(GoalPtr goal, Goal::WorkResult how);
/**
* Put `goal` to sleep until a build slot becomes available (which
* might be right away).
*/
void waitForBuildSlot(GoalPtr goal);
/**
* Wait for a few seconds and then retry this goal. Used when
* waiting for a lock held by another process. This kind of
* polling is inefficient, but POSIX doesn't really provide a way
* to wait for multiple locks in the main select() loop.
*/
void waitForAWhile(GoalPtr goal);
/**
* Wake up a goal (i.e., there is something for it to do).
*/
void wakeUp(GoalPtr goal);
/**
* Wait for input to become available.
*/
void waitForInput();
/**
* Remove a dead goal.
*/
void removeGoal(GoalPtr goal);
/**
* Registers a running child process. `inBuildSlot` means that
* the process counts towards the jobs limit.
*/
void childStarted(GoalPtr goal, const std::set<int> & fds,
bool inBuildSlot);
/**
* Pass current stats counters to the logger for progress bar updates.
*/
void updateStatistics();
kj::Promise<Result<Results>> updateStatistics();
bool statisticsOutdated = true;
AsyncSemaphore statisticsUpdateSignal{1};
std::optional<AsyncSemaphore::Token> statisticsUpdateInhibitor;
/**
* Mark statistics as outdated, such that `updateStatistics` will be called.
*/
void updateStatisticsLater()
{
statisticsOutdated = true;
statisticsUpdateInhibitor = {};
}
kj::Promise<Result<Results>> runImpl(Targets topGoals);
kj::Promise<Result<Results>> boopGC(LocalStore & localStore);
public:
const Activity act;
@ -239,7 +160,12 @@ public:
Store & store;
Store & evalStore;
kj::AsyncIoContext & aio;
AsyncSemaphore substitutions, localBuilds;
private:
kj::TaskSet children;
public:
struct HookState {
std::unique_ptr<HookInstance> instance;
@ -277,21 +203,35 @@ public:
* @ref DerivationGoal "derivation goal"
*/
private:
std::shared_ptr<DerivationGoal> makeDerivationGoalCommon(
const StorePath & drvPath, const OutputsSpec & wantedOutputs,
std::function<std::shared_ptr<DerivationGoal>()> mkDrvGoal);
std::shared_ptr<DerivationGoal> makeDerivationGoal(
template<typename ID, std::derived_from<Goal> G>
std::pair<std::shared_ptr<G>, kj::Promise<Result<Goal::WorkResult>>> makeGoalCommon(
std::map<ID, CachedGoal<G>> & map,
const ID & key,
InvocableR<std::unique_ptr<G>> auto create,
InvocableR<bool, G &> auto modify
);
std::pair<std::shared_ptr<DerivationGoal>, kj::Promise<Result<Goal::WorkResult>>> makeDerivationGoal(
const StorePath & drvPath,
const OutputsSpec & wantedOutputs, BuildMode buildMode = bmNormal) override;
std::shared_ptr<DerivationGoal> makeBasicDerivationGoal(
std::pair<std::shared_ptr<DerivationGoal>, kj::Promise<Result<Goal::WorkResult>>> makeBasicDerivationGoal(
const StorePath & drvPath, const BasicDerivation & drv,
const OutputsSpec & wantedOutputs, BuildMode buildMode = bmNormal) override;
/**
* @ref SubstitutionGoal "substitution goal"
*/
std::shared_ptr<PathSubstitutionGoal> makePathSubstitutionGoal(const StorePath & storePath, RepairFlag repair = NoRepair, std::optional<ContentAddress> ca = std::nullopt) override;
std::shared_ptr<DrvOutputSubstitutionGoal> makeDrvOutputSubstitutionGoal(const DrvOutput & id, RepairFlag repair = NoRepair, std::optional<ContentAddress> ca = std::nullopt) override;
std::pair<std::shared_ptr<PathSubstitutionGoal>, kj::Promise<Result<Goal::WorkResult>>>
makePathSubstitutionGoal(
const StorePath & storePath,
RepairFlag repair = NoRepair,
std::optional<ContentAddress> ca = std::nullopt
) override;
std::pair<std::shared_ptr<DrvOutputSubstitutionGoal>, kj::Promise<Result<Goal::WorkResult>>>
makeDrvOutputSubstitutionGoal(
const DrvOutput & id,
RepairFlag repair = NoRepair,
std::optional<ContentAddress> ca = std::nullopt
) override;
/**
* Make a goal corresponding to the `DerivedPath`.
@ -299,18 +239,14 @@ private:
* It will be a `DerivationGoal` for a `DerivedPath::Built` or
* a `SubstitutionGoal` for a `DerivedPath::Opaque`.
*/
GoalPtr makeGoal(const DerivedPath & req, BuildMode buildMode = bmNormal) override;
std::pair<GoalPtr, kj::Promise<Result<Goal::WorkResult>>>
makeGoal(const DerivedPath & req, BuildMode buildMode = bmNormal) override;
public:
/**
* Unregisters a running child process.
*/
void childTerminated(Goal * goal);
/**
* Loop until the specified top-level goals have finished.
*/
Goals run(std::function<Goals (GoalFactory &)> req);
Results run(std::function<Targets (GoalFactory &)> req);
/***
* The exit status in case of failure.

View file

@ -6,7 +6,7 @@
namespace nix {
// TODO: make pluggable.
void builtinFetchurl(const BasicDerivation & drv, const std::string & netrcData);
void builtinFetchurl(const BasicDerivation & drv, const std::string & netrcData, const std::string & caFileData);
void builtinUnpackChannel(const BasicDerivation & drv);
}

View file

@ -7,7 +7,7 @@
namespace nix {
void builtinFetchurl(const BasicDerivation & drv, const std::string & netrcData)
void builtinFetchurl(const BasicDerivation & drv, const std::string & netrcData, const std::string & caFileData)
{
/* Make the host's netrc data available. Too bad curl requires
this to be stored in a file. It would be nice if we could just
@ -17,6 +17,9 @@ void builtinFetchurl(const BasicDerivation & drv, const std::string & netrcData)
writeFile(settings.netrcFile, netrcData, 0600);
}
settings.caFile = "ca-certificates.crt";
writeFile(settings.caFile, caFileData, 0600);
auto getAttr = [&](const std::string & name) {
auto i = drv.env.find(name);
if (i == drv.env.end()) throw Error("attribute '%s' missing", name);
@ -33,10 +36,7 @@ void builtinFetchurl(const BasicDerivation & drv, const std::string & netrcData)
auto fetch = [&](const std::string & url) {
/* No need to do TLS verification, because we check the hash of
the result anyway. */
FileTransferRequest request(url);
request.verifyTLS = false;
auto raw = fileTransfer->download(std::move(request));
auto decompressor = makeDecompressionSource(

View file

@ -115,7 +115,7 @@ struct curlFileTransfer : public FileTransfer
if (!done)
fail(FileTransferError(Interrupted, {}, "download of '%s' was interrupted", request.uri));
} catch (...) {
ignoreException();
ignoreExceptionInDestructor();
}
}
@ -337,7 +337,7 @@ struct curlFileTransfer : public FileTransfer
// wrapping user `callback`s instead is not possible because the
// Callback api expects std::functions, and copying Callbacks is
// not possible due the promises they hold.
if (code == CURLE_OK && !dataCallback) {
if (code == CURLE_OK && !dataCallback && result.data.length() > 0) {
result.data = decompress(encoding, result.data);
}

View file

@ -923,8 +923,8 @@ void LocalStore::autoGC(bool sync)
} catch (...) {
// FIXME: we could propagate the exception to the
// future, but we don't really care.
ignoreException();
// future, but we don't really care. (what??)
ignoreExceptionInDestructor();
}
}).detach();

View file

@ -443,7 +443,7 @@ static bool initLibStoreDone = false;
void assertLibStoreInitialized() {
if (!initLibStoreDone) {
printError("The program must call nix::initNix() before calling any libstore library functions.");
abort();
std::terminate();
};
}

View file

@ -62,8 +62,6 @@ struct LocalStore::State::Stmts {
SQLiteStmt QueryReferences;
SQLiteStmt QueryReferrers;
SQLiteStmt InvalidatePath;
SQLiteStmt InvalidatePhantomReferrers;
SQLiteStmt QueryPhantomReferrers;
SQLiteStmt AddDerivationOutput;
SQLiteStmt RegisterRealisedOutput;
SQLiteStmt UpdateRealisedOutput;
@ -386,10 +384,6 @@ LocalStore::LocalStore(const Params & params)
"select path from Refs join ValidPaths on referrer = id where reference = (select id from ValidPaths where path = ?);");
state->stmts->InvalidatePath.create(state->db,
"delete from ValidPaths where path = ?;");
state->stmts->InvalidatePhantomReferrers.create(state->db,
"delete from Refs where referrer IN (select referrer from Refs left join ValidPaths on referrer = id where reference = (select id from ValidPaths where path = ?));");
state->stmts->QueryPhantomReferrers.create(state->db,
"select referrer from Refs left join ValidPaths on referrer = id where reference = (select id from ValidPaths where path = ?);");
state->stmts->AddDerivationOutput.create(state->db,
"insert or replace into DerivationOutputs (drv, id, path) values (?, ?, ?);");
state->stmts->QueryValidDerivers.create(state->db,
@ -487,7 +481,7 @@ LocalStore::~LocalStore()
unlink(fnTempRoots.c_str());
}
} catch (...) {
ignoreException();
ignoreExceptionInDestructor();
}
}
@ -670,27 +664,6 @@ static void canonicalisePathMetaData_(
if (!(S_ISREG(st.st_mode) || S_ISDIR(st.st_mode) || S_ISLNK(st.st_mode)))
throw Error("file '%1%' has an unsupported type", path);
#if __linux__
/* Remove extended attributes / ACLs. */
ssize_t eaSize = llistxattr(path.c_str(), nullptr, 0);
if (eaSize < 0) {
if (errno != ENOTSUP && errno != ENODATA)
throw SysError("querying extended attributes of '%s'", path);
} else if (eaSize > 0) {
std::vector<char> eaBuf(eaSize);
if ((eaSize = llistxattr(path.c_str(), eaBuf.data(), eaBuf.size())) < 0)
throw SysError("querying extended attributes of '%s'", path);
for (auto & eaName: tokenizeString<Strings>(std::string(eaBuf.data(), eaSize), std::string("\000", 1))) {
if (settings.ignoredAcls.get().count(eaName)) continue;
if (lremovexattr(path.c_str(), eaName.c_str()) == -1)
throw SysError("removing extended attribute '%s' from '%s'", eaName, path);
}
}
#endif
/* Fail if the file is not owned by the build user. This prevents
us from messing up the ownership/permissions of files
hard-linked into the output (e.g. "ln /etc/shadow $out/foo").
@ -705,6 +678,29 @@ static void canonicalisePathMetaData_(
return;
}
#if __linux__
/* Remove extended attributes / ACLs. */
ssize_t eaSize = llistxattr(path.c_str(), nullptr, 0);
if (eaSize < 0) {
if (errno != ENOTSUP && errno != ENODATA)
throw SysError("querying extended attributes of '%s'", path);
} else if (eaSize > 0) {
std::vector<char> eaBuf(eaSize);
if ((eaSize = llistxattr(path.c_str(), eaBuf.data(), eaBuf.size())) < 0)
throw SysError("querying extended attributes of '%s'", path);
if (S_ISREG(st.st_mode) || S_ISDIR(st.st_mode))
chmod(path.c_str(), st.st_mode | S_IWUSR);
for (auto & eaName: tokenizeString<Strings>(std::string(eaBuf.data(), eaSize), std::string("\000", 1))) {
if (settings.ignoredAcls.get().count(eaName)) continue;
if (lremovexattr(path.c_str(), eaName.c_str()) == -1)
throw SysError("removing extended attribute '%s' from '%s'", eaName, path);
}
}
#endif
inodesSeen.insert(Inode(st.st_dev, st.st_ino));
canonicaliseTimestampAndPermissions(path, st);
@ -1222,11 +1218,11 @@ void LocalStore::addToStore(const ValidPathInfo & info, Source & source,
bool narRead = false;
Finally cleanup = [&]() {
if (!narRead) {
ParseSink sink;
NARParseVisitor sink;
try {
parseDump(sink, source);
} catch (...) {
ignoreException();
ignoreExceptionExceptInterrupt();
}
}
};
@ -1528,18 +1524,6 @@ void LocalStore::invalidatePathChecked(const StorePath & path)
if (!referrers.empty())
throw PathInUse("cannot delete path '%s' because it is in use by %s",
printStorePath(path), showPaths(referrers));
// Note: `queryReferrers` will only return *valid* referrers.
// i.e. referrer for which there is a *ValidPath* row in the SQLite database.
// In the unfortunate situation where a valid path is removed but its corresponding `Refs` are not removed (*), we better just invalidate all these phantom referrers,
// otherwise we will create a foreign key violation when we actually try to invalidate paths.
//
// (*) : yes, there's a "ON DELETE CASCADE" on the referrer foreign key.
// Unfortunately, in practice, it doesn't ensure integrity over large SQLite databases.
if (hasPhantomReferrers(*state, path)) {
warn("'%s' has phantom referrers (disappeared referrers from the valid path table)", printStorePath(path));
invalidatePhantomReferrers(*state, path);
}
invalidatePath(*state, path);
}
@ -1547,24 +1531,6 @@ void LocalStore::invalidatePathChecked(const StorePath & path)
});
}
bool LocalStore::hasPhantomReferrers(State & state, const StorePath & path)
{
return retrySQLite<bool>([&]() -> bool {
debug("checking for phantom referrers for '%s'", printStorePath(path));
auto useQueryPhantomReferrers(state.stmts->QueryPhantomReferrers.use()(printStorePath(path)));
return useQueryPhantomReferrers.next();
});
}
void LocalStore::invalidatePhantomReferrers(State & state, const StorePath & path)
{
retrySQLite<void>([&]() {
debug("invalidating phantom referrers to '%s'", printStorePath(path));
state.stmts->InvalidatePhantomReferrers.use()(printStorePath(path)).exec();
});
}
bool LocalStore::verifyStore(bool checkContents, RepairFlag repair)
{

View file

@ -322,14 +322,6 @@ private:
* Delete a path from the Nix store.
*/
void invalidatePathChecked(const StorePath & path);
/**
* Check if there's phantom referrers for a certain path in the Nix SQLite database
*/
bool hasPhantomReferrers(State & state, const StorePath & path);
/**
* Invalidate all phantom referrers from the Nix SQLite database.
*/
void invalidatePhantomReferrers(State & state, const StorePath & path);
void verifyPath(const StorePath & path, const StorePathSet & store,
StorePathSet & done, StorePathSet & validPaths, RepairFlag repair, bool & errors);

View file

@ -73,8 +73,16 @@ struct SimpleUserLock : UserLock
debug("trying user '%s'", i);
struct passwd * pw = getpwnam(i.c_str());
if (!pw)
throw Error("the user '%s' in the group '%s' does not exist", i, settings.buildUsersGroup);
if (!pw) {
#ifdef __APPLE__
#define APPLE_HINT "\n\nhint: this may be caused by an update to macOS Sequoia breaking existing Lix installations.\n" \
"See the macOS Sequoia page on the Lix wiki for detailed repair instructions: https://wiki.lix.systems/link/81"
#else
#define APPLE_HINT
#endif
throw Error("the user '%s' in the group '%s' does not exist" APPLE_HINT, i, settings.buildUsersGroup);
#undef APPLE_HINT
}
auto fnUserLock = fmt("%s/userpool/%s", settings.nixStateDir,pw->pw_uid);

View file

@ -2,6 +2,7 @@
#include "archive.hh"
#include <map>
#include <memory>
#include <stack>
#include <algorithm>
@ -33,7 +34,7 @@ struct NarAccessor : public FSAccessor
NarMember root;
struct NarIndexer : ParseSink, Source
struct NarIndexer : NARParseVisitor, Source
{
NarAccessor & acc;
Source & source;
@ -44,11 +45,12 @@ struct NarAccessor : public FSAccessor
uint64_t pos = 0;
public:
NarIndexer(NarAccessor & acc, Source & source)
: acc(acc), source(source)
{ }
void createMember(const Path & path, NarMember member)
NarMember & createMember(const Path & path, NarMember member)
{
size_t level = std::count(path.begin(), path.end(), '/');
while (parents.size() > level) parents.pop();
@ -62,6 +64,8 @@ struct NarAccessor : public FSAccessor
auto result = parents.top()->children.emplace(baseNameOf(path), std::move(member));
parents.push(&result.first->second);
}
return *parents.top();
}
void createDirectory(const Path & path) override
@ -69,28 +73,17 @@ struct NarAccessor : public FSAccessor
createMember(path, {FSAccessor::Type::tDirectory, false, 0, 0});
}
void createRegularFile(const Path & path) override
std::unique_ptr<FileHandle> createRegularFile(const Path & path, uint64_t size, bool executable) override
{
createMember(path, {FSAccessor::Type::tRegular, false, 0, 0});
}
auto & memb = createMember(path, {FSAccessor::Type::tRegular, false, 0, 0});
void closeRegularFile() override
{ }
void isExecutable() override
{
parents.top()->isExecutable = true;
}
void preallocateContents(uint64_t size) override
{
assert(size <= std::numeric_limits<uint64_t>::max());
parents.top()->size = (uint64_t) size;
parents.top()->start = pos;
}
memb.size = (uint64_t) size;
memb.start = pos;
memb.isExecutable = executable;
void receiveContents(std::string_view data) override
{ }
return std::make_unique<FileHandle>();
}
void createSymlink(const Path & path, const std::string & target) override
{

View file

@ -31,7 +31,7 @@ struct MakeReadOnly
/* This will make the path read-only. */
if (path != "") canonicaliseTimestampAndPermissions(path);
} catch (...) {
ignoreException();
ignoreExceptionInDestructor();
}
}
};

View file

@ -145,7 +145,7 @@ PathLocks::~PathLocks()
try {
unlock();
} catch (...) {
ignoreException();
ignoreExceptionInDestructor();
}
}

View file

@ -1,6 +1,7 @@
#pragma once
///@file
#include "error.hh"
#include "file-descriptor.hh"
namespace nix {
@ -53,7 +54,7 @@ struct FdLock
if (acquired)
lockFile(fd, ltNone, false);
} catch (SysError &) {
ignoreException();
ignoreExceptionInDestructor();
}
}
};

View file

@ -25,7 +25,7 @@ std::shared_ptr<LocalStore> LocalStore::makeLocalStore(const Params & params)
#endif
}
std::shared_ptr<LocalDerivationGoal> LocalDerivationGoal::makeLocalDerivationGoal(
std::unique_ptr<LocalDerivationGoal> LocalDerivationGoal::makeLocalDerivationGoal(
const StorePath & drvPath,
const OutputsSpec & wantedOutputs,
Worker & worker,
@ -34,17 +34,17 @@ std::shared_ptr<LocalDerivationGoal> LocalDerivationGoal::makeLocalDerivationGoa
)
{
#if __linux__
return std::make_shared<LinuxLocalDerivationGoal>(drvPath, wantedOutputs, worker, isDependency, buildMode);
return std::make_unique<LinuxLocalDerivationGoal>(drvPath, wantedOutputs, worker, isDependency, buildMode);
#elif __APPLE__
return std::make_shared<DarwinLocalDerivationGoal>(drvPath, wantedOutputs, worker, isDependency, buildMode);
return std::make_unique<DarwinLocalDerivationGoal>(drvPath, wantedOutputs, worker, isDependency, buildMode);
#elif __FreeBSD__
return std::make_shared<FreeBSDLocalDerivationGoal>(drvPath, wantedOutputs, worker, isDependency, buildMode);
return std::make_unique<FreeBSDLocalDerivationGoal>(drvPath, wantedOutputs, worker, isDependency, buildMode);
#else
return std::make_shared<FallbackLocalDerivationGoal>(drvPath, wantedOutputs, worker, isDependency, buildMode);
return std::make_unique<FallbackLocalDerivationGoal>(drvPath, wantedOutputs, worker, isDependency, buildMode);
#endif
}
std::shared_ptr<LocalDerivationGoal> LocalDerivationGoal::makeLocalDerivationGoal(
std::unique_ptr<LocalDerivationGoal> LocalDerivationGoal::makeLocalDerivationGoal(
const StorePath & drvPath,
const BasicDerivation & drv,
const OutputsSpec & wantedOutputs,
@ -54,19 +54,19 @@ std::shared_ptr<LocalDerivationGoal> LocalDerivationGoal::makeLocalDerivationGoa
)
{
#if __linux__
return std::make_shared<LinuxLocalDerivationGoal>(
return std::make_unique<LinuxLocalDerivationGoal>(
drvPath, drv, wantedOutputs, worker, isDependency, buildMode
);
#elif __APPLE__
return std::make_shared<DarwinLocalDerivationGoal>(
return std::make_unique<DarwinLocalDerivationGoal>(
drvPath, drv, wantedOutputs, worker, isDependency, buildMode
);
#elif __FreeBSD__
return std::make_shared<FreeBSDLocalDerivationGoal>(
return std::make_unique<FreeBSDLocalDerivationGoal>(
drvPath, drv, wantedOutputs, worker, isDependency, buildMode
);
#else
return std::make_shared<FallbackLocalDerivationGoal>(
return std::make_unique<FallbackLocalDerivationGoal>(
drvPath, drv, wantedOutputs, worker, isDependency, buildMode
);
#endif

View file

@ -29,7 +29,7 @@ ref<FSAccessor> RemoteFSAccessor::addToCache(std::string_view hashPart, std::str
/* FIXME: do this asynchronously. */
writeFile(makeCacheFile(hashPart, "nar"), nar);
} catch (...) {
ignoreException();
ignoreExceptionExceptInterrupt();
}
}
@ -41,7 +41,7 @@ ref<FSAccessor> RemoteFSAccessor::addToCache(std::string_view hashPart, std::str
nlohmann::json j = listNar(narAccessor, "", true);
writeFile(makeCacheFile(hashPart, "ls"), j.dump());
} catch (...) {
ignoreException();
ignoreExceptionExceptInterrupt();
}
}

View file

@ -1,3 +1,4 @@
#include "error.hh"
#include "serialise.hh"
#include "signals.hh"
#include "path-with-outputs.hh"
@ -855,7 +856,7 @@ RemoteStore::Connection::~Connection()
try {
to.flush();
} catch (...) {
ignoreException();
ignoreExceptionInDestructor();
}
}
@ -985,7 +986,7 @@ void RemoteStore::ConnectionHandle::withFramedSink(std::function<void(Sink & sin
try {
std::rethrow_exception(ex);
} catch (...) {
ignoreException();
ignoreExceptionExceptInterrupt();
}
}
}

View file

@ -85,7 +85,7 @@ SQLite::~SQLite()
if (db && sqlite3_close(db) != SQLITE_OK)
SQLiteError::throw_(db, "closing database");
} catch (...) {
ignoreException();
ignoreExceptionInDestructor();
}
}
@ -124,7 +124,7 @@ SQLiteStmt::~SQLiteStmt()
if (stmt && sqlite3_finalize(stmt) != SQLITE_OK)
SQLiteError::throw_(db, "finalizing statement '%s'", sql);
} catch (...) {
ignoreException();
ignoreExceptionInDestructor();
}
}
@ -248,7 +248,7 @@ SQLiteTxn::~SQLiteTxn()
if (active && sqlite3_exec(db, "rollback;", 0, 0, 0) != SQLITE_OK)
SQLiteError::throw_(db, "aborting transaction");
} catch (...) {
ignoreException();
ignoreExceptionInDestructor();
}
}

View file

@ -379,6 +379,48 @@ void Store::addMultipleToStore(
}
}
namespace {
/**
* If the NAR archive contains a single file at top-level, then save
* the contents of the file to `s`. Otherwise assert.
*/
struct RetrieveRegularNARVisitor : NARParseVisitor
{
struct MyFileHandle : public FileHandle
{
Sink & sink;
void receiveContents(std::string_view data) override
{
sink(data);
}
private:
MyFileHandle(Sink & sink) : sink(sink) {}
friend struct RetrieveRegularNARVisitor;
};
Sink & sink;
RetrieveRegularNARVisitor(Sink & sink) : sink(sink) { }
std::unique_ptr<FileHandle> createRegularFile(const Path & path, uint64_t size, bool executable) override
{
return std::unique_ptr<MyFileHandle>(new MyFileHandle{sink});
}
void createDirectory(const Path & path) override
{
assert(false && "RetrieveRegularNARVisitor::createDirectory must not be called");
}
void createSymlink(const Path & path, const std::string & target) override
{
assert(false && "RetrieveRegularNARVisitor::createSymlink must not be called");
}
};
}
/*
The aim of this function is to compute in one pass the correct ValidPathInfo for
@ -413,7 +455,7 @@ ValidPathInfo Store::addToStoreSlow(std::string_view name, const Path & srcPath,
/* Note that fileSink and unusualHashTee must be mutually exclusive, since
they both write to caHashSink. Note that that requisite is currently true
because the former is only used in the flat case. */
RetrieveRegularNARSink fileSink { caHashSink };
RetrieveRegularNARVisitor fileSink { caHashSink };
TeeSink unusualHashTee { narHashSink, caHashSink };
auto & narSink = method == FileIngestionMethod::Recursive && hashAlgo != HashType::SHA256
@ -429,7 +471,7 @@ ValidPathInfo Store::addToStoreSlow(std::string_view name, const Path & srcPath,
information to narSink. */
TeeSource tapped { fileSource, narSink };
ParseSink blank;
NARParseVisitor blank;
auto & parseSink = method == FileIngestionMethod::Flat
? fileSink
: blank;
@ -1121,7 +1163,7 @@ std::map<StorePath, StorePath> copyPaths(
// not be within our control to change that, and we might still want
// to at least copy the output paths.
if (e.missingFeature == Xp::CaDerivations)
ignoreException();
ignoreExceptionExceptInterrupt();
else
throw;
}

View file

@ -334,7 +334,7 @@ Generator<Entry> parse(Source & source)
}
static WireFormatGenerator restore(ParseSink & sink, Generator<nar::Entry> nar)
static WireFormatGenerator restore(NARParseVisitor & sink, Generator<nar::Entry> nar)
{
while (auto entry = nar.next()) {
co_yield std::visit(
@ -347,16 +347,13 @@ static WireFormatGenerator restore(ParseSink & sink, Generator<nar::Entry> nar)
},
[&](nar::File f) {
return [](auto f, auto & sink) -> WireFormatGenerator {
sink.createRegularFile(f.path);
sink.preallocateContents(f.size);
if (f.executable) {
sink.isExecutable();
}
auto handle = sink.createRegularFile(f.path, f.size, f.executable);
while (auto block = f.contents.next()) {
sink.receiveContents(std::string_view{block->data(), block->size()});
handle->receiveContents(std::string_view{block->data(), block->size()});
co_yield *block;
}
sink.closeRegularFile();
handle->close();
}(std::move(f), sink);
},
[&](nar::Symlink sl) {
@ -377,12 +374,12 @@ static WireFormatGenerator restore(ParseSink & sink, Generator<nar::Entry> nar)
}
}
WireFormatGenerator parseAndCopyDump(ParseSink & sink, Source & source)
WireFormatGenerator parseAndCopyDump(NARParseVisitor & sink, Source & source)
{
return restore(sink, nar::parse(source));
}
void parseDump(ParseSink & sink, Source & source)
void parseDump(NARParseVisitor & sink, Source & source)
{
auto parser = parseAndCopyDump(sink, source);
while (parser.next()) {
@ -390,11 +387,99 @@ void parseDump(ParseSink & sink, Source & source)
}
}
struct RestoreSink : ParseSink
/*
* Note [NAR restoration security]:
* It's *critical* that NAR restoration will never overwrite anything even if
* duplicate filenames are passed in. It is inevitable that not all NARs are
* fit to actually successfully restore to the target filesystem; errors may
* occur due to collisions, and this *must* cause the NAR to be rejected.
*
* Although the filenames are blocked from being *the same bytes* by a higher
* layer, filesystems have other ideas on every platform:
* - The store may be on a case-insensitive filesystem like APFS, ext4 with
* casefold directories, zfs with casesensitivity=insensitive
* - The store may be on a Unicode normalizing (or normalization-insensitive)
* filesystem like APFS (where files are looked up by
* hash(normalize(fname))), HFS+ (where file names are always normalized to
* approximately NFD), or zfs with normalization=formC, etc.
*
* It is impossible to know the version of Unicode being used by the underlying
* filesystem, thus it is *impossible* to stop these collisions.
*
* Overwriting files as a result of invalid NARs will cause a security bug like
* CppNix's CVE-2024-45593 (GHSA-h4vv-h3jq-v493)
*/
/**
* This code restores NARs from disk.
*
* See Note [NAR restoration security] for security invariants in this procedure.
*
*/
struct NARRestoreVisitor : NARParseVisitor
{
Path dstPath;
AutoCloseFD fd;
private:
class MyFileHandle : public FileHandle
{
AutoCloseFD fd;
MyFileHandle(AutoCloseFD && fd, uint64_t size, bool executable) : FileHandle(), fd(std::move(fd))
{
if (executable) {
makeExecutable();
}
maybePreallocateContents(size);
}
void makeExecutable()
{
struct stat st;
if (fstat(fd.get(), &st) == -1)
throw SysError("fstat");
if (fchmod(fd.get(), st.st_mode | (S_IXUSR | S_IXGRP | S_IXOTH)) == -1)
throw SysError("fchmod");
}
void maybePreallocateContents(uint64_t len)
{
if (!archiveSettings.preallocateContents)
return;
#if HAVE_POSIX_FALLOCATE
if (len) {
errno = posix_fallocate(fd.get(), 0, len);
/* Note that EINVAL may indicate that the underlying
filesystem doesn't support preallocation (e.g. on
OpenSolaris). Since preallocation is just an
optimisation, ignore it. */
if (errno && errno != EINVAL && errno != EOPNOTSUPP && errno != ENOSYS)
throw SysError("preallocating file of %1% bytes", len);
}
#endif
}
public:
~MyFileHandle() = default;
virtual void close() override
{
/* Call close explicitly to make sure the error is checked */
fd.close();
}
void receiveContents(std::string_view data) override
{
writeFull(fd.get(), data);
}
friend struct NARRestoreVisitor;
};
public:
void createDirectory(const Path & path) override
{
Path p = dstPath + path;
@ -402,49 +487,13 @@ struct RestoreSink : ParseSink
throw SysError("creating directory '%1%'", p);
};
void createRegularFile(const Path & path) override
std::unique_ptr<FileHandle> createRegularFile(const Path & path, uint64_t size, bool executable) override
{
Path p = dstPath + path;
fd = AutoCloseFD{open(p.c_str(), O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC, 0666)};
AutoCloseFD fd = AutoCloseFD{open(p.c_str(), O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC, 0666)};
if (!fd) throw SysError("creating file '%1%'", p);
}
void closeRegularFile() override
{
/* Call close explicitly to make sure the error is checked */
fd.close();
}
void isExecutable() override
{
struct stat st;
if (fstat(fd.get(), &st) == -1)
throw SysError("fstat");
if (fchmod(fd.get(), st.st_mode | (S_IXUSR | S_IXGRP | S_IXOTH)) == -1)
throw SysError("fchmod");
}
void preallocateContents(uint64_t len) override
{
if (!archiveSettings.preallocateContents)
return;
#if HAVE_POSIX_FALLOCATE
if (len) {
errno = posix_fallocate(fd.get(), 0, len);
/* Note that EINVAL may indicate that the underlying
filesystem doesn't support preallocation (e.g. on
OpenSolaris). Since preallocation is just an
optimisation, ignore it. */
if (errno && errno != EINVAL && errno != EOPNOTSUPP && errno != ENOSYS)
throw SysError("preallocating file of %1% bytes", len);
}
#endif
}
void receiveContents(std::string_view data) override
{
writeFull(fd.get(), data);
return std::unique_ptr<MyFileHandle>(new MyFileHandle(std::move(fd), size, executable));
}
void createSymlink(const Path & path, const std::string & target) override
@ -457,7 +506,7 @@ struct RestoreSink : ParseSink
void restorePath(const Path & path, Source & source)
{
RestoreSink sink;
NARRestoreVisitor sink;
sink.dstPath = path;
parseDump(sink, source);
}
@ -468,10 +517,9 @@ WireFormatGenerator copyNAR(Source & source)
// FIXME: if 'source' is the output of dumpPath() followed by EOF,
// we should just forward all data directly without parsing.
static ParseSink parseSink; /* null sink; just parse the NAR */
static NARParseVisitor parseSink; /* null sink; just parse the NAR */
return parseAndCopyDump(parseSink, source);
}
}

View file

@ -76,45 +76,47 @@ WireFormatGenerator dumpString(std::string_view s);
/**
* \todo Fix this API, it sucks.
* A visitor for NAR parsing that performs filesystem (or virtual-filesystem)
* actions to restore a NAR.
*
* Methods of this may arbitrarily fail due to filename collisions.
*/
struct ParseSink
struct NARParseVisitor
{
virtual void createDirectory(const Path & path) { };
virtual void createRegularFile(const Path & path) { };
virtual void closeRegularFile() { };
virtual void isExecutable() { };
virtual void preallocateContents(uint64_t size) { };
virtual void receiveContents(std::string_view data) { };
virtual void createSymlink(const Path & path, const std::string & target) { };
};
/**
* If the NAR archive contains a single file at top-level, then save
* the contents of the file to `s`. Otherwise barf.
*/
struct RetrieveRegularNARSink : ParseSink
{
bool regular = true;
Sink & sink;
RetrieveRegularNARSink(Sink & sink) : sink(sink) { }
void createDirectory(const Path & path) override
/**
* A type-erased file handle specific to this particular NARParseVisitor.
*/
struct FileHandle
{
regular = false;
FileHandle() {}
FileHandle(FileHandle const &) = delete;
FileHandle & operator=(FileHandle &) = delete;
/** Puts one block of data into the file */
virtual void receiveContents(std::string_view data) { }
/**
* Explicitly closes the file. Further operations may throw an assert.
* This exists so that closing can fail and throw an exception without doing so in a destructor.
*/
virtual void close() { }
virtual ~FileHandle() = default;
};
virtual void createDirectory(const Path & path) { }
/**
* Creates a regular file in the extraction output with the given size and executable flag.
* The size is guaranteed to be the true size of the file.
*/
[[nodiscard]]
virtual std::unique_ptr<FileHandle> createRegularFile(const Path & path, uint64_t size, bool executable)
{
return std::make_unique<FileHandle>();
}
void receiveContents(std::string_view data) override
{
sink(data);
}
void createSymlink(const Path & path, const std::string & target) override
{
regular = false;
}
virtual void createSymlink(const Path & path, const std::string & target) { }
};
namespace nar {
@ -160,8 +162,8 @@ Generator<Entry> parse(Source & source);
}
WireFormatGenerator parseAndCopyDump(ParseSink & sink, Source & source);
void parseDump(ParseSink & sink, Source & source);
WireFormatGenerator parseAndCopyDump(NARParseVisitor & sink, Source & source);
void parseDump(NARParseVisitor & sink, Source & source);
void restorePath(const Path & path, Source & source);

View file

@ -0,0 +1,101 @@
#pragma once
/// @file
#include <kj/async.h>
#include <kj/common.h>
#include <kj/vector.h>
#include <list>
#include <optional>
#include <type_traits>
namespace nix {
template<typename K, typename V>
class AsyncCollect
{
public:
using Item = std::conditional_t<std::is_void_v<V>, K, std::pair<K, V>>;
private:
kj::ForkedPromise<void> allPromises;
std::list<Item> results;
size_t remaining;
kj::ForkedPromise<void> signal;
kj::Maybe<kj::Own<kj::PromiseFulfiller<void>>> notify;
void oneDone(Item item)
{
results.emplace_back(std::move(item));
remaining -= 1;
KJ_IF_MAYBE (n, notify) {
(*n)->fulfill();
notify = nullptr;
}
}
kj::Promise<void> collectorFor(K key, kj::Promise<V> promise)
{
if constexpr (std::is_void_v<V>) {
return promise.then([this, key{std::move(key)}] { oneDone(std::move(key)); });
} else {
return promise.then([this, key{std::move(key)}](V v) {
oneDone(Item{std::move(key), std::move(v)});
});
}
}
kj::ForkedPromise<void> waitForAll(kj::Array<std::pair<K, kj::Promise<V>>> & promises)
{
kj::Vector<kj::Promise<void>> wrappers;
for (auto & [key, promise] : promises) {
wrappers.add(collectorFor(std::move(key), std::move(promise)));
}
return kj::joinPromisesFailFast(wrappers.releaseAsArray()).fork();
}
public:
AsyncCollect(kj::Array<std::pair<K, kj::Promise<V>>> && promises)
: allPromises(waitForAll(promises))
, remaining(promises.size())
, signal{nullptr}
{
}
kj::Promise<std::optional<Item>> next()
{
if (remaining == 0 && results.empty()) {
return {std::nullopt};
}
if (!results.empty()) {
auto result = std::move(results.front());
results.pop_front();
return {{std::move(result)}};
}
if (notify == nullptr) {
auto pair = kj::newPromiseAndFulfiller<void>();
notify = std::move(pair.fulfiller);
signal = pair.promise.fork();
}
return signal.addBranch().exclusiveJoin(allPromises.addBranch()).then([this] {
return next();
});
}
};
/**
* Collect the results of a list of promises, in order of completion.
* Once any input promise is rejected all promises that have not been
* resolved or rejected will be cancelled and the exception rethrown.
*/
template<typename K, typename V>
AsyncCollect<K, V> asyncCollect(kj::Array<std::pair<K, kj::Promise<V>>> promises)
{
return AsyncCollect<K, V>(std::move(promises));
}
}

View file

@ -0,0 +1,122 @@
#pragma once
/// @file
/// @brief A semaphore implementation usable from within a KJ event loop.
#include <cassert>
#include <kj/async.h>
#include <kj/common.h>
#include <kj/exception.h>
#include <kj/list.h>
#include <kj/source-location.h>
#include <memory>
#include <optional>
namespace nix {
class AsyncSemaphore
{
public:
class [[nodiscard("destroying a semaphore guard releases the semaphore immediately")]] Token
{
struct Release
{
void operator()(AsyncSemaphore * sem) const
{
sem->unsafeRelease();
}
};
std::unique_ptr<AsyncSemaphore, Release> parent;
public:
Token() = default;
Token(AsyncSemaphore & parent, kj::Badge<AsyncSemaphore>) : parent(&parent) {}
bool valid() const
{
return parent != nullptr;
}
};
private:
struct Waiter
{
kj::PromiseFulfiller<Token> & fulfiller;
kj::ListLink<Waiter> link;
kj::List<Waiter, &Waiter::link> & list;
Waiter(kj::PromiseFulfiller<Token> & fulfiller, kj::List<Waiter, &Waiter::link> & list)
: fulfiller(fulfiller)
, list(list)
{
list.add(*this);
}
~Waiter()
{
if (link.isLinked()) {
list.remove(*this);
}
}
};
const unsigned capacity_;
unsigned used_ = 0;
kj::List<Waiter, &Waiter::link> waiters;
void unsafeRelease()
{
used_ -= 1;
while (used_ < capacity_ && !waiters.empty()) {
used_ += 1;
auto & w = waiters.front();
w.fulfiller.fulfill(Token{*this, {}});
waiters.remove(w);
}
}
public:
explicit AsyncSemaphore(unsigned capacity) : capacity_(capacity) {}
KJ_DISALLOW_COPY_AND_MOVE(AsyncSemaphore);
~AsyncSemaphore()
{
assert(waiters.empty() && "destroyed a semaphore with active waiters");
}
std::optional<Token> tryAcquire()
{
if (used_ < capacity_) {
used_ += 1;
return Token{*this, {}};
} else {
return {};
}
}
kj::Promise<Token> acquire()
{
if (auto t = tryAcquire()) {
return std::move(*t);
} else {
return kj::newAdaptedPromise<Token, Waiter>(waiters);
}
}
unsigned capacity() const
{
return capacity_;
}
unsigned used() const
{
return used_;
}
unsigned available() const
{
return capacity_ - used_;
}
};
}

View file

@ -144,6 +144,7 @@ struct BrotliDecompressionSource : Source
std::unique_ptr<char[]> buf;
size_t avail_in = 0;
const uint8_t * next_in;
std::exception_ptr inputEofException = nullptr;
Source * inner;
std::unique_ptr<BrotliDecoderState, void (*)(BrotliDecoderState *)> state;
@ -167,23 +168,42 @@ struct BrotliDecompressionSource : Source
while (len && !BrotliDecoderIsFinished(state.get())) {
checkInterrupt();
while (avail_in == 0) {
while (avail_in == 0 && inputEofException == nullptr) {
try {
avail_in = inner->read(buf.get(), BUF_SIZE);
} catch (EndOfFile &) {
// No more data, but brotli may still have output remaining
// from the last call.
inputEofException = std::current_exception();
break;
}
next_in = charptr_cast<const uint8_t *>(buf.get());
}
if (!BrotliDecoderDecompressStream(
state.get(), &avail_in, &next_in, &len, &out, nullptr
))
{
BrotliDecoderResult res = BrotliDecoderDecompressStream(
state.get(), &avail_in, &next_in, &len, &out, nullptr
);
switch (res) {
case BROTLI_DECODER_RESULT_SUCCESS:
// We're done here!
goto finish;
case BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT:
// Grab more input. Don't try if we already have exhausted our input stream.
if (inputEofException != nullptr) {
std::rethrow_exception(inputEofException);
} else {
continue;
}
case BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT:
// Need more output space: we can only get another buffer by someone calling us again, so get out.
goto finish;
case BROTLI_DECODER_RESULT_ERROR:
throw CompressionError("error while decompressing brotli file");
}
}
finish:
if (begin != out) {
return out - begin;
} else {

View file

@ -49,7 +49,7 @@ unsigned int getMaxCPU()
auto period = cpuMaxParts[1];
if (quota != "max")
return std::ceil(std::stoi(quota) / std::stof(period));
} catch (Error &) { ignoreException(lvlDebug); }
} catch (Error &) { ignoreExceptionInDestructor(lvlDebug); }
#endif
return 0;

View file

@ -4,6 +4,7 @@
#include "position.hh"
#include "terminal.hh"
#include "strings.hh"
#include "signals.hh"
#include <iostream>
#include <optional>
@ -416,7 +417,7 @@ std::ostream & showErrorInfo(std::ostream & out, const ErrorInfo & einfo, bool s
return out;
}
void ignoreException(Verbosity lvl)
void ignoreExceptionInDestructor(Verbosity lvl)
{
/* Make sure no exceptions leave this function.
printError() also throws when remote is closed. */
@ -429,4 +430,15 @@ void ignoreException(Verbosity lvl)
} catch (...) { }
}
void ignoreExceptionExceptInterrupt(Verbosity lvl)
{
try {
throw;
} catch (const Interrupted & e) {
throw;
} catch (std::exception & e) {
printMsg(lvl, "error (ignored): %1%", e.what());
}
}
}

View file

@ -204,7 +204,22 @@ public:
/**
* Exception handling in destructors: print an error message, then
* ignore the exception.
*
* If you're not in a destructor, you usually want to use `ignoreExceptionExceptInterrupt()`.
*
* This function might also be used in callbacks whose caller may not handle exceptions,
* but ideally we propagate the exception using an exception_ptr in such cases.
* See e.g. `PackBuilderContext`
*/
void ignoreException(Verbosity lvl = lvlError);
void ignoreExceptionInDestructor(Verbosity lvl = lvlError);
/**
* Not destructor-safe.
* Print an error message, then ignore the exception.
* If the exception is an `Interrupted` exception, rethrow it.
*
* This may be used in a few places where Interrupt can't happen, but that's ok.
*/
void ignoreExceptionExceptInterrupt(Verbosity lvl = lvlError);
}

View file

@ -247,7 +247,7 @@ constexpr std::array<ExperimentalFeatureDetails, numXpFeatures> xpFeatureDetails
.tag = Xp::ReplAutomation,
.name = "repl-automation",
.description = R"(
Makes the repl not use readline/editline, print ENQ (U+0005) when ready for a command, and take commands followed by newline.
Makes the repl not use editline, print ENQ (U+0005) when ready for a command, and take commands followed by newline.
)",
},
}};

View file

@ -146,7 +146,7 @@ AutoCloseFD::~AutoCloseFD()
try {
close();
} catch (...) {
ignoreException();
ignoreExceptionInDestructor();
}
}

View file

@ -522,7 +522,7 @@ AutoDelete::~AutoDelete()
}
}
} catch (...) {
ignoreException();
ignoreExceptionInDestructor();
}
}

View file

@ -210,7 +210,7 @@ inline Paths createDirs(PathView path)
}
/**
* Create a symlink.
* Create a symlink. Throws if the symlink exists.
*/
void createSymlink(const Path & target, const Path & link);

View file

@ -136,11 +136,17 @@ inline std::string fmt(const char * s)
template<typename... Args>
inline std::string fmt(const std::string & fs, const Args &... args)
{
try {
boost::format f(fs);
fmt_internal::setExceptions(f);
(f % ... % args);
return f.str();
} catch (boost::io::format_error & fe) {
// I don't care who catches this, we do not put up with boost format errors
// Give me a stack trace and a core dump
std::cerr << "nix::fmt threw format error. Original format string: '";
std::cerr << fs << "'; number of arguments: " << sizeof...(args) << "\n";
std::terminate();
}
/**
@ -174,15 +180,13 @@ public:
std::cerr << "HintFmt received incorrect number of format args. Original format string: '";
std::cerr << format << "'; number of arguments: " << sizeof...(args) << "\n";
// And regardless of the coredump give me a damn stacktrace.
printStackTrace();
abort();
std::terminate();
}
} catch (boost::io::format_error & ex) {
// Same thing, but for anything that happens in the member initializers.
std::cerr << "HintFmt received incorrect format string. Original format string: '";
std::cerr << format << "'; number of arguments: " << sizeof...(args) << "\n";
printStackTrace();
abort();
std::terminate();
}
HintFmt(const HintFmt & hf) : fmt(hf.fmt) {}

View file

@ -352,7 +352,7 @@ Activity::~Activity()
try {
logger.stopActivity(id);
} catch (...) {
ignoreException();
ignoreExceptionInDestructor();
}
}

View file

@ -53,6 +53,8 @@ libutil_headers = files(
'archive.hh',
'args/root.hh',
'args.hh',
'async-collect.hh',
'async-semaphore.hh',
'backed-string-view.hh',
'box_ptr.hh',
'canon-path.hh',

View file

@ -83,7 +83,7 @@ void BufferedSink::flush()
FdSink::~FdSink()
{
try { flush(); } catch (...) { ignoreException(); }
try { flush(); } catch (...) { ignoreExceptionInDestructor(); }
}

View file

@ -77,6 +77,11 @@ struct Source
* Store up to len in the buffer pointed to by data, and
* return the number of bytes stored. It blocks until at least
* one byte is available.
*
* Should not return 0 (generally you want to throw EndOfFile), but nothing
* stops that.
*
* \throws EndOfFile if there is no more data.
*/
virtual size_t read(char * data, size_t len) = 0;
@ -544,7 +549,7 @@ struct FramedSource : Source
}
}
} catch (...) {
ignoreException();
ignoreExceptionInDestructor();
}
}
@ -590,7 +595,7 @@ struct FramedSink : nix::BufferedSink
to << 0;
to.flush();
} catch (...) {
ignoreException();
ignoreExceptionInDestructor();
}
}

View file

@ -12,13 +12,18 @@ std::atomic<bool> _isInterrupted = false;
thread_local std::function<bool()> interruptCheck;
Interrupted makeInterrupted()
{
return Interrupted("interrupted by the user");
}
void _interrupted()
{
/* Block user interrupts while an exception is being handled.
Throwing an exception while another exception is being handled
kills the program! */
if (!std::uncaught_exceptions()) {
throw Interrupted("interrupted by the user");
throw makeInterrupted();
}
}
@ -78,7 +83,7 @@ void triggerInterrupt()
try {
callback();
} catch (...) {
ignoreException();
ignoreExceptionInDestructor();
}
}
}

View file

@ -16,10 +16,13 @@ namespace nix {
/* User interruption. */
class Interrupted;
extern std::atomic<bool> _isInterrupted;
extern thread_local std::function<bool()> interruptCheck;
Interrupted makeInterrupted();
void _interrupted();
void inline checkInterrupt()

View file

@ -109,9 +109,8 @@ void ThreadPool::doWork(bool mainThread)
try {
std::rethrow_exception(exc);
} catch (std::exception & e) {
if (!dynamic_cast<Interrupted*>(&e) &&
!dynamic_cast<ThreadPoolShutDown*>(&e))
ignoreException();
if (!dynamic_cast<ThreadPoolShutDown*>(&e))
ignoreExceptionExceptInterrupt();
} catch (...) {
}
}

View file

@ -1,5 +1 @@
configure_file(
input : 'unpack-channel.nix',
output : 'unpack-channel.nix',
copy : true,
)
fs.copyfile('unpack-channel.nix')

View file

@ -639,7 +639,7 @@ struct CmdDevelop : Common, MixEnvironment
throw Error("package 'nixpkgs#bashInteractive' does not provide a 'bin/bash'");
} catch (Error &) {
ignoreException();
ignoreExceptionExceptInterrupt();
}
// Override SHELL with the one chosen for this environment.

View file

@ -16,6 +16,7 @@
#include "eval-cache.hh"
#include "markdown.hh"
#include "terminal.hh"
#include "signals.hh"
#include <limits>
#include <nlohmann/json.hpp>
@ -367,9 +368,11 @@ struct CmdFlakeCheck : FlakeCommand
auto reportError = [&](const Error & e) {
try {
throw e;
} catch (Interrupted & e) {
throw;
} catch (Error & e) {
if (settings.keepGoing) {
ignoreException();
ignoreExceptionExceptInterrupt();
hasErrors = true;
}
else

View file

@ -39,14 +39,8 @@ struct CmdFmt : SourceExprCommand {
Strings programArgs{app.program};
// Propagate arguments from the CLI
if (args.empty()) {
// Format the current flake out of the box
programArgs.push_back(".");
} else {
// User wants more power, let them decide which paths to include/exclude
for (auto &i : args) {
programArgs.push_back(i);
}
for (auto &i : args) {
programArgs.push_back(i);
}
runProgramInStore(store, UseSearchPath::DontUse, app.program, programArgs);

View file

@ -82,6 +82,10 @@ struct CmdPathInfo : StorePathsCommand, MixJSON
void run(ref<Store> store, StorePaths && storePaths) override
{
// Wipe the progress bar to prevent interference with the output.
// It's not needed any more because expensive evaluation or builds are already done here.
logger->pause();
size_t pathLen = 0;
for (auto & storePath : storePaths)
pathLen = std::max(pathLen, store->printStorePath(storePath).size());

View file

@ -144,13 +144,10 @@ test "$(<<<"$out" grep -E '^error:' | wc -l)" = 1
# --keep-going and FOD
out="$(nix build -f fod-failing.nix -L 2>&1)" && status=0 || status=$?
test "$status" = 1
# one "hash mismatch" error, one "build of ... failed"
test "$(<<<"$out" grep -E '^error:' | wc -l)" = 2
<<<"$out" grepQuiet -E "hash mismatch in fixed-output derivation '.*-x1\\.drv'"
<<<"$out" grepQuiet -vE "hash mismatch in fixed-output derivation '.*-x3\\.drv'"
<<<"$out" grepQuiet -vE "hash mismatch in fixed-output derivation '.*-x2\\.drv'"
<<<"$out" grepQuiet -E "likely URL: https://meow.puppy.forge/puppy.tar.gz"
<<<"$out" grepQuiet -vE "likely URL: https://kitty.forge/cat.tar.gz"
# at least one "hash mismatch" error, one "build of ... failed"
test "$(<<<"$out" grep -E '^error:' | wc -l)" -ge 2
<<<"$out" grepQuiet -E "hash mismatch in fixed-output derivation '.*-x.\\.drv'"
<<<"$out" grepQuiet -E "likely URL: "
<<<"$out" grepQuiet -E "error: build of '.*-x[1-4]\\.drv\\^out', '.*-x[1-4]\\.drv\\^out', '.*-x[1-4]\\.drv\\^out', '.*-x[1-4]\\.drv\\^out' failed"
out="$(nix build -f fod-failing.nix -L x1 x2 x3 --keep-going 2>&1)" && status=0 || status=$?
@ -167,9 +164,9 @@ test "$(<<<"$out" grep -E '^error:' | wc -l)" = 4
out="$(nix build -f fod-failing.nix -L x4 2>&1)" && status=0 || status=$?
test "$status" = 1
test "$(<<<"$out" grep -E '^error:' | wc -l)" = 2
<<<"$out" grepQuiet -E "error: 1 dependencies of derivation '.*-x4\\.drv' failed to build"
<<<"$out" grepQuiet -E "hash mismatch in fixed-output derivation '.*-x2\\.drv'"
test "$(<<<"$out" grep -E '^error:' | wc -l)" -ge 2
<<<"$out" grepQuiet -E "error: [12] dependencies of derivation '.*-x4\\.drv' failed to build"
<<<"$out" grepQuiet -E "hash mismatch in fixed-output derivation '.*-x[23]\\.drv'"
out="$(nix build -f fod-failing.nix -L x4 --keep-going 2>&1)" && status=0 || status=$?
test "$status" = 1

View file

@ -16,7 +16,6 @@ fi
export NIX_LOCALSTATE_DIR=$TEST_ROOT/var
export NIX_LOG_DIR=$TEST_ROOT/var/log/nix
export NIX_STATE_DIR=$TEST_ROOT/var/nix
export NIX_SQLITE_DATABASE=$NIX_STATE_DIR/db/db.sqlite
export NIX_CONF_DIR=$TEST_ROOT/etc
export NIX_DAEMON_SOCKET_PATH=$TEST_ROOT/dSocket
unset NIX_USER_CONF_FILES
@ -165,10 +164,6 @@ requireDaemonNewerThan () {
isDaemonNewer "$1" || skipTest "Daemon is too old"
}
requireSqliteDatabase() {
[[ -f "$NIX_SQLITE_DATABASE" ]] || skipTest "SQLite database is not used for this store implementation"
}
canUseSandbox() {
[[ ${_canUseSandbox-} ]]
}

View file

@ -53,8 +53,17 @@ out=$(nix eval --impure --raw --expr "builtins.fetchGit { url = \"file://$repo\"
[[ $status == 1 ]]
[[ $out =~ 'Cannot find Git revision' ]]
# allow revs as refs (for 2.3 compat)
[[ $(nix eval --raw --expr "builtins.readFile (builtins.fetchGit { url = \"file://$repo\"; rev = \"$devrev\"; allRefs = true; } + \"/differentbranch\")") = 'different file' ]]
rm -rf "$TEST_ROOT/test-home"
[[ $(nix eval --raw --expr "builtins.readFile (builtins.fetchGit { url = \"file://$repo\"; rev = \"$devrev\"; allRefs = true; } + \"/differentbranch\")") = 'different file' ]]
rm -rf "$TEST_ROOT/test-home"
out=$(nix eval --raw --expr "builtins.readFile (builtins.fetchGit { url = \"file://$repo\"; rev = \"$devrev\"; ref = \"lolkek\"; } + \"/differentbranch\")" 2>&1) || status=$?
[[ $status == 1 ]]
[[ $out =~ 'Cannot find Git revision' ]]
# In pure eval mode, fetchGit without a revision should fail.
[[ $(nix eval --impure --raw --expr "builtins.readFile (fetchGit \"file://$repo\" + \"/hello\")") = world ]]
(! nix eval --raw --expr "builtins.readFile (fetchGit \"file://$repo\" + \"/hello\")")
@ -228,6 +237,12 @@ export _NIX_FORCE_HTTP=1
rev_tag1_nix=$(nix eval --impure --raw --expr "(builtins.fetchGit { url = \"file://$repo\"; ref = \"refs/tags/tag1\"; }).rev")
rev_tag1=$(git -C $repo rev-parse refs/tags/tag1)
[[ $rev_tag1_nix = $rev_tag1 ]]
# Allow fetching tags w/o specifying refs/tags
rm -rf "$TEST_ROOT/test-home"
rev_tag1_nix_alt=$(nix eval --impure --raw --expr "(builtins.fetchGit { url = \"file://$repo\"; ref = \"tag1\"; }).rev")
[[ $rev_tag1_nix_alt = $rev_tag1 ]]
rev_tag2_nix=$(nix eval --impure --raw --expr "(builtins.fetchGit { url = \"file://$repo\"; ref = \"refs/tags/tag2\"; }).rev")
rev_tag2=$(git -C $repo rev-parse refs/tags/tag2)
[[ $rev_tag2_nix = $rev_tag2 ]]
@ -254,3 +269,33 @@ git -C "$repo" add hello .gitignore
git -C "$repo" commit -m 'Bla1'
cd "$repo"
path11=$(nix eval --impure --raw --expr "(builtins.fetchGit ./.).outPath")
# test behavior if both branch and tag with same name exist
repo="$TEST_ROOT/git"
rm -rf "$repo"/.git
git init "$repo"
git -C "$repo" config user.email "foobar@example.com"
git -C "$repo" config user.name "Foobar"
touch "$repo"/test
echo "hello world" > "$repo"/test
git -C "$repo" checkout -b branch
git -C "$repo" add test
git -C "$repo" commit -m "Init"
git -C "$repo" tag branch
echo "goodbye world" > "$repo"/test
git -C "$repo" add test
git -C "$repo" commit -m "Update test"
path12=$(nix eval --impure --raw --expr "(builtins.fetchGit { url = \"file://$repo\"; ref = \"branch\"; }).outPath")
[[ "$(cat "$path12"/test)" =~ 'hello world' ]]
[[ "$(cat "$repo"/test)" =~ 'goodbye world' ]]
path13=$(nix eval --impure --raw --expr "(builtins.fetchGit { url = \"file://$repo\"; ref = \"refs/heads/branch\"; }).outPath")
[[ "$(cat "$path13"/test)" =~ 'goodbye world' ]]
path14=$(nix eval --impure --raw --expr "(builtins.fetchGit { url = \"file://$repo\"; ref = \"refs/tags/branch\"; }).outPath")
[[ "$path14" = "$path12" ]]

View file

@ -26,7 +26,10 @@ cat << EOF > flake.nix
};
}
EOF
nix fmt ./file ./folder | grep 'Formatting: ./file ./folder'
# No arguments check
[[ "$(nix fmt)" = "Formatting(0):" ]]
# Argument forwarding check
nix fmt ./file ./folder | grep 'Formatting(2): ./file ./folder'
nix flake check
nix flake show | grep -P "package 'formatter'"

View file

@ -1,3 +1,3 @@
#!/usr/bin/env bash
echo Formatting: "${@}"
echo "Formatting(${#}):" "${@}"

View file

@ -76,7 +76,6 @@ functional_tests_scripts = [
'flakes/flake-registry.sh',
'flakes/subdir-flake.sh',
'gc.sh',
'phantom-referrers-gc.sh',
'nix-collect-garbage-d.sh',
'nix-collect-garbage-dry-run.sh',
'remote-store.sh',

View file

@ -1,62 +0,0 @@
source common.sh
startDaemon
requireDaemonNewerThan "2.92.0"
requireSqliteDatabase
clearStore
depOutPath=$(nix-build --no-out-link -E '
with import ./config.nix;
mkDerivation {
name = "phantom";
outputs = [ "out" ];
buildCommand = "
echo i will become a phantom soon > $out
";
}
')
finalOutPath=$(nix-build --no-out-link -E '
with import ./config.nix;
let dep = mkDerivation {
name = "phantom";
outputs = [ "out" ];
buildCommand = "
echo i will become a phantom soon > $out
";
}; in
mkDerivation {
name = "phantom-gc";
outputs = [ "out" ];
buildCommand = "
echo UNUSED: ${dep} > $out
";
}
')
echo "displaying all valid paths"
sqlite3 "$NIX_SQLITE_DATABASE" <<EOF
select * from validpaths;
EOF
echo "displaying the relevant IDs..."
sqlite3 "$NIX_SQLITE_DATABASE" <<EOF
select r.referrer, r.reference from Refs r join ValidPaths vp on r.referrer = vp.id where path = '$finalOutPath';
EOF
echo "corrupting the SQLite database manually..."
sqlite3 "$NIX_SQLITE_DATABASE" <<EOF
pragma foreign_keys = off;
delete from ValidPaths where path = '$finalOutPath';
select * from Refs;
EOF
restartDaemon
# expect this to work and maybe warn about phantom referrers
expectStderr 0 nix-collect-garbage -vvvv | grepQuiet 'phantom referrers'

View file

@ -157,4 +157,6 @@ in
coredumps = runNixOSTestFor "x86_64-linux" ./coredumps;
io_uring = runNixOSTestFor "x86_64-linux" ./io_uring;
fetchurl = runNixOSTestFor "x86_64-linux" ./fetchurl.nix;
}

84
tests/nixos/fetchurl.nix Normal file
View file

@ -0,0 +1,84 @@
# Test whether builtin:fetchurl properly performs TLS certificate
# checks on HTTPS servers.
{ lib, config, pkgs, ... }:
let
makeTlsCert = name: pkgs.runCommand name {
nativeBuildInputs = with pkgs; [ openssl ];
} ''
mkdir -p $out
openssl req -x509 \
-subj '/CN=${name}/' -days 49710 \
-addext 'subjectAltName = DNS:${name}' \
-keyout "$out/key.pem" -newkey ed25519 \
-out "$out/cert.pem" -noenc
'';
goodCert = makeTlsCert "good";
badCert = makeTlsCert "bad";
in
{
name = "fetchurl";
nodes = {
machine = { lib, pkgs, ... }: {
services.nginx = {
enable = true;
virtualHosts."good" = {
addSSL = true;
sslCertificate = "${goodCert}/cert.pem";
sslCertificateKey = "${goodCert}/key.pem";
root = pkgs.runCommand "nginx-root" {} ''
mkdir "$out"
echo 'hello world' > "$out/index.html"
'';
};
virtualHosts."bad" = {
addSSL = true;
sslCertificate = "${badCert}/cert.pem";
sslCertificateKey = "${badCert}/key.pem";
root = pkgs.runCommand "nginx-root" {} ''
mkdir "$out"
echo 'foobar' > "$out/index.html"
'';
};
};
security.pki.certificateFiles = [ "${goodCert}/cert.pem" ];
networking.hosts."127.0.0.1" = [ "good" "bad" ];
virtualisation.writableStore = true;
nix.settings.experimental-features = "nix-command";
};
};
testScript = { nodes, ... }: ''
machine.wait_for_unit("nginx")
machine.wait_for_open_port(443)
out = machine.succeed("curl https://good/index.html")
assert out == "hello world\n"
out = machine.succeed("cat ${badCert}/cert.pem > /tmp/cafile.pem; curl --cacert /tmp/cafile.pem https://bad/index.html")
assert out == "foobar\n"
# Fetching from a server with a trusted cert should work.
machine.succeed("nix build --no-substitute --expr 'import <nix/fetchurl.nix> { url = \"https://good/index.html\"; hash = \"sha256-qUiQTy8PR5uPgZdpSzAYSw0u0cHNKh7A+4XSmaGSpEc=\"; }'")
# Fetching from a server with an untrusted cert should fail.
err = machine.fail("nix build --no-substitute --expr 'import <nix/fetchurl.nix> { url = \"https://bad/index.html\"; hash = \"sha256-rsBwZF/lPuOzdjBZN2E08FjMM3JHyXit0Xi2zN+wAZ8=\"; }' 2>&1")
print(err)
assert "SSL certificate problem: self-signed certificate" in err or "SSL peer certificate or SSH remote key was not OK" in err
# Fetching from a server with a trusted cert should work via environment variable override.
machine.succeed("NIX_SSL_CERT_FILE=/tmp/cafile.pem nix build --no-substitute --expr 'import <nix/fetchurl.nix> { url = \"https://bad/index.html\"; hash = \"sha256-rsBwZF/lPuOzdjBZN2E08FjMM3JHyXit0Xi2zN+wAZ8=\"; }'")
'';
}

View file

@ -0,0 +1,56 @@
#include <gtest/gtest.h>
#include "crash-handler.hh"
namespace nix {
class OopsException : public std::exception
{
const char * msg;
public:
OopsException(const char * msg) : msg(msg) {}
const char * what() const noexcept override
{
return msg;
}
};
void causeCrashForTesting(std::function<void()> fixture)
{
registerCrashHandler();
std::cerr << "time to crash\n";
try {
fixture();
} catch (...) {
std::terminate();
}
}
TEST(CrashHandler, exceptionName)
{
ASSERT_DEATH(
causeCrashForTesting([]() { throw OopsException{"lol oops"}; }),
"time to crash\nLix crashed.*OopsException: lol oops"
);
}
TEST(CrashHandler, unknownTerminate)
{
ASSERT_DEATH(
causeCrashForTesting([]() { std::terminate(); }),
"time to crash\nLix crashed.*std::terminate\\(\\) called without exception"
);
}
TEST(CrashHandler, nonStdException)
{
ASSERT_DEATH(
causeCrashForTesting([]() {
// NOLINTNEXTLINE(hicpp-exception-baseclass): intentional
throw 4;
}),
"time to crash\nLix crashed.*Unknown exception! Spooky\\."
);
}
}

View file

@ -0,0 +1,104 @@
#include "async-collect.hh"
#include <gtest/gtest.h>
#include <kj/array.h>
#include <kj/async.h>
#include <kj/exception.h>
#include <stdexcept>
namespace nix {
TEST(AsyncCollect, void)
{
kj::EventLoop loop;
kj::WaitScope waitScope(loop);
auto a = kj::newPromiseAndFulfiller<void>();
auto b = kj::newPromiseAndFulfiller<void>();
auto c = kj::newPromiseAndFulfiller<void>();
auto d = kj::newPromiseAndFulfiller<void>();
auto collect = asyncCollect(kj::arr(
std::pair(1, std::move(a.promise)),
std::pair(2, std::move(b.promise)),
std::pair(3, std::move(c.promise)),
std::pair(4, std::move(d.promise))
));
auto p = collect.next();
ASSERT_FALSE(p.poll(waitScope));
// collection is ordered
c.fulfiller->fulfill();
b.fulfiller->fulfill();
ASSERT_TRUE(p.poll(waitScope));
ASSERT_EQ(p.wait(waitScope), 3);
p = collect.next();
ASSERT_TRUE(p.poll(waitScope));
ASSERT_EQ(p.wait(waitScope), 2);
p = collect.next();
ASSERT_FALSE(p.poll(waitScope));
// exceptions propagate
a.fulfiller->rejectIfThrows([] { throw std::runtime_error("test"); });
p = collect.next();
ASSERT_TRUE(p.poll(waitScope));
ASSERT_THROW(p.wait(waitScope), kj::Exception);
// first exception aborts collection
p = collect.next();
ASSERT_TRUE(p.poll(waitScope));
ASSERT_THROW(p.wait(waitScope), kj::Exception);
}
TEST(AsyncCollect, nonVoid)
{
kj::EventLoop loop;
kj::WaitScope waitScope(loop);
auto a = kj::newPromiseAndFulfiller<int>();
auto b = kj::newPromiseAndFulfiller<int>();
auto c = kj::newPromiseAndFulfiller<int>();
auto d = kj::newPromiseAndFulfiller<int>();
auto collect = asyncCollect(kj::arr(
std::pair(1, std::move(a.promise)),
std::pair(2, std::move(b.promise)),
std::pair(3, std::move(c.promise)),
std::pair(4, std::move(d.promise))
));
auto p = collect.next();
ASSERT_FALSE(p.poll(waitScope));
// collection is ordered
c.fulfiller->fulfill(1);
b.fulfiller->fulfill(2);
ASSERT_TRUE(p.poll(waitScope));
ASSERT_EQ(p.wait(waitScope), std::pair(3, 1));
p = collect.next();
ASSERT_TRUE(p.poll(waitScope));
ASSERT_EQ(p.wait(waitScope), std::pair(2, 2));
p = collect.next();
ASSERT_FALSE(p.poll(waitScope));
// exceptions propagate
a.fulfiller->rejectIfThrows([] { throw std::runtime_error("test"); });
p = collect.next();
ASSERT_TRUE(p.poll(waitScope));
ASSERT_THROW(p.wait(waitScope), kj::Exception);
// first exception aborts collection
p = collect.next();
ASSERT_TRUE(p.poll(waitScope));
ASSERT_THROW(p.wait(waitScope), kj::Exception);
}
}

View file

@ -0,0 +1,74 @@
#include "async-semaphore.hh"
#include <gtest/gtest.h>
#include <kj/async.h>
namespace nix {
TEST(AsyncSemaphore, counting)
{
kj::EventLoop loop;
kj::WaitScope waitScope(loop);
AsyncSemaphore sem(2);
ASSERT_EQ(sem.available(), 2);
ASSERT_EQ(sem.used(), 0);
auto a = kj::evalNow([&] { return sem.acquire(); });
ASSERT_EQ(sem.available(), 1);
ASSERT_EQ(sem.used(), 1);
auto b = kj::evalNow([&] { return sem.acquire(); });
ASSERT_EQ(sem.available(), 0);
ASSERT_EQ(sem.used(), 2);
auto c = kj::evalNow([&] { return sem.acquire(); });
auto d = kj::evalNow([&] { return sem.acquire(); });
ASSERT_TRUE(a.poll(waitScope));
ASSERT_TRUE(b.poll(waitScope));
ASSERT_FALSE(c.poll(waitScope));
ASSERT_FALSE(d.poll(waitScope));
a = nullptr;
ASSERT_TRUE(c.poll(waitScope));
ASSERT_FALSE(d.poll(waitScope));
{
auto lock = b.wait(waitScope);
ASSERT_FALSE(d.poll(waitScope));
}
ASSERT_TRUE(d.poll(waitScope));
ASSERT_EQ(sem.available(), 0);
ASSERT_EQ(sem.used(), 2);
c = nullptr;
ASSERT_EQ(sem.available(), 1);
ASSERT_EQ(sem.used(), 1);
d = nullptr;
ASSERT_EQ(sem.available(), 2);
ASSERT_EQ(sem.used(), 0);
}
TEST(AsyncSemaphore, cancelledWaiter)
{
kj::EventLoop loop;
kj::WaitScope waitScope(loop);
AsyncSemaphore sem(1);
auto a = kj::evalNow([&] { return sem.acquire(); });
auto b = kj::evalNow([&] { return sem.acquire(); });
auto c = kj::evalNow([&] { return sem.acquire(); });
ASSERT_TRUE(a.poll(waitScope));
ASSERT_FALSE(b.poll(waitScope));
b = nullptr;
a = nullptr;
ASSERT_TRUE(c.poll(waitScope));
}
}

View file

@ -3,105 +3,161 @@
namespace nix {
/* ----------------------------------------------------------------------------
* compress / decompress
* --------------------------------------------------------------------------*/
/* ----------------------------------------------------------------------------
* compress / decompress
* --------------------------------------------------------------------------*/
TEST(compress, compressWithUnknownMethod) {
ASSERT_THROW(compress("invalid-method", "something-to-compress"), UnknownCompressionMethod);
}
TEST(compress, noneMethodDoesNothingToTheInput) {
auto o = compress("none", "this-is-a-test");
ASSERT_EQ(o, "this-is-a-test");
}
TEST(decompress, decompressNoneCompressed) {
auto method = "none";
auto str = "slfja;sljfklsa;jfklsjfkl;sdjfkl;sadjfkl;sdjf;lsdfjsadlf";
auto o = decompress(method, str);
ASSERT_EQ(o, str);
}
TEST(decompress, decompressEmptyCompressed) {
// Empty-method decompression used e.g. by S3 store
// (Content-Encoding == "").
auto method = "";
auto str = "slfja;sljfklsa;jfklsjfkl;sdjfkl;sadjfkl;sdjf;lsdfjsadlf";
auto o = decompress(method, str);
ASSERT_EQ(o, str);
}
TEST(decompress, decompressXzCompressed) {
auto method = "xz";
auto str = "slfja;sljfklsa;jfklsjfkl;sdjfkl;sadjfkl;sdjf;lsdfjsadlf";
auto o = decompress(method, compress(method, str));
ASSERT_EQ(o, str);
}
TEST(decompress, decompressBzip2Compressed) {
auto method = "bzip2";
auto str = "slfja;sljfklsa;jfklsjfkl;sdjfkl;sadjfkl;sdjf;lsdfjsadlf";
auto o = decompress(method, compress(method, str));
ASSERT_EQ(o, str);
}
TEST(decompress, decompressBrCompressed) {
auto method = "br";
auto str = "slfja;sljfklsa;jfklsjfkl;sdjfkl;sadjfkl;sdjf;lsdfjsadlf";
auto o = decompress(method, compress(method, str));
ASSERT_EQ(o, str);
}
TEST(decompress, decompressInvalidInputThrowsCompressionError) {
auto method = "bzip2";
auto str = "this is a string that does not qualify as valid bzip2 data";
ASSERT_THROW(decompress(method, str), CompressionError);
}
TEST(decompress, veryLongBrotli) {
auto method = "br";
auto str = std::string(65536, 'a');
auto o = decompress(method, compress(method, str));
// This is just to not print 64k of "a" for most failures
ASSERT_EQ(o.length(), str.length());
ASSERT_EQ(o, str);
}
/* ----------------------------------------------------------------------------
* compression sinks
* --------------------------------------------------------------------------*/
TEST(makeCompressionSink, noneSinkDoesNothingToInput) {
StringSink strSink;
auto inputString = "slfja;sljfklsa;jfklsjfkl;sdjfkl;sadjfkl;sdjf;lsdfjsadlf";
auto sink = makeCompressionSink("none", strSink);
(*sink)(inputString);
sink->finish();
ASSERT_STREQ(strSink.s.c_str(), inputString);
}
TEST(makeCompressionSink, compressAndDecompress) {
auto inputString = "slfja;sljfklsa;jfklsjfkl;sdjfkl;sadjfkl;sdjf;lsdfjsadlf";
StringSink strSink;
auto sink = makeCompressionSink("bzip2", strSink);
(*sink)(inputString);
sink->finish();
StringSource strSource{strSink.s};
auto decompressionSource = makeDecompressionSource("bzip2", strSource);
ASSERT_STREQ(decompressionSource->drain().c_str(), inputString);
TEST(compress, compressWithUnknownMethod)
{
ASSERT_THROW(compress("invalid-method", "something-to-compress"), UnknownCompressionMethod);
}
TEST(compress, noneMethodDoesNothingToTheInput)
{
auto o = compress("none", "this-is-a-test");
ASSERT_EQ(o, "this-is-a-test");
}
TEST(decompress, decompressEmptyString)
{
// Empty-method decompression used e.g. by S3 store
// (Content-Encoding == "").
auto o = decompress("", "this-is-a-test");
ASSERT_EQ(o, "this-is-a-test");
}
/* ----------------------------------------------------------------------------
* compression sinks
* --------------------------------------------------------------------------*/
TEST(makeCompressionSink, noneSinkDoesNothingToInput)
{
auto method = "none";
StringSink strSink;
auto inputString = "slfja;sljfklsa;jfklsjfkl;sdjfkl;sadjfkl;sdjf;lsdfjsadlf";
auto sink = makeCompressionSink(method, strSink);
(*sink)(inputString);
sink->finish();
ASSERT_STREQ(strSink.s.c_str(), inputString);
}
/** Tests applied to all compression types */
class PerTypeCompressionTest : public testing::TestWithParam<const char *>
{};
/** Tests applied to non-passthrough compression types */
class PerTypeNonNullCompressionTest : public testing::TestWithParam<const char *>
{};
constexpr const char * COMPRESSION_TYPES_NONNULL[] = {
// libarchive
"bzip2",
"compress",
"gzip",
"lzip",
"lzma",
"xz",
"zstd",
// Uses external program via libarchive so cannot be used :(
/*
"grzip",
"lrzip",
"lzop",
"lz4",
*/
// custom
"br",
};
INSTANTIATE_TEST_SUITE_P(
compressionNonNull, PerTypeNonNullCompressionTest, testing::ValuesIn(COMPRESSION_TYPES_NONNULL)
);
INSTANTIATE_TEST_SUITE_P(
compressionNonNull, PerTypeCompressionTest, testing::ValuesIn(COMPRESSION_TYPES_NONNULL)
);
INSTANTIATE_TEST_SUITE_P(
compressionNull, PerTypeCompressionTest, testing::Values("none")
);
/* ---------------------------------------
* All compression types
* --------------------------------------- */
TEST_P(PerTypeCompressionTest, roundTrips)
{
auto method = GetParam();
auto str = "slfja;sljfklsa;jfklsjfkl;sdjfkl;sadjfkl;sdjf;lsdfjsadlf";
auto o = decompress(method, compress(method, str));
ASSERT_EQ(o, str);
}
TEST_P(PerTypeCompressionTest, longerThanBuffer)
{
// This is targeted originally at regression testing a brotli bug, but we might as well do it to
// everything
auto method = GetParam();
auto str = std::string(65536, 'a');
auto o = decompress(method, compress(method, str));
// This is just to not print 64k of "a" for most failures
ASSERT_EQ(o.length(), str.length());
ASSERT_EQ(o, str);
}
TEST_P(PerTypeCompressionTest, sinkAndSource)
{
auto method = GetParam();
auto inputString = "slfja;sljfklsa;jfklsjfkl;sdjfkl;sadjfkl;sdjf;lsdfjsadlf";
StringSink strSink;
auto sink = makeCompressionSink(method, strSink);
(*sink)(inputString);
sink->finish();
StringSource strSource{strSink.s};
auto decompressionSource = makeDecompressionSource(method, strSource);
ASSERT_STREQ(decompressionSource->drain().c_str(), inputString);
}
/* ---------------------------------------
* Non null compression types
* --------------------------------------- */
TEST_P(PerTypeNonNullCompressionTest, bogusInputDecompression)
{
auto param = GetParam();
auto bogus = "this data is bogus and should throw when decompressing";
ASSERT_THROW(decompress(param, bogus), CompressionError);
}
TEST_P(PerTypeNonNullCompressionTest, truncatedValidInput)
{
auto method = GetParam();
auto inputString = "the quick brown fox jumps over the lazy doggos";
auto compressed = compress(method, inputString);
/* n.b. This also tests zero-length input, which is also invalid.
* As of the writing of this comment, it returns empty output, but is
* allowed to throw a compression error instead. */
for (int i = 0; i < compressed.length(); ++i) {
auto newCompressed = compressed.substr(compressed.length() - i);
try {
decompress(method, newCompressed);
// Success is acceptable as well, even though it is corrupt data.
// The compression method is not expected to provide integrity,
// just, not break explosively on bad input.
} catch (CompressionError &) {
// Acceptable
}
}
}
}

View file

@ -39,6 +39,8 @@ liblixutil_test_support = declare_dependency(
)
libutil_tests_sources = files(
'libutil/async-collect.cc',
'libutil/async-semaphore.cc',
'libutil/canon-path.cc',
'libutil/checked-arithmetic.cc',
'libutil/chunked-vector.cc',
@ -76,6 +78,7 @@ libutil_tester = executable(
liblixexpr_mstatic,
liblixutil_test_support,
nlohmann_json,
kj,
],
cpp_pch : cpp_pch,
)
@ -262,9 +265,14 @@ test(
protocol : 'gtest',
)
libmain_tests_sources = files(
'libmain/crash.cc',
'libmain/progress-bar.cc',
)
libmain_tester = executable(
'liblixmain-tests',
files('libmain/progress-bar.cc'),
libmain_tests_sources,
dependencies : [
liblixmain,
liblixexpr,