tooldl

package module
v0.21.1 Latest Latest
Warning

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

Go to latest
Published: Oct 16, 2026 License: BSD-3-Clause-Clear Imports: 20 Imported by: 0

README

sasayaki-tooldl (tooldl)

tooldl finds, downloads, verifies and caches external command-line tools (ffmpeg, yt-dlp, whisper builds, and so on) for desktop apps. You describe a tool once, and tooldl makes sure it is available: it uses a copy already on PATH if there is a good one, and otherwise downloads a pinned, checksum-verified build into a per-user directory.

It has a Wails v2 adapter (wailsdl) that exposes the whole flow to your frontend as bound methods and events.

Status and maintenance

tooldl is written for sasayaki and is only really maintained for use with it. Changes are driven by what sasayaki needs.

It is built to be general purpose, so it should be reasonably stable in other projects too. That is an observation, not a promise: nothing here is a guarantee of fitness, stability or support, and the author accepts no liability from your use of it (see the license).

Repository: https://codeberg.org/its-astro/sasayaki-tooldl

Install

go get codeberg.org/its-astro/sasayaki-tooldl@latest
go get codeberg.org/its-astro/sasayaki-tooldl/wailsdl@latest   # only if you use Wails

The core package has no Wails dependency; only wailsdl imports it.

Concepts

  • Manager: owns any number of tools. Create it, Register tools, call Start(ctx) from your app's startup hook.
  • Config: describes one tool: name, version, the binaries it provides, and where each one downloads from.
  • Binary / Source: a binary has one Source (URL, SHA256, archive details) per GOOS/GOARCH.
  • Candidate: an existing, complete set of binaries that could satisfy a tool (found on PATH, or in the managed install dir).
  • Emitter: a two-method interface used to send events and logs to your UI.

Quick start

import tooldl "codeberg.org/its-astro/sasayaki-tooldl"

mgr := tooldl.NewManager(tooldl.Options{AppName: "myapp"})

err := mgr.Register(tooldl.Config{
    Name:    "yt-dlp",
    Version: "2024.08.06",
    Mode:    tooldl.AutoInstall,
    Binaries: []tooldl.Binary{{
        Name:       "yt-dlp",
        VerifyArgs: []string{"--version"},
        Sources: map[string]tooldl.Source{
            "linux/amd64": {URL: "https://example.com/yt-dlp_linux", SHA256: "..."},
        },
    }},
})

mgr.Start(ctx)

if err := mgr.Ensure("yt-dlp"); err != nil {
    // declined, or the install failed
}
path := mgr.Path("yt-dlp", "yt-dlp")

Using it with Wails

Wails cannot bind methods that take a context.Context, so the Manager itself is not bound. wailsdl.New returns the manager plus a small Bindings type that is safe to bind.

import (
    tooldl "codeberg.org/its-astro/sasayaki-tooldl"
    "codeberg.org/its-astro/sasayaki-tooldl/wailsdl"
)

mgr, bindings := wailsdl.New(tooldl.Options{AppName: "myapp"})

cfg := myToolConfig()
cfg.OnDecline = func() { wailsruntime.Quit(app.ctx) } // optional
if err := mgr.Register(cfg); err != nil { panic(err) }

wails.Run(&options.App{
    OnStartup: func(ctx context.Context) {
        app.ctx = ctx
        mgr.Start(ctx) // must run before the frontend calls EnsureTool
    },
    Bind: []any{app, bindings},
})

Frontend:

import {
  EnsureTool,
  RespondToInstallPrompt,
} from "../wailsjs/go/wailsdl/Bindings";
import { EventsOn } from "../wailsjs/runtime/runtime";

// Register listeners before calling EnsureTool.
EventsOn("mytool:missing", () => showModal()); // buttons call RespondToInstallPrompt('mytool', true | false)
EventsOn("mytool:progress", (p) => updateBar(p));
EventsOn("mytool:error", (msg) => showError(msg));

await EnsureTool("mytool"); // resolves when ready; rejects on decline or failure

The generated JS import path depends on what Wails generates for your project, so check your wailsjs folder.

Bound methods: EnsureTool(name), RespondToInstallPrompt(name, accept), ToolPath(name, binary), ToolPaths(name).

Manager API

