chore: import upstream snapshot with attribution
container project - merge build / Invoke build (push) Successful in 1s

This commit is contained in:
wehub-resource-sync
2026-07-13 12:06:18 +08:00
commit e84fb4a79e
474 changed files with 68321 additions and 0 deletions
@@ -0,0 +1,70 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the container 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
/// A type that provides custom decoding logic for a specific `Decodable` type
/// when using `ConfigSnapshotDecoder`. This enables modification of `Decodable` behavior
/// for types that already conform to `Decodable`, such as `URL` and `Measurement`.
public protocol ConfigDecodingStrategy: Sendable {
associatedtype Value: Decodable
func decode(from decoder: Decoder) throws -> Value
}
/// A type that provides custom encoding logic for a specific `Encodable` type.
/// This enables modification of `Encodable` behavior for types that already conform
// to `Encodable`, such as `URL` and `Measurement`.
public protocol ConfigEncodingStrategy: Sendable {
associatedtype Value: Encodable
func encode(_ value: Value, to encoder: Encoder) throws
}
/// A type that provides both custom encoding and decoding for the same type. Similar to `Codable`
public typealias ConfigCodingStrategy = ConfigDecodingStrategy & ConfigEncodingStrategy
/// Decodes a `URL` from a string value. e.g. "https://www.apple.com"
public struct URLConfigDecodingStrategy: ConfigDecodingStrategy {
public init() {}
public func decode(from decoder: Decoder) throws -> URL {
let container = try decoder.singleValueContainer()
let string = try container.decode(String.self)
guard let url = URL(string: string) else {
throw DecodingError.dataCorrupted(
DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription: "Invalid URL string: \"\(string)\"."
)
)
}
return url
}
}
// A type erased version of `ConfigDecodingStrategy`
struct AnyConfigDecodingStrategy: Sendable {
let valueTypeID: ObjectIdentifier
private let decode: @Sendable (Decoder) throws -> Any
init<S: ConfigDecodingStrategy>(_ strategy: S) {
self.valueTypeID = ObjectIdentifier(S.Value.self)
self.decode = { decoder in try strategy.decode(from: decoder) as Any }
}
func decode(from decoder: Decoder) throws -> Any {
try decode(decoder)
}
}
@@ -0,0 +1,120 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the container 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 Configuration
/// A decoder that decodes `Decodable` types from a ``ConfigSnapshotReader``.
///
/// `ConfigSnapshotDecoder` bridges Swift's `Decodable` protocol with the configuration
/// provider system, allowing you to decode structured configuration into typed
/// Swift values.
///
/// ```swift
/// struct AppConfig: Decodable {
/// var host: String
/// var port: Int
/// var database: DatabaseConfig
/// }
///
/// struct DatabaseConfig: Decodable {
/// var connectionString: String
/// var maxConnections: Int
/// }
///
/// let reader = ConfigReader(providers: [envProvider, jsonProvider])
/// let snapshot = reader.snapshot()
/// let config = try ConfigSnapshotDecoder().decode(AppConfig.self, from: snapshot)
/// ```
///
/// Nested structs map to dot-separated key paths. In the example above,
/// `database.connectionString` and `database.maxConnections` are looked up
/// from the snapshot.
public struct ConfigSnapshotDecoder: Sendable {
public var userInfo: [CodingUserInfoKey: any Sendable]
private let typeDecodingStrategies: [ObjectIdentifier: AnyConfigDecodingStrategy]
public init(decodingStrategies: [any ConfigDecodingStrategy] = [URLConfigDecodingStrategy()]) {
self.userInfo = [:]
var strategies: [ObjectIdentifier: AnyConfigDecodingStrategy] = [:]
for strategy in decodingStrategies {
let erased = AnyConfigDecodingStrategy(strategy)
strategies[erased.valueTypeID] = erased
}
self.typeDecodingStrategies = strategies
}
public func decode<T: Decodable>(
_ type: T.Type,
from snapshot: ConfigSnapshotReader
) throws -> T {
if type is any UnsupportedDictionaryDecoding.Type {
throw DecodingError.typeMismatch(
T.self,
DecodingError.Context(
codingPath: [],
debugDescription:
"ConfigSnapshotDecoder does not support decoding dictionaries (got \(T.self)). Represent dynamic keys as nested structs with known property names."
)
)
}
let decoder = ConfigSnapshotDecoderImpl(
snapshot: snapshot,
codingPath: [],
userInfo: userInfo.mapValues { $0 as Any },
typeDecodingStrategies: typeDecodingStrategies
)
return try T(from: decoder)
}
}
struct ConfigSnapshotDecoderImpl: Decoder {
let snapshot: ConfigSnapshotReader
let codingPath: [any CodingKey]
let userInfo: [CodingUserInfoKey: Any]
let typeDecodingStrategies: [ObjectIdentifier: AnyConfigDecodingStrategy]
func container<Key: CodingKey>(
keyedBy type: Key.Type
) throws -> KeyedDecodingContainer<Key> {
KeyedDecodingContainer(
KeyedContainer<Key>(
snapshot: snapshot,
codingPath: codingPath,
userInfo: userInfo,
typeDecodingStrategies: typeDecodingStrategies
)
)
}
func unkeyedContainer() throws -> any UnkeyedDecodingContainer {
UnkeyedContainer(
snapshot: snapshot,
codingPath: codingPath,
userInfo: userInfo,
typeDecodingStrategies: typeDecodingStrategies
)
}
func singleValueContainer() throws -> any SingleValueDecodingContainer {
SingleValueContainer(
snapshot: snapshot,
codingPath: codingPath,
userInfo: userInfo,
typeDecodingStrategies: typeDecodingStrategies
)
}
}
@@ -0,0 +1,659 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the container 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 Configuration
import Foundation
// MARK: - Shared helpers
extension ConfigSnapshotReader {
/// Returns true when the snapshot holds a primitive value at `key`, regardless of
/// type. `ConfigSnapshotReader` stores typed values each primitive accessor returns
/// nil both when the key is absent and when the stored value is of a different type.
/// Callers that need to distinguish "absent" from "present but wrong type" must check
/// `hasValue` first, then the typed accessor.
func hasValue(forKey key: ConfigKey) -> Bool {
string(forKey: key) != nil
|| int(forKey: key) != nil
|| double(forKey: key) != nil
|| bool(forKey: key) != nil
}
}
/// Marker used by `decode<T>` to reject `Dictionary`-valued properties. The snapshot
/// has no key-enumeration API, so a `Dictionary` would silently decode to `[:]` via
/// `allKeys == []`. We reject the attempt explicitly instead.
protocol UnsupportedDictionaryDecoding {}
extension Dictionary: UnsupportedDictionaryDecoding {}
struct KeyedContainer<Key: CodingKey>: KeyedDecodingContainerProtocol {
let snapshot: ConfigSnapshotReader
let codingPath: [any CodingKey]
let userInfo: [CodingUserInfoKey: Any]
let typeDecodingStrategies: [ObjectIdentifier: AnyConfigDecodingStrategy]
// ConfigSnapshotReader has no "key exists" API, so allKeys cannot enumerate
// available keys and contains always returns true. This works for structs with
// known properties. Types that iterate allKeys for dynamic keys (e.g. dictionaries)
// will see an empty collection.
var allKeys: [Key] { [] }
func contains(_ key: Key) -> Bool { true }
// Always return false the flat config snapshot stores nested struct keys as
// dot-separated paths (e.g. "build.cpus") but has no entry for the parent key
// itself (e.g. "build"). Returning true here would cause decodeIfPresent to skip
// structs whose child keys DO exist. Instead, we always attempt to decode and
// rely on decodeIfPresent overrides for primitive optionals.
func decodeNil(forKey key: Key) throws -> Bool {
false
}
// MARK: - Primitive decodeIfPresent overrides
//
// Each override distinguishes three cases:
// 1. key absent return nil
// 2. key present, right type return the value
// 3. key present, wrong type throw DecodingError.typeMismatch
//
// The typed accessors on ConfigSnapshotReader collapse (1) and (3) into nil,
// so we use `hasValue` to disambiguate. Without this, a user config mistake
// like `cpus = "8"` would silently fall back to the property's default.
func decodeIfPresent(_ type: Bool.Type, forKey key: Key) throws -> Bool? {
let ck = configKey(appending: key)
guard snapshot.hasValue(forKey: ck) else { return nil }
guard let value = snapshot.bool(forKey: ck) else {
throw typeMismatch(Bool.self, at: ck, for: key)
}
return value
}
func decodeIfPresent(_ type: String.Type, forKey key: Key) throws -> String? {
let ck = configKey(appending: key)
guard snapshot.hasValue(forKey: ck) else { return nil }
guard let value = snapshot.string(forKey: ck) else {
throw typeMismatch(String.self, at: ck, for: key)
}
return value
}
func decodeIfPresent(_ type: Int.Type, forKey key: Key) throws -> Int? {
let ck = configKey(appending: key)
guard snapshot.hasValue(forKey: ck) else { return nil }
guard let value = snapshot.int(forKey: ck) else {
throw typeMismatch(Int.self, at: ck, for: key)
}
return value
}
func decodeIfPresent(_ type: Double.Type, forKey key: Key) throws -> Double? {
let ck = configKey(appending: key)
guard snapshot.hasValue(forKey: ck) else { return nil }
guard let value = snapshot.double(forKey: ck) else {
throw typeMismatch(Double.self, at: ck, for: key)
}
return value
}
func decodeIfPresent(_ type: Float.Type, forKey key: Key) throws -> Float? {
let ck = configKey(appending: key)
guard snapshot.hasValue(forKey: ck) else { return nil }
guard let value = snapshot.double(forKey: ck) else {
throw typeMismatch(Float.self, at: ck, for: key)
}
return Float(value)
}
func decodeIfPresent<T: Decodable>(_ type: T.Type, forKey key: Key) throws -> T? {
// For non-primitive Decodable types, always attempt decode.
// If the nested struct's init(from:) uses decodeIfPresent for its own keys,
// missing keys will resolve to defaults correctly.
// Catch keyNotFound/valueNotFound at this level they indicate the key is absent.
do {
return try decode(type, forKey: key)
} catch DecodingError.keyNotFound(let k, _) where k.stringValue == key.stringValue {
return nil
} catch DecodingError.valueNotFound {
return nil
}
}
func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool {
try decodeValue(forKey: key) { try snapshot.requiredBool(forKey: $0) }
}
func decode(_ type: String.Type, forKey key: Key) throws -> String {
try decodeValue(forKey: key) { try snapshot.requiredString(forKey: $0) }
}
func decode(_ type: Double.Type, forKey key: Key) throws -> Double {
try decodeValue(forKey: key) { try snapshot.requiredDouble(forKey: $0) }
}
func decode(_ type: Float.Type, forKey key: Key) throws -> Float {
Float(try decodeValue(forKey: key) { try snapshot.requiredDouble(forKey: $0) })
}
func decode(_ type: Int.Type, forKey key: Key) throws -> Int {
try decodeValue(forKey: key) { try snapshot.requiredInt(forKey: $0) }
}
func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 {
try integerValue(forKey: key)
}
func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 {
try integerValue(forKey: key)
}
func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 {
try integerValue(forKey: key)
}
func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 {
try integerValue(forKey: key)
}
func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt {
try integerValue(forKey: key)
}
func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 {
try integerValue(forKey: key)
}
func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 {
try integerValue(forKey: key)
}
func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 {
try integerValue(forKey: key)
}
func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 {
try integerValue(forKey: key)
}
func decode<T: Decodable>(_ type: T.Type, forKey key: Key) throws -> T {
if type is any UnsupportedDictionaryDecoding.Type {
throw DecodingError.typeMismatch(
T.self,
DecodingError.Context(
codingPath: codingPath + [key],
debugDescription:
"ConfigSnapshotDecoder does not support decoding dictionaries (got \(T.self)). Represent dynamic keys as nested structs with known property names."
)
)
}
let impl = ConfigSnapshotDecoderImpl(
snapshot: snapshot,
codingPath: codingPath + [key],
userInfo: userInfo,
typeDecodingStrategies: typeDecodingStrategies
)
if let strategy = typeDecodingStrategies[ObjectIdentifier(type)] {
guard let typed = try strategy.decode(from: impl) as? T else {
throw DecodingError.typeMismatch(
T.self,
DecodingError.Context(
codingPath: codingPath + [key],
debugDescription: "Strategy returned value of unexpected type for \(T.self)."
)
)
}
return typed
}
return try T(from: impl)
}
func nestedContainer<NestedKey: CodingKey>(
keyedBy type: NestedKey.Type,
forKey key: Key
) throws -> KeyedDecodingContainer<NestedKey> {
KeyedDecodingContainer(
KeyedContainer<NestedKey>(
snapshot: snapshot,
codingPath: codingPath + [key],
userInfo: userInfo,
typeDecodingStrategies: typeDecodingStrategies
)
)
}
func nestedUnkeyedContainer(forKey key: Key) throws -> any UnkeyedDecodingContainer {
UnkeyedContainer(
snapshot: snapshot,
codingPath: codingPath + [key],
userInfo: userInfo,
typeDecodingStrategies: typeDecodingStrategies
)
}
func superDecoder() throws -> any Decoder {
throw DecodingError.dataCorrupted(
DecodingError.Context(
codingPath: codingPath,
debugDescription: "ConfigSnapshotDecoder does not support superDecoder()."
)
)
}
func superDecoder(forKey key: Key) throws -> any Decoder {
throw DecodingError.dataCorrupted(
DecodingError.Context(
codingPath: codingPath + [key],
debugDescription: "ConfigSnapshotDecoder does not support superDecoder(forKey:)."
)
)
}
// MARK: - Private helpers
private func configKey(appending key: Key) -> ConfigKey {
ConfigKey(codingPath.map(\.stringValue) + [key.stringValue])
}
private func decodeValue<V>(forKey key: Key, _ body: (ConfigKey) throws -> V) throws -> V {
let configKey = configKey(appending: key)
do {
return try body(configKey)
} catch {
throw DecodingError.keyNotFound(
key,
DecodingError.Context(
codingPath: codingPath,
debugDescription: "No value found for key \"\(configKey)\"."
)
)
}
}
private func integerValue<T: FixedWidthInteger>(forKey key: Key) throws -> T {
let intValue: Int = try decodeValue(forKey: key) { try snapshot.requiredInt(forKey: $0) }
guard let converted = T(exactly: intValue) else {
throw DecodingError.typeMismatch(
T.self,
DecodingError.Context(
codingPath: codingPath + [key],
debugDescription: "Value \(intValue) does not fit in \(T.self)."
)
)
}
return converted
}
private func typeMismatch<T>(_ type: T.Type, at configKey: ConfigKey, for key: Key) -> DecodingError {
DecodingError.typeMismatch(
T.self,
DecodingError.Context(
codingPath: codingPath + [key],
debugDescription: "Expected \(T.self) at \"\(configKey)\" but found a value of a different type."
)
)
}
}
struct SingleValueContainer: SingleValueDecodingContainer {
let snapshot: ConfigSnapshotReader
let codingPath: [any CodingKey]
let userInfo: [CodingUserInfoKey: Any]
let typeDecodingStrategies: [ObjectIdentifier: AnyConfigDecodingStrategy]
// ConfigSnapshotReader stores typed values string(forKey:) returns nil for
// int/double/bool values. `hasValue` checks all primitive accessors.
func decodeNil() -> Bool {
!snapshot.hasValue(forKey: configKey())
}
func decode(_ type: Bool.Type) throws -> Bool {
try decodeValue { try snapshot.requiredBool(forKey: $0) }
}
func decode(_ type: String.Type) throws -> String {
try decodeValue { try snapshot.requiredString(forKey: $0) }
}
func decode(_ type: Double.Type) throws -> Double {
try decodeValue { try snapshot.requiredDouble(forKey: $0) }
}
func decode(_ type: Float.Type) throws -> Float {
Float(try decodeValue { try snapshot.requiredDouble(forKey: $0) })
}
func decode(_ type: Int.Type) throws -> Int {
try decodeValue { try snapshot.requiredInt(forKey: $0) }
}
func decode(_ type: Int8.Type) throws -> Int8 { try integerValue() }
func decode(_ type: Int16.Type) throws -> Int16 { try integerValue() }
func decode(_ type: Int32.Type) throws -> Int32 { try integerValue() }
func decode(_ type: Int64.Type) throws -> Int64 { try integerValue() }
func decode(_ type: UInt.Type) throws -> UInt { try integerValue() }
func decode(_ type: UInt8.Type) throws -> UInt8 { try integerValue() }
func decode(_ type: UInt16.Type) throws -> UInt16 { try integerValue() }
func decode(_ type: UInt32.Type) throws -> UInt32 { try integerValue() }
func decode(_ type: UInt64.Type) throws -> UInt64 { try integerValue() }
func decode<T: Decodable>(_ type: T.Type) throws -> T {
if type is any UnsupportedDictionaryDecoding.Type {
throw DecodingError.typeMismatch(
T.self,
DecodingError.Context(
codingPath: codingPath,
debugDescription:
"ConfigSnapshotDecoder does not support decoding dictionaries (got \(T.self)). Represent dynamic keys as nested structs with known property names."
)
)
}
let impl = ConfigSnapshotDecoderImpl(
snapshot: snapshot,
codingPath: codingPath,
userInfo: userInfo,
typeDecodingStrategies: typeDecodingStrategies
)
if let strategy = typeDecodingStrategies[ObjectIdentifier(type)] {
guard let typed = try strategy.decode(from: impl) as? T else {
throw DecodingError.typeMismatch(
T.self,
DecodingError.Context(
codingPath: codingPath,
debugDescription: "Strategy returned value of unexpected type for \(T.self)."
)
)
}
return typed
}
return try T(from: impl)
}
// MARK: - Private helpers
private func configKey() -> ConfigKey {
ConfigKey(codingPath.map(\.stringValue))
}
private func decodeValue<V>(_ body: (ConfigKey) throws -> V) throws -> V {
let configKey = configKey()
do {
return try body(configKey)
} catch {
throw DecodingError.valueNotFound(
V.self,
DecodingError.Context(
codingPath: codingPath,
debugDescription: "No value found for key \"\(configKey)\"."
)
)
}
}
private func integerValue<T: FixedWidthInteger>() throws -> T {
let intValue: Int = try decodeValue { try snapshot.requiredInt(forKey: $0) }
guard let converted = T(exactly: intValue) else {
throw DecodingError.typeMismatch(
T.self,
DecodingError.Context(
codingPath: codingPath,
debugDescription: "Value \(intValue) does not fit in \(T.self)."
)
)
}
return converted
}
}
struct UnkeyedContainer: UnkeyedDecodingContainer {
let snapshot: ConfigSnapshotReader
let codingPath: [any CodingKey]
let userInfo: [CodingUserInfoKey: Any]
let typeDecodingStrategies: [ObjectIdentifier: AnyConfigDecodingStrategy]
private enum ArrayValue {
case strings([String])
case ints([Int])
case doubles([Double])
case bools([Bool])
case unsupported
}
private var resolved: ArrayValue = .unsupported
private(set) var currentIndex: Int = 0
var count: Int? {
switch resolved {
case .strings(let a): a.count
case .ints(let a): a.count
case .doubles(let a): a.count
case .bools(let a): a.count
case .unsupported: nil
}
}
var isAtEnd: Bool {
switch resolved {
case .unsupported: false
default: currentIndex >= (count ?? 0)
}
}
init(
snapshot: ConfigSnapshotReader,
codingPath: [any CodingKey],
userInfo: [CodingUserInfoKey: Any],
typeDecodingStrategies: [ObjectIdentifier: AnyConfigDecodingStrategy]
) {
self.snapshot = snapshot
self.codingPath = codingPath
self.userInfo = userInfo
self.typeDecodingStrategies = typeDecodingStrategies
self.resolved = .unsupported
}
mutating func decodeNil() throws -> Bool { false }
mutating func decode(_ type: String.Type) throws -> String {
let array = try resolveStrings()
try checkBounds(array.count)
defer { currentIndex += 1 }
return array[currentIndex]
}
mutating func decode(_ type: Bool.Type) throws -> Bool {
let array = try resolveBools()
try checkBounds(array.count)
defer { currentIndex += 1 }
return array[currentIndex]
}
mutating func decode(_ type: Int.Type) throws -> Int {
let array = try resolveInts()
try checkBounds(array.count)
defer { currentIndex += 1 }
return array[currentIndex]
}
mutating func decode(_ type: Double.Type) throws -> Double {
let array = try resolveDoubles()
try checkBounds(array.count)
defer { currentIndex += 1 }
return array[currentIndex]
}
mutating func decode(_ type: Float.Type) throws -> Float {
Float(try decode(Double.self))
}
mutating func decode(_ type: Int8.Type) throws -> Int8 { try integerElement() }
mutating func decode(_ type: Int16.Type) throws -> Int16 { try integerElement() }
mutating func decode(_ type: Int32.Type) throws -> Int32 { try integerElement() }
mutating func decode(_ type: Int64.Type) throws -> Int64 { try integerElement() }
mutating func decode(_ type: UInt.Type) throws -> UInt { try integerElement() }
mutating func decode(_ type: UInt8.Type) throws -> UInt8 { try integerElement() }
mutating func decode(_ type: UInt16.Type) throws -> UInt16 { try integerElement() }
mutating func decode(_ type: UInt32.Type) throws -> UInt32 { try integerElement() }
mutating func decode(_ type: UInt64.Type) throws -> UInt64 { try integerElement() }
mutating func decode<T: Decodable>(_ type: T.Type) throws -> T {
if type == String.self { return try decode(String.self) as! T }
if type == Int.self { return try decode(Int.self) as! T }
if type == Double.self { return try decode(Double.self) as! T }
if type == Bool.self { return try decode(Bool.self) as! T }
if type == Float.self { return try decode(Float.self) as! T }
if type == Int8.self { return try decode(Int8.self) as! T }
if type == Int16.self { return try decode(Int16.self) as! T }
if type == Int32.self { return try decode(Int32.self) as! T }
if type == Int64.self { return try decode(Int64.self) as! T }
if type == UInt.self { return try decode(UInt.self) as! T }
if type == UInt8.self { return try decode(UInt8.self) as! T }
if type == UInt16.self { return try decode(UInt16.self) as! T }
if type == UInt32.self { return try decode(UInt32.self) as! T }
if type == UInt64.self { return try decode(UInt64.self) as! T }
throw DecodingError.dataCorrupted(
DecodingError.Context(
codingPath: codingPath,
debugDescription:
"ConfigSnapshotDecoder does not support decoding arrays of \(T.self). Only arrays of primitive types (String, Int, Double, Bool) are supported."
)
)
}
mutating func nestedContainer<NestedKey: CodingKey>(
keyedBy type: NestedKey.Type
) throws -> KeyedDecodingContainer<NestedKey> {
throw DecodingError.dataCorrupted(
DecodingError.Context(
codingPath: codingPath,
debugDescription: "ConfigSnapshotDecoder does not support nested containers inside arrays."
)
)
}
mutating func nestedUnkeyedContainer() throws -> any UnkeyedDecodingContainer {
throw DecodingError.dataCorrupted(
DecodingError.Context(
codingPath: codingPath,
debugDescription: "ConfigSnapshotDecoder does not support nested arrays."
)
)
}
func superDecoder() throws -> any Decoder {
throw DecodingError.dataCorrupted(
DecodingError.Context(
codingPath: codingPath,
debugDescription: "ConfigSnapshotDecoder does not support superDecoder()."
)
)
}
// MARK: - Private helpers
private func configKey() -> ConfigKey {
ConfigKey(codingPath.map(\.stringValue))
}
private mutating func resolveStrings() throws -> [String] {
if case .strings(let a) = resolved { return a }
let configKey = configKey()
guard let result = snapshot.stringArray(forKey: configKey) else {
throw DecodingError.valueNotFound(
[String].self,
DecodingError.Context(
codingPath: codingPath,
debugDescription: "No string array found for key \"\(configKey)\"."
)
)
}
resolved = .strings(result)
return result
}
private mutating func resolveInts() throws -> [Int] {
if case .ints(let a) = resolved { return a }
let configKey = configKey()
guard let result = snapshot.intArray(forKey: configKey) else {
throw DecodingError.valueNotFound(
[Int].self,
DecodingError.Context(
codingPath: codingPath,
debugDescription: "No int array found for key \"\(configKey)\"."
)
)
}
resolved = .ints(result)
return result
}
private mutating func resolveDoubles() throws -> [Double] {
if case .doubles(let a) = resolved { return a }
let configKey = configKey()
guard let result = snapshot.doubleArray(forKey: configKey) else {
throw DecodingError.valueNotFound(
[Double].self,
DecodingError.Context(
codingPath: codingPath,
debugDescription: "No double array found for key \"\(configKey)\"."
)
)
}
resolved = .doubles(result)
return result
}
private mutating func resolveBools() throws -> [Bool] {
if case .bools(let a) = resolved { return a }
let configKey = configKey()
guard let result = snapshot.boolArray(forKey: configKey) else {
throw DecodingError.valueNotFound(
[Bool].self,
DecodingError.Context(
codingPath: codingPath,
debugDescription: "No bool array found for key \"\(configKey)\"."
)
)
}
resolved = .bools(result)
return result
}
private mutating func integerElement<T: FixedWidthInteger>() throws -> T {
let intValue = try decode(Int.self)
guard let converted = T(exactly: intValue) else {
throw DecodingError.typeMismatch(
T.self,
DecodingError.Context(
codingPath: codingPath,
debugDescription: "Value \(intValue) does not fit in \(T.self)."
)
)
}
return converted
}
private func checkBounds(_ count: Int) throws {
guard currentIndex < count else {
throw DecodingError.valueNotFound(
Any.self,
DecodingError.Context(
codingPath: codingPath,
debugDescription: "Unkeyed container is at end (index \(currentIndex), count \(count))."
)
)
}
}
}
@@ -0,0 +1,218 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the container 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 Configuration
import ConfigurationTOML
import ContainerizationError
import Foundation
import SystemPackage
public protocol Initable {
init()
}
public typealias LoadableConfiguration = Codable & Sendable & Initable
public protocol LoadablePluginConfiguration: LoadableConfiguration {
static var pluginId: String { get }
}
public enum ConfigurationLoader {
private static let configFilename = "config.toml"
private static let configDirectory = "config"
private static let READ_ONLY: Int = 0o444
private static let READ_AND_WRITE: Int = 0o644
/// Returns the configuration file path for a given base kind, resolving the base
/// directory via `BaseConfigPath.basePath()` (env-driven, with fallbacks).
///
/// Use `configurationFile(in:of:)` when you need to supply an explicit base
/// e.g. a CLI flag like `--app-root` that bypasses env lookup.
///
/// - Parameter kind: The base directory role to resolve.
public static func configurationFile(_ kind: PathUtils.BaseConfigPath) -> FilePath {
configurationFile(in: kind.basePath(), of: kind)
}
/// Returns the configuration file path under an explicit base directory.
///
/// Path shape depends on `kind`:
/// - `.home`: `<base>/config.toml` (user source under `~/.config/container`)
/// - e.g. `~/.config/container/config.toml`
/// - `.appRoot`: `<base>/config/config.toml` (read-only copy of user config)
/// - e.g. `~/Library/Application Support/com.apple.container/config/config.toml`
/// - `.installRoot`: `<base>/etc/container/config.toml` (system defaults shipped with install)
/// - e.g. `/usr/local/etc/container/config.toml`
///
/// - Parameters:
/// - base: Directory to resolve against.
/// - kind: Base directory role. Defaults to `.appRoot`.
public static func configurationFile(
in base: FilePath,
of kind: PathUtils.BaseConfigPath = .appRoot
) -> FilePath {
switch kind {
case .home: base.appending(configFilename)
case .appRoot: base.appending(configDirectory).appending(configFilename)
case .installRoot: base.appending("etc/container").appending(configFilename)
}
}
/// Default ordered TOML layers consumed by `load` and `loadForPlugin`:
/// user config (`.appRoot`) followed by system defaults (`.installRoot`).
public static func defaultConfigFiles() -> [FilePath] {
[
configurationFile(.appRoot),
configurationFile(.installRoot),
]
}
/// Load the `ContainerSystemConfig` by layering TOML files with first-match-wins precedence.
///
/// Providers are consulted in the order given values from earlier files override
/// later ones. The default order is user config (`<appRoot>/config/config.toml`)
/// > system config (`<installRoot>/etc/container/config/config.toml`).
///
/// An empty `configurationFiles` array falls back to `defaultConfigFiles()`.
///
/// When a key is absent from every file, `ContainerSystemConfig.init(from:)` uses
/// `decodeIfPresent` and falls back to the property's default value "code defaults"
/// are not a provider layer.
///
/// Missing files are tolerated; malformed TOML still throws.
///
/// - Parameter configurationFiles: Ordered TOML layers, highest precedence first.
/// Defaults to `defaultConfigFiles()`.
/// - Returns: The decoded `ContainerSystemConfig`.
/// - Throws: `ContainerizationError.invalidArgument` if any layer fails to load or decode.
public static func load(
configurationFiles: [FilePath] = defaultConfigFiles()
) async throws -> ContainerSystemConfig {
try await loadAndDecode(
ContainerSystemConfig.self,
configurationFiles: configurationFiles,
decodeErrorContext: "failed to decode configuration"
)
}
/// Load a plugin-scoped configuration from the `[plugin.<P.pluginId>]` section of
/// the layered TOML files.
///
/// Uses the same layering and precedence rules as `load`, but scopes the snapshot
/// to `plugin.<P.pluginId>` before decoding. A missing `[plugin.<P.pluginId>]`
/// section falls back to `P()`.
///
/// - Parameter configurationFiles: Ordered TOML layers, highest precedence first.
/// Defaults to `defaultConfigFiles()`.
/// - Returns: The decoded plugin configuration, or `P()` if no files exist.
/// - Throws: `ContainerizationError.invalidArgument` if `P.pluginId` is empty, a
/// layer fails to load, or the `[plugin.<P.pluginId>]` section is malformed.
public static func loadForPlugin<P: LoadablePluginConfiguration>(
configurationFiles: [FilePath] = defaultConfigFiles()
) async throws -> P {
let id = P.pluginId
guard !id.isEmpty else {
throw ContainerizationError(.invalidArgument, message: "plugin id must not be empty")
}
return try await loadAndDecode(
P.self,
configurationFiles: configurationFiles,
scope: ConfigKey("plugin.\(id)"),
decodeErrorContext: "failed to decode plugin configuration for '\(id)'"
)
}
/// Shared implementation for `load` and `loadForPlugin`. Builds TOML providers
/// from `configurationFiles`, optionally scopes the snapshot, then decodes into `T`.
/// Short-circuits to `T()` when every path is missing on disk.
///
/// - Parameters:
/// - type: The concrete `LoadableConfiguration` type to decode.
/// - configurationFiles: Ordered TOML layers; empty falls back to `defaultConfigFiles()`.
/// - scope: Optional `ConfigKey` to scope the snapshot before decoding.
/// - decodeErrorContext: Prefix used in the `invalidArgument` error thrown on decode failure.
private static func loadAndDecode<T: LoadableConfiguration>(
_ type: T.Type,
configurationFiles: [FilePath],
scope: ConfigKey? = nil,
decodeErrorContext: String
) async throws -> T {
let paths = configurationFiles.isEmpty ? defaultConfigFiles() : configurationFiles
let fm = FileManager.default
if paths.allSatisfy({ !fm.fileExists(atPath: $0.string) }) {
return T()
}
var providers: [FileProvider<TOMLSnapshot>] = []
for path in paths {
do {
try providers.append(await FileProvider<TOMLSnapshot>(filePath: path, allowMissing: true))
} catch {
throw ContainerizationError(
.invalidArgument,
message: "failed to load configuration from '\(path)': \(error)"
)
}
}
let reader = ConfigReader(providers: providers)
let snapshot = scope.map { reader.snapshot().scoped(to: $0) } ?? reader.snapshot()
do {
return try ConfigSnapshotDecoder().decode(T.self, from: snapshot)
} catch {
throw ContainerizationError(
.invalidArgument,
message: "\(decodeErrorContext): \(error)"
)
}
}
/// Copies the user's runtime configuration into the app-root as a read-only snapshot.
///
/// If `source` does not exist, this is a no-op. Otherwise, any existing destination
/// is deleted and replaced with a fresh copy, which is then marked read-only.
///
/// - Parameters:
/// - source: File to copy from. Defaults to `<home>/container/config.toml`.
/// - destination: Directory to copy into the filename is appended automatically.
/// Defaults to `<appRoot>/config/config.toml`.
public static func copyConfigurationToReadOnly(
from source: FilePath? = nil,
to destination: FilePath? = nil
) throws {
let sourcePath = source ?? configurationFile(.home)
let destBase = destination ?? PathUtils.BaseConfigPath.appRoot.basePath()
let destPath = configurationFile(in: destBase)
let fm = FileManager.default
if fm.fileExists(atPath: destPath.string) {
try fm.setAttributes([.posixPermissions: READ_AND_WRITE], ofItemAtPath: destPath.string)
try fm.removeItem(at: URL(filePath: destPath.string))
}
guard fm.fileExists(atPath: sourcePath.string) else { return }
let destDir = destPath.removingLastComponent()
try fm.createDirectory(atPath: destDir.string, withIntermediateDirectories: true)
try fm.copyItem(
at: URL(filePath: sourcePath.string),
to: URL(filePath: destPath.string)
)
try fm.setAttributes([.posixPermissions: READ_ONLY], ofItemAtPath: destPath.string)
}
}
@@ -0,0 +1,238 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the container 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 CVersion
import ContainerVersion
import ContainerizationExtras
import Foundation
/// Top-level configuration decoded from config.toml.
///
/// Each section maps to a nested struct. Missing keys fall back to
/// hardcoded defaults via custom `init(from:)` implementations.
public final class ContainerSystemConfig: Codable, Sendable, Initable {
public let build: BuildConfig
public let container: ContainerConfig
public let dns: DNSConfig
public let kernel: KernelConfig
public let machine: MachineConfig
public let network: NetworkConfig
public let registry: RegistryConfig
public let vminit: VminitConfig
public init(
build: BuildConfig = .init(),
container: ContainerConfig = .init(),
dns: DNSConfig = .init(),
kernel: KernelConfig = .init(),
machine: MachineConfig = MachineConfig.default,
network: NetworkConfig = .init(),
registry: RegistryConfig = .init(),
vminit: VminitConfig = .init()
) {
self.build = build
self.container = container
self.dns = dns
self.kernel = kernel
self.machine = machine
self.network = network
self.registry = registry
self.vminit = vminit
}
public init() {
self.build = .init()
self.container = .init()
self.dns = .init()
self.kernel = .init()
self.machine = MachineConfig.default
self.network = .init()
self.registry = .init()
self.vminit = .init()
}
public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.build = try container.decodeIfPresent(BuildConfig.self, forKey: .build) ?? .init()
self.container = try container.decodeIfPresent(ContainerConfig.self, forKey: .container) ?? .init()
self.dns = try container.decodeIfPresent(DNSConfig.self, forKey: .dns) ?? .init()
self.kernel = try container.decodeIfPresent(KernelConfig.self, forKey: .kernel) ?? .init()
self.machine = try container.decodeIfPresent(MachineConfig.self, forKey: .machine) ?? MachineConfig.default
self.network = try container.decodeIfPresent(NetworkConfig.self, forKey: .network) ?? .init()
self.registry = try container.decodeIfPresent(RegistryConfig.self, forKey: .registry) ?? .init()
self.vminit = try container.decodeIfPresent(VminitConfig.self, forKey: .vminit) ?? .init()
}
}
final public class BuildConfig: Codable, Sendable {
public static let defaultRosetta = true
public static let defaultCPUs = 2
public static let defaultMemory = try! MemorySize("2048MB")
public static var defaultImage: String {
let tag = String(cString: get_container_builder_shim_version())
return "ghcr.io/apple/container-builder-shim/builder:\(tag)"
}
public let rosetta: Bool
public let cpus: Int
public let memory: MemorySize
public let image: String
public init(
rosetta: Bool = defaultRosetta,
cpus: Int = defaultCPUs,
memory: MemorySize = defaultMemory,
image: String = defaultImage
) {
self.rosetta = rosetta
self.cpus = cpus
self.memory = memory
self.image = image
}
public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.rosetta = try container.decodeIfPresent(Bool.self, forKey: .rosetta) ?? Self.defaultRosetta
self.cpus = try container.decodeIfPresent(Int.self, forKey: .cpus) ?? Self.defaultCPUs
self.memory = try container.decodeIfPresent(MemorySize.self, forKey: .memory) ?? Self.defaultMemory
self.image = try container.decodeIfPresent(String.self, forKey: .image) ?? Self.defaultImage
}
}
final public class ContainerConfig: Codable, Sendable {
public static let defaultCPUs = 4
public static let defaultMemory = try! MemorySize("1g")
public let cpus: Int
public let memory: MemorySize
public init(cpus: Int = defaultCPUs, memory: MemorySize = defaultMemory) {
self.cpus = cpus
self.memory = memory
}
public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.cpus = try container.decodeIfPresent(Int.self, forKey: .cpus) ?? Self.defaultCPUs
self.memory = try container.decodeIfPresent(MemorySize.self, forKey: .memory) ?? Self.defaultMemory
}
}
final public class DNSConfig: Codable, Sendable {
public let domain: String?
public init(domain: String? = nil) {
self.domain = domain
}
public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.domain = try container.decodeIfPresent(String.self, forKey: .domain)
}
}
final public class VminitConfig: Codable, Sendable {
public static var defaultImage: String {
let tag = String(cString: get_swift_containerization_version())
return tag == "latest"
? "vminit:latest"
: "ghcr.io/apple/containerization/vminit:\(tag)"
}
public let image: String
public init(image: String = defaultImage) {
self.image = image
}
public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.image = try container.decodeIfPresent(String.self, forKey: .image) ?? Self.defaultImage
}
}
final public class KernelConfig: Codable, Sendable {
public static let defaultBinaryPath = "opt/kata/share/kata-containers/vmlinux-6.18.15-186"
public static let defaultURL: URL =
URL(string: "https://github.com/kata-containers/kata-containers/releases/download/3.28.0/kata-static-3.28.0-arm64.tar.zst")!
private enum CodingKeys: String, CodingKey {
case binaryPath
case url
}
public let binaryPath: String
public let url: URL
public init(
binaryPath: String = defaultBinaryPath,
url: URL = defaultURL
) {
self.binaryPath = binaryPath
self.url = url
}
public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.binaryPath =
try container.decodeIfPresent(String.self, forKey: .binaryPath)
?? Self.defaultBinaryPath
if let urlString = try container.decodeIfPresent(String.self, forKey: .url),
let parsed = URL(string: urlString)
{
self.url = parsed
} else {
self.url = Self.defaultURL
}
}
// JSONEncoder special-cases URL to encode as absoluteString, but third-party
// encoders (e.g. TOMLEncoder) hit Foundation's default Codable conformance which
// encodes into a keyed container with a "relative" key. Encode as a plain string
// so all formats produce a consistent URL representation.
// If more config types start using URL, consider a property wrapper or a wrapper
// type (like MemorySize) that encodes/decodes URL as a string uniformly.
public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(binaryPath, forKey: .binaryPath)
try container.encode(url.absoluteString, forKey: .url)
}
}
final public class NetworkConfig: Codable, Sendable {
public let subnet: CIDRv4?
public let subnetv6: CIDRv6?
public init(subnet: CIDRv4? = nil, subnetv6: CIDRv6? = nil) {
self.subnet = subnet
self.subnetv6 = subnetv6
}
}
final public class RegistryConfig: Codable, Sendable {
public static let defaultDomain = "docker.io"
public let domain: String
public init(domain: String = defaultDomain) {
self.domain = domain
}
public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.domain = try container.decodeIfPresent(String.self, forKey: .domain) ?? Self.defaultDomain
}
}
@@ -0,0 +1,134 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025-2026 Apple Inc. and the container 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 ContainerizationError
import Foundation
import Logging
import SystemPackage
private let metadataFilename: String = "entity.json"
public protocol EntityStore<T> {
associatedtype T: Codable & Identifiable<String> & Sendable
func list() async throws -> [T]
func create(_ entity: T) async throws
func retrieve(_ id: String) async throws -> T?
func update(_ entity: T) async throws
func upsert(_ entity: T) async throws
func delete(_ id: String) async throws
}
public actor FilesystemEntityStore<T>: EntityStore where T: Codable & Identifiable<String> & Sendable {
typealias Index = [String: T]
private let path: FilePath
private let type: String
private var index: Index
private let log: Logger
private let encoder = JSONEncoder()
public init(path: FilePath, type: String, log: Logger) throws {
self.path = path
self.type = type
self.log = log
self.index = try Self.load(path: path, log: log)
}
public func list() async throws -> [T] {
Array(index.values)
}
public func create(_ entity: T) async throws {
let metadataPath = try metadataPath(entity.id)
guard !FileManager.default.fileExists(atPath: metadataPath.string) else {
throw ContainerizationError(.exists, message: "entity \(entity.id) already exist")
}
let entityPath = try entityPath(entity.id)
try FileManager.default.createDirectory(atPath: entityPath.string, withIntermediateDirectories: true)
let data = try encoder.encode(entity)
try data.write(to: URL(filePath: metadataPath.string))
index[entity.id] = entity
}
public func retrieve(_ id: String) throws -> T? {
index[id]
}
public func update(_ entity: T) async throws {
let metadataPath = try metadataPath(entity.id)
guard FileManager.default.fileExists(atPath: metadataPath.string) else {
throw ContainerizationError(.notFound, message: "entity \(entity.id) not found")
}
let data = try encoder.encode(entity)
try data.write(to: URL(filePath: metadataPath.string))
index[entity.id] = entity
}
public func upsert(_ entity: T) async throws {
let entityPath = try entityPath(entity.id)
try FileManager.default.createDirectory(atPath: entityPath.string, withIntermediateDirectories: true)
let metadataPath = try metadataPath(entity.id)
let data = try encoder.encode(entity)
try data.write(to: URL(filePath: metadataPath.string))
index[entity.id] = entity
}
public func delete(_ id: String) async throws {
let metadataPath = try entityPath(id)
guard FileManager.default.fileExists(atPath: metadataPath.string) else {
throw ContainerizationError(.notFound, message: "entity \(id) not found")
}
try FileManager.default.removeItem(atPath: metadataPath.string)
index.removeValue(forKey: id)
}
public nonisolated func entityPath(_ id: String) throws -> FilePath {
guard let component = FilePath.Component(id) else {
throw ContainerizationError(.invalidArgument, message: "entity ID \(id) cannot be a path component")
}
return path.appending(component)
}
private static func load(path: FilePath, log: Logger) throws -> Index {
let directories = try FileManager.default.contentsOfDirectory(atPath: path.string)
var index: FilesystemEntityStore<T>.Index = Index()
let decoder = JSONDecoder()
for filename in directories {
let metadataPath = path.appending(filename).appending(metadataFilename)
do {
let data = try Data(contentsOf: URL(filePath: metadataPath.string))
let entity = try decoder.decode(T.self, from: data)
index[entity.id] = entity
} catch {
log.warning(
"failed to load entity, ignoring",
metadata: [
"path": "\(metadataPath.string)"
])
}
}
return index
}
private func metadataPath(_ id: String) throws -> FilePath {
try entityPath(id).appending(metadataFilename)
}
}
@@ -0,0 +1,39 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the container 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 Darwin
import SystemPackage
extension FilePath {
/// Returns a new `FilePath` with all symlinks resolved and `.`/`..`
/// components normalized, by calling `realpath(3)`.
///
/// Unlike ``lexicallyNormalized()``, this method accesses the file system.
/// It throws ``Errno/noSuchFileOrDirectory`` if any component of the path
/// does not exist.
///
/// The returned path is always absolute. If the receiver is a relative path,
/// it is resolved against the process's current working directory.
public func resolvingSymlinks() throws -> FilePath {
try withPlatformString { cPath in
guard let resolved = Darwin.realpath(cPath, nil) else {
throw Errno(rawValue: Darwin.errno)
}
defer { free(resolved) }
return FilePath(platformString: resolved)
}
}
}
@@ -0,0 +1,251 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the container 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 ContainerizationError
import Foundation
import SystemPackage
/// Boot-time configuration for a container machine.
///
/// These values can be modified without recreating the container machine.
/// Changes take effect on the next boot. `nil` values mean
/// "use the container runtime default."
public struct MachineConfig: Codable, Sendable {
public static let `default`: MachineConfig = try! .init(
cpus: nil, memory: nil, homeMount: nil, virtualization: nil, kernelPath: nil)
public static var defaultCPUs: Int {
max(ProcessInfo.processInfo.processorCount / 2, 4)
}
public static var defaultMemory: MemorySize {
let bytes = max(ProcessInfo.processInfo.physicalMemory / 2, 1024 * 1024 * 1024)
let gb = bytes / (1024 * 1024 * 1024)
return try! MemorySize("\(gb)gb")
}
public static let defaultHomeMount: HomeMountOption = .rw
/// Home mount option for the /Users/<name> directory.
public enum HomeMountOption: String, Sendable, Codable {
case ro
case rw
case none
}
/// Number of virtual CPUs.
public let cpus: Int
/// Memory in bytes.
public let memory: MemorySize
/// Home mount configuration. nil = system default.
public let homeMount: HomeMountOption
/// Whether to expose nested virtualization to the container machine.
public let virtualization: Bool
/// Optional path to a custom kernel binary. nil falls back to the system default.
public let kernelPath: FilePath?
private enum CodingKeys: String, CodingKey {
case cpus
case memory
case homeMount
case virtualization
case kernelPath
}
/// Settable keys and their descriptions, for CLI help text generation.
public static let settableKeys: [(key: String, valueName: String, description: String)] = [
("cpus", "<number>", "Number of virtual CPUs"),
("memory", "<size>", "Memory allocation (e.g., 2G, 1G). Default: half of system memory"),
("home-mount", "<string>", "User home directory mount option (ro, rw, none). Default: rw"),
("virtualization", "<bool>", "Enable nested virtualization (true|false). Requires Apple Silicon M3+ and macOS 15+ and kernel with CONFIG_KVM=y."),
("kernel", "<path>", "Path to a custom kernel binary. Empty value resets to the system default."),
]
public init(
cpus: Int?,
memory: MemorySize?,
homeMount: HomeMountOption?,
virtualization: Bool?,
kernelPath: FilePath?
) throws {
self.cpus = cpus ?? Self.defaultCPUs
self.memory = memory ?? Self.defaultMemory
self.homeMount = homeMount ?? Self.defaultHomeMount
self.virtualization = virtualization ?? false
self.kernelPath = kernelPath
try self.validate()
}
public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let cpus = try container.decodeIfPresent(Int.self, forKey: .cpus)
let memory = try container.decodeIfPresent(MemorySize.self, forKey: .memory)
let homeMount = try container.decodeIfPresent(HomeMountOption.self, forKey: .homeMount)
let virtualization = try container.decodeIfPresent(Bool.self, forKey: .virtualization)
// FilePath's default Codable conformance encodes its internal SystemChar storage,
// which the project's ConfigSnapshotDecoder can't handle. Persist as a plain String
// and lift to FilePath in memory.
let kernelPath = try container.decodeIfPresent(String.self, forKey: .kernelPath).map { FilePath($0) }
try self.init(
cpus: cpus,
memory: memory,
homeMount: homeMount,
virtualization: virtualization,
kernelPath: kernelPath)
}
public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(cpus, forKey: .cpus)
try container.encode(memory, forKey: .memory)
try container.encode(homeMount, forKey: .homeMount)
try container.encode(virtualization, forKey: .virtualization)
try container.encodeIfPresent(kernelPath?.string, forKey: .kernelPath)
}
private func validate() throws {
guard self.cpus > 0 else {
throw ContainerizationError(
.invalidArgument,
message: "invalid CPU count '\(self.cpus)'. Must be a positive integer (e.g., 4)."
)
}
guard self.memory.toUInt64(unit: .bytes) >= 1024 * 1024 * 1024 else {
throw ContainerizationError(
.invalidArgument,
message: "invalid memory value '\(self.memory)'. Must be greater than 1gb."
)
}
}
}
extension MachineConfig {
/// Generate a help discussion string listing all settable keys.
public static func helpText() -> String {
settableKeys.map { entry in
let label = "\(entry.key)=\(entry.valueName)"
let padding = String(repeating: " ", count: max(1, 24 - label.count))
return "\(label)\(padding)\(entry.description)"
}.joined(separator: "\n")
}
/// Create a new MachineConfig from `self`, applying fields defined in `kwargs`
/// This function is used in both `machine create` and `machine set`
public func with(_ kwargs: [String: String]) throws -> MachineConfig {
let validKeys = Set(Self.settableKeys.map(\.key))
let unknownKeys = Set(kwargs.keys).subtracting(validKeys)
guard unknownKeys.isEmpty else {
throw ContainerizationError(
.invalidArgument,
message: "unknown fields '\(unknownKeys.joined(separator: ", "))'. Valid: \(validKeys.joined(separator: ", "))")
}
let cpus = try kwargs["cpus"].map { try Self.parseInt($0, for: "cpus") }
let memory = try kwargs["memory"].map { try MemorySize($0) }
let homeMount = try kwargs["home-mount"].map { try Self.parseHomeMount($0) }
let virtualization = try kwargs["virtualization"].map { try Self.parseBool($0, for: "virtualization") }
// Empty string explicitly clears the kernel override; absent key leaves it unchanged.
let kernelPath: FilePath?
if let raw = kwargs["kernel"] {
kernelPath = raw.isEmpty ? nil : FilePath(raw)
} else {
kernelPath = self.kernelPath
}
return try .init(
cpus: cpus ?? self.cpus,
memory: memory ?? self.memory,
homeMount: homeMount ?? self.homeMount,
virtualization: virtualization ?? self.virtualization,
kernelPath: kernelPath
)
}
/// Parse and validate a CPU count from user input.
private static func parseInt(_ value: String, for key: String) throws -> Int {
guard let num = Int(value) else {
throw ContainerizationError(
.invalidArgument,
message: "failed to parse \(value) for \(key)"
)
}
return num
}
/// Parse and validate a home mount option from user input.
private static func parseHomeMount(_ value: String) throws -> MachineConfig.HomeMountOption {
guard let opt = MachineConfig.HomeMountOption(rawValue: value) else {
throw ContainerizationError(
.invalidArgument,
message: "invalid home mount option '\(value)'. Valid options: ro, rw, none"
)
}
return opt
}
/// Parse a boolean setting accepting only "true" or "false".
private static func parseBool(_ value: String, for key: String) throws -> Bool {
guard let result = Parsers.parseBool(string: value) else {
throw ContainerizationError(
.invalidArgument,
message: "invalid value '\(value)' for \(key). Expected 'true' or 'false'."
)
}
return result
}
}
extension MachineConfig {
/// Resolves the user-supplied kernel path to absolute and confirms it points to a
/// readable, non-empty regular file. Used at create, set, and boot time.
public static func validateKernelPath(_ path: String) throws -> FilePath {
let absolute = URL(fileURLWithPath: path).path
let fm = FileManager.default
var isDirectory: ObjCBool = false
guard fm.fileExists(atPath: absolute, isDirectory: &isDirectory) else {
throw ContainerizationError(
.invalidArgument,
message: "kernel binary not found at '\(absolute)'"
)
}
guard !isDirectory.boolValue else {
throw ContainerizationError(
.invalidArgument,
message: "kernel path '\(absolute)' is a directory, expected a file"
)
}
guard fm.isReadableFile(atPath: absolute) else {
throw ContainerizationError(
.invalidArgument,
message: "kernel binary at '\(absolute)' is not readable"
)
}
let attrs = try fm.attributesOfItem(atPath: absolute)
let size = (attrs[.size] as? NSNumber)?.uint64Value ?? 0
guard size > 0 else {
throw ContainerizationError(
.invalidArgument,
message: "kernel binary at '\(absolute)' is empty"
)
}
return FilePath(absolute)
}
}
@@ -0,0 +1,84 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the container 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
let binaryUnits: [Character: UnitInformationStorage] = [
"b": .bytes,
"k": .kibibytes,
"m": .mebibytes,
"g": .gibibytes,
"t": .tebibytes,
"p": .pebibytes,
]
extension Measurement {
public enum ParseError: Swift.Error, CustomStringConvertible {
case invalidSize
case invalidSymbol(String)
public var description: String {
switch self {
case .invalidSize:
return "invalid size"
case .invalidSymbol(let symbol):
return "invalid symbol: \(symbol)"
}
}
}
/// parseMemory the provided string into a measurement that is able to be converted to various byte sizes using binary exponents
public static func parse(parsing: String) throws -> Measurement<UnitInformationStorage> {
let check = "01234567890."
let trimmed = parsing.trimmingCharacters(in: .whitespaces).lowercased()
guard !trimmed.isEmpty else {
throw ParseError.invalidSize
}
let i = trimmed.firstIndex {
!check.contains($0)
}
let rawValue =
i
.map { trimmed[..<$0].trimmingCharacters(in: .whitespaces) }
?? trimmed
let rawUnit = i.map { trimmed[$0...].trimmingCharacters(in: .whitespaces) } ?? ""
let value = Double(rawValue)
guard let value else {
throw ParseError.invalidSize
}
let unitSymbol = try Self.parseUnit(rawUnit)
let unit = binaryUnits[unitSymbol]
guard let unit else {
throw ParseError.invalidSymbol(rawUnit)
}
return Measurement<UnitInformationStorage>(value: value, unit: unit)
}
static func parseUnit(_ unit: String) throws -> Character {
let s = unit.dropFirst()
let unitSymbol = unit.first ?? "b"
switch s {
case "", "ib", "b":
return unitSymbol
default:
throw ParseError.invalidSymbol(unit)
}
}
}
@@ -0,0 +1,62 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the container 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
/// This is a thin wrapper around Measurement<UnitInformationStorage> to enable
/// better Codable implementations for user provided options. With this wrapper
/// values will get encoded and decoded from the format "1g" or "10mb".
public struct MemorySize: Codable, Sendable, Equatable, CustomStringConvertible {
public var description: String { formatted }
public let measurement: Measurement<UnitInformationStorage>
public init(_ string: String) throws {
self.measurement = try .parse(parsing: string)
}
public init(from decoder: any Decoder) throws {
let container = try decoder.singleValueContainer()
let string = try container.decode(String.self)
try self.init(string)
}
public func encode(to encoder: any Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(formatted)
}
private static let unitLabels: [UnitInformationStorage: String] = [
.bytes: "b",
.kibibytes: "kb",
.mebibytes: "mb",
.gibibytes: "gb",
.tebibytes: "tb",
.pebibytes: "pb",
]
public var formatted: String {
let value = Int64(measurement.value)
let label = Self.unitLabels[measurement.unit] ?? "unknown"
return "\(value)\(label)"
}
}
extension MemorySize {
public func toUInt64(unit: UnitInformationStorage) -> UInt64 {
UInt64(self.measurement.converted(to: unit).value.rounded())
}
}
@@ -0,0 +1,32 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the container 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
/// Generic value parsers shared across the project. Lives in `ContainerPersistence`
/// so both higher-level CLI parsers and the persistence layer can reuse the same
/// canonical implementations.
public enum Parsers {
/// Parse a boolean string accepting "true"/"t"/"false"/"f" (case-insensitive).
/// Returns nil if the input matches none.
public static func parseBool(string: String) -> Bool? {
switch string.lowercased() {
case "true", "t": return true
case "false", "f": return false
default: return nil
}
}
}
@@ -0,0 +1,61 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the container 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 ContainerVersion
import Foundation
import SystemPackage
public enum PathUtils {
public enum BaseConfigPath {
case home
case appRoot
case installRoot
public func basePath(env: [String: String] = ProcessInfo.processInfo.environment) -> FilePath {
switch self {
case .home:
let configHome: String
if let xdg = env["XDG_CONFIG_HOME"], !xdg.isEmpty {
configHome = xdg
} else {
configHome = NSHomeDirectory() + "/.config"
}
return FilePath(configHome).appending("container")
case .appRoot:
if let envPath = env["CONTAINER_APP_ROOT"], !envPath.isEmpty {
return FilePath(envPath)
}
let appSupportURL = FileManager.default.urls(
for: .applicationSupportDirectory,
in: .userDomainMask
).first!.appendingPathComponent("com.apple.container")
return FilePath(appSupportURL.path(percentEncoded: false))
case .installRoot:
if let envPath = env["CONTAINER_INSTALL_ROOT"], !envPath.isEmpty {
return FilePath(envPath)
}
// Use the kernel-recorded executable path (via _NSGetExecutablePath)
// rather than argv[0]: when the binary is invoked through PATH (e.g.
// `container ...`), argv[0] is just the basename and resolves to an
// empty FilePath, which FileManager treats as CWD-relative.
let installRootPath = CommandLine.executablePath
.removingLastComponent()
.removingLastComponent()
return installRootPath
}
}
}
}