containerization-framework 0.2.2

Rust bindings for Apple's Containerization framework: Linux containers in lightweight VMs, in-process and without a daemon.
//===----------------------------------------------------------------------===//
// The end of a build: the built rootfs, exported and stored as an image.
//
// The fragile step: users, modes, symlinks, hardlinks and extended attributes
// must survive EXT4Reader's inode reading and libarchive's pax format. Only a
// session on the built image verifies they did.
//===----------------------------------------------------------------------===//

import Containerization
import ContainerizationEXT4
import ContainerizationOCI
import Foundation
import Synchronization
import SystemPackage

extension Build {
  /// Exports the built rootfs and writes it into the store as a single-layer
  /// image, returning the index descriptor the reference points at.
  ///
  /// The layer is an uncompressed tar: nothing pushes this image, and an
  /// uncompressed blob's digest is also its diffID, so both are correct in
  /// one pass. (Containerization's `InitImage.create` has a standing `TODO`
  /// for writing a gzip layer's compressed digest as its diffID.)
  static func ingest(
    rootfs: URL,
    plan: BuildPlan,
    base: ImageConfig?,
    environment: [String],
    platform: Platform,
    contentStore: ContentStore
  ) async throws -> Descriptor {
    let layer = rootfs.deletingLastPathComponent().appending(path: "layer.tar")
    try? FileManager.default.removeItem(at: layer)

    let reader = try EXT4.EXT4Reader(blockDevice: FilePath(rootfs.path(percentEncoded: false)))
    try reader.export(archive: FilePath(layer.path(percentEncoded: false)))

    let index = Box<Descriptor>()
    let user = plan.user ?? base?.user
    let workingDirectory = plan.workingDirectory ?? base?.workingDir
    // The base's labels carry over, as `LABEL` does; the plan's win on a
    // key both set.
    let labels = (base?.labels ?? [:]).merging(plan.labels) { _, mine in mine }
    let entrypoint = base?.entrypoint
    let command = base?.cmd

    try await contentStore.ingest { directory in
      let writer = try ContentWriter(for: directory)

      var result = try writer.create(from: layer)
      let layerDescriptor = Descriptor(
        mediaType: MediaTypes.imageLayer,
        digest: result.digest.digestString,
        size: result.size
      )
      let diffID = result.digest.digestString

      let config = ContainerizationOCI.Image(
        architecture: platform.architecture,
        os: platform.os,
        variant: platform.variant,
        config: ImageConfig(
          user: user,
          env: environment,
          entrypoint: entrypoint,
          cmd: command,
          workingDir: workingDirectory,
          labels: labels
        ),
        rootfs: Rootfs(type: "layers", diffIDs: [diffID])
      )
      result = try writer.create(from: config)
      let configDescriptor = Descriptor(
        mediaType: MediaTypes.imageConfig,
        digest: result.digest.digestString,
        size: result.size
      )

      result = try writer.create(from: Manifest(config: configDescriptor, layers: [layerDescriptor]))
      let manifestDescriptor = Descriptor(
        mediaType: MediaTypes.imageManifest,
        digest: result.digest.digestString,
        size: result.size,
        platform: platform
      )

      result = try writer.create(from: Index(manifests: [manifestDescriptor]))
      index.value = Descriptor(
        mediaType: MediaTypes.index,
        digest: result.digest.digestString,
        size: result.size
      )
    }

    try? FileManager.default.removeItem(at: layer)

    guard let descriptor = index.value else {
      throw BridgeError.notIngested(plan.tag)
    }

    return descriptor
  }
}

/// Carries a result out of `ingest`'s `@Sendable` body, which cannot return one
/// or capture a bare `Mutex`.
private final class Box<Value: Sendable>: Sendable {
  private let stored = Mutex<Value?>(nil)

  var value: Value? {
    get { stored.withLock { $0 } }
    set { stored.withLock { $0 = newValue } }
  }
}