Method Description
NewManager(Options) Create a manager.
Register(Config) error Validate and add a tool.
Start(ctx) Store the app context. Cancelling it aborts in-flight downloads. Must be called before Ensure.
Ensure(name) error Find or install the tool. Blocks until ready.
Respond(name, accept) Answer the install prompt (PromptUser mode).
Check(ctx, name) Report whether the tool is usable. Never installs or prompts.
Path(name, binary) Resolved path of one binary ("" until Ensure succeeds).
Paths(name) Copy of the full binary-name to path map.
Options
Field Description
BaseDir A folder path to install tools into, used exactly as given.
AppName Your app's name. Used to pick a standard per-user folder when BaseDir is not set.
Emitter Optional. Sends events and log messages to your UI. Without one, they are dropped.

You must set BaseDir or AppName. If you set neither, Register returns ErrNoBaseDir.

Where do tools install?

Every tool is installed to:

<base>/<DirName>/<Version>/

DirName and Version come from each tool's Config. DirName defaults to the tool's Name. The <base> part is decided by BaseDir and AppName:

You set <base> is
only AppName <per-user data dir>/<AppName>/bin
only BaseDir exactly BaseDir
both BaseDir (AppName is ignored)
neither not allowed (ErrNoBaseDir)

AppName is the easy option. You give a name, and tooldl puts tools in the standard per-user data folder for the operating system:

tooldl.Options{AppName: "myapp"}
// A tool named "ffmpeg" at version "6.1" installs to:
//   Linux:   ~/.local/share/myapp/bin/ffmpeg/6.1/   (or $XDG_DATA_HOME/myapp/bin/...)
//   macOS:   ~/Library/Application Support/myapp/bin/ffmpeg/6.1/
//   Windows: %LOCALAPPDATA%\myapp\bin\ffmpeg\6.1\

This is a data folder rather than a cache folder, because the operating system may clear caches and users would have to download the tools again.

BaseDir is for when you want to choose the location yourself, for example a portable app that keeps tools beside its executable, or a temporary folder in tests:

tooldl.Options{BaseDir: "/opt/myapp/tools"}
// A tool named "ffmpeg" at version "6.1" installs to:
//   /opt/myapp/tools/ffmpeg/6.1/

AppName is only used to build this default path. It does not affect event names, logging, or which tools are found.

Because the version is part of the path, changing a tool's Version makes tooldl look in a new, empty folder and download it again, leaving the old version where it was. The same happens if you change BaseDir, AppName or DirName: copies installed under the old path are no longer found. This folder is also the second place Ensure looks for an existing tool (after PATH).

Config
Field Description
Name Unique ID. Default event prefix and install dir name. Must be a valid single path segment (see below).
Version Part of the install path. Bump it to force a clean re-download.
Binaries The executables the tool provides (at least one).
Mode PromptUser (default) or AutoInstall.
ShouldInstall Optional func(ctx, Candidate) bool that can reject an existing install.
SkipSystem Ignore PATH; only use the managed dir.
OnDecline Called when the user declines in PromptUser mode.
EventPrefix Overrides the event name prefix.
DirName Overrides the install dir name.
ForceInstallEnv Name of an env var that, when set, ignores existing installs. A testing aid.
Binary and Source

Binary: Name (the name you would type in a terminal, without an extension), Sources (map keyed by "GOOS/GOARCH", e.g. "linux/amd64"), and VerifyArgs (run after install as a smoke test; empty skips it).

Source:

Field Description
URL Required.
SHA256 Required. Hex hash of the downloaded file (the archive, not the extracted binary).
Format FormatAuto (default, inferred from the URL), FormatRaw, FormatZip or FormatTarGz.
Member Base name of the file to pull out of an archive. Defaults to the binary's Name (plus .exe on Windows). Ignored for raw downloads.
Validation

Register rejects a Config whose values would be unsafe to use in file paths. Name, Version, DirName (if set) and every Binary.Name must each be a single valid path segment:

  • not empty, . or ..
  • none of / \ : * ? " < > | or a NUL character
  • no trailing dot or space (Windows strips these, so 6.1. and 6.1 would collide)

Binary names must also be unique within a tool, and each binary needs at least one Source. This keeps a typo from installing outside the base directory.

How Ensure works

  1. A newer call for the same tool supersedes one still waiting on the prompt or downloading (useful after a frontend hot reload).
  2. It looks for an existing install, as a complete set of binaries from a single place: PATH first (unless SkipSystem), then the managed dir.
  3. If ShouldInstall is set, it is called once per candidate. Returning true rejects that candidate and moves on. The first accepted candidate is used and no install happens.
  4. If nothing was accepted, PromptUser mode emits <prefix>:missing and waits for Respond; AutoInstall mode goes straight to installing.
  5. Install checks that every binary has a source for the current platform with a URL and checksum, downloads each distinct asset once, verifies the SHA256, extracts the member, writes it atomically, and runs VerifyArgs.
  6. It records the paths and emits <prefix>:ready.

