diff --git a/Sources/ContainerCommands/Application.swift b/Sources/ContainerCommands/Application.swift index c77f51a5..6845bb15 100644 --- a/Sources/ContainerCommands/Application.swift +++ b/Sources/ContainerCommands/Application.swift @@ -129,7 +129,7 @@ public struct Application: AsyncLoggableCommand { // by ArgumentParser and lands here. let containsHelp = fullArgs.contains("-h") || fullArgs.contains("--help") if fullArgs.count <= 2 && containsHelp { - let pluginLoader = try? await createPluginLoader() + let pluginLoader = try? await Utility.createPluginLoader(log: bootstrapLogger) await Self.printModifiedHelpText(pluginLoader: pluginLoader) return } @@ -143,51 +143,6 @@ public struct Application: AsyncLoggableCommand { } } - public static func createPluginLoader() async throws -> PluginLoader { - 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 - - // plugins built into the application installed as a macOS app bundle - let appBundlePluginsURL = Bundle.main.resourceURL?.appending(path: "plugins") - - // plugins built into the application installed as a Unix-like application - 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, - installRootPluginsURL, - ].compactMap { $0 } - - let pluginFactories: [any PluginFactory] = [ - DefaultPluginFactory(logger: bootstrapLogger), - AppBundlePluginFactory(logger: bootstrapLogger), - ] - - guard let systemHealth = try? await ClientHealthCheck.ping(timeout: .seconds(10)) else { - throw ContainerizationError(.timeout, message: "unable to retrieve application data root from API server") - } - return try PluginLoader( - appRoot: systemHealth.appRoot, - installRoot: systemHealth.installRoot, - logRoot: systemHealth.logRoot, - pluginDirectories: pluginDirectories, - pluginFactories: pluginFactories, - log: bootstrapLogger - ) - } - /// Load the system configuration using `appRoot` / `installRoot` reported by the /// daemon. `container system start` MUST have previously been run to start the daemon. public static func loadContainerSystemConfig() async throws -> ContainerSystemConfig { diff --git a/Sources/ContainerCommands/DefaultCommand.swift b/Sources/ContainerCommands/DefaultCommand.swift index 3df8f570..e1e6ac39 100644 --- a/Sources/ContainerCommands/DefaultCommand.swift +++ b/Sources/ContainerCommands/DefaultCommand.swift @@ -35,7 +35,7 @@ struct DefaultCommand: AsyncLoggableCommand { func run() async throws { // See if we have a possible plugin command. - let pluginLoader = try? await Application.createPluginLoader() + let pluginLoader = try? await Utility.createPluginLoader(log: log) guard let command = remaining.first else { await Application.printModifiedHelpText(pluginLoader: pluginLoader) return diff --git a/Sources/ContainerCommands/HelpCommand.swift b/Sources/ContainerCommands/HelpCommand.swift index 43e9107c..e6d3015b 100644 --- a/Sources/ContainerCommands/HelpCommand.swift +++ b/Sources/ContainerCommands/HelpCommand.swift @@ -31,7 +31,7 @@ struct HelpCommand: AsyncLoggableCommand { func run() async throws { if subcommandPath.isEmpty { - let pluginLoader = try? await Application.createPluginLoader() + let pluginLoader = try? await Utility.createPluginLoader(log: log) await Application.printModifiedHelpText(pluginLoader: pluginLoader) return } diff --git a/Sources/Services/ContainerAPIService/Client/Utility+PluginLoader.swift b/Sources/Services/ContainerAPIService/Client/Utility+PluginLoader.swift new file mode 100644 index 00000000..d631e67c --- /dev/null +++ b/Sources/Services/ContainerAPIService/Client/Utility+PluginLoader.swift @@ -0,0 +1,68 @@ +//===----------------------------------------------------------------------===// +// 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 ContainerPlugin +import ContainerizationError +import Foundation +import Logging +import SystemPackage + +extension Utility { + public static func createPluginLoader(log: Logger) async throws -> PluginLoader { + 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 + + // plugins built into the application installed as a macOS app bundle + let appBundlePluginsURL = Bundle.main.resourceURL?.appending(path: "plugins") + + // plugins built into the application installed as a Unix-like application + 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, + installRootPluginsURL, + ].compactMap { $0 } + + let pluginFactories: [any PluginFactory] = [ + DefaultPluginFactory(logger: log), + AppBundlePluginFactory(logger: log), + ] + + guard let systemHealth = try? await ClientHealthCheck.ping(timeout: .seconds(10)) else { + throw ContainerizationError(.timeout, message: "unable to retrieve application data root from API server") + } + return try PluginLoader( + appRoot: systemHealth.appRoot, + installRoot: systemHealth.installRoot, + logRoot: systemHealth.logRoot, + pluginDirectories: pluginDirectories, + pluginFactories: pluginFactories, + log: log + ) + } +}