mirror of
https://github.com/apple/container.git
synced 2026-09-28 01:46:25 -04:00
Use FilePath for app, install, and log roots. (#1558)
- Closes #1557. - Replaces `executableURL` utility function for getting app executable path with `executablePath`. - Adds `FilePath.resolvingSymlinks()` extension. - Also converts for FilePath for types in `ContainerVersion` target.
This commit is contained in:
@@ -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 {
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
@@ -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
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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,
|
||||
@@ -310,9 +320,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,
|
||||
@@ -355,9 +363,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)
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import ContainerizationError
|
||||
import ContainerizationOS
|
||||
import Foundation
|
||||
import Logging
|
||||
import SystemPackage
|
||||
import TerminalProgress
|
||||
|
||||
// This logger is only used until `asyncCommand.run()`.
|
||||
@@ -136,11 +137,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
|
||||
@@ -149,13 +151,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,
|
||||
|
||||
@@ -19,6 +19,7 @@ import ContainerAPIClient
|
||||
import ContainerPlugin
|
||||
import Darwin
|
||||
import Foundation
|
||||
import SystemPackage
|
||||
|
||||
struct DefaultCommand: AsyncLoggableCommand {
|
||||
public static let configuration = CommandConfiguration(
|
||||
@@ -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 - ")
|
||||
|
||||
@@ -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(
|
||||
@@ -72,9 +72,7 @@ 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
|
||||
@@ -82,8 +80,8 @@ extension Application {
|
||||
// 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.
|
||||
@@ -91,29 +89,27 @@ extension Application {
|
||||
// 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",
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,19 +15,34 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import Foundation
|
||||
import SystemPackage
|
||||
|
||||
/// Provides the application data root path.
|
||||
public struct ApplicationRoot {
|
||||
/// The environment variable that if set, determines the root directory for the application data store.
|
||||
/// Otherwise, the system uses the default "~/Library/Application Support/com.apple.container".
|
||||
public static let environmentName = "CONTAINER_APP_ROOT"
|
||||
|
||||
public static let defaultURL = FileManager.default.urls(
|
||||
for: .applicationSupportDirectory,
|
||||
in: .userDomainMask
|
||||
).first!.appendingPathComponent("com.apple.container")
|
||||
/// The default root directory used when ``environmentName`` is not set:
|
||||
/// `~/Library/Application Support/com.apple.container`.
|
||||
public static let defaultPath = FilePath(
|
||||
FileManager.default.urls(
|
||||
for: .applicationSupportDirectory,
|
||||
in: .userDomainMask
|
||||
).first!.path(percentEncoded: false)
|
||||
)
|
||||
.appending(FilePath.Component("com.apple.container"))
|
||||
|
||||
private static let envPath = ProcessInfo.processInfo.environment[Self.environmentName]
|
||||
/// The resolved root directory path, always lexically normalized.
|
||||
///
|
||||
/// If the environment variable is set to an absolute path, that path is used directly.
|
||||
/// If it is set to a relative path, the path is resolved against the working directory.
|
||||
/// Otherwise, ``defaultPath`` is used.
|
||||
public static let path = FilePath(FileManager.default.currentDirectoryPath).resolve(
|
||||
ProcessInfo.processInfo.environment[environmentName],
|
||||
defaultPath: defaultPath
|
||||
)
|
||||
|
||||
public static let url = envPath.map { URL(fileURLWithPath: $0) } ?? defaultURL
|
||||
|
||||
public static let path = url.path(percentEncoded: false)
|
||||
/// The pathname to the root directory
|
||||
public static let pathname = path.string
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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 SystemPackage
|
||||
|
||||
extension FilePath {
|
||||
/// Resolves a pathname string relative to this path.
|
||||
///
|
||||
/// The result is lexically normalized — `.` components are removed and `..` components
|
||||
/// collapse the preceding component. Absolute pathnames are returned normalized as-is;
|
||||
/// relative pathnames are appended to `self` before normalizing.
|
||||
///
|
||||
/// - Parameter pathname: The pathname to resolve.
|
||||
/// - Returns: The resolved ``FilePath``, or `nil` if `pathname` is `nil` or empty.
|
||||
package func resolve(_ pathname: String?) -> FilePath? {
|
||||
guard let pathname, !pathname.isEmpty else { return nil }
|
||||
let path = FilePath(pathname)
|
||||
guard !path.isAbsolute else { return path.lexicallyNormalized() }
|
||||
return self.appending(path.components).lexicallyNormalized()
|
||||
}
|
||||
|
||||
/// Resolves a pathname string relative to this path, falling back to a default.
|
||||
///
|
||||
/// The result is lexically normalized — `.` components are removed and `..` components
|
||||
/// collapse the preceding component. Absolute pathnames are returned normalized as-is;
|
||||
/// relative pathnames are appended to `self` before normalizing.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - pathname: The pathname to resolve.
|
||||
/// - defaultPath: The path returned when `pathname` is `nil` or empty.
|
||||
/// - Returns: The resolved ``FilePath``, or `defaultPath` lexically normalized if `pathname` is `nil` or empty.
|
||||
package func resolve(_ pathname: String?, defaultPath: FilePath) -> FilePath {
|
||||
resolve(pathname) ?? defaultPath.lexicallyNormalized()
|
||||
}
|
||||
}
|
||||
@@ -16,19 +16,34 @@
|
||||
|
||||
import ContainerVersion
|
||||
import Foundation
|
||||
import SystemPackage
|
||||
|
||||
/// Provides the application installation root path.
|
||||
public struct InstallRoot {
|
||||
/// The environment variable that if set, determines the root directory for installed application.
|
||||
/// Otherwise, the system computes the install path as the parent of the directory containing the
|
||||
/// application binary (for example, "/usr/local/bin/container" -> "/usr/local").
|
||||
public static let environmentName = "CONTAINER_INSTALL_ROOT"
|
||||
|
||||
public static let defaultURL = CommandLine.executablePathUrl
|
||||
.deletingLastPathComponent()
|
||||
.appendingPathComponent("..")
|
||||
.standardized
|
||||
/// The default root directory used when the environment variable is not set.
|
||||
///
|
||||
/// Computed as the grandparent of ``CommandLine/executablePath``
|
||||
/// (for example, `/usr/local/bin/container` → `/usr/local`).
|
||||
/// Lexically normalized but not canonical, as symlinks in the executable path are not resolved.
|
||||
public static let defaultPath = CommandLine.executablePath
|
||||
.removingLastComponent()
|
||||
.removingLastComponent()
|
||||
|
||||
private static let envPath = ProcessInfo.processInfo.environment[Self.environmentName]
|
||||
/// The resolved root directory path, always lexically normalized.
|
||||
///
|
||||
/// If the environment variable is set to an absolute path, that path is used directly.
|
||||
/// If it is set to a relative path, the path is resolved against the working directory.
|
||||
/// Otherwise, ``defaultPath`` is used.
|
||||
public static let path = FilePath(FileManager.default.currentDirectoryPath).resolve(
|
||||
ProcessInfo.processInfo.environment[environmentName],
|
||||
defaultPath: defaultPath
|
||||
)
|
||||
|
||||
public static let url = envPath.map { URL(fileURLWithPath: $0) } ?? defaultURL
|
||||
|
||||
public static let path = url.path(percentEncoded: false)
|
||||
/// The pathname to the root directory
|
||||
public static let pathname = path.string
|
||||
}
|
||||
|
||||
@@ -19,21 +19,19 @@ import SystemPackage
|
||||
|
||||
/// Provides the application data root path.
|
||||
public struct LogRoot {
|
||||
|
||||
private static let envPath = ProcessInfo.processInfo.environment[Self.environmentName].flatMap {
|
||||
$0.isEmpty ? nil : FilePath($0)
|
||||
}
|
||||
|
||||
/// The environment variable that if set, determines the root directory for log files.
|
||||
/// Otherwise, the application uses the macOS log facility.
|
||||
public static let environmentName = "CONTAINER_LOG_ROOT"
|
||||
|
||||
/// The path object for the log file root directory
|
||||
public static let path = envPath.map {
|
||||
guard !$0.isAbsolute else { return $0 }
|
||||
return FilePath(FileManager.default.currentDirectoryPath).appending($0.components)
|
||||
}
|
||||
/// The resolved root directory for log files, or `nil` if the environment variable is not set.
|
||||
///
|
||||
/// When non-nil, the path is always lexically normalized.
|
||||
/// If the environment variable is set to an absolute path, that path is used directly.
|
||||
/// If it is set to a relative path, the path is resolved against the working directory.
|
||||
public static let path = FilePath(FileManager.default.currentDirectoryPath).resolve(
|
||||
ProcessInfo.processInfo.environment[environmentName]
|
||||
)
|
||||
|
||||
/// The pathname to the log file root directory
|
||||
/// The pathname to the root directory
|
||||
public static let pathname = path?.string
|
||||
}
|
||||
|
||||
@@ -14,18 +14,33 @@
|
||||
// limitations under the License.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import Darwin
|
||||
import Foundation
|
||||
import SystemPackage
|
||||
|
||||
/// Retrieve the application bundle for a path that refers to a macOS executable.
|
||||
extension Bundle {
|
||||
public static func appBundle(executableURL: URL) -> Bundle? {
|
||||
let resolvedURL = executableURL.resolvingSymlinksInPath()
|
||||
let macOSURL = resolvedURL.deletingLastPathComponent()
|
||||
let contentsURL = macOSURL.deletingLastPathComponent()
|
||||
let bundleURL = contentsURL.deletingLastPathComponent()
|
||||
if bundleURL.pathExtension == "app" {
|
||||
return Bundle(url: bundleURL)
|
||||
}
|
||||
return nil
|
||||
/// Retrieves the application bundle for a path that refers to a macOS executable.
|
||||
///
|
||||
/// Resolves symlinks in `executablePath`, then walks up the standard macOS bundle layout
|
||||
/// (`MacOS/` → `Contents/` → `Foo.app/`) and verifies the `.app` extension.
|
||||
///
|
||||
/// - Parameter executablePath: The path to a macOS executable inside a bundle.
|
||||
/// - Returns: The ``Bundle`` at the resolved `.app` directory, or `nil` if the executable
|
||||
/// is not inside a valid macOS application bundle.
|
||||
public static func appBundle(executablePath: FilePath) -> Bundle? {
|
||||
let resolvedPath =
|
||||
executablePath.withPlatformString { cPath in
|
||||
Darwin.realpath(cPath, nil).map { ptr -> FilePath in
|
||||
defer { free(ptr) }
|
||||
return FilePath(platformString: ptr)
|
||||
}
|
||||
} ?? executablePath
|
||||
let bundlePath =
|
||||
resolvedPath
|
||||
.removingLastComponent() // MacOS/
|
||||
.removingLastComponent() // Contents/
|
||||
.removingLastComponent() // Foo.app/
|
||||
guard bundlePath.lastComponent?.extension == "app" else { return nil }
|
||||
return Bundle(url: URL(fileURLWithPath: bundlePath.string))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,9 +15,14 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import Foundation
|
||||
import SystemPackage
|
||||
|
||||
extension CommandLine {
|
||||
public static var executablePathUrl: URL {
|
||||
/// The path of the running executable.
|
||||
///
|
||||
/// Obtained via `_NSGetExecutablePath`, which returns an absolute, lexically normalized path
|
||||
/// (no `.` or `..` components). Symlinks are not resolved, so the path is not canonical.
|
||||
public static var executablePath: FilePath {
|
||||
/// _NSGetExecutablePath with a zero-length buffer returns the needed buffer length
|
||||
var bufferSize: Int32 = 0
|
||||
var buffer = [CChar](repeating: 0, count: Int(bufferSize))
|
||||
@@ -31,6 +36,6 @@ extension CommandLine {
|
||||
|
||||
/// Return the path with the executable file component removed the last component and
|
||||
let executablePath = String(cString: &buffer)
|
||||
return URL(filePath: executablePath)
|
||||
return FilePath(executablePath)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ public struct ReleaseVersion {
|
||||
}
|
||||
|
||||
public static func version() -> String {
|
||||
let appBundle = Bundle.appBundle(executableURL: CommandLine.executablePathUrl)
|
||||
let appBundle = Bundle.appBundle(executablePath: CommandLine.executablePath)
|
||||
let bundleVersion = appBundle?.infoDictionary?["CFBundleShortVersionString"] as? String
|
||||
return bundleVersion ?? get_release_version().map { String(cString: $0) } ?? "0.0.0"
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import ContainerXPC
|
||||
import Containerization
|
||||
import Foundation
|
||||
import Logging
|
||||
import SystemPackage
|
||||
|
||||
@main
|
||||
struct ImagesHelper: AsyncParsableCommand {
|
||||
@@ -51,9 +52,9 @@ extension ImagesHelper {
|
||||
@Option(name: .long, help: "XPC service prefix")
|
||||
var serviceIdentifier: String = "com.apple.container.core.container-core-images"
|
||||
|
||||
var appRoot = ApplicationRoot.url
|
||||
var appRoot = ApplicationRoot.path
|
||||
|
||||
var installRoot = InstallRoot.url
|
||||
var installRoot = InstallRoot.path
|
||||
|
||||
var logRoot = LogRoot.path
|
||||
|
||||
@@ -90,11 +91,13 @@ extension ImagesHelper {
|
||||
}
|
||||
}
|
||||
|
||||
private func initializeImagesService(root: URL, containerSystemConfig: ContainerSystemConfig, log: Logger, routes: inout [String: XPCServer.RouteHandler]) throws {
|
||||
private func initializeImagesService(root: FilePath, containerSystemConfig: ContainerSystemConfig, log: Logger, routes: inout [String: XPCServer.RouteHandler]) throws {
|
||||
// TODO: remove as part of ImageStore URL removal PR
|
||||
let rootURL = URL(fileURLWithPath: root.string)
|
||||
let contentStore = RemoteContentStoreClient()
|
||||
let imageStore = try ImageStore(path: root, contentStore: contentStore)
|
||||
let imageStore = try ImageStore(path: rootURL, contentStore: contentStore)
|
||||
let unpackStrategy = SnapshotStore.defaultUnpackStrategy(initImage: containerSystemConfig.vminit.image)
|
||||
let snapshotStore = try SnapshotStore(path: root, unpackStrategy: unpackStrategy, log: log)
|
||||
let snapshotStore = try SnapshotStore(path: rootURL, unpackStrategy: unpackStrategy, log: log)
|
||||
let service = try ImagesService(contentStore: contentStore, imageStore: imageStore, snapshotStore: snapshotStore, log: log)
|
||||
let harness = ImagesServiceHarness(service: service, log: log)
|
||||
|
||||
@@ -112,8 +115,10 @@ extension ImagesHelper {
|
||||
routes[ImagesServiceXPCRoute.snapshotGet.rawValue] = XPCServer.route(harness.getSnapshot)
|
||||
}
|
||||
|
||||
private func initializeContentService(root: URL, log: Logger, routes: inout [String: XPCServer.RouteHandler]) throws {
|
||||
let service = try ContentStoreService(root: root, log: log)
|
||||
private func initializeContentService(root: FilePath, log: Logger, routes: inout [String: XPCServer.RouteHandler]) throws {
|
||||
// TODO: remove as part of ImageStore URL removal PR
|
||||
let rootURL = URL(fileURLWithPath: root.string)
|
||||
let service = try ContentStoreService(root: rootURL, log: log)
|
||||
let harness = ContentServiceHarness(service: service, log: log)
|
||||
|
||||
routes[ImagesServiceXPCRoute.contentClean.rawValue] = XPCServer.route(harness.clean)
|
||||
|
||||
Reference in New Issue
Block a user