pinstall

package module
v1.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 29, 2026 License: GPL-2.0, GPL-3.0 Imports: 22 Imported by: 0

README

pinstall

Go Reference Go version Test coverage Mutation OpenSSF Best Practices OpenSSF Scorecard

Digest-pinned installs of an upstream release: version-addressed, revalidated, with a verdict

A Go library for programs that must install a specific version of some other program and then depend on it. You give it a URL template, the version you pinned, and the SHA-256 of each architecture's archive. It downloads the archive, refuses anything whose digest does not match, installs it into a version-addressed directory, re-probes the binary it is about to activate, re-asserts the settings your install depends on, keeps N predecessors as a fallback, and reports whether the result is usable.

It is the piece you would otherwise write in a shell script, with the failure modes that script usually has: a half-written install directory that looks finished, a binary that silently self-updated out from under the digest you verified, a partial download reported as a checksum mismatch, and no way to tell "still installing" from "gave up".

Nothing here exits your process, reads the environment, or does work at import time. Zero dependencies outside the standard library. Linux only.

Why it is shaped this way

Four decisions drive everything else:

  • A version directory is complete or it does not exist. Artifacts are written into a staging tree, each one is synced, a .complete sentinel naming the version is written and synced last, the directory is synced, and only then is it renamed into place. An interrupted install is detectable by the absence of the sentinel and is never a selection candidate. Atomic visibility is not crash durability, which is why the syncs are separate steps rather than an afterthought.
  • Nothing reaches the installation root before the digest matches. The archive is downloaded and verified in a process-local temp directory. On a mismatch there is not even an empty directory to find later.
  • A sentinel is not proof. Before a version is activated its primary artifact is probed and must answer with the version its own directory claims. An artifact replaced on the volume under an intact sentinel is excluded, which falls through to another complete version and leaves the pin unsatisfied so the next pass reinstalls.
  • A failed install is survivable. Every complete version already on the volume keeps serving. Pruning runs only after a successful publish, because those directories are the fallback set. Retries are bounded, and a repair made in place is picked up by Rescan without restarting your process.

Install

go get github.com/cplieger/pinstall@latest

Usage

A Release is everything true of the package, independent of where it runs — write it once. A Config is one deployment of it: the pin, the digests, the root and your local policy.

package main

import (
	"context"
	"log"
	"os/exec"

	"github.com/cplieger/pinstall"
)

// The pinned digests. Whatever bumps your version literal bumps these with it.
const (
	widgetVersion = "1.4.2"
	amd64SHA      = "9f2b...64 lowercase hex characters..."
	arm64SHA      = "3ce1...64 lowercase hex characters..."
)

// The profile: written once, reused by every deployment.
func widgetRelease() pinstall.Release {
	return pinstall.Release{
		Name:        "widget",
		URLTemplate: "https://widgets.example/dl/{arch}/widget_{version}.zip",
		ArchTokens:  map[string]string{"amd64": "linux-64", "arm64": "linux-arm"},
		ArtifactDir: "dist/bin", // the archive already holds the artifacts
		ProbeArgs:   []string{"--version"},
		Mandatory: []pinstall.Assertion{
			// Must hold after every install; a deployment cannot drop it.
			{Name: "autoupdate", Args: []string{"config", "set", "autoupdate", "off"}},
		},
	}
}

func main() {
	mgr, err := pinstall.New(&pinstall.Config{
		Release: widgetRelease(),
		Version: widgetVersion,
		Digests: map[string]string{"amd64": amd64SHA, "arm64": arm64SHA},
		Root:    "/var/lib/example/tools",
		LinkDir: "bin",                     // optional convenience symlink
		Require: []string{"widget-helper"}, // artifacts you cannot run without
	})
	if err != nil {
		log.Fatal(err)
	}

	// Bounded, retrying, and non-fatal: a failure leaves your program running.
	if err := mgr.EnsureWithRetry(context.Background()); err != nil {
		log.Printf("widget install failed, serving degraded: %v", err)
	}

	if ready, why := mgr.Ready(); !ready {
		log.Printf("widget is not usable yet: %s", why)
		return
	}

	// Always the absolute version-directory path, never the convenience link.
	out, err := exec.Command(mgr.Path(), "run").Output()
	if err != nil {
		log.Fatal(err)
	}
	log.Printf("%s", out)
}

