Packages
A package is the unit Rune installs with pkg install: a config overlay
that wires something into Rune, plus whatever that thing needs to run. It is
the shared delivery format underneath both extensions,
which communicate with Rune through the SDK, and
plugins, which wrap command-line tools.
Distributing from a git repository
You distribute your own extension by pushing it to a public git repository and telling users to install it by its repository ID, which is the repository's host and path:
pkg install github.com/<owner>/<repo>
The ID works for any git host Rune can clone over HTTPS, so GitHub, GitLab, Bitbucket, and self-hosted servers all install the same way. The host has to carry a dot, and the path needs at least two segments, which also covers nested groups:
pkg install gitlab.com/<group>/<subgroup>/<repo>
pkg install git.example.com/<owner>/<repo>
Rune clones the repository, reads a config.yaml at its root as the
config overlay, installs anything the overlay
requires, and runs your extension from source
through the matching language toolchain. Because it runs from source, one
repository serves every platform, and there is nothing to build or host. The
repository must be public: Rune clones anonymously over HTTPS.
What the repository must contain
A git package is a normal repository with two things at its root:
- A
config.yamloverlay that declares at least one extension underextensions.<id>(see Registering an extension). Every extension'spathmust point at a source entrypoint the runner can start: amain.go, a.pyfile, or a.rsfile (see source-file extensions). Pointpathback into the checkout with$RUNE_DATADIR/lib/$RUNE_PKG_ID, which resolves to your repository's files on disk. - The extension's sources and their project manifest, so the toolchain
can build and run them:
go.mod(andgo.sum) for Go,pyproject.tomlfor Python,Cargo.tomlfor Rust.
A requirements: list naming the language package (python, go, or
rust) belongs in the overlay so pkg install provisions the toolchain
first. Rune verifies all of this while cloning and aborts the install with a
descriptive error before writing anything if the config.yaml is missing,
declares no extensions, or points an entrypoint at something the runner
cannot start.
Example: a Go extension repository
A minimal Go extension repository:
.
├── go.mod # module and its dependencies
├── go.sum # pinned dependency checksums
├── main.go # entry point
└── config.yaml
requirements:
- go
extensions:
<extension_id>:
path: '$RUNE_DATADIR/lib/$RUNE_PKG_ID/main.go'
module github.com/<owner>/<repo>
go 1.25
require github.com/unstablebuild/rune-go-sdk v0.0.107
On install, the go requirement provides the toolchain, and Rune runs
main.go with go run against the checked-out module. Commit go.sum:
Rune runs the module in read-only module mode, so every dependency must
already be pinned there. See the Go SDK guide for writing the
extension itself, and the Python guide for the equivalent
layout in that language.
Versions: latest, tags, and commits
A git package's version is a point in the repository's history. Users select
it as the optional third argument to pkg install:
pkg install <host>/<owner>/<repo> <version> | Installs |
|---|---|
(no version) or latest | The tip of the default branch. Recorded as the short commit SHA, so a later commit shows up as an update. |
a tag (for example v1.2.0) | That tag's commit, pinned. |
| a commit SHA | That exact commit, pinned. |
Tag your releases and users can pin to them; pkg update-check and
pkg update-all compare against the default branch's tip, so an install
that tracks latest picks up new commits automatically. You do not publish
or register anything: pushing a commit or tag is the release.
What pkg install does
Packages are installed from the Rune console with
pkg install <name>.
When a user installs one, Rune:
- Installs required packages first. If the package's config declares a
top-level
requirements:list, Rune resolves each named package to its latest version and installs any that are not already installed, config merge and environment included, before the package itself. If a requirement cannot be installed, the whole install is aborted. - Puts executables on the
PATH. Every executable the package ships is copied into Rune's shared binary directory, which is always first on thePATHof Rune's terminals and of anything Rune launches. The tools are available immediately, with no separate step. - Merges the config overlay. The package's config is deep-merged into
the user's config: keys the package introduces are added, including new
leaves under a map the user already has. A package only needs to carry the
settings that connect it to Rune, and it will not disturb the rest of the
user's configuration. A value the user has already customized is left as
is, so the package never clobbers a user setting silently. The one
exception is a value the package derives from
$RUNE_PKG_VERSION: when that changes across versions, Rune shows the user the diff and asks before applying it. Therequirementskey itself is package metadata, not config: it is stripped from the merge and never appears in the user's config.
This is the same on a git install and a first-party archive install: the difference is only where the files come from. A git-hosted extension usually has no executables of its own (step 2 is a no-op for it); its toolchain comes from a required language package. The layout the install produces on disk is described under the on-disk package format.
The overlay does not need a PATH entry for a bundled executable. Rune
places its shared binary directory first on the PATH at startup, so a
bundled binary is reachable as soon as it is installed. Only set gui.env
in the overlay when a tool needs extra environment variables of its own, not
to expose the binary.
The config overlay
Every package carries a config.yaml (or config.star) at its root: the
config overlay. In a git package it is the repository's config.yaml;
in a first-party archive it is the file at the top of the tarball. Either
way, Rune deep-merges it into the user's config at install
time (as described above) and it is where a package
wires itself into Rune: registering an extension, requiring other packages,
and pointing at its own files. If both config.yaml and config.star are
present, config.yaml wins.
Referencing the install location
If a value in the overlay must point back into the installed package, use these variables. They expand to the install location when the configuration is applied:
| Variable | Expands to |
|---|---|
$RUNE_DATADIR | The user's Rune data directory. |
$RUNE_PKG_ID | The package's name. |
$RUNE_PKG_VERSION | The installed version. |
A package's own files are reachable at $RUNE_DATADIR/lib/$RUNE_PKG_ID,
which resolves to the version currently in use: the cloned repository for a
git package, or the extracted archive for a first-party one. Point at those
files by joining onto it. For example, an overlay can set an
environment variable to a directory the package ships, or name a bundled
asset by its path:
gui:
env:
# a directory bundled in the package, e.g. a toolchain root
<TOOL_HOME>: '$RUNE_DATADIR/lib/$RUNE_PKG_ID'
extensions:
<pkg>:
config:
# a single bundled file
asset_path: '$RUNE_DATADIR/lib/$RUNE_PKG_ID/<data-file>'
A plugin that only puts a CLI on the PATH usually does not need these
variables at all, since the overlay can refer to the tool by name.
Requiring other packages
A package that depends on another Rune package declares it in a top-level
requirements: list in its config overlay. Each entry is a package name,
the same name users type after pkg install:
requirements:
- go
extensions:
<extension_id>:
path: '$RUNE_DATADIR/lib/$RUNE_PKG_ID/main.go'
At install time, each requirement is resolved to its latest version and
installed in full before the declaring package, so anything the requirement
contributes (executables on the PATH, environment variables from its
overlay) is in place by the time the declaring package starts anything.
A requirement that is already installed is left at its installed version.
If a requirement fails to install, the declaring package's install is
aborted.
Registering an extension
An extension package registers itself through the config overlay. Add an
entry under extensions.<extension_id> with a path to its entrypoint and,
optionally, a config map with defaults:
extensions:
<extension_id>:
path: '$RUNE_DATADIR/lib/$RUNE_PKG_ID/main.go'
config:
<setting>: <default>
<group>:
<setting>: <default>
Rune starts what path names and passes the config block to the extension
as its config.Config. For a git package, path is a source entrypoint
in the checkout (a main.go, a .py file, or a .rs file), which Rune runs
through the required language toolchain; the example above points at the
repository's main.go. A first-party archive that ships a compiled binary
instead points path at that executable (for example
/bin/<extension_binary>). Including useful defaults makes the extension's
configurable surface visible in the user's config immediately after install,
and the user can edit those values like any other config. See the
Extensions guide for the SDK side of writing the extension
process, and source-file extensions
for how each source entrypoint is run.
Registering a tutorial
A package can also ship an in-IDE tutorial and register it
from the overlay. Include the tutorial's .star file in the package and add
a tutorials.<name> entry that points at it with the install-location
variables:
tutorials:
<name>: '$RUNE_DATADIR/lib/$RUNE_PKG_ID/<tutorial>.star'
After install, the tutorial is available like any other. See the Tutorials guide for how to author one and register it additively.
The on-disk package format
Whichever way a package is delivered, Rune installs it into the same layout
under the user's data directory ($RUNE_DATADIR, by default ~/.rune). You
do not assemble this by hand for a git package (Rune builds it from your
clone), but understanding it explains where $RUNE_DATADIR/lib/$RUNE_PKG_ID
points and how bundled executables reach the PATH. In the trees below,
<pkg> is the package's name and <version> is the installed version.
A first-party package is a gzipped tar archive holding up to three kinds of content:
.
├── bin/ # executables, copied onto PATH
│ ├── <tool>
│ └── <tool2>
├── lib/ # payload, kept in place
│ ├── <shared-library>
│ └── <data-file>
└── config.yaml # config overlay, merged into the user config
Installing it, or cloning a git repository, produces this layout:
.
├── bin/ # package executables, copied here by base name
│ ├── <tool> # (this directory is always first on PATH)
│ └── <tool2>
├── pkg/
│ └── <pkg>/
│ └── <version>/ # the archive extracted, or the repo cloned, verbatim
│ ├── bin/
│ ├── lib/
│ └── config.yaml
├── lib/
│ └── <pkg> -> pkg/<pkg>/<version>/ # symlink to the in-use version
│ # ($RUNE_DATADIR/lib/$RUNE_PKG_ID)
└── config.yaml # user config, with the overlay merged in
Three kinds of content, three destinations:
- Executables. Any non-hidden file with an execute bit set is copied
into Rune's shared binary directory by its base name, and that directory is
always first on the
PATH. Abin/directory is the convention, not a requirement, and the file can be a compiled binary or a script with a shebang. Because names are taken by base name, keep each executable's file name unique. A git-hosted extension usually ships no executables at all: it runs from source through a required language toolchain. - Everything else (the payload). All other files stay in place under
pkg/<pkg>/<version>/, reachable through$RUNE_DATADIR/lib/$RUNE_PKG_ID. For a git package this is the whole checked-out repository, including the extension's sources; for a first-party archive it is data files, shared libraries, or a bundled toolchain. - The config overlay. The
config.yamlat the root, merged into the user's config at install time.
Example: a first-party plugin (ripgrep)
A plugin that wraps a command-line tool is the simplest
first-party package: one binary plus an overlay that points a Rune feature at
it. Rune's own ripgrep (rg) plugin
backs fuzzy search: the archive carries the rg binary
under bin/, and the overlay wires the two commands fuzzy search reads:
extensions:
fuzzy_search:
config:
file:
command: rg -l ""
line:
command: rg --color never -n --no-heading --max-columns 500 ""
It refers to rg by name because the binary is already on the PATH.
Everything else in the user's extensions and fuzzy_search configuration
is left untouched by the merge. A user installs it by name from the
console:
pkg install rg
The ripgrep package is a complete, working example of this format.
Third-party developers do not build or host these archives. They are how
Unstable Build ships first-party packages (language runtimes, the Rune Agent,
runectl, plugins like the one above), installed by a bare name. To
distribute your own extension, use a git repository.
See also
- Plugins: package a CLI so a Rune feature can use it.
- Extensions: package a program that communicates with Rune through the SDK.
- Console: install and manage packages with
pkg. - Config: how Rune configuration is structured and merged.