mirror of
https://github.com/cirruslabs/tart.git
synced 2026-10-18 03:53:56 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b03408f856 | ||
|
|
c749bdeaf1 | ||
|
|
8e75a59d54 | ||
|
|
1d3aa5ac81 | ||
|
|
261c1806df | ||
|
|
92a4b3164d | ||
|
|
3fdf82079a | ||
|
|
a05684157e | ||
|
|
e62e921eec | ||
|
|
e1bb565c3b | ||
|
|
9b26d30c42 | ||
|
|
ab32cb7e60 | ||
|
|
ea6fd814f5 | ||
|
|
e72fcd9b19 | ||
|
|
33a51b7344 | ||
|
|
5da1a085d3 | ||
|
|
f62949b6f4 | ||
|
|
d7561cab0b | ||
|
|
066f585c53 | ||
|
|
d8ac36b3bd | ||
|
|
4a454a3115 | ||
|
|
94e9425db7 |
+1
-1
@@ -5,7 +5,7 @@ set -e
|
|||||||
export VERSION="${CIRRUS_TAG:-0}"
|
export VERSION="${CIRRUS_TAG:-0}"
|
||||||
|
|
||||||
mkdir -p .ci/pkg/
|
mkdir -p .ci/pkg/
|
||||||
cp .build/arm64-apple-macosx/debug/tart .ci/pkg/tart
|
cp .build/arm64-apple-macosx/release/tart .ci/pkg/tart
|
||||||
cp Resources/embedded.provisionprofile .ci/pkg/embedded.provisionprofile
|
cp Resources/embedded.provisionprofile .ci/pkg/embedded.provisionprofile
|
||||||
pkgbuild --root .ci/pkg/ --identifier com.github.cirruslabs.tart --version $VERSION \
|
pkgbuild --root .ci/pkg/ --identifier com.github.cirruslabs.tart --version $VERSION \
|
||||||
--scripts .ci/pkg/scripts \
|
--scripts .ci/pkg/scripts \
|
||||||
|
|||||||
+1
-3
@@ -80,8 +80,6 @@ task:
|
|||||||
always:
|
always:
|
||||||
dist_artifacts:
|
dist_artifacts:
|
||||||
path: "dist/*"
|
path: "dist/*"
|
||||||
pkg_artifacts:
|
|
||||||
path: ".ci/*.pkg"
|
|
||||||
|
|
||||||
task:
|
task:
|
||||||
name: Release
|
name: Release
|
||||||
@@ -118,7 +116,7 @@ task:
|
|||||||
- swift -version
|
- swift -version
|
||||||
release_script: goreleaser
|
release_script: goreleaser
|
||||||
upload_sentry_debug_files_script:
|
upload_sentry_debug_files_script:
|
||||||
- cd .build/arm64-apple-macosx/debug/
|
- cd .build/arm64-apple-macosx/release/
|
||||||
# Generate and upload symbols
|
# Generate and upload symbols
|
||||||
- dsymutil tart
|
- dsymutil tart
|
||||||
- sentry-cli debug-files upload tart.dSYM/
|
- sentry-cli debug-files upload tart.dSYM/
|
||||||
|
|||||||
+3
-8
@@ -3,7 +3,7 @@ project_name: tart
|
|||||||
before:
|
before:
|
||||||
hooks:
|
hooks:
|
||||||
- .ci/set-version.sh
|
- .ci/set-version.sh
|
||||||
- swift build -c debug --product tart
|
- swift build -c release --product tart
|
||||||
|
|
||||||
builds:
|
builds:
|
||||||
- builder: prebuilt
|
- builder: prebuilt
|
||||||
@@ -12,7 +12,7 @@ builds:
|
|||||||
goarch:
|
goarch:
|
||||||
- arm64
|
- arm64
|
||||||
prebuilt:
|
prebuilt:
|
||||||
path: .build/arm64-apple-macosx/debug/tart
|
path: .build/arm64-apple-macosx/release/tart
|
||||||
hooks:
|
hooks:
|
||||||
post:
|
post:
|
||||||
- gon gon.hcl
|
- gon gon.hcl
|
||||||
@@ -23,17 +23,12 @@ archives:
|
|||||||
- src: Resources/embedded.provisionprofile
|
- src: Resources/embedded.provisionprofile
|
||||||
dst: tart.app/Contents
|
dst: tart.app/Contents
|
||||||
strip_parent: true
|
strip_parent: true
|
||||||
- src: ".build/arm64-apple-macosx/debug/tart"
|
- src: ".build/arm64-apple-macosx/release/tart"
|
||||||
dst: tart.app/Contents/MacOS
|
dst: tart.app/Contents/MacOS
|
||||||
strip_parent: true
|
strip_parent: true
|
||||||
hooks:
|
|
||||||
after:
|
|
||||||
- ./.ci/create-pkg.sh
|
|
||||||
|
|
||||||
release:
|
release:
|
||||||
prerelease: auto
|
prerelease: auto
|
||||||
extra_files:
|
|
||||||
- glob: ./.ci/Tart-{{ .Tag }}.pkg
|
|
||||||
|
|
||||||
brews:
|
brews:
|
||||||
- name: tart
|
- name: tart
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
<component name="ProjectRunConfigurationManager">
|
|
||||||
<configuration default="false" name="sign debug" type="ShConfigurationType">
|
|
||||||
<option name="SCRIPT_TEXT" value="codesign --sign - --entitlements Resources/tart-dev.entitlements --force .build/debug/tart" />
|
|
||||||
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
|
|
||||||
<option name="SCRIPT_PATH" value="$PROJECT_DIR$/scripts/sign.sh" />
|
|
||||||
<option name="SCRIPT_OPTIONS" value="" />
|
|
||||||
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
|
|
||||||
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
|
||||||
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
|
|
||||||
<option name="INTERPRETER_PATH" value="/bin/zsh" />
|
|
||||||
<option name="INTERPRETER_OPTIONS" value="" />
|
|
||||||
<option name="EXECUTE_IN_TERMINAL" value="true" />
|
|
||||||
<option name="EXECUTE_SCRIPT_FILE" value="false" />
|
|
||||||
<envs />
|
|
||||||
<method v="2" />
|
|
||||||
</configuration>
|
|
||||||
</component>
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
<component name="ProjectRunConfigurationManager">
|
|
||||||
<configuration default="false" name="tart create" type="SwiftPackageManagerRunConfiguration" factoryName="Swift Package Run" PROGRAM_PARAMS="create latest --from-ipsw=latest" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" PASS_PARENT_ENVS_2="true" PROJECT_NAME="tart" TARGET_NAME="tart" CONFIG_NAME="tart" RUN_TARGET_PROJECT_NAME="tart" RUN_TARGET_NAME="tart" WAS_MODIFIED="">
|
|
||||||
<method v="2">
|
|
||||||
<option name="SPM.BUILD_TASK_PROVIDER" enabled="true" />
|
|
||||||
<option name="RunConfigurationTask" enabled="true" run_configuration_name="sign debug" run_configuration_type="ShConfigurationType" />
|
|
||||||
</method>
|
|
||||||
</configuration>
|
|
||||||
</component>
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
<component name="ProjectRunConfigurationManager">
|
|
||||||
<configuration default="false" name="tart run" type="SwiftPackageManagerRunConfiguration" factoryName="Swift Package Run" PROGRAM_PARAMS="run latest" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" PASS_PARENT_ENVS_2="true" PROJECT_NAME="tart" TARGET_NAME="tart" CONFIG_NAME="tart" RUN_TARGET_PROJECT_NAME="tart" RUN_TARGET_NAME="tart" WAS_MODIFIED="">
|
|
||||||
<method v="2">
|
|
||||||
<option name="SPM.BUILD_TASK_PROVIDER" enabled="true" />
|
|
||||||
<option name="RunConfigurationTask" enabled="true" run_configuration_name="sign debug" run_configuration_type="ShConfigurationType" />
|
|
||||||
</method>
|
|
||||||
</configuration>
|
|
||||||
</component>
|
|
||||||
+2
-2
@@ -23,8 +23,8 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/getsentry/sentry-cocoa",
|
"location" : "https://github.com/getsentry/sentry-cocoa",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "c3c19e29f775ee95b77aa3168f3e2fd6c20deba6",
|
"revision" : "ac224c437a3070ffe34460137ac8761eddaf2852",
|
||||||
"version" : "7.31.3"
|
"version" : "8.3.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
+1
-1
@@ -18,7 +18,7 @@ let package = Package(
|
|||||||
.package(url: "https://github.com/antlr/antlr4", branch: "dev"),
|
.package(url: "https://github.com/antlr/antlr4", branch: "dev"),
|
||||||
.package(url: "https://github.com/apple/swift-atomics.git", .upToNextMajor(from: "1.0.0")),
|
.package(url: "https://github.com/apple/swift-atomics.git", .upToNextMajor(from: "1.0.0")),
|
||||||
.package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.50.6"),
|
.package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.50.6"),
|
||||||
.package(url: "https://github.com/getsentry/sentry-cocoa", from: "7.31.3"),
|
.package(url: "https://github.com/getsentry/sentry-cocoa", from: "8.3.3"),
|
||||||
.package(url: "https://github.com/cfilipov/TextTable", branch: "master"),
|
.package(url: "https://github.com/cfilipov/TextTable", branch: "master"),
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
|
|||||||
@@ -1,16 +1,44 @@
|
|||||||
import ArgumentParser
|
import ArgumentParser
|
||||||
|
import Foundation
|
||||||
|
|
||||||
struct Export: AsyncParsableCommand {
|
struct Export: AsyncParsableCommand {
|
||||||
static var configuration = CommandConfiguration(abstract: "Export VM to a file")
|
static var configuration = CommandConfiguration(abstract: "Export VM to a compressed .tvm file")
|
||||||
|
|
||||||
@Argument(help: "Source VM name.")
|
@Argument(help: "Source VM name.")
|
||||||
var name: String
|
var name: String
|
||||||
|
|
||||||
@Argument(help: "Path to the destination file.")
|
@Argument(help: "Path to the destination file.")
|
||||||
var path: String
|
var path: String?
|
||||||
|
|
||||||
func run() async throws {
|
func run() async throws {
|
||||||
|
let correctedPath: String
|
||||||
|
|
||||||
|
if let path = path {
|
||||||
|
correctedPath = path
|
||||||
|
} else {
|
||||||
|
correctedPath = "\(name).tvm"
|
||||||
|
|
||||||
|
if FileManager.default.fileExists(atPath: correctedPath) {
|
||||||
|
while true {
|
||||||
|
if userWantsOverwrite(correctedPath) {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
print("exporting...")
|
print("exporting...")
|
||||||
try VMStorageHelper.open(name).exportToArchive(path: path)
|
|
||||||
|
try VMStorageHelper.open(name).exportToArchive(path: correctedPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func userWantsOverwrite(_ filename: String) -> Bool {
|
||||||
|
print("file \(filename) already exists, are you sure you want to overwrite it? (yes, [no])? ", terminator: "")
|
||||||
|
|
||||||
|
let answer = readLine()!
|
||||||
|
|
||||||
|
return answer == "yes"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ struct Get: AsyncParsableCommand {
|
|||||||
let vmConfig = try VMConfig(fromURL: vmDir.configURL)
|
let vmConfig = try VMConfig(fromURL: vmDir.configURL)
|
||||||
let diskSizeInGb = try vmDir.sizeGB()
|
let diskSizeInGb = try vmDir.sizeGB()
|
||||||
let memorySizeInMb = vmConfig.memorySize / 1024 / 1024
|
let memorySizeInMb = vmConfig.memorySize / 1024 / 1024
|
||||||
let running = try PIDLock(lockURL: vmDir.configURL).pid() > 0
|
|
||||||
|
|
||||||
let info = VMInfo(CPU: vmConfig.cpuCount, Memory: memorySizeInMb, Disk: diskSizeInGb, Display: vmConfig.display.description, Running: running)
|
let info = VMInfo(CPU: vmConfig.cpuCount, Memory: memorySizeInMb, Disk: diskSizeInGb,
|
||||||
|
Display: vmConfig.display.description, Running: try vmDir.running())
|
||||||
print(format.renderSingle(info))
|
print(format.renderSingle(info))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,12 @@ import Network
|
|||||||
import SystemConfiguration
|
import SystemConfiguration
|
||||||
import Sentry
|
import Sentry
|
||||||
|
|
||||||
|
enum IPResolutionStrategy: String, ExpressibleByArgument, CaseIterable {
|
||||||
|
case dhcp, arp
|
||||||
|
|
||||||
|
private(set) static var allValueStrings: [String] = Format.allCases.map { "\($0)"}
|
||||||
|
}
|
||||||
|
|
||||||
struct IP: AsyncParsableCommand {
|
struct IP: AsyncParsableCommand {
|
||||||
static var configuration = CommandConfiguration(abstract: "Get VM's IP address")
|
static var configuration = CommandConfiguration(abstract: "Get VM's IP address")
|
||||||
|
|
||||||
@@ -13,46 +19,51 @@ struct IP: AsyncParsableCommand {
|
|||||||
@Option(help: "Number of seconds to wait for a potential VM booting")
|
@Option(help: "Number of seconds to wait for a potential VM booting")
|
||||||
var wait: UInt16 = 0
|
var wait: UInt16 = 0
|
||||||
|
|
||||||
|
@Option(help: ArgumentHelp("Strategy for resolving IP address: dhcp or arp",
|
||||||
|
discussion: """
|
||||||
|
By default, Tart is looking up and parsing DHCP lease file to determine the IP of the VM.\n
|
||||||
|
This method is fast and the most reliable but only returns local IP adresses.\n
|
||||||
|
Alternatively, Tart can call external `arp` executable and parse it's output.\n
|
||||||
|
In case of enabled Bridged Networking this method will return VM's IP address on the network interface used for Bridged Networking.\n
|
||||||
|
Note that `arp` strategy won't work for VMs using `--net-softnet`.
|
||||||
|
"""))
|
||||||
|
var resolver: IPResolutionStrategy = .dhcp
|
||||||
|
|
||||||
func run() async throws {
|
func run() async throws {
|
||||||
let vmDir = try VMStorageLocal().open(name)
|
let vmDir = try VMStorageLocal().open(name)
|
||||||
let vmConfig = try VMConfig.init(fromURL: vmDir.configURL)
|
let vmConfig = try VMConfig.init(fromURL: vmDir.configURL)
|
||||||
let vmMACAddress = MACAddress(fromString: vmConfig.macAddress.string)!
|
let vmMACAddress = MACAddress(fromString: vmConfig.macAddress.string)!
|
||||||
|
|
||||||
guard let ipViaDHCP = try await IP.resolveIP(vmMACAddress, secondsToWait: wait) else {
|
guard let ip = try await IP.resolveIP(vmMACAddress, resolutionStrategy: resolver, secondsToWait: wait) else {
|
||||||
throw RuntimeError.NoIPAddressFound("no IP address found, is your VM running?")
|
var message = "no IP address found"
|
||||||
}
|
|
||||||
|
|
||||||
let arpCache = try ARPCache()
|
if try !vmDir.running() {
|
||||||
|
message += ", is your VM running?"
|
||||||
if let ipViaARP = try arpCache.ResolveMACAddress(macAddress: vmMACAddress), ipViaARP != ipViaDHCP {
|
|
||||||
// Capture the warning into Sentry
|
|
||||||
SentrySDK.capture(message: "DHCP lease and ARP cache entries for a single MAC address differ") { scope in
|
|
||||||
scope.setLevel(.warning)
|
|
||||||
|
|
||||||
scope.setContext(value: [
|
|
||||||
"MAC address": vmMACAddress,
|
|
||||||
"IP via ARP": ipViaARP,
|
|
||||||
"IP via DHCP": ipViaDHCP,
|
|
||||||
], key: "Address conflict details")
|
|
||||||
|
|
||||||
scope.add(Attachment(path: "/var/db/dhcpd_leases", filename: "dhcpd_leases.txt", contentType: "text/plain"))
|
|
||||||
scope.add(Attachment(data: arpCache.arpCommandOutput, filename: "arp-an-output.txt", contentType: "text/plain"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fputs("WARNING: DHCP lease and ARP cache entries for MAC address \(vmMACAddress) differ: "
|
if (vmConfig.os == .linux && resolver == .arp) {
|
||||||
+ "got \(ipViaDHCP) and \(ipViaARP) respectively, consider reporting this case to"
|
message += " (not all Linux distributions are compatible with the ARP resolver)"
|
||||||
+ " https://github.com/cirruslabs/tart/issues/172\n", stderr)
|
}
|
||||||
|
|
||||||
|
throw RuntimeError.NoIPAddressFound(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
print(ipViaDHCP)
|
print(ip)
|
||||||
}
|
}
|
||||||
|
|
||||||
static public func resolveIP(_ vmMACAddress: MACAddress, secondsToWait: UInt16) async throws -> IPv4Address? {
|
static public func resolveIP(_ vmMACAddress: MACAddress, resolutionStrategy: IPResolutionStrategy = .dhcp, secondsToWait: UInt16 = 0) async throws -> IPv4Address? {
|
||||||
let waitUntil = Calendar.current.date(byAdding: .second, value: Int(secondsToWait), to: Date.now)!
|
let waitUntil = Calendar.current.date(byAdding: .second, value: Int(secondsToWait), to: Date.now)!
|
||||||
|
|
||||||
repeat {
|
repeat {
|
||||||
if let leases = try Leases(), let ip = try leases.resolveMACAddress(macAddress: vmMACAddress) {
|
switch resolutionStrategy {
|
||||||
return ip
|
case .arp:
|
||||||
|
if let ip = try ARPCache().ResolveMACAddress(macAddress: vmMACAddress) {
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
case .dhcp:
|
||||||
|
if let leases = try Leases(), let ip = try leases.ResolveMACAddress(macAddress: vmMACAddress) {
|
||||||
|
return ip
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// wait a second
|
// wait a second
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import ArgumentParser
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct Import: AsyncParsableCommand {
|
struct Import: AsyncParsableCommand {
|
||||||
static var configuration = CommandConfiguration(abstract: "Import VM from a file")
|
static var configuration = CommandConfiguration(abstract: "Import VM from a compressed .tvm file")
|
||||||
|
|
||||||
@Argument(help: "Path to a file created with \"tart export\".")
|
@Argument(help: "Path to a file created with \"tart export\".")
|
||||||
var path: String
|
var path: String
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ fileprivate struct VMInfo: Encodable {
|
|||||||
let Source: String
|
let Source: String
|
||||||
let Name: String
|
let Name: String
|
||||||
let Size: Int
|
let Size: Int
|
||||||
|
let Running: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
struct List: AsyncParsableCommand {
|
struct List: AsyncParsableCommand {
|
||||||
@@ -32,17 +33,19 @@ struct List: AsyncParsableCommand {
|
|||||||
|
|
||||||
func run() async throws {
|
func run() async throws {
|
||||||
var infos: [VMInfo] = []
|
var infos: [VMInfo] = []
|
||||||
|
|
||||||
if source == nil || source == "local" {
|
if source == nil || source == "local" {
|
||||||
infos += sortedInfos(try VMStorageLocal().list().map { (name, vmDir) in
|
infos += sortedInfos(try VMStorageLocal().list().map { (name, vmDir) in
|
||||||
try VMInfo(Source: "local", Name: name, Size: vmDir.sizeGB())
|
try VMInfo(Source: "local", Name: name, Size: vmDir.sizeGB(), Running: vmDir.running())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if source == nil || source == "oci" {
|
if source == nil || source == "oci" {
|
||||||
infos += sortedInfos(try VMStorageOCI().list().map { (name, vmDir, _) in
|
infos += sortedInfos(try VMStorageOCI().list().map { (name, vmDir, _) in
|
||||||
try VMInfo(Source: "oci", Name: name, Size: vmDir.sizeGB())
|
try VMInfo(Source: "oci", Name: name, Size: vmDir.sizeGB(), Running: vmDir.running())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (quiet) {
|
if (quiet) {
|
||||||
for info in infos {
|
for info in infos {
|
||||||
print(info.Name)
|
print(info.Name)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import Compression
|
|||||||
struct Push: AsyncParsableCommand {
|
struct Push: AsyncParsableCommand {
|
||||||
static var configuration = CommandConfiguration(abstract: "Push a VM to a registry")
|
static var configuration = CommandConfiguration(abstract: "Push a VM to a registry")
|
||||||
|
|
||||||
@Argument(help: "local VM name")
|
@Argument(help: "local or remote VM name")
|
||||||
var localName: String
|
var localName: String
|
||||||
|
|
||||||
@Argument(help: "remote VM name(s)")
|
@Argument(help: "remote VM name(s)")
|
||||||
@@ -28,7 +28,8 @@ struct Push: AsyncParsableCommand {
|
|||||||
var populateCache: Bool = false
|
var populateCache: Bool = false
|
||||||
|
|
||||||
func run() async throws {
|
func run() async throws {
|
||||||
let localVMDir = try VMStorageLocal().open(localName)
|
let ociStorage = VMStorageOCI()
|
||||||
|
let localVMDir = try VMStorageHelper.open(localName)
|
||||||
|
|
||||||
// Parse remote names supplied by the user
|
// Parse remote names supplied by the user
|
||||||
let remoteNames = try remoteNames.map{
|
let remoteNames = try remoteNames.map{
|
||||||
@@ -53,23 +54,55 @@ struct Push: AsyncParsableCommand {
|
|||||||
defaultLogger.appendNewLine("pushing \(localName) to "
|
defaultLogger.appendNewLine("pushing \(localName) to "
|
||||||
+ "\(registryIdentifier.host)/\(registryIdentifier.namespace)\(remoteNamesForRegistry.referenceNames())...")
|
+ "\(registryIdentifier.host)/\(registryIdentifier.namespace)\(remoteNamesForRegistry.referenceNames())...")
|
||||||
|
|
||||||
let pushedRemoteName = try await localVMDir.pushToRegistry(
|
let references = remoteNamesForRegistry.map{ $0.reference.value }
|
||||||
registry: registry,
|
|
||||||
references: remoteNamesForRegistry.map{ $0.reference.value },
|
|
||||||
chunkSizeMb: chunkSize
|
|
||||||
)
|
|
||||||
|
|
||||||
// Populate the local cache (if requested)
|
let pushedRemoteName: RemoteName
|
||||||
|
// If we're pushing a local OCI VM, check if points to an already existing registry manifest
|
||||||
|
// and if so, only upload manifests (without config, disk and NVRAM) to the user-specified references
|
||||||
|
if let remoteName = try? RemoteName(localName) {
|
||||||
|
pushedRemoteName = try await lightweightPushToRegistry(
|
||||||
|
registry: registry,
|
||||||
|
remoteName: remoteName,
|
||||||
|
references: references
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
pushedRemoteName = try await localVMDir.pushToRegistry(
|
||||||
|
registry: registry,
|
||||||
|
references: references,
|
||||||
|
chunkSizeMb: chunkSize
|
||||||
|
)
|
||||||
|
// Populate the local cache (if requested)
|
||||||
|
if populateCache {
|
||||||
|
let expectedPushedVMDir = try ociStorage.create(pushedRemoteName)
|
||||||
|
try localVMDir.clone(to: expectedPushedVMDir, generateMAC: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// link the rest remote names
|
||||||
if populateCache {
|
if populateCache {
|
||||||
let ociStorage = VMStorageOCI()
|
|
||||||
let expectedPushedVMDir = try ociStorage.create(pushedRemoteName)
|
|
||||||
try localVMDir.clone(to: expectedPushedVMDir, generateMAC: false)
|
|
||||||
for remoteName in remoteNamesForRegistry {
|
for remoteName in remoteNamesForRegistry {
|
||||||
try ociStorage.link(from: remoteName, to: pushedRemoteName)
|
try ociStorage.link(from: remoteName, to: pushedRemoteName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func lightweightPushToRegistry(registry: Registry, remoteName: RemoteName, references: [String]) async throws -> RemoteName {
|
||||||
|
// Is the local OCI VM already present in the registry?
|
||||||
|
let digest = try VMStorageOCI().digest(remoteName)
|
||||||
|
|
||||||
|
let (remoteManifest, _) = try await registry.pullManifest(reference: digest)
|
||||||
|
|
||||||
|
// Overwrite registry's references with the retrieved manifest
|
||||||
|
for reference in references {
|
||||||
|
defaultLogger.appendNewLine("pushing manifest for \(reference)...")
|
||||||
|
|
||||||
|
_ = try await registry.pushManifest(reference: reference, manifest: remoteManifest)
|
||||||
|
}
|
||||||
|
|
||||||
|
return RemoteName(host: registry.baseURL.host!, namespace: registry.namespace,
|
||||||
|
reference: Reference(digest: digest))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Collection where Element == RemoteName {
|
extension Collection where Element == RemoteName {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import ArgumentParser
|
import ArgumentParser
|
||||||
|
import Cocoa
|
||||||
import Dispatch
|
import Dispatch
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Virtualization
|
import Virtualization
|
||||||
@@ -17,13 +18,24 @@ struct Run: AsyncParsableCommand {
|
|||||||
|
|
||||||
@Flag(help: ArgumentHelp(
|
@Flag(help: ArgumentHelp(
|
||||||
"Don't open a UI window.",
|
"Don't open a UI window.",
|
||||||
discussion: "Useful for integrating Tart VMs into other tools.\nUse `tart ip` in order to get an IP for SSHing or VNCing into the VM."))
|
discussion: "Useful for integrating Tart VMs into other tools.\nUse `tart ip` in order to get an IP for SSHing or VNCing into the VM."))
|
||||||
var noGraphics: Bool = false
|
var noGraphics: Bool = false
|
||||||
|
|
||||||
|
@Flag(help: ArgumentHelp(
|
||||||
|
"Open serial console in /dev/ttySXX",
|
||||||
|
discussion: "Useful for debugging Linux Kernel."))
|
||||||
|
var serial: Bool = false
|
||||||
|
|
||||||
|
@Option(help: ArgumentHelp(
|
||||||
|
"Attach an externally created serial console",
|
||||||
|
discussion: "Alternative to `--serial` flag for programmatic integrations."
|
||||||
|
))
|
||||||
|
var serialPath: String?
|
||||||
|
|
||||||
@Flag(help: "Force open a UI window, even when VNC is enabled.")
|
@Flag(help: "Force open a UI window, even when VNC is enabled.")
|
||||||
var graphics: Bool = false
|
var graphics: Bool = false
|
||||||
|
|
||||||
@Flag(help: "Boot into recovery mode")
|
@Flag(help: "Boot into recovery mode")
|
||||||
var recovery: Bool = false
|
var recovery: Bool = false
|
||||||
|
|
||||||
@Flag(help: ArgumentHelp(
|
@Flag(help: ArgumentHelp(
|
||||||
@@ -118,11 +130,30 @@ struct Run: AsyncParsableCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var serialPorts: [VZSerialPortConfiguration] = []
|
||||||
|
if serial {
|
||||||
|
let tty_fd = createPTY()
|
||||||
|
if (tty_fd < 0) {
|
||||||
|
throw RuntimeError.VMConfigurationError("Failed to create PTY")
|
||||||
|
}
|
||||||
|
let tty_read = FileHandle.init(fileDescriptor: tty_fd)
|
||||||
|
let tty_write = FileHandle.init(fileDescriptor: tty_fd)
|
||||||
|
serialPorts.append(createSerialPortConfiguration(tty_read, tty_write))
|
||||||
|
} else if serialPath != nil {
|
||||||
|
let tty_read = FileHandle.init(forReadingAtPath: serialPath!)
|
||||||
|
let tty_write = FileHandle.init(forWritingAtPath: serialPath!)
|
||||||
|
if (tty_read == nil || tty_write == nil) {
|
||||||
|
throw RuntimeError.VMConfigurationError("Failed to open PTY")
|
||||||
|
}
|
||||||
|
serialPorts.append(createSerialPortConfiguration(tty_read!, tty_write!))
|
||||||
|
}
|
||||||
|
|
||||||
vm = try VM(
|
vm = try VM(
|
||||||
vmDir: vmDir,
|
vmDir: vmDir,
|
||||||
network: userSpecifiedNetwork(vmDir: vmDir) ?? NetworkShared(),
|
network: userSpecifiedNetwork(vmDir: vmDir) ?? NetworkShared(),
|
||||||
additionalDiskAttachments: additionalDiskAttachments,
|
additionalDiskAttachments: additionalDiskAttachments,
|
||||||
directorySharingDevices: directoryShares() + rosettaDirectoryShare()
|
directorySharingDevices: directoryShares() + rosettaDirectoryShare(),
|
||||||
|
serialPorts: serialPorts
|
||||||
)
|
)
|
||||||
|
|
||||||
let vncImpl: VNC? = try {
|
let vncImpl: VNC? = try {
|
||||||
@@ -198,6 +229,16 @@ struct Run: AsyncParsableCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func createSerialPortConfiguration(_ tty_read: FileHandle, _ tty_write: FileHandle) -> VZVirtioConsoleDeviceSerialPortConfiguration {
|
||||||
|
let serialPortConfiguration = VZVirtioConsoleDeviceSerialPortConfiguration()
|
||||||
|
let serialPortAttachment = VZFileHandleSerialPortAttachment(
|
||||||
|
fileHandleForReading: tty_read,
|
||||||
|
fileHandleForWriting: tty_write)
|
||||||
|
|
||||||
|
serialPortConfiguration.attachment = serialPortAttachment
|
||||||
|
return serialPortConfiguration
|
||||||
|
}
|
||||||
|
|
||||||
func isInteractiveSession() -> Bool {
|
func isInteractiveSession() -> Bool {
|
||||||
isatty(STDOUT_FILENO) == 1
|
isatty(STDOUT_FILENO) == 1
|
||||||
}
|
}
|
||||||
@@ -351,13 +392,21 @@ struct Run: AsyncParsableCommand {
|
|||||||
nsApp.applicationIconImage = NSImage(data: AppIconData)
|
nsApp.applicationIconImage = NSImage(data: AppIconData)
|
||||||
|
|
||||||
struct MainApp: App {
|
struct MainApp: App {
|
||||||
|
@NSApplicationDelegateAdaptor private var appDelegate: MinimalMenuAppDelegate
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup(vm!.name) {
|
WindowGroup(vm!.name) {
|
||||||
Group {
|
Group {
|
||||||
VMView(vm: vm!).onAppear {
|
VMView(vm: vm!).onAppear {
|
||||||
NSWindow.allowsAutomaticWindowTabbing = false
|
NSWindow.allowsAutomaticWindowTabbing = false
|
||||||
}.onDisappear {
|
}.onDisappear {
|
||||||
NSApplication.shared.terminate(self)
|
let ret = kill(getpid(), SIGINT)
|
||||||
|
if ret != 0 {
|
||||||
|
// Fallback to the old termination method that doesn't
|
||||||
|
// propagate the cancellation to Task's in case graceful
|
||||||
|
// termination via kill(2) is not successful
|
||||||
|
NSApplication.shared.terminate(self)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}.frame(width: CGFloat(vm!.config.display.width), height: CGFloat(vm!.config.display.height))
|
}.frame(width: CGFloat(vm!.config.display.width), height: CGFloat(vm!.config.display.height))
|
||||||
}.commands {
|
}.commands {
|
||||||
@@ -369,7 +418,18 @@ struct Run: AsyncParsableCommand {
|
|||||||
CommandGroup(replacing: .undoRedo, addition: {})
|
CommandGroup(replacing: .undoRedo, addition: {})
|
||||||
CommandGroup(replacing: .windowSize, addition: {})
|
CommandGroup(replacing: .windowSize, addition: {})
|
||||||
// Replace some standard menu options
|
// Replace some standard menu options
|
||||||
CommandGroup(replacing: .appInfo) { AboutTart() }
|
CommandGroup(replacing: .appInfo) { AboutTart(config: vm!.config) }
|
||||||
|
CommandMenu("Control") {
|
||||||
|
Button("Start") {
|
||||||
|
Task { try await vm!.virtualMachine.start() }
|
||||||
|
}
|
||||||
|
Button("Stop") {
|
||||||
|
Task { try await vm!.virtualMachine.stop() }
|
||||||
|
}
|
||||||
|
Button("Request Stop") {
|
||||||
|
Task { try await vm!.virtualMachine.requestStop() }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -378,14 +438,42 @@ struct Run: AsyncParsableCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The only way to fully remove Edit menu item.
|
||||||
|
class MinimalMenuAppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
|
||||||
|
let indexOfEditMenu = 2
|
||||||
|
|
||||||
|
func applicationDidFinishLaunching(_ : Notification) {
|
||||||
|
NSApplication.shared.mainMenu?.removeItem(at: indexOfEditMenu)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct AboutTart: View {
|
struct AboutTart: View {
|
||||||
|
var credits: NSAttributedString
|
||||||
|
|
||||||
|
init(config: VMConfig) {
|
||||||
|
let mutableAttrStr = NSMutableAttributedString()
|
||||||
|
let style = NSMutableParagraphStyle()
|
||||||
|
style.alignment = NSTextAlignment.center
|
||||||
|
let attrCenter: [NSAttributedString.Key : Any] = [
|
||||||
|
.paragraphStyle: style,
|
||||||
|
]
|
||||||
|
mutableAttrStr.append(NSAttributedString(string: "CPU: \(config.cpuCount) cores\n", attributes: attrCenter))
|
||||||
|
mutableAttrStr.append(NSAttributedString(string: "Memory: \(config.memorySize / 1024 / 1024) MB\n", attributes: attrCenter))
|
||||||
|
mutableAttrStr.append(NSAttributedString(string: "Display: \(config.display.description)\n", attributes: attrCenter))
|
||||||
|
mutableAttrStr.append(NSAttributedString(string: "https://github.com/cirruslabs/tart", attributes: [
|
||||||
|
.paragraphStyle: style,
|
||||||
|
.link : "https://github.com/cirruslabs/tart"
|
||||||
|
]))
|
||||||
|
credits = mutableAttrStr
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Button("About Tart") {
|
Button("About Tart") {
|
||||||
NSApplication.shared.orderFrontStandardAboutPanel(options: [
|
NSApplication.shared.orderFrontStandardAboutPanel(options: [
|
||||||
NSApplication.AboutPanelOptionKey.applicationIcon: NSApplication.shared.applicationIconImage as Any,
|
NSApplication.AboutPanelOptionKey.applicationIcon: NSApplication.shared.applicationIconImage as Any,
|
||||||
NSApplication.AboutPanelOptionKey.applicationName: "Tart",
|
NSApplication.AboutPanelOptionKey.applicationName: "Tart",
|
||||||
NSApplication.AboutPanelOptionKey.applicationVersion: CI.version,
|
NSApplication.AboutPanelOptionKey.applicationVersion: CI.version,
|
||||||
NSApplication.AboutPanelOptionKey.credits: try! NSAttributedString(markdown: "https://github.com/cirruslabs/tart"),
|
NSApplication.AboutPanelOptionKey.credits: credits,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -398,7 +486,8 @@ struct VMView: NSViewRepresentable {
|
|||||||
|
|
||||||
func makeNSView(context: Context) -> NSViewType {
|
func makeNSView(context: Context) -> NSViewType {
|
||||||
let machineView = VZVirtualMachineView()
|
let machineView = VZVirtualMachineView()
|
||||||
machineView.capturesSystemKeys = true
|
// so keys like take a windows screenshot (cmd+shift+4+space) works on the host and not guest
|
||||||
|
machineView.capturesSystemKeys = false
|
||||||
return machineView
|
return machineView
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ struct ARPCache {
|
|||||||
self.arpCommandOutput = arpCommandOutput
|
self.arpCommandOutput = arpCommandOutput
|
||||||
}
|
}
|
||||||
|
|
||||||
func ResolveMACAddress(macAddress: MACAddress, bridgeOnly: Bool = true) throws -> IPv4Address? {
|
func ResolveMACAddress(macAddress: MACAddress) throws -> IPv4Address? {
|
||||||
let lines = String(decoding: arpCommandOutput, as: UTF8.self)
|
let lines = String(decoding: arpCommandOutput, as: UTF8.self)
|
||||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
.components(separatedBy: "\n")
|
.components(separatedBy: "\n")
|
||||||
@@ -95,11 +95,6 @@ struct ARPCache {
|
|||||||
throw ARPCommandYieldedInvalidOutputError(explanation: "failed to parse MAC address \(rawMAC)")
|
throw ARPCommandYieldedInvalidOutputError(explanation: "failed to parse MAC address \(rawMAC)")
|
||||||
}
|
}
|
||||||
|
|
||||||
let interface = try match.getCaptureGroup(name: "interface", for: line)
|
|
||||||
if bridgeOnly && !interface.starts(with: "bridge") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if macAddress == mac {
|
if macAddress == mac {
|
||||||
return ip
|
return ip
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ class Leases {
|
|||||||
return rawLeases
|
return rawLeases
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveMACAddress(macAddress: MACAddress) throws -> IPv4Address? {
|
func ResolveMACAddress(macAddress: MACAddress) throws -> IPv4Address? {
|
||||||
leases[macAddress]?.ip
|
leases[macAddress]?.ip
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
func createPTY() -> Int32 {
|
||||||
|
var tty_fd: Int32 = -1
|
||||||
|
var sfd: Int32 = -1
|
||||||
|
var termios_ = termios()
|
||||||
|
let tty_path = UnsafeMutablePointer<CChar>.allocate(capacity: 1024)
|
||||||
|
|
||||||
|
var res = openpty(&tty_fd, &sfd, tty_path, nil, nil);
|
||||||
|
if (res < 0) {
|
||||||
|
perror("openpty error")
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// close slave file descriptor
|
||||||
|
close(sfd)
|
||||||
|
|
||||||
|
res = fcntl(tty_fd, F_GETFL)
|
||||||
|
if (res < 0) {
|
||||||
|
perror("fcntl F_GETFL error")
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// set serial nonblocking
|
||||||
|
res = fcntl(tty_fd, F_SETFL, res | O_NONBLOCK)
|
||||||
|
if (res < 0) {
|
||||||
|
perror("fcntl F_SETFL O_NONBLOCK error")
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// set baudrate to 115200
|
||||||
|
tcgetattr(tty_fd, &termios_)
|
||||||
|
cfsetispeed(&termios_, speed_t(B115200))
|
||||||
|
cfsetospeed(&termios_, speed_t(B115200))
|
||||||
|
if (tcsetattr(tty_fd, TCSANOW, &termios_) != 0) {
|
||||||
|
perror("tcsetattr error")
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
print("Successfully open pty \(String(cString: tty_path))")
|
||||||
|
|
||||||
|
tty_path.deallocate()
|
||||||
|
return tty_fd
|
||||||
|
}
|
||||||
+13
-5
@@ -40,7 +40,8 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
|||||||
init(vmDir: VMDirectory,
|
init(vmDir: VMDirectory,
|
||||||
network: Network = NetworkShared(),
|
network: Network = NetworkShared(),
|
||||||
additionalDiskAttachments: [VZDiskImageStorageDeviceAttachment] = [],
|
additionalDiskAttachments: [VZDiskImageStorageDeviceAttachment] = [],
|
||||||
directorySharingDevices: [VZDirectorySharingDeviceConfiguration] = []
|
directorySharingDevices: [VZDirectorySharingDeviceConfiguration] = [],
|
||||||
|
serialPorts: [VZSerialPortConfiguration] = []
|
||||||
) throws {
|
) throws {
|
||||||
name = vmDir.name
|
name = vmDir.name
|
||||||
config = try VMConfig.init(fromURL: vmDir.configURL)
|
config = try VMConfig.init(fromURL: vmDir.configURL)
|
||||||
@@ -54,7 +55,8 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
|||||||
let configuration = try Self.craftConfiguration(diskURL: vmDir.diskURL,
|
let configuration = try Self.craftConfiguration(diskURL: vmDir.diskURL,
|
||||||
nvramURL: vmDir.nvramURL, vmConfig: config,
|
nvramURL: vmDir.nvramURL, vmConfig: config,
|
||||||
network: network, additionalDiskAttachments: additionalDiskAttachments,
|
network: network, additionalDiskAttachments: additionalDiskAttachments,
|
||||||
directorySharingDevices: directorySharingDevices
|
directorySharingDevices: directorySharingDevices,
|
||||||
|
serialPorts: serialPorts
|
||||||
)
|
)
|
||||||
virtualMachine = VZVirtualMachine(configuration: configuration)
|
virtualMachine = VZVirtualMachine(configuration: configuration)
|
||||||
|
|
||||||
@@ -132,7 +134,8 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
|||||||
diskSizeGB: UInt16,
|
diskSizeGB: UInt16,
|
||||||
network: Network = NetworkShared(),
|
network: Network = NetworkShared(),
|
||||||
additionalDiskAttachments: [VZDiskImageStorageDeviceAttachment] = [],
|
additionalDiskAttachments: [VZDiskImageStorageDeviceAttachment] = [],
|
||||||
directorySharingDevices: [VZDirectorySharingDeviceConfiguration] = []
|
directorySharingDevices: [VZDirectorySharingDeviceConfiguration] = [],
|
||||||
|
serialPorts: [VZSerialPortConfiguration] = []
|
||||||
) async throws {
|
) async throws {
|
||||||
var ipswURL = ipswURL
|
var ipswURL = ipswURL
|
||||||
|
|
||||||
@@ -179,7 +182,8 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
|||||||
let configuration = try Self.craftConfiguration(diskURL: vmDir.diskURL, nvramURL: vmDir.nvramURL,
|
let configuration = try Self.craftConfiguration(diskURL: vmDir.diskURL, nvramURL: vmDir.nvramURL,
|
||||||
vmConfig: config, network: network,
|
vmConfig: config, network: network,
|
||||||
additionalDiskAttachments: additionalDiskAttachments,
|
additionalDiskAttachments: additionalDiskAttachments,
|
||||||
directorySharingDevices: directorySharingDevices
|
directorySharingDevices: directorySharingDevices,
|
||||||
|
serialPorts: serialPorts
|
||||||
)
|
)
|
||||||
virtualMachine = VZVirtualMachine(configuration: configuration)
|
virtualMachine = VZVirtualMachine(configuration: configuration)
|
||||||
|
|
||||||
@@ -261,7 +265,8 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
|||||||
vmConfig: VMConfig,
|
vmConfig: VMConfig,
|
||||||
network: Network = NetworkShared(),
|
network: Network = NetworkShared(),
|
||||||
additionalDiskAttachments: [VZDiskImageStorageDeviceAttachment],
|
additionalDiskAttachments: [VZDiskImageStorageDeviceAttachment],
|
||||||
directorySharingDevices: [VZDirectorySharingDeviceConfiguration]
|
directorySharingDevices: [VZDirectorySharingDeviceConfiguration],
|
||||||
|
serialPorts: [VZSerialPortConfiguration]
|
||||||
) throws -> VZVirtualMachineConfiguration {
|
) throws -> VZVirtualMachineConfiguration {
|
||||||
let configuration = VZVirtualMachineConfiguration()
|
let configuration = VZVirtualMachineConfiguration()
|
||||||
|
|
||||||
@@ -308,6 +313,9 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
|||||||
// Directory sharing devices
|
// Directory sharing devices
|
||||||
configuration.directorySharingDevices = directorySharingDevices
|
configuration.directorySharingDevices = directorySharingDevices
|
||||||
|
|
||||||
|
// Serial Port
|
||||||
|
configuration.serialPorts = serialPorts
|
||||||
|
|
||||||
try configuration.validate()
|
try configuration.validate()
|
||||||
|
|
||||||
return configuration
|
return configuration
|
||||||
|
|||||||
@@ -81,7 +81,8 @@ extension VMDirectory {
|
|||||||
try? decodeStream.close()
|
try? decodeStream.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let extractStream = ArchiveStream.extractStream(extractingTo: FilePath(baseURL.path)) else {
|
guard let extractStream = ArchiveStream.extractStream(extractingTo: FilePath(baseURL.path),
|
||||||
|
flags: [.ignoreOperationNotPermitted]) else {
|
||||||
let details = Errno(rawValue: CInt(errno))
|
let details = Errno(rawValue: CInt(errno))
|
||||||
|
|
||||||
throw RuntimeError.ImportFailed("ArchiveStream.extractStream() failed: \(details)")
|
throw RuntimeError.ImportFailed("ArchiveStream.extractStream() failed: \(details)")
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ struct VMDirectory: Prunable {
|
|||||||
baseURL
|
baseURL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func running() throws -> Bool {
|
||||||
|
try PIDLock(lockURL: configURL).pid() != 0
|
||||||
|
}
|
||||||
|
|
||||||
static func temporary() throws -> VMDirectory {
|
static func temporary() throws -> VMDirectory {
|
||||||
let tmpDir = try Config().tartTmpDir.appendingPathComponent(UUID().uuidString)
|
let tmpDir = try Config().tartTmpDir.appendingPathComponent(UUID().uuidString)
|
||||||
try FileManager.default.createDirectory(at: tmpDir, withIntermediateDirectories: false)
|
try FileManager.default.createDirectory(at: tmpDir, withIntermediateDirectories: false)
|
||||||
@@ -51,9 +55,9 @@ struct VMDirectory: Prunable {
|
|||||||
try? FileManager.default.removeItem(at: nvramURL)
|
try? FileManager.default.removeItem(at: nvramURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
func validate() throws {
|
func validate(userFriendlyName: String) throws {
|
||||||
if !FileManager.default.fileExists(atPath: baseURL.path) {
|
if !FileManager.default.fileExists(atPath: baseURL.path) {
|
||||||
throw RuntimeError.VMDoesNotExist(name: baseURL.lastPathComponent)
|
throw RuntimeError.VMDoesNotExist(name: userFriendlyName)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !initialized {
|
if !initialized {
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ extension Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum RuntimeError : Error {
|
enum RuntimeError : Error {
|
||||||
|
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 VMNotRunning(_ message: String)
|
||||||
@@ -56,6 +57,7 @@ enum RuntimeError : Error {
|
|||||||
case ExportFailed(_ message: String)
|
case ExportFailed(_ message: String)
|
||||||
case ImportFailed(_ message: String)
|
case ImportFailed(_ message: String)
|
||||||
case SoftnetFailed(_ message: String)
|
case SoftnetFailed(_ message: String)
|
||||||
|
case OCIStorageError(_ message: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol HasExitCode {
|
protocol HasExitCode {
|
||||||
@@ -65,6 +67,8 @@ protocol HasExitCode {
|
|||||||
extension RuntimeError : CustomStringConvertible {
|
extension RuntimeError : CustomStringConvertible {
|
||||||
public var description: String {
|
public var description: String {
|
||||||
switch self {
|
switch self {
|
||||||
|
case .VMConfigurationError(let message):
|
||||||
|
return message
|
||||||
case .VMDoesNotExist(let name):
|
case .VMDoesNotExist(let name):
|
||||||
return "the specified VM \"\(name)\" does not exist"
|
return "the specified VM \"\(name)\" does not exist"
|
||||||
case .VMMissingFiles(let message):
|
case .VMMissingFiles(let message):
|
||||||
@@ -95,6 +99,8 @@ extension RuntimeError : CustomStringConvertible {
|
|||||||
return "VM import failed: \(message)"
|
return "VM import failed: \(message)"
|
||||||
case .SoftnetFailed(let message):
|
case .SoftnetFailed(let message):
|
||||||
return "Softnet failed: \(message)"
|
return "Softnet failed: \(message)"
|
||||||
|
case .OCIStorageError(let message):
|
||||||
|
return "OCI storage error: \(message)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class VMStorageLocal {
|
|||||||
func open(_ name: String) throws -> VMDirectory {
|
func open(_ name: String) throws -> VMDirectory {
|
||||||
let vmDir = VMDirectory(baseURL: vmURL(name))
|
let vmDir = VMDirectory(baseURL: vmURL(name))
|
||||||
|
|
||||||
try vmDir.validate()
|
try vmDir.validate(userFriendlyName: name)
|
||||||
|
|
||||||
return vmDir
|
return vmDir
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,20 @@ class VMStorageOCI: PrunableStorage {
|
|||||||
VMDirectory(baseURL: vmURL(name)).initialized
|
VMDirectory(baseURL: vmURL(name)).initialized
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func digest(_ name: RemoteName) throws -> String {
|
||||||
|
let digest = vmURL(name).resolvingSymlinksInPath().lastPathComponent
|
||||||
|
|
||||||
|
if !digest.starts(with: "sha256:") {
|
||||||
|
throw RuntimeError.OCIStorageError("\(name) is not a digest and doesn't point to a digest")
|
||||||
|
}
|
||||||
|
|
||||||
|
return digest
|
||||||
|
}
|
||||||
|
|
||||||
func open(_ name: RemoteName) throws -> VMDirectory {
|
func open(_ name: RemoteName) throws -> VMDirectory {
|
||||||
let vmDir = VMDirectory(baseURL: vmURL(name))
|
let vmDir = VMDirectory(baseURL: vmURL(name))
|
||||||
|
|
||||||
try vmDir.validate()
|
try vmDir.validate(userFriendlyName: name.description)
|
||||||
|
|
||||||
try vmDir.baseURL.updateAccessDate()
|
try vmDir.baseURL.updateAccessDate()
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ final class MACAddressResolverTests: XCTestCase {
|
|||||||
""")
|
""")
|
||||||
|
|
||||||
XCTAssertEqual(IPv4Address("1.2.3.4"),
|
XCTAssertEqual(IPv4Address("1.2.3.4"),
|
||||||
try leases.resolveMACAddress(macAddress: MACAddress(fromString: "00:11:22:33:44:55")!))
|
try leases.ResolveMACAddress(macAddress: MACAddress(fromString: "00:11:22:33:44:55")!))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testMultipleEntries() throws {
|
func testMultipleEntries() throws {
|
||||||
@@ -28,8 +28,8 @@ final class MACAddressResolverTests: XCTestCase {
|
|||||||
""")
|
""")
|
||||||
|
|
||||||
XCTAssertEqual(IPv4Address("1.2.3.4"),
|
XCTAssertEqual(IPv4Address("1.2.3.4"),
|
||||||
try leases.resolveMACAddress(macAddress: MACAddress(fromString: "00:11:22:33:44:55")!))
|
try leases.ResolveMACAddress(macAddress: MACAddress(fromString: "00:11:22:33:44:55")!))
|
||||||
XCTAssertEqual(IPv4Address("5.6.7.8"),
|
XCTAssertEqual(IPv4Address("5.6.7.8"),
|
||||||
try leases.resolveMACAddress(macAddress: MACAddress(fromString: "AA:BB:CC:DD:EE:FF")!))
|
try leases.ResolveMACAddress(macAddress: MACAddress(fromString: "AA:BB:CC:DD:EE:FF")!))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
@@ -4,6 +4,17 @@ Tart already powers several CI services mentioned above including our own [Cirru
|
|||||||
For services that haven't leveraged Tart yet, we offer fully managed runners via a monthly subscription.
|
For services that haven't leveraged Tart yet, we offer fully managed runners via a monthly subscription.
|
||||||
*Cirrus Runners* is the fastest way to get your current CI workflows to benefit from Apple Silicon hardware. No need to manage infrastructure or migrate to another CI provider.
|
*Cirrus Runners* is the fastest way to get your current CI workflows to benefit from Apple Silicon hardware. No need to manage infrastructure or migrate to another CI provider.
|
||||||
|
|
||||||
|
## Testimonials from customers
|
||||||
|
|
||||||
|
Sebastian Jachec, Mobile Engineer at [Daybridge](https://www.daybridge.com/).
|
||||||
|
|
||||||
|
> It’s been plain-sailing with the Cirrus Runners — they’ve been great! They’re consistently 60+% faster on workflows that we previously used Github Actions’ macOS runners for.
|
||||||
|
|
||||||
|
Max Lapides, Senior Mobile Engineer at [Tonal](https://www.tonal.com/).
|
||||||
|
|
||||||
|
> Previously, we were using the GitHub‑hosted macOS runners and our iOS build took ~30 minutes. Now with Cirrus Runners, the iOS build only takes ~12 minutes. That’s a huge boost to our productivity, and for only $150/month per runner it is much less expensive too.
|
||||||
|
|
||||||
|
|
||||||
## Configuring Cirrus Runners
|
## Configuring Cirrus Runners
|
||||||
|
|
||||||
Configuring Cirrus Runners for GitHub Actions is as simple as installing [Cirrus Runners App](https://github.com/apps/cirrus-runners).
|
Configuring Cirrus Runners for GitHub Actions is as simple as installing [Cirrus Runners App](https://github.com/apps/cirrus-runners).
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# GitLab Runner Executor
|
||||||
|
|
||||||
|
It is possible to run GitLab jobs in isolated ephemeral Tart Virtual Machines via [Tart Executor](https://github.com/cirruslabs/gitlab-tart-executor).
|
||||||
|
Tart Executor utilizes [custom executor](https://docs.gitlab.com/runner/executors/custom.html) feature of GitLab Runner.
|
||||||
|
|
||||||
|
# Basic Configuration
|
||||||
|
|
||||||
|
Configuring Tart Executor for GitLab Runner is as simple as installing `gitlab-tart-executor` binary from Homebrew:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew install cirruslabs/cli/gitlab-tart-executor
|
||||||
|
```
|
||||||
|
|
||||||
|
And updating configuration of your self-hosted GitLab Runner to use `gitlab-tart-executor` binary:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
concurrent = 2
|
||||||
|
|
||||||
|
[[runners]]
|
||||||
|
# ...
|
||||||
|
executor = "custom"
|
||||||
|
builds_dir = "/Users/admin/builds" # directory inside the
|
||||||
|
cache_dir = "/Users/admin/cache"
|
||||||
|
[runners.feature_flags]
|
||||||
|
FF_RESOLVE_FULL_TLS_CHAIN = false
|
||||||
|
[runners.custom]
|
||||||
|
prepare_exec = "gitlab-tart-executor"
|
||||||
|
prepare_args = ["prepare"]
|
||||||
|
run_exec = "gitlab-tart-executor"
|
||||||
|
run_args = ["run"]
|
||||||
|
cleanup_exec = "gitlab-tart-executor"
|
||||||
|
cleanup_args = ["cleanup"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Now you can use Tart Images in your `.gitlab-ci.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# You can use any remote Tart Image.
|
||||||
|
# Tart Executor will pull it from the registry and use it for creating ephemeral VMs.
|
||||||
|
image: ghcr.io/cirruslabs/macos-ventura-base:latest
|
||||||
|
|
||||||
|
test:
|
||||||
|
tags:
|
||||||
|
- tart-installed # in case you tagged runners with Tart Executor installed
|
||||||
|
script:
|
||||||
|
- uname -a
|
||||||
|
```
|
||||||
|
|
||||||
|
For more advanced configuration please refer to [GitLab Tart Executor repository](https://github.com/cirruslabs/gitlab-tart-executor).
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
source = [".build/arm64-apple-macosx/debug/tart"]
|
source = [".build/arm64-apple-macosx/release/tart"]
|
||||||
bundle_id = "com.github.cirruslabs.tart"
|
bundle_id = "com.github.cirruslabs.tart"
|
||||||
|
|
||||||
apple_id {
|
apple_id {
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ nav:
|
|||||||
- "Quick Start": quick-start.md
|
- "Quick Start": quick-start.md
|
||||||
- "Integrations":
|
- "Integrations":
|
||||||
- "GitHub Actions": integrations/github-actions.md
|
- "GitHub Actions": integrations/github-actions.md
|
||||||
|
- "GitLab Runner": integrations/gitlab-runner.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
|
||||||
- "Licensing": licensing.md
|
- "Licensing": licensing.md
|
||||||
|
|||||||
Executable
+11
@@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# helper script to build and run a signed tart binary
|
||||||
|
# usage: ./scripts/run-signed.sh run ventura-base
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
swift build --product tart
|
||||||
|
codesign --sign - --entitlements Resources/tart-dev.entitlements --force .build/debug/tart
|
||||||
|
|
||||||
|
.build/debug/tart "$@"
|
||||||
Reference in New Issue
Block a user