The library writes only under Root:

<Root>/
├── <Name>-versions/
│   ├── 1.4.2/            # the active version: artifacts + .complete
│   └── 1.4.1/            # the retained predecessor
├── <Name>-state.json     # a diagnostic record; never an input to Ready
└── bin/<Binary>          # the optional convenience symlink

Configuration reference

Release — the package profile
Field Description
Name The package identity. Fixes the versions root and the state file. Required
Binary The primary artifact's file name: what gets probed, linked, and always required. Empty defaults to Name
URLTemplate The archive URL, carrying {version} and {arch}. Required
ArchTokens GOARCH to the publisher's token ("amd64" to "x86_64-linux"). An unmapped architecture is ErrUnsupportedArch
ProbeArgs The argv that makes the primary artifact print its version. Required
ParseVersion Parses the probe's output. Nil uses LastFieldOfFirstLine
Unpack Extracts the verified archive. Nil uses UnpackZip
Installer An installer script shipped inside the archive. Nil means the archive already holds the artifacts
ArtifactDir Where the artifacts land: relative to the installer's private home when Installer is set, else to the extracted tree
Mandatory Assertions a deployment cannot configure away. At least one is required — see below
Notice Logged once per install attempt, for a licence acknowledgement the upstream requires
Config — one deployment
Field Description
Release The profile above
Version The pin, constrained to a path- and URL-safe character set
Digests GOARCH to the lowercase hex SHA-256 of that architecture's archive. The running architecture needs one
Root The absolute installation root; the only tree touched
GOARCH Overrides the architecture. Empty resolves from runtime.GOARCH
URLTemplate Overrides the release's template, for a mirror
Require Artifacts a version directory must hold to count as complete. The primary artifact is always added
Optional Artifacts installed when the archive provides them, warned about when it does not
Assert Assertions re-asserted on every pass. Release.Mandatory is merged in
Purge A one-shot sweep of a layout a previous installer left behind. Nil skips it
LinkDir A directory under Root for the convenience symlink. Empty publishes none
Retain Predecessors kept besides the active version. Zero uses 1
RetryBackoff The first EnsureWithRetry backoff, doubling to a ten minute cap. Zero uses 30s
MaxAttempts Bounds EnsureWithRetry. Zero uses 4
Untrusted Root was writable by others: activate only what this process installed
Manager
Method Description
Ensure(ctx) error One idempotent pass: purge, prune partials, select, install if needed, assert
EnsureWithRetry(ctx) error Ensure with bounded exponential backoff. Never exits the process
Rescan(ctx) (bool, error) Re-derive from disk, download nothing. Makes an in-place repair observable
Ready() (bool, Reason) The verdict, and why it is withheld
Active() (State, bool) The diagnostic record, and whether a version is active
PathEntry() string The directory to lead PATH with, or ""
Path() string The absolute primary artifact, or ""

Reason is an enum, not a message: ReasonReady, ReasonInstalling, ReasonRetrying, ReasonUnavailable, ReasonAssertion. Your program owns the wording it shows its own users; the library owns only the distinction. Typed errors for classification: ErrDigestMismatch, ErrUnsupportedArch, ErrNoVersion, ErrVersionMismatch.

Assertions, and why one is mandatory

An Assertion is a bounded command run against the installed artifact — the full argv after the artifact's path, so the library needs to know nothing about how your package is configured. Required assertions run twice: against the staged artifact before publication, so a candidate they cannot hold on never becomes a version directory, and against the active artifact on every pass, because an assertion's effect usually lives in the package's own mutable configuration and cannot be remembered. A required failure withholds readiness; anything else only warns.

