mirror of
https://github.com/apple/container.git
synced 2026-10-12 17:19:13 +00:00
202 lines
7.4 KiB
Swift
202 lines
7.4 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
// 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 ArgumentParser
|
|
import ContainerAPIClient
|
|
import ContainerLog
|
|
import Containerization
|
|
import ContainerizationOCI
|
|
import Foundation
|
|
import Logging
|
|
|
|
/// Options shared by `container build` and plugins that wrap it.
|
|
public struct BuildOptions: ParsableArguments {
|
|
public init() {}
|
|
|
|
public enum ProgressType: String, ExpressibleByArgument {
|
|
case auto
|
|
case plain
|
|
case tty
|
|
}
|
|
|
|
public enum SecretType: Decodable {
|
|
case data(Data)
|
|
case file(String)
|
|
}
|
|
|
|
@Option(
|
|
name: .shortAndLong,
|
|
help: ArgumentHelp("Add the architecture type to the build", valueName: "value"),
|
|
transform: { val in val.split(separator: ",").map { String($0) } }
|
|
)
|
|
public var arch: [[String]] = {
|
|
[[Arch.hostArchitecture().rawValue]]
|
|
}()
|
|
|
|
@Option(name: .long, help: ArgumentHelp("Set build-time variables", valueName: "key=val"))
|
|
public var buildArg: [String] = []
|
|
|
|
@Option(name: .long, help: ArgumentHelp("Cache imports for the build", valueName: "value", visibility: .hidden))
|
|
public var cacheIn: [String] = {
|
|
[]
|
|
}()
|
|
|
|
@Option(name: .long, help: ArgumentHelp("Cache exports for the build", valueName: "value", visibility: .hidden))
|
|
public var cacheOut: [String] = {
|
|
[]
|
|
}()
|
|
|
|
@Option(name: .shortAndLong, help: "Number of CPUs to allocate to the builder container")
|
|
public var cpus: Int64?
|
|
|
|
@Option(name: .shortAndLong, help: ArgumentHelp("Path to Dockerfile", valueName: "path"))
|
|
public var file: String?
|
|
|
|
@Option(name: .shortAndLong, help: ArgumentHelp("Set a label", valueName: "key=val"))
|
|
public var label: [String] = []
|
|
|
|
@Option(
|
|
name: .shortAndLong,
|
|
help: "Amount of builder container memory (1MiByte granularity), with optional K, M, G, T, or P suffix"
|
|
)
|
|
public var memory: String?
|
|
|
|
@Flag(name: .long, help: "Do not use cache")
|
|
public var noCache: Bool = false
|
|
|
|
@Option(name: .shortAndLong, help: ArgumentHelp("Output configuration for the build (format: type=<oci|tar|local>[,dest=])", valueName: "value"))
|
|
public var output: [String] = {
|
|
["type=oci"]
|
|
}()
|
|
|
|
@Option(
|
|
name: .long,
|
|
help: ArgumentHelp("Add the OS type to the build", valueName: "value"),
|
|
transform: { val in val.split(separator: ",").map { String($0) } }
|
|
)
|
|
public var os: [[String]] = {
|
|
[["linux"]]
|
|
}()
|
|
|
|
@Option(
|
|
name: .long,
|
|
help: "Add the platform to the build (format: os/arch[/variant], takes precedence over --os and --arch) [environment: CONTAINER_DEFAULT_PLATFORM]",
|
|
transform: { val in val.split(separator: ",").map { String($0) } }
|
|
)
|
|
public var platform: [[String]] = [[]]
|
|
|
|
@Option(name: .long, help: ArgumentHelp("Progress type (format: auto|plain|tty)", valueName: "type"))
|
|
public var progress: ProgressType = .auto
|
|
|
|
@Flag(name: .shortAndLong, help: "Suppress build output")
|
|
public var quiet: Bool = false
|
|
|
|
@Option(name: .long, help: ArgumentHelp("Set build-time secrets (format: id=<key>[,env=<ENV_VAR>|,src=<local/path>])", valueName: "id=key,..."))
|
|
public var secret: [String] = []
|
|
|
|
public private(set) var secrets: [String: SecretType] = [:]
|
|
|
|
@Option(
|
|
name: .long,
|
|
help: ArgumentHelp("Forward SSH agent authentication to the build (format: default)", valueName: "default")
|
|
)
|
|
public var ssh: String = ""
|
|
|
|
@Option(name: [.short, .customLong("tag")], help: ArgumentHelp("Name for the built image", valueName: "name"))
|
|
public var targetImageNames: [String] = {
|
|
[UUID().uuidString.lowercased()]
|
|
}()
|
|
|
|
@Option(name: .long, help: ArgumentHelp("Set the target build stage", valueName: "stage"))
|
|
public var target: String = ""
|
|
|
|
@Option(name: .long, help: ArgumentHelp("Builder shim vsock port", valueName: "port"))
|
|
public var vsockPort: UInt32 = 8088
|
|
|
|
@OptionGroup
|
|
public var logOptions: Flags.Logging
|
|
|
|
@OptionGroup
|
|
public var dns: Flags.DNS
|
|
|
|
@Argument(help: "Build directory")
|
|
public var contextDir: String = "."
|
|
|
|
@Flag(name: .long, help: "Pull latest image")
|
|
public var pull: Bool = false
|
|
|
|
public var log: Logger {
|
|
var logger = Logger(label: "container", factory: { _ in StderrLogHandler() })
|
|
logger.logLevel = logOptions.debug ? .debug : .info
|
|
return logger
|
|
}
|
|
|
|
public mutating func validate() throws {
|
|
guard FileManager.default.fileExists(atPath: contextDir) else {
|
|
throw ValidationError("context dir does not exist \(contextDir)")
|
|
}
|
|
for name in targetImageNames {
|
|
guard let _ = try? Reference.parse(name) else {
|
|
throw ValidationError("invalid reference \(name)")
|
|
}
|
|
}
|
|
|
|
if let filepath = file, filepath != "-" {
|
|
let fileURL = URL(fileURLWithPath: filepath, relativeTo: .currentDirectory())
|
|
guard FileManager.default.fileExists(atPath: fileURL.path) else {
|
|
throw ValidationError("file does not exist \(filepath)")
|
|
}
|
|
}
|
|
|
|
// Parse --secret args
|
|
for secret in self.secret {
|
|
let parts = secret.split(separator: ",", maxSplits: 1, omittingEmptySubsequences: false)
|
|
guard parts[0].hasPrefix("id=") else {
|
|
throw ValidationError("secret must start with id=<key> \(secret)")
|
|
}
|
|
let key = String(parts[0].dropFirst(3))
|
|
guard !key.contains("=") else {
|
|
throw ValidationError("secret id cannot contain '=' \(key)")
|
|
}
|
|
if parts.count == 1 || parts[1].hasPrefix("env=") {
|
|
let env = parts.count == 1 ? key : String(parts[1].dropFirst(4))
|
|
// Using getenv/strlen over processInfo.environment to support
|
|
// non-UTF-8 env var data.
|
|
guard let ptr = getenv(env) else {
|
|
throw ValidationError("secret env var doesn't exist \(env)")
|
|
}
|
|
self.secrets[key] = .data(Data(bytes: ptr, count: strlen(ptr)))
|
|
} else if parts[1].hasPrefix("src=") {
|
|
let path = String(parts[1].dropFirst(4))
|
|
self.secrets[key] = .file(path)
|
|
} else {
|
|
throw ValidationError("secret bad value \(parts[1])")
|
|
}
|
|
}
|
|
|
|
switch ssh {
|
|
case "":
|
|
break
|
|
case "default" where ProcessInfo.processInfo.environment["SSH_AUTH_SOCK"] != nil:
|
|
break
|
|
case "default":
|
|
throw ValidationError("--ssh default requires SSH_AUTH_SOCK to be set")
|
|
default:
|
|
throw ValidationError("only --ssh default is currently supported")
|
|
}
|
|
}
|
|
}
|