//===----------------------------------------------------------------------===// // Copyright © 2026 Apple Inc. and the container project authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //===----------------------------------------------------------------------===// import Containerization import ContainerizationError import Foundation public struct Bundle: Sendable { private static let initfsFilename = "initfs.ext4" private static let kernelFilename = "kernel.json" private static let kernelBinaryFilename = "kernel.bin" private static let containerRootFsBlockFilename = "rootfs.ext4" private static let containerRootFsFilename = "rootfs.json" static let containerConfigFilename = "config.json" /// The path to the bundle. public let path: URL public init(path: URL) { self.path = path } public var bootlog: URL { self.path.appendingPathComponent("vminitd.log") } public var containerRootfsBlock: URL { self.path.appendingPathComponent(Self.containerRootFsBlockFilename) } private var containerRootfsConfig: URL { self.path.appendingPathComponent(Self.containerRootFsFilename) } public var containerRootfs: Filesystem { get throws { let data = try Data(contentsOf: containerRootfsConfig) let fs = try JSONDecoder().decode(Filesystem.self, from: data) return fs } } /// Return the initial filesystem for a sandbox. public var initialFilesystem: Filesystem { .block( format: "ext4", source: self.path.appendingPathComponent(Self.initfsFilename).path, destination: "/", options: ["ro"] ) } public var kernel: Kernel { get throws { try load(path: self.path.appendingPathComponent(Self.kernelFilename)) } } public var configuration: ContainerConfiguration { get throws { try load(path: self.path.appendingPathComponent(Self.containerConfigFilename)) } } } extension Bundle { public static func create( path: URL, initialFilesystem: Filesystem, kernel: Kernel, containerConfiguration: ContainerConfiguration? = nil, containerRootFilesystem: Filesystem? = nil, options: ContainerCreateOptions? = nil ) throws -> Bundle { try FileManager.default.createDirectory(at: path, withIntermediateDirectories: true) let kbin = path.appendingPathComponent(Self.kernelBinaryFilename) try FileManager.default.copyItem(at: kernel.path, to: kbin) var k = kernel k.path = kbin try write(path.appendingPathComponent(Self.kernelFilename), value: k) switch initialFilesystem.type { case .block(let fmt, _, _): guard fmt == "ext4" else { fatalError("ext4 is the only supported format for initial filesystem") } // when saving the Initial Filesystem to the bundle // discard any filesystem information and just persist // the block into the Bundle. _ = try initialFilesystem.clone(to: path.appendingPathComponent(Self.initfsFilename).path) default: fatalError("invalid filesystem type for initial filesystem") } let bundle = Bundle(path: path) if let containerConfiguration { try bundle.write(filename: Self.containerConfigFilename, value: containerConfiguration) } if let rootFsOverride = options?.rootFsOverride { try bundle.setContainerRootFs(fs: rootFsOverride) } else if let containerRootFilesystem { let readonly = containerConfiguration?.readOnly ?? false try bundle.cloneContainerRootFs(cloning: containerRootFilesystem, readonly: readonly) } if let options { try bundle.write(filename: "options.json", value: options) } return bundle } } extension Bundle { /// Set the value of the configuration for the Bundle. public func set(configuration: ContainerConfiguration) throws { try write(filename: Self.containerConfigFilename, value: configuration) } /// Return the full filepath for a named resource in the Bundle. public func filePath(for name: String) -> URL { path.appendingPathComponent(name) } public func setContainerRootFs(fs: Filesystem) throws { let fsData = try JSONEncoder().encode(fs) try fsData.write(to: self.containerRootfsConfig) } public func cloneContainerRootFs(cloning fs: Filesystem, readonly: Bool = false) throws { var mutableFs = fs if readonly && !mutableFs.options.contains("ro") { mutableFs.options.append("ro") } let cloned = try mutableFs.clone(to: self.containerRootfsBlock.absolutePath()) try setContainerRootFs(fs: cloned) } /// Delete the bundle and all of the resources contained inside. public func delete() throws { try FileManager.default.removeItem(at: self.path) } public func write(filename: String, value: Encodable) throws { try Self.write(self.path.appendingPathComponent(filename), value: value) } private static func write(_ path: URL, value: Encodable) throws { let data = try JSONEncoder().encode(value) try data.write(to: path) } public func load(filename: String) throws -> T where T: Decodable { try load(path: self.path.appendingPathComponent(filename)) } private func load(path: URL) throws -> T where T: Decodable { let data = try Data(contentsOf: path) return try JSONDecoder().decode(T.self, from: data) } }