Files
libglacier-ng/PACKAGE_SCOPES.txt
2026-07-19 17:49:23 -04:00

178 lines
7.7 KiB
Plaintext

+------------------------------------+
| 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/<uid>
/glacier/usr/store/<uid>
/glacier/usr/links/<uid>
Symlinks always land under /glacier/usr/links/<uid>/{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/<uid>
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.