diff --git a/BuildSupport/Galaxy.entitlements b/BuildSupport/Galaxy.entitlements new file mode 100644 index 00000000..5aada797 --- /dev/null +++ b/BuildSupport/Galaxy.entitlements @@ -0,0 +1,13 @@ + + + + + com.apple.security.network.client + + com.apple.security.automation.apple-events + + com.apple.security.cs.allow-unsigned-executable-memory + + + diff --git a/GALAXY.md b/GALAXY.md index dd067a6f..319c319a 100644 --- a/GALAXY.md +++ b/GALAXY.md @@ -165,6 +165,46 @@ When adding/editing match statements, avoid using the wildcard _ when at all pos **pull_warp_feature** (`.warp/skills/pull_warp_feature/SKILL.md`): Ports features from upstream Warp into Galaxy. Clones Warp source into `.galaxy/warp-upstream/` (gitignored), analyzes the feature for Warp-specific dependencies (AI → Bedrock, cloud → local storage, telemetry → removed), presents a compatibility plan, and implements the port after approval. All AI must go through `BedrockClient`; all cloud storage must be local. +### Code Signing & Distribution + +Galaxy uses Samsung's Developer ID Application certificate for macOS code signing and Apple notarization. + +**Signing identity:** `Developer ID Application: SAMSUNG ELECTRONICS AMERICA, INC. (3JU72Z7Y3J)` + +**Entitlements** (`BuildSupport/Galaxy.entitlements`): +- `com.apple.security.network.client` — AWS Bedrock API calls +- `com.apple.security.automation.apple-events` — osascript for CLI install +- `com.apple.security.cs.allow-unsigned-executable-memory` — Metal shader compilation + +**Build & distribute pipeline:** +```bash +# Debug (unoptimized, ~149MB) +./build-debug.sh + +# Release (optimized, smaller) +./build-release.sh + +# Or step by step: +cargo bundle [--release] --bin galaxy-oss --package galaxy +./sign.sh [--release] +./notarize.sh [--release] +./package.sh [--release] + +# Copy DMGs to ~/Downloads with date suffix +./copy-dmgs.sh +``` + +**Output:** `target/{debug|release}/bundle/osx/Galaxy.dmg` — signed, notarized, stapled. Users can download, open, drag to Applications, launch without Gatekeeper warnings. + +**Install from source (end users):** +```bash +./script/install-galaxy.sh +``` + +### Bedrock Diagnostics + +When `GALAXY_BEDROCK_DIAGNOSTICS=1` is set, the diagnostic logger writes to `bedrock-diagnostics.log` in the log directory. On API errors, a comprehensive JSON snapshot (`Error_YYYYMMDD_HHMMSS_mmm.txt`) is written to the repo root (or cwd/tmp) containing the full request context, captured log lines, and log tails. + ## Future Work ### IDE-Level LSP Integration diff --git a/build-debug.sh b/build-debug.sh new file mode 100755 index 00000000..ce414e09 --- /dev/null +++ b/build-debug.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -e +cd "$(dirname "$0")" + +echo "=== Galaxy Debug Build + Sign + Notarize + Package ===" +echo "" + +cargo bundle --bin galaxy-oss --package galaxy +./sign.sh +./notarize.sh +./package.sh + +echo "" +echo "=== Done. Distributable DMG: target/debug/bundle/osx/Galaxy.dmg ===" diff --git a/build-release.sh b/build-release.sh new file mode 100755 index 00000000..04d3079e --- /dev/null +++ b/build-release.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -e +cd "$(dirname "$0")" + +echo "=== Galaxy Release Build + Sign + Notarize + Package ===" +echo "" + +cargo bundle --release --bin galaxy-oss --package galaxy +./sign.sh --release +./notarize.sh --release +./package.sh --release + +echo "" +echo "=== Done. Distributable DMG: target/release/bundle/osx/Galaxy.dmg ===" diff --git a/copy-dmgs.sh b/copy-dmgs.sh new file mode 100755 index 00000000..0c450fe2 --- /dev/null +++ b/copy-dmgs.sh @@ -0,0 +1,35 @@ +#!/bin/bash +set -e +cd "$(dirname "$0")" + +DATE=$(date +%Y%m%d) +DEST="$HOME/Downloads" + +next_letter() { + local prefix="$1" + local letter="a" + while [ -f "${prefix}${letter}.dmg" ]; do + letter=$(echo "$letter" | tr 'a-y' 'b-z') + if [ "$letter" = "z" ] && [ -f "${prefix}z.dmg" ]; then + echo "Error: ran out of letter indices for today" >&2 + exit 1 + fi + done + echo "$letter" +} + +if [ -f "target/debug/bundle/osx/Galaxy.dmg" ]; then + LETTER=$(next_letter "$DEST/Galaxy-debug-${DATE}") + cp "target/debug/bundle/osx/Galaxy.dmg" "$DEST/Galaxy-debug-${DATE}${LETTER}.dmg" + echo "Copied: $DEST/Galaxy-debug-${DATE}${LETTER}.dmg" +else + echo "Skipped: no debug DMG found" +fi + +if [ -f "target/release/bundle/osx/Galaxy.dmg" ]; then + LETTER=$(next_letter "$DEST/Galaxy-release-${DATE}") + cp "target/release/bundle/osx/Galaxy.dmg" "$DEST/Galaxy-release-${DATE}${LETTER}.dmg" + echo "Copied: $DEST/Galaxy-release-${DATE}${LETTER}.dmg" +else + echo "Skipped: no release DMG found" +fi diff --git a/notarize.sh b/notarize.sh new file mode 100755 index 00000000..cf748387 --- /dev/null +++ b/notarize.sh @@ -0,0 +1,49 @@ +#!/bin/bash +set -e +cd "$(dirname "$0")" + +if [ "$1" = "--release" ]; then + APP_PATH="target/release/bundle/osx/Galaxy.app" + ZIP_PATH="target/release/bundle/osx/Galaxy.zip" +else + APP_PATH="target/debug/bundle/osx/Galaxy.app" + ZIP_PATH="target/debug/bundle/osx/Galaxy.zip" +fi + +trap 'rm -f "$ZIP_PATH"' EXIT + +if [ ! -d "$APP_PATH" ]; then + echo "Error: $APP_PATH not found." + exit 1 +fi + +echo "Verifying app is signed before notarization..." +codesign --verify --strict "$APP_PATH" || { + echo "Error: App is not properly signed. Run ./sign.sh first." + exit 1 +} + +echo "Creating zip for notarization submission..." +rm -f "$ZIP_PATH" +ditto -c -k --keepParent "$APP_PATH" "$ZIP_PATH" + +echo "Submitting to Apple notary service (this may take a few minutes)..." +RESULT=$(xcrun notarytool submit "$ZIP_PATH" \ + --keychain-profile notarytool-profile \ + --wait 2>&1) +echo "$RESULT" + +if ! echo "$RESULT" | grep -q "status: Accepted"; then + echo "" + echo "Error: Notarization was not accepted. Check output above." + exit 1 +fi + +echo "Stapling notarization ticket to app..." +xcrun stapler staple "$APP_PATH" + +echo "Validating stapled ticket..." +xcrun stapler validate "$APP_PATH" + +echo "" +echo "Done. Galaxy.app is notarized and stapled." diff --git a/package.sh b/package.sh new file mode 100755 index 00000000..f0583931 --- /dev/null +++ b/package.sh @@ -0,0 +1,82 @@ +#!/bin/bash +set -e +cd "$(dirname "$0")" + +SIGNING_IDENTITY="Developer ID Application: SAMSUNG ELECTRONICS AMERICA, INC. (3JU72Z7Y3J)" + +if [ "$1" = "--release" ]; then + APP_PATH="target/release/bundle/osx/Galaxy.app" + DMG_PATH="target/release/bundle/osx/Galaxy.dmg" + STAGING_DIR="target/release/bundle/osx/dmg-staging" +else + APP_PATH="target/debug/bundle/osx/Galaxy.app" + DMG_PATH="target/debug/bundle/osx/Galaxy.dmg" + STAGING_DIR="target/debug/bundle/osx/dmg-staging" +fi + +if [ ! -d "$APP_PATH" ]; then + echo "Error: $APP_PATH not found." + exit 1 +fi + +echo "Verifying app is notarized..." +xcrun stapler validate "$APP_PATH" || { + echo "Error: App does not have a valid notarization ticket. Run ./sign.sh and ./notarize.sh first." + exit 1 +} + +echo "Preparing DMG staging directory..." +rm -rf "$STAGING_DIR" +mkdir -p "$STAGING_DIR" +cp -R "$APP_PATH" "$STAGING_DIR/" +ln -s /Applications "$STAGING_DIR/Applications" + +echo "Creating DMG..." +hdiutil detach /Volumes/Galaxy -force 2>/dev/null || true +rm -f "$DMG_PATH" +# Prevent Spotlight/fseventsd from locking the staging dir +touch "$STAGING_DIR/.metadata_never_index" +sleep 1 +for attempt in 1 2 3; do + if hdiutil create -volname "Galaxy" \ + -srcfolder "$STAGING_DIR" \ + -ov -format UDZO \ + "$DMG_PATH"; then + break + fi + if [ "$attempt" -eq 3 ]; then + echo "Error: hdiutil create failed after 3 attempts" + exit 1 + fi + echo "hdiutil failed (attempt $attempt/3), retrying in 3s..." + sleep 3 +done + +echo "Signing DMG..." +codesign --force --sign "$SIGNING_IDENTITY" --timestamp "$DMG_PATH" + +echo "Submitting DMG to Apple notary service..." +RESULT=$(xcrun notarytool submit "$DMG_PATH" \ + --keychain-profile notarytool-profile \ + --wait 2>&1) +echo "$RESULT" + +if ! echo "$RESULT" | grep -q "status: Accepted"; then + echo "" + echo "Error: DMG notarization was not accepted. Check output above." + exit 1 +fi + +echo "Stapling notarization ticket to DMG..." +xcrun stapler staple "$DMG_PATH" + +echo "Cleaning up staging directory..." +rm -rf "$STAGING_DIR" + +echo "" +echo "Verifying final DMG..." +xcrun stapler validate "$DMG_PATH" + +echo "" +echo "Done. Galaxy.dmg is ready for distribution at:" +echo " $DMG_PATH" diff --git a/sign.sh b/sign.sh new file mode 100755 index 00000000..30cb780f --- /dev/null +++ b/sign.sh @@ -0,0 +1,56 @@ +#!/bin/bash +set -e +cd "$(dirname "$0")" + +SIGNING_IDENTITY="Developer ID Application: SAMSUNG ELECTRONICS AMERICA, INC. (3JU72Z7Y3J)" +ENTITLEMENTS="BuildSupport/Galaxy.entitlements" + +if [ "$1" = "--release" ]; then + APP_PATH="target/release/bundle/osx/Galaxy.app" +else + APP_PATH="target/debug/bundle/osx/Galaxy.app" +fi + +if [ ! -d "$APP_PATH" ]; then + echo "Error: $APP_PATH not found." + echo "Run first: cargo bundle --bin galaxy-oss --package galaxy" + exit 1 +fi + +if [ ! -f "$ENTITLEMENTS" ]; then + echo "Error: $ENTITLEMENTS not found." + exit 1 +fi + +JUNK=$(find "$APP_PATH" -maxdepth 1 -not -name "Contents" -not -path "$APP_PATH" 2>/dev/null || true) +if [ -n "$JUNK" ]; then + echo "Removing unsealed items from bundle root: $JUNK" + echo "$JUNK" | xargs rm -rf +fi + +echo "Removing .DS_Store files..." +find "$APP_PATH" -name '.DS_Store' -delete 2>/dev/null || true + +echo "Stripping extended attributes..." +xattr -cr "$APP_PATH" + +echo "Signing $APP_PATH..." +codesign --force --sign "$SIGNING_IDENTITY" \ + --entitlements "$ENTITLEMENTS" \ + --options runtime \ + --timestamp \ + "$APP_PATH/Contents/MacOS/galaxy-oss" + +codesign --force --sign "$SIGNING_IDENTITY" \ + --entitlements "$ENTITLEMENTS" \ + --options runtime \ + --timestamp \ + "$APP_PATH" + +echo "Verifying signature..." +codesign --verify --strict "$APP_PATH" +codesign -dvvv "$APP_PATH" 2>&1 | grep -E "Authority|TeamIdentifier|Runtime" +echo "Architecture: $(lipo -archs "$APP_PATH/Contents/MacOS/galaxy-oss")" + +echo "" +echo "Done. Galaxy.app is signed and ready for notarization."