Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 16 additions & 8 deletions Sources/Containerization/ContainerManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -54,7 +55,8 @@ public struct ContainerManager: Sendable {
kernel: kernel,
initialFilesystem: initfs,
rosetta: rosetta,
nestedVirtualization: nestedVirtualization
nestedVirtualization: nestedVirtualization,
rosettaConfiguration: rosettaConfiguration
)
}

Expand All @@ -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)
Expand All @@ -80,7 +83,8 @@ public struct ContainerManager: Sendable {
kernel: kernel,
initialFilesystem: initfs,
rosetta: rosetta,
nestedVirtualization: nestedVirtualization
nestedVirtualization: nestedVirtualization,
rosettaConfiguration: rosettaConfiguration
)
}

Expand All @@ -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
Expand Down Expand Up @@ -121,7 +126,8 @@ public struct ContainerManager: Sendable {
kernel: kernel,
initialFilesystem: initfs,
rosetta: rosetta,
nestedVirtualization: nestedVirtualization
nestedVirtualization: nestedVirtualization,
rosettaConfiguration: rosettaConfiguration
)
}

Expand All @@ -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)
Expand Down Expand Up @@ -165,7 +172,8 @@ public struct ContainerManager: Sendable {
kernel: kernel,
initialFilesystem: initfs,
rosetta: rosetta,
nestedVirtualization: nestedVirtualization
nestedVirtualization: nestedVirtualization,
rosettaConfiguration: rosettaConfiguration
)
}

Expand Down
70 changes: 70 additions & 0 deletions Sources/Containerization/RosettaConfiguration.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
33 changes: 29 additions & 4 deletions Sources/Containerization/VZVirtualMachineInstance.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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")
Expand All @@ -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)
Expand Down Expand Up @@ -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:
Expand All @@ -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)
Expand Down
10 changes: 8 additions & 2 deletions Sources/Containerization/VZVirtualMachineManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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
Expand Down
6 changes: 5 additions & 1 deletion Sources/Containerization/Vminitd+Rosetta.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
44 changes: 44 additions & 0 deletions Tests/ContainerizationTests/RosettaConfigurationTests.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}