From a2435b9e8e1736c119a05bee9f01e3189294e793 Mon Sep 17 00:00:00 2001 From: Tony Arnold Date: Sun, 14 Sep 2025 14:31:30 +1000 Subject: [PATCH 1/3] Add virtualMachineArguments to SettingsStore --- .../SettingsData/AppStorageSettingsStore.swift | 12 ++++++++++++ .../Sources/SettingsDomain/SettingsStore.swift | 1 + 2 files changed, 13 insertions(+) diff --git a/Packages/Settings/Sources/SettingsData/AppStorageSettingsStore.swift b/Packages/Settings/Sources/SettingsData/AppStorageSettingsStore.swift index 8f46e73..536f251 100644 --- a/Packages/Settings/Sources/SettingsData/AppStorageSettingsStore.swift +++ b/Packages/Settings/Sources/SettingsData/AppStorageSettingsStore.swift @@ -10,6 +10,7 @@ public final class AppStorageSettingsStore: SettingsStore { static let virtualMachine = "virtualMachine" static let numberOfVirtualMachines = "numberOfVirtualMachines" static let startVirtualMachinesOnLaunch = "startVirtualMachinesOnLaunch" + static let virtualMachineArguments = "virtualMachineArguments" static let gitHubPrivateKeyName = "gitHubPrivateKeyName" static let gitHubRunnerDisableUpdates = "gitHubRunnerDisableUpdates" static let gitHubRunnerLabels = "gitHubRunnerLabels" @@ -83,6 +84,17 @@ public final class AppStorageSettingsStore: SettingsStore { } } } + public var virtualMachineArguments: [String] { + get { + access(keyPath: \.virtualMachineArguments) + return userDefaults.stringArray(forKey: AppStorageKey.virtualMachineArguments) ?? [] + } + set { + withMutation(keyPath: \.virtualMachineArguments) { + userDefaults.setValue(newValue, forKey: AppStorageKey.virtualMachineArguments) + } + } + } public var gitHubPrivateKeyName: String? { get { access(keyPath: \.gitHubPrivateKeyName) diff --git a/Packages/Settings/Sources/SettingsDomain/SettingsStore.swift b/Packages/Settings/Sources/SettingsDomain/SettingsStore.swift index 2353fba..806918d 100644 --- a/Packages/Settings/Sources/SettingsDomain/SettingsStore.swift +++ b/Packages/Settings/Sources/SettingsDomain/SettingsStore.swift @@ -7,6 +7,7 @@ public protocol SettingsStore: AnyObject { var virtualMachine: VirtualMachine { get set } var numberOfVirtualMachines: Int { get set } var startVirtualMachinesOnLaunch: Bool { get set } + var virtualMachineArguments: [String] { get set } var gitHubPrivateKeyName: String? { get set } var gitHubRunnerDisableUpdates: Bool { get set } var gitHubRunnerLabels: String { get set } From c97d66781c3b9cb5d29135ebdbf40210f262731d Mon Sep 17 00:00:00 2001 From: Tony Arnold Date: Sun, 14 Sep 2025 14:32:21 +1000 Subject: [PATCH 2/3] Support passing additional launch arguments to tart at launch --- Packages/MenuBar/Sources/MenuBar/MenuBarItem.swift | 7 +++++-- .../Sources/VirtualMachineData/Tart.swift | 3 ++- .../VirtualMachineData/TartVirtualMachine.swift | 4 ++-- .../SSH/SSHConnectingVirtualMachine.swift | 8 ++++---- .../VirtualMachineDomain/VirtualMachine.swift | 2 +- .../VirtualMachineDomain/VirtualMachineEditor.swift | 4 ++-- .../VirtualMachineDomain/VirtualMachineFleet.swift | 12 ++++++------ Tartelet/Sources/AppDelegate.swift | 5 ++++- Tartelet/Sources/SettingsVirtualMachine.swift | 4 ++-- 9 files changed, 28 insertions(+), 21 deletions(-) diff --git a/Packages/MenuBar/Sources/MenuBar/MenuBarItem.swift b/Packages/MenuBar/Sources/MenuBar/MenuBarItem.swift index 2fa7911..9f46fb5 100644 --- a/Packages/MenuBar/Sources/MenuBar/MenuBarItem.swift +++ b/Packages/MenuBar/Sources/MenuBar/MenuBarItem.swift @@ -92,11 +92,14 @@ private extension MenuBarItem { ) { action in switch action { case .startFleet: - fleet.start(numberOfMachines: settingsStore.numberOfVirtualMachines) + fleet.start( + numberOfMachines: settingsStore.numberOfVirtualMachines, + arguments: settingsStore.virtualMachineArguments + ) case .stopFleet: fleet.stop() case .startEditor: - editor.start() + editor.start(arguments: settingsStore.virtualMachineArguments) } } } diff --git a/Packages/VirtualMachine/Sources/VirtualMachineData/Tart.swift b/Packages/VirtualMachine/Sources/VirtualMachineData/Tart.swift index 40bd5df..80a6133 100644 --- a/Packages/VirtualMachine/Sources/VirtualMachineData/Tart.swift +++ b/Packages/VirtualMachine/Sources/VirtualMachineData/Tart.swift @@ -20,7 +20,7 @@ public struct Tart { try await executeCommand(withArguments: ["clone", sourceName, newName]) } - public func run(name: String) async throws { + public func run(name: String, additionalArguments: [String] = []) async throws { let homeFolderURL = homeProvider.homeFolderURL ?? FileManager.default.homeDirectoryForCurrentUser.appending(component: ".tart") let cacheFolder = homeFolderURL.appendingPathComponent("cache") @@ -28,6 +28,7 @@ public struct Tart { try FileManager.default.createDirectory(atPath: cacheFolder.path, withIntermediateDirectories: true) } var runArgs = ["run", "--dir=cache:\(cacheFolder.path())"] + runArgs.append(contentsOf: additionalArguments) if let tartRunOptions = ProcessInfo.processInfo.environment["TARTELET_RUN_OPTIONS"] { runArgs.append(tartRunOptions) } diff --git a/Packages/VirtualMachine/Sources/VirtualMachineData/TartVirtualMachine.swift b/Packages/VirtualMachine/Sources/VirtualMachineData/TartVirtualMachine.swift index f3390d0..1c256ec 100644 --- a/Packages/VirtualMachine/Sources/VirtualMachineData/TartVirtualMachine.swift +++ b/Packages/VirtualMachine/Sources/VirtualMachineData/TartVirtualMachine.swift @@ -17,8 +17,8 @@ public final class TartVirtualMachine: VirtualMachine { self.vmName = vmName } - public func start() async throws { - try await tart.run(name: vmName) + public func start(arguments: [String]) async throws { + try await tart.run(name: vmName, additionalArguments: arguments) } public func clone(named newName: String) async throws -> VirtualMachine { diff --git a/Packages/VirtualMachine/Sources/VirtualMachineDomain/SSH/SSHConnectingVirtualMachine.swift b/Packages/VirtualMachine/Sources/VirtualMachineDomain/SSH/SSHConnectingVirtualMachine.swift index b527fa0..fe28aa2 100644 --- a/Packages/VirtualMachine/Sources/VirtualMachineDomain/SSH/SSHConnectingVirtualMachine.swift +++ b/Packages/VirtualMachine/Sources/VirtualMachineDomain/SSH/SSHConnectingVirtualMachine.swift @@ -52,10 +52,10 @@ public final class SSHConnectingVirtualMachine: Virtua self.sshClient = sshClient } - public func start() async throws { + public func start(arguments: [String]) async throws { try await withThrowingTaskGroup(of: StartVirtualMachineResult.self) { group in group.addTask { - return try await self.startVirtualMachine() + return try await self.startVirtualMachine(arguments: arguments) } group.addTask { return try await self.connect(to: self.virtualMachine) @@ -108,9 +108,9 @@ public final class SSHConnectingVirtualMachine: Virtua } private extension SSHConnectingVirtualMachine { - private func startVirtualMachine() async throws -> StartVirtualMachineResult { + private func startVirtualMachine(arguments: [String]) async throws -> StartVirtualMachineResult { do { - try await self.virtualMachine.start() + try await self.virtualMachine.start(arguments: arguments) return .success(.virtualMachineTerminated) } catch { if error is CancellationError { diff --git a/Packages/VirtualMachine/Sources/VirtualMachineDomain/VirtualMachine.swift b/Packages/VirtualMachine/Sources/VirtualMachineDomain/VirtualMachine.swift index 77e5037..1244a14 100644 --- a/Packages/VirtualMachine/Sources/VirtualMachineDomain/VirtualMachine.swift +++ b/Packages/VirtualMachine/Sources/VirtualMachineDomain/VirtualMachine.swift @@ -3,7 +3,7 @@ import Foundation public protocol VirtualMachine { var name: String { get } var canStart: Bool { get } - func start() async throws + func start(arguments: [String]) async throws func clone(named newName: String) async throws -> VirtualMachine func delete() async throws func getIPAddress() async throws -> String diff --git a/Packages/VirtualMachine/Sources/VirtualMachineDomain/VirtualMachineEditor.swift b/Packages/VirtualMachine/Sources/VirtualMachineDomain/VirtualMachineEditor.swift index 62e3aa2..c20522e 100644 --- a/Packages/VirtualMachine/Sources/VirtualMachineDomain/VirtualMachineEditor.swift +++ b/Packages/VirtualMachine/Sources/VirtualMachineDomain/VirtualMachineEditor.swift @@ -19,7 +19,7 @@ public final class VirtualMachineEditor { self.virtualMachine = virtualMachine } - public func start() { + public func start(arguments: [String]) { guard runTask == nil else { return } @@ -29,7 +29,7 @@ public final class VirtualMachineEditor { defer { self.runTask = nil } - try await virtualMachine.start() + try await virtualMachine.start(arguments: arguments) } onCancel: { self.stop() } diff --git a/Packages/VirtualMachine/Sources/VirtualMachineDomain/VirtualMachineFleet.swift b/Packages/VirtualMachine/Sources/VirtualMachineDomain/VirtualMachineFleet.swift index 07df66f..76d09d4 100644 --- a/Packages/VirtualMachine/Sources/VirtualMachineDomain/VirtualMachineFleet.swift +++ b/Packages/VirtualMachine/Sources/VirtualMachineDomain/VirtualMachineFleet.swift @@ -15,7 +15,7 @@ public final class VirtualMachineFleet { self.baseVirtualMachine = baseVirtualMachine } - public func start(numberOfMachines: Int) { + public func start(numberOfMachines: Int, arguments: [String]) { guard !isStarted else { return } @@ -25,7 +25,7 @@ public final class VirtualMachineFleet { isStarted = true for index in 0 ..< numberOfMachines { let name = baseVirtualMachine.name + "-\(index + 1)" - startSequentiallyRunningVirtualMachines(named: name) + startSequentiallyRunningVirtualMachines(named: name, arguments: arguments) } } @@ -44,12 +44,12 @@ public final class VirtualMachineFleet { } private extension VirtualMachineFleet { - private func startSequentiallyRunningVirtualMachines(named name: String) { + private func startSequentiallyRunningVirtualMachines(named name: String, arguments: [String]) { let task = Task { while !Task.isCancelled { do { let virtualMachine = try await baseVirtualMachine.clone(named: name) - try await runVirtualMachine(virtualMachine) + try await runVirtualMachine(virtualMachine, arguments: arguments) if isStopping { activeTasks[name]?.cancel() } @@ -69,11 +69,11 @@ private extension VirtualMachineFleet { activeTasks[name] = task } - private func runVirtualMachine(_ virtualMachine: VirtualMachine) async throws { + private func runVirtualMachine(_ virtualMachine: VirtualMachine, arguments: [String]) async throws { try await withTaskCancellationHandler { logger.info("Start virtual machine named \(virtualMachine.name)") do { - try await virtualMachine.start() + try await virtualMachine.start(arguments: arguments) logger.info("Did stop virtual machine named \(virtualMachine.name)") do { try await virtualMachine.delete() diff --git a/Tartelet/Sources/AppDelegate.swift b/Tartelet/Sources/AppDelegate.swift index 0573bb4..b84acb2 100644 --- a/Tartelet/Sources/AppDelegate.swift +++ b/Tartelet/Sources/AppDelegate.swift @@ -14,7 +14,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_ notification: Notification) { beginObservingAppIconVisibility() if Composers.settingsStore.startVirtualMachinesOnLaunch { - Composers.fleet.start(numberOfMachines: Composers.settingsStore.numberOfVirtualMachines) + Composers.fleet.start( + numberOfMachines: Composers.settingsStore.numberOfVirtualMachines, + arguments: Composers.settingsStore.virtualMachineArguments + ) } // If Tartelet is launched as a login item, we can keep the window hidden diff --git a/Tartelet/Sources/SettingsVirtualMachine.swift b/Tartelet/Sources/SettingsVirtualMachine.swift index ad63481..b2f81f3 100644 --- a/Tartelet/Sources/SettingsVirtualMachine.swift +++ b/Tartelet/Sources/SettingsVirtualMachine.swift @@ -27,8 +27,8 @@ struct SettingsVirtualMachine: VirtualMachineD TartVirtualMachine(tart: tart, vmName: name) } - func start() async throws { - try await virtualMachine.start() + func start(arguments: [String]) async throws { + try await virtualMachine.start(arguments: arguments) } func clone(named newName: String) async throws -> VirtualMachineDomain.VirtualMachine { From aa2143975cc4a0d71087dc71be00f6668f07a342 Mon Sep 17 00:00:00 2001 From: Tony Arnold Date: Sun, 14 Sep 2025 14:32:32 +1000 Subject: [PATCH 3/3] Provide settings UI for editing the launch arguments --- .../Settings/Sources/SettingsUI/Internal/L10n.swift | 9 ++++++++- .../Sources/SettingsUI/Internal/Localizable.strings | 4 +++- .../VirtualMachineSettingsView.swift | 13 +++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/Packages/Settings/Sources/SettingsUI/Internal/L10n.swift b/Packages/Settings/Sources/SettingsUI/Internal/L10n.swift index 0d75b3a..aad4b51 100644 --- a/Packages/Settings/Sources/SettingsUI/Internal/L10n.swift +++ b/Packages/Settings/Sources/SettingsUI/Internal/L10n.swift @@ -85,6 +85,8 @@ internal enum L10n { } } internal enum GithubRunner { + /// Disable default labels + internal static let disableDefaultLabels = L10n.tr("Localizable", "settings.github_runner.disableDefaultLabels", fallback: "Disable default labels") /// Disable runner auto-update internal static let disableUpdates = L10n.tr("Localizable", "settings.github_runner.disableUpdates", fallback: "Disable runner auto-update") /// Group @@ -101,7 +103,6 @@ internal enum L10n { /// acme internal static let prompt = L10n.tr("Localizable", "settings.github_runner.group.prompt", fallback: "acme") } - internal static let disableDefaultLabels = L10n.tr("Localizable", "settings.github_runner.disableDefaultLabels", fallback: "Disable default labels") internal enum Labels { /// Comma-separated list of labels. internal static let footer = L10n.tr("Localizable", "settings.github_runner.labels.footer", fallback: "Comma-separated list of labels.") @@ -122,6 +123,8 @@ internal enum L10n { internal static let repository = L10n.tr("Localizable", "settings.runner_scope.repository", fallback: "Repository") } internal enum VirtualMachine { + /// Additional Arguments + internal static let additionalArguments = L10n.tr("Localizable", "settings.virtual_machine.additional_arguments", fallback: "Additional Arguments") /// Number of Machines internal static let count = L10n.tr("Localizable", "settings.virtual_machine.count", fallback: "Number of Machines") /// Use the Tart CLI to create a virtual machine. @@ -134,6 +137,10 @@ internal enum L10n { internal static let tartHome = L10n.tr("Localizable", "settings.virtual_machine.tart_home", fallback: "Tart Home") /// Unknown internal static let unknown = L10n.tr("Localizable", "settings.virtual_machine.unknown", fallback: "Unknown") + internal enum AdditionalArguments { + /// Specify additional tart launch arguments. + internal static let prompt = L10n.tr("Localizable", "settings.virtual_machine.additional_arguments.prompt", fallback: "Specify additional tart launch arguments.") + } internal enum Count { /// One internal static let one = L10n.tr("Localizable", "settings.virtual_machine.count.one", fallback: "One") diff --git a/Packages/Settings/Sources/SettingsUI/Internal/Localizable.strings b/Packages/Settings/Sources/SettingsUI/Internal/Localizable.strings index 28d795e..fc99546 100644 --- a/Packages/Settings/Sources/SettingsUI/Internal/Localizable.strings +++ b/Packages/Settings/Sources/SettingsUI/Internal/Localizable.strings @@ -23,6 +23,8 @@ "settings.virtual_machine.ssh.password" = "Password"; "settings.virtual_machine.ssh.footer" = "The credentials are used to SSH into the virtual machine to configure it to start the GitHub Actions runner. Make sure \"Remote Login\" is enabled in the virtual machine."; "settings.virtual_machine.start_virtual_machines_on_app_launch" = "Start Virtual Machines on App Launch"; +"settings.virtual_machine.additional_arguments" = "Additional Arguments"; +"settings.virtual_machine.additional_arguments.prompt" = "Specify additional tart launch arguments."; "settings.github" = "GitHub"; "settings.github.organization_name" = "Organization Name"; @@ -41,7 +43,7 @@ "settings.github.create_app" = "Create GitHub App"; "settings.github_runner" = "Runner"; -"settings.github_runner.disableDefaultLabels" = "Disable default labels" +"settings.github_runner.disableDefaultLabels" = "Disable default labels"; "settings.github_runner.disableUpdates" = "Disable runner auto-update"; "settings.github_runner.disableUpdates.subtitle" = "This is meant to be used when a fixed version is pre-installed, otherwise Tartelet will install the latest version when the VM starts."; "settings.github_runner.labels" = "Labels"; diff --git a/Packages/Settings/Sources/SettingsUI/Internal/VirtualMachineSettings/VirtualMachineSettingsView.swift b/Packages/Settings/Sources/SettingsUI/Internal/VirtualMachineSettings/VirtualMachineSettingsView.swift index b39aae3..19afb8d 100644 --- a/Packages/Settings/Sources/SettingsUI/Internal/VirtualMachineSettings/VirtualMachineSettingsView.swift +++ b/Packages/Settings/Sources/SettingsUI/Internal/VirtualMachineSettings/VirtualMachineSettingsView.swift @@ -13,6 +13,7 @@ struct VirtualMachineSettingsView @State private var isRefreshingVirtualMachines = false @State private var sshUsername = "" @State private var sshPassword = "" + @State private var additionalArguments = "" var body: some View { Form { @@ -32,6 +33,12 @@ struct VirtualMachineSettingsView Toggle(isOn: $settingsStore.startVirtualMachinesOnLaunch) { Text(L10n.Settings.VirtualMachine.startVirtualMachinesOnAppLaunch) } + TextField( + L10n.Settings.VirtualMachine.additionalArguments, + text: $additionalArguments, + prompt: Text(L10n.Settings.VirtualMachine.AdditionalArguments.prompt) + ) + .disabled(!isSettingsEnabled) } Section { TextField( @@ -71,6 +78,12 @@ struct VirtualMachineSettingsView await refreshVirtualMachines() } } + .onChange(of: settingsStore.virtualMachineArguments) { _, newValue in + additionalArguments = newValue.joined(separator: " ") + } + .onChange(of: additionalArguments) { _, newValue in + settingsStore.virtualMachineArguments = newValue.split(separator: " ").map(String.init) + } .onChange(of: sshUsername) { _, newValue in if !newValue.isEmpty { credentialsStore.setUsername(newValue)