From 54980872d9260fb9eda73ca308a71874fed6f1ab Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Fri, 15 Aug 2025 03:12:14 -0400 Subject: [PATCH] add ability to export initfs as block (#268) This refactors the rootfs command to produce an image and have the ability to create an ext4 formatted block of the init filesystem directly from the command. closes #220 Signed-off-by: crosbymichael --- Makefile | 7 +- .../Image/Unpacker/EXT4Unpacker.swift | 51 +++++++++++++-- Sources/cctl/RootfsCommand.swift | 64 +++++++++++++++---- 3 files changed, 104 insertions(+), 18 deletions(-) diff --git a/Makefile b/Makefile index d154a611..e46817ce 100644 --- a/Makefile +++ b/Makefile @@ -57,7 +57,12 @@ containerization: init: containerization vminitd @echo Creating init.ext4... @rm -f bin/init.rootfs.tar.gz bin/init.block - @./bin/cctl rootfs create --vminitd vminitd/bin/vminitd --labels org.opencontainers.image.source=https://github.com/apple/containerization --vmexec vminitd/bin/vmexec bin/init.rootfs.tar.gz vminit:latest + @./bin/cctl rootfs create \ + --vminitd vminitd/bin/vminitd \ + --labels org.opencontainers.image.source=https://github.com/apple/containerization \ + --vmexec vminitd/bin/vmexec \ + --image vminit:latest \ + bin/init.rootfs.tar.gz .PHONY: cross-prep cross-prep: diff --git a/Sources/Containerization/Image/Unpacker/EXT4Unpacker.swift b/Sources/Containerization/Image/Unpacker/EXT4Unpacker.swift index e99d6897..762a2c42 100644 --- a/Sources/Containerization/Image/Unpacker/EXT4Unpacker.swift +++ b/Sources/Containerization/Image/Unpacker/EXT4Unpacker.swift @@ -32,13 +32,56 @@ public struct EXT4Unpacker: Unpacker { self.blockSizeInBytes = blockSizeInBytes } - public func unpack(_ image: Image, for platform: Platform, at path: URL, progress: ProgressHandler? = nil) async throws -> Mount { + #if os(macOS) + /// Performs the unpacking of a tar archive into a filesystem. + /// - Parameters: + /// - archive: The archive to unpack. + /// - compression: The compression to use when unpacking the image. + /// - path: The path to the filesystem that will be created. + public func unpack( + archive: URL, + compression: ContainerizationArchive.Filter, + at path: URL + ) throws { + let cleanedPath = try prepareUnpackPath(path: path) + let filesystem = try EXT4.Formatter( + FilePath(cleanedPath), + minDiskSize: blockSizeInBytes + ) + defer { try? filesystem.close() } + + try filesystem.unpack( + source: archive, + format: .paxRestricted, + compression: compression, + progress: nil + ) + } + #endif + + /// Returns a `Mount` point after unpacking the image into a filesystem. + /// - Parameters: + /// - image: The image to unpack. + /// - platform: The platform content to unpack. + /// - path: The path to the directory where the filesystem will be created. + /// - progress: The progress handler to invoke as the unpacking progresses. + public func unpack( + _ image: Image, + for platform: Platform, + at path: URL, + progress: ProgressHandler? = nil + ) async throws -> Mount { #if !os(macOS) throw ContainerizationError(.unsupported, message: "Cannot unpack an image on current platform") #else - let blockPath = try prepareUnpackPath(path: path) + let cleanedPath = try prepareUnpackPath(path: path) let manifest = try await image.manifest(for: platform) - let filesystem = try EXT4.Formatter(FilePath(path), minDiskSize: blockSizeInBytes) + let filesystem = try EXT4.Formatter( + FilePath( + cleanedPath + ), + minDiskSize: blockSizeInBytes + ) defer { try? filesystem.close() } for layer in manifest.layers { @@ -64,7 +107,7 @@ public struct EXT4Unpacker: Unpacker { return .block( format: "ext4", - source: blockPath, + source: cleanedPath, destination: "/", options: [] ) diff --git a/Sources/cctl/RootfsCommand.swift b/Sources/cctl/RootfsCommand.swift index 2445741e..db0a123a 100644 --- a/Sources/cctl/RootfsCommand.swift +++ b/Sources/cctl/RootfsCommand.swift @@ -46,9 +46,14 @@ extension Application { @Option(name: .long, help: "Labels to add to the built image of the form =, [=,...]") var labels: [String] = [] - @Argument var rootfsPath: String + @Option(name: .customLong("image"), help: "The name of the image to produce.") + var imageName: String? - @Argument var tag: String + @Option(name: .customLong("ext4"), help: "The path to an ext4 image to create.") + var ext4File: String? + + // The path where the intermediate tar archive is created. + @Argument var tarPath: String private static let directories = [ "bin", @@ -63,19 +68,51 @@ extension Application { ] func run() async throws { - try await writeArchive() - let p = try Platform(from: platformString) - let rootfs = URL(filePath: rootfsPath) - let labels = Application.parseKeyValuePairs(from: labels) - _ = try await InitImage.create( - reference: tag, rootfs: rootfs, - platform: p, labels: labels, - imageStore: Application.imageStore, - contentStore: Application.contentStore) + 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 writeArchive() async throws { - let writer = try ArchiveWriter(format: .pax, filter: .gzip, file: URL(filePath: rootfsPath)) + private func outputExt4(archive: URL, to path: URL) async throws { + let unpacker = EXT4Unpacker(blockSizeInBytes: 256.mib()) + + try unpacker.unpack(archive: archive, compression: .gzip, at: path) + } + + private func outputImage(path: URL, reference: String) async throws { + let p = try Platform(from: platformString) + let labels = Application.parseKeyValuePairs(from: labels) + _ = try await InitImage.create( + reference: reference, + rootfs: path, + platform: p, + labels: labels, + 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 @@ -84,6 +121,7 @@ extension Application { entry.group = 0 entry.owner = 0 entry.fileType = .directory + // create the initial directory structure. for dir in Self.directories { entry.path = dir