From 3396d4e8720adf7202031c36c33ef1e3a6efc081 Mon Sep 17 00:00:00 2001 From: rcfa <1831995+rcfa@users.noreply.github.com> Date: Tue, 18 Aug 2026 22:10:00 +0200 Subject: [PATCH] Let callers choose the image platform in ContainerManager.create MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `create` resolves an image against `.current` in two places — the image config and the rootfs unpack: let imageConfig = try await image.config(for: .current).config // :295 return try await unpacker.unpack(image, for: .current, at: destination) // :346 So a container can only ever be created from an image variant matching the host architecture. On an arm64 host, creating a container from an amd64 image fails with `platform linux/arm64` — naming the platform that was requested rather than the one the image actually has, which reads as a corrupt or mistagged image. This is reachable even with Rosetta enabled on the `ContainerManager`: the failure happens at config resolution, before the runtime would apply any translation, so `rosetta: true` does not help and gives no hint as to why. Both underlying APIs already take a platform (`Image.config(for:)` and `EXT4Unpacker.unpack(_:for:at:progress:)`); only the `create` path pins it. This adds `platform: Platform = .current` to the three `create` overloads and the private `unpack`, passing it through. The default preserves existing behaviour, so the change is additive and no caller needs updating. Motivation: running SWE-bench's task images on Apple silicon. That corpus is mixed — of the 500 SWE-bench Verified instances, 281 publish arm64 images and 219 publish amd64 only — so a host-pinned platform makes 44% of the benchmark unrunnable, and evaluating only the remainder would report a score over a non-random subset. Verified on an arm64 host with both variants, same command shape: * arm64 image — conda env activates, `import astropy` succeeds * amd64 image — `uname -m` reports x86_64 under Rosetta, `import _pytest` succeeds The amd64 case is the one that could not be expressed before. --- .../Containerization/ContainerManager.swift | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/Sources/Containerization/ContainerManager.swift b/Sources/Containerization/ContainerManager.swift index 27e9fbe47..7c2309126 100644 --- a/Sources/Containerization/ContainerManager.swift +++ b/Sources/Containerization/ContainerManager.swift @@ -205,6 +205,7 @@ public struct ContainerManager: Sendable { writableLayerSizeInBytes: UInt64? = nil, readOnly: Bool = false, networking: Bool = true, + platform: Platform = .current, progress: ProgressHandler? = nil, configuration: (inout LinuxContainer.Configuration) throws -> Void ) async throws -> LinuxContainer { @@ -216,6 +217,7 @@ public struct ContainerManager: Sendable { writableLayerSizeInBytes: writableLayerSizeInBytes, readOnly: readOnly, networking: networking, + platform: platform, progress: progress, configuration: configuration ) @@ -231,6 +233,11 @@ public struct ContainerManager: Sendable { /// - readOnly: Whether to mount the root filesystem as read-only. /// - networking: Whether to create a network interface for this container. Defaults to `true`. /// When `false`, no network resources are allocated and `releaseNetwork`/`delete` remain safe to call. + /// - platform: Which platform's image variant to use. Defaults to `.current`, preserving the + /// previous behaviour. A caller running a FOREIGN-architecture image (e.g. an x86_64 image + /// on an arm64 host under Rosetta) must pass that image's platform: resolving the config + /// and unpacking the rootfs both need the variant that actually exists in the image, and + /// `.current` fails before any translation layer is consulted. /// - progress: Optional handler for tracking rootfs unpacking progress. public mutating func create( _ id: String, @@ -239,6 +246,7 @@ public struct ContainerManager: Sendable { writableLayerSizeInBytes: UInt64? = nil, readOnly: Bool = false, networking: Bool = true, + platform: Platform = .current, progress: ProgressHandler? = nil, configuration: (inout LinuxContainer.Configuration) throws -> Void ) async throws -> LinuxContainer { @@ -248,6 +256,7 @@ public struct ContainerManager: Sendable { image: image, destination: path.appendingPathComponent("rootfs.ext4"), size: rootfsSizeInBytes, + platform: platform, progress: progress ) if readOnly { @@ -269,6 +278,7 @@ public struct ContainerManager: Sendable { rootfs: rootfs, writableLayer: writableLayer, networking: networking, + platform: platform, configuration: configuration ) } @@ -290,9 +300,10 @@ public struct ContainerManager: Sendable { rootfs: Mount, writableLayer: Mount? = nil, networking: Bool = true, + platform: Platform = .current, configuration: (inout LinuxContainer.Configuration) throws -> Void ) async throws -> LinuxContainer { - let imageConfig = try await image.config(for: .current).config + let imageConfig = try await image.config(for: platform).config return try LinuxContainer( id, rootfs: rootfs, @@ -340,10 +351,13 @@ public struct ContainerManager: Sendable { return path } - private func unpack(image: Image, destination: URL, size: UInt64, progress: ProgressHandler? = nil) async throws -> Mount { + private func unpack( + image: Image, destination: URL, size: UInt64, platform: Platform = .current, + progress: ProgressHandler? = nil + ) async throws -> Mount { do { let unpacker = EXT4Unpacker(capacityInBytes: size) - return try await unpacker.unpack(image, for: .current, at: destination, progress: progress) + return try await unpacker.unpack(image, for: platform, at: destination, progress: progress) } catch let err as ContainerizationError { if err.code == .exists { return .block(