BSDT(1) General Commands Manual BSDT(1)

bsdt — repeatable FreeBSD development VMs from a TOML file

bsdt [-f file] command [args]

bsdt boots a FreeBSD virtual machine for the project in the current directory, described by a bsdt.toml environment file, much as ‘docker compose up’ starts containers from a compose file. It is meant for building and testing software on FreeBSD while working on Linux or macOS.

The guest is booted with qemu-system-x86_64(1) or qemu-system-aarch64(1) from the official FreeBSD VM image, which is downloaded once, checked against the release's CHECKSUM.SHA256 and cached. Each project gets its own copy-on-write overlay disk on top of the cached image, so a VM can be thrown away and recreated in seconds.

On first boot, a small configuration disk is attached that FreeBSD's nuageinit(7) reads to create a ‘bsdt’ user and to allow key-based SSH logins as that user and as root. bsdt then installs the listed packages, copies the project into the guest with rsync(1) and runs the provision commands. Later commands reach the guest over SSH on a forwarded port bound to 127.0.0.1.

Hardware acceleration is used when the guest architecture matches the host: KVM on Linux and the Hypervisor framework on macOS. Otherwise QEMU emulates the CPU, which works but is much slower.

NetBSD and OpenBSD guests are planned. They are accepted in the environment file but rejected by up for now.

Write a commented starter bsdt.toml in the current directory, or to the path given with -f.
Boot the VM. On first use this downloads the base image if needed, creates the overlay disk, waits for first-boot setup to finish, installs packages, syncs the project and runs the provision commands. On later runs it boots the existing disk and syncs. Does nothing if the VM is already running.
Shut the guest down through ACPI, waiting up to a minute before stopping QEMU forcibly. The disk is kept.
Stop QEMU immediately and delete the VM's state directory, including its disk. The cached base image is kept.
Show the environment file, guest, sync destination, whether the VM is running, and its forwarded ports.
[--root]
Open a login shell in the guest directory matching the current directory.
[--root] [--no-sync] -- command [arg ...]
Sync the project, then run command in the guest directory matching the current directory. Each argument is passed through unchanged, so shell syntax needs an explicit ‘sh -c’. A terminal is allocated when standard input and output are terminals. The exit status is that of command.
Copy the project directory into the guest; see SYNCING.
Install the packages and run the provision commands again on the running VM.
Open a VNC viewer window on the VM's desktop; see DESKTOP. up does this already unless gui.open is false, so this is for reopening a closed window.
chord ...
Press each chord in turn on the VM's keyboard, as if typed on it: the keys go through the emulated USB keyboard, so the guest sees them in /dev/input. A chord is key names joined by ‘+’ or ‘-’, pressed in order and released in reverse, such as ‘super+return’ or ‘ctrl-alt-f2’. Names are QEMU's key codes, such as ‘a’, ‘1’, ‘f5’, ‘tab’, ‘slash’ or ‘minus’, plus the aliases ‘super’ (also ‘meta, ‘win, ‘cmd’’’), ‘ctrl’, ‘alt’, ‘shift’, ‘enter’, ‘space’, ‘escape’ and ‘plus’. Needs vm.input, which vm.gui turns on.
[--enter] text
Type text on the VM's keyboard, one key at a time, assuming a US layout. With --enter, press Return afterwards.
[-F]
Print the guest's serial console log, which shows the boot and is the first place to look when up times out.
Download and cache the base image for the environment file without booting anything.
List cached base images and their sizes.
[--install]
Print this manual page to standard output. With --install, write it to ../share/man/man1/bsdt.1 relative to the directory holding the bsdt binary instead, and print the path written.

, --file file
Use file as the environment file instead of the first bsdt.toml found in the current directory or its parents. Each environment file gets its own VM, so one project can have several.
For ssh and exec, log in as root instead of the ‘bsdt’ user.
For exec, skip syncing the project first.
For type, press Return after the text.
, --follow
For logs, keep printing as the log grows.
, --help
Print a short usage summary.
, --version
Print the version.

The environment file is TOML. Only vm.os and vm.version are required.

The guest operating system. Only ‘freebsd’ is supported so far.
The release to boot, such as ‘15.1’ or ‘14.5’.
‘auto’ (the default) to match the host, ‘amd64’ or ‘aarch64’.
Number of virtual CPUs. Defaults to 2.
Memory size in QEMU syntax. Defaults to ‘2G’.
Size of the overlay disk; the guest grows its file system to fill it on first boot. Defaults to ‘20G’.
‘ufs’ (the default) or ‘zfs’, choosing between the two images FreeBSD publishes.
The guest's hostname. Defaults to the project directory's name.
When true, let the image install FreeBSD security updates on first boot and reboot, as it does by default outside bsdt. Defaults to false, so every VM starts from exactly the release image and the first up is much faster. Only read when the disk is created.
When true, run a desktop in the guest and open a window on it; see DESKTOP. Defaults to false.
When true, attach a USB keyboard and tablet, which the guest sees as evdev devices in /dev/input, and which key and type press keys on. Defaults to the value of gui.
TCP ports to forward from 127.0.0.1 on the host, as ‘HOST:GUEST’ strings, or a single port number when both sides are the same.

Packages to install with pkg(8). rsync(1) is always installed as well, since bsdt needs it to sync.

Absolute guest path to sync the project to. Defaults to /home/bsdt/name, where name is the project directory's name.
rsync(1) exclude patterns, such as build output directories.

