mirror of
https://github.com/cirruslabs/tart.git
synced 2026-10-21 09:42:13 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1890909d28 | ||
|
|
0cad2e454c | ||
|
|
17dcf942c1 | ||
|
|
6296df7c0c | ||
|
|
c54b140750 | ||
|
|
048a5506df | ||
|
|
553b36349b |
+14
-1
@@ -22,10 +22,23 @@ task:
|
|||||||
macos_instance:
|
macos_instance:
|
||||||
image: ghcr.io/cirruslabs/macos-ventura-xcode:latest
|
image: ghcr.io/cirruslabs/macos-ventura-xcode:latest
|
||||||
env:
|
env:
|
||||||
|
MACOS_CERTIFICATE: ENCRYPTED[0dcc39b6d243b7eafba4412741c41dcb6779ebcbc40d78e54dbbb92632e64cdd5b665595e3c1251d746d9e6a124e4121]
|
||||||
|
AC_PASSWORD: ENCRYPTED[4a761023e7e06fe2eb350c8b6e8e7ca961af193cb9ba47605f25f1d353abc3142606f412e405be48fd897a78787ea8c2]
|
||||||
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
|
install_script:
|
||||||
|
- brew install go goreleaser/tap/goreleaser-pro
|
||||||
|
- brew install mitchellh/gon/gon
|
||||||
|
setup_script:
|
||||||
|
- cd $HOME
|
||||||
|
- echo $MACOS_CERTIFICATE | base64 --decode > certificate.p12
|
||||||
|
- security create-keychain -p password101 build.keychain
|
||||||
|
- security default-keychain -s build.keychain
|
||||||
|
- security unlock-keychain -p password101 build.keychain
|
||||||
|
- security import certificate.p12 -k build.keychain -P password101 -T /usr/bin/codesign
|
||||||
|
- security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k password101 build.keychain
|
||||||
info_script:
|
info_script:
|
||||||
|
- security find-identity -v
|
||||||
- xcodebuild -version
|
- xcodebuild -version
|
||||||
- swift -version
|
- swift -version
|
||||||
release_script: goreleaser
|
release_script: goreleaser
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ builds:
|
|||||||
- arm64
|
- arm64
|
||||||
prebuilt:
|
prebuilt:
|
||||||
path: .build/{{ .Arch }}-apple-macosx/debug/tart
|
path: .build/{{ .Arch }}-apple-macosx/debug/tart
|
||||||
|
hooks:
|
||||||
|
post: gon gon.hcl
|
||||||
|
|
||||||
before:
|
before:
|
||||||
hooks:
|
hooks:
|
||||||
|
|||||||
@@ -191,10 +191,20 @@ tart push my-local-vm-name acme.io/remoteorg/name:latest acme.io/remoteorg/name:
|
|||||||
|
|
||||||
#### Pulling a Remote Image
|
#### Pulling a Remote Image
|
||||||
|
|
||||||
|
You can either pull an image:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
tart pull acme.io/remoteorg/name:latest my-local-vm-name
|
tart pull acme.io/remoteorg/name:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
|
...or instantiate a VM from a remote image:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
tart clone acme.io/remoteorg/name:latest my-local-vm-name
|
||||||
|
```
|
||||||
|
|
||||||
|
This invocation calls the `tart pull` implicitly (if the image is not being present) before doing the actual cloning.
|
||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ struct Create: AsyncParsableCommand {
|
|||||||
if #available(macOS 13, *) {
|
if #available(macOS 13, *) {
|
||||||
_ = try await VM.linux(vmDir: tmpVMDir, diskSizeGB: diskSize)
|
_ = try await VM.linux(vmDir: tmpVMDir, diskSizeGB: diskSize)
|
||||||
} else {
|
} else {
|
||||||
throw UnsupportedOSError()
|
throw UnsupportedOSError("Linux VMs", "are")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ struct Login: AsyncParsableCommand {
|
|||||||
|
|
||||||
do {
|
do {
|
||||||
let registry = try Registry(host: host, namespace: "", insecure: insecure,
|
let registry = try Registry(host: host, namespace: "", insecure: insecure,
|
||||||
credentialsProvider: credentialsProvider)
|
credentialsProviders: [credentialsProvider])
|
||||||
try await registry.ping()
|
try await registry.ping()
|
||||||
} catch {
|
} catch {
|
||||||
print("invalid credentials: \(error)")
|
print("invalid credentials: \(error)")
|
||||||
|
|||||||
@@ -37,13 +37,21 @@ struct Run: AsyncParsableCommand {
|
|||||||
@Flag var withSoftnet: Bool = false
|
@Flag var withSoftnet: Bool = false
|
||||||
|
|
||||||
@Option(help: ArgumentHelp("""
|
@Option(help: ArgumentHelp("""
|
||||||
Additional disk attachments with an optional read-only specifier\n(e.g. --disk=\"disk.bin\" --disk=\"disk.bin:ro\")
|
Additional disk attachments with an optional read-only specifier\n(e.g. --disk=\"disk.bin\" --disk=\"ubuntu.iso:ro\")
|
||||||
""", discussion: """
|
""", discussion: """
|
||||||
Learn how to create a disk image using Disk Utility here:
|
Learn how to create a disk image using Disk Utility here:
|
||||||
https://support.apple.com/en-gb/guide/disk-utility/dskutl11888/mac
|
https://support.apple.com/en-gb/guide/disk-utility/dskutl11888/mac
|
||||||
"""))
|
""", valueName: "path[:ro]"))
|
||||||
var disk: [String] = []
|
var disk: [String] = []
|
||||||
|
|
||||||
|
@Option(help: ArgumentHelp("""
|
||||||
|
Additional directory shares with an optional read-only specifier\n(e.g. --dir=\"build:~/src/build\" --dir=\"sources:~/src/sources:ro\")
|
||||||
|
""", discussion: """
|
||||||
|
All shared directories are automatically mounted to "/Volumes/My Shared Files" directory on macOS,
|
||||||
|
while on Linux you have to do it manually: "mount -t virtiofs com.apple.virtio-fs.automount /mount/point".
|
||||||
|
""", valueName: "name:path[:ro]"))
|
||||||
|
var dir: [String] = []
|
||||||
|
|
||||||
func validate() throws {
|
func validate() throws {
|
||||||
if vnc && vncExperimental {
|
if vnc && vncExperimental {
|
||||||
throw ValidationError("--vnc and --vnc-experimental are mutually exclusive")
|
throw ValidationError("--vnc and --vnc-experimental are mutually exclusive")
|
||||||
@@ -56,7 +64,8 @@ struct Run: AsyncParsableCommand {
|
|||||||
vm = try VM(
|
vm = try VM(
|
||||||
vmDir: vmDir,
|
vmDir: vmDir,
|
||||||
withSoftnet: withSoftnet,
|
withSoftnet: withSoftnet,
|
||||||
additionalDiskAttachments: additionalDiskAttachments()
|
additionalDiskAttachments: additionalDiskAttachments(),
|
||||||
|
directoryShares: directoryShares()
|
||||||
)
|
)
|
||||||
|
|
||||||
let vncImpl: VNC? = try {
|
let vncImpl: VNC? = try {
|
||||||
@@ -135,6 +144,34 @@ struct Run: AsyncParsableCommand {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func directoryShares() throws -> [DirectoryShare] {
|
||||||
|
var result: [DirectoryShare] = []
|
||||||
|
|
||||||
|
for rawDir in dir {
|
||||||
|
let splits = rawDir.split(maxSplits: 2) { $0 == ":" }
|
||||||
|
|
||||||
|
if splits.count < 2 {
|
||||||
|
throw ValidationError("invalid --dir syntax: should at least include name and path, colon-separated")
|
||||||
|
}
|
||||||
|
|
||||||
|
var readOnly: Bool = false
|
||||||
|
|
||||||
|
if splits.count == 3 {
|
||||||
|
if splits[2] == "ro" {
|
||||||
|
readOnly = true
|
||||||
|
} else {
|
||||||
|
throw ValidationError("invalid --dir syntax: optional read-only specifier can only be \"ro\"")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (name, path) = (String(splits[0]), String(splits[1]))
|
||||||
|
|
||||||
|
result.append(DirectoryShare(name: name, path: URL(fileURLWithPath: path), readOnly: readOnly))
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
private func runUI() {
|
private func runUI() {
|
||||||
let nsApp = NSApplication.shared
|
let nsApp = NSApplication.shared
|
||||||
nsApp.setActivationPolicy(.regular)
|
nsApp.setActivationPolicy(.regular)
|
||||||
@@ -148,6 +185,8 @@ struct Run: AsyncParsableCommand {
|
|||||||
Group {
|
Group {
|
||||||
VMView(vm: vm!).onAppear {
|
VMView(vm: vm!).onAppear {
|
||||||
NSWindow.allowsAutomaticWindowTabbing = false
|
NSWindow.allowsAutomaticWindowTabbing = false
|
||||||
|
}.onDisappear {
|
||||||
|
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 {
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
class HelperProgramCredentialsProvider: CredentialsProvider {
|
||||||
|
func retrieve(host: String) throws -> (String, String)? {
|
||||||
|
let dockerConfigURL = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".docker").appendingPathComponent("config.json")
|
||||||
|
if !FileManager.default.fileExists(atPath: dockerConfigURL.path) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let config = try JSONDecoder().decode(DockerConfig.self, from: Data(contentsOf: dockerConfigURL))
|
||||||
|
|
||||||
|
if let helperProgram = config.credHelpers?[host] {
|
||||||
|
return try executeHelper(binaryName: "docker-credential-\(helperProgram)", host: host)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func executeHelper(binaryName: String, host: String) throws -> (String, String)? {
|
||||||
|
guard let executableURL = resolveBinaryPath(binaryName) else {
|
||||||
|
throw CredentialsProviderError.Failed(message: "\(binaryName) not found in PATH")
|
||||||
|
}
|
||||||
|
|
||||||
|
let process = Process.init()
|
||||||
|
process.executableURL = executableURL
|
||||||
|
process.arguments = ["get"]
|
||||||
|
|
||||||
|
let outPipe = Pipe()
|
||||||
|
let inPipe = Pipe()
|
||||||
|
|
||||||
|
process.standardOutput = outPipe
|
||||||
|
process.standardError = outPipe
|
||||||
|
process.standardInput = inPipe
|
||||||
|
|
||||||
|
process.launch()
|
||||||
|
|
||||||
|
inPipe.fileHandleForWriting.write("\(host)\n".data(using: .utf8)!)
|
||||||
|
inPipe.fileHandleForWriting.closeFile()
|
||||||
|
|
||||||
|
process.waitUntilExit()
|
||||||
|
|
||||||
|
if !(process.terminationReason == .exit && process.terminationStatus == 0) {
|
||||||
|
throw CredentialsProviderError.Failed(message: "Docker helper failed!")
|
||||||
|
}
|
||||||
|
|
||||||
|
let getOutput = try JSONDecoder().decode(
|
||||||
|
DockerGetOutput.self, from: outPipe.fileHandleForReading.readDataToEndOfFile()
|
||||||
|
)
|
||||||
|
return (getOutput.Username, getOutput.Secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
func store(host: String, user: String, password: String) throws {
|
||||||
|
throw CredentialsProviderError.Failed(message: "Docker helpers don't support storing!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DockerConfig: Codable {
|
||||||
|
var credHelpers: Dictionary<String, String>? = Dictionary()
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DockerGetOutput: Codable {
|
||||||
|
var Username: String
|
||||||
|
var Secret: String
|
||||||
|
}
|
||||||
@@ -88,29 +88,29 @@ class Registry {
|
|||||||
|
|
||||||
let baseURL: URL
|
let baseURL: URL
|
||||||
let namespace: String
|
let namespace: String
|
||||||
let credentialsProvider: CredentialsProvider
|
let credentialsProviders: [CredentialsProvider]
|
||||||
|
|
||||||
var currentAuthToken: Authentication? = nil
|
var currentAuthToken: Authentication? = nil
|
||||||
|
|
||||||
init(urlComponents: URLComponents,
|
init(urlComponents: URLComponents,
|
||||||
namespace: String,
|
namespace: String,
|
||||||
credentialsProvider: CredentialsProvider = KeychainCredentialsProvider()
|
credentialsProviders: [CredentialsProvider] = [HelperProgramCredentialsProvider(), KeychainCredentialsProvider()]
|
||||||
) throws {
|
) throws {
|
||||||
baseURL = urlComponents.url!
|
baseURL = urlComponents.url!
|
||||||
self.namespace = namespace
|
self.namespace = namespace
|
||||||
self.credentialsProvider = credentialsProvider
|
self.credentialsProviders = credentialsProviders
|
||||||
}
|
}
|
||||||
|
|
||||||
convenience init(
|
convenience init(
|
||||||
host: String,
|
host: String,
|
||||||
namespace: String,
|
namespace: String,
|
||||||
insecure: Bool = false,
|
insecure: Bool = false,
|
||||||
credentialsProvider: CredentialsProvider = KeychainCredentialsProvider()
|
credentialsProviders: [CredentialsProvider] = [HelperProgramCredentialsProvider(), KeychainCredentialsProvider()]
|
||||||
) throws {
|
) throws {
|
||||||
let proto = insecure ? "http" : "https"
|
let proto = insecure ? "http" : "https"
|
||||||
let baseURLComponents = URLComponents(string: proto + "://" + host + "/v2/")!
|
let baseURLComponents = URLComponents(string: proto + "://" + host + "/v2/")!
|
||||||
|
|
||||||
try self.init(urlComponents: baseURLComponents, namespace: namespace, credentialsProvider: credentialsProvider)
|
try self.init(urlComponents: baseURLComponents, namespace: namespace, credentialsProviders: credentialsProviders)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ping() async throws {
|
func ping() async throws {
|
||||||
@@ -303,7 +303,7 @@ class Registry {
|
|||||||
let wwwAuthenticate = try WWWAuthenticate(rawHeaderValue: wwwAuthenticateRaw)
|
let wwwAuthenticate = try WWWAuthenticate(rawHeaderValue: wwwAuthenticateRaw)
|
||||||
|
|
||||||
if wwwAuthenticate.scheme == "Basic" {
|
if wwwAuthenticate.scheme == "Basic" {
|
||||||
if let (user, password) = try credentialsProvider.retrieve(host: baseURL.host!) {
|
if let (user, password) = try lookupCredentials(host: baseURL.host!) {
|
||||||
currentAuthToken = BasicAuthentication(user: user, password: password)
|
currentAuthToken = BasicAuthentication(user: user, password: password)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,7 +340,7 @@ class Registry {
|
|||||||
|
|
||||||
var headers: Dictionary<String, String> = Dictionary()
|
var headers: Dictionary<String, String> = Dictionary()
|
||||||
|
|
||||||
if let (user, password) = try credentialsProvider.retrieve(host: baseURL.host!) {
|
if let (user, password) = try lookupCredentials(host: baseURL.host!) {
|
||||||
let encodedCredentials = "\(user):\(password)".data(using: .utf8)?.base64EncodedString()
|
let encodedCredentials = "\(user):\(password)".data(using: .utf8)?.base64EncodedString()
|
||||||
headers["Authorization"] = "Basic \(encodedCredentials!)"
|
headers["Authorization"] = "Basic \(encodedCredentials!)"
|
||||||
}
|
}
|
||||||
@@ -356,6 +356,15 @@ class Registry {
|
|||||||
currentAuthToken = try TokenResponse.parse(fromData: bodyData)
|
currentAuthToken = try TokenResponse.parse(fromData: bodyData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func lookupCredentials(host: String) throws -> (String, String)? {
|
||||||
|
for provider in credentialsProviders {
|
||||||
|
if let (user, password) = try provider.retrieve(host: host) {
|
||||||
|
return (user, password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
private func authAwareRequest(request: HTTPClientRequest) async throws -> HTTPClientResponse {
|
private func authAwareRequest(request: HTTPClientRequest) async throws -> HTTPClientResponse {
|
||||||
var request = request
|
var request = request
|
||||||
|
|
||||||
|
|||||||
@@ -84,4 +84,14 @@ struct Darwin: Platform {
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func pointingDevices() -> [VZPointingDeviceConfiguration] {
|
||||||
|
if #available(macOS 13, *) {
|
||||||
|
// Trackpad is only supported starting with macOS Ventura
|
||||||
|
// macOS Monterey will continue using a USB device == .darwin
|
||||||
|
return [VZMacTrackpadConfiguration(), VZUSBScreenCoordinatePointingDeviceConfiguration()]
|
||||||
|
} else {
|
||||||
|
return [VZUSBScreenCoordinatePointingDeviceConfiguration()]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,4 +30,8 @@ struct Linux: Platform {
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func pointingDevices() -> [VZPointingDeviceConfiguration] {
|
||||||
|
[VZUSBScreenCoordinatePointingDeviceConfiguration()]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,4 +5,5 @@ protocol Platform: Codable {
|
|||||||
func bootLoader(nvramURL: URL) throws -> VZBootLoader
|
func bootLoader(nvramURL: URL) throws -> VZBootLoader
|
||||||
func platform(nvramURL: URL) -> VZPlatformConfiguration
|
func platform(nvramURL: URL) -> VZPlatformConfiguration
|
||||||
func graphicsDevice(vmConfig: VMConfig) -> VZGraphicsDeviceConfiguration
|
func graphicsDevice(vmConfig: VMConfig) -> VZGraphicsDeviceConfiguration
|
||||||
|
func pointingDevices() -> [VZPointingDeviceConfiguration]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class Softnet {
|
|||||||
init(vmMACAddress: String) throws {
|
init(vmMACAddress: String) throws {
|
||||||
let binaryName = "softnet"
|
let binaryName = "softnet"
|
||||||
|
|
||||||
guard let executableURL = Self.resolveBinaryPath(binaryName) else {
|
guard let executableURL = resolveBinaryPath(binaryName) else {
|
||||||
throw SoftnetError.InitializationFailed(why: "\(binaryName) not found in PATH")
|
throw SoftnetError.InitializationFailed(why: "\(binaryName) not found in PATH")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,23 +43,6 @@ class Softnet {
|
|||||||
process.waitUntilExit()
|
process.waitUntilExit()
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func resolveBinaryPath(_ name: String) -> URL? {
|
|
||||||
guard let path = ProcessInfo.processInfo.environment["PATH"] else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for pathComponent in path.split(separator: ":") {
|
|
||||||
let url = URL(fileURLWithPath: String(pathComponent))
|
|
||||||
.appendingPathComponent(name, isDirectory: false)
|
|
||||||
|
|
||||||
if FileManager.default.fileExists(atPath: url.path) {
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setSocketBuffers(_ fd: Int32, _ sizeBytes: Int) throws {
|
private func setSocketBuffers(_ fd: Int32, _ sizeBytes: Int) throws {
|
||||||
var option_value = sizeBytes
|
var option_value = sizeBytes
|
||||||
let option_len = socklen_t(MemoryLayout<Int>.size)
|
let option_len = socklen_t(MemoryLayout<Int>.size)
|
||||||
|
|||||||
@@ -5,3 +5,20 @@ extension Collection {
|
|||||||
indices.contains(index) ? self[index] : nil
|
indices.contains(index) ? self[index] : nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resolveBinaryPath(_ name: String) -> URL? {
|
||||||
|
guard let path = ProcessInfo.processInfo.environment["PATH"] else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for pathComponent in path.split(separator: ":") {
|
||||||
|
let url = URL(fileURLWithPath: String(pathComponent))
|
||||||
|
.appendingPathComponent(name, isDirectory: false)
|
||||||
|
|
||||||
|
if FileManager.default.fileExists(atPath: url.path) {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
+34
-6
@@ -11,7 +11,11 @@ struct DownloadFailed: Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct UnsupportedOSError: Error, CustomStringConvertible {
|
struct UnsupportedOSError: Error, CustomStringConvertible {
|
||||||
private(set) var description: String = "error: Linux VMs are only supported on macOS 13.0 (Ventura) or newer"
|
let description: String
|
||||||
|
|
||||||
|
init(_ what: String, _ plural: String) {
|
||||||
|
description = "error: \(what) \(plural) only supported on macOS 13.0 (Ventura) or newer"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct UnsupportedArchitectureError: Error {
|
struct UnsupportedArchitectureError: Error {
|
||||||
@@ -34,7 +38,8 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
|||||||
|
|
||||||
init(vmDir: VMDirectory,
|
init(vmDir: VMDirectory,
|
||||||
withSoftnet: Bool = false,
|
withSoftnet: Bool = false,
|
||||||
additionalDiskAttachments: [VZDiskImageStorageDeviceAttachment] = []
|
additionalDiskAttachments: [VZDiskImageStorageDeviceAttachment] = [],
|
||||||
|
directoryShares: [DirectoryShare] = []
|
||||||
) throws {
|
) throws {
|
||||||
name = vmDir.name
|
name = vmDir.name
|
||||||
config = try VMConfig.init(fromURL: vmDir.configURL)
|
config = try VMConfig.init(fromURL: vmDir.configURL)
|
||||||
@@ -50,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,
|
||||||
softnet: softnet, additionalDiskAttachments: additionalDiskAttachments)
|
softnet: softnet, additionalDiskAttachments: additionalDiskAttachments,
|
||||||
|
directoryShares: directoryShares)
|
||||||
virtualMachine = VZVirtualMachine(configuration: configuration)
|
virtualMachine = VZVirtualMachine(configuration: configuration)
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
@@ -149,7 +155,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, softnet: softnet,
|
vmConfig: config, softnet: softnet,
|
||||||
additionalDiskAttachments: additionalDiskAttachments)
|
additionalDiskAttachments: additionalDiskAttachments,
|
||||||
|
directoryShares: [])
|
||||||
virtualMachine = VZVirtualMachine(configuration: configuration)
|
virtualMachine = VZVirtualMachine(configuration: configuration)
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
@@ -228,7 +235,8 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
|||||||
nvramURL: URL,
|
nvramURL: URL,
|
||||||
vmConfig: VMConfig,
|
vmConfig: VMConfig,
|
||||||
softnet: Softnet? = nil,
|
softnet: Softnet? = nil,
|
||||||
additionalDiskAttachments: [VZDiskImageStorageDeviceAttachment]
|
additionalDiskAttachments: [VZDiskImageStorageDeviceAttachment],
|
||||||
|
directoryShares: [DirectoryShare]
|
||||||
) throws -> VZVirtualMachineConfiguration {
|
) throws -> VZVirtualMachineConfiguration {
|
||||||
let configuration = VZVirtualMachineConfiguration()
|
let configuration = VZVirtualMachineConfiguration()
|
||||||
|
|
||||||
@@ -256,7 +264,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
|||||||
|
|
||||||
// Keyboard and mouse
|
// Keyboard and mouse
|
||||||
configuration.keyboards = [VZUSBKeyboardConfiguration()]
|
configuration.keyboards = [VZUSBKeyboardConfiguration()]
|
||||||
configuration.pointingDevices = [VZUSBScreenCoordinatePointingDeviceConfiguration()]
|
configuration.pointingDevices = vmConfig.platform.pointingDevices()
|
||||||
|
|
||||||
// Networking
|
// Networking
|
||||||
let vio = VZVirtioNetworkDeviceConfiguration()
|
let vio = VZVirtioNetworkDeviceConfiguration()
|
||||||
@@ -278,6 +286,20 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
|||||||
// Entropy
|
// Entropy
|
||||||
configuration.entropyDevices = [VZVirtioEntropyDeviceConfiguration()]
|
configuration.entropyDevices = [VZVirtioEntropyDeviceConfiguration()]
|
||||||
|
|
||||||
|
// Directory share
|
||||||
|
if #available(macOS 13, *) {
|
||||||
|
var directories: [String : VZSharedDirectory] = Dictionary()
|
||||||
|
directoryShares.forEach { directories[$0.name] = VZSharedDirectory(url: $0.path, readOnly: $0.readOnly) }
|
||||||
|
|
||||||
|
let automountTag = VZVirtioFileSystemDeviceConfiguration.macOSGuestAutomountTag
|
||||||
|
let sharingDevice = VZVirtioFileSystemDeviceConfiguration(tag: automountTag)
|
||||||
|
sharingDevice.share = VZMultipleDirectoryShare(directories: directories)
|
||||||
|
|
||||||
|
configuration.directorySharingDevices = [sharingDevice]
|
||||||
|
} else if !directoryShares.isEmpty {
|
||||||
|
throw UnsupportedOSError("directory sharing", "is")
|
||||||
|
}
|
||||||
|
|
||||||
try configuration.validate()
|
try configuration.validate()
|
||||||
|
|
||||||
return configuration
|
return configuration
|
||||||
@@ -298,3 +320,9 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
|||||||
sema.signal()
|
sema.signal()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct DirectoryShare {
|
||||||
|
let name: String
|
||||||
|
let path: URL
|
||||||
|
let readOnly: Bool
|
||||||
|
}
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ struct VMConfig: Codable {
|
|||||||
if #available(macOS 13, *) {
|
if #available(macOS 13, *) {
|
||||||
platform = try Linux(from: decoder)
|
platform = try Linux(from: decoder)
|
||||||
} else {
|
} else {
|
||||||
throw UnsupportedOSError()
|
throw UnsupportedOSError("Linux VMs", "are")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cpuCountMin = try container.decode(Int.self, forKey: .cpuCountMin)
|
cpuCountMin = try container.decode(Int.self, forKey: .cpuCountMin)
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
source = [".build/arm64-apple-macosx/debug/tart"]
|
||||||
|
bundle_id = "com.github.cirruslabs.tart"
|
||||||
|
|
||||||
|
apple_id {
|
||||||
|
username = "hello@cirruslabs.org"
|
||||||
|
password = "@env:AC_PASSWORD"
|
||||||
|
}
|
||||||
|
|
||||||
|
sign {
|
||||||
|
application_identity = "Developer ID Application: Fedor Korotkov"
|
||||||
|
entitlements_file = "Resources/tart.entitlements"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user