If a good install was already present, Ensure returns nil without emitting ready.

Version checks
cfg.ShouldInstall = func(ctx context.Context, c tooldl.Candidate) bool {
    // c.Source is tooldl.SourceSystem or tooldl.SourceManaged
    return majorVersion(ctx, c.Paths["ffmpeg"]) < 6
}

An old copy on PATH is rejected, the managed copy is considered next, and if neither passes, the tool is installed. ShouldInstall is not called when there are no candidates, and not called again after an install. Keep the check consistent with the version you pin, or you will reinstall on every launch.

Several binaries in one archive

Point multiple binaries at the same Source. Each distinct URL and checksum is downloaded once and then extracted per binary.

Different name inside the archive
tooldl.Source{URL: "https://.../whisper-bin-x64.zip", SHA256: "...", Member: "main.exe"}

Member is matched by base name anywhere in the archive. The file is installed under the binary's Name. If you set Member yourself, include the extension where needed.

Events

Event names use the tool's prefix (its Name unless EventPrefix is set).

Event Payload
<prefix>:missing none (PromptUser mode only)
<prefix>:progress {tool, stage, step, steps, percent, indeterminate}
<prefix>:error error string
<prefix>:ready map of binary name to path (after a fresh install only)

stage is "downloading" or "extracting". step and steps count binaries, not bytes. When indeterminate is true the server reported no size and percent is meaningless.

Errors

  • ErrDeclined: the user declined the install.
  • ErrNotStarted: Ensure was called before Start.
  • ErrUnknownTool: the name was never registered.
  • ErrNoBaseDir: neither BaseDir nor AppName was set.

Download, checksum and verification failures are returned as wrapped errors and also emitted as <prefix>:error, unless the call was superseded.

Behaviour worth knowing

  • Checksums are mandatory. There is deliberately no option to skip verification. You hash each asset when you pin a version.
  • Binaries are resolved as a set. If only some of a tool's binaries are on PATH, that counts as no PATH candidate, so you never mix binaries from different installs.
  • PATH lookup uses the bare name. On Windows the OS applies PATHEXT itself. Shell aliases and functions are not on PATH and will not be detected.
  • Ensure is serialised per tool. Different tools can install concurrently.
  • Check never prompts, so it is safe for a settings page showing "installed / not installed".

Layout

tooldl.go     Manager, Config, events, detection
install.go    download, checksum, extract, install
wailsdl/      Wails v2 adapter (separate module)

License

Licensed under The Clear BSD License. See LICENSE.

The software is provided as is, with no warranty, and the license's disclaimer of liability applies.

Documentation

Overview

Package tooldl downloads, verifies and caches external command-line tools (ffmpeg, yt-dlp, whisper.cpp, ...) for desktop apps.

It has no dependency on Wails. UI integration goes through the small Emitter interface; see the wailsdl subpackage for the Wails adapter.

