Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,7 @@ jobs:
xcrun stapler validate "$app_path"
spctl -a -vv --type execute "$app_path"
./scripts/smoke-launch-macos-app.sh "$app_path"
./scripts/verify-app-bundle-channel-metadata.sh "$app_path" nightly
rm -f "$zip_submit"

dmg_tmp_dir="$(mktemp -d)"
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ jobs:
xcrun stapler validate "$APP_PATH"
spctl -a -vv --type execute "$APP_PATH"
./scripts/smoke-launch-macos-app.sh "$APP_PATH"
./scripts/verify-app-bundle-channel-metadata.sh "$APP_PATH" stable
rm -f "$ZIP_SUBMIT"
# create-dmg generates a styled drag-to-install DMG
create-dmg \
Expand Down
23 changes: 23 additions & 0 deletions Sources/AppBundleIconPersistencePolicy.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Foundation

enum AppBundleIconPersistencePolicy {
private static let stableReleaseBundleIdentifier = "com.cmuxterm.app"
private static let stableReleaseAppBundleName = "cmux.app"
static let disablePersistenceArgument = "--cmux-disable-bundle-icon-persistence"

static func shouldPersist(
bundleIdentifier: String?,
appBundleLastPathComponent: String?,
launchArguments: [String] = ProcessInfo.processInfo.arguments
) -> Bool {
guard !launchArguments.contains(disablePersistenceArgument) else {
return false
}

// Channel variants own their identity through build-time bundle metadata.
// Persisted Finder icons would override that metadata and can leak into
// packaged artifacts after CI smoke launches the app bundle.
return bundleIdentifier == stableReleaseBundleIdentifier
&& appBundleLastPathComponent == stableReleaseAppBundleName
}
}
7 changes: 4 additions & 3 deletions Sources/AppIconDockTilePlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,10 @@ final class CmuxDockTilePlugin: NSObject, NSDockTilePlugIn {

private var shouldPersistBundleIcon: Bool {
guard let appBundleURL else { return false }
// The default untagged Debug app is rebuilt and re-signed in place during CI.
// Persisting a custom icon there leaves Finder metadata behind and breaks codesign.
return appBundleURL.lastPathComponent != "cmux DEV.app"
return AppBundleIconPersistencePolicy.shouldPersist(
bundleIdentifier: appBundle?.bundleIdentifier,
appBundleLastPathComponent: appBundleURL.lastPathComponent
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}

private var appDefaults: UserDefaults? {
Expand Down
6 changes: 6 additions & 0 deletions cmux.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@
D7AB34300000000000000005 /* SidebarWorkspaceDropPlannerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AB34300000000000000006 /* SidebarWorkspaceDropPlannerTests.swift */; };
D7AB34400000000000000001 /* WorkspaceSurfaceConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AB34400000000000000002 /* WorkspaceSurfaceConfig.swift */; };
D7AB34400000000000000003 /* GhosttyTerminalViewVisibilityPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AB34400000000000000004 /* GhosttyTerminalViewVisibilityPolicyTests.swift */; };
F4350A110000000000000001 /* AppBundleIconPersistencePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4350A130000000000000001 /* AppBundleIconPersistencePolicy.swift */; };
F4350A120000000000000001 /* AppBundleIconPersistencePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4350A130000000000000001 /* AppBundleIconPersistencePolicy.swift */; };
5EDB6027B346C46521A93C74 /* CMUXAuthCore in Frameworks */ = {isa = PBXBuildFile; productRef = 29813FE5A6CBC1019289A251 /* CMUXAuthCore */; };
AA11BB22CC33DD44EE550001 /* CMUXWorkstream in Frameworks */ = {isa = PBXBuildFile; productRef = AA11BB22CC33DD44EE550002 /* CMUXWorkstream */; };
3069F1D10000000000000005 /* CMUXPasteboardFidelity in Frameworks */ = {isa = PBXBuildFile; productRef = 3069F1D10000000000000006 /* CMUXPasteboardFidelity */; };
Expand Down Expand Up @@ -963,6 +965,7 @@
D0E0F0B3A1B2C3D4E5F60718 /* BrowserOmnibarSuggestionsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserOmnibarSuggestionsUITests.swift; sourceTree = "<group>"; };
D1320AA0D1320AA0D1320AA4 /* AppIconDockTilePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconDockTilePlugin.swift; sourceTree = "<group>"; };
D1320AA0D1320AA0D1320AA5 /* CmuxDockTilePlugin.plugin */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CmuxDockTilePlugin.plugin; sourceTree = BUILT_PRODUCTS_DIR; };
F4350A130000000000000001 /* AppBundleIconPersistencePolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppBundleIconPersistencePolicy.swift; sourceTree = "<group>"; };
D1BEF00001A1B2C3D4E5F719 /* open */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = Resources/bin/open; sourceTree = SOURCE_ROOT; };
D2C075029771815DD5DA1332 /* NotificationAndMenuBarTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationAndMenuBarTests.swift; sourceTree = "<group>"; };
DA7A10CA710E000000000001 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1332,6 +1335,7 @@
A5354302A5354302A5354302 /* SocketControlSettings+SocketPathMarkers.swift */,
A5001600 /* SentryHelper.swift */,
A5001620 /* AppleScriptSupport.swift */,
F4350A130000000000000001 /* AppBundleIconPersistencePolicy.swift */,
D1320AA0D1320AA0D1320AA4 /* AppIconDockTilePlugin.swift */,
A5001090 /* AppDelegate.swift */,
3865B0063865B0063865B006 /* AppDelegate+GlobalSearch.swift */,
Expand Down Expand Up @@ -2068,6 +2072,7 @@
A5354301A5354301A5354301 /* SocketControlSettings+SocketPathMarkers.swift in Sources */,
A5001601 /* SentryHelper.swift in Sources */,
A5001621 /* AppleScriptSupport.swift in Sources */,
F4350A110000000000000001 /* AppBundleIconPersistencePolicy.swift in Sources */,
C3873001C3873001C3873001 /* GhosttyCrashBreadcrumb.swift in Sources */,
C3873003C3873003C3873003 /* GhosttyCrashReportMetadata.swift in Sources */,
A5001093 /* AppDelegate.swift in Sources */,
Expand Down Expand Up @@ -2239,6 +2244,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
F4350A120000000000000001 /* AppBundleIconPersistencePolicy.swift in Sources */,
D1320AA0D1320AA0D1320AA1 /* AppIconDockTilePlugin.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
64 changes: 64 additions & 0 deletions cmuxTests/AppearanceSettingsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,70 @@ import SwiftUI

@MainActor
final class AppearanceSettingsTests: XCTestCase {
func testBundleIconPersistenceAllowsStableReleaseBundle() {
XCTAssertTrue(
AppBundleIconPersistencePolicy.shouldPersist(
bundleIdentifier: "com.cmuxterm.app",
appBundleLastPathComponent: "cmux.app",
launchArguments: []
)
)
}

func testBundleIconPersistenceSkipsNightlyBundles() {
XCTAssertFalse(
AppBundleIconPersistencePolicy.shouldPersist(
bundleIdentifier: "com.cmuxterm.app.nightly",
appBundleLastPathComponent: "cmux NIGHTLY.app",
launchArguments: []
)
)
XCTAssertFalse(
AppBundleIconPersistencePolicy.shouldPersist(
bundleIdentifier: "com.cmuxterm.app.nightly.issue-4350",
appBundleLastPathComponent: "cmux NIGHTLY issue-4350.app",
launchArguments: []
)
)
}

func testBundleIconPersistenceRejectsMismatchedStableIdentifierAndPath() {
XCTAssertFalse(
AppBundleIconPersistencePolicy.shouldPersist(
bundleIdentifier: "com.cmuxterm.app",
appBundleLastPathComponent: "cmux NIGHTLY.app",
launchArguments: []
)
)
}

func testBundleIconPersistenceSkipsDebugBundles() {
XCTAssertFalse(
AppBundleIconPersistencePolicy.shouldPersist(
bundleIdentifier: "com.cmuxterm.app.debug",
appBundleLastPathComponent: "cmux DEV.app",
launchArguments: []
)
)
XCTAssertFalse(
AppBundleIconPersistencePolicy.shouldPersist(
bundleIdentifier: "com.cmuxterm.app.debug.issue-4350",
appBundleLastPathComponent: "cmux DEV issue-4350.app",
launchArguments: []
)
)
}

func testBundleIconPersistenceHonorsSmokeLaunchDisableArgument() {
XCTAssertFalse(
AppBundleIconPersistencePolicy.shouldPersist(
bundleIdentifier: "com.cmuxterm.app",
appBundleLastPathComponent: "cmux.app",
launchArguments: [AppBundleIconPersistencePolicy.disablePersistenceArgument]
)
)
}

func testAppConfigReloadRefreshUpdatesSurfaceConfigBeforeRedraw() throws {
let fakeSurface = try XCTUnwrap(UnsafeMutableRawPointer(bitPattern: 0x3851))
var events: [String] = []
Expand Down
2 changes: 1 addition & 1 deletion scripts/smoke-launch-macos-app.sh
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ dump_system_log() {
}

echo "==> smoke launching $APP_PATH"
/usr/bin/open -n -g "$APP_PATH" --args -ApplePersistenceIgnoreState YES >"$OPEN_LOG" 2>&1 &
/usr/bin/open -n -g "$APP_PATH" --args -ApplePersistenceIgnoreState YES --cmux-disable-bundle-icon-persistence >"$OPEN_LOG" 2>&1 &
OPEN_PID=$!

# CI-only LaunchServices smoke: open returns before the app process is visible.
Expand Down
92 changes: 92 additions & 0 deletions scripts/verify-app-bundle-channel-metadata.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#!/usr/bin/env bash
set -euo pipefail

usage() {
cat <<'EOF'
usage: verify-app-bundle-channel-metadata.sh <app-path> <stable|nightly>

Verifies the built app bundle metadata that LaunchServices and Sparkle use for
system prompts. This intentionally checks the final bundle artifact, not the
checked-in plist template.
EOF
}

if [[ $# -ne 2 ]]; then
usage >&2
exit 2
fi

APP_PATH="$1"
CHANNEL="$2"
INFO_PLIST="$APP_PATH/Contents/Info.plist"

if [[ ! -d "$APP_PATH/Contents" ]]; then
echo "error: app bundle not found at $APP_PATH" >&2
exit 1
fi

if [[ ! -f "$INFO_PLIST" ]]; then
echo "error: Info.plist not found at $INFO_PLIST" >&2
exit 1
fi

case "$CHANNEL" in
stable)
EXPECTED_NAME="cmux"
EXPECTED_BUNDLE_ID="com.cmuxterm.app"
EXPECTED_ICON_NAME="AppIcon"
;;
nightly)
EXPECTED_NAME="cmux NIGHTLY"
EXPECTED_BUNDLE_ID="com.cmuxterm.app.nightly"
EXPECTED_ICON_NAME="AppIcon-Nightly"
;;
*)
usage >&2
exit 2
;;
esac

plist_value() {
/usr/libexec/PlistBuddy -c "Print :$1" "$INFO_PLIST"
}

expect_plist_value() {
local key="$1"
local expected="$2"
local actual
actual="$(plist_value "$key" 2>/dev/null || true)"
if [[ "$actual" != "$expected" ]]; then
echo "error: $key expected '$expected', found '${actual:-<missing>}'" >&2
exit 1
fi
}

expect_plist_value CFBundleName "$EXPECTED_NAME"
expect_plist_value CFBundleDisplayName "$EXPECTED_NAME"
expect_plist_value CFBundleIdentifier "$EXPECTED_BUNDLE_ID"
expect_plist_value CFBundleIconFile "$EXPECTED_ICON_NAME"
expect_plist_value CFBundleIconName "$EXPECTED_ICON_NAME"

ICON_PATH="$APP_PATH/Contents/Resources/${EXPECTED_ICON_NAME}.icns"
if [[ ! -s "$ICON_PATH" ]]; then
echo "error: expected icon file missing or empty at $ICON_PATH" >&2
exit 1
fi

CUSTOM_ICON_PATH="$APP_PATH/Icon"$'\r'
if [[ -e "$CUSTOM_ICON_PATH" ]]; then
echo "error: app bundle contains Finder custom icon file: $CUSTOM_ICON_PATH" >&2
exit 1
fi

if xattr -p com.apple.FinderInfo "$APP_PATH" >/dev/null 2>&1; then
echo "error: app bundle has com.apple.FinderInfo; packaged apps must use Info.plist icon metadata" >&2
exit 1
fi

if [[ -d "$APP_PATH/Contents/_CodeSignature" ]]; then
codesign --verify --deep --strict --verbose=2 "$APP_PATH"
fi

echo "verified $CHANNEL app bundle metadata: $APP_PATH"
Loading