containerization-framework 0.1.0

Rust bindings for Apple's Containerization framework: Linux containers in lightweight VMs, in-process and without a daemon.
//===----------------------------------------------------------------------===//
// The surface Rust calls.
//
// Flat and synchronous: no Swift types cross, every function blocks, and
// failure returns a negative code with the message in `lastError` for Rust to
// collect. swift-bridge handles `async` poorly in this direction.
//
// Argument labels are the Rust parameter names; swift-bridge's generated
// `@_cdecl` shims call with them, so they are part of the contract.
//
// Lists cross as newline-separated strings, so no element may contain a
// newline; `spec::lines` writes them on the Rust side.
//===----------------------------------------------------------------------===//

import Foundation
import Synchronization

/// Set by the last failing bridged call, read by `czbridge_last_error`.
/// Locked because two attached terminals can fail at once.
private let lastError = Mutex("")

/// Bridge failure. Outside the guest exit code range 0...255.
private let failed: Int32 = -1

/// Turns a thrown error into `failed`, storing its message in `lastError`.
private func reporting(_ body: () throws -> Int32) -> Int32 {
    do {
        return try body()
    } catch {
        lastError.withLock { $0 = "\(error)" }
        return failed
    }
}

/// A line in the build log, on stderr, unprefixed for the host to label.
func note(_ message: String) {
    try? FileHandle.standardError.write(contentsOf: Data("\(message)\n".utf8))
}

private func decoding<T: Decodable>(_ type: T.Type, from json: RustStr) throws -> T {
    try JSONDecoder().decode(type, from: Data(json.toString().utf8))
}

private func lines(_ text: RustStr) -> [String] {
    let string = text.toString()

    return string.isEmpty ? [] : string.components(separatedBy: "\n")
}

func czbridge_last_error() -> String {
    lastError.withLock { $0 }
}

/// Boots a session's VM and leaves it running, owned by this process.
func czbridge_boot(
    name: RustStr,
    store_root: RustStr,
    kernel_path: RustStr,
    initfs_reference: RustStr,
    image_reference: RustStr,
    cpus: Int32,
    memory_in_bytes: UInt64,
    rootfs_capacity_in_bytes: UInt64,
    mounts: RustStr,
    sockets: RustStr,
    environment: RustStr,
    arguments: RustStr,
    working_directory: RustStr,
    ipv4_address: RustStr,
    ipv4_gateway: RustStr
) -> Int32 {
    reporting {
        let spec = BootSpec(
            name: name.toString(),
            storeRoot: store_root.toString(),
            kernelPath: kernel_path.toString(),
            initfsReference: initfs_reference.toString(),
            imageReference: image_reference.toString(),
            cpus: Int(cpus),
            memoryInBytes: memory_in_bytes,
            rootfsCapacityInBytes: rootfs_capacity_in_bytes,
            mounts: lines(mounts),
            sockets: lines(sockets),
            environment: lines(environment),
            arguments: lines(arguments),
            workingDirectory: working_directory.toString(),
            ipv4Address: ipv4_address.toString(),
            ipv4Gateway: ipv4_gateway.toString()
        )

        try blocking { try await Session.boot(spec) }

        return 0
    }
}

/// Builds an image from a plan, with no builder and no daemon.
///
/// The plan crosses as JSON because it nests and build steps are arbitrary
/// shell that may contain newlines.
func czbridge_build(plan: RustStr) -> Int32 {
    reporting {
        let plan = try decoding(BuildPlan.self, from: plan)

        try blocking { try await Build.run(plan) }

        return 0
    }
}

/// Puts a kernel and an init image in the store, fetching whatever is missing.
func czbridge_provision(spec: RustStr) -> Int32 {
    reporting {
        let spec = try decoding(ProvisionSpec.self, from: spec)

        try blocking { try await Provision.run(spec) }

        return 0
    }
}

/// Runs a process in a booted session and blocks until it exits, returning its
/// exit code.
///
/// `terminal` is a descriptor in this process: its own, or one a joining caller
/// passed over the control socket. `-1` runs without a terminal. An empty
/// `user` means the image's default, and an empty `term` leaves `TERM` to the
/// image.
func czbridge_exec(
    name: RustStr,
    id: RustStr,
    arguments: RustStr,
    environment: RustStr,
    user: RustStr,
    working_directory: RustStr,
    term: RustStr,
    terminal: Int32,
    stdin: Int32,
    stdout: Int32,
    stderr: Int32
) -> Int32 {
    reporting {
        let user = user.toString()
        let term = term.toString()
        let request = ExecRequest(
            name: name.toString(),
            id: id.toString(),
            arguments: lines(arguments),
            environment: lines(environment),
            user: user.isEmpty ? nil : user,
            workingDirectory: working_directory.toString(),
            term: term.isEmpty ? nil : term,
            terminal: terminal,
            stdin: stdin,
            stdout: stdout,
            stderr: stderr
        )

        return try blocking { try await Session.exec(request) }
    }
}

/// Tells the guest the terminal changed size. A no-op once the process has gone.
///
/// A failure here is dropped rather than recorded: resizes run beside the
/// attach they belong to, and one that stored its message in `lastError` would
/// be read back as the reason that attach failed.
func czbridge_resize(id: RustStr, terminal: Int32) -> Int32 {
    do {
        let id = id.toString()

        try blocking { try await Session.resize(id: id, terminal: terminal) }

        return 0
    } catch {
        return failed
    }
}

/// 1 when this image is already unpacked, 0 when the next run must unpack it.
func czbridge_is_unpacked(store_root: RustStr, image_reference: RustStr) -> Int32 {
    reporting {
        let unpacked = Unpacked(store: URL(filePath: store_root.toString()))
        let reference = image_reference.toString()

        return try blocking { try await unpacked.holds(reference) } ? 1 : 0
    }
}

/// Whether this process owns a running session by that name.
func czbridge_is_running(name: RustStr) -> Bool {
    Sessions.shared.get(name.toString()) != nil
}