mirror of
https://github.com/cirruslabs/tart.git
synced 2026-10-18 11:55:36 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20dcfc83f2 | ||
|
|
c192de20f5 | ||
|
|
e28d9337a5 | ||
|
|
68ffa6c5e4 | ||
|
|
1b091e9db0 | ||
|
|
902b1a6c9c | ||
|
|
90d9500133 | ||
|
|
b05c731510 | ||
|
|
d762fe6fc1 |
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
use_compute_credits: true
|
use_compute_credits: true
|
||||||
|
|
||||||
task:
|
task:
|
||||||
name: Test on Sequoia
|
name: Test
|
||||||
alias: test
|
alias: test
|
||||||
persistent_worker:
|
persistent_worker:
|
||||||
labels:
|
labels:
|
||||||
|
|||||||
@@ -134,11 +134,12 @@ struct Run: AsyncParsableCommand {
|
|||||||
|
|
||||||
Learn how to create a disk image using Disk Utility here: https://support.apple.com/en-gb/guide/disk-utility/dskutl11888/mac
|
Learn how to create a disk image using Disk Utility here: https://support.apple.com/en-gb/guide/disk-utility/dskutl11888/mac
|
||||||
|
|
||||||
To work with block devices, the easiest way is to modify their permissions (e.g. by using "sudo chown $USER /dev/diskX") or to run the Tart binary as root, which affects locating Tart VMs.
|
To work with block devices, the easiest way is to modify their permissions to be accessible to the current user:
|
||||||
|
|
||||||
To work around this pass TART_HOME explicitly:
|
sudo chown $USER /dev/diskX
|
||||||
|
tart run sequoia --disk=/dev/diskX
|
||||||
|
|
||||||
sudo TART_HOME="$HOME/.tart" tart run sequoia --disk=/dev/disk0
|
Warning: after running the chown command above, all software running under the current user will be able to access /dev/diskX. If that violates your threat model, we recommend avoiding mounting block devices altogether.
|
||||||
""", valueName: "path[:options]"), completion: .file())
|
""", valueName: "path[:options]"), completion: .file())
|
||||||
var disk: [String] = []
|
var disk: [String] = []
|
||||||
|
|
||||||
@@ -201,15 +202,28 @@ struct Run: AsyncParsableCommand {
|
|||||||
"""))
|
"""))
|
||||||
var netSoftnet: Bool = false
|
var netSoftnet: Bool = false
|
||||||
|
|
||||||
@Option(help: ArgumentHelp("Comma-separated list of CIDRs to allow the traffic to when using Softnet isolation\n(e.g. --net-softnet-allow=192.168.0.0/24)", discussion: """
|
@Option(help: ArgumentHelp("Comma-separated list of CIDRs to allow the traffic to when using Softnet isolation (e.g. --net-softnet-allow=192.168.0.0/24)", discussion: """
|
||||||
This option allows you bypass the private IPv4 address space restrictions imposed by --net-softnet.
|
This option allows you bypass the private IPv4 address space restrictions imposed by --net-softnet.
|
||||||
|
|
||||||
For example, you can allow the VM to communicate with the local network with e.g. --net-softnet-allow=10.0.0.0/16 or to completely disable the destination based restrictions with --net-softnet-allow=0.0.0.0/0.
|
For example, you can allow the VM to communicate with the local network with e.g. --net-softnet-allow=10.0.0.0/16 or with --net-softnet-allow=0.0.0.0/0 to completely disable the destination based restrictions, including VMs bridge isolation.
|
||||||
|
|
||||||
|
When used with --net-softnet-block, the longest prefix match always wins. In case the same prefix is both allowed and blocked, blocking takes precedence.
|
||||||
|
|
||||||
Implies --net-softnet.
|
Implies --net-softnet.
|
||||||
""", valueName: "comma-separated CIDRs"))
|
""", valueName: "comma-separated CIDRs"))
|
||||||
var netSoftnetAllow: String?
|
var netSoftnetAllow: String?
|
||||||
|
|
||||||
|
@Option(help: ArgumentHelp("Comma-separated list of CIDRs to block the traffic to when using Softnet isolation (e.g. --net-softnet-block=66.66.0.0/16)", discussion: """
|
||||||
|
This option allows you to tighten the IPv4 address space restrictions imposed by --net-softnet even further.
|
||||||
|
|
||||||
|
For example --net-softnet-block=0.0.0.0/0 may be used to establish a default deny policy that is further relaxed with --net-softnet-allow.
|
||||||
|
|
||||||
|
When used with --net-softnet-allow, the longest prefix match always wins. In case the same prefix is both allowed and blocked, blocking takes precedence.
|
||||||
|
|
||||||
|
Implies --net-softnet.
|
||||||
|
""", valueName: "comma-separated CIDRs"))
|
||||||
|
var netSoftnetBlock: String?
|
||||||
|
|
||||||
@Option(help: ArgumentHelp("Comma-separated list of TCP ports to expose (e.g. --net-softnet-expose 2222:22,8080:80)", discussion: """
|
@Option(help: ArgumentHelp("Comma-separated list of TCP ports to expose (e.g. --net-softnet-expose 2222:22,8080:80)", discussion: """
|
||||||
Options are comma-separated and are as follows:
|
Options are comma-separated and are as follows:
|
||||||
|
|
||||||
@@ -265,13 +279,19 @@ struct Run: AsyncParsableCommand {
|
|||||||
#endif
|
#endif
|
||||||
var noTrackpad: Bool = false
|
var noTrackpad: Bool = false
|
||||||
|
|
||||||
|
@Flag(help: ArgumentHelp("Disable the pointer"))
|
||||||
|
var noPointer: Bool = false
|
||||||
|
|
||||||
|
@Flag(help: ArgumentHelp("Disable the keyboard"))
|
||||||
|
var noKeyboard: Bool = false
|
||||||
|
|
||||||
mutating func validate() throws {
|
mutating func validate() throws {
|
||||||
if vnc && vncExperimental {
|
if vnc && vncExperimental {
|
||||||
throw ValidationError("--vnc and --vnc-experimental are mutually exclusive")
|
throw ValidationError("--vnc and --vnc-experimental are mutually exclusive")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Automatically enable --net-softnet when any of its related options are specified
|
// Automatically enable --net-softnet when any of its related options are specified
|
||||||
if netSoftnetAllow != nil || netSoftnetExpose != nil {
|
if netSoftnetAllow != nil || netSoftnetBlock != nil || netSoftnetExpose != nil {
|
||||||
netSoftnet = true
|
netSoftnet = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,8 +336,15 @@ struct Run: AsyncParsableCommand {
|
|||||||
if noTrackpad {
|
if noTrackpad {
|
||||||
throw ValidationError("--no-trackpad cannot be used with --suspendable")
|
throw ValidationError("--no-trackpad cannot be used with --suspendable")
|
||||||
}
|
}
|
||||||
|
if noKeyboard {
|
||||||
|
throw ValidationError("--no-keyboard cannot be used with --suspendable")
|
||||||
|
}
|
||||||
|
if noPointer {
|
||||||
|
throw ValidationError("--no-pointer cannot be used with --suspendable")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if noTrackpad {
|
if noTrackpad {
|
||||||
let config = try VMConfig.init(fromURL: vmDir.configURL)
|
let config = try VMConfig.init(fromURL: vmDir.configURL)
|
||||||
if config.os != .darwin {
|
if config.os != .darwin {
|
||||||
@@ -393,7 +420,9 @@ struct Run: AsyncParsableCommand {
|
|||||||
clipboard: !noClipboard,
|
clipboard: !noClipboard,
|
||||||
sync: VZDiskImageSynchronizationMode(diskOptions.syncModeRaw),
|
sync: VZDiskImageSynchronizationMode(diskOptions.syncModeRaw),
|
||||||
caching: VZDiskImageCachingMode(diskOptions.cachingModeRaw),
|
caching: VZDiskImageCachingMode(diskOptions.cachingModeRaw),
|
||||||
noTrackpad: noTrackpad
|
noTrackpad: noTrackpad,
|
||||||
|
noPointer: noPointer,
|
||||||
|
noKeyboard: noKeyboard
|
||||||
)
|
)
|
||||||
|
|
||||||
let vncImpl: VNC? = try {
|
let vncImpl: VNC? = try {
|
||||||
@@ -594,6 +623,10 @@ struct Run: AsyncParsableCommand {
|
|||||||
softnetExtraArguments += ["--allow", netSoftnetAllow]
|
softnetExtraArguments += ["--allow", netSoftnetAllow]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let netSoftnetBlock = netSoftnetBlock {
|
||||||
|
softnetExtraArguments += ["--block", netSoftnetBlock]
|
||||||
|
}
|
||||||
|
|
||||||
if let netSoftnetExpose = netSoftnetExpose {
|
if let netSoftnetExpose = netSoftnetExpose {
|
||||||
softnetExtraArguments += ["--expose", netSoftnetExpose]
|
softnetExtraArguments += ["--expose", netSoftnetExpose]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ struct Set: AsyncParsableCommand {
|
|||||||
@Option(help: "VM memory size in megabytes")
|
@Option(help: "VM memory size in megabytes")
|
||||||
var memory: UInt64?
|
var memory: UInt64?
|
||||||
|
|
||||||
@Option(help: "VM display resolution in a format of <width>x<height>. For example, 1200x800")
|
@Option(help: "VM display resolution in a format of WIDTHxHEIGHT[pt|px]. For example, 1200x800, 1200x800pt or 1920x1080px. Units are treated as hints and default to \"pt\" (points) for macOS VMs and \"px\" (pixels) for Linux VMs when not specified.")
|
||||||
var display: VMDisplayConfig?
|
var display: VMDisplayConfig?
|
||||||
|
|
||||||
@Flag(inversion: .prefixedNo, help: ArgumentHelp("Whether to automatically reconfigure the VM's display to fit the window"))
|
@Flag(inversion: .prefixedNo, help: ArgumentHelp("Whether to automatically reconfigure the VM's display to fit the window"))
|
||||||
@@ -56,6 +56,7 @@ struct Set: AsyncParsableCommand {
|
|||||||
if (display.height > 0) {
|
if (display.height > 0) {
|
||||||
vmConfig.display.height = display.height
|
vmConfig.display.height = display.height
|
||||||
}
|
}
|
||||||
|
vmConfig.display.unit = display.unit
|
||||||
}
|
}
|
||||||
|
|
||||||
vmConfig.displayRefit = displayRefit
|
vmConfig.displayRefit = displayRefit
|
||||||
@@ -88,12 +89,24 @@ struct Set: AsyncParsableCommand {
|
|||||||
|
|
||||||
extension VMDisplayConfig: ExpressibleByArgument {
|
extension VMDisplayConfig: ExpressibleByArgument {
|
||||||
public init(argument: String) {
|
public init(argument: String) {
|
||||||
|
var argument = argument
|
||||||
|
var unit: Unit? = nil
|
||||||
|
|
||||||
|
if argument.hasSuffix(Unit.pixel.rawValue) {
|
||||||
|
argument = String(argument.dropLast(Unit.pixel.rawValue.count))
|
||||||
|
unit = Unit.pixel
|
||||||
|
} else if argument.hasSuffix(Unit.point.rawValue) {
|
||||||
|
argument = String(argument.dropLast(Unit.point.rawValue.count))
|
||||||
|
unit = Unit.point
|
||||||
|
}
|
||||||
|
|
||||||
let parts = argument.components(separatedBy: "x").map {
|
let parts = argument.components(separatedBy: "x").map {
|
||||||
Int($0) ?? 0
|
Int($0) ?? 0
|
||||||
}
|
}
|
||||||
self = VMDisplayConfig(
|
self = VMDisplayConfig(
|
||||||
width: parts[safe: 0] ?? 0,
|
width: parts[safe: 0] ?? 0,
|
||||||
height: parts[safe: 1] ?? 0
|
height: parts[safe: 1] ?? 0,
|
||||||
|
unit: unit,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ struct UnsupportedHostOSError: Error, CustomStringConvertible {
|
|||||||
func graphicsDevice(vmConfig: VMConfig) -> VZGraphicsDeviceConfiguration {
|
func graphicsDevice(vmConfig: VMConfig) -> VZGraphicsDeviceConfiguration {
|
||||||
let result = VZMacGraphicsDeviceConfiguration()
|
let result = VZMacGraphicsDeviceConfiguration()
|
||||||
|
|
||||||
if let hostMainScreen = NSScreen.main {
|
if (vmConfig.display.unit ?? .point) == .point, let hostMainScreen = NSScreen.main {
|
||||||
let vmScreenSize = NSSize(width: vmConfig.display.width, height: vmConfig.display.height)
|
let vmScreenSize = NSSize(width: vmConfig.display.width, height: vmConfig.display.height)
|
||||||
result.displays = [
|
result.displays = [
|
||||||
VZMacGraphicsDisplayConfiguration(for: hostMainScreen, sizeInPoints: vmScreenSize)
|
VZMacGraphicsDisplayConfiguration(for: hostMainScreen, sizeInPoints: vmScreenSize)
|
||||||
|
|||||||
+26
-14
@@ -70,20 +70,30 @@ struct Root: AsyncParsableCommand {
|
|||||||
HttpStatusCodeRange(min: 400, max: 400),
|
HttpStatusCodeRange(min: 400, max: 400),
|
||||||
HttpStatusCodeRange(min: 402, max: 599)
|
HttpStatusCodeRange(min: 402, max: 599)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// https://github.com/cirruslabs/tart/issues/1163
|
||||||
|
options.enableAppLaunchProfiling = false
|
||||||
|
options.configureProfiling = {
|
||||||
|
$0.profileAppStarts = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SentrySDK.configureScope { scope in
|
||||||
|
scope.setExtra(value: ProcessInfo.processInfo.arguments, key: "Command-line arguments")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enrich future events with Cirrus CI-specific tags
|
||||||
|
if let tags = ProcessInfo.processInfo.environment["CIRRUS_SENTRY_TAGS"] {
|
||||||
|
SentrySDK.configureScope { scope in
|
||||||
|
for (key, value) in tags.split(separator: ",").compactMap({ parseCirrusSentryTag($0) }) {
|
||||||
|
scope.setTag(value: value, key: key)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
defer { SentrySDK.flush(timeout: 2.seconds.timeInterval) }
|
defer {
|
||||||
|
if ProcessInfo.processInfo.environment["SENTRY_DSN"] != nil {
|
||||||
SentrySDK.configureScope { scope in
|
SentrySDK.flush(timeout: 2.seconds.timeInterval)
|
||||||
scope.setExtra(value: ProcessInfo.processInfo.arguments, key: "Command-line arguments")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enrich future events with Cirrus CI-specific tags
|
|
||||||
if let tags = ProcessInfo.processInfo.environment["CIRRUS_SENTRY_TAGS"] {
|
|
||||||
SentrySDK.configureScope { scope in
|
|
||||||
for (key, value) in tags.split(separator: ",").compactMap({ parseCirrusSentryTag($0) }) {
|
|
||||||
scope.setTag(value: value, key: key)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,8 +119,10 @@ struct Root: AsyncParsableCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Capture the error into Sentry
|
// Capture the error into Sentry
|
||||||
SentrySDK.capture(error: error)
|
if ProcessInfo.processInfo.environment["SENTRY_DSN"] != nil {
|
||||||
SentrySDK.flush(timeout: 2.seconds.timeInterval)
|
SentrySDK.capture(error: error)
|
||||||
|
SentrySDK.flush(timeout: 2.seconds.timeInterval)
|
||||||
|
}
|
||||||
|
|
||||||
// Handle a non-ArgumentParser's exception that requires a specific exit code to be set
|
// Handle a non-ArgumentParser's exception that requires a specific exit code to be set
|
||||||
if let errorWithExitCode = error as? HasExitCode {
|
if let errorWithExitCode = error as? HasExitCode {
|
||||||
|
|||||||
+19
-5
@@ -51,7 +51,9 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
|||||||
clipboard: Bool = true,
|
clipboard: Bool = true,
|
||||||
sync: VZDiskImageSynchronizationMode = .full,
|
sync: VZDiskImageSynchronizationMode = .full,
|
||||||
caching: VZDiskImageCachingMode? = nil,
|
caching: VZDiskImageCachingMode? = nil,
|
||||||
noTrackpad: Bool = false
|
noTrackpad: Bool = false,
|
||||||
|
noPointer: Bool = false,
|
||||||
|
noKeyboard: Bool = false
|
||||||
) throws {
|
) throws {
|
||||||
name = vmDir.name
|
name = vmDir.name
|
||||||
config = try VMConfig.init(fromURL: vmDir.configURL)
|
config = try VMConfig.init(fromURL: vmDir.configURL)
|
||||||
@@ -73,7 +75,9 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
|||||||
clipboard: clipboard,
|
clipboard: clipboard,
|
||||||
sync: sync,
|
sync: sync,
|
||||||
caching: caching,
|
caching: caching,
|
||||||
noTrackpad: noTrackpad
|
noTrackpad: noTrackpad,
|
||||||
|
noPointer: noPointer,
|
||||||
|
noKeyboard: noKeyboard
|
||||||
)
|
)
|
||||||
virtualMachine = VZVirtualMachine(configuration: configuration)
|
virtualMachine = VZVirtualMachine(configuration: configuration)
|
||||||
|
|
||||||
@@ -316,7 +320,9 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
|||||||
clipboard: Bool = true,
|
clipboard: Bool = true,
|
||||||
sync: VZDiskImageSynchronizationMode = .full,
|
sync: VZDiskImageSynchronizationMode = .full,
|
||||||
caching: VZDiskImageCachingMode? = nil,
|
caching: VZDiskImageCachingMode? = nil,
|
||||||
noTrackpad: Bool = false
|
noTrackpad: Bool = false,
|
||||||
|
noPointer: Bool = false,
|
||||||
|
noKeyboard: Bool = false
|
||||||
) throws -> VZVirtualMachineConfiguration {
|
) throws -> VZVirtualMachineConfiguration {
|
||||||
let configuration = VZVirtualMachineConfiguration()
|
let configuration = VZVirtualMachineConfiguration()
|
||||||
|
|
||||||
@@ -356,8 +362,16 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
|||||||
configuration.keyboards = platformSuspendable.keyboardsSuspendable()
|
configuration.keyboards = platformSuspendable.keyboardsSuspendable()
|
||||||
configuration.pointingDevices = platformSuspendable.pointingDevicesSuspendable()
|
configuration.pointingDevices = platformSuspendable.pointingDevicesSuspendable()
|
||||||
} else {
|
} else {
|
||||||
configuration.keyboards = vmConfig.platform.keyboards()
|
|
||||||
if noTrackpad {
|
if noKeyboard {
|
||||||
|
configuration.keyboards = []
|
||||||
|
} else {
|
||||||
|
configuration.keyboards = vmConfig.platform.keyboards()
|
||||||
|
}
|
||||||
|
|
||||||
|
if noPointer {
|
||||||
|
configuration.pointingDevices = []
|
||||||
|
} else if noTrackpad {
|
||||||
configuration.pointingDevices = vmConfig.platform.pointingDevicesSimplified()
|
configuration.pointingDevices = vmConfig.platform.pointingDevicesSimplified()
|
||||||
} else {
|
} else {
|
||||||
configuration.pointingDevices = vmConfig.platform.pointingDevices()
|
configuration.pointingDevices = vmConfig.platform.pointingDevices()
|
||||||
|
|||||||
@@ -32,14 +32,24 @@ enum CodingKeys: String, CodingKey {
|
|||||||
case hardwareModel
|
case hardwareModel
|
||||||
}
|
}
|
||||||
|
|
||||||
struct VMDisplayConfig: Codable {
|
struct VMDisplayConfig: Codable, Equatable {
|
||||||
|
enum Unit: String, Codable {
|
||||||
|
case point = "pt"
|
||||||
|
case pixel = "px"
|
||||||
|
}
|
||||||
|
|
||||||
var width: Int = 1024
|
var width: Int = 1024
|
||||||
var height: Int = 768
|
var height: Int = 768
|
||||||
|
var unit: Unit?
|
||||||
}
|
}
|
||||||
|
|
||||||
extension VMDisplayConfig: CustomStringConvertible {
|
extension VMDisplayConfig: CustomStringConvertible {
|
||||||
var description: String {
|
var description: String {
|
||||||
"\(width)x\(height)"
|
if let unit {
|
||||||
|
"\(width)x\(height)\(unit.rawValue)"
|
||||||
|
} else {
|
||||||
|
"\(width)x\(height)"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import XCTest
|
||||||
|
@testable import tart
|
||||||
|
|
||||||
|
final class VMConfigTests: XCTestCase {
|
||||||
|
func testVMDisplayConfig() throws {
|
||||||
|
// Defaults units (points)
|
||||||
|
var vmDisplayConfig = VMDisplayConfig.init(argument: "1234x5678")
|
||||||
|
XCTAssertEqual(VMDisplayConfig(width: 1234, height: 5678, unit: nil), vmDisplayConfig)
|
||||||
|
|
||||||
|
// Explicit units (points)
|
||||||
|
vmDisplayConfig = VMDisplayConfig.init(argument: "1234x5678pt")
|
||||||
|
XCTAssertEqual(VMDisplayConfig(width: 1234, height: 5678, unit: .point), vmDisplayConfig)
|
||||||
|
|
||||||
|
// Explicit units (pixels)
|
||||||
|
vmDisplayConfig = VMDisplayConfig.init(argument: "1234x5678px")
|
||||||
|
XCTAssertEqual(VMDisplayConfig(width: 1234, height: 5678, unit: .pixel), vmDisplayConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
---
|
||||||
|
draft: false
|
||||||
|
date: 2025-10-27
|
||||||
|
search:
|
||||||
|
exclude: true
|
||||||
|
authors:
|
||||||
|
- fkorotkov
|
||||||
|
categories:
|
||||||
|
- announcement
|
||||||
|
---
|
||||||
|
|
||||||
|
# Press Release: Cirrus Labs Successfully Enforces Its Fair Source License
|
||||||
|
|
||||||
|
**New York City, NY – October 27th, 2025 – Cirrus Labs, Inc.**, a leading provider of platforms for digital transformation, today announced that it has reached a settlement agreement regarding a violation of its Fair Source License.
|
||||||
|
|
||||||
|
<!-- more -->
|
||||||
|
|
||||||
|
Cirrus Labs makes its Tart Virtualization Toolset, a leading virtualization toolset to build, run and manage macOS and Linux virtual machines (VMs) on Apple Silicon,
|
||||||
|
freely available on GitHub under the Fair Source License, a source-available license. Tart is used by tens of thousands of engineers at no charge within its generous free‑use limits.
|
||||||
|
Many large enterprises that need to exceed those limits support continued development through paid licenses. Cirrus Labs also uses Tart to power [Cirrus Runners](https://cirrus-runners.app/)
|
||||||
|
— a drop‑in replacement for macOS and Linux runners for GitHub Actions — offered at a fixed monthly price for unlimited usage.
|
||||||
|
|
||||||
|
Cirrus Labs discovered that, **despite a prior licensing request that was declined due to a conflict of interest**, another company used Tart in a manner that exceeded the license’s free‑use limits,
|
||||||
|
in order to create a competing product.
|
||||||
|
|
||||||
|
After several months of negotiations, the matter was settled and a settlement payment to Cirrus Labs was agreed upon.
|
||||||
|
|
||||||
|
!!! quote "Comment by Fedor Korotkov, CEO of Cirrus Labs"
|
||||||
|
|
||||||
|
As a company we embrace healthy competition that ultimately benefits the end user. Most of our users have no trouble complying with our license,
|
||||||
|
and even when they need something more than our free use limits, we can almost always grant them a license that fits their needs. **This was an exceptional case.**
|
||||||
|
We are pleased to have reached this settlement, which validates our source-available licensing strategy and reinforces our commitment to protecting our company and serving our community.
|
||||||
|
|
||||||
|
Cirrus Labs was represented in this matter by [Jordan Raphael](https://byronraphael.com/attorneys/jordan-raphael/) of Byron Raphael LLP, a boutique intellectual property law firm,
|
||||||
|
and [Heather Meeker](https://www.techlawpartners.com/heather), a well-known specialist in open source and source available licensing.
|
||||||
|
|
||||||
|
The specific financial terms of the settlement and the identity of the counterparty remain confidential.
|
||||||
|
|
||||||
|
**About Cirrus Labs:** Cirrus Labs, Inc. is a bootstrapped developer-infrastructure company founded in 2017. Our offerings among others include Tart and Cirrus Runners,
|
||||||
|
and our software is used by teams at category-leading companies including Atlassian, Figma, Zendesk, Sentry and many more.
|
||||||
|
|
||||||
|
Learn more at [https://tart.run/](https://tart.run/) and [https://cirrus-runners.app/](https://cirrus-runners.app/).
|
||||||
|
|
||||||
|
**Contact:** [hello@cirruslabs.org](mailto:hello@cirruslabs.org)
|
||||||
+39
-3
@@ -5,6 +5,42 @@ title: Frequently Asked Questions
|
|||||||
description: Advanced configuration and troubleshooting tips for advanced configurations.
|
description: Advanced configuration and troubleshooting tips for advanced configurations.
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Headless machines
|
||||||
|
|
||||||
|
Starting from macOS 15 (Sequoia), there's an undocumented requirement from [Virtualization.Framework](https://developer.apple.com/documentation/virtualization) (which Tart uses) to have an unlocked `login.keychain` available at the times when running a VM.
|
||||||
|
|
||||||
|
Without an existing and unlocked `login.keychain`, the VM won't start with errors like:
|
||||||
|
|
||||||
|
* `SecKeyCreateRandomKey_ios failed`
|
||||||
|
* `Failed to generate keypair`
|
||||||
|
* `Interaction is not allowed with the Security Server`
|
||||||
|
|
||||||
|
Below you'll find a couple of workarounds for this behavior.
|
||||||
|
|
||||||
|
### Log in via GUI at least once
|
||||||
|
|
||||||
|
Connect to the headless machine via [Screen Sharing](https://support.apple.com/guide/mac-help/share-the-screen-of-another-mac-mh14066/mac) and log in to a Mac user account. If you haven't done already, you can enable Screen Sharing [via the terminal](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/connect-to-mac-instance.html#mac-instance-vnc).
|
||||||
|
|
||||||
|
Logging in graphically will automatically create the `login.keychain`. Afterward, you have two options:
|
||||||
|
|
||||||
|
* configure [automatic log in to a Mac user account](https://support.apple.com/en-us/102316)
|
||||||
|
* this will maintain a running user session (GUI) even after the machine reboots
|
||||||
|
* moreover, you can still lock the screen (either manually [or automatically](https://support.apple.com/guide/mac-help/change-lock-screen-settings-on-mac-mh11784/mac)), however, the security benefit of this is questionable
|
||||||
|
* use `security unlock-keychain login.keychain` to unlock the login keychain via the terminal
|
||||||
|
* this command also supports the `-p` command-line argument, which allows you to supply a password and unlock non-interactively
|
||||||
|
|
||||||
|
### Create and unlock the login keychain via the terminal
|
||||||
|
|
||||||
|
Compared to the previous approach, this one is fully automated, but might stop working at some point in the future:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
security create-keychain -p '' login.keychain
|
||||||
|
security unlock-keychain -p '' login.keychain
|
||||||
|
security login-keychain -s login.keychain
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that this will create a `login.keychain` with an empty password. Consider supplying a different value to `-p` or omitting the `-p` to enter the password interactively.
|
||||||
|
|
||||||
## Troubleshooting crashes
|
## Troubleshooting crashes
|
||||||
|
|
||||||
If you experience a crash or encounter another error while using the tart executable, you can collect debug information to assist with troubleshooting. Run the following command in a separate terminal window to gather logs from the Tart process and the macOS Virtualization subsystem:
|
If you experience a crash or encounter another error while using the tart executable, you can collect debug information to assist with troubleshooting. Run the following command in a separate terminal window to gather logs from the Tart process and the macOS Virtualization subsystem:
|
||||||
@@ -143,12 +179,12 @@ This is because Tart uses [Keychain](https://en.wikipedia.org/wiki/Keychain_(sof
|
|||||||
To unlock the Keychain in an SSH session, run the following command, which will ask for your user's password:
|
To unlock the Keychain in an SSH session, run the following command, which will ask for your user's password:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
security unlock-keychain
|
security unlock-keychain login.keychain
|
||||||
```
|
```
|
||||||
|
|
||||||
This command also supports the `-p` command-line argument that allows you to supply the password and unlock non-interactively, which is great for scripts.
|
This command also supports the `-p` command-line argument that allows you to supply a password and unlock non-interactively, which is great for scripts.
|
||||||
|
|
||||||
If that doesn't work for you for some reason, you can pass the credentials via the environment variables, see [Registry Authorization](integrations/vm-management.md#registry-authorization) for more details on how to do that.
|
Alternatively, you can pass the credentials via the environment variables, see [Registry Authorization](integrations/vm-management.md#registry-authorization) for more details on how to do that.
|
||||||
|
|
||||||
## How is Tart different from Anka?
|
## How is Tart different from Anka?
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
pytest
|
pytest
|
||||||
testcontainers
|
testcontainers
|
||||||
requests == 2.31.0 # work around https://github.com/psf/requests/issues/6707
|
requests
|
||||||
bitmath
|
bitmath
|
||||||
pytest-dependency
|
pytest-dependency
|
||||||
paramiko
|
paramiko
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ def test_run(tart, run_opts):
|
|||||||
vm_name = f"integration-test-run-{uuid.uuid4()}"
|
vm_name = f"integration-test-run-{uuid.uuid4()}"
|
||||||
|
|
||||||
# Instantiate a VM with admin:admin SSH access
|
# Instantiate a VM with admin:admin SSH access
|
||||||
tart.run(["clone", "ghcr.io/cirruslabs/macos-sonoma-base:latest", vm_name])
|
tart.run(["clone", "ghcr.io/cirruslabs/macos-tahoe-base:latest", vm_name])
|
||||||
|
|
||||||
# Run the VM asynchronously
|
# Run the VM asynchronously
|
||||||
tart_run_process = tart.run_async(["run", vm_name] + run_opts)
|
tart_run_process = tart.run_async(["run", vm_name] + run_opts)
|
||||||
|
|||||||
Reference in New Issue
Block a user