From 09a970af64126107ac41d8c217ee0764a75cb27d Mon Sep 17 00:00:00 2001 From: Marcus Eagan Date: Sat, 5 Sep 2026 12:28:32 -0700 Subject: [PATCH] fix: stop linking released macOS binaries to /nix/store libiconv The aarch64-apple-darwin release binary recorded an absolute /nix/store/...-libiconv-113/lib/libiconv.2.dylib install name, so dyld refused to launch it on any machine but the one that built it. Nix's darwin libiconv is Apple's own libiconv, and it exports the same symbols as the copy macOS keeps in the dyld shared cache. Repointing the install name at /usr/lib/libiconv.2.dylib is therefore ABI-safe and leaves the binary with no Nix store references at all. install_name_tool invalidates the ad-hoc signature arm64 macOS requires, so the rewrite runs in preFixup and autoSignDarwinBinariesHook re-signs it during fixup. The build now fails outright if any /nix/store reference survives, and the release workflow additionally unpacks the finished tarball and runs the binary, so a regression cannot reach a published release unnoticed. Fixes #2727 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01UmmEKcy5V7MJvqQz2Gu1dD --- .github/workflows/release.yaml | 38 +++++++++++++++++++++++ flake.nix | 57 +++++++++++++++++++++++++++++++++- 2 files changed, 94 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index f0fec4e2e..b21fe0e33 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -63,12 +63,18 @@ jobs: - target: x86_64-unknown-linux-musl os: ubuntu-24.04 attr: nativelink-x86_64-linux + # The runner shares this target's architecture, so the packaged + # binary can be executed to prove it actually starts. + smoke_test: true - target: aarch64-unknown-linux-musl os: ubuntu-24.04 attr: nativelink-aarch64-linux + # Cross-built from x86_64, so the runner cannot execute it. + smoke_test: false - target: aarch64-apple-darwin os: macos-26 attr: nativelink-aarch64-darwin + smoke_test: true runs-on: ${{ matrix.os }} timeout-minutes: 90 env: @@ -125,6 +131,38 @@ jobs: sha256 "${ASSET}.tar.gz" | tee "${ASSET}.tar.gz.sha256" echo "asset=${ASSET}" >> "${GITHUB_OUTPUT}" + # The v1.6.6 macOS tarball shipped with an absolute /nix/store install + # name for libiconv and could not launch on any machine but the builder + # (#2727). The Nix build now fails on a leftover store reference, and + # this re-checks the packaged tarball so a regression cannot reach a + # published release unnoticed. + - name: Smoke test packaged binary + if: matrix.smoke_test + run: | + set -euo pipefail + ASSET="${{ steps.package.outputs.asset }}" + CHECK="$(mktemp -d)" + tar -C "${CHECK}" -xzf "${ASSET}.tar.gz" + + if [[ "${{ runner.os }}" == "macOS" ]]; then + otool -L "${CHECK}/nativelink" + if otool -L "${CHECK}/nativelink" | tail -n +2 | grep '/nix/store'; then + echo "::error::Packaged macOS binary links against /nix/store paths that do not exist on user machines." + exit 1 + fi + codesign --verify --verbose "${CHECK}/nativelink" + fi + + # nativelink exits non-zero with a usage message when handed no + # config file. Reaching that message proves dyld loaded the binary + # and it ran, which is all this check needs to establish. + output="$("${CHECK}/nativelink" 2>&1 || true)" + echo "${output}" + if ! echo "${output}" | grep -q 'Usage: nativelink'; then + echo "::error::Packaged binary for ${{ matrix.target }} did not start." + exit 1 + fi + - name: Generate SBOM uses: >- # v0.24.0 anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 diff --git a/flake.nix b/flake.nix index 42a15e6c5..0b5f65392 100644 --- a/flake.nix +++ b/flake.nix @@ -171,6 +171,60 @@ cargoArtifactsFor = p: (craneLibFor p).buildDepsOnly (commonArgsFor p); nightlyCargoArtifactsFor = p: (nightlyCraneLibFor p).buildDepsOnly (commonArgsFor p); + # Darwin binaries link against Nix's `libiconv`, which records an + # absolute `/nix/store/...` install name. That path only exists on the + # machine that built the binary, so the published macOS tarball failed + # to launch anywhere else with a dyld "Library not loaded" error. + # + # Nix's darwin `libiconv` is Apple's own libiconv and exports the same + # symbols as the copy macOS keeps in the dyld shared cache, so pointing + # the install name at `/usr/lib` is ABI-safe and leaves the binary with + # no Nix store references at all. + # + # See https://github.com/TraceMachina/nativelink/issues/2727. + darwinSystemDylibArgs = p: { + # `install_name_tool` invalidates the ad-hoc signature that arm64 + # macOS requires. This hook re-signs during fixup, i.e. after the + # `preFixup` rewrite below. + nativeBuildInputs = + (commonArgsFor p).nativeBuildInputs + ++ [p.darwin.autoSignDarwinBinariesHook]; + + preFixup = '' + for binary in "$out"/bin/*; do + [ -f "$binary" ] || continue + + # Skip anything that isn't a Mach-O image, such as wrapper scripts. + linkage="$(otool -L "$binary" 2>/dev/null)" || continue + + printf '%s\n' "$linkage" | tail -n +2 | awk '{print $1}' \ + | while read -r dylib; do + case "$dylib" in + /nix/store/*/lib/libiconv*.dylib | /nix/store/*/lib/libcharset*.dylib) + echo "relocating $dylib -> /usr/lib/''${dylib##*/} in $binary" + install_name_tool \ + -change "$dylib" "/usr/lib/''${dylib##*/}" "$binary" + ;; + esac + done + + remaining="$( + otool -L "$binary" | tail -n +2 | awk '{print $1}' \ + | grep '^/nix/store' || true + )" + if [ -n "$remaining" ]; then + echo "error: $binary still links against Nix store libraries:" >&2 + echo "$remaining" >&2 + echo "Those paths do not exist on machines without Nix, so the" >&2 + echo "released binary would fail to launch. Either relocate the" >&2 + echo "library to its /usr/lib equivalent above, or link it" >&2 + echo "statically." >&2 + exit 1 + fi + done + ''; + }; + nativelinkFor = p: (craneLibFor p).buildPackage ((commonArgsFor p) // { @@ -178,7 +232,8 @@ # If you're testing Nativelink locally, doing a dev profile will # massively speedup build times. Just don't commit/push anything build with dev! # CARGO_PROFILE = "dev"; - }); + } + // pkgs.lib.optionalAttrs p.stdenv.targetPlatform.isDarwin (darwinSystemDylibArgs p)); nativeTargetPkgs = if pkgs.stdenv.hostPlatform.system == "x86_64-linux"