Problem
When dx is killed while writing the fat-binary deps archive libdeps-<hash>.a (Ctrl+C, SIGTERM/SIGKILL, a supervisor restarting it, machine crash…), it leaves an empty or truncated archive behind — and every subsequent build then fails forever with:
ERROR Failed to generate fat binary: ld: file is empty in
'~/.cargo/target/aarch64-apple-darwin/desktop-dev/libdeps-2241ac6f.a'
clang: error: linker command failed with exit code 1
ERROR Build failed: No such file or directory (os error 2)
The only way out is to manually delete the profile dir (e.g. rm -rf <target>/<triple>/desktop-dev). Nothing in dx ever detects or repairs the poisoned cache.
Root cause
Two things combine in packages/cli/src/build/link.rs (still present on current main, verified today):
-
The archive is written in place — no temp file + atomic rename (link.rs:1000-1001):
let bytes = out_ar.into_inner().context("Failed to finalize archive")?;
std::fs::write(&out_ar_path, bytes).context("Failed to write archive")?;
std::fs::write truncates then writes at the final path, so dying mid-write leaves an empty/partial libdeps-<hash>.a at the cached location.
-
The cache-hit check only tests existence — no content validation (link.rs:923-941):
let mut archive_has_contents = out_ar_path.exists();
...
if !archive_has_contents || cfg!(debug_assertions) {
On a release build of dx, cfg!(debug_assertions) is false, so any pre-existing file — including a zero-byte one — is accepted as a valid cache hit and fed straight to the linker.
Reproduction (dx 0.7.3, also applies to current main)
- Run
dx serve (or any hot-patch workflow) on a project with many dependencies.
- While the fat binary is being generated (first compile),
kill -9 the dx process (touching Cargo.toml at the wrong moment does it too if a wrapper restarts dx on manifest changes).
- Relaunch →
ld: file is empty in .../libdeps-<hash>.a on every build until the file is manually deleted.
We hit this repeatedly in a wrapper tool that restarts dx when Cargo.toml changes: a version bump triggered the restart exactly during the post-bump archive regeneration, and every developer build was broken until a manual purge.
Suggested fix
Either (ideally both):
- Write the archive to a temp path (
libdeps-<hash>.a.tmp) and atomically rename it into place, so a partial write is never visible at the cached path.
- Validate the archive on cache hit before using it: size > 0 and the
!<arch> ar magic; regenerate on mismatch.
Both are cheap and would make the cache self-healing after any interruption.
Environment: dx 0.7.3 (macOS aarch64), behavior confirmed unchanged in released 0.7.9 and on current main.
Problem
When
dxis killed while writing the fat-binary deps archivelibdeps-<hash>.a(Ctrl+C, SIGTERM/SIGKILL, a supervisor restarting it, machine crash…), it leaves an empty or truncated archive behind — and every subsequent build then fails forever with:The only way out is to manually delete the profile dir (e.g.
rm -rf <target>/<triple>/desktop-dev). Nothing in dx ever detects or repairs the poisoned cache.Root cause
Two things combine in
packages/cli/src/build/link.rs(still present on currentmain, verified today):The archive is written in place — no temp file + atomic rename (link.rs:1000-1001):
std::fs::writetruncates then writes at the final path, so dying mid-write leaves an empty/partiallibdeps-<hash>.aat the cached location.The cache-hit check only tests existence — no content validation (link.rs:923-941):
On a release build of dx,
cfg!(debug_assertions)is false, so any pre-existing file — including a zero-byte one — is accepted as a valid cache hit and fed straight to the linker.Reproduction (dx 0.7.3, also applies to current main)
dx serve(or any hot-patch workflow) on a project with many dependencies.kill -9the dx process (touchingCargo.tomlat the wrong moment does it too if a wrapper restarts dx on manifest changes).ld: file is empty in .../libdeps-<hash>.aon every build until the file is manually deleted.We hit this repeatedly in a wrapper tool that restarts dx when
Cargo.tomlchanges: a version bump triggered the restart exactly during the post-bump archive regeneration, and every developer build was broken until a manual purge.Suggested fix
Either (ideally both):
libdeps-<hash>.a.tmp) and atomicallyrenameit into place, so a partial write is never visible at the cached path.!<arch>ar magic; regenerate on mismatch.Both are cheap and would make the cache self-healing after any interruption.
Environment: dx 0.7.3 (macOS aarch64), behavior confirmed unchanged in released 0.7.9 and on current
main.