mirror of
https://github.com/cirruslabs/tart.git
synced 2026-10-06 22:21:12 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
537f0ae5db | ||
|
|
02f1ff5238 | ||
|
|
9c9bcd586e | ||
|
|
60f0eac7a8 | ||
|
|
35377a3475 | ||
|
|
dda4e91a91 | ||
|
|
ac5f794e6d | ||
|
|
5bcbc77249 | ||
|
|
bf03873c8d |
@@ -65,6 +65,12 @@ Many more companies are using Tart in their internal setups. Here are a few of t
|
|||||||
|
|
||||||
**Note:** If your company or project is using Tart please consider [adding yourself to the list above](/Resources/Users/HowToAddYourself.md).
|
**Note:** If your company or project is using Tart please consider [adding yourself to the list above](/Resources/Users/HowToAddYourself.md).
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://aws.amazon.com/marketplace/pp/prodview-qczco34wlkdws?utm_source=github&utm_medium=referral" target=_blank>
|
||||||
|
<img src="https://github.com/cirruslabs/tart/raw/main/Resources/AWSMarkeplaceLogo.png" height="90"/>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Try running a Tart VM on your Apple Silicon device running macOS 13.0 (Ventura) or later (will download a 25 GB image):
|
Try running a Tart VM on your Apple Silicon device running macOS 13.0 (Ventura) or later (will download a 25 GB image):
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
@@ -140,6 +140,12 @@ struct Run: AsyncParsableCommand {
|
|||||||
throw ValidationError("Suspending VMs with shared directories is not supported")
|
throw ValidationError("Suspending VMs with shared directories is not supported")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for disk in disk {
|
||||||
|
if disk.hasSuffix("-amd64.iso") {
|
||||||
|
throw ValidationError("Seems you have a disk targeting x86 architecture (hence amd64 in the name). Please use an 'arm64' version of the disk.")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@@ -218,7 +224,7 @@ struct Run: AsyncParsableCommand {
|
|||||||
// configuration file, otherwise we will loose the lock.
|
// configuration file, otherwise we will loose the lock.
|
||||||
//
|
//
|
||||||
// [1]: https://man.openbsd.org/fcntl
|
// [1]: https://man.openbsd.org/fcntl
|
||||||
let lock = try PIDLock(lockURL: vmDir.configURL)
|
let lock = try vmDir.lock()
|
||||||
if try !lock.trylock() {
|
if try !lock.trylock() {
|
||||||
throw RuntimeError.VMAlreadyRunning("VM \"\(name)\" is already running!")
|
throw RuntimeError.VMAlreadyRunning("VM \"\(name)\" is already running!")
|
||||||
}
|
}
|
||||||
@@ -653,7 +659,7 @@ struct DirectoryShare {
|
|||||||
|
|
||||||
let archiveRequest = URLRequest(url: path, cachePolicy: .returnCacheDataElseLoad)
|
let archiveRequest = URLRequest(url: path, cachePolicy: .returnCacheDataElseLoad)
|
||||||
var response: CachedURLResponse? = urlCache.cachedResponse(for: archiveRequest)
|
var response: CachedURLResponse? = urlCache.cachedResponse(for: archiveRequest)
|
||||||
if (response == nil) {
|
if (response == nil || response?.data.isEmpty == true) {
|
||||||
print("Downloading \(path)...")
|
print("Downloading \(path)...")
|
||||||
// download and unarchive remote directories if needed here
|
// download and unarchive remote directories if needed here
|
||||||
// use old school API to prevent deadlocks since we are running via MainActor
|
// use old school API to prevent deadlocks since we are running via MainActor
|
||||||
@@ -661,8 +667,12 @@ struct DirectoryShare {
|
|||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
let (archiveData, archiveResponse) = try await URLSession.shared.data(for: archiveRequest)
|
let (archiveData, archiveResponse) = try await URLSession.shared.data(for: archiveRequest)
|
||||||
urlCache.storeCachedResponse(CachedURLResponse(response: archiveResponse, data: archiveData, storagePolicy: .allowed), for: archiveRequest)
|
if archiveData.isEmpty {
|
||||||
print("Cached for future invocations!")
|
print("Remote archive is empty!")
|
||||||
|
} else {
|
||||||
|
urlCache.storeCachedResponse(CachedURLResponse(response: archiveResponse, data: archiveData, storagePolicy: .allowed), for: archiveRequest)
|
||||||
|
print("Cached for future invocations!")
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
print("Download failed: \(error)")
|
print("Download failed: \(error)")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,12 +29,12 @@ struct Stop: AsyncParsableCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func stopRunning(_ vmDir: VMDirectory) async throws {
|
func stopRunning(_ vmDir: VMDirectory) async throws {
|
||||||
let lock = try PIDLock(lockURL: vmDir.configURL)
|
let lock = try vmDir.lock()
|
||||||
|
|
||||||
// Find the VM's PID
|
// Find the VM's PID
|
||||||
var pid = try lock.pid()
|
var pid = try lock.pid()
|
||||||
if pid == 0 {
|
if pid == 0 {
|
||||||
throw RuntimeError.VMNotRunning("VM \"\(name)\" is not running")
|
throw RuntimeError.VMNotRunning(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to gracefully terminate the VM
|
// Try to gracefully terminate the VM
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ struct Suspend: AsyncParsableCommand {
|
|||||||
|
|
||||||
func run() async throws {
|
func run() async throws {
|
||||||
let vmDir = try VMStorageLocal().open(name)
|
let vmDir = try VMStorageLocal().open(name)
|
||||||
let lock = try PIDLock(lockURL: vmDir.configURL)
|
let lock = try vmDir.lock()
|
||||||
|
|
||||||
// Find the VM's PID
|
// Find the VM's PID
|
||||||
var pid = try lock.pid()
|
var pid = try lock.pid()
|
||||||
|
|||||||
+12
-3
@@ -326,9 +326,18 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Storage
|
// Storage
|
||||||
var devices: [VZStorageDeviceConfiguration] = [
|
let attachment: VZDiskImageStorageDeviceAttachment = vmConfig.os == .linux ?
|
||||||
VZVirtioBlockDeviceConfiguration(attachment: try VZDiskImageStorageDeviceAttachment(url: diskURL, readOnly: false))
|
// Use "cached" caching mode for virtio drive to prevent fs corruption on linux
|
||||||
]
|
try VZDiskImageStorageDeviceAttachment(url: diskURL, readOnly: false, cachingMode: .cached, synchronizationMode: .full) :
|
||||||
|
try VZDiskImageStorageDeviceAttachment(url: diskURL, readOnly: false)
|
||||||
|
|
||||||
|
var device: VZStorageDeviceConfiguration
|
||||||
|
if #available(macOS 14, *), vmConfig.os == .linux {
|
||||||
|
device = VZNVMExpressControllerDeviceConfiguration(attachment: attachment)
|
||||||
|
} else {
|
||||||
|
device = VZVirtioBlockDeviceConfiguration(attachment: attachment)
|
||||||
|
}
|
||||||
|
var devices: [VZStorageDeviceConfiguration] = [device]
|
||||||
devices.append(contentsOf: additionalStorageDevices)
|
devices.append(contentsOf: additionalStorageDevices)
|
||||||
configuration.storageDevices = devices
|
configuration.storageDevices = devices
|
||||||
|
|
||||||
|
|||||||
@@ -30,13 +30,17 @@ struct VMDirectory: Prunable {
|
|||||||
baseURL
|
baseURL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func lock() throws -> PIDLock {
|
||||||
|
try PIDLock(lockURL: configURL)
|
||||||
|
}
|
||||||
|
|
||||||
func running() throws -> Bool {
|
func running() throws -> Bool {
|
||||||
// The most common reason why PIDLock() instantiation fails is a race with "tart delete" (ENOENT),
|
// The most common reason why PIDLock() instantiation fails is a race with "tart delete" (ENOENT),
|
||||||
// which is fine to report as "not running".
|
// which is fine to report as "not running".
|
||||||
//
|
//
|
||||||
// The other reasons are unlikely and the cost of getting a false positive is way less than
|
// The other reasons are unlikely and the cost of getting a false positive is way less than
|
||||||
// the cost of crashing with an exception when calling "tart list" on a busy machine, for example.
|
// the cost of crashing with an exception when calling "tart list" on a busy machine, for example.
|
||||||
guard let lock = try? PIDLock(lockURL: configURL) else {
|
guard let lock = try? lock() else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,7 +141,15 @@ struct VMDirectory: Prunable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func delete() throws {
|
func delete() throws {
|
||||||
|
let lock = try lock()
|
||||||
|
|
||||||
|
if try !lock.trylock() {
|
||||||
|
throw RuntimeError.VMIsRunning(name)
|
||||||
|
}
|
||||||
|
|
||||||
try FileManager.default.removeItem(at: baseURL)
|
try FileManager.default.removeItem(at: baseURL)
|
||||||
|
|
||||||
|
try lock.unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func accessDate() throws -> Date {
|
func accessDate() throws -> Date {
|
||||||
|
|||||||
@@ -50,7 +50,8 @@ enum RuntimeError : Error {
|
|||||||
case VMConfigurationError(_ message: String)
|
case VMConfigurationError(_ message: String)
|
||||||
case VMDoesNotExist(name: String)
|
case VMDoesNotExist(name: String)
|
||||||
case VMMissingFiles(_ message: String)
|
case VMMissingFiles(_ message: String)
|
||||||
case VMNotRunning(_ message: String)
|
case VMIsRunning(_ name: String)
|
||||||
|
case VMNotRunning(_ name: String)
|
||||||
case VMAlreadyRunning(_ message: String)
|
case VMAlreadyRunning(_ message: String)
|
||||||
case NoIPAddressFound(_ message: String)
|
case NoIPAddressFound(_ message: String)
|
||||||
case DiskAlreadyInUse(_ message: String)
|
case DiskAlreadyInUse(_ message: String)
|
||||||
@@ -81,8 +82,10 @@ extension RuntimeError : CustomStringConvertible {
|
|||||||
return "the specified VM \"\(name)\" does not exist"
|
return "the specified VM \"\(name)\" does not exist"
|
||||||
case .VMMissingFiles(let message):
|
case .VMMissingFiles(let message):
|
||||||
return message
|
return message
|
||||||
case .VMNotRunning(let message):
|
case .VMIsRunning(let name):
|
||||||
return message
|
return "VM \"\(name)\" is running"
|
||||||
|
case .VMNotRunning(let name):
|
||||||
|
return "VM \"\(name)\" is not running"
|
||||||
case .VMAlreadyRunning(let message):
|
case .VMAlreadyRunning(let message):
|
||||||
return message
|
return message
|
||||||
case .NoIPAddressFound(let message):
|
case .NoIPAddressFound(let message):
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class VMStorageLocal: PrunableStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func delete(_ name: String) throws {
|
func delete(_ name: String) throws {
|
||||||
try FileManager.default.removeItem(at: vmURL(name))
|
try VMDirectory(baseURL: vmURL(name)).delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
func list() throws -> [(String, VMDirectory)] {
|
func list() throws -> [(String, VMDirectory)] {
|
||||||
|
|||||||
@@ -219,9 +219,7 @@ class VMStorageOCI: PrunableStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func link(from: RemoteName, to: RemoteName) throws {
|
func link(from: RemoteName, to: RemoteName) throws {
|
||||||
if FileManager.default.fileExists(atPath: vmURL(from).path) {
|
try? FileManager.default.removeItem(at: vmURL(from))
|
||||||
try FileManager.default.removeItem(at: vmURL(from))
|
|
||||||
}
|
|
||||||
|
|
||||||
try FileManager.default.createSymbolicLink(at: vmURL(from), withDestinationURL: vmURL(to))
|
try FileManager.default.createSymbolicLink(at: vmURL(from), withDestinationURL: vmURL(to))
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 232 KiB |
@@ -0,0 +1,21 @@
|
|||||||
|
# Buildkite
|
||||||
|
|
||||||
|
It is possible to run [Buildkite](https://buildkite.com/) pipeline steps in isolated ephemeral Tart Virtual Machines with the help of [Tart Buildkite Plugin](https://github.com/cirruslabs/tart-buildkite-plugin):
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The most basic configuration looks like this:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
steps:
|
||||||
|
- command: uname -a
|
||||||
|
plugins:
|
||||||
|
- cirruslabs/tart#main:
|
||||||
|
image: ghcr.io/cirruslabs/macos-sonoma-base:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
This will run `uname -r` in a macOS Tart VM cloned from `ghcr.io/cirruslabs/macos-sonoma-base:latest`.
|
||||||
|
|
||||||
|
See plugin's [Configuration section](https://github.com/cirruslabs/tart-buildkite-plugin#configuration) for the full list of available options.
|
||||||
@@ -28,6 +28,38 @@ tart run sonoma-base
|
|||||||
<img src="https://github.com/cirruslabs/tart/raw/main/Resources/TartScreenshot.png"/>
|
<img src="https://github.com/cirruslabs/tart/raw/main/Resources/TartScreenshot.png"/>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
## VM images
|
||||||
|
|
||||||
|
The following macOS images are currently available:
|
||||||
|
|
||||||
|
* macOS 14 (Sonoma)
|
||||||
|
* `ghcr.io/cirruslabs/macos-sonoma-vanilla:latest`
|
||||||
|
* `ghcr.io/cirruslabs/macos-sonoma-base:latest`
|
||||||
|
* `ghcr.io/cirruslabs/macos-sonoma-xcode:latest`
|
||||||
|
* macOS 13 (Ventura)
|
||||||
|
* `ghcr.io/cirruslabs/macos-ventura-vanilla:latest`
|
||||||
|
* `ghcr.io/cirruslabs/macos-ventura-base:latest`
|
||||||
|
* `ghcr.io/cirruslabs/macos-ventura-xcode:latest`
|
||||||
|
* macOS 12 (Monterey)
|
||||||
|
* `ghcr.io/cirruslabs/macos-monterey-vanilla:latest`
|
||||||
|
* `ghcr.io/cirruslabs/macos-monterey-base:latest`
|
||||||
|
* `ghcr.io/cirruslabs/macos-monterey-xcode:latest`
|
||||||
|
|
||||||
|
There's also a [full list of images](https://github.com/orgs/cirruslabs/packages?tab=packages&q=macos-) in which you can discovery specific tags (e.g. `ghcr.io/cirruslabs/macos-monterey-xcode:15`) and [macOS-specific Packer templates](https://github.com/cirruslabs/macos-image-templates) that were used to generate these images.
|
||||||
|
|
||||||
|
For, Linux the options are as follows:
|
||||||
|
|
||||||
|
* Ubuntu
|
||||||
|
* `ghcr.io/cirruslabs/ubuntu:latest`
|
||||||
|
* Debian
|
||||||
|
* `ghcr.io/cirruslabs/debian:latest`
|
||||||
|
* Fedora
|
||||||
|
* `ghcr.io/cirruslabs/fedora:latest`
|
||||||
|
|
||||||
|
These Linux images can be ran natively on [Vetu](https://github.com/cirruslabs/vetu), our virtualization solution for Linux, assuming that Vetu itself is running on an `arm64` machine.
|
||||||
|
|
||||||
|
Similarly to macOS, there's also a [full list of images](https://github.com/orgs/cirruslabs/packages?repo_name=linux-image-templates) in which you can discovery specific tags (e.g. `ghcr.io/cirruslabs/ubuntu:22.04`) and [Linux-specific Packer templates](https://github.com/cirruslabs/linux-image-templates) that were used to generate these images.
|
||||||
|
|
||||||
## SSH access
|
## SSH access
|
||||||
|
|
||||||
If the guest VM is running and configured to accept incoming SSH connections you can conveniently connect to it like so:
|
If the guest VM is running and configured to accept incoming SSH connections you can conveniently connect to it like so:
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ nav:
|
|||||||
- "Integrations":
|
- "Integrations":
|
||||||
- "GitHub Actions": integrations/github-actions.md
|
- "GitHub Actions": integrations/github-actions.md
|
||||||
- "GitLab Runner": integrations/gitlab-runner.md
|
- "GitLab Runner": integrations/gitlab-runner.md
|
||||||
|
- "Buildkite": integrations/buildkite.md
|
||||||
- "Self-hosted CI": integrations/cirrus-cli.md
|
- "Self-hosted CI": integrations/cirrus-cli.md
|
||||||
- "Managing VMs": integrations/vm-management.md
|
- "Managing VMs": integrations/vm-management.md
|
||||||
- "Support & Licensing": licensing.md
|
- "Support & Licensing": licensing.md
|
||||||
|
|||||||
Reference in New Issue
Block a user