Release.Mandatory is the set a deployment cannot weaken, reword or drop: whatever a caller passes, each one is forced Required and substituted with the profile's own argv. A profile that declares none is refused at construction. The most common thing to assert is that the package will not update itself — a self-replacing binary invalidates the digest you verified — and "this package needs no post-install guarantee" is indistinguishable from "the profile author forgot". A package that genuinely needs no gate declares its cheapest positive check instead, which makes it a claim someone had to write on purpose.

Included profiles

pinstall names no vendor. A ready-made profile for one release ships alongside it:

  • pinstall/kirocli — the kiro-cli release: URL shape, architecture tokens, in-archive installer, probe argv, licence notice, and the auto-update assertion. Also kirocli.Setting(key, bool) / kirocli.SettingRaw(key, value) for building assertions in that package's own settings grammar.
mgr, err := pinstall.New(&pinstall.Config{
	Release: kirocli.Release(),
	Version: version,
	Digests: map[string]string{"amd64": amd64SHA, "arm64": armSHA},
	Root:    toolsDir,
	LinkDir: "bin",
	Assert:  []pinstall.Assertion{kirocli.Setting("telemetry.enabled", false)},
})

The pin stays with the consumer, so whatever bumps your version and digests keeps working unchanged.

Sweeping a previous installer's layout

If your program used to install the same package a different way, Config.Purge removes what that installer left behind — once per volume, recorded by a marker file. It is injected rather than built in, because the residue is a fact about one deployment's history, not about the package.

Every target is removed only when what is on disk has the shape the old installer left there: a declared artifact must be a regular file, a staging tree must be a directory. A directory holding a convenience link is often co-owned, and another installer publishes symlinks there; a symlink at a swept path is somebody else's live pointer and is refused rather than deleted. Deletes are confined by os.Root, so a redirected entry cannot reach outside Root. A target that could not be removed withholds the marker, so the next pass retries instead of recording a job it did not finish.

Unsupported by Design

Deliberate non-goals, not TODOs:

Not included Rationale
Signature or attestation verification The trust anchor is the digest you pinned, which you obtained out of band. Verify a signature where you produce the pin, not where you consume it
Resolving "latest" A resolved version is not a pin. Whatever bumps your version and digest literals owns that decision; this library installs exactly what it was told
Rollback, journals, backups Nothing is ever overwritten in place, so there is no partial-promotion window to recover from. The retained predecessor is the recovery mechanism
Archive formats beyond zip One Unpacker is shipped and tested. A tar.gz consumer supplies a function; an enum with one implemented value would be a partially built public surface
Windows and macOS The publish protocol relies on same-filesystem rename plus fsync of a directory, and the confined deletes use os.Root. Linux only, like the rest of this account's libraries
Live in-process version upgrades Retention assumes a new pin arrives by restarting the consumer. Enabling live upgrades would require per-version leases before a directory could be pruned

Contributing

Issues and PRs are welcome. See CONTRIBUTING.md for the conventions and how to run the checks locally.

Disclaimer

This project is built with care and follows security best practices, but it is intended for personal / self-hosted use. No guarantees of fitness for production environments. Use at your own risk.

This project was built with AI-assisted tooling using Claude, GPT, and Kiro. The human maintainer defines architecture, supervises implementation, and makes all final decisions.

License

GPL-3.0. See LICENSE.

Documentation

Overview

Package pinstall installs, activates and maintains a digest-pinned upstream release.

The unit of installation is <Root>/<Name>-versions/<version>/. It is populated only from a digest-verified archive, published by a single same-filesystem rename, and marked by a ".complete" sentinel written LAST — so an interrupted install is detectable by absence of the sentinel and never becomes a selection candidate. Nothing at all is placed under Root before the archive digest matches the pin.

Every start re-probes the artifact it is about to activate, re-asserts the caller's assertions against it, retains N predecessors and reports a readiness verdict. Nothing here exits the process, reads the environment, or performs work at import time: a failed install is returned or logged so the caller's own surfaces stay alive and the installation can be repaired in place.

