diff --git a/internal/driver/ioscompanion/companionassets/prepare.sh b/internal/driver/ioscompanion/companionassets/prepare.sh new file mode 100755 index 0000000..d69c27d --- /dev/null +++ b/internal/driver/ioscompanion/companionassets/prepare.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# +# Builds the embeddable simulator companion tarball. +# +# Copies the runtime files from a local install, preserving the bin/ and +# Frameworks/ layout (the binary resolves frameworks through @rpath, so the +# two directories must stay siblings). Build-time metadata that is never +# needed at runtime is stripped to keep the embedded payload small. The +# stripped layout is re-signed ad-hoc and proven to execute before it is +# packaged. +# +# Output: assets/companion-1.1.8.tar.gz next to this script. + +set -euo pipefail + +# Keep the archiver from emitting AppleDouble (._name) sidecar entries that +# would otherwise duplicate extended attributes into the payload. +export COPYFILE_DISABLE=1 + +VERSION="1.1.8" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ASSETS_DIR="${SCRIPT_DIR}/assets" +OUTPUT="${ASSETS_DIR}/companion-${VERSION}.tar.gz" + +# Default source is the local formula install. Resolve the symlink so the +# rsync below copies real files rather than a dangling link. +SOURCE="${COMPANION_SOURCE:-/opt/homebrew/opt/idb-companion}" +if [ ! -e "${SOURCE}" ]; then + echo "source not found: ${SOURCE}" >&2 + echo "set COMPANION_SOURCE to the install prefix" >&2 + exit 1 +fi +SOURCE="$(cd "${SOURCE}" && pwd -P)" + +if [ ! -x "${SOURCE}/bin/idb_companion" ]; then + echo "no companion binary at ${SOURCE}/bin/idb_companion" >&2 + exit 1 +fi + +STAGE="$(mktemp -d)" +PROOF="$(mktemp -d)" +trap 'rm -rf "${STAGE}" "${PROOF}"' EXIT + +echo "staging from ${SOURCE}" + +# Copy the runtime layout. -a preserves symlinks (the macOS framework +# Versions/Current links must survive intact). +mkdir -p "${STAGE}/bin" +cp -a "${SOURCE}/bin/idb_companion" "${STAGE}/bin/idb_companion" +cp -aR "${SOURCE}/Frameworks" "${STAGE}/Frameworks" + +# Strip build-time metadata from every framework. The framework binary, its +# Info.plist, the code signature, and any runtime dylibs are kept; headers and +# the Swift module/source-info artifacts are not loaded at runtime. +for framework in "${STAGE}"/Frameworks/*.framework; do + version_dir="${framework}/Versions/A" + [ -d "${version_dir}" ] || continue + + rm -rf "${version_dir}/Headers" "${version_dir}/PrivateHeaders" + rm -rf "${version_dir}/Modules" + rm -f "${framework}/Headers" "${framework}/PrivateHeaders" "${framework}/Modules" + + find "${version_dir}" -name '*.swiftmodule' -prune -exec rm -rf {} + 2>/dev/null || true + find "${version_dir}" \( -name '*.swiftdoc' -o -name '*.swiftsourceinfo' \) -delete 2>/dev/null || true +done + +# Re-sign each framework and the main binary ad-hoc. Removing files breaks the +# existing signature seal, which the loader rejects on arm64 macOS. +for framework in "${STAGE}"/Frameworks/*.framework; do + codesign --force --sign - --timestamp=none "${framework}" >/dev/null 2>&1 +done +codesign --force --sign - --timestamp=none "${STAGE}/bin/idb_companion" >/dev/null 2>&1 + +# Prove the stripped, re-signed layout still loads and runs. tar stores +# symlinks as symlinks so the extracted copy mirrors the embedded payload. +# --version dynamically links every framework and exits zero on success, +# which fails if a strip or re-sign broke a load command or the seal. +STRIPPED_BYTES="$(find "${STAGE}" -type f -exec stat -f%z {} + | awk '{sum += $1} END {print sum}')" + +PROOF_TAR="${PROOF}/payload.tar.gz" +tar -czf "${PROOF_TAR}" -C "${STAGE}" . +tar -xzf "${PROOF_TAR}" -C "${PROOF}" + +if ! "${PROOF}/bin/idb_companion" --version >/dev/null 2>&1; then + echo "stripped companion failed to execute" >&2 + exit 1 +fi +echo "stripped layout executes" + +mkdir -p "${ASSETS_DIR}" +tar -czf "${OUTPUT}" -C "${STAGE}" . + +SHA="$(shasum -a 256 "${OUTPUT}" | awk '{print $1}')" + +echo "wrote ${OUTPUT}" +echo "stripped uncompressed size: ${STRIPPED_BYTES} bytes" +echo "sha256: ${SHA}"