2025-06-03 15:23:07 -07:00
|
|
|
//===----------------------------------------------------------------------===//
|
2026-01-05 13:09:34 -08:00
|
|
|
// Copyright © 2025-2026 Apple Inc. and the container project authors.
|
2025-06-03 15:23:07 -07:00
|
|
|
//
|
|
|
|
|
// 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
|
2026-01-06 08:27:14 -08:00
|
|
|
import ContainerAPIClient
|
2025-06-03 15:23:07 -07:00
|
|
|
import ContainerBuild
|
|
|
|
|
import ContainerImagesServiceClient
|
|
|
|
|
import Containerization
|
|
|
|
|
import ContainerizationError
|
|
|
|
|
import ContainerizationOCI
|
|
|
|
|
import ContainerizationOS
|
|
|
|
|
import Foundation
|
|
|
|
|
import NIO
|
|
|
|
|
import TerminalProgress
|
|
|
|
|
|
|
|
|
|
extension Application {
|
2026-01-20 14:38:29 -08:00
|
|
|
public struct BuildCommand: AsyncLoggableCommand {
|
2026-03-16 12:58:32 -07:00
|
|
|
private static let hiddenDockerDir = ".com.apple.container.dockerfiles"
|
|
|
|
|
|
2025-09-17 15:24:26 -07:00
|
|
|
public init() {}
|
2025-06-03 15:23:07 -07:00
|
|
|
public static var configuration: CommandConfiguration {
|
|
|
|
|
var config = CommandConfiguration()
|
|
|
|
|
config.commandName = "build"
|
2025-10-27 08:28:29 -07:00
|
|
|
config.abstract = "Build an image from a Dockerfile or Containerfile"
|
2025-06-03 15:23:07 -07:00
|
|
|
config._superCommandName = "container"
|
|
|
|
|
config.helpNames = NameSpecification(arrayLiteral: .customShort("h"), .customLong("help"))
|
|
|
|
|
return config
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-30 11:38:25 -07:00
|
|
|
enum ProgressType: String, ExpressibleByArgument {
|
|
|
|
|
case auto
|
|
|
|
|
case plain
|
|
|
|
|
case tty
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-16 17:04:24 -07:00
|
|
|
enum SecretType: Decodable {
|
|
|
|
|
case data(Data)
|
|
|
|
|
case file(String)
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-03 15:23:07 -07:00
|
|
|
@Option(
|
2025-09-22 16:36:07 -07:00
|
|
|
name: .shortAndLong,
|
|
|
|
|
help: ArgumentHelp("Add the architecture type to the build", valueName: "value"),
|
|
|
|
|
transform: { val in val.split(separator: ",").map { String($0) } }
|
2025-06-03 15:23:07 -07:00
|
|
|
)
|
2025-09-22 16:36:07 -07:00
|
|
|
var arch: [[String]] = {
|
|
|
|
|
[[Arch.hostArchitecture().rawValue]]
|
|
|
|
|
}()
|
2025-06-03 15:23:07 -07:00
|
|
|
|
|
|
|
|
@Option(name: .long, help: ArgumentHelp("Set build-time variables", valueName: "key=val"))
|
|
|
|
|
var buildArg: [String] = []
|
|
|
|
|
|
2025-09-22 16:36:07 -07:00
|
|
|
@Option(name: .long, help: ArgumentHelp("Cache imports for the build", valueName: "value", visibility: .hidden))
|
|
|
|
|
var cacheIn: [String] = {
|
|
|
|
|
[]
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
@Option(name: .long, help: ArgumentHelp("Cache exports for the build", valueName: "value", visibility: .hidden))
|
|
|
|
|
var cacheOut: [String] = {
|
|
|
|
|
[]
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
@Option(name: .shortAndLong, help: "Number of CPUs to allocate to the builder container")
|
2026-03-05 17:53:47 -08:00
|
|
|
var cpus: Int64?
|
2025-06-03 15:23:07 -07:00
|
|
|
|
|
|
|
|
@Option(name: .shortAndLong, help: ArgumentHelp("Path to Dockerfile", valueName: "path"))
|
2025-10-27 08:28:29 -07:00
|
|
|
var file: String?
|
2025-06-03 15:23:07 -07:00
|
|
|
|
2026-03-16 12:58:32 -07:00
|
|
|
var dockerfile: String = "-"
|
|
|
|
|
|
2025-06-03 15:23:07 -07:00
|
|
|
@Option(name: .shortAndLong, help: ArgumentHelp("Set a label", valueName: "key=val"))
|
|
|
|
|
var label: [String] = []
|
|
|
|
|
|
2025-09-22 16:36:07 -07:00
|
|
|
@Option(
|
|
|
|
|
name: .shortAndLong,
|
2025-09-23 10:55:04 -07:00
|
|
|
help: "Amount of builder container memory (1MiByte granularity), with optional K, M, G, T, or P suffix"
|
2025-09-22 16:36:07 -07:00
|
|
|
)
|
2026-03-05 17:53:47 -08:00
|
|
|
var memory: String?
|
2025-09-22 16:36:07 -07:00
|
|
|
|
2025-06-03 15:23:07 -07:00
|
|
|
@Flag(name: .long, help: "Do not use cache")
|
|
|
|
|
var noCache: Bool = false
|
|
|
|
|
|
2025-09-22 16:36:07 -07:00
|
|
|
@Option(name: .shortAndLong, help: ArgumentHelp("Output configuration for the build (format: type=<oci|tar|local>[,dest=])", valueName: "value"))
|
2025-06-03 15:23:07 -07:00
|
|
|
var output: [String] = {
|
|
|
|
|
["type=oci"]
|
|
|
|
|
}()
|
|
|
|
|
|
Uniform support for `--platform`, `--os`, `--arch`. (#545)
- Fixes #231.
- Extends #313 (@dcantah) so that all of `container create`, `container
run`, `container build`, `container image pull`, and `container image
save` accept the three options.
- `container build` now processes comma-separated lists for
`--platform`, `--arch`, and `--os`. It first checks `--platform`,
assembling the union of all platform values. If that set is non-empty,
the builder builds the values in the set. Otherwise, the set consists of
all combinations of the specified architecture and os values, finally
defaulting to `linux` and the host architecture if no options are
provided.
- All other commands work accept a single platform, preferring the
`--platform` option over `--arch` and `--os` when both are specified.
`--os` defaults to `linux`, and `--arch` defaults to the host
architecture.
- Clarify help messages and present the args in consistent order, with
platform first since it takes precedence if present.
- Deduplicate redundant platform options for `container build`.
2025-08-27 13:26:01 -07:00
|
|
|
@Option(
|
|
|
|
|
name: .long,
|
2025-09-22 16:36:07 -07:00
|
|
|
help: ArgumentHelp("Add the OS type to the build", valueName: "value"),
|
Uniform support for `--platform`, `--os`, `--arch`. (#545)
- Fixes #231.
- Extends #313 (@dcantah) so that all of `container create`, `container
run`, `container build`, `container image pull`, and `container image
save` accept the three options.
- `container build` now processes comma-separated lists for
`--platform`, `--arch`, and `--os`. It first checks `--platform`,
assembling the union of all platform values. If that set is non-empty,
the builder builds the values in the set. Otherwise, the set consists of
all combinations of the specified architecture and os values, finally
defaulting to `linux` and the host architecture if no options are
provided.
- All other commands work accept a single platform, preferring the
`--platform` option over `--arch` and `--os` when both are specified.
`--os` defaults to `linux`, and `--arch` defaults to the host
architecture.
- Clarify help messages and present the args in consistent order, with
platform first since it takes precedence if present.
- Deduplicate redundant platform options for `container build`.
2025-08-27 13:26:01 -07:00
|
|
|
transform: { val in val.split(separator: ",").map { String($0) } }
|
|
|
|
|
)
|
|
|
|
|
var os: [[String]] = {
|
|
|
|
|
[["linux"]]
|
2025-06-03 15:23:07 -07:00
|
|
|
}()
|
|
|
|
|
|
Uniform support for `--platform`, `--os`, `--arch`. (#545)
- Fixes #231.
- Extends #313 (@dcantah) so that all of `container create`, `container
run`, `container build`, `container image pull`, and `container image
save` accept the three options.
- `container build` now processes comma-separated lists for
`--platform`, `--arch`, and `--os`. It first checks `--platform`,
assembling the union of all platform values. If that set is non-empty,
the builder builds the values in the set. Otherwise, the set consists of
all combinations of the specified architecture and os values, finally
defaulting to `linux` and the host architecture if no options are
provided.
- All other commands work accept a single platform, preferring the
`--platform` option over `--arch` and `--os` when both are specified.
`--os` defaults to `linux`, and `--arch` defaults to the host
architecture.
- Clarify help messages and present the args in consistent order, with
platform first since it takes precedence if present.
- Deduplicate redundant platform options for `container build`.
2025-08-27 13:26:01 -07:00
|
|
|
@Option(
|
2025-09-22 16:36:07 -07:00
|
|
|
name: .long,
|
2026-03-07 02:38:16 -05:00
|
|
|
help: "Add the platform to the build (format: os/arch[/variant], takes precedence over --os and --arch) [environment: CONTAINER_DEFAULT_PLATFORM]",
|
Uniform support for `--platform`, `--os`, `--arch`. (#545)
- Fixes #231.
- Extends #313 (@dcantah) so that all of `container create`, `container
run`, `container build`, `container image pull`, and `container image
save` accept the three options.
- `container build` now processes comma-separated lists for
`--platform`, `--arch`, and `--os`. It first checks `--platform`,
assembling the union of all platform values. If that set is non-empty,
the builder builds the values in the set. Otherwise, the set consists of
all combinations of the specified architecture and os values, finally
defaulting to `linux` and the host architecture if no options are
provided.
- All other commands work accept a single platform, preferring the
`--platform` option over `--arch` and `--os` when both are specified.
`--os` defaults to `linux`, and `--arch` defaults to the host
architecture.
- Clarify help messages and present the args in consistent order, with
platform first since it takes precedence if present.
- Deduplicate redundant platform options for `container build`.
2025-08-27 13:26:01 -07:00
|
|
|
transform: { val in val.split(separator: ",").map { String($0) } }
|
|
|
|
|
)
|
2025-09-22 16:36:07 -07:00
|
|
|
var platform: [[String]] = [[]]
|
2025-06-03 15:23:07 -07:00
|
|
|
|
2025-10-30 11:38:25 -07:00
|
|
|
@Option(name: .long, help: ArgumentHelp("Progress type (format: auto|plain|tty)", valueName: "type"))
|
|
|
|
|
var progress: ProgressType = .auto
|
2025-06-03 15:23:07 -07:00
|
|
|
|
2025-09-22 16:36:07 -07:00
|
|
|
@Flag(name: .shortAndLong, help: "Suppress build output")
|
|
|
|
|
var quiet: Bool = false
|
2025-06-03 15:23:07 -07:00
|
|
|
|
2026-03-16 17:04:24 -07:00
|
|
|
@Option(name: .long, help: ArgumentHelp("Set build-time secrets (format: id=<key>[,env=<ENV_VAR>|,src=<local/path>])", valueName: "id=key,..."))
|
|
|
|
|
var secret: [String] = []
|
|
|
|
|
|
|
|
|
|
var secrets: [String: SecretType] = [:]
|
|
|
|
|
|
2025-09-22 16:36:07 -07:00
|
|
|
@Option(name: [.short, .customLong("tag")], help: ArgumentHelp("Name for the built image", valueName: "name"))
|
2025-10-21 13:03:46 -07:00
|
|
|
var targetImageNames: [String] = {
|
|
|
|
|
[UUID().uuidString.lowercased()]
|
|
|
|
|
}()
|
2025-06-03 15:23:07 -07:00
|
|
|
|
|
|
|
|
@Option(name: .long, help: ArgumentHelp("Set the target build stage", valueName: "stage"))
|
|
|
|
|
var target: String = ""
|
|
|
|
|
|
2025-09-22 16:36:07 -07:00
|
|
|
@Option(name: .long, help: ArgumentHelp("Builder shim vsock port", valueName: "port"))
|
|
|
|
|
var vsockPort: UInt32 = 8088
|
|
|
|
|
|
2026-01-20 14:38:29 -08:00
|
|
|
@OptionGroup
|
|
|
|
|
public var logOptions: Flags.Logging
|
|
|
|
|
|
2026-01-23 23:01:26 +02:00
|
|
|
@OptionGroup
|
|
|
|
|
public var dns: Flags.DNS
|
|
|
|
|
|
2025-09-22 16:36:07 -07:00
|
|
|
@Argument(help: "Build directory")
|
|
|
|
|
var contextDir: String = "."
|
2025-06-03 15:23:07 -07:00
|
|
|
|
2026-02-05 03:13:36 -05:00
|
|
|
@Flag(name: .long, help: "Pull latest image")
|
|
|
|
|
var pull: Bool = false
|
|
|
|
|
|
2025-09-17 15:24:26 -07:00
|
|
|
public func run() async throws {
|
2025-06-03 15:23:07 -07:00
|
|
|
do {
|
|
|
|
|
let timeout: Duration = .seconds(300)
|
|
|
|
|
let progressConfig = try ProgressConfig(
|
|
|
|
|
showTasks: true,
|
|
|
|
|
showItems: true
|
|
|
|
|
)
|
|
|
|
|
let progress = ProgressBar(config: progressConfig)
|
|
|
|
|
defer {
|
|
|
|
|
progress.finish()
|
|
|
|
|
}
|
|
|
|
|
progress.start()
|
|
|
|
|
|
|
|
|
|
progress.set(description: "Dialing builder")
|
|
|
|
|
|
2026-01-23 23:01:26 +02:00
|
|
|
let dnsNameservers = self.dns.nameservers
|
|
|
|
|
let builder: Builder? = try await withThrowingTaskGroup(of: Builder.self) { [vsockPort, cpus, memory, dnsNameservers] group in
|
2025-06-03 15:23:07 -07:00
|
|
|
defer {
|
|
|
|
|
group.cancelAll()
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-23 23:01:26 +02:00
|
|
|
group.addTask { [vsockPort, cpus, memory, log, dnsNameservers] in
|
2026-02-04 09:52:09 -08:00
|
|
|
let client = ContainerClient()
|
2025-06-03 15:23:07 -07:00
|
|
|
while true {
|
|
|
|
|
do {
|
2026-02-04 09:52:09 -08:00
|
|
|
let fh = try await client.dial(id: "buildkit", port: vsockPort)
|
2025-06-03 15:23:07 -07:00
|
|
|
|
|
|
|
|
let threadGroup: MultiThreadedEventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
|
2026-03-18 01:33:21 -07:00
|
|
|
let b = try Builder(socket: fh, group: threadGroup)
|
2025-06-03 15:23:07 -07:00
|
|
|
|
|
|
|
|
// If this call succeeds, then BuildKit is running.
|
|
|
|
|
let _ = try await b.info()
|
|
|
|
|
return b
|
|
|
|
|
} catch {
|
|
|
|
|
// If we get here, "Dialing builder" is shown for such a short period
|
|
|
|
|
// of time that it's invisible to the user.
|
|
|
|
|
progress.set(tasks: 0)
|
|
|
|
|
progress.set(totalTasks: 3)
|
|
|
|
|
|
|
|
|
|
try await BuilderStart.start(
|
2025-09-17 15:24:26 -07:00
|
|
|
cpus: cpus,
|
|
|
|
|
memory: memory,
|
2026-01-20 14:38:29 -08:00
|
|
|
log: log,
|
2026-01-23 23:01:26 +02:00
|
|
|
dnsNameservers: dnsNameservers,
|
2025-06-03 15:23:07 -07:00
|
|
|
progressUpdate: progress.handler
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// wait (seconds) for builder to start listening on vsock
|
|
|
|
|
try await Task.sleep(for: .seconds(5))
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
group.addTask {
|
|
|
|
|
try await Task.sleep(for: timeout)
|
|
|
|
|
throw ValidationError(
|
|
|
|
|
"""
|
|
|
|
|
Timeout waiting for connection to builder
|
|
|
|
|
"""
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return try await group.next()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
guard let builder else {
|
|
|
|
|
throw ValidationError("builder is not running")
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-18 07:25:11 -08:00
|
|
|
let buildFileData: Data
|
2026-03-16 12:58:32 -07:00
|
|
|
var ignoreFileData: Data? = nil
|
|
|
|
|
var hiddenDockerDir: String? = nil
|
2025-11-18 07:25:11 -08:00
|
|
|
// Dockerfile should be read from stdin
|
2026-03-16 12:58:32 -07:00
|
|
|
if dockerfile == "-" {
|
2025-11-18 07:25:11 -08:00
|
|
|
let tempFile = FileManager.default.temporaryDirectory.appendingPathComponent("Dockerfile-\(UUID().uuidString)")
|
|
|
|
|
defer {
|
|
|
|
|
try? FileManager.default.removeItem(at: tempFile)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
guard FileManager.default.createFile(atPath: tempFile.path(), contents: nil) else {
|
|
|
|
|
throw ContainerizationError(.internalError, message: "unable to create temporary file")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
guard let fileHandle = try? FileHandle(forWritingTo: tempFile) else {
|
|
|
|
|
throw ContainerizationError(.internalError, message: "unable to open temporary file for writing")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let bufferSize = 4096
|
|
|
|
|
while true {
|
|
|
|
|
let chunk = FileHandle.standardInput.readData(ofLength: bufferSize)
|
|
|
|
|
if chunk.isEmpty { break }
|
|
|
|
|
fileHandle.write(chunk)
|
|
|
|
|
}
|
|
|
|
|
try fileHandle.close()
|
|
|
|
|
buildFileData = try Data(contentsOf: URL(filePath: tempFile.path()))
|
|
|
|
|
} else {
|
2026-03-16 12:58:32 -07:00
|
|
|
let ignoreFileURL = URL(filePath: dockerfile + ".dockerignore")
|
|
|
|
|
buildFileData = try Data(contentsOf: URL(filePath: dockerfile))
|
|
|
|
|
ignoreFileData = try? Data(contentsOf: ignoreFileURL)
|
|
|
|
|
|
|
|
|
|
if var ignoreFileData {
|
|
|
|
|
hiddenDockerDir = Self.hiddenDockerDir
|
|
|
|
|
let hiddenDirInContext = URL(fileURLWithPath: contextDir).appendingPathComponent(Self.hiddenDockerDir)
|
|
|
|
|
|
|
|
|
|
try FileManager.default.createDirectory(at: hiddenDirInContext, withIntermediateDirectories: true)
|
|
|
|
|
try buildFileData.write(to: hiddenDirInContext.appendingPathComponent("Dockerfile"))
|
|
|
|
|
|
|
|
|
|
ignoreFileData.append("\n\(Self.hiddenDockerDir)".data(using: .utf8) ?? Data())
|
|
|
|
|
try ignoreFileData.write(to: hiddenDirInContext.appendingPathComponent("Dockerfile.dockerignore"))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
defer {
|
|
|
|
|
if let hiddenDockerDir {
|
|
|
|
|
let hiddenDirInContext = URL(fileURLWithPath: contextDir).appendingPathComponent(hiddenDockerDir)
|
|
|
|
|
try? FileManager.default.removeItem(at: hiddenDirInContext)
|
|
|
|
|
}
|
2025-11-18 07:25:11 -08:00
|
|
|
}
|
2025-10-27 08:28:29 -07:00
|
|
|
|
2026-03-16 17:04:24 -07:00
|
|
|
let secretsData: [String: Data] = try self.secrets.mapValues { secret in
|
|
|
|
|
switch secret {
|
|
|
|
|
case .data(let data):
|
|
|
|
|
return data
|
|
|
|
|
case .file(let path):
|
|
|
|
|
return try Data(contentsOf: URL(fileURLWithPath: path))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-06 14:49:09 -07:00
|
|
|
let systemHealth = try await ClientHealthCheck.ping(timeout: .seconds(10))
|
2025-10-10 10:19:05 -07:00
|
|
|
let exportPath = systemHealth.appRoot
|
|
|
|
|
.appendingPathComponent(Application.BuilderCommand.builderResourceDir)
|
2025-06-03 15:23:07 -07:00
|
|
|
let buildID = UUID().uuidString
|
|
|
|
|
let tempURL = exportPath.appendingPathComponent(buildID)
|
|
|
|
|
try FileManager.default.createDirectory(at: tempURL, withIntermediateDirectories: true, attributes: nil)
|
|
|
|
|
defer {
|
|
|
|
|
try? FileManager.default.removeItem(at: tempURL)
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-21 13:03:46 -07:00
|
|
|
let imageNames: [String] = try targetImageNames.map { name in
|
|
|
|
|
let parsedReference = try Reference.parse(name)
|
2025-06-03 15:23:07 -07:00
|
|
|
parsedReference.normalize()
|
|
|
|
|
return parsedReference.description
|
2025-10-21 13:03:46 -07:00
|
|
|
}
|
2025-06-03 15:23:07 -07:00
|
|
|
|
|
|
|
|
var terminal: Terminal?
|
|
|
|
|
switch self.progress {
|
2025-10-30 11:38:25 -07:00
|
|
|
case .tty:
|
2025-06-03 15:23:07 -07:00
|
|
|
terminal = try Terminal(descriptor: STDERR_FILENO)
|
2025-10-30 11:38:25 -07:00
|
|
|
case .auto:
|
2025-06-03 15:23:07 -07:00
|
|
|
terminal = try? Terminal(descriptor: STDERR_FILENO)
|
2025-10-30 11:38:25 -07:00
|
|
|
case .plain:
|
2025-06-03 15:23:07 -07:00
|
|
|
terminal = nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
defer { terminal?.tryReset() }
|
|
|
|
|
|
|
|
|
|
let exports: [Builder.BuildExport] = try output.map { output in
|
|
|
|
|
var exp = try Builder.BuildExport(from: output)
|
|
|
|
|
if exp.destination == nil {
|
|
|
|
|
exp.destination = tempURL.appendingPathComponent("out.tar")
|
|
|
|
|
}
|
|
|
|
|
return exp
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try await withThrowingTaskGroup(of: Void.self) { [terminal] group in
|
|
|
|
|
defer {
|
|
|
|
|
group.cancelAll()
|
|
|
|
|
}
|
|
|
|
|
group.addTask {
|
|
|
|
|
let handler = AsyncSignalHandler.create(notify: [SIGTERM, SIGINT, SIGUSR1, SIGUSR2])
|
|
|
|
|
for await sig in handler.signals {
|
|
|
|
|
throw ContainerizationError(.interrupted, message: "exiting on signal \(sig)")
|
|
|
|
|
}
|
|
|
|
|
}
|
Uniform support for `--platform`, `--os`, `--arch`. (#545)
- Fixes #231.
- Extends #313 (@dcantah) so that all of `container create`, `container
run`, `container build`, `container image pull`, and `container image
save` accept the three options.
- `container build` now processes comma-separated lists for
`--platform`, `--arch`, and `--os`. It first checks `--platform`,
assembling the union of all platform values. If that set is non-empty,
the builder builds the values in the set. Otherwise, the set consists of
all combinations of the specified architecture and os values, finally
defaulting to `linux` and the host architecture if no options are
provided.
- All other commands work accept a single platform, preferring the
`--platform` option over `--arch` and `--os` when both are specified.
`--os` defaults to `linux`, and `--arch` defaults to the host
architecture.
- Clarify help messages and present the args in consistent order, with
platform first since it takes precedence if present.
- Deduplicate redundant platform options for `container build`.
2025-08-27 13:26:01 -07:00
|
|
|
let platforms: Set<Platform> = try {
|
|
|
|
|
var results: Set<Platform> = []
|
|
|
|
|
for platform in (self.platform.flatMap { $0 }) {
|
|
|
|
|
guard let p = try? Platform(from: platform) else {
|
|
|
|
|
throw ValidationError("invalid platform specified \(platform)")
|
|
|
|
|
}
|
|
|
|
|
results.insert(p)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !results.isEmpty {
|
|
|
|
|
return results
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-07 02:38:16 -05:00
|
|
|
if let envPlatform = try DefaultPlatform.fromEnvironment(log: log) {
|
|
|
|
|
return [envPlatform]
|
|
|
|
|
}
|
|
|
|
|
|
Uniform support for `--platform`, `--os`, `--arch`. (#545)
- Fixes #231.
- Extends #313 (@dcantah) so that all of `container create`, `container
run`, `container build`, `container image pull`, and `container image
save` accept the three options.
- `container build` now processes comma-separated lists for
`--platform`, `--arch`, and `--os`. It first checks `--platform`,
assembling the union of all platform values. If that set is non-empty,
the builder builds the values in the set. Otherwise, the set consists of
all combinations of the specified architecture and os values, finally
defaulting to `linux` and the host architecture if no options are
provided.
- All other commands work accept a single platform, preferring the
`--platform` option over `--arch` and `--os` when both are specified.
`--os` defaults to `linux`, and `--arch` defaults to the host
architecture.
- Clarify help messages and present the args in consistent order, with
platform first since it takes precedence if present.
- Deduplicate redundant platform options for `container build`.
2025-08-27 13:26:01 -07:00
|
|
|
for o in (self.os.flatMap { $0 }) {
|
|
|
|
|
for a in (self.arch.flatMap { $0 }) {
|
2025-06-03 15:23:07 -07:00
|
|
|
guard let platform = try? Platform(from: "\(o)/\(a)") else {
|
|
|
|
|
throw ValidationError("invalid os/architecture combination \(o)/\(a)")
|
|
|
|
|
}
|
Uniform support for `--platform`, `--os`, `--arch`. (#545)
- Fixes #231.
- Extends #313 (@dcantah) so that all of `container create`, `container
run`, `container build`, `container image pull`, and `container image
save` accept the three options.
- `container build` now processes comma-separated lists for
`--platform`, `--arch`, and `--os`. It first checks `--platform`,
assembling the union of all platform values. If that set is non-empty,
the builder builds the values in the set. Otherwise, the set consists of
all combinations of the specified architecture and os values, finally
defaulting to `linux` and the host architecture if no options are
provided.
- All other commands work accept a single platform, preferring the
`--platform` option over `--arch` and `--os` when both are specified.
`--os` defaults to `linux`, and `--arch` defaults to the host
architecture.
- Clarify help messages and present the args in consistent order, with
platform first since it takes precedence if present.
- Deduplicate redundant platform options for `container build`.
2025-08-27 13:26:01 -07:00
|
|
|
results.insert(platform)
|
2025-06-03 15:23:07 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return results
|
|
|
|
|
}()
|
2026-03-30 16:29:50 -03:00
|
|
|
group.addTask {
|
|
|
|
|
[terminal, buildArg, secretsData, contextDir, hiddenDockerDir, label, noCache, target, quiet, cacheIn, cacheOut, pull, exports, imageNames, tempURL, log] in
|
2025-09-22 16:36:07 -07:00
|
|
|
let config = Builder.BuildConfig(
|
2025-06-03 15:23:07 -07:00
|
|
|
buildID: buildID,
|
|
|
|
|
contentStore: RemoteContentStoreClient(),
|
|
|
|
|
buildArgs: buildArg,
|
2026-03-16 17:04:24 -07:00
|
|
|
secrets: secretsData,
|
2025-06-03 15:23:07 -07:00
|
|
|
contextDir: contextDir,
|
2025-10-27 08:28:29 -07:00
|
|
|
dockerfile: buildFileData,
|
2026-03-16 12:58:32 -07:00
|
|
|
hiddenDockerDir: hiddenDockerDir,
|
2025-06-03 15:23:07 -07:00
|
|
|
labels: label,
|
|
|
|
|
noCache: noCache,
|
Uniform support for `--platform`, `--os`, `--arch`. (#545)
- Fixes #231.
- Extends #313 (@dcantah) so that all of `container create`, `container
run`, `container build`, `container image pull`, and `container image
save` accept the three options.
- `container build` now processes comma-separated lists for
`--platform`, `--arch`, and `--os`. It first checks `--platform`,
assembling the union of all platform values. If that set is non-empty,
the builder builds the values in the set. Otherwise, the set consists of
all combinations of the specified architecture and os values, finally
defaulting to `linux` and the host architecture if no options are
provided.
- All other commands work accept a single platform, preferring the
`--platform` option over `--arch` and `--os` when both are specified.
`--os` defaults to `linux`, and `--arch` defaults to the host
architecture.
- Clarify help messages and present the args in consistent order, with
platform first since it takes precedence if present.
- Deduplicate redundant platform options for `container build`.
2025-08-27 13:26:01 -07:00
|
|
|
platforms: [Platform](platforms),
|
2025-06-03 15:23:07 -07:00
|
|
|
terminal: terminal,
|
2025-10-21 13:03:46 -07:00
|
|
|
tags: imageNames,
|
2025-06-03 15:23:07 -07:00
|
|
|
target: target,
|
|
|
|
|
quiet: quiet,
|
|
|
|
|
exports: exports,
|
|
|
|
|
cacheIn: cacheIn,
|
2026-02-05 03:13:36 -05:00
|
|
|
cacheOut: cacheOut,
|
|
|
|
|
pull: pull
|
2025-06-03 15:23:07 -07:00
|
|
|
)
|
|
|
|
|
progress.finish()
|
|
|
|
|
|
|
|
|
|
try await builder.build(config)
|
|
|
|
|
|
2026-03-30 16:29:50 -03:00
|
|
|
let unpackProgressConfig = try ProgressConfig(
|
|
|
|
|
description: "Unpacking built image",
|
|
|
|
|
itemsName: "entries",
|
|
|
|
|
showTasks: exports.count > 1,
|
|
|
|
|
totalTasks: exports.count
|
|
|
|
|
)
|
|
|
|
|
let unpackProgress = ProgressBar(config: unpackProgressConfig)
|
|
|
|
|
defer {
|
|
|
|
|
unpackProgress.finish()
|
2026-01-16 16:26:13 -08:00
|
|
|
}
|
2026-03-30 16:29:50 -03:00
|
|
|
unpackProgress.start()
|
|
|
|
|
|
|
|
|
|
var finalMessage = "Successfully built \(imageNames.joined(separator: ", "))"
|
|
|
|
|
let taskManager = ProgressTaskCoordinator()
|
|
|
|
|
// Currently, only a single export can be specified.
|
|
|
|
|
for exp in exports {
|
|
|
|
|
unpackProgress.add(tasks: 1)
|
|
|
|
|
let unpackTask = await taskManager.startTask()
|
|
|
|
|
switch exp.type {
|
|
|
|
|
case "oci":
|
2025-10-21 13:03:46 -07:00
|
|
|
try Task.checkCancellation()
|
2026-03-30 16:29:50 -03:00
|
|
|
guard let dest = exp.destination else {
|
|
|
|
|
throw ContainerizationError(.invalidArgument, message: "dest is required \(exp.rawValue)")
|
|
|
|
|
}
|
|
|
|
|
let result = try await ClientImage.load(from: dest.absolutePath(), force: false)
|
|
|
|
|
guard result.rejectedMembers.isEmpty else {
|
|
|
|
|
log.error("archive contains invalid members", metadata: ["paths": "\(result.rejectedMembers)"])
|
|
|
|
|
throw ContainerizationError(.internalError, message: "failed to load archive")
|
|
|
|
|
}
|
|
|
|
|
for image in result.images {
|
|
|
|
|
try Task.checkCancellation()
|
|
|
|
|
try await image.unpack(platform: nil, progressUpdate: ProgressTaskCoordinator.handler(for: unpackTask, from: unpackProgress.handler))
|
|
|
|
|
|
|
|
|
|
// Tag the unpacked image with all requested tags
|
|
|
|
|
for tagName in imageNames {
|
|
|
|
|
try Task.checkCancellation()
|
|
|
|
|
_ = try await image.tag(new: tagName)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
case "tar":
|
|
|
|
|
guard let dest = exp.destination else {
|
|
|
|
|
throw ContainerizationError(.invalidArgument, message: "dest is required \(exp.rawValue)")
|
|
|
|
|
}
|
|
|
|
|
let tarURL = tempURL.appendingPathComponent("out.tar")
|
|
|
|
|
try FileManager.default.moveItem(at: tarURL, to: dest)
|
|
|
|
|
finalMessage = "Successfully exported to \(dest.absolutePath())"
|
|
|
|
|
case "local":
|
|
|
|
|
guard let dest = exp.destination else {
|
|
|
|
|
throw ContainerizationError(.invalidArgument, message: "dest is required \(exp.rawValue)")
|
|
|
|
|
}
|
|
|
|
|
let localDir = tempURL.appendingPathComponent("local")
|
2025-07-23 22:59:28 +00:00
|
|
|
|
2026-03-30 16:29:50 -03:00
|
|
|
guard FileManager.default.fileExists(atPath: localDir.path) else {
|
|
|
|
|
throw ContainerizationError(.invalidArgument, message: "expected local output not found")
|
|
|
|
|
}
|
|
|
|
|
try FileManager.default.copyItem(at: localDir, to: dest)
|
|
|
|
|
finalMessage = "Successfully exported to \(dest.absolutePath())"
|
|
|
|
|
default:
|
|
|
|
|
throw ContainerizationError(.invalidArgument, message: "invalid exporter \(exp.rawValue)")
|
|
|
|
|
}
|
2025-07-23 22:59:28 +00:00
|
|
|
}
|
2026-03-30 16:29:50 -03:00
|
|
|
await taskManager.finish()
|
|
|
|
|
unpackProgress.finish()
|
|
|
|
|
print(finalMessage)
|
2025-06-03 15:23:07 -07:00
|
|
|
}
|
2026-03-30 16:29:50 -03:00
|
|
|
|
|
|
|
|
try await group.next()
|
2025-06-03 15:23:07 -07:00
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
throw NSError(domain: "Build", code: 1, userInfo: [NSLocalizedDescriptionKey: "\(error)"])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-16 12:58:32 -07:00
|
|
|
public mutating func validate() throws {
|
|
|
|
|
// NOTE: Here we check the Dockerfile exists, and set `dockerfile` to point the valid Dockerfile path or stdin
|
2025-06-03 15:23:07 -07:00
|
|
|
guard FileManager.default.fileExists(atPath: contextDir) else {
|
|
|
|
|
throw ValidationError("context dir does not exist \(contextDir)")
|
|
|
|
|
}
|
2025-10-21 13:03:46 -07:00
|
|
|
for name in targetImageNames {
|
|
|
|
|
guard let _ = try? Reference.parse(name) else {
|
|
|
|
|
throw ValidationError("invalid reference \(name)")
|
|
|
|
|
}
|
2025-06-03 15:23:07 -07:00
|
|
|
}
|
2026-03-16 12:58:32 -07:00
|
|
|
|
|
|
|
|
switch file {
|
|
|
|
|
case "-":
|
|
|
|
|
dockerfile = "-"
|
|
|
|
|
break
|
|
|
|
|
case .some(let filepath):
|
|
|
|
|
let fileURL = URL(fileURLWithPath: filepath, relativeTo: .currentDirectory())
|
|
|
|
|
guard FileManager.default.fileExists(atPath: fileURL.path) else {
|
|
|
|
|
throw ValidationError("dockerfile does not exist \(filepath)")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dockerfile = fileURL.path
|
|
|
|
|
break
|
|
|
|
|
case .none:
|
|
|
|
|
guard let defaultDockerfile = try BuildFile.resolvePath(contextDir: contextDir) else {
|
|
|
|
|
throw ValidationError("dockerfile not found in context dir")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
guard FileManager.default.fileExists(atPath: defaultDockerfile) else {
|
|
|
|
|
throw ValidationError("dockerfile does not exist \(defaultDockerfile)")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dockerfile = defaultDockerfile
|
|
|
|
|
break
|
|
|
|
|
}
|
2026-03-16 17:04:24 -07:00
|
|
|
|
|
|
|
|
// 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])")
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-06-03 15:23:07 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|