Building from source
Building from source
The binary embeds the web build. Browser renderers are fetched separately from the configured asset mirror.
web/(Svelte, Skeleton, CodeMirror 6, Loro) uses bun and vite.crates/(server, CLI and companion, as internal sub-crates) uses cargo.
Renderer implementations live in wasm-* repositories. The browser uses pinned WebAssembly artifacts; the production server binary does not link native document renderers. Some renderer crates remain dev-dependencies for fixtures and tests. The web build writes to web/dist, which the binary embeds.
make web # pages from web/
node tools/assets/pins.mjs fetch # pinned browser renderers
make build # dist/librepaper with embedded pages
make install # to ~/.local/bin (override PREFIX= or BINDIR=)
make test # rustfmt, clippy and test suite
tools/test/suite external # Quarto/R/Python and local-service integrations
Build needs bun and Node.js. Rust builds for x86_64-unknown-linux-gnu also require mold, configured in .cargo/config.toml. Browser renderers are fetched from exact tags and SHA256 digests in assets.lock. Update a renderer with node tools/assets/pins.mjs update wasm --repo wasm-markdown --tag v0.2.0. Then review the lockfile diff.
The four browser modules (markdown, bibliography, citations, typst) are not embedded. node tools/assets/pins.mjs fetch fetches them to web/wasm/, which is ignored. tools/assets/mirror publish publishes to the asset mirror at wasm/<sha256>/<module>. The SHA256 comes from assets.lock. The same lock pins LaTeX at latex/<sha256>/. The server passes these URLs to browsers using [assets].mirror from TOML, or the built-in project mirror when omitted. See asset mirrors.
make demo builds the static site and runs the deployment services from deploy/compose.yaml with an app image built from this checkout. It requires Docker Compose 2.24.4 or later. The demo uses the isolated librepaper-demo Compose project and loopback ports 8081 (app) and 8082 (site). PORT=, SITE_PORT=, and DEMO_PROJECT= can change these. Its named volumes persist after Ctrl-C and make demo-stop. Use make demo-wipe to explicitly delete the demo volumes. These targets do not affect native make serve or make wipe.
The demo serves operator graphs at http://admin.localhost:8081/ with the password librepaper-demo-only-password. The first run compiles both the container server and the host companion; subsequent runs reuse their build caches. Custom DEMO_PROJECT names must start with librepaper-demo.
The default container-ready configs are tools/dev/demo/config.toml and tools/dev/demo/config-oauth.toml. GitHub sign-in comes from tools/dev/keys.yaml when sops can decrypt it, or from inherited GitHub client credentials. Otherwise the demo starts without sign-in. That file holds only the development OAuth app, so the demo never loads production secrets. CONFIG=path/to/config.toml selects a config mounted unchanged into the app container, so custom files must use container paths and bind the app to 0.0.0.0:8080. The LaTeX mirror check still runs at MIRROR= (default ../wasm-latex/mirror). The host companion’s LIBREPAPER_SERVER is pinned to the local app origin so tray Settings and approvals return to this app. A companion already running before the demo starts is left alone.
Workspace
crates/ holds internal sub-crates and the librepaper facade. An edit recompiles the crate it touches and its dependents.
cargo check -p librepaper-engine # inner loop: one crate
cargo nextest run -p librepaper-engine # its tests
make test # everything, before handing work back
Test suites
make test runs the ordinary Rust and web checks, leaving out PostgreSQL-gated cases and benchmarks. Run tools/dev/db test to run the gated suite in a throwaway Docker container. This is the same command CI runs. For a focused single case, point LIBREPAPER_TEST_POSTGRES_URL at a dedicated disposable database and select one test by name:
LIBREPAPER_TEST_POSTGRES_URL='postgresql://postgres:password@127.0.0.1:5432/librepaper_test' \
cargo test -p librepaper-server --lib server::history_frontier_tests::delegated_agent_bearer_stops_working_after_session_revocation -- --ignored --test-threads=1 --exact
The log and catalogue tests live in librepaper-engine, the others in librepaper-room and librepaper-server. Change -p to match.
The selected case may clear tables, so do not use a shared or production database. Shared PostgreSQL fixtures fail when explicitly run without their required database setting.
Keep benchmarks opt-in by selecting one ignored test by its function name. Do not run all ignored library tests together. For example, the release throughput measurement uses the destructive benchmark database setting and a bounded override when a short diagnostic is intended:
LIBREPAPER_BENCHMARK_POSTGRES_URL='postgresql://postgres:password@127.0.0.1:5432/librepaper_bench' \
LIBREPAPER_BENCHMARK_SECONDS=30 \
cargo test -p librepaper --test storage_benchmarks typing_throughput_release_benchmark --release -- --ignored --nocapture --test-threads=1
The short run is diagnostic only. Use the default 600-second run for release
acceptance. Run the relevant benchmark before a performance release, and run
the isolated recovery drill before a storage or recovery release. The throughput
benchmark truncates its configured database. The socket benchmark instead
uses LIBREPAPER_BENCH_POSTGRES_URL. The mixed workload and its PostgreSQL
setup are documented in the frugal_mixed_bench test at crates/librepaper/tests/frugal_mixed_bench.rs.
The isolated backup/restore recovery drill has its own disposable-database
checks and exact prerequisites in tools/test/backup/README.md.
Run tools/test/suite backup for the complete end-to-end test.
The real Quarto PDF preview test is still unfinished CI coverage. Its current symptom is repeated CI runner termination. The cause is unconfirmed. To reproduce it, install Quarto and a PDF engine, then run the focused ignored test:
cargo test -p librepaper-companion --lib real_quarto_pdf_preview_publishes_complete_pdf_bytes -- --ignored --test-threads=1
tools/test/suite external is the current manual coverage path for Quarto/R/Python
and local-service integrations. The preview can return to CI after the
termination cause is isolated and the test has a bounded, reliable CI run.
Web tests
unit/runs isolated production modules under Node without Chromium, a local service, or a built compiler artifact.integration/crosses module boundaries or exercises a real WASM artifact, local companion, renderer, compiler adapter, or example document.browser/drives the built Svelte application or a real browser page with Chromium.fixtures/holds checked-in inputs shared by tests. The Typst corpus is used by both the PDF and viewer checks.
Entry points are the Bun scripts in web/package.json and tools/test/suite.
Browser selection:
- Set
LIBREPAPER_CHROMIUMto an executable path to pick a browser. If it is unset, the resolver looks forchromium,chromium-browser,google-chrome, orgoogle-chrome-stableonPATH. - Running
tools/test/suite browserortools/test/suite smokewithout a usable browser fails the requested suite. - Browser-selecting fixtures are pinned to Chromium even when
BROWSERis set. - The LaTeX browser fixture can be run directly with
firefox,chromium, orbothfor standalone cross-browser coverage.
slow-subscriber-recovery prerequisites:
- Chromium on
PATH - installed web dependencies
- built
target/debug/librepaperorLIBREPAPER_TEST_BINARY LIBREPAPER_TEST_POSTGRES_URLpointing to an administrative PostgreSQL database. The role must be able to create and drop databasesLIBREPAPER_TEST_PSQLandLIBREPAPER_TEST_PSQL_URLwhenpsqlruns in a container
Run with cd web && bun run check:recovery.
The interface
Pages use Skeleton on Tailwind 4. Skeleton provides buttons, cards, inputs, tables, dialogs, tooltips and toasts. web/src/styles/theme.css colours everything from four colours. The palette is written once. make test enforces three rules:
- Colours and sizes come from theme, not hex values or arbitrary Tailwind sizes.
- A control is a component. There is one
IconButton. - Layout comes from
Page,StackandRowand is assembled, not measured.
Agent highlights are drawn on the document origin, not in this stylesheet. Identifying colours in shared sessions are sent over the wire, not chosen locally.