A caller builds a Release once (everything true of the package, independent of any deployment), then one Config per deployment (the pin, the digests, the root and the local policy), then calls New:

mgr, err := pinstall.New(&pinstall.Config{
	Release: myprofile.Release(),
	Version: "1.4.2",
	Digests: map[string]string{"amd64": amd64SHA256, "arm64": arm64SHA256},
	Root:    "/var/lib/example/tools",
})
if err != nil {
	return err
}
if err := mgr.EnsureWithRetry(ctx); err != nil {
	log.Printf("install failed, serving degraded: %v", err)
}
if ready, why := mgr.Ready(); !ready {
	return fmt.Errorf("not ready: %s", why)
}
cmd := exec.CommandContext(ctx, mgr.Path(), "--help")

Durability, version selection, retry, retention and the readiness verdict are the library's and are deliberately not configurable. What varies between packages is data: the URL shape, the architecture tokens, the in-archive installer, the probe argv, the assertions.

Linux only. The publish protocol relies on same-filesystem rename and fsync of a directory, and the confined deletes use os.Root.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrDigestMismatch reports that the downloaded archive is not the artifact
	// the pinned digest names. Nothing is placed under Root when it is returned.
	ErrDigestMismatch = errors.New("archive SHA-256 mismatch")
	// ErrUnsupportedArch reports an architecture the release publishes no
	// archive for, or one with no pinned digest.
	ErrUnsupportedArch = errors.New("unsupported architecture")
	// ErrNoVersion reports that no complete version is installed and none could
	// be installed, so there is nothing to activate.
	ErrNoVersion = errors.New("no complete version is installed")
	// ErrVersionMismatch reports that a candidate artifact answered the version
	// probe with something other than the version its directory and sentinel
	// claim.
	ErrVersionMismatch = errors.New("binary reported a version its install directory does not claim")
)

Errors callers can classify. Everything else is wrapped with context.

Functions

func LastFieldOfFirstLine

func LastFieldOfFirstLine(out string) string

LastFieldOfFirstLine is the default Release.ParseVersion: the last whitespace-separated field of the first line, and "" when there is none. It covers the common `<name> <version>` and bare `<version>` shapes and never panics on arbitrary input.

A release whose probe prints something else — JSON, a multi-column banner — sets its own parser, because probe output has no universal shape.

Example

ExampleLastFieldOfFirstLine shows the default version parser, which covers the two common probe shapes and returns "" when there is nothing to take.

package main

import (
	"fmt"

	"github.com/cplieger/pinstall"
)

func main() {
	fmt.Printf("%q\n", pinstall.LastFieldOfFirstLine("widget 1.4.2\n"))
	fmt.Printf("%q\n", pinstall.LastFieldOfFirstLine("1.4.2\n"))
	fmt.Printf("%q\n", pinstall.LastFieldOfFirstLine("widget 1.4.2\nbuilt 2026-01-01\n"))
	fmt.Printf("%q\n", pinstall.LastFieldOfFirstLine("\n"))

}
Output:
"1.4.2"
"1.4.2"
"1.4.2"
""

func UnpackZip

func UnpackZip(ctx context.Context, archive, dir string) error

UnpackZip is the shipped Unpacker: archive/zip with traversal, entry-count and total-size guards. It is used whenever Release.Unpack is nil.

Executable bits survive (an in-archive installer has to run) but nothing wider than owner-write does, because the extracted tree lands on a persistent volume.

Types

type ArchiveInstaller

type ArchiveInstaller struct {
	// Path is the script's path relative to the extraction directory
	// (e.g. "pkg/install.sh").
	Path string
	// HomeEnv is the environment variable pointed at the private staging home.
	// Empty means HOME.
	HomeEnv string
	// Args is the argv passed to the script (e.g. {"--no-confirm"}).
	Args []string
	// Timeout bounds the run. Zero uses two minutes.
	Timeout time.Duration
}

