mirror of
https://github.com/apple/container.git
synced 2026-09-28 01:46:25 -04:00
Rename CLI and ExecutableCLI folders (#635)
## Motivation and Context This is an extension of #603 to cleanup the folder structure and have it match with the new library and target names.
This commit is contained in:
@@ -0,0 +1,335 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved.
|
||||
//
|
||||
// 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 ContainerClient
|
||||
import ContainerLog
|
||||
import ContainerPlugin
|
||||
import ContainerVersion
|
||||
import ContainerizationError
|
||||
import ContainerizationOS
|
||||
import Foundation
|
||||
import Logging
|
||||
import TerminalProgress
|
||||
|
||||
// `log` is updated only once in the `validate()` method.
|
||||
nonisolated(unsafe) var log = {
|
||||
LoggingSystem.bootstrap(StreamLogHandler.standardError)
|
||||
var log = Logger(label: "com.apple.container")
|
||||
log.logLevel = .info
|
||||
return log
|
||||
}()
|
||||
|
||||
public struct Application: AsyncParsableCommand {
|
||||
@OptionGroup
|
||||
var global: Flags.Global
|
||||
|
||||
public init() {}
|
||||
|
||||
public static let configuration = CommandConfiguration(
|
||||
commandName: "container",
|
||||
abstract: "A container platform for macOS",
|
||||
version: ReleaseVersion.singleLine(appName: "container CLI"),
|
||||
subcommands: [
|
||||
DefaultCommand.self
|
||||
],
|
||||
groupedSubcommands: [
|
||||
CommandGroup(
|
||||
name: "Container",
|
||||
subcommands: [
|
||||
ContainerCreate.self,
|
||||
ContainerDelete.self,
|
||||
ContainerExec.self,
|
||||
ContainerInspect.self,
|
||||
ContainerKill.self,
|
||||
ContainerList.self,
|
||||
ContainerLogs.self,
|
||||
ContainerRunCommand.self,
|
||||
ContainerStart.self,
|
||||
ContainerStop.self,
|
||||
]
|
||||
),
|
||||
CommandGroup(
|
||||
name: "Image",
|
||||
subcommands: [
|
||||
BuildCommand.self,
|
||||
ImageCommand.self,
|
||||
RegistryCommand.self,
|
||||
]
|
||||
),
|
||||
CommandGroup(
|
||||
name: "Volume",
|
||||
subcommands: [
|
||||
VolumeCommand.self
|
||||
]
|
||||
),
|
||||
CommandGroup(
|
||||
name: "Other",
|
||||
subcommands: Self.otherCommands()
|
||||
),
|
||||
],
|
||||
// Hidden command to handle plugins on unrecognized input.
|
||||
defaultSubcommand: DefaultCommand.self
|
||||
)
|
||||
|
||||
public static func main() async throws {
|
||||
restoreCursorAtExit()
|
||||
|
||||
#if DEBUG
|
||||
let warning = "Running debug build. Performance may be degraded."
|
||||
let formattedWarning = "\u{001B}[33mWarning!\u{001B}[0m \(warning)\n"
|
||||
let warningData = Data(formattedWarning.utf8)
|
||||
FileHandle.standardError.write(warningData)
|
||||
#endif
|
||||
|
||||
let fullArgs = CommandLine.arguments
|
||||
let args = Array(fullArgs.dropFirst())
|
||||
|
||||
do {
|
||||
// container -> defaultHelpCommand
|
||||
var command = try Application.parseAsRoot(args)
|
||||
if var asyncCommand = command as? AsyncParsableCommand {
|
||||
try await asyncCommand.run()
|
||||
} else {
|
||||
try command.run()
|
||||
}
|
||||
} catch {
|
||||
// Regular ol `command` with no args will get caught by DefaultCommand. --help
|
||||
// on the root command will land here.
|
||||
let containsHelp = fullArgs.contains("-h") || fullArgs.contains("--help")
|
||||
if fullArgs.count <= 2 && containsHelp {
|
||||
let pluginLoader = try? await createPluginLoader()
|
||||
await Self.printModifiedHelpText(pluginLoader: pluginLoader)
|
||||
return
|
||||
}
|
||||
let errorAsString: String = String(describing: error)
|
||||
if errorAsString.contains("XPC connection error") {
|
||||
let modifiedError = ContainerizationError(.interrupted, message: "\(error)\nEnsure container system service has been started with `container system start`.")
|
||||
Application.exit(withError: modifiedError)
|
||||
} else {
|
||||
Application.exit(withError: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static func createPluginLoader() async throws -> PluginLoader {
|
||||
let installRoot = CommandLine.executablePathUrl
|
||||
.deletingLastPathComponent()
|
||||
.appendingPathComponent("..")
|
||||
.standardized
|
||||
let pluginsURL = PluginLoader.userPluginsDir(installRoot: installRoot)
|
||||
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 installRootPluginsURL =
|
||||
installRoot
|
||||
.appendingPathComponent("libexec")
|
||||
.appendingPathComponent("container")
|
||||
.appendingPathComponent("plugins")
|
||||
.standardized
|
||||
|
||||
let pluginDirectories = [
|
||||
userPluginsURL,
|
||||
appBundlePluginsURL,
|
||||
installRootPluginsURL,
|
||||
].compactMap { $0 }
|
||||
|
||||
let pluginFactories: [any PluginFactory] = [
|
||||
DefaultPluginFactory(),
|
||||
AppBundlePluginFactory(),
|
||||
]
|
||||
|
||||
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,
|
||||
pluginDirectories: pluginDirectories,
|
||||
pluginFactories: pluginFactories,
|
||||
log: log
|
||||
)
|
||||
}
|
||||
|
||||
public static func handleProcess(io: ProcessIO, process: ClientProcess) async throws -> Int32 {
|
||||
let signals = AsyncSignalHandler.create(notify: Application.signalSet)
|
||||
return try await withThrowingTaskGroup(of: Int32?.self, returning: Int32.self) { group in
|
||||
let waitAdded = group.addTaskUnlessCancelled {
|
||||
let code = try await process.wait()
|
||||
try await io.wait()
|
||||
return code
|
||||
}
|
||||
|
||||
guard waitAdded else {
|
||||
group.cancelAll()
|
||||
return -1
|
||||
}
|
||||
|
||||
try await process.start()
|
||||
try io.closeAfterStart()
|
||||
|
||||
if let current = io.console {
|
||||
let size = try current.size
|
||||
// It's supremely possible the process could've exited already. We shouldn't treat
|
||||
// this as fatal.
|
||||
try? await process.resize(size)
|
||||
_ = group.addTaskUnlessCancelled {
|
||||
let winchHandler = AsyncSignalHandler.create(notify: [SIGWINCH])
|
||||
for await _ in winchHandler.signals {
|
||||
do {
|
||||
try await process.resize(try current.size)
|
||||
} catch {
|
||||
log.error(
|
||||
"failed to send terminal resize event",
|
||||
metadata: [
|
||||
"error": "\(error)"
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
_ = group.addTaskUnlessCancelled {
|
||||
for await sig in signals.signals {
|
||||
do {
|
||||
try await process.kill(sig)
|
||||
} catch {
|
||||
log.error(
|
||||
"failed to send signal",
|
||||
metadata: [
|
||||
"signal": "\(sig)",
|
||||
"error": "\(error)",
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
while true {
|
||||
let result = try await group.next()
|
||||
if result == nil {
|
||||
return -1
|
||||
}
|
||||
let status = result!
|
||||
if let status {
|
||||
group.cancelAll()
|
||||
return status
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
public func validate() throws {
|
||||
// Not really a "validation", but a cheat to run this before
|
||||
// any of the commands do their business.
|
||||
let debugEnvVar = ProcessInfo.processInfo.environment["CONTAINER_DEBUG"]
|
||||
if self.global.debug || debugEnvVar != nil {
|
||||
log.logLevel = .debug
|
||||
}
|
||||
// Ensure we're not running under Rosetta.
|
||||
if try isTranslated() {
|
||||
throw ValidationError(
|
||||
"""
|
||||
`container` is currently running under Rosetta Translation, which could be
|
||||
caused by your terminal application. Please ensure this is turned off.
|
||||
"""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private static func otherCommands() -> [any ParsableCommand.Type] {
|
||||
guard #available(macOS 26, *) else {
|
||||
return [
|
||||
BuilderCommand.self,
|
||||
SystemCommand.self,
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
BuilderCommand.self,
|
||||
NetworkCommand.self,
|
||||
SystemCommand.self,
|
||||
]
|
||||
}
|
||||
|
||||
private static func restoreCursorAtExit() {
|
||||
let signalHandler: @convention(c) (Int32) -> Void = { signal in
|
||||
let exitCode = ExitCode(signal + 128)
|
||||
Application.exit(withError: exitCode)
|
||||
}
|
||||
// Termination by Ctrl+C.
|
||||
signal(SIGINT, signalHandler)
|
||||
// Termination using `kill`.
|
||||
signal(SIGTERM, signalHandler)
|
||||
// Normal and explicit exit.
|
||||
atexit {
|
||||
if let progressConfig = try? ProgressConfig() {
|
||||
let progressBar = ProgressBar(config: progressConfig)
|
||||
progressBar.resetCursor()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Application {
|
||||
// Because we support plugins, we need to modify the help text to display
|
||||
// any if we found some.
|
||||
static func printModifiedHelpText(pluginLoader: PluginLoader?) async {
|
||||
let original = Application.helpMessage(for: Application.self)
|
||||
guard let pluginLoader else {
|
||||
print(original)
|
||||
print("PLUGINS: not available, run `container system start`")
|
||||
return
|
||||
}
|
||||
let altered = pluginLoader.alterCLIHelpText(original: original)
|
||||
print(altered)
|
||||
}
|
||||
|
||||
public enum ListFormat: String, CaseIterable, ExpressibleByArgument {
|
||||
case json
|
||||
case table
|
||||
}
|
||||
|
||||
static let signalSet: [Int32] = [
|
||||
SIGTERM,
|
||||
SIGINT,
|
||||
SIGUSR1,
|
||||
SIGUSR2,
|
||||
SIGWINCH,
|
||||
]
|
||||
|
||||
func isTranslated() throws -> Bool {
|
||||
do {
|
||||
return try Sysctl.byName("sysctl.proc_translated") == 1
|
||||
} catch let posixErr as POSIXError {
|
||||
if posixErr.code == .ENOENT {
|
||||
return false
|
||||
}
|
||||
throw posixErr
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user