680845cb1c
Linux build / Determine Swift version (push) Waiting to run
Linux build / Linux compile check (push) Blocked by required conditions
Build containerization / Verify commit signatures (push) Has been skipped
Build containerization / containerization (push) Successful in 0s
341 lines
13 KiB
Swift
341 lines
13 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
// 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
|
|
}
|