mirror of
https://github.com/cirruslabs/tart.git
synced 2026-10-18 11:55:36 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ed2ce159f | ||
|
|
89217c23a2 | ||
|
|
0d633de04a | ||
|
|
f59ef2722d | ||
|
|
7759c72a76 | ||
|
|
4cc8a9925a | ||
|
|
b16bbf587a | ||
|
|
022317bc17 |
+13
-5
@@ -1,14 +1,16 @@
|
|||||||
persistent_worker:
|
|
||||||
labels:
|
|
||||||
name: Mac-Mini-M1
|
|
||||||
|
|
||||||
task:
|
task:
|
||||||
name: Test
|
name: Test on Ventura
|
||||||
|
persistent_worker:
|
||||||
|
labels:
|
||||||
|
name: Mac-Mini-M1
|
||||||
|
build_script: swift test
|
||||||
test_script: swift test
|
test_script: swift test
|
||||||
|
|
||||||
task:
|
task:
|
||||||
name: Build
|
name: Build
|
||||||
only_if: $CIRRUS_TAG == ''
|
only_if: $CIRRUS_TAG == ''
|
||||||
|
macos_instance:
|
||||||
|
image: ghcr.io/cirruslabs/macos-monterey-xcode:14
|
||||||
build_script: swift build --product tart
|
build_script: swift build --product tart
|
||||||
sign_script: codesign --sign - --entitlements Resources/tart.entitlements --force .build/debug/tart
|
sign_script: codesign --sign - --entitlements Resources/tart.entitlements --force .build/debug/tart
|
||||||
binary_artifacts:
|
binary_artifacts:
|
||||||
@@ -17,7 +19,13 @@ task:
|
|||||||
task:
|
task:
|
||||||
name: Release
|
name: Release
|
||||||
only_if: $CIRRUS_TAG != ''
|
only_if: $CIRRUS_TAG != ''
|
||||||
|
macos_instance:
|
||||||
|
image: ghcr.io/cirruslabs/macos-monterey-xcode:14
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ENCRYPTED[!98ace8259c6024da912c14d5a3c5c6aac186890a8d4819fad78f3e0c41a4e0cd3a2537dd6e91493952fb056fa434be7c!]
|
GITHUB_TOKEN: ENCRYPTED[!98ace8259c6024da912c14d5a3c5c6aac186890a8d4819fad78f3e0c41a4e0cd3a2537dd6e91493952fb056fa434be7c!]
|
||||||
GORELEASER_KEY: ENCRYPTED[!9b80b6ef684ceaf40edd4c7af93014ee156c8aba7e6e5795f41c482729887b5c31f36b651491d790f1f668670888d9fd!]
|
GORELEASER_KEY: ENCRYPTED[!9b80b6ef684ceaf40edd4c7af93014ee156c8aba7e6e5795f41c482729887b5c31f36b651491d790f1f668670888d9fd!]
|
||||||
|
install_script: brew install go goreleaser/tap/goreleaser-pro
|
||||||
|
info_script:
|
||||||
|
- xcodebuild -version
|
||||||
|
- swift -version
|
||||||
release_script: goreleaser
|
release_script: goreleaser
|
||||||
|
|||||||
+3
-3
@@ -7,13 +7,13 @@ builds:
|
|||||||
goarch:
|
goarch:
|
||||||
- arm64
|
- arm64
|
||||||
prebuilt:
|
prebuilt:
|
||||||
path: .build/{{ .Arch }}-apple-macosx/release/tart
|
path: .build/{{ .Arch }}-apple-macosx/debug/tart
|
||||||
|
|
||||||
before:
|
before:
|
||||||
hooks:
|
hooks:
|
||||||
- .ci/set-version.sh
|
- .ci/set-version.sh
|
||||||
- swift build -c release --product tart
|
- swift build -c debug --product tart
|
||||||
- codesign --sign - --entitlements Resources/tart.entitlements --force .build/arm64-apple-macosx/release/tart
|
- codesign --sign - --entitlements Resources/tart.entitlements --force .build/arm64-apple-macosx/debug/tart
|
||||||
|
|
||||||
archives:
|
archives:
|
||||||
- id: binary
|
- id: binary
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ struct Create: AsyncParsableCommand {
|
|||||||
var fromIPSW: String?
|
var fromIPSW: String?
|
||||||
|
|
||||||
@Option(help: ArgumentHelp("Disk size in Gb"))
|
@Option(help: ArgumentHelp("Disk size in Gb"))
|
||||||
var diskSize: UInt8 = 50
|
var diskSize: UInt16 = 50
|
||||||
|
|
||||||
func validate() throws {
|
func validate() throws {
|
||||||
if fromIPSW == nil {
|
if fromIPSW == nil {
|
||||||
|
|||||||
@@ -37,45 +37,40 @@ struct Run: AsyncParsableCommand {
|
|||||||
let vmDir = try VMStorageLocal().open(name)
|
let vmDir = try VMStorageLocal().open(name)
|
||||||
vm = try VM(vmDir: vmDir)
|
vm = try VM(vmDir: vmDir)
|
||||||
|
|
||||||
await withThrowingTaskGroup(of: Void.self) { group in
|
Task {
|
||||||
if vnc {
|
do {
|
||||||
group.addTask(operation: {
|
try await vm!.run(recovery)
|
||||||
do {
|
|
||||||
print("Waiting for the VM to boot...")
|
|
||||||
let resolvedIP = try await IP.resolveIP(vm!.config, secondsToWait: 60)
|
|
||||||
guard let ip = resolvedIP else {
|
|
||||||
throw IPNotFound()
|
|
||||||
}
|
|
||||||
let url = URL(string: "vnc://\(ip)")!
|
|
||||||
print("Opening \(url)")
|
|
||||||
NSWorkspace.shared.open(url)
|
|
||||||
} catch {
|
|
||||||
print("Failed to get an IP for screen sharing: \(error)")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
group.addTask {
|
|
||||||
do {
|
|
||||||
try await vm!.run(recovery)
|
|
||||||
|
|
||||||
Foundation.exit(0)
|
Foundation.exit(0)
|
||||||
} catch {
|
} catch {
|
||||||
if error.localizedDescription.contains("Failed to lock auxiliary storage.") {
|
if error.localizedDescription.contains("Failed to lock auxiliary storage.") {
|
||||||
print("Virtual machine \"\(name)\" is already running!")
|
print("Virtual machine \"\(name)\" is already running!")
|
||||||
} else {
|
} else {
|
||||||
print(error)
|
print(error)
|
||||||
}
|
|
||||||
|
|
||||||
Foundation.exit(1)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if noGraphics || vnc {
|
Foundation.exit(1)
|
||||||
dispatchMain()
|
|
||||||
} else {
|
|
||||||
runUI()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if vnc {
|
||||||
|
do {
|
||||||
|
print("Waiting for the VM to boot...")
|
||||||
|
let resolvedIP = try await IP.resolveIP(vm!.config, secondsToWait: 60)
|
||||||
|
guard let ip = resolvedIP else {
|
||||||
|
throw IPNotFound()
|
||||||
|
}
|
||||||
|
let url = URL(string: "vnc://\(ip)")!
|
||||||
|
print("Opening \(url)")
|
||||||
|
NSWorkspace.shared.open(url)
|
||||||
|
} catch {
|
||||||
|
print("Failed to get an IP for screen sharing: \(error)")
|
||||||
|
}
|
||||||
|
while !(vm?.inFinalState ?? false) {
|
||||||
|
try await Task.sleep(nanoseconds: 1_000_000)
|
||||||
|
}
|
||||||
|
} else if !noGraphics {
|
||||||
|
runUI()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func runUI() {
|
private func runUI() {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ struct Set: AsyncParsableCommand {
|
|||||||
var display: VMDisplayConfig?
|
var display: VMDisplayConfig?
|
||||||
|
|
||||||
@Option(help: .hidden)
|
@Option(help: .hidden)
|
||||||
var diskSize: UInt8?
|
var diskSize: UInt16?
|
||||||
|
|
||||||
func run() async throws {
|
func run() async throws {
|
||||||
do {
|
do {
|
||||||
|
|||||||
@@ -3,15 +3,38 @@ import Foundation
|
|||||||
let ociManifestMediaType = "application/vnd.oci.image.manifest.v1+json"
|
let ociManifestMediaType = "application/vnd.oci.image.manifest.v1+json"
|
||||||
let ociConfigMediaType = "application/vnd.oci.image.config.v1+json"
|
let ociConfigMediaType = "application/vnd.oci.image.config.v1+json"
|
||||||
|
|
||||||
|
// Annotations
|
||||||
|
let uncompressedDiskSizeAnnotation = "org.cirruslabs.tart.uncompressed-disk-size"
|
||||||
|
|
||||||
struct OCIManifest: Codable, Equatable {
|
struct OCIManifest: Codable, Equatable {
|
||||||
var schemaVersion: Int = 2
|
var schemaVersion: Int = 2
|
||||||
var mediaType: String = ociManifestMediaType
|
var mediaType: String = ociManifestMediaType
|
||||||
var config: OCIManifestConfig
|
var config: OCIManifestConfig
|
||||||
var layers: [OCIManifestLayer] = Array()
|
var layers: [OCIManifestLayer] = Array()
|
||||||
|
var annotations: Dictionary<String, String> = Dictionary()
|
||||||
|
|
||||||
|
init(config: OCIManifestConfig, layers: [OCIManifestLayer], uncompressedDiskSize: UInt64? = nil) {
|
||||||
|
self.config = config
|
||||||
|
self.layers = layers
|
||||||
|
|
||||||
|
if let uncompressedDiskSize = uncompressedDiskSize {
|
||||||
|
annotations = [
|
||||||
|
uncompressedDiskSizeAnnotation: String(uncompressedDiskSize)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func digest() throws -> String {
|
func digest() throws -> String {
|
||||||
try Digest.hash(JSONEncoder().encode(self))
|
try Digest.hash(JSONEncoder().encode(self))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func uncompressedDiskSize() -> UInt64? {
|
||||||
|
guard let value = annotations[uncompressedDiskSizeAnnotation] else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return UInt64(value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct OCIManifestConfig: Codable, Equatable {
|
struct OCIManifestConfig: Codable, Equatable {
|
||||||
|
|||||||
@@ -184,8 +184,8 @@ class Registry {
|
|||||||
body: fromData)
|
body: fromData)
|
||||||
if putResponse.status != .created {
|
if putResponse.status != .created {
|
||||||
let body = try await postResponse.body.readTextResponse()
|
let body = try await postResponse.body.readTextResponse()
|
||||||
throw RegistryError.UnexpectedHTTPStatusCode(when: "pushing blob (PUT)", code: putResponse.status.code,
|
throw RegistryError.UnexpectedHTTPStatusCode(when: "pushing blob (PUT) to \(uploadLocation)",
|
||||||
details: body ?? "")
|
code: putResponse.status.code, details: body ?? "")
|
||||||
}
|
}
|
||||||
|
|
||||||
return digest
|
return digest
|
||||||
|
|||||||
+10
-1
@@ -78,7 +78,16 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
|||||||
return expectedIPSWLocation
|
return expectedIPSWLocation
|
||||||
}
|
}
|
||||||
|
|
||||||
init(vmDir: VMDirectory, ipswURL: URL?, diskSizeGB: UInt8) async throws {
|
var inFinalState: Bool {
|
||||||
|
get {
|
||||||
|
virtualMachine.state == VZVirtualMachine.State.stopped ||
|
||||||
|
virtualMachine.state == VZVirtualMachine.State.paused ||
|
||||||
|
virtualMachine.state == VZVirtualMachine.State.error
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init(vmDir: VMDirectory, ipswURL: URL?, diskSizeGB: UInt16) async throws {
|
||||||
let ipswURL = ipswURL != nil ? ipswURL! : try await VM.retrieveLatestIPSW();
|
let ipswURL = ipswURL != nil ? ipswURL! : try await VM.retrieveLatestIPSW();
|
||||||
|
|
||||||
// Load the restore image and try to get the requirements
|
// Load the restore image and try to get the requirements
|
||||||
|
|||||||
@@ -117,8 +117,10 @@ extension VMDirectory {
|
|||||||
// Read VM's compressed disk as chunks
|
// Read VM's compressed disk as chunks
|
||||||
// and sequentially upload them as blobs
|
// and sequentially upload them as blobs
|
||||||
let disk = try FileHandle(forReadingFrom: diskURL)
|
let disk = try FileHandle(forReadingFrom: diskURL)
|
||||||
|
var diskReadBytes: UInt64 = 0
|
||||||
let compressingFilter = try InputFilter<Data>(.compress, using: .lz4, bufferCapacity: Self.bufferSizeBytes) { _ in
|
let compressingFilter = try InputFilter<Data>(.compress, using: .lz4, bufferCapacity: Self.bufferSizeBytes) { _ in
|
||||||
let data = try disk.read(upToCount: Self.bufferSizeBytes)
|
let data = try disk.read(upToCount: Self.bufferSizeBytes)
|
||||||
|
diskReadBytes += UInt64(data?.count ?? 0)
|
||||||
|
|
||||||
progress.completedUnitCount += Int64(data?.count ?? 0)
|
progress.completedUnitCount += Int64(data?.count ?? 0)
|
||||||
|
|
||||||
@@ -146,7 +148,8 @@ extension VMDirectory {
|
|||||||
let ociConfigDigest = try await registry.pushBlob(fromData: ociConfigJSON)
|
let ociConfigDigest = try await registry.pushBlob(fromData: ociConfigJSON)
|
||||||
let manifest = OCIManifest(
|
let manifest = OCIManifest(
|
||||||
config: OCIManifestConfig(size: ociConfigJSON.count, digest: ociConfigDigest),
|
config: OCIManifestConfig(size: ociConfigJSON.count, digest: ociConfigDigest),
|
||||||
layers: layers
|
layers: layers,
|
||||||
|
uncompressedDiskSize: diskReadBytes
|
||||||
)
|
)
|
||||||
|
|
||||||
// Manifest
|
// Manifest
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ struct VMDirectory {
|
|||||||
try newVMConfig.save(toURL: to.configURL)
|
try newVMConfig.save(toURL: to.configURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
func resizeDisk(_ sizeGB: UInt8) throws {
|
func resizeDisk(_ sizeGB: UInt16) throws {
|
||||||
if !FileManager.default.fileExists(atPath: diskURL.path) {
|
if !FileManager.default.fileExists(atPath: diskURL.path) {
|
||||||
FileManager.default.createFile(atPath: diskURL.path, contents: nil, attributes: nil)
|
FileManager.default.createFile(atPath: diskURL.path, contents: nil, attributes: nil)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user