Build vminitd and initfs inside the dev container (#810)

Signed-off-by: michael_crosby <michael_crosby@apple.com>
This commit is contained in:
Michael Crosby authored and GitHub committed 2026-07-24 11:04:06 -04:00
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 -4
View File
@@ -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.
+46 -12
View File
@@ -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:
+9 -21
View File
@@ -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
+17 -128
View File
@@ -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()
+7 -5
View File
@@ -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}`.
+1
View File
@@ -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 \
+8 -10
View File
@@ -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
+106
View File
@@ -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
View File
@@ -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)