Shell commands run as root after packages are installed.
Shell commands run as the ‘bsdt’ user in the synced directory, after root.

Provision commands run in order on the first up and on every provision, and stop at the first failure.

Only read when vm.gui is true. Every key has a default, so the table can be left out.

‘sway’ (the default) or ‘none’. With ‘sway’, bsdt installs and runs the desktop described in DESKTOP. With ‘none’, it runs only start, which must bring up something serving VNC on vnc.
A shell command run as the ‘bsdt’ user in the synced directory once the desktop is up, on every boot. With ‘sway’, it runs inside the desktop, and defaults to ‘foot’, a terminal. An empty string runs nothing.
Port the guest's VNC server listens on. Defaults to 5900.
Host port on 127.0.0.1 forwarded to vnc. Defaults to a free port picked at each up, which status shows.
Whether up opens a viewer window. Defaults to true.
Size of the desktop, as widthxheight. Defaults to ‘1280x800’.
Sway's modifier key in the configuration bsdt generates: ‘alt’ (the default) or ‘super’. Desktops on the host usually keep Super combinations for themselves while the viewer is in a window, so Alt reaches the VM more reliably.
Open the TigerVNC viewer full screen, where it passes system keys such as Super and Alt+Tab to the VM instead of the host. Defaults to false.

FreeBSD has no driver for QEMU's virtual graphics cards, so a desktop cannot draw to the VM's own screen. Instead, with vm.gui set, bsdt runs sway(1) on a headless output, drawn in software, and shares it over VNC with wayvnc(1). Input still comes from the VM's USB keyboard and tablet through and seatd(1).

The first up installs , , , and , enables seatd(1), and adds the ‘bsdt’ user to the ‘video’ group. Every up then starts sway, wayvnc and gui.start, waits for the VNC server to answer, and opens a viewer on the forwarded port. On macOS that is always the TigerVNC viewer (‘brew install --cask tigervnc-viewer’), since the built-in Screen Sharing requires a password and the desktop has none. On other systems it is the first of vncviewer(1), xtigervncviewer(1), remmina(1) and krdc(1) that is installed, or whatever xdg-open(1) opens ‘vnc://’ links with.

Sway reads ~/.config/sway/config in the guest. When that file does not exist, or still starts with the comment bsdt writes, bsdt generates it from sway's stock configuration with gui.modifier as the modifier, so ‘alt+return’ opens a terminal by default. Delete the comment to keep your own edits. Its output and wayvnc's go to /tmp/bsdt-sway.log and /tmp/bsdt-wayvnc.log in the guest.

Keys typed into the VNC viewer reach sway through wayvnc, not through /dev/input. Programs that read input devices directly, such as hotkey daemons, only see keys sent with key and type.

The directory holding the environment file is mirrored to sync.dest as the ‘bsdt’ user by up, sync and exec. Files deleted on the host are deleted in the guest, except for paths matching sync.exclude, which are neither copied nor deleted, so build output in the guest survives a sync. The .bsdt directory is always excluded.

Syncing is one way, from host to guest. Use scp(1) or exec to get results back.

Cache directory to use instead of the default.
When set, the cache is $XDG_CACHE_HOME/bsdt.

bsdt.toml
The environment file.
.bsdt/stem/
Per-VM state beside the environment file, where stem is the environment file's name without .toml: the overlay disk disk.qcow2, the configuration disk seed.img, the SSH key pair id_ed25519, the serial console log console.log, QEMU's pid file qemu.pid and the forwarded ports in state.json. It contains a .gitignore that ignores everything in it.
~/.cache/bsdt/images/
Cached base images on Linux; ~/Library/Caches/bsdt/images/ on macOS. Delete files here to free space.

ssh and exec exit with the status of the remote shell or command. Otherwise,
The bsdt utility exits 0 on success, and >0 if an error occurs.

Start a project with a FreeBSD 15.1 VM that has Rust installed:

$ bsdt init
$ $EDITOR bsdt.toml
$ bsdt up

with a bsdt.toml like:

[vm]
os = "freebsd"
version = "15.1"
memory = "4G"
ports = ["8080"]

[packages]
install = ["rust", "git"]

[sync]
exclude = ["target"]

[provision]
run = ["cargo fetch"]

Build and test after editing on the host:

$ bsdt exec -- cargo test

Run a pipeline, which needs a shell in the guest:

$ bsdt exec -- sh -c 'uname -a | tee uname.txt'

Keep a second VM for an older release next to the default one:

$ cp bsdt.toml freebsd14.toml
$ sed -i.bak 's/15.1/14.5/' freebsd14.toml
$ bsdt -f freebsd14.toml up

Open a sway desktop, then drive it from a script:

$ cat bsdt.toml
[vm]
os = "freebsd"
version = "15.1"
gui = true
$ bsdt up
$ bsdt type --enter 'uname -a'
$ bsdt key alt+return

Start again from a clean disk:

$ bsdt destroy && bsdt up

Install this page after ‘cargo install bsdt’, or read it without installing:

$ bsdt man --install
$ bsdt man | man -l -

qemu-img(1), rsync(1), ssh(1), nuageinit(7)

Divan Visagie <me@divanv.com>

The desktop's VNC server has no password. Like SSH, it is only reachable through a port bound to 127.0.0.1 on the host.

The guest's root account has no password on the serial console, as in the official images, and accepts the project's SSH key. The VM is meant for development, not for anything exposed to a network.