binkit

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Aug 1, 2026 License: MIT Imports: 22 Imported by: 0

README

binkit

Provision, verify, pin, and update external CLI binaries from Go.

Some tools you depend on aren't written in Go, so go tool can't manage them. binkit fetches them from GitHub releases, verifies them against a pinned SHA-256, caches them per version, and hands your program back a path.

path, err := resolver.Ensure(ctx, catalog.Typst)
if err != nil {
    return err
}
cmd := exec.CommandContext(ctx, path, "compile", "in.typ", "out.pdf")

The one-dependency promise

go.mod has exactly one require line: github.com/ulikunitz/xz, which is itself stdlib-only with zero transitive dependencies.

That dependency exists for one reason — many projects (Typst among them) ship .tar.xz, and the Go standard library has archive/tar, archive/zip, and compress/gzip but no xz decoder. Shelling out to tar -xJf would trade a compile-time dependency for a runtime environment dependency and break on Windows and minimal containers, so it isn't done.

Everything else is stdlib or deliberately hand-rolled: semver comparison instead of golang.org/x/mod, TTY detection instead of mattn/go-isatty, atomic directory rename instead of a file-locking library.

Pinning

tools.json lives in your repo and is checked in:

{
  "typst": {
    "version": "0.15.1",
    "repo": "typst/typst",
    "digests": {
      "linux/amd64":   "sha256:...",
      "darwin/arm64":  "sha256:...",
      "windows/amd64": "sha256:..."
    }
  }
}

Digests are captured for every platform at pin time, so a colleague on macOS gets a verified install from a lockfile generated on Linux.

Ensure never resolves "latest" and never silently reaches the network — a pinned version downloads by direct URL, which also means no GitHub API rate limit and no token. Only an explicit Update call changes the pin.

Update checks

Once every seven days, binkit checks whether a newer release exists and reports it — to stderr, and only when stderr is a terminal, so it never pollutes piped output or CI logs. When there is nowhere to show a notice the check is skipped entirely rather than performed and discarded.

binkit: typst 0.15.1 is pinned; 0.16.0 is available.
        run: massprep --update-tools typst

The second line appears only if you set Resolver.UpdateHint — binkit cannot know your CLI's flags.

A failed check backs off for an hour rather than consuming the seven-day window, so being offline for a week neither retries on every invocation nor silences the next real check. Nothing here can fail a build, and nothing is ever updated automatically. Set BINKIT_NO_UPDATE_CHECK=1, or Resolver.NoCheck, to disable.

Escape hatch

BINKIT_<TOOLNAME> (e.g. BINKIT_TYPST=/usr/bin/typst) short-circuits everything and uses that binary. Useful for CI images, Nix, and distro packages.

License

MIT

Documentation

Overview

Package binkit provisions, verifies, pins, and updates external CLI binaries that a Go program depends on but cannot build itself.

The Go toolchain can manage Go-built tools via "go tool", but nothing in the toolchain helps with a Rust or C binary you shell out to. binkit fills that gap: it downloads a pinned release from GitHub, verifies it against a recorded SHA-256, caches it by version, and returns a path.

Pinning

Pins live in a lock file — tools.json by convention — which belongs in the consuming project's repository. Resolver.Ensure reads the pin and never resolves "latest", never calls the GitHub API, and therefore never depends on API rate limits or a token. Only Resolver.Update changes a pin.

Design

binkit knows nothing about any particular tool. A Tool is plain data supplied by the caller; ready-made definitions live in the separate catalog subpackage, which this package deliberately does not import. Ensure returns a path and never executes anything — what to run is the caller's decision.

Index

Constants

View Source
const (
	// DefaultCheckEvery is the minimum interval between upstream update checks.
	DefaultCheckEvery = 7 * 24 * time.Hour

	// EnvNoUpdateCheck disables update checks when set to any non-empty value.
	EnvNoUpdateCheck = "BINKIT_NO_UPDATE_CHECK"
)
View Source
const (
	// EnvCacheDir overrides the cache location.
	EnvCacheDir = "BINKIT_CACHE"
)

Environment variables binkit consults.

Variables

View Source
var (
	ErrInvalidTool         = errors.New("binkit: invalid tool definition")
	ErrNotPinned           = errors.New("binkit: tool is not pinned")
	ErrNoDigest            = errors.New("binkit: no pinned digest for this platform")
	ErrDigestMismatch      = errors.New("binkit: digest mismatch")
	ErrUnsupportedPlatform = errors.New("binkit: tool is not published for this platform")
	ErrUnsupportedArchive  = errors.New("binkit: unsupported archive format")
	ErrBinaryNotInArchive  = errors.New("binkit: binary not found in archive")
	ErrAssetNotFound       = errors.New("binkit: release has no such asset")
)

Errors reported by this package. All are wrapped, so match with errors.Is.