Events emitted for a tool (prefix defaults to the tool's Name):

<prefix>:missing   (no payload)  -> ask the user whether to install (PromptUser mode only)
<prefix>:progress  {tool, stage, step, steps, percent, indeterminate}
<prefix>:error     string
<prefix>:ready     map[binaryName]path   (only after a fresh install)

Index

Constants

View Source
const (
	SourceSystem  = "system"  // found on PATH
	SourceManaged = "managed" // found in the library's own install dir
)

Source of a Candidate.

Variables

View Source
var (
	ErrDeclined    = errors.New("tooldl: install declined")
	ErrNotStarted  = errors.New("tooldl: manager not started (call Start first)")
	ErrUnknownTool = errors.New("tooldl: unknown tool")
	ErrNoBaseDir   = errors.New("tooldl: set Options.BaseDir or Options.AppName")
)

Functions

This section is empty.

Types

type Binary

type Binary struct {
	Name string // logical name, also the lookup name on PATH

	// Sources maps "GOOS/GOARCH" (e.g. "linux/amd64") to a download.
	Sources map[string]Source

	// VerifyArgs, if non-empty, are run after install as a smoke test,
	// e.g. []string{"-version"}. Catches wrong-arch binaries, missing Rosetta, etc.
	VerifyArgs []string
}

Binary is one executable provided by a tool.

type Candidate

type Candidate struct {
	Source string            // SourceSystem or SourceManaged
	Paths  map[string]string // binary name -> absolute path
}

Candidate is an existing, complete set of binaries that could satisfy a tool.

type Config

type Config struct {
	Name     string // unique; used for the default event prefix and install dir
	Version  string // part of the install dir; bump it to force a clean re-download
	Binaries []Binary

	Mode Mode

	// ShouldInstall lets callers veto existing installs. It is called once for
	// each complete candidate (PATH first, then the managed dir). Return true to
	// say "this one isn't good enough, I need an install" - e.g. its version is
	// too old. The first candidate for which it returns false is used. If no
	// candidate exists at all, an install is needed and this isn't called.
	// Nil means "any complete candidate is fine".
	ShouldInstall func(ctx context.Context, c Candidate) bool

	// SkipSystem ignores PATH and only uses the managed install dir.
	SkipSystem bool

	// OnDecline runs when the user answers "no" in PromptUser mode
	// (e.g. quit the app). Ensure still returns ErrDeclined afterwards.
	OnDecline func()

	// EventPrefix overrides the event name prefix. Default: Name.
	EventPrefix string

	// DirName overrides the install dir name under BaseDir. Default: Name.
	DirName string

	// ForceInstallEnv names an env var; when it is set (non-empty), existing
	// installs are ignored so the whole flow runs every launch. Testing aid.
	ForceInstallEnv string
}

Config describes one managed tool.

type Emitter

type Emitter interface {
	Emit(ctx context.Context, event string, payload any)
	Log(ctx context.Context, msg string)
}

Emitter delivers events to the frontend and logs. Implement it for your UI toolkit; wailsdl.Emitter is the Wails one.

type Format

type Format int

Format is how a download is packaged.

const (
	FormatAuto Format = iota // infer from the URL (.zip, .tar.gz/.tgz, otherwise raw)
	FormatRaw                // the download is the executable itself
	FormatZip
	FormatTarGz
)

type Manager

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

Manager owns any number of tools. Create it with NewManager, Register tools, then call Start from your app's startup hook.

func NewManager

func NewManager(opts Options) *Manager

func (*Manager) Check

func (m *Manager) Check(ctx context.Context, name string) (map[string]string, bool)

Check reports whether the tool is already usable, without installing or prompting. ShouldInstall is honoured.

func (*Manager) Ensure

func (m *Manager) Ensure(name string) error

Ensure resolves once every binary of the named tool is available. It returns an error if the user declines or the install fails. A newer call supersedes one still waiting on the prompt or downloading (useful after a frontend hot reload, where the old page can never answer).

func (*Manager) Path

func (m *Manager) Path(name, binary string) string

Path returns the resolved path of one binary, or "" before Ensure succeeds.

func (*Manager) Paths

func (m *Manager) Paths(name string) map[string]string

Paths returns a copy of the resolved binary paths for a tool.

func (*Manager) Register

func (m *Manager) Register(cfg Config) error

Register adds a tool. Do this before the frontend can call Ensure.

func (*Manager) Respond

func (m *Manager) Respond(name string, accept bool)

Respond answers the "missing" prompt (true = install, false = decline).

func (*Manager) Start

func (m *Manager) Start(ctx context.Context)

Start stores the app context (e.g. Wails' OnStartup ctx). Cancelling it aborts in-flight downloads.

type Mode

type Mode int

Mode controls whether the user is asked before installing.

const (
	// PromptUser emits "<prefix>:missing" and waits for Respond(name, bool).
	PromptUser Mode = iota
	// AutoInstall installs immediately with no modal.
	AutoInstall
)

type Options

type Options struct {
	// BaseDir is where tools are installed: <BaseDir>/<DirName>/<Version>.
	// If empty, it defaults to <xdg.DataHome>/<AppName>/bin. This is a per-user
	// data dir, not a cache dir, because the OS may clear caches.
	BaseDir string
	AppName string

	Emitter Emitter // optional
}

Options configures a Manager.

type Source

type Source struct {
	URL    string
	SHA256 string // hex of the downloaded file (the archive, not the extracted binary)
	Format Format

	// Member is the base name of the file to pull out of an archive.
	// Defaults to the binary's Name (+ ".exe" on Windows). Ignored for FormatRaw.
	Member string
}

Source describes one downloadable asset. Both URL and SHA256 are required: the library refuses to install anything it cannot verify.

Directories

Path Synopsis
wailsdl module

Jump to

Keyboard shortcuts

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