ArchiveInstaller describes an installer script shipped inside the archive. Its exit code is deliberately not fatal: an upstream installer commonly touches shell profiles and other surfaces that legitimately fail in a minimal container, so what decides is whether the artifacts it produced pass the staged gates.

type Assertion

type Assertion struct {
	// Name identifies the assertion in logs and is the key
	// [Release.Mandatory] overrides on. It is not passed to the artifact.
	Name string
	// Args is the full argv after the artifact path
	// (e.g. {"settings", "app.disableAutoupdates", "true"}).
	Args []string
	// Required marks an assertion whose failure is integrity-relevant.
	Required bool
}

Assertion is one bounded command run against the installed artifact on every start, and against the STAGED artifact before publication when Required. Args is the full argv after the artifact path, so the library needs no knowledge of the package's configuration grammar.

A Required failure withholds readiness (and, before publication, refuses to publish). Anything else only warns.

type Config

type Config struct {
	// Digests maps a GOARCH to the lowercase hex SHA-256 of that
	// architecture's archive. The resolved architecture must have an entry.
	Digests map[string]string
	// Purge sweeps a previous installer's layout once. Nil skips it.
	Purge *Purge
	// Assert are the assertions re-asserted against the active artifact on
	// every start. [Release.Mandatory] is merged in, so a required assertion
	// cannot be dropped here.
	Assert []Assertion
	// Require names artifacts a version directory MUST hold to count as
	// complete. The release's primary artifact is always included.
	Require []string
	// Release is the package profile.
	Release Release
	// Version is the pin, validated against a path- and URL-safe character set
	// because it is interpolated into both a URL and a filesystem path.
	Version string
	// Root is the persistent installation root: the only tree this package
	// reads, writes or deletes.
	Root string
	// GOARCH selects the architecture. Empty resolves it from runtime.GOARCH.
	GOARCH string
	// URLTemplate overrides [Release.URLTemplate], for a mirror or a test
	// server. Empty uses the release's own template.
	URLTemplate string
	// LinkDir is a directory under Root holding a NON-AUTHORITATIVE
	// convenience symlink at the active artifact, for an operator shelling
	// into the environment. Empty publishes none. Nothing in this package ever
	// reads the link.
	LinkDir string
	// Optional names artifacts installed when the archive provides them, and
	// only warned about when it does not. It closes the pointer-bearing fields
	// rather than sitting beside Require only because a trailing slice costs the
	// garbage collector less to scan.
	Optional []string
	// Retain is how many predecessors to keep besides the active version. Zero
	// uses 1, which is what makes a bad activation recoverable without a
	// rollback journal.
	Retain int
	// RetryBackoff is the first [Manager.EnsureWithRetry] backoff; it doubles
	// per attempt up to a ten minute cap. Zero uses 30s.
	RetryBackoff time.Duration
	// MaxAttempts bounds [Manager.EnsureWithRetry]. Zero uses 4. The retries
	// are bounded deliberately: an endless loop re-downloading a large archive
	// is worse than a visible failure an operator can repair.
	MaxAttempts int
	// Untrusted records that Root was writable by others. A sentinel is
	// trivially forgeable, unlike a digest, so when it is set no pre-existing
	// version directory may be activated: only a version THIS process
	// installed from a verified archive counts.
	Untrusted bool
}

Config is one deployment of one Release: the pin, the digests, the root and the local policy. The zero value is not usable — call New.

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

Manager installs, activates and maintains one pinned release. It is safe for concurrent use: Manager.Ensure and Manager.Rescan serialise against each other, while the readers never block behind an install.

func New

func New(cfg *Config) (*Manager, error)

New validates cfg and returns a manager.

It resolves the architecture from runtime.GOARCH when Config.GOARCH is empty, requires a well-formed digest for the resolved architecture, merges Release.Mandatory into Config.Assert, and refuses a profile that declares no mandatory assertions. The caller's Config is not modified, and every fact this manager depends on is copied, so a later mutation of the caller's maps or slices cannot change its behaviour.

Example

