Vendor apple/containerization with a VM-extensions forwarding patch
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]>
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// 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
|
||||
Reference in New Issue
Block a user