How to set up a machine to work on vfox-plus: Ubuntu 26.04 LTS, on its own or in WSL2 on Windows 11, or a dev container that has all of it. What you install depends on what you do:
| You want to | You need |
|---|---|
| Have every tool in your editor at once, and change nothing on your machine | Docker, and an editor that opens dev containers (The dev container) |
| Run every check, the end-to-end tests and the packaging, and change nothing on your machine | Docker, and Go to build vfxtool (The sandbox) |
| Edit and test the Go code quickly | Go, a few packages and golangci-lint (Go and the native tools) |
| Build the Android client | A JDK 17 and the Android SDK (Android) |
| Run the Android client on an emulator | KVM, an adb device, or CI (The emulator and KVM) |
The pinned versions live in
mygo/versions.env and
android/versions.env. The commands below read
them, so that this page does not go stale when one changes, and so does the image of
the dev container when it is built.
vfxtool sandbox runs the checks of CI, the end-to-end tests and the packaging in
throw-away Docker containers (see Shared sandbox), so that nothing
but Docker has to be installed, and nothing on the machine changes: not its
toolchains, not its home directory, not a vfox that is installed. Docker Engine
works, rootful or rootless (vfxtool sandbox tells them apart). In WSL2 install the
Engine inside the distribution; it needs systemd (see WSL2).
vfxtool is a Go program: build it once with the Go of the next section, then run
everything through it. The first run builds the images, which takes a while.
go -C shared/tooling build -o ~/.local/bin/vfxtool ./cmd/vfxtool
vfxtool sandbox allBehind a slow connection, set SANDBOX_UBUNTU_MIRROR, SANDBOX_GOPROXY and SANDBOX_NPM_REGISTRY (see
the mygo guide).
.devcontainer describes a container that has what the
sections below install, so that you install none of it: Ubuntu 26.04 with Go,
golangci-lint and vfox in the pinned versions, the packages that CI installs, a
virtual display, a JDK and the Android SDK, and Docker. VS Code (with the Dev
Containers extension), GitHub Codespaces and the dev container
CLI open it. The image is for x86-64, like
the archives it installs; Docker runs it under emulation on another processor.
In VS Code, open the repository and run Dev Containers: Reopen in Container. From a terminal, with Docker running:
npm install --global @devcontainers/cli
devcontainer up --workspace-folder .
devcontainer exec --workspace-folder . bashThe first start builds the image, which takes minutes, downloads about 1 GB and fills
3.6 GB of disk, and then runs the postCreateCommand of
devcontainer.json: it builds vfxtool
into /home/vscode/go/bin (~/go/bin of the user, on the PATH), downloads the Go
modules and prepares the assets that the application embeds (without the licenses of
the Go modules, which only a package needs; vfxtool prepare without
MYGO_SKIP_LICENSES collects them). After that the commands of the mygo
guide and of
Contributing run as they are written:
cd mygo
golangci-lint run && CGO_ENABLED=1 go test -race ./...
vfxtool build --e2e build/vfox-plus-e2e
xvfb-run -a -s "-screen 0 1280x800x24" dbus-run-session -- vfxtool e2e-run real build/vfox-plus-e2eVFOX_PLUS_E2E_VFOX names the vfox that the image installs, in the version that
mygo/versions.env pins, so the scenario against the real vfox needs no prefix and
has no ~/.vfox of yours to touch. ANDROID_HOME is set, and cd android && ./gradlew :app:assembleRelease builds the Android client (Android).
The container runs with an init process (init), which reaps the processes that the
tests leave behind, and with 1 GB of shared memory (--shm-size), because software
rendering and the embedded test server need more than the 64 MB that Docker gives a
container by default. The sandbox runs its containers the same way.
The Dockerfile reads mygo/versions.env and android/versions.env while the image is
built, as CI and the sandbox do, so a version that changes there (in a git pull,
say) reaches the container when you rebuild it: Dev Containers: Rebuild Container,
or devcontainer up --workspace-folder . --remove-existing-container. The
tools are in /usr/local and /opt, for the user to use and not to change. What
the user downloads lives under /home/vscode, and two Docker volumes keep it when the
container is rebuilt: vfox-plus-devcontainer-go (the modules and the build cache of
Go) and vfox-plus-devcontainer-gradle (Gradle, and the libraries of the Android
app). docker volume rm deletes them.
The variables of the sandbox work here too. Set SANDBOX_UBUNTU_MIRROR (for the
build of the image), SANDBOX_GOPROXY and SANDBOX_NPM_REGISTRY (they reach the
container as GOPROXY and VFOX_PLUS_NPM_REGISTRY) in the environment that starts the
editor or the CLI. Go, golangci-lint, vfox and the Android command-line tools come
from go.dev, GitHub and dl.google.com, and the Docker feature from ghcr.io, with no
mirror to name for any of them (Docker inside says what to do where
ghcr.io cannot be reached). Your ~/.gradle is not
the container's: the download mirror that the Gradle wrapper reads from
gradle-wrapper-neo.json (Building) belongs in the
Gradle user home of the container, /home/vscode/.gradle ($GRADLE_USER_HOME).
Rootless Docker maps the root of a container to you and every other user to a
subordinate user, who may not write to your files: in a container that runs as
vscode, the repository is read-only. Set VFOX_PLUS_DEVCONTAINER_USER=root in the
environment that starts the editor or the CLI (in ~/.profile, before code . or
devcontainer up), and the container runs as root, which is you outside it: the files
it writes are yours. With Docker Desktop, Docker Engine as root, or Codespaces, leave
it alone. The user is vscode, with sudo and no password, and on Linux it gets
your uid (updateRemoteUserUID).
Docker Engine runs in the container, with images of its own (the
docker-in-docker
feature, which makes the container privileged). The three things of this
repository that use Docker work in it as they are written: vfxtool sandbox, and the
license header check and the Markdown check of Contributing.
The container already is a throw-away environment with the tools of the sandbox, so
you need the sandbox inside it only for what it fixes: the installation into a clean
Ubuntu (package), the memory (measure) and the screenshots of the desktop
application (screenshots). Where privileged containers are not allowed, or ghcr.io
(where the feature comes from) cannot be reached, delete features from
devcontainer.json; nothing else needs it.
- There is no emulator and no KVM (The emulator and KVM): use
the
measurejob of CI, or an adb device, as in Without KVM. - There is no display. The window of the application needs one: the tests run in
xvfb, and to use the application, runvfox-plus-serveand pair a browser outside the container. - There is no Chrome, which
vfxtool webheapdrives (Go and the native tools). Install one and name it with--chrome. - A git worktree is a folder whose
.gitis a file that points into the clone it belongs to, outside the folder that the container mounts, so git does not work in it there (Go still builds). Open the clone.
Go, from its release archive and checked against the checksum that
mygo/versions.env pins (go.mod asks for 1.27.4 or later). Run this from the
root of the repository:
. ./mygo/versions.env
curl -fsSL -o /tmp/go.tar.gz "https://go.dev/dl/go$GO_VERSION.linux-amd64.tar.gz"
echo "$GO_SHA256 /tmp/go.tar.gz" | sha256sum --check --strict -
sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf /tmp/go.tar.gz
echo 'export PATH="$HOME/.local/bin:/usr/local/go/bin:$PATH"' >> ~/.profilegolangci-lint, the pinned version, the way the sandbox installs it:
. ./mygo/versions.env
archive="golangci-lint-$GOLANGCI_LINT_VERSION-linux-amd64"
curl -fsSL -o "/tmp/$archive.tar.gz" \
"https://github.com/golangci/golangci-lint/releases/download/v$GOLANGCI_LINT_VERSION/$archive.tar.gz"
echo "$GOLANGCI_LINT_SHA256 /tmp/$archive.tar.gz" | sha256sum --check --strict -
tar -xzf "/tmp/$archive.tar.gz" -C /tmp
install -D -m 755 "/tmp/$archive/golangci-lint" ~/.local/bin/golangci-lintThe Ubuntu packages are the ones that CI installs (LINUX_PACKAGES in
mygo.yml):
sudo apt install build-essential libgtk-3-0t64 libayatana-appindicator3-1 fontconfig fonts-ubuntu fonts-noto-cjk \
xvfb xauth dbus-x11build-essential is only for the race detector of the unit tests, which needs a C
compiler; the application is built without one. GTK 3 and the fonts are what the
application loads when it starts. xvfb, xauth and dbus-x11 give the
end-to-end tests a virtual display. Add lintian desktop-file-utils to check a
.deb the way CI does. libayatana-appindicator3-1 draws the icon that stands in the tray for the application
while its window is closed, which the end-to-end scenario tray needs; it runs with the session bus of
dbus-run-session, for it uses a stand-in for the host of the tray icons (see
The tray). To see the icon on your own desktop, the desktop has to be GNOME
(with its AppIndicator extension) or Windows. The scenario tray-gnome runs in the container of
shared/sandbox/gnome.Dockerfile, so it needs Docker. The commands to build, test and package are in
the mygo guide.
vfxtool webheap, which screenshots the web interface
(of a page, or of a scene such as the distribution of Java) with a look at its
JavaScript heap for local diagnosis, and vfxtool gateway-shots, which takes the
pictures of the pages of the gateway, drive a Chrome that speaks the DevTools Protocol: name it with
--chrome, or let the tool look for google-chrome, chromium and the like (see
Screenshots, which also says which Chrome gives the
size you ask for). The end-to-end scenarios against the real vfox run in the
sandbox, which brings its own pinned vfox, so that nothing touches your ~/.vfox.
The web interface is TypeScript built with Vite, in mygo/web (Node 20 or
later; the dev container and CI install Node 24). Its build bundles the page
into the single mygo/pair/webapp.html that the Go server embeds, so the
checked-in page is generated: edit the sources, not the page.
cd mygo/web
npm ci # once, or when package-lock.json changes
npm run dev # develop against a running application, see below
npm test # the unit tests of the page (esc, i18n helpers)
npm run build # type-check, bundle, and write ../pair/webapp.htmlnpm run dev serves the page with hot reload and proxies /api to a running
application: start one (for example the end-to-end build with fixtures, the
way the screenshots do), then VFOX_WEB_PROXY=http://127.0.0.1:PORT npm run dev, and open the printed address. CI builds the page, runs its tests, and
fails when the built page differs from the committed one, so build and commit
it together with its sources.
The build needs a JDK 17 (CI uses Temurin) and the Android SDK with platform 36 and build-tools 35.0.0, and runs through the committed wrapper; see Packaging Android. That needs no KVM. The emulator does.
The measure job of android.yml
installs the release APK on an emulator (API 36, google_apis, x86_64) and runs
vfxtool measure-apk there. The emulator will not start an x86_64 image without
hardware acceleration, which on Linux is KVM, so to do the same on your machine
/dev/kvm must work. CI enables it with a udev rule on the runner. Nothing else
in this repository needs KVM.
Inside Ubuntu:
grep -cE '\b(vmx|svm)\b' /proc/cpuinfo # more than 0: the processor offers virtualization
ls -l /dev/kvm # exists once kvm_intel or kvm_amd is loaded
id -nG | grep -w kvm # you may use it: the group of /dev/kvm
journalctl -k -b | grep -E 'Hyper-V: (Host Build|Nested features)'The last line is for WSL2. Nested features: 0x0 means that Windows does not
offer virtualization to the WSL2 virtual machine, and nothing inside Ubuntu can
change that; a value other than 0 means that it does (0x4a0000 on a machine
where it worked). journalctl --list-boots and journalctl -k -b -<n> show the
same lines for the earlier boots, which tells when a change happened. sudo apt install cpu-checker adds kvm-ok, which says it in words.
-
On WSL2 it needs Windows 11, virtualization (Intel VT-x or AMD-V, SVM) enabled in the firmware, and
nestedVirtualizationin%UserProfile%\.wslconfig, which istrueunless the file says otherwise (Microsoft's documentation). After a change,wsl --shutdownin a Windows terminal restarts WSL2. -
WSL2 needs systemd for the next step:
[boot]withsystemd=truein/etc/wsl.conf. The kernel of WSL2 builds KVM as modules, and with systemd running, udev loadskvm_intelorkvm_amdwhen the processor offers the extension. Where/dev/kvmis still missing although/proc/cpuinfoshowsvmxorsvm,sudo modprobe kvm_intel(orkvm_amd) says why. -
/dev/kvmbelongs to the groupkvm:sudo usermod -aG kvm "$USER", then start a new session (in WSL2,wsl --shutdown, which also ends everything that runs in it). To skip the new session, give the device to everybody with the rule that CI uses. It takes effect at once, programs that already run included, and it stays after a restart; on a computer that one person uses there is no one to keep the device from:echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm
ls -l /dev/kvmshowscrw-rw-rw-then. This asks the kernel whether KVM works, and prints 12 when it does (aPermissionErrorsays that you may not use the device yet):python3 -I -c 'import fcntl,os; print(fcntl.ioctl(os.open("/dev/kvm", os.O_RDWR), 0xAE00))'
The cumulative update KB5127648 of Windows 11 (October 2026; build 26100.9711, and
26200.9711 on 25H2) took the virtualization away from WSL2 on AMD processors: no
svm in /proc/cpuinfo, no /dev/kvm, Nested features: 0x0, and kvm_amd: SVM not supported by CPU when the module is loaded, although .wslconfig is right and
the firmware has SVM on. WSL asks Windows what the processor can do, the answer
fails with ERROR_INVALID_DATA, and WSL leaves nested virtualization out without a
word. The reports come from AMD processors of Zen 2 to Zen 4; a report of an Intel
laptop that was not affected is among them.
microsoft/WSL#41917 tracks it. On
one development machine the journal of the kernel shows the day: the boot of
2026-10-13 has Host Build 10.0.26100.9444, Nested features: 0x4a0000 and
kvm_amd: Nested Virtualization enabled, and the boot of 2026-10-14 has
Host Build 10.0.26100.9711 and Nested features: 0x0.
Microsoft wrote on 2026-10-26 that it believed a fix would come through Windows
Update the next day. In Windows, install the updates that are offered, restart, run
wsl --shutdown, and check again as above. Until then the update can be removed
with wusa /uninstall /kb:5127648 in an administrator terminal (it is a security
update, and for some it cannot be removed this way). WSL 3.0.4 only adds a warning,
wsl: Failed to detect nested virtualization support, error: ERROR_INVALID_DATA,
and does not help.
- Let CI do it: the
measurejob ofandroid.ymlruns on every pull request that touchesandroid/ormygo/pair/, and uploadsmemory-androidandscreenshot-android. vfxtool measure-apkneeds onlyadbonPATH(sudo apt install adb) and a device thatadb deviceslists: a phone with wireless debugging (adb pair, thenadb connect), or an emulator that runs on the Windows side and is reached through an adb server there (setANDROID_ADB_SERVER_ADDRESSandANDROID_ADB_SERVER_PORTin WSL, let that server listen withadb -a nodaemon server, and let the Windows firewall through). Its numbers belong to that device: they do not compare with the emulator of CI.
With KVM working, the emulator needs the command-line tools of the SDK, the build that
android/versions.env pins (the zip commandlinetools-linux-<build>_latest.zip of
dl.google.com, checked against its checksum, unpacked to $ANDROID_HOME/cmdline-tools/latest), and
three packages. The download is about 3 GB, and sdkmanager asks you to accept the licenses of the
Android SDK:
export ANDROID_HOME=$HOME/Android/Sdk
export PATH=$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator:$PATH
sdkmanager --licenses
sdkmanager platform-tools emulator "system-images;android-36;google_apis;x86_64"
avdmanager create avd -n pixel6 -k "system-images;android-36;google_apis;x86_64" -d pixel_6
emulator -avd pixel6 -no-window -no-audio -no-boot-anim -no-snapshot -gpu swiftshader_indirect &
adb wait-for-device && adb shell getprop sys.boot_completed # 1 once it has booted, about a minuteThe profile pixel_6 is 1080 by 2400 at 420 dpi, as the emulator of CI is. The back camera of the
scanner shows a living room when hw.camera.back=virtualscene is in
~/.android/avd/pixel6.avd/config.ini (avdmanager writes emulated, which shows a
plain animation); hw.ramSize=3072M is a better size than its 1536M for the WebView. Leave out -no-window to see the
emulator on a display (WSLg). To install the release APK that CI has built, without building it,
download it from the run of the pull request: gh run download RUN -n vfox-plus-android.
With an emulator that has booted (adb devices lists it), build the app, install
it and pair it with the application on your machine:
cd android && ./gradlew :app:assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apk
adb shell pm grant dev.vfox.vfoxplus android.permission.CAMERA # for the scannerThe emulator reaches your machine as 10.0.2.2. The session cookie of the servers is
Secure, and a WebView keeps it for https:// and for loopback only, so pairing over
http://10.0.2.2:PORT does not work; the server for other devices speaks HTTPS anyway. Make a
self-signed certificate that names 10.0.2.2, and give it to the application:
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes -days 30 \
-subj /CN=vfox-plus-emulator -addext subjectAltName=IP:10.0.2.2,IP:127.0.0.1 \
-keyout key.pem -out cert.pemStart the application (vfxtool build build/vfox-plus && build/vfox-plus) and, in its settings,
turn on the web interface and Other devices, then choose Certificate → Browse… and the
files cert.pem and key.pem. (The same settings can be written to
~/.config/vfox-plus/settings.json before the start: "webInterface": true, "webRemote": true,
"webCertificate": "/path/cert.pem" and "webKey": "/path/key.pem".)
The end-to-end build (vfxtool build --e2e) needs neither the certificate nor the replacing of the address:
with VFOX_PLUS_E2E_ADDRESSES=10.0.2.2 in its environment, the certificate that it makes for itself names
10.0.2.2, and so do the settings and the link that Show code gives. That is how the pictures of the guide
are taken (Screenshots).
Choose Show code next to Pair a phone, copy the link, and type it into the app, with the
address of your machine replaced by 10.0.2.2. The app shows the fingerprint of the
certificate; it has to be the one that the settings show and that
openssl x509 -in cert.pem -noout -fingerprint -sha256 prints.
vfxtool measure-apk runs on the same emulator. It installs without -r, so
uninstall the app first (adb uninstall dev.vfox.vfoxplus).
- systemd is needed by the steps above and by rootless Docker.
[boot],systemd=truein/etc/wsl.conf, thenwsl --shutdown. - Memory is half of the machine's unless
%UserProfile%\.wslconfigsays[wsl2]withmemory=<size>. The race tests of the Go code, the Docker builds and Chrome are the heavy ones. - A display for graphical programs comes from WSLg (
guiApplicationsin.wslconfigistrueunless it says otherwise). Tests that need a display without a window usexvfbor the sandbox. - Files in the Linux file system (
~/...) are much faster than the ones under/mnt/c: clone the repository into your Linux home. - The dev container works with the WSL integration of Docker Desktop and with a
Docker Engine in the distribution. Open the clone in the WSL window of VS Code
(
code .in the distribution) and reopen it in the container. With a rootless Engine, setVFOX_PLUS_DEVCONTAINER_USER=rootfirst (Rootless Docker).