ExampleNew builds a manager for one deployment of the widget release. The pin and the digests come from wherever the consumer keeps them; nothing is downloaded until Ensure runs.

package main

import (
	"fmt"
	"strings"
	"time"

	"github.com/cplieger/pinstall"
)

// widgetRelease is the profile a consumer of a fictional "widget" release writes
// once. Everything in it is true of the package regardless of where it is deployed.
func widgetRelease() pinstall.Release {
	return pinstall.Release{
		Name:        "widget",
		Binary:      "widget-cli",
		URLTemplate: "https://widgets.example/dl/{arch}/widget_{version}.zip",
		ArchTokens:  map[string]string{"amd64": "linux-64", "arm64": "linux-arm"},
		Installer: &pinstall.ArchiveInstaller{
			Path:    "widget/install.sh",
			Args:    []string{"--no-confirm"},
			Timeout: 2 * time.Minute,
		},
		ArtifactDir: ".local/bin",
		ProbeArgs:   []string{"--version"},
		Mandatory: []pinstall.Assertion{

			{Name: "autoupdate", Args: []string{"config", "set", "autoupdate", "off"}},
		},
	}
}

func main() {
	mgr, err := pinstall.New(&pinstall.Config{
		Release: widgetRelease(),
		Version: "1.4.2",
		Digests: map[string]string{
			"amd64": strings.Repeat("a", 64),
			"arm64": strings.Repeat("b", 64),
		},
		Root:    "/var/lib/example/tools",
		GOARCH:  "amd64",
		LinkDir: "bin",
		Require: []string{"widget-helper"},
		Assert: []pinstall.Assertion{
			{Name: "telemetry", Args: []string{"config", "set", "telemetry", "off"}},
		},
		Retain: 2,
	})
	if err != nil {
		fmt.Println("config error:", err)
		return
	}

	// Before the first Ensure nothing is active, and the reason says why.
	ready, why := mgr.Ready()
	fmt.Println("ready:", ready, "reason:", why)
	fmt.Printf("path: %q\n", mgr.Path())

}
Output:
ready: false reason: installing
path: ""
Example (MissingMandatory)

ExampleNew_missingMandatory shows the one construction error a profile author is most likely to hit: a release with no mandatory assertion is refused, so a guarantee cannot be lost by omitting it.

package main

import (
	"fmt"
	"strings"
	"time"

	"github.com/cplieger/pinstall"
)

// widgetRelease is the profile a consumer of a fictional "widget" release writes
// once. Everything in it is true of the package regardless of where it is deployed.
func widgetRelease() pinstall.Release {
	return pinstall.Release{
		Name:        "widget",
		Binary:      "widget-cli",
		URLTemplate: "https://widgets.example/dl/{arch}/widget_{version}.zip",
		ArchTokens:  map[string]string{"amd64": "linux-64", "arm64": "linux-arm"},
		Installer: &pinstall.ArchiveInstaller{
			Path:    "widget/install.sh",
			Args:    []string{"--no-confirm"},
			Timeout: 2 * time.Minute,
		},
		ArtifactDir: ".local/bin",
		ProbeArgs:   []string{"--version"},
		Mandatory: []pinstall.Assertion{

			{Name: "autoupdate", Args: []string{"config", "set", "autoupdate", "off"}},
		},
	}
}

func main() {
	release := widgetRelease()
	release.Mandatory = nil

	_, err := pinstall.New(&pinstall.Config{
		Release: release,
		Version: "1.4.2",
		Digests: map[string]string{"amd64": strings.Repeat("a", 64)},
		Root:    "/var/lib/example/tools",
		GOARCH:  "amd64",
	})
	fmt.Println(strings.SplitN(err.Error(), ";", 2)[0])

}
Output:
pinstall: Release.Mandatory is empty

func (*Manager) Active

func (m *Manager) Active() (State, bool)

Active returns the diagnostic state record and whether a version is active.

func (*Manager) Ensure

func (m *Manager) Ensure(ctx context.Context) error

