mirror of
https://github.com/cirruslabs/tart.git
synced 2026-10-01 19:51:10 +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).
|
||||
|
||||
<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
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -218,7 +224,7 @@ struct Run: AsyncParsableCommand {
|
||||
// configuration file, otherwise we will loose the lock.
|
||||
//
|
||||
// [1]: https://man.openbsd.org/fcntl
|
||||
let lock = try PIDLock(lockURL: vmDir.configURL)
|
||||
let lock = try vmDir.lock()
|
||||
if try !lock.trylock() {
|
||||
throw RuntimeError.VMAlreadyRunning("VM \"\(name)\" is already running!")
|
||||
}
|
||||
@@ -653,7 +659,7 @@ struct DirectoryShare {
|
||||
|
||||
let archiveRequest = URLRequest(url: path, cachePolicy: .returnCacheDataElseLoad)
|
||||
var response: CachedURLResponse? = urlCache.cachedResponse(for: archiveRequest)
|
||||
if (response == nil) {
|
||||
if (response == nil || response?.data.isEmpty == true) {
|
||||
print("Downloading \(path)...")
|
||||
// download and unarchive remote directories if needed here
|
||||
// use old school API to prevent deadlocks since we are running via MainActor
|
||||
@@ -661,8 +667,12 @@ struct DirectoryShare {
|
||||
Task {
|
||||
do {
|
||||
let (archiveData, archiveResponse) = try await URLSession.shared.data(for: archiveRequest)
|
||||
urlCache.storeCachedResponse(CachedURLResponse(response: archiveResponse, data: archiveData, storagePolicy: .allowed), for: archiveRequest)
|
||||
print("Cached for future invocations!")
|
||||
if archiveData.isEmpty {
|
||||
print("Remote archive is empty!")
|
||||
} else {
|
||||
urlCache.storeCachedResponse(CachedURLResponse(response: archiveResponse, data: archiveData, storagePolicy: .allowed), for: archiveRequest)
|
||||
print("Cached for future invocations!")
|
||||
}
|
||||
} catch {
|
||||
print("Download failed: \(error)")
|
||||
}
|
||||
|
||||
@@ -29,12 +29,12 @@ struct Stop: AsyncParsableCommand {
|
||||
}
|
||||
|
||||
func stopRunning(_ vmDir: VMDirectory) async throws {
|
||||
let lock = try PIDLock(lockURL: vmDir.configURL)
|
||||
let lock = try vmDir.lock()
|
||||
|
||||
// Find the VM's PID
|
||||
var pid = try lock.pid()
|
||||
if pid == 0 {
|
||||
throw RuntimeError.VMNotRunning("VM \"\(name)\" is not running")
|
||||
throw RuntimeError.VMNotRunning(name)
|
||||
}
|
||||
|
||||
// Try to gracefully terminate the VM
|
||||
|
||||
@@ -11,7 +11,7 @@ struct Suspend: AsyncParsableCommand {
|
||||
|
||||
func run() async throws {
|
||||
let vmDir = try VMStorageLocal().open(name)
|
||||
let lock = try PIDLock(lockURL: vmDir.configURL)
|
||||
let lock = try vmDir.lock()
|
||||
|
||||
// Find the VM's PID
|
||||
var pid = try lock.pid()
|
||||
|
||||
+12
-3
@@ -326,9 +326,18 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
||||
}
|
||||
|
||||
// Storage
|
||||
var devices: [VZStorageDeviceConfiguration] = [
|
||||
VZVirtioBlockDeviceConfiguration(attachment: try VZDiskImageStorageDeviceAttachment(url: diskURL, readOnly: false))
|
||||
]
|
||||
let attachment: VZDiskImageStorageDeviceAttachment = vmConfig.os == .linux ?
|
||||
// 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)
|
||||
configuration.storageDevices = devices
|
||||
|
||||
|
||||
@@ -30,13 +30,17 @@ struct VMDirectory: Prunable {
|
||||
baseURL
|
||||
}
|
||||
|
||||
func lock() throws -> PIDLock {
|
||||
try PIDLock(lockURL: configURL)
|
||||
}
|
||||
|
||||
func running() throws -> Bool {
|
||||
// The most common reason why PIDLock() instantiation fails is a race with "tart delete" (ENOENT),
|
||||
// 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 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
|
||||
}
|
||||
|
||||
@@ -137,7 +141,15 @@ struct VMDirectory: Prunable {
|
||||
}
|
||||
|
||||
func delete() throws {
|
||||
let lock = try lock()
|
||||
|
||||
if try !lock.trylock() {
|
||||
throw RuntimeError.VMIsRunning(name)
|
||||
}
|
||||
|
||||
try FileManager.default.removeItem(at: baseURL)
|
||||
|
||||
try lock.unlock()
|
||||
}
|
||||
|
||||
func accessDate() throws -> Date {
|
||||
|
||||
@@ -50,7 +50,8 @@ enum RuntimeError : Error {
|
||||
case VMConfigurationError(_ message: String)
|
||||
case VMDoesNotExist(name: String)
|
||||
case VMMissingFiles(_ message: String)
|
||||
case VMNotRunning(_ message: String)
|
||||
case VMIsRunning(_ name: String)
|
||||
case VMNotRunning(_ name: String)
|
||||
case VMAlreadyRunning(_ message: String)
|
||||
case NoIPAddressFound(_ message: String)
|
||||
case DiskAlreadyInUse(_ message: String)
|
||||
@@ -81,8 +82,10 @@ extension RuntimeError : CustomStringConvertible {
|
||||
return "the specified VM \"\(name)\" does not exist"
|
||||
case .VMMissingFiles(let message):
|
||||
return message
|
||||
case .VMNotRunning(let message):
|
||||
return message
|
||||
case .VMIsRunning(let name):
|
||||
return "VM \"\(name)\" is running"
|
||||
case .VMNotRunning(let name):
|
||||
return "VM \"\(name)\" is not running"
|
||||
case .VMAlreadyRunning(let message):
|
||||
return message
|
||||
case .NoIPAddressFound(let message):
|
||||
|
||||
@@ -39,7 +39,7 @@ class VMStorageLocal: PrunableStorage {
|
||||
}
|
||||
|
||||
func delete(_ name: String) throws {
|
||||
try FileManager.default.removeItem(at: vmURL(name))
|
||||
try VMDirectory(baseURL: vmURL(name)).delete()
|
||||
}
|
||||
|
||||
func list() throws -> [(String, VMDirectory)] {
|
||||
|
||||
@@ -219,9 +219,7 @@ class VMStorageOCI: PrunableStorage {
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
|
||||
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"/>
|
||||
</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
|
||||
|
||||
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":
|
||||
- "GitHub Actions": integrations/github-actions.md
|
||||
- "GitLab Runner": integrations/gitlab-runner.md
|
||||
- "Buildkite": integrations/buildkite.md
|
||||
- "Self-hosted CI": integrations/cirrus-cli.md
|
||||
- "Managing VMs": integrations/vm-management.md
|
||||
- "Support & Licensing": licensing.md
|
||||
|
||||
Reference in New Issue
Block a user