Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions .github/workflows/release-binaries.yml
Original file line number Diff line number Diff line change
Expand Up @@ -539,10 +539,18 @@ jobs:
"aarch64/${binary}-aarch64-apple-darwin" \
-output "universal/${binary}-universal-apple-darwin"
chmod 755 "universal/${binary}-universal-apple-darwin"
# Xcode 16.4 lipo requires the input file before the
# -verify_arch command/arch flags; the legacy flags-first order
# parses the file path as an architecture and fails.
lipo "universal/${binary}-universal-apple-darwin" -verify_arch x86_64 arm64
# Newer lipo (Xcode 27) rejects several architectures in one
# -verify_arch ("requires exactly one input file"), and the
# input file must precede the flags: verify each architecture
# separately, then require exactly the two slices.
for arch in x86_64 arm64; do
lipo "universal/${binary}-universal-apple-darwin" -verify_arch "$arch"
done
archs="$(lipo -archs "universal/${binary}-universal-apple-darwin")"
[ "$(printf '%s' "$archs" | tr ' ' '\n' | LC_ALL=C sort | tr '\n' ' ')" = "arm64 x86_64 " ] || {
echo "unexpected architectures: $archs" >&2
exit 1
}
done
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
Expand Down
13 changes: 11 additions & 2 deletions .github/workflows/release-mcp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -269,8 +269,17 @@ jobs:
aarch64/terraphim_mcp_server-aarch64-apple-darwin \
-output "$out"
chmod 755 "$out"
# The input file must precede -verify_arch with Xcode 16.4 lipo.
lipo "$out" -verify_arch x86_64 arm64
# Newer lipo (Xcode 27) rejects several architectures in one
# -verify_arch ("requires exactly one input file"): verify each
# separately, then require exactly the two slices.
for arch in x86_64 arm64; do
lipo "$out" -verify_arch "$arch"
done
archs="$(lipo -archs "$out")"
[ "$(printf '%s' "$archs" | tr ' ' '\n' | LC_ALL=C sort | tr '\n' ' ')" = "arm64 x86_64 " ] || {
echo "unexpected architectures: $archs" >&2
exit 1
}
scripts/validate_release_binary.py universal-apple-darwin "$out"
reported="$("$out" --version | tail -n1 | awk '{print $NF}')"
[ "$reported" = "$CRATE_VERSION" ] || {
Expand Down
27 changes: 15 additions & 12 deletions tests/test_release_binaries_workflow_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -484,28 +484,31 @@ def test_managed_package_job_and_seal_gates_fail_closed(self) -> None:
self.assert_seal_package_result_contract(mutant)

def assert_lipo_verify_arch_order(self, block: str) -> None:
# Xcode 16.4 lipo requires the input file before the -verify_arch
# command and its architecture flags: the legacy order
# `lipo -verify_arch x86_64 arm64 FILE` parses FILE as an
# architecture and fails the universal macOS step. The only safe
# order is `lipo FILE -verify_arch x86_64 arm64`.
# Newer lipo (Xcode 27) rejects several architectures in a single
# -verify_arch ("requires exactly one input file"), and the input file
# must precede the flags. Verify each architecture on its own, then
# require exactly the two slices from `lipo -archs`.
self.assertIn("for arch in x86_64 arm64; do", block)
self.assertIn(
'lipo "universal/${binary}-universal-apple-darwin" -verify_arch x86_64 arm64',
block,
'lipo "universal/${binary}-universal-apple-darwin" -verify_arch "$arch"', block
)
self.assertNotIn("lipo -verify_arch x86_64 arm64", block)
self.assertIn('lipo -archs "universal/${binary}-universal-apple-darwin"', block)
self.assertIn('= "arm64 x86_64 "', block)
self.assertNotIn("-verify_arch x86_64 arm64", block)
self.assertNotIn("lipo -verify_arch", block)

def test_universal_lipo_verify_arch_uses_safe_argument_order(self) -> None:
block = job_block("create-universal-macos")
self.assert_lipo_verify_arch_order(block)

safe = 'lipo "universal/${binary}-universal-apple-darwin" -verify_arch "$arch"'
mutations = {
"flags-first": block.replace(
'lipo "universal/${binary}-universal-apple-darwin" -verify_arch x86_64 arm64',
'lipo -verify_arch x86_64 arm64 "universal/${binary}-universal-apple-darwin"',
1,
safe, 'lipo -verify_arch "$arch" "universal/${binary}-universal-apple-darwin"', 1
),
"verify-dropped": block.replace(" -verify_arch x86_64 arm64", "", 1),
"multi-arch": block.replace(safe, safe.replace('"$arch"', "x86_64 arm64"), 1),
"verify-dropped": block.replace(safe, "true", 1),
"archs-check-dropped": block.replace('= "arm64 x86_64 "', '= ""', 1),
}
for name, mutant in mutations.items():
with self.subTest(mutation=name):
Expand Down
28 changes: 28 additions & 0 deletions tests/test_release_mcp_workflow_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,34 @@ def test_assembled_artefact_preserves_the_assets_directory(self) -> None:
self.assertIn("test -d release/assets", publish)
self.assertIn("test -s release/checksums.txt", publish)

@staticmethod
def assert_lipo_verification(block: str) -> None:
# Newer lipo rejects several architectures in one -verify_arch and
# wants the file first: verify each separately, then require exactly
# the two slices.
assert "for arch in x86_64 arm64; do" in block
assert 'lipo "$out" -verify_arch "$arch"' in block
assert 'lipo -archs "$out"' in block
assert '= "arm64 x86_64 "' in block
assert "-verify_arch x86_64 arm64" not in block
assert "lipo -verify_arch" not in block

def test_universal_binary_verifies_each_architecture_separately(self) -> None:
block = job_block("universal-macos")
self.assert_lipo_verification(block)
safe = 'lipo "$out" -verify_arch "$arch"'
mutations = {
"flags-first": block.replace(safe, 'lipo -verify_arch "$arch" "$out"', 1),
"multi-arch": block.replace(safe, 'lipo "$out" -verify_arch x86_64 arm64', 1),
"verify-dropped": block.replace(safe, "true", 1),
"archs-check-dropped": block.replace('= "arm64 x86_64 "', '= ""', 1),
}
for name, mutant in mutations.items():
with self.subTest(mutation=name):
self.assertNotEqual(mutant, block)
with self.assertRaises(AssertionError):
self.assert_lipo_verification(mutant)

def test_source_is_trusted_before_any_self_hosted_job(self) -> None:
preflight = job_block("preflight")
self.assertIn('git merge-base --is-ancestor "$SOURCE_SHA" origin/main', preflight)
Expand Down
Loading