bsdkrun — Gleam SDK

Package VersionHex 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