Skip to content

cli: interrupted write of libdeps-<hash>.a permanently poisons the fat-binary cache (written in place, cache hit accepts empty archives) #5684

Description

@Plawn

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

  1. 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.

  2. 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)

  1. Run dx serve (or any hot-patch workflow) on a project with many dependencies.
  2. 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).
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions