mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-25 11:05:45 +00:00
tstest/tailmac: add customized macOS virtualization tooling (#13146)
updates tailcale/corp#22371 Adds custom macOS vm tooling. See the README for the general gist, but this will spin up VMs with unixgram capable network interfaces listening to a named socket, and with a virtio socket device for host-guest communication. We can add other devices like consoles, serial, etc as needed. The whole things is buildable with a single make command, and everything is controllable via the command line using the TailMac utility. This should all be generally functional but takes a few shortcuts with error handling and the like. The virtio socket device support has not been tested and may require some refinement. Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
This commit is contained in:
parent
1e8f8ee5f1
commit
8fad8c4b9b
6
.gitignore
vendored
6
.gitignore
vendored
@ -43,3 +43,9 @@ client/web/build/assets
|
|||||||
|
|
||||||
/gocross
|
/gocross
|
||||||
/dist
|
/dist
|
||||||
|
|
||||||
|
# Ignore xcode userstate and workspace data
|
||||||
|
*.xcuserstate
|
||||||
|
*.xcworkspacedata
|
||||||
|
/tstest/tailmac/bin
|
||||||
|
/tstest/tailmac/build
|
||||||
|
84
tstest/natlab/machost/README.md
Normal file
84
tstest/natlab/machost/README.md
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
# macOS VM's for tstest and natlab
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```
|
||||||
|
%make all
|
||||||
|
```
|
||||||
|
|
||||||
|
Will build both the TailMac and the VMHost app. You will need a developer account. The default bundle identifiers
|
||||||
|
default to tailscale owned ids, so if you don't have (or aren't using) a tailscale dev account, you will need to change this.
|
||||||
|
This should build automatically as long as you have a valid developer cert. Signing is automatic. The binaries both
|
||||||
|
require proper entitlements, so they do need to be signed.
|
||||||
|
|
||||||
|
There are separate recipes in the makefile to rebuild the individual components if needed.
|
||||||
|
|
||||||
|
All binaries are copied to the bin directory.
|
||||||
|
|
||||||
|
You can generally do all interactions via the TailMac command line util.
|
||||||
|
|
||||||
|
## Locations
|
||||||
|
|
||||||
|
Everything is persisted at ~/VM.bundle
|
||||||
|
|
||||||
|
Each vm gets it's own directory under there.
|
||||||
|
|
||||||
|
RestoreImage.ipsw is used to build new VMs. You may replace this manually if you wish.
|
||||||
|
|
||||||
|
Individual parameters for each instance are saved in a json config file (config.json)
|
||||||
|
|
||||||
|
## Installing
|
||||||
|
|
||||||
|
### Default a parameters
|
||||||
|
|
||||||
|
The default virtio socket device port is 51009
|
||||||
|
The default server socket for the virtual network device is /tmp/qemu.sock
|
||||||
|
The default memory size is 4Gb
|
||||||
|
The default mac address for the socket based network is 5a:94:ef:e4:0c:ee
|
||||||
|
The defualt mac address for normal ethernet is 5a:94:ef:e4:0c:ef
|
||||||
|
|
||||||
|
All of these parameters are configurable.
|
||||||
|
|
||||||
|
### Creating and managing VMs
|
||||||
|
|
||||||
|
To create a new VM (this will grab a restore image if needed). Restore images are large. Installation takes a minute
|
||||||
|
```
|
||||||
|
TailMac create --id my_vm_id
|
||||||
|
```
|
||||||
|
|
||||||
|
To delete a new VM
|
||||||
|
```
|
||||||
|
TailMac delete --id my_vm_id
|
||||||
|
```
|
||||||
|
|
||||||
|
To refresh an existing restore image:
|
||||||
|
```
|
||||||
|
TailMac refresh
|
||||||
|
```
|
||||||
|
|
||||||
|
To clone an existing vm (this will clone the mac and port as well)
|
||||||
|
```
|
||||||
|
TailMac clone --id old_vm_id --target-id new_vm_id
|
||||||
|
```
|
||||||
|
|
||||||
|
To reconfigure a vm with a specific mac and a virtio socket device port:
|
||||||
|
```
|
||||||
|
TailMac configure --id vm_id --mac 11:22:33:44:55:66 --port 12345 --ethermac 22:33:44:55:66:77 --mem 4000000000 --sock "/var/netdevice.sock"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running a VM
|
||||||
|
|
||||||
|
MacHost is an app bundle, but the main binary behaves as a command line util. You can invoke it
|
||||||
|
thusly:
|
||||||
|
|
||||||
|
```
|
||||||
|
TailMac --id machine_1
|
||||||
|
```
|
||||||
|
|
||||||
|
You may invoke multiple vms, but the limit on the number of concurrent instances is on the order of 2.
|
||||||
|
|
||||||
|
To stop a running VM (this is a fire and forget thing):
|
||||||
|
|
||||||
|
```
|
||||||
|
TailMac stop --id machine_1
|
||||||
|
```
|
8
tstest/tailmac/Host.entitlements
Normal file
8
tstest/tailmac/Host.entitlements
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.virtualization</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
8
tstest/tailmac/LICENSE/LICENSE.txt
Normal file
8
tstest/tailmac/LICENSE/LICENSE.txt
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
Copyright © 2023 Apple Inc.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
23
tstest/tailmac/Makefile
Normal file
23
tstest/tailmac/Makefile
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
XCPRETTIFIER := xcpretty
|
||||||
|
ifeq (, $(shell which $(XCPRETTIFIER)))
|
||||||
|
XCPRETTIFIER := cat
|
||||||
|
endif
|
||||||
|
|
||||||
|
.PHONY: tailmac
|
||||||
|
tailmac:
|
||||||
|
xcodebuild -scheme tailmac -destination 'platform=macOS,arch=arm64' -derivedDataPath build -configuration Release build | $(XCPRETTIFIER)
|
||||||
|
cp -r ./build/Build/Products/Release/tailmac ./bin/tailmac
|
||||||
|
|
||||||
|
.PHONY: host
|
||||||
|
host:
|
||||||
|
xcodebuild -scheme host -destination 'platform=macOS,arch=arm64' -derivedDataPath build -configuration Release build | $(XCPRETTIFIER)
|
||||||
|
cp -r ./build/Build/Products/Release/Host.app ./bin/Host.app
|
||||||
|
|
||||||
|
.PHONY: clean
|
||||||
|
clean:
|
||||||
|
rm -rf ./bin
|
||||||
|
rm -rf ./build
|
||||||
|
mkdir -p ./bin
|
||||||
|
|
||||||
|
.PHONY: all
|
||||||
|
all: clean tailmac host
|
161
tstest/tailmac/README.md
Normal file
161
tstest/tailmac/README.md
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
# Lightweight macOS VM's for tstest and natlab
|
||||||
|
|
||||||
|
This utility is designed to provide custom virtual machine tooling support for macOS. The intent
|
||||||
|
is to quickly create and spin up small, preconfigured virtual machines, for executing integration
|
||||||
|
and unit tests.
|
||||||
|
|
||||||
|
The primary driver is to provide support for VZVirtioNetworkDeviceConfiguration which is not
|
||||||
|
supported by other popular macOS VM hosts. This also gives us the freedom to fully customize and script
|
||||||
|
all virtual machine setup and interaction. VZVirtioNetworkDeviceConfiguration lets us
|
||||||
|
directly inject and sink network traffic for simulating various network conditions,
|
||||||
|
protocols, and topologies and ensure that the TailScale clients handle all of these situations correctly.
|
||||||
|
|
||||||
|
This may also be used as a drop-in replacement for UTM or Tart on ARM Macs for quickly spinning up
|
||||||
|
test VMs. It has the added benefit that, unlike UTM which uses AppleScript, it can be run
|
||||||
|
via SSH.
|
||||||
|
|
||||||
|
This uses Virtualization.framework which only supports arm64. The binaries only build for arm64.
|
||||||
|
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
The application is built in two components:
|
||||||
|
|
||||||
|
The tailmac command line utility is used to set up and configure VM instances. The Host.app does the heavy lifting.
|
||||||
|
|
||||||
|
You will typically initiate all interactions via the tailmac command-line util.
|
||||||
|
|
||||||
|
For a full list of options:
|
||||||
|
```
|
||||||
|
tailmac -h
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```
|
||||||
|
% make all
|
||||||
|
```
|
||||||
|
|
||||||
|
Will build both the tailmac command line util and Host.app. You will need a developer account. The default bundle identifiers
|
||||||
|
default to TailScale owned ids, so if you don't have (or aren't using) a TailScale dev account, you will need to change this.
|
||||||
|
This should build automatically as long as you have a valid developer cert. Signing is automatic. The binaries both
|
||||||
|
require the virtualization entitlement, so they do need to be signed.
|
||||||
|
|
||||||
|
There are separate recipes in the makefile to rebuild the individual components if needed.
|
||||||
|
|
||||||
|
All binaries are copied to the bin directory.
|
||||||
|
|
||||||
|
|
||||||
|
## Locations
|
||||||
|
|
||||||
|
All vm images, restore images, block device files, save states, and other supporting files are persisted at ~/VM.bundle
|
||||||
|
|
||||||
|
Each vm gets its own directory. These can be archived for posterity to preserve a particular image and/or state.
|
||||||
|
The mere existence of a directory containing all of the required files in ~/VM.bundle is sufficient for tailmac to
|
||||||
|
be able to see and run it. ~/VM.bundle and it's contents *is* tailmac's state. No other state is maintained elsewhere.
|
||||||
|
|
||||||
|
Each vm has its own custom configuration which can be modified while the vm is idle. It's simple JSON - you may
|
||||||
|
modify this directly, or using 'tailmac configure'.
|
||||||
|
|
||||||
|
|
||||||
|
## Installing
|
||||||
|
|
||||||
|
### Default a parameters
|
||||||
|
|
||||||
|
* The default virtio socket device port is 51009
|
||||||
|
* The default server socket for the virtual network device is /tmp/qemu-dgram.sock
|
||||||
|
* The default memory size is 4Gb
|
||||||
|
* The default mac address for the socket based networking is 52:cc:cc:cc:cc:01
|
||||||
|
* The default mac address for the standard ethernet interface is 52:cc:cc:cc:ce:01
|
||||||
|
|
||||||
|
### Creating and managing VMs
|
||||||
|
|
||||||
|
You generally perform all interactions via the tailmac command line util. A NAT ethernet device is provided so
|
||||||
|
you can ssh into your instance. The ethernet IP will be dhcp assigned by the host and can be determined by parsing
|
||||||
|
the contents of /var/db/dhcpd_leases
|
||||||
|
|
||||||
|
#### Creation
|
||||||
|
|
||||||
|
To create a new VM (this will grab a restore image for what apples deems a 'latest; if needed). Restore images are large
|
||||||
|
(on the order of 10 Gb) and installation after downloading takes a few minutes. If you wish to use a custom restore image,
|
||||||
|
specify it with the --image option. If RestoreImage.ipsw exists in ~/VM.bundle, it will be used. macOS versions from
|
||||||
|
12 to 15 have been tested and appear to work correctly.
|
||||||
|
```
|
||||||
|
tailmac create --id my_vm_id
|
||||||
|
```
|
||||||
|
|
||||||
|
With a custom restore image and parameters:
|
||||||
|
```
|
||||||
|
tailmac create --id my_custom_vm_id --image "/images/macos_ventura.ipsw" --mac 52:cc:cc:cc:cc:07 --mem 8000000000 --sock "/temp/custom.sock" --port 52345
|
||||||
|
```
|
||||||
|
|
||||||
|
A typical workflow would be to create single VM, manually set it up the way you wish including the installation of any required client side software
|
||||||
|
(tailscaled or the client-side test harness for example) then clone that images as required and back up your
|
||||||
|
images for future use.
|
||||||
|
|
||||||
|
Fetching and persisting pre-configured images is left as an exercise for the reader (for now). A previously used image can simply be copied to the
|
||||||
|
~/VM.bundle directory under a unique path and tailmac will automatically pick it up. No versioning is supported so old images may stop working in
|
||||||
|
the future.
|
||||||
|
|
||||||
|
To delete a VM image, you may simply remove it's directory under ~/VM.bundle or
|
||||||
|
```
|
||||||
|
tailmac delete --id my_stale_vm
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that the disk size is fixed, but should be sufficient (perhaps even excessive) for most lightweight workflows.
|
||||||
|
|
||||||
|
#### Restore Images
|
||||||
|
|
||||||
|
To refresh an existing restore image:
|
||||||
|
```
|
||||||
|
tailmac refresh
|
||||||
|
```
|
||||||
|
|
||||||
|
Restore images can also be obtained directly from Apple for all macOS releases. Note Apple restore images are raw installs, and the OS will require
|
||||||
|
configuration, user setup, etc before being useful. Cloning a vm after clicking through the setup, creating a user and disabling things like the
|
||||||
|
lock screen and enabling auto-login will save you time in the future.
|
||||||
|
|
||||||
|
|
||||||
|
#### Cloning
|
||||||
|
|
||||||
|
To clone an existing vm (this will clone the mac and port as well)
|
||||||
|
```
|
||||||
|
tailmac clone --id old_vm_id --target-id new_vm_id
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Configuration
|
||||||
|
|
||||||
|
To reconfigure a existing vm:
|
||||||
|
```
|
||||||
|
tailmac configure --id vm_id --mac 11:22:33:44:55:66 --port 12345 --ethermac 22:33:44:55:66:77 -sock "/tmp/my.sock"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running a VM
|
||||||
|
|
||||||
|
To list the available VM images
|
||||||
|
```
|
||||||
|
tailmac ls
|
||||||
|
```
|
||||||
|
|
||||||
|
To launch an VM
|
||||||
|
```
|
||||||
|
tailmac run --id machine_1
|
||||||
|
```
|
||||||
|
|
||||||
|
You may invoke multiple vms, but the limit on the number of concurrent instances is on the order of 2. Use the --tail option to watch the stdout of the
|
||||||
|
Host.app process. There is currently no way to list the running VM instances, but invoking stop or halt for a vm instance
|
||||||
|
that is not running is perfectly safe.
|
||||||
|
|
||||||
|
To gracefully stop a running VM and save its state (this is a fire and forget thing):
|
||||||
|
|
||||||
|
```
|
||||||
|
tailmac stop --id machine_1
|
||||||
|
```
|
||||||
|
|
||||||
|
Manually closing a VM's window will save the VM's state (if possible) and is the equivalent of running 'tailmac stop --id vm_id'
|
||||||
|
|
||||||
|
To halt a running vm without saving its state:
|
||||||
|
```
|
||||||
|
tailmac halt --id machine_1
|
||||||
|
```
|
125
tstest/tailmac/Swift/Common/Config.swift
Normal file
125
tstest/tailmac/Swift/Common/Config.swift
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
let kDefaultDiskSizeGb: Int64 = 72
|
||||||
|
let kDefaultMemSizeGb: UInt64 = 72
|
||||||
|
|
||||||
|
|
||||||
|
/// Represents a configuration for a virtual machine
|
||||||
|
class Config: Codable {
|
||||||
|
var serverSocket = "/tmp/qemu-dgram.sock"
|
||||||
|
var memorySize = (kDefaultMemSizeGb * 1024 * 1024 * 1024) as UInt64
|
||||||
|
var mac = "52:cc:cc:cc:cc:01"
|
||||||
|
var ethermac = "52:cc:cc:cc:ce:01"
|
||||||
|
var port: UInt32 = 51009
|
||||||
|
|
||||||
|
// The virtual machines ID. Also double as the directory name under which
|
||||||
|
// we will store configuration, block device, etc.
|
||||||
|
let vmID: String
|
||||||
|
|
||||||
|
required init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
if let ethermac = try container.decodeIfPresent(String.self, forKey: .ethermac) {
|
||||||
|
self.ethermac = ethermac
|
||||||
|
}
|
||||||
|
if let serverSocket = try container.decodeIfPresent(String.self, forKey: .serverSocket) {
|
||||||
|
self.serverSocket = serverSocket
|
||||||
|
}
|
||||||
|
if let memorySize = try container.decodeIfPresent(UInt64.self, forKey: .memorySize) {
|
||||||
|
self.memorySize = memorySize
|
||||||
|
}
|
||||||
|
if let port = try container.decodeIfPresent(UInt32.self, forKey: .port) {
|
||||||
|
self.port = port
|
||||||
|
}
|
||||||
|
if let mac = try container.decodeIfPresent(String.self, forKey: .mac) {
|
||||||
|
self.mac = mac
|
||||||
|
}
|
||||||
|
if let vmID = try container.decodeIfPresent(String.self, forKey: .vmID) {
|
||||||
|
self.vmID = vmID
|
||||||
|
} else {
|
||||||
|
self.vmID = "default"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init(_ vmID: String = "default") {
|
||||||
|
self.vmID = vmID
|
||||||
|
let configFile = vmDataURL.appendingPathComponent("config.json")
|
||||||
|
if FileManager.default.fileExists(atPath: configFile.path()) {
|
||||||
|
print("Using config file at path \(configFile)")
|
||||||
|
if let jsonData = try? Data(contentsOf: configFile) {
|
||||||
|
let config = try! JSONDecoder().decode(Config.self, from: jsonData)
|
||||||
|
self.serverSocket = config.serverSocket
|
||||||
|
self.memorySize = config.memorySize
|
||||||
|
self.mac = config.mac
|
||||||
|
self.port = config.port
|
||||||
|
self.ethermac = config.ethermac
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func persist() {
|
||||||
|
let configFile = vmDataURL.appendingPathComponent("config.json")
|
||||||
|
let data = try! JSONEncoder().encode(self)
|
||||||
|
try! data.write(to: configFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy var restoreImageURL: URL = {
|
||||||
|
vmBundleURL.appendingPathComponent("RestoreImage.ipsw")
|
||||||
|
}()
|
||||||
|
|
||||||
|
// The VM Data URL holds the specific files composing a unique VM guest instance
|
||||||
|
// By default, VM's are persisted at ~/VM.bundle/<vmID>
|
||||||
|
lazy var vmDataURL = {
|
||||||
|
let dataURL = vmBundleURL.appendingPathComponent(vmID)
|
||||||
|
return dataURL
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var auxiliaryStorageURL = {
|
||||||
|
vmDataURL.appendingPathComponent("AuxiliaryStorage")
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var diskImageURL = {
|
||||||
|
vmDataURL.appendingPathComponent("Disk.img")
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var diskSize: Int64 = {
|
||||||
|
kDefaultDiskSizeGb * 1024 * 1024 * 1024
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var hardwareModelURL = {
|
||||||
|
vmDataURL.appendingPathComponent("HardwareModel")
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var machineIdentifierURL = {
|
||||||
|
vmDataURL.appendingPathComponent("MachineIdentifier")
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var saveFileURL = {
|
||||||
|
vmDataURL.appendingPathComponent("SaveFile.vzvmsave")
|
||||||
|
}()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// The VM Bundle URL holds the restore image and a set of VM images
|
||||||
|
// By default, VM's are persisted at ~/VM.bundle
|
||||||
|
var vmBundleURL: URL = {
|
||||||
|
let vmBundlePath = NSHomeDirectory() + "/VM.bundle/"
|
||||||
|
createDir(vmBundlePath)
|
||||||
|
let bundleURL = URL(fileURLWithPath: vmBundlePath)
|
||||||
|
return bundleURL
|
||||||
|
}()
|
||||||
|
|
||||||
|
|
||||||
|
func createDir(_ path: String) {
|
||||||
|
do {
|
||||||
|
try FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: true)
|
||||||
|
} catch {
|
||||||
|
fatalError("Unable to create dir at \(path) \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
12
tstest/tailmac/Swift/Common/Notifications.swift
Normal file
12
tstest/tailmac/Swift/Common/Notifications.swift
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct Notifications {
|
||||||
|
// Stops the virtual machine and saves its state
|
||||||
|
static var stop = Notification.Name("io.tailscale.macvmhost.stop")
|
||||||
|
|
||||||
|
// Pauses the virtual machine and exits without saving its state
|
||||||
|
static var halt = Notification.Name("io.tailscale.macvmhost.halt")
|
||||||
|
}
|
145
tstest/tailmac/Swift/Common/TailMacConfigHelper.swift
Normal file
145
tstest/tailmac/Swift/Common/TailMacConfigHelper.swift
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Virtualization
|
||||||
|
|
||||||
|
struct TailMacConfigHelper {
|
||||||
|
let config: Config
|
||||||
|
|
||||||
|
func computeCPUCount() -> Int {
|
||||||
|
let totalAvailableCPUs = ProcessInfo.processInfo.processorCount
|
||||||
|
|
||||||
|
var virtualCPUCount = totalAvailableCPUs <= 1 ? 1 : totalAvailableCPUs - 1
|
||||||
|
virtualCPUCount = max(virtualCPUCount, VZVirtualMachineConfiguration.minimumAllowedCPUCount)
|
||||||
|
virtualCPUCount = min(virtualCPUCount, VZVirtualMachineConfiguration.maximumAllowedCPUCount)
|
||||||
|
|
||||||
|
return virtualCPUCount
|
||||||
|
}
|
||||||
|
|
||||||
|
func computeMemorySize() -> UInt64 {
|
||||||
|
// Set the amount of system memory to 4 GB; this is a baseline value
|
||||||
|
// that you can change depending on your use case.
|
||||||
|
var memorySize = config.memorySize
|
||||||
|
memorySize = max(memorySize, VZVirtualMachineConfiguration.minimumAllowedMemorySize)
|
||||||
|
memorySize = min(memorySize, VZVirtualMachineConfiguration.maximumAllowedMemorySize)
|
||||||
|
|
||||||
|
return memorySize
|
||||||
|
}
|
||||||
|
|
||||||
|
func createBootLoader() -> VZMacOSBootLoader {
|
||||||
|
return VZMacOSBootLoader()
|
||||||
|
}
|
||||||
|
|
||||||
|
func createGraphicsDeviceConfiguration() -> VZMacGraphicsDeviceConfiguration {
|
||||||
|
let graphicsConfiguration = VZMacGraphicsDeviceConfiguration()
|
||||||
|
graphicsConfiguration.displays = [
|
||||||
|
// The system arbitrarily chooses the resolution of the display to be 1920 x 1200.
|
||||||
|
VZMacGraphicsDisplayConfiguration(widthInPixels: 1920, heightInPixels: 1200, pixelsPerInch: 80)
|
||||||
|
]
|
||||||
|
|
||||||
|
return graphicsConfiguration
|
||||||
|
}
|
||||||
|
|
||||||
|
func createBlockDeviceConfiguration() -> VZVirtioBlockDeviceConfiguration {
|
||||||
|
do {
|
||||||
|
let diskImageAttachment = try VZDiskImageStorageDeviceAttachment(url: config.diskImageURL, readOnly: false)
|
||||||
|
let disk = VZVirtioBlockDeviceConfiguration(attachment: diskImageAttachment)
|
||||||
|
return disk
|
||||||
|
} catch {
|
||||||
|
fatalError("Failed to create Disk image. \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createSocketDeviceConfiguration() -> VZVirtioSocketDeviceConfiguration {
|
||||||
|
return VZVirtioSocketDeviceConfiguration()
|
||||||
|
}
|
||||||
|
|
||||||
|
func createNetworkDeviceConfiguration() -> VZVirtioNetworkDeviceConfiguration {
|
||||||
|
let networkDevice = VZVirtioNetworkDeviceConfiguration()
|
||||||
|
networkDevice.macAddress = VZMACAddress(string: config.ethermac)!
|
||||||
|
|
||||||
|
/* Bridged networking requires special entitlements from Apple
|
||||||
|
if let interface = VZBridgedNetworkInterface.networkInterfaces.first(where: { $0.identifier == "en0" }) {
|
||||||
|
let networkAttachment = VZBridgedNetworkDeviceAttachment(interface: interface)
|
||||||
|
networkDevice.attachment = networkAttachment
|
||||||
|
} else {
|
||||||
|
print("Assuming en0 for bridged ethernet. Could not findd adapter")
|
||||||
|
}*/
|
||||||
|
|
||||||
|
/// But we can do NAT without Tim Apple's approval
|
||||||
|
let networkAttachment = VZNATNetworkDeviceAttachment()
|
||||||
|
networkDevice.attachment = networkAttachment
|
||||||
|
|
||||||
|
return networkDevice
|
||||||
|
}
|
||||||
|
|
||||||
|
func createSocketNetworkDeviceConfiguration() -> VZVirtioNetworkDeviceConfiguration {
|
||||||
|
let networkDevice = VZVirtioNetworkDeviceConfiguration()
|
||||||
|
networkDevice.macAddress = VZMACAddress(string: config.mac)!
|
||||||
|
|
||||||
|
let socket = Darwin.socket(AF_UNIX, SOCK_DGRAM, 0)
|
||||||
|
|
||||||
|
// Outbound network packets
|
||||||
|
let serverSocket = config.serverSocket
|
||||||
|
|
||||||
|
// Inbound network packets
|
||||||
|
let clientSockId = config.vmID
|
||||||
|
let clientSocket = "/tmp/qemu-dgram-\(clientSockId).sock"
|
||||||
|
|
||||||
|
unlink(clientSocket)
|
||||||
|
var clientAddr = sockaddr_un()
|
||||||
|
clientAddr.sun_family = sa_family_t(AF_UNIX)
|
||||||
|
clientSocket.withCString { ptr in
|
||||||
|
withUnsafeMutablePointer(to: &clientAddr.sun_path.0) { dest in
|
||||||
|
_ = strcpy(dest, ptr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let bindRes = Darwin.bind(socket,
|
||||||
|
withUnsafePointer(to: &clientAddr, { $0.withMemoryRebound(to: sockaddr.self, capacity: 1) { $0 } }),
|
||||||
|
socklen_t(MemoryLayout<sockaddr_un>.size))
|
||||||
|
|
||||||
|
if bindRes == -1 {
|
||||||
|
print("Error binding virtual network client socket - \(String(cString: strerror(errno)))")
|
||||||
|
return networkDevice
|
||||||
|
}
|
||||||
|
|
||||||
|
var serverAddr = sockaddr_un()
|
||||||
|
serverAddr.sun_family = sa_family_t(AF_UNIX)
|
||||||
|
serverSocket.withCString { ptr in
|
||||||
|
withUnsafeMutablePointer(to: &serverAddr.sun_path.0) { dest in
|
||||||
|
_ = strcpy(dest, ptr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let connectRes = Darwin.connect(socket,
|
||||||
|
withUnsafePointer(to: &serverAddr, { $0.withMemoryRebound(to: sockaddr.self, capacity: 1) { $0 } }),
|
||||||
|
socklen_t(MemoryLayout<sockaddr_un>.size))
|
||||||
|
|
||||||
|
if connectRes == -1 {
|
||||||
|
print("Error binding virtual network server socket - \(String(cString: strerror(errno)))")
|
||||||
|
return networkDevice
|
||||||
|
}
|
||||||
|
|
||||||
|
print("Virtual if mac address is \(config.mac)")
|
||||||
|
print("Client bound to \(clientSocket)")
|
||||||
|
print("Connected to server at \(serverSocket)")
|
||||||
|
print("Socket fd is \(socket)")
|
||||||
|
|
||||||
|
|
||||||
|
let handle = FileHandle(fileDescriptor: socket)
|
||||||
|
let device = VZFileHandleNetworkDeviceAttachment(fileHandle: handle)
|
||||||
|
networkDevice.attachment = device
|
||||||
|
return networkDevice
|
||||||
|
}
|
||||||
|
|
||||||
|
func createPointingDeviceConfiguration() -> VZPointingDeviceConfiguration {
|
||||||
|
return VZMacTrackpadConfiguration()
|
||||||
|
}
|
||||||
|
|
||||||
|
func createKeyboardConfiguration() -> VZKeyboardConfiguration {
|
||||||
|
return VZMacKeyboardConfiguration()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
52
tstest/tailmac/Swift/Host/AppDelegate.swift
Normal file
52
tstest/tailmac/Swift/Host/AppDelegate.swift
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
import Cocoa
|
||||||
|
import Foundation
|
||||||
|
import Virtualization
|
||||||
|
|
||||||
|
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
|
@IBOutlet var window: NSWindow!
|
||||||
|
|
||||||
|
@IBOutlet weak var virtualMachineView: VZVirtualMachineView!
|
||||||
|
|
||||||
|
var runner: VMController!
|
||||||
|
|
||||||
|
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
||||||
|
DispatchQueue.main.async { [self] in
|
||||||
|
runner = VMController()
|
||||||
|
runner.createVirtualMachine()
|
||||||
|
virtualMachineView.virtualMachine = runner.virtualMachine
|
||||||
|
virtualMachineView.capturesSystemKeys = true
|
||||||
|
|
||||||
|
// Configure the app to automatically respond to changes in the display size.
|
||||||
|
virtualMachineView.automaticallyReconfiguresDisplay = true
|
||||||
|
|
||||||
|
let fileManager = FileManager.default
|
||||||
|
if fileManager.fileExists(atPath: config.saveFileURL.path) {
|
||||||
|
print("Restoring virtual machine state from \(config.saveFileURL)")
|
||||||
|
runner.restoreVirtualMachine()
|
||||||
|
} else {
|
||||||
|
print("Restarting virtual machine")
|
||||||
|
runner.startVirtualMachine()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
|
||||||
|
if runner.virtualMachine.state == .running {
|
||||||
|
runner.pauseAndSaveVirtualMachine(completionHandler: {
|
||||||
|
sender.reply(toApplicationShouldTerminate: true)
|
||||||
|
})
|
||||||
|
|
||||||
|
return .terminateLater
|
||||||
|
}
|
||||||
|
|
||||||
|
return .terminateNow
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "16x16"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "16x16"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "32x32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "32x32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "128x128"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "128x128"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "256x256"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "256x256"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "512x512"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "512x512"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
6
tstest/tailmac/Swift/Host/Assets.xcassets/Contents.json
Normal file
6
tstest/tailmac/Swift/Host/Assets.xcassets/Contents.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
696
tstest/tailmac/Swift/Host/Base.lproj/MainMenu.xib
Normal file
696
tstest/tailmac/Swift/Host/Base.lproj/MainMenu.xib
Normal file
@ -0,0 +1,696 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||||
|
<dependencies>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22690"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<objects>
|
||||||
|
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
|
||||||
|
<connections>
|
||||||
|
<outlet property="delegate" destination="Voe-Tx-rLC" id="GzC-gU-4Uq"/>
|
||||||
|
</connections>
|
||||||
|
</customObject>
|
||||||
|
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||||
|
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||||
|
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="vnetMacHost" customModuleProvider="target">
|
||||||
|
<connections>
|
||||||
|
<outlet property="virtualMachineView" destination="EiT-Mj-1SZ" id="KBI-Ak-yeW"/>
|
||||||
|
<outlet property="window" destination="QvC-M9-y7g" id="gIp-Ho-8D9"/>
|
||||||
|
</connections>
|
||||||
|
</customObject>
|
||||||
|
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
|
||||||
|
<menu title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
|
||||||
|
<items>
|
||||||
|
<menuItem title="TailMac" id="1Xt-HY-uBw">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="TailMac" systemMenu="apple" id="uQy-DD-JDr">
|
||||||
|
<items>
|
||||||
|
<menuItem title="About TailMac" id="5kV-Vb-QxS">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="orderFrontStandardAboutPanel:" target="-1" id="Exp-CZ-Vem"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
|
||||||
|
<menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/>
|
||||||
|
<menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
|
||||||
|
<menuItem title="Services" id="NMo-om-nkz">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
|
||||||
|
<menuItem title="Hide TailMac" keyEquivalent="h" id="Olw-nP-bQN">
|
||||||
|
<connections>
|
||||||
|
<action selector="hide:" target="-1" id="PnN-Uc-m68"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="hideOtherApplications:" target="-1" id="VT4-aY-XCT"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Show All" id="Kd2-mp-pUS">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="unhideAllApplications:" target="-1" id="Dhg-Le-xox"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
|
||||||
|
<menuItem title="Save and quit TailMac" keyEquivalent="q" id="4sb-4s-VLi">
|
||||||
|
<connections>
|
||||||
|
<action selector="terminate:" target="-1" id="Te7-pn-YzF"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="File" id="dMs-cI-mzQ">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="File" id="bib-Uj-vzu">
|
||||||
|
<items>
|
||||||
|
<menuItem title="New" keyEquivalent="n" id="Was-JA-tGl">
|
||||||
|
<connections>
|
||||||
|
<action selector="newDocument:" target="-1" id="4Si-XN-c54"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Open…" keyEquivalent="o" id="IAo-SY-fd9">
|
||||||
|
<connections>
|
||||||
|
<action selector="openDocument:" target="-1" id="bVn-NM-KNZ"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Open Recent" id="tXI-mr-wws">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Open Recent" systemMenu="recentDocuments" id="oas-Oc-fiZ">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Clear Menu" id="vNY-rz-j42">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="clearRecentDocuments:" target="-1" id="Daa-9d-B3U"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="m54-Is-iLE"/>
|
||||||
|
<menuItem title="Close" keyEquivalent="w" id="DVo-aG-piG">
|
||||||
|
<connections>
|
||||||
|
<action selector="performClose:" target="-1" id="HmO-Ls-i7Q"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Save…" keyEquivalent="s" id="pxx-59-PXV">
|
||||||
|
<connections>
|
||||||
|
<action selector="saveDocument:" target="-1" id="teZ-XB-qJY"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Save As…" keyEquivalent="S" id="Bw7-FT-i3A">
|
||||||
|
<connections>
|
||||||
|
<action selector="saveDocumentAs:" target="-1" id="mDf-zr-I0C"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Revert to Saved" keyEquivalent="r" id="KaW-ft-85H">
|
||||||
|
<connections>
|
||||||
|
<action selector="revertDocumentToSaved:" target="-1" id="iJ3-Pv-kwq"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="aJh-i4-bef"/>
|
||||||
|
<menuItem title="Page Setup…" keyEquivalent="P" id="qIS-W8-SiK">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="runPageLayout:" target="-1" id="Din-rz-gC5"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Print…" keyEquivalent="p" id="aTl-1u-JFS">
|
||||||
|
<connections>
|
||||||
|
<action selector="print:" target="-1" id="qaZ-4w-aoO"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Edit" id="5QF-Oa-p0T">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Edit" id="W48-6f-4Dl">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg">
|
||||||
|
<connections>
|
||||||
|
<action selector="undo:" target="-1" id="M6e-cu-g7V"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam">
|
||||||
|
<connections>
|
||||||
|
<action selector="redo:" target="-1" id="oIA-Rs-6OD"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/>
|
||||||
|
<menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG">
|
||||||
|
<connections>
|
||||||
|
<action selector="cut:" target="-1" id="YJe-68-I9s"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU">
|
||||||
|
<connections>
|
||||||
|
<action selector="copy:" target="-1" id="G1f-GL-Joy"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL">
|
||||||
|
<connections>
|
||||||
|
<action selector="paste:" target="-1" id="UvS-8e-Qdg"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="pasteAsPlainText:" target="-1" id="cEh-KX-wJQ"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Delete" id="pa3-QI-u2k">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="delete:" target="-1" id="0Mk-Ml-PaM"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m">
|
||||||
|
<connections>
|
||||||
|
<action selector="selectAll:" target="-1" id="VNm-Mi-diN"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/>
|
||||||
|
<menuItem title="Find" id="4EN-yA-p0u">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Find" id="1b7-l0-nxx">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Find…" tag="1" keyEquivalent="f" id="Xz5-n4-O0W">
|
||||||
|
<connections>
|
||||||
|
<action selector="performFindPanelAction:" target="-1" id="cD7-Qs-BN4"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="YEy-JH-Tfz">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="performFindPanelAction:" target="-1" id="WD3-Gg-5AJ"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Find Next" tag="2" keyEquivalent="g" id="q09-fT-Sye">
|
||||||
|
<connections>
|
||||||
|
<action selector="performFindPanelAction:" target="-1" id="NDo-RZ-v9R"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Find Previous" tag="3" keyEquivalent="G" id="OwM-mh-QMV">
|
||||||
|
<connections>
|
||||||
|
<action selector="performFindPanelAction:" target="-1" id="HOh-sY-3ay"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="buJ-ug-pKt">
|
||||||
|
<connections>
|
||||||
|
<action selector="performFindPanelAction:" target="-1" id="U76-nv-p5D"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Jump to Selection" keyEquivalent="j" id="S0p-oC-mLd">
|
||||||
|
<connections>
|
||||||
|
<action selector="centerSelectionInVisibleArea:" target="-1" id="IOG-6D-g5B"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Spelling and Grammar" id="Dv1-io-Yv7">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Spelling" id="3IN-sU-3Bg">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="HFo-cy-zxI">
|
||||||
|
<connections>
|
||||||
|
<action selector="showGuessPanel:" target="-1" id="vFj-Ks-hy3"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Check Document Now" keyEquivalent=";" id="hz2-CU-CR7">
|
||||||
|
<connections>
|
||||||
|
<action selector="checkSpelling:" target="-1" id="fz7-VC-reM"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="bNw-od-mp5"/>
|
||||||
|
<menuItem title="Check Spelling While Typing" id="rbD-Rh-wIN">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleContinuousSpellChecking:" target="-1" id="7w6-Qz-0kB"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Check Grammar With Spelling" id="mK6-2p-4JG">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleGrammarChecking:" target="-1" id="muD-Qn-j4w"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Correct Spelling Automatically" id="78Y-hA-62v">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleAutomaticSpellingCorrection:" target="-1" id="2lM-Qi-WAP"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Substitutions" id="9ic-FL-obx">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Substitutions" id="FeM-D8-WVr">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Show Substitutions" id="z6F-FW-3nz">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="orderFrontSubstitutionsPanel:" target="-1" id="oku-mr-iSq"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="gPx-C9-uUO"/>
|
||||||
|
<menuItem title="Smart Copy/Paste" id="9yt-4B-nSM">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleSmartInsertDelete:" target="-1" id="3IJ-Se-DZD"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Smart Quotes" id="hQb-2v-fYv">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleAutomaticQuoteSubstitution:" target="-1" id="ptq-xd-QOA"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Smart Dashes" id="rgM-f4-ycn">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleAutomaticDashSubstitution:" target="-1" id="oCt-pO-9gS"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Smart Links" id="cwL-P1-jid">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleAutomaticLinkDetection:" target="-1" id="Gip-E3-Fov"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Data Detectors" id="tRr-pd-1PS">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleAutomaticDataDetection:" target="-1" id="R1I-Nq-Kbl"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Text Replacement" id="HFQ-gK-NFA">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleAutomaticTextReplacement:" target="-1" id="DvP-Fe-Py6"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Transformations" id="2oI-Rn-ZJC">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Transformations" id="c8a-y6-VQd">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Make Upper Case" id="vmV-6d-7jI">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="uppercaseWord:" target="-1" id="sPh-Tk-edu"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Make Lower Case" id="d9M-CD-aMd">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="lowercaseWord:" target="-1" id="iUZ-b5-hil"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Capitalize" id="UEZ-Bs-lqG">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="capitalizeWord:" target="-1" id="26H-TL-nsh"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Speech" id="xrE-MZ-jX0">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Speech" id="3rS-ZA-NoH">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Start Speaking" id="Ynk-f8-cLZ">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="startSpeaking:" target="-1" id="654-Ng-kyl"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Stop Speaking" id="Oyz-dy-DGm">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="stopSpeaking:" target="-1" id="dX8-6p-jy9"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Format" id="jxT-CU-nIS">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Format" id="GEO-Iw-cKr">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Font" id="Gi5-1S-RQB">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Font" systemMenu="font" id="aXa-aM-Jaq">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Show Fonts" keyEquivalent="t" id="Q5e-8K-NDq">
|
||||||
|
<connections>
|
||||||
|
<action selector="orderFrontFontPanel:" target="YLy-65-1bz" id="WHr-nq-2xA"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Bold" tag="2" keyEquivalent="b" id="GB9-OM-e27">
|
||||||
|
<connections>
|
||||||
|
<action selector="addFontTrait:" target="YLy-65-1bz" id="hqk-hr-sYV"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Italic" tag="1" keyEquivalent="i" id="Vjx-xi-njq">
|
||||||
|
<connections>
|
||||||
|
<action selector="addFontTrait:" target="YLy-65-1bz" id="IHV-OB-c03"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Underline" keyEquivalent="u" id="WRG-CD-K1S">
|
||||||
|
<connections>
|
||||||
|
<action selector="underline:" target="-1" id="FYS-2b-JAY"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="5gT-KC-WSO"/>
|
||||||
|
<menuItem title="Bigger" tag="3" keyEquivalent="+" id="Ptp-SP-VEL">
|
||||||
|
<connections>
|
||||||
|
<action selector="modifyFont:" target="YLy-65-1bz" id="Uc7-di-UnL"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Smaller" tag="4" keyEquivalent="-" id="i1d-Er-qST">
|
||||||
|
<connections>
|
||||||
|
<action selector="modifyFont:" target="YLy-65-1bz" id="HcX-Lf-eNd"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="kx3-Dk-x3B"/>
|
||||||
|
<menuItem title="Kern" id="jBQ-r6-VK2">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Kern" id="tlD-Oa-oAM">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Use Default" id="GUa-eO-cwY">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="useStandardKerning:" target="-1" id="6dk-9l-Ckg"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Use None" id="cDB-IK-hbR">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="turnOffKerning:" target="-1" id="U8a-gz-Maa"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Tighten" id="46P-cB-AYj">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="tightenKerning:" target="-1" id="hr7-Nz-8ro"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Loosen" id="ogc-rX-tC1">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="loosenKerning:" target="-1" id="8i4-f9-FKE"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Ligatures" id="o6e-r0-MWq">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Ligatures" id="w0m-vy-SC9">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Use Default" id="agt-UL-0e3">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="useStandardLigatures:" target="-1" id="7uR-wd-Dx6"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Use None" id="J7y-lM-qPV">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="turnOffLigatures:" target="-1" id="iX2-gA-Ilz"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Use All" id="xQD-1f-W4t">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="useAllLigatures:" target="-1" id="KcB-kA-TuK"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Baseline" id="OaQ-X3-Vso">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Baseline" id="ijk-EB-dga">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Use Default" id="3Om-Ey-2VK">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="unscript:" target="-1" id="0vZ-95-Ywn"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Superscript" id="Rqc-34-cIF">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="superscript:" target="-1" id="3qV-fo-wpU"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Subscript" id="I0S-gh-46l">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="subscript:" target="-1" id="Q6W-4W-IGz"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Raise" id="2h7-ER-AoG">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="raiseBaseline:" target="-1" id="4sk-31-7Q9"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Lower" id="1tx-W0-xDw">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="lowerBaseline:" target="-1" id="OF1-bc-KW4"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="Ndw-q3-faq"/>
|
||||||
|
<menuItem title="Show Colors" keyEquivalent="C" id="bgn-CT-cEk">
|
||||||
|
<connections>
|
||||||
|
<action selector="orderFrontColorPanel:" target="-1" id="mSX-Xz-DV3"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="iMs-zA-UFJ"/>
|
||||||
|
<menuItem title="Copy Style" keyEquivalent="c" id="5Vv-lz-BsD">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="copyFont:" target="-1" id="GJO-xA-L4q"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Paste Style" keyEquivalent="v" id="vKC-jM-MkH">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="pasteFont:" target="-1" id="JfD-CL-leO"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Text" id="Fal-I4-PZk">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Text" id="d9c-me-L2H">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Align Left" keyEquivalent="{" id="ZM1-6Q-yy1">
|
||||||
|
<connections>
|
||||||
|
<action selector="alignLeft:" target="-1" id="zUv-R1-uAa"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Center" keyEquivalent="|" id="VIY-Ag-zcb">
|
||||||
|
<connections>
|
||||||
|
<action selector="alignCenter:" target="-1" id="spX-mk-kcS"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Justify" id="J5U-5w-g23">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="alignJustified:" target="-1" id="ljL-7U-jND"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Align Right" keyEquivalent="}" id="wb2-vD-lq4">
|
||||||
|
<connections>
|
||||||
|
<action selector="alignRight:" target="-1" id="r48-bG-YeY"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="4s2-GY-VfK"/>
|
||||||
|
<menuItem title="Writing Direction" id="H1b-Si-o9J">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Writing Direction" id="8mr-sm-Yjd">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Paragraph" enabled="NO" id="ZvO-Gk-QUH">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem id="YGs-j5-SAR">
|
||||||
|
<string key="title"> Default</string>
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="makeBaseWritingDirectionNatural:" target="-1" id="qtV-5e-UBP"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem id="Lbh-J2-qVU">
|
||||||
|
<string key="title"> Left to Right</string>
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="makeBaseWritingDirectionLeftToRight:" target="-1" id="S0X-9S-QSf"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem id="jFq-tB-4Kx">
|
||||||
|
<string key="title"> Right to Left</string>
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="makeBaseWritingDirectionRightToLeft:" target="-1" id="5fk-qB-AqJ"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="swp-gr-a21"/>
|
||||||
|
<menuItem title="Selection" enabled="NO" id="cqv-fj-IhA">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem id="Nop-cj-93Q">
|
||||||
|
<string key="title"> Default</string>
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="makeTextWritingDirectionNatural:" target="-1" id="lPI-Se-ZHp"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem id="BgM-ve-c93">
|
||||||
|
<string key="title"> Left to Right</string>
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="makeTextWritingDirectionLeftToRight:" target="-1" id="caW-Bv-w94"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem id="RB4-Sm-HuC">
|
||||||
|
<string key="title"> Right to Left</string>
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="makeTextWritingDirectionRightToLeft:" target="-1" id="EXD-6r-ZUu"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="fKy-g9-1gm"/>
|
||||||
|
<menuItem title="Show Ruler" id="vLm-3I-IUL">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleRuler:" target="-1" id="FOx-HJ-KwY"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Copy Ruler" keyEquivalent="c" id="MkV-Pr-PK5">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="copyRuler:" target="-1" id="71i-fW-3W2"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Paste Ruler" keyEquivalent="v" id="LVM-kO-fVI">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="pasteRuler:" target="-1" id="cSh-wd-qM2"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="View" id="H8h-7b-M4v">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="View" id="HyV-fh-RgO">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Show Toolbar" keyEquivalent="t" id="snW-S8-Cw5">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleToolbarShown:" target="-1" id="BXY-wc-z0C"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Customize Toolbar…" id="1UK-8n-QPP">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="runToolbarCustomizationPalette:" target="-1" id="pQI-g3-MTW"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="hB3-LF-h0Y"/>
|
||||||
|
<menuItem title="Show Sidebar" keyEquivalent="s" id="kIP-vf-haE">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleSidebar:" target="-1" id="iwa-gc-5KM"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Enter Full Screen" keyEquivalent="f" id="4J7-dP-txa">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="toggleFullScreen:" target="-1" id="dU3-MA-1Rq"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Window" id="aUF-d1-5bR">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo">
|
||||||
|
<items>
|
||||||
|
<menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV">
|
||||||
|
<connections>
|
||||||
|
<action selector="performMiniaturize:" target="-1" id="VwT-WD-YPe"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Zoom" id="R4o-n2-Eq4">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="performZoom:" target="-1" id="DIl-cC-cCs"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/>
|
||||||
|
<menuItem title="Bring All to Front" id="LE2-aR-0XJ">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="arrangeInFront:" target="-1" id="DRN-fu-gQh"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Help" id="wpr-3q-Mcd">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<menu key="submenu" title="Help" systemMenu="help" id="F2S-fz-NVQ">
|
||||||
|
<items>
|
||||||
|
<menuItem title="TailMac Help" keyEquivalent="?" id="FKE-Sm-Kum">
|
||||||
|
<connections>
|
||||||
|
<action selector="showHelp:" target="-1" id="y7X-2Q-9no"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
</menu>
|
||||||
|
</menuItem>
|
||||||
|
</items>
|
||||||
|
<point key="canvasLocation" x="200" y="121"/>
|
||||||
|
</menu>
|
||||||
|
<window title="TailMac" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" id="QvC-M9-y7g">
|
||||||
|
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
|
||||||
|
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||||
|
<rect key="contentRect" x="335" y="390" width="960" height="600"/>
|
||||||
|
<rect key="screenRect" x="0.0" y="0.0" width="3840" height="2135"/>
|
||||||
|
<view key="contentView" id="EiT-Mj-1SZ" customClass="VZVirtualMachineView">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="960" height="600"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
</view>
|
||||||
|
<point key="canvasLocation" x="200" y="400"/>
|
||||||
|
</window>
|
||||||
|
</objects>
|
||||||
|
</document>
|
30
tstest/tailmac/Swift/Host/HostCli.swift
Normal file
30
tstest/tailmac/Swift/Host/HostCli.swift
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
import Cocoa
|
||||||
|
import Foundation
|
||||||
|
import Virtualization
|
||||||
|
import ArgumentParser
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct HostCli: ParsableCommand {
|
||||||
|
static var configuration = CommandConfiguration(
|
||||||
|
abstract: "A utility for running virtual machines",
|
||||||
|
subcommands: [Run.self],
|
||||||
|
defaultSubcommand: Run.self)
|
||||||
|
}
|
||||||
|
|
||||||
|
var config: Config = Config()
|
||||||
|
|
||||||
|
extension HostCli {
|
||||||
|
struct Run: ParsableCommand {
|
||||||
|
@Option var id: String
|
||||||
|
|
||||||
|
mutating func run() {
|
||||||
|
print("Running vm with identifier \(id)")
|
||||||
|
config = Config(id)
|
||||||
|
_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
5
tstest/tailmac/Swift/Host/Info.plist
Normal file
5
tstest/tailmac/Swift/Host/Info.plist
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict/>
|
||||||
|
</plist>
|
179
tstest/tailmac/Swift/Host/VMController.swift
Normal file
179
tstest/tailmac/Swift/Host/VMController.swift
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
import Cocoa
|
||||||
|
import Foundation
|
||||||
|
import Virtualization
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class VMController: NSObject, VZVirtualMachineDelegate {
|
||||||
|
var virtualMachine: VZVirtualMachine!
|
||||||
|
|
||||||
|
lazy var helper = TailMacConfigHelper(config: config)
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
super.init()
|
||||||
|
listenForNotifications()
|
||||||
|
}
|
||||||
|
|
||||||
|
func listenForNotifications() {
|
||||||
|
let nc = DistributedNotificationCenter()
|
||||||
|
nc.addObserver(forName: Notifications.stop, object: nil, queue: nil) { notification in
|
||||||
|
if let vmID = notification.userInfo?["id"] as? String {
|
||||||
|
if config.vmID == vmID {
|
||||||
|
print("We've been asked to stop... Saving state and exiting")
|
||||||
|
self.pauseAndSaveVirtualMachine {
|
||||||
|
exit(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nc.addObserver(forName: Notifications.halt, object: nil, queue: nil) { notification in
|
||||||
|
if let vmID = notification.userInfo?["id"] as? String {
|
||||||
|
if config.vmID == vmID {
|
||||||
|
print("We've been asked to stop... Saving state and exiting")
|
||||||
|
self.virtualMachine.pause { (result) in
|
||||||
|
if case let .failure(error) = result {
|
||||||
|
fatalError("Virtual machine failed to pause with \(error)")
|
||||||
|
}
|
||||||
|
exit(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createMacPlaform() -> VZMacPlatformConfiguration {
|
||||||
|
let macPlatform = VZMacPlatformConfiguration()
|
||||||
|
|
||||||
|
let auxiliaryStorage = VZMacAuxiliaryStorage(contentsOf: config.auxiliaryStorageURL)
|
||||||
|
macPlatform.auxiliaryStorage = auxiliaryStorage
|
||||||
|
|
||||||
|
if !FileManager.default.fileExists(atPath: config.vmDataURL.path()) {
|
||||||
|
fatalError("Missing Virtual Machine Bundle at \(config.vmDataURL). Run InstallationTool first to create it.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve the hardware model and save this value to disk during installation.
|
||||||
|
guard let hardwareModelData = try? Data(contentsOf: config.hardwareModelURL) else {
|
||||||
|
fatalError("Failed to retrieve hardware model data.")
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let hardwareModel = VZMacHardwareModel(dataRepresentation: hardwareModelData) else {
|
||||||
|
fatalError("Failed to create hardware model.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hardwareModel.isSupported {
|
||||||
|
fatalError("The hardware model isn't supported on the current host")
|
||||||
|
}
|
||||||
|
macPlatform.hardwareModel = hardwareModel
|
||||||
|
|
||||||
|
// Retrieve the machine identifier and save this value to disk during installation.
|
||||||
|
guard let machineIdentifierData = try? Data(contentsOf: config.machineIdentifierURL) else {
|
||||||
|
fatalError("Failed to retrieve machine identifier data.")
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let machineIdentifier = VZMacMachineIdentifier(dataRepresentation: machineIdentifierData) else {
|
||||||
|
fatalError("Failed to create machine identifier.")
|
||||||
|
}
|
||||||
|
macPlatform.machineIdentifier = machineIdentifier
|
||||||
|
|
||||||
|
return macPlatform
|
||||||
|
}
|
||||||
|
|
||||||
|
func createVirtualMachine() {
|
||||||
|
let virtualMachineConfiguration = VZVirtualMachineConfiguration()
|
||||||
|
|
||||||
|
virtualMachineConfiguration.platform = createMacPlaform()
|
||||||
|
virtualMachineConfiguration.bootLoader = helper.createBootLoader()
|
||||||
|
virtualMachineConfiguration.cpuCount = helper.computeCPUCount()
|
||||||
|
virtualMachineConfiguration.memorySize = helper.computeMemorySize()
|
||||||
|
virtualMachineConfiguration.graphicsDevices = [helper.createGraphicsDeviceConfiguration()]
|
||||||
|
virtualMachineConfiguration.storageDevices = [helper.createBlockDeviceConfiguration()]
|
||||||
|
virtualMachineConfiguration.networkDevices = [helper.createNetworkDeviceConfiguration(), helper.createSocketNetworkDeviceConfiguration()]
|
||||||
|
virtualMachineConfiguration.pointingDevices = [helper.createPointingDeviceConfiguration()]
|
||||||
|
virtualMachineConfiguration.keyboards = [helper.createKeyboardConfiguration()]
|
||||||
|
virtualMachineConfiguration.socketDevices = [helper.createSocketDeviceConfiguration()]
|
||||||
|
|
||||||
|
try! virtualMachineConfiguration.validate()
|
||||||
|
try! virtualMachineConfiguration.validateSaveRestoreSupport()
|
||||||
|
|
||||||
|
virtualMachine = VZVirtualMachine(configuration: virtualMachineConfiguration)
|
||||||
|
virtualMachine.delegate = self
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func startVirtualMachine() {
|
||||||
|
virtualMachine.start(completionHandler: { (result) in
|
||||||
|
if case let .failure(error) = result {
|
||||||
|
fatalError("Virtual machine failed to start with \(error)")
|
||||||
|
}
|
||||||
|
self.startSocketDevice()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func startSocketDevice() {
|
||||||
|
if let device = virtualMachine.socketDevices.first as? VZVirtioSocketDevice {
|
||||||
|
print("Configuring socket device at port \(config.port)")
|
||||||
|
device.connect(toPort: config.port) { connection in
|
||||||
|
//TODO: Anything? Or is this enough to bootstrap it on both ends?
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
print("Virtual machine could not start it's socket device")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resumeVirtualMachine() {
|
||||||
|
virtualMachine.resume(completionHandler: { (result) in
|
||||||
|
if case let .failure(error) = result {
|
||||||
|
fatalError("Virtual machine failed to resume with \(error)")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func restoreVirtualMachine() {
|
||||||
|
virtualMachine.restoreMachineStateFrom(url: config.saveFileURL, completionHandler: { [self] (error) in
|
||||||
|
// Remove the saved file. Whether success or failure, the state no longer matches the VM's disk.
|
||||||
|
let fileManager = FileManager.default
|
||||||
|
try! fileManager.removeItem(at: config.saveFileURL)
|
||||||
|
|
||||||
|
if error == nil {
|
||||||
|
self.resumeVirtualMachine()
|
||||||
|
} else {
|
||||||
|
self.startVirtualMachine()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveVirtualMachine(completionHandler: @escaping () -> Void) {
|
||||||
|
virtualMachine.saveMachineStateTo(url: config.saveFileURL, completionHandler: { (error) in
|
||||||
|
guard error == nil else {
|
||||||
|
fatalError("Virtual machine failed to save with \(error!)")
|
||||||
|
}
|
||||||
|
|
||||||
|
completionHandler()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func pauseAndSaveVirtualMachine(completionHandler: @escaping () -> Void) {
|
||||||
|
virtualMachine.pause { result in
|
||||||
|
if case let .failure(error) = result {
|
||||||
|
fatalError("Virtual machine failed to pause with \(error)")
|
||||||
|
}
|
||||||
|
|
||||||
|
self.saveVirtualMachine(completionHandler: completionHandler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - VZVirtualMachineDeleate
|
||||||
|
|
||||||
|
func virtualMachine(_ virtualMachine: VZVirtualMachine, didStopWithError error: Error) {
|
||||||
|
print("Virtual machine did stop with error: \(error.localizedDescription)")
|
||||||
|
exit(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func guestDidStop(_ virtualMachine: VZVirtualMachine) {
|
||||||
|
print("Guest did stop virtual machine.")
|
||||||
|
exit(0)
|
||||||
|
}
|
||||||
|
}
|
58
tstest/tailmac/Swift/TailMac/RestoreImage.swift
Normal file
58
tstest/tailmac/Swift/TailMac/RestoreImage.swift
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Virtualization
|
||||||
|
|
||||||
|
class RestoreImage: NSObject {
|
||||||
|
private var downloadObserver: NSKeyValueObservation?
|
||||||
|
|
||||||
|
// MARK: Observe the download progress.
|
||||||
|
|
||||||
|
var restoreImageURL: URL
|
||||||
|
|
||||||
|
init(_ dest: URL) {
|
||||||
|
restoreImageURL = dest
|
||||||
|
}
|
||||||
|
|
||||||
|
public func download(completionHandler: @escaping () -> Void) {
|
||||||
|
print("Attempting to download latest available restore image.")
|
||||||
|
VZMacOSRestoreImage.fetchLatestSupported { [self](result: Result<VZMacOSRestoreImage, Error>) in
|
||||||
|
switch result {
|
||||||
|
case let .failure(error):
|
||||||
|
fatalError(error.localizedDescription)
|
||||||
|
|
||||||
|
case let .success(restoreImage):
|
||||||
|
downloadRestoreImage(restoreImage: restoreImage, completionHandler: completionHandler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func downloadRestoreImage(restoreImage: VZMacOSRestoreImage, completionHandler: @escaping () -> Void) {
|
||||||
|
let downloadTask = URLSession.shared.downloadTask(with: restoreImage.url) { localURL, response, error in
|
||||||
|
if let error = error {
|
||||||
|
fatalError("Download failed. \(error.localizedDescription).")
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
try FileManager.default.moveItem(at: localURL!, to: self.restoreImageURL)
|
||||||
|
} catch {
|
||||||
|
fatalError("Failed to move downloaded restore image to \(self.restoreImageURL) \(error).")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
completionHandler()
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastPct = 0
|
||||||
|
downloadObserver = downloadTask.progress.observe(\.fractionCompleted, options: [.initial, .new]) { (progress, change) in
|
||||||
|
let pct = Int(change.newValue! * 100)
|
||||||
|
if pct != lastPct {
|
||||||
|
print("Restore image download progress: \(pct)%")
|
||||||
|
lastPct = pct
|
||||||
|
}
|
||||||
|
}
|
||||||
|
downloadTask.resume()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
334
tstest/tailmac/Swift/TailMac/TailMac.swift
Normal file
334
tstest/tailmac/Swift/TailMac/TailMac.swift
Normal file
@ -0,0 +1,334 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Virtualization
|
||||||
|
import ArgumentParser
|
||||||
|
|
||||||
|
var usage =
|
||||||
|
"""
|
||||||
|
Installs and configures VMs suitable for use with natlab
|
||||||
|
|
||||||
|
To create a new VM (this will grab a restore image if needed)
|
||||||
|
tailmac create --id <vm_id>
|
||||||
|
|
||||||
|
To refresh an existing restore image:
|
||||||
|
tailmac refresh
|
||||||
|
|
||||||
|
To clone a vm (this will clone the mac and port as well)
|
||||||
|
tailmac clone --identfier <old_vm_id> --target-id <new_vm_id>
|
||||||
|
|
||||||
|
To reconfigure a vm:
|
||||||
|
tailmac configure --id <vm_id> --mac 11:22:33:44:55:66 --port 12345 --mem 8000000000000 -sock "/tmp/mySock.sock"
|
||||||
|
|
||||||
|
To run a vm:
|
||||||
|
tailmac run --id <vm_id>
|
||||||
|
|
||||||
|
To stop a vm: (this may take a minute - the vm needs to persist it's state)
|
||||||
|
tailmac stop --id <vm_id>
|
||||||
|
|
||||||
|
To halt a vm without persisting its state
|
||||||
|
tailmac halt --id <vm_id>
|
||||||
|
|
||||||
|
To delete a vm:
|
||||||
|
tailmac delete --id <vm_id>
|
||||||
|
|
||||||
|
To list the available VM images:
|
||||||
|
tailmac ls
|
||||||
|
"""
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct Tailmac: ParsableCommand {
|
||||||
|
static var configuration = CommandConfiguration(
|
||||||
|
abstract: "A utility for setting up VM images",
|
||||||
|
usage: usage,
|
||||||
|
subcommands: [Create.self, Clone.self, Delete.self, Configure.self, Stop.self, Run.self, Ls.self, Halt.self],
|
||||||
|
defaultSubcommand: Ls.self)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Tailmac {
|
||||||
|
struct Ls: ParsableCommand {
|
||||||
|
mutating func run() {
|
||||||
|
do {
|
||||||
|
let dirs = try FileManager.default.contentsOfDirectory(atPath: vmBundleURL.path())
|
||||||
|
var images = [String]()
|
||||||
|
|
||||||
|
// This assumes we don't put anything else interesting in our VM.bundle dir
|
||||||
|
// You may need to add some other exclusions or checks here if that's the case.
|
||||||
|
for dir in dirs {
|
||||||
|
if !dir.contains("ipsw") {
|
||||||
|
images.append(URL(fileURLWithPath: dir).lastPathComponent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
print("Available images:\n\(images)")
|
||||||
|
} catch {
|
||||||
|
fatalError("Failed to query available images \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Tailmac {
|
||||||
|
struct Stop: ParsableCommand {
|
||||||
|
@Option(help: "The vm identifier") var id: String
|
||||||
|
|
||||||
|
mutating func run() {
|
||||||
|
print("Stopping vm with id \(id). This may take some time!")
|
||||||
|
let nc = DistributedNotificationCenter()
|
||||||
|
nc.post(name: Notifications.stop, object: nil, userInfo: ["id": id])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Tailmac {
|
||||||
|
struct Halt: ParsableCommand {
|
||||||
|
@Option(help: "The vm identifier") var id: String
|
||||||
|
|
||||||
|
mutating func run() {
|
||||||
|
print("Halting vm with id \(id)")
|
||||||
|
let nc = DistributedNotificationCenter()
|
||||||
|
nc.post(name: Notifications.halt, object: nil, userInfo: ["id": id])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Tailmac {
|
||||||
|
struct Run: ParsableCommand {
|
||||||
|
@Option(help: "The vm identifier") var id: String
|
||||||
|
@Flag(help: "Tail the TailMac log output instead of returning immediatly") var tail
|
||||||
|
|
||||||
|
mutating func run() {
|
||||||
|
let process = Process()
|
||||||
|
let stdOutPipe = Pipe()
|
||||||
|
let appPath = "./Host.app/Contents/MacOS/Host"
|
||||||
|
|
||||||
|
process.executableURL = URL(
|
||||||
|
fileURLWithPath: appPath,
|
||||||
|
isDirectory: false,
|
||||||
|
relativeTo: NSRunningApplication.current.bundleURL
|
||||||
|
)
|
||||||
|
|
||||||
|
if !FileManager.default.fileExists(atPath: appPath) {
|
||||||
|
fatalError("Could not find Host.app. This must be co-located with the tailmac utility")
|
||||||
|
}
|
||||||
|
|
||||||
|
process.arguments = ["run", "--id", id]
|
||||||
|
|
||||||
|
do {
|
||||||
|
process.standardOutput = stdOutPipe
|
||||||
|
try process.run()
|
||||||
|
} catch {
|
||||||
|
fatalError("Unable to launch the vm process")
|
||||||
|
}
|
||||||
|
|
||||||
|
// This doesn't print until we exit which is not ideal, but at least we
|
||||||
|
// get the output
|
||||||
|
if tail != 0 {
|
||||||
|
let outHandle = stdOutPipe.fileHandleForReading
|
||||||
|
|
||||||
|
let queue = OperationQueue()
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
forName: NSNotification.Name.NSFileHandleDataAvailable,
|
||||||
|
object: outHandle, queue: queue)
|
||||||
|
{
|
||||||
|
notification -> Void in
|
||||||
|
let data = outHandle.availableData
|
||||||
|
if data.count > 0 {
|
||||||
|
if let str = String(data: data, encoding: String.Encoding.utf8) {
|
||||||
|
print(str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
outHandle.waitForDataInBackgroundAndNotify()
|
||||||
|
}
|
||||||
|
outHandle.waitForDataInBackgroundAndNotify()
|
||||||
|
process.waitUntilExit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Tailmac {
|
||||||
|
struct Configure: ParsableCommand {
|
||||||
|
@Option(help: "The vm identifier") var id: String
|
||||||
|
@Option(help: "The mac address of the socket network interface") var mac: String?
|
||||||
|
@Option(help: "The port for the virtio socket device") var port: String?
|
||||||
|
@Option(help: "The named socket for the socket network interface") var sock: String?
|
||||||
|
@Option(help: "The desired RAM in bytes") var mem: String?
|
||||||
|
@Option(help: "The ethernet address for a standard NAT adapter") var ethermac: String?
|
||||||
|
|
||||||
|
mutating func run() {
|
||||||
|
let config = Config(id)
|
||||||
|
|
||||||
|
let vmExists = FileManager.default.fileExists(atPath: config.vmDataURL.path())
|
||||||
|
if !vmExists {
|
||||||
|
print("VM with id \(id) doesn't exist. Cannot configure.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if let mac {
|
||||||
|
config.mac = mac
|
||||||
|
}
|
||||||
|
if let port, let portInt = UInt32(port) {
|
||||||
|
config.port = portInt
|
||||||
|
}
|
||||||
|
if let ethermac {
|
||||||
|
config.ethermac = ethermac
|
||||||
|
}
|
||||||
|
if let mem, let membytes = UInt64(mem) {
|
||||||
|
config.memorySize = membytes
|
||||||
|
}
|
||||||
|
if let sock {
|
||||||
|
config.serverSocket = sock
|
||||||
|
}
|
||||||
|
|
||||||
|
config.persist()
|
||||||
|
|
||||||
|
let str = String(data:try! JSONEncoder().encode(config), encoding: .utf8)!
|
||||||
|
print("New Config: \(str)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Tailmac {
|
||||||
|
struct Delete: ParsableCommand {
|
||||||
|
@Option(help: "The vm identifer") var id: String?
|
||||||
|
|
||||||
|
mutating func run() {
|
||||||
|
guard let id else {
|
||||||
|
print("Usage: Installer delete --id=<id>")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let config = Config(id)
|
||||||
|
|
||||||
|
let vmExists = FileManager.default.fileExists(atPath: config.vmDataURL.path())
|
||||||
|
if !vmExists {
|
||||||
|
print("VM with id \(id) doesn't exist. Cannot delete.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
try FileManager.default.removeItem(at: config.vmDataURL)
|
||||||
|
} catch {
|
||||||
|
print("Whoops... Deletion failed \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extension Tailmac {
|
||||||
|
struct Clone: ParsableCommand {
|
||||||
|
@Option(help: "The vm identifier") var id: String
|
||||||
|
@Option(help: "The vm identifier for the cloned vm") var targetId: String
|
||||||
|
|
||||||
|
mutating func run() {
|
||||||
|
|
||||||
|
let config = Config(id)
|
||||||
|
let targetConfig = Config(targetId)
|
||||||
|
|
||||||
|
if id == targetId {
|
||||||
|
fatalError("The ids match. Clone failed.")
|
||||||
|
}
|
||||||
|
|
||||||
|
let vmExists = FileManager.default.fileExists(atPath: config.vmDataURL.path())
|
||||||
|
if !vmExists {
|
||||||
|
print("VM with id \(id) doesn't exist. Cannot clone.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
print("Cloning \(config.vmDataURL) to \(targetConfig.vmDataURL)")
|
||||||
|
do {
|
||||||
|
try FileManager.default.copyItem(at: config.vmDataURL, to: targetConfig.vmDataURL)
|
||||||
|
} catch {
|
||||||
|
print("Whoops... Cloning failed \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Tailmac {
|
||||||
|
struct RefreshImage: ParsableCommand {
|
||||||
|
mutating func run() {
|
||||||
|
let config = Config()
|
||||||
|
let exists = FileManager.default.fileExists(atPath: config.restoreImageURL.path())
|
||||||
|
if exists {
|
||||||
|
try? FileManager.default.removeItem(at: config.restoreImageURL)
|
||||||
|
}
|
||||||
|
let restoreImage = RestoreImage(config.restoreImageURL)
|
||||||
|
restoreImage.download {
|
||||||
|
print("Restore image refreshed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Tailmac {
|
||||||
|
struct Create: ParsableCommand {
|
||||||
|
@Option(help: "The vm identifier. Each VM instance needs a unique ID.") var id: String
|
||||||
|
@Option(help: "The mac address of the socket network interface") var mac: String?
|
||||||
|
@Option(help: "The port for the virtio socket device") var port: String?
|
||||||
|
@Option(help: "The named socket for the socket network interface") var sock: String?
|
||||||
|
@Option(help: "The desired RAM in bytes") var mem: String?
|
||||||
|
@Option(help: "The ethernet address for a standard NAT adapter") var ethermac: String?
|
||||||
|
@Option(help: "The image name to build from. If omitted we will use RestoreImage.ipsw in ~/VM.bundle and download it if needed") var image: String?
|
||||||
|
|
||||||
|
mutating func run() {
|
||||||
|
buildVM(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildVM(_ id: String) {
|
||||||
|
print("Configuring vm with id \(id)")
|
||||||
|
|
||||||
|
let config = Config(id)
|
||||||
|
let installer = VMInstaller(config)
|
||||||
|
|
||||||
|
let vmExists = FileManager.default.fileExists(atPath: config.vmDataURL.path())
|
||||||
|
if vmExists {
|
||||||
|
print("VM with id \(id) already exists. No action taken.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
createDir(config.vmDataURL.path())
|
||||||
|
|
||||||
|
if let mac {
|
||||||
|
config.mac = mac
|
||||||
|
}
|
||||||
|
if let port, let portInt = UInt32(port) {
|
||||||
|
config.port = portInt
|
||||||
|
}
|
||||||
|
if let ethermac {
|
||||||
|
config.ethermac = ethermac
|
||||||
|
}
|
||||||
|
if let mem, let membytes = UInt64(mem) {
|
||||||
|
config.memorySize = membytes
|
||||||
|
}
|
||||||
|
if let sock {
|
||||||
|
config.serverSocket = sock
|
||||||
|
}
|
||||||
|
|
||||||
|
config.persist()
|
||||||
|
|
||||||
|
let restoreImagePath = image ?? config.restoreImageURL.path()
|
||||||
|
|
||||||
|
let exists = FileManager.default.fileExists(atPath: restoreImagePath)
|
||||||
|
if exists {
|
||||||
|
print("Using existing restore image at \(restoreImagePath)")
|
||||||
|
installer.installMacOS(ipswURL: URL(fileURLWithPath: restoreImagePath))
|
||||||
|
} else {
|
||||||
|
if image != nil {
|
||||||
|
fatalError("Unable to find custom restore image")
|
||||||
|
}
|
||||||
|
|
||||||
|
print("Downloading default restore image to \(config.restoreImageURL)")
|
||||||
|
let restoreImage = RestoreImage(URL(fileURLWithPath: restoreImagePath))
|
||||||
|
restoreImage.download {
|
||||||
|
// Install from the restore image that you downloaded.
|
||||||
|
installer.installMacOS(ipswURL: URL(fileURLWithPath: restoreImagePath))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatchMain()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
140
tstest/tailmac/Swift/TailMac/VMInstaller.swift
Normal file
140
tstest/tailmac/Swift/TailMac/VMInstaller.swift
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Virtualization
|
||||||
|
|
||||||
|
class VMInstaller: NSObject {
|
||||||
|
private var installationObserver: NSKeyValueObservation?
|
||||||
|
private var virtualMachine: VZVirtualMachine!
|
||||||
|
|
||||||
|
private var config: Config
|
||||||
|
private var helper: TailMacConfigHelper
|
||||||
|
|
||||||
|
init(_ config: Config) {
|
||||||
|
self.config = config
|
||||||
|
helper = TailMacConfigHelper(config: config)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func installMacOS(ipswURL: URL) {
|
||||||
|
print("Attempting to install from IPSW at \(ipswURL).")
|
||||||
|
VZMacOSRestoreImage.load(from: ipswURL, completionHandler: { [self](result: Result<VZMacOSRestoreImage, Error>) in
|
||||||
|
switch result {
|
||||||
|
case let .failure(error):
|
||||||
|
fatalError(error.localizedDescription)
|
||||||
|
|
||||||
|
case let .success(restoreImage):
|
||||||
|
installMacOS(restoreImage: restoreImage)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Internal helper functions.
|
||||||
|
|
||||||
|
private func installMacOS(restoreImage: VZMacOSRestoreImage) {
|
||||||
|
guard let macOSConfiguration = restoreImage.mostFeaturefulSupportedConfiguration else {
|
||||||
|
fatalError("No supported configuration available.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !macOSConfiguration.hardwareModel.isSupported {
|
||||||
|
fatalError("macOSConfiguration configuration isn't supported on the current host.")
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async { [self] in
|
||||||
|
setupVirtualMachine(macOSConfiguration: macOSConfiguration)
|
||||||
|
startInstallation(restoreImageURL: restoreImage.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Create the Mac platform configuration.
|
||||||
|
|
||||||
|
private func createMacPlatformConfiguration(macOSConfiguration: VZMacOSConfigurationRequirements) -> VZMacPlatformConfiguration {
|
||||||
|
let macPlatformConfiguration = VZMacPlatformConfiguration()
|
||||||
|
|
||||||
|
|
||||||
|
let auxiliaryStorage: VZMacAuxiliaryStorage
|
||||||
|
do {
|
||||||
|
auxiliaryStorage = try VZMacAuxiliaryStorage(creatingStorageAt: config.auxiliaryStorageURL,
|
||||||
|
hardwareModel: macOSConfiguration.hardwareModel,
|
||||||
|
options: [])
|
||||||
|
} catch {
|
||||||
|
fatalError("Unable to create aux storage at \(config.auxiliaryStorageURL) \(error)")
|
||||||
|
}
|
||||||
|
macPlatformConfiguration.auxiliaryStorage = auxiliaryStorage
|
||||||
|
macPlatformConfiguration.hardwareModel = macOSConfiguration.hardwareModel
|
||||||
|
macPlatformConfiguration.machineIdentifier = VZMacMachineIdentifier()
|
||||||
|
|
||||||
|
// Store the hardware model and machine identifier to disk so that you
|
||||||
|
// can retrieve them for subsequent boots.
|
||||||
|
try! macPlatformConfiguration.hardwareModel.dataRepresentation.write(to: config.hardwareModelURL)
|
||||||
|
try! macPlatformConfiguration.machineIdentifier.dataRepresentation.write(to: config.machineIdentifierURL)
|
||||||
|
|
||||||
|
return macPlatformConfiguration
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupVirtualMachine(macOSConfiguration: VZMacOSConfigurationRequirements) {
|
||||||
|
let virtualMachineConfiguration = VZVirtualMachineConfiguration()
|
||||||
|
|
||||||
|
virtualMachineConfiguration.platform = createMacPlatformConfiguration(macOSConfiguration: macOSConfiguration)
|
||||||
|
virtualMachineConfiguration.cpuCount = helper.computeCPUCount()
|
||||||
|
if virtualMachineConfiguration.cpuCount < macOSConfiguration.minimumSupportedCPUCount {
|
||||||
|
fatalError("CPUCount isn't supported by the macOS configuration.")
|
||||||
|
}
|
||||||
|
|
||||||
|
virtualMachineConfiguration.memorySize = helper.computeMemorySize()
|
||||||
|
if virtualMachineConfiguration.memorySize < macOSConfiguration.minimumSupportedMemorySize {
|
||||||
|
fatalError("memorySize isn't supported by the macOS configuration.")
|
||||||
|
}
|
||||||
|
|
||||||
|
createDiskImage()
|
||||||
|
|
||||||
|
virtualMachineConfiguration.bootLoader = helper.createBootLoader()
|
||||||
|
virtualMachineConfiguration.graphicsDevices = [helper.createGraphicsDeviceConfiguration()]
|
||||||
|
virtualMachineConfiguration.storageDevices = [helper.createBlockDeviceConfiguration()]
|
||||||
|
virtualMachineConfiguration.networkDevices = [helper.createNetworkDeviceConfiguration(), helper.createSocketNetworkDeviceConfiguration()]
|
||||||
|
virtualMachineConfiguration.pointingDevices = [helper.createPointingDeviceConfiguration()]
|
||||||
|
virtualMachineConfiguration.keyboards = [helper.createKeyboardConfiguration()]
|
||||||
|
|
||||||
|
try! virtualMachineConfiguration.validate()
|
||||||
|
try! virtualMachineConfiguration.validateSaveRestoreSupport()
|
||||||
|
|
||||||
|
virtualMachine = VZVirtualMachine(configuration: virtualMachineConfiguration)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startInstallation(restoreImageURL: URL) {
|
||||||
|
let installer = VZMacOSInstaller(virtualMachine: virtualMachine, restoringFromImageAt: restoreImageURL)
|
||||||
|
|
||||||
|
print("Starting installation.")
|
||||||
|
installer.install(completionHandler: { (result: Result<Void, Error>) in
|
||||||
|
if case let .failure(error) = result {
|
||||||
|
fatalError(error.localizedDescription)
|
||||||
|
} else {
|
||||||
|
print("Installation succeeded.")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Observe installation progress.
|
||||||
|
installationObserver = installer.progress.observe(\.fractionCompleted, options: [.initial, .new]) { (progress, change) in
|
||||||
|
print("Installation progress: \(change.newValue! * 100).")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an empty disk image for the virtual machine.
|
||||||
|
private func createDiskImage() {
|
||||||
|
let diskFd = open(config.diskImageURL.path, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR)
|
||||||
|
if diskFd == -1 {
|
||||||
|
fatalError("Cannot create disk image.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 72 GB disk space.
|
||||||
|
var result = ftruncate(diskFd, config.diskSize)
|
||||||
|
if result != 0 {
|
||||||
|
fatalError("ftruncate() failed.")
|
||||||
|
}
|
||||||
|
|
||||||
|
result = close(diskFd)
|
||||||
|
if result != 0 {
|
||||||
|
fatalError("Failed to close the disk image.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
tstest/tailmac/Swift/TailMac/main
Executable file
BIN
tstest/tailmac/Swift/TailMac/main
Executable file
Binary file not shown.
8
tstest/tailmac/TailMac.entitlements
Normal file
8
tstest/tailmac/TailMac.entitlements
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.virtualization</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
581
tstest/tailmac/TailMac.xcodeproj/project.pbxproj
Normal file
581
tstest/tailmac/TailMac.xcodeproj/project.pbxproj
Normal file
@ -0,0 +1,581 @@
|
|||||||
|
// !$*UTF8*$!
|
||||||
|
{
|
||||||
|
archiveVersion = 1;
|
||||||
|
classes = {
|
||||||
|
};
|
||||||
|
objectVersion = 55;
|
||||||
|
objects = {
|
||||||
|
|
||||||
|
/* Begin PBXBuildFile section */
|
||||||
|
8F87D52126C34111000EADA4 /* HostCli.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F87D52026C34111000EADA4 /* HostCli.swift */; };
|
||||||
|
8F87D52326C34111000EADA4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8F87D52226C34111000EADA4 /* Assets.xcassets */; };
|
||||||
|
8F87D52626C34111000EADA4 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 8F87D52426C34111000EADA4 /* MainMenu.xib */; };
|
||||||
|
8F87D53426C341AC000EADA4 /* TailMac.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F87D53326C341AC000EADA4 /* TailMac.swift */; };
|
||||||
|
8F87D54026C34259000EADA4 /* TailMacConfigHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F87D53D26C34259000EADA4 /* TailMacConfigHelper.swift */; };
|
||||||
|
8F87D54426C34269000EADA4 /* TailMacConfigHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F87D53D26C34259000EADA4 /* TailMacConfigHelper.swift */; };
|
||||||
|
8F87D54726C3427C000EADA4 /* Virtualization.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8F87D54626C3427C000EADA4 /* Virtualization.framework */; };
|
||||||
|
8F87D54826C34286000EADA4 /* Virtualization.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8F87D54626C3427C000EADA4 /* Virtualization.framework */; };
|
||||||
|
C266EA7F2C5D2AD800DC57E3 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = C266EA7E2C5D2AD800DC57E3 /* Config.swift */; };
|
||||||
|
C266EA802C5D2AE700DC57E3 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = C266EA7E2C5D2AD800DC57E3 /* Config.swift */; };
|
||||||
|
C28759A42C6BB68D0032283D /* VMInstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = C28759A32C6BB68D0032283D /* VMInstaller.swift */; };
|
||||||
|
C28759A72C6BB7F90032283D /* RestoreImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C28759A62C6BB7F90032283D /* RestoreImage.swift */; };
|
||||||
|
C28759AC2C6C00840032283D /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = C28759AB2C6C00840032283D /* ArgumentParser */; };
|
||||||
|
C28759AE2C6D0FC10032283D /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = C28759AD2C6D0FC10032283D /* ArgumentParser */; };
|
||||||
|
C28759BC2C6D19D40032283D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C28759BB2C6D19D40032283D /* AppDelegate.swift */; };
|
||||||
|
C28759BE2C6D1A0F0032283D /* VMController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C28759BD2C6D1A0F0032283D /* VMController.swift */; };
|
||||||
|
C28759C02C6D1E980032283D /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = C28759BF2C6D1E980032283D /* Notifications.swift */; };
|
||||||
|
C28759C12C6D1E980032283D /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = C28759BF2C6D1E980032283D /* Notifications.swift */; };
|
||||||
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
|
8F87D52F26C341AC000EADA4 /* CopyFiles */ = {
|
||||||
|
isa = PBXCopyFilesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
dstPath = /usr/share/man/man1/;
|
||||||
|
dstSubfolderSpec = 0;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 1;
|
||||||
|
};
|
||||||
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXFileReference section */
|
||||||
|
8F87D51D26C34111000EADA4 /* Host.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Host.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
8F87D52026C34111000EADA4 /* HostCli.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostCli.swift; sourceTree = "<group>"; };
|
||||||
|
8F87D52226C34111000EADA4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
|
8F87D52526C34111000EADA4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
|
||||||
|
8F87D53126C341AC000EADA4 /* TailMac */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = TailMac; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
8F87D53326C341AC000EADA4 /* TailMac.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TailMac.swift; sourceTree = "<group>"; };
|
||||||
|
8F87D53826C3423F000EADA4 /* TailMac.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = TailMac.entitlements; sourceTree = "<group>"; };
|
||||||
|
8F87D53B26C34250000EADA4 /* Host.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Host.entitlements; sourceTree = "<group>"; };
|
||||||
|
8F87D53D26C34259000EADA4 /* TailMacConfigHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TailMacConfigHelper.swift; sourceTree = "<group>"; };
|
||||||
|
8F87D54626C3427C000EADA4 /* Virtualization.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Virtualization.framework; path = System/Library/Frameworks/Virtualization.framework; sourceTree = SDKROOT; };
|
||||||
|
8FB90BE826D422FD00988F51 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
B0E246092DFBF28FAEA2709F /* LICENSE.txt */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = LICENSE.txt; sourceTree = "<group>"; };
|
||||||
|
C266EA7E2C5D2AD800DC57E3 /* Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = "<group>"; };
|
||||||
|
C28759A32C6BB68D0032283D /* VMInstaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMInstaller.swift; sourceTree = "<group>"; };
|
||||||
|
C28759A62C6BB7F90032283D /* RestoreImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestoreImage.swift; sourceTree = "<group>"; };
|
||||||
|
C28759A92C6BF8800032283D /* Makefile */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.make; path = Makefile; sourceTree = "<group>"; };
|
||||||
|
C28759AF2C6D10060032283D /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
|
||||||
|
C28759BB2C6D19D40032283D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
C28759BD2C6D1A0F0032283D /* VMController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMController.swift; sourceTree = "<group>"; };
|
||||||
|
C28759BF2C6D1E980032283D /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = "<group>"; };
|
||||||
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
8F87D51A26C34111000EADA4 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
C28759AE2C6D0FC10032283D /* ArgumentParser in Frameworks */,
|
||||||
|
8F87D54826C34286000EADA4 /* Virtualization.framework in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
8F87D52E26C341AC000EADA4 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
C28759AC2C6C00840032283D /* ArgumentParser in Frameworks */,
|
||||||
|
8F87D54726C3427C000EADA4 /* Virtualization.framework in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXGroup section */
|
||||||
|
09E329497FB7E44895839D88 /* LICENSE */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
B0E246092DFBF28FAEA2709F /* LICENSE.txt */,
|
||||||
|
);
|
||||||
|
path = LICENSE;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
8F87D51426C34111000EADA4 = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
C28759AF2C6D10060032283D /* README.md */,
|
||||||
|
C28759A92C6BF8800032283D /* Makefile */,
|
||||||
|
8F87D53B26C34250000EADA4 /* Host.entitlements */,
|
||||||
|
8F87D53826C3423F000EADA4 /* TailMac.entitlements */,
|
||||||
|
8FDABC17270D0F9100D7FC60 /* Swift */,
|
||||||
|
8F87D51E26C34111000EADA4 /* Products */,
|
||||||
|
8F87D54526C3427C000EADA4 /* Frameworks */,
|
||||||
|
09E329497FB7E44895839D88 /* LICENSE */,
|
||||||
|
);
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
8F87D51E26C34111000EADA4 /* Products */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
8F87D51D26C34111000EADA4 /* Host.app */,
|
||||||
|
8F87D53126C341AC000EADA4 /* TailMac */,
|
||||||
|
);
|
||||||
|
name = Products;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
8F87D51F26C34111000EADA4 /* Host */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
8F87D52026C34111000EADA4 /* HostCli.swift */,
|
||||||
|
C28759BD2C6D1A0F0032283D /* VMController.swift */,
|
||||||
|
C28759BB2C6D19D40032283D /* AppDelegate.swift */,
|
||||||
|
8F87D52226C34111000EADA4 /* Assets.xcassets */,
|
||||||
|
8F87D52426C34111000EADA4 /* MainMenu.xib */,
|
||||||
|
8FB90BE826D422FD00988F51 /* Info.plist */,
|
||||||
|
);
|
||||||
|
path = Host;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
8F87D52C26C3418F000EADA4 /* Common */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
C266EA7E2C5D2AD800DC57E3 /* Config.swift */,
|
||||||
|
8F87D53D26C34259000EADA4 /* TailMacConfigHelper.swift */,
|
||||||
|
C28759BF2C6D1E980032283D /* Notifications.swift */,
|
||||||
|
);
|
||||||
|
path = Common;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
8F87D53226C341AC000EADA4 /* TailMac */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
8F87D53326C341AC000EADA4 /* TailMac.swift */,
|
||||||
|
C28759A62C6BB7F90032283D /* RestoreImage.swift */,
|
||||||
|
C28759A32C6BB68D0032283D /* VMInstaller.swift */,
|
||||||
|
);
|
||||||
|
path = TailMac;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
8F87D54526C3427C000EADA4 /* Frameworks */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
8F87D54626C3427C000EADA4 /* Virtualization.framework */,
|
||||||
|
);
|
||||||
|
name = Frameworks;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
8FDABC17270D0F9100D7FC60 /* Swift */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
8F87D52C26C3418F000EADA4 /* Common */,
|
||||||
|
8F87D51F26C34111000EADA4 /* Host */,
|
||||||
|
8F87D53226C341AC000EADA4 /* TailMac */,
|
||||||
|
);
|
||||||
|
path = Swift;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXGroup section */
|
||||||
|
|
||||||
|
/* Begin PBXNativeTarget section */
|
||||||
|
8F87D51C26C34111000EADA4 /* host */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 8F87D52926C34111000EADA4 /* Build configuration list for PBXNativeTarget "host" */;
|
||||||
|
buildPhases = (
|
||||||
|
8F87D51926C34111000EADA4 /* Sources */,
|
||||||
|
8F87D51A26C34111000EADA4 /* Frameworks */,
|
||||||
|
8F87D51B26C34111000EADA4 /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = host;
|
||||||
|
packageProductDependencies = (
|
||||||
|
C28759AD2C6D0FC10032283D /* ArgumentParser */,
|
||||||
|
);
|
||||||
|
productName = macOSVirtualMachineSampleApp;
|
||||||
|
productReference = 8F87D51D26C34111000EADA4 /* Host.app */;
|
||||||
|
productType = "com.apple.product-type.application";
|
||||||
|
};
|
||||||
|
8F87D53026C341AC000EADA4 /* tailmac */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 8F87D53526C341AC000EADA4 /* Build configuration list for PBXNativeTarget "tailmac" */;
|
||||||
|
buildPhases = (
|
||||||
|
8F87D52D26C341AC000EADA4 /* Sources */,
|
||||||
|
8F87D52E26C341AC000EADA4 /* Frameworks */,
|
||||||
|
8F87D52F26C341AC000EADA4 /* CopyFiles */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = tailmac;
|
||||||
|
packageProductDependencies = (
|
||||||
|
C28759AB2C6C00840032283D /* ArgumentParser */,
|
||||||
|
);
|
||||||
|
productName = InstallationTool;
|
||||||
|
productReference = 8F87D53126C341AC000EADA4 /* TailMac */;
|
||||||
|
productType = "com.apple.product-type.tool";
|
||||||
|
};
|
||||||
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
|
/* Begin PBXProject section */
|
||||||
|
8F87D51526C34111000EADA4 /* Project object */ = {
|
||||||
|
isa = PBXProject;
|
||||||
|
attributes = {
|
||||||
|
BuildIndependentTargetsInParallel = 1;
|
||||||
|
DefaultBuildSystemTypeForWorkspace = Latest;
|
||||||
|
LastSwiftUpdateCheck = 1540;
|
||||||
|
LastUpgradeCheck = 1300;
|
||||||
|
ORGANIZATIONNAME = Apple;
|
||||||
|
TargetAttributes = {
|
||||||
|
8F87D51C26C34111000EADA4 = {
|
||||||
|
CreatedOnToolsVersion = 13.0;
|
||||||
|
};
|
||||||
|
8F87D53026C341AC000EADA4 = {
|
||||||
|
CreatedOnToolsVersion = 13.0;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
buildConfigurationList = 8F87D51826C34111000EADA4 /* Build configuration list for PBXProject "TailMac" */;
|
||||||
|
compatibilityVersion = "Xcode 13.0";
|
||||||
|
developmentRegion = en;
|
||||||
|
hasScannedForEncodings = 0;
|
||||||
|
knownRegions = (
|
||||||
|
en,
|
||||||
|
Base,
|
||||||
|
);
|
||||||
|
mainGroup = 8F87D51426C34111000EADA4;
|
||||||
|
packageReferences = (
|
||||||
|
C28759AA2C6BFF0F0032283D /* XCRemoteSwiftPackageReference "swift-argument-parser" */,
|
||||||
|
);
|
||||||
|
productRefGroup = 8F87D51E26C34111000EADA4 /* Products */;
|
||||||
|
projectDirPath = "";
|
||||||
|
projectRoot = "";
|
||||||
|
targets = (
|
||||||
|
8F87D51C26C34111000EADA4 /* host */,
|
||||||
|
8F87D53026C341AC000EADA4 /* tailmac */,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
/* End PBXProject section */
|
||||||
|
|
||||||
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
|
8F87D51B26C34111000EADA4 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
8F87D52326C34111000EADA4 /* Assets.xcassets in Resources */,
|
||||||
|
8F87D52626C34111000EADA4 /* MainMenu.xib in Resources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
8F87D51926C34111000EADA4 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
8F87D52126C34111000EADA4 /* HostCli.swift in Sources */,
|
||||||
|
C28759C02C6D1E980032283D /* Notifications.swift in Sources */,
|
||||||
|
C266EA7F2C5D2AD800DC57E3 /* Config.swift in Sources */,
|
||||||
|
C28759BC2C6D19D40032283D /* AppDelegate.swift in Sources */,
|
||||||
|
C28759BE2C6D1A0F0032283D /* VMController.swift in Sources */,
|
||||||
|
8F87D54026C34259000EADA4 /* TailMacConfigHelper.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
8F87D52D26C341AC000EADA4 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
8F87D54426C34269000EADA4 /* TailMacConfigHelper.swift in Sources */,
|
||||||
|
C28759C12C6D1E980032283D /* Notifications.swift in Sources */,
|
||||||
|
C28759A72C6BB7F90032283D /* RestoreImage.swift in Sources */,
|
||||||
|
C266EA802C5D2AE700DC57E3 /* Config.swift in Sources */,
|
||||||
|
C28759A42C6BB68D0032283D /* VMInstaller.swift in Sources */,
|
||||||
|
8F87D53426C341AC000EADA4 /* TailMac.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXVariantGroup section */
|
||||||
|
8F87D52426C34111000EADA4 /* MainMenu.xib */ = {
|
||||||
|
isa = PBXVariantGroup;
|
||||||
|
children = (
|
||||||
|
8F87D52526C34111000EADA4 /* Base */,
|
||||||
|
);
|
||||||
|
name = MainMenu.xib;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXVariantGroup section */
|
||||||
|
|
||||||
|
/* Begin XCBuildConfiguration section */
|
||||||
|
8F87D52726C34111000EADA4 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_TESTABILITY = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
|
GCC_DYNAMIC_NO_PIC = NO;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_OPTIMIZATION_LEVEL = 0;
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
|
"DEBUG=1",
|
||||||
|
"$(inherited)",
|
||||||
|
);
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
|
MTL_FAST_MATH = YES;
|
||||||
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
|
SDKROOT = macosx;
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
8F87D52826C34111000EADA4 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
MTL_FAST_MATH = YES;
|
||||||
|
SDKROOT = macosx;
|
||||||
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
8F87D52A26C34111000EADA4 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ARCHS = arm64;
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = Host.entitlements;
|
||||||
|
CODE_SIGN_IDENTITY = "Mac Developer";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = W5364U7YZB;
|
||||||
|
ENABLE_APP_SANDBOX = NO;
|
||||||
|
ENABLE_USER_SELECTED_FILES = readwrite;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_FILE = Swift/Host/Info.plist;
|
||||||
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
|
INFOPLIST_KEY_NSMainNibFile = MainMenu;
|
||||||
|
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Allow for using audio input devices.";
|
||||||
|
INFOPLIST_KEY_NSPrincipalClass = NSApplication;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/../Frameworks",
|
||||||
|
);
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.tailscale.vnetMacHost;
|
||||||
|
PRODUCT_NAME = Host;
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
8F87D52B26C34111000EADA4 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ARCHS = arm64;
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = Host.entitlements;
|
||||||
|
CODE_SIGN_IDENTITY = "Mac Developer";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = W5364U7YZB;
|
||||||
|
ENABLE_APP_SANDBOX = NO;
|
||||||
|
ENABLE_USER_SELECTED_FILES = readwrite;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_FILE = Swift/Host/Info.plist;
|
||||||
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
|
INFOPLIST_KEY_NSMainNibFile = MainMenu;
|
||||||
|
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Allow for using audio input devices.";
|
||||||
|
INFOPLIST_KEY_NSPrincipalClass = NSApplication;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/../Frameworks",
|
||||||
|
);
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.tailscale.vnetMacHost;
|
||||||
|
PRODUCT_NAME = Host;
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
8F87D53626C341AC000EADA4 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ARCHS = arm64;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = TailMac.entitlements;
|
||||||
|
CODE_SIGN_IDENTITY = "Mac Developer";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
DEVELOPMENT_TEAM = W5364U7YZB;
|
||||||
|
ENABLE_USER_SELECTED_FILES = readwrite;
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.tailscale.vnetMacHostSetupTool;
|
||||||
|
PRODUCT_NAME = TailMac;
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
8F87D53726C341AC000EADA4 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ARCHS = arm64;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = TailMac.entitlements;
|
||||||
|
CODE_SIGN_IDENTITY = "Mac Developer";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
DEVELOPMENT_TEAM = W5364U7YZB;
|
||||||
|
ENABLE_USER_SELECTED_FILES = readwrite;
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.tailscale.vnetMacHostSetupTool;
|
||||||
|
PRODUCT_NAME = TailMac;
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
|
/* Begin XCConfigurationList section */
|
||||||
|
8F87D51826C34111000EADA4 /* Build configuration list for PBXProject "TailMac" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
8F87D52726C34111000EADA4 /* Debug */,
|
||||||
|
8F87D52826C34111000EADA4 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
8F87D52926C34111000EADA4 /* Build configuration list for PBXNativeTarget "host" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
8F87D52A26C34111000EADA4 /* Debug */,
|
||||||
|
8F87D52B26C34111000EADA4 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
8F87D53526C341AC000EADA4 /* Build configuration list for PBXNativeTarget "tailmac" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
8F87D53626C341AC000EADA4 /* Debug */,
|
||||||
|
8F87D53726C341AC000EADA4 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
|
/* Begin XCRemoteSwiftPackageReference section */
|
||||||
|
C28759AA2C6BFF0F0032283D /* XCRemoteSwiftPackageReference "swift-argument-parser" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/apple/swift-argument-parser.git";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 1.5.0;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
C28759AB2C6C00840032283D /* ArgumentParser */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = C28759AA2C6BFF0F0032283D /* XCRemoteSwiftPackageReference "swift-argument-parser" */;
|
||||||
|
productName = ArgumentParser;
|
||||||
|
};
|
||||||
|
C28759AD2C6D0FC10032283D /* ArgumentParser */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = C28759AA2C6BFF0F0032283D /* XCRemoteSwiftPackageReference "swift-argument-parser" */;
|
||||||
|
productName = ArgumentParser;
|
||||||
|
};
|
||||||
|
/* End XCSwiftPackageProductDependency section */
|
||||||
|
};
|
||||||
|
rootObject = 8F87D51526C34111000EADA4 /* Project object */;
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>BuildSystemType</key>
|
||||||
|
<string>Latest</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"originHash" : "59ba1edda695b389d6c9ac1809891cd779e4024f505b0ce1a9d5202b6762e38a",
|
||||||
|
"pins" : [
|
||||||
|
{
|
||||||
|
"identity" : "swift-argument-parser",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-argument-parser.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "41982a3656a71c768319979febd796c6fd111d5c",
|
||||||
|
"version" : "1.5.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version" : 3
|
||||||
|
}
|
@ -0,0 +1,78 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1320"
|
||||||
|
version = "1.3">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "8F87D51C26C34111000EADA4"
|
||||||
|
BuildableName = "TailMac.app"
|
||||||
|
BlueprintName = "host"
|
||||||
|
ReferencedContainer = "container:TailMac.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
|
<Testables>
|
||||||
|
</Testables>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "8F87D51C26C34111000EADA4"
|
||||||
|
BuildableName = "TailMac.app"
|
||||||
|
BlueprintName = "host"
|
||||||
|
ReferencedContainer = "container:TailMac.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "8F87D51C26C34111000EADA4"
|
||||||
|
BuildableName = "TailMac.app"
|
||||||
|
BlueprintName = "host"
|
||||||
|
ReferencedContainer = "container:TailMac.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
@ -0,0 +1,78 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1320"
|
||||||
|
version = "1.3">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "8F87D53026C341AC000EADA4"
|
||||||
|
BuildableName = "TailMac"
|
||||||
|
BlueprintName = "tailmac"
|
||||||
|
ReferencedContainer = "container:TailMac.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
|
<Testables>
|
||||||
|
</Testables>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "8F87D53026C341AC000EADA4"
|
||||||
|
BuildableName = "TailMac"
|
||||||
|
BlueprintName = "tailmac"
|
||||||
|
ReferencedContainer = "container:TailMac.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "8F87D53026C341AC000EADA4"
|
||||||
|
BuildableName = "TailMac"
|
||||||
|
BlueprintName = "tailmac"
|
||||||
|
ReferencedContainer = "container:TailMac.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
@ -0,0 +1,37 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>SchemeUserState</key>
|
||||||
|
<dict>
|
||||||
|
<key>VMRunner.xcscheme_^#shared#^_</key>
|
||||||
|
<dict>
|
||||||
|
<key>orderHint</key>
|
||||||
|
<integer>2</integer>
|
||||||
|
</dict>
|
||||||
|
<key>host.xcscheme_^#shared#^_</key>
|
||||||
|
<dict>
|
||||||
|
<key>orderHint</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
</dict>
|
||||||
|
<key>tailmac.xcscheme_^#shared#^_</key>
|
||||||
|
<dict>
|
||||||
|
<key>orderHint</key>
|
||||||
|
<integer>0</integer>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
<key>SuppressBuildableAutocreation</key>
|
||||||
|
<dict>
|
||||||
|
<key>8FDABC39270D1DC600D7FC60</key>
|
||||||
|
<dict>
|
||||||
|
<key>primary</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
<key>8FDABC58270D1FFE00D7FC60</key>
|
||||||
|
<dict>
|
||||||
|
<key>primary</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
Loading…
Reference in New Issue
Block a user