Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -514,9 +514,17 @@ let package = Package(
.target(
name: "ContainerVersion",
dependencies: [
"CVersion"
.product(name: "SystemPackage", package: "swift-system"),
"CVersion",
],
),
.testTarget(
name: "ContainerVersionTests",
dependencies: [
.product(name: "SystemPackage", package: "swift-system"),
"ContainerVersion",
]
),
.target(
name: "CVersion",
dependencies: [],
Expand Down
50 changes: 28 additions & 22 deletions Sources/APIServer/APIServer+Start.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,16 @@ extension APIServer {
@Flag(name: .long, help: "Enable debug logging")
var debug = false

var appRoot = ApplicationRoot.url
var appRoot = ApplicationRoot.path

var installRoot = InstallRoot.url
var installRoot = InstallRoot.path

var logRoot = LogRoot.path

func run() async throws {
let containerSystemConfig: ContainerSystemConfig = try await ConfigurationLoader.load()
let commandName = APIServer._commandName
let logPath = logRoot.map { $0.appending("\(commandName).log") }
let logPath = logRoot.map { $0.appending(FilePath.Component("\(commandName).log") ?? "unknown") }
let log = ServiceLogger.bootstrap(category: "APIServer", debug: debug, logPath: logPath)
log.info("starting helper", metadata: ["name": "\(commandName)"])
defer {
Expand Down Expand Up @@ -179,22 +179,24 @@ extension APIServer {
log.info(
"initializing plugin loader",
metadata: [
"installRoot": "\(installRoot.path(percentEncoded: false))"
"installRoot": "\(installRoot.string)"
])

let pluginsURL = PluginLoader.userPluginsDir(installRoot: installRoot)
// TODO: Remove when we convert PluginLoader to FilePath
let installRootURL = URL(fileURLWithPath: installRoot.string)
let pluginsURL = PluginLoader.userPluginsDir(installRoot: installRootURL)
log.info("detecting user plugins directory", metadata: ["path": "\(pluginsURL.path(percentEncoded: false))"])
var directoryExists: ObjCBool = false
_ = FileManager.default.fileExists(atPath: pluginsURL.path, isDirectory: &directoryExists)
let userPluginsURL = directoryExists.boolValue ? pluginsURL : nil

// plugins built into the application installed as a Unix-like application
let installRootPluginsURL =
let installRootPluginsPath =
installRoot
.appendingPathComponent("libexec")
.appendingPathComponent("container")
.appendingPathComponent("plugins")
.standardized
.appending(FilePath.Component("libexec"))
.appending(FilePath.Component("container"))
.appending(FilePath.Component("plugins"))
let installRootPluginsURL = URL(fileURLWithPath: installRootPluginsPath.string)

let pluginDirectories = [
userPluginsURL,
Expand All @@ -210,9 +212,10 @@ extension APIServer {
log.info("discovered plugin directory", metadata: ["path": "\(pluginDirectory.path(percentEncoded: false))"])
}

let appRootURL = URL(fileURLWithPath: appRoot.string)
return try PluginLoader(
appRoot: appRoot,
installRoot: installRoot,
appRoot: appRootURL,
installRoot: installRootURL,
logRoot: logRoot,
pluginDirectories: pluginDirectories,
pluginFactories: pluginFactories,
Expand Down Expand Up @@ -245,9 +248,12 @@ extension APIServer {
private func initializeHealthCheckService(log: Logger, routes: inout [XPCRoute: XPCServer.RouteHandler]) {
log.info("initializing health check service")

// TODO: Remove when we convert HealthCheckHarness to FilePath
let installRootURL = URL(fileURLWithPath: installRoot.string)
let appRootURL = URL(fileURLWithPath: appRoot.string)
let svc = HealthCheckHarness(
appRoot: appRoot,
installRoot: installRoot,
appRoot: appRootURL,
installRoot: installRootURL,
logRoot: logRoot,
log: log
)
Expand All @@ -257,7 +263,9 @@ extension APIServer {
private func initializeKernelService(log: Logger, routes: inout [XPCRoute: XPCServer.RouteHandler]) throws {
log.info("initializing kernel service")

let svc = try KernelService(log: log, appRoot: appRoot)
// TODO: Remove when we convert KernelService to FilePath
let appRootURL = URL(fileURLWithPath: appRoot.string)
let svc = try KernelService(log: log, appRoot: appRootURL)
let harness = KernelHarness(service: svc, log: log)
routes[XPCRoute.installKernel] = XPCServer.route(harness.install)
routes[XPCRoute.getDefaultKernel] = XPCServer.route(harness.getDefaultKernel)
Expand All @@ -271,8 +279,10 @@ extension APIServer {
) throws -> ContainersService {
log.info("initializing containers service")

// TODO: Remove when we convert ContainersService to FilePath
let appRootURL = URL(fileURLWithPath: appRoot.string)
let service = try ContainersService(
appRoot: appRoot,
appRoot: appRootURL,
pluginLoader: pluginLoader,
containerSystemConfig: containerSystemConfig,
log: log,
Expand Down Expand Up @@ -308,9 +318,7 @@ extension APIServer {
) async throws -> NetworksService {
log.info("initializing networks service")

// TODO: This goes away when we convert our roots to FilePath
let appPath = FilePath(appRoot.absolutePath())
let resourceRoot = appPath.appending("networks")
let resourceRoot = appRoot.appending(FilePath.Component("networks"))
let service = try await NetworksService(
pluginLoader: pluginLoader,
resourceRoot: resourceRoot,
Expand Down Expand Up @@ -353,9 +361,7 @@ extension APIServer {
) throws -> VolumesService {
log.info("initializing volume service")

// TODO: This goes away when we convert our roots to FilePath
let appPath = FilePath(appRoot.absolutePath())
let resourceRoot = appPath.appending("volumes")
let resourceRoot = appRoot.appending(FilePath.Component("volumes"))
let service = try VolumesService(resourceRoot: resourceRoot, containersService: containersService, log: log)
let harness = VolumesHarness(service: service, log: log)

Expand Down
25 changes: 13 additions & 12 deletions Sources/ContainerCommands/Application.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import ContainerizationError
import ContainerizationOS
import Foundation
import Logging
import SystemPackage
import TerminalProgress

// This logger is only used until `asyncCommand.run()`.
Expand Down Expand Up @@ -135,11 +136,12 @@ public struct Application: AsyncLoggableCommand {
}

public static func createPluginLoader() async throws -> PluginLoader {
let installRoot = CommandLine.executablePathUrl
.deletingLastPathComponent()
.appendingPathComponent("..")
.standardized
let pluginsURL = PluginLoader.userPluginsDir(installRoot: installRoot)
let installRootPath = CommandLine.executablePath
.removingLastComponent()
.removingLastComponent()
// TODO: Remove when we convert PluginLoader to FilePath.
let installRootURL = URL(fileURLWithPath: installRootPath.string)
let pluginsURL = PluginLoader.userPluginsDir(installRoot: installRootURL)
var directoryExists: ObjCBool = false
_ = FileManager.default.fileExists(atPath: pluginsURL.path, isDirectory: &directoryExists)
let userPluginsURL = directoryExists.boolValue ? pluginsURL : nil
Expand All @@ -148,13 +150,12 @@ public struct Application: AsyncLoggableCommand {
let appBundlePluginsURL = Bundle.main.resourceURL?.appending(path: "plugins")

// plugins built into the application installed as a Unix-like application
let installRootPluginsURL =
installRoot
.appendingPathComponent("libexec")
.appendingPathComponent("container")
.appendingPathComponent("plugins")
.standardized

let installRootPluginsPath =
installRootPath
.appending(FilePath.Component("libexec"))
.appending(FilePath.Component("container"))
.appending(FilePath.Component("plugins"))
let installRootPluginsURL = URL(fileURLWithPath: installRootPluginsPath.string)
let pluginDirectories = [
userPluginsURL,
appBundlePluginsURL,
Expand Down
23 changes: 13 additions & 10 deletions Sources/ContainerCommands/DefaultCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import ContainerAPIClient
import ContainerPlugin
import Darwin
import Foundation
import SystemPackage

struct DefaultCommand: AsyncLoggableCommand {
public static let configuration = CommandConfiguration(
Expand Down Expand Up @@ -48,17 +49,19 @@ struct DefaultCommand: AsyncLoggableCommand {
}

// Compute canonical plugin directories to show in helpful errors (avoid hard-coded paths)
let installRoot = CommandLine.executablePathUrl
.deletingLastPathComponent()
.appendingPathComponent("..")
.standardized
let userPluginsURL = PluginLoader.userPluginsDir(installRoot: installRoot)
let installRootPluginsURL =
let installRoot = CommandLine.executablePath
.removingLastComponent()
.removingLastComponent()

// TODO: Remove when we convert PluginLoader to FilePath
let installRootURL = URL(fileURLWithPath: installRoot.string)
let userPluginsURL = PluginLoader.userPluginsDir(installRoot: installRootURL)
let installRootPluginsPath =
installRoot
.appendingPathComponent("libexec")
.appendingPathComponent("container")
.appendingPathComponent("plugins")
.standardized
.appending(FilePath.Component("libexec"))
.appending(FilePath.Component("container"))
.appending(FilePath.Component("plugins"))
let installRootPluginsURL = URL(fileURLWithPath: installRootPluginsPath.string)
let hintPaths = [userPluginsURL, installRootPluginsURL]
.map { $0.appendingPathComponent(command).path(percentEncoded: false) }
.joined(separator: "\n - ")
Expand Down
45 changes: 21 additions & 24 deletions Sources/ContainerCommands/System/SystemStart.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,19 @@ extension Application {
@Option(
name: .shortAndLong,
help: "Path to the root directory for application data",
transform: { URL(filePath: $0) })
var appRoot = ApplicationRoot.defaultURL
transform: { FilePath(FileManager.default.currentDirectoryPath).resolve($0, defaultPath: FilePath($0)) })
var appRoot = ApplicationRoot.defaultPath

@Option(
name: .long,
help: "Path to the root directory for application executables and plugins",
transform: { URL(filePath: $0) })
var installRoot = InstallRoot.defaultURL
transform: { FilePath(FileManager.default.currentDirectoryPath).resolve($0, defaultPath: FilePath($0)) })
var installRoot = InstallRoot.defaultPath

@Option(
name: .long,
help: "Path to the root directory for log data, using macOS log facility if not set",
transform: { FilePath($0) })
transform: { FilePath(FileManager.default.currentDirectoryPath).resolve($0, defaultPath: FilePath($0)) })
var logRoot: FilePath? = nil

@Flag(
Expand All @@ -72,48 +72,44 @@ extension Application {
public init() {}

public func run() async throws {
let appRootPath = FilePath(appRoot.path(percentEncoded: false))
let installRootPath = FilePath(installRoot.path(percentEncoded: false))
try ConfigurationLoader.copyConfigurationToReadOnly(to: appRootPath)
try ConfigurationLoader.copyConfigurationToReadOnly(to: appRoot)
// 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),
ConfigurationLoader.configurationFile(in: appRoot, of: .appRoot),
ConfigurationLoader.configurationFile(in: installRoot, 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.
// Gatekeeper / amfid validates code signatures relative to the enclosing .app bundle
// hierarchy; launching via a symlink outside the bundle fails that check.
// TODO: Can we use the plugin loader to bootstrap the API server?
let executableUrl = CommandLine.executablePathUrl
.deletingLastPathComponent()
.appendingPathComponent("container-apiserver")
.resolvingSymlinksInPath()
let executablePath = try CommandLine.executablePath
.removingLastComponent()
.appending(FilePath.Component("container-apiserver"))
.resolvingSymlinks()

var args = [executableUrl.absolutePath()]
var args = [executablePath.string]

args.append("start")
if logOptions.debug {
args.append("--debug")
}

let apiServerDataUrl = appRoot.appending(path: "apiserver")
try! FileManager.default.createDirectory(at: apiServerDataUrl, withIntermediateDirectories: true)
let apiServerDataPath = appRoot.appending(FilePath.Component("apiserver"))
let apiServerDataURL = URL(fileURLWithPath: apiServerDataPath.string)
try! FileManager.default.createDirectory(at: apiServerDataURL, withIntermediateDirectories: true)

var env = PluginLoader.filterEnvironment()
env[ApplicationRoot.environmentName] = appRoot.path(percentEncoded: false)
env[InstallRoot.environmentName] = installRoot.path(percentEncoded: false)
env[ApplicationRoot.environmentName] = appRoot.string
env[InstallRoot.environmentName] = installRoot.string
if let logRoot {
env[LogRoot.environmentName] =
logRoot.isAbsolute
? logRoot.string
: FilePath(FileManager.default.currentDirectoryPath).appending(logRoot.components).string
env[LogRoot.environmentName] = logRoot.string
}
let plist = LaunchPlist(
label: "com.apple.container.apiserver",
Expand All @@ -124,7 +120,8 @@ extension Application {
machServices: ["com.apple.container.apiserver"]
)

let plistURL = apiServerDataUrl.appending(path: "apiserver.plist")
let plistPath = apiServerDataPath.appending(FilePath.Component("apiserver.plist"))
let plistURL = URL(fileURLWithPath: plistPath.string)
let data = try plist.encode()
try data.write(to: plistURL)

Expand Down
39 changes: 39 additions & 0 deletions Sources/ContainerPersistence/FilePath+Symlinks.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
9 changes: 4 additions & 5 deletions Sources/ContainerPersistence/PathUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,10 @@ public enum PathUtils {
// 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))
let installRootPath = CommandLine.executablePath
.removingLastComponent()
.removingLastComponent()
return installRootPath
}
}
}
Expand Down
Loading