mirror of
https://github.com/apple/container.git
synced 2026-10-13 01:29:22 +00:00
Build vminitd and initfs inside the dev container (#810)
Signed-off-by: michael_crosby <michael_crosby@apple.com>
This commit is contained in:
1 parent
450d44ecb6
commit
74ace148de
14 files changed
+339
-219
No files matched your search
@@ -15,10 +15,74 @@ on:
|
||||
description: Version of containerization
|
||||
default: test
|
||||
|
||||
jobs:
|
||||
buildAndTest:
|
||||
jobs:
|
||||
swift-version:
|
||||
name: Determine Swift version
|
||||
if: github.repository == 'apple/containerization'
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
image: ${{ steps.version.outputs.image }}
|
||||
steps:
|
||||
- name: Checkout .swift-version
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
with:
|
||||
sparse-checkout: .swift-version
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
- name: Read Swift version
|
||||
id: version
|
||||
run: echo "image=swift:$(cat .swift-version)-noble" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Build the guest initfs (vminitd/vmexec compiled static-musl, then packed
|
||||
# into initfs.ext4 + a rootfs tar) inside a GitHub-native Swift Linux
|
||||
# container (Docker-backed) — NOT via apple/container, which isn't available
|
||||
# on GitHub runners. The macOS job below consumes these as an artifact and
|
||||
# creates the vminit:latest image natively with cctl. The container job is
|
||||
# unprivileged, so build-initfs.sh uses its `mke2fs -d` fallback (no loop
|
||||
# mount / no CAP_SYS_ADMIN needed).
|
||||
buildGuest:
|
||||
name: Build guest initfs
|
||||
if: github.repository == 'apple/containerization'
|
||||
needs: swift-version
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-24.04
|
||||
container: ${{ needs.swift-version.outputs.image }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install system dependencies
|
||||
run: apt-get update && apt-get install -y curl make e2fsprogs libarchive-dev libbz2-dev liblzma-dev libssl-dev
|
||||
|
||||
- name: Install Static Linux SDK
|
||||
run: make -C vminitd linux-sdk
|
||||
|
||||
- name: Build vminitd (static musl, aarch64)
|
||||
# Force aarch64 even though this runner is x86_64: the macOS buildAndTest
|
||||
# job boots arm64 VZ VMs, so vminitd (PID 1) must be an aarch64 binary or
|
||||
# the guest fails to exec and the integration boot hangs. The Static Linux
|
||||
# SDK cross-targets both arches from any host (same mechanism dist-x86_64
|
||||
# uses in reverse).
|
||||
run: make -C vminitd MUSL_ARCH=aarch64 BUILD_CONFIGURATION=${{ inputs.release && 'release' || 'debug' }}
|
||||
|
||||
- name: Build initfs.ext4
|
||||
run: ./scripts/build-initfs.sh --vminitd vminitd/bin/vminitd --vmexec vminitd/bin/vmexec --ext4 bin/initfs.ext4 --tar bin/init.rootfs.tar.gz
|
||||
|
||||
- name: Upload guest initfs
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
name: initfs
|
||||
path: |
|
||||
bin/initfs.ext4
|
||||
bin/init.rootfs.tar.gz
|
||||
if-no-files-found: error
|
||||
|
||||
buildAndTest:
|
||||
name: Build and Test repo
|
||||
if: github.repository == 'apple/containerization'
|
||||
needs: buildGuest
|
||||
timeout-minutes: 60
|
||||
runs-on: [self-hosted, macos, tahoe, ARM64]
|
||||
permissions:
|
||||
@@ -33,11 +97,6 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Activate Swiftly
|
||||
run: |
|
||||
source ~/.swiftly/env.sh
|
||||
cat ~/.swiftly/env.sh
|
||||
|
||||
- name: Check formatting
|
||||
run: |
|
||||
./scripts/install-hawkeye.sh
|
||||
@@ -57,11 +116,15 @@ jobs:
|
||||
env:
|
||||
BUILD_CONFIGURATION: ${{ inputs.release && 'release' || 'debug' }}
|
||||
|
||||
- name: Make vminitd image
|
||||
- name: Download guest initfs
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
name: initfs
|
||||
path: bin
|
||||
|
||||
- name: Make vminit image
|
||||
run: |
|
||||
source ~/.swiftly/env.sh
|
||||
make -C vminitd swift linux-sdk
|
||||
make init
|
||||
make init-image
|
||||
env:
|
||||
BUILD_CONFIGURATION: ${{ inputs.release && 'release' || 'debug' }}
|
||||
|
||||
|
||||
@@ -4,11 +4,11 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
## Build / Test / Format
|
||||
|
||||
The project is built via `make`, not directly with `swift build`. Two Swift packages live in this repo: the root package (Containerization libraries + `cctl` + macOS-only integration binary) and `vminitd/` (the Linux guest init system, cross-compiled with the Static Linux SDK).
|
||||
The project is built via `make`, not directly with `swift build`. Two Swift packages live in this repo: the root package (Containerization libraries + `cctl` + macOS-only integration binary) and `vminitd/` (the Linux guest init system, compiled as a static musl binary inside the Linux dev container via the apple/`container` CLI — see `make vminitd`).
|
||||
|
||||
- `make all` — build everything (`containerization` + `vminitd` + `init.ext4` rootfs in `bin/`). Default `BUILD_CONFIGURATION=debug`; pass `release` (or use `make release`) for optimized builds.
|
||||
- `make containerization` — build just the host-side Swift package (skips vminitd).
|
||||
- `make vminitd` — build vminitd / vmexec only. By default uses `LIBC=musl` via the Static Linux SDK; `make linux-build LIBC=glibc` builds via a Linux dev container.
|
||||
- `make vminitd` — build vminitd / vmexec only. On macOS this runs `swift build --swift-sdk …-swift-linux-musl` *inside the Linux dev container* via the `container` CLI (the cloud-hypervisor build model), producing static musl binaries at `vminitd/bin/`; no host Swiftly/SDK needed. `make linux-build LIBC=glibc` builds via a Linux dev container.
|
||||
- `make test` — unit tests with code coverage. `make coverage` regenerates the coverage report.
|
||||
- `make integration` — runs `bin/containerization-integration`. Requires an in-repo kernel under `bin/` (`bin/vmlinux-arm64` on arm64, `bin/vmlinuz-x86_64` or `bin/vmlinux-x86_64` on x86_64); if absent, run `make fetch-default-kernel` to download the Kata-provided kernel for the host arch.
|
||||
- Single test: `swift test --filter ContainerizationOCITests.ReferenceTests/testParsing` (Swift Testing / XCTest filter syntax). Targets are listed in `Package.swift`.
|
||||
@@ -22,7 +22,7 @@ The project is built via `make`, not directly with `swift build`. Two Swift pack
|
||||
- `make check` — formatting + license-header lint (this is what the pre-commit hook runs). Uses `.swift-format-nolint` for stricter linting.
|
||||
- `make pre-commit` — installs `scripts/pre-commit.fmt` as a git pre-commit hook.
|
||||
- `make protos` — regenerates `Sources/Containerization/SandboxContext/SandboxContext.{pb,grpc}.swift` from the `.proto`. Touch this whenever the proto changes; never hand-edit the generated files.
|
||||
- `make cross-prep` — installs Swiftly, the pinned Swift toolchain (see `.swift-version`), and the Static Linux SDK. Run once before the first build.
|
||||
- `make init` / `make init-image` — `init` compiles the guest and builds `bin/initfs.ext4` (+ a rootfs tar) inside the dev container via `scripts/build-initfs.sh` (mkfs + loop mount, with a `mke2fs -d` fallback), then `init-image` creates the `vminit:latest` OCI image from the tar with the native `cctl` (`cctl rootfs create --rootfs <tar> --image vminit:latest`). CI splits these: a Linux container job builds the initfs artifact, the macOS job runs `init-image`. Building the guest on macOS requires the apple/`container` CLI — there is no host Swiftly / Static Linux SDK setup step anymore.
|
||||
|
||||
`WARNINGS_AS_ERRORS=true` is the default for both packages. Don't disable it casually — CI builds with it on.
|
||||
|
||||
@@ -88,4 +88,4 @@ These are independently consumable Swift modules. Keep their dependencies narrow
|
||||
|
||||
## Requirements
|
||||
|
||||
Apple silicon Mac, macOS 26, Xcode 26. Swift toolchain version is pinned in `.swift-version` (currently `6.3.0`) and installed via Swiftly during `make cross-prep`. Older macOS releases are not supported.
|
||||
Apple silicon Mac, macOS 26, Xcode 26. The host-side build uses Xcode's Swift toolchain (`/usr/bin/swift`); the Linux guest is built inside the dev container, so the apple/`container` CLI is required (see the README). The pinned Swift version (`.swift-version`, currently `6.3.0`) tags the dev image and the CI Swift Linux container. Older macOS releases are not supported.
|
||||
@@ -294,26 +294,60 @@ ifeq ($(UNAME_S),Darwin)
|
||||
@codesign --force --sign - --timestamp=none --entitlements=signing/vz.entitlements bin/containerization-integration
|
||||
endif
|
||||
|
||||
# Shell fragments run inside the Linux dev container (see linux_run). Kept as
|
||||
# variables so `vminitd` (compile only) and `init` (compile + build the initfs
|
||||
# in a single container run) don't duplicate the command.
|
||||
VMINITD_BUILD_CMD = make -C vminitd BUILD_CONFIGURATION=$(BUILD_CONFIGURATION) WARNINGS_AS_ERRORS=$(WARNINGS_AS_ERRORS)
|
||||
INITFS_BUILD_CMD = ./scripts/build-initfs.sh --vminitd vminitd/bin/vminitd --vmexec vminitd/bin/vmexec --ext4 bin/initfs.ext4 --tar bin/init.rootfs.tar.gz
|
||||
|
||||
.PHONY: init
|
||||
init: containerization vminitd
|
||||
@echo Creating init.ext4...
|
||||
@rm -f bin/init.rootfs.tar.gz bin/init.block bin/initfs.ext4
|
||||
ifeq ($(UNAME_S),Darwin)
|
||||
# Compile the guest and build the initfs (ext4 + rootfs tar) in a single dev
|
||||
# container run — where mkfs/loop-mount live — then create the vminit:latest
|
||||
# OCI image natively from the tar. The container's output is the finished
|
||||
# initfs, not raw binaries.
|
||||
init: containerization
|
||||
@mkdir -p ./bin
|
||||
$(call linux_run,$(VMINITD_BUILD_CMD) && $(INITFS_BUILD_CMD))
|
||||
@"$(MAKE)" init-image
|
||||
else
|
||||
init: containerization
|
||||
@mkdir -p ./bin
|
||||
@$(VMINITD_BUILD_CMD)
|
||||
@$(INITFS_BUILD_CMD)
|
||||
@"$(MAKE)" init-image
|
||||
endif
|
||||
|
||||
# Create the vminit:latest OCI image from the container-built rootfs tar, using
|
||||
# the native cctl. Split out from `init` so CI can create the image after
|
||||
# downloading the initfs artifact built by the Linux container job — no
|
||||
# apple/container needed on the macOS runner. The integration suite and the
|
||||
# release ghcr push consume this image.
|
||||
.PHONY: init-image
|
||||
init-image:
|
||||
@echo Creating vminit:latest image...
|
||||
@rm -f bin/init.block
|
||||
@./bin/cctl rootfs create \
|
||||
--vminitd vminitd/bin/vminitd \
|
||||
--vmexec vminitd/bin/vmexec \
|
||||
--ext4 ./bin/initfs.ext4 \
|
||||
--label org.opencontainers.image.source=https://github.com/apple/containerization \
|
||||
--image vminit:latest \
|
||||
--label org.opencontainers.image.source=https://github.com/apple/containerization \
|
||||
bin/init.rootfs.tar.gz
|
||||
|
||||
.PHONY: cross-prep
|
||||
cross-prep:
|
||||
@"$(MAKE)" -C vminitd cross-prep
|
||||
|
||||
.PHONY: vminitd
|
||||
ifeq ($(UNAME_S),Darwin)
|
||||
# On macOS, vminitd/vmexec are static musl Linux binaries. Rather than
|
||||
# cross-compiling on the host (which used to require Swiftly + the Static
|
||||
# Linux SDK via `make cross-prep`), build them inside the Linux dev container
|
||||
# via `linux_run` — the same model `build-cloud-hypervisor` uses. The dev
|
||||
# image bakes in the Static Linux SDK, and the /workspace mount makes the
|
||||
# resulting binaries visible on the host at vminitd/bin/.
|
||||
vminitd:
|
||||
@mkdir -p ./bin
|
||||
@"$(MAKE)" -C vminitd BUILD_CONFIGURATION=$(BUILD_CONFIGURATION) WARNINGS_AS_ERRORS=$(WARNINGS_AS_ERRORS)
|
||||
$(call linux_run,$(VMINITD_BUILD_CMD))
|
||||
else
|
||||
vminitd:
|
||||
@mkdir -p ./bin
|
||||
@$(VMINITD_BUILD_CMD)
|
||||
endif
|
||||
|
||||
.PHONY: update-libarchive-source
|
||||
update-libarchive-source:
|
||||
|
||||
@@ -100,31 +100,19 @@ Set the active developer directory to the installed Xcode (replace `<PATH_TO_XCO
|
||||
sudo xcode-select -s <PATH_TO_XCODE>
|
||||
```
|
||||
|
||||
Install [Swiftly](https://github.com/swiftlang/swiftly), [Swift](https://www.swift.org), and [Static Linux SDK](https://www.swift.org/documentation/articles/static-linux-getting-started.html):
|
||||
The Linux guest init (`vminitd`/`vmexec`) is compiled as a static binary
|
||||
*inside a Linux container* rather than cross-compiled on your Mac, so no Swift
|
||||
toolchain, Swiftly, or Static Linux SDK setup is required on the host. Install
|
||||
the [`container`](https://github.com/apple/container) CLI, which the build uses
|
||||
to compile the guest:
|
||||
|
||||
```bash
|
||||
make cross-prep
|
||||
# Install per https://github.com/apple/container, then verify it is on PATH:
|
||||
container --version
|
||||
```
|
||||
|
||||
If you use a custom terminal application, you may need to move this command from `.zprofile` to `.zshrc` (replace `<USERNAME>`):
|
||||
|
||||
```bash
|
||||
# Added by swiftly
|
||||
. "/Users/<USERNAME>/.swiftly/env.sh"
|
||||
```
|
||||
|
||||
Restart the terminal application. Ensure this command returns `/Users/<USERNAME>/.swiftly/bin/swift` (replace `<USERNAME>`):
|
||||
|
||||
```bash
|
||||
which swift
|
||||
```
|
||||
|
||||
If you've installed or used a Static Linux SDK previously, you may need to remove older SDK versions from the system (replace `<SDK-ID>`):
|
||||
|
||||
```bash
|
||||
swift sdk list
|
||||
swift sdk remove <SDK-ID>
|
||||
```
|
||||
The first build automatically builds the Linux dev image used to compile the
|
||||
guest, which can take a few minutes.
|
||||
|
||||
## Build the package
|
||||
|
||||
|
||||
@@ -16,11 +16,7 @@
|
||||
|
||||
import ArgumentParser
|
||||
import Containerization
|
||||
import ContainerizationArchive
|
||||
import ContainerizationEXT4
|
||||
import ContainerizationError
|
||||
import ContainerizationOCI
|
||||
import ContainerizationOS
|
||||
import Foundation
|
||||
|
||||
extension Application {
|
||||
@@ -34,147 +30,40 @@ extension Application {
|
||||
)
|
||||
|
||||
struct Create: AsyncParsableCommand {
|
||||
@Option(name: [.short, .customLong("add-file")], help: "Additional file to add (format src-path:dst-path)")
|
||||
var addFiles: [String] = []
|
||||
|
||||
@Option(name: .customLong("ext4"), help: "The path to an ext4 image to create.")
|
||||
var ext4File: String?
|
||||
static let configuration = CommandConfiguration(
|
||||
commandName: "create",
|
||||
abstract: "Create an init image from a prebuilt rootfs tar archive"
|
||||
)
|
||||
|
||||
@Option(name: .customLong("image"), help: "The name of the image to produce.")
|
||||
var imageName: String?
|
||||
var imageName: String
|
||||
|
||||
@Option(name: .customLong("label"), help: "Label to add to the image (format: key=value)")
|
||||
var labels: [String] = []
|
||||
|
||||
@Option(name: .long, help: "Platform of the built binaries being packaged into the block")
|
||||
@Option(name: .long, help: "Platform of the binaries packaged into the rootfs")
|
||||
var platformString: String = Platform.current.description
|
||||
|
||||
@Option(name: .long, help: "Path to vmexec")
|
||||
var vmexec: String
|
||||
|
||||
@Option(name: .long, help: "Path to vminitd")
|
||||
var vminitd: String
|
||||
|
||||
@Option(name: .long, help: "Path to OCI runtime")
|
||||
var ociRuntime: String?
|
||||
|
||||
// The path where the intermediate tar archive is created.
|
||||
@Argument var tarPath: String
|
||||
|
||||
private static let directories = [
|
||||
"bin",
|
||||
"sbin",
|
||||
"dev",
|
||||
"sys",
|
||||
"proc/self", // hack for swift init's booting
|
||||
"run",
|
||||
"tmp",
|
||||
"mnt",
|
||||
"var",
|
||||
]
|
||||
// The gzip-compressed rootfs tar archive whose contents make up the
|
||||
// image layer — e.g. produced by `scripts/build-initfs.sh --tar`,
|
||||
// which also builds the matching initfs.ext4. The rootfs layout is
|
||||
// owned by that script; this command only wraps it into an image.
|
||||
@Argument(help: "Path to the gzip-compressed rootfs tar archive")
|
||||
var rootfs: String
|
||||
|
||||
func run() async throws {
|
||||
let path = URL(filePath: self.tarPath)
|
||||
try await writeArchive(path: path)
|
||||
|
||||
if let image = self.imageName {
|
||||
print("creating initfs image \(image)...")
|
||||
try await outputImage(
|
||||
path: path,
|
||||
reference: image
|
||||
)
|
||||
}
|
||||
|
||||
if let ext4Path = self.ext4File {
|
||||
print("creating initfs ext4 image at \(ext4Path)...")
|
||||
try await outputExt4(
|
||||
archive: path,
|
||||
to: URL(filePath: ext4Path)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func outputExt4(archive: URL, to path: URL) async throws {
|
||||
let unpacker = EXT4Unpacker(capacityInBytes: 256.mib())
|
||||
try await unpacker.unpack(archive: archive, compression: .gzip, at: path)
|
||||
}
|
||||
|
||||
private func outputImage(path: URL, reference: String) async throws {
|
||||
let p = try Platform(from: platformString)
|
||||
let platform = try Platform(from: platformString)
|
||||
let parsedLabels = Application.parseKeyValuePairs(from: labels)
|
||||
print("creating initfs image \(imageName)...")
|
||||
_ = try await InitImage.create(
|
||||
reference: reference,
|
||||
rootfs: path,
|
||||
platform: p,
|
||||
reference: imageName,
|
||||
rootfs: URL(filePath: rootfs),
|
||||
platform: platform,
|
||||
labels: parsedLabels,
|
||||
imageStore: Application.imageStore,
|
||||
contentStore: Application.contentStore
|
||||
)
|
||||
}
|
||||
|
||||
private func writeArchive(path: URL) async throws {
|
||||
let writer = try ArchiveWriter(
|
||||
format: .pax,
|
||||
filter: .gzip,
|
||||
file: path,
|
||||
)
|
||||
let ts = Date()
|
||||
let entry = WriteEntry()
|
||||
entry.permissions = 0o755
|
||||
entry.modificationDate = ts
|
||||
entry.creationDate = ts
|
||||
entry.group = 0
|
||||
entry.owner = 0
|
||||
entry.fileType = .directory
|
||||
|
||||
// create the initial directory structure.
|
||||
for dir in Self.directories {
|
||||
entry.path = dir
|
||||
try writer.writeEntry(entry: entry, data: nil)
|
||||
}
|
||||
|
||||
entry.fileType = .regular
|
||||
entry.path = "sbin/vminitd"
|
||||
|
||||
var src = URL(fileURLWithPath: vminitd)
|
||||
var data = try Data(contentsOf: src)
|
||||
entry.size = Int64(data.count)
|
||||
try writer.writeEntry(entry: entry, data: data)
|
||||
|
||||
src = URL(fileURLWithPath: vmexec)
|
||||
data = try Data(contentsOf: src)
|
||||
entry.path = "sbin/vmexec"
|
||||
entry.size = Int64(data.count)
|
||||
try writer.writeEntry(entry: entry, data: data)
|
||||
|
||||
if let ociRuntimePath = self.ociRuntime {
|
||||
src = URL(fileURLWithPath: ociRuntimePath)
|
||||
let fileName = src.lastPathComponent
|
||||
data = try Data(contentsOf: src)
|
||||
entry.path = "sbin/\(fileName)"
|
||||
entry.size = Int64(data.count)
|
||||
try writer.writeEntry(entry: entry, data: data)
|
||||
}
|
||||
|
||||
for addFile in addFiles {
|
||||
let paths = addFile.components(separatedBy: ":")
|
||||
guard paths.count == 2 else {
|
||||
throw ContainerizationError(.invalidArgument, message: "use src-path:dst-path for --add-file")
|
||||
}
|
||||
src = URL(fileURLWithPath: paths[0])
|
||||
data = try Data(contentsOf: src)
|
||||
entry.path = paths[1]
|
||||
entry.size = Int64(data.count)
|
||||
try writer.writeEntry(entry: entry, data: data)
|
||||
}
|
||||
|
||||
entry.fileType = .symbolicLink
|
||||
entry.path = "proc/self/exe"
|
||||
entry.symlinkTarget = "sbin/vminitd"
|
||||
entry.size = nil
|
||||
try writer.writeEntry(entry: entry, data: nil)
|
||||
try writer.finishEncoding()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,10 @@ import Musl
|
||||
@Suite("BidirectionalRelay tests")
|
||||
final class BidirectionalRelayTests {
|
||||
|
||||
// Raw write() to a socketpair whose peer is closed must not kill the
|
||||
// `swift test` process with SIGPIPE on Linux. See ignoreSIGPIPEForTests().
|
||||
init() { ignoreSIGPIPEForTests() }
|
||||
|
||||
/// Creates a Unix domain socket pair and returns (fd0, fd1).
|
||||
private func makeSocketPair() throws -> (Int32, Int32) {
|
||||
var fds: [Int32] = [0, 0]
|
||||
|
||||
@@ -30,6 +30,10 @@ import Glibc
|
||||
@Suite("Epoll tests")
|
||||
final class EpollTests {
|
||||
|
||||
// write() to a pipe whose read end is closed must not kill the
|
||||
// `swift test` process with SIGPIPE on Linux. See ignoreSIGPIPEForTests().
|
||||
init() { ignoreSIGPIPEForTests() }
|
||||
|
||||
@Suite("Mask option set")
|
||||
struct MaskTests {
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
#if canImport(Darwin)
|
||||
import Darwin
|
||||
#elseif canImport(Glibc)
|
||||
import Glibc
|
||||
#elseif canImport(Musl)
|
||||
import Musl
|
||||
#endif
|
||||
|
||||
/// Ignore SIGPIPE for the test process.
|
||||
///
|
||||
/// Several suites in this target drive sockets/pipes with raw `write(2)` /
|
||||
/// `sendmsg(2)` whose peer may already be closed (e.g. the BidirectionalRelay,
|
||||
/// SCM_RIGHTS, and epoll pipe tests). On Linux a write to a closed peer raises
|
||||
/// SIGPIPE, whose default disposition terminates the entire `swift test`
|
||||
/// process (signal 13); swift-testing runs suites concurrently, so this shows
|
||||
/// up as an intermittent whole-run crash. macOS masks it per-socket, so this is
|
||||
/// Linux-specific.
|
||||
///
|
||||
/// swift-testing has no global setUp hook, so suites that touch sockets/pipes
|
||||
/// call this from `init()`. `signal` sets a process-wide disposition and the
|
||||
/// call is idempotent, so invoking it before each such suite's test bodies run
|
||||
/// is enough to keep any of their writes from killing the run.
|
||||
func ignoreSIGPIPEForTests() {
|
||||
_ = signal(SIGPIPE, SIG_IGN)
|
||||
}
|
||||
@@ -31,6 +31,10 @@ import Musl
|
||||
@Suite("Socket SCM_RIGHTS tests")
|
||||
final class SocketTests {
|
||||
|
||||
// sendmsg() over a socketpair whose peer is closed must not kill the
|
||||
// `swift test` process with SIGPIPE on Linux. See ignoreSIGPIPEForTests().
|
||||
init() { ignoreSIGPIPEForTests() }
|
||||
|
||||
/// Helper function to send a file descriptor via SCM_RIGHTS
|
||||
private func sendFileDescriptor(socket: Socket, fd: Int32) throws {
|
||||
var msg = msghdr()
|
||||
|
||||
@@ -88,10 +88,11 @@ unchanged components are skipped on subsequent runs.
|
||||
`/opt/cross-x86_64-gnu/`.
|
||||
|
||||
5. **`initfs.ext4` packaging.**
|
||||
A native aarch64 `cctl` is built (Swift release) and used as the packer:
|
||||
`cctl rootfs create --vminitd … --vmexec …` writes a ready-to-mount
|
||||
ext4 image with the x86_64 guest binaries inside. The native build
|
||||
only runs when this stage runs.
|
||||
`scripts/build-initfs.sh --vminitd … --vmexec … --ext4 …` stages the guest
|
||||
rootfs and writes a ready-to-mount ext4 image with the x86_64 guest binaries
|
||||
inside (loop mount where available, else `mke2fs -d`). The x86_64 tarball
|
||||
ships this raw ext4 and boots it via `cctl run --initfs`, so — unlike the
|
||||
arm64 flow — no `vminit` OCI image is built here.
|
||||
|
||||
6. **Stage and tar.** Always runs. Lays out the staging tree at
|
||||
`bin/dist-x86_64/<dist-name>/`:
|
||||
@@ -158,7 +159,8 @@ ship statically linked so the artifacts are host-libc independent.
|
||||
the deployment host provides glibc, libseccomp, and libcap-ng.
|
||||
|
||||
- **Swift** uses Apple's Static Linux SDK (`x86_64-swift-linux-musl`),
|
||||
installed by `make cross-prep` at dev-image build time. The same SDK
|
||||
installed into the dev image by `make linux-image` (Dockerfile
|
||||
`SWIFT_SDK_URL`/`SWIFT_SDK_CHECKSUM` build args). The same SDK
|
||||
is used for both `cctl` and `vminitd` cross-builds.
|
||||
- **Rust C cross-compiler is Zig.** For musl stages, `zig cc -target
|
||||
x86_64-linux-musl` is wrapped as `x86_64-linux-musl-{gcc,g++,ar,ranlib,strip}`.
|
||||
|
||||
@@ -18,6 +18,7 @@ FROM swift:${SWIFT_VERSION}-noble
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
make \
|
||||
e2fsprogs \
|
||||
libarchive-dev \
|
||||
libbz2-dev \
|
||||
liblzma-dev \
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
# Force-rebuild env vars (default = skip stages whose outputs are
|
||||
# up-to-date):
|
||||
# REBUILD_VMINITD=1 vminitd + vmexec
|
||||
# REBUILD_INITFS=1 initfs.ext4 (and the native aarch64 cctl packer)
|
||||
# REBUILD_INITFS=1 initfs.ext4
|
||||
# REBUILD_CH=1 cloud-hypervisor
|
||||
# REBUILD_VIRTIOFSD=1 virtiofsd
|
||||
# cctl x86 cross always rebuilds (Swift incremental handles no-ops).
|
||||
@@ -212,19 +212,17 @@ else
|
||||
fi
|
||||
|
||||
if [ "${NEED_INITFS}" = "1" ]; then
|
||||
echo "==> Building aarch64 cctl natively (used to pack initfs.ext4)"
|
||||
swift build -c release --product cctl -Xswiftc -warnings-as-errors --disable-automatic-resolution
|
||||
NATIVE_CCTL="$(swift build -c release --show-bin-path)/cctl"
|
||||
|
||||
echo "==> Building initfs.ext4 with x86_64 guest binaries"
|
||||
# The x86_64 tarball ships the raw initfs.ext4 (booted via `cctl run
|
||||
# --initfs`); it does not ship a vminit OCI image, so no cctl step is
|
||||
# needed here. build-initfs.sh builds the ext4 in-container (loop mount,
|
||||
# or mke2fs -d fallback) and is arch-agnostic — the x86_64 guest binaries
|
||||
# are just packed files.
|
||||
rm -f "${DIST_DIR}/init.rootfs.tar.gz" "${DIST_DIR}/initfs.ext4"
|
||||
"${NATIVE_CCTL}" rootfs create \
|
||||
./scripts/build-initfs.sh \
|
||||
--vminitd "${DIST_DIR}/vminitd" \
|
||||
--vmexec "${DIST_DIR}/vmexec" \
|
||||
--ext4 "${DIST_DIR}/initfs.ext4" \
|
||||
--label org.opencontainers.image.source=https://github.com/apple/containerization \
|
||||
--image vminit-x86_64:latest \
|
||||
"${DIST_DIR}/init.rootfs.tar.gz"
|
||||
--ext4 "${DIST_DIR}/initfs.ext4"
|
||||
else
|
||||
echo "==> Reusing staged initfs.ext4 (vminitd/vmexec unchanged; set REBUILD_INITFS=1 to force)"
|
||||
fi
|
||||
|
||||
Executable
+106
@@ -0,0 +1,106 @@
|
||||
#!/bin/bash
|
||||
# Copyright © 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.
|
||||
|
||||
# Builds the guest init filesystem (initfs.ext4) — and optionally a rootfs
|
||||
# tar.gz for OCI image creation — from the compiled vminitd/vmexec binaries.
|
||||
#
|
||||
# Runs INSIDE the Linux dev container (invoked by the root Makefile's `init`
|
||||
# target via linux_run on macOS, or directly on Linux). A real loop mount is
|
||||
# preferred; when loop devices aren't available (an unprivileged CI container,
|
||||
# or a dev VM without loop support) it falls back to `mke2fs -d`, which
|
||||
# populates the filesystem without mounting. Both paths yield an equivalent
|
||||
# ext4, so the build works whether or not the container is privileged.
|
||||
#
|
||||
# The rootfs layout below MUST stay in sync with cctl's InitImage rootfs
|
||||
# (Sources/cctl/RootfsCommand.swift): directories bin/ sbin/ dev/ sys/
|
||||
# proc/self/ run/ tmp/ mnt/ var/, sbin/vminitd + sbin/vmexec at mode 0755, and
|
||||
# a proc/self/exe -> sbin/vminitd symlink ("hack for swift init's booting").
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
echo "usage: $0 --vminitd PATH --vmexec PATH --ext4 OUT.ext4 [--tar OUT.tar.gz] [--size 512M]" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
VMINITD=
|
||||
VMEXEC=
|
||||
EXT4=
|
||||
TAR=
|
||||
SIZE=512M
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--vminitd) VMINITD=$2; shift 2 ;;
|
||||
--vmexec) VMEXEC=$2; shift 2 ;;
|
||||
--ext4) EXT4=$2; shift 2 ;;
|
||||
--tar) TAR=$2; shift 2 ;;
|
||||
--size) SIZE=$2; shift 2 ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[ -n "$VMINITD" ] && [ -n "$VMEXEC" ] && [ -n "$EXT4" ] || usage
|
||||
[ -f "$VMINITD" ] || { echo "ERROR: vminitd not found: $VMINITD" >&2; exit 1; }
|
||||
[ -f "$VMEXEC" ] || { echo "ERROR: vmexec not found: $VMEXEC" >&2; exit 1; }
|
||||
|
||||
umask 022
|
||||
STAGING=$(mktemp -d)
|
||||
MNT=
|
||||
cleanup() {
|
||||
if [ -n "$MNT" ]; then
|
||||
mountpoint -q "$MNT" 2>/dev/null && umount "$MNT" 2>/dev/null || true
|
||||
rmdir "$MNT" 2>/dev/null || true
|
||||
fi
|
||||
rm -rf "$STAGING"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# Directory structure — keep in sync with RootfsCommand.directories.
|
||||
for d in bin sbin dev sys proc/self run tmp mnt var; do
|
||||
mkdir -p "$STAGING/$d"
|
||||
done
|
||||
install -m 0755 "$VMINITD" "$STAGING/sbin/vminitd"
|
||||
install -m 0755 "$VMEXEC" "$STAGING/sbin/vmexec"
|
||||
ln -sf sbin/vminitd "$STAGING/proc/self/exe"
|
||||
|
||||
mkdir -p "$(dirname "$EXT4")"
|
||||
rm -f "$EXT4"
|
||||
truncate -s "$SIZE" "$EXT4"
|
||||
|
||||
# Prefer a real loop mount; fall back to `mke2fs -d` when it isn't available.
|
||||
if mkfs.ext4 -F -q "$EXT4" \
|
||||
&& MNT=$(mktemp -d) \
|
||||
&& mount -o loop "$EXT4" "$MNT" 2>/dev/null; then
|
||||
echo "==> populating $EXT4 via loop mount"
|
||||
cp -a "$STAGING"/. "$MNT"/
|
||||
sync
|
||||
umount "$MNT"
|
||||
rmdir "$MNT"
|
||||
MNT=
|
||||
else
|
||||
echo "==> loop mount unavailable; populating $EXT4 via mke2fs -d"
|
||||
if [ -n "$MNT" ]; then rmdir "$MNT" 2>/dev/null || true; MNT=; fi
|
||||
rm -f "$EXT4"
|
||||
truncate -s "$SIZE" "$EXT4"
|
||||
mkfs.ext4 -F -q -d "$STAGING" "$EXT4"
|
||||
fi
|
||||
echo "==> wrote initfs $EXT4"
|
||||
|
||||
if [ -n "$TAR" ]; then
|
||||
mkdir -p "$(dirname "$TAR")"
|
||||
rm -f "$TAR"
|
||||
tar -czf "$TAR" -C "$STAGING" .
|
||||
echo "==> wrote rootfs tar $TAR"
|
||||
fi
|
||||
+14
-28
@@ -45,16 +45,17 @@ SWIFT_SDK_URL := https://download.swift.org/swift-6.3-release/static-sdk/swift-6
|
||||
SWIFT_SDK_CHECKSUM := d2078b69bdeb5c31202c10e9d8a11d6f66f82938b51a4b75f032ccb35c4c286c
|
||||
SWIFT_SDK_PATH := /tmp/$(notdir $(SWIFT_SDK_URL))
|
||||
|
||||
# vminitd is built inside the Linux dev container (see the root Makefile's
|
||||
# `vminitd` target, which routes through `linux_run` on macOS), so `swift`
|
||||
# is always the toolchain on PATH inside that container. The host no longer
|
||||
# needs Swiftly or a locally-installed Static Linux SDK.
|
||||
SYSTEM_TYPE := $(shell uname -s)
|
||||
ifeq ($(SYSTEM_TYPE),Darwin)
|
||||
SWIFTLY_URL := https://download.swift.org/swiftly/darwin/swiftly.pkg
|
||||
SWIFTLY_FILENAME := $(notdir $(SWIFTLY_URL))
|
||||
SWIFTLY_BIN_DIR ?= ~/.swiftly/bin
|
||||
SWIFT := $(SWIFTLY_BIN_DIR)/swift
|
||||
else
|
||||
SWIFT ?= swift
|
||||
endif
|
||||
BUILD_BIN_DIR := $(shell $(SWIFT) build -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --show-bin-path)
|
||||
# Lazily evaluated (`=`, not `:=`) so `swift build --swift-sdk ... --show-bin-path`
|
||||
# only runs for the `all` target — which runs inside the Linux dev container
|
||||
# where the Static Linux SDK exists. Evaluating it at parse time would fail on a
|
||||
# macOS host (which no longer installs the SDK) for targets like `clean`.
|
||||
BUILD_BIN_DIR = $(shell $(SWIFT) build -c $(BUILD_CONFIGURATION) $(SWIFT_CONFIGURATION) --show-bin-path)
|
||||
|
||||
ifeq ($(SYSTEM_TYPE),Darwin)
|
||||
MACOS_VERSION := $(shell sw_vers -productVersion)
|
||||
@@ -80,27 +81,12 @@ all:
|
||||
@install "$(BUILD_BIN_DIR)/vminitd" $(INSTALL_DIR)/
|
||||
@install "$(BUILD_BIN_DIR)/vmexec" $(INSTALL_DIR)/
|
||||
|
||||
.PHONY: cross-prep
|
||||
cross-prep: swift linux-sdk
|
||||
|
||||
.PHONY: swiftly
|
||||
swiftly:
|
||||
@if ! command -v ${SWIFTLY_BIN_DIR}/swiftly > /dev/null 2>&1; then \
|
||||
echo "Installing Swiftly..."; \
|
||||
curl -o /var/tmp/$(SWIFTLY_FILENAME) $(SWIFTLY_URL) && \
|
||||
installer -pkg /var/tmp/$(SWIFTLY_FILENAME) -target CurrentUserHomeDirectory && \
|
||||
${SWIFTLY_BIN_DIR}/swiftly init --quiet-shell-followup --skip-install && \
|
||||
. ~/.swiftly/env.sh && \
|
||||
hash -r && \
|
||||
rm /var/tmp/$(SWIFTLY_FILENAME); \
|
||||
fi
|
||||
|
||||
.PHONY: swift
|
||||
swift: swiftly
|
||||
@echo Installing Swift $(SWIFT_VERSION)...
|
||||
@${SWIFTLY_BIN_DIR}/swiftly install $(SWIFT_VERSION)
|
||||
|
||||
.PHONY: linux-sdk
|
||||
# Installs the Static Linux SDK into the active Swift toolchain. Used inside
|
||||
# plain Swift Linux containers that don't already bake in the SDK (e.g. the
|
||||
# `linux-build.yml` compile-check and the CI `buildGuest` job). The dev image
|
||||
# built by `make linux-image` installs the same SDK at image-build time, so it
|
||||
# does not need this target.
|
||||
linux-sdk:
|
||||
@echo Installing Static Linux SDK...
|
||||
@curl -L -o $(SWIFT_SDK_PATH) $(SWIFT_SDK_URL)
|
||||
|
||||
Reference in new issue
Block a user