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"