diff --git a/.github/workflows/compile.yml b/.github/workflows/compile.yml index ca7d83f2..05245006 100644 --- a/.github/workflows/compile.yml +++ b/.github/workflows/compile.yml @@ -17,40 +17,51 @@ jobs: steps: - uses: actions/checkout@v4 - name: Run clang-format style check for Objective-C files. - uses: jidicula/clang-format-action@v4.8.0 + uses: jidicula/clang-format-action@v4.13.0 with: clang-format-version: '13' build: - runs-on: - - macos-14 + needs: formatting-check + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + os: + - macos-13 # Intel + - macos-14 + - macos-15 + go: + - '^1.22' + - '^1.23' steps: - name: Check out repository code uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: - go-version: '1.22' + go-version: ${{ matrix.go }} - name: vet run: go vet ./... - name: Build Linux run: make -C example/linux - name: Build GUI Linux run: make -C example/gui-linux + - name: Build macOS + run: make -C example/macOS + if: runner.arch == 'ARM64' test: - needs: formatting-check + needs: build runs-on: ${{ matrix.os }} - timeout-minutes: 7 + timeout-minutes: 30 strategy: fail-fast: false + # Can't expand the matrix due to the flakiness of the CI infra matrix: os: - - macos-11 - - macos-12 - - macos-13 - # - macos-14 # we cannot use this in testing. see: https://github.com/Code-Hex/vz/issues/109#issuecomment-1962815058 + - macos-13 # because macOS 14, 15 does not support build on nested virtualization go: - - '1.21' - - '1.22' + - '^1.23' steps: - name: Check out repository code uses: actions/checkout@v4 @@ -62,5 +73,4 @@ jobs: run: make download_kernel - name: Unit Test run: make test - timeout-minutes: 5 - + timeout-minutes: 10 \ No newline at end of file diff --git a/example/macOS/main.go b/example/macOS/main.go index aac5e68d..897702db 100644 --- a/example/macOS/main.go +++ b/example/macOS/main.go @@ -14,9 +14,11 @@ import ( ) var install bool +var nbdURL string func init() { flag.BoolVar(&install, "install", false, "run command as install mode") + flag.StringVar(&nbdURL, "nbd-url", "", "nbd url (e.g. nbd+unix:///export?socket=nbd.sock)") } func main() { @@ -142,21 +144,33 @@ func computeMemorySize() uint64 { } func createBlockDeviceConfiguration(diskPath string) (*vz.VirtioBlockDeviceConfiguration, error) { - // create disk image with 64 GiB - if err := vz.CreateDiskImage(diskPath, 64*1024*1024*1024); err != nil { - if !os.IsExist(err) { - return nil, fmt.Errorf("failed to create disk image: %w", err) + var attachment vz.StorageDeviceAttachment + var err error + + if nbdURL == "" { + // create disk image with 64 GiB + if err := vz.CreateDiskImage(diskPath, 64*1024*1024*1024); err != nil { + if !os.IsExist(err) { + return nil, fmt.Errorf("failed to create disk image: %w", err) + } } - } - diskImageAttachment, err := vz.NewDiskImageStorageDeviceAttachment( - diskPath, - false, - ) + attachment, err = vz.NewDiskImageStorageDeviceAttachment( + diskPath, + false, + ) + } else { + attachment, err = vz.NewNetworkBlockDeviceStorageDeviceAttachment( + nbdURL, + 10*time.Second, + false, + vz.DiskSynchronizationModeFull, + ) + } if err != nil { return nil, err } - return vz.NewVirtioBlockDeviceConfiguration(diskImageAttachment) + return vz.NewVirtioBlockDeviceConfiguration(attachment) } func createGraphicsDeviceConfiguration() (*vz.MacGraphicsDeviceConfiguration, error) { diff --git a/osversion.go b/osversion.go index ba54d633..02a8a373 100644 --- a/osversion.go +++ b/osversion.go @@ -105,6 +105,8 @@ func macOSBuildTargetAvailable(version float64) error { target = 130000 // __MAC_13_0 case 14: target = 140000 // __MAC_14_0 + case 15: + target = 150000 // __MAC_15_0 } if allowedVersion < target { return fmt.Errorf("%w for %.1f (the binary was built with __MAC_OS_X_VERSION_MAX_ALLOWED=%d; needs recompilation)", diff --git a/osversion_test.go b/osversion_test.go index dc4ae6a4..348fa077 100644 --- a/osversion_test.go +++ b/osversion_test.go @@ -319,6 +319,10 @@ func TestAvailableVersion(t *testing.T) { _, err := NewDiskBlockDeviceStorageDeviceAttachment(nil, false, DiskSynchronizationModeFull) return err }, + "NewNetworkBlockDeviceStorageDeviceAttachment": func() error { + _, err := NewNetworkBlockDeviceStorageDeviceAttachment("", 0, false, DiskSynchronizationModeFull) + return err + }, } for name, fn := range cases { t.Run(name, func(t *testing.T) { diff --git a/platform.go b/platform.go index 1c3b8f06..fc3bf8e6 100644 --- a/platform.go +++ b/platform.go @@ -6,6 +6,7 @@ package vz # include "virtualization_11.h" # include "virtualization_12.h" # include "virtualization_13.h" +# include "virtualization_15.h" */ import "C" import ( @@ -40,6 +41,28 @@ func (m *GenericPlatformConfiguration) MachineIdentifier() *GenericMachineIdenti return m.machineIdentifier } +// IsNestedVirtualizationSupported reports if nested virtualization is supported. +func IsNestedVirtualizationSupported() bool { + if err := macOSAvailable(15); err != nil { + return false + } + + return (bool)(C.isNestedVirtualizationSupported()) +} + +// SetNestedVirtualizationEnabled toggles nested virtualization. +func (m *GenericPlatformConfiguration) SetNestedVirtualizationEnabled(enable bool) error { + if err := macOSAvailable(15); err != nil { + return err + } + + C.setNestedVirtualizationEnabled( + objc.Ptr(m), + C.bool(enable), + ) + return nil +} + var _ PlatformConfiguration = (*GenericPlatformConfiguration)(nil) // NewGenericPlatformConfiguration creates a new generic platform configuration. diff --git a/storage.go b/storage.go index 53be874f..f1c51c43 100644 --- a/storage.go +++ b/storage.go @@ -12,6 +12,7 @@ package vz import "C" import ( "os" + "time" "github.com/Code-Hex/vz/v3/internal/objc" ) @@ -397,3 +398,59 @@ func NewDiskBlockDeviceStorageDeviceAttachment(file *os.File, readOnly bool, syn }) return attachment, nil } + +// NetworkBlockDeviceStorageDeviceAttachment is a storage device attachment that is backed by a +// NBD (Network Block Device) server. +// +// Using this attachment requires the app to have the com.apple.security.network.client entitlement +// because this attachment opens an outgoing network connection. +// +// For more information about the NBD URL format read: +// https://github.com/NetworkBlockDevice/nbd/blob/master/doc/uri.md +type NetworkBlockDeviceStorageDeviceAttachment struct { + *pointer + + *baseStorageDeviceAttachment +} + +var _ StorageDeviceAttachment = (*NetworkBlockDeviceStorageDeviceAttachment)(nil) + +// NewNetworkBlockDeviceStorageDeviceAttachment creates a new network block device storage attachment from an NBD +// Uniform Resource Indicator (URI) represented as a URL, timeout value, and read-only and synchronization modes +// that you provide. +// +// - url is the NBD server URI. The format specified by https://github.com/NetworkBlockDevice/nbd/blob/master/doc/uri.md +// - timeout is the duration for the connection between the client and server. When the timeout expires, an attempt to reconnect with the server takes place. +// - forcedReadOnly if true forces the disk attachment to be read-only, regardless of whether or not the NBD server supports write requests. +// - syncMode is one of the available DiskSynchronizationMode options. +// +// This is only supported on macOS 14 and newer, error will +// be returned on older versions. +func NewNetworkBlockDeviceStorageDeviceAttachment(url string, timeout time.Duration, forcedReadOnly bool, syncMode DiskSynchronizationMode) (*NetworkBlockDeviceStorageDeviceAttachment, error) { + if err := macOSAvailable(14); err != nil { + return nil, err + } + + nserrPtr := newNSErrorAsNil() + + urlChar := charWithGoString(url) + defer urlChar.Free() + attachment := &NetworkBlockDeviceStorageDeviceAttachment{ + pointer: objc.NewPointer( + C.newVZNetworkBlockDeviceStorageDeviceAttachment( + urlChar.CString(), + C.double(timeout.Seconds()), + C.bool(forcedReadOnly), + C.int(syncMode), + &nserrPtr, + ), + ), + } + if err := newNSError(nserrPtr); err != nil { + return nil, err + } + objc.SetFinalizer(attachment, func(self *NetworkBlockDeviceStorageDeviceAttachment) { + objc.Release(self) + }) + return attachment, nil +} diff --git a/virtualization_14.h b/virtualization_14.h index 680bc952..c8abb273 100644 --- a/virtualization_14.h +++ b/virtualization_14.h @@ -15,4 +15,5 @@ /* macOS 14 API */ void *newVZNVMExpressControllerDeviceConfiguration(void *attachment); -void *newVZDiskBlockDeviceStorageDeviceAttachment(int fileDescriptor, bool readOnly, int syncMode, void **error); \ No newline at end of file +void *newVZDiskBlockDeviceStorageDeviceAttachment(int fileDescriptor, bool readOnly, int syncMode, void **error); +void *newVZNetworkBlockDeviceStorageDeviceAttachment(const char *url, double timeout, bool forcedReadOnly, int syncMode, void **error); \ No newline at end of file diff --git a/virtualization_14.m b/virtualization_14.m index da692e98..f528518a 100644 --- a/virtualization_14.m +++ b/virtualization_14.m @@ -50,4 +50,37 @@ } #endif RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +/*! + @abstract Initialize a network block device storage attachment from an NBD URI. + @param uri The NBD’s URI represented as a URL. + @param timeout The timeout value in seconds for the connection between the client and server. When the timeout expires, an attempt to reconnect with the server takes place. + @param forcedReadOnly If YES, the framework forces the disk attachment to be read-only, regardless of whether or not the NBD server supports write requests. + @param synchronizationMode Defines how the disk synchronizes with the underlying storage when the guest operating system flushes data. + @param error If not nil, assigned with the error if the initialization failed. + @return An initialized `VZDiskBlockDeviceStorageDeviceAttachment` or nil if there was an error. + @discussion + The forcedReadOnly parameter affects how framework exposes the NBD client to the guest operating + system by the storage controller. As part of the NBD protocol, the NBD server advertises whether + or not the disk exposed by the NBD client is read-only during the handshake phase of the protocol. + + Setting forcedReadOnly to YES forces the NBD client to show up as read-only to the guest + regardless of whether or not the NBD server advertises itself as read-only. + */ +void *newVZNetworkBlockDeviceStorageDeviceAttachment(const char *uri, double timeout, bool forcedReadOnly, int syncMode, void **error) +{ +#ifdef INCLUDE_TARGET_OSX_14 + if (@available(macOS 14, *)) { + NSURL *url = [NSURL URLWithString:[NSString stringWithUTF8String:uri]]; + + return [[VZNetworkBlockDeviceStorageDeviceAttachment alloc] + initWithURL:url + timeout:(NSTimeInterval)timeout + forcedReadOnly:(BOOL)forcedReadOnly + synchronizationMode:(VZDiskSynchronizationMode)syncMode + error:(NSError *_Nullable *_Nullable)error]; + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); } \ No newline at end of file diff --git a/virtualization_15.h b/virtualization_15.h new file mode 100644 index 00000000..071e6b04 --- /dev/null +++ b/virtualization_15.h @@ -0,0 +1,15 @@ +// +// virtualization_15.h + +#pragma once + +// FIXME(codehex): this is dirty hack to avoid clang-format error like below +// "Configuration file(s) do(es) not support C++: /github.com/Code-Hex/vz/.clang-format" +#define NSURLComponents NSURLComponents + +#import "virtualization_helper.h" +#import + +/* macOS 15 API */ +bool isNestedVirtualizationSupported(); +void setNestedVirtualizationEnabled(void *config, bool nestedVirtualizationEnabled); diff --git a/virtualization_15.m b/virtualization_15.m new file mode 100644 index 00000000..29552673 --- /dev/null +++ b/virtualization_15.m @@ -0,0 +1,33 @@ +// +// virtualization_15.m +// +#import "virtualization_15.h" + +/*! + @abstract Check if nested virtualization is supported. + @return true if supported. + */ +bool isNestedVirtualizationSupported() +{ +#ifdef INCLUDE_TARGET_OSX_15 + if (@available(macOS 15, *)) { + return (bool) VZGenericPlatformConfiguration.isNestedVirtualizationSupported; + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +/*! + @abstract Set nestedVirtualizationEnabled. The default is false. + */ +void setNestedVirtualizationEnabled(void *config, bool nestedVirtualizationEnabled) +{ +#ifdef INCLUDE_TARGET_OSX_15 + if (@available(macOS 15, *)) { + VZGenericPlatformConfiguration *platformConfig = (VZGenericPlatformConfiguration *)config; + platformConfig.nestedVirtualizationEnabled = (BOOL) nestedVirtualizationEnabled; + return; + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} diff --git a/virtualization_helper.h b/virtualization_helper.h index 44b4227d..7914efdd 100644 --- a/virtualization_helper.h +++ b/virtualization_helper.h @@ -39,6 +39,13 @@ NSDictionary *dumpProcessinfo(); #pragma message("macOS 14 API has been disabled") #endif +// for macOS 15 API +#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 150000 +#define INCLUDE_TARGET_OSX_15 1 +#else +#pragma message("macOS 15 API has been disabled") +#endif + static inline int mac_os_x_version_max_allowed() { #ifdef __MAC_OS_X_VERSION_MAX_ALLOWED