How do I build the system?

Table of Contents

Configure first if you haven't (see How do I configure the project?). For preset / output / parallelism details, see CMake setup.

1. Question

How do I build ORE Studio?

2. Answer

2.1. Day-to-day with compass

compass build resolves the checkout's preset from ORES_PRESET in .env and configures the build directory automatically on first use:

./compass.sh build                          # everything
./compass.sh build ores.iam.core            # specific target(s)
./compass.sh build site                     # friendly alias (deploy_site)
./compass.sh build --preset linux-clang-release-make   # preset override
./compass.sh build -j 4 --dry-run           # jobs override / print-only
./compass.sh build -k                       # keep going past a failed target

-k (--keep-going) passes the native tool's own option after cmake's --: -k 0 for ninja and -k for make. One build then reports every failing target, not only the first one. -k N stops ninja after N failures, as ninja's own -k does; make takes no count.

With ninja 1.13, a build first runs ninja -t recompact on the build directory. Ninja 1.13 cannot remove a damaged record from its deps log (ninja#2703): each load stops at that record and drops all later records, so every build is a full one. The recompact writes the log again without the damaged record, in less than a second. Other ninja versions build directly.

Friendly aliases: site → deploy_site, manual → deploy_manual, org-roam-db-sync → org_roam_db_sync; any raw cmake target is also accepted.

For documentation/agile targets, add --direct to skip cmake entirely and run the Emacs build script directly (no cmake/vcpkg — works in light environments): ./compass.sh build --direct site skills settings manual. See How do I deploy the site?.

Always build via compass build (not raw cmake --build) — never bypass it, even to "just quickly build one target". See Build locking below for why.

2.2. Build locking

This only matters on a host running several ORE Studio worktrees at once (each their own environment) — the common case for this repo's LLM-driven workflow, where multiple environments can be building in parallel. On a single-checkout machine with nothing else building, the lock is a no-op (you always get a free slot immediately).

Where it applies: concurrent, uncoordinated cmake --build invocations from different worktrees on the same host contend for the same CPUs and have been observed to corrupt shared library outputs when one build is killed mid-link while another links against the same target. A non-direct compass build automatically waits for one of two host-wide lock slots before invoking cmake, so at most two environments on the host build at once, each capped to that slot's -j:

./compass.sh build --status

reports each slot as held (with the holding environment's identity, pid, and start time) or free, plus a tail of the holder's build log, read from the holder's own checkout. ./compass.sh build status is the same command, and both answer while a build of this worktree runs. A free slot also reports the last holder recorded in this checkout, from tmp/ores-build-last-holder.<slot>, or says that no build has taken it yet. The abstract socket itself leaves nothing behind, so this sidecar is the only way to see a previous holder. Every build's combined stdout/stderr is also streamed live to a well-known path per slot, tmp/ores-build.log.<slot> under the checkout (overwritten each run) — tail -f it directly if you want to watch a build you (or another environment) kicked off rather than polling --status. The log lives in the checkout rather than in the host's /tmp so that a sandboxed reader, which mounts its own /tmp, can still read it. If both slots are busy, compass build prints a waiting message and blocks until one frees — this is expected, not a hang; let it wait rather than killing it or falling back to a raw cmake --build. --jobs overrides the slot's job count if you deliberately need a different value for one invocation. --dry-run does not take a lock or write a log.

The lock only works because the sandbox shares the host's network namespace: an abstract socket name is exclusive within that namespace, not across filesystems. A launcher can publish the host namespace in ORES_HOST_NETNS (the inode of the host's /proc/self/ns/net); compass build then refuses a sandbox whose namespace differs. Without that reference nothing inside the sandbox can prove the namespace is shared, so the build header prints the namespace it holds the slot in and marks the sharing unverified. Two builds that both report slot ='a' with different namespace inodes are the symptom of a private network namespace.

The actual cmake --build step (not the configure step) also runs inside its own memory-capped app-build-<env>.slice when a user systemd manager is available — deployed on demand from a checked-in dash-truncated template drop-in (same mechanism as How do I launch Claude Code inside a systemd scope?'s app-claude-.slice.d/), so an overrunning build is killed by its own cgroup OOM instead of pushing the whole machine into swap. Inspect with:

systemd-cgls --user-unit=app-build-<env>.slice
systemctl --user show app-build-<env>.slice -p MemoryMax -p CPUWeight

2.3. Direct CMake

Only for local single-environment work where you know no other worktree on the host is building concurrently — it bypasses the lock entirely, so simultaneous invocations from other environments can corrupt shared build outputs (see Build locking above). Prefer compass build otherwise.

cmake --build --preset <preset> [--target <target_name>]

3. Script

compass build (projects/ores.compass/src/compass.py) wrapping cmake --build --preset. Locking is _acquire_build_lock=/ =BUILD_LOCK_SLOTS in the same file — two slots (each -j3), held by binding the abstract AF_UNIX socket names @ores-build-slot-a and @ores-build-slot-b. Abstract names live in the network namespace, not on a filesystem, so every worktree and every bubblewrap sandbox on the host contends for the same two slots; a lock file in /tmp would be private to each sandbox. Two environments build at once and six cores are in flight at most. The memory cap comes from _ensure_build_slice_deployed=/=_build_slice_name in the same file, syncing projects/ores.compass/src/systemd/app-build-.slice.d/50-limits.conf.

4. Tested by

CI builds every preset on every push.

5. See also

Emacs 29.3 (Org mode 9.6.15)