#!/usr/bin/env bash # Chanora macOS build helper. # # Run this on a macOS host that has already completed §4 of # docs/release/macos-build.md: # - Xcode 26+ with Command Line Tools # - Rust stable with aarch64-apple-darwin and/or x86_64-apple-darwin targets # - Flutter 3.41.9+ stable on PATH # - flutter_rust_bridge_codegen 2.12.0 (only if regenerating bindings) # # Usage (from the repo root ~/chanora): # ./tools/build-macos.sh # ./tools/build-macos.sh --no-rust # skip Rust rebuild (cached) # ./tools/build-macos.sh --regenerate-bindings # rerun FRB codegen first # ./tools/build-macos.sh --version v0.2.0-beta.1 # ./tools/build-macos.sh --target aarch64-apple-darwin # # Produces: # target//release/libchanora_bridge.dylib # apps/chanora_flutter/build/macos/Build/Products/Release/chanora_flutter.app # chanora--macos-.zip (release bundle) set -euo pipefail VERSION="v0.2.0-beta.1" SKIP_RUST=0 REGEN=0 TARGET="$(rustc -vV | grep '^host:' | cut -d' ' -f2)" while [[ $# -gt 0 ]]; do case "$1" in --version) VERSION="$2"; shift 2;; --no-rust) SKIP_RUST=1; shift;; --regenerate-bindings) REGEN=1; shift;; --target) TARGET="$2"; shift 2;; *) echo "unknown arg: $1" >&2; exit 2;; esac done cd "$(dirname "$0")/.." REPO_ROOT="$(pwd)" bar() { printf '%s\n' '========================================================================'; } ARCH="${TARGET%%-*}" bar echo "Chanora macOS build" echo "Repo root : $REPO_ROOT" echo "Version : $VERSION" echo "Target : $TARGET" echo "Arch : $ARCH" echo "Build Rust: $([ "$SKIP_RUST" = 1 ] && echo skip || echo yes)" bar # ---------- 1. Verify toolchain ---------- echo "[1/7] Verify toolchain" for cmd in cargo rustc flutter xcodebuild xcrun codesign install_name_tool; do if ! command -v "$cmd" >/dev/null; then echo " ERROR: '$cmd' not on PATH. See docs/release/macos-build.md." >&2 exit 1 fi printf " %-20s : %s\n" "$cmd" "$($cmd --version 2>&1 | head -1)" done # ---------- 2. Verify Rust target ---------- if [[ "$TARGET" != "$(rustc -vV | grep '^host:' | cut -d' ' -f2)" ]]; then if ! rustup target list --installed | grep -q "^$TARGET$"; then echo " Adding rustup target $TARGET..." rustup target add "$TARGET" fi else echo "[2/7] Rust target $TARGET is the host triple (already available)" fi # ---------- 3. Add macOS platform if missing ---------- FLUTTER_APP="$REPO_ROOT/apps/chanora_flutter" if [[ ! -d "$FLUTTER_APP/macos" ]]; then echo "[3/7] Add macOS platform to apps/chanora_flutter" (cd "$FLUTTER_APP" && flutter create --platforms=macos .) else echo "[3/7] macOS platform already present in apps/chanora_flutter" fi # ---------- 4. (Optional) regenerate FRB bindings ---------- if [[ $REGEN -eq 1 ]]; then echo "[4/7] Regenerate flutter_rust_bridge bindings" if ! command -v flutter_rust_bridge_codegen >/dev/null; then echo " flutter_rust_bridge_codegen not on PATH; install it first." >&2 exit 1 fi flutter_rust_bridge_codegen generate else echo "[4/7] Skipping FRB codegen (use --regenerate-bindings if api.rs changed)" fi # ---------- 5. Build the Rust cdylib ---------- DYLIB="$REPO_ROOT/target/$TARGET/release/libchanora_bridge.dylib" if [[ $SKIP_RUST -eq 1 ]]; then echo "[5/7] Skipping Rust build (cached)" if [[ ! -f "$DYLIB" ]]; then echo " ERROR: --no-rust but $DYLIB not found. Run without --no-rust first." >&2 exit 1 fi else echo "[5/7] cargo build --release -p chanora_bridge --target $TARGET" export CMAKE_POLICY_VERSION_MINIMUM=3.5 cargo build --release --target "$TARGET" -p chanora_bridge fi echo " Bridge dylib: $DYLIB" if [[ ! -f "$DYLIB" ]]; then echo " ERROR: $DYLIB not found after build." >&2 exit 1 fi # ---------- 6. flutter build macos --release ---------- echo "[6/7] flutter build macos --release" cd "$FLUTTER_APP" flutter pub get flutter build macos --release APP="$FLUTTER_APP/build/macos/Build/Products/Release/chanora_flutter.app" if [[ ! -d "$APP" ]]; then echo " ERROR: $APP not found after flutter build." >&2 exit 1 fi # Wrap the bridge dylib into a proper .framework bundle, rewrite its # LC_ID_DYLIB, and ad-hoc codesign — same steps as macos-postbuild.sh. FW="$APP/Contents/Frameworks/chanora_bridge.framework" echo " Wrapping bridge into chanora_bridge.framework" rm -rf "$FW" mkdir -p "$FW/Versions/A/Resources" cp "$DYLIB" "$FW/Versions/A/chanora_bridge" cat > "$FW/Versions/A/Resources/Info.plist" << 'PLIST' CFBundleExecutable chanora_bridge CFBundleIdentifier app.chanora.bridge CFBundleName chanora_bridge CFBundlePackageType FMWK CFBundleShortVersionString 1.0.0 CFBundleVersion 1 PLIST ln -sf A "$FW/Versions/Current" ln -sf Versions/Current/chanora_bridge "$FW/chanora_bridge" ln -sf Versions/Current/Resources "$FW/Resources" echo " install_name_tool -id @rpath/chanora_bridge.framework/chanora_bridge" install_name_tool \ -id "@rpath/chanora_bridge.framework/chanora_bridge" \ "$FW/Versions/A/chanora_bridge" # Drop a stale plain dylib if a previous build left one alongside the framework. rm -f "$APP/Contents/Frameworks/libchanora_bridge.dylib" echo " codesigning framework + app (ad-hoc)" codesign --force -s - --timestamp=none "$FW/Versions/A/chanora_bridge" codesign --force -s - --timestamp=none "$FW" codesign --force --deep -s - --timestamp=none "$APP" codesign --verify --deep --strict "$APP" echo " App bundle : $APP" # ---------- 7. Package the bundle ---------- echo "[7/7] Package release zip" cd "$REPO_ROOT" ZIP_NAME="chanora-$VERSION-macos-$ARCH.zip" ZIP_PATH="$REPO_ROOT/$ZIP_NAME" rm -f "$ZIP_PATH" # zip the .app bundle from inside the Products directory so the archive # contains chanora_flutter.app/ at the top level. (cd "$FLUTTER_APP/build/macos/Build/Products/Release" && \ zip -r -q "$ZIP_PATH" chanora_flutter.app) echo " Zip : $ZIP_PATH" echo " Size : $(du -h "$ZIP_PATH" | cut -f1)" shasum -a 256 "$ZIP_PATH" bar echo "macOS build complete." bar echo "Artefacts:" echo " Rust lib:" echo " target/$TARGET/release/libchanora_bridge.dylib" echo " App bundle:" echo " $APP" echo " Zip:" echo " $ZIP_PATH"