ZEUGHAUS-INSTALL(8) Zeughaus Manual

NAME

zeughaus-install - build, install and run zeughaus from source

SYNOPSIS

spacetime start
spacetime publish --server local zeughaus --module-path zeughaus-module
cargo run -p zeughaus-runner
cargo run -p zeughaus
deploy/install.sh

DESCRIPTION

A zeughaus session is three processes: the store (SpacetimeDB with the zeughaus-module published into it), which holds the graph; the runner (zeughaus-runner), which executes the graph and owns the terminals; and any number of editors (zeughaus), which edit and display but never execute. They start in that order. See zeughaus-architecture(7) for how they talk.

This page goes from an empty machine to a running session, first by hand from the checkout, then installed as systemd user services. It covers Linux, where zeughaus is developed (Wayland, systemd). Every command runs from the root of the zeughaus checkout unless a section says otherwise.

CHECKOUT LAYOUT

The workspace depends on the QUIC transport weida by path (weida = { path = "../weida/crates/weida" } in the workspace Cargo.toml), so both repositories are checked out next to each other. This page uses ~/src:

~/src/
├── zeughaus/     this workspace; every command below runs here
└── weida/        the QUIC transport, ../weida as seen from zeughaus/
mkdir -p ~/src
cd ~/src
git clone https://github.com/tuco86/zeughaus
git clone https://github.com/tuco86/weida
cd zeughaus

REQUIREMENTS

Rust
A stable toolchain through rustup. No rust-toolchain file pins one. The workspace uses edition 2024 and declares no rust-version; weida declares rust-version = "1.88", the highest floor either repository states. The store module and the browser build compile to WebAssembly: rustup target add wasm32-unknown-unknown.
SpacetimeDB CLI 2.10
The spacetime binary runs the store and publishes the module. Its version must share major and minor with the client SDK, which the workspace pins as spacetimedb-sdk = "2.10" (locked at 2.10.0); the module is built against spacetimedb = "2.10". The installed unit runs it from ~/.local/bin/spacetime, so install it there. With the CLI present, spacetime version install 2.10.0 --use selects the matching version and spacetime --version shows it.
weida
The sibling checkout ../weida from CHECKOUT LAYOUT.
PipeWire headers and libclang
zeughaus-capture links the system libpipewire-0.3 and libspa-0.2 through bindgen on Linux, so the build host needs their development headers and libclang.
xdg-desktop-portal
Screen capture (capture.screen) goes through the portal's ScreenCast and Screenshot interfaces when WAYLAND_DISPLAY is set. Needed on the machine that runs the runner, and only for that node.
LM Studio (optional)
The llm.chat node talks to LM Studio's OpenAI-compatible endpoint, by default http://localhost:1234/v1 (its base_url setting); an empty model setting takes the first loaded model. Without LM Studio the other nodes work and llm.chat reports that it cannot reach it.
ffmpeg (optional)
Only zeughaus ctl <socket> record uses it, in the headless editor built with the remote feature. Nothing in a normal session calls it.
trunk and a WebGPU browser (optional)
Only for the browser build: cargo install trunk wasm-bindgen-cli, and a browser with WebGPU. See THE BROWSER BUILD.

BUILD

cargo build -p zeughaus-runner -p zeughaus

zeughaus-module is excluded from the native workspace; spacetime publish builds it for its WebAssembly target in the next step. The dev profile optimizes dependencies (opt-level = 3) and keeps the workspace crates at opt-level = 1, so a debug build draws large graphs without stutter.

PUBLISH THE MODULE

The module is published into a running store. Start the store in a terminal of its own and leave it running:

spacetime start

It listens on 0.0.0.0:3000, its default --listen-addr. In a second terminal, publish the module as the database zeughaus, the session every process joins when it is given no join argument:

spacetime publish --server local zeughaus --module-path zeughaus-module

Publish again after every change to the schema in zeughaus-module/src. The Rust bindings both binaries use are generated from the module and checked in; after a schema change, regenerate them before building:

spacetime generate --lang rust --out-dir zeughaus-sync/src/module_bindings --module-path zeughaus-module

A fresh checkout needs no generate: the checked-in bindings match the checked-in module.

RUN BY HAND

