cat / goq
clone

turns a linux box into a private steam streaming appliance: plug in, tap your key, play from another computer

fix/architecture-review-findings
goq / scripts / package-macos-client.sh
package-macos-client.sh 7.9K executable
1#!/usr/bin/env bash23set -euo pipefail4umask 07756usage() {7 cat <<'EOF'8Usage: scripts/package-macos-client.sh --release-tag vVERSION9 --output-dir /absolute/path1011Build and verify a distributable macOS DMG. This gate requires:1213 - a clean worktree;14 - a release tag resolving exactly to HEAD and matching every package version;15 - an Apple Silicon runner and the aarch64-apple-darwin Rust target;16 - APPLE_SIGNING_IDENTITY naming a Developer ID Application identity;17 - Tauri notarization credentials (App Store Connect API or Apple ID);18 - strict code-signing, hardened runtime, Gatekeeper, and stapling checks.1920The normal release is built without optional Cargo features. Development21features, including demo-direct-node and22experimental-non-macos-pointer-capture, and ad-hoc signatures are23intentionally not accepted by this command.24EOF25}2627die() {28 printf 'macOS package failed: %s\n' "$*" >&229 exit 130}3132script_dir="$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd -P)"33repo_dir="$(CDPATH='' cd -- "$script_dir/.." && pwd -P)"34output_dir=""35release_tag=""3637while [[ $# -gt 0 ]]; do38 case "$1" in39 --output-dir)40 [[ $# -ge 2 ]] || die "--output-dir requires a path"41 output_dir="$2"42 shift 243 ;;44 --release-tag)45 [[ $# -ge 2 ]] || die "--release-tag requires a tag"46 release_tag="$2"47 shift 248 ;;49 --help|-h)50 usage51 exit 052 ;;53 *) die "unknown argument: $1" ;;54 esac55done5657[[ "$(uname -s)" == Darwin ]] || die "this package must be built on macOS"58[[ "$(uname -m)" == arm64 ]] || die "the first public Portal release is macOS arm64 only"59[[ -n "$output_dir" && "$output_dir" == /* ]] || die "--output-dir must be absolute"60[[ -n "$release_tag" ]] || die "--release-tag is required"61[[ -d "$output_dir" ]] || die "output directory does not exist"62output_dir="$(CDPATH='' cd -- "$output_dir" && pwd -P)"63case "$output_dir/" in64 "$repo_dir/"*) die "--output-dir must be outside the source repository" ;;65esac66if find "$output_dir" -mindepth 1 -maxdepth 1 -print -quit | grep -q .; then67 die "output directory must be empty"68fi6970for command in cargo codesign find git grep hdiutil lipo plutil python3 rustup security sed shasum spctl xcrun; do71 command -v "$command" >/dev/null 2>&1 || die "$command is required"72done7374verifier="$script_dir/verify-portal-release.py"75[[ -f "$verifier" ]] || die "Portal release verifier is missing"76python3 "$verifier" source --repo-dir "$repo_dir" --release-tag "$release_tag" >/dev/null77rustup target list --installed | grep -Fxq aarch64-apple-darwin \78 || die "the aarch64-apple-darwin Rust target is required"7980signing_identity="${APPLE_SIGNING_IDENTITY:-}"81[[ "$signing_identity" == Developer\ ID\ Application:* ]] \82 || die "APPLE_SIGNING_IDENTITY must name a Developer ID Application identity"83security find-identity -v -p codesigning | grep -Fq "\"$signing_identity\"" \84 || die "APPLE_SIGNING_IDENTITY is not available in the keychain"85expected_team_id_file="$repo_dir/release/portal-apple-team-id.txt"86[[ -f "$expected_team_id_file" && ! -L "$expected_team_id_file" ]] \87 || die "committed Portal Apple team identifier is missing or unsafe"88expected_team_id="$(<"$expected_team_id_file")"89[[ "$expected_team_id" =~ ^[A-Z0-9]{10}$ ]] \90 || die "release/portal-apple-team-id.txt must contain one Apple TeamIdentifier"91[[ "${APPLE_TEAM_ID:-}" == "$expected_team_id" ]] \92 || die "APPLE_TEAM_ID does not match the committed Portal signer"9394api_credentials=false95apple_id_credentials=false96if [[ -n "${APPLE_API_ISSUER:-}" && -n "${APPLE_API_KEY:-}" && -n "${APPLE_API_KEY_PATH:-}" ]]; then97 [[ "$APPLE_API_KEY_PATH" == /* && -f "$APPLE_API_KEY_PATH" ]] \98 || die "APPLE_API_KEY_PATH must name an absolute private-key file"99 api_credentials=true100fi101if [[ -n "${APPLE_ID:-}" && -n "${APPLE_PASSWORD:-}" && -n "${APPLE_TEAM_ID:-}" ]]; then102 apple_id_credentials=true103fi104$api_credentials || $apple_id_credentials \105 || die "set complete App Store Connect API or Apple ID notarization credentials"106107# shellcheck source=/dev/null108source "$HOME/.cargo/env"109(110 cd "$repo_dir"111 cargo tauri build --locked --ci --target aarch64-apple-darwin --bundles app,dmg112)113114bundle_root="$repo_dir/target/aarch64-apple-darwin/release/bundle"115app_path="$bundle_root/macos/Portal.app"116[[ -d "$app_path" ]] || die "Tauri app bundle is missing"117dmg_count="$(find "$bundle_root/dmg" -maxdepth 1 -type f -name '*.dmg' -print | wc -l | tr -d ' ')"118[[ "$dmg_count" -eq 1 ]] || die "expected exactly one DMG artifact"119dmg_path="$(find "$bundle_root/dmg" -maxdepth 1 -type f -name '*.dmg' -print)"120121hdiutil verify "$dmg_path"122codesign --verify --deep --strict --verbose=2 "$app_path"123signature_details="$(codesign -d --verbose=4 "$app_path" 2>&1)"124grep -Fq "Authority=$signing_identity" <<<"$signature_details" \125 || die "application is not signed by the configured Developer ID identity"126team_identifier="$(sed -n 's/^TeamIdentifier=//p' <<<"$signature_details")"127[[ "$team_identifier" == "$expected_team_id" ]] \128 || die "application TeamIdentifier does not match the committed Portal signer"129grep -Eq '^flags=.*runtime' <<<"$signature_details" \130 || die "application signature does not enable hardened runtime"131spctl --assess --type execute --verbose=4 "$app_path"132xcrun stapler validate "$app_path"133xcrun stapler validate "$dmg_path"134spctl --assess --type open --context context:primary-signature --verbose=4 "$dmg_path"135136executable="$app_path/Contents/MacOS/portal"137[[ -x "$executable" ]] || die "client executable is missing"138architectures="$(lipo -archs "$executable")"139[[ "$architectures" == arm64 ]] || die "client executable must contain only arm64"140identifier="$(plutil -extract CFBundleIdentifier raw "$app_path/Contents/Info.plist")"141[[ "$identifier" == sh.goq.portal ]] || die "unexpected bundle identifier: $identifier"142if find "$app_path" -type f \( -name sigil -o -name sigil-host -o -name sigil-probe -o -name host.toml -o -name '*.key' \) \143 | grep -q .144then145 die "client bundle contains a forbidden host or credential artifact"146fi147148version="$(plutil -extract CFBundleShortVersionString raw "$app_path/Contents/Info.plist")"149[[ "$release_tag" == "v$version" ]] || die "app version does not match release tag"150git_commit="$(git -C "$repo_dir" rev-parse --verify HEAD)"151artifact_name="Portal-${version}-arm64.dmg"152artifact_path="$output_dir/$artifact_name"153manifest_path="$output_dir/Portal-${version}-arm64.json"154[[ ! -e "$artifact_path" && ! -e "$manifest_path" && ! -e "$artifact_path.sha256" ]] \155 || die "release output already exists"156install -m 0644 "$dmg_path" "$artifact_path"157artifact_sha256="$(shasum -a 256 "$artifact_path" | awk '{print $1}')"158printf '%s %s\n' "$artifact_sha256" "$artifact_name" >"$artifact_path.sha256"159python3 - "$manifest_path" "$release_tag" "$version" "$git_commit" "$artifact_name" "$artifact_sha256" <<'PY'160import json161import pathlib162import sys163164path, release_tag, version, git_commit, asset, digest = sys.argv[1:]165manifest = {166 "architecture": "arm64",167 "architectures": ["arm64"],168 "asset": asset,169 "bundle_identifier": "sh.goq.portal",170 "checksum_asset": f"{asset}.sha256",171 "demo_direct_node": False,172 "developer_id_signed": True,173 "format": 1,174 "gatekeeper_verified": True,175 "git_commit": git_commit,176 "hardened_runtime": True,177 "notarized": True,178 "platform": "macos",179 "product": "portal-client",180 "release_tag": release_tag,181 "sha256": digest,182 "stapled": True,183 "version": version,184}185pathlib.Path(path).write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n")186PY187chmod 0644 "$manifest_path"188python3 "$verifier" assets \189 --repo-dir "$repo_dir" \190 --release-tag "$release_tag" \191 --asset-dir "$output_dir" >/dev/null192193printf 'client_package=%s\n' "$artifact_path"194printf 'client_package_sha256=%s\n' "$artifact_sha256"195printf 'client_architectures=%s\n' "$architectures"196printf 'client_release_tag=%s\n' "$release_tag"197printf 'client_git_commit=%s\n' "$git_commit"198printf 'client_signing=developer-id-hardened-runtime\n'199printf 'client_notarization=stapled-and-validated\n'