From 92b64f6b9a9a6c94e872932ff63bcbb7d0dbce12 Mon Sep 17 00:00:00 2001 From: Ben Boeckel Date: Tue, 7 Jul 2026 12:54:01 -0400 Subject: [PATCH] fix(save): match tart push format exactly for skopeo compatibility Use the same OCI manifest format and custom Cirrus Labs layer media types (config.v1, disk.v2, nvram.v1) that tart push produces. skopeo copy --format oci already works with images pushed by tart push, so the archive now matches that format exactly. --- Sources/tart/OCI/Manifest.swift | 3 -- Sources/tart/OCI/OCIArchiveWriter.swift | 2 +- Sources/tart/VMDirectory+OCIArchive.swift | 51 ++++++++--------------- 3 files changed, 19 insertions(+), 37 deletions(-) diff --git a/Sources/tart/OCI/Manifest.swift b/Sources/tart/OCI/Manifest.swift index 77a6bdd..c466435 100644 --- a/Sources/tart/OCI/Manifest.swift +++ b/Sources/tart/OCI/Manifest.swift @@ -8,9 +8,6 @@ let ociConfigMediaType = "application/vnd.oci.image.config.v1+json" let dockerManifestMediaType = "application/vnd.docker.distribution.manifest.v2+json" let dockerConfigMediaType = "application/vnd.docker.container.image.v1+json" -// Standard OCI layer media type -let ociLayerMediaType = "application/vnd.oci.image.layer.v1.tar+gzip" - // Layer media types let configMediaType = "application/vnd.cirruslabs.tart.config.v1" let diskV2MediaType = "application/vnd.cirruslabs.tart.disk.v2" diff --git a/Sources/tart/OCI/OCIArchiveWriter.swift b/Sources/tart/OCI/OCIArchiveWriter.swift index 49ab36a..6c814d4 100644 --- a/Sources/tart/OCI/OCIArchiveWriter.swift +++ b/Sources/tart/OCI/OCIArchiveWriter.swift @@ -1,7 +1,7 @@ import Foundation class OCIArchiveWriter { - let tmpDir: URL + private let tmpDir: URL private let blobsDir: URL private let lock: FileLock private var manifestDigest: String? diff --git a/Sources/tart/VMDirectory+OCIArchive.swift b/Sources/tart/VMDirectory+OCIArchive.swift index 6d3c63a..c336989 100644 --- a/Sources/tart/VMDirectory+OCIArchive.swift +++ b/Sources/tart/VMDirectory+OCIArchive.swift @@ -4,40 +4,27 @@ extension VMDirectory { func saveToArchive(path: String, concurrency: UInt, labels: [String: String] = [:], tag: String? = nil) async throws { let archive = try OCIArchiveWriter() - let diskSize = try FileManager.default.attributesOfItem(atPath: diskURL.path)[.size] as! Int64 - - // Create a standard tar+gzip layer containing VM files - defaultLogger.appendNewLine("archiving disk... this will take a while...") - let progress = Progress(totalUnitCount: diskSize) - ProgressObserver(progress).log(defaultLogger) - - let layerTarGz = archive.tmpDir.appendingPathComponent("layer.tar.gz") - - let tarProcess = Process() - tarProcess.executableURL = URL(fileURLWithPath: "/usr/bin/tar") - tarProcess.arguments = ["-czf", layerTarGz.path, "-C", baseURL.path, - "disk.img", "nvram.bin", "config.json"] - - let tarPipe = Pipe() - tarProcess.standardError = tarPipe - - try tarProcess.run() - tarProcess.waitUntilExit() - - if tarProcess.terminationStatus != 0 { - let errorData = tarPipe.fileHandleForReading.readDataToEndOfFile() - throw RuntimeError.Generic( - "creating archive layer failed: \(String(data: errorData, encoding: .utf8) ?? "unknown error")" - ) - } - - let layerData = try Data(contentsOf: layerTarGz, options: .alwaysMapped) - let layerDigest = try await archive.pushBlob(fromData: layerData, chunkSizeMb: 0, digest: nil) - progress.completedUnitCount = diskSize + var layers = [OCIManifestLayer]() let config = try VMConfig(fromURL: configURL) var labels = labels labels[diskFormatLabel] = config.diskFormat.rawValue + let configJSON = try JSONEncoder().encode(config) + defaultLogger.appendNewLine("saving config...") + let configDigest = try await archive.pushBlob(fromData: configJSON, chunkSizeMb: 0, digest: nil) + layers.append(OCIManifestLayer(mediaType: configMediaType, size: configJSON.count, digest: configDigest)) + + let diskSize = try FileManager.default.attributesOfItem(atPath: diskURL.path)[.size] as! Int64 + defaultLogger.appendNewLine("saving disk... this will take a while...") + let progress = Progress(totalUnitCount: diskSize) + ProgressObserver(progress).log(defaultLogger) + + layers.append(contentsOf: try await DiskV2.push(diskURL: diskURL, registry: archive, chunkSizeMb: 0, concurrency: concurrency, progress: progress)) + + defaultLogger.appendNewLine("saving NVRAM...") + let nvram = try FileHandle(forReadingFrom: nvramURL).readToEnd()! + let nvramDigest = try await archive.pushBlob(fromData: nvram, chunkSizeMb: 0, digest: nil) + layers.append(OCIManifestLayer(mediaType: nvramMediaType, size: nvram.count, digest: nvramDigest)) let ociConfigContainer = OCIConfig.ConfigContainer(Labels: labels) let ociConfigJSON = try OCIConfig(architecture: config.arch, os: config.os, config: ociConfigContainer).toJSON() @@ -45,9 +32,7 @@ extension VMDirectory { let manifest = OCIManifest( config: OCIManifestConfig(size: ociConfigJSON.count, digest: ociConfigDigest), - layers: [ - OCIManifestLayer(mediaType: ociLayerMediaType, size: layerData.count, digest: layerDigest) - ], + layers: layers, uncompressedDiskSize: UInt64(diskSize), uploadDate: Date() )