Ensure brings the pinned version online and is idempotent: on a start that already has the pin complete and activatable it downloads nothing and still re-asserts every assertion.

The order is the design. The one-shot purge runs first (it deletes a previous layout outright, so nothing downstream can be fooled by it), then partial directories go (a partial is never a selection candidate), then selection probes each candidate's own version before accepting it, then — only when the pin is not already activatable — the install runs, then the assertions are re-asserted against whatever was selected, then the convenience link is republished, and only then may pruning run.

func (*Manager) EnsureWithRetry

func (m *Manager) EnsureWithRetry(ctx context.Context) error

EnsureWithRetry drives Manager.Ensure with bounded exponential backoff. A single one-shot attempt would leave a failed first start withholding readiness forever with no further effort. It never exits the process, and it returns the last error after the attempts are exhausted.

func (*Manager) Path

func (m *Manager) Path() string

Path returns the absolute path of the active primary artifact, or "" when no version is active. This — never the convenience link — is what a consumer runs.

func (*Manager) PathEntry

func (m *Manager) PathEntry() string

PathEntry returns the absolute directory to lead PATH with, or "" when no version is active. It holds only this release's own artifacts, so leading with it shadows nothing else.

func (*Manager) Ready

func (m *Manager) Ready() (ready bool, why Reason)

Ready reports whether the installation may be used, and why not when it may not.

It is true only when a version is active AND this process asserted every required assertion against that exact artifact. The persisted State.AssertionsOK is never consulted: an assertion's effect lives in the package's own mutable configuration, so remembering that it once succeeded is stale evidence.

func (*Manager) Rescan

func (m *Manager) Rescan(ctx context.Context) (bool, error)

Rescan re-derives the active version from what is on disk right now, without downloading anything, and re-asserts the assertions. It makes a repair performed in place — an operator restoring a version directory, or replacing a wedged artifact — observable without a fresh process. It returns whether a version is active afterwards.

type Purge

type Purge struct {
	// Artifacts are paths relative to Root, removed only when they are regular
	// files.
	Artifacts []string
	// StagePrefix matches orphan staging trees directly under Root, removed
	// only when they are directories. Empty matches nothing.
	StagePrefix string
	// Marker is a dot-file under Root recording that the sweep completed, so it
	// runs once per volume rather than on every start. Empty reruns it every
	// start.
	Marker string
	// Names are entries directly under LinkDir, removed only when they are
	// regular files. A symlink at one of these paths belongs to another owner.
	Names []string
}

Purge is the one-shot sweep of a layout a PREVIOUS installer left on this volume. It is injected rather than built in because the residue is a fact about one deployment's history, not about the package: two consumers of the same upstream release can have arrived from different installers.

Each target is removed only when what is on disk has the SHAPE the old installer left there, so another owner's entry at the same path is refused rather than deleted.

type Reason

type Reason uint8

Reason explains a withheld readiness verdict. It is an enum rather than a string because the wording a consumer shows its own users is the consumer's: the library owns only the distinction between the states.

const (
	// ReasonReady means readiness is not withheld. Its String is empty.
	ReasonReady Reason = iota
	// ReasonInstalling means the first attempt of this process is still in
	// flight and no version has ever been activated.
	ReasonInstalling
	// ReasonRetrying means an attempt failed and another one is scheduled.
	ReasonRetrying
	// ReasonUnavailable means no version is active and no further attempt is
	// scheduled. Only a repair plus [Manager.Rescan], or a fresh process, can
	// clear it.
	ReasonUnavailable
	// ReasonAssertion means a version is active but a Required assertion could
	// not be asserted against it, so a guarantee the install depends on does
	// not hold.
	ReasonAssertion
)

The readiness reasons, in the order a start walks them.

func (Reason) String

func (r Reason) String() string

String returns a short, package-agnostic description of the reason, and "" for ReasonReady.

type Release

