<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="stylesheet" href="style.css" type="text/css" media="all"/>
<title>BSDT(1)</title>
</head>
<body>
<table class="head">
<tr>
<td class="head-ltitle">BSDT(1)</td>
<td class="head-vol">General Commands Manual</td>
<td class="head-rtitle">BSDT(1)</td>
</tr>
</table>
<div class="manual-text">
<section class="Sh">
<h1 class="Sh" id="NAME"><a class="permalink" href="#NAME">NAME</a></h1>
<p class="Pp"><code class="Nm">bsdt</code> — <span class="Nd">repeatable
FreeBSD development VMs from a TOML file</span></p>
</section>
<section class="Sh">
<h1 class="Sh" id="SYNOPSIS"><a class="permalink" href="#SYNOPSIS">SYNOPSIS</a></h1>
<table class="Nm">
<tr>
<td><code class="Nm">bsdt</code></td>
<td>[<code class="Fl">-f</code> <var class="Ar">file</var>]
<var class="Ar">command</var> [<var class="Ar">args</var>]</td>
</tr>
</table>
</section>
<section class="Sh">
<h1 class="Sh" id="DESCRIPTION"><a class="permalink" href="#DESCRIPTION">DESCRIPTION</a></h1>
<p class="Pp"><code class="Nm">bsdt</code> boots a FreeBSD virtual machine for
the project in the current directory, described by a
<span class="Pa">bsdt.toml</span> environment file, much as
‘<code class="Li">docker compose up</code>’ starts containers
from a compose file. It is meant for building and testing software on
FreeBSD while working on Linux or macOS.</p>
<p class="Pp" id="BASIC-CLOUDINIT">The guest is booted with
<a class="Xr" href="https://man.freebsd.org/cgi/man.cgi?query=qemu-system-x86_64&sektion=1">qemu-system-x86_64(1)</a>
or
<a class="Xr" href="https://man.freebsd.org/cgi/man.cgi?query=qemu-system-aarch64&sektion=1">qemu-system-aarch64(1)</a>
from the official FreeBSD
<a class="permalink" href="#BASIC-CLOUDINIT"><b class="Sy">BASIC-CLOUDINIT</b></a>
VM image, which is downloaded once, checked against the release's
<span class="Pa">CHECKSUM.SHA256</span> 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.</p>
<p class="Pp">On first boot, a small configuration disk is attached that
FreeBSD's
<a class="Xr" href="https://man.freebsd.org/cgi/man.cgi?query=nuageinit&sektion=7">nuageinit(7)</a>
reads to create a ‘<code class="Li">bsdt</code>’ user and to
allow key-based SSH logins as that user and as root.
<code class="Nm">bsdt</code> then installs the listed packages, copies the
project into the guest with
<a class="Xr" href="https://man.freebsd.org/cgi/man.cgi?query=rsync&sektion=1">rsync(1)</a>
and runs the provision commands. Later commands reach the guest over SSH on
a forwarded port bound to 127.0.0.1.</p>
<p class="Pp">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.</p>
<p class="Pp">NetBSD and OpenBSD guests are planned. They are accepted in the
environment file but rejected by <code class="Cm">up</code> for now.</p>
</section>
<section class="Sh">
<h1 class="Sh" id="COMMANDS"><a class="permalink" href="#COMMANDS">COMMANDS</a></h1>
<dl class="Bl-tag">
<dt id="init"><a class="permalink" href="#init"><code class="Cm">init</code></a></dt>
<dd>Write a commented starter <span class="Pa">bsdt.toml</span> in the current
directory, or to the path given with <code class="Fl">-f</code>.</dd>
<dt id="up"><a class="permalink" href="#up"><code class="Cm">up</code></a></dt>
<dd>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.</dd>
<dt id="down"><a class="permalink" href="#down"><code class="Cm">down</code></a></dt>
<dd>Shut the guest down through ACPI, waiting up to a minute before stopping
QEMU forcibly. The disk is kept.</dd>
<dt id="destroy"><a class="permalink" href="#destroy"><code class="Cm">destroy</code></a></dt>
<dd>Stop QEMU immediately and delete the VM's state directory, including its
disk. The cached base image is kept.</dd>
<dt id="status"><a class="permalink" href="#status"><code class="Cm">status</code></a></dt>
<dd>Show the environment file, guest, sync destination, whether the VM is
running, and its forwarded ports.</dd>
<dt id="ssh"><a class="permalink" href="#ssh"><code class="Cm">ssh</code></a>
[<code class="Fl">--root</code>]</dt>
<dd>Open a login shell in the guest directory matching the current
directory.</dd>
<dt id="exec"><a class="permalink" href="#exec"><code class="Cm">exec</code></a>
[<code class="Fl">--root</code>] [<code class="Fl">--no-sync</code>]
<code class="Fl">--</code> <var class="Ar">command</var>
[<var class="Ar">arg ...</var>]</dt>
<dd>Sync the project, then run <var class="Ar">command</var> in the guest
directory matching the current directory. Each argument is passed through
unchanged, so shell syntax needs an explicit ‘<code class="Li">sh
-c</code>’. A terminal is allocated when standard input and output
are terminals. The exit status is that of
<var class="Ar">command</var>.</dd>
<dt id="sync"><a class="permalink" href="#sync"><code class="Cm">sync</code></a></dt>
<dd>Copy the project directory into the guest; see
<a class="Sx" href="#SYNCING">SYNCING</a>.</dd>
<dt id="provision"><a class="permalink" href="#provision"><code class="Cm">provision</code></a></dt>
<dd>Install the packages and run the provision commands again on the running
VM.</dd>
<dt id="logs"><a class="permalink" href="#logs"><code class="Cm">logs</code></a>
[<code class="Fl">-F</code>]</dt>
<dd>Print the guest's serial console log, which shows the boot and is the
first place to look when <code class="Cm">up</code> times out.</dd>
<dt id="pull"><a class="permalink" href="#pull"><code class="Cm">pull</code></a></dt>
<dd>Download and cache the base image for the environment file without booting
anything.</dd>
<dt id="images"><a class="permalink" href="#images"><code class="Cm">images</code></a></dt>
<dd>List cached base images and their sizes.</dd>
<dt id="man"><a class="permalink" href="#man"><code class="Cm">man</code></a>
[<code class="Fl">--install</code>]</dt>
<dd>Print this manual page to standard output. With
<code class="Fl">--install</code>, write it to
<span class="Pa">../share/man/man1/bsdt.1</span> relative to the directory
holding the <code class="Nm">bsdt</code> binary instead, and print the
path written.</dd>
</dl>
</section>
<section class="Sh">
<h1 class="Sh" id="OPTIONS"><a class="permalink" href="#OPTIONS">OPTIONS</a></h1>
<dl class="Bl-tag">
<dt id="f"><a class="permalink" href="#f"><code class="Fl">-f</code></a>,
<code class="Fl">--file</code> <var class="Ar">file</var></dt>
<dd>Use <var class="Ar">file</var> as the environment file instead of the
first <span class="Pa">bsdt.toml</span> found in the current directory or
its parents. Each environment file gets its own VM, so one project can
have several.</dd>
<dt id="root"><a class="permalink" href="#root"><code class="Fl">--root</code></a></dt>
<dd>For <code class="Cm">ssh</code> and <code class="Cm">exec</code>, log in
as root instead of the ‘<code class="Li">bsdt</code>’
user.</dd>
<dt id="no-sync"><a class="permalink" href="#no-sync"><code class="Fl">--no-sync</code></a></dt>
<dd>For <code class="Cm">exec</code>, skip syncing the project first.</dd>
<dt id="F"><a class="permalink" href="#F"><code class="Fl">-F</code></a>,
<code class="Fl">--follow</code></dt>
<dd>For <code class="Cm">logs</code>, keep printing as the log grows.</dd>
<dt id="h"><a class="permalink" href="#h"><code class="Fl">-h</code></a>,
<code class="Fl">--help</code></dt>
<dd>Print a short usage summary.</dd>
<dt id="V"><a class="permalink" href="#V"><code class="Fl">-V</code></a>,
<code class="Fl">--version</code></dt>
<dd>Print the version.</dd>
</dl>
</section>
<section class="Sh">
<h1 class="Sh" id="ENVIRONMENT_FILE"><a class="permalink" href="#ENVIRONMENT_FILE">ENVIRONMENT
FILE</a></h1>
<p class="Pp">The environment file is TOML. Only <code class="Ic">vm.os</code>
and <code class="Ic">vm.version</code> are required.</p>
<section class="Ss">
<h2 class="Ss" id="_vm_"><a class="permalink" href="#_vm_">[vm]</a></h2>
<dl class="Bl-tag">
<dt id="os"><a class="permalink" href="#os"><code class="Ic">os</code></a></dt>
<dd>The guest operating system. Only
‘<code class="Li">freebsd</code>’ is supported so far.</dd>
<dt id="version"><a class="permalink" href="#version"><code class="Ic">version</code></a></dt>
<dd>The release to boot, such as ‘<code class="Li">15.1</code>’
or ‘<code class="Li">14.5</code>’.</dd>
<dt id="arch"><a class="permalink" href="#arch"><code class="Ic">arch</code></a></dt>
<dd>‘<code class="Li">auto</code>’ (the default) to match the
host, ‘<code class="Li">amd64</code>’ or
‘<code class="Li">aarch64</code>’.</dd>
<dt id="cpus"><a class="permalink" href="#cpus"><code class="Ic">cpus</code></a></dt>
<dd>Number of virtual CPUs. Defaults to 2.</dd>
<dt id="memory"><a class="permalink" href="#memory"><code class="Ic">memory</code></a></dt>
<dd>Memory size in QEMU syntax. Defaults to
‘<code class="Li">2G</code>’.</dd>
<dt id="disk"><a class="permalink" href="#disk"><code class="Ic">disk</code></a></dt>
<dd>Size of the overlay disk; the guest grows its file system to fill it on
first boot. Defaults to ‘<code class="Li">20G</code>’.</dd>
<dt id="filesystem"><a class="permalink" href="#filesystem"><code class="Ic">filesystem</code></a></dt>
<dd>‘<code class="Li">ufs</code>’ (the default) or
‘<code class="Li">zfs</code>’, choosing between the two
images FreeBSD publishes.</dd>
<dt id="hostname"><a class="permalink" href="#hostname"><code class="Ic">hostname</code></a></dt>
<dd>The guest's hostname. Defaults to the project directory's name.</dd>
<dt id="update"><a class="permalink" href="#update"><code class="Ic">update</code></a></dt>
<dd>When true, let the image install FreeBSD security updates on first boot
and reboot, as it does by default outside <code class="Nm">bsdt</code>.
Defaults to false, so every VM starts from exactly the release image and
the first <code class="Cm">up</code> is much faster. Only read when the
disk is created.</dd>
<dt id="ports"><a class="permalink" href="#ports"><code class="Ic">ports</code></a></dt>
<dd>TCP ports to forward from 127.0.0.1 on the host, as
‘<code class="Li">HOST:GUEST</code>’ strings, or a single
port number when both sides are the same.</dd>
</dl>
</section>
<section class="Ss">
<h2 class="Ss" id="_packages_"><a class="permalink" href="#_packages_">[packages]</a></h2>
<dl class="Bl-tag">
<dt id="install"><a class="permalink" href="#install"><code class="Ic">install</code></a></dt>
<dd>Packages to install with
<a class="Xr" href="https://man.freebsd.org/cgi/man.cgi?query=pkg&sektion=8">pkg(8)</a>.
<a class="Xr" href="https://man.freebsd.org/cgi/man.cgi?query=rsync&sektion=1">rsync(1)</a>
is always installed as well, since <code class="Nm">bsdt</code> needs it
to sync.</dd>
</dl>
</section>
<section class="Ss">
<h2 class="Ss" id="_sync_"><a class="permalink" href="#_sync_">[sync]</a></h2>
<dl class="Bl-tag">
<dt id="dest"><a class="permalink" href="#dest"><code class="Ic">dest</code></a></dt>
<dd>Absolute guest path to sync the project to. Defaults to
<span class="Pa">/home/bsdt/</span><var class="Ar">name</var>, where
<var class="Ar">name</var> is the project directory's name.</dd>
<dt id="exclude"><a class="permalink" href="#exclude"><code class="Ic">exclude</code></a></dt>
<dd><a class="Xr" href="https://man.freebsd.org/cgi/man.cgi?query=rsync&sektion=1">rsync(1)</a>
exclude patterns, such as build output directories.</dd>
</dl>
</section>
<section class="Ss">
<h2 class="Ss" id="_provision_"><a class="permalink" href="#_provision_">[provision]</a></h2>
<dl class="Bl-tag">
<dt id="root~2"><a class="permalink" href="#root~2"><code class="Ic">root</code></a></dt>
<dd>Shell commands run as root after packages are installed.</dd>
<dt id="run"><a class="permalink" href="#run"><code class="Ic">run</code></a></dt>
<dd>Shell commands run as the ‘<code class="Li">bsdt</code>’
user in the synced directory, after <code class="Ic">root</code>.</dd>
</dl>
<p class="Pp">Provision commands run in order on the first
<code class="Cm">up</code> and on every <code class="Cm">provision</code>,
and stop at the first failure.</p>
</section>
</section>
<section class="Sh">
<h1 class="Sh" id="SYNCING"><a class="permalink" href="#SYNCING">SYNCING</a></h1>
<p class="Pp">The directory holding the environment file is mirrored to
<code class="Ic">sync.dest</code> as the
‘<code class="Li">bsdt</code>’ user by
<code class="Cm">up</code>, <code class="Cm">sync</code> and
<code class="Cm">exec</code>. Files deleted on the host are deleted in the
guest, except for paths matching <code class="Ic">sync.exclude</code>, which
are neither copied nor deleted, so build output in the guest survives a
sync. The <span class="Pa">.bsdt</span> directory is always excluded.</p>
<p class="Pp">Syncing is one way, from host to guest. Use
<a class="Xr" href="https://man.freebsd.org/cgi/man.cgi?query=scp&sektion=1">scp(1)</a>
or <code class="Cm">exec</code> to get results back.</p>
</section>
<section class="Sh">
<h1 class="Sh" id="ENVIRONMENT"><a class="permalink" href="#ENVIRONMENT">ENVIRONMENT</a></h1>
<dl class="Bl-tag">
<dt id="BSDT_CACHE_DIR"><a class="permalink" href="#BSDT_CACHE_DIR"><code class="Ev">BSDT_CACHE_DIR</code></a></dt>
<dd>Cache directory to use instead of the default.</dd>
<dt id="XDG_CACHE_HOME"><a class="permalink" href="#XDG_CACHE_HOME"><code class="Ev">XDG_CACHE_HOME</code></a></dt>
<dd>When set, the cache is <span class="Pa">$XDG_CACHE_HOME/bsdt</span>.</dd>
</dl>
</section>
<section class="Sh">
<h1 class="Sh" id="FILES"><a class="permalink" href="#FILES">FILES</a></h1>
<dl class="Bl-tag">
<dt><span class="Pa">bsdt.toml</span></dt>
<dd>The environment file.</dd>
<dt><span class="Pa">.bsdt/</span><var class="Ar">stem</var><span class="Pa">/</span></dt>
<dd>Per-VM state beside the environment file, where <var class="Ar">stem</var>
is the environment file's name without <span class="Pa">.toml</span>: the
overlay disk <span class="Pa">disk.qcow2</span>, the configuration disk
<span class="Pa">seed.img</span>, the SSH key pair
<span class="Pa">id_ed25519</span>, the serial console log
<span class="Pa">console.log</span>, QEMU's pid file
<span class="Pa">qemu.pid</span> and the forwarded ports in
<span class="Pa">state.json</span>. It contains a
<span class="Pa">.gitignore</span> that ignores everything in it.</dd>
<dt><span class="Pa">~/.cache/bsdt/images/</span></dt>
<dd>Cached base images on Linux;
<span class="Pa">~/Library/Caches/bsdt/images/</span> on macOS. Delete
files here to free space.</dd>
</dl>
</section>
<section class="Sh">
<h1 class="Sh" id="EXIT_STATUS"><a class="permalink" href="#EXIT_STATUS">EXIT
STATUS</a></h1>
<p class="Pp"><code class="Cm">ssh</code> and <code class="Cm">exec</code> exit
with the status of the remote shell or command. Otherwise,
<br/>
The <code class="Nm">bsdt</code> utility exits 0 on success,
and >0 if an error occurs.</p>
</section>
<section class="Sh">
<h1 class="Sh" id="EXAMPLES"><a class="permalink" href="#EXAMPLES">EXAMPLES</a></h1>
<p class="Pp">Start a project with a FreeBSD 15.1 VM that has Rust
installed:</p>
<div class="Bd Pp Bd-indent Li">
<pre>$ bsdt init
$ $EDITOR bsdt.toml
$ bsdt up</pre>
</div>
<p class="Pp">with a <span class="Pa">bsdt.toml</span> like:</p>
<div class="Bd Pp Bd-indent Li">
<pre>[vm]
os = "freebsd"
version = "15.1"
memory = "4G"
ports = ["8080"]
[packages]
install = ["rust", "git"]
[sync]
exclude = ["target"]
[provision]
run = ["cargo fetch"]</pre>
</div>
<p class="Pp">Build and test after editing on the host:</p>
<div class="Bd Pp Bd-indent Li">
<pre>$ bsdt exec -- cargo test</pre>
</div>
<p class="Pp">Run a pipeline, which needs a shell in the guest:</p>
<div class="Bd Pp Bd-indent Li">
<pre>$ bsdt exec -- sh -c 'uname -a | tee uname.txt'</pre>
</div>
<p class="Pp">Keep a second VM for an older release next to the default one:</p>
<div class="Bd Pp Bd-indent Li">
<pre>$ cp bsdt.toml freebsd14.toml
$ sed -i.bak 's/15.1/14.5/' freebsd14.toml
$ bsdt -f freebsd14.toml up</pre>
</div>
<p class="Pp">Start again from a clean disk:</p>
<div class="Bd Pp Bd-indent Li">
<pre>$ bsdt destroy && bsdt up</pre>
</div>
<p class="Pp">Install this page after ‘<code class="Li">cargo install
bsdt</code>’, or read it without installing:</p>
<div class="Bd Pp Bd-indent Li">
<pre>$ bsdt man --install
$ bsdt man | man -l -</pre>
</div>
</section>
<section class="Sh">
<h1 class="Sh" id="SEE_ALSO"><a class="permalink" href="#SEE_ALSO">SEE
ALSO</a></h1>
<p class="Pp"><a class="Xr" href="https://man.freebsd.org/cgi/man.cgi?query=qemu-img&sektion=1">qemu-img(1)</a>,
<a class="Xr" href="https://man.freebsd.org/cgi/man.cgi?query=rsync&sektion=1">rsync(1)</a>,
<a class="Xr" href="https://man.freebsd.org/cgi/man.cgi?query=ssh&sektion=1">ssh(1)</a>,
<a class="Xr" href="https://man.freebsd.org/cgi/man.cgi?query=nuageinit&sektion=7">nuageinit(7)</a></p>
</section>
<section class="Sh">
<h1 class="Sh" id="AUTHORS"><a class="permalink" href="#AUTHORS">AUTHORS</a></h1>
<p class="Pp"><span class="An">Divan Visagie</span>
<<a class="Mt" href="mailto:me@divanv.com">me@divanv.com</a>></p>
</section>
<section class="Sh">
<h1 class="Sh" id="CAVEATS"><a class="permalink" href="#CAVEATS">CAVEATS</a></h1>
<p class="Pp">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.</p>
</section>
</div>
<table class="foot">
<tr>
<td class="foot-date">October 8, 2026</td>
<td class="foot-os">Ubuntu</td>
</tr>
</table>
</body>
</html>