pmacs/docs/packages.md

232 lines
8.6 KiB
Markdown

# Package installation
pmacs's package surface lives at `pmacs.packages.*` in Lua. Two
install variants ship in v0.1: `install` (user-config scope) and
`install_project` (project scope). Both run synchronously during
init; mid-session install is not supported.
## `pmacs.packages.install { ... }` — user-config scope
Installs to `$XDG_DATA_HOME/pmacs/packages/<basename>/` (or
`$HOME/.local/share/pmacs/packages/...` if `XDG_DATA_HOME` is unset).
The package's entry module is wired into Lua's require resolution so
`require("<basename>")` returns the module's table.
```lua
pmacs.packages.install {
"github:owner/repo",
version = "^1.0.0",
}
```
The shorthand string form is also accepted (version pins only):
```lua
pmacs.packages.install "github:owner/repo@^1.0.0"
```
### Pin kinds: `version`, `branch`, `commit`
Each install pins exactly one revision. The spec table chooses the
pin via one of three mutually-exclusive fields:
```lua
-- Highest semver tag matching the constraint. Recommended default.
pmacs.packages.install { "github:owner/repo", version = "^1.0.0" }
-- HEAD of the named branch at install time. Not reproducible across
-- time --- the upstream's branch HEAD moves --- so use sparingly.
pmacs.packages.install { "github:owner/repo", branch = "main" }
-- Exact commit. Reproducible: the same SHA always installs the same
-- snapshot. Useful for pinning to a known-good state before the
-- upstream has tagged a release.
pmacs.packages.install { "github:owner/repo", commit = "abc1234" }
```
Mutual exclusion is enforced: a spec table with two of these fields
errors with a "must specify exactly one" message naming every
conflicting field. With none of the three, the pin defaults to
`version = "*"` (any tag).
The shorthand string form (`"address@^1.0"`) is **version-pin only**.
Branch and commit pins must use the table form because there is no
unambiguous sigil that distinguishes a branch named "main" from a
malformed semver constraint without surprising users.
For version pins, pmacs additionally cross-checks that the
manifest's `version` field at the matched tag satisfies the user's
constraint, catching upstreams whose tag and `pmacs.toml` disagree.
Branch and commit pins skip that check (the user explicitly asked
for that revision regardless of what the manifest says).
## `pmacs.packages.install_project { ... }` — project scope
Installs to `<project_root>/.pmacs/packages/<basename>/`. Project
installs are prepended to `package.path` so they take precedence
over user-config installs of the same basename.
```lua
pmacs.packages.install_project {
"github:owner/repo",
version = "^1.0.0",
project_root = "/abs/path/to/project",
}
```
### `project_root` is required
`install_project` requires an explicit `project_root`. There is **no
fallback to the process CWD**: at init time CWD is whatever shell
directory the user happened to invoke pmacs from, which is almost
never a meaningful project root.
If you omit the field you get a typed error that names two concrete
patterns for filling it in. Pick whichever fits:
#### Pattern A: an environment variable (CI, scripts, multiple machines)
```lua
pmacs.packages.install_project {
"github:owner/repo",
version = "^1.0.0",
project_root = os.getenv("PMACS_PROJECT"),
}
```
The user (or the CI runner) sets `PMACS_PROJECT=/path/to/project`
before invoking `pmacs`. The path is stable across invocations
regardless of where the shell happened to be.
#### Pattern B: a path relative to the loading `init.lua`
```lua
pmacs.packages.install_project {
"github:owner/repo",
version = "^1.0.0",
project_root = ".",
}
```
Relative paths in `project_root` resolve against the directory
**containing the loading `init.lua`**, *not* against CWD. So
`project_root = "."` means "alongside this init.lua";
`project_root = "subdir"` means a subdirectory of the init.lua's
directory.
This works because pmacs's loader stamps each chunk with a `@<path>`
source label (the standard Lua convention for file-loaded chunks);
the install binding reads that label back from a per-eval app-data
slot to recover the chunk's directory.
Edge case: when running pmacs's package API from a Lua chunk that
was *not* loaded from a file (e.g., via `pmacs --eval ...`, or from
the M-x command-line evaluator), there is no source label. Relative
`project_root` values then fall through to "as-is," matching the
pre-v0.1 CWD interpretation. This is intentionally ad-hoc: the only
flow that matters in v0.1 is init.lua, and string-loaded chunks
that need an exact path can use Pattern A or pass an absolute path
literally.
### Forward planning: project-local `init.lua`
When project-local `init.lua` lands (post-v0.1), the project loader
will set a "current project root" before evaluating the project's
init.lua, and `install_project` from inside that init.lua will pick
up the project root automatically — no `project_root` field needed.
The user-global init.lua path will continue to require an explicit
field, since it has no implicit project context.
The change will be relaxation, not breakage: code that explicitly
passes `project_root` keeps working unchanged.
## How `require` resolution works
pmacs uses Lua's standard require machinery, augmented at install
time:
1. **Path-based search.** Each install prepends
`<install_root>/?.lua;<install_root>/?/init.lua` to
`package.path`. Packages with the conventional layout
(`<basename>.lua` or `<basename>/init.lua`) resolve via this
path with no further machinery — exactly as a hand-written Lua
project would.
2. **Custom searcher.** When the path-based search misses (e.g.
the manifest declares `entry = "main.lua"` or `entry =
"lib/foo.lua"`), a custom searcher pmacs registered in
`package.searchers` (Lua 5.4) / `package.loaders` (LuaJIT and
Lua 5.1) consults the install roster, finds the matching
package by basename, and returns a loader for the exact entry
path declared in the manifest.
The searcher iterates the roster in install order, most-recent
first, so a project-scope install of a basename overrides a prior
user-scope install of the same basename — mirroring the
"newer-installs-prepend-to-path" semantics of the path-based
search.
When `require` cannot find a name through any searcher, the
combined error message names every searcher's contribution; the
custom searcher's contribution looks like:
```
no installed pmacs package named 'whatever'
```
so a user with a typo can spot it without digging into pmacs's
internals.
## `pmacs.packages.installed()`
Returns an array of records describing every package installed
during the current init pass. Each record has the same shape as
`install`'s return value:
```lua
{
name = "samplepkg", -- manifest's name
version = "1.0.0", -- manifest's declared version
tag = "v1.0.0", -- resolution descriptor (see below)
commit = "abc...", -- full SHA of the installed snapshot
install_path = "...",
entry = "...",
scope = "user", -- or "project"
summary = "...",
pin = { -- structured user request
kind = "version", -- or "branch" or "commit"
value = "^1.0.0", -- echoes the spec field exactly
},
}
```
The `tag` field is a stable, non-empty descriptor:
- For version pins: the matched tag (`"v1.2.3"`).
- For branch pins: `"branch:<name>"`.
- For commit pins: `"commit:<short-sha>"`.
The `pin` table is the source of truth for "what did the user
request." The flat fields (`tag`, `version`, `commit`) record the
resolution. They differ for branch/commit pins, where the resolved
commit is what got installed but the user's request was the branch
name or SHA prefix.
## `pmacs.packages.update(...)`
Stubbed for v0.1. M7.6 implements re-resolution and lockfile
regeneration. Until then, re-running `install` with a new constraint
upgrades in place.
## Errors and how they're shaped
Every install error message names the operation, the input that
caused the failure, and (where applicable) the workaround. Examples:
- `pmacs.packages.install_project requires an explicit project_root field. Pass project_root = "/path/to/your/project" (often os.getenv("PMACS_PROJECT") or a path relative to the directory containing your init.lua).`
- `package at tag v2.0.0 of github:owner/repo requires pmacs ">= 2.0.0", but this pmacs is "0.1.0". Upgrade pmacs, or pin a package version compatible with "0.1.0".`
- `no tag for github:owner/repo satisfies "^99.0.0". Available tags: ["v1.0.0", "v1.1.0"]`
The convention is that the error stands on its own — read in a CI
log or stack trace, the user can see what to do without context.