Repository navigation
Merge Determinate Nix v3.23.1 - #78
Closed
joshheinrichs-shopify wants to merge 1355 commits into
Closed
joshheinrichs-shopify wants to merge 1355 commits into
joshheinrichs-shopify wants to merge 1355 commits into
Conversation
getDefaultExtraPlatforms() ran `arch -arch x86_64 /usr/bin/true` to decide whether to add x86_64-darwin to extra-platforms. Because this is the default value of the extra-platforms setting, it runs during static initialization of the global `settings` object, so every libstore linked process on Apple silicon paid roughly 13ms of subprocess startup, even ones that never read extra-platforms (devenv version, hook-should-activate, nix --version). Check for the Rosetta runtime file instead. It is present exactly when Rosetta 2 is installed, which is exactly when the exec probe succeeded, and a stat costs about 0.01ms versus 12.5ms for the subprocess. Assisted-by: Claude Code (Claude Opus 4.8) (cherry picked from commit a8419ef8e770401fd6e6ce4bfeacdbe0d7392b4f)
[Backport 2.35-maintenance] libstore: detect Rosetta via runtime file instead of spawning arch
`processConnection()` cleared the interrupt flag in a `Finally` that ran *before* the `MonitorFdHup` was destroyed. So if the monitor thread noticed the client hanging up after that, it set the flag again, and the daemon's subsequent OpenTelemetry export failed with `Interrupted` (which is deliberately not reported). Whether the monitor thread won that race is why the daemon's spans sometimes went missing. Clear the flag only once the monitor is gone, i.e. its thread joined. Assisted-by: Claude Fable 5.1 <noreply@anthropic.com>
Pass the trace context to the build hook in the `TRACEPARENT` environment variable, and have the root span of any Nix process be parented to that, if set. So a remote build now shows up in the trace of the `nix` invocation that requested it: `nix` -> `build-remote` -> the remote daemon (the latter via the existing `ssh-ng://` handshake). Two things stood in the way. `build-remote` replaces the global logger with a JSON logger, discarding the OpenTelemetry logger that `main()` had attached, so like the daemon it now sets up tracing itself. And `HookInstance` sent the hook `SIGTERM` on teardown, which interrupted its export; since the hook exits by itself on EOF, just give it a chance to do so. A root span parented to a remote context is no longer implicitly a server span; the daemon says so explicitly. Assisted-by: Claude Fable 5.1 <noreply@anthropic.com>
The build hook is started for every build, but usually ends up declining (e.g. because there are no builders for the required system). It set up tracing at startup regardless, so every build uploaded a trace consisting of nothing but the hook's root span. Instead, set up tracing when we're actually about to connect to a remote builder. That also makes the workaround in the OpenTelemetry test unnecessary, which disabled the hook to keep its uploads out of the count. Assisted-by: Claude Fable 5.1 <noreply@anthropic.com>
Rather than in the two places (the forked child of the socket daemon, and the stdio daemon) that lead to it. It's a no-op unless configured, and a connection is served once per process, so this is exactly the point at which to set up tracing. Assisted-by: Claude Fable 5.1 <noreply@anthropic.com>
Every caller of `initOtel()` immediately created the logger and added it to the global logger, so do that in `initOtel()` itself, which now takes the root span's name and parent. That leaves `initOtel()` as the only thing in the header, so the `OpenTelemetryLogger` class there (an empty base of the actual implementation) goes away too; the implementation takes its name. Assisted-by: Claude Fable 5.1 <noreply@anthropic.com>
Now that `initOtel()` creates the logger, there's exactly one tracer provider per logger, so the logger can own it (and the root span's debug trace ID) rather than keeping it in a global. The logger creates the provider itself; `initOtel()` is left with resolving the configuration and creating the exporter from it. That also makes the fork callback unnecessary: it discarded the global state in forked children, but `startProcess()` already gives them a fresh global logger (leaking the parent's), so nothing in a child references the inherited provider; a child that wants tracing calls `initOtel()` and gets its own. Assisted-by: Claude Fable 5.1 <noreply@anthropic.com>
The parent sends the settings of every registered `Config`, but the hook applied them via `settings.set()`, which only knows `Settings` and silently returns false for anything else. So settings living in other `Config`s never reached the hook: the OpenTelemetry ones (so `nix build --otlp ...` didn't trace the hook), but also e.g. `experimental-features` and the file transfer settings. Apply them via `globalConfig.set()` instead. Only apply those that differ from our own values, though, since we've read the same configuration files: setting a deprecated setting warns even when it's set to its default, which showed up in the output of every build. (Having the parent send only its overridden settings instead doesn't work: things like `--store` set the setting without marking it as overridden, so the hook would end up using the wrong store.) Assisted-by: Claude Fable 5.1 <noreply@anthropic.com>
A collector's probabilistic sampler may drop a trace regardless of the W3C "sampled" flag, which makes the trace ID printed under NIX_DEBUG_OTEL useless if the trace never arrives. A non-zero `sampling.priority` attribute is the documented override, so set it on all spans when debugging. Assisted-by: Claude Fable 5.1 <noreply@anthropic.com>
The build hook exports its own telemetry (parented to ours via TRACEPARENT), so replaying its activities into our logger produced duplicate spans. Mark them as coming from a remote log source, like we do for messages forwarded by the daemon. Assisted-by: Claude Fable 5.1 <noreply@anthropic.com>
Commands that exec() another program (nix run/shell/develop/fmt, nix-shell, and `--help` running man) stopped the logger but never flushed it, so their traces were lost along with the exporter thread. Assisted-by: Claude Fable 5.1 <noreply@anthropic.com>
The exporter uploads periodically as well as on exit, so a slow build (e.g. on a loaded CI machine) spreads its spans over several uploads, which shifted the upload numbers the test relied on. Instead, delete the uploads before each step and select spans by name across all the uploads received since. Assisted-by: Claude Fable 5.1 <noreply@anthropic.com>
To get rid of: hint: Using 'master' as the name for the initial branch. This default branch name hint: will change to "main" in Git 3.0. Changing that from master breaks the tests. Also dedups test to use createGitRepo (cherry picked from commit 242d09e49546d0edb386d109bfbe070631ab9aaa)
A rev-pinned fetch left the rev un-refed, so a later fetch of the same URL had nothing to negotiate from and re-downloaded shared history. Fetch into refs/nix/tip-<2 hex chars of rev> instead. Bucketing (256 slots) means unrelated revs don't keep evicting each other's tip the way a single shared ref would. Verified against a real ~7.7M-object repo: a third fetch that would've re-downloaded 67,656 objects with a single shared ref dropped to 7 with bucketing. (cherry picked from commit 74d9f993be1ca84e056984a092eb2836b321645c)
Without a network (as in the Nix sandbox on CI), `nix` turns off substitution unless it's requested explicitly, so the build never started a substitution goal and the test couldn't find its span. Assisted-by: Claude Fable 5.1 <noreply@anthropic.com>
Fetches an exact rev, checks its bucket ref was created, fetches a second rev, and asserts via GIT_TRACE_PACKET that the second fetch negotiated against the first rev's ref instead of re-downloading. (cherry picked from commit 99b5af91148e0aa92e292ac38c50b48af552d950)
(cherry picked from commit e20887e6a62567bdf124ea2412ac68bf498cbf71)
fetchers/git: negotiate rev-pinned fetches from bucketed refs
Outputs that no flake schema recognises are recorded in outputs.json
as { "unknown": true } without an "output" attribute, which made
evaluation of every baked flake fail with "attribute 'output' missing".
Assisted-by: Claude Fable 5.1 <noreply@anthropic.com>
By default, only outputs for the current system are baked, matching 'nix flake show'. With --all-systems, outputs for all systems are included. Assisted-by: Claude Fable 5.1 <noreply@anthropic.com>
Bakes the simple test flake and checks that the baked flake shows the same outputs, that the baked derivations are builtin:substitute derivations with the original output paths, that building them fails without a substituter and succeeds by substituting from a binary cache, and that --all-systems and legacyPackages work. Assisted-by: Claude Fable 5.1 <noreply@anthropic.com>
Previously it returned only the derivation path, and the baked flake took the output paths from outputs.json as plain strings. As a result, a derivation using a baked output as an input didn't record the fake derivation in its inputDrvs, so the output was never substituted. Now it returns an attribute set of the same shape as derivationStrict: drvPath plus one string per output, with proper string context. The baked flake uses those for drvPath and outPath. Add a test for depending on a baked flake. Assisted-by: Claude Fable 5.1 <noreply@anthropic.com>
Substitution is disabled automatically when there is no network access (as in the CI sandbox), so the file:// substituter was ignored and the test failed. Pass --substitute explicitly, like substitution.sh does. Assisted-by: Claude Fable 5.1 <noreply@anthropic.com>
Assisted-by: Claude Fable 5.1 <noreply@anthropic.com>
Flake schemas such as nixosConfigurations declare that the derivation lives at a sub-path of the output attribute (e.g. config.system.build.toplevel). The inventory didn't record this, so the baked flake put the derivation at nixosConfigurations.<name> instead of nixosConfigurations.<name>.config.system.build.toplevel. The inventory JSON now includes a 'derivationAttrPath' attribute on leaves with a non-empty path, and the baked flake nests the derivation accordingly. Also don't require 'shortDescription', which these schemas don't set. Assisted-by: Claude Fable 5.1 <noreply@anthropic.com>
`CmdFlakeCheck` called `logEvalError()`, and thereby `CachedEvalError::force()`, from within a catch handler. `force()` evaluates the original attribute, which may have to wait on a thunk that is being evaluated by another fiber. A fiber must not suspend while an exception is being handled (the C++ exception state is thread-local), so this tripped the assertion in `suspendFiber()`: Assertion '!std::current_exception() && !std::uncaught_exceptions()' failed in void nix::suspendFiber(...) Fix this by capturing the exception as a `std::exception_ptr` in the catch handler, and forcing/logging it only after the handler has exited. Assisted-by: Claude Fable 5.1 <noreply@anthropic.com>
nix flake check: Don't force cached eval errors inside a catch handler
Build bdwgc with `-DNO_BLACK_LISTING`. Black-listing avoids allocating objects on pages that false pointers refer to. Its page sets are hash tables of 2^21 bits (one per 4 KiB block, i.e. 8 GiB of address space with LARGE_CONFIG on 64-bit). Beyond that, pages alias, and since `GC_add_to_black_list_normal()` re-sets a bit whenever the previous cycle's table already had it, bits never clear once aliasing starts. Eventually every page looks black-listed, `GC_allochblk()` fails even after a successful heap expansion, and `GC_allocobj()` gives up with "Too many retries in GC_allocobj" (SIGABRT). This is what the Sentry reports from `nix flake show` on 3.23.0 show; upstream bdwgc issues #691 and #726 describe the same loop and are unresolved. Nix gets little out of black-listing because interior pointers are disabled, while it pays for it on every mark: Values hold many pointers to non-GC memory (`Expr *`, `PrimOp *`, ...) inside the plausible heap range, each of which updates the black lists. Before/after on nixpkgs with a 64 MiB initial heap (~200 GC cycles): `nix search` 77 s -> 46 s, `nix flake show --legacy` 94 s -> 53 s, with identical peak RSS, heap size and allocation volume. bdwgc's own statistics attribute the gain entirely to world-stopped marking (62.8 s -> 37.0 s). With the default 4 GiB initial heap there is no measurable difference. Functional tests pass. Assisted-by: Claude Fable 5.1 <noreply@anthropic.com>
A forked child (e.g. a builtin builder such as `builtin:fetchurl`, or an in-process build sandbox running on a `clone()` stack) inherits the parent thread's `GC_current_stack` and registered stacks, but doesn't execute on the stack that `GC_current_stack` describes and never runs the collector. So the stack bookkeeping done by the coroutine GC hooks is useless there at best; at worst it trips the consistency check in `gcSaveStackPointer()` (as happened in the `githubFlakes` VM test, where a `builtin:fetchurl` derivation panicked with "stack pointer ... is not within the stack ... that is being switched away from"), or takes the collector's lock, which another thread of the parent may have been holding at the time of the fork. Register a fork callback that resets the hooks (and `GC_current_stack`) in the child, and make the "closing" half of each hook pair tolerate the hooks having been reset since the "opening" half ran. Assisted-by: Claude Fable 5.1 <noreply@anthropic.com>
Disable bdwgc black-listing
…abase Closing a file descriptor drops all POSIX locks that the process holds on that file. So opening and closing db.sqlite-shm while another connection to the same database is open in this process dropped SQLite's lock on that file. Another process could then truncate it, causing a SIGBUS in the first process. This showed up with the tarball cache, which opens multiple connections per process. We hold the lock on `shmFilesSynced` until the descriptor has been closed, so that a concurrent open of the same database in another thread cannot create a SQLite connection (and thus acquire POSIX locks on db.sqlite-shm) while the first thread still has the file open. Assisted-by: Claude Fable 5.1 <noreply@anthropic.com> (cherry picked from commit cc1cfbb3e629d3bae8178236bdfaa83f9a97a185)
SQLite: Only apply the ZFS -shm workaround on the first open of a database
Previously, if a package was replaced by a different store path with the same version (e.g. a rebuild due to a dependency or source change), `nix profile history` reported "No changes" for that generation. Now it prints a `changed` line. Also add a `--show-source` flag that shows the (abbreviated) locked flake reference of each added, removed, upgraded or changed package. For upgraded/changed packages, the old and new references are shown, which explains where the change came from. Assisted-by: Claude Fable 5.1 <noreply@anthropic.com>
…ne-yield Fix GC stack scanning when a coroutine yields from another coroutine's stack
nix profile history: Show store path changes with the same version
The periodic purge of expired entries from the narinfo disk cache
does
delete from NARs where (present = 0 and timestamp < ?)
or (present = 1 and timestamp < ?)
which, with only the (cache, hashPart) primary key available, is a
full table scan. On busy builders the cache grows to millions of rows
and several GiB, and the scan was measured to hold the SQLite write
lock for over two minutes on a cold file cache (NIX-491). Every
concurrent `nix copy` blocks on that lock, and uploader timeouts that
kill the purging process just cause the next process to start the
scan over.
Add an index on NARs(present, timestamp). SQLite then executes the
purge as two covering-index range searches. On a synthetic 1.5M-row,
3.1 GiB cache with 5000 expired entries, the warm-cache purge drops
from ~9 s and ~3 GiB of page reads to ~1 s and <100 MiB of page
reads for the delete itself (~3.5 s including the commit), and the
`nix` process startup that triggers it goes from 9.8 s to 4.4 s.
Bump the database filename to v4 so the index is created as part of
a fresh, empty database. Building the index in place on an existing
multi-GiB cache would itself hold the write lock for a full table
read, which is exactly the stall this is meant to remove.
Assisted-by: Claude Fable 5.1 <noreply@anthropic.com>
Assisted-by: Claude Fable 5.1 <noreply@anthropic.com>
The purge ran as a single DELETE in whichever process happened to open the cache, holding the SQLite write lock for its entire duration. Concurrent processes (e.g. parallel `nix copy` uploads) blocked on it for the whole time, and if the purging process was killed, e.g. by a caller's timeout, all work was rolled back and the next process started over from scratch. Delete in chunks of 1000 rows instead, each in its own autocommit transaction: * The write lock is released after every chunk, so concurrent writers wait at most for one chunk (a few hundred ms warm, under a second cold on NVMe) instead of the whole purge. * Committed chunks survive if the process is killed. LastPurge is only updated once a chunk comes back short, so the next process to open the cache resumes where the killed one left off, and concurrent processes that both find the purge due simply delete chunks cooperatively. The chunk subquery is served by the (present, timestamp) index, with the outer delete going by rowid. Total purge cost is unchanged: on tmpfs, deleting 100k expired rows from a 1.5M-row cache takes ~3 s either way. In a contention test with a concurrent writer, the longest wait for the write lock dropped from 33 s to 4.5 s (on ZFS, which inflates both numbers). Killing the purge after 6 s left the committed chunks in place with LastPurge unchanged, and the next run finished the job. Assisted-by: Claude Fable 5.1 <noreply@anthropic.com>
Sync with upstream 2.35-maintenance
Processes that open the cache at around the same time all see that the purge is due, since LastPurge is only updated when a purge completes. With chunked deletion they would interleave harmlessly, but they'd all contend for the SQLite write lock and do redundant work. Take a non-blocking exclusive lock on a lock file next to the database before purging. Processes that fail to get it skip the purge. The lock is released when its holder exits or is killed, and since LastPurge is still only updated on completion, the next process to open the cache then takes over. Tested by starting eight processes at once with the purge due: one of them did the entire purge, the other seven skipped it and started up in under a second. Killing the lock holder half-way left LastPurge unchanged and the next process picked up the remaining rows. Assisted-by: Claude Fable 5.1 <noreply@anthropic.com>
Make purging the narinfo disk cache cheap and non-blocking
…manual Add derivation metadata documentation to the Determinate Nix manual
Flake lock file updates:
• Updated input 'nix':
'path:../..'
→ 'path:../..'
• Updated input 'nix/nixpkgs':
'https://api.flakehub.com/f/pinned/DeterminateSystems/secure-packages-26.05/0.1.1015029%2Brev-bb04742a2b3f59073509b332de34a678bfdee9c4/01a0f2bf-d071-73b2-a585-3f332b21e4d9/source.tar.gz' (2026-09-30)
→ 'https://api.flakehub.com/f/pinned/DeterminateSystems/secure-packages-26.05/0.1.1015033%2Brev-ad9e1d72007a5b115c4ef59edaa50ac12f868d5d/01a0fda6-2748-7282-b564-aaa6761bf51a/source.tar.gz' (2026-10-02)
Assisted-by: Claude Fable 5.1 <noreply@anthropic.com>
This skill describes how to edit the auto-generated release notes (v<version>.md and changes.md) into shape during the release process. Assisted-by: Claude Fable 5.1 <noreply@anthropic.com>
…f1b-a099-48b4-8864-e0cd3ab2be0b Release v3.23.1
Determinate Nix 3.23.1 (Nix 2.35.2), from 3.21.9. Conflict resolutions and adaptations of Tecnix code to upstream changes worth knowing about: * CLAUDE.md -> AGENTS.md follows the upstream rename; our build line is kept, the DeterminateSystems-only PR/branch rules are not. * .github/workflows/ci.yml and build.yml stay ours (upstream only bumped action hashes in code we had replaced). * Boehm GC: upstream replaced the stack-pointer corrector (GC_set_sp_corrector) with a bdwgc fork that registers fiber and coroutine stacks explicitly. BoehmThreadStack (6fd6483) fixed the corrector and has nothing left to fix, so it and its test are dropped. * AllowListSourceAccessor::isAllowed: the knownAllowed memo (c7c1e8e) is kept, as a boost::concurrent_flat_set like upstream's allowedPaths. * filetransfer: the GCS bearer-token header moves into the per-attempt header setup upstream introduced. * AsyncPathWriter::writePaths: upstream's version (temp roots, then a validity query, then skip valid paths) replaces ours, which did the same; addedPaths/wasAdded() stay for `nix provenance`. * git.cc getAccessorFromCommit: our shallow-submodule optimism (#39) and the legacy-export cache guard are re-applied on top of upstream's getTree(legacy) restructuring. * git-lfs: upstream now tests endpoint derivation; it is split out as lfsEndpointForRemote() and used by getLfsEndpointUrl(), so explicit lfs.url / remote.origin.lfsurl are still used verbatim and getLfsApi() still only resolves auth (git-lfs-authenticate / git credential fill). runProgram() lost stdin redirection upstream, so `git credential fill` is spawned directly. fixGitURL("") now throws, so a repository without an origin remote or lfs.url no longer fails merely by having LFS smudging enabled; it fails on an actual LFS fetch instead. * Upstream's new CachingSourceAccessor in rootFS forwards tracksEvalAccesses()/recordEvalAccess() and does not answer readLink() from its cache for tracked accessors, which record the read themselves. Without this, tracked evaluation used the untracked import caches and recorded nothing after an untracked warm-up. * -Werror=weak-vtables (nix-util/-store/-expr) and the dropped per-library unity default: key functions for our error/accessor/provider classes, and the includes the unity build used to supply. * Derivations are now validated when written, so the eval-cache race test builds its fixture with Derivation::fillInOutputPaths().
Since Determinate Nix 3.22 the parallel evaluator runs work items on fibers: when one blocks on a thunk another fiber owns, it is suspended and its worker thread starts or resumes another fiber. Upstream parks its own per-evaluation thread-locals (evalContext, the call depth, the eval thread id) in the fiber record and swaps them on every switch. currentTecnixThreadState got no such treatment, so a suspended tracked work item left its trackingContext and source-deps frames -- pointers into its own stack -- visible to whichever fiber ran next on that thread, and saw that fiber's state when it resumed. Give the fiber record a TecnixThreadState and swap it alongside evalContext. The tecnixMemoize cycle guard was a thread-local stack with LIFO push/pop, which interleaved fibers would both corrupt and trip (two fibers legitimately missing on the same key looked like a cycle). Make it a chain of frames on the evaluating stack, linked through the same thread state, so it travels with the fiber too.
libgit2's `GIT_RMDIR_EMPTY_PARENTS` walks up a removed path's parents and calls rmdir() on each with a trailing slash. On macOS a trailing slash makes rmdir() resolve a symbolic link in that position and act on its target: for a link to an unrelated empty directory it deletes that directory; for a link to an ancestor it fails, with ENOTEMPTY on earlier macOS and EINVAL on macOS 27. Linux returns ENOTDIR, which is what libgit2's error handling expects. Checkout reaches this on a case-insensitive filesystem whenever a file is removed from a directory whose name collides with a symlink, which is exactly libgit2's checkout::icase::overwrites_links_for_folders_when_forced test. On macOS 27 it fails with "invalid path for filesystem '.../a/': Invalid argument", so the libgit2 that upstream now pins (built from source, unlike nixpkgs' cached one) could not be built on a default, case-insensitive Nix store volume: no dev shell, no macos CI. The patch strips the trailing slash before rmdir(), so the link itself is named and every platform reports ENOTDIR. libgit2's full test suite passes with it on macOS 27, inside the Nix sandbox. To be submitted upstream.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
Sync with Determinate Nix: v3.21.9 → v3.23.1 (Nix 2.34.8 → 2.35.2), 1352 upstream commits. Same shape as the previous syncs: a merge of the release tag with conflicts resolved in the merge commit, followed by fixups as their own commits.
Context
Three commits; read them in order.
Merge tag 'v3.23.1'— 27 conflicted files. The merge message lists every resolution worth knowing about. The ones that are more than mechanical:CachingSourceAccessorinrootFSdidn't forwardtracksEvalAccesses/recordEvalAccessand servedreadLinkfrom its cache, so tracked dependency discovery recorded nothing after an untracked warm-up. Fixed in the wrapper; caught bytecnix/builtins.sh.BoehmThreadStack(Fix GC deadlock by caching native thread stack bounds #64) had nothing left to fix and is dropped with its test.git.cc: shallow-submodule optimism (Optimistically shallow clone submodules #39) and the legacy-export cache guard re-applied on top of upstream'sgetTree(legacy)restructuring.lfsEndpointForRemote()so explicitlfs.url/remote.origin.lfsurlstay verbatim andgetLfsApi()still only resolves auth.runProgramlost stdin redirection, sogit credential fillis spawned directly.fixGitURL("")now throws, so a repo with noorigin/lfs.urlfails on an actual LFS fetch rather than on accessor creation.AsyncPathWriter::writePaths: upstream's version replaces ours (same fix);addedPaths/wasAdded()kept fornix provenance.-Werror=weak-vtablesand the dropped per-library unity default: key functions and the includes the unity build used to supply.libexpr: keep Tecnix tracking state with the fiber that owns it— the parallel evaluator now interleaves work items as fibers on one thread;currentTecnixThreadStateand thetecnixMemoizecycle guard werethread_local. Without this the bare merge segfaults intecnix/builtins.shat the--eval-cores 2step.packaging: patch libgit2 …— upstream pins libgit2 2.0.0-rc.1 (built from source, not cached). ItsGIT_RMDIR_EMPTY_PARENTScallsrmdir()on parent paths with a trailing slash, which macOS resolves through a symlink (deleting an unrelated empty dir, orEINVALon macOS 27 for a link to an ancestor). Its own test suite trips over it on any case-insensitive volume on macOS 27, i.e. the default Nix store volume, so no dev shell and no macOS build. Small upstreamable patch; libgit2's full suite passes with it.Not in this PR:
withOtelis left at upstream's default (opt-in at runtime viaOTEL_EXPORTER_OTLP_ENDPOINT; say if it should be off like Sentry).tecnix-read-gcswill need a rebase onto this (filetransfer.cc,gcs-creds.hh,libstore-tests/meson.buildconflict).Verified locally (aarch64-darwin): build, all unit-test suites,
tecnix/tectonixfunctional suites 11/11, 18 targeted upstream functional tests (fetchGit*, lang, eval, gc, provenance, symlinks, …),maintainers/format.sh. The full functional suite and Linux are for CI.