Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
4f72da1
fix(sandbox): clear cargo's progress line before the next line (3.5.0…
KeithRaghubar Oct 22, 2026
4cae532
fix(sandbox): apply cpu_quota and mem_limit to the build container (3…
KeithRaghubar Oct 23, 2026
9150440
fix(update): keep the cursor in place on the end-of-run terminal rese…
KeithRaghubar Oct 23, 2026
1d54870
fix(progress): reserve the bar row without scrolling under a saved cu…
KeithRaghubar Oct 23, 2026
b0e9f73
docs(design): note plain-mode progress as the one ui() narration exce…
KeithRaghubar Oct 23, 2026
bc7ae78
docs(readme): open with a demo and a "What SysForge touches" section …
KeithRaghubar Oct 23, 2026
0887aab
dev: make readme-preview renders Markdown as GitHub does and opens it…
KeithRaghubar Oct 23, 2026
80ccb5b
docs(readme): one paragraph per section, detail moved into guides (3.…
KeithRaghubar Oct 23, 2026
51d542b
fix(fs_provision): grant a per-user ACL until the sysforge group appl…
KeithRaghubar Oct 23, 2026
384f599
fix(build): report an unwritable build record as an error (3.5.0-B135)
KeithRaghubar Oct 23, 2026
541e4b7
fix(init): the first-install notice clears after a standalone reconfi…
KeithRaghubar Oct 23, 2026
f647868
fix(lock): keep the stage lock usable by the user after a sudo run (3…
KeithRaghubar Oct 23, 2026
c8ed827
docs(roadmap): file 3.5.0-B138, a pre-install failure leaves a stale …
KeithRaghubar Oct 23, 2026
a0dbaa7
dev(demo): record the docs' recordings in a fresh Arch container (3.5…
KeithRaghubar Oct 23, 2026
50f7a38
docs(roadmap): file 3.5.0-DEV21, an end-to-end test tier on a fresh s…
KeithRaghubar Oct 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions DESIGN.md

Large diffs are not rendered by default.

21 changes: 18 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.PHONY: all help surface check-surface install-hooks dev venv build install dev-install dev-uninstall clean distclean test test-x lint typecheck coverage coverage-ratchet coverage-ratchet-update man \
dev-deps dev-deps-core dev-deps-pkg dev-deps-container dev-deps-release dev-deps-list \
.PHONY: all help surface check-surface install-hooks dev venv build install dev-install dev-uninstall clean distclean test test-x lint typecheck coverage coverage-ratchet coverage-ratchet-update man demo-gif readme-preview \
dev-deps dev-deps-core dev-deps-pkg dev-deps-container dev-deps-release dev-deps-docs dev-deps-list \
lint-py lint-sh \
bench-completions check-shipped check-personal check-standards check-bump check-standards-at next-id next-bump design check-design pre-release print-gates audit \
roadmap-table check-roadmap-table roadmap-view \
Expand Down Expand Up @@ -46,9 +46,12 @@ DEV_DEPS_VM = qemu-desktop edk2-ovmf gtk-vnc socat
DEV_DEPS_CONTAINER = podman
# Release: GPG-signed commit/tag/tarball + the GitHub release upload.
DEV_DEPS_RELEASE = gnupg github-cli
# Terminal recordings: demo-gif (plus the container tier's podman); the tapes
# set Fira Code as their font.
DEV_DEPS_DOCS = vhs ttf-fira-code

DEV_DEPS_ALL = $(DEV_DEPS_CORE) $(DEV_DEPS_PKG) $(DEV_DEPS_VM) \
$(DEV_DEPS_CONTAINER) $(DEV_DEPS_RELEASE)
$(DEV_DEPS_CONTAINER) $(DEV_DEPS_RELEASE) $(DEV_DEPS_DOCS)

# $(call pacman_needed,<packages>) — install only if the set isn't already
# satisfied, so a complete environment never prompts for sudo. `pacman -Qq`
Expand Down Expand Up @@ -119,6 +122,9 @@ dev-deps-container: ## Install the container test tier (podman)
dev-deps-release: ## Install the release tier (gnupg, github-cli)
$(call pacman_needed,$(DEV_DEPS_RELEASE))

dev-deps-docs: ## Install the recordings tier (vhs, Fira Code)
$(call pacman_needed,$(DEV_DEPS_DOCS))

# Show what each tier needs and whether it is installed. Read-only, no sudo —
# run this before dev-deps to see what a full install would add.
dev-deps-list: ## Show each tier's deps and whether they are present
Expand All @@ -129,6 +135,7 @@ dev-deps-list: ## Show each tier's deps and whether they are present
@printf '%-12s %s\n' vm '$(DEV_DEPS_VM)'
@printf '%-12s %s\n' container '$(DEV_DEPS_CONTAINER)'
@printf '%-12s %s\n' release '$(DEV_DEPS_RELEASE)'
@printf '%-12s %s\n' docs '$(DEV_DEPS_DOCS)'
@echo
@for p in $(DEV_DEPS_ALL); do \
if pacman -Qq "$$p" >/dev/null 2>&1; then s=installed; else s=MISSING; fi; \
Expand Down Expand Up @@ -407,6 +414,14 @@ man: ## Render the man page from the scdoc source
--out man/sysforge.1.scd
scdoc < man/sysforge.1.scd > man/sysforge.1

# Real commands in a fresh Arch container (tools/demo/); TAPE= re-records just one.
demo-gif: ## Re-record the terminal recordings in docs/assets/ (TAPE=, needs vhs + podman)
bash tools/record-demo.sh $(TAPE)

# GitHub's own renderer; FILE= previews any other Markdown file (a guide, a concept page).
readme-preview: ## Render README.md (or FILE=) as GitHub does and open it in a browser
bash tools/readme-preview.sh $(FILE)

clean: ## Remove build artefacts
rm -rf dist/ __pycache__/ *.egg-info/ .pytest_cache/ .ruff_cache/ .coverage coverage.json
rm -f man/sysforge.1.scd
Expand Down
437 changes: 46 additions & 391 deletions README.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ path = [
"tools/*.sh",
"tools/vm/**",
"tools/container/**",
"tools/demo/**",
"**.md",
"**.toml",
"**.lock",
Expand Down
34 changes: 34 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,46 @@ canonical ordering.
<!-- BEGIN roadmap-table (generated by tools/gen_roadmap_table.py — do not edit by hand; run `make roadmap-table`) -->
| ID | Item | Priority | Effort | Bump |
|----|------|----------|--------|------|
| `3.5.0-DEV21` | an end-to-end test tier that runs the documented paths on a fresh system | high | large | patch |
| `3.5.0-B138` | a verb that fails before installing anything leaves a stale sentinel | med | medium | patch |
<!-- END roadmap-table -->

### Features

### Bugs

- **`3.5.0-B138` — a verb that fails before installing anything leaves a stale sentinel.**
`verbs.runner.run_verb` wraps all of a `requires_sentinel` verb's `execute` in
`stage_sentinel.sentinel_scope`, which keeps the record on any exception. So `sysforge build`
failing on a missing `pkgctl` (a `RuntimeError` raised before any install) leaves
`stage = "build"` behind, and the next run refuses to start ("Stale stage-in-progress sentinel
present"): a recovery prompt at a terminal, a hard stop for a timer or cron run. Found by the
demo-recording container on 2026-10-23. Two shapes to choose between: arm the sentinel at the
first mutation (the install) rather than at `execute` entry, or move pre-mutation checks such
as the `pkgctl` probe into `pre_check`, which runs before the scope opens. The first fixes the
class; the second only the cases found so far.
*Priority: med · Effort: medium · Bump: patch* — blocks unattended runs after an ordinary,
harmless failure.

### Developer tooling

- **`3.5.0-DEV21` — an end-to-end test tier that runs the documented paths on a fresh system.**
The unit suite stubs out exactly where six bugs found on 2026-10-23 lived: root versus the user
and group membership that only applies at the next login (`3.5.0-B134`, `B135`, `B137`), state one
command leaves for the next (`B136`, whose test called `mark_done` directly, a state the real
code never produced) and real terminal behaviour (`B132`, `B133`). The demo-recording container
(`tools/demo/`, `make demo-gif`) caught them by following the README. Add `make e2e`: shell
scenarios run as an unprivileged user in that same image, asserting on exit codes, `pacman -Qi`,
the state files and leftover sentinels (not vhs). Start with four: the Quick start as one login
session (setup, AUR build, `update`); a repo package's opt-in; flag drift after a profile change;
and a failure before install leaving no sentinel (`3.5.0-B138`'s regression test once fixed).
Prefer local PKGBUILDs, with at most one networked scenario, so outages don't flake the tier. CI
already runs in a fresh `archlinux:base-devel` container, so the same scripts can run there
directly without podman; wire `make e2e` into `pre-release` and `docs/RELEASE-CHECKLIST.md`. A
cheaper complement for the terminal class: a test helper that replays the progress bar's output
through a terminal model and asserts where the cursor ends up, under both xterm's and xterm.js's
saved-cursor rules, which would have caught B132 and B133 in the unit suite.
*Priority: high · Effort: large · Bump: patch* — the only tier that exercises privilege
boundaries, multi-command sequences and the documented first run, where users meet sysforge.

### Open questions
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Releases are GPG-signed end to end with the maintainer key:
The AUR `sysforge` package declares the maintainer key in `validpgpkeys` and
lists the detached signature as a source, so `makepkg` verifies the maintainer
signature at install time and aborts on mismatch. To verify manually, see the
*Verifying releases* section of [README.md](README.md). The maintainer public key
[verifying releases](docs/guides/verifying-releases.md) guide. The maintainer public key
(and its fingerprint) lives at [`keys/sysforge.asc`](keys/sysforge.asc).

Signing applies from the first release cut after this policy landed; earlier
Expand Down
Binary file added docs/assets/drift.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
40 changes: 40 additions & 0 deletions docs/assets/drift.tape
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# drift.tape -- the drift guide's recording: after a profile change, update
# finds the package built with the old flags and rebuilds it.
# Render with `make demo-gif`, which starts the container this tape drives.

Output docs/assets/drift.gif
Require podman

Set Shell "bash"
Set FontFamily "Fira Code"
Set FontSize 15
Set Width 1200
Set Height 640
Set Padding 24
Set Theme "GitHub Dark"
Set TypingSpeed 35ms
Set Framerate 20
Set WaitTimeout 3m

# Off camera: build a package with the shipped default profile (standard),
# then switch the default to optimized.
Hide
Type "podman exec -it -u demo -w /home/demo $DEMO_CONTAINER bash" Enter
Sleep 1s
Type "sysforge build cbonsai -si" Enter
Wait+Screen /\[a\]ccept/
Type "a"
Wait+Line /\$ ?$/
Type `sudo sed -i 's/^profile = "standard"/profile = "optimized"/' /etc/sysforge/profiles.toml; PS1='$ '; clear` Enter
Sleep 1s
Show

Sleep 500ms
Type `grep '^profile' /etc/sysforge/profiles.toml # was "standard" when cbonsai was built` Sleep 300ms Enter
Sleep 2500ms
Type "sysforge update --dry-run --explain-drift" Sleep 400ms Enter
Wait+Line /^\$ ?$/
Sleep 3500ms
Type "sysforge update --rebuild-on-flag-drift --yes" Sleep 400ms Enter
Wait+Line /^\$ ?$/
Sleep 4s
Binary file added docs/assets/quickstart.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
44 changes: 44 additions & 0 deletions docs/assets/quickstart.tape
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# quickstart.tape -- the README recording: Quick start steps 4 and 5 on a fresh
# system, an AUR package built with the default profile and then maintained.
# Render with `make demo-gif`, which starts the container this tape drives
# ($DEMO_CONTAINER, set up to the end of Quick start step 3).

Output docs/assets/quickstart.gif
Require podman

Set Shell "bash"
Set FontFamily "Fira Code"
Set FontSize 15
Set Width 1200
Set Height 720
Set Padding 24
Set Theme "GitHub Dark"
Set TypingSpeed 35ms
Set Framerate 20
Set WaitTimeout 3m

Hide
Type "podman exec -it -u demo -w /home/demo $DEMO_CONTAINER bash" Enter
Sleep 1s
Type "PS1='$ '; clear" Enter
Sleep 500ms
Show

Sleep 500ms
Type "sysforge build cbonsai -si" Sleep 400ms Enter
# The first build of an AUR package asks for a review of its PKGBUILD.
Wait+Screen /\[a\]ccept/
Sleep 1500ms
Type "v"
Sleep 3s
Type "q"
Wait+Screen /\[a\]ccept/
Sleep 1s
Type "a"
Wait+Line /^\$ ?$/
Sleep 1500ms
Type "cbonsai -p" Sleep 300ms Enter
Sleep 3s
Type "sysforge update" Sleep 300ms Enter
Wait+Line /^\$ ?$/
Sleep 4s
Binary file added docs/assets/repo-build.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 37 additions & 0 deletions docs/assets/repo-build.tape
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# repo-build.tape -- the getting-started guide's recording: a package from the
# Arch repos rebuilt from its own PKGBUILD with the default profile.
# Render with `make demo-gif`, which starts the container this tape drives.

Output docs/assets/repo-build.gif
Require podman

Set Shell "bash"
Set FontFamily "Fira Code"
Set FontSize 15
Set Width 1200
Set Height 640
Set Padding 24
Set Theme "GitHub Dark"
Set TypingSpeed 35ms
Set Framerate 20
Set WaitTimeout 3m

Hide
Type "podman exec -it -u demo -w /home/demo $DEMO_CONTAINER bash" Enter
Sleep 1s
Type "PS1='$ '; clear" Enter
Sleep 500ms
Show

Sleep 500ms
Type "pacman -Si tree | grep -E '^(Name|Version|Groups)'" Sleep 300ms Enter
Sleep 2500ms
Type "sysforge build tree -si" Sleep 400ms Enter
# A repo package is only built from source once you opt in; the answer is saved.
Wait+Screen /build from source\?/
Sleep 1500ms
Type "y" Enter
Wait+Line /^\$ ?$/
Sleep 1500ms
Type "pacman -Qi tree | grep -E '^(Name|Version|Groups)'" Sleep 300ms Enter
Sleep 4s
4 changes: 2 additions & 2 deletions docs/concepts/arch-packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,6 @@ repos are built this way.
## In sysforge

sysforge builds every package with makepkg, giving each build a temporary `makepkg.conf`
with the flags of the profile that matches it. The [README](../../README.md) covers building,
tracking and updating packages; the [freeze guide](../guides/freeze.md) covers reviewing
with the flags of the profile that matches it. The [getting started guide](../guides/getting-started.md) covers
building, tracking and updating packages; the [freeze guide](../guides/freeze.md) covers reviewing
changed PKGBUILDs and fetching PGP keys.
4 changes: 2 additions & 2 deletions docs/concepts/compiler-optimization.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ They cost one register and a couple of instructions per call.

sysforge applies these flags through profiles in `profiles.toml`: `standard` (`-march=native
-O2`), `optimized` (`-O3`, `-fno-plt`, lld with `--icf=all`), `hardened` (`standard` plus
extra hardening) and your own, chosen per package by rules. The [README](../../README.md) shows
how a profile is chosen, and `sysforge resolve <pkg>` shows which one applies to a package and
extra hardening) and your own, chosen per package by rules. The comments at the top of the shipped
`profiles.toml` explain how a profile is chosen, and `sysforge resolve <pkg>` shows which one applies to a package and
why. The hardening flags `/etc/makepkg.conf` sets are kept even when a profile replaces `CFLAGS`,
unless the profile sets `preserve_system_tokens = false`.
4 changes: 2 additions & 2 deletions docs/design/06-config-layer.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,9 @@ Both `sysforge build` and `sysforge pipeline` accept `--profile-conf FILE` to su
| `[build]` | `python` | `system` | Python interpreter for PKGBUILD `build()` steps, pinned ahead of any pyenv/asdf/conda shim on `PATH` so a bare `python` resolves to the interpreter its `python-*` makedepends were installed against. `system` / unset → `/usr/bin/python`; a bare version like `3.12` → `/usr/bin/python3.12`; or an absolute path. Resolved choice logged at DEBUG; an unusable value warns and falls back to the system python |
| `[build]` | `nice` | — | Build CPU-throttle: scheduling niceness `0..19` (out-of-range clamped). Applied as a `nice -n` front-end on the makepkg invocation — soft yield, full speed when idle. See §Flag/Profile System (build throttling) |
| `[build]` | `ionice` | — | Build IO-throttle class: `"idle"` or `"best-effort"`. Applied as `ionice -c {3,2}` |
| `[build]` | `cpu_quota` | — | Hard CPU ceiling, either `"N%"` (100% = one core) or a decimal fraction of total cores (`0.5` → half the host, translated against `os.cpu_count()` for portability). Enforced by wrapping makepkg in a transient `systemd-run --scope --user -p CPUQuota=N%` (folds in `Nice=`/`IOSchedulingClass=`); degrades to nice/ionice with a warning when `systemd-run` is absent or the user slice lacks CPU-controller delegation |
| `[build]` | `cpu_quota` | — | Hard CPU ceiling, either `"N%"` (100% = one core) or a decimal fraction of total cores (`0.5` → half the host, translated against `os.cpu_count()` for portability). Enforced by wrapping makepkg in a transient `systemd-run --scope --user -p CPUQuota=N%` (folds in `Nice=`/`IOSchedulingClass=`); degrades to nice/ionice with a warning when `systemd-run` is absent or the user slice lacks CPU-controller delegation. A sandboxed build is capped on its container's `devtools-<user>.slice` instead (`build_sandbox.slice_limits`) |
| `[build]` | `jobs` | — | Parallel build jobs; rewrites the `-jN` token in the emitted `MAKEFLAGS` (appends `-jN` if absent — make honours the last `-j`, so a `-j$(nproc)` baseline is still capped) |
| `[build]` | `mem_limit` | — | Per-build memory ceiling so a runaway build (OOM-prone link, sanitizer, LTO) can't take down the workstation. A byte count or binary-suffixed size (`"24G"`, `"512M"`). Off the `cpu_quota` path it's applied as an `RLIMIT_AS` clamp in the makepkg child's preexec; on the `cpu_quota` (`systemd-run --scope`) path it's delivered as a tighter cgroup `-p MemoryMax=` instead, so the two mechanisms never double-apply. Junk/zero/negative dropped with a warning. See §Flag/Profile System (build throttling) |
| `[build]` | `mem_limit` | — | Per-build memory ceiling so a runaway build (OOM-prone link, sanitizer, LTO) can't take down the workstation. A byte count or binary-suffixed size (`"24G"`, `"512M"`). Off the `cpu_quota` path it's applied as an `RLIMIT_AS` clamp in the makepkg child's preexec; on the `cpu_quota` (`systemd-run --scope`) path it's delivered as a tighter cgroup `-p MemoryMax=` instead, so the two mechanisms never double-apply. A sandboxed build gets it as `MemoryMax=` on its container's slice, like `cpu_quota`. Junk/zero/negative dropped with a warning. See §Flag/Profile System (build throttling) |
| `[build]` | `repo_track` | `"stable"` | Which sync-DB release a `source = "repo"` checkout tracks: `"stable"` pins to the release tag matching pacman's currently-resolvable repo version (`get_repo_candidate_version` — the sync DB cross-checked against a `checkupdates` side-copy refresh, so a stale local DB cannot pin a superseded release) so a source build matches what pacman would install; `"main"` leaves the checkout on the packaging repo's default branch (testing-track). Single read chokepoint `config.resolve_repo_track`; unrecognised values **warn and** fall back to `"stable"` via the shared `config.resolve_enum(raw, known, default, *, key)` seam (the one home for known-vocabulary string options — validate, warn on mismatch, fall back to the documented default). `resolve_repo_mode` routes through the same seam (falling back to `"pacman"`) — this lenient path serves the several defensive readers that only compare the resolved value. Its **authoritative** load point, `packages._load_packages`, is instead strict: it validates the raw `repo_mode` token against `config.REPO_MODE_ACCEPTED_INPUTS` and **hard-fails** on a typo, because silently falling back to `"pacman"` at that boundary would drop the source builds the user configured (strict-at-the-boundary, lenient-in-the-interior). The legacy `"profiled"` token was removed in 3.0.0 and is rejected by both. Distinct from packages.toml's `repo_mode` (source-vs-pacman install strategy) — this key only affects *which commit* a repo checkout sits on |
| `[aur]` | `enabled` | `true` | AUR posture (`3.1.0-F3`). `false`: sysforge never contacts the AUR — no clones (`AurDisabled`), no RPC lookups, no search results, no package-name list. An AUR-tracked package is reported as `AUR disabled` (non-failing) and left alone; `build` refuses one in `pre_check` (exit 2); `search` omits its AUR section; a dependency only the AUR has is named in a warning. A checkout whose `origin` is not the AUR still syncs (`aur.is_aur_checkout`). Only a literal `false` disables; not liftable per run. Separately, `update` refreshes the name list only when build_state tracks a `source = "aur"` package |
| `[aur]` | `min_fetch_interval_ms` | `500` | Minimum gap between consecutive git fetches against aur.archlinux.org (millisecond resolution) |
Expand Down
Loading
Loading