From 91a4b7949f0cfa2162a40c9130530be131207d84 Mon Sep 17 00:00:00 2001 From: Simba Zhang Date: Fri, 9 Oct 2026 06:22:26 -0700 Subject: [PATCH 1/2] test: stop copying metallib into .xctest bundles; check the Metal toolchain instead SwiftPM compiles the Cmlx kernels itself and embeds default.metallib in mlx-swift_Cmlx.bundle inside every .xctest. Hiding that file makes the MLX tests abort with 'Failed to load the default metallib'; with it present the suites pass without any copied metallib. Copying a metallib into .xctest/Contents/MacOS invalidates the bundle signature and the next incremental 'swift build --build-tests' fails at CodeSign. install-test-metallib.sh, bootstrap_local_tests.sh and the build_and_unit_test CI step all did that. - add scripts/check-metal-toolchain.sh (fails early with the fix) - drop scripts/install-test-metallib.sh (its search path no longer matched the Swift 6.4 layout and it fell back to pip-installing the mlx wheel) - bootstrap_local_tests.sh: toolchain check + build, optional --with-server - coverage-report.sh: use the toolchain check - ci.yml: remove only the Contents/MacOS copy; the copy next to the release binary stays Refs #128 Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/ci.yml | 1 - scripts/bootstrap_local_tests.sh | 37 ++++++++++-------------- scripts/check-metal-toolchain.sh | 22 +++++++++++++++ scripts/coverage-report.sh | 6 ++-- scripts/install-test-metallib.sh | 48 -------------------------------- 5 files changed, 39 insertions(+), 75 deletions(-) create mode 100755 scripts/check-metal-toolchain.sh delete mode 100755 scripts/install-test-metallib.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 219b5f6..878aea9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,7 +67,6 @@ jobs: python3 -m venv /tmp/mlx_venv /tmp/mlx_venv/bin/pip install --quiet mlx cp /tmp/mlx_venv/lib/python*/site-packages/mlx/lib/mlx.metallib .build/release/ - find .build -type d -name "MacOS" -exec cp /tmp/mlx_venv/lib/python*/site-packages/mlx/lib/mlx.metallib {}/ \; - name: SwiftBuddy Tests (MemPalace & Lifecycle) run: swift test --skip-build --filter SwiftBuddyTests --disable-swift-testing diff --git a/scripts/bootstrap_local_tests.sh b/scripts/bootstrap_local_tests.sh index f3770c6..99b48ff 100755 --- a/scripts/bootstrap_local_tests.sh +++ b/scripts/bootstrap_local_tests.sh @@ -1,33 +1,26 @@ #!/bin/bash # Makes `swift test` runnable locally without CI's help. # -# A bare `swift test` aborts with "Failed to load the default metallib" -# because Package.swift links MLX but nothing on a local machine ever builds -# or installs mlx.metallib. CI works around this in .github/workflows/ci.yml -# ("Install MLX Metal library" step) by pip-installing the `mlx` wheel and -# copying its bundled metallib into every built .xctest bundle. This script -# does the same thing locally. +# SwiftPM compiles the MLX kernels itself and embeds the metallib in each test bundle, so +# nothing needs to be copied around; the only prerequisite is the Metal Toolchain (Xcode 26+ +# ships it as a separate download). Do NOT copy a metallib into .xctest/Contents/MacOS: +# it invalidates the bundle signature and the next incremental build fails at CodeSign. +# +# Usage: scripts/bootstrap_local_tests.sh [--with-server] +# --with-server also build the release SwiftLM binary. A few SwiftBuddyTests (VLM, audio) +# launch it from .build/release/SwiftLM and fail with "Could not find +# SwiftLM executable" without it. set -eo pipefail +cd "$(dirname "$0")/.." -VENV_DIR="${MLX_METALLIB_VENV:-/tmp/swiftlm_mlx_venv}" +bash scripts/check-metal-toolchain.sh echo "=> Building test harness (swift build --build-tests)..." swift build --build-tests -echo "=> Installing MLX Metal library..." -if [ ! -d "$VENV_DIR" ]; then - python3 -m venv "$VENV_DIR" -fi -"$VENV_DIR/bin/pip" install --quiet --upgrade mlx - -METALLIB=$(find "$VENV_DIR" -name "mlx.metallib" | head -1) -if [ -z "$METALLIB" ]; then - echo "error: mlx.metallib not found after pip install mlx" >&2 - exit 1 +if [ "${1:-}" = "--with-server" ]; then + echo "=> Building the SwiftLM server binary (swift build -c release)..." + swift build -c release fi -cp "$METALLIB" .build/debug/ 2>/dev/null || true -cp "$METALLIB" .build/release/ 2>/dev/null || true -find .build -type d -name "MacOS" -exec cp "$METALLIB" {}/ \; - -echo "=> Done. Run tests with: swift test --skip-build" +echo "=> Done. Run tests with: swift test --skip-build --filter SwiftLMTests --disable-swift-testing" diff --git a/scripts/check-metal-toolchain.sh b/scripts/check-metal-toolchain.sh new file mode 100755 index 0000000..74a851c --- /dev/null +++ b/scripts/check-metal-toolchain.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# check-metal-toolchain.sh — fail early, with the fix, when the Metal compiler is missing. +# +# SwiftPM compiles the Cmlx kernels itself and embeds `default.metallib` in +# mlx-swift_Cmlx.bundle inside every .xctest, so `swift build --build-tests` followed by +# `swift test --skip-build` needs no hand-copied metallib. Copying one into +# .xctest/Contents/MacOS invalidates the bundle signature and the next incremental +# build then fails at CodeSign. The one thing the build does need is the Metal compiler, +# which Xcode 26+ ships as a separate download. See #128 Tier 3. +set -euo pipefail + +if xcrun -find metal >/dev/null 2>&1; then + exit 0 +fi + +cat >&2 <<'MSG' +error: the Metal Toolchain is not installed, so the MLX kernels cannot be compiled. +Install it with: + + xcodebuild -downloadComponent MetalToolchain +MSG +exit 1 diff --git a/scripts/coverage-report.sh b/scripts/coverage-report.sh index d57bc81..9fd6a14 100755 --- a/scripts/coverage-report.sh +++ b/scripts/coverage-report.sh @@ -106,10 +106,8 @@ run_suite() { ) } -# The package suite aborts on its second test without this; see #128 Tier 3. -if [ -x "$REPO_ROOT/scripts/install-test-metallib.sh" ]; then - bash "$REPO_ROOT/scripts/install-test-metallib.sh" >/dev/null 2>&1 -fi +# SwiftPM embeds the metallib in each test bundle; only the Metal compiler is needed (#128 Tier 3). +bash "$REPO_ROOT/scripts/check-metal-toolchain.sh" case "$WHICH" in package) run_suite "$REPO_ROOT" "SwiftLM package" ;; diff --git a/scripts/install-test-metallib.sh b/scripts/install-test-metallib.sh deleted file mode 100755 index 10d1ed6..0000000 --- a/scripts/install-test-metallib.sh +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env bash -# install-test-metallib.sh — put mlx.metallib where the test bundles can find it. -# -# MLX aborts the process when it cannot load its Metal library, so without this a bare -# `swift test` dies on the first test that touches MLX — in practice the second test of -# the run, leaving ~250 tests unexecuted while the output still looks like a normal -# (short) pass. CI does this inline; this script is the same step for local use. -# -# Usage: scripts/install-test-metallib.sh [debug|release] (default: both, if present) - -set -euo pipefail -cd "$(dirname "$0")/.." - -CONFIGS=("${1:-debug release}") - -find_metallib() { - # 1. Already built by build.sh - for candidate in .build/*/release/mlx.metallib .build/*/release/default.metallib default.metallib; do - [ -f "$candidate" ] && { echo "$candidate"; return 0; } - done - # 2. From the mlx Python wheel, as CI does - local venv="${TMPDIR:-/tmp}/swiftlm_mlx_venv" - if [ ! -f "$venv"/lib/python*/site-packages/mlx/lib/mlx.metallib ]; then - echo "Fetching mlx.metallib from the mlx wheel…" >&2 - python3 -m venv "$venv" >&2 - "$venv/bin/pip" install --quiet mlx >&2 - fi - ls "$venv"/lib/python*/site-packages/mlx/lib/mlx.metallib 2>/dev/null | head -1 -} - -METALLIB="$(find_metallib)" -[ -n "$METALLIB" ] && [ -f "$METALLIB" ] || { echo "error: could not obtain mlx.metallib" >&2; exit 1; } -echo "Using $METALLIB" - -INSTALLED=0 -# Next to the built products, and inside every .xctest bundle — MLX looks for a metallib -# colocated with the running binary, which for tests is inside the bundle. -while IFS= read -r dir; do - cp "$METALLIB" "$dir/mlx.metallib" - cp "$METALLIB" "$dir/default.metallib" - INSTALLED=$((INSTALLED + 1)) -done < <( - { for c in $CONFIGS; do ls -d .build/*/"$c" .build/"$c" 2>/dev/null; done - find .build -type d -name "MacOS" 2>/dev/null; } | sort -u -) - -echo "Installed into $INSTALLED location(s)." -[ "$INSTALLED" -gt 0 ] || { echo "error: no build directories found — run 'swift build --build-tests' first" >&2; exit 1; } From 58f8f4b0a33ae5ebc09c4462f107f727fa258629 Mon Sep 17 00:00:00 2001 From: Simba Zhang Date: Fri, 9 Oct 2026 07:05:08 -0700 Subject: [PATCH 2/2] ci: restore metallib copy on Swift 6.2 CI; skip it only on the Swift 6.4 build layout The previous commit removed the metallib copy from ci.yml, but CI runs Xcode 26.3 (Swift 6.2, .build/arm64-apple-macosx/ layout) where default.metallib is not embedded in the test bundles, so the SwiftBuddy tests aborted with 'Failed to load the default metallib'. The embedding only exists on Swift 6.4 (.build/out layout), so the scripts now detect that layout and skip the copy there; ci.yml is back to its original state. Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/ci.yml | 1 + scripts/bootstrap_local_tests.sh | 45 ++++++++++++++++-------- scripts/coverage-report.sh | 6 ++-- scripts/install-test-metallib.sh | 59 ++++++++++++++++++++++++++++++++ 4 files changed, 94 insertions(+), 17 deletions(-) create mode 100755 scripts/install-test-metallib.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 878aea9..219b5f6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,6 +67,7 @@ jobs: python3 -m venv /tmp/mlx_venv /tmp/mlx_venv/bin/pip install --quiet mlx cp /tmp/mlx_venv/lib/python*/site-packages/mlx/lib/mlx.metallib .build/release/ + find .build -type d -name "MacOS" -exec cp /tmp/mlx_venv/lib/python*/site-packages/mlx/lib/mlx.metallib {}/ \; - name: SwiftBuddy Tests (MemPalace & Lifecycle) run: swift test --skip-build --filter SwiftBuddyTests --disable-swift-testing diff --git a/scripts/bootstrap_local_tests.sh b/scripts/bootstrap_local_tests.sh index 99b48ff..4082f24 100755 --- a/scripts/bootstrap_local_tests.sh +++ b/scripts/bootstrap_local_tests.sh @@ -1,26 +1,41 @@ #!/bin/bash # Makes `swift test` runnable locally without CI's help. # -# SwiftPM compiles the MLX kernels itself and embeds the metallib in each test bundle, so -# nothing needs to be copied around; the only prerequisite is the Metal Toolchain (Xcode 26+ -# ships it as a separate download). Do NOT copy a metallib into .xctest/Contents/MacOS: -# it invalidates the bundle signature and the next incremental build fails at CodeSign. -# -# Usage: scripts/bootstrap_local_tests.sh [--with-server] -# --with-server also build the release SwiftLM binary. A few SwiftBuddyTests (VLM, audio) -# launch it from .build/release/SwiftLM and fail with "Could not find -# SwiftLM executable" without it. +# A bare `swift test` aborts with "Failed to load the default metallib" +# because Package.swift links MLX but nothing on a local machine ever builds +# or installs mlx.metallib. CI works around this in .github/workflows/ci.yml +# ("Install MLX Metal library" step) by pip-installing the `mlx` wheel and +# copying its bundled metallib into every built .xctest bundle. This script +# does the same thing locally. set -eo pipefail -cd "$(dirname "$0")/.." -bash scripts/check-metal-toolchain.sh +VENV_DIR="${MLX_METALLIB_VENV:-/tmp/swiftlm_mlx_venv}" echo "=> Building test harness (swift build --build-tests)..." swift build --build-tests -if [ "${1:-}" = "--with-server" ]; then - echo "=> Building the SwiftLM server binary (swift build -c release)..." - swift build -c release +if [ -d .build/out ]; then + # Swift 6.4+ layout: the metallib is embedded in each test bundle (see + # scripts/install-test-metallib.sh); copying one in breaks the next incremental build. + bash scripts/check-metal-toolchain.sh + echo "=> Done. Run tests with: swift test --skip-build" + exit 0 +fi + +echo "=> Installing MLX Metal library..." +if [ ! -d "$VENV_DIR" ]; then + python3 -m venv "$VENV_DIR" fi +"$VENV_DIR/bin/pip" install --quiet --upgrade mlx + +METALLIB=$(find "$VENV_DIR" -name "mlx.metallib" | head -1) +if [ -z "$METALLIB" ]; then + echo "error: mlx.metallib not found after pip install mlx" >&2 + exit 1 +fi + +cp "$METALLIB" .build/debug/ 2>/dev/null || true +cp "$METALLIB" .build/release/ 2>/dev/null || true +find .build -type d -name "MacOS" -exec cp "$METALLIB" {}/ \; -echo "=> Done. Run tests with: swift test --skip-build --filter SwiftLMTests --disable-swift-testing" +echo "=> Done. Run tests with: swift test --skip-build" diff --git a/scripts/coverage-report.sh b/scripts/coverage-report.sh index 9fd6a14..d57bc81 100755 --- a/scripts/coverage-report.sh +++ b/scripts/coverage-report.sh @@ -106,8 +106,10 @@ run_suite() { ) } -# SwiftPM embeds the metallib in each test bundle; only the Metal compiler is needed (#128 Tier 3). -bash "$REPO_ROOT/scripts/check-metal-toolchain.sh" +# The package suite aborts on its second test without this; see #128 Tier 3. +if [ -x "$REPO_ROOT/scripts/install-test-metallib.sh" ]; then + bash "$REPO_ROOT/scripts/install-test-metallib.sh" >/dev/null 2>&1 +fi case "$WHICH" in package) run_suite "$REPO_ROOT" "SwiftLM package" ;; diff --git a/scripts/install-test-metallib.sh b/scripts/install-test-metallib.sh new file mode 100755 index 0000000..8bd2a40 --- /dev/null +++ b/scripts/install-test-metallib.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# install-test-metallib.sh — put mlx.metallib where the test bundles can find it. +# +# MLX aborts the process when it cannot load its Metal library, so without this a bare +# `swift test` dies on the first test that touches MLX — in practice the second test of +# the run, leaving ~250 tests unexecuted while the output still looks like a normal +# (short) pass. CI does this inline; this script is the same step for local use. +# +# Usage: scripts/install-test-metallib.sh [debug|release] (default: both, if present) + +set -euo pipefail +cd "$(dirname "$0")/.." + +# Swift 6.4+ (Xcode 27) builds into .build/out/ and embeds default.metallib in +# mlx-swift_Cmlx.bundle inside every .xctest, so there is nothing to install. Copying a +# metallib into .xctest/Contents/MacOS there invalidates the bundle signature and +# the next incremental build fails at CodeSign. Older toolchains (Swift 6.2 / Xcode 26, the +# layout CI uses: .build/arm64-apple-macosx/) do not embed one and still need the copy below. +if [ -d .build/out ]; then + bash scripts/check-metal-toolchain.sh + echo "Swift 6.4+ build layout: metallib is embedded in the test bundles, nothing to install." + exit 0 +fi + +CONFIGS=("${1:-debug release}") + +find_metallib() { + # 1. Already built by build.sh + for candidate in .build/release/mlx.metallib .build/*/release/mlx.metallib .build/*/release/default.metallib default.metallib; do + [ -f "$candidate" ] && { echo "$candidate"; return 0; } + done + # 2. From the mlx Python wheel, as CI does + local venv="${TMPDIR:-/tmp}/swiftlm_mlx_venv" + if [ ! -f "$venv"/lib/python*/site-packages/mlx/lib/mlx.metallib ]; then + echo "Fetching mlx.metallib from the mlx wheel…" >&2 + python3 -m venv "$venv" >&2 + "$venv/bin/pip" install --quiet mlx >&2 + fi + ls "$venv"/lib/python*/site-packages/mlx/lib/mlx.metallib 2>/dev/null | head -1 +} + +METALLIB="$(find_metallib)" +[ -n "$METALLIB" ] && [ -f "$METALLIB" ] || { echo "error: could not obtain mlx.metallib" >&2; exit 1; } +echo "Using $METALLIB" + +INSTALLED=0 +# Next to the built products, and inside every .xctest bundle — MLX looks for a metallib +# colocated with the running binary, which for tests is inside the bundle. +while IFS= read -r dir; do + cp "$METALLIB" "$dir/mlx.metallib" + cp "$METALLIB" "$dir/default.metallib" + INSTALLED=$((INSTALLED + 1)) +done < <( + { for c in $CONFIGS; do ls -d .build/*/"$c" .build/"$c" 2>/dev/null; done + find .build -type d -name "MacOS" 2>/dev/null; } | sort -u +) + +echo "Installed into $INSTALLED location(s)." +[ "$INSTALLED" -gt 0 ] || { echo "error: no build directories found — run 'swift build --build-tests' first" >&2; exit 1; }