diff --git a/.github/scripts/bundle-macos.sh b/.github/scripts/bundle-macos.sh index 84ccc934..3886b209 100755 --- a/.github/scripts/bundle-macos.sh +++ b/.github/scripts/bundle-macos.sh @@ -27,18 +27,24 @@ if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then exit 1 fi PACKAGE_UPDATE_ZIP="${TTY7_PACKAGE_UPDATE_ZIP:-1}" -APP="dist/tty7.app" +# A rebranded or local build sets its own name and id, binary dir and output +# dir. TTY7_BUNDLE_ONLY stops after the .app, before the update zip and DMG. +APP_NAME="${TTY7_APP_NAME:-tty7}" +BUNDLE_ID="${TTY7_BUNDLE_ID:-com.github.tty7}" +BIN_DIR="${TTY7_BIN_DIR:-target/${TARGET}/release}" +DIST="${TTY7_DIST:-dist}" +APP="$DIST/$APP_NAME.app" -rm -rf dist +rm -rf "$DIST" mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources" -cp "target/${TARGET}/release/tty7-app" "$APP/Contents/MacOS/tty7-app" +cp "$BIN_DIR/tty7-app" "$APP/Contents/MacOS/tty7-app" chmod +x "$APP/Contents/MacOS/tty7-app" # The CLI rides inside the bundle rather than beside it: a DMG is drag-to- # Applications, so anything not in the .app never reaches the user's disk. The # GUI symlinks it onto PATH at launch (see core::cli_install), which is why it # sits next to tty7-app under MacOS/ — that is the directory the GUI resolves # relative to its own executable. -cp "target/${TARGET}/release/tty7" "$APP/Contents/MacOS/tty7" +cp "$BIN_DIR/tty7" "$APP/Contents/MacOS/tty7" chmod +x "$APP/Contents/MacOS/tty7" if [[ "$PACKAGE_UPDATE_ZIP" != "0" ]]; then # A focused out-of-process updater can replace the bundle after the GUI @@ -47,7 +53,7 @@ if [[ "$PACKAGE_UPDATE_ZIP" != "0" ]]; then # is covered by the outer bundle — including Nightly, whose users are # offered the stable release that supersedes their prerelease and need a # working helper to get there. - cp "target/${TARGET}/release/tty7-updater" "$APP/Contents/MacOS/tty7-updater" + cp "$BIN_DIR/tty7-updater" "$APP/Contents/MacOS/tty7-updater" chmod +x "$APP/Contents/MacOS/tty7-updater" fi cp assets/tty7.icns "$APP/Contents/Resources/tty7.icns" @@ -56,15 +62,19 @@ cp assets/tty7.icns "$APP/Contents/Resources/tty7.icns" mkdir -p "$APP/Contents/Resources/completions" cp assets/completions/*.json "$APP/Contents/Resources/completions/" printf 'APPL????' > "$APP/Contents/PkgInfo" +if [[ -n "${TTY7_LOCAL_BUILD_ID:-}" ]]; then + # A local install's build id, which a running app can watch to offer a restart. + printf '%s\n' "$TTY7_LOCAL_BUILD_ID" > "$APP/Contents/Resources/local-build-id" +fi cat > "$APP/Contents/Info.plist" < - CFBundleNametty7 - CFBundleDisplayNametty7 - CFBundleIdentifiercom.github.tty7 + CFBundleName${APP_NAME} + CFBundleDisplayName${APP_NAME} + CFBundleIdentifier${BUNDLE_ID} CFBundleVersion${VERSION} CFBundleShortVersionString${VERSION} CFBundleExecutabletty7-app @@ -145,7 +155,12 @@ PLIST SIGN_ID="${APPLE_SIGNING_IDENTITY:-}" -if [[ -n "$SIGN_ID" && -n "${APPLE_CERTIFICATE:-}" ]]; then +if [[ -n "$SIGN_ID" ]]; then + # A local build signs with an identity already in the login keychain; only CI + # imports one, and only CI pays for a secure timestamp. + TIMESTAMP=--timestamp=none + if [[ -n "${APPLE_CERTIFICATE:-}" ]]; then + TIMESTAMP=--timestamp # ---- Developer ID signing ------------------------------------------------ # Import the cert into a throwaway keychain so we never touch the login one. KEYCHAIN="${RUNNER_TEMP:-/tmp}/tty7-sign.keychain-db" @@ -154,7 +169,7 @@ if [[ -n "$SIGN_ID" && -n "${APPLE_CERTIFICATE:-}" ]]; then # Scrub the decoded cert + temp keychain on any exit path. cleanup() { security delete-keychain "$KEYCHAIN" >/dev/null 2>&1 || true - rm -f "$CERT_PATH" + rm -f "$CERT_PATH" "${ASC_KEY_PATH:-}" } trap cleanup EXIT @@ -167,11 +182,12 @@ if [[ -n "$SIGN_ID" && -n "${APPLE_CERTIFICATE:-}" ]]; then security set-key-partition-list -S apple-tool:,apple:,codesign: \ -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN" >/dev/null security list-keychains -d user -s "$KEYCHAIN" login.keychain + fi # Hardened runtime forbids JIT / unsigned executable memory by default; the # GPU/Metal path gpui uses needs them, so grant them explicitly or the # notarized build crashes on launch. - ENTITLEMENTS="dist/entitlements.plist" + ENTITLEMENTS="$DIST/entitlements.plist" cat > "$ENTITLEMENTS" <<'ENT' @@ -205,28 +221,35 @@ ENT # entitlements: the JIT and library-validation exemptions exist for gpui's # Metal path, and a CLI that never renders anything has no business holding # them. - codesign --force --options runtime --timestamp \ + codesign --force --options runtime "$TIMESTAMP" \ --sign "$SIGN_ID" "$APP/Contents/MacOS/tty7" if [[ "$PACKAGE_UPDATE_ZIP" != "0" ]]; then - codesign --force --options runtime --timestamp \ + codesign --force --options runtime "$TIMESTAMP" \ --sign "$SIGN_ID" "$APP/Contents/MacOS/tty7-updater" fi - codesign --force --options runtime --timestamp --entitlements "$ENTITLEMENTS" \ + codesign --force --options runtime "$TIMESTAMP" --entitlements "$ENTITLEMENTS" \ --sign "$SIGN_ID" "$APP/Contents/MacOS/tty7-app" - codesign --force --options runtime --timestamp --entitlements "$ENTITLEMENTS" \ + codesign --force --options runtime "$TIMESTAMP" --entitlements "$ENTITLEMENTS" \ --sign "$SIGN_ID" "$APP" codesign --verify --strict --verbose=2 "$APP" # ---- Notarization -------------------------------------------------------- - if [[ -n "${APPLE_ID:-}" && -n "${APPLE_PASSWORD:-}" && -n "${APPLE_TEAM_ID:-}" ]]; then + NOTARY_AUTH=() + if [[ -n "${ASC_KEY_P8:-}" && -n "${ASC_KEY_ID:-}" && -n "${ASC_ISSUER_ID:-}" ]]; then + # An App Store Connect API key, the alternative to an Apple ID password. + ASC_KEY_PATH="${RUNNER_TEMP:-/tmp}/AuthKey_${ASC_KEY_ID}.p8" + printf '%s\n' "$ASC_KEY_P8" > "$ASC_KEY_PATH" + NOTARY_AUTH=(--key "$ASC_KEY_PATH" --key-id "$ASC_KEY_ID" --issuer "$ASC_ISSUER_ID") + elif [[ -n "${APPLE_ID:-}" && -n "${APPLE_PASSWORD:-}" && -n "${APPLE_TEAM_ID:-}" ]]; then + NOTARY_AUTH=(--apple-id "$APPLE_ID" --password "$APPLE_PASSWORD" --team-id "$APPLE_TEAM_ID") + fi + if [[ ${#NOTARY_AUTH[@]} -gt 0 ]]; then # Submit a zip of the .app; on success staple the ticket onto the bundle # so it validates offline (the distributed zip below then carries it). - ditto -c -k --keepParent "$APP" "dist/notarize.zip" - xcrun notarytool submit "dist/notarize.zip" \ - --apple-id "$APPLE_ID" --password "$APPLE_PASSWORD" \ - --team-id "$APPLE_TEAM_ID" --wait + ditto -c -k --keepParent "$APP" "$DIST/notarize.zip" + xcrun notarytool submit "$DIST/notarize.zip" "${NOTARY_AUTH[@]}" --wait xcrun stapler staple "$APP" - rm -f "dist/notarize.zip" + rm -f "$DIST/notarize.zip" echo "✅ signed + notarized + stapled" else echo "⚠️ signed with Developer ID but notarization secrets missing — skipping notarize" @@ -317,6 +340,9 @@ if [[ "$BUNDLE_FAIL" -ne 0 ]]; then exit 1 fi echo "✅ every Mach-O in $APP is a thin ${ARCH} binary (${SWEEP_SEEN} checked)" +if [[ -n "${TTY7_BUNDLE_ONLY:-}" ]]; then + exit 0 +fi # The in-app updater needs the signed, notarized .app itself rather than a disk # image that requires Finder interaction. The helper re-reads the full embedded @@ -324,13 +350,13 @@ echo "✅ every Mach-O in $APP is a thin ${ARCH} binary (${SWEEP_SEEN} checked)" # it was told to install. ZIP="" if [[ "$PACKAGE_UPDATE_ZIP" != "0" ]]; then - ZIP="dist/tty7-${VERSION}-macos-${ARCH}.zip" + ZIP="$DIST/tty7-${VERSION}-macos-${ARCH}.zip" ditto -c -k --keepParent "$APP" "$ZIP" fi # Package the (now stapled) bundle as a drag-to-Applications DMG. -DMG="dist/tty7-${VERSION}-macos-${ARCH}.dmg" -STAGE="dist/dmg-stage" +DMG="$DIST/tty7-${VERSION}-macos-${ARCH}.dmg" +STAGE="$DIST/dmg-stage" rm -rf "$STAGE" mkdir "$STAGE" # `mv`, not `cp -R`: this is the peak, and a second full copy of the bundle is @@ -354,7 +380,7 @@ ln -s /Applications "$STAGE/Applications" # measured against a stage of this shape, 127 MiB of empty volume cost 672 KiB # in the published DMG. STAGE_KB="$(du -sk "$STAGE" | awk '{print $1}')" -hdiutil create -volname "tty7" -srcfolder "$STAGE" -ov -format UDZO \ +hdiutil create -volname "$APP_NAME" -srcfolder "$STAGE" -ov -format UDZO \ -size "$(( STAGE_KB * 2 + 65536 ))k" "$DMG" rm -rf "$STAGE" if [[ -n "$SIGN_ID" && -n "${APPLE_CERTIFICATE:-}" ]]; then