mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 08:02:24 +00:00
* feat(updater): add windows online updates
* feat(updater): support online updates for windows portable zip builds
f
* feat(updater): support online updates for nightly build
* fix(updater): strengthen post-download update verification
* feat(updater): support explicit stable and nightly channel switching
* fix(i18n): localize update settings ui
* fix(settings): prevent slider value labels from wrapping
* feat(updater): drop the nightly channel, refuse all-users Windows installs
Follow-up to the Windows updater work on this branch, applying maintainer
review.
Nightly is a build channel, not an update channel. The updater consults
`/releases/latest` again and nothing else, so it behaves on Windows exactly
as it already does on macOS: a Nightly build is offered the stable release
that supersedes it and graduates out of the prerelease, and no rolling
prerelease can become a source of code that gets executed on a user's
machine. Removed with it: the `UpdateChannel` enum and its version-string
inference, the `tags/nightly` query, the cross-channel version-ordering
bypass, the Settings → About channel row, the rolling-tag
`update-manifest.json` and the i18n keys that only served them.
`parse_version` and `is_update_available` are byte-identical to main again.
Nightly builds are untouched, and still carry tty7-updater plus the macOS
update archive — a Nightly user needs a working helper to reach the stable
release that replaces their build.
An all-users Windows installation is no longer updated in place. Running the
release Setup silently as the signed-in user cannot replace
`C:\Program Files\tty7`: Inno resolves `{autopf}` to `%LocalAppData%\Programs`
and installs a second copy beside the real one, or re-launches itself
elevated and puts a bare UAC prompt for an unsigned executable in `%TEMP%` in
front of a user whose GUI just vanished. tty7 declines both and points at the
release page. Detection reads Inno's own `HKLM` state for the frozen AppId and
independently probes whether the directory accepts writes, so a relocated or
pruned installation is caught too; the decision is a pure function with unit
tests, and it is re-checked before the download as well as during it.
Release and Nightly now verify the Windows packages they just built, mirroring
the macOS update-archive step: the install marker, tty7-updater.exe, the ZIP
layout the updater will accept and the PE versions it will demand. Every fact
the updater checks on the user's machine after downloading is checked here
instead, so a packaging mistake fails the build.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
182 lines
8.3 KiB
Bash
Executable File
182 lines
8.3 KiB
Bash
Executable File
#!/bin/bash
|
|
# Usage: bundle-macos.sh <target-triple> <arch-label>
|
|
# Package the release binary into dist/tty7.app, then publish both:
|
|
# dist/tty7-<version>-macos-<arch>.zip (in-app updater)
|
|
# dist/tty7-<version>-macos-<arch>.dmg (drag-to-Applications install)
|
|
#
|
|
# Signing posture is chosen from the environment:
|
|
# * Developer ID secrets present (APPLE_SIGNING_IDENTITY + APPLE_CERTIFICATE)
|
|
# -> hardened-runtime signature, then notarize + staple. Passes Gatekeeper.
|
|
# * Otherwise -> adhoc signature, same as before. Fine for local dev, but the
|
|
# OS will quarantine it on other machines.
|
|
set -euo pipefail
|
|
|
|
TARGET="$1"
|
|
ARCH="$2"
|
|
# Anchored on `= "` because the root manifest's `[package]` section leads with
|
|
# `version.workspace = true` — a bare `^version` match grabs that line, finds no
|
|
# quotes to substitute, and passes it through as the "version", which then lands
|
|
# in CFBundleVersion and the .dmg filename. Guard against a silent recurrence.
|
|
VERSION="$(grep -m1 '^version = "' Cargo.toml | sed -E 's/.*"([^"]+)".*/\1/')"
|
|
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then
|
|
echo "bundle-macos: could not read a version from Cargo.toml (got '$VERSION')" >&2
|
|
exit 1
|
|
fi
|
|
PACKAGE_UPDATE_ZIP="${TTY7_PACKAGE_UPDATE_ZIP:-1}"
|
|
APP="dist/tty7.app"
|
|
|
|
rm -rf dist
|
|
mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources"
|
|
cp "target/${TARGET}/release/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"
|
|
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
|
|
# exits, then relaunch or roll back without teaching the GUI to mutate
|
|
# itself. Every macOS build carries it beside the app/CLI so its signature
|
|
# 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"
|
|
chmod +x "$APP/Contents/MacOS/tty7-updater"
|
|
fi
|
|
cp assets/tty7.icns "$APP/Contents/Resources/tty7.icns"
|
|
# Completion signatures are loaded at runtime (not embedded), resolved relative
|
|
# to the executable as ../Resources/completions — see terminal::signature.
|
|
mkdir -p "$APP/Contents/Resources/completions"
|
|
cp assets/completions/*.json "$APP/Contents/Resources/completions/"
|
|
printf 'APPL????' > "$APP/Contents/PkgInfo"
|
|
|
|
cat > "$APP/Contents/Info.plist" <<PLIST
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>CFBundleName</key><string>tty7</string>
|
|
<key>CFBundleDisplayName</key><string>tty7</string>
|
|
<key>CFBundleIdentifier</key><string>com.github.tty7</string>
|
|
<key>CFBundleVersion</key><string>${VERSION}</string>
|
|
<key>CFBundleShortVersionString</key><string>${VERSION}</string>
|
|
<key>CFBundleExecutable</key><string>tty7-app</string>
|
|
<key>CFBundleIconFile</key><string>tty7</string>
|
|
<key>CFBundlePackageType</key><string>APPL</string>
|
|
<key>NSHighResolutionCapable</key><true/>
|
|
<key>NSPrincipalClass</key><string>NSApplication</string>
|
|
</dict>
|
|
</plist>
|
|
PLIST
|
|
|
|
SIGN_ID="${APPLE_SIGNING_IDENTITY:-}"
|
|
|
|
if [[ -n "$SIGN_ID" && -n "${APPLE_CERTIFICATE:-}" ]]; then
|
|
# ---- 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"
|
|
CERT_PATH="${RUNNER_TEMP:-/tmp}/tty7-cert.p12"
|
|
KEYCHAIN_PASSWORD="${KEYCHAIN_PASSWORD:-tty7-ci}"
|
|
# Scrub the decoded cert + temp keychain on any exit path.
|
|
cleanup() {
|
|
security delete-keychain "$KEYCHAIN" >/dev/null 2>&1 || true
|
|
rm -f "$CERT_PATH"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
|
|
security set-keychain-settings -lut 21600 "$KEYCHAIN"
|
|
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
|
|
echo "$APPLE_CERTIFICATE" | base64 --decode > "$CERT_PATH"
|
|
security import "$CERT_PATH" -P "${APPLE_CERTIFICATE_PASSWORD:-}" \
|
|
-A -t cert -f pkcs12 -k "$KEYCHAIN"
|
|
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
|
|
|
|
# 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"
|
|
cat > "$ENTITLEMENTS" <<'ENT'
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>com.apple.security.cs.allow-jit</key><true/>
|
|
<key>com.apple.security.cs.allow-unsigned-executable-memory</key><true/>
|
|
<key>com.apple.security.cs.disable-library-validation</key><true/>
|
|
</dict>
|
|
</plist>
|
|
ENT
|
|
|
|
# Sign inner-out: the executables first, then the bundle. The CLI must be
|
|
# signed explicitly — notarization rejects a bundle carrying an unsigned
|
|
# Mach-O, and the outer `codesign "$APP"` does not descend into MacOS/ for
|
|
# anything but CFBundleExecutable.
|
|
#
|
|
# It gets hardened runtime (notarization requires it) but none of the GUI's
|
|
# 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 \
|
|
--sign "$SIGN_ID" "$APP/Contents/MacOS/tty7"
|
|
if [[ "$PACKAGE_UPDATE_ZIP" != "0" ]]; then
|
|
codesign --force --options runtime --timestamp \
|
|
--sign "$SIGN_ID" "$APP/Contents/MacOS/tty7-updater"
|
|
fi
|
|
codesign --force --options runtime --timestamp --entitlements "$ENTITLEMENTS" \
|
|
--sign "$SIGN_ID" "$APP/Contents/MacOS/tty7-app"
|
|
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
|
|
# 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
|
|
xcrun stapler staple "$APP"
|
|
rm -f "dist/notarize.zip"
|
|
echo "✅ signed + notarized + stapled"
|
|
else
|
|
echo "⚠️ signed with Developer ID but notarization secrets missing — skipping notarize"
|
|
fi
|
|
else
|
|
echo "⚠️ no Developer ID secrets — adhoc signing (won't pass Gatekeeper on other machines)"
|
|
codesign --force --deep --sign - "$APP"
|
|
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
|
|
# version out of the staged bundle and refuses anything that is not the release
|
|
# it was told to install.
|
|
ZIP=""
|
|
if [[ "$PACKAGE_UPDATE_ZIP" != "0" ]]; then
|
|
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"
|
|
rm -rf "$STAGE"
|
|
mkdir "$STAGE"
|
|
cp -R "$APP" "$STAGE/"
|
|
ln -s /Applications "$STAGE/Applications"
|
|
hdiutil create -volname "tty7" -srcfolder "$STAGE" -ov -format UDZO "$DMG"
|
|
rm -rf "$STAGE"
|
|
if [[ -n "$SIGN_ID" && -n "${APPLE_CERTIFICATE:-}" ]]; then
|
|
codesign --force --timestamp --sign "$SIGN_ID" "$DMG"
|
|
fi
|
|
if [[ -n "$ZIP" ]]; then
|
|
echo "✅ $ZIP"
|
|
fi
|
|
echo "✅ $DMG"
|