View Source
var DefaultPlatforms = []Platform{
	{OS: "linux", Arch: "amd64"},
	{OS: "linux", Arch: "arm64"},
	{OS: "darwin", Arch: "amd64"},
	{OS: "darwin", Arch: "arm64"},
	{OS: "windows", Arch: "amd64"},
	{OS: "windows", Arch: "arm64"},
}

DefaultPlatforms is the set Resolver.Update records digests for. A tool that does not publish for one of these is skipped, so an over-broad list costs nothing.

Functions

This section is empty.

Types

type LockEntry

type LockEntry struct {
	Version string            `json:"version"`
	Repo    string            `json:"repo"`
	Digests map[string]string `json:"digests,omitzero"`
}

LockEntry is one tool's pinned state.

Digests are keyed by "GOOS/GOARCH" and hold the SHA-256 of the *release asset*, not of the extracted binary. They are captured for every platform the tool supports at pin time, so a lock file generated on Linux still yields a verified install on macOS.

type LockFile

type LockFile map[string]LockEntry

LockFile maps tool name to pin. It belongs in the consuming project's repository and is meant to be committed — it is what makes a build reproducible.

type Platform

type Platform struct {
	OS   string
	Arch string
}

Platform is a GOOS/GOARCH pair.

func (Platform) String

func (p Platform) String() string

type Resolver

type Resolver struct {
	// CacheDir overrides the cache location. Falls back to $BINKIT_CACHE, then to
	// <user cache dir>/binkit.
	CacheDir string

	// Lock is the path to the lock file. Defaults to "tools.json".
	Lock string

	// HTTP is the client used for all requests. Defaults to [http.DefaultClient].
	HTTP *http.Client

	// Platforms are the platforms Update records digests for. Defaults to
	// [DefaultPlatforms].
	Platforms []Platform

	// Now returns the current time. Defaults to [time.Now].
	Now func() time.Time

	// Stderr receives update notices. Defaults to [os.Stderr]. Notices never go to
	// stdout, and are suppressed entirely when the default stderr is not a terminal —
	// a CI log has no one to read them.
	Stderr io.Writer

	// CheckEvery is the minimum interval between upstream update checks. Defaults to
	// [DefaultCheckEvery].
	CheckEvery time.Duration

	// NoCheck disables update checks. The BINKIT_NO_UPDATE_CHECK environment variable
	// does the same for an end user.
	NoCheck bool

	// UpdateHint returns the command that updates the named tool, shown as a second
	// line of the update notice. binkit cannot know a consuming CLI's flags, so
	// without this the notice reports versions only.
	UpdateHint func(toolName string) string
	// contains filtered or unexported fields
}

Resolver installs tools. The zero value is usable: it caches under the user cache directory and reads tools.json from the working directory.

A Resolver is safe for concurrent use and must not be copied after first use.

func (*Resolver) Ensure

func (r *Resolver) Ensure(ctx context.Context, t Tool) (string, error)

Ensure returns the path to the pinned version of t, downloading and verifying it if it is not already cached.

It never reaches the network when the tool is already cached, and never contacts the GitHub API at all — a pinned version downloads by direct URL. A tool with no pin is an error rather than an implicit "fetch latest": changing what a build runs should be a deliberate, reviewable act.

func (*Resolver) Update

func (r *Resolver) Update(ctx context.Context, t Tool, version string) (string, error)

Update resolves a version — the latest release when version is empty — installs it, and rewrites the lock file.

Digests are recorded for every platform in Resolver.Platforms from the single release response, so one run on Linux produces a lock file that verifies correctly on macOS and Windows too.

type Tool

type Tool struct {
	// Name keys the cache and the lock file, and forms the per-tool environment
	// override. Required.
	Name string

	// Repo is the GitHub "owner/name" hosting the releases. Required.
	Repo string

	// Tag maps a version to its release tag. Optional; defaults to "v" + version.
	Tag func(version string) string

	// Asset returns the release asset filename for a version and platform. Returning
	// an error means the tool is not published for that platform, which Update treats
	// as "skip" rather than as a failure. Required.
	Asset func(version, goos, goarch string) (string, error)

	// BinaryPath returns the path of the executable inside the archive. Required.
	BinaryPath func(version, goos, goarch string) string
}

Tool describes an external binary and where to obtain it. It is data, not behaviour: the two function fields exist only because asset naming varies per project and cannot be expressed as a format string in general.

func (Tool) EnvKey

func (t Tool) EnvKey() string

EnvKey is the environment variable that overrides this tool entirely, e.g. BINKIT_TYPST. When set, Ensure returns its value untouched.

Directories

Path Synopsis
Package catalog holds ready-made binkit.Tool definitions for tools people commonly need, so a consuming project does not have to re-derive release asset naming.
Package catalog holds ready-made binkit.Tool definitions for tools people commonly need, so a consuming project does not have to re-derive release asset naming.

Jump to

Keyboard shortcuts

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