Switch the containerization dependency from the github URL to a vendored copy (third_party/containerization, upstream commit 6b7b42ca) referenced by path, so we can carry a small local patch that upstream lacks: LinuxContainer.Configuration gains a `vmExtensions` field forwarded into VMConfiguration.extensions. Upstream already supports VMConfiguration.extensions + the VZInstanceExtension hook, but LinuxContainer — our only entry point — never forwarded them, so there was no way to attach a device (e.g. a memory balloon) to a container's VM. Tests/, docs/, examples/, images/ and the corresponding test targets are trimmed for footprint (we never build the dependency's tests). See PATCHES.md for the full diff vs. upstream and the re-vendoring procedure. Also adds the ContainerizationExtras product to NucleicCore (AddressAllocator, named in the configureVZ signature). Co-Authored-By: Claude Opus 4.8 <[email protected]>
198 lines
7.2 KiB
Swift
198 lines
7.2 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
// Copyright © 2025-2026 Apple Inc. and the Containerization 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 Containerization
|
|
import ContainerizationError
|
|
import ContainerizationExtras
|
|
import ContainerizationOCI
|
|
import ContainerizationOS
|
|
import Foundation
|
|
|
|
#if os(macOS)
|
|
extension Application {
|
|
struct Run: AsyncParsableCommand {
|
|
static let configuration = CommandConfiguration(
|
|
commandName: "run",
|
|
abstract: "Run a container"
|
|
)
|
|
|
|
@Option(name: [.customLong("image"), .customShort("i")], help: "Image reference to base the container on")
|
|
var imageReference: String = "docker.io/library/alpine:3.16"
|
|
|
|
@Option(name: .long, help: "id for the container")
|
|
var id: String = "cctl"
|
|
|
|
@Option(name: [.customLong("cpus"), .customShort("c")], help: "Number of CPUs to allocate to the container")
|
|
var cpus: Int = 2
|
|
|
|
@Option(name: [.customLong("memory"), .customShort("m")], help: "Amount of memory in megabytes")
|
|
var memory: UInt64 = 1024
|
|
|
|
@Option(name: .customLong("fs-size"), help: "The size to create the block filesystem as")
|
|
var fsSizeInMB: UInt64 = 2048
|
|
|
|
@Flag(name: .customLong("rosetta"), help: "Enable rosetta x64 emulation")
|
|
var rosetta = false
|
|
|
|
@Option(name: .customLong("mount"), help: "Directory to share into the container (Example: /foo:/bar)")
|
|
var mounts: [String] = []
|
|
|
|
@Option(name: .customLong("ns"), help: "Nameserver addresses")
|
|
var nameservers: [String] = []
|
|
|
|
@Option(name: .long, help: "Path to OCI runtime to use for spawning the container")
|
|
var ociRuntimePath: String?
|
|
|
|
@Flag(name: .long, help: "Make rootfs readonly")
|
|
var readOnly: Bool = false
|
|
|
|
@Flag(name: .long, help: "Run with an init process for signal forwarding and zombie reaping")
|
|
var `init`: Bool = false
|
|
|
|
@Option(
|
|
name: [.customLong("kernel"), .customShort("k")], help: "Kernel binary path", completion: .file(),
|
|
transform: { str in
|
|
URL(fileURLWithPath: str, relativeTo: .currentDirectory()).absoluteURL.path(percentEncoded: false)
|
|
})
|
|
public var kernel: String
|
|
|
|
@Option(name: .long, help: "Current working directory")
|
|
var cwd: String = "/"
|
|
|
|
@Argument(parsing: .captureForPassthrough)
|
|
var arguments: [String] = ["/bin/sh"]
|
|
|
|
func run() async throws {
|
|
let kernel = Kernel(
|
|
path: URL(fileURLWithPath: kernel),
|
|
platform: .linuxArm
|
|
)
|
|
|
|
// Choose network implementation based on macOS version
|
|
let network: Network?
|
|
if #available(macOS 26, *) {
|
|
network = try VmnetNetwork()
|
|
} else {
|
|
network = nil
|
|
}
|
|
|
|
var manager = try await ContainerManager(
|
|
kernel: kernel,
|
|
initfsReference: "vminit:latest",
|
|
network: network,
|
|
rosetta: rosetta
|
|
)
|
|
let sigwinchStream = AsyncSignalHandler.create(notify: [SIGWINCH])
|
|
|
|
let current = try Terminal.current
|
|
try current.setraw()
|
|
defer { current.tryReset() }
|
|
|
|
let container = try await manager.create(
|
|
id,
|
|
reference: imageReference,
|
|
rootfsSizeInBytes: fsSizeInMB.mib(),
|
|
readOnly: readOnly,
|
|
networking: true
|
|
) { config in
|
|
config.cpus = cpus
|
|
config.memoryInBytes = memory.mib()
|
|
config.process.setTerminalIO(terminal: current)
|
|
config.process.arguments = arguments
|
|
config.process.workingDirectory = cwd
|
|
config.process.capabilities = .allCapabilities
|
|
|
|
for mount in self.mounts {
|
|
let paths = mount.split(separator: ":")
|
|
if paths.count != 2 {
|
|
throw ContainerizationError(
|
|
.invalidArgument,
|
|
message: "incorrect mount format detected: \(mount)"
|
|
)
|
|
}
|
|
let host = String(paths[0])
|
|
let guest = String(paths[1])
|
|
let czMount = Containerization.Mount.share(
|
|
source: host,
|
|
destination: guest
|
|
)
|
|
config.mounts.append(czMount)
|
|
}
|
|
|
|
var hosts = Hosts.default
|
|
if !nameservers.isEmpty {
|
|
if #available(macOS 26, *) {
|
|
config.dns = DNS(nameservers: nameservers)
|
|
} else {
|
|
print("Warning: Networking not supported on macOS < 26, ignoring DNS configuration")
|
|
}
|
|
}
|
|
|
|
// Add host entry for the container using just the IP (not CIDR)
|
|
if #available(macOS 26, *), !config.interfaces.isEmpty {
|
|
let interface = config.interfaces[0]
|
|
hosts.entries.append(
|
|
Hosts.Entry(
|
|
ipAddress: interface.ipv4Address.address.description,
|
|
hostnames: [id]
|
|
))
|
|
}
|
|
|
|
config.hosts = hosts
|
|
if let ociRuntimePath {
|
|
config.ociRuntimePath = ociRuntimePath
|
|
config.mounts = LinuxContainer.defaultOCIMounts()
|
|
}
|
|
|
|
config.useInit = self.`init`
|
|
}
|
|
|
|
defer {
|
|
try? manager.delete(id)
|
|
}
|
|
|
|
try await container.create()
|
|
try await container.start()
|
|
|
|
// Resize the containers pty to the current terminal window.
|
|
try? await container.resize(to: try current.size)
|
|
|
|
try await withThrowingTaskGroup(of: Void.self) { group in
|
|
group.addTask {
|
|
for await _ in sigwinchStream.signals {
|
|
try await container.resize(to: try current.size)
|
|
}
|
|
}
|
|
|
|
try await container.wait()
|
|
group.cancelAll()
|
|
|
|
try await container.stop()
|
|
}
|
|
}
|
|
|
|
private static let appRoot: URL = {
|
|
FileManager.default.urls(
|
|
for: .applicationSupportDirectory,
|
|
in: .userDomainMask
|
|
).first!
|
|
.appendingPathComponent("com.apple.containerization")
|
|
}()
|
|
}
|
|
}
|
|
#endif
|