bsdkrun — Gleam SDK

Package Version Hex Docs

A Gleam SDK for bsdkrun — a Firecracker-style microVM launcher for BSD and Linux guests on macOS and Linux, built on libkrun.

The SDK is a thin, stateless wrapper around the bsdkrun binary: it builds argv, shells out through an Erlang port, and decodes the JSON output. There is no daemon and no long-lived state — every call is one CLI invocation.

Erlang target only. The SDK spawns a subprocess, so it does not run on the JavaScript target.

Install

gleam add bsdkrun

You also need the bsdkrun binary itself — see the install instructions:

brew install tsirysndr/tap/bsdkrun

Finding the binary

Resolution order, first match wins:

#Source
1binary.set_binary_path("…")
2the $BSDKRUN_BIN environment variable
3bsdkrun on $PATH
4an in-repo dev build — target/release/bsdkrun, then debug

If nothing matches you get error.BinaryNotFound, listing every path tried.

Quick start

import bsdkrun
import bsdkrun/args
import bsdkrun/types

pub fn main() {
  let assert Ok(box) = bsdkrun.create(args.linux("alpine"))
  let assert Ok(res) = bsdkrun.exec(box, ["uname", "-a"])
  echo types.text(res)
  let assert Ok(Nil) = bsdkrun.stop(box)
}

Every fallible call returns Result(a, bsdkrun/error.Error); nothing in the SDK panics on its own. Render an error with error.to_string.

Creating machines

bsdkrun/args builds the create options. Start from a per-guest constructor and refine with the with_* helpers:

import bsdkrun/args

// an OCI image, as a microVM
args.linux("alpine")
|> args.with_name("web")
|> args.with_cpus(2)
|> args.with_mem(2048)
|> args.with_ports([args.Port(8080, 80)])
|> args.with_mounts(["/host/src:/src"])
|> args.with_command(["sh", "-c", "httpd -f"])

// FreeBSD, on a persistent disk
args.freebsd()
|> args.with_version("15.0")
|> args.with_persist(True)

// NetBSD
args.netbsd()

// an arbitrary disk, booted through UEFI firmware
args.firmware("/path/edk2.fd", "/path/disk.raw")

// an arbitrary kernel, no bootloader
args.kernel("/path/vmlinuz")

Setters that do not apply to the chosen guest kind are ignored rather than rejected — with_command on a NetBSD guest is a no-op, since only Linux guests take a trailing command.

Running commands

bsdkrun.exec covers the common case. For env vars, a TTY, stdin, or a working directory, use sandbox.exec with sandbox.exec_options():

import bsdkrun/sandbox
import bsdkrun/types

let assert Ok(res) =
  sandbox.exec(
    box,
    ["sh", "-c", "cat > out.txt && wc -l < out.txt"],
    sandbox.exec_options()
      |> sandbox.with_env([#("RUST_LOG", "debug")])
      |> sandbox.with_stdin("one\ntwo\n")
      |> sandbox.with_cwd("/tmp"),
  )

types.text(res)      // stdout, trailing newlines trimmed
types.lines(res)     // non-empty stdout lines
types.is_ok(res)     // exit_code == 0
res.exit_code
res.stderr

A non-zero exit is not an error by default — it comes back in the CommandResult. Pass sandbox.with_fail_on_error(True) to turn it into error.CommandFailed instead.

Lifecycle

bsdkrun.stop(box)
bsdkrun.start(box)             // restart in place: same id, disk, resources
bsdkrun.remove(box, True)      // force: stop first if running
bsdkrun.status(box)            // Ok(Some(SandboxInfo)) or Ok(None) if gone
bsdkrun.is_running(box)
bsdkrun.logs(box)              // console log
sandbox.boot_logs(box)         // bsdkrun's own boot log
sandbox.update(box, Some(4), Some(4096))  // cpus, mem — applies on next start
sandbox.shell(box)             // interactive shell, inherits stdio

Reconnect to a machine you already booted with bsdkrun.get(id) — a unique id prefix is enough — or enumerate with bsdkrun.list() / bsdkrun.list_all(True).

Host operations

import bsdkrun/images
import bsdkrun/networks
import bsdkrun/system
import bsdkrun/volumes

images.list()

volumes.list()
volumes.remove(["scratch"], False)

networks.list()
networks.create("lab")
networks.connect("web", "lab")
networks.members("lab")
networks.sync("lab")           // refresh name resolution without restarting
networks.disconnect("web")
networks.remove(["lab"], False)

system.probe()                 // does the toolchain work? does not boot
system.versions(system.Freebsd)
system.fetch_image(system.Netbsd, Some("10.1"), None, False)
system.grow_disk("/path/disk.raw", "20G")

Machines on a global network reach each other by name: Linux and FreeBSD resolve via the network’s DNS, NetBSD via a synced /etc/hosts block.

SSH & Tailscale

// install key-based SSH via the guest agent
sandbox.ssh_setup(box, None, [])                       // your local ~/.ssh/*.pub
sandbox.ssh_setup(box, Some("tsiry"), ["~/.ssh/work.pub"])

// put the guest on your tailnet
sandbox.tailscale_up(box, Some("tskey-auth-…"), Some("web"), [])

The Tailscale auth key travels in the environment as TS_AUTHKEY, so it never appears in an argument list.

Errors

bsdkrun/error.Error is a single type with one variant per failure mode:

VariantMeaning
BinaryNotFoundthe bsdkrun binary wasn’t found; carries the paths tried
CommandFaileda command exited non-zero; carries code, stdout, stderr
SandboxNotFoundno machine matched the given id or prefix
DecodeFailed--json output could not be decoded; carries the raw text
InvalidOptionsthe create options were inconsistent, e.g. an empty image

Development

gleam test    # unit tests, incl. real subprocess round-trips through the FFI
gleam format  # format
gleam check   # type-check
gleam docs build

The subprocess tests use test/support/fake-bsdkrun, a small shell script that absorbs the SDK’s global --log-level prefix and then behaves like sh, so stdout, stderr, stdin and exit codes can all be driven from a shell snippet.

License

MIT

Search Document