Three processes, in this order, each in its own terminal. Do not run them next to the installed services (see TROUBLESHOOTING).

  1. The store, already running from the previous section:

    spacetime start
    
  2. The runner. It executes the graph and owns the terminals:

    cargo run -p zeughaus-runner
    

    At start it prints, on standard error:

    [runner] terminal locale (...): LANG=...
    [runner] state dir /home/you/.local/state/zeughaus
    [runner] session 10.0.0.8:3000/zeughaus -> http://127.0.0.1:3000 / zeughaus
    [runner] weida endpoint weida://sha256:<fingerprint>@127.0.0.1:<port>/
    [runner] trusted client sha256:<fingerprint>
    

    The session line carries the token another machine joins with (this machine's LAN address, port 3000, database zeughaus). The weida endpoint line is the URL the runner announces to editors through the store, and the one zeughaus-runner trigger and zeughaus-runner hold take (see zeughaus-runner(1)). On first start the runner creates runner.pem and client.pem in its state directory (--state-dir, else ZEUGHAUS_STATE_DIR, else $XDG_STATE_HOME/zeughaus, else ~/.local/state/zeughaus).

  3. The editor, as many as you like:

    cargo run -p zeughaus
    

    It prints [stdb] session token: <token> when it reached the store. The token is also in the command palette (Ctrl+Space) as a copy command. It finds the runner through the store and dials its endpoint with the client.pem the runner left in the shared state directory. The tab bar shows one section per connected runner. See zeughaus(1).

INSTALL AS USER SERVICES

deploy/install.sh

Installs this checkout as the user's running zeughaus (Linux, systemd user manager). cargo and spacetime must be on PATH. In order, it:

  1. runs cargo install --locked for zeughaus and zeughaus-runner into ~/.cargo/bin ($CARGO_INSTALL_ROOT/bin or $CARGO_HOME/bin when set). This happens first, so an already running stack keeps serving while they compile.
  2. copies zeughaus-store.service and zeughaus-runner.service to ~/.config/systemd/user/, writes the desktop entry ~/.local/share/applications/net.doodleshnookie.Zeughaus.desktop with the binary directory filled in, and installs the icon as ~/.local/share/icons/hicolor/scalable/apps/net.doodleshnookie.Zeughaus.svg. It refreshes an existing icon-theme.cache in that hicolor directory (never creates one) and runs kbuildsycoca6 when present, then systemctl --user daemon-reload and enables both units.
  3. starts zeughaus-store and waits up to ten seconds for spacetime server ping local to answer; if it does not, it exits and points at the store's journal.
  4. publishes the module: spacetime publish --server local zeughaus --module-path zeughaus-module, without -y.
  5. reloads zeughaus-runner if it is active, else starts it.
  6. sends SIGUSR1 to every running zeughaus whose /proc/<pid>/exe is the installed binary and which handles that signal; each one reopens as it was. An editor built before the handler existed is left alone with the message editor <pid> cannot restart itself; close and reopen it.

The units hard-code their binaries: zeughaus-store runs ~/.local/bin/spacetime and zeughaus-runner runs ~/.cargo/bin/zeughaus-runner. With a different install root, the desktop entry follows it but the units do not.

The units

zeughaus-store.service
spacetime start --non-interactive: listens on 0.0.0.0:3000 like a store started by hand, so other machines can join, and fails instead of asking when the port is taken. Restart=on-failure after 2 s. Wanted by default.target.
zeughaus-runner.service
~/.cargo/bin/zeughaus-runner, with Requires= and After= on the store. A runner that cannot connect exits, and Restart=on-failure starts it again after 3 s, which covers a store still starting after login. KillMode=process: every terminal lives in a shim process the runner forked, and these share the unit's cgroup; with this setting a stop or restart ends only the runner, and the next one reattaches the shims, so shells and job runs survive. ExecReload sends SIGUSR1: the runner saves <state-dir>/workspace.json and executes its binary again at the same path under the same PID, so a fresh cargo install is picked up without touching a terminal. PATH is set to ~/.local/bin, ~/.cargo/bin, /usr/local/bin, /usr/bin and /bin; shells started in terminals are login shells and build their own. Wanted by default.target.

Desktop entry

The editor is started from the application menu as Zeughaus. The entry's file name and its StartupWMClass are the window's Wayland app id, net.doodleshnookie.Zeughaus; that is how the compositor finds the entry and its icon, because the window itself cannot send one on Wayland. The entry runs the editor without join, so it joins the local session.

Logs

journalctl --user -u zeughaus-runner
journalctl --user -u zeughaus-store

Add -f to follow. Editors started from the menu are not units.

JOINING FROM ANOTHER MACHINE

Machine A runs the store and a runner; machine B runs an editor (or another runner). Below, 10.0.0.8 stands for A's LAN address.

The store on A already listens on all interfaces (0.0.0.0:3000, by hand and as a unit). The runner does not: its weida listener binds loopback with an OS-chosen port by default, and it announces the address it bound, so an editor on B could reach the store but not the runner (no values, frames or terminals). Start A's runner on a routable address with a fixed port:

cargo run -p zeughaus-runner -- --feed-addr 10.0.0.8:7443

0.0.0.0 does not help: an unspecified bind is announced as 127.0.0.1. For the installed runner, override ExecStart in a drop-in opened with systemctl --user edit zeughaus-runner:

[Service]
ExecStart=
ExecStart=%h/.cargo/bin/zeughaus-runner --feed-addr 10.0.0.8:7443

The runner only accepts clients whose certificates it trusts: its own client.pem plus every clients/*.pem in its state directory, read at start. The editor never creates a key. Copy A's client.pem into B's state directory:

mkdir -p ~/.local/state/zeughaus
scp 10.0.0.8:.local/state/zeughaus/client.pem ~/.local/state/zeughaus/client.pem
chmod 600 ~/.local/state/zeughaus/client.pem

Then join with the token A printed ([runner] session ... or [stdb] session token: ...), of the form host[:port]/database; the port defaults to 3000, and a bare database means the store on 127.0.0.1:3000:

zeughaus join 10.0.0.8:3000/zeughaus

zeughaus-runner join 10.0.0.8:3000/zeughaus adds a runner on B to the same session; it keeps its own state directory and executes only the graphs that name it.

B's editor needs A's port 3000 (TCP) and the runner's --feed-addr port (QUIC, which is UDP).

THE BROWSER BUILD

cd zeughaus
trunk serve

Run from the zeughaus/ crate directory, not the workspace root. It serves on http://127.0.0.1:8080 (zeughaus/Trunk.toml). The browser needs WebGPU: iced_nodegraph has no WebGL fallback. The browser editor edits locally: it does not connect to a store, so it neither sees nor changes a session, and no runner computes what it shows. The native-only plugins (capture, db, record, llm) are not in it, and it draws no terminals.

UPDATING

git pull
cd ../weida && git pull && cd ../zeughaus
deploy/install.sh

install.sh is also the update: it installs over the running binaries, then reloads the runner (same PID, terminals stay) and restarts every editor of this install in place. To confirm the runner picked up the new binary:

readlink /proc/$(systemctl --user show -p MainPID --value zeughaus-runner)/exe

The path no longer ends in (deleted) once it has.

The publish step never passes -y. When a schema change needs the data cleared, the publish refuses and the script stops there, before the runner and the editors are touched. Clearing the database deletes every graph in the session; that decision is the user's. Once it is settled, for example with spacetime publish --server local zeughaus --module-path zeughaus-module --delete-data, run install.sh again.

TROUBLESHOOTING

A second store fails on the port
Port 3000 is held by another spacetime start: one started by hand next to zeughaus-store, or the other way round. The unit uses --non-interactive and fails at once; install.sh then stops at "store". Stop the one started by hand.
A second runner on the same state directory fights over the shims
Terminals live in shims under <state-dir>/terminals/, and a runner reattaches every shim it finds there. Never run a runner by hand next to the installed one. A second runner on one machine needs its own --state-dir.
Runner or editor cannot talk to the store on connect
The spacetime host and spacetimedb-sdk differ in major or minor version, and the wire format does not match. spacetime --version must report 2.10; spacetime version install 2.10.0 --use fixes it.
The runner exits at once
[runner] cannot connect to http://127.0.0.1:3000 / zeughaus: ... (start it with `spacetime start`). The runner refuses to start without a store. Start the store, and publish the module if the database does not exist yet. As a unit it retries every 3 s on its own.
The editor edits a graph nobody runs
[stdb] cannot reach ... -- editing locally. Without a store the editor edits a local scratch graph under the tab bar section Local: nothing computes it, the status bar says so, and it is gone on close unless saved as a .zgh file with Ctrl+S. Start the store and restart the editor.
The editor shows no values from a runner
[editor] no client identity in ...: no runner ever ran on this machine and no client.pem was copied here. See JOINING FROM ANOTHER MACHINE.
The Wayland window has no icon
winit cannot hand a window icon to a Wayland compositor; the compositor finds it through the desktop entry named after the app id net.doodleshnookie.Zeughaus. Install the entry and the icon with deploy/install.sh. On KDE, a stale menu cache updates after kbuildsycoca6 or the next login.

FUTURE DIRECTIONS

Not built: a browser editor that syncs with the store. The wasm build edits locally only.

SEE ALSO

zeughaus(1), zeughaus-runner(1), zeughaus-architecture(7), zeughaus-files(5), deploy/install.sh, deploy/zeughaus-store.service, deploy/zeughaus-runner.service, deploy/net.doodleshnookie.Zeughaus.desktop, zeughaus/Trunk.toml, CLAUDE.md

install(8)source