diff --git a/.gitignore b/.gitignore index 1c2162e..c81a492 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ Build/ DerivedData/ *.app +dist # Swift Package Manager .swiftpm/ diff --git a/BUILD.md b/BUILD.md new file mode 100644 index 0000000..81c7505 --- /dev/null +++ b/BUILD.md @@ -0,0 +1,245 @@ +# Build and Release (Signed + Notarized DMG) + +This project includes `scripts/build_dmg.sh` to produce a distributable macOS DMG: + +- Output: `dist/Aether-.dmg` (`Aether-universal.dmg`, `Aether-arm64.dmg`, or `Aether-x86_64.dmg`) +- Default build target: universal (`arm64` + `x86_64`) +- Signs the `.app` and `.dmg` +- Notarizes and staples both (unless `--skip-notarization`) +- Generates checksum/manifest artifacts for verification publishing + +## What "can run on any computer" means on macOS + +For normal Gatekeeper-friendly distribution to other Macs, you need: + +1. A `Developer ID Application` signing certificate +2. Apple notarization +3. Stapled notarization ticket + +Without notarization/signing, users can still sometimes run the app by bypassing security prompts, but it is not a clean "works anywhere" install experience. + +## What notarization is + +Notarization is Apple scanning your signed app/archive for malicious content. If accepted, Apple issues a ticket. When you staple that ticket to your app/DMG, macOS can validate it offline and Gatekeeper is much less likely to block first launch. + +## Prerequisites + +1. macOS with Xcode Command Line Tools +2. Apple Developer Program membership (paid) +3. Installed `Developer ID Application` certificate in your keychain +4. `xcrun notarytool` credentials stored in keychain (profile name) + +## Apple ID, Team ID, and certificate details + +### Apple ID: can this be any email? + +No. It must be the Apple Account that has access to your Apple Developer team. In practice: + +- It can be any email address format only if that email is the sign-in for an Apple Account in the developer team +- For Apple ID auth with notarytool, you also need an app-specific password +- If you are in multiple teams with the same Apple ID, `--team-id` selects which team notarytool uses + +### Team ID: what is it? + +`TEAMID` is your Apple Developer Team identifier (usually 10 uppercase alphanumeric characters), for example `ABCDE12345`. + +Use the team that owns the `Developer ID Application` certificate used to sign the app. If team/certificate/auth do not match, notarization will fail. + +You can find Team ID in: + +- App Store Connect -> Users and Access -> Membership +- developer.apple.com -> Account -> Membership + +If you are in multiple teams, use the team that issued the `Developer ID Application` certificate you pass in `APP_SIGN_IDENTITY`. + +### Signing identity used by this script + +Set `APP_SIGN_IDENTITY` to your certificate common name, for example: + +```bash +APP_SIGN_IDENTITY="Developer ID Application: Your Name (ABCDE12345)" +``` + +You can inspect available code-sign identities with: + +```bash +security find-identity -v -p codesigning +``` + +## Configure notarytool credentials locally + +Create or update a local keychain profile (example profile name: `AETHER_NOTARY`): + +```bash +xcrun notarytool store-credentials AETHER_NOTARY \ + --apple-id "you@example.com" \ + --team-id "ABCDE12345" \ + --password "" \ + --validate +``` + +Notes: + +- `--password` is an Apple app-specific password, not your Apple Account login password +- If you omit `--password`, `notarytool` prompts securely in terminal +- Running `store-credentials` again with the same profile name updates/replaces the saved credentials + +## How to update stored credentials later + +Common cases: + +1. Password rotated/revoked: rerun `store-credentials` with the same profile name +2. Switched Apple ID or Team: rerun with new values under same or new profile name +3. Multiple environments: use separate profile names (for example `AETHER_NOTARY_DEV`, `AETHER_NOTARY_CI`) + +A quick validity check is to submit a build; `store-credentials --validate` also performs a credential validation request. + +## Build command + +From repo root: + +```bash +APP_SIGN_IDENTITY="Developer ID Application: Your Name (ABCDE12345)" \ +NOTARY_PROFILE="AETHER_NOTARY" \ +scripts/build_dmg.sh --bundle-id "com.yourcompany.aether" --version "1.2.1" --arch universal +``` + +If `--version` is omitted, the script uses the latest git tag (without leading `v`) or falls back to `0.0.0`. + +By default, the script sets: + +- `CFBundleShortVersionString` from `APP_VERSION` (what macOS shows as `Version X.Y.Z`) +- `CFBundleVersion` from `APP_BUILD` (defaults to git short hash) +- `AetherBuildTimestamp` from current UTC time +- `AetherBuildCommit` from git short hash +- `AetherBuildTargetArch` from `--arch` +- `AetherLicense` from `LICENSE_NAME` (default `MIT License`) + +So About shows `Version X.Y.Z ()` instead of duplicating the same value twice. + +## Useful script options + +```bash +scripts/build_dmg.sh --help +``` + +- `--bundle-id `: CFBundleIdentifier in `Info.plist` +- `--version `: app short/build version +- `--arch `: `universal` (default), `arm64`, or `x86_64` +- `--app-only`: build/sign only `dist/Aether-.app` (no DMG/notarization) +- `--open-app`: open the resulting app bundle after build +- `--skip-notarization`: sign only, skip notary submission/stapling + +Additional env var: + +- `APP_BUILD`: explicit build info for `CFBundleVersion` (for example `a1b2c3d4` or CI build number) +- `TARGET_ARCH`: same as `--arch` +- `BUILD_TIMESTAMP`: explicit UTC timestamp in ISO8601 format +- `BUILD_COMMIT`: explicit source marker (commit, tag, or CI revision) +- `LICENSE_NAME`: license label shown in About + +## Integrity artifacts generated + +For each build, the script writes: + +- `dist/Aether-.dmg` +- `dist/Aether-.dmg.sha256` +- `dist/Aether-.app-executable.sha256` +- `dist/Aether-.build-manifest.json` + +Recommended release publishing: + +1. Publish the DMG +2. Publish the `.dmg.sha256` and `.build-manifest.json` +3. Optionally publish the executable checksum file for in-app SHA comparison + +## Resource handling in the DMG build + +`scripts/build_dmg.sh` includes resources from both: + +- SwiftPM-generated resource bundle(s) (for `Bundle.module`, e.g. `Aether_Aether.bundle`) +- Source resources under `Sources/Aether/Resources` (copied to `Contents/Resources/AetherResources`) + +It also generates `Contents/Resources/AppIcon.icns` from: + +- `Sources/Aether/Resources/Assets.xcassets/AppIcon.appiconset` + +and sets `CFBundleIconFile=AppIcon` in the app `Info.plist` so Finder/Dock can use that icon. + +You do not need to move `Sources/Aether/Resources` for this packaging flow. + +## Local dry run (no Apple account required) + +For packaging flow validation only: + +```bash +APP_SIGN_IDENTITY="-" scripts/build_dmg.sh --skip-notarization +``` + +This uses ad-hoc signing and is not suitable for public distribution. + +## Run from IntelliJ as a real `.app` bundle + +`SwiftRunPackage` runs the executable directly, not from an `.app` bundle. +That is why App-menu behavior (including About integration) can differ from installed/package builds. + +Use the helper script to run the app as a bundle locally: + +```bash +scripts/run_app_bundle.sh +``` + +This defaults to your host architecture (`arm64` on Apple Silicon, `x86_64` on Intel), builds `dist/Aether-.app`, and launches it. + +To force universal: + +```bash +scripts/run_app_bundle.sh --arch universal +``` + +IntelliJ Run Configuration (recommended): + +1. Run | Edit Configurations... +2. Add New Configuration | Shell Script +3. Name: `Aether (Run App Bundle)` +4. Script path: `$PROJECT_DIR$/scripts/run_app_bundle.sh` +5. Working directory: `$PROJECT_DIR$` +6. Run + +Optional env vars in that config: + +- `APP_VERSION=1.2.1` (or your target version) + +By default, `scripts/run_app_bundle.sh` always uses ad-hoc signing (`-`) to avoid keychain/timestamp prompts. +If you need certificate signing for local runs, pass it explicitly: + +```bash +scripts/run_app_bundle.sh --sign-identity "Developer ID Application: Your Name (ABCDE12345)" +``` + +or set: + +```bash +RUN_APP_SIGN_IDENTITY="Developer ID Application: Your Name (ABCDE12345)" scripts/run_app_bundle.sh +``` + +## Verify output + +After build: + +```bash +ls -lh dist/Aether-*.dmg +cat dist/Aether-universal.dmg.sha256 +cat dist/Aether-universal.build-manifest.json +spctl -a -vvv -t open dist/Aether-universal.dmg +``` + +You can also mount and inspect: + +```bash +hdiutil attach dist/Aether-universal.dmg +``` + +## CI note + +Apple ID + app-specific password works, but App Store Connect API key auth is often preferred for CI (`notarytool store-credentials --key ... --key-id ... --issuer ...`). diff --git a/scripts/build_dmg.sh b/scripts/build_dmg.sh new file mode 100755 index 0000000..8f5b52e --- /dev/null +++ b/scripts/build_dmg.sh @@ -0,0 +1,465 @@ +#!/usr/bin/env bash +set -euo pipefail + +APP_NAME="Aether" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" +DIST_DIR="${ROOT_DIR}/dist" +WORK_DIR="${ROOT_DIR}/.build/release-dmg" +DMG_STAGE_DIR="${WORK_DIR}/dmg-stage" +PROJECT_RESOURCES_DIR="${ROOT_DIR}/Sources/Aether/Resources" + +APP_SIGN_IDENTITY="${APP_SIGN_IDENTITY:-}" +NOTARY_PROFILE="${NOTARY_PROFILE:-}" +SKIP_NOTARIZATION="${SKIP_NOTARIZATION:-0}" +TARGET_ARCH="${TARGET_ARCH:-universal}" +BUNDLE_ID="${BUNDLE_ID:-com.aether.app}" +APP_VERSION="${APP_VERSION:-}" +APP_BUILD="${APP_BUILD:-}" +BUILD_TIMESTAMP="${BUILD_TIMESTAMP:-}" +BUILD_COMMIT="${BUILD_COMMIT:-}" +LICENSE_NAME="${LICENSE_NAME:-MIT License}" +MIN_MACOS_VERSION="${MIN_MACOS_VERSION:-14.0}" +DMG_VOLUME_NAME="${DMG_VOLUME_NAME:-Aether}" +APP_ONLY="${APP_ONLY:-0}" +OPEN_APP="${OPEN_APP:-0}" + +usage() { + cat <.dmg. + +Options: + --version App version (default: latest git tag or 0.0.0) + --arch universal (default), arm64, x86_64 + --bundle-id Bundle identifier (default: com.aether.app) + --app-only Build/sign app bundle only (no DMG/notarization) + --open-app Open resulting app bundle when done + --skip-notarization Skip notarization/stapling + -h, --help Show this help + +Required env vars (unless --app-only is used): + APP_SIGN_IDENTITY Developer ID Application identity for codesign + +Required unless --skip-notarization is used: + NOTARY_PROFILE Keychain profile configured via xcrun notarytool store-credentials + +Optional env vars: + APP_VERSION Same as --version + APP_BUILD CFBundleVersion (default: git short hash, fallback APP_VERSION) + BUILD_TIMESTAMP ISO8601 UTC timestamp (default: current UTC time) + BUILD_COMMIT Source revision marker (default: git short hash) + LICENSE_NAME License label shown in About window (default: MIT License) + TARGET_ARCH Same as --arch + BUNDLE_ID Same as --bundle-id + MIN_MACOS_VERSION Defaults to 14.0 + DMG_VOLUME_NAME Defaults to Aether +USAGE +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --version) + APP_VERSION="$2" + shift 2 + ;; + --arch) + TARGET_ARCH="$2" + shift 2 + ;; + --bundle-id) + BUNDLE_ID="$2" + shift 2 + ;; + --app-only) + APP_ONLY=1 + shift + ;; + --open-app) + OPEN_APP=1 + shift + ;; + --skip-notarization) + SKIP_NOTARIZATION=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown argument: $1" >&2 + usage >&2 + exit 1 + ;; + esac +done + +require_cmd() { + command -v "$1" >/dev/null 2>&1 || { + echo "Missing required command: $1" >&2 + exit 1 + } +} + +generate_app_icon_icns() { + local app_iconset_src="$1" + local app_resources_dir="$2" + local iconset_dir="${WORK_DIR}/AppIcon.iconset" + local base_png="${app_iconset_src}/icon_1024.png" + + if [[ ! -d "${app_iconset_src}" ]]; then + echo "Warning: App iconset not found at ${app_iconset_src}" >&2 + return 0 + fi + + if [[ ! -f "${base_png}" ]]; then + base_png="$(find "${app_iconset_src}" -maxdepth 1 -type f -name '*.png' -print | head -n 1 || true)" + fi + + if [[ -z "${base_png}" || ! -f "${base_png}" ]]; then + echo "Warning: no PNG files found in ${app_iconset_src}" >&2 + return 0 + fi + + rm -rf "${iconset_dir}" + mkdir -p "${iconset_dir}" + + sips -z 16 16 "${base_png}" --out "${iconset_dir}/icon_16x16.png" >/dev/null + sips -z 32 32 "${base_png}" --out "${iconset_dir}/icon_16x16@2x.png" >/dev/null + sips -z 32 32 "${base_png}" --out "${iconset_dir}/icon_32x32.png" >/dev/null + sips -z 64 64 "${base_png}" --out "${iconset_dir}/icon_32x32@2x.png" >/dev/null + sips -z 128 128 "${base_png}" --out "${iconset_dir}/icon_128x128.png" >/dev/null + sips -z 256 256 "${base_png}" --out "${iconset_dir}/icon_128x128@2x.png" >/dev/null + sips -z 256 256 "${base_png}" --out "${iconset_dir}/icon_256x256.png" >/dev/null + sips -z 512 512 "${base_png}" --out "${iconset_dir}/icon_256x256@2x.png" >/dev/null + sips -z 512 512 "${base_png}" --out "${iconset_dir}/icon_512x512.png" >/dev/null + cp "${base_png}" "${iconset_dir}/icon_512x512@2x.png" + + iconutil -c icns "${iconset_dir}" -o "${app_resources_dir}/AppIcon.icns" +} + +log() { + printf '\n[%s] %s\n' "$(date +'%H:%M:%S')" "$*" +} + +resolve_release_dir() { + local arch="$1" + local path + + path="$(find "${ROOT_DIR}/.build" -type d -path "*/${arch}-apple-macosx*/release" -print | head -n 1 || true)" + if [[ -z "${path}" ]]; then + echo "Could not locate release directory for architecture: ${arch}" >&2 + return 1 + fi + + printf '%s\n' "${path}" +} + +if [[ "${APP_ONLY}" == "1" ]]; then + SKIP_NOTARIZATION=1 +fi + +if [[ -z "${APP_SIGN_IDENTITY}" ]]; then + if [[ "${APP_ONLY}" == "1" ]]; then + APP_SIGN_IDENTITY="-" + else + echo "APP_SIGN_IDENTITY is required." >&2 + exit 1 + fi +fi + +if [[ "${TARGET_ARCH}" != "universal" && "${TARGET_ARCH}" != "arm64" && "${TARGET_ARCH}" != "x86_64" ]]; then + echo "Invalid --arch value: ${TARGET_ARCH}. Expected one of: universal, arm64, x86_64" >&2 + exit 1 +fi + +if [[ "${SKIP_NOTARIZATION}" != "1" && -z "${NOTARY_PROFILE}" ]]; then + echo "NOTARY_PROFILE is required unless --skip-notarization is used." >&2 + exit 1 +fi + +if [[ -z "${APP_VERSION}" ]]; then + APP_VERSION="$(git -C "${ROOT_DIR}" describe --tags --abbrev=0 2>/dev/null || true)" + APP_VERSION="${APP_VERSION#v}" +fi + +if [[ -z "${APP_VERSION}" ]]; then + APP_VERSION="0.0.0" +fi + +if [[ -z "${APP_BUILD}" ]]; then + APP_BUILD="$(git -C "${ROOT_DIR}" rev-parse --short=8 HEAD 2>/dev/null || true)" +fi + +if [[ -z "${APP_BUILD}" ]]; then + APP_BUILD="${APP_VERSION}" +fi + +if [[ -z "${BUILD_TIMESTAMP}" ]]; then + BUILD_TIMESTAMP="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" +fi + +if [[ -z "${BUILD_COMMIT}" ]]; then + BUILD_COMMIT="$(git -C "${ROOT_DIR}" rev-parse --short=12 HEAD 2>/dev/null || true)" +fi + +if [[ -z "${BUILD_COMMIT}" ]]; then + BUILD_COMMIT="${APP_BUILD}" +fi + +require_cmd swift +require_cmd lipo +require_cmd codesign +require_cmd sips +require_cmd iconutil +if [[ "${APP_ONLY}" != "1" ]]; then + require_cmd hdiutil +fi +if [[ "${SKIP_NOTARIZATION}" != "1" ]]; then + require_cmd ditto + require_cmd xcrun +fi +if [[ "${OPEN_APP}" == "1" ]]; then + require_cmd open +fi + +log "Cleaning previous artifacts" +rm -rf "${WORK_DIR}" "${DIST_DIR}" +mkdir -p "${WORK_DIR}" "${DIST_DIR}" + +log "Build metadata: version=${APP_VERSION}, build=${APP_BUILD}, commit=${BUILD_COMMIT}, arch=${TARGET_ARCH}" + +APP_DIR="${WORK_DIR}/${APP_NAME}.app" +mkdir -p "${APP_DIR}/Contents/MacOS" "${APP_DIR}/Contents/Resources" + +RESOURCE_RELEASE_DIR="" +if [[ "${TARGET_ARCH}" == "universal" ]]; then + log "Building release binaries (arm64 + x86_64)" + swift build --package-path "${ROOT_DIR}" -c release --arch arm64 + swift build --package-path "${ROOT_DIR}" -c release --arch x86_64 + + ARM_RELEASE_DIR="$(resolve_release_dir arm64)" + X86_RELEASE_DIR="$(resolve_release_dir x86_64)" + ARM_BINARY="${ARM_RELEASE_DIR}/${APP_NAME}" + X86_BINARY="${X86_RELEASE_DIR}/${APP_NAME}" + + if [[ ! -f "${ARM_BINARY}" ]]; then + echo "Missing arm64 binary: ${ARM_BINARY}" >&2 + exit 1 + fi + + if [[ ! -f "${X86_BINARY}" ]]; then + echo "Missing x86_64 binary: ${X86_BINARY}" >&2 + exit 1 + fi + + log "Creating universal executable" + lipo -create "${ARM_BINARY}" "${X86_BINARY}" -output "${APP_DIR}/Contents/MacOS/${APP_NAME}" + RESOURCE_RELEASE_DIR="${ARM_RELEASE_DIR}" +else + log "Building release binary (${TARGET_ARCH})" + swift build --package-path "${ROOT_DIR}" -c release --arch "${TARGET_ARCH}" + RELEASE_DIR="$(resolve_release_dir "${TARGET_ARCH}")" + ARCH_BINARY="${RELEASE_DIR}/${APP_NAME}" + + if [[ ! -f "${ARCH_BINARY}" ]]; then + echo "Missing ${TARGET_ARCH} binary: ${ARCH_BINARY}" >&2 + exit 1 + fi + + cp "${ARCH_BINARY}" "${APP_DIR}/Contents/MacOS/${APP_NAME}" + RESOURCE_RELEASE_DIR="${RELEASE_DIR}" +fi + +chmod 755 "${APP_DIR}/Contents/MacOS/${APP_NAME}" + +log "Copying SwiftPM resource bundles" +found_bundle=0 +while IFS= read -r -d '' bundle_path; do + cp -R "${bundle_path}" "${APP_DIR}/Contents/Resources/" + found_bundle=1 +done < <(find "${RESOURCE_RELEASE_DIR}" -maxdepth 1 -type d -name "*.bundle" -print0) + +if [[ "${found_bundle}" -eq 0 ]]; then + echo "Warning: no .bundle resources found in ${RESOURCE_RELEASE_DIR}" >&2 +fi + +if [[ -d "${PROJECT_RESOURCES_DIR}" ]]; then + log "Copying source resources from ${PROJECT_RESOURCES_DIR}" + mkdir -p "${APP_DIR}/Contents/Resources/AetherResources" + cp -R "${PROJECT_RESOURCES_DIR}/." "${APP_DIR}/Contents/Resources/AetherResources/" +fi + +if [[ -f "${ROOT_DIR}/LICENSE" ]]; then + cp "${ROOT_DIR}/LICENSE" "${APP_DIR}/Contents/Resources/LICENSE.txt" +fi + +log "Generating AppIcon.icns from source iconset (if available)" +generate_app_icon_icns \ + "${PROJECT_RESOURCES_DIR}/Assets.xcassets/AppIcon.appiconset" \ + "${APP_DIR}/Contents/Resources" + +log "Writing Info.plist" +cat > "${APP_DIR}/Contents/Info.plist" < + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${APP_NAME} + CFBundleIdentifier + ${BUNDLE_ID} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${APP_NAME} + CFBundlePackageType + APPL + CFBundleIconFile + AppIcon + CFBundleShortVersionString + ${APP_VERSION} + CFBundleVersion + ${APP_BUILD} + AetherBuildTimestamp + ${BUILD_TIMESTAMP} + AetherBuildTargetArch + ${TARGET_ARCH} + AetherBuildCommit + ${BUILD_COMMIT} + AetherLicense + ${LICENSE_NAME} + LSMinimumSystemVersion + ${MIN_MACOS_VERSION} + NSHighResolutionCapable + + NSPrincipalClass + NSApplication + + +PLIST + +log "Signing app bundle" +if [[ "${APP_ONLY}" == "1" ]]; then + # Local run path: avoid timestamp/runtime requirements that can block on keychain/network. + codesign --force --sign "${APP_SIGN_IDENTITY}" "${APP_DIR}" +else + codesign --force --timestamp --options runtime --sign "${APP_SIGN_IDENTITY}" "${APP_DIR}" +fi +codesign --verify --deep --strict --verbose=2 "${APP_DIR}" + +if [[ "${SKIP_NOTARIZATION}" != "1" ]]; then + APP_ZIP="${WORK_DIR}/${APP_NAME}.zip" + log "Creating zip for app notarization" + ditto -c -k --keepParent "${APP_DIR}" "${APP_ZIP}" + + log "Submitting app for notarization" + xcrun notarytool submit "${APP_ZIP}" --keychain-profile "${NOTARY_PROFILE}" --wait + + log "Stapling notarization ticket to app" + xcrun stapler staple "${APP_DIR}" + xcrun stapler validate "${APP_DIR}" +fi + +FINAL_APP="${DIST_DIR}/${APP_NAME}-${TARGET_ARCH}.app" +rm -rf "${FINAL_APP}" +cp -R "${APP_DIR}" "${FINAL_APP}" + +APP_EXEC_SHA256="$(shasum -a 256 "${APP_DIR}/Contents/MacOS/${APP_NAME}" | awk '{print $1}')" +APP_SHA_FILE="${DIST_DIR}/${APP_NAME}-${TARGET_ARCH}.app-executable.sha256" +printf '%s %s\n' "${APP_EXEC_SHA256}" "${APP_NAME}.app/Contents/MacOS/${APP_NAME}" > "${APP_SHA_FILE}" + +if [[ "${APP_ONLY}" == "1" ]]; then + MANIFEST_FILE="${DIST_DIR}/${APP_NAME}-${TARGET_ARCH}.build-manifest.json" + cat > "${MANIFEST_FILE}" < "${APP_SHA_FILE}" +printf '%s %s\n' "${DMG_SHA256}" "$(basename "${FINAL_DMG}")" > "${DMG_SHA_FILE}" + +cat > "${MANIFEST_FILE}" <] + +Builds Aether.app as a bundle (no DMG), then launches it. + +Options: + --arch arm64, x86_64, or universal + --sign-identity Override signing identity for local run (default: -) + -h, --help Show this help + +Examples: + scripts/run_app_bundle.sh + scripts/run_app_bundle.sh --arch universal + scripts/run_app_bundle.sh --sign-identity "Developer ID Application: Your Name (TEAMID)" + scripts/run_app_bundle.sh -- --version 1.2.1 +USAGE +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --arch) + TARGET_ARCH="$2" + shift 2 + ;; + --sign-identity) + SIGN_IDENTITY="$2" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + --) + shift + EXTRA_ARGS+=("$@") + break + ;; + *) + EXTRA_ARGS+=("$1") + shift + ;; + esac +done + +if [[ -z "${TARGET_ARCH}" ]]; then + host_arch="$(uname -m)" + case "${host_arch}" in + arm64|x86_64) + TARGET_ARCH="${host_arch}" + ;; + *) + TARGET_ARCH="universal" + ;; + esac +fi + +cd "${ROOT_DIR}" +APP_SIGN_IDENTITY="${SIGN_IDENTITY}" \ + "${SCRIPT_DIR}/build_dmg.sh" \ + --app-only \ + --open-app \ + --arch "${TARGET_ARCH}" \ + "${EXTRA_ARGS[@]}"