diff --git a/Sources/Containerization/ContainerManager.swift b/Sources/Containerization/ContainerManager.swift index 2e2edd7b..5478965d 100644 --- a/Sources/Containerization/ContainerManager.swift +++ b/Sources/Containerization/ContainerManager.swift @@ -45,7 +45,8 @@ public struct ContainerManager: Sendable { imageStore: ImageStore, network: Network? = nil, rosetta: Bool = false, - nestedVirtualization: Bool = false + nestedVirtualization: Bool = false, + rosettaConfiguration: RosettaConfiguration? = nil ) throws { self.imageStore = imageStore self.network = network @@ -54,7 +55,8 @@ public struct ContainerManager: Sendable { kernel: kernel, initialFilesystem: initfs, rosetta: rosetta, - nestedVirtualization: nestedVirtualization + nestedVirtualization: nestedVirtualization, + rosettaConfiguration: rosettaConfiguration ) } @@ -67,7 +69,8 @@ public struct ContainerManager: Sendable { root: URL? = nil, network: Network? = nil, rosetta: Bool = false, - nestedVirtualization: Bool = false + nestedVirtualization: Bool = false, + rosettaConfiguration: RosettaConfiguration? = nil ) throws { if let root { self.imageStore = try ImageStore(path: root) @@ -80,7 +83,8 @@ public struct ContainerManager: Sendable { kernel: kernel, initialFilesystem: initfs, rosetta: rosetta, - nestedVirtualization: nestedVirtualization + nestedVirtualization: nestedVirtualization, + rosettaConfiguration: rosettaConfiguration ) } @@ -93,7 +97,8 @@ public struct ContainerManager: Sendable { imageStore: ImageStore, network: Network? = nil, rosetta: Bool = false, - nestedVirtualization: Bool = false + nestedVirtualization: Bool = false, + rosettaConfiguration: RosettaConfiguration? = nil ) async throws { self.imageStore = imageStore self.network = network @@ -121,7 +126,8 @@ public struct ContainerManager: Sendable { kernel: kernel, initialFilesystem: initfs, rosetta: rosetta, - nestedVirtualization: nestedVirtualization + nestedVirtualization: nestedVirtualization, + rosettaConfiguration: rosettaConfiguration ) } @@ -133,7 +139,8 @@ public struct ContainerManager: Sendable { root: URL? = nil, network: Network? = nil, rosetta: Bool = false, - nestedVirtualization: Bool = false + nestedVirtualization: Bool = false, + rosettaConfiguration: RosettaConfiguration? = nil ) async throws { if let root { self.imageStore = try ImageStore(path: root) @@ -165,7 +172,8 @@ public struct ContainerManager: Sendable { kernel: kernel, initialFilesystem: initfs, rosetta: rosetta, - nestedVirtualization: nestedVirtualization + nestedVirtualization: nestedVirtualization, + rosettaConfiguration: rosettaConfiguration ) } diff --git a/Sources/Containerization/RosettaConfiguration.swift b/Sources/Containerization/RosettaConfiguration.swift new file mode 100644 index 00000000..6472d6f0 --- /dev/null +++ b/Sources/Containerization/RosettaConfiguration.swift @@ -0,0 +1,70 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the Containerization project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import Foundation + +/// Configuration for Rosetta x86_64 emulation in Linux virtual machines. +public struct RosettaConfiguration: Equatable, Sendable { + /// Translation caching configuration used by Rosetta. + public enum CachingOptions: Equatable, Sendable { + /// Use Virtualization.framework's default Unix domain socket. + case defaultUnixSocket + /// Use a Unix domain socket at the provided guest path. + case unixSocket(String) + /// Use a Linux abstract socket with the provided name. + case abstractSocket(String) + } + + /// Virtualization.framework's default Rosetta cache socket path. + public static let defaultUnixSocketPath = "/run/rosettad/rosetta.sock" + + /// Translation caching configuration. Set to `nil` to leave caching disabled. + public var cachingOptions: CachingOptions? + + public init(cachingOptions: CachingOptions? = .defaultUnixSocket) { + self.cachingOptions = cachingOptions + } + + /// Rosetta enabled with the default translation cache. + public static let cached = RosettaConfiguration() + + /// Rosetta enabled without translation caching. + public static let uncached = RosettaConfiguration(cachingOptions: nil) +} + +extension RosettaConfiguration.CachingOptions { + var unixSocketPath: String? { + switch self { + case .defaultUnixSocket: + RosettaConfiguration.defaultUnixSocketPath + case .unixSocket(let path): + path + case .abstractSocket: + nil + } + } + + var unixSocketDirectoryPath: String? { + guard let path = unixSocketPath else { + return nil + } + let directory = (path as NSString).deletingLastPathComponent + guard !directory.isEmpty, directory != "." else { + return nil + } + return directory + } +} diff --git a/Sources/Containerization/VZVirtualMachineInstance.swift b/Sources/Containerization/VZVirtualMachineInstance.swift index a711b2b5..8748fb9e 100644 --- a/Sources/Containerization/VZVirtualMachineInstance.swift +++ b/Sources/Containerization/VZVirtualMachineInstance.swift @@ -71,6 +71,8 @@ public final class VZVirtualMachineInstance: Sendable { public var memoryInBytes: UInt64 /// Toggle rosetta's x86_64 emulation support. public var rosetta: Bool + /// Configure Rosetta's translation cache when Rosetta is enabled. + public var rosettaCachingOptions: RosettaConfiguration.CachingOptions? /// Toggle nested virtualization support. public var nestedVirtualization: Bool /// Mount attachments organized by metadata ID. @@ -90,10 +92,15 @@ public final class VZVirtualMachineInstance: Sendable { self.cpus = 4 self.memoryInBytes = 1024.mib() self.rosetta = false + self.rosettaCachingOptions = .defaultUnixSocket self.nestedVirtualization = false self.mountsByID = [:] self.interfaces = [] } + + var rosettaEnabled: Bool { + self.rosetta + } } // `vm` isn't used concurrently. @@ -197,8 +204,8 @@ extension VZVirtualMachineInstance: VirtualMachineInstance { ) do { - if self.config.rosetta { - try await agent.enableRosetta() + if self.config.rosettaEnabled { + try await agent.enableRosetta(cachingOptions: self.config.rosettaCachingOptions) } } catch { try await agent.close() @@ -366,7 +373,7 @@ extension VZVirtualMachineInstance { } func prestart() async throws { - if self.config.rosetta { + if self.config.rosettaEnabled { #if arch(arm64) if VZLinuxRosettaDirectoryShare.availability == .notInstalled { self.logger?.info("installing rosetta") @@ -380,6 +387,23 @@ extension VZVirtualMachineInstance { } extension VZVirtualMachineInstance.Configuration { + #if arch(arm64) + private func vzRosettaCachingOptions() -> VZLinuxRosettaDirectoryShare.CachingOptions? { + guard let options = self.rosettaCachingOptions else { + return nil + } + + switch options { + case .defaultUnixSocket: + return .defaultUnixSocket + case .unixSocket(let path): + return .unixSocket(path) + case .abstractSocket(let name): + return .abstractSocket(name) + } + } + #endif + public static func installRosetta() async throws { do { #if arch(arm64) @@ -433,7 +457,7 @@ extension VZVirtualMachineInstance.Configuration { return try vzi.device() } - if self.rosetta { + if self.rosettaEnabled { #if arch(arm64) switch VZLinuxRosettaDirectoryShare.availability { case .notSupported: @@ -447,6 +471,7 @@ extension VZVirtualMachineInstance.Configuration { fallthrough case .installed: let share = try VZLinuxRosettaDirectoryShare() + try share.setCachingOptions(self.vzRosettaCachingOptions()) let device = VZVirtioFileSystemDeviceConfiguration(tag: "rosetta") device.share = share config.directorySharingDevices.append(device) diff --git a/Sources/Containerization/VZVirtualMachineManager.swift b/Sources/Containerization/VZVirtualMachineManager.swift index 4959bee4..4a2f3a98 100644 --- a/Sources/Containerization/VZVirtualMachineManager.swift +++ b/Sources/Containerization/VZVirtualMachineManager.swift @@ -26,6 +26,7 @@ public struct VZVirtualMachineManager: VirtualMachineManager { private let kernel: Kernel private let initialFilesystem: Mount private let rosetta: Bool + private let rosettaConfiguration: RosettaConfiguration? private let nestedVirtualization: Bool private let group: EventLoopGroup? private let logger: Logger? @@ -36,11 +37,13 @@ public struct VZVirtualMachineManager: VirtualMachineManager { rosetta: Bool = false, nestedVirtualization: Bool = false, group: EventLoopGroup? = nil, - logger: Logger? = nil + logger: Logger? = nil, + rosettaConfiguration: RosettaConfiguration? = nil ) { self.kernel = kernel self.initialFilesystem = initialFilesystem self.rosetta = rosetta + self.rosettaConfiguration = rosettaConfiguration self.nestedVirtualization = nestedVirtualization self.group = group self.logger = logger @@ -58,6 +61,8 @@ public struct VZVirtualMachineManager: VirtualMachineManager { // Clamp to system CPU count as Virtualization.framework bounds us to this. let cpus = min(vmConfig.cpus, ProcessInfo.processInfo.activeProcessorCount) + let rosettaConfiguration = self.rosettaConfiguration ?? (self.rosetta ? .cached : nil) + return try VZVirtualMachineInstance( group: self.group, logger: self.logger, @@ -73,7 +78,8 @@ public struct VZVirtualMachineManager: VirtualMachineManager { } instanceConfig.interfaces = vmConfig.interfaces - instanceConfig.rosetta = self.rosetta + instanceConfig.rosetta = rosettaConfiguration != nil + instanceConfig.rosettaCachingOptions = rosettaConfiguration?.cachingOptions instanceConfig.nestedVirtualization = useNestedVirtualization instanceConfig.mountsByID = vmConfig.mountsByID diff --git a/Sources/Containerization/Vminitd+Rosetta.swift b/Sources/Containerization/Vminitd+Rosetta.swift index af3a12f6..49e6eae0 100644 --- a/Sources/Containerization/Vminitd+Rosetta.swift +++ b/Sources/Containerization/Vminitd+Rosetta.swift @@ -18,7 +18,11 @@ import ContainerizationOS extension Vminitd { /// Enable Rosetta's x86_64 emulation. - public func enableRosetta() async throws { + public func enableRosetta(cachingOptions: RosettaConfiguration.CachingOptions? = .defaultUnixSocket) async throws { + if let socketDirectory = cachingOptions?.unixSocketDirectoryPath { + try await self.mkdir(path: socketDirectory, all: true, perms: 0o755) + } + let path = "/run/rosetta" try await self.mount( .init( diff --git a/Tests/ContainerizationTests/RosettaConfigurationTests.swift b/Tests/ContainerizationTests/RosettaConfigurationTests.swift new file mode 100644 index 00000000..4503bcc6 --- /dev/null +++ b/Tests/ContainerizationTests/RosettaConfigurationTests.swift @@ -0,0 +1,44 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the Containerization project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import Testing + +@testable import Containerization + +struct RosettaConfigurationTests { + @Test func rosettaConfigurationDefaultsToCachedTranslations() { + #expect(RosettaConfiguration().cachingOptions == .defaultUnixSocket) + } + + @Test func virtualMachineInstanceConfigurationDefaultsRosettaToCachedTranslations() { + var config = VZVirtualMachineInstance.Configuration() + config.rosetta = true + + #expect(config.rosettaCachingOptions == .defaultUnixSocket) + } + + @Test func defaultUnixSocketCreatesDefaultParentDirectory() { + #expect(RosettaConfiguration.CachingOptions.defaultUnixSocket.unixSocketDirectoryPath == "/run/rosettad") + } + + @Test func customUnixSocketCreatesParentDirectory() { + #expect(RosettaConfiguration.CachingOptions.unixSocket("/tmp/rosetta/cache.sock").unixSocketDirectoryPath == "/tmp/rosetta") + } + + @Test func abstractSocketDoesNotCreateDirectory() { + #expect(RosettaConfiguration.CachingOptions.abstractSocket("rosetta-cache").unixSocketDirectoryPath == nil) + } +}