From c4bc18d906b8b6faba9bb07f077e335f3987f4d9 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Sat, 28 Mar 2026 03:05:00 -0700 Subject: [PATCH] Fix ARC workspace inheritance crash and native Zig helper builds (#2283) * Fix ARC workspace inheritance crash and native Zig helper builds * Fix Nightly Cmd+N workspace creation crash * Restore safe terminal config snapshots for Intel Nightly --- Sources/GhosttyTerminalView.swift | 67 ++++---- Sources/Panels/TerminalPanel.swift | 2 +- Sources/TabManager.swift | 256 +++++++++++++++------------- Sources/Workspace.swift | 76 +++++++-- cmuxTests/WorkspaceUnitTests.swift | 53 ++---- scripts/build-ghostty-cli-helper.sh | 24 ++- 6 files changed, 265 insertions(+), 213 deletions(-) diff --git a/Sources/GhosttyTerminalView.swift b/Sources/GhosttyTerminalView.swift index a8fdb7c6..978ab9df 100644 --- a/Sources/GhosttyTerminalView.swift +++ b/Sources/GhosttyTerminalView.swift @@ -2973,7 +2973,7 @@ final class TerminalSurface: Identifiable, ObservableObject { return val > 0 ? val : 10 }() private let surfaceContext: ghostty_surface_context_e - private let configTemplate: ghostty_surface_config_s? + private let configTemplate: CmuxSurfaceConfigTemplate? private let workingDirectory: String? private let initialCommand: String? private let initialEnvironmentOverrides: [String: String] @@ -3061,7 +3061,7 @@ final class TerminalSurface: Identifiable, ObservableObject { init( tabId: UUID, context: ghostty_surface_context_e, - configTemplate: ghostty_surface_config_s?, + configTemplate: CmuxSurfaceConfigTemplate?, workingDirectory: String? = nil, initialCommand: String? = nil, initialEnvironmentOverrides: [String: String] = [:], @@ -3585,7 +3585,10 @@ final class TerminalSurface: Identifiable, ObservableObject { let scaleFactors = scaleFactors(for: view) - var surfaceConfig = configTemplate ?? ghostty_surface_config_new() + let baseConfig = configTemplate ?? CmuxSurfaceConfigTemplate() + var surfaceConfig = ghostty_surface_config_new() + surfaceConfig.font_size = baseConfig.fontSize + surfaceConfig.wait_after_command = baseConfig.waitAfterCommand surfaceConfig.platform_tag = GHOSTTY_PLATFORM_MACOS surfaceConfig.platform = ghostty_platform_u(macos: ghostty_platform_macos_s( nsview: Unmanaged.passUnretained(view).toOpaque() @@ -3612,19 +3615,7 @@ final class TerminalSurface: Identifiable, ObservableObject { } } - var env: [String: String] = [:] - if surfaceConfig.env_var_count > 0, let existingEnv = surfaceConfig.env_vars { - let count = Int(surfaceConfig.env_var_count) - if count > 0 { - for i in 0.. = [] func setManagedEnvironmentValue(_ key: String, _ value: String) { @@ -3762,26 +3753,36 @@ final class TerminalSurface: Identifiable, ObservableObject { } } - let createWithCommandAndWorkingDirectory = { [self] in + let resolvedWorkingDirectory: String? = { + if let workingDirectory, !workingDirectory.isEmpty { + return workingDirectory + } + return baseConfig.workingDirectory + }() + let resolvedCommand: String? = { if let initialCommand, !initialCommand.isEmpty { - initialCommand.withCString { cCommand in - surfaceConfig.command = cCommand - if let workingDirectory, !workingDirectory.isEmpty { - workingDirectory.withCString { cWorkingDir in - surfaceConfig.working_directory = cWorkingDir - createSurface() - } - } else { + return initialCommand + } + return baseConfig.command + }() + let resolvedInitialInput = baseConfig.initialInput + func withOptionalCString(_ value: String?, _ body: (UnsafePointer?) -> T) -> T { + guard let value else { + return body(nil) + } + return value.withCString(body) + } + + let createWithCommandAndWorkingDirectory = { + withOptionalCString(resolvedCommand) { cCommand in + surfaceConfig.command = cCommand + withOptionalCString(resolvedWorkingDirectory) { cWorkingDir in + surfaceConfig.working_directory = cWorkingDir + withOptionalCString(resolvedInitialInput) { cInitialInput in + surfaceConfig.initial_input = cInitialInput createSurface() } } - } else if let workingDirectory, !workingDirectory.isEmpty { - workingDirectory.withCString { cWorkingDir in - surfaceConfig.working_directory = cWorkingDir - createSurface() - } - } else { - createSurface() } } @@ -3844,7 +3845,7 @@ final class TerminalSurface: Identifiable, ObservableObject { // config/scale reconciliation. If runtime points don't match the inherited // template points, re-apply via binding action so all creation paths // (new surface, split, new workspace) preserve zoom from the source terminal. - if let inheritedFontPoints = configTemplate?.font_size, + if let inheritedFontPoints = configTemplate?.fontSize, inheritedFontPoints > 0 { let currentFontPoints = cmuxCurrentSurfaceFontSizePoints(createdSurface) let shouldReapply = { diff --git a/Sources/Panels/TerminalPanel.swift b/Sources/Panels/TerminalPanel.swift index 0fabb34a..c5d3a79f 100644 --- a/Sources/Panels/TerminalPanel.swift +++ b/Sources/Panels/TerminalPanel.swift @@ -90,7 +90,7 @@ final class TerminalPanel: Panel, ObservableObject { convenience init( workspaceId: UUID, context: ghostty_surface_context_e = GHOSTTY_SURFACE_CONTEXT_SPLIT, - configTemplate: ghostty_surface_config_s? = nil, + configTemplate: CmuxSurfaceConfigTemplate? = nil, workingDirectory: String? = nil, portOrdinal: Int = 0, initialCommand: String? = nil, diff --git a/Sources/TabManager.swift b/Sources/TabManager.swift index 9776cf03..10ced5dc 100644 --- a/Sources/TabManager.swift +++ b/Sources/TabManager.swift @@ -1155,7 +1155,7 @@ class TabManager: ObservableObject { title: String, workingDirectory: String?, portOrdinal: Int, - configTemplate: ghostty_surface_config_s?, + configTemplate: CmuxSurfaceConfigTemplate?, initialTerminalCommand: String?, initialTerminalEnvironment: [String: String] ) -> Workspace { @@ -1208,105 +1208,106 @@ class TabManager: ObservableObject { placementOverride: NewWorkspacePlacement? = nil, autoWelcomeIfNeeded: Bool = true ) -> Workspace { - // Extract Workspace-dependent data through `self` BEFORE capturing locals. - // Accessing through `self` is safe because the method retains `self` for its - // duration, keeping all Workspace objects reachable via `self.tabs`. Local copies - // of Workspace references (like a `selectedWorkspace` local) are vulnerable to - // Xcode 16.x's aggressive ARC optimizer eliding retains through inlined call - // chains (workspace → panel → surface → C pointer), causing use-after-free. - let preferredDir = preferredWorkingDirectoryForNewTab() - let inheritedFontPoints = inheritedTerminalFontPointsForNewWorkspace() - + let sourceWorkspace = selectedWorkspace let capturedTabs = tabs - let capturedSelectedTabId = selectedTabId - - let snapshot = workspaceCreationSnapshotLite( - currentTabs: capturedTabs, - currentSelectedTabId: capturedSelectedTabId, - preferredWorkingDirectory: preferredDir, - inheritedTerminalFontPoints: inheritedFontPoints - ) - didCaptureWorkspaceCreationSnapshot() -#if DEBUG - maybeMutateSelectionDuringWorkspaceCreationForDev(snapshot: snapshot) -#endif - let nextTabCount = snapshot.tabs.count + 1 - sentryBreadcrumb("workspace.create", data: ["tabCount": nextTabCount]) - let explicitWorkingDirectory = normalizedWorkingDirectory(overrideWorkingDirectory) - let workingDirectory = explicitWorkingDirectory ?? snapshot.preferredWorkingDirectory - let inheritedConfig = workspaceCreationConfigTemplate( - inheritedTerminalFontPoints: snapshot.inheritedTerminalFontPoints - ) - // Resolve placement against the pre-creation snapshot before Workspace init - // boots terminal state. The ssh/new-workspace path can otherwise crash while - // reading @Published placement state from existing workspaces mid-creation. - let insertIndex = newTabInsertIndex(snapshot: snapshot, placementOverride: placementOverride) - let ordinal = Self.nextPortOrdinal - Self.nextPortOrdinal += 1 - let newWorkspace = makeWorkspaceForCreation( - title: title ?? "Terminal \(nextTabCount)", - workingDirectory: workingDirectory, - portOrdinal: ordinal, - configTemplate: inheritedConfig, - initialTerminalCommand: initialTerminalCommand, - initialTerminalEnvironment: initialTerminalEnvironment - ) - newWorkspace.owningTabManager = self - if title != nil { - newWorkspace.setCustomTitle(title) - } - wireClosedBrowserTracking(for: newWorkspace) - if eagerLoadTerminal && !select { - requestBackgroundWorkspaceLoad(for: newWorkspace.id) - } - // Apply insertion to the current live array so post-snapshot closes/reorders - // are preserved instead of reintroducing stale workspace instances. - var updatedTabs = tabs - if insertIndex >= 0 && insertIndex <= updatedTabs.count { - updatedTabs.insert(newWorkspace, at: insertIndex) - } else { - updatedTabs.append(newWorkspace) - } - tabs = updatedTabs - if let explicitWorkingDirectory, - let terminalPanel = newWorkspace.focusedTerminalPanel { - scheduleInitialWorkspaceGitMetadataRefresh( - workspaceId: newWorkspace.id, - panelId: terminalPanel.id, - directory: explicitWorkingDirectory + // Snapshot the selected tab from the pinned workspace instead of rereading the + // @Published selectedTabId storage after the inheritance helpers. The arm64 Nightly + // Cmd+N crash is in PublishedSubject.value.getter on that second getter read. + let capturedSelectedTabId = sourceWorkspace?.id + // Keep both the source workspace and the pre-creation workspace array alive for the + // entire creation path. Release ARC can otherwise drop retains early across the + // helper/insertion chain, which reintroduces use-after-free crashes in optimized builds. + return withExtendedLifetime((capturedTabs, sourceWorkspace)) { + let dir = preferredWorkingDirectoryForNewTab(workspace: sourceWorkspace) + let font = inheritedTerminalFontPointsForNewWorkspace(workspace: sourceWorkspace) + let snapshot = workspaceCreationSnapshotLite( + currentTabs: capturedTabs, + currentSelectedTabId: capturedSelectedTabId, + preferredWorkingDirectory: dir, + inheritedTerminalFontPoints: font ) - } - if eagerLoadTerminal { - if select { - newWorkspace.focusedTerminalPanel?.surface.requestBackgroundSurfaceStartIfNeeded() + didCaptureWorkspaceCreationSnapshot() +#if DEBUG + maybeMutateSelectionDuringWorkspaceCreationForDev(snapshot: snapshot) +#endif + let nextTabCount = snapshot.tabs.count + 1 + sentryBreadcrumb("workspace.create", data: ["tabCount": nextTabCount]) + let explicitWorkingDirectory = normalizedWorkingDirectory(overrideWorkingDirectory) + let workingDirectory = explicitWorkingDirectory ?? snapshot.preferredWorkingDirectory + let inheritedConfig = workspaceCreationConfigTemplate( + inheritedTerminalFontPoints: snapshot.inheritedTerminalFontPoints + ) + // Resolve placement against the pre-creation snapshot before Workspace init + // boots terminal state. The ssh/new-workspace path can otherwise crash while + // reading @Published placement state from existing workspaces mid-creation. + let insertIndex = newTabInsertIndex(snapshot: snapshot, placementOverride: placementOverride) + let ordinal = Self.nextPortOrdinal + Self.nextPortOrdinal += 1 + let newWorkspace = makeWorkspaceForCreation( + title: title ?? "Terminal \(nextTabCount)", + workingDirectory: workingDirectory, + portOrdinal: ordinal, + configTemplate: inheritedConfig, + initialTerminalCommand: initialTerminalCommand, + initialTerminalEnvironment: initialTerminalEnvironment + ) + newWorkspace.owningTabManager = self + if title != nil { + newWorkspace.setCustomTitle(title) } - } - if select { -#if DEBUG - debugPrimeWorkspaceSwitchTrigger("create", to: newWorkspace.id) -#endif - selectedTabId = newWorkspace.id - NotificationCenter.default.post( - name: .ghosttyDidFocusTab, - object: nil, - userInfo: [GhosttyNotificationKey.tabId: newWorkspace.id] - ) - } -#if DEBUG - UITestRecorder.incrementInt("addTabInvocations") - UITestRecorder.record([ - "tabCount": String(updatedTabs.count), - "selectedTabId": select ? newWorkspace.id.uuidString : (snapshot.selectedTabId?.uuidString ?? "") - ]) -#endif - if autoWelcomeIfNeeded && select && !UserDefaults.standard.bool(forKey: WelcomeSettings.shownKey) { - if let appDelegate = AppDelegate.shared { - appDelegate.sendWelcomeCommandWhenReady(to: newWorkspace, markShownOnSend: true) + wireClosedBrowserTracking(for: newWorkspace) + if eagerLoadTerminal && !select { + requestBackgroundWorkspaceLoad(for: newWorkspace.id) + } + // Apply insertion to the current live array so post-snapshot closes/reorders + // are preserved instead of reintroducing stale workspace instances. + var updatedTabs = tabs + if insertIndex >= 0 && insertIndex <= updatedTabs.count { + updatedTabs.insert(newWorkspace, at: insertIndex) } else { - sendWelcomeWhenReady(to: newWorkspace) + updatedTabs.append(newWorkspace) } + tabs = updatedTabs + if let explicitWorkingDirectory, + let terminalPanel = newWorkspace.focusedTerminalPanel { + scheduleInitialWorkspaceGitMetadataRefresh( + workspaceId: newWorkspace.id, + panelId: terminalPanel.id, + directory: explicitWorkingDirectory + ) + } + if eagerLoadTerminal { + if select { + newWorkspace.focusedTerminalPanel?.surface.requestBackgroundSurfaceStartIfNeeded() + } + } + if select { +#if DEBUG + debugPrimeWorkspaceSwitchTrigger("create", to: newWorkspace.id) +#endif + selectedTabId = newWorkspace.id + NotificationCenter.default.post( + name: .ghosttyDidFocusTab, + object: nil, + userInfo: [GhosttyNotificationKey.tabId: newWorkspace.id] + ) + } +#if DEBUG + UITestRecorder.incrementInt("addTabInvocations") + UITestRecorder.record([ + "tabCount": String(updatedTabs.count), + "selectedTabId": select ? newWorkspace.id.uuidString : (snapshot.selectedTabId?.uuidString ?? "") + ]) +#endif + if autoWelcomeIfNeeded && select && !UserDefaults.standard.bool(forKey: WelcomeSettings.shownKey) { + if let appDelegate = AppDelegate.shared { + appDelegate.sendWelcomeCommandWhenReady(to: newWorkspace, markShownOnSend: true) + } else { + sendWelcomeWhenReady(to: newWorkspace) + } + } + return newWorkspace } - return newWorkspace } @MainActor @@ -2206,7 +2207,17 @@ class TabManager: ObservableObject { preferredWorkingDirectory: String?, inheritedTerminalFontPoints: Float? ) -> WorkspaceCreationSnapshot { - let tabSnapshots = currentTabs.map { WorkspaceCreationTabSnapshot(workspace: $0) } + var tabSnapshots: [WorkspaceCreationTabSnapshot] = [] + tabSnapshots.reserveCapacity(currentTabs.count) + for workspace in currentTabs { + // Keep each Workspace alive while copying the tiny value snapshot out of it. + // The optimized arm64 Nightly build can otherwise over-release during + // Collection.map, crashing here in swift_release / snapshot creation. + let snapshot = withExtendedLifetime(workspace) { + WorkspaceCreationTabSnapshot(workspace: workspace) + } + tabSnapshots.append(snapshot) + } let selectedTabSnapshot = currentSelectedTabId.flatMap { selectedTabId in tabSnapshots.first(where: { $0.id == selectedTabId }) } @@ -2281,26 +2292,36 @@ class TabManager: ObservableObject { return candidates.first } - private func inheritedTerminalConfigForNewWorkspace() -> ghostty_surface_config_s? { + private func inheritedTerminalConfigForNewWorkspace() -> CmuxSurfaceConfigTemplate? { inheritedTerminalConfigForNewWorkspace(workspace: selectedWorkspace) } + private func cachedInheritedTerminalFontPointsForNewWorkspace( + workspace: Workspace? + ) -> Float? { + guard let workspace else { return nil } + // New workspace creation only seeds font size into a fresh Swift-owned template. + // Avoid reading live panel/surface state here; the arm64 Nightly Cmd+N crash path + // was repeatedly dereferencing pointer-backed terminal objects while preparing the + // new workspace. The workspace already caches the rooted font lineage we need. + return withExtendedLifetime(workspace) { + guard let fontPoints = workspace.lastRememberedTerminalFontPointsForConfigInheritance(), + fontPoints > 0 else { + return nil + } + return fontPoints + } + } + func inheritedTerminalConfigForNewWorkspace( workspace: Workspace? - ) -> ghostty_surface_config_s? { - if let panel = terminalPanelForWorkspaceConfigInheritanceSource(workspace: workspace), - let sourceSurface = panel.surface.surface { - return cmuxInheritedSurfaceConfig( - sourceSurface: sourceSurface, - context: GHOSTTY_SURFACE_CONTEXT_TAB - ) + ) -> CmuxSurfaceConfigTemplate? { + guard let fontPoints = cachedInheritedTerminalFontPointsForNewWorkspace(workspace: workspace) else { + return nil } - if let fallbackFontPoints = workspace?.lastRememberedTerminalFontPointsForConfigInheritance() { - var config = ghostty_surface_config_new() - config.font_size = fallbackFontPoints - return config - } - return nil + var config = CmuxSurfaceConfigTemplate() + config.fontSize = fontPoints + return config } private func inheritedTerminalFontPointsForNewWorkspace() -> Float? { @@ -2310,24 +2331,19 @@ class TabManager: ObservableObject { private func inheritedTerminalFontPointsForNewWorkspace( workspace: Workspace? ) -> Float? { - guard let inheritedConfig = inheritedTerminalConfigForNewWorkspace(workspace: workspace), - inheritedConfig.font_size > 0 else { - return nil - } - return inheritedConfig.font_size + cachedInheritedTerminalFontPointsForNewWorkspace(workspace: workspace) } private func workspaceCreationConfigTemplate( inheritedTerminalFontPoints: Float? - ) -> ghostty_surface_config_s? { + ) -> CmuxSurfaceConfigTemplate? { guard let inheritedTerminalFontPoints, inheritedTerminalFontPoints > 0 else { return nil } - // ghostty_surface_config_s can carry raw C pointers owned by the source surface. - // New workspace creation only needs the inherited zoom level, so rebuild a clean - // config instead of snapshotting pointer-backed fields across workspace creation. - var config = ghostty_surface_config_new() - config.font_size = inheritedTerminalFontPoints + // Rebuild a clean Swift-owned template instead of carrying over any pointer-backed + // inherited config state from the source workspace. + var config = CmuxSurfaceConfigTemplate() + config.fontSize = inheritedTerminalFontPoints return config } diff --git a/Sources/Workspace.swift b/Sources/Workspace.swift index ab1b9c37..bd2fb799 100644 --- a/Sources/Workspace.swift +++ b/Sources/Workspace.swift @@ -8,6 +8,40 @@ import Darwin import Network import CoreText +struct CmuxSurfaceConfigTemplate { + var fontSize: Float32 = 0 + var workingDirectory: String? + var command: String? + var environmentVariables: [String: String] = [:] + var initialInput: String? + var waitAfterCommand: Bool = false + + init() {} + + init(cConfig: ghostty_surface_config_s) { + fontSize = cConfig.font_size + if let workingDirectory = cConfig.working_directory { + self.workingDirectory = String(cString: workingDirectory, encoding: .utf8) + } + if let command = cConfig.command { + self.command = String(cString: command, encoding: .utf8) + } + if let initialInput = cConfig.initial_input { + self.initialInput = String(cString: initialInput, encoding: .utf8) + } + if cConfig.env_var_count > 0, let envVars = cConfig.env_vars { + for index in 0.. String { switch context { case GHOSTTY_SURFACE_CONTEXT_WINDOW: @@ -54,21 +88,21 @@ func cmuxCurrentSurfaceFontSizePoints(_ surface: ghostty_surface_t) -> Float? { func cmuxInheritedSurfaceConfig( sourceSurface: ghostty_surface_t, context: ghostty_surface_context_e -) -> ghostty_surface_config_s { +) -> CmuxSurfaceConfigTemplate { let inherited = ghostty_surface_inherited_config(sourceSurface, context) - var config = inherited + var config = CmuxSurfaceConfigTemplate(cConfig: inherited) // Make runtime zoom inheritance explicit, even when Ghostty's // inherit-font-size config is disabled. let runtimePoints = cmuxCurrentSurfaceFontSizePoints(sourceSurface) if let points = runtimePoints { - config.font_size = points + config.fontSize = points } #if DEBUG let inheritedText = String(format: "%.2f", inherited.font_size) let runtimeText = runtimePoints.map { String(format: "%.2f", $0) } ?? "nil" - let finalText = String(format: "%.2f", config.font_size) + let finalText = String(format: "%.2f", config.fontSize) dlog( "zoom.inherit context=\(cmuxSurfaceContextName(context)) " + "inherited=\(inheritedText) runtime=\(runtimeText) final=\(finalText)" @@ -5664,7 +5698,7 @@ final class Workspace: Identifiable, ObservableObject { title: String = "Terminal", workingDirectory: String? = nil, portOrdinal: Int = 0, - configTemplate: ghostty_surface_config_s? = nil, + configTemplate: CmuxSurfaceConfigTemplate? = nil, initialTerminalCommand: String? = nil, initialTerminalEnvironment: [String: String] = [:] ) { @@ -7221,9 +7255,9 @@ final class Workspace: Identifiable, ObservableObject { private func seedTerminalInheritanceFontPoints( panelId: UUID, - configTemplate: ghostty_surface_config_s? + configTemplate: CmuxSurfaceConfigTemplate? ) { - guard let fontPoints = configTemplate?.font_size, fontPoints > 0 else { return } + guard let fontPoints = configTemplate?.fontSize, fontPoints > 0 else { return } terminalInheritanceFontPointsByPanelId[panelId] = fontPoints lastTerminalConfigInheritanceFontPoints = fontPoints } @@ -7231,7 +7265,7 @@ final class Workspace: Identifiable, ObservableObject { private func resolvedTerminalInheritanceFontPoints( for terminalPanel: TerminalPanel, sourceSurface: ghostty_surface_t, - inheritedConfig: ghostty_surface_config_s + inheritedConfig: CmuxSurfaceConfigTemplate ) -> Float? { let runtimePoints = cmuxCurrentSurfaceFontSizePoints(sourceSurface) if let rooted = terminalInheritanceFontPointsByPanelId[terminalPanel.id], rooted > 0 { @@ -7242,8 +7276,8 @@ final class Workspace: Identifiable, ObservableObject { } return rooted } - if inheritedConfig.font_size > 0 { - return inheritedConfig.font_size + if inheritedConfig.fontSize > 0 { + return inheritedConfig.fontSize } return runtimePoints } @@ -7341,14 +7375,20 @@ final class Workspace: Identifiable, ObservableObject { private func inheritedTerminalConfig( preferredPanelId: UUID? = nil, inPane preferredPaneId: PaneID? = nil - ) -> ghostty_surface_config_s? { + ) -> CmuxSurfaceConfigTemplate? { // Walk candidates in priority order and use the first panel that still exposes // a runtime surface pointer. for terminalPanel in terminalPanelConfigInheritanceCandidates( preferredPanelId: preferredPanelId, inPane: preferredPaneId ) { - guard let sourceSurface = terminalPanel.surface.surface else { continue } + // Pin the panel and its TerminalSurface wrapper for the duration of + // this iteration. The raw ghostty_surface_t extracted below is owned + // by `surface` (the TerminalSurface) — ARC must not release it while + // ghostty_surface_inherited_config or cmuxCurrentSurfaceFontSizePoints + // is still reading through the pointer. + let surface = terminalPanel.surface + guard let sourceSurface = surface.surface else { continue } var config = cmuxInheritedSurfaceConfig( sourceSurface: sourceSurface, context: GHOSTTY_SURFACE_CONTEXT_SPLIT @@ -7358,19 +7398,21 @@ final class Workspace: Identifiable, ObservableObject { sourceSurface: sourceSurface, inheritedConfig: config ), rootedFontPoints > 0 { - config.font_size = rootedFontPoints + config.fontSize = rootedFontPoints terminalInheritanceFontPointsByPanelId[terminalPanel.id] = rootedFontPoints } + // Prevent ARC from releasing panel/surface before the C calls above complete. + withExtendedLifetime((terminalPanel, surface)) {} rememberTerminalConfigInheritanceSource(terminalPanel) - if config.font_size > 0 { - lastTerminalConfigInheritanceFontPoints = config.font_size + if config.fontSize > 0 { + lastTerminalConfigInheritanceFontPoints = config.fontSize } return config } if let fallbackFontPoints = lastTerminalConfigInheritanceFontPoints { - var config = ghostty_surface_config_new() - config.font_size = fallbackFontPoints + var config = CmuxSurfaceConfigTemplate() + config.fontSize = fallbackFontPoints #if DEBUG dlog( "zoom.inherit fallback=lastKnownFont context=split font=\(String(format: "%.2f", fallbackFontPoints))" diff --git a/cmuxTests/WorkspaceUnitTests.swift b/cmuxTests/WorkspaceUnitTests.swift index aa719f38..c420b49e 100644 --- a/cmuxTests/WorkspaceUnitTests.swift +++ b/cmuxTests/WorkspaceUnitTests.swift @@ -306,7 +306,7 @@ final class WorkspaceCreationPlacementTests: XCTestCase { title: String, workingDirectory: String?, portOrdinal: Int, - configTemplate: ghostty_surface_config_s?, + configTemplate: CmuxSurfaceConfigTemplate?, initialTerminalCommand: String?, initialTerminalEnvironment: [String: String] ) -> Workspace { @@ -543,47 +543,21 @@ final class WorkspaceCreationPlacementTests: XCTestCase { @MainActor final class WorkspaceCreationConfigSanitizationTests: XCTestCase { private final class UnsafeConfigSnapshotTabManager: TabManager { - private var retainedCStringPointers: [UnsafeMutablePointer] = [] - private var retainedEnvVars: UnsafeMutablePointer? - private var injectedConfig: ghostty_surface_config_s? - var capturedConfigTemplate: ghostty_surface_config_s? - - deinit { - retainedEnvVars?.deinitialize(count: 1) - retainedEnvVars?.deallocate() - for pointer in retainedCStringPointers { - free(pointer) - } - } + private var injectedConfig: CmuxSurfaceConfigTemplate? + var capturedConfigTemplate: CmuxSurfaceConfigTemplate? func installInjectedConfig(fontSize: Float) { - let workingDirectory = strdup("/tmp/cmux-workspace-snapshot") - let command = strdup("echo snapshot") - let envKey = strdup("CMUX_INHERITED_ENV") - let envValue = strdup("1") - let envVars = UnsafeMutablePointer.allocate(capacity: 1) - envVars.initialize( - to: ghostty_env_var_s( - key: UnsafePointer(envKey), - value: UnsafePointer(envValue) - ) - ) - - retainedCStringPointers = [workingDirectory, command, envKey, envValue].compactMap { $0 } - retainedEnvVars = envVars - - var config = ghostty_surface_config_new() - config.font_size = fontSize - config.working_directory = UnsafePointer(workingDirectory) - config.command = UnsafePointer(command) - config.env_vars = envVars - config.env_var_count = 1 + var config = CmuxSurfaceConfigTemplate() + config.fontSize = fontSize + config.workingDirectory = "/tmp/cmux-workspace-snapshot" + config.command = "echo snapshot" + config.environmentVariables = ["CMUX_INHERITED_ENV": "1"] injectedConfig = config } override func inheritedTerminalConfigForNewWorkspace( workspace: Workspace? - ) -> ghostty_surface_config_s? { + ) -> CmuxSurfaceConfigTemplate? { injectedConfig ?? super.inheritedTerminalConfigForNewWorkspace(workspace: workspace) } @@ -591,7 +565,7 @@ final class WorkspaceCreationConfigSanitizationTests: XCTestCase { title: String, workingDirectory: String?, portOrdinal: Int, - configTemplate: ghostty_surface_config_s?, + configTemplate: CmuxSurfaceConfigTemplate?, initialTerminalCommand: String?, initialTerminalEnvironment: [String: String] ) -> Workspace { @@ -618,11 +592,10 @@ final class WorkspaceCreationConfigSanitizationTests: XCTestCase { return } - XCTAssertEqual(capturedConfig.font_size, 19, accuracy: 0.001) - XCTAssertNil(capturedConfig.working_directory) + XCTAssertEqual(capturedConfig.fontSize, 19, accuracy: 0.001) + XCTAssertNil(capturedConfig.workingDirectory) XCTAssertNil(capturedConfig.command) - XCTAssertNil(capturedConfig.env_vars) - XCTAssertEqual(capturedConfig.env_var_count, 0) + XCTAssertTrue(capturedConfig.environmentVariables.isEmpty) } } diff --git a/scripts/build-ghostty-cli-helper.sh b/scripts/build-ghostty-cli-helper.sh index d38e641c..043a1449 100755 --- a/scripts/build-ghostty-cli-helper.sh +++ b/scripts/build-ghostty-cli-helper.sh @@ -78,6 +78,16 @@ if [[ -n "$TARGET_TRIPLE" ]]; then exit 1 ;; esac + + # When the requested target matches zig's native output arch, drop -Dtarget + # so zig uses native compilation. This avoids cross-linker issues on newer + # SDKs (e.g., macOS Tahoe + zig 0.15.x). Note: zig may run under Rosetta, + # so we detect native output arch from the zig binary itself, not uname -m. + ZIG_ARCH="$(file "$(command -v zig)" 2>/dev/null | grep -oE '(arm64|x86_64)' | head -1)" + case "$TARGET_TRIPLE" in + aarch64-macos) [[ "$ZIG_ARCH" == "arm64" ]] && TARGET_TRIPLE="" ;; + x86_64-macos) [[ "$ZIG_ARCH" == "x86_64" ]] && TARGET_TRIPLE="" ;; + esac fi if ! command -v zig >/dev/null 2>&1; then @@ -122,8 +132,18 @@ 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" + ZIG_ARCH="$(file "$(command -v zig)" 2>/dev/null | grep -oE '(arm64|x86_64)' | head -1)" + # Use native compilation for the matching arch to avoid cross-linker issues + if [[ "$ZIG_ARCH" == "arm64" ]]; then + build_helper "$ARM64_PREFIX" "" + build_helper "$X86_PREFIX" "x86_64-macos" + elif [[ "$ZIG_ARCH" == "x86_64" ]]; then + build_helper "$ARM64_PREFIX" "aarch64-macos" + build_helper "$X86_PREFIX" "" + else + build_helper "$ARM64_PREFIX" "aarch64-macos" + build_helper "$X86_PREFIX" "x86_64-macos" + fi /usr/bin/lipo -create \ "$ARM64_PREFIX/bin/ghostty" \ "$X86_PREFIX/bin/ghostty" \