Compare commits

...
8 Commits
Author SHA1 Message Date
Fedor Korotkov 227301436c Revert "Drop Monterey Support (#843)" (#893)
This reverts commit 017592075f.
2024-08-14 14:57:55 -04:00
Nikolay Edigaryev 106eb5a2c8 tart push: re-try when encountering errors when pushing disk layers (#888)
* tart push: re-try when encountering errors when pushing disk layers

* Only re-try on URLError
2024-08-10 13:06:39 -04:00
Nikolay Edigaryev 10bf706653 tart push: avoid uploading blobs if they are already present (#887)
By issuing HEAD requests to the registry before doing the actual upload.
2024-08-09 17:26:20 +04:00
Fedor Korotkov ff928ad77d Optimize DiskV2 Deduplication (#878)
* Revert "Lowercase `tart.app` (#751)"

This reverts commit a9e2a19015.

* Optimize DiskV2 deduplication logic

In case we cloned `disk.img` from a local image, check if data at offset has the expected contents already.

* Hole punch only if needed

* Calculate hash only if needed

* subdataChunks optimization

* Reapply "Lowercase `tart.app` (#751)"

This reverts commit e74e9c845a.

* format

* Save at least 1GB on deduplication logic

* Build separately

* Revert "subdataChunks optimization"

This reverts commit e59382aeba.

* Another optimization

* Removed debug log

* reformat

* Revert "Hole punch only if needed"

This reverts commit 8c569fc5
2024-08-05 12:24:31 -04:00
Nikolay Edigaryev 33b5cfe2ed tart run: delay tilde (~) expansion until we're dealing with local path (#880) 2024-08-05 15:42:10 +04:00
Nikolay Edigaryev 3892cdb00d tart run: replace --sync with --root-disk-opts (#879)
* VZDiskImageSynchronizationMode's "description" field is a dead code

* Re-use the VZDiskImageSynchronizationMode extension

* tart run: replace --sync with --root-disk-opts

* VM: support root disk synchronization mode on macOS
2024-08-05 15:17:58 +04:00
Nicholas FitzRoy-Dale 5f2199ef3e Support setting root disk synchronization mode (#875)
* Support setting root disk synchronization mode

Adds a new VMConfig parameter (tart get / tart set) called 'sync' which
can be set to 'full' (default), 'fsync', or 'none', corresponding with
the values of VZDiskImageSynchronizationMode and allowing a tradeoff
between data integrity and speed.

* Remove unused import

* Fix formatting

* Make root disk sync behaviour a commandline option
2024-08-05 13:12:43 +04:00
Fedor Korotkov 3f26baa341 Update testimonials to focus on Tart (#876)
Cirrus Runners have their own testimonials and a website now. No need to mix things together.
2024-08-01 17:09:08 +04:00
15 changed files with 296 additions and 200 deletions
+2 -2
View File
@@ -8,11 +8,11 @@ task:
name: dev-mini
resources:
tart-vms: 1
build_script:
- swift build
test_script:
- swift test
integration_test_script:
# Build Tart
- swift build
- codesign --sign - --entitlements Resources/tart-dev.entitlements --force .build/debug/tart
- export PATH=$(pwd)/.build/arm64-apple-macosx/debug:$PATH
# Run integration tests
+4 -1
View File
@@ -17,7 +17,7 @@ service — a drop-in replacement for the standard GitHub-hosted runners, offeri
</a>
</p>
Many companies are using Tart in their internal setups. Here are a few of them:
Many companies are using Tart in their internal setups. Here are just a few of them:
<p align="center">
<a href="https://atlassian.com/" target=_blank>
@@ -47,6 +47,9 @@ Many companies are using Tart in their internal setups. Here are a few of them:
<a href="https://www.pitsdatarecovery.net/" target=_blank>
<img src="https://github.com/cirruslabs/tart/raw/main/Resources/Users/PITSGlobalDataRecoveryServices.png" height="65"/>
</a>
<a href="https://expo.dev/" target=_blank>
<img src="https://github.com/cirruslabs/tart/raw/main/Resources/Users/Expo.png" height="65"/>
</a>
</p>
**Note:** If your company or project is using Tart please consider [sharing with the community](https://github.com/cirruslabs/tart/discussions/857).
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

+82 -59
View File
@@ -12,6 +12,39 @@ var vm: VM?
struct IPNotFound: Error {
}
@available(macOS 14, *)
extension VZDiskSynchronizationMode {
public init(_ description: String) throws {
switch description {
case "none":
self = .none
case "full":
self = .full
case "":
self = .full
default:
throw RuntimeError.VMConfigurationError("unsupported disk synchronization mode: \"\(description)\"")
}
}
}
extension VZDiskImageSynchronizationMode {
public init(_ description: String) throws {
switch description {
case "none":
self = .none
case "fsync":
self = .fsync
case "full":
self = .full
case "":
self = .full
default:
throw RuntimeError.VMConfigurationError("unsupported disk image synchronization mode: \"\(description)\"")
}
}
}
struct Run: AsyncParsableCommand {
static var configuration = CommandConfiguration(abstract: "Run a VM")
@@ -140,6 +173,20 @@ struct Run: AsyncParsableCommand {
@Flag(help: ArgumentHelp("Restrict network access to the host-only network"))
var netHost: Bool = false
@Option(help: ArgumentHelp("Set the root disk options (e.g. --root-disk-opts=\"ro\" or --root-disk-opts=\"sync=none\")",
discussion: """
Options are comma-separated and are as follows:
* ro — attach the root disk in read-only mode instead of the default read-write (e.g. --root-disk-opts="ro")
* sync=none — disable data synchronization with the permanent storage to increase performance at the cost of a higher chance of data loss (e.g. --root-disk-opts="sync=none")
* sync=fsync — enable data synchronization with the permanent storage, but don't ensure that it was actually written (e.g. --root-disk-opts="sync=fsync")
* sync=full — enable data synchronization with the permanent storage and ensure that it was actually written (e.g. --root-disk-opts="sync=full")
""", valueName: "options"))
var rootDiskOpts: String = ""
#if arch(arm64)
@Flag(help: ArgumentHelp("Disables audio and entropy devices and switches to only Mac-specific input devices.", discussion: "Useful for running a VM that can be suspended via \"tart suspend\"."))
#endif
@@ -182,7 +229,7 @@ struct Run: AsyncParsableCommand {
if suspendable {
let config = try VMConfig.init(fromURL: vmDir.configURL)
if (config.platform is Linux) {
if !(config.platform is PlatformSuspendable) {
throw ValidationError("You can only suspend macOS VMs")
}
if dir.count > 0 {
@@ -219,8 +266,6 @@ struct Run: AsyncParsableCommand {
try Softnet.configureSUIDBitIfNeeded()
}
let additionalDiskAttachments = try additionalDiskAttachments()
var serialPorts: [VZSerialPortConfiguration] = []
if serial {
let tty_fd = createPTY()
@@ -239,15 +284,19 @@ struct Run: AsyncParsableCommand {
serialPorts.append(createSerialPortConfiguration(tty_read!, tty_write!))
}
// Parse root disk options
let diskOptions = DiskOptions(rootDiskOpts)
vm = try VM(
vmDir: vmDir,
network: userSpecifiedNetwork(vmDir: vmDir) ?? NetworkShared(),
additionalStorageDevices: additionalDiskAttachments,
additionalStorageDevices: try additionalDiskAttachments(),
directorySharingDevices: directoryShares() + rosettaDirectoryShare(),
serialPorts: serialPorts,
suspendable: suspendable,
audio: !noAudio,
clipboard: !noClipboard
clipboard: !noClipboard,
sync: VZDiskImageSynchronizationMode(diskOptions.syncModeRaw)
)
let vncImpl: VNC? = try {
@@ -455,15 +504,9 @@ struct Run: AsyncParsableCommand {
}
func additionalDiskAttachments() throws -> [VZStorageDeviceConfiguration] {
var result: [VZStorageDeviceConfiguration] = []
let expandedDiskPaths = disk.map { NSString(string:$0).expandingTildeInPath }
for rawDisk in expandedDiskPaths {
result.append(try AdditionalDisk(parseFrom: rawDisk).configuration)
try disk.map {
try AdditionalDisk(parseFrom: $0).configuration
}
return result
}
func directoryShares() throws -> [VZDirectorySharingDeviceConfiguration] {
@@ -700,18 +743,20 @@ struct AdditionalDisk {
throw UnsupportedOSError("attaching Network Block Devices", "are")
}
let syncMode = try parseSyncMode(syncModeRaw)
let nbdAttachment = try VZNetworkBlockDeviceStorageDeviceAttachment(
url: diskURL!,
timeout: 30,
isForcedReadOnly: diskReadOnly,
synchronizationMode: syncMode
synchronizationMode: try VZDiskSynchronizationMode(syncModeRaw)
)
return VZVirtioBlockDeviceConfiguration(attachment: nbdAttachment)
}
// Expand the tilde (~) since at this point we're dealing with a local path,
// and "expandingTildeInPath" seems to corrupt the remote URLs like nbd://
let diskPath = NSString(string: diskPath).expandingTildeInPath
let diskFileURL = URL(fileURLWithPath: diskPath)
if pathHasMode(diskPath, mode: S_IFBLK) {
@@ -734,7 +779,7 @@ struct AdditionalDisk {
}
let blockAttachment = try VZDiskBlockDeviceStorageDeviceAttachment(fileHandle: FileHandle(fileDescriptor: fd, closeOnDealloc: true),
readOnly: diskReadOnly, synchronizationMode: try parseSyncMode(syncModeRaw))
readOnly: diskReadOnly, synchronizationMode: try VZDiskSynchronizationMode(syncModeRaw))
return VZVirtioBlockDeviceConfiguration(attachment: blockAttachment)
}
@@ -768,7 +813,7 @@ struct AdditionalDisk {
url: diskFileURL,
readOnly: diskReadOnly,
cachingMode: .automatic,
synchronizationMode: try Self.parseImageSyncMode(syncModeRaw)
synchronizationMode: try VZDiskImageSynchronizationMode(syncModeRaw)
)
return VZVirtioBlockDeviceConfiguration(attachment: diskImageAttachment)
@@ -776,58 +821,36 @@ struct AdditionalDisk {
static func parseOptions(_ parseFrom: String) -> (String, Bool, String) {
var arguments = parseFrom.split(separator: ":")
let options = arguments.last!.split(separator: ",")
var readOnly: Bool = false
var syncModeRaw: String = ""
let options = DiskOptions(String(arguments.last!))
if options.foundAtLeastOneOption {
arguments.removeLast()
}
var foundAtLeastOneOption: Bool = false
return (arguments.joined(separator: ":"), options.readOnly, options.syncModeRaw)
}
}
struct DiskOptions {
var readOnly: Bool = false
var syncModeRaw: String = ""
var foundAtLeastOneOption: Bool = false
init(_ parseFrom: String) {
let options = parseFrom.split(separator: ",")
for option in options {
switch true {
case option == "ro":
readOnly = true
foundAtLeastOneOption = true
self.readOnly = true
self.foundAtLeastOneOption = true
case option.hasPrefix("sync="):
syncModeRaw = String(option.dropFirst("sync=".count))
foundAtLeastOneOption = true
self.syncModeRaw = String(option.dropFirst("sync=".count))
self.foundAtLeastOneOption = true
default:
continue
}
}
if foundAtLeastOneOption {
arguments.removeLast()
}
return (arguments.joined(separator: ":"), readOnly, syncModeRaw)
}
@available(macOS 14, *)
static func parseSyncMode(_ parseFrom: String) throws -> VZDiskSynchronizationMode {
switch parseFrom {
case "none":
return .none
case "full":
return .full
case "":
return .full
default:
throw RuntimeError.VMConfigurationError("unsupported disk synchronization mode: \"\(parseFrom)\"")
}
}
static func parseImageSyncMode(_ parseFrom: String) throws -> VZDiskImageSynchronizationMode {
switch parseFrom {
case "none":
return .none
case "full":
return .full
case "":
return .full
default:
throw RuntimeError.VMConfigurationError("unsupported disk image synchronization mode: \"\(parseFrom)\"")
}
}
}
+23 -6
View File
@@ -1,12 +1,19 @@
import Foundation
struct LocalLayerCache {
struct DigestInfo {
let range: Range<Data.Index>
let compressedDigest: String
let uncompressedContentDigest: String?
}
let name: String
let deduplicatedBytes: UInt64
let diskURL: URL
private let mappedDisk: Data
private var digestToRange: [String : Range<Data.Index>] = [:]
private var digestToRange: [String: DigestInfo] = [:]
private var offsetToRange: [UInt64: DigestInfo] = [:]
init?(_ name: String, _ deduplicatedBytes: UInt64, _ diskURL: URL, _ manifest: OCIManifest) throws {
self.name = name
@@ -24,17 +31,27 @@ struct LocalLayerCache {
return nil
}
self.digestToRange[layer.digest] = Int(offset)..<Int(offset+uncompressedSize)
let info = DigestInfo(
range: Int(offset)..<Int(offset + uncompressedSize),
compressedDigest: layer.digest,
uncompressedContentDigest: layer.uncompressedContentDigest()!
)
self.digestToRange[layer.digest] = info
self.offsetToRange[offset] = info
offset += uncompressedSize
}
}
func find(_ digest: String) -> Data? {
guard let foundRange = self.digestToRange[digest] else {
return nil
func findInfo(digest: String, offsetHint: UInt64) -> DigestInfo? {
// Layers can have the same digests, for example, empty ones. Let's use the offset hint to make a better guess.
if let info = self.offsetToRange[offsetHint], info.compressedDigest == digest {
return info
}
return self.digestToRange[digest]
}
return self.mappedDisk.subdata(in: foundRange)
func subdata(_ range: Range<Data.Index>) -> Data {
return self.mappedDisk.subdata(in: range)
}
}
+35 -9
View File
@@ -1,6 +1,7 @@
import Foundation
import Compression
import System
import Retry
class DiskV2: Disk {
private static let bufferSizeBytes = 4 * 1024 * 1024
@@ -26,8 +27,22 @@ class DiskV2: Disk {
// Launch a disk layer pushing task
group.addTask {
let compressedData = try (data as NSData).compressed(using: .lz4) as Data
let compressedDataDigest = Digest.hash(compressedData)
let layerDigest = try await registry.pushBlob(fromData: compressedData, chunkSizeMb: chunkSizeMb)
try await retry(maxAttempts: 5, backoff: .exponentialWithFullJitter(baseDelay: .seconds(5), maxDelay: .seconds(60))) {
if try await !registry.blobExists(compressedDataDigest) {
_ = try await registry.pushBlob(fromData: compressedData, chunkSizeMb: chunkSizeMb, digest: compressedDataDigest)
}
} recoverFromFailure: { error in
if error is URLError {
print("Error: \(error.localizedDescription)")
print("Attempting to re-try...")
return .retry
}
return .throw
}
// Update progress using a relative value
progress.completedUnitCount += Int64(data.count)
@@ -35,7 +50,7 @@ class DiskV2: Disk {
return (index, OCIManifestLayer(
mediaType: diskV2MediaType,
size: compressedData.count,
digest: layerDigest,
digest: compressedDataDigest,
uncompressedSize: UInt64(data.count),
uncompressedContentDigest: Digest.hash(data)
))
@@ -121,11 +136,14 @@ class DiskV2: Disk {
// Launch a fetching and decompression task
group.addTask {
// No need to fetch and decompress anything if we've already done so
if try pullResumed && Digest.hash(diskURL, offset: diskWritingOffset, size: uncompressedLayerSize) == uncompressedLayerContentDigest {
// Update the progress
progress.completedUnitCount += Int64(diskLayer.size)
if pullResumed {
// do not check hash in the condition above to make it lazy e.g. only do expensive calculations if needed
if try Digest.hash(diskURL, offset: diskWritingOffset, size: uncompressedLayerSize) == uncompressedLayerContentDigest {
// Update the progress
progress.completedUnitCount += Int64(diskLayer.size)
return
return
}
}
// Open the disk file for writing
@@ -140,9 +158,17 @@ class DiskV2: Disk {
}
// Check if we already have this layer contents in the local layer cache
if let localLayerCache = localLayerCache, let data = localLayerCache.find(diskLayer.digest), Digest.hash(data) == uncompressedLayerContentDigest {
// Fulfil the layer contents from the local blob cache
_ = try zeroSkippingWrite(disk, rdisk, fsBlockSize, diskWritingOffset, data)
if let localLayerCache = localLayerCache, let localLayerInfo = localLayerCache.findInfo(digest: diskLayer.digest, offsetHint: diskWritingOffset) {
// indicates that the locally cloned disk image has the same content at the given offset
let localHit = localLayerInfo.uncompressedContentDigest == uncompressedLayerContentDigest
&& localLayerInfo.range.lowerBound == diskWritingOffset
// doesn't seem that localHit can ever be false if the localLayerCache is not nil
// but let's just add extra safety here and check it
if !localHit {
// Fulfil the layer contents from the local blob cache
let data = localLayerCache.subdata(localLayerInfo.range)
_ = try zeroSkippingWrite(disk, rdisk, fsBlockSize, diskWritingOffset, data)
}
try disk.close()
// Update the progress
+17 -2
View File
@@ -10,6 +10,7 @@ enum RegistryError: Error {
}
enum HTTPMethod: String {
case HEAD = "HEAD"
case GET = "GET"
case POST = "POST"
case PUT = "PUT"
@@ -21,6 +22,7 @@ enum HTTPCode: Int {
case Created = 201
case Accepted = 202
case Unauthorized = 401
case NotFound = 404
}
extension Data {
@@ -189,7 +191,7 @@ class Registry {
return URLComponents(url: uploadLocation.absolutize(baseURL), resolvingAgainstBaseURL: true)!
}
public func pushBlob(fromData: Data, chunkSizeMb: Int = 0) async throws -> String {
public func pushBlob(fromData: Data, chunkSizeMb: Int = 0, digest: String? = nil) async throws -> String {
// Initiate a blob upload
let (data, postResponse) = try await dataRequest(.POST, endpointURL("\(namespace)/blobs/uploads/"),
headers: ["Content-Length": "0"])
@@ -201,7 +203,7 @@ class Registry {
// Figure out where to upload the blob
var uploadLocation = try uploadLocationFromResponse(postResponse)
let digest = Digest.hash(fromData)
let digest = digest ?? Digest.hash(fromData)
if chunkSizeMb == 0 {
// monolithic upload
@@ -249,6 +251,19 @@ class Registry {
return digest
}
public func blobExists(_ digest: String) async throws -> Bool {
let (data, response) = try await dataRequest(.HEAD, endpointURL("\(namespace)/blobs/\(digest)"))
switch response.statusCode {
case HTTPCode.Ok.rawValue:
return true
case HTTPCode.NotFound.rawValue:
return false
default:
throw RegistryError.UnexpectedHTTPStatusCode(when: "checking blob", code: response.statusCode, details: data.asText())
}
}
public func pullBlob(_ digest: String, handler: (Data) async throws -> Void) async throws {
let (channel, response) = try await channelRequest(.GET, endpointURL("\(namespace)/blobs/\(digest)"), viaFile: true)
if response.statusCode != HTTPCode.Ok.rawValue {
+18 -4
View File
@@ -8,7 +8,7 @@ struct UnsupportedHostOSError: Error, CustomStringConvertible {
#if arch(arm64)
struct Darwin: Platform {
struct Darwin: PlatformSuspendable {
var ecid: VZMacMachineIdentifier
var hardwareModel: VZMacHardwareModel
@@ -103,18 +103,32 @@ struct UnsupportedHostOSError: Error, CustomStringConvertible {
func keyboards() -> [VZKeyboardConfiguration] {
if #available(macOS 14, *) {
// Mac keyboard is only supported by guests starting with macOS Ventura
return [VZMacKeyboardConfiguration()]
return [VZUSBKeyboardConfiguration(), VZMacKeyboardConfiguration()]
} else {
return [VZUSBKeyboardConfiguration()]
}
}
func keyboardsSuspendable() -> [VZKeyboardConfiguration] {
if #available(macOS 14, *) {
return [VZMacKeyboardConfiguration()]
} else {
// fallback to the regular configuration
return keyboards()
}
}
func pointingDevices() -> [VZPointingDeviceConfiguration] {
if #available(macOS 13, *) {
// Trackpad is only supported by guests starting with macOS Ventura
[VZUSBScreenCoordinatePointingDeviceConfiguration(), VZMacTrackpadConfiguration()]
}
func pointingDevicesSuspendable() -> [VZPointingDeviceConfiguration] {
if #available(macOS 14, *) {
return [VZMacTrackpadConfiguration()]
} else {
// fallback to the regular configuration
return [VZUSBScreenCoordinatePointingDeviceConfiguration()]
return pointingDevices()
}
}
}
+5
View File
@@ -8,3 +8,8 @@ protocol Platform: Codable {
func keyboards() -> [VZKeyboardConfiguration]
func pointingDevices() -> [VZPointingDeviceConfiguration]
}
protocol PlatformSuspendable: Platform {
func pointingDevicesSuspendable() -> [VZPointingDeviceConfiguration]
func keyboardsSuspendable() -> [VZKeyboardConfiguration]
}
-1
View File
@@ -17,7 +17,6 @@ extension URL: Prunable {
func deduplicatedSizeBytes() throws -> Int {
let values = try resourceValues(forKeys: [.totalFileAllocatedSizeKey, .mayShareFileContentKey])
// make sure the file's origin file is there and duplication works
var dedublicatedSize = 0
if values.mayShareFileContent == true {
return Int(deduplicatedBytes())
}
+15 -7
View File
@@ -48,7 +48,8 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
serialPorts: [VZSerialPortConfiguration] = [],
suspendable: Bool = false,
audio: Bool = true,
clipboard: Bool = true
clipboard: Bool = true,
sync: VZDiskImageSynchronizationMode = .full
) throws {
name = vmDir.name
config = try VMConfig.init(fromURL: vmDir.configURL)
@@ -66,7 +67,8 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
serialPorts: serialPorts,
suspendable: suspendable,
audio: audio,
clipboard: clipboard
clipboard: clipboard,
sync: sync
)
virtualMachine = VZVirtualMachine(configuration: configuration)
@@ -294,7 +296,8 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
serialPorts: [VZSerialPortConfiguration],
suspendable: Bool = false,
audio: Bool = true,
clipboard: Bool = true
clipboard: Bool = true,
sync: VZDiskImageSynchronizationMode = .full
) throws -> VZVirtualMachineConfiguration {
let configuration = VZVirtualMachineConfiguration()
@@ -326,8 +329,13 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
configuration.audioDevices = [soundDeviceConfiguration]
// Keyboard and mouse
configuration.keyboards = vmConfig.platform.keyboards()
configuration.pointingDevices = vmConfig.platform.pointingDevices()
if suspendable, let platformSuspendable = vmConfig.platform.self as? PlatformSuspendable {
configuration.keyboards = platformSuspendable.keyboardsSuspendable()
configuration.pointingDevices = platformSuspendable.pointingDevicesSuspendable()
} else {
configuration.keyboards = vmConfig.platform.keyboards()
configuration.pointingDevices = vmConfig.platform.pointingDevices()
}
// Networking
configuration.networkDevices = network.attachments().map {
@@ -350,8 +358,8 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
// Storage
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)
try VZDiskImageStorageDeviceAttachment(url: diskURL, readOnly: false, cachingMode: .cached, synchronizationMode: sync) :
try VZDiskImageStorageDeviceAttachment(url: diskURL, readOnly: false, cachingMode: .automatic, synchronizationMode: sync)
var device: VZStorageDeviceConfiguration
if #available(macOS 14, *), vmConfig.os == .linux {
+7 -7
View File
@@ -208,14 +208,14 @@ class VMStorageOCI: PrunableStorage {
try await tmpVMDir.pullFromRegistry(registry: registry, manifest: manifest, concurrency: concurrency, localLayerCache: localLayerCache)
} recoverFromFailure: { error in
if error is RuntimeError {
return .throw
if error is Retryable {
print("Error: \(error.localizedDescription)")
print("Attempting to re-try...")
return .retry
}
print("Error: \(error.localizedDescription)")
print("Attempting to re-try...")
return .retry
return .throw
}
try move(digestName, from: tmpVMDir)
transaction.finish()
@@ -297,7 +297,7 @@ class VMStorageOCI: PrunableStorage {
// Now, find the best match based on how many bytes we'll deduplicate
let choosen = candidates.filter {
$0.deduplicatedBytes > 0
$0.deduplicatedBytes > 1024 * 1024 * 1024 // save at least 1GB
}.max { left, right in
return left.deduplicatedBytes < right.deduplicatedBytes
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

+88 -102
View File
@@ -85,10 +85,10 @@
<!-- landing image -->
<div class="tx-landing__image">
<dotlottie-player
src="/assets/animations/TartLogo.lottie"
mode="normal"
style="width: 75%; margin: auto"
autoplay
src="/assets/animations/TartLogo.lottie"
mode="normal"
style="width: 75%; margin: auto"
autoplay
/>
</div>
@@ -114,10 +114,9 @@
<header class="md-typeset">
<h1 id="virtualization-and-beyond">
Virtualization and beyond
<a
href="#virtualization-and-beyond"
class="headerlink"
title="Permanent link"
<a href="#virtualization-and-beyond"
class="headerlink"
title="Permanent link"
>
</a>
@@ -125,12 +124,11 @@
</header>
<div class="mdx-spotlight">
<figure class="mdx-spotlight__feature">
<img
src="assets/images/spotlight/virtualization-framework.webp"
alt="Apple’s native Virtualization.Framework"
loading="lazy"
width="500"
height="212"
<img src="assets/images/spotlight/virtualization-framework.webp"
alt="Apple’s native Virtualization.Framework"
loading="lazy"
width="500"
height="212"
/>
<figcaption class="md-typeset">
<h2>Native performance</h2>
@@ -144,12 +142,11 @@
</figcaption>
</figure>
<figure class="mdx-spotlight__feature">
<img
src="assets/images/spotlight/supported-registries.webp"
alt="OCI-compatible container registries"
loading="lazy"
width="500"
height="160"
<img src="assets/images/spotlight/supported-registries.webp"
alt="OCI-compatible container registries"
loading="lazy"
width="500"
height="160"
/>
<figcaption class="md-typeset">
<h2>Remote storage for Virtual Machines</h2>
@@ -161,39 +158,39 @@
</figcaption>
</figure>
<figure class="mdx-spotlight__feature">
<img
src="assets/images/spotlight/github-actions-runners.webp"
alt="GitHub Actions Runners"
loading="lazy"
width="500"
height="280"
<img src="assets/images/spotlight/github-actions-runners.webp"
alt="GitHub Actions Runners"
loading="lazy"
width="500"
height="280"
/>
<figcaption class="md-typeset">
<h2>Seamless integration with your existing automations</h2>
<p>
Tart integrates with many continuous integration systems, including a dedicated
service of on-demand GitHub Actions Runners. With a single line change, you can cut your
CI/CD costs by up to <b>30 times</b> by using <a href="https://cirrus-runners.app/">Cirrus Runners</a>
CI/CD costs by up to <b>30 times</b> by using <a href="https://cirrus-runners.app/">Cirrus
Runners</a>
to run your workflows.
</p>
</figcaption>
</figure>
<figure class="mdx-spotlight__feature">
<div id="lottie-player">
<dotlottie-player
src="/assets/animations/Orchard.lottie"
mode="normal"
style="height: 280px; margin: auto"
autoplay
loop
<dotlottie-player src="/assets/animations/Orchard.lottie"
mode="normal"
style="height: 280px; margin: auto"
autoplay
loop
/>
</div>
<figcaption class="md-typeset">
<h2>Run at scale with <a href="https://github.com/cirruslabs/orchard">Orchard</a></h2>
<p>
Tart toolset includes Orchard Orchestration &mdash; tool to run and manage Tart virtual machines
at scale on a cluster of Apple Silicon hosts. An Orchard Cluster exposes a simple REST API to manage
thousands virtual machines. Orchard CLI allows accessing remote virtual machines like they run locally.
Tart toolset includes Orchard Orchestration &mdash; tool to run and manage Tart virtual
machines at scale on a cluster of Apple Silicon hosts. An Orchard Cluster exposes a simple REST API to
manage thousands virtual machines. Orchard CLI allows accessing remote virtual machines like they run
locally.
</p>
</figcaption>
</figure>
@@ -208,38 +205,38 @@
<header class="md-typeset">
<h1 id="powerhouse">
Automation Powerhouse
<a
href="#powerhouse"
class="headerlink"
title="Permanent link"
<a href="#powerhouse"
class="headerlink"
title="Permanent link"
>
</a>
</h1>
</header>
<script>
fetch("https://api.github.com/repos/cirruslabs/tart/releases?per_page=100")
.then((response) => response.json())
.then((releases) => {
let allDownloads = 0;
for (let release of releases) {
for (let asset of release.assets) {
if (asset && asset.content_type === "application/octet-stream") {
allDownloads += asset.download_count || 0
}
}
}
let counterElement = document.getElementById('installation-counter');
if (counterElement) {
// Live installation count is available starting version 1.0.0
// Prior Tart was installed a little over 14,000 times, let's count them too
let installationPriorV1 = 14
counterElement.textContent = (installationPriorV1 + Math.round(allDownloads / 1000)) + ",000"
}
})
fetch("https://api.github.com/repos/cirruslabs/tart/releases?per_page=100")
.then((response) => response.json())
.then((releases) => {
let allDownloads = 0;
for (let release of releases) {
for (let asset of release.assets) {
if (asset && asset.content_type === "application/octet-stream") {
allDownloads += asset.download_count || 0
}
}
}
let counterElement = document.getElementById('installation-counter');
if (counterElement) {
// Live installation count is available starting version 1.0.0
// Prior Tart was installed a little over 14,000 times, let's count them too
let installationPriorV1 = 14
counterElement.textContent = (installationPriorV1 + Math.round(allDownloads / 1000)) + ",000"
}
})
</script>
<h2>
With more than <strong id="installation-counter">25,000</strong> installations to date, Tart has been adopted for various scenarios.
With more than <strong id="installation-counter">25,000</strong> installations to date, Tart has been
adopted for various scenarios.
Its applications range from powering CI/CD pipelines and reproducible local development environments,
to helping in the testing of device management systems without actual physical devices.
</h2>
@@ -254,10 +251,9 @@
<header class="md-typeset">
<h1 id="what-our-users-say">
What our users say
<a
href="#what-our-users-say"
class="headerlink"
title="Permanent link"
<a href="#what-our-users-say"
class="headerlink"
title="Permanent link"
>
</a>
@@ -265,70 +261,60 @@
</header>
<div class="mdx-users">
<figure class="mdx-users__testimonial">
<img
src="assets/images/users/mitchell-hashimoto.webp"
alt="Mitchell Hashimoto"
loading="lazy"
width="200"
height="200"
<img src="assets/images/users/mikhail-tokarev.webp"
alt="Mikhail Tokarev"
loading="lazy"
width="200"
height="200"
/>
<figcaption class="md-typeset">
<h2>Mitchell Hashimoto</h2>
<h3>
<a href="https://www.hashicorp.com/" target="_blank">HashiCorp</a> co-founder
Mikhail Tokarev, CTO at <a href="https://codemagic.io/start/" target="_blank">Codemagic</a>
</h3>
<hr/>
<cite>
I've been using "Cirrus Runners" since <a href="https://x.com/mitchellh/status/1731071326201561194" target="_blank">that tweet</a> and
it has been fantastic. Huge speed increase, huge cost decrease, zero maintenance, exactly what I wanted.
Thanks to the minimal overhead of using the Apple Virtualization
API, we’ve seen some performance improvements in booting new
virtual machines compared with Anka.
</cite>
</figcaption>
</figure>
<figure class="mdx-users__testimonial">
<img
src="assets/images/users/seb-jachec.webp"
alt="Sebastian Jachec"
loading="lazy"
width="200"
height="200"
<img src="assets/images/users/expo.webp"
alt="Expo"
loading="lazy"
width="200"
height="200"
/>
<figcaption class="md-typeset">
<h2>Sebastian Jachec</h2>
<h3>
Mobile Engineer at
<a href="https://daybridge.com/" target="_blank">Daybridge</a>
Infrastructure Team at <a href="https://expo.dev/" target="_blank">Expo</a>
</h3>
<hr/>
<cite>
It&rsquo;s been plain-sailing with the
<a href="/integrations/github-actions">Cirrus Runners</a>&nbsp;&mdash;
they&rsquo;ve been great! They&rsquo;re consistently&nbsp;60+%
faster on&nbsp;workflows that we&nbsp;previously used Github
Actions&rsquo; macOS runners for.
Tart was the practical way for us to use the Virtualization framework. Cirrus Labs’
continued maintenance and support gives us confidence, and it is also important for us
to be able to read the source code when we need to understand an abstraction layer below.
</cite>
</figcaption>
</figure>
<figure class="mdx-users__testimonial">
<img
src="assets/images/users/max-lapides.webp"
alt="Max Lapides"
loading="lazy"
width="200"
height="200"
<img src="assets/images/users/snowflake.webp"
alt="Snowflake"
loading="lazy"
width="200"
height="200"
/>
<figcaption class="md-typeset">
<h2>Max Lapides</h2>
<h3>
Senior Mobile Engineer at
<a href="https://www.tonal.com/" target="_blank">Tonal</a>
Red Team at <a href="https://www.snowflake.com/" target="_blank">Snowflake</a>
</h3>
<hr/>
<cite>
Previously, we were using the GitHub&#8209;hosted macOS runners
and our iOS build took ~30&nbsp;minutes. Now with
<a href="/integrations/github-actions">Cirrus Runners</a>, the iOS build only
takes ~12&nbsp;minutes. That’s a huge boost to our productivity,
and for only $150/month per runner it is much less expensive too.
The Snowflake Red Team had a need for macOS CI/CD and a segmented macOS development
environment. We solved this problem and shared our implementation with macOS EC2 and Tart.
We also automated this process with Terraform/Packer to simplify the deployment of our
infrastructure and machine images.
</cite>
</figcaption>
</figure>