fix(dart): keep the build directory out of the vodozemac bindings - #62
Open
nikzen wants to merge 13 commits into
Open
fix(dart): keep the build directory out of the vodozemac bindings#62nikzen wants to merge 13 commits into
nikzen wants to merge 13 commits into
Conversation
nikzen
force-pushed
the
fix/vodozemac-darwin-build-paths
branch
from
August 3, 2026 16:33
1fcd558 to
49ca2ff
Compare
A registry entry and a scanner both ask an image where it is from, and ours had no answer: the annotations OCI reserves for that were absent, so the only handle on a published image was its tag — and a tag moves. The build takes the repository, the commit and the ref name as optional arguments and turns them into annotations. Optional, because a build by hand has no commit to name, and refusing to build then would be worse than an image that says less. Deliberately without `org.opencontainers.image.created`: a timestamp would make two builds of one commit differ, and the commit named here has a date already. Co-authored-by: Cursor <cursoragent@cursor.com>
The server sent none of them, because it takes headers only from a file and had none. It has one now, generated from the same option the smoke test reads, so the image and the test that fetches from it cannot disagree about what it sends. What is in it by default says nothing about any particular application: no sniffing, no framing, a referrer only within one origin, no camera, microphone, location, payment or usb, and HSTS in case the ingress ever forgets it. `Content-Security-Policy` is not among them — a policy that fits one application forbids another one's inline bootstrap, so it stays with the project. Cross-origin isolation is an option and off by default: it buys a Flutter build the threaded renderer it otherwise ships without using, and costs every cross-origin resource that does not opt in. The server also answers `/health` now, which is what Kubernetes asks before it sends anyone here, and the smoke test asks the same. Co-authored-by: Cursor <cursoragent@cursor.com>
Three things every one of these workflows was missing. Each workflow now starts its jobs at `contents: read`, so a job added later reads the repository and nothing else until it says otherwise. The two that publish — Pages and the review app cleanup — already asked for what they need, and now stand out for it. Each job carries a timeout. Not to hurry a build that works: a runner waiting for something that will never arrive holds a queue for six hours, and the only workflow this replaced that bounded anything bounded it at five minutes. And the checkout no longer leaves the token in `.git/config`, where every later step could read it — including code generators and package scripts. Nothing of ours talks to the remote after the checkout; the steps that use the API take the token as their own environment variable. Co-authored-by: Cursor <cursoragent@cursor.com>
Nobody could answer what is inside one of our images without pulling it apart, which is the question every advisory starts with. An SPDX document is now written for each archive before it is pushed, from the archive rather than from the recipe, so it describes what is about to be published and not what was meant to be. A second one is written from the lockfile: a compiled bundle carries no trace of the packages it came from, so the image's own contents say nothing about the application's dependencies — and those are the ones advisories are written about. Both are kept as an artefact of the run. Tying them to the image in the registry follows. Co-authored-by: Cursor <cursoragent@cursor.com>
A pull by tag got whatever the registry had under that tag, and there was no way to tell whether it came from here. Now every digest we push is signed, and the SPDX documents from the previous commit are attached to the digests they describe, so what an image holds travels with it instead of expiring with the run that built it. Keyless, so there is no key of ours to hold, leak or rotate: the signature is bound to this workflow's identity and GitHub vouches for that over OIDC, which is why the job now asks for `id-token`. The price is a public record in the transparency log naming the image and its digest. The manifest list is signed too, and it is the one that matters: it is what a pull by tag resolves to, so it is what a policy will check. Co-authored-by: Cursor <cursoragent@cursor.com>
Nothing held our images or their dependencies against an advisory database, so a package with a known hole travelled to production and nobody was told. The scanner reads the documents from the previous commits rather than digging through the archives again, which means what it reports is about exactly the list of packages that ships. The report lands in the run's summary, where it is read without opening a log. It reports and does not block, and `ci.advisories.failOn` is what turns it into a gate. Off to begin with on purpose: a threshold set before anyone has seen a report either stops every release on the day it lands or teaches everyone to scroll past it. Co-authored-by: Cursor <cursoragent@cursor.com>
A tag says a version exists and nothing more. Whoever is looking for what changed has to find the changelog in the tree and read the right section of it, and anyone watching the repository for releases sees nothing at all. The workflow takes that section — whichever heading level it is written at — and publishes it as the release for the tag. A version with a suffix is marked as a prerelease, so a release candidate does not present itself as the current version. When the changelog has nothing to say about the version, GitHub's summary of the commits stands in with a warning rather than failing the run: by the time the tag is pushed, the file can no longer be fixed for it. Co-authored-by: Cursor <cursoragent@cursor.com>
A Flutter web application needs the WebAssembly module and its glue, which the native library is not a variant of. Both are cut from one pinned source now, so the Dart package can never talk to bindings from another revision. Collects everything vodozemac under nix/dart/vodozemac/ on the way. Signed-off-by: Niklas Zender <n.zender@famedly.com> Co-authored-by: Cursor <cursoragent@cursor.com>
A Flutter project depends on flutter_vodozemac, not on the plain package, and the hook then found no constraint at all and failed every commit. Both names are cut from the tag the bindings are built from. Signed-off-by: Niklas Zender <n.zender@famedly.com> Co-authored-by: Cursor <cursoragent@cursor.com>
The vodozemac module and LiveKit's encryption worker are compiled artefacts of dependencies that a web build expects in web/, and a repository that carried them would carry copies that silently age. The worker is compiled from the package the project already resolved, so it follows pubspec.lock instead of a second clone pinned by hand. One script does both, and the devshell carries it: an asset only CI can produce is one a developer cannot reproduce when the site misbehaves in the browser. Signed-off-by: Niklas Zender <n.zender@famedly.com> Co-authored-by: Cursor <cursoragent@cursor.com>
`./pkg/` is the default `wasmPath` of both the plugin and the plain package, so an application that gets the module from us now needs to pass nothing at all. An application that passes a path of its own has to drop it. Signed-off-by: Niklas Zender <n.zender@famedly.com>
The eight-core larger runner was the only way to build for arm64 when these workflows were written: GitHub's standard arm64 runners were public repositories only. They reached private ones in January, so the choice is now one of price against wait. Measured, both runners on the same commit, twice each. The larger one has eight cores, 31 GB and 393 GB of disk; the standard one four, 15 GB and 109 GB — more than the documentation promises, and far more than a three gigabyte Flutter closure needs. Fetching and unpacking the Dart SDK takes the same time on both, compiling the vodozemac bindings 44 against 59 seconds, and the job as a whole 137 and 122 against 157 and 148. That is 1.16 times faster for 2.8 times the minute, since a larger runner also draws nothing from the included minutes. It would have to reach 2.8 to break even. Half of a build of ours is spent moving bytes rather than compiling them, and no number of cores shortens that. The option stays for the project whose arm64 build really is compilation from end to end, and `arm64Release` still follows it for one that wants to spend more on a release than on a nightly. Signed-off-by: Niklas Zender <n.zender@famedly.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Nix builds in `/nix/var/nix/builds/nix-<pid>-<random>`, and on darwin, where no sandbox remaps that to a constant, the name reached the artefacts: a hundred and four times in the shared library, two hundred and twelve in the static one, plus the hashes LLVM derives from a module's path and the install name the linker writes. Two builds were never the same bytes, so nobody could rebuild the library and see that it matched what was shipped — which is most of what pinning a dependency by version is for. Rewriting the prefix leaves the static library reproducible down to the byte. The shared one keeps a UUID and an ad-hoc signature the linker derives from the rest; dropping the UUID would buy the last forty-eight bytes at the cost of symbolicating a crash, which is the worse trade. The install name is worth fixing whether or not any of that matters. Until now the library announced its own location as a directory that stopped existing when the build finished. Nothing reads it, because `flutter_rust_bridge` opens the file by the path it is handed, but anything that linked against it would have kept a reference to somewhere that was never there. Signed-off-by: Niklas Zender <n.zender@famedly.com> Co-authored-by: Cursor <cursoragent@cursor.com>
nikzen
force-pushed
the
fix/vodozemac-darwin-build-paths
branch
from
August 4, 2026 07:29
49ca2ff to
d7355b0
Compare
5 tasks
yash-garg
approved these changes
Aug 5, 2026
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.
nix build --rebuildshows that two builds offamedly-vodozemacon a Mac arenever the same bytes. Nix builds in
/nix/var/nix/builds/nix-<pid>-<random>,and darwin has no sandbox to remap that to a constant, so the path reaches the
artefacts: 104 occurrences in the shared library, 212 in the static one, the
symbol suffixes LLVM hashes from a module's path, and
LC_ID_DYLIB, which ld64fills with the full output path when no install name is given.
Remapping the prefix leaves the static library reproducible down to the
byte and takes the shared one from ~1500 differing bytes to 48: the Mach-O
UUID and the ad-hoc signature over it.
-Wl,-no_uuidwould remove those at thecost of ever symbolicating a crash, which is the worse trade.
The install name is worth fixing either way. The library announced its own
location as a build directory that stopped existing when the build finished.
Nothing reads it, since
flutter_rust_bridgeopens the file by the path it ishanded, but anything that linked against it would have kept that reference.
Inert on Linux: the hook is darwin-only, and the sandbox already mounts the
build directory at a constant
/build.web.nixis a different artefact by adifferent route and would need its own measurement.
Stacked on #56, where
native.nixlives — the file this used to patch does notsurvive that split. The derivation does: with this applied, both branches
produce the same store path.
Found while measuring what was worth putting in the shared binary cache (#59),
which it does not block.
Test plan
nix flake check --all-systems,prek --all-files --stage pre-pushnix build --rebuildonaarch64-darwin: the.ais byte-identical,the
.dylibdown to 48 bytes, no build directory paths leftotool -lshows the install name under$out/libMade with Cursor
The stack
#63 and #59 stand outside the chain and can land in any order. #59 and #56
both touch the vodozemac module, so whichever goes second wants a rebase.