//===----------------------------------------------------------------------===// // 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 ArgumentParser import ContainerAPIClient import ContainerPersistence import ContainerPlugin import ContainerXPC import ContainerizationError import Foundation import SystemPackage import TerminalProgress extension Application { public struct SystemStart: AsyncLoggableCommand { public static let configuration = CommandConfiguration( commandName: "start", abstract: "Start `container` services" ) @Option( name: .shortAndLong, help: "Path to the root directory for application data", transform: { URL(filePath: $0) }) var appRoot = ApplicationRoot.defaultURL @Option( name: .long, help: "Path to the root directory for application executables and plugins", transform: { URL(filePath: $0) }) var installRoot = InstallRoot.defaultURL @Option( name: .long, help: "Path to the root directory for log data, using macOS log facility if not set", transform: { FilePath($0) }) var logRoot: FilePath? = nil @Flag( name: .long, inversion: .prefixedEnableDisable, help: "Specify whether the default kernel should be installed or not (default: prompt user)") var kernelInstall: Bool? @Option( help: "Number of seconds to wait for API service to become responsive", transform: { guard let timeoutSeconds = Double($0) else { throw ValidationError("Invalid timeout value: \($0)") } return .seconds(timeoutSeconds) } ) var timeout: Duration = XPCClient.xpcRegistrationTimeout @OptionGroup public var logOptions: Flags.Logging 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) // 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. // 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() var args = [executableUrl.absolutePath()] args.append("start") if logOptions.debug { args.append("--debug") } let apiServerDataUrl = appRoot.appending(path: "apiserver") 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) if let logRoot { env[LogRoot.environmentName] = logRoot.isAbsolute ? logRoot.string : FilePath(FileManager.default.currentDirectoryPath).appending(logRoot.components).string } let plist = LaunchPlist( label: "com.apple.container.apiserver", arguments: args, environment: env, limitLoadToSessionType: [.Aqua, .Background, .System], runAtLoad: true, machServices: ["com.apple.container.apiserver"] ) let plistURL = apiServerDataUrl.appending(path: "apiserver.plist") let data = try plist.encode() try data.write(to: plistURL) print("Registering API server with launchd...") try ServiceManager.register(plistPath: plistURL.path) // Now ping our friendly daemon. Fail if we don't get a response. do { print("Verifying apiserver is running...") _ = try await ClientHealthCheck.ping(timeout: timeout) } catch { throw ContainerizationError( .internalError, message: "failed to get a response from apiserver: \(error)" ) } if await !initImageExists(containerSystemConfig: containerSystemConfig) { try? await installInitialFilesystem(initImage: containerSystemConfig.vminit.image) } guard await !kernelExists() else { return } try await installDefaultKernel(kernelURL: containerSystemConfig.kernel.url, kernelBinaryPath: containerSystemConfig.kernel.binaryPath) } private func installInitialFilesystem(initImage: String) async throws { var pullCommand = try ImagePull.parse() pullCommand.reference = initImage print("Installing base container filesystem...") do { try await pullCommand.run() } catch { log.error("failed to install base container filesystem", metadata: ["error": "\(error)"]) } } private func installDefaultKernel(kernelURL: URL, kernelBinaryPath: String) async throws { var shouldInstallKernel = false if kernelInstall == nil { print("No default kernel configured.") print("Install the recommended default kernel from [\(kernelURL)]? [Y/n]: ", terminator: "") guard let read = readLine(strippingNewline: true) else { throw ContainerizationError(.internalError, message: "failed to read user input") } guard read.lowercased() == "y" || read.count == 0 else { print("Please use the `container system kernel set --recommended` command to configure the default kernel") return } shouldInstallKernel = true } else { shouldInstallKernel = kernelInstall ?? false } guard shouldInstallKernel else { return } print("Installing kernel...") try await KernelSet.downloadAndInstallWithProgressBar(tarRemoteURL: kernelURL, kernelFilePath: kernelBinaryPath, force: true) } private func initImageExists(containerSystemConfig: ContainerSystemConfig) async -> Bool { do { let img = try await ClientImage.get( reference: containerSystemConfig.vminit.image, containerSystemConfig: containerSystemConfig ) let _ = try await img.getSnapshot(platform: .current) return true } catch { return false } } private func kernelExists() async -> Bool { do { try await ClientKernel.getDefaultKernel(for: .current) return true } catch { return false } } } }