From a0db0d0a8ea6ecef7c35a254525a033d184a53ca Mon Sep 17 00:00:00 2001 From: jess Date: Sat, 9 May 2026 04:40:04 -0700 Subject: [PATCH] Okay, it is posted for review on the app store. so here marks that line. Coming for you next, android --- macos/Info.plist | 14 ++- scripts/macos/generate-icons.sh | 64 ++++++++++ scripts/macos/release.sh | 206 ++++++++++++++++++++++++++++++++ xtask/src/main.rs | 1 + 4 files changed, 281 insertions(+), 4 deletions(-) create mode 100755 scripts/macos/generate-icons.sh create mode 100755 scripts/macos/release.sh diff --git a/macos/Info.plist b/macos/Info.plist index 4b4c816..37b4a7e 100644 --- a/macos/Info.plist +++ b/macos/Info.plist @@ -5,17 +5,21 @@ CFBundleExecutable yr_crystals CFBundleIdentifier - org.else-if.yrcrystals + org.else-if.yrxtals CFBundleName - Yr Xtals + YrXtals CFBundleDisplayName Yr Xtals CFBundlePackageType APPL + CFBundleSupportedPlatforms + + MacOSX + CFBundleVersion - 0.1.0 + 1.0.0 CFBundleShortVersionString - 0.1.0 + 1.0.0 LSMinimumSystemVersion 14.0 LSApplicationCategoryType @@ -28,6 +32,8 @@ NSSupportsSuddenTermination + ITSAppUsesNonExemptEncryption + NSMicrophoneUsageDescription Yr Xtals does not record audio. This entry is here only because the audio framework asks for it on some macOS versions. diff --git a/scripts/macos/generate-icons.sh b/scripts/macos/generate-icons.sh new file mode 100755 index 0000000..680ee49 --- /dev/null +++ b/scripts/macos/generate-icons.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +SVG="$ROOT/assets/Icon.svg" +OUT="${1:-$ROOT/build/AppIcon.icns}" + +if [ ! -f "$SVG" ]; then + echo "ERROR: $SVG not found" >&2 + exit 1 +fi + +if ! command -v rsvg-convert >/dev/null 2>&1; then + echo "ERROR: rsvg-convert not on PATH (brew install librsvg)" >&2 + exit 1 +fi + +if ! command -v iconutil >/dev/null 2>&1; then + echo "ERROR: iconutil not on PATH (macOS only)" >&2 + exit 1 +fi + +WORK_SVG="$(mktemp -t icon-svg.XXXXXX)" +ICONSET="$(mktemp -d -t iconset.XXXXXX)/AppIcon.iconset" +mkdir -p "$ICONSET" +trap 'rm -rf "$WORK_SVG" "$(dirname "$ICONSET")"' EXIT + +# pulls the gradient labeled #bg-color and paints it across the whole viewBox under the existing artwork. +BG_FLAG=() +if grep -q 'id="bg-color"' "$SVG"; then + VIEWBOX="$(grep -oE 'viewBox="[^"]+"' "$SVG" | head -1 | sed -E 's/viewBox="//;s/"$//')" + read -r VBX VBY VBW VBH <<<"$VIEWBOX" + INJECT="" + awk -v inj="$INJECT" '/<\/defs>/ {print; print inj; next} {print}' "$SVG" > "$WORK_SVG" + echo "Injected #bg-color full-canvas fill" +else + cp "$SVG" "$WORK_SVG" + BG_FLAG=(--background-color="${YRXTALS_ICON_BG:-black}") + echo "No #bg-color id; falling back to solid ${YRXTALS_ICON_BG:-black}" +fi + +ICNS_SIZES=( + "16x16 16" + "16x16@2x 32" + "32x32 32" + "32x32@2x 64" + "128x128 128" + "128x128@2x 256" + "256x256 256" + "256x256@2x 512" + "512x512 512" + "512x512@2x 1024" +) + +for entry in "${ICNS_SIZES[@]}"; do + name="${entry%% *}" + size="${entry##* }" + rsvg-convert "${BG_FLAG[@]}" --width="$size" --height="$size" "$WORK_SVG" -o "$ICONSET/icon_${name}.png" +done + +mkdir -p "$(dirname "$OUT")" +iconutil -c icns "$ICONSET" -o "$OUT" + +echo "Wrote $OUT" diff --git a/scripts/macos/release.sh b/scripts/macos/release.sh new file mode 100755 index 0000000..f1032b7 --- /dev/null +++ b/scripts/macos/release.sh @@ -0,0 +1,206 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +cd "$ROOT" + +case "$(uname -s)" in + Darwin) ;; + *) echo "wrong platform: $(uname -s), macOS release requires macOS" >&2; exit 1;; +esac + +CARGO_PROFILE="release" + +PROFILE="${YRXTALS_MACOS_APPSTORE_PROFILE:-/Volumes/External/prvProfiles/Yr_Xtals_MacOS.provisionprofile}" +if [ ! -f "$PROFILE" ]; then + echo "ERROR: Mac App Store provisioning profile not found at $PROFILE" >&2 + echo " set YRXTALS_MACOS_APPSTORE_PROFILE to override" >&2 + exit 1 +fi + +# pins an exact VER=x.y.z when set; otherwise the post-build BUMP step handles the next. +if [ -n "${VER:-}" ]; then + if ! [[ "$VER" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "ERROR: VER must be x.y.z (got '$VER')" >&2 + exit 1 + fi + /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VER" "$ROOT/macos/Info.plist" + /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $VER" "$ROOT/macos/Info.plist" + echo "Pinned macos/Info.plist version to $VER" +fi + +BUILD="$ROOT/build" +APP="$BUILD/macos-release/Yr Xtals.app" +CONTENTS="$APP/Contents" +MACOS_DIR="$CONTENTS/MacOS" +RESOURCES="$CONTENTS/Resources" +PKG="$BUILD/macos-release/YrXtals.pkg" + +export MACOSX_DEPLOYMENT_TARGET=14.0 + +echo "Building Rust binary (profile=$CARGO_PROFILE)..." +cargo build --profile "$CARGO_PROFILE" --bin yr_crystals + +BIN="/tmp/yr_crystals-target/$CARGO_PROFILE/yr_crystals" +if [ ! -f "$BIN" ]; then + echo "ERROR: yr_crystals binary not found at $BIN" >&2 + exit 1 +fi + +rm -rf "$BUILD/macos-release" +mkdir -p "$MACOS_DIR" "$RESOURCES" + +bash "$ROOT/scripts/macos/generate-icons.sh" "$BUILD/macos-release/AppIcon.icns" +cp "$BUILD/macos-release/AppIcon.icns" "$RESOURCES/AppIcon.icns" + +cp "$ROOT/macos/Info.plist" "$CONTENTS/Info.plist" +cp "$BIN" "$MACOS_DIR/yr_crystals" +chmod +x "$MACOS_DIR/yr_crystals" + +plutil -replace CFBundleSupportedPlatforms -json '["MacOSX"]' "$CONTENTS/Info.plist" + +# injects the DT* and BuildMachineOSBuild keys App Store validation demands. +SDK_VER="$(xcrun --sdk macosx --show-sdk-version)" +SDK_BUILD="$(xcrun --sdk macosx --show-sdk-build-version)" +XCODE_VER="$(xcodebuild -version | head -1 | awk '{print $2}')" +XCODE_BUILD="$(xcodebuild -version | grep 'Build version' | awk '{print $3}')" +IFS='.' read -r XV_MAJ XV_MIN XV_PAT <<< "$XCODE_VER" +XV_MAJ="${XV_MAJ:-0}"; XV_MIN="${XV_MIN:-0}"; XV_PAT="${XV_PAT:-0}" +DT_XCODE="$((XV_MAJ * 100 + XV_MIN * 10 + XV_PAT))" +OS_BUILD="$(sw_vers -buildVersion)" + +set_or_add() { + local key="$1" val="$2" + /usr/libexec/PlistBuddy -c "Set :$key $val" "$CONTENTS/Info.plist" 2>/dev/null \ + || /usr/libexec/PlistBuddy -c "Add :$key string $val" "$CONTENTS/Info.plist" +} +set_or_add BuildMachineOSBuild "$OS_BUILD" +set_or_add DTCompiler com.apple.compilers.llvm.clang.1_0 +set_or_add DTPlatformBuild "$SDK_BUILD" +set_or_add DTPlatformName macosx +set_or_add DTPlatformVersion "$SDK_VER" +set_or_add DTSDKBuild "$SDK_BUILD" +set_or_add DTSDKName "macosx${SDK_VER}" +set_or_add DTXcode "$DT_XCODE" +set_or_add DTXcodeBuild "$XCODE_BUILD" + +cp "$PROFILE" "$CONTENTS/embedded.provisionprofile" + +ENT="$BUILD/macos-release/entitlements.plist" +security cms -D -i "$PROFILE" 2>/dev/null \ + | plutil -extract Entitlements xml1 -o "$ENT" - \ + || { echo "ERROR: could not extract entitlements from profile" >&2; exit 1; } + +# pins wildcard application-identifier to the concrete TEAM.bundle.id form. +BUNDLE_ID="$(/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "$CONTENTS/Info.plist")" +TEAM_ID="$(/usr/libexec/PlistBuddy -c "Print :com.apple.developer.team-identifier" "$ENT" 2>/dev/null \ + || /usr/libexec/PlistBuddy -c "Print :application-identifier" "$ENT" | cut -d. -f1)" +case "$(/usr/libexec/PlistBuddy -c "Print :application-identifier" "$ENT")" in + *\*) /usr/libexec/PlistBuddy -c "Set :application-identifier ${TEAM_ID}.${BUNDLE_ID}" "$ENT" ;; +esac + +# Mac App Store mandates the app-sandbox entitlement. +/usr/libexec/PlistBuddy -c "Set :com.apple.security.app-sandbox true" "$ENT" 2>/dev/null \ + || /usr/libexec/PlistBuddy -c "Add :com.apple.security.app-sandbox bool true" "$ENT" +/usr/libexec/PlistBuddy -c "Set :com.apple.security.files.user-selected.read-only true" "$ENT" 2>/dev/null \ + || /usr/libexec/PlistBuddy -c "Add :com.apple.security.files.user-selected.read-only bool true" "$ENT" +/usr/libexec/PlistBuddy -c "Set :get-task-allow false" "$ENT" 2>/dev/null \ + || /usr/libexec/PlistBuddy -c "Add :get-task-allow bool false" "$ENT" + +# locates an Apple Distribution / Mac App Distribution identity by SHA-matching the profile's certs. +TMPDIR_PROF="$(mktemp -d)" +PROFILE_PLIST="$TMPDIR_PROF/profile.plist" +security cms -D -i "$PROFILE" > "$PROFILE_PLIST" 2>/dev/null + +PROFILE_SHAS="" +for i in 0 1 2 3 4 5 6 7 8 9; do + if ! plutil -extract "DeveloperCertificates.$i" raw -o "$TMPDIR_PROF/c$i.b64" "$PROFILE_PLIST" >/dev/null 2>&1; then + break + fi + base64 -D -i "$TMPDIR_PROF/c$i.b64" -o "$TMPDIR_PROF/c$i.cer" + sha=$(openssl x509 -inform der -in "$TMPDIR_PROF/c$i.cer" -fingerprint -noout 2>/dev/null \ + | sed 's/.*=//;s/://g') + PROFILE_SHAS="$PROFILE_SHAS $sha" +done + +KEYCHAIN_SHAS=$(security find-identity -v -p codesigning 2>/dev/null \ + | awk '/[0-9A-F]{40}/ {gsub(/[^0-9A-F]/, "", $2); print $2}') + +APP_IDENTITY="" +for s in $PROFILE_SHAS; do + if echo "$KEYCHAIN_SHAS" | grep -qi "^$s$"; then + APP_IDENTITY="$s" + break + fi +done +rm -rf "$TMPDIR_PROF" + +if [ -z "$APP_IDENTITY" ]; then + echo "ERROR: no Apple/Mac App Distribution identity in keychain matches the profile's certs" >&2 + echo " profile certs:$PROFILE_SHAS" >&2 + exit 1 +fi + +INSTALLER_IDENTITY="$(security find-identity -v 2>/dev/null \ + | grep -E 'Mac Installer Distribution|3rd Party Mac Developer Installer' \ + | head -1 | awk '{print $2}')" +if [ -z "$INSTALLER_IDENTITY" ]; then + echo "ERROR: no 'Mac Installer Distribution' identity in keychain — required to sign the .pkg" >&2 + exit 1 +fi + +echo "Signing app with $APP_IDENTITY..." +codesign --force --deep \ + --sign "$APP_IDENTITY" \ + --entitlements "$ENT" \ + --options runtime \ + --timestamp \ + "$APP" + +codesign --verify --deep --strict --verbose=2 "$APP" + +echo "Building .pkg with $INSTALLER_IDENTITY..." +productbuild --component "$APP" /Applications --sign "$INSTALLER_IDENTITY" "$PKG" + +SHIPPED_VER="$(/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "$CONTENTS/Info.plist")" + +# resolves BUMP from --bump flag, BUMP env var, or fallback default. +EXPLICIT_BUMP="" +for arg in "$@"; do + case "$arg" in + --bump) EXPLICIT_BUMP=patch ;; + --bump=*) EXPLICIT_BUMP="${arg#--bump=}" ;; + esac +done +if [ -n "$EXPLICIT_BUMP" ]; then + BUMP="$EXPLICIT_BUMP" +elif [ -z "${BUMP:-}" ]; then + if [ -n "${VER:-}" ]; then + BUMP=none + else + BUMP=patch + fi +fi +case "$BUMP" in + none) + echo "Skipping version bump (BUMP=none)" + ;; + major|minor|patch) + IFS='.' read -r CUR_MAJ CUR_MIN CUR_PATCH <<< "$SHIPPED_VER" + CUR_MAJ="${CUR_MAJ:-0}"; CUR_MIN="${CUR_MIN:-0}"; CUR_PATCH="${CUR_PATCH:-0}" + case "$BUMP" in + major) NEW_VER="$((CUR_MAJ + 1)).0.0" ;; + minor) NEW_VER="$CUR_MAJ.$((CUR_MIN + 1)).0" ;; + patch) NEW_VER="$CUR_MAJ.$CUR_MIN.$((CUR_PATCH + 1))" ;; + esac + /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $NEW_VER" "$ROOT/macos/Info.plist" + /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $NEW_VER" "$ROOT/macos/Info.plist" + echo "Bumped macos/Info.plist: $SHIPPED_VER -> $NEW_VER ($BUMP) for next release" + ;; + *) + echo "ERROR: BUMP must be patch|minor|major|none, got '$BUMP'" >&2 + exit 1 + ;; +esac + +echo "Built: $PKG ($BUNDLE_ID @ $SHIPPED_VER)" diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 28ce2be..6e9bb36 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -118,6 +118,7 @@ fn print_help() { eprintln!(" generate-icons-android rasterize assets/Icon.svg into android mipmap buckets"); eprintln!(" xcodeproj-ios generate ios/YrXtals.xcodeproj via xcodegen"); eprintln!(" release-ios build an App Store-signed .ipa for Transporter"); + eprintln!(" release-macos build a Mac App Store-signed .pkg for Transporter"); eprintln!(" clean wipe build/, target/, /tmp/yr_crystals-target"); eprintln!(" clean-ios wipe iOS build dirs + generated assets (--cargo also runs cargo clean)"); eprintln!(" clean-android wipe android build dirs + generated assets (--cargo also runs cargo clean)");