|
| 1 | +# Config Package Repeatable Design |
| 2 | + |
| 3 | +> Date: 2026-05-27 |
| 4 | +> Status: design note for PR #306 and follow-up libxpkg/spec work |
| 5 | +
|
| 6 | +## Summary |
| 7 | + |
| 8 | +`type = "config"` should mean a repeatable configuration procedure, not an installed package. |
| 9 | + |
| 10 | +A config package may depend on normal packages and may execute configuration logic after those dependencies are available. It should not own install artifacts, should not create an implicit installed state, and should generally avoid registering itself through xvm. Its core value is to provide a reentrant configuration code path that can be run more than once safely. |
| 11 | + |
| 12 | +This definition intentionally narrows `config` so package authors do not need to reason about install markers, versioned payload directories, uninstall snapshots, or xvm deregistration for a package that is only meant to configure an environment. |
| 13 | + |
| 14 | +## Definition |
| 15 | + |
| 16 | +A `config` package is: |
| 17 | + |
| 18 | +- a repeatable configuration procedure; |
| 19 | +- dependency-aware; |
| 20 | +- reentrant and expected to tolerate repeated execution; |
| 21 | +- usually implemented with `config()`; |
| 22 | +- normally not uninstallable, because it has no owned install payload. |
| 23 | + |
| 24 | +A `config` package is not: |
| 25 | + |
| 26 | +- a package with its own installed payload; |
| 27 | +- a package that owns package payload files under `pkginfo.install_dir()`; |
| 28 | +- a package that should become installed because xlings created metadata; |
| 29 | +- an xvm registration unit; |
| 30 | +- a good place for irreversible or non-idempotent system mutation. |
| 31 | + |
| 32 | +## Lifecycle Contract |
| 33 | + |
| 34 | +Recommended shape: |
| 35 | + |
| 36 | +```lua |
| 37 | +package = { |
| 38 | + spec = "1", |
| 39 | + name = "example-config", |
| 40 | + type = "config", |
| 41 | + xpm = { |
| 42 | + linux = { |
| 43 | + deps = { "some-tool" }, |
| 44 | + ["latest"] = { ref = "1.0.0" }, |
| 45 | + ["1.0.0"] = {}, |
| 46 | + }, |
| 47 | + }, |
| 48 | +} |
| 49 | + |
| 50 | +function config() |
| 51 | + -- Reentrant configuration logic only. |
| 52 | + return true |
| 53 | +end |
| 54 | +``` |
| 55 | + |
| 56 | +Lifecycle constraints: |
| 57 | + |
| 58 | +- `install()` is discouraged for `type = "config"`. |
| 59 | +- `config()` is the primary entrypoint. |
| 60 | +- `installed()` is discouraged because config packages should not rely on a persistent installed state. |
| 61 | +- `uninstall()` is optional and should only exist when the config procedure creates a deliberate, reversible external side effect. |
| 62 | +- Hooks must be idempotent. Running `xlings install <config>` multiple times should not corrupt user or system state. |
| 63 | +- Hooks should use explicit paths such as user config paths, dependency paths, or `system.rundir()` when needed. They should not rely on `pkginfo.install_dir()` as durable package-owned storage. |
| 64 | + |
| 65 | +## Dependency Semantics |
| 66 | + |
| 67 | +Config packages may have dependencies. |
| 68 | + |
| 69 | +Those dependencies are normal packages and keep their own install state, xvm state, payloads, and uninstall semantics. The config package itself does not inherit or proxy those states. |
| 70 | + |
| 71 | +This allows patterns such as: |
| 72 | + |
| 73 | +- install normal toolchain packages as dependencies; |
| 74 | +- run a config procedure that wires editor settings or project state; |
| 75 | +- rerun the config procedure after user changes without reinstalling the toolchain. |
| 76 | + |
| 77 | +## XVM Semantics |
| 78 | + |
| 79 | +Config packages should avoid `xvm.add()` as part of their own definition. |
| 80 | + |
| 81 | +Reason: `xvm.add()` creates durable registration state. Durable registration implies a corresponding `xvm.remove()` and therefore an uninstall contract. That contradicts the narrow config definition: a repeatable configuration procedure without its own install/uninstall lifecycle. |
| 82 | + |
| 83 | +If a package needs xvm registration, it should usually be one of these instead: |
| 84 | + |
| 85 | +- a normal `package` that owns an installed payload and registers its programs; |
| 86 | +- a future explicit registration-oriented package type or policy; |
| 87 | +- a dependency whose own `config()` registers itself. |
| 88 | + |
| 89 | +Short-term compatibility note: existing packages may still use `xvm.add()` from config hooks. The stricter rule should first be documented and linted in libxpkg/index tooling before being made a hard runtime error. |
| 90 | + |
| 91 | +## Install State Semantics |
| 92 | + |
| 93 | +For `type = "config"` in the current soft-constraint PR: |
| 94 | + |
| 95 | +- xlings should not create `.xim-installed`; |
| 96 | +- xlings should not copy `.xpkg.lua` into the package install directory; |
| 97 | +- xlings should not treat xlings-owned metadata as evidence that the config package is installed. |
| 98 | +- an empty install directory is allowed by the existing hook flow and must not count as installed. |
| 99 | + |
| 100 | +If a config hook deliberately writes files somewhere, those files are external configuration state, not package payload. The hook author is responsible for making that write idempotent. |
| 101 | + |
| 102 | +If the package truly needs owned files, backups, snapshots, or versioned removal, it should not be modeled as a pure config package. |
| 103 | + |
| 104 | +## Current PR #306 Evaluation |
| 105 | + |
| 106 | +PR #306 already moves in the right direction: |
| 107 | + |
| 108 | +- it skips the `.xim-installed` auto-stamp for config packages; |
| 109 | +- it skips the `.xpkg.lua` install-dir snapshot for config packages; |
| 110 | +- it tests that a no-payload config package runs repeatedly instead of being mistaken for installed. |
| 111 | + |
| 112 | +Current PR #306 soft policy: |
| 113 | + |
| 114 | +- keep existing hook and cwd semantics unchanged; |
| 115 | +- allow the generic flow to leave an empty install directory; |
| 116 | +- treat only non-empty author-created content as installed state; |
| 117 | +- avoid xlings-owned stamp/snapshot files for config packages; |
| 118 | +- keep xvm compatibility for now, but document that new config packages should avoid `xvm.add()`. |
| 119 | + |
| 120 | +This keeps the code change small while leaving room to refine the config contract in libxpkg/spec. |
| 121 | + |
| 122 | +## Discussion Options |
| 123 | + |
| 124 | +Option A: soft runtime policy, current PR scope. |
| 125 | + |
| 126 | +- Keep hook semantics unchanged. |
| 127 | +- Allow an empty install directory. |
| 128 | +- Skip only xlings-owned install markers and snapshots for `type = "config"`. |
| 129 | +- Add TODO comments and tests. |
| 130 | + |
| 131 | +Option B: stricter no-install-dir runtime policy. |
| 132 | + |
| 133 | +- Do not precreate `install_dir` for config packages. |
| 134 | +- Do not run config hooks from `install_dir`. |
| 135 | +- This is cleaner theoretically, but it changes hook runtime behavior and may break existing recipes. |
| 136 | + |
| 137 | +Option C: spec/lint first. |
| 138 | + |
| 139 | +- Keep runtime compatibility. |
| 140 | +- Update libxpkg/spec and index checks to discourage `install()`, `installed()`, `pkginfo.install_dir()`, and `xvm.add()` in config packages. |
| 141 | +- Migrate existing packages gradually before any hard runtime behavior change. |
| 142 | + |
| 143 | +Recommended now: Option A in PR #306, then Option C as follow-up. Option B should wait until existing package usage has been audited. |
| 144 | + |
| 145 | +## Follow-Up Work |
| 146 | + |
| 147 | +Spec and libxpkg should later make the contract explicit: |
| 148 | + |
| 149 | +- update `docs/spec/xpkg-manifest-v1.md` with the narrow config definition; |
| 150 | +- add libxpkg/index lint checks: |
| 151 | + - warn when `type = "config"` defines `install()`; |
| 152 | + - warn when it calls `xvm.add()`; |
| 153 | + - warn when it uses `pkginfo.install_dir()`; |
| 154 | + - warn when it defines `installed()` as persistent install-state logic; |
| 155 | +- add migration guidance for existing config packages that are actually xvm registration or managed-state packages; |
| 156 | +- consider a future explicit package type or policy for registration/managed external state. |
| 157 | + |
| 158 | +## Recommended Author Rule |
| 159 | + |
| 160 | +Use `type = "config"` only when this sentence is true: |
| 161 | + |
| 162 | +> This package is just a repeatable configuration procedure over its dependencies and environment; it has no owned install payload and no durable registration state of its own. |
| 163 | +
|
| 164 | +If that sentence is not true, use a normal package or introduce a more specific type/policy. |
0 commit comments