From 5d3a8c6a0b927d7907c068cde6d6169d31e70adb Mon Sep 17 00:00:00 2001 From: michael_crosby Date: Wed, 22 Jul 2026 15:01:06 -0400 Subject: [PATCH] Build vminitd and initfs inside the dev container Signed-off-by: michael_crosby --- .../containerization-build-template.yml | 85 ++++++++-- CLAUDE.md | 8 +- Makefile | 58 +++++-- README.md | 30 ++-- Sources/cctl/RootfsCommand.swift | 145 ++---------------- .../BidirectionalRelayTests.swift | 4 + .../ContainerizationOSTests/EpollTests.swift | 4 + .../IgnoreSIGPIPE.swift | 41 +++++ .../ContainerizationOSTests/SocketTests.swift | 4 + docs/x86_64-build.md | 12 +- images/linux-dev/Dockerfile | 1 + scripts/build-dist-x86_64.sh | 18 +-- scripts/build-initfs.sh | 106 +++++++++++++ vminitd/Makefile | 42 ++--- 14 files changed, 339 insertions(+), 219 deletions(-) create mode 100644 Tests/ContainerizationOSTests/IgnoreSIGPIPE.swift create mode 100755 scripts/build-initfs.sh diff --git a/.github/workflows/containerization-build-template.yml b/.github/workflows/containerization-build-template.yml index c67b54678..3e6084d8e 100644 --- a/.github/workflows/containerization-build-template.yml +++ b/.github/workflows/containerization-build-template.yml @@ -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' }} diff --git a/CLAUDE.md b/CLAUDE.md index d3d775b9b..2289c916c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 --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. diff --git a/Makefile b/Makefile index 76825da81..82f635e54 100644 --- a/Makefile +++ b/Makefile @@ -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: diff --git a/README.md b/README.md index 3bf639802..f658b08dd 100644 --- a/README.md +++ b/README.md @@ -100,31 +100,19 @@ Set the active developer directory to the installed Xcode (replace ` ``` -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 ``): - -```bash -# Added by swiftly -. "/Users//.swiftly/env.sh" -``` - -Restart the terminal application. Ensure this command returns `/Users//.swiftly/bin/swift` (replace ``): - -```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 ``): - -```bash -swift sdk list -swift sdk remove -``` +The first build automatically builds the Linux dev image used to compile the +guest, which can take a few minutes. ## Build the package diff --git a/Sources/cctl/RootfsCommand.swift b/Sources/cctl/RootfsCommand.swift index 2145d61dd..e995a893a 100644 --- a/Sources/cctl/RootfsCommand.swift +++ b/Sources/cctl/RootfsCommand.swift @@ -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() - } } } } diff --git a/Tests/ContainerizationOSTests/BidirectionalRelayTests.swift b/Tests/ContainerizationOSTests/BidirectionalRelayTests.swift index 518940251..e15401a9e 100644 --- a/Tests/ContainerizationOSTests/BidirectionalRelayTests.swift +++ b/Tests/ContainerizationOSTests/BidirectionalRelayTests.swift @@ -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] diff --git a/Tests/ContainerizationOSTests/EpollTests.swift b/Tests/ContainerizationOSTests/EpollTests.swift index 636d73994..6ebbbede4 100644 --- a/Tests/ContainerizationOSTests/EpollTests.swift +++ b/Tests/ContainerizationOSTests/EpollTests.swift @@ -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 diff --git a/Tests/ContainerizationOSTests/IgnoreSIGPIPE.swift b/Tests/ContainerizationOSTests/IgnoreSIGPIPE.swift new file mode 100644 index 000000000..96d4abfe8 --- /dev/null +++ b/Tests/ContainerizationOSTests/IgnoreSIGPIPE.swift @@ -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) +} diff --git a/Tests/ContainerizationOSTests/SocketTests.swift b/Tests/ContainerizationOSTests/SocketTests.swift index e941cd4a6..908f7860d 100644 --- a/Tests/ContainerizationOSTests/SocketTests.swift +++ b/Tests/ContainerizationOSTests/SocketTests.swift @@ -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() diff --git a/docs/x86_64-build.md b/docs/x86_64-build.md index 5d4e2c4ff..102a1bbfb 100644 --- a/docs/x86_64-build.md +++ b/docs/x86_64-build.md @@ -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//`: @@ -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}`. diff --git a/images/linux-dev/Dockerfile b/images/linux-dev/Dockerfile index 8d3a8f345..0b499e316 100644 --- a/images/linux-dev/Dockerfile +++ b/images/linux-dev/Dockerfile @@ -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 \ diff --git a/scripts/build-dist-x86_64.sh b/scripts/build-dist-x86_64.sh index f0e0cf971..5a1f03470 100755 --- a/scripts/build-dist-x86_64.sh +++ b/scripts/build-dist-x86_64.sh @@ -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 diff --git a/scripts/build-initfs.sh b/scripts/build-initfs.sh new file mode 100755 index 000000000..d2c56fdfb --- /dev/null +++ b/scripts/build-initfs.sh @@ -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 diff --git a/vminitd/Makefile b/vminitd/Makefile index 1c996d91f..718d17c81 100644 --- a/vminitd/Makefile +++ b/vminitd/Makefile @@ -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)