pmacs/docs/packages.md

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:

  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:

{
    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.