type Release struct {
	// Unpack extracts the verified archive. A nil Unpack uses [UnpackZip].
	Unpack Unpacker
	// Installer describes an installer script shipped INSIDE the archive. A nil
	// Installer means the archive already holds the artifacts.
	Installer *ArchiveInstaller
	// ParseVersion extracts a version from the probe's output. A nil
	// ParseVersion uses [LastFieldOfFirstLine].
	ParseVersion func(out string) string
	// ArchTokens maps a GOARCH to the token the publisher uses in its URLs
	// (e.g. "amd64" -> "x86_64-linux"). An architecture absent from the map is
	// [ErrUnsupportedArch].
	ArchTokens map[string]string
	// ProbeArgs is the argv that makes the primary artifact print its version
	// (e.g. {"--version"}). Required: every start probes the artifact it is
	// about to activate, so a release with no probe cannot be verified.
	ProbeArgs []string
	// Name is the package identity. It fixes the versions root
	// (<Root>/<Name>-versions), the state file (<Root>/<Name>-state.json) and
	// the sentinel's claim. It is NOT the artifact name — see Binary.
	Name string
	// Binary is the primary artifact: the file the version probe runs, the file
	// the convenience link points at, and an always-present member of the
	// required set. Empty defaults to Name, which covers the common case; a
	// package named "aws-cli" that ships a binary called "aws" sets both.
	Binary string
	// URLTemplate is the archive URL, carrying the {version} and {arch}
	// placeholders (e.g. "https://example.invalid/{version}/pkg-{arch}.zip").
	// {arch} is substituted with the ArchTokens entry for the resolved GOARCH.
	URLTemplate string
	// ArtifactDir is where the artifacts land, relative to the installer's
	// private home when Installer is set and to the extraction directory
	// otherwise. Empty means the root of whichever of the two applies.
	ArtifactDir string
	// Notice is logged once per install attempt when non-empty: the licence
	// acknowledgement a proprietary upstream requires. Profile-owned, because
	// the library must not carry one vendor's licence text.
	Notice string
	// Mandatory are the assertions a deployment cannot configure away. Each one
	// is forced Required and merged over any caller assertion with the same
	// Name, so an integrity gate the package depends on (a self-update switch,
	// a telemetry kill switch) cannot be lost by a deployment's omission.
	//
	// At least one is required: [New] refuses a profile that declares none,
	// because "this package needs no post-install guarantee" and "the profile
	// author forgot" are indistinguishable from the outside.
	//
	// It closes the field list rather than sitting beside ProbeArgs only because
	// a trailing slice costs the garbage collector less to scan.
	Mandatory []Assertion
}

Release is everything true of the PACKAGE, independent of any deployment: the profile a consumer builds once and reuses across every environment. It is data, not policy — durability, version selection, retry, retention and the readiness verdict belong to the library and are not configurable.

type State

type State struct {
	UpdatedAt     time.Time `json:"updated_at"`
	ActiveVersion string    `json:"active_version"`
	Dir           string    `json:"dir"`
	Pinned        string    `json:"pinned"`
	LastError     string    `json:"last_error,omitempty"`
	AssertionsOK  bool      `json:"assertions_ok"`
}

State is the manager's persisted record, written to <Root>/<Name>-state.json. It is DIAGNOSTIC ONLY: every field is there for an operator reading the volume, and none of them is an input to Manager.Ready. AssertionsOK in particular records the last result as history — the assertion THIS process performed is the only thing that gates readiness, because an assertion's effect lives in the package's own mutable configuration rather than in the immutable version directory.

type Unpacker

type Unpacker func(ctx context.Context, archive, dir string) error

Unpacker extracts a verified archive into dir.

A custom implementation MUST refuse absolute and traversing entry names rather than sanitising them (a legitimate archive has no such entry, so rewriting one hides the archive that carries it), and MUST bound both the entry count and the total bytes written. The archive it is handed has already been proved against the pinned digest.

Directories

Path Synopsis
Package kirocli is the pinstall profile for the kiro-cli release.
Package kirocli is the pinstall profile for the kiro-cli release.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL