Repository: s-u/macosvm
Branch: master
Commit: ce2bc38af489
Files: 10
Total size: 101.5 KB
Directory structure:
gitextract_t669lnye/
├── LICENSE
├── Makefile
├── NEWS.md
├── README.md
├── macosvm/
│ ├── Makefile
│ ├── VMInstance.h
│ ├── VMInstance.m
│ ├── macosvm.entitlements
│ └── main.m
└── macosvm.xcodeproj/
└── project.pbxproj
================================================
FILE CONTENTS
================================================
================================================
FILE: LICENSE
================================================
macosvm Virtualization Tool
Copyright (C) 2021 Simon Urbanek
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) version 3.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
================================================
FILE: Makefile
================================================
all:
make -C macosvm macosvm
clean:
make -C macosvm clean
================================================
FILE: NEWS.md
================================================
## NEWS
### 0.2-3
* added `--pid-file <path>` argument which writes the process id (pid) of the `macosvm` process into the specified file path before starting the VM and removes it on exit, making it easier to track running VMs if desired.
### 0.2-2
* added `--script` argument which allows to specify a script that should be called then the VM is successfully launched. Additional two arguments are added to the provided script string: the `pid` of the `macosvm` process and the MAC address of the first interface (if is exists). The script is executed via `/bin/bash -c` so the actual call for `--script <script>` will be similar to `/bin/bash -c '<script> <pid> <mac>'` and thus respects `PATH` etc.
### 0.2-1
* Linux guest would fail with "Number of pointing devices is greater than the maximum number supported" since VZ framework allows the choice of trackpad and USB only for macOS guests (#21, regression from 0.1-4).
* Add `--recovery` option to start macOS VM in recovery mode on macOS 13 host and up (#22, thanks to Jim Lake).
### 0.2-0
* command line parameters are now parsed __after__ the specified configuration file is loaded and will cause the settings to be __added__ to the configuration. This allows the use of pre-specified configurations which can be supplemented by command line arguments. This behavior is more intuitive, but different from 0.1 versions which is why we chose to increase the version.
* added `--save <path>` which will write the resulting configuration after all arguments were parsed into a JSON file specified by `<path>`. Note that `--restore` already creates the configuration file without this option, so `--save` should only be used when it is desired to update an existing configuration augmented with command line options to create a new configuration file.
* optional capabilities that depend on the host environment are listed as `Capabilities:` in the output of `--version`
### 0.1-4
* `--net unix:<socket>[,mac=<mac>][,mtu=<mtu>]` creates a network interface which routes network traffic to a unix socket `<socket>` on the host. The default (and minimum) MTU is 1500, but it can be increased (only on macOS 13 and higher). A temporary socket is created in the temporary directory by default, but the directory can be overridden by the `TMPSOCKDIR` environment variable. (#20, thanks to Alexander Graf!)
* ephemeral files are now also removed on `SIGABRT` which can happen if the Virtualization framework raises an execption
* added support for `usb` disk type (macOS 13 and above only)
* added support for Mac trackpad if both the guest and host are macOS 13
* added `--spice` which will enable clipboard shaing between the host and guest using the SPICE protocol (experimental). Requires macOS 13 host and `spice-vdagent` in the guest. Due to an issue in the Apple VZ framework this currently only works with Linux guests (macOS guests crash).
### 0.1-3
* MAC address of each network interface created will be shown at startup, e.g.:
```
+ network: ether 9a:74:8c:65:6d:e0
```
to make it easier to associate IP addresses to VMs via `arp -a`.
* `--net nat:<MAC>` defines a NAT network interface with a given pre-defined MAC address. Similarly, the interfaces in the `"networks"` configuration section can have `"mac"` keys that define the MAC address.
* additonal option `--mac <MAC>` on the command line will override the MAC address of the first interface, regardless how it was defined (configuration file or command line). This is useful when creating multiple VMs from the same configuration file, typically with `--ephemeral`.
* added support for VirtIOFS shared directories via `--vol` option. The syntax is `--vol <path>[,ro][,{name=<name>|automount}]` where `<path>` is the path on the host OS to share and `<name>` is the name (also known as "tag") of the share. On macOS 13 (Ventura) and higher `automount` option can be specified in which case the share is automatically mounted in `/Volumes/My Shared Files`. If not specifed, the share has to be mounted by name with `mount_virtiofs <name> <mountpoint>` in the guest OS.
* guest serial console is now also enabled in macOS guests (in `/dev/cu.virtio` and `/dev/tty.virtio`). Previous versions enabled it only for Linux guests. It can be explicitly disabled using `--no-serial` option.
* experimental `--pty` option allows the creation of pseudo-tty device for the guest serial console. Without this option the serial console is mapped to the stdin/out streams of the `macosvm` process. If the `--pty` option is specified then `macosvm` will create a new `pty` (typically in `/dev/ptys.<n>`) and map VM's serial port to it.
Unfortuantely, Apple Virtialization Framework requires that the pty is connected before the VM is started. Therefore currently `macosvm` will print the `pty` path and wait for user input so that the user can connect to the newly created pty before starting the VM. Proceeding without connected pty leads to an error. This behavior may change in the future, which is why it is considered experimental.
### 0.1-2
* added `--ephemeral` flag: when specified, all (read-write) disks (including auxiliary) will be cloned (see `man clonefile`) prior to starting the VM (by appending `-clone-<pid>` to their paths) and the clones are used instead of the original. Upon termination all clones are deleted. This is functionally similar to the `--rm` flag in Docker. IMPORTANT: you will lose any changes to the mounted disks made by the VM. This is intended for runners that pick up work, do something and then post the results somewhere, but don't keep them locally. `macosvm` attempts to clean up clones even on abnormal termination where possible. Individual disks can specify `keep` option which prevents them from being cloned in the ephemeral mode, e.g.: `--disk results.img,keep` will cause `results.img` to be used directly and modified by the VM even if `--ephemeral` is specified.
* added heuristic to detect ECID from the auxiliary storage if it is not supplied by the configuration file
* make the configuration file optional. It is now possible to start VMs simply by specifying the desired CPU/RAM and disk images and `macosvm` will try to infer all necessary settings automatically. I.e., if you have existing disk images `aux.img` and `disk.img` from previously restored/created VM, you can use the following to start it:
```
macosvm -g --disk disk.img --aux aux.img -c 2 -r 4g
```
* fixed a bug where the `"readOnly"` flag specified in the configuration file was not honored
* added `os` and `bootInfo` entries in the configuration. Currently valid entries for `os` are `"macos"` (default) and `"linux"`. The latter uses `bootInfo` dictionary with entries `kernel` (path, mandatory) and `parameters` (string, optional). Also a new storage type `"initrd"` has been added to support the Linux boot process (untested).
### 0.1-1
* initial version
================================================
FILE: README.md
================================================
## macosvm
`macosvm` is a command line tool which allows creating and running of virtual machines on macOS 12 (Monterey) and higher using the new Virtualization framework. It has been primarily developed for running macOS guest opearting systems inside virtual machines on M1-based Macs (arm64) with macOS hosts to support CI/CD such as GitHub M1-based runners and our [R](https://www.R-project.org) builds (see [vm-scripts-mini-r](https://github.com/R-macos/vm-scripts-mini-r) for a small example).
### Download
See [releases](https://github.com/s-u/macosvm/releases) for downloads of released binaries (arm64 macOS 12 and higher only). See [NEWS](https://github.com/s-u/macosvm/blob/master/NEWS.md) for latest changes.
Please use `curl` for download and not browsers since the latter will quarantine the downloaded file, e.g.:
curl -L https://github.com/s-u/macosvm/releases/download/0.2-2/macosvm-0.2-2-darwin21.tar.gz | tar vxz
### Build
The project can be built either with `xcodebuild` or `make`. The former requires Xcode installation while the latter only requires command line tools (see `xcode-select --install`).
### Quick Start
The tool requires at least macOS 12 (Monterey) since that is the first system implementing the necessary pieces of the Virtualization framework. To create a macOS guest VM you need the following steps:
```
## Download the desired macOS ipsw image, e.g.:
curl -LO https://updates.cdn-apple.com/2024SummerFCS/fullrestores/062-52859/932E0A8F-6644-4759-82DA-F8FA8DEA806A/UniversalMac_14.6.1_23G93_Restore.ipsw
## create a new VM with 32Gb disk image and install macOS 14:
macosvm --disk disk.img,size=32g --aux aux.img --restore UniversalMac_14.6.1_23G93_Restore.ipsw vm.json
## start the created image with GUI window:
macosvm -g vm.json
```
A full list of ipsw images for all versions of macOS is availabe [here](https://mrmacintosh.com/apple-silicon-m1-full-macos-restore-ipsw-firmware-files-database/). Note that the guest macOS version must be equal to or lower than the host version.
After your started the VM it will go through the Apple Setup Assistant - you need the GUI to get through that. Once done, I strongly recommend going to *Sharing* system preferences, setting a unique name and enabling *Remote Login* and *Screen Sharing*. Then you can shut down the VM (using *Shut Down* in the macOS guest). Note that the default is to use NAT networking and your VM will show up on your host's network (details below) so you can use *Finder* to connect to its screen even if you start without the GUI.
After the minimal setup it ts possible to create a "clone" of your image to keep for later, e.g.:
```
cp -c disk.img master.img
```
Note the `-c` flag which will make a copy-on-write clone, i.e., the cloned image doesn't use any actual space on disk (if you use APFS). This allows you to store different modifications of your base operating system without duplicating storage space. See also the `--ephemeral` option of `macosvm` if you don't want to persist any changes made while the VM runs.
See `macosvm -h` for a minimal help page. Note that this is an experimental expert tool. There is a lot of debugging output, errors include stack traces etc - this is intentional at this point, nothing horrible is happening, but you may need to read more text than you want to on errors.
See [the Wiki](https://github.com/s-u/macosvm/wiki) for more tips and information.
Since version 0.2-2 you can launch scripts when the VM is up - e.g. see [launch.sh](https://github.com/R-macos/vm-scripts-mini-r/blob/master/launch.sh) for a simple example that retrieves the IP address and can be used as a base for automation.
### Details
Each virtual machine requires one auxiliary storage (specified with `--aux`) and at least one root device (specified with `--disk`). You can specify multiple disk images for additional devices. The `--disk` option has the form `--disk <file>[,<option>[,<option>...]]`. Valid options are: `ro` = read-only device, `size=<spec>` allocate empty disk with that size. The `<spec>` argument allows `k`, `m` and `g` suffix for powers of 1024 so `32g` means `32*(1024^3)`.
You can specify the number of CPUs with `-c <cpus>` and the available memory (RAM) with `-r <spec>`. If not specified, `--restore` uses the image's minimal requirements (for macOS 12 that is 2 CPUs and 4Gb RAM).
During the macOS installation (`--restore`) step a unique machine identifier (ECID) is generated. The resulting images only work with that one identifier. This identifier is stored in the configuration file (as `machineId` entry) and the VM won't boot without it. Since `macosvm` version 0.1-2 this information can be extracted from the auxiliary file if a configuration file is not present, but that is a hack that may not work with future macOS versions.
This is what the configuration file looks like generated by the above example:
```
{
"hardwareModel":"YnBsaX[...]AAAAABt",
"storage":[
{"type":"disk", "file":"disk.img", "readOnly":false},
{"type":"aux", "file":"aux.img", "readOnly":false}
],
"ram":4294967296,
"machineId":"YnBsaXN0MDDRAQJURUN[...]AAAACE=",
"displays":[{"dpi":200, "width":2560, "height":1600}],
"version":1,
"cpus":2,
"networks":[{"type":"nat"}],
"audio":false
}
```
You can edit the file if you want to change the parameters (like CPUs, RAM, ...), but keep a copy in case you break something. The `"hardwareModel"` is just the OS/architecture spec from the image. (FWIW both `hardwareModel` and `machineId` are base64-encoded binary property lists so you can look at the payload with `base64 -d | plutil -p -` )
Note that the virtualization framework imposes some restrictions on what is allowed, e.g., you have to define at least one display even if you don't intend to use it (the `displays` entry is added automatically by `--restore`).
Unless run with `-g`/`--gui` the tool will run solely as a command line tool and can be run in the background. Terminating the tool terminates the VMs. However, if the GUI is used closing the window does NOT terminate the VM. Note that currently macOS guest systems don't support VM control, so even though it is in theory possible to request VM stop via the VZ framework, it is not actually honored by the macOS guest (as of 12.0.1), so you should use guest-side shutdown (e.g. `sudo shutdown -h now` works). When the guest OS shuts down the tool terminates with exit code 0.
### Networking
The default setup is NAT networking which means your host will allocate a separate local network for the VM. You can use `--net <type>[:{<mac>|<iface>}]` to specify different network adapters and different types. `macosvm` currently implements `nat`, `bridge` and `unix`. `bridge` requires the name of the host interface to bridge to (if left blank the first interface is used). Note, however, that bridging requires a special entitlement that can only be obtained from Apple so it is not supported by "normal" binaries. Since 0.1-3 it is possible to override the MAC address of the first interface with `--mac <MAC>` which makes it possible to script the IP address retrieval from `arp -a`.
If you are not running any discovery/bonjour services in the guest to find the IPs, you can typically find the IP addresses of your VMs using `arp -a`. Currently macOS VMs in NAT mode will be on the interface `bridge100`, typically with `192.168.64.x` IP address (where `x=1` is the host so the other numbers are VMs). There doesn't seem to be any direct control over the networking, but apparently the guests can talk to the host and NAT out, but can't talk to each other even though they appear on the same subnet.
The `unix` target is useful in combination with a [slirp proxy](https://github.com/agraf/slirp-unix) (for binaries see [releases](https://github.com/s-u/slirp-unix/releases)) which then allows NAT-like access in environments that are sensitive to network configuration.
### File Sharing
Since macOS 13 (Ventura) VirtIOFS is supported both in the guest and host macOS. A typical use is something like `--vol /Users/myself/shared,automount` which will make the contents of the `/Users/myself/shared` directory on the host avaiable as `/Volumes/My Shared Files` in the guest macOS. If `automount` is not specified, then the guest OS has to mount the virtiofs share (via `mount_virtiofs` or similar) by specifying its name (tag) which defaults to `macosvm`, but can be set, e.g., by appending `,name=mysharename`.
================================================
FILE: macosvm/Makefile
================================================
CFLAGS=-Wall
LIBS=-framework AppKit -framework Virtualization -fobjc-arc -fobjc-link-runtime
CC=clang
ifeq ($(DEVID),)
DEVID=-
endif
macosvm: VMInstance.o main.o macosvm.entitlements
$(CC) -o $@ VMInstance.o main.o $(LDFLAGS) $(LIBS)
codesign --force --sign $(DEVID) -o runtime --entitlements macosvm.entitlements --timestamp\=none --generate-entitlement-der $@ || rm -f $@
VMInstance.o: VMInstance.h VMInstance.m
$(CC) $(CPPFLAGS) $(CFLAGS) -c VMInstance.m
main.o: VMInstance.h main.m
$(CC) $(CPPFLAGS) $(CFLAGS) -c main.m
clean:
rm -f VMInstance.o main.o macosvm
================================================
FILE: macosvm/VMInstance.h
================================================
#import <Foundation/Foundation.h>
#import <Virtualization/Virtualization.h>
#ifdef __arm64__
#define MACOS_GUEST 1
#endif
@interface VMSpec : VZVirtualMachineConfiguration {
NSData *machineIdentifierData, *hardwareModelData;
NSArray *storage;
/* type: disk / aux / initrd
file: / url:
readOnly: true/false */
NSArray *displays;
/* width: height: dpi: */
NSArray *networks;
/* type: mac: interface: */
NSArray *shares;
/* */
NSString *os;
/* macos / linux */
NSDictionary *bootInfo;
/* Linux: kernel, parameters */
NSString *ptyPath; /* internally generated */
@public
int cpus;
unsigned long ram;
BOOL audio, use_serial, pty, spice, use_recovery;
NSString *spawnScript;
}
#ifdef MACOS_GUEST
@property (strong) VZMacOSRestoreImage *restoreImage;
#endif
- (instancetype) init;
- (NSError *) readFromJSON: (NSInputStream*) jsonStream;
- (NSError*) writeToJSON: (NSOutputStream*) jsonStream;
- (void) addFileStorage: (NSString*) path type: (NSString*) type readOnly: (BOOL) ro;
- (void) addFileStorage: (NSString*) path type: (NSString*) type options: (NSArray*) options;
- (void) addDefaults;
- (void) addDisplayWithWidth: (int) width height: (int) height dpi: (int) dpi;
- (void) addNetwork: (NSString*) type;
- (void) addNetwork: (NSString*) type mac:(NSString*) mac;
- (void) addNetwork: (NSString*) type interface: (NSString*) iface;
- (void) addNetwork: (NSString*) type interface: (NSString*) iface mac: (NSString*) mac;
- (void) addNetworkSpecification: (NSDictionary*) spec;
- (void) addDirectoryShare: (NSString*) path volume: (NSString*) volume readOnly: (BOOL) readOnly;
- (void) addDirectoryShares: (NSArray*) paths volume: (NSString*) volume readOnly: (BOOL) readOnly;
- (void) addAutomountDirectoryShare: (NSString*) path readOnly: (BOOL) readOnly;
- (void) addAutomountDirectoryShares: (NSArray*) paths readOnly: (BOOL) readOnly;
- (void) setPrimaryMAC: (NSString*) mac;
- (void) setSpawnScript: (NSString*) mac;
- (instancetype) configure;
- (void) cloneAllStorage;
@end
@interface VMInstance : NSObject
{
@public
dispatch_queue_t queue;
// VZMacOSInstaller *installer;
}
@property (strong) VZVirtualMachine *virtualMachine;
@property (strong) VMSpec *spec;
- (instancetype) initWithSpec: (VMSpec*) spec;
- (void) start;
- (BOOL) stop;
- (void) performVM: (id) target selector: (SEL) aSelector withObject:(nullable id)anArgument;
@end
================================================
FILE: macosvm/VMInstance.m
================================================
#import "VMInstance.h"
/* for cloneAllStorage */
#include <sys/clonefile.h>
#include <unistd.h>
#include <sys/errno.h>
#include <sys/stat.h>
/* for socket networking */
#include <sys/un.h>
#include <sys/socket.h>
@implementation VMSpec
- (instancetype) init {
self = [super init];
machineIdentifierData = hardwareModelData = nil;
storage = displays = networks = nil;
cpus = 0;
ram = 0;
os = nil;
bootInfo = nil;
audio = NO;
#ifdef MACOS_GUEST
_restoreImage = nil;
#endif
use_serial = YES;
pty = NO;
spice = NO;
ptyPath = nil;
spawnScript = nil;
return self;
}
- (NSError *) readFromJSON: (NSInputStream*) jsonStream {
NSError *err = nil;
NSDictionary *root = [NSJSONSerialization JSONObjectWithStream:jsonStream options: NSJSONReadingTopLevelDictionaryAssumed error: &err];
if (err)
return err;
NSString *hardwareModel = root[@"hardwareModel"];
hardwareModelData = hardwareModel ? [[NSData alloc] initWithBase64EncodedString:hardwareModel options:0] : nil;
NSString *machineIdentifier = root[@"machineId"];
machineIdentifierData = machineIdentifier ? [[NSData alloc] initWithBase64EncodedString:machineIdentifier options:0] : nil;
os = root[@"os"];
hardwareModelData = hardwareModel ? [[NSData alloc] initWithBase64EncodedString:hardwareModel options:0] : nil;
id tmp = root[@"cpus"];
if (tmp && [tmp isKindOfClass:[NSNumber class]])
cpus = (int) [(NSNumber*)tmp integerValue];
tmp = root[@"ram"];
if (tmp && [tmp isKindOfClass:[NSNumber class]])
ram = [(NSNumber*)tmp unsignedLongValue];
tmp = root[@"storage"];
if (tmp && [tmp isKindOfClass:[NSArray class]])
storage = tmp;
tmp = root[@"bootInfo"];
if (tmp && [tmp isKindOfClass:[NSDictionary class]])
bootInfo = tmp;
tmp = root[@"networks"];
if (tmp && [tmp isKindOfClass:[NSArray class]])
networks = tmp;
tmp = root[@"displays"];
if (tmp && [tmp isKindOfClass:[NSArray class]])
displays = tmp;
tmp = root[@"shares"];
if (tmp && [tmp isKindOfClass:[NSArray class]])
shares = tmp;
tmp = root[@"audio"];
if (tmp && [tmp isKindOfClass:[NSNumber class]])
audio = [(NSNumber*)tmp unsignedLongValue] ? YES : NO;
tmp = root[@"serial"];
if (tmp && [tmp isKindOfClass:[NSNumber class]])
use_serial = [(NSNumber*)tmp unsignedLongValue] ? YES : NO;
return nil;
}
- (NSError*) writeToJSON: (NSOutputStream*) jsonStream {
NSDictionary *src = @{
@"version": @1,
@"os" : os ? os : @"macos",
@"cpus": [NSNumber numberWithInteger: cpus],
@"ram": [NSNumber numberWithUnsignedLong: ram],
@"storage" : storage ? storage : [NSArray array],
@"audio" : audio ? @(YES) : @(NO),
@"serial" : use_serial ? @(YES) : @(NO)
};
NSMutableDictionary *root = [[NSMutableDictionary alloc] init];
[root setDictionary:src];
if (hardwareModelData)
[root setObject:[hardwareModelData base64EncodedStringWithOptions:0] forKey:@"hardwareModel"];
if (bootInfo)
[root setObject:bootInfo forKey:@"bootInfo"];
if (machineIdentifierData)
[root setObject:[machineIdentifierData base64EncodedStringWithOptions:0] forKey:@"machineId"];
if (displays)
[root setObject:displays forKey:@"displays"];
if (networks)
[root setObject:networks forKey:@"networks"];
if (shares)
[root setObject:shares forKey:@"shares"];
NSError *err = nil;
[NSJSONSerialization writeJSONObject:root toStream:jsonStream
options:NSJSONWritingWithoutEscapingSlashes
error:&err];
return err;
}
- (void) addDefaults {
if (!displays) {
displays = @[
@{
@"width": @2560,
@"height": @1600,
@"dpi": @200
}
];
}
if (!networks)
networks = @[ @{ @"type": @"nat" } ];
}
- (void) addDisplayWithWidth: (int) width height: (int) height dpi: (int) dpi {
NSDictionary *display = @{
@"width": [NSNumber numberWithInteger:width],
@"height": [NSNumber numberWithInteger:height],
@"dpi": [NSNumber numberWithInteger:dpi]
};
displays = displays ? [displays arrayByAddingObject:display] : @[display];
}
- (void) addDirectoryShare: (NSString*) path volume: (NSString*) volume readOnly: (BOOL) readOnly {
NSDictionary *root = @{
@"path" : path,
@"volume" : volume,
@"readOnly" : @(readOnly)
};
shares = shares ? [shares arrayByAddingObject:root] : @[root];
}
- (void) addDirectoryShares: (NSArray*) paths volume: (NSString*) volume readOnly: (BOOL) readOnly {
NSDictionary *root = @{
@"paths" : paths,
@"volume" : volume,
@"readOnly" : @(readOnly)
};
shares = shares ? [shares arrayByAddingObject:root] : @[root];
}
- (void) addAutomountDirectoryShare: (NSString*) path readOnly: (BOOL) readOnly {
NSDictionary *root = @{
@"path" : path,
@"automount" : @(YES),
@"readOnly" : @(readOnly)
};
shares = shares ? [shares arrayByAddingObject:root] : @[root];
}
- (void) addAutomountDirectoryShares: (NSArray*) paths readOnly: (BOOL) readOnly {
NSDictionary *root = @{
@"paths" : paths,
@"automount" : @(YES),
@"readOnly" : @(readOnly)
};
shares = shares ? [shares arrayByAddingObject:root] : @[root];
}
- (void) addNetwork: (NSString*) type {
NSDictionary *root = @{
@"type" : type
};
networks = networks ? [networks arrayByAddingObject:root] : @[root];
}
- (void) addNetworkSpecification: (NSDictionary*) spec {
networks = networks ? [networks arrayByAddingObject:spec] : @[spec];
}
- (void) addNetwork: (NSString*) type mac:(NSString*) mac {
NSDictionary *root = @{
@"type" : type,
@"mac" : mac
};
networks = networks ? [networks arrayByAddingObject:root] : @[root];
}
- (void) setPrimaryMAC: (NSString*) mac {
if (networks && [networks count]) {
NSDictionary *fa = [networks objectAtIndex:0];
NSMutableDictionary *nn = [NSMutableDictionary dictionaryWithDictionary: fa];
[nn setObject: mac forKey:@"mac"];
if ([networks count] == 1)
networks = @[nn];
else {
NSMutableArray *ma = [NSMutableArray arrayWithObject: nn];
networks = [ma arrayByAddingObjectsFromArray:[networks subarrayWithRange: NSMakeRange(1, [ma count] - 1)]];
}
} else [self addNetwork: @"nat" mac:mac];
}
- (void) setSpawnScript: (NSString*) script {
spawnScript = [script retain];
}
- (void) addNetwork: (NSString*) type interface: (NSString*) iface {
NSDictionary *root = @{
@"type" : type,
@"interface" : iface
};
networks = networks ? [networks arrayByAddingObject:root] : @[root];
}
- (void) addNetwork: (NSString*) type interface: (NSString*) iface mac: (NSString*) mac {
NSDictionary *root = @{
@"type" : type,
@"interface" : iface,
@"mac" : mac
};
networks = networks ? [networks arrayByAddingObject:root] : @[root];
}
- (void) addFileStorage: (NSString*) path type: (NSString*) type readOnly: (BOOL) ro {
NSDictionary *root = @{
@"type" : type,
@"file" : path,
@"readOnly" : ro ? @(YES) : @(NO)
};
storage = storage ? [storage arrayByAddingObject:root] : @[root];
}
- (void) addFileStorage: (NSString*) path type: (NSString*) type options: (NSArray*) options {
NSDictionary *initial = @{
@"type" : type,
@"file" : path
};
NSMutableDictionary *root = [NSMutableDictionary dictionaryWithDictionary:initial];
for (NSString *option in options)
[root setValue: @(YES) forKey: option];
storage = storage ? [storage arrayByAddingObject:root] : @[root];
}
void add_unlink_on_exit(const char *fn); /* from main.m - a bit hacky but more safe ... ;) */
/* this is not the most elegant design .. but we need to re-design the loading of options
otherwise we really have to do it here so we can cover both the JSON specs as well
as disks added by the storage API calls .. and we want to allow opt-outs, but for now
it does the job we need: run ephemeral runners .. */
- (void) cloneAllStorage {
if (storage) {
NSMutableArray *cloned = [[NSMutableArray alloc] init];
for (NSDictionary *d in storage) {
NSDictionary *e = d;
id tmp;
NSString *path = d[@"file"];
BOOL ro = (d[@"readOnly"] && [d[@"readOnly"] boolValue]) ? YES : NO;
BOOL keep = (d[@"keep"] && [d[@"keep"] boolValue]) ? YES : NO;
if ((tmp = d[@"type"]) && path && !ro && !keep) {
if ([tmp isEqualToString:@"disk"] || [tmp isEqualToString:@"aux"]) {
NSString *target = [path stringByAppendingFormat: @"-clone-%ld", (long) getpid()];
NSLog(@" . cloning %@ to ephemeral %@", path, target);
if (clonefile([path UTF8String], [target UTF8String], 0)) {
NSString *desc = [NSString stringWithFormat:@"Failed to clone '%@' to '%@': [errno=%d] %s", path, target, errno, strerror(errno)];
@throw [NSException exceptionWithName:@"FSClone" reason:desc userInfo:nil];
}
add_unlink_on_exit([target UTF8String]);
NSMutableDictionary *emu = [[NSMutableDictionary alloc] initWithDictionary:d];
emu[@"file"] = target;
e = emu;
}
}
[cloned addObject: e];
}
storage = cloned;
}
}
/* this is a hack to allow booting macOS without config files, extract
ECID from the aux disk. This is purely empirical so may break
with future versions of macOS */
- (NSData*) inferMachineIdFromAuxFile: (NSString*) path {
#define ECID_SIG_LEN 7
#define ECID_LEN 8
/* We're looking for (len = 4) "ECID" (type = 2) (len = 8) [<ecid>] */
NSData *ecidSig = [NSData dataWithBytes:"\04ECID\02\010" length: ECID_SIG_LEN];
NSFileHandle *f = [NSFileHandle fileHandleForReadingAtPath:path];
NSError *err;
uint64_t ecid = 0;
if (!f)
return nil;
/* Attempt #1: use known location */
if ([f seekToOffset: 0x6c804 error:&err]) {
NSData *data = [f readDataUpToLength: ECID_SIG_LEN error:&err], *ecidData;
if ([data isEqualToData: ecidSig] && /* match! */
(ecidData = [f readDataUpToLength: ECID_LEN error:&err]) &&
ecidData.length == ECID_LEN)
memcpy(&ecid, ecidData.bytes, ECID_LEN);
}
/* Attempt #2: search and pray .. */
if (!ecid && [f seekToOffset:0 error:&err]) { /* rewind */
/* we're lazy, aux is typically 32M so do it in memory for simplicity */
NSData *data = [f readDataToEndOfFileAndReturnError:&err];
NSRange r;
if (data && (r = [data rangeOfData: ecidSig options:0 range: NSMakeRange(0, data.length)]).length) {
NSData *ecidData = [data subdataWithRange: NSMakeRange(r.location + r.length, ECID_LEN)];
if (ecidData)
memcpy(&ecid, ecidData.bytes, ECID_LEN);
}
}
[f closeAndReturnError:&err];
if (ecid) {
ecid = CFSwapInt64HostToBig(ecid);
NSDictionary *payload = @{
@"ECID": @(ecid)
};
NSLog(@" + ECID obtained from aux file: 0x%016lx (%lu)", (unsigned long)ecid, (unsigned long)ecid);
return [NSPropertyListSerialization dataWithPropertyList: payload
format: NSPropertyListBinaryFormat_v1_0
options: 0
error: &err];
}
NSLog(@"WARNING: ECID not known and cannot be inferred from auxiliary storage!");
return nil;
}
- (instancetype) configure {
if (!os)
os = @"macos";
#ifdef MACOS_GUEST
NSLog(@"%@ - configure for %@, OS: %@", self, self.restoreImage ? @"restore" : @"run", os);
if ([os isEqualToString:@"macos"] && self.restoreImage) {
VZMacOSRestoreImage *img = self.restoreImage;
VZMacOSConfigurationRequirements *req = [img mostFeaturefulSupportedConfiguration];
hardwareModelData = req.hardwareModel.dataRepresentation;
NSLog(@"configure with restore, minimum requirements: %d CPUs, %lu RAM",
(int)req.minimumSupportedCPUCount, (unsigned long)req.minimumSupportedMemorySize);
if (!cpus)
cpus = (int) req.minimumSupportedCPUCount;
if (!ram)
ram = (unsigned long)req.minimumSupportedMemorySize;
if (req.minimumSupportedCPUCount > cpus)
@throw [NSException exceptionWithName:@"VMConfig" reason:[NSString stringWithFormat:@"Image requires %d CPUs, but only %d configured", (int)req.minimumSupportedCPUCount, cpus] userInfo:nil];
if (req.minimumSupportedMemorySize > ram)
@throw [NSException exceptionWithName:@"VMConfig" reason:[NSString stringWithFormat:@"Image requires %lu bytes of RAM, but only %lu configured", (unsigned long)req.minimumSupportedMemorySize, ram] userInfo:nil];
}
#else
NSLog(@"%@ - configure for running, OS: %@", self, os);
#endif
if (!cpus)
@throw [NSException exceptionWithName:@"VMConfigCPU" reason:@"Number of CPUs not specified" userInfo:nil];
if (!ram)
@throw [NSException exceptionWithName:@"VMConfigRAM" reason:@"RAM size not specified" userInfo:nil];
#if MACOS_GUEST
VZMacPlatformConfiguration *macPlatform = nil;
#endif
if ([os isEqualToString:@"macos"]) {
#if MACOS_GUEST
self.bootLoader = [[VZMacOSBootLoader alloc] init];
macPlatform = [[VZMacPlatformConfiguration alloc] init];
#else
@throw [NSException exceptionWithName:@"VMConfig" reason:@"This Mac does not support macOS as guest system" userInfo:nil];
#endif
} else if ([os isEqualToString:@"linux"]) {
NSURL *initrd = nil, *kernel = nil;
NSString *params = nil;
/* look for initrd */
if (storage) for (NSDictionary *d in storage) {
id tmp;
NSString *path = d[@"file"];
NSURL *url = nil;
if ((tmp = d[@"url"])) url = [NSURL URLWithString:tmp];
if ((tmp = d[@"type"]) && (url || path) && [tmp isEqualToString:@"initrd"]) {
if (initrd)
fprintf(stderr, "WARNING: initrd specified more than once, using the first instance\n");
else
initrd = url ? url : [NSURL fileURLWithPath:path];
}
}
if (bootInfo) {
NSString *kpath = bootInfo[@"kernel"];
if (kpath)
kernel = [NSURL fileURLWithPath:kpath];
params = bootInfo[@"parameters"];
}
if (!kernel)
@throw [NSException exceptionWithName:@"VMLinuxConfig" reason:@"Missing kernel path specification" userInfo:nil];
NSLog(@" + Linux kernel %@", kernel);
VZLinuxBootLoader *bootLoader = [[VZLinuxBootLoader alloc] initWithKernelURL: kernel];
if (params) {
bootLoader.commandLine = params;
NSLog(@" + kernel boot parameters: %@", params);
}
if (initrd) {
bootLoader.initialRamdiskURL = initrd;
NSLog(@" + inital RAM disk: %@", initrd);
}
self.bootLoader = bootLoader;
} else {
NSLog(@"ERROR: unsupported os specification '%@', can only handle 'macos' and 'linux'.", os);
@throw [NSException exceptionWithName:@"VMConfig" reason:@"Unsupported os specification" userInfo:nil];
}
if (use_serial) {
NSFileHandle *readHandle = nil, *writeHandle = nil;
if (pty) {
int masterfd, slavefd;
char *slavedevice;
masterfd = posix_openpt(O_RDWR|O_NOCTTY);
if (masterfd == -1
|| grantpt (masterfd) == -1
|| unlockpt (masterfd) == -1
|| (slavedevice = ptsname (masterfd)) == NULL) {
perror("ERROR: cannot allocate pty");
@throw [NSException exceptionWithName:@"PTYSetup" reason:@"Cannot allocate PTY for serial console" userInfo:nil];
}
/* FIXME: this is a bad hack - the VZ framework fails to spawn the VM if the tty is not connected. */
printf("PTY allocated for serial link: %s\nPress <enter> here on stdin once connected to the tty to proceed.\n", slavedevice);
static char tmp1[16];
fgets(tmp1, sizeof(tmp1), stdin);
writeHandle = readHandle = [[NSFileHandle alloc] initWithFileDescriptor: masterfd];
//writeHandle = [[NSFileHandle alloc] initWithFileDescriptor: dup(masterfd)];
ptyPath = [[NSString alloc] initWithUTF8String: slavedevice];
} else {
readHandle = [NSFileHandle fileHandleWithStandardInput];
writeHandle = [NSFileHandle fileHandleWithStandardOutput];
}
VZVirtioConsoleDeviceSerialPortConfiguration *serial = [[VZVirtioConsoleDeviceSerialPortConfiguration alloc] init];
VZFileHandleSerialPortAttachment *sata = [[VZFileHandleSerialPortAttachment alloc]
initWithFileHandleForReading: readHandle
fileHandleForWriting: writeHandle];
serial.attachment = sata;
self.serialPorts = @[ serial ];
}
self.entropyDevices = @[[[VZVirtioEntropyDeviceConfiguration alloc] init]];
NSMutableArray *netList = [NSMutableArray arrayWithCapacity: networks ? [networks count] : 1];
int net_count = 0;
if (networks) for (NSDictionary *d in networks) {
VZVirtioNetworkDeviceConfiguration *networkDevice = [[VZVirtioNetworkDeviceConfiguration alloc] init];
NSString *type = d[@"type"];
net_count++;
/* default to NAT */
if (!type || [type isEqualToString:@"nat"]) {
NSLog(@" + NAT network");
networkDevice.attachment = [[VZNATNetworkDeviceAttachment alloc] init];
} else if (type && [type hasPrefix:@"br"]) {
NSString *iface = d[@"interface"];
VZBridgedNetworkInterface *brInterface = nil;
NSArray *hostInterfaces = VZBridgedNetworkInterface.networkInterfaces;
if ([hostInterfaces count] < 1)
@throw [NSException exceptionWithName:@"VMConfigNet" reason: @"No host interfaces are available for bridging" userInfo:nil];
if (iface) {
for (VZBridgedNetworkInterface *hostIface in hostInterfaces)
if ([hostIface.identifier isEqualToString: iface]) {
brInterface = hostIface;
break;
}
} else {
brInterface = (VZBridgedNetworkInterface*) hostInterfaces[0];
fprintf(stderr, "%s", [[NSString stringWithFormat: @"WARNING: no network interface specified for bridging, using first: %@ (%@)\n",
brInterface.identifier, brInterface.localizedDisplayName] UTF8String]);
}
if (!brInterface) {
fprintf(stderr, "%s", [[NSString stringWithFormat:@"ERROR: Network interface '%@' not found. Available interfaces for bridging:\n%@",
iface, hostInterfaces] UTF8String]);
@throw [NSException exceptionWithName:@"VMConfigNet" reason:
[NSString stringWithFormat:@"Network interface '%@' not found or not available", iface]
userInfo:nil];
}
NSLog(@" + Bridged network to %@", brInterface);
networkDevice.attachment = [[VZBridgedNetworkDeviceAttachment alloc] initWithInterface:brInterface];
[netList addObject: networkDevice];
} else if (type && [type isEqualToString:@"unix"]) {
NSString *path = d[@"path"];
int mtu = 1500;
struct sockaddr_un caddr = {
.sun_family = AF_UNIX,
};
struct sockaddr_un addr = {
.sun_family = AF_UNIX,
.sun_path = "/tmp/slirp"
};
NSFileHandle *fh;
int sndbuflen = 2 * 1024 * 1024; /* for SO_RCVBUF/SO_SNDBUF - see below */
int rcvbuflen = 6 * 1024 * 1024;
int fd;
id smtu = d[@"mtu"];
if (smtu) {
if ([smtu isKindOfClass:[NSString class]])
mtu = (int) ((NSString*)smtu).integerValue;
else if ([smtu isKindOfClass:[NSNumber class]])
mtu = (int) ((NSNumber*)smtu).integerValue;
if (mtu < 1500)
@throw [NSException exceptionWithName:@"VMConfigNet" reason:
[NSString stringWithFormat:@"MTU value %d is invalid, it must be at least 1500", mtu]
userInfo:nil];
}
NSLog(@" + UNIX domain socket network");
if (path)
strncpy(addr.sun_path, [path UTF8String], sizeof(addr.sun_path) - 1);
const char *tsd = getenv("TMPSOCKDIR");
NSString *tmpDir = (tsd && *tsd) ? [NSString stringWithUTF8String: tsd] : NSTemporaryDirectory();
NSString *tmpSock = [NSString stringWithFormat: @"%@/macosvm.net.%d.%d", tmpDir, (int) getpid(), net_count];
if ([tmpSock lengthOfBytesUsingEncoding:NSUTF8StringEncoding] >= sizeof(caddr.sun_path))
@throw [NSException exceptionWithName:@"VMConfigNet" reason:
[NSString stringWithFormat:@"Temporary socket path '%@' is too long, consider setting TMPSOCKDIR to shorter path.", tmpSock]
userInfo:nil];
strcpy(caddr.sun_path, [tmpSock UTF8String]);
{ /* for security reasons we don't allow the target to be anything other
that a previously created socket (especially not a link) */
struct stat st;
if (lstat(caddr.sun_path, &st) == 0) { /* target exists */
if ((st.st_mode & S_IFMT) != S_IFSOCK)
@throw [NSException exceptionWithName:@"VMConfigNet" reason:
[NSString stringWithFormat:@"Temporary socket path '%@' already exists and is not a socket.", tmpSock]
userInfo:nil];
/* ok, unlink it */
if (unlink(caddr.sun_path))
@throw [NSException exceptionWithName:@"VMConfigNet" reason:
[NSString stringWithFormat:@"Cannot remove stale temporary socket '%@': %s", tmpSock, strerror(errno)]
userInfo:nil];
} /* if it doesn't exist, we're all good */
}
fd = socket(AF_UNIX, SOCK_DGRAM, 0);
/* bind is mandatory */
if (bind(fd, (struct sockaddr *)&caddr, sizeof(caddr)))
@throw [NSException exceptionWithName:@"VMConfigNet" reason:
[NSString stringWithFormat:@"Could not bind UNIX socket to '%s': %s", caddr.sun_path, strerror(errno)]
userInfo:nil];
NSLog(@" Bound to '%s', connecting to '%s'", caddr.sun_path, addr.sun_path);
add_unlink_on_exit(caddr.sun_path);
/* connect is optional for DGRAM, but fixes the peer so we force the desired target */
if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)))
@throw [NSException exceptionWithName:@"VMConfigNet" reason:
[NSString stringWithFormat:@"Could not connect to UNIX socket '%s': %s", addr.sun_path, strerror(errno)]
userInfo:nil];
/* according to VZFileHandleNetworkDeviceAttachment docs SO_RCVBUF has to be
at least double of SO_SNDBUF, ideally 4x. Modern macOS have kern.ipc.maxsockbuf
of 8Mb, so we try 2Mb + 6Mb first and fall back by halving */
while (setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &sndbuflen, sizeof(sndbuflen)) ||
setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &rcvbuflen, sizeof(rcvbuflen))) {
sndbuflen /= 2;
rcvbuflen /= 2;
if (rcvbuflen < 128 * 1024) {
@throw [NSException exceptionWithName:@"VMConfigNet" reason:
[NSString stringWithFormat:@"Could not set socket buffer sizes: %s", strerror(errno)]
userInfo:nil];
}
}
fh = [[NSFileHandle alloc] initWithFileDescriptor:fd];
VZFileHandleNetworkDeviceAttachment *fhda = [[VZFileHandleNetworkDeviceAttachment alloc] initWithFileHandle:fh];
if (mtu > 1500) {
#if (TARGET_OS_OSX && __MAC_OS_X_VERSION_MAX_ALLOWED >= 130000)
if (@available(macOS 13, *))
fhda.maximumTransmissionUnit = mtu;
else
fprintf(stderr, "WARNING: your macOS does not support MTU changes, using default 1500\n");
#else
fprintf(stderr, "WARNING: This build does not support MTU changes, using default 1500\n");
#endif
}
networkDevice.attachment = fhda;
}
NSString *macAddr = d[@"mac"];
if (macAddr) {
VZMACAddress *addr = [[VZMACAddress alloc] initWithString: macAddr];
if (!addr) {
@throw [NSException exceptionWithName:@"VMConfigNetworkError"
reason:[NSString stringWithFormat:@"Invalid MAC address specification: '%@'", macAddr] userInfo:nil];
}
networkDevice.MACAddress = addr;
} else {
networkDevice.MACAddress = [VZMACAddress randomLocallyAdministeredAddress];
}
NSLog(@" + network: ether %@\n", [networkDevice.MACAddress string]);
[netList addObject: networkDevice];
}
self.networkDevices = netList;
if (@available(macOS 12, *)) {
if (shares) {
NSMutableArray *shDevs = [[NSMutableArray alloc] init];
for (NSDictionary *d in shares) {
id tmp;
NSString *path = d[@"path"];
NSString *volume = d[@"volume"];
NSArray *paths = d[@"paths"];
NSURL *url = nil;
BOOL ro = (d[@"readOnly"] && [d[@"readOnly"] boolValue]) ? YES : NO;
BOOL automount = (d[@"automount"] && [d[@"automount"] boolValue]) ? YES : NO;
NSString *shareTag = @"macosvm";
if (automount) {
BOOL canAutomount = NO;
#if (TARGET_OS_OSX && __MAC_OS_X_VERSION_MAX_ALLOWED >= 130000)
if (@available(macOS 13, *)) {
shareTag = VZVirtioFileSystemDeviceConfiguration.macOSGuestAutomountTag;
canAutomount = YES;
}
#endif
if (!canAutomount) {
NSLog(@"WARNING: you macOS does NOT support automounts, setting the share name to 'automount', you have to use 'mount_virtiofs automount <directory>' in the guest OS\n");
shareTag = @"automount";
automount = NO;
}
} else if (volume)
shareTag = volume;
VZVirtioFileSystemDeviceConfiguration *shareCfg = [[VZVirtioFileSystemDeviceConfiguration alloc] initWithTag: shareTag];
if (automount)
NSLog(@" + automount share (in /Volumes/My Shared Files)\n");
else
NSLog(@" + share, run `mount_virtiofs '%@' <mountpoint>` in guest OS to connect\n", shareTag);
if (path)
url = [NSURL fileURLWithPath:path];
if ((tmp = d[@"url"]))
url = [NSURL URLWithString:tmp];
if (path) {
VZSharedDirectory *directory = [[VZSharedDirectory alloc] initWithURL: url readOnly: ro];
NSLog(@" sharing single %@ (%@)\n", url, ro ? @"read-only" : @"read-write");
shareCfg.share = [[VZSingleDirectoryShare alloc] initWithDirectory: directory];
} else if (paths) {
NSMutableDictionary *dirs = [[NSMutableDictionary alloc] init];
for (NSString *path in paths) {
VZSharedDirectory *directory = [[VZSharedDirectory alloc] initWithURL: [NSURL fileURLWithPath: path] readOnly: ro];
NSLog(@" sharing multi %@ (%@)\n", url, ro ? @"read-only" : @"read-write");
[dirs setObject: directory forKey:[path lastPathComponent]];
}
shareCfg.share = [[VZMultipleDirectoryShare alloc] initWithDirectories: dirs];
}
[shDevs addObject: shareCfg];
}
self.directorySharingDevices = shDevs;
}
} else {
if (shares)
@throw [NSException exceptionWithName:@"VMConfigSharesError" reason:@"Your macOS does not support directory sharing, you need macOS 12 or higher." userInfo:nil];
}
#ifdef MACOS_GUEST
VZMacGraphicsDeviceConfiguration *graphics = [[VZMacGraphicsDeviceConfiguration alloc] init];
NSMutableArray *disp = [NSMutableArray arrayWithCapacity:displays ? [displays count] : 1];
if (displays) for (NSDictionary *d in displays) {
int width = 2560, height = 1600, dpi = 200;
id tmp;
if ((tmp = d[@"width"]) && [tmp isKindOfClass:[NSNumber class]]) width = (int)[tmp integerValue];
if ((tmp = d[@"height"]) && [tmp isKindOfClass:[NSNumber class]]) height = (int)[tmp integerValue];
if ((tmp = d[@"dpi"]) && [tmp isKindOfClass:[NSNumber class]]) dpi = (int)[tmp integerValue];
NSLog(@" + display: %d x %d @ %d", width, height, dpi);
[disp addObject: [[VZMacGraphicsDisplayConfiguration alloc]
initWithWidthInPixels:width heightInPixels:height pixelsPerInch:dpi]];
}
graphics.displays = disp;
self.graphicsDevices = @[graphics];
#endif
self.keyboards = @[[[VZUSBKeyboardConfiguration alloc] init]];
#if (TARGET_OS_OSX && __MAC_OS_X_VERSION_MAX_ALLOWED >= 130000 && defined(MACOS_GUEST))
if (@available(macOS 13, *)) {
if (macPlatform) self.pointingDevices = @[
[[VZUSBScreenCoordinatePointingDeviceConfiguration alloc] init], /* < macOS 13 guest */
[[VZMacTrackpadConfiguration alloc] init]]; /* macOS >= 13 guest */
}
#endif
if (!self.pointingDevices || [self.pointingDevices count] == 0)
self.pointingDevices = @[[[VZUSBScreenCoordinatePointingDeviceConfiguration alloc] init]];
#if (TARGET_OS_OSX && __MAC_OS_X_VERSION_MAX_ALLOWED >= 130000)
if (@available(macOS 13, *)) if (spice) { /* SPICE-based clipboard sharing */
VZVirtioConsoleDeviceConfiguration *consoleDevice = [[VZVirtioConsoleDeviceConfiguration alloc] init];
VZVirtioConsolePortConfiguration *consolePort = [[VZVirtioConsolePortConfiguration alloc] init];
consolePort.name = VZSpiceAgentPortAttachment.spiceAgentPortName;
VZSpiceAgentPortAttachment *spiceAgent = [[VZSpiceAgentPortAttachment alloc] init];
spiceAgent.sharesClipboard = YES;
consolePort.attachment = spiceAgent;
consoleDevice.ports[0] = consolePort;
self.consoleDevices = @[ consoleDevice ];
}
#endif
#ifdef MACOS_GUEST
VZMacHardwareModel *hwm = hardwareModelData ? [[VZMacHardwareModel alloc] initWithDataRepresentation:hardwareModelData] : nil;
if (macPlatform && !hardwareModelData) {
fprintf(stderr, "WARNING: no hardware information found, using arm64 macOS 12.0.0 specs\n");
hardwareModelData = [[NSData alloc] initWithBase64EncodedString: @"YnBsaXN0MDDTAQIDBAUGXxAZRGF0YVJlcHJlc2VudGF0aW9uVmVyc2lvbl8QD1BsYXRmb3JtVmVyc2lvbl8QEk1pbmltdW1TdXBwb3J0ZWRPUxQAAAAAAAAAAAAAAAAAAAABEAKjBwgIEAwQAAgPKz1SY2VpawAAAAAAAAEBAAAAAAAAAAkAAAAAAAAAAAAAAAAAAABt" options:0];
}
#endif
NSMutableArray *std = [NSMutableArray arrayWithCapacity:storage ? [storage count] : 1];
if (storage) for (NSDictionary *d in storage) {
id tmp;
NSString *path = d[@"file"];
NSURL *url = nil;
BOOL ro = (d[@"readOnly"] && [d[@"readOnly"] boolValue]) ? YES : NO;
if ((tmp = d[@"url"])) url = [NSURL URLWithString:tmp];
if ((tmp = d[@"type"]) && (url || path)) {
if ([tmp isEqualToString:@"disk"] || [tmp isEqualToString:@"usb"]) {
NSError *err = nil;
NSURL *imageURL = url ? url : [NSURL fileURLWithPath:path];
NSLog(@" + %@ image %@ (%@)", tmp, imageURL, ro ? @"read-only" : @"read-write");
VZDiskImageStorageDeviceAttachment *a = [[VZDiskImageStorageDeviceAttachment alloc] initWithURL:imageURL readOnly:ro error:&err];
if (err)
@throw [NSException exceptionWithName:@"VMConfigDiskStorageError" reason:[err description] userInfo:nil];
if ([tmp isEqualToString:@"disk"])
[std addObject:[[VZVirtioBlockDeviceConfiguration alloc] initWithAttachment:a]];
else {
#if (TARGET_OS_OSX && __MAC_OS_X_VERSION_MAX_ALLOWED >= 130000)
if (@available(macOS 13, *))
[std addObject:[[VZUSBMassStorageDeviceConfiguration alloc] initWithAttachment:a]];
else
#endif
@throw [NSException exceptionWithName:@"VMConfigDiskStorageError" reason:@"USB storage is not supported by this macOS/build" userInfo:nil];
}
}
if ([tmp isEqualToString:@"aux"]) {
NSError *err = nil;
NSURL *imageURL = url ? url : [NSURL fileURLWithPath:path];
BOOL useExisting = url || (path && [[NSFileManager defaultManager] fileExistsAtPath:path]);
#ifdef MACOS_GUEST
if (self.restoreImage)
useExisting = NO;
if (macPlatform) {
NSLog(@" + %@ aux storage %@", useExisting ? @"existing" : @"new", path);
if (useExisting && path && !machineIdentifierData)
machineIdentifierData = [self inferMachineIdFromAuxFile: path];
VZMacAuxiliaryStorage *aux = useExisting ?
[[VZMacAuxiliaryStorage alloc] initWithContentsOfURL: imageURL] :
[[VZMacAuxiliaryStorage alloc] initCreatingStorageAtURL: imageURL hardwareModel:hwm options:VZMacAuxiliaryStorageInitializationOptionAllowOverwrite error:&err];
if (err)
@throw [NSException exceptionWithName:@"VMConfigAuxStorageError" reason:[err description] userInfo:nil];
macPlatform.auxiliaryStorage = aux;
} else
#endif
NSLog(@"WARNING: auxiliary storage is only supported for macOS guests, ignoring\n");
}
}
}
self.storageDevices = std;
if (audio) {
NSLog(@" + audio");
VZVirtioSoundDeviceConfiguration *soundDevice = [[VZVirtioSoundDeviceConfiguration alloc] init];
VZVirtioSoundDeviceOutputStreamConfiguration *outputStream = [[VZVirtioSoundDeviceOutputStreamConfiguration alloc] init];
outputStream.sink = [[VZHostAudioOutputStreamSink alloc] init];
VZVirtioSoundDeviceInputStreamConfiguration *inputStream = [[VZVirtioSoundDeviceInputStreamConfiguration alloc] init];
inputStream.source = [[VZHostAudioInputStreamSource alloc] init];
soundDevice.streams = @[outputStream, inputStream];
self.audioDevices = @[soundDevice];
}
if ([os isEqualToString:@"macos"]) {
#ifdef MACOS_GUEST
if (hwm) macPlatform.hardwareModel = hwm;
/* either load existing or create new one */
VZMacMachineIdentifier *mid = [VZMacMachineIdentifier alloc];
mid = (machineIdentifierData) ? [mid initWithDataRepresentation:machineIdentifierData] : [mid init];
macPlatform.machineIdentifier = mid;
if (!machineIdentifierData)
machineIdentifierData = mid.dataRepresentation;
self.platform = macPlatform;
#endif
} else { /* generic platform */
self.platform = [[VZGenericPlatformConfiguration alloc] init];
}
NSLog(@" + %d CPUs", (int) cpus);
self.CPUCount = cpus;
NSLog(@" + %lu RAM", ram);
self.memorySize = ram;
return self;
}
@end
@implementation VMInstance
{
NSError *stopError;
}
- (instancetype) initWithSpec: (VMSpec*) spec_ {
self = [super init];
self.spec = [spec_ configure];
stopError = nil;
NSError *err = nil;
[_spec validateWithError:&err];
NSLog(@"validateWithError = %@", err ? err : @"OK");
if (err)
@throw [NSException exceptionWithName:@"VMConfigError" reason:[err description] userInfo:nil];
//queue = dispatch_get_main_queue(); //dispatch_queue_create("macvm", DISPATCH_QUEUE_SERIAL);
queue = dispatch_queue_create("macvm", DISPATCH_QUEUE_SERIAL);
self.virtualMachine = [[VZVirtualMachine alloc] initWithConfiguration:_spec queue:queue];
NSLog(@" init OK");
return self;
}
- (void) start {
dispatch_async(queue, ^{ [self start_ ]; });
}
- (void) performVM: (id) target selector: (SEL) aSelector withObject:(id)anArgument {
dispatch_sync(queue, ^{
NSLog(@" - on VM queue start");
/* the selector is void, so no leaks, tell clang that ... */
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[target performSelector:aSelector withObject:anArgument];
#pragma clang diagnostic pop
NSLog(@" - on VM queue end");
});
}
- (void) start_ {
void (^completionHandler)(NSError *errorOrNil) = ^(NSError *err) {
NSLog(@"start completed err=%@", err ? err : @"nil");
if (err)
@throw [NSException exceptionWithName:@"VMStartError" reason:[err description] userInfo:nil];
if (_spec->spawnScript) {
NSError *terr = nil;
NSArray *netList = _spec.networkDevices;
NSString *macAddr = (netList && [netList count] > 0) ? [[((VZNetworkDeviceConfiguration*)[netList objectAtIndex: 0]) MACAddress] string] : @"";
NSArray *args = @[ @"-c", [NSString stringWithFormat: @"%@ %lu %@", _spec->spawnScript, (unsigned long) getpid(), macAddr]];
[NSTask launchedTaskWithExecutableURL: [NSURL fileURLWithPath:@"/bin/bash" isDirectory:NO]
arguments: args
error: &terr
terminationHandler: ^(NSTask *t) { }];
}
};
#if (TARGET_OS_OSX && defined (__arm64__) && __MAC_OS_X_VERSION_MAX_ALLOWED >= 130000)
/* recovery has been introduced in 13.0 */
if (@available(macOS 13.0, *)) {
VZMacOSVirtualMachineStartOptions *opts = [[VZMacOSVirtualMachineStartOptions alloc] init];
opts.startUpFromMacOSRecovery = _spec->use_recovery;
NSLog(@"starting macOS in %@ mode", _spec->use_recovery ? @"recovery" : @"normal");
[_virtualMachine startWithOptions:opts completionHandler:completionHandler];
} else
#endif
/* macOS SDK <13.0 can only do normal start */
[_virtualMachine startWithCompletionHandler:completionHandler];
}
- (void) stop_ {
NSError *err = nil;
BOOL res = [_virtualMachine requestStopWithError:&err];
if (err) NSLog(@"VM.stop rejected with %@", err);
NSLog(@"stop requested %@, err=%@", res ? @"OK" : @"FAIL", err ? err : @"nil");
/* neither should trigger since the API defines that err much match the result */
if (res)
err = nil;
else if (!err)
err = [NSError errorWithDomain:@"VMInstance" code:1 userInfo:nil];
stopError = err;
}
- (BOOL) stop {
dispatch_sync(queue, ^{ [self stop_ ]; });
return ! stopError;
}
@end
/* Local Variables: */
/* c-basic-offset: 4 */
/* indent-tabs-mode: nil */
/* End: */
================================================
FILE: macosvm/macosvm.entitlements
================================================
<?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.app-sandbox</key>
<false/>
<key>com.apple.security.device.usb</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.virtualization</key>
<true/>
</dict>
</plist>
================================================
FILE: macosvm/main.m
================================================
#import <Foundation/Foundation.h>
#import "VMInstance.h"
static const char *version = "0.2-3";
@interface App : NSObject <NSApplicationDelegate, NSWindowDelegate, VZVirtualMachineDelegate> {
@public
NSWindow *window;
VZVirtualMachineView *view;
VMInstance *vm;
VMSpec *spec;
NSTimer *installProgressTimer;
int tick;
}
@property BOOL useGUI;
@property (strong) NSString *configPath;
@property (strong) NSString *restorePath;
#ifdef MACOS_GUEST
@property (strong) VZMacOSInstaller *installer;
#endif
@end
@implementation App
- (instancetype) init {
self = [super init];
window = nil;
view = nil;
vm = nil;
spec = nil;
_configPath = nil;
_restorePath = nil;
#ifdef MACOS_GUEST
_installer = nil;
#endif
tick = 0;
installProgressTimer = nil;
return self;
}
- (void)windowWillClose:(NSNotification *)notification {
NSLog(@"Window will close");
}
static void cleanup();
- (void)applicationWillTerminate:(NSNotification *)notification {
cleanup();
}
/* IMPORTANT: delegate methods are called from VM's queue */
- (void)guestDidStopVirtualMachine:(VZVirtualMachine *)virtualMachine {
NSLog(@"VM %@ guest stopped", virtualMachine);
[NSApp performSelectorOnMainThread:@selector(terminate:) withObject:self waitUntilDone:NO];
}
- (void)virtualMachine:(VZVirtualMachine *)virtualMachine didStopWithError:(NSError *)error {
NSLog(@"VM %@ didStopWithError: %@", virtualMachine, error);
[NSApp performSelectorOnMainThread:@selector(terminate:) withObject:self waitUntilDone:NO];
}
- (void)virtualMachine:(VZVirtualMachine *)virtualMachine networkDevice:(VZNetworkDevice *)networkDevice
attachmentWasDisconnectedWithError:(NSError *)error {
NSLog(@"VM %@ networkDevice:%@ disconnected:%@", virtualMachine, networkDevice, error);
}
- (void) startApp {
[self->spec addDefaults];
#ifdef MACOS_GUEST
if (_restorePath) {
NSLog(@"Restoring from %@", _restorePath);
[VZMacOSRestoreImage loadFileURL:[NSURL fileURLWithPath:_restorePath] completionHandler:^(VZMacOSRestoreImage *img, NSError *err) {
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@" image load: %@", err ? err : @"OK");
if (err)
@throw [NSException exceptionWithName:@"VMRestoreError" reason:[err description] userInfo:nil];
self->spec.restoreImage = img;
[self createVM:nil];
});
}];
} else
#endif
[self createVM: nil];
}
- (void)applicationWillFinishLaunching:(NSNotification *)notification {
if (!self.useGUI) [self startApp];
}
/* this is never called without a session, so we use WillFinish for those instead (#33) */
- (void)applicationDidFinishLaunching:(NSNotification *)notification {
if (self.useGUI) [self startApp];
}
- (void) updateProgess: (id) object {
const char ticks[] = "-\\|/";
#ifdef MACOS_GUEST
NSProgress *progress = (_installer && _installer.progress) ? _installer.progress : nil;
tick++;
tick &= 3;
if (progress)
printf("\r [%c] Progress: %.1f%%\r", ticks[tick], [progress fractionCompleted] * 100.0);
else
printf("\r [%c] Progress: ?????\r", ticks[tick]);
fflush(stdout);
#endif
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary *)change context:(void *)context {
if ([keyPath isEqualToString:NSStringFromSelector(@selector(fractionCompleted))])
[self updateProgess: object];
else
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
- (void) installMacOS: (id) dummy {
#ifdef MACOS_GUEST
// beats me why Installer doesn't take VZMacOSRestoreImage ..
NSLog(@" installMacOS: from %@", _restorePath);
self.installer = [[VZMacOSInstaller alloc] initWithVirtualMachine:vm.virtualMachine restoreImageURL:[NSURL fileURLWithPath:_restorePath]];
[_installer.progress addObserver:self forKeyPath:NSStringFromSelector(@selector(fractionCompleted)) options:NSKeyValueObservingOptionInitial context:0];
installProgressTimer = [NSTimer timerWithTimeInterval: 1.0 target:self selector:@selector(updateProgess:) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:installProgressTimer forMode:NSRunLoopCommonModes];
[self.installer installWithCompletionHandler:^(NSError *err) {
dispatch_async(dispatch_get_main_queue(), ^{
if (self->installProgressTimer) {
[self->installProgressTimer invalidate];
self->installProgressTimer = nil;
}
NSLog(@" Installer done: %@", err ? err : @"OK");
if (err)
@throw [NSException exceptionWithName:@"MacOSInstall" reason:[err description] userInfo:nil];
});
}];
#endif
}
- (void) createVM: (id) dummy {
@try {
NSLog(@"Creating instance ...");
vm = [[VMInstance alloc] initWithSpec:spec];
VZVirtualMachine *vz = vm.virtualMachine;
vz.delegate = self;
#ifdef MACOS_GUEST
if (spec.restoreImage && _restorePath) {
/* dump config */
@try {
NSLog(@"Save configuration to %@ ...", _configPath);
NSOutputStream *ostr = [NSOutputStream outputStreamToFileAtPath:_configPath append:NO];
[ostr open];
[spec writeToJSON:ostr];
[ostr close];
}
@catch (NSException *ex) {
NSLog(@"WARNING: unable to save configuration to %@: %@", _configPath, [ex description]);
printf("--- dumping configuration to stdout ---\n");
NSOutputStream *ostr = [NSOutputStream outputStreamToFileAtPath:@"/dev/stdout" append:YES];
[ostr open];
[spec writeToJSON:ostr];
[ostr close];
printf("\n\n");
}
}
#endif
if (self.useGUI) {
NSLog(@"Create GUI");
view = [[VZVirtualMachineView alloc] init];
view.capturesSystemKeys = YES;
view.virtualMachine = vz;
NSRect rect = NSMakeRect(10, 10, 1024, 768);
window = [[NSWindow alloc] initWithContentRect: rect
styleMask:NSWindowStyleMaskTitled|NSWindowStyleMaskClosable|
NSWindowStyleMaskMiniaturizable|NSWindowStyleMaskResizable//|NSTexturedBackgroundWindowMask
backing:NSBackingStoreBuffered defer:NO];
[window setOpaque:NO];
[window setDelegate: self];
[window setContentView: view];
[window setInitialFirstResponder: view];
[window setTitle: @"VirtualMac"];
if (![NSApp mainMenu]) { /* normally, there is no menu so we have to create it */
NSLog(@"Create menu ...");
NSMenu *menu, *mainMenu = [[NSMenu alloc] init];
NSMenuItem *menuItem;
menu = [[NSMenu alloc] initWithTitle:@"Window"];
menuItem = [[NSMenuItem alloc] initWithTitle:@"Minimize" action:@selector(performMiniaturize:) keyEquivalent:@"m"]; [menu addItem:menuItem];
menuItem = [[NSMenuItem alloc] initWithTitle:@"Zoom" action:@selector(performZoom:) keyEquivalent:@""]; [menu addItem:menuItem];
menuItem = [[NSMenuItem alloc] initWithTitle:@"Close Window" action:@selector(performClose:) keyEquivalent:@"w"]; [menu addItem:menuItem];
menuItem = [[NSMenuItem alloc] initWithTitle:@"Copy" action:@selector(copy:) keyEquivalent:@"c"]; [menu addItem:menuItem];
menuItem = [[NSMenuItem alloc] initWithTitle:@"Paste" action:@selector(paste:) keyEquivalent:@"v"]; [menu addItem:menuItem];
menuItem = [[NSMenuItem alloc] initWithTitle:@"Window" action:nil keyEquivalent:@""];
[menuItem setSubmenu:menu];
[mainMenu addItem:menuItem];
[NSApp setMainMenu:mainMenu];
}
NSLog(@"Activate window...");
[window makeKeyAndOrderFront: view];
if (![[NSRunningApplication currentApplication] isActive]) {
NSLog(@"Make application active");
[[NSRunningApplication currentApplication] activateWithOptions:NSApplicationActivateAllWindows];
}
{ /* we have to make us foreground process so we can receive keyboard
events - I know of no way that doesn't involve deprecated API .. */
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
void CPSEnableForegroundOperation(ProcessSerialNumber* psn);
ProcessSerialNumber myProc;
if (GetCurrentProcess(&myProc) == noErr)
CPSEnableForegroundOperation(&myProc);
#pragma clang diagnostic pop
}
}
#ifdef MACOS_GUEST
if (spec.restoreImage && _restorePath) {
NSLog(@"Restore requested, starting macOS installer from %@", _restorePath);
NSLog(@"Installing macOS version %d.%d.%d (build %@)",
(int) spec.restoreImage.operatingSystemVersion.majorVersion,
(int) spec.restoreImage.operatingSystemVersion.minorVersion,
(int) spec.restoreImage.operatingSystemVersion.patchVersion,
spec.restoreImage.buildVersion);
dispatch_async(vm->queue, ^{
[self installMacOS:self];
});
} else {
#endif
NSLog(@"Starting instance...");
[vm start];
#ifdef MACOS_GUEST
}
#endif
}
@catch (NSException *ex){
NSLog(@"Exception in VM init: %@", ex);
[NSApp terminate:self];
}
#ifdef TEST_STOP
[self performSelector:@selector(timer:) withObject:self afterDelay:15.0];
#endif
NSLog(@" - start done");
}
#ifdef TEST_STOP /* testing */
- (void) timer: (id) foo {
dispatch_sync(vm->queue, ^{
NSLog(@"INFO: canStart: %@, canPause: %@, canResume: %@, canRequestStop: %@, state: %d",
vm.virtualMachine.canStart ? @"YES": @"NO",
vm.virtualMachine.canPause ? @"YES" : @"NO",
vm.virtualMachine.canResume ? @"YES" : @"NO",
vm.virtualMachine.canRequestStop? @"YES" : @"NO",
(int) vm.virtualMachine.state
);
});
NSLog(@"timer triggered, requesting stop");
[vm stop];
}
#endif
@end
static double parse_size(const char *val) {
const char *eov;
double vf = atof(val);
if (vf < 0) {
fprintf(stderr, "ERROR: invalid size '%s', may not be negative\n", val);
return -1.0;
}
eov = val;
while ((*eov >= '0' && *eov <= '9') || *eov == '.') eov++;
switch (*eov) {
case 'g': vf *= 1024.0;
case 'm': vf *= 1024.0;
case 'k': vf *= 1024.0; break;
case 0: break;
default:
fprintf(stderr, "ERROR: invalid size qualifier '%c', must be k, m or g\n", *eov);
return -1.0;
}
return vf;
}
#include <stdio.h>
#include <sys/stat.h>
static char *pid_file;
/* simple low-level registry of files to unlink on exit */
#define MAX_UNLINKS 32
static char *unlink_me[MAX_UNLINKS];
void add_unlink_on_exit(const char *fn) {
int i = 0;
while (i < MAX_UNLINKS) {
if (!unlink_me[i]) {
unlink_me[i] = strdup(fn);
return;
}
i++;
}
fprintf(stderr, "ERROR: too many ephemeral files, aborting\n");
exit(1);
}
static void cleanup() {
int i = 0;
while (i < MAX_UNLINKS) {
if (unlink_me[i]) {
printf("INFO: removing ephemeral %s\n", unlink_me[i]);
unlink(unlink_me[i]);
free(unlink_me[i]);
unlink_me[i] = 0;
}
i++;
}
if (pid_file)
unlink(pid_file);
}
#include <signal.h>
static sig_t orig_INT;
static sig_t orig_TERM;
static sig_t orig_KILL;
static sig_t orig_ABRT;
static void sig_handler(int sig) {
cleanup();
/* restore original behavior */
signal(sig, (sig == SIGINT) ? orig_INT :
((sig == SIGTERM) ? orig_TERM :
((sig == SIGKILL) ? orig_KILL :
((sig == SIGABRT) ? orig_ABRT : SIG_DFL))));
raise(sig);
}
static int unlink_handling_active = 0;
static void setup_unlink_handling() {
if (unlink_handling_active)
return;
/* regular termination */
atexit(cleanup);
/* signal termination */
orig_INT = signal(SIGINT, sig_handler);
orig_TERM= signal(SIGTERM, sig_handler);
orig_KILL= signal(SIGKILL, sig_handler);
orig_ABRT= signal(SIGABRT, sig_handler);
unlink_handling_active = 1;
}
int main(int ac, char**av) {
App *main = [[App alloc] init];
VMSpec *spec = [[VMSpec alloc] init];
main->spec = spec;
BOOL create = NO, ephemeral = NO;
NSString *configPath = nil;
NSString *macOverride = nil;
NSString *outputPath = nil;
int i = 0;
spec->use_serial = YES; /* we default to registering a serial console */
/* options that require an argument so we can skip them */
const char *multi_options[] = {
"--restore", "--vol", "--disk", "--usb", "--aux", "--initrd", "--net", "--save", "--mac", "--script", "--pid-file", 0
};
/* in retrospect this was a bad idea, but we have to find the config file
first since we want the options to override the contents of the config
file and not vice-versa. Hence Pass #1: find and load the config */
while (++i < ac)
if (av[i][0] == '-') {
switch (av[i][1]) {
case '-':
{
const char **opt = multi_options;
if (!strcmp(av[i], "--init") || !strcmp(av[i], "--restore"))
create = YES;
while (*opt)
if (!strcmp(av[i], *(opt++))) {
i++;
break;
}
break;
}
/* special case: -c and -r can be either single or multi */
case 'r':
case 'c':
if (!(av[i][2]))
i++;
break;
}
} else {
if (configPath) {
fprintf(stderr, "ERROR: configuration path can only be specified once\n");
return 1;
}
configPath = [NSString stringWithUTF8String:av[i]];
@try {
NSInputStream *istr = [NSInputStream inputStreamWithFileAtPath:configPath];
[istr open];
if ([istr streamError]) {
if (!create) { /* only in create mode (init/restore) it's ok to not have the config */
NSLog(@"Cannot open '%@': %@", configPath, [istr streamError]);
@throw [NSException exceptionWithName:@"ConfigError" reason:[[istr streamError] description] userInfo:nil];
}
} else {
[spec readFromJSON:istr];
}
[istr close];
}
@catch (NSException *ex) {
NSLog(@"ERROR: %@", [ex description]);
cleanup();
return 1;
}
}
/* Pass #2: process the options */
i = 0;
while (++i < ac)
if (av[i][0] == '-') {
if (av[i][1] == 'g' || !strcmp(av[i], "--gui")) {
main.useGUI = YES; continue;
}
if (!strcmp(av[i], "--ephemeral")) {
ephemeral = YES; continue;
}
if (!strcmp(av[i], "--restore")) {
if (++i >= ac) {
fprintf(stderr, "ERROR: %s missing file name", av[i-1]);
return 1;
}
printf("INFO: restore from %s\n", av[i]);
main.restorePath = [NSString stringWithUTF8String:av[i]];
if (![[NSFileManager defaultManager] fileExistsAtPath:main.restorePath]) {
fprintf(stderr, "ERROR: restore image '%s' not found\n", av[i]);
return 1;
}
create = YES; continue;
}
if (!strcmp(av[i], "--init")) {
create = YES; continue;
}
if (!strcmp(av[i], "--pid-file")) {
if (++i >= ac) {
fprintf(stderr, "ERROR: %s missing file name", av[i-1]);
return 1;
}
pid_file = av[i];
continue;
}
if (!strcmp(av[i], "--pty")) {
spec->use_serial = spec->pty = YES; continue;
}
if (!strcmp(av[i], "--no-serial")) {
spec->use_serial = NO; continue;
}
if (!strcmp(av[i], "--spice")) { /* SPICE clipboard sharing */
spec->spice = YES; continue;
}
if (!strcmp(av[i], "--recovery")) {
spec->use_recovery = YES; continue;
}
if (!strcmp(av[i], "--mac")) {
if (++i >= ac) {
fprintf(stderr, "ERROR: %s missing MAC address", av[i-1]);
return 1;
}
macOverride = [NSString stringWithUTF8String: av[i]];
}
if (!strcmp(av[i], "--vol")) {
BOOL readOnly = NO, autoMount = NO;
char *dop, *path = 0, *vol = 0;
if (++i >= ac) {
fprintf(stderr, "ERROR: %s missing share specification", av[i-1]);
return 1;
}
path = av[i]; /* first is path */
dop = strchr(path, ',');
while (dop) {
*dop = 0; dop++;
if (!strncmp(dop, "name=", 5))
vol = dop + 5;
else if (!strncmp(dop, "automount", 9))
autoMount = YES;
else if (!strncmp(dop, "ro", 2))
readOnly = YES;
else if (!strncmp(dop, "rw", 2))
readOnly = NO;
else {
fprintf(stderr, "ERROR: invalid share option: '%s'\n", dop);
return 1;
}
dop = strchr(dop, ',');
}
if (autoMount)
[spec addAutomountDirectoryShare: [NSString stringWithUTF8String: path]
readOnly: readOnly];
else
[spec addDirectoryShare: [NSString stringWithUTF8String: path]
volume: vol ? [NSString stringWithUTF8String:vol] : @"macosvm"
readOnly: readOnly];
}
if (!strcmp(av[i], "--disk") || !strcmp(av[i], "--usb") || !strcmp(av[i], "--aux") || !strcmp(av[i], "--initrd")) {
BOOL readOnly = NO;
BOOL keep = NO;
size_t create_size = 0;
char *c, *dop;
if (++i >= ac) {
fprintf(stderr, "ERROR: %s missing file name", av[i-1]);
return 1;
}
c = av[i];
dop = strchr(c, ',');
while (dop) {
*dop = 0; dop++;
if (!strncmp(dop, "ro", 2))
readOnly = YES;
else if (!strncmp(dop, "keep", 2))
keep = YES;
else if (!strncmp(dop, "size=", 5)) {
double sz = parse_size(dop + 5);
if (sz < 0)
return 1;
if (sz < 1024.0*1024.0*32.0) {
fprintf(stderr, "ERROR: invalid disk size, must be at least 32m\n");
return 1;
}
create_size = (size_t) sz;
} else {
fprintf(stderr, "ERROR: invalid disk option: '%s'\n", dop);
return 1;
}
dop = strchr(dop, ',');
}
if (create_size) {
FILE *f;
struct stat fst;
if (!stat(av[i], &fst)) {
fprintf(stderr, "ERROR: create size specified but file '%s' already exists\n",
av[i]);
return 1;
}
printf("INFO: creating new disk image %s with size %lu bytes\n", av[i], create_size);
f = fopen(av[i], "wb");
if (!f ||
fseek(f, create_size - 1, SEEK_SET) ||
fwrite("", 1, 1, f) != 1) {
fprintf(stderr, "ERROR: cannot create disk file %s\n", av[i]);
return 1;
}
fclose(f);
}
NSMutableArray *options = [[NSMutableArray alloc] init];
printf("INFO: add storage '%s' type '%s' %s %s\n", av[i],
av[i - 1] + 2, readOnly ? "read-only" : "read-write", keep ? "keep" : "");
if (readOnly) [options addObject:@"readOnly"];
if (keep) [options addObject:@"keep"];
[spec addFileStorage:[NSString stringWithUTF8String:av[i]]
type:[NSString stringWithUTF8String:av[i - 1] + 2]
options:options];
continue;
}
if (!strcmp(av[i], "--version")) {
const char *caps12 = "", *caps13 = "";
if (@available(macOS 12, *)) {
#ifdef __arm64__
caps12 = "Capabilities: macOS guest, vol";
#else
caps12 = "Capabilities: vol";
#endif
}
if (@available(macOS 13, *))
caps13 = ", vol:automount, net:unix:mtu, usb";
printf("macosvm %s\n\nCopyright (C) 2022-5 Simon Urbanek\nThere is NO warranty.\nLicenses: GPLv2 or GPLv3\n%s%s\n",
version, caps12, caps13);
return 0;
}
if (!strcmp(av[i], "--save")) {
if (++i >= ac) {
fprintf(stderr, "ERROR: %s missing file name\n", av[i - 1]);
return 1;
}
outputPath = [NSString stringWithUTF8String: av[i]];
continue;
}
if (!strcmp(av[i], "--script")) {
if (++i >= ac) {
fprintf(stderr, "ERROR: %s missing script name\n", av[i - 1]);
return 1;
}
[spec setSpawnScript: [NSString stringWithUTF8String:av[i]]];
continue;
}
if (!strcmp(av[i], "--net")) {
NSString *ifName = nil;
NSString *type = nil;
NSString *mac = nil;
NSNumber *mtu = nil;
char *c, *dop;
if (++i >= ac) {
fprintf(stderr, "ERROR: %s missing network specification", av[i-1]);
return 1;
}
c = av[i];
dop = strchr(c, ':');
if (dop) {
*dop = 0; dop++;
ifName = [NSString stringWithUTF8String:dop];
}
if (!strcmp(av[i], "nat")) {
type = @"nat";
if (ifName) {
mac = ifName;
ifName = nil;
}
printf("INFO: add NAT network %s%s\n",
mac ? "with MAC address " : "(random MAC address)",
mac ? [mac UTF8String] : "");
} else if (!strncmp(av[i], "br", 2)) {
type = @"bridge";
printf("INFO: add bridged network %s%s\n",
ifName ? "on interface " : "(no interface specified!)",
ifName ? [ifName UTF8String] : "");
} else if (!strncmp(av[i], "unix", 4)) {
char *c = dop;
type = @"unix";
if (!ifName) {
fprintf(stderr, "ERROR: unix socket network requires socket path\n");
return 1;
}
c = strchr(c, ',');
if (c) { /* additional options */
*c = 0; c++;
ifName = [NSString stringWithUTF8String: dop];
dop = c;
while (c) {
c = strchr(c, ',');
if (c) { *c = 0; c++; }
if (!strncmp(dop, "mac=", 4))
mac = [NSString stringWithUTF8String: dop + 4];
else if (!strncmp(dop, "mtu=", 4))
mtu = [NSNumber numberWithInteger: atoi(dop + 4)];
else {
fprintf(stderr, "ERROR: invalid option '%s' in --net unix\n", dop);
return 1;
}
}
}
} else {
fprintf(stderr, "ERROR: invalid network specification '%s'\n", av[i]);
return 1;
}
if ([type isEqualToString: @"unix"]) { /* in unix we (ab)use ifName for socket path */
if (mac && mtu)
[spec addNetworkSpecification: @{
@"type": type, @"path": ifName, @"mac": mac, @"mtu": mtu }];
else if (mac)
[spec addNetworkSpecification: @{
@"type": type, @"path": ifName, @"mac": mac }];
else if (mtu)
[spec addNetworkSpecification: @{
@"type": type, @"path": ifName, @"mtu": mtu }];
else
[spec addNetworkSpecification: @{
@"type": type, @"path": ifName }];
} else if (ifName && mac)
[spec addNetwork:type interface:ifName mac:mac];
else if (ifName)
[spec addNetwork:type interface:ifName];
else if (mac)
[spec addNetwork:type mac:mac];
else
[spec addNetwork:type];
continue;
}
switch(av[i][1]) {
case 'h':
{
printf("\n\
Usage: %s [-g|--[no-]gui] [--[no-]audio]\n\
[--restore <path>] [--ephemeral] [--recovery]\n\
[--{disk|usb} <path>[,ro][,size=<spec>][,keep]] [--aux <path>]\n\
[--vol <path>[,ro][,{name=<name>|automount}]]\n\
[--net <spec>] [--mac <addr>] [-c <cpus>] [-r <ram>]\n\
[--no-serial] [--pty] [--pid-file <path>] [--script <cmd>]\n\
<config.json>\n\
%s --version\n\
%s -h\n\
\n\
--restore requires path to ipsw image and will create aux as well as the configuration file.\n\
If no CPU/RAM is specified then image's minimal settings are used.\n\
\n\
If no --restore is performed then settings are read from the configuration file\n\
and only --gui / --audio options are honored.\n\
Size specifications allow suffix k, m and g for the powers of 1024.\n\
\n\
Network specification is <type>[:<options>], one of the following:\n\
nat[:<mac>] (NAT network, default, most common), br[:<interface>]\n\
(bridged network on <interface>), unix:<socket>[,mac=<mac>][,mtu=<mtu>]\n\
(unix socket to which all network traffic will be routed).\n\
Note that br requires special entitlement rarely given by Apple.\n\
\n\
Note that the --mac option is special and will override the first interface\n\
from the configuration file and/or --net (typically used with --ephemeral).\n\
\n\
Examples:\n\
# create a new VM with 32Gb disk image and macOS 12:\n\
%s --disk disk.img,size=32g --aux aux.img --restore UniversalMac_12.0.1_21A559_Restore.ipsw vm.json\n\
# start the created image with GUI:\n\
%s -g vm.json\n\
\n\
Experimental, use at your own risk!\n\
\n", av[0], av[0], av[0], av[0], av[0]);
return 0;
}
case 'c':
if (av[i][2]) spec->cpus = atoi(av[i] + 2); else {
if (++i >= ac) {
fprintf(stderr, "ERROR: %s missing CPU count\n", av[i - 1]);
return 1;
}
spec->cpus = atoi(av[i]);
printf("INFO: CPUs: %d\n", spec->cpus);
}
if (spec->cpus < 1) {
fprintf(stderr, "ERROR: invalid number of CPUs\n");
return 1;
}
break;
case 'r':
{
char *val;
if (av[i][2]) val = av[i] + 2; else {
if (++i >= ac) {
fprintf(stderr, "ERROR: %s missing RAM specification\n", av[i - 1]);
return 1;
}
val = av[i];
}
double vf = parse_size(val);
if (vf < 0)
return 1;
if (vf < 1024.0*1024.0*64.0) {
fprintf(stderr, "ERROR: invalid RAM size, must be at least 64m\n");
return 1;
}
spec->ram = (unsigned long) vf;
printf("INFO: RAM %lu\n", spec->ram);
break;
} /* -r */
}
if (av[i][1] == '-') {
if (!strcmp(av[i], "--no-gui")) { main.useGUI = NO; continue; }
if (!strcmp(av[i], "--no-audio")) { spec->audio = NO; continue; }
if (!strcmp(av[i], "--audio")) { spec->audio = YES; continue; }
}
}
if (create && !configPath) {
fprintf(stderr, "\nERROR: no configuration path supplied, try -h for help\n");
return 1;
}
main.configPath = configPath;
if (macOverride)
[spec setPrimaryMAC: macOverride];
if (outputPath) {
@try {
NSLog(@"Save configuration to %@ ...", outputPath);
NSOutputStream *ostr = [NSOutputStream outputStreamToFileAtPath:outputPath append:NO];
[ostr open];
[spec writeToJSON:ostr];
[ostr close];
}
@catch (NSException *ex) {
NSLog(@"ERROR: unable to save configuration to %@: %@", outputPath, [ex description]);
return 1;
}
}
if (ephemeral) {
/* register the callback first such that if something fails in the middle
the already created clones can be unlinked */
setup_unlink_handling();
[spec cloneAllStorage];
} else if (unlink_me[0]) /* outside of ephemeral unix sockets also use this */
setup_unlink_handling();
if (pid_file) {
FILE *f = fopen(pid_file, "w");
if (f) {
fprintf(f, "%lu\n", (unsigned long) getpid());
fclose(f);
/* Note: it will be unlinked by cleanup() on exit or signal */
} else
fprintf(stderr, "WARNING: cannot create pid-file '%s'\n", pid_file);
}
[NSApplication sharedApplication];
NSApp.delegate = main;
[NSApp run];
return 0;
}
================================================
FILE: macosvm.xcodeproj/project.pbxproj
================================================
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 55;
objects = {
/* Begin PBXBuildFile section */
68642D4B27434F1E00B70B7C /* Virtualization.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 68642D4A27434F1E00B70B7C /* Virtualization.framework */; };
68CB16482743A4890057E929 /* VMInstance.m in Sources */ = {isa = PBXBuildFile; fileRef = 68CB16472743A4890057E929 /* VMInstance.m */; };
68CB164A2743D8E90057E929 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 68CB16492743D8E90057E929 /* main.m */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
68642D3A27434B7600B70B7C /* CopyFiles */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = /usr/share/man/man1/;
dstSubfolderSpec = 0;
files = (
);
runOnlyForDeploymentPostprocessing = 1;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
68642D3C27434B7600B70B7C /* macosvm */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = macosvm; sourceTree = BUILT_PRODUCTS_DIR; };
68642D4827434E4A00B70B7C /* macosvm.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = macosvm.entitlements; sourceTree = "<group>"; };
68642D4A27434F1E00B70B7C /* Virtualization.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Virtualization.framework; path = System/Library/Frameworks/Virtualization.framework; sourceTree = SDKROOT; };
68CB16472743A4890057E929 /* VMInstance.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VMInstance.m; sourceTree = "<group>"; };
68CB16492743D8E90057E929 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
68CB164B2743D9320057E929 /* VMInstance.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VMInstance.h; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
68642D3927434B7600B70B7C /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
68642D4B27434F1E00B70B7C /* Virtualization.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
68642D3327434B7600B70B7C = {
isa = PBXGroup;
children = (
68642D3E27434B7600B70B7C /* macosvm */,
68642D3D27434B7600B70B7C /* Products */,
68642D4927434F1E00B70B7C /* Frameworks */,
);
sourceTree = "<group>";
};
68642D3D27434B7600B70B7C /* Products */ = {
isa = PBXGroup;
children = (
68642D3C27434B7600B70B7C /* macosvm */,
);
name = Products;
sourceTree = "<group>";
};
68642D3E27434B7600B70B7C /* macosvm */ = {
isa = PBXGroup;
children = (
68642D4827434E4A00B70B7C /* macosvm.entitlements */,
68CB16472743A4890057E929 /* VMInstance.m */,
68CB164B2743D9320057E929 /* VMInstance.h */,
68CB16492743D8E90057E929 /* main.m */,
);
path = macosvm;
sourceTree = "<group>";
};
68642D4927434F1E00B70B7C /* Frameworks */ = {
isa = PBXGroup;
children = (
68642D4A27434F1E00B70B7C /* Virtualization.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
68642D3B27434B7600B70B7C /* macosvm */ = {
isa = PBXNativeTarget;
buildConfigurationList = 68642D4327434B7600B70B7C /* Build configuration list for PBXNativeTarget "macosvm" */;
buildPhases = (
68642D3827434B7600B70B7C /* Sources */,
68642D3927434B7600B70B7C /* Frameworks */,
68642D3A27434B7600B70B7C /* CopyFiles */,
);
buildRules = (
);
dependencies = (
);
name = macosvm;
productName = macosvm;
productReference = 68642D3C27434B7600B70B7C /* macosvm */;
productType = "com.apple.product-type.tool";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
68642D3427434B7600B70B7C /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1310;
LastUpgradeCheck = 1310;
TargetAttributes = {
68642D3B27434B7600B70B7C = {
CreatedOnToolsVersion = 13.1;
LastSwiftMigration = 1310;
};
};
};
buildConfigurationList = 68642D3727434B7600B70B7C /* Build configuration list for PBXProject "macosvm" */;
compatibilityVersion = "Xcode 13.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 68642D3327434B7600B70B7C;
productRefGroup = 68642D3D27434B7600B70B7C /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
68642D3B27434B7600B70B7C /* macosvm */,
);
};
/* End PBXProject section */
/* Begin PBXSourcesBuildPhase section */
68642D3827434B7600B70B7C /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
68CB164A2743D8E90057E929 /* main.m in Sources */,
68CB16482743A4890057E929 /* VMInstance.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
68642D4127434B7600B70B7C /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ARCHS = arm64;
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 = 12.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;
};
68642D4227434B7600B70B7C /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ARCHS = arm64;
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 = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
};
name = Release;
};
68642D4427434B7600B70B7C /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = macosvm/macosvm.entitlements;
CODE_SIGN_STYLE = Automatic;
ENABLE_HARDENED_RUNTIME = YES;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
"@loader_path/../Frameworks",
);
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "macosvm/macosvm-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
};
name = Debug;
};
68642D4527434B7600B70B7C /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = macosvm/macosvm.entitlements;
CODE_SIGN_STYLE = Automatic;
ENABLE_HARDENED_RUNTIME = YES;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
"@loader_path/../Frameworks",
);
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "macosvm/macosvm-Bridging-Header.h";
SWIFT_VERSION = 5.0;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
68642D3727434B7600B70B7C /* Build configuration list for PBXProject "macosvm" */ = {
isa = XCConfigurationList;
buildConfigurations = (
68642D4127434B7600B70B7C /* Debug */,
68642D4227434B7600B70B7C /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
68642D4327434B7600B70B7C /* Build configuration list for PBXNativeTarget "macosvm" */ = {
isa = XCConfigurationList;
buildConfigurations = (
68642D4427434B7600B70B7C /* Debug */,
68642D4527434B7600B70B7C /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 68642D3427434B7600B70B7C /* Project object */;
}
gitextract_t669lnye/
├── LICENSE
├── Makefile
├── NEWS.md
├── README.md
├── macosvm/
│ ├── Makefile
│ ├── VMInstance.h
│ ├── VMInstance.m
│ ├── macosvm.entitlements
│ └── main.m
└── macosvm.xcodeproj/
└── project.pbxproj
SYMBOL INDEX (1 symbols across 1 files)
FILE: macosvm/VMInstance.h
function interface (line 8) | interface VMSpec : VZVirtualMachineConfiguration {
Condensed preview — 10 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (109K chars).
[
{
"path": "LICENSE",
"chars": 737,
"preview": "macosvm Virtualization Tool\nCopyright (C) 2021 Simon Urbanek\n\nThis program is free software; you can redistribute it and"
},
{
"path": "Makefile",
"chars": 62,
"preview": "all:\n\tmake -C macosvm macosvm\n\nclean:\n\tmake -C macosvm clean\n\n"
},
{
"path": "NEWS.md",
"chars": 6930,
"preview": "## NEWS\n\n### 0.2-3\n* added `--pid-file <path>` argument which writes the process id (pid) of the `macosvm` process into "
},
{
"path": "README.md",
"chars": 8483,
"preview": "## macosvm\n`macosvm` is a command line tool which allows creating and running of virtual machines on macOS 12 (Monterey)"
},
{
"path": "macosvm/Makefile",
"chars": 574,
"preview": "CFLAGS=-Wall\nLIBS=-framework AppKit -framework Virtualization -fobjc-arc -fobjc-link-runtime\nCC=clang\nifeq ($(DEVID),)\nD"
},
{
"path": "macosvm/VMInstance.h",
"chars": 2460,
"preview": "#import <Foundation/Foundation.h>\n#import <Virtualization/Virtualization.h>\n\n#ifdef __arm64__\n#define MACOS_GUEST 1\n#end"
},
{
"path": "macosvm/VMInstance.m",
"chars": 40847,
"preview": "#import \"VMInstance.h\"\n\n/* for cloneAllStorage */\n#include <sys/clonefile.h>\n#include <unistd.h>\n#include <sys/errno.h>\n"
},
{
"path": "macosvm/macosvm.entitlements",
"chars": 457,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "macosvm/main.m",
"chars": 31773,
"preview": "#import <Foundation/Foundation.h>\n\n#import \"VMInstance.h\"\n\nstatic const char *version = \"0.2-3\";\n\n@interface App : NSObj"
},
{
"path": "macosvm.xcodeproj/project.pbxproj",
"chars": 11655,
"preview": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 55;\n\tobjects = {\n\n/* Begin PBXBuildFile section *"
}
]
About this extraction
This page contains the full source code of the s-u/macosvm GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 10 files (101.5 KB), approximately 26.1k tokens, and a symbol index with 1 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.