8.6 KiB
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.
pmacs.packages.install {
"github:owner/repo",
version = "^1.0.0",
}
The shorthand string form is also accepted (version pins only):
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:
-- 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.
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)
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
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:
-
Path-based search. Each install prepends
<install_root>/?.lua;<install_root>/?/init.luatopackage.path. Packages with the conventional layout (<basename>.luaor<basename>/init.lua) resolve via this path with no further machinery — exactly as a hand-written Lua project would. -
Custom searcher. When the path-based search misses (e.g. the manifest declares
entry = "main.lua"orentry = "lib/foo.lua"), a custom searcher pmacs registered inpackage.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:
{
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.