+------------------------------------+ | Package scopes and symlink routing | +------------------------------------+ This document records the design decisions behind how glacier packages are scoped, how their repo determines where their symlinks land, and what "boot-critical" means in this system. It exists because these rules aren't obvious from reading the code alone, and getting them wrong (particularly the symlink-placement rule) has real consequences up to and including destroying a live /usr. +------------------------------------+ | 1. The two scopes | +------------------------------------+ USR (GL_SCOPE_USR) Per-uid packages. Index/store/links all live under a specific uid's tree: /glacier/usr/index/ /glacier/usr/store/ /glacier/usr/links/ Symlinks always land under /glacier/usr/links//{bin,...}. There is no base-repo exception for USR scope. Nothing installed under a per-user scope is ever considered part of the base system, so nothing here ever touches /usr. SYS (GL_SCOPE_SYS) System-wide packages, not owned by any particular uid: /glacier/sys/index /glacier/sys/store Symlink destination depends on the package's repo (see section 3). Requires root. gpkg enforces this via geteuid() before it will even attempt a system-scope operation. +------------------------------------+ | 2. Repo semantics | +------------------------------------+ A package's repo (PKG_REPO in its manifest) is not just a label for organizing the store — under system scope, it is the one thing that decides where that package's activation symlinks physically land. base Packages that make up a minimal working system. ALL packages in this repo are considered boot-critical BY DEFINITION — this is a repo-level classification, not a per-package flag. A package doesn't have to be individually essential for booting to count; if it's filed under base, it's treated as part of the base system, full stop. extra Software that may be important but is not necessary to run Everest, nor to install it. community Everything else. NOTE: extra and community are currently NOT distinguished by the library itself — both simply mean "not base," and both route to the same default system-scope links location. If they need to diverge further later (different confirmation prompts, different trust handling, whatever), that is a deliberate future change, not something already implemented. +------------------------------------+ | 3. Symlink placement rule | +------------------------------------+ scope == USR -> /glacier/usr/links/ scope == SYS && repo == "base" -> /usr (*) scope == SYS && repo != "base" -> /glacier/sys/links (*) This is the base-system exception. Packages that are part of the minimal working system get their symlinks placed directly into /usr/bin, /usr/lib, etc. — real FHS locations — so that anything with a hardcoded path (init scripts, systemd units, shebang lines) works without needing glacier-aware PATH setup. Everything an operator installs under system scope that ISN'T part of the base system stays isolated under /glacier/sys/links, same as it always has. The base system is the exception to the norm, not the other way around. This decision is made per-package, at the point where a package's repo is actually known (inside gl_link_pkg / gl_unlink_pkg / the walk loop in gl_relink_store) — NOT baked into the context at creation time. gl_context_t's links_path field always holds the DEFAULT destination for that scope; the /usr override is computed separately per package. +------------------------------------+ | 4. Why /usr as a target is safe | +------------------------------------+ /usr is not glacier-exclusive territory the way /glacier/sys/links is. It holds plenty of content glacier has no business touching. Every place that reads or writes into a links destination therefore treats that destination as "possibly shared, not owned": - is_glacier_symlink(path, store_prefix) is the single source of truth for "did glacier itself create this." It reports true only if `path` is a symlink whose target lives under the relevant store path. Anything else — a real file, a directory, a foreign symlink pointing somewhere else entirely — is never touched. - gl_link_pkg refuses to overwrite an existing path unless is_glacier_symlink says it's safe to replace. It will not clobber a foreign file that happens to occupy the same path. - gl_relink_store no longer wipes and rebuilds its links directory wholesale (that was safe only when the target was glacier-only territory). It prunes ONLY stale symlinks glacier itself created (identified via is_glacier_symlink, removed only if their store target no longer exists) and leaves everything else alone. - A single system-scope relink pass prunes BOTH possible destinations (/glacier/sys/links and /usr), since it can't know in advance whether any base-repo packages are involved without checking both. - The stage tree's links directory is no longer hardlink-seeded at all. It was always unused (gl_link_pkg only ever runs against a LIVE context; gl_relink_store always builds its own fresh live context rather than touching a staged one) — and now that live links_path can be /usr, seeding it would mean hardlinking the entire /usr tree on every staged system transaction, and risking EXDEV outright if /usr and the stage area are on different filesystems. +------------------------------------+ | 5. Updating base-system packages | +------------------------------------+ Base-repo packages are tracked in the system index like anything else, and updating them works exactly the same way updating any other package does — same staged transaction, same commit/rollback. No separate mechanism was built for this, because none was needed: gpkg -s newmusl.gpkg ...updates musl in place under system scope, symlinks and all, whether it was originally installed by a bootstrap tool or by a normal `gpkg -s` call later. There is no meaningful distinction between "how it originally got there" and "how you update it." Replacing something as fundamental as libc on a LIVE running system is safe under this model specifically because of POSIX unlink semantics: a process that already has a shared object open keeps working off the old inode even after the symlink swap happens. Only newly-spawned processes after the swap see the new version. This falls directly out of the atomic-rename commit design already in place — nothing extra was added to make it work. +------------------------------------+ | 6. Open items | +------------------------------------+ - grootstrap has not been rebuilt against this design yet. The simplification this design enables: once gpkg/gstore themselves exist on the target (still a bootstrapping problem that needs solving separately), EVERY package — base included — can go through the same `gpkg -s -l` path. There is no longer a need for a separate raw-extraction special-case for base packages specifically; the repo alone routes them to /usr correctly. - extra vs community currently behave identically (see section 2). Whether they should diverge, and how, is undecided. - The bootstrapping-a-bootstrapper problem (getting gpkg/gstore onto a target filesystem before there's a working package manager to install them with) is still unsolved and orthogonal to everything in this document.