//===----------------------------------------------------------------------===// // Copyright © 2025-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. //===----------------------------------------------------------------------===// // Source: https://github.com/opencontainers/image-spec/blob/main/specs-go/v1/config.go import ContainerizationError import Foundation /// Platform describes the platform which the image in the manifest runs on. public struct Platform: Sendable, Equatable { /// Normalizes a raw architecture string (e.g. from uname) to its OCI equivalent. static func normalizeArch(_ raw: String) -> (arch: String, variant: String?) { switch raw { case "aarch64", "arm64": return ("arm64", "v8") case "x86_64", "x86-64", "amd64": return ("amd64", nil) case "arm", "armhf", "armel": return ("arm", "v7") default: return (raw, nil) } } public static var current: Self { var systemInfo = utsname() uname(&systemInfo) let arch = withUnsafePointer(to: &systemInfo.machine) { $0.withMemoryRebound(to: CChar.self, capacity: 1) { String(cString: $0) } } let normalized = normalizeArch(arch) return .init(arch: normalized.arch, os: "linux", variant: normalized.variant) } /// The computed description, for example, `linux/arm64/v8`. public var description: String { let architecture = architecture if let variant = variant { return "\(os)/\(architecture)/\(variant)" } return "\(os)/\(architecture)" } /// The CPU architecture, for example, `amd64` or `arm64`. public var architecture: String { Self.normalizeArch(_rawArch).arch } /// The operating system, for example, `linux` or `windows`. public var os: String { _rawOS } /// An optional field specifying the operating system version, for example on Windows `10.0.14393.1066`. public var osVersion: String? /// An optional field specifying an array of strings, each listing a required OS feature (for example on Windows `win32k`). public var osFeatures: [String]? /// An optional field specifying a variant of the CPU, for example `v7` to specify ARMv7 when architecture is `arm`. public var variant: String? /// The operation system of the image (eg. `linux`). private let _rawOS: String /// The CPU architecture (eg. `arm64`). private let _rawArch: String public init(arch: String, os: String, osVersion: String? = nil, osFeatures: [String]? = nil, variant: String? = nil) { self._rawArch = arch self._rawOS = os self.osVersion = osVersion self.osFeatures = osFeatures self.variant = variant } /// Initializes a new platform from a string. /// - Parameters: /// - platform: A `string` value representing the platform. /// ```swift /// // Create a new `ImagePlatform` from string. /// let platform = try Platform(from: "linux/amd64") /// ``` /// ## Throws ## /// - Throws: `Error.missingOS` if input is empty /// - Throws: `Error.invalidOS` if os is not `linux` /// - Throws: `Error.missingArch` if only one `/` is present /// - Throws: `Error.invalidArch` if an unrecognized architecture is provided /// - Throws: `Error.invalidVariant` if a variant is provided, and it does not apply to the specified architecture public init(from platform: String) throws { let items = platform.split(separator: "/", maxSplits: 1) guard let osValue = items.first else { throw ContainerizationError(.invalidArgument, message: "missing OS in \(platform)") } switch osValue { case "linux", "windows", "darwin": _rawOS = osValue.description default: throw ContainerizationError(.invalidArgument, message: "unknown OS in \(osValue)") } guard items.count > 1 else { throw ContainerizationError(.invalidArgument, message: "missing architecture in \(platform)") } guard let archItems = items.last?.split(separator: "/", maxSplits: 1, omittingEmptySubsequences: false) else { throw ContainerizationError(.invalidArgument, message: "missing architecture in \(platform)") } guard let archName = archItems.first else { throw ContainerizationError(.invalidArgument, message: "missing architecture in \(platform)") } switch archName { case "arm", "armhf", "armel": _rawArch = "arm" variant = "v7" case "aarch64", "arm64": variant = "v8" _rawArch = "arm64" case "x86_64", "x86-64", "amd64": _rawArch = "amd64" default: _rawArch = archName.description } if archItems.count == 2 { guard let archVariant = archItems.last else { throw ContainerizationError(.invalidArgument, message: "missing variant in \(platform)") } switch archName { case "arm": switch archVariant { case "v5", "v6", "v7", "v8": variant = archVariant.description default: throw ContainerizationError(.invalidArgument, message: "invalid variant \(archVariant)") } case "armhf": switch archVariant { case "v7": variant = "v7" default: throw ContainerizationError(.invalidArgument, message: "invalid variant \(archVariant)") } case "armel": switch archVariant { case "v6": variant = "v6" default: throw ContainerizationError(.invalidArgument, message: "invalid variant \(archVariant)") } case "aarch64", "arm64": switch archVariant { case "v8", "8": variant = "v8" default: throw ContainerizationError(.invalidArgument, message: "invalid variant \(archVariant)") } case "x86_64", "x86-64", "amd64": switch archVariant { case "v1": variant = nil default: throw ContainerizationError(.invalidArgument, message: "invalid variant \(archVariant)") } case "i386", "386", "ppc64le", "riscv64": throw ContainerizationError(.invalidArgument, message: "invalid variant \(archVariant)") default: throw ContainerizationError(.invalidArgument, message: "invalid variant \(archVariant)") } } } } extension Platform: Hashable { /// `~=` compares two platforms to check if **lhs** platform images are compatible with **rhs** platform /// This operator can be used to check if an image of **lhs** platform can run on **rhs**: /// - `true`: when **rhs**=`arm/v8`, **lhs** is any of `arm/v8`, `arm/v7`, `arm/v6` and `arm/v5` /// - `true`: when **rhs**=`arm/v7`, **lhs** is any of `arm/v7`, `arm/v6` and `arm/v5` /// - `true`: when **rhs**=`arm/v6`, **lhs** is any of `arm/v6` and `arm/v5` /// - `true`: when **rhs**=`amd64`, **lhs** is any of `amd64` and `386` /// - `true`: when **rhs**=**lhs** /// - `false`: otherwise /// - Parameters: /// - lhs: platform whose compatibility is being checked /// - rhs: platform against which compatibility is being checked /// - Returns: `true | false` public static func ~= (lhs: Platform, rhs: Platform) -> Bool { if lhs.os == rhs.os { if lhs._rawArch == rhs._rawArch { switch rhs._rawArch { case "arm": guard let lVariant = lhs.variant else { return lhs == rhs } guard let rVariant = rhs.variant else { return lhs == rhs } switch rVariant { case "v8": switch lVariant { case "v5", "v6", "v7", "v8": return true default: return false } case "v7": switch lVariant { case "v5", "v6", "v7": return true default: return false } case "v6": switch lVariant { case "v5", "v6": return true default: return false } default: return lhs == rhs } default: return lhs == rhs } } if lhs._rawArch == "386" && rhs._rawArch == "amd64" { return true } } return false } /// `==` compares if **lhs** and **rhs** are the exact same platforms. public static func == (lhs: Platform, rhs: Platform) -> Bool { // NOTE: // If the platform struct was created by setting the fields directly and not using (from: String) // then, there is a possibility that for arm64 architecture, the variant may be set to nil // In that case, the variant should be assumed to v8 if lhs.architecture == "arm64" && rhs.architecture == "arm64" { // The following checks effectively verify // that one operand has nil value and other has "v8" if lhs.variant == nil || rhs.variant == nil { if lhs.variant == "v8" || rhs.variant == "v8" { return true } } } let osEqual = lhs.os == rhs.os let archEqual = lhs.architecture == rhs.architecture let variantEqual = lhs.variant == rhs.variant return osEqual && archEqual && variantEqual } public func hash(into hasher: inout Swift.Hasher) { hasher.combine(os) hasher.combine(architecture) // arm64 with no variant is equivalent to arm64/v8 per the == implementation if architecture == "arm64" { hasher.combine(variant ?? "v8") } else { hasher.combine(variant) } } } extension Platform: Codable { enum CodingKeys: String, CodingKey { case os = "os" case architecture = "architecture" case variant = "variant" } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(os, forKey: .os) try container.encode(architecture, forKey: .architecture) try container.encodeIfPresent(variant, forKey: .variant) } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let architecture = try container.decodeIfPresent(String.self, forKey: .architecture) guard let architecture else { throw ContainerizationError(.invalidArgument, message: "missing architecture") } let os = try container.decodeIfPresent(String.self, forKey: .os) guard let os else { throw ContainerizationError(.invalidArgument, message: "missing OS") } let variant = try container.decodeIfPresent(String.self, forKey: .variant) self.init(arch: architecture, os: os, variant: variant) } } public func createPlatformMatcher(for platform: Platform?) -> @Sendable (Platform) -> Bool { if let platform { return { other in platform == other } } return { _ in true } } public func filterPlatforms(matcher: (Platform) -> Bool, _ descriptors: [Descriptor]) throws -> [Descriptor] { var outDescriptors: [Descriptor] = [] for desc in descriptors { guard let p = desc.platform else { // pass along descriptor if the platform is not defined outDescriptors.append(desc) continue } if matcher(p) { outDescriptors.append(desc) } } return outDescriptors }