Add support for layered and plugin configurations (#1543)

- This adds support for reading configurations from
  a three layer hierarchy:
  1. User provided TOML
  2. Install root TOML
  3. Code defaults
- We add some code to support plugin configurations
  via the ConfigurationLoader. Each plugin can provide
  a struct with an accompanying id that gets used to
  parse the scoped section of the TOML.
This commit is contained in:
Noah Thornton
2026-05-12 14:35:21 -07:00
committed by GitHub
parent 940fefb7cd
commit a967399d48
28 changed files with 1165 additions and 712 deletions
+1 -1
View File
@@ -50,7 +50,7 @@ extension APIServer {
var logRoot = LogRoot.path
func run() async throws {
let containerSystemConfig: ContainerSystemConfig = try ConfigurationLoader.load()
let containerSystemConfig: ContainerSystemConfig = try await ConfigurationLoader.load()
let commandName = APIServer._commandName
let logPath = logRoot.map { $0.appending("\(commandName).log") }
let log = ServiceLogger.bootstrap(category: "APIServer", debug: debug, logPath: logPath)
+1 -1
View File
@@ -149,7 +149,7 @@ extension Application {
var pull: Bool = false
public func run() async throws {
let containerSystemConfig: ContainerSystemConfig = try ConfigurationLoader.load()
let containerSystemConfig: ContainerSystemConfig = try await ConfigurationLoader.load()
do {
let timeout: Duration = .seconds(300)
let progressConfig = try ProgressConfig(
@@ -55,7 +55,7 @@ extension Application {
public init() {}
public func run() async throws {
let containerSystemConfig: ContainerSystemConfig = try ConfigurationLoader.load()
let containerSystemConfig: ContainerSystemConfig = try await ConfigurationLoader.load()
let progressConfig = try ProgressConfig(
showTasks: true,
showItems: true,
@@ -56,7 +56,7 @@ extension Application {
var arguments: [String] = []
public func run() async throws {
let containerSystemConfig: ContainerSystemConfig = try ConfigurationLoader.load()
let containerSystemConfig: ContainerSystemConfig = try await ConfigurationLoader.load()
let progressConfig = try ProgressConfig(
showTasks: true,
showItems: true,
@@ -63,7 +63,7 @@ extension Application {
var arguments: [String] = []
public func run() async throws {
let containerSystemConfig: ContainerSystemConfig = try ConfigurationLoader.load()
let containerSystemConfig: ContainerSystemConfig = try await ConfigurationLoader.load()
var exitCode: Int32 = 127
let id = Utility.createContainerID(name: self.managementFlags.name)
@@ -109,7 +109,7 @@ extension Application {
}
public mutating func run() async throws {
let containerSystemConfig: ContainerSystemConfig = try ConfigurationLoader.load()
let containerSystemConfig: ContainerSystemConfig = try await ConfigurationLoader.load()
try await DeleteImageImplementation.removeImage(options: options, containerSystemConfig: containerSystemConfig, log: log)
}
}
@@ -45,7 +45,7 @@ extension Application {
}
public func run() async throws {
let containerSystemConfig: ContainerSystemConfig = try ConfigurationLoader.load()
let containerSystemConfig: ContainerSystemConfig = try await ConfigurationLoader.load()
var printable: [ImageDetail] = []
var succeededImages: [String] = []
var allErrors: [(String, Error)] = []
@@ -45,7 +45,7 @@ extension Application {
public var logOptions: Flags.Logging
public mutating func run() async throws {
let containerSystemConfig: ContainerSystemConfig = try ConfigurationLoader.load()
let containerSystemConfig: ContainerSystemConfig = try await ConfigurationLoader.load()
try Self.validate(format: format, quiet: quiet, verbose: verbose)
var images = try await ClientImage.list().filter { img in
@@ -69,7 +69,7 @@ extension Application {
}
public func run() async throws {
let containerSystemConfig: ContainerSystemConfig = try ConfigurationLoader.load()
let containerSystemConfig: ContainerSystemConfig = try await ConfigurationLoader.load()
let p = try DefaultPlatform.resolve(platform: platform, os: os, arch: arch, log: log)
let scheme = try RequestScheme(registry.scheme)
@@ -57,7 +57,7 @@ extension Application {
public init() {}
public func run() async throws {
let containerSystemConfig: ContainerSystemConfig = try ConfigurationLoader.load()
let containerSystemConfig: ContainerSystemConfig = try await ConfigurationLoader.load()
let p = try DefaultPlatform.resolve(platform: platform, os: os, arch: arch, log: log)
let scheme = try RequestScheme(registry.scheme)
@@ -62,7 +62,7 @@ extension Application {
@Argument var references: [String]
public func run() async throws {
let containerSystemConfig: ContainerSystemConfig = try ConfigurationLoader.load()
let containerSystemConfig: ContainerSystemConfig = try await ConfigurationLoader.load()
let p = try DefaultPlatform.resolve(platform: platform, os: os, arch: arch, log: log)
let progressConfig = try ProgressConfig(
@@ -36,7 +36,7 @@ extension Application {
public var logOptions: Flags.Logging
public func run() async throws {
let containerSystemConfig: ContainerSystemConfig = try ConfigurationLoader.load()
let containerSystemConfig: ContainerSystemConfig = try await ConfigurationLoader.load()
let existing = try await ClientImage.get(reference: source, containerSystemConfig: containerSystemConfig)
let targetReference = try ClientImage.normalizeReference(target, containerSystemConfig: containerSystemConfig)
try await existing.tag(new: targetReference)
@@ -47,7 +47,7 @@ extension Application {
var server: String
public func run() async throws {
let containerSystemConfig: ContainerSystemConfig = try ConfigurationLoader.load()
let containerSystemConfig: ContainerSystemConfig = try await ConfigurationLoader.load()
var username = self.username
var password = ""
if passwordStdin {
@@ -53,7 +53,7 @@ extension Application {
public init() {}
public func run() async throws {
let containerSystemConfig: ContainerSystemConfig = try ConfigurationLoader.load()
let containerSystemConfig: ContainerSystemConfig = try await ConfigurationLoader.load()
if recommended {
let url = containerSystemConfig.kernel.url
let path: String = containerSystemConfig.kernel.binaryPath
@@ -42,7 +42,7 @@ extension Application {
public init() {}
public func run() async throws {
let containerSystemConfig: ContainerSystemConfig = try ConfigurationLoader.load()
let containerSystemConfig: ContainerSystemConfig = try await ConfigurationLoader.load()
let output =
switch format {
case .json: try Output.renderJSON(containerSystemConfig)
@@ -73,9 +73,18 @@ extension Application {
public func run() async throws {
let appRootPath = FilePath(appRoot.path(percentEncoded: false))
let installRootPath = FilePath(installRoot.path(percentEncoded: false))
try ConfigurationLoader.copyConfigurationToReadOnly(to: appRootPath)
let containerSystemConfig: ContainerSystemConfig = try ConfigurationLoader.load(
configurationFile: ConfigurationLoader.configurationFile(in: appRootPath))
// Pass appRoot before installRoot: ConfigurationLoader uses first-match-wins
// precedence, so user-provided config in appRoot overrides the defaults
// shipped under installRoot. Both layers are passed explicitly because
// users can override --app-root and --install-root from the CLI, and the
// loader's default search would otherwise ignore those overrides.
let containerSystemConfig: ContainerSystemConfig = try await ConfigurationLoader.load(
configurationFiles: [
ConfigurationLoader.configurationFile(in: appRootPath, of: .appRoot),
ConfigurationLoader.configurationFile(in: installRootPath, of: .installRoot),
])
// Without the true path to the binary in the plist, `container-apiserver` won't launch properly.
// Resolve the symlink to get the true binary path before writing the launchd plist.
@@ -61,6 +61,16 @@ public struct ConfigSnapshotDecoder: Sendable {
_ 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: [],
@@ -20,9 +20,11 @@ import Foundation
// MARK: - Shared helpers
extension ConfigSnapshotReader {
// ConfigSnapshotReader stores typed values — string(forKey:) returns nil for
// int/double/bool values. Check all primitive accessors to avoid incorrectly
// treating non-string values as nil (e.g. Optional<Int> with an .int value).
/// 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
@@ -31,6 +33,12 @@ extension ConfigSnapshotReader {
}
}
/// 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]
@@ -45,8 +53,83 @@ struct KeyedContainer<Key: CodingKey>: KeyedDecodingContainerProtocol {
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 {
!snapshot.hasValue(forKey: configKey(appending: key))
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 {
@@ -106,6 +189,16 @@ struct KeyedContainer<Key: CodingKey>: KeyedDecodingContainerProtocol {
}
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],
@@ -202,6 +295,16 @@ struct KeyedContainer<Key: CodingKey>: KeyedDecodingContainerProtocol {
}
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 {
@@ -211,14 +314,9 @@ struct SingleValueContainer: SingleValueDecodingContainer {
let typeDecodingStrategies: [ObjectIdentifier: AnyConfigDecodingStrategy]
// ConfigSnapshotReader stores typed values — string(forKey:) returns nil for
// int/double/bool values. Check all primitive accessors to avoid incorrectly
// treating non-string values as nil (e.g. Optional<Int> with an .int value).
// int/double/bool values. `hasValue` checks all primitive accessors.
func decodeNil() -> Bool {
let key = configKey()
return snapshot.string(forKey: key) == nil
&& snapshot.int(forKey: key) == nil
&& snapshot.double(forKey: key) == nil
&& snapshot.bool(forKey: key) == nil
!snapshot.hasValue(forKey: configKey())
}
func decode(_ type: Bool.Type) throws -> Bool {
@@ -252,6 +350,16 @@ struct SingleValueContainer: SingleValueDecodingContainer {
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,
@@ -14,10 +14,11 @@
// limitations under the License.
//===----------------------------------------------------------------------===//
import Configuration
import ConfigurationTOML
import ContainerizationError
import Foundation
import SystemPackage
import TOML
public protocol Initable {
init()
@@ -25,87 +26,192 @@ public protocol Initable {
public typealias LoadableConfiguration = Codable & Sendable & Initable
public protocol LoadablePluginConfiguration: LoadableConfiguration {
static var pluginId: String { get }
}
public enum ConfigurationLoader {
private static let configFilename = "runtime-config.toml"
private static let configDirectory = "config"
private static let READ_ONLY: Int = 0o444
private static let READ_AND_WRITE: Int = 0o644
/// Returns the canonical configuration file path under an appRoot base directory:
/// `<base>/config/runtime-config.toml`.
public static func configurationFile(in base: FilePath) -> FilePath {
base.appending(configDirectory).appending(configFilename)
/// 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)
}
/// Loads and decodes a TOML configuration file as type `T`.
/// Returns the configuration file path under an explicit base directory.
///
/// - Parameter configurationFile: Absolute path to the configuration file.
/// When `nil`, falls back to
/// `configurationFile(in: PathUtils.BaseConfigPath.appRoot.basePath())`.
/// - Returns: A decoded value of type `T`, or a default-initialized `T` if the
/// configuration file does not exist.
public static func load<T: LoadableConfiguration>(configurationFile: FilePath? = nil) throws -> T {
let path = configurationFile ?? Self.configurationFile(in: PathUtils.BaseConfigPath.appRoot.basePath())
guard FileManager.default.fileExists(atPath: path.string) else {
/// Path shape depends on `kind`:
/// - `.home`: `<base>/runtime-config.toml` (user source under `~/.config/container`)
/// - e.g. `~/.config/container/runtime-config.toml`
/// - `.appRoot`: `<base>/config/runtime-config.toml` (read-only copy of user config)
/// - e.g. `~/Library/Application Support/com.apple.container/config/runtime-config.toml`
/// - `.installRoot`: `<base>/etc/container/runtime-config.toml` (system defaults shipped with install)
/// - e.g. `/usr/local/etc/container/runtime-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/runtime-config.toml`)
/// > system config (`<installRoot>/etc/container/config/runtime-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 {
let data = try Data(contentsOf: URL(filePath: path.string))
return try TOMLDecoder().decode(T.self, from: data)
return try ConfigSnapshotDecoder().decode(T.self, from: snapshot)
} catch {
throw ContainerizationError(
.invalidArgument,
message: "failed to load configuration from '\(path)': \(error)"
message: "\(decodeErrorContext): \(error)"
)
}
}
/// Copies a TOML configuration file into a read-only destination under an appRoot base.
/// 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: The file to copy. When `nil`, defaults to
/// `<home>/container/runtime-config.toml`. If the source does not exist,
/// this is a no-op.
/// - destination: Base directory under which the file is written at
/// `<destination>/config/runtime-config.toml`. When `nil`, falls back to
/// `PathUtils.BaseConfigPath.appRoot.basePath()`. The destination file is written
/// with `READ_ONLY` (`0o444`) permissions.
/// - source: File to copy from. Defaults to `<home>/container/runtime-config.toml`.
/// - destination: Directory to copy into — the filename is appended automatically.
/// Defaults to `<appRoot>/config/runtime-config.toml`.
public static func copyConfigurationToReadOnly(
from source: FilePath? = nil,
to destination: FilePath? = nil
) throws {
let source =
source
?? PathUtils.BaseConfigPath.home.basePath()
.appending(configFilename)
let destinationFile = Self.configurationFile(in: destination ?? PathUtils.BaseConfigPath.appRoot.basePath())
do {
let fm = FileManager.default
guard fm.fileExists(atPath: source.string) else { return }
let sourcePath = source ?? configurationFile(.home)
let destBase = destination ?? PathUtils.BaseConfigPath.appRoot.basePath()
let destPath = configurationFile(in: destBase)
let destDir = destinationFile.removingLastComponent()
try fm.createDirectory(
atPath: destDir.string,
withIntermediateDirectories: true
)
if fm.fileExists(atPath: destinationFile.string) {
try fm.setAttributes(
[.posixPermissions: READ_AND_WRITE],
ofItemAtPath: destinationFile.string
)
try fm.removeItem(at: URL(filePath: destinationFile.string))
}
try fm.copyItem(
at: URL(filePath: source.string),
to: URL(filePath: destinationFile.string)
)
try fm.setAttributes(
[.posixPermissions: READ_ONLY],
ofItemAtPath: destinationFile.string
)
} catch {
throw ContainerizationError(
.invalidState, message: "Failed to copy user TOML to AppRoot `\(error)`")
let fm = FileManager.default
guard fm.fileExists(atPath: sourcePath.string) else { return }
let destDir = destPath.removingLastComponent()
try fm.createDirectory(atPath: destDir.string, withIntermediateDirectories: true)
if fm.fileExists(atPath: destPath.string) {
try fm.setAttributes([.posixPermissions: READ_AND_WRITE], ofItemAtPath: destPath.string)
try fm.removeItem(at: URL(filePath: destPath.string))
}
try fm.copyItem(
at: URL(filePath: sourcePath.string),
to: URL(filePath: destPath.string)
)
try fm.setAttributes([.posixPermissions: READ_ONLY], ofItemAtPath: destPath.string)
}
}
@@ -14,6 +14,7 @@
// limitations under the License.
//===----------------------------------------------------------------------===//
import ContainerVersion
import Foundation
import SystemPackage
@@ -21,6 +22,7 @@ public enum PathUtils {
public enum BaseConfigPath {
case home
case appRoot
case installRoot
public func basePath(env: [String: String] = ProcessInfo.processInfo.environment) -> FilePath {
switch self {
@@ -41,6 +43,19 @@ public enum PathUtils {
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 installRootURL = CommandLine.executablePathUrl
.deletingLastPathComponent()
.appendingPathComponent("..")
.standardized
return FilePath(installRootURL.path(percentEncoded: false))
}
}
}
@@ -58,7 +58,7 @@ extension ImagesHelper {
var logRoot = LogRoot.path
func run() async throws {
let containerSystemConfig: ContainerSystemConfig = try ConfigurationLoader.load()
let containerSystemConfig: ContainerSystemConfig = try await ConfigurationLoader.load()
let commandName = ImagesHelper._commandName
let logPath = logRoot.map { $0.appending("\(commandName).log") }
let log = ServiceLogger.bootstrap(category: "ImagesHelper", debug: debug, logPath: logPath)