diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..cdfbfb9 --- /dev/null +++ b/.clang-format @@ -0,0 +1,16 @@ +BasedOnStyle: LLVM + +BreakBeforeBraces: Custom +BraceWrapping: + AfterFunction: true + AfterControlStatement: false + AfterEnum: true + AfterStruct: true + AfterUnion: true + AfterClass: true + +AlwaysBreakAfterReturnType: All + +IndentWidth: 8 +TabWidth: 8 +UseTab: Always \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index f2a944b..4064097 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,261 +1,448 @@ # CLAUDE.md -Context for Claude Code sessions on this repo (libglacier-ng, the package -manager for the Everest/Glacier Linux distribution). This file exists -because there's no built-in way to hand off a claude.ai planning -conversation into Claude Code — this is that handoff, written down. +Context for Claude Code sessions on libglacier-ng: the package manager +for Everest Linux. Kept as a handoff, so a fresh session starts with full +continuity instead of re-deriving everything. Last full rewrite: +2026-10-04, at the end of a long session. **Update it as you go.** -For the symlink-routing / package-scope design specifically, see -`PACKAGE_SCOPES.txt` at the repo root — that doc is the source of truth -for that topic and isn't duplicated here. +## Start here + +1. `ROADMAP.txt`: what's left, in priority order, plus the decisions that + are still open (section 6). Pick work from it. +2. This file: how things work, what the user wants, how to test. +3. The design docs, which are authoritative for their topics and not + repeated here: + - `SECURITY_DESIGN.txt`: integrity verification, scope separation, + refusal matrix, syspkg's privilege model. + - `PACKAGE_SCOPES.txt`: layout, link routing, `/etc` config handling. + - `IMAGE_DESIGN.txt`: PROPOSAL for system images (bootstrap OUTPUT, + the sysroot library change). Open decisions are in section 9. + - `GLACIER_BOOTSTRAP.txt`: PLAN for `glacier-bootstrap`. Prerequisites + are in section 7, decisions in section 9 (9c and 9d are decided). + - `SECURITY_REVIEW_2026-07-16.md`: older review findings. + +Nothing from the 2026-10-04 session is committed. libglacier-ng is on +branch `dev` with a large uncommitted diff (`src/`, the docs). Commit only +when the user asks. + +## The three projects + +| Path | What | Git? | +|---|---|---| +| `~/Projects/libglacier-ng` | C library, design docs, this file | yes (`dev`) | +| `~/Projects/glacier-ng` | Frontend: `gpkg`, `gstore`, `syspkg`, `gbuild`, `gworld`, `lib/*.lua`, `tests/`, Dockerfiles, `pkgs/` | **no**: back files up before editing (`file.old`; if that exists, `.old2`, `.old3`, ...) | +| `~/Projects/everest` | The distro build: `everest.sh build\|initramfs\|boot`, packages, bootable QEMU image | no | + +`glacier-ng/lib/glacier/` holds `.so`s built on the host (glibc). They +won't load on musl. Every Alpine image and test builds libglacier-ng from +source instead. ## What this project is -A from-scratch Linux distribution (Everest, built on the Glacier -package-management model) with its own package manager -(`libglacier-ng` + `gpkg`/`gstore` CLI tools) and its own build -tooling (`gbuild`/`gworld`) for bootstrapping a set of packages up to -a minimal bootable system. +A from-scratch, security-focused Linux distribution (Everest) with its own +package manager (libglacier-ng + `gpkg`/`gstore`/`syspkg`) and build tooling +(`gbuild`/`gworld`). It's a C library, LuaJIT+FFI CLI tools and bash build +scripts. The user writes it; Claude implements and designs alongside them. + +## How the user wants things done + +These are preferences stated in sessions. Follow them. + +- **Output format:** every message uses glacier's `lg_printf` levels: + `[x]` is error (2), `[!]` warning (1), `[i]` info (0), `[✓]` success (3). + Messages are capitalized. Exact strings the user chose include + `Cannot open system index: Permission denied` (no period) and + `Nothing to do.` (success level). + - The pre-trust-check `die()` in syspkg/gstore reproduces the `[x]` + format byte for byte, because it can't load `libglacier_log.so` + before that file has been verified. +- **No hint messages.** Don't print lines that point users to other flags + or commands (for example "use -lO to ..."). The user called these + clutter. A refusal states the fact, in one line. The user also removed a + `Run doas gstore -n ...` follow-up hint from gstore themselves. +- **Ask before settling a design choice that is the user's to make**, and + give a recommendation. They have answered every such question promptly. + Don't silently pick a layout or semantics. +- **Verify by running, in Docker or QEMU, never by inspection alone.** For + any security fix, also prove the test catches the bug: remove the + defence in a throwaway copy and confirm the test fails ("mutation + check"). Several defences here are verified that way. +- Use doas, not sudo. syspkg is never setuid. + +## Decisions the user has made (don't relitigate) + +- **Per-uid user layout** (`/glacier/usr//...`, root-owned parent). + Both root and the user can create it: root runs `gstore -n UID`; a user + runs `doas gstore -n` through a doas rule pinned to `args -n` + (option "B"). +- **`syspkg -n` creates the top-level tree itself** (GLACIER_BOOTSTRAP 9d). +- **`/etc` config files: option 1.** Copy on install, `.new` on conflict + (GLACIER_BOOTSTRAP 9c). +- **`-O` with `-l` = replace.** `-O` displaces non-glacier files, and + `-lO` also replaces an installed *different version* (upgrade or + downgrade from a local file). Plain `-l` with another version installed + refuses in one line. +- **`KEEP_OLD`** in `/etc/glacier/glacier.conf`: the number of backups of + replaced versions to keep per package (0 = none). + - It has **no default.** It ships as `KEEP_OLD = 1`. + - If it's missing or invalid, an operation that would replace a version + errors. Only then: other operations don't need it. + - "Backed up" means the old package's files are compressed into a + `.gpkg`. A restore interface comes later. +- **`gpkg -u` / `syspkg -u` upgrade from a REMOTE repository**, and + `gpkg -f` is `-l` but fetched by name from a remote repository. Both wait + for remote repos + signing (ROADMAP 3.1). Don't implement them against + local files. +- `everest-bootscripts` is the name of the base `/etc` package. Its source + lives in `~/Projects/everest/pkgsrc/`. ## Core architecture -**`gl_context_t`** (transaction.h) is the central abstraction: `uid`, -`root_path`, `index_path`, `store_path`, `links_path`, `mode` -(`GL_CTX_LIVE` / `GL_CTX_STAGE`), `scope` (`GL_SCOPE_USR` / -`GL_SCOPE_SYS`), `lock_fd`. +**`gl_context_t`** (transaction.h) is the central abstraction. Fields: +`uid`, `root_path`, `index_path`, `store_path`, `links_path`, `mode` +(`GL_CTX_LIVE`/`GL_CTX_STAGE`), `scope` (`GL_SCOPE_USR`/`GL_SCOPE_SYS`), +`lock_fd`, and `overwrite` (last field, zeroed by the init functions). -**Transaction lifecycle**: `gl_init_live_context` / `gl_init_stage_context` -→ `gl_commit_transaction` or `gl_abort_transaction`. Staged installs -hardlink-seed `index`/`store` from live at stage-init time (copy-on-write, -so removing a live package inside a transaction doesn't require special -casing). The `links` tree is deliberately **not** seeded — see below. +**Layout.** Each scope owns ONE base directory holding +`{index,store,links,stage,old,lock,backup}`. Every create, rename and lock +a transaction does stays inside it. -**Commit** does atomic rename-based promotion (stage → live, with the -previous live moved to `old/` as a rollback point), fsyncs the parent -dirs, then calls `gl_relink_store` to regenerate symlinks and -`gl_rebuild_index` to regenerate the index — both from a fresh -`GL_CTX_LIVE` context, not the just-committed stage context. +| Scope | Base dir | Ownership | +|---|---|---| +| `GL_SCOPE_USR` | `/glacier/usr//` | uid, mode 0700, inside a root-owned 0755 `/glacier/usr` | +| `GL_SCOPE_SYS` | `/glacier/sys/` | root | -**Scopes**: -- `GL_SCOPE_USR`: per-uid, `/glacier/usr/{index,store,links}/`. -- `GL_SCOPE_SYS`: system-wide, `/glacier/sys/{index,store}`. Symlink - destination depends on the package's **repo** — see `PACKAGE_SCOPES.txt`. - Short version: `repo == "base"` → `/usr`; anything else → - `/glacier/sys/links`. This is decided per-package (in `gl_link_pkg` / - `gl_relink_store`), not baked into the context. +- The helpers `gl_usr_base`, `gl_usr_base_check` and the layout comment + are in `common.h`. +- `gl_init_stage_context` refuses a user base that isn't a real directory + (lstat) owned by its uid with no group/other bits, or whose + `/glacier/usr` is loosened. `gl_usr_base_status` exports that check, so + gpkg can say why before any prompt. -**Repos**: `base` (minimal working system, boot-critical by definition — -this is a repo-level classification, not a per-package flag), `extra` -(important but not required), `community` (everything else). `extra` -and `community` currently behave identically — see open items. +**Transaction lifecycle.** `gl_init_live_context` / `gl_init_stage_context` +→ `gl_commit_transaction` or `gl_abort_transaction`. +- Stage init takes the lock FIRST, then clears any leftover stage. +- Stage contexts hardlink-seed `index`/`store` from live, so removing a + live package inside a transaction needs no special casing. Seeding + copies dotfiles: only `.` and `..` are skipped. +- A stage on another filesystem fails at seeding with `GL_TXN_ERR_XDEV`. +- **Commit:** rename-based promotion (stage → live, previous live → `old/`), + then fsync the base. Then `gl_relink_store` + `gl_rebuild_index` + regenerate links and index from a *fresh live context* and rescan the + WHOLE store (every commit relinks every package, so link-time logic must + be idempotent). `old/` is then deleted. -## Safety mechanisms (read before touching links/symlink code) +**Link routing** (per package, in `gl_link_pkg`): -`ctx->links_path` is **not always glacier-exclusive territory** — -for system-scope `base` packages it's literally `/usr`, which holds -plenty of content glacier has no business touching. Everything that -reads or writes a links destination has to treat it as "possibly -shared": +| Scope | Repo | Destination | +|---|---|---| +| USR | any | `/links` | +| SYS | `base` | `/usr` | +| SYS | anything else | `/glacier/sys/links` | -- `is_glacier_symlink(path, store_prefix)` (istoreutils.c) is the one - source of truth for "did glacier create this." True only if `path` - is a symlink whose target lives under `store_prefix`. Never touch a - path this returns false for. -- `gl_link_pkg` will not overwrite an existing path unless - `is_glacier_symlink` says it's safe to replace. -- `gl_relink_store` prunes stale symlinks (`prune_stale_links`) rather - than wiping and rebuilding the whole links directory — a full - `rm -rf` of `ctx->links_path` would be catastrophic once that path - can be `/usr`. It prunes **both** possible system-scope destinations - (`/glacier/sys/links` and `/usr`) since a single pass can't know in - advance whether any base-repo packages are involved. -- The stage links tree is never hardlink-seeded — it's genuinely - unused (`gl_link_pkg` only ever runs against `GL_CTX_LIVE`; - `gl_relink_store` always builds its own fresh live context). Seeding - it would mean hardlinking all of `/usr` on every staged system - transaction, and risking `EXDEV` if `/usr` and the stage area are on - different filesystems. +- A package's leading `usr/` is stripped. +- In system scope, a package's top-level `etc/` is never linked: it's + COPIED into `/etc` (see `/etc` config handling below). -## Bug history (so it doesn't get relitigated or reintroduced) +**Repos.** `base` (the minimal system; boot-critical by definition, a +repo-level rule), `extra`, `community`. `extra` and `community` behave +identically. -Roughly chronological, all already fixed in the current tree: +**Hidden files in the store** (dot-prefixed, so linking, index rebuild and +the store scanners skip them, while seeding carries them through +transactions): -1. **`gl_abort_transaction` never actually released the lock file** — a - copy-paste bug wrote into the wrong variable. Fixed by moving off - `O_EXCL`-based locking entirely: `gl_begin_transaction` now just - `open(O_CREAT)` + `flock()`, and lock files are never `unlink()`'d - (the kernel releases `flock()` automatically on process death, even - `SIGKILL`; explicit unlinking has its own TOCTOU race with a second - process creating a new inode at the same path). -2. **`EXDEV` wasn't checked before renaming** — `gl_commit_transaction` - now `stat()`s both sides and fails cleanly with `GL_TXN_ERR_XDEV` - before attempting any renames if stage and live aren't on the same - filesystem. -3. **Segfault on install**, root cause: the Makefile's `transaction` - target didn't link `libglacier_transaction.so` against - `libglacier_istoreutils.so`/`libglacier_log.so`, even though - `transaction.c` calls `gl_relink_store()` and `lg_printf()`. Fixed - by adding those `-l` flags and an explicit `transaction: log - istoreutils` prerequisite (build order matters now). -4. **Symlinks going stale after commit** — `gl_link_pkg` used to run - during staged installs too, embedding the *stage* path as the - symlink target. Fixed: `gl_link_pkg` only runs for `GL_CTX_LIVE`; - links are fully regenerated post-commit by `gl_relink_store`. -5. **Dangling symlinks never cleaned up on removal** — see "Safety - mechanisms" above; this is what `prune_stale_links` fixes. -6. **`GL_TXN_ERR_RENAME` on every commit after the first** — leftover - `old/` directory from a previous commit made the next commit's - `rename()` fail with `ENOTEMPTY`. Fixed: `gl_commit_transaction` - wipes `old_base` if present before using it. -7. **`-Wstringop-truncation` errors** (real bug, not noise) — four - `strncpy(dst, ctx->foo_path, PATH_MAX - 1)` calls that could leave - `dst` unterminated. Fixed by switching to `snprintf(dst, - sizeof(dst), "%s", ...)`, which always terminates. - (`-Wformat-truncation`, separately, is suppressed via - `-Wno-error=format-truncation` in `config.mk` — those warnings are - about genuinely-safe `snprintf` truncation, not a real bug.) -8. **`gbuild` build-system autodetection** checked for a bare - `Makefile` before `configure`/`configure.ac`. Broke musl (and any - project with a hand-rolled, non-autotools build system that ships - both) since its `Makefile` is non-functional without `configure` - generating `config.mak` first. Fixed by reordering the checks. -9. **Index only stored `repo::pkgname`, no version.** Now - `repo::pkgname::version` (`gl_rebuild_index` looks up the - `-` store subdirectory). `struct gpkg_entry` in - `istoreutils.h` needs a `char *ver;` field — this was a manual - header edit, double check it's actually present. -10. **`gworld` recipe parser silently dropped `SYS_PROFILE`** — the - key-matching regex only matched uppercase letters, not - underscores. Fixed to `^([%u_][%u%d_]*)=(.*)$`. Caught by an - actual precedence test, not code review — worth remembering that - class of bug (a field silently never parsing, falling back to a - default that happens to look right) is easy to miss by inspection - alone. +| File | Purpose | +|---|---| +| `/.glacier-manifest` | the package's manifest, saved at install, used by `gl_backup_pkg` | +| `/.glacier-fresh` | one-shot "installed in this transaction"; link conflicts are only reported when present | +| `/.glacier-overwrite` | one-shot `-O` permission for this package's next link pass | +| `/.displaced/` | originals displaced by `-O`, restored when the link that replaced them is pruned | +| `/.etc-state/` | the last version of each `/etc` file a package offered | -## CLI additions +**Configuration.** `/etc/glacier/glacier.conf` (libconfig syntax). +- It's read with `gl_conf_get_int(path, key, &out)` in `pkg.c`. Missing + file, missing key and wrong type are distinct statuses, and there are no + defaults. +- The shipped copy is `glacier-ng/etc/glacier/glacier.conf`. Every + Dockerfile installs it. -- `gpkg -s` / `--system`: operate against `GL_SCOPE_SYS` instead of a - uid's tree. Requires root (checked via `geteuid()`). Must appear - *before* `-l`/`-x` in a grouped flag string (e.g. `-sl`, not `-ls`) — - flags are handled in parse order, same constraint `-V`/`-S` already - had. -- `gstore -N` / `--new-system`: calls `gl_init_sys()`. Requires root. -- `gbuild -P` / `--profile PROFILE`: system profile, `ARCH-LIBC[-FEATURE...]` - (e.g. `x86_64-musl`, `x86_64-glibc-multilib`). Derives a cross-compile - prefix (translating `glibc`→`gnu` per real GNU triplet convention), - detects native-vs-cross against the build host, and only pre-fills - `-t`/`-x` defaults for whichever the user didn't already set - explicitly. Features export as `GL_FEATURE_=1` env vars for - custom `-b`/`-i` commands to branch on — gbuild itself doesn't - hardcode per-feature behavior, that's intentional. +## Safety invariants: read before touching links/symlink code -## Build system +`/usr` is NOT glacier-exclusive territory. Treat every link destination as +possibly shared: -`Makefile` builds separate `.so`s per module -(`libglacier_log.so`, `libglacier_pkg.so`, `libglacier_istoreutils.so`, -`libglacier_transaction.so`, `libglacier_dag.so`) — **this split is -deliberate Unix-philosophy design**, not an accident: a third-party -glacier-compatible program should be able to link only -`libglacier_log.so` without pulling in everything else. `gpkg`/`gstore` -load the specific per-module `.so`s they need directly, matching this. +- **Ownership test.** `is_glacier_symlink(path, store_prefix)` is the only + test for "did glacier create this". Never touch anything it returns + false for, except through `-O`'s displace/restore. +- **Overwriting.** `gl_link_pkg` won't overwrite a non-glacier path without + the `-O` marker. It never descends through *another package's* directory + symlink into that package's store directory. +- **Pruning.** `gl_relink_store` PRUNES stale links; it never wipes a links + destination. A link is stale only if its own store target is gone + (checked with `lstat`, not by following it): a package symlink may + legitimately dangle. +- **Stage links.** The stage links tree is never seeded. +- **Extraction never writes through a symlink.** + - Symlink entries are created only after all regular content. + - Any entry whose parent is a symlink is refused (`crosses_symlink`, + plus `O_NOFOLLOW`). + - Any entry with `..` or an absolute path refuses the whole package. + - A failed extraction removes its half-made version directory. +- **Recursive deletes.** `gl_rmdir_recursive` skips only `.`/`..`. Every + caller passes a glacier-only path; keep it that way. `gl_delete_user` + lstat-checks its target first, because a symlinked base would otherwise + have its *target* emptied. +- **Wide wrappers.** Don't add convenience wrappers that silently touch a + broader filesystem scope than asked. This codebase has been bitten twice + by exactly that. -There's also a `libglacier-ng` Makefile target producing a **combined** -`.so` — this exists *solely* for the Lua FFI test harness's -convenience (one thing to `ffi.load()` instead of juggling rpath -chains in a throwaway test process). It is **not** in `all` and **not** -installed. Don't reach for it outside `tests/` — that's a sign -something should be split-loaded instead. +## Feature reference (current behaviour) -`config.mk` holds `CFLAGS`, including `-Wno-error=format-truncation` -(see bug #7 above for why that specific warning is suppressed and -others aren't). +**Integrity verification** (`gpkg_common.lua`: `verify_and_copy`, +`verify_all`): +- Each package is copied into a private `mktemp -d` directory while being + hashed with `libglacier_verify.so`. The result is checked against + `FILE.sha256` (`sha256:`). +- Everything after that (summary, manifest read, `gl_install_pkg`) reads + ONLY the copy. +- A missing or malformed sidecar counts as a mismatch and is refused before + the prompt; one bad package refuses the whole batch. No bypass exists. +- This lives in the CLIs' shared module, not the library (as + SECURITY_DESIGN section 4 specifies), so a third-party caller of + `gl_install_pkg` is not verified (ROADMAP 3.2). +- The base→USR refusal (`GL_INSTALL_ERR_SCOPE = 7`), by contrast, *is* in + the library. -## Lua tooling +**`-O` / displace:** +- A displaced foreign file or symlink moves to `/.displaced/`: + renamed, or on `EXDEV` copied (keeping mode, owner and mtime) and then + unlinked. +- Directories, special files and paths that already have a backup are + still refused. +- In user scope `-O` is effectively a no-op, because the links tree is + rebuilt from scratch on every commit. -- `gpkg`, `gstore`: LuaJIT + FFI, calling directly into the `.so`s. -- `lib/glacier_cdef.lua`: single shared source of truth for every - `ffi.cdef()` declaration, used by `gpkg`, `gstore`, and the test - suite. This used to be duplicated three ways, which is exactly what - let a stale signature drift silently into `gpkg` at one point — if - you're editing a C function signature, this file needs the matching - update, and it's the *only* place that needs it now. -- `tests/*.lua`: LuaJIT FFI test suite exercising real filesystem/process - behavior (`fork`+`SIGKILL` for lock crash-recovery, real `mount()` - for the `EXDEV` check, etc.) — not mocked. Run via: - ```sh - cd tests - make -C .. libglacier-ng # builds the test-only combined .so - GLACIER_TEST_CONFIRM=yes LIBGLACIER_SO=$(pwd)/../build/lib/shared/libglacier-ng.so luajit run_all.lua - ``` - `test_exdev.lua` needs `--cap-add=SYS_ADMIN` in Docker to actually - exercise the mount path; it skips cleanly (not a failure) without it. +**Version replacement** (`-lO`, `M.install` in `gpkg_common.lua`): +1. Installed entries are read from the live index's `current` file + (`repo::name::version` lines). +2. A same name+repo with a different version is a replacement. Without + `-O` it's refused; with `-O`, `keep_old()` reads `KEEP_OLD`, and the + summary shows `old -> new`. +3. In the transaction: `gl_backup_pkg(live, repo, name, X.gpkg.part)`, then + `gl_remove_pkg` from the stage, then install. +4. After the commit: rename `.part` → `/backup///-.gpkg`, + write its sidecar, and run `gl_prune_backups(dir, KEEP_OLD)` (newest by + nanosecond mtime). +- On any abort, `finish()` deletes the `.part`. +- A backup is a complete installable `.gpkg`, so `-lO backup.gpkg` + restores it. -## Build automation (bootstrap toolchain) +**`/etc` config handling** (`install_config` / `prune_config` in +istoreutils.c), system scope only: -- `gbuild` (bash): single-package builder. Autodetects build system - (cmake/meson/cargo/go/autotools/make), supports cross-compilation - (`-t`/`-x`, now also `-P` for profiles). -- `gworld` (Lua): batch orchestrator. Reads `recipes/*.recipe`, - topologically sorts by `DEPS`, invokes `gbuild` once per package in - order. `-s SYSROOT` gives every package in the batch a shared - staging dir — relies on `gbuild -p` never wiping its target, so - headers/libs accumulate across the batch for free. -- Recipe format: `NAME VER REPO URL/REF-or-SRC DEPS BUILD INSTALL - FLAGS SYS_PROFILE` (plain `key=value`, see `recipes/musl.recipe` for - a real example). -- Real multi-pass toolchain bootstraps (LFS-style — gcc built twice, - etc.) should use separate recipes per pass (`gcc-pass1`, - `gcc-pass2`), publishing only the final pass under the real - name/repo. The DAG models build steps, not abstract packages. -- **`grootstrap` needs a rebuild, not yet done.** The old design (raw - `tar` extraction into a target dir + chroot + register everything - under an arbitrary bootstrap uid) predates the `GL_SCOPE_SYS` + - base-repo design and is now more complicated than necessary. Once - `gpkg`/`gstore` themselves exist on the target (still an unsolved - bootstrapping-the-bootstrapper problem — how do they get there - before a working package manager exists to install them with), - every package including `base` ones can go through the same `gpkg - -s -l` path; the repo alone routes symlinks correctly. No more - special-casing base packages during bootstrap. +| Situation | Result | +|---|---| +| `/etc/F` missing, no record | install | +| missing, record exists | the admin deleted it: stays deleted | +| unchanged since glacier wrote it | updated in place | +| admin-modified, or never glacier's | never touched; a package change is offered ONCE as `F.new` | +| no package ships it any more | removed if unchanged, kept otherwise | -## Known open items / stubs +User-scope `etc/` is still linked (ROADMAP 2.9). -- `gpkg -f` / `-u` (merge/update) are unimplemented stubs. `-f` - references an undefined `uid` variable (should be `uidn`) — will - nil-concatenation-error the moment it's actually implemented. -- `gpkg -x`'s confirmation summary shows placeholder `unknown::pkg - 0.0.0 (unknown)` since it has no local manifest to read version/repo - from for a bare package name. Would need `resolve_repo` called - before building the summary, not just before the removal itself. -- `gpkg -x` doesn't print a completion message on success (`-l` does: - "Completed with no errors."). Cosmetic inconsistency. -- Reinstalling an up-to-date package double-logs ("Installed X" + - "staged from X" for the same event). -- `extra` vs `community` repos don't currently diverge in behavior — - both just mean "not base" for symlink-routing purposes. -- `gl_relink_store`/`gl_rebuild_index` are full rescans of the store on - every commit — fine at current scale, worth revisiting if large - transactions become common (a dirty-set of touched packages instead - of full rescans). -- Package manifests don't record which `SYS_PROFILE` they were built - under — deliberately not added yet (would need a `pkg.c` / - `gl_gpm2gpkg` change), flagged as a future option, not a gap that - needs fixing now. -- A stray nested `lib/glacier/lib/glacier/` directory was spotted once - in a `tree` listing — looked like a leftover `make install` - artifact, not in any load path, harmless but worth a `rm -rf` - eventually. +**Top-level tree.** `gl_create_fs_tree(root, verbose)` (exported) creates +the merged-/usr tree: `/bin` and `/sbin` → `usr/bin`, `/lib` → `usr/lib`, +`/usr/sbin` → `bin`, all RELATIVE links. It also creates `/usr/{bin,lib}`, +`/etc`, `/dev`, `/proc`, `/sys`, `/run`, `/tmp` and `/var/tmp` (1777), +`/var`, `/root` (0700) and `/home`. +- It creates only what's missing, of any type. +- `gl_init_sys` runs it before writing the index. The index marks `-n` as + complete, so a second `syspkg -n` is refused: `Cannot create + /glacier/sys: already exists`. +- It takes a root prefix, ready for `syspkg --root`. -## Conventions worth knowing before editing +**Package symlinks** are extracted verbatim and linked like files. +Hardlink, device and fifo entries are skipped. `add_to_archive` (used by +backups and `gstore -b`) archives symlinks. -- C source generally uses tabs for indentation, though the codebase is - inconsistent about it in places (some blocks use spaces). ALWAYS USE TABS WHEN POSSIBLE. -- `PATH_MAX`-sized stack buffers + `snprintf` is the standard pattern - for path construction throughout; prefer it over `strncpy` for - anything copying into a fixed buffer (see bug #7). -- Don't add convenience wrappers that silently touch a broader - filesystem scope than what's asked — this codebase has already been - bitten twice by exactly that class of mistake (`gl_relink_store`'s - wipe, `gl_link_pkg`'s blind `unlink`), both only becoming dangerous - once a path assumption (`links_path` is glacier-exclusive) quietly - stopped holding. +## CLIs (glacier-ng) + +| Tool | Flags | Runs as | +|---|---|---| +| `gpkg` | `-l FILE...` `-x NAME...` `-O` `-S` `-V` `-h` `-v`; `-f`/`-u` are stubs | the user; per-user scope only, NEVER constructs `GL_SCOPE_SYS` | +| `syspkg` | `-n` `-l FILE...` `-x NAME...` `-O` `-S` `-V` `-h` `-v` | root via doas; `-h`/`-v` without root | +| `gstore` | `-n [UID]` `-b UID OUTFILE` `-x UID` `-s` (stub) `-V` `-h` `-v` | `-n` needs root (direct, or `doas gstore -n` for yourself) | + +- **gpkg:** modifiers `-O`/`-S`/`-V` are applied in a pre-pass, so their + position doesn't matter (`-lO` == `-Ol`). It finds its user via + `/proc/self/loginuid`, falling back to `getuid()`. +- **syspkg, `-n`:** creates the tree plus `/glacier/sys`. +- **syspkg as non-root:** `-n`/`-l`/`-x` are refused after argument + parsing with `Cannot open system index: Permission denied`. +- **gstore, `-n` without a UID, under doas:** acts for the invoking user + (loginuid, then `DOAS_USER`, then getuid). The doas rule + `permit nopass :wheel as root cmd /path/gstore args -n` is the + boundary that stops a user naming another UID. +- **Shared code:** `lib/gpkg_common.lua` (`M.install`, `M.remove`, + verification, replacement, backups). +- **The `-x` summary** comes from the live index; a name that isn't + installed fails before the prompt. +- **Hardened loading** (syspkg and gstore): `#!/usr/bin/luajit -E`, + `package.path` and every `ffi.load` pinned to their own `realpath`, and + when euid is 0, every ancestor directory and every loaded `.lua`/`.so` + must be root-owned and not group/world-writable. The preamble is + DUPLICATED in both files on purpose: change one, change both. +- **gpkg** uses the older CWD-relative convention (`require("lib/...")`, + `ffi.load("./lib/glacier/...")`): run it from its own directory. +- **Declarations:** `lib/glacier_cdef.lua` is the single source of truth + for every `ffi.cdef`. `tests/glacier_ffi.lua` is only a wrapper around + it. Declaration ORDER matters: a declaration using `gl_context_t` must + come after its typedef. + +## Build tooling + +- **gbuild** (bash, the only copy is `glacier-ng/gbuild`): + - `-n -V -g -c -d -m -D -C -b -i -o -p -j -F -t -x -u -r -s`; + `-v`/`--version` prints the version, `--verbose` turns on verbose + output. + - Autodetects the build system, with autotools checked BEFORE a bare + Makefile. + - Writes `NAME-VER.gpkg` plus a `.sha256` sidecar, using `gsha256sum` + from `$GSHA256SUM_BIN`, else `