diff --git a/.github/workflows/ci-macos-compat.yml b/.github/workflows/ci-macos-compat.yml index b6ba18dc..2b7e06c9 100644 --- a/.github/workflows/ci-macos-compat.yml +++ b/.github/workflows/ci-macos-compat.yml @@ -70,6 +70,12 @@ jobs: rm GhosttyKit.xcframework.tar.gz test -d GhosttyKit.xcframework + - name: Install zig + run: | + if ! command -v zig >/dev/null 2>&1; then + brew install zig + fi + - name: Clean DerivedData run: rm -rf ~/Library/Developer/Xcode/DerivedData/GhosttyTabs-* diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5f41a302..34a52dbc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,6 +79,12 @@ jobs: run: | ./scripts/download-prebuilt-ghosttykit.sh + - name: Install zig + run: | + if ! command -v zig >/dev/null 2>&1; then + brew install zig + fi + - name: Clean DerivedData run: | # Remove stale build cache to avoid incremental build errors @@ -209,6 +215,12 @@ jobs: run: | ./scripts/download-prebuilt-ghosttykit.sh + - name: Install zig + run: | + if ! command -v zig >/dev/null 2>&1; then + brew install zig + fi + - name: Clean DerivedData run: rm -rf ~/Library/Developer/Xcode/DerivedData/GhosttyTabs-* diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index adcadcad..8f7e48de 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -151,6 +151,9 @@ jobs: - name: Install build deps if: needs.decide.outputs.should_publish != 'true' || steps.current_head_prebuild.outputs.still_current == 'true' run: | + if ! command -v zig >/dev/null 2>&1; then + brew install zig + fi npm install --global "create-dmg@${CREATE_DMG_VERSION}" - name: Download pre-built GhosttyKit.xcframework @@ -195,12 +198,16 @@ jobs: set -euo pipefail APP_BINARY="build-universal/Build/Products/Release/cmux.app/Contents/MacOS/cmux" CLI_BINARY="build-universal/Build/Products/Release/cmux.app/Contents/Resources/bin/cmux" + HELPER_BINARY="build-universal/Build/Products/Release/cmux.app/Contents/Resources/bin/ghostty" APP_ARCHS="$(lipo -archs "$APP_BINARY")" CLI_ARCHS="$(lipo -archs "$CLI_BINARY")" + HELPER_ARCHS="$(lipo -archs "$HELPER_BINARY")" echo "App binary architectures: $APP_ARCHS" echo "CLI binary architectures: $CLI_ARCHS" + echo "Ghostty helper architectures: $HELPER_ARCHS" [[ "$APP_ARCHS" == *arm64* && "$APP_ARCHS" == *x86_64* ]] [[ "$CLI_ARCHS" == *arm64* && "$CLI_ARCHS" == *x86_64* ]] + [[ "$HELPER_ARCHS" == *arm64* && "$HELPER_ARCHS" == *x86_64* ]] - name: Run CLI version memory guard regression if: needs.decide.outputs.should_publish != 'true' || steps.current_head_prebuild.outputs.still_current == 'true' @@ -324,9 +331,13 @@ jobs: "build-universal/Build/Products/Release/cmux NIGHTLY.app" do CLI_PATH="$APP_PATH/Contents/Resources/bin/cmux" + HELPER_PATH="$APP_PATH/Contents/Resources/bin/ghostty" if [ -f "$CLI_PATH" ]; then /usr/bin/codesign --force --options runtime --timestamp --sign "$APPLE_SIGNING_IDENTITY" --entitlements "$ENTITLEMENTS" "$CLI_PATH" fi + if [ -f "$HELPER_PATH" ]; then + /usr/bin/codesign --force --options runtime --timestamp --sign "$APPLE_SIGNING_IDENTITY" --entitlements "$ENTITLEMENTS" "$HELPER_PATH" + fi /usr/bin/codesign --force --options runtime --timestamp --sign "$APPLE_SIGNING_IDENTITY" --entitlements "$ENTITLEMENTS" --deep "$APP_PATH" /usr/bin/codesign --verify --deep --strict --verbose=2 "$APP_PATH" done diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6a58f07f..ff6b33b1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -99,6 +99,9 @@ jobs: - name: Install build deps if: steps.guard_release_assets.outputs.skip_all != 'true' run: | + if ! command -v zig >/dev/null 2>&1; then + brew install zig + fi npm install --global "create-dmg@${CREATE_DMG_VERSION}" - name: Download pre-built GhosttyKit.xcframework @@ -142,6 +145,13 @@ jobs: [ -x "$CLI_BINARY" ] || { echo "cmux CLI binary not found at $CLI_BINARY" >&2; exit 1; } CMUX_CLI_BIN="$CLI_BINARY" python3 tests/test_cli_version_memory_guard.py + - name: Verify bundled Ghostty theme picker helper + if: steps.guard_release_assets.outputs.skip_all != 'true' + run: | + set -euo pipefail + HELPER_BINARY="build/Build/Products/Release/cmux.app/Contents/Resources/bin/ghostty" + [ -x "$HELPER_BINARY" ] || { echo "Ghostty theme picker helper not found at $HELPER_BINARY" >&2; exit 1; } + - name: Inject Sparkle keys into Info.plist if: steps.guard_release_assets.outputs.skip_all != 'true' run: | @@ -192,9 +202,13 @@ jobs: APP_PATH="build/Build/Products/Release/cmux.app" ENTITLEMENTS="cmux.entitlements" CLI_PATH="$APP_PATH/Contents/Resources/bin/cmux" + HELPER_PATH="$APP_PATH/Contents/Resources/bin/ghostty" if [ -f "$CLI_PATH" ]; then /usr/bin/codesign --force --options runtime --timestamp --sign "$APPLE_SIGNING_IDENTITY" --entitlements "$ENTITLEMENTS" "$CLI_PATH" fi + if [ -f "$HELPER_PATH" ]; then + /usr/bin/codesign --force --options runtime --timestamp --sign "$APPLE_SIGNING_IDENTITY" --entitlements "$ENTITLEMENTS" "$HELPER_PATH" + fi /usr/bin/codesign --force --options runtime --timestamp --sign "$APPLE_SIGNING_IDENTITY" --entitlements "$ENTITLEMENTS" --deep "$APP_PATH" /usr/bin/codesign --verify --deep --strict --verbose=2 "$APP_PATH" diff --git a/.github/workflows/test-depot.yml b/.github/workflows/test-depot.yml index ca636bf6..536c5a14 100644 --- a/.github/workflows/test-depot.yml +++ b/.github/workflows/test-depot.yml @@ -82,6 +82,12 @@ jobs: rm GhosttyKit.xcframework.tar.gz test -d GhosttyKit.xcframework + - name: Install zig + run: | + if ! command -v zig >/dev/null 2>&1; then + brew install zig + fi + - name: Create virtual display run: | set -euo pipefail diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index 23d595c7..a61e5eee 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -90,6 +90,12 @@ jobs: rm GhosttyKit.xcframework.tar.gz test -d GhosttyKit.xcframework + - name: Install zig + run: | + if ! command -v zig >/dev/null 2>&1; then + brew install zig + fi + - name: Create virtual display run: | set -euo pipefail diff --git a/GhosttyTabs.xcodeproj/project.pbxproj b/GhosttyTabs.xcodeproj/project.pbxproj index 479f0295..d7c4cb9a 100644 --- a/GhosttyTabs.xcodeproj/project.pbxproj +++ b/GhosttyTabs.xcodeproj/project.pbxproj @@ -330,7 +330,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "set -euo pipefail\nDEST=\"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}\"\nGHOSTTY_DEST=\"${DEST}/ghostty\"\nTERMINFO_DEST=\"${DEST}/terminfo\"\nCMUX_SHELL_DEST=\"${DEST}/shell-integration\"\nSRC_SHARE=\"${SRCROOT}/ghostty/zig-out/share\"\nGHOSTTY_SRC=\"${SRC_SHARE}/ghostty\"\nTERMINFO_SRC=\"${SRC_SHARE}/terminfo\"\nFALLBACK_GHOSTTY=\"${SRCROOT}/Resources/ghostty\"\nFALLBACK_TERMINFO=\"${SRCROOT}/Resources/ghostty/terminfo\"\nTERMINFO_OVERLAY=\"${SRCROOT}/Resources/terminfo-overlay\"\nCMUX_SHELL_SRC=\"${SRCROOT}/Resources/shell-integration\"\nCMUX_GHOSTTY_ZSH_SRC=\"${SRCROOT}/ghostty/src/shell-integration/zsh/ghostty-integration\"\nif [ -d \"$GHOSTTY_SRC\" ]; then\n mkdir -p \"$GHOSTTY_DEST\"\n rsync -a --delete \"$GHOSTTY_SRC/\" \"$GHOSTTY_DEST/\"\nelif [ -d \"$FALLBACK_GHOSTTY\" ]; then\n mkdir -p \"$GHOSTTY_DEST\"\n rsync -a --delete \"$FALLBACK_GHOSTTY/\" \"$GHOSTTY_DEST/\"\nfi\nif [ -d \"$TERMINFO_SRC\" ]; then\n mkdir -p \"$TERMINFO_DEST\"\n rsync -a --delete \"$TERMINFO_SRC/\" \"$TERMINFO_DEST/\"\nelif [ -d \"$FALLBACK_TERMINFO\" ]; then\n mkdir -p \"$TERMINFO_DEST\"\n rsync -a --delete \"$FALLBACK_TERMINFO/\" \"$TERMINFO_DEST/\"\nfi\n# Overlay any cmux-specific terminfo adjustments.\n# This intentionally does not use --delete so we only patch specific entries.\nif [ -d \"$TERMINFO_OVERLAY\" ]; then\n mkdir -p \"$TERMINFO_DEST\"\n rsync -a \"$TERMINFO_OVERLAY/\" \"$TERMINFO_DEST/\"\nfi\nif [ -d \"$CMUX_SHELL_SRC\" ]; then\n mkdir -p \"$CMUX_SHELL_DEST\"\n # Use '/.' so dotfiles like .zshenv/.zprofile are copied too.\n rsync -a \"$CMUX_SHELL_SRC/.\" \"$CMUX_SHELL_DEST/\"\nfi\nif [ -f \"$CMUX_GHOSTTY_ZSH_SRC\" ]; then\n mkdir -p \"$CMUX_SHELL_DEST\"\n rsync -a \"$CMUX_GHOSTTY_ZSH_SRC\" \"$CMUX_SHELL_DEST/ghostty-integration.zsh\"\nfi\nINFO_PLIST=\"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\nCOMMIT=\"$(git -C \"${SRCROOT}\" rev-parse --short=9 HEAD 2>/dev/null || true)\"\nif [ -n \"$COMMIT\" ] && [ -f \"$INFO_PLIST\" ]; then\n /usr/libexec/PlistBuddy -c \"Set :CMUXCommit $COMMIT\" \"$INFO_PLIST\" >/dev/null 2>&1 || /usr/libexec/PlistBuddy -c \"Add :CMUXCommit string $COMMIT\" \"$INFO_PLIST\" >/dev/null 2>&1 || true\nfi\n"; + shellScript = "set -euo pipefail\nDEST=\"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}\"\nGHOSTTY_DEST=\"${DEST}/ghostty\"\nTERMINFO_DEST=\"${DEST}/terminfo\"\nCMUX_SHELL_DEST=\"${DEST}/shell-integration\"\nBIN_DEST=\"${DEST}/bin\"\nSRC_SHARE=\"${SRCROOT}/ghostty/zig-out/share\"\nGHOSTTY_SRC=\"${SRC_SHARE}/ghostty\"\nTERMINFO_SRC=\"${SRC_SHARE}/terminfo\"\nFALLBACK_GHOSTTY=\"${SRCROOT}/Resources/ghostty\"\nFALLBACK_TERMINFO=\"${SRCROOT}/Resources/ghostty/terminfo\"\nTERMINFO_OVERLAY=\"${SRCROOT}/Resources/terminfo-overlay\"\nCMUX_SHELL_SRC=\"${SRCROOT}/Resources/shell-integration\"\nCMUX_GHOSTTY_ZSH_SRC=\"${SRCROOT}/ghostty/src/shell-integration/zsh/ghostty-integration\"\nBUILD_GHOSTTY_HELPER=\"${SRCROOT}/scripts/build-ghostty-cli-helper.sh\"\nGHOSTTY_HELPER_DEST=\"${BIN_DEST}/ghostty\"\nif [ -d \"$GHOSTTY_SRC\" ]; then\n mkdir -p \"$GHOSTTY_DEST\"\n rsync -a --delete \"$GHOSTTY_SRC/\" \"$GHOSTTY_DEST/\"\nelif [ -d \"$FALLBACK_GHOSTTY\" ]; then\n mkdir -p \"$GHOSTTY_DEST\"\n rsync -a --delete \"$FALLBACK_GHOSTTY/\" \"$GHOSTTY_DEST/\"\nfi\nif [ -d \"$TERMINFO_SRC\" ]; then\n mkdir -p \"$TERMINFO_DEST\"\n rsync -a --delete \"$TERMINFO_SRC/\" \"$TERMINFO_DEST/\"\nelif [ -d \"$FALLBACK_TERMINFO\" ]; then\n mkdir -p \"$TERMINFO_DEST\"\n rsync -a --delete \"$FALLBACK_TERMINFO/\" \"$TERMINFO_DEST/\"\nfi\n# Overlay any cmux-specific terminfo adjustments.\n# This intentionally does not use --delete so we only patch specific entries.\nif [ -d \"$TERMINFO_OVERLAY\" ]; then\n mkdir -p \"$TERMINFO_DEST\"\n rsync -a \"$TERMINFO_OVERLAY/\" \"$TERMINFO_DEST/\"\nfi\nif [ -d \"$CMUX_SHELL_SRC\" ]; then\n mkdir -p \"$CMUX_SHELL_DEST\"\n # Use '/.' so dotfiles like .zshenv/.zprofile are copied too.\n rsync -a \"$CMUX_SHELL_SRC/.\" \"$CMUX_SHELL_DEST/\"\nfi\nif [ -f \"$CMUX_GHOSTTY_ZSH_SRC\" ]; then\n mkdir -p \"$CMUX_SHELL_DEST\"\n rsync -a \"$CMUX_GHOSTTY_ZSH_SRC\" \"$CMUX_SHELL_DEST/ghostty-integration.zsh\"\nfi\nif [ ! -x \"$BUILD_GHOSTTY_HELPER\" ]; then\n echo \"error: missing Ghostty CLI helper build script at $BUILD_GHOSTTY_HELPER\" >&2\n exit 1\nfi\nARCHS_LIST=\" ${ARCHS:-} \"\nHAS_ARM64=0\nHAS_X86_64=0\nGHOSTTY_HELPER_TARGET=\"\"\ncase \"$ARCHS_LIST\" in\n *\" arm64 \"*) HAS_ARM64=1 ;;\nesac\ncase \"$ARCHS_LIST\" in\n *\" x86_64 \"*) HAS_X86_64=1 ;;\nesac\nif [ \"$HAS_ARM64\" -eq 1 ] && [ \"$HAS_X86_64\" -eq 1 ]; then\n \"$BUILD_GHOSTTY_HELPER\" --universal --output \"$GHOSTTY_HELPER_DEST\"\nelif [ \"$HAS_ARM64\" -eq 1 ]; then\n GHOSTTY_HELPER_TARGET=\"aarch64-macos\"\nelif [ \"$HAS_X86_64\" -eq 1 ]; then\n GHOSTTY_HELPER_TARGET=\"x86_64-macos\"\nfi\nif [ -n \"$GHOSTTY_HELPER_TARGET\" ]; then\n \"$BUILD_GHOSTTY_HELPER\" --target \"$GHOSTTY_HELPER_TARGET\" --output \"$GHOSTTY_HELPER_DEST\"\nelif [ \"$HAS_ARM64\" -eq 0 ] || [ \"$HAS_X86_64\" -eq 0 ]; then\n \"$BUILD_GHOSTTY_HELPER\" --output \"$GHOSTTY_HELPER_DEST\"\nfi\nif [ ! -x \"$GHOSTTY_HELPER_DEST\" ]; then\n echo \"error: Ghostty CLI helper was not created at $GHOSTTY_HELPER_DEST\" >&2\n exit 1\nfi\nINFO_PLIST=\"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\nCOMMIT=\"$(git -C \"${SRCROOT}\" rev-parse --short=9 HEAD 2>/dev/null || true)\"\nif [ -n \"$COMMIT\" ] && [ -f \"$INFO_PLIST\" ]; then\n /usr/libexec/PlistBuddy -c \"Set :CMUXCommit $COMMIT\" \"$INFO_PLIST\" >/dev/null 2>&1 || /usr/libexec/PlistBuddy -c \"Add :CMUXCommit string $COMMIT\" \"$INFO_PLIST\" >/dev/null 2>&1 || true\nfi\n"; }; /* End PBXShellScriptBuildPhase section */ diff --git a/scripts/build-ghostty-cli-helper.sh b/scripts/build-ghostty-cli-helper.sh new file mode 100755 index 00000000..ac8cda4b --- /dev/null +++ b/scripts/build-ghostty-cli-helper.sh @@ -0,0 +1,127 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: ./scripts/build-ghostty-cli-helper.sh [--universal | --target ] --output + +Options: + --universal Build a universal macOS helper (arm64 + x86_64). + --target + Build a single target, e.g. `aarch64-macos` or `x86_64-macos`. + --output Destination path for the built helper. +EOF +} + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +GHOSTTY_DIR="$REPO_ROOT/ghostty" + +OUTPUT_PATH="" +TARGET_TRIPLE="" +UNIVERSAL="false" + +while [[ $# -gt 0 ]]; do + case "$1" in + --universal) + UNIVERSAL="true" + shift + ;; + --target) + TARGET_TRIPLE="${2:-}" + shift 2 + ;; + --output) + OUTPUT_PATH="${2:-}" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown option: $1" >&2 + usage >&2 + exit 1 + ;; + esac +done + +if [[ -z "$OUTPUT_PATH" ]]; then + echo "Missing required --output path" >&2 + usage >&2 + exit 1 +fi + +if [[ "$UNIVERSAL" == "true" && -n "$TARGET_TRIPLE" ]]; then + echo "--universal and --target are mutually exclusive" >&2 + usage >&2 + exit 1 +fi + +if [[ -n "$TARGET_TRIPLE" ]]; then + case "$TARGET_TRIPLE" in + aarch64-macos|x86_64-macos) + ;; + *) + echo "Unsupported --target value: $TARGET_TRIPLE" >&2 + exit 1 + ;; + esac +fi + +if ! command -v zig >/dev/null 2>&1; then + echo "error: zig is required to build the Ghostty CLI helper" >&2 + exit 1 +fi + +if [[ ! -f "$GHOSTTY_DIR/build.zig" ]]; then + echo "error: Ghostty submodule is missing at $GHOSTTY_DIR" >&2 + exit 1 +fi + +build_helper() { + local prefix="$1" + local target="${2:-}" + local args=( + zig build + cli-helper + -Dapp-runtime=none + -Demit-macos-app=false + -Demit-xcframework=false + -Doptimize=ReleaseFast + --prefix + "$prefix" + ) + + if [[ -n "$target" ]]; then + args+=("-Dtarget=$target") + fi + + ( + cd "$GHOSTTY_DIR" + "${args[@]}" + ) +} + +TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/cmux-ghostty-helper.XXXXXX")" +trap 'rm -rf "$TMP_DIR"' EXIT + +mkdir -p "$(dirname "$OUTPUT_PATH")" + +if [[ "$UNIVERSAL" == "true" ]]; then + ARM64_PREFIX="$TMP_DIR/arm64" + X86_PREFIX="$TMP_DIR/x86_64" + build_helper "$ARM64_PREFIX" "aarch64-macos" + build_helper "$X86_PREFIX" "x86_64-macos" + /usr/bin/lipo -create \ + "$ARM64_PREFIX/bin/ghostty" \ + "$X86_PREFIX/bin/ghostty" \ + -output "$OUTPUT_PATH" +else + SINGLE_PREFIX="$TMP_DIR/single" + build_helper "$SINGLE_PREFIX" "$TARGET_TRIPLE" + install -m 755 "$SINGLE_PREFIX/bin/ghostty" "$OUTPUT_PATH" +fi + +chmod +x "$OUTPUT_PATH" diff --git a/scripts/build-sign-upload.sh b/scripts/build-sign-upload.sh index 08d1f84c..86ec511e 100755 --- a/scripts/build-sign-upload.sh +++ b/scripts/build-sign-upload.sh @@ -74,6 +74,12 @@ rm -rf build/ xcodebuild -scheme cmux -configuration Release -derivedDataPath build CODE_SIGNING_ALLOWED=NO build 2>&1 | tail -5 echo "Build succeeded" +HELPER_PATH="$APP_PATH/Contents/Resources/bin/ghostty" +if [ ! -x "$HELPER_PATH" ]; then + echo "Ghostty theme picker helper not found at $HELPER_PATH" >&2 + exit 1 +fi + # --- Inject Sparkle keys --- echo "Injecting Sparkle keys..." SPARKLE_PUBLIC_KEY_DERIVED=$(swift scripts/derive_sparkle_public_key.swift "$SPARKLE_PRIVATE_KEY") @@ -90,6 +96,9 @@ CLI_PATH="$APP_PATH/Contents/Resources/bin/cmux" if [ -f "$CLI_PATH" ]; then /usr/bin/codesign --force --options runtime --timestamp --sign "$SIGN_HASH" --entitlements "$ENTITLEMENTS" "$CLI_PATH" fi +if [ -f "$HELPER_PATH" ]; then + /usr/bin/codesign --force --options runtime --timestamp --sign "$SIGN_HASH" --entitlements "$ENTITLEMENTS" "$HELPER_PATH" +fi /usr/bin/codesign --force --options runtime --timestamp --sign "$SIGN_HASH" --entitlements "$ENTITLEMENTS" --deep "$APP_PATH" /usr/bin/codesign --verify --deep --strict --verbose=2 "$APP_PATH" echo "Codesign verified"