23 KiB
Remove Python & Sommelier, Add Rust Tools
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Remove Python runtime dependency and sommelier wayland proxy; replace with two new Rust CLI tools in the vmsilo-tools workspace.
Architecture: Two new Rust crates (desktop-file and vsock-proxy) are added to the existing vmsilo-tools/ Cargo workspace. Nix modules are updated to call the new binaries instead of Python scripts. Sommelier is removed entirely, simplifying wayland proxy code to a single codepath.
Tech Stack: Rust (edition 2021), Nix/NixOS modules, shell scripts
Important context:
nix builduses the git index — all new files must begit add'd before building.- The vmsilo-tools workspace builds as a single package:
cfg._internal."vmsilo-tools". Binaries are at${cfg._internal."vmsilo-tools"}/bin/<name>. - There are no automated tests for the Nix modules. Rust crates should have unit tests.
- Run
nix fmtbefore committing. - Don't vary guest rootfs contents per-VM; gate features on the host side only.
Task 1: Add desktop-file crate to vmsilo-tools workspace
Files:
- Create:
vmsilo-tools/desktop-file/Cargo.toml - Create:
vmsilo-tools/desktop-file/src/main.rs - Create:
vmsilo-tools/desktop-file/src/parser.rs - Modify:
vmsilo-tools/Cargo.toml(add workspace member)
This is a generic .desktop file manipulation tool. The freedesktop .desktop format is INI-like: [Group] headers, Key=Value lines, comments (#), blank lines. Locale keys like Name[fr] are distinct keys.
Subcommands:
get-key <file> <group> <key>— print value, exit 0 if found, exit 1 if missingset-key <file> <group> <key> <value>— output full file with key set (create key/group if missing)list-groups <file>— one group name per linelist-entries <file> <group>— one key per line for groupfilter-groups <file> <group> [<group>...]— output file keeping only listed groupsfilter-keys <file> <group> <key> [<key>...]— output file keeping only listed keys in group; other groups pass through unchanged
All commands accept - as file argument to read from stdin. Output to stdout.
- Step 1: Create
desktop-file/Cargo.toml
[package]
name = "vmsilo-desktop-file"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1"
- Step 2: Add workspace member
In vmsilo-tools/Cargo.toml, change members = ["tap-open"] to members = ["tap-open", "desktop-file"].
- Step 3: Write the parser module (
src/parser.rs)
Line-oriented parser that classifies each line as: blank/comment, group header [Name], or key-value Key=Value. Preserves original text for passthrough. Data model:
/// A single line from a .desktop file, classified but preserving original text.
pub enum Line {
/// Blank line or comment
Other(String),
/// Group header: [GroupName]
Group { name: String, raw: String },
/// Key=Value entry
Entry { key: String, value: String, raw: String },
}
/// Parsed .desktop file preserving order and formatting.
pub struct DesktopFile {
pub lines: Vec<Line>,
}
Implement:
DesktopFile::parse(input: &str) -> DesktopFile— parse linesDesktopFile::get_key(group: &str, key: &str) -> Option<&str>— find key in groupDesktopFile::set_key(group: &str, key: &str, value: &str)— set key (create if needed, create group if needed)DesktopFile::list_groups() -> Vec<&str>— unique group names in orderDesktopFile::list_entries(group: &str) -> Vec<&str>— key names in groupDesktopFile::filter_groups(keep: &[&str])— remove groups not in keep listDesktopFile::filter_keys(group: &str, keep: &[&str])— remove keys not in keep list for given group, other groups unchangedDesktopFile::write(&self) -> String— reconstruct file text
Key parsing rules:
-
Group header: line starts with
[, ends with](after trim) -
Key-value: split on first
= -
Everything else is Other (comments, blank lines)
-
Preserve original line text in
rawfield for exact passthrough when not modified -
Step 4: Write parser unit tests
Test at minimum:
-
Parse a file with multiple groups, keys, comments, blank lines
-
get_keyreturns correct value, returnsNonefor missing key/group -
set_keyon existing key changes value -
set_keyon new key in existing group appends it -
set_keyon new key in new group creates group at end -
list_groupsreturns groups in order -
list_entriesreturns keys in order -
filter_groupskeeps only listed groups plus their keys -
filter_keyskeeps only listed keys in target group, other groups unchanged -
Round-trip: parse then write preserves original text
-
Locale keys (
Name[fr]) treated as distinct fromName -
Step 5: Run tests
Run: cd /home/david/git/vmsilo/vmsilo-tools && cargo test -p vmsilo-desktop-file
Expected: All tests pass.
- Step 6: Write
src/main.rs
Parse CLI args manually (no clap dependency — keep it minimal like tap-open). Read file (or stdin if -), dispatch to parser methods, write output to stdout.
Usage:
desktop-file get-key <file> <group> <key>
desktop-file set-key <file> <group> <key> <value>
desktop-file list-groups <file>
desktop-file list-entries <file> <group>
desktop-file filter-groups <file> <group> [<group>...]
desktop-file filter-keys <file> <group> <key> [<key>...]
Exit codes: 0 = success, 1 = key/group not found (get-key), 2 = usage error.
- Step 7: Write main integration tests
Test stdin piping: write a test that calls the binary as a subprocess, pipes input via stdin with - arg, verifies stdout output. Test the pipeline pattern: set-key | set-key | filter-keys.
- Step 8: Run all tests
Run: cd /home/david/git/vmsilo/vmsilo-tools && cargo test -p vmsilo-desktop-file
Expected: All tests pass.
- Step 9: Commit
cd /home/david/git/vmsilo
git add vmsilo-tools/desktop-file/ vmsilo-tools/Cargo.toml
# Cargo.lock will be updated — add it too
git add vmsilo-tools/Cargo.lock
git commit -m "Add desktop-file tool to vmsilo-tools workspace"
Task 2: Add vsock-proxy crate to vmsilo-tools workspace
Files:
- Create:
vmsilo-tools/vsock-proxy/Cargo.toml - Create:
vmsilo-tools/vsock-proxy/src/main.rs - Modify:
vmsilo-tools/Cargo.toml(add workspace member)
Synchronous vsock CONNECT handshake + stdin/stdout proxy for cloud-hypervisor hybrid vsock.
Usage: vmsilo-vsock-proxy <socket-path> <port>
Protocol:
- Connect to Unix socket at
<socket-path> - Send
CONNECT <port>\n - Read response byte-by-byte until
\n - Validate response starts with
OK - Write any data received after the OK line to stdout
- Bidirectional proxy: stdin->socket (spawned thread), socket->stdout (main thread)
- On stdin EOF:
shutdown(Write)on socket (half-close) - Continue reading socket->stdout until EOF
- Exit 0 on success, non-zero on handshake failure or connection error
Reference implementation: The existing Rust CONNECT handshake in vmsilo-dbus-proxy/src/host/vsock.rs:20-60 does the same protocol (async/tokio). This tool is the synchronous, standalone equivalent.
- Step 1: Create
vsock-proxy/Cargo.toml
[package]
name = "vmsilo-vsock-proxy"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1"
- Step 2: Add workspace member
In vmsilo-tools/Cargo.toml, add "vsock-proxy" to members list (should now be ["tap-open", "desktop-file", "vsock-proxy"]).
- Step 3: Write
src/main.rs
Implementation outline:
fn main() -> Result<()> {
let args: Vec<String> = env::args().collect();
// Validate: exactly 3 args (binary, socket_path, port)
let socket_path = &args[1];
let port: u32 = args[2].parse()?;
// Connect to Unix socket
let stream = UnixStream::connect(socket_path)?;
// Send CONNECT handshake
// write_all(format!("CONNECT {port}\n").as_bytes())
// Read response byte-by-byte until \n
// Validate starts with "OK "
// If any trailing data after \n, write to stdout
// Clone stream for the stdin->socket thread
let writer = stream.try_clone()?;
// Spawn stdin->socket thread
// On stdin EOF: writer.shutdown(Shutdown::Write)
// Main thread: socket->stdout loop
// Read until EOF, write to stdout
// Wait for stdin thread (optional, just exit)
Ok(())
}
Use std::os::unix::net::UnixStream, std::net::Shutdown, std::thread, std::io::{Read, Write, stdin, stdout}. No async.
- Step 4: Write unit tests for handshake
Use std::os::unix::net::UnixListener to create a mock server in a tempdir. Test:
-
Successful handshake (send CONNECT, get OK, verify stream works)
-
Failed handshake (server responds with error)
-
Trailing data after OK line is forwarded
-
Step 5: Run tests
Run: cd /home/david/git/vmsilo/vmsilo-tools && cargo test -p vmsilo-vsock-proxy
Expected: All tests pass.
- Step 6: Commit
cd /home/david/git/vmsilo
git add vmsilo-tools/vsock-proxy/ vmsilo-tools/Cargo.toml vmsilo-tools/Cargo.lock
git commit -m "Add vsock-proxy tool to vmsilo-tools workspace"
Task 3: Rewrite desktop.nix to use desktop-file tool, remove Python
Files:
- Modify:
modules/desktop.nix:97-175(remove processDesktopScript, pythonWithPyxdg; rewrite shell function)
The shell function process_desktop in mkDesktopEntries (line 224) currently calls python3 processDesktopScript. Replace with calls to vmsilo-desktop-file.
- Step 1: Add desktopFile variable
At the top of the let block in desktop.nix (after cfg = config.programs.vmsilo;), add:
desktopFile = "${cfg._internal."vmsilo-tools"}/bin/vmsilo-desktop-file";
- Step 2: Delete the Python script and interpreter
Remove lines 96-175 from desktop.nix:
-
The
processDesktopScriptdefinition (lines 96-172) -
The
pythonWithPyxdgdefinition (lines 174-175) -
Step 3: Rewrite the
process_desktopshell function
Replace the body of process_desktop (currently at lines 224-238) with:
process_desktop() {
local desktop="$1"
local pkg="$2"
# Get output filename
local basename
basename=$(basename "$desktop")
local outfile="$out/share/applications/vmsilo.${vm.name}.$basename"
# Check filters — skip NoDisplay=true and non-Application types
local nodisplay dtype
nodisplay=$(${desktopFile} get-key "$desktop" "Desktop Entry" NoDisplay) || nodisplay=""
if [ "$nodisplay" = "true" ]; then return; fi
dtype=$(${desktopFile} get-key "$desktop" "Desktop Entry" Type) || dtype=""
if [ -n "$dtype" ] && [ "$dtype" != "Application" ]; then return; fi
# Get original values for transformation
local icon name exec_val
icon=$(${desktopFile} get-key "$desktop" "Desktop Entry" Icon) || icon=""
name=$(${desktopFile} get-key "$desktop" "Desktop Entry" Name) || name=""
exec_val=$(${desktopFile} get-key "$desktop" "Desktop Entry" Exec) || exec_val=""
# Build transformation pipeline
{
if [ -n "$name" ]; then
${desktopFile} set-key "$desktop" "Desktop Entry" Name "${vm.name}: $name"
else
cat "$desktop"
fi
} \
| ${desktopFile} set-key - "Desktop Entry" Exec "vm-run ${vm.name} $exec_val" \
| ${desktopFile} set-key - "Desktop Entry" Icon "vmsilo.${vm.name}.''${icon:-unknown}" \
| ${desktopFile} set-key - "Desktop Entry" Categories "X-Vmsilo-${vm.name};" \
| ${desktopFile} set-key - "Desktop Entry" X-VmSilo-Color "${vm.color}" \
| ${desktopFile} filter-keys - "Desktop Entry" Type Version Name GenericName Comment Icon Exec Terminal Categories Keywords NoDisplay OnlyShowIn NotShowIn X-VmSilo-Color \
| ${desktopFile} filter-groups - "Desktop Entry" \
> "$outfile"
# ... icon copying logic continues unchanged below ...
The rest of the function (icon copying, lines 240-277) stays as-is — it uses $icon which is still set.
- Step 4: Verify no Python references remain in desktop.nix
Search for python, pyxdg, processDesktop in the file. Should be zero matches.
- Step 5: Run
nix fmt
Run: cd /home/david/git/vmsilo && nix fmt
- Step 6: Commit
git add modules/desktop.nix
git commit -m "Replace Python processDesktopScript with desktop-file tool"
Task 4: Rewrite scripts.nix to use vsock-proxy, remove Python
Files:
-
Modify:
modules/scripts.nix:229-312(rewrite mkChVsockConnectScript, remove pyProxy) -
Modify:
modules/scripts.nix:375-378(update stale comment about Python) -
Step 1: Rewrite
mkChVsockConnectScript
Replace the entire function (lines 229-312) with:
mkChVsockConnectScript =
vmName: port:
let
vsockProxy = "${cfg._internal."vmsilo-tools"}/bin/vmsilo-vsock-proxy";
in
pkgs.writeShellScript "vsock-connect-${vmName}-${toString port}" ''
VSOCK_SOCKET="/run/vmsilo/${vmName}/vsock.socket"
PORT=${toString port}
TIMEOUT=30
ELAPSED=0
# Wait for vsock socket to appear
while [ $ELAPSED -lt $TIMEOUT ] && [ ! -S "$VSOCK_SOCKET" ]; do
sleep 0.5
ELAPSED=$((ELAPSED + 1))
done
[ -S "$VSOCK_SOCKET" ] || { echo "Timeout: vsock socket not found" >&2; exit 1; }
# Retry until vsock port is ready (guest command listener may not be up yet).
ELAPSED=0
while [ $ELAPSED -lt $TIMEOUT ]; do
${vsockProxy} "$VSOCK_SOCKET" "$PORT" && exit 0
sleep 0.5
ELAPSED=$((ELAPSED + 1))
done
echo "Timeout waiting for VM ${vmName} vsock:${toString port}" >&2
exit 1
'';
This removes pyProxy and the pkgs.python3 dependency entirely.
- Step 2: Update the stale comment in vmRunScript
At line 376-377, change:
# -t5: wait up to 5s for response after stdin EOF (default 0.5s is too short
# for cloud-hypervisor proxy startup: Python interpreter + CONNECT handshake)
to:
# -t5: wait up to 5s for response after stdin EOF (default 0.5s is too short
# for cloud-hypervisor proxy startup and CONNECT handshake)
- Step 3: Verify no Python references remain in scripts.nix
Search for python, pyProxy in the file. Should be zero matches.
- Step 4: Run
nix fmt
Run: cd /home/david/git/vmsilo && nix fmt
- Step 5: Commit
git add modules/scripts.nix
git commit -m "Replace Python vsock-proxy with vmsilo-vsock-proxy tool"
Task 5: Replace socat CONNECT probe with vsock-proxy in USB service
Files:
-
Modify:
modules/services.nix:668-674(replace socat probe with vsock-proxy) -
Step 1: Replace the cloud-hypervisor CONNECT probe
In modules/services.nix, the USB service start script (inside mkUsbServices) has a block at lines 668-674 that checks if the guest vsock port 5002 is reachable. The cloud-hypervisor branch currently uses socat:
if echo "CONNECT 5002" | socat - UNIX-CONNECT:/run/vmsilo/${vm.name}/vsock.socket 2>/dev/null | head -1 | grep -q '^OK '; then break; fi
Replace the entire ${if vm.hypervisor == "crosvm" then ... else ...} block (lines 669-674) with:
${
if vm.hypervisor == "crosvm" then
"if ${pkgs.socat}/bin/socat -u OPEN:/dev/null VSOCK-CONNECT:${toString vm.id}:5002 2>/dev/null; then break; fi"
else
"if ${cfg._internal."vmsilo-tools"}/bin/vmsilo-vsock-proxy /run/vmsilo/${vm.name}/vsock.socket 5002 </dev/null 2>/dev/null; then break; fi"
}
The crosvm path stays as socat (kernel vsock, no CONNECT protocol). The cloud-hypervisor path uses vmsilo-vsock-proxy with stdin from /dev/null — handshake succeeds, stdin EOF triggers shutdown(Write), socket closes, exit 0.
- Step 2: Update the stale comment
At approximately line 666 in modules/services.nix, update the comment:
# Use socat probe for crosvm (kernel vsock), socat via unix socket for cloud-hypervisor
to:
# Use socat probe for crosvm (kernel vsock), vsock-proxy for cloud-hypervisor
- Step 3: Run
nix fmt
Run: cd /home/david/git/vmsilo && nix fmt
- Step 4: Commit
git add modules/services.nix
git commit -m "Use vmsilo-vsock-proxy for CH vsock probe in USB service"
Task 6: Remove sommelier entirely
Files:
- Delete:
packages/sommelier.nix - Modify:
flake.nix:170(remove sommelier package build) - Modify:
flake.nix:235(remove sommelier injection) - Modify:
modules/options.nix:230-239(removewaylandProxy.typeoption) - Modify:
modules/options.nix:250(update logLevel description) - Modify:
modules/options.nix:1018-1022(remove internal sommelier option) - Modify:
modules/lib/vm-config.nix:94,98(remove sommelier and waylandProxy args) - Modify:
rootfs-nixos/default.nix:12,15,39,41-42(remove sommelier/waylandProxy params, update comment) - Modify:
rootfs-nixos/guest/wayland.nix(remove sommelier codepath, simplify)
Important: All sommelier removal must happen in a single commit to avoid broken intermediate states (options.nix declares the option, vm-config.nix references it — removing one without the other breaks evaluation).
- Step 1: Delete
packages/sommelier.nix
rm packages/sommelier.nix
- Step 2: Remove sommelier from
flake.nix
Remove line 170:
sommelier = nixpkgs.legacyPackages.${system}.callPackage ./packages/sommelier.nix { };
Remove line 235 (the sommelier = line in the _internal block):
sommelier = pkgs.callPackage ./packages/sommelier.nix { };
- Step 3: Remove
waylandProxy.typeoption fromoptions.nix
Remove the entire waylandProxy.type option block at lines 231-239 (the type = lib.mkOption { type = lib.types.enum [...]; ... } block, but keep the waylandProxy = { wrapper and logLevel option).
The structure should remain:
waylandProxy = {
logLevel = lib.mkOption { ... };
};
- Step 4: Update
waylandProxy.logLeveldescription
At line 250, change:
description = "Log level for wayland-proxy-virtwl. Ignored for sommelier.";
to:
description = "Log level for wayland-proxy-virtwl.";
- Step 5: Remove internal sommelier option
Remove lines 1018-1022:
sommelier = lib.mkOption {
type = lib.types.package;
description = "sommelier package (injected by flake).";
internal = true;
};
- Step 6: Update
modules/lib/vm-config.nix
At line 94, change:
inherit (cfg._internal) wayland-proxy-virtwl sommelier;
to:
inherit (cfg._internal) wayland-proxy-virtwl;
Remove line 98 (waylandProxy = vm.waylandProxy.type;). Keep line 99 (waylandProxyLogLevel = vm.waylandProxy.logLevel;).
- Step 7: Update
rootfs-nixos/default.nix
Remove sommelier, from the function args (line 12).
Remove waylandProxy ? "wayland-proxy-virtwl", from the function args (line 15). Keep waylandProxyLogLevel ? "info", (line 16).
Update the comment at line 39 from:
# Pass wayland proxy packages and selection to the configuration
to:
# Pass wayland proxy package and config to the configuration
Remove these _module.args injections (lines 41-42):
_module.args.sommelier = sommelier;
_module.args.waylandProxy = waylandProxy;
Keep _module.args.waylandProxyLogLevel = waylandProxyLogLevel; (line 43).
- Step 8: Simplify
rootfs-nixos/guest/wayland.nix
Update the file header comment (line 3) from:
# - wayland-proxy-virtwl or sommelier (mutually exclusive)
to:
# - wayland-proxy-virtwl proxy service
Remove sommelier, and waylandProxy, from the function args (lines 9-10). Keep waylandProxyLogLevel,.
Remove the sommelier-related let variables (lines 17-19 — NOT line 16 which is kernelParamHelper):
isSommelier = waylandProxy == "sommelier";
proxyServiceName = if isSommelier then "sommelier" else "wayland-proxy-virtwl";
waylandDisplayName = if isSommelier then "wayland-0" else "wayland-1";
On the wayland-proxy-virtwl service (line 23), remove the lib.mkIf (!isSommelier) guard — the service is now unconditional.
Delete the entire sommelier service block (lines 63-94).
In vmsilo-session-setup (line 110), change:
requires = [ "${proxyServiceName}.service" ];
after = [ "${proxyServiceName}.service" ];
to:
requires = [ "wayland-proxy-virtwl.service" ];
after = [ "wayland-proxy-virtwl.service" ];
In the session setup script (line 128), change:
export WAYLAND_DISPLAY="${waylandDisplayName}"
to:
export WAYLAND_DISPLAY="wayland-1"
- Step 9: Run
nix fmt
Run: cd /home/david/git/vmsilo && nix fmt
- Step 10: Commit
git add -u packages/sommelier.nix flake.nix modules/options.nix modules/lib/vm-config.nix rootfs-nixos/default.nix rootfs-nixos/guest/wayland.nix
git commit -m "Remove sommelier wayland proxy entirely"
Task 7: Update README.md and CLAUDE.md
Files:
-
Modify:
README.md:383-389(simplify wayland proxy section) -
Modify:
README.md:590(remove sommelier reference) -
Modify:
CLAUDE.md(update if needed) -
Step 1: Simplify the Wayland Proxy section in README.md
Replace lines 383-389:
### Wayland Proxy
```nix
waylandProxy.type = "wayland-proxy-virtwl"; # Default: wayland-proxy-virtwl by Thomas Leonard
waylandProxy.type = "sommelier"; # ChromeOS sommelier (experiment, does not work currently)
waylandProxy.logLevel = "debug"; # Log level for wayland-proxy-virtwl (default: info)
with:
```markdown
### Wayland Proxy
```nix
waylandProxy.logLevel = "debug"; # Log level for wayland-proxy-virtwl (default: info)
- [ ] **Step 2: Update architecture section in README.md**
At line 590, change:
- Wayland proxy for GPU passthrough (wayland-proxy-virtwl or sommelier)
to:
- Wayland proxy for GPU passthrough (wayland-proxy-virtwl)
- [ ] **Step 3: Update CLAUDE.md if needed**
Check for any references to Python, sommelier, or `waylandProxy.type` in CLAUDE.md. Update:
- If `waylandProxy` option documentation mentions the type enum, remove it
- If sommelier is mentioned anywhere, remove it
- No Python-specific references are expected but verify
- [ ] **Step 4: Run `nix fmt`**
Run: `cd /home/david/git/vmsilo && nix fmt`
- [ ] **Step 5: Commit**
```bash
git add README.md CLAUDE.md
git commit -m "Update docs: remove sommelier references, simplify wayland proxy section"
Task 8: Build verification
Files: None (verification only)
- Step 1: Verify Rust tools build
cd /home/david/git/vmsilo/vmsilo-tools && cargo build && cargo test
Expected: All crates build, all tests pass.
- Step 2: Verify no Python references remain
Search the entire modules/ and rootfs-nixos/ directories for python, pyxdg, sommelier. Only valid hits should be in git history, not in current files.
cd /home/david/git/vmsilo
grep -r "python\|pyxdg\|sommelier" modules/ rootfs-nixos/ packages/ --include="*.nix" -l
Expected: No matches (or only false positives like comments mentioning removal).
- Step 3: Verify nix fmt is clean
cd /home/david/git/vmsilo && nix fmt -- --check .
Expected: No formatting changes needed.
- Step 4: Verify nix build succeeds
cd /home/david/git/vmsilo && git add -A && nix build .#
Expected: Build succeeds. Note: git add -A is needed because nix build uses git index for source filtering.