From 079440f7a9c700ccb3caec2cfad9c4493b8463ff Mon Sep 17 00:00:00 2001 From: sechmachine <97589681+sechmachine727@users.noreply.github.com> Date: Wed, 12 Aug 2026 23:39:48 +0700 Subject: [PATCH] build(core): package deterministic arm64 framework --- core/Cargo.toml | 6 ++ core/include/module.modulemap | 4 ++ core/scripts/build-xcframework.sh | 91 +++++++++++++++++++++++++ core/tests/packaging.sh | 106 ++++++++++++++++++++++++++++++ 4 files changed, 207 insertions(+) create mode 100644 core/include/module.modulemap create mode 100755 core/scripts/build-xcframework.sh create mode 100755 core/tests/packaging.sh diff --git a/core/Cargo.toml b/core/Cargo.toml index 31065f5..f2e9957 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -24,3 +24,9 @@ path = "tests/protocol_fixtures.rs" [workspace] resolver = "2" + +[profile.release] +panic = "abort" +lto = "fat" +codegen-units = 1 +incremental = false diff --git a/core/include/module.modulemap b/core/include/module.modulemap new file mode 100644 index 0000000..f5cd403 --- /dev/null +++ b/core/include/module.modulemap @@ -0,0 +1,4 @@ +module VerseVDICore { + header "versevdi_core.h" + export * +} diff --git a/core/scripts/build-xcframework.sh b/core/scripts/build-xcframework.sh new file mode 100755 index 0000000..1fe0cc7 --- /dev/null +++ b/core/scripts/build-xcframework.sh @@ -0,0 +1,91 @@ +#!/bin/sh +set -eu + +usage() { + echo "usage: $0 --output ABSOLUTE_DIR --target-dir ABSOLUTE_DIR" >&2 + exit 2 +} + +output= +target_dir= +while test "$#" -gt 0; do + case "$1" in + --output) + test "$#" -ge 2 || usage + output=$2 + shift 2 + ;; + --target-dir) + test "$#" -ge 2 || usage + target_dir=$2 + shift 2 + ;; + *) usage ;; + esac +done + +test -n "$output" || usage +test -n "$target_dir" || usage +case "$output:$target_dir" in + /*:/*) ;; + *) usage ;; +esac +case "$output/:$target_dir/" in + "$target_dir/"*:*|*:"$output/"*) + echo "output and target directory must be separate" >&2 + exit 2 + ;; +esac + +ROOT=$(CDPATH= cd -- "$(dirname "$0")/../.." && pwd) +case "$output" in + "$ROOT"|"$ROOT"/*) + echo "output must be outside the repository" >&2 + exit 2 + ;; +esac +case "$target_dir" in + "$ROOT"|"$ROOT"/*) + echo "target directory must be outside the repository" >&2 + exit 2 + ;; +esac +test ! -e "$output" || { + echo "output already exists: $output" >&2 + exit 2 +} +test ! -e "$target_dir" || { + echo "target directory already exists: $target_dir" >&2 + exit 2 +} +test "$(uname -s)" = Darwin || { + echo "XCFramework packaging requires macOS" >&2 + exit 2 +} + +source_epoch=$(git -C "$ROOT" show -s --format=%ct HEAD) +export CARGO_INCREMENTAL=0 +export CARGO_TARGET_DIR="$target_dir" +export MACOSX_DEPLOYMENT_TARGET=14.0 +export RUSTFLAGS="--remap-path-prefix=$ROOT=." +export SOURCE_DATE_EPOCH="$source_epoch" +export ZERO_AR_DATE=1 + +cargo build \ + --manifest-path "$ROOT/core/Cargo.toml" \ + --target aarch64-apple-darwin \ + --release \ + --locked + +headers="$target_dir/xcframework-headers" +mkdir -p "$headers" "$output" +cp "$ROOT/core/include/versevdi_core.h" "$headers/" +cp "$ROOT/core/include/module.modulemap" "$headers/" + +xcodebuild -create-xcframework \ + -library "$target_dir/aarch64-apple-darwin/release/libversevdi_core.a" \ + -headers "$headers" \ + -output "$output/VerseVDICore.xcframework" + +timestamp=$(date -r "$source_epoch" +%Y%m%d%H%M.%S) +find "$output/VerseVDICore.xcframework" -exec touch -h -t "$timestamp" {} + diff --git a/core/tests/packaging.sh b/core/tests/packaging.sh new file mode 100755 index 0000000..8d2c500 --- /dev/null +++ b/core/tests/packaging.sh @@ -0,0 +1,106 @@ +#!/bin/sh +set -eu + +ROOT=$(CDPATH= cd -- "$(dirname "$0")/../.." && pwd) +BUILDER="$ROOT/core/scripts/build-xcframework.sh" +EXPECTED_EXPORTS='_verse_core_abi_version +_verse_core_cancel_v1 +_verse_core_connect_v1 +_verse_core_create_v1 +_verse_core_destroy_v1 +_verse_core_request_idr_v1 +_verse_core_send_input_v1' + +test "$(uname -s)" = Darwin +test "$(uname -m)" = arm64 +test -x "$BUILDER" + +WORK=$(mktemp -d "${TMPDIR:-/tmp}/versevdi-core-package.XXXXXX") +trap 'rm -rf "$WORK"' EXIT HUP INT TERM + +"$BUILDER" --output "$WORK/one" --target-dir "$WORK/target-one" +"$BUILDER" --output "$WORK/two" --target-dir "$WORK/target-two" + +framework_one="$WORK/one/VerseVDICore.xcframework" +framework_two="$WORK/two/VerseVDICore.xcframework" +library_one=$(find "$framework_one" -type f -name libversevdi_core.a -print) +library_two=$(find "$framework_two" -type f -name libversevdi_core.a -print) +test "$(printf '%s\n' "$library_one" | grep -c .)" -eq 1 +test "$(printf '%s\n' "$library_two" | grep -c .)" -eq 1 + +test "$(/usr/libexec/PlistBuddy -c 'Print :AvailableLibraries:0:SupportedPlatform' "$framework_one/Info.plist")" = macos +test "$(/usr/libexec/PlistBuddy -c 'Print :AvailableLibraries:0:SupportedArchitectures:0' "$framework_one/Info.plist")" = arm64 +test "$(/usr/libexec/PlistBuddy -c 'Print :AvailableLibraries' "$framework_one/Info.plist" | grep -c 'Dict {')" -eq 1 +test "$(xcrun lipo -archs "$library_one")" = arm64 +file "$library_one" | grep -F 'current ar archive' >/dev/null + +consumer="$WORK/consumer" +xcrun clang \ + -arch arm64 \ + -mmacosx-version-min=14.0 \ + -std=c11 \ + -Wall -Wextra -Werror -Wpedantic \ + -fmodules \ + -fmodules-cache-path="$WORK/module-cache" \ + -I"$(dirname "$library_one")/Headers" \ + "$ROOT/core/tests/ffi/abi_smoke.c" \ + "$library_one" \ + -framework Security \ + -framework SystemConfiguration \ + -framework CoreFoundation \ + -lresolv \ + -o "$consumer" +test "$(xcrun lipo -archs "$consumer")" = arm64 +file "$consumer" | grep -F 'Mach-O 64-bit executable arm64' >/dev/null + +symbols=$(xcrun nm -gjU "$consumer") +actual_exports=$(printf '%s\n' "$symbols" | grep '^_verse_core_' | LC_ALL=C sort -u) +test "$actual_exports" = "$EXPECTED_EXPORTS" + +dependencies=$(xcrun otool -L "$consumer") +unexpected_dependencies=$(printf '%s\n' "$dependencies" | tail -n +2 | awk '{print $1}' | grep -Ev '^(/usr/lib/(libSystem\.B|libresolv\.9)\.dylib|/System/Library/Frameworks/(CoreFoundation|Security|SystemConfiguration)\.framework/Versions/A/[^/]+)$' || true) +test -z "$unexpected_dependencies" + +"$consumer" +i=0 +pids= +while test "$i" -lt 32; do + "$consumer" & + pids="$pids $!" + i=$((i + 1)) +done +for pid in $pids; do + wait "$pid" +done + +canonical_tree() { + ( + cd "$1" + find . -type f -print | LC_ALL=C sort | while IFS= read -r path; do + digest=$(shasum -a 256 "$path" | awk '{print $1}') + printf '%s %s\n' "$digest" "$path" + done + ) +} + +canonical_tree "$framework_one" >"$WORK/one.tree" +canonical_tree "$framework_two" >"$WORK/two.tree" +cmp "$WORK/one.tree" "$WORK/two.tree" +test "$(shasum -a 256 "$library_one" | awk '{print $1}')" = "$(shasum -a 256 "$library_two" | awk '{print $1}')" + +source_epoch=$(git -C "$ROOT" show -s --format=%ct HEAD) +find "$framework_one" -exec stat -f '%m' {} \; | while IFS= read -r epoch; do + test "$epoch" = "$source_epoch" +done + +strings "$library_one" >"$WORK/library.strings" +if grep -F "$ROOT" "$WORK/library.strings" >/dev/null; then + echo "repository path leaked into static archive" >&2 + exit 1 +fi + +archive_members=$(xcrun ar -tv "$library_one") +printf '%s\n' "$archive_members" | awk '$7 != "1970" { exit 1 }' + +printf 'framework_sha256=%s\n' "$(shasum -a 256 "$WORK/one.tree" | awk '{print $1}')" +printf 'library_sha256=%s\n' "$(shasum -a 256 "$library_one" | awk '{print $1}')"