# 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. 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. ## 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. ## 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`. **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. **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. **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. **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. ## Safety mechanisms (read before touching links/symlink code) `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": - `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. ## Bug history (so it doesn't get relitigated or reintroduced) Roughly chronological, all already fixed in the current tree: 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. ## CLI additions - `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. ## Build system `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. 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. `config.mk` holds `CFLAGS`, including `-Wno-error=format-truncation` (see bug #7 above for why that specific warning is suppressed and others aren't). ## Lua tooling - `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. ## Build automation (bootstrap toolchain) - `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. ## Known open items / stubs - `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. ## Conventions worth knowing before editing - 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.