Compare commits

...

4 Commits

Author SHA1 Message Date
AJ ONeal
a5c265d8ee fix(installer-skill): revise docs per second adversarial review
- Move set -e/set -u inside __init_ (matches _example canonical form)
- Fix PS1 framework claim: template exists, provides helpers, but package
  script must download and extract itself
- Fix WEBI_SINGLE description: linking strategy, not default-deps rule
- Fix Pattern G pkg_link to use $pkg_src instead of $pkg_src_dir
- Fix Pattern H skeleton to match real pwsh (no bin/ subdir, uses pkg_link)
- Fix Pattern A WEBI_SINGLE description in PATTERNS.md
- Remove goreleaser from Pattern C representative list (it's Pattern A layout)
- Drop goreleaser man page from Pattern C man page location list
- Remove Python snippet from ARCHIVE-LAYOUTS.md zst inspection (use zstd -dc)
2026-03-12 03:22:51 -06:00
AJ ONeal
bd330897c1 fix(installer-skill): revise SKILL.md per first adversarial review
- Add __init_pkgname() wrapper as canonical install.sh structure
- Correct WEBI_SINGLE: linking strategy, not archive flatness
- Document pkg_src_bin/pkg_dst_bin as framework-derived (not set manually)
- Fix single-quoted glob bug in zsh completion check
- Add scope note: releases.js is a separate concern
- Explain why install.ps1 is self-contained (no PowerShell framework)
- Add Pattern D reference in skeletons section
- Clarify Pattern A vs F distinction
- Add completion dir name caveat to Pattern C skeleton
- Fix same single-quoted glob bug in PATTERNS.md rg example
2026-03-12 03:07:32 -06:00
AJ ONeal
80fc00de21 style(skills): use test instead of [ ] in POSIX shell examples 2026-03-12 02:55:54 -06:00
AJ ONeal
734455a6c5 docs(skills): add installer skill for writing install.sh and install.ps1
Covers the full workflow: inspect GitHub releases API to discover archive
layout, choose from 9 patterns (A-I), write POSIX shell and PowerShell
scripts, and identify classification/variant issues.

Reference files:
- PATTERNS.md: install.sh/ps1 skeletons for all 9 patterns
- ARCHIVE-LAYOUTS.md: real tar -t output for representative packages
- CLASSIFICATION.md: when to add variant tags, canonical vocab
2026-03-12 02:45:37 -06:00
4 changed files with 1302 additions and 0 deletions

442
_skills/installer/SKILL.md Normal file
View File

@@ -0,0 +1,442 @@
---
name: installer
description: >
Create or update install.sh and install.ps1 scripts for a webi package.
Use when adding a new package to webi-installers, or when an existing
install script needs to be updated to match a changed archive structure.
Covers discovering archive layout from GitHub releases, identifying the
right install pattern (AI), and writing both the POSIX shell and
PowerShell scripts that the webi framework calls.
Note: this skill covers install scripts only — writing releases.js /
releases.conf (the release-fetcher config) is a separate concern.
license: MIT
compatibility: Requires git, curl, tar. GitHub API access needed for
discovery phase. Designed for Claude Code in the webi-installers repo.
metadata:
author: AJ ONeal
version: "1.1"
---
# Webi Installer Skill
Write `install.sh` and `install.ps1` for a webi package. These scripts are
called by the webi framework **after** it has already downloaded and verified
the archive — your job is only to unpack and place the files.
> **Scope:** This skill covers `install.sh` and `install.ps1` only. A
> separate `releases.js` / `releases.conf` file is needed to tell webi where
> to fetch releases from. That config must already exist (or be written
> separately) before these install scripts are useful.
## Quick overview
1. [Discover the archive layout](#1-discover-the-archive-layout) — inspect
GitHub releases with `curl` + `tar -t` to understand what's inside.
2. [Choose the install pattern](#2-choose-the-install-pattern) — nine
patterns (AI) cover almost every real-world case.
3. [Write `install.sh`](#3-write-installsh) — POSIX shell, ~2040 lines.
4. [Write `install.ps1`](#4-write-installps1) — PowerShell, ~4060 lines.
5. [Check for classification issues](#5-check-for-classification-issues) —
look for variant assets, non-standard OS/arch naming, or installer .exe
files that need special handling.
Full reference: [`references/PATTERNS.md`](references/PATTERNS.md)
Archive layout details: [`references/ARCHIVE-LAYOUTS.md`](references/ARCHIVE-LAYOUTS.md)
Classification guide: [`references/CLASSIFICATION.md`](references/CLASSIFICATION.md)
---
## 1. Discover the archive layout
### Use the webi releases API (fastest, if the package already exists)
```sh
# JSON with all releases for a package
curl -s https://webinstall.dev/api/releases/bat.json | jq '.releases[:3]'
```
Each entry has `name` (filename), `version`, `os`, `arch`, `ext`, `download`.
### Or inspect GitHub releases directly
```sh
# List asset filenames for the latest release
curl -s "https://api.github.com/repos/sharkdp/bat/releases?per_page=3" \
| jq '.[0].assets[] | .name'
```
### Inspect what's inside an archive
Download one representative asset and list its contents **without extracting**:
```sh
# tar.gz / tar.xz
curl -fsSL "$DOWNLOAD_URL" | tar -tz
# tar.zst (modern systems — GNU tar / bsdtar both support this)
curl -fsSL "$DOWNLOAD_URL" | tar --zstd -tz
# zip
curl -fsSL "$DOWNLOAD_URL" -o /tmp/pkg.zip && unzip -l /tmp/pkg.zip
# bare binary (no archive extension, e.g. jq-linux-amd64)
# The file IS the binary — no unpacking needed. Set WEBI_SINGLE=true.
```
Look for:
- Is the binary at the top level or inside a subdirectory?
- Does the subdirectory name include the version and/or triplet?
- Are there completions (`completions/`, `autocomplete/`, `complete/`)?
- Are there man pages (`*.1`, `doc/*.1`, `man/man1/`)?
- Are there shared libraries (`.so`, `.dylib`, `.dll`) alongside the binary?
- Is the binary name different from the package command name?
See [`references/ARCHIVE-LAYOUTS.md`](references/ARCHIVE-LAYOUTS.md) for
what each pattern looks like, with real examples.
---
## 2. Choose the install pattern
| Pattern | Description | Examples |
|---------|-------------|---------|
| **A** | Bare binary (or binary+docs) at archive root | caddy, fzf, k9s, terraform |
| **B** | Binary inside a version/triplet-named subdirectory | delta, shellcheck, trip, xsv |
| **C** | Like B, plus shell completions and/or man pages | bat, fd, rg, sd, watchexec, zoxide |
| **D** | Binary + shared libraries (bundled) | ollama (Linux), psql, sass, syncthing |
| **E** | FHS-like layout (`bin/`, `share/man/`) | gh, pandoc |
| **F** | Renamed binary needing install-time rename | pathman, yq |
| **G** | Full SDK/toolchain (many files) | go, node, zig, flutter, julia |
| **H** | .NET runtime bundle | pwsh |
| **I** | Multi-binary distribution | dashcore, mutagen |
**Pattern A** is by far the most common (~28 packages). When in doubt,
download the archive and `tar -tz` it before writing a single line of code.
---
## 3. Write `install.sh`
The framework (`_webi/package-install.tpl.sh`) handles: user-agent detection,
version resolution, download, checksum verification, and PATH management.
Your script is **injected into** the framework and provides the
package-specific part: where to find the binary and how to move it.
### Script structure
Every `install.sh` wraps its definitions in an `__init_pkgname()` function
and immediately calls it. This prevents variable leakage when the script is
sourced by the framework:
```sh
#!/bin/sh
__init_toolname() {
set -e
set -u
####################
# Install toolname #
####################
pkg_cmd_name="toolname"
WEBI_SINGLE=true # if applicable — see below
pkg_dst_cmd="$HOME/.local/bin/toolname"
pkg_dst="$pkg_dst_cmd"
pkg_src_cmd="$HOME/.local/opt/toolname-v$WEBI_VERSION/bin/toolname"
pkg_src_dir="$HOME/.local/opt/toolname-v$WEBI_VERSION"
pkg_src="$pkg_src_cmd"
pkg_install() {
# ...
}
pkg_get_current_version() {
# ...
}
}
__init_toolname
```
### Variables
| Variable | Description |
|----------|-------------|
| `pkg_cmd_name` | The command name that ends up on `$PATH` |
| `pkg_dst_cmd` | Final destination: `~/.local/bin/<cmd>` (the symlink) |
| `pkg_dst` | Same as `pkg_dst_cmd` for single-binary packages; `~/.local/opt/<cmd>` for SDKs |
| `pkg_src_cmd` | Versioned binary: `~/.local/opt/<pkg>-v<ver>/bin/<cmd>` |
| `pkg_src_dir` | Versioned install dir: `~/.local/opt/<pkg>-v<ver>` |
| `pkg_src` | Same as `pkg_src_cmd` for single-binary packages; same as `pkg_src_dir` for SDKs |
**Framework-derived (set by the framework before calling `pkg_install` — do not set manually):**
- `pkg_src_bin``$(dirname "$pkg_src_cmd")` — the versioned `bin/` dir
- `pkg_dst_bin``$(dirname "$pkg_dst_cmd")``~/.local/bin`
### `WEBI_SINGLE`
`WEBI_SINGLE=true` affects the default values the framework uses for
`pkg_src` and `pkg_dst`, and how `webi_link()` creates the symlink:
- **With `WEBI_SINGLE=true`**: links the binary file directly:
`~/.local/bin/cmd → ~/.local/opt/cmd-vX.Y.Z/bin/cmd`
- **Without it (default)**: links the directory:
`~/.local/opt/cmd → ~/.local/opt/cmd-vX.Y.Z`
Set `WEBI_SINGLE=true` when using the conventional Pattern A skeleton
(where `pkg_src` and `pkg_dst` are not set to custom values). When you
explicitly assign all six variables yourself (as in Patterns BF),
`WEBI_SINGLE` is not strictly required but can still be set for clarity.
Pattern G (SDKs) and Pattern H (.NET bundles) do NOT use `WEBI_SINGLE`
they define `pkg_link()` manually because the whole directory tree must
be linked, not just a single binary.
### Required function: `pkg_install`
Moves files from the extracted archive into the versioned opt directory.
The framework has already extracted the archive into a temp directory and
`cd`'d into it before calling `pkg_install`.
```sh
pkg_install() {
mkdir -p "$pkg_src_bin"
mv ./tool-*/tool "$pkg_src_cmd"
chmod a+x "$pkg_src_cmd"
}
```
### Recommended function: `pkg_get_current_version`
Used to detect whether the package is already installed at the right version:
```sh
pkg_get_current_version() {
# 'tool --version' output: "tool 1.2.3 (rev abc)"
# trim to just the version number
tool --version 2>/dev/null | head -n 1 | cut -d' ' -f2
}
```
### Skeletons by pattern
**Pattern A** — binary at archive root (`WEBI_SINGLE=true`):
```sh
WEBI_SINGLE=true
pkg_install() {
mkdir -p "$pkg_src_bin"
mv ./"$pkg_cmd_name"* "$pkg_src_cmd"
chmod a+x "$pkg_src_cmd"
}
```
Use `$pkg_cmd_name*` as the glob — it matches the binary and avoids
accidentally moving LICENSE or README into the binary path.
**Pattern B** — binary inside a `tool-{ver}-{triplet}/` subdirectory:
```sh
WEBI_SINGLE=true
pkg_install() {
mkdir -p "$pkg_src_bin"
mv ./tool-*/tool "$pkg_src_cmd"
chmod a+x "$pkg_src_cmd"
}
```
**Pattern C** — like B, plus completions and man pages.
The completion directory and filename vary per package — always check
`tar -tz` output first. Common variants: `completions/`, `autocomplete/`,
`complete/`. See [`references/PATTERNS.md`](references/PATTERNS.md) for
a full example with guards:
```sh
WEBI_SINGLE=true
pkg_install() {
mkdir -p "$pkg_src_bin"
mv ./tool-*/tool "$pkg_src_cmd"
chmod a+x "$pkg_src_cmd"
# bash completion (directory name varies — check tar -tz)
if test -e ./tool-*/completions/tool.bash; then
mkdir -p "$pkg_src_dir/share/bash-completion/completions"
mv ./tool-*/completions/tool.bash \
"$pkg_src_dir/share/bash-completion/completions/tool"
fi
if test -e ./tool-*/completions/tool.fish; then
mkdir -p "$pkg_src_dir/share/fish/vendor_completions.d"
mv ./tool-*/completions/tool.fish \
"$pkg_src_dir/share/fish/vendor_completions.d/tool.fish"
fi
if test -e ./tool-*/completions/_tool; then
mkdir -p "$pkg_src_dir/share/zsh/site-functions"
mv ./tool-*/completions/_tool \
"$pkg_src_dir/share/zsh/site-functions/_tool"
fi
if test -e ./tool-*/tool.1; then
mkdir -p "$pkg_src_dir/share/man/man1"
mv ./tool-*/tool.1 "$pkg_src_dir/share/man/man1/tool.1"
fi
}
```
**Pattern D** — binary + shared libraries. The entire directory structure
must be preserved. See [`references/PATTERNS.md`](references/PATTERNS.md)
for the ollama and psql examples.
**Pattern E** — FHS layout (archive already has `bin/` and `share/`):
```sh
WEBI_SINGLE=true
pkg_install() {
mkdir -p "$(dirname "$pkg_src_dir")"
mv ./tool-*/ "$pkg_src_dir"
}
```
**Pattern F** — binary needs rename (archive name ≠ command name).
Use when the binary in the archive cannot be matched by `$pkg_cmd_name*`
— e.g., `yq_linux_amd64` for a command named `yq`:
```sh
WEBI_SINGLE=true
pkg_install() {
mkdir -p "$pkg_src_bin"
mv ./yq_* "$pkg_src_cmd"
chmod a+x "$pkg_src_cmd"
}
```
**Pattern G** — full SDK (do NOT set `WEBI_SINGLE`):
```sh
# pkg_src = directory, not a binary
pkg_src="$pkg_src_dir"
pkg_dst="$HOME/.local/opt/tool"
pkg_install() {
mkdir -p "$(dirname "$pkg_src_dir")"
mv ./tool-*/ "$pkg_src_dir"
}
pkg_link() {
rm -f "$pkg_dst"
ln -s "$pkg_src" "$pkg_dst"
}
```
---
## 4. Write `install.ps1`
A PowerShell framework template exists (`_webi/package-install.tpl.ps1`)
and injects the `install.ps1` script at the `# {{ installer }}` placeholder.
The template provides: error handling, directory setup, `Invoke-DownloadUrl`
helper, and PATH management via `webi_path_add`. However, unlike the shell
side, the PS1 framework does **not** download or extract the archive — the
package script must handle that itself. The same path conventions apply
(opt/bin layout), but Windows uses `Copy-Item` instead of symlinks for
the final `bin/` step.
### Variable block (always at top)
```powershell
$pkg_cmd_name = "tool"
$pkg_dst_cmd = "$Env:USERPROFILE\.local\bin\tool.exe"
$pkg_dst_bin = "$Env:USERPROFILE\.local\bin"
$pkg_dst = "$pkg_dst_cmd"
$pkg_src_cmd = "$Env:USERPROFILE\.local\opt\tool-v$Env:WEBI_VERSION\bin\tool.exe"
$pkg_src_bin = "$Env:USERPROFILE\.local\opt\tool-v$Env:WEBI_VERSION\bin"
$pkg_src_dir = "$Env:USERPROFILE\.local\opt\tool-v$Env:WEBI_VERSION"
$pkg_src = "$pkg_src_cmd"
```
### Standard body
```powershell
New-Item "$Env:USERPROFILE\Downloads\webi" -ItemType Directory -Force | Out-Null
$pkg_download = "$Env:USERPROFILE\Downloads\webi\$Env:WEBI_PKG_FILE"
# Fetch archive
if (!(Test-Path -Path "$pkg_download")) {
Write-Output "Downloading tool from $Env:WEBI_PKG_URL to $pkg_download"
& curl.exe -A "$Env:WEBI_UA" -fsSL "$Env:WEBI_PKG_URL" -o "$pkg_download.part"
& Move-Item "$pkg_download.part" "$pkg_download"
}
if (!(Test-Path -Path "$pkg_src_cmd")) {
Write-Output "Installing tool"
Push-Location .local\tmp
Remove-Item -Path ".\tool-v*" -Recurse -ErrorAction Ignore
# Unpack — Windows BSD-tar handles zip too
Write-Output "Unpacking $pkg_download"
& tar xf "$pkg_download"
# Move binary into place — adjust glob for your archive structure
Write-Output "Install Location: $pkg_src_cmd"
New-Item "$pkg_src_bin" -ItemType Directory -Force | Out-Null
Move-Item -Path ".\tool-*\tool.exe" -Destination "$pkg_src_bin"
Pop-Location
}
# Windows has no symlinks in the webi sense — copy to bin/
Write-Output "Copying into '$pkg_dst_cmd' from '$pkg_src_cmd'"
Remove-Item -Path "$pkg_dst_cmd" -Recurse -ErrorAction Ignore | Out-Null
New-Item "$pkg_dst_bin" -ItemType Directory -Force | Out-Null
Copy-Item -Path "$pkg_src" -Destination "$pkg_dst" -Recurse
```
For Pattern A (binary at archive root), change the `Move-Item` line to:
```powershell
Move-Item -Path ".\tool.exe" -Destination "$pkg_src_bin"
```
---
## 5. Check for classification issues
Before writing any scripts, scan the asset list for red flags:
### Non-standard OS/arch names in filenames
The webi classifier recognises most patterns automatically. Watch for:
- `darwin` vs `macos` — both recognised; output normalised to `macos`
- `x86_64` vs `amd64` — both recognised; output normalised to `amd64`
- `aarch64` vs `arm64` — both recognised; output normalised to `arm64`
- `armv7` (missing trailing `l`) — normalised to `armv7l`
These are handled automatically. Only flag them if the asset list contains
something genuinely unusual that the classifier would not recognise.
### Variant assets needing tags
Flag if you see multiple assets for the same OS/arch that serve different
hardware or runtime requirements:
- **GPU variants**: `*-rocm*`, `*-cuda*`, `*-vulkan*` alongside a baseline build
- **Windows installer**: `*Setup.exe` or `*Install.exe` alongside a bare `*.exe`
- **Framework-dependent .NET**: `*-fxdependent*` vs self-contained
- **AppImage**: `*.AppImage` — not supported by the webi installer
- **Electron/GUI app**: `*.dmg` or `*.AppImage` that is a full GUI app, not a CLI
If you find variants, see [`references/CLASSIFICATION.md`](references/CLASSIFICATION.md)
for how to write a variant tagger.
### Formats to drop
These are automatically filtered by the framework — no action needed:
- `.deb`, `.rpm`, `.snap`, `.AppImage`
- Checksums (`*.sha256`, `*.sha512`, `*.asc`, `*.sig`)
- Source archives (`*-src.tar.gz`, `*.tar.gz` with no OS in name)
---
## Reference files
- [`references/PATTERNS.md`](references/PATTERNS.md) — detailed pattern
descriptions with real package examples and complete install script snippets
- [`references/ARCHIVE-LAYOUTS.md`](references/ARCHIVE-LAYOUTS.md) — actual
`tar -t` output for representative packages in each pattern
- [`references/CLASSIFICATION.md`](references/CLASSIFICATION.md) — when and
how to write variant taggers; non-standard filename conventions

View File

@@ -0,0 +1,289 @@
# Archive Layouts — Real Package Examples
Actual `tar -t` / `unzip -l` output for representative packages.
Use these to calibrate your eye for what each pattern looks like.
---
## Pattern A — Flat archive (no subdirectory)
### caddy 2.9.1 — linux/amd64 tar.gz
```
caddy
LICENSE
README.md
```
Binary `caddy` is at the top level. Set `WEBI_SINGLE=true`.
### fzf 0.70.0 — linux/amd64 tar.gz
```
fzf
```
Minimal — just the binary.
### terraform 1.9.8 — linux/amd64 zip
```
terraform
LICENSE.txt
```
Zip archive but same flat layout.
### k9s — linux/amd64 tar.gz
```
k9s
LICENSE
README.md
```
---
## Pattern B — Named subdirectory, binary only
### delta 0.18.2 — linux/amd64 tar.gz
```
delta-0.18.2-x86_64-unknown-linux-musl/
delta-0.18.2-x86_64-unknown-linux-musl/delta
delta-0.18.2-x86_64-unknown-linux-musl/LICENSE
delta-0.18.2-x86_64-unknown-linux-musl/README.md
```
Glob to move: `./delta-*/delta`
### shellcheck 0.10.0 — linux/x86_64 tar.xz
```
shellcheck-v0.10.0/
shellcheck-v0.10.0/shellcheck
shellcheck-v0.10.0/LICENSE.txt
shellcheck-v0.10.0/README.txt
```
Glob to move: `./shellcheck-*/shellcheck`
### xsv 0.13.0 — linux/x86_64 tar.gz
```
xsv-0.13.0-x86_64-unknown-linux-musl/
xsv-0.13.0-x86_64-unknown-linux-musl/xsv
xsv-0.13.0-x86_64-unknown-linux-musl/UNLICENSE
```
---
## Pattern C — Subdirectory + completions + man pages
### rg/ripgrep 14.1.1 — linux/amd64 tar.gz
```
ripgrep-14.1.1-x86_64-unknown-linux-musl/
ripgrep-14.1.1-x86_64-unknown-linux-musl/rg
ripgrep-14.1.1-x86_64-unknown-linux-musl/complete/
ripgrep-14.1.1-x86_64-unknown-linux-musl/complete/_rg
ripgrep-14.1.1-x86_64-unknown-linux-musl/complete/_rg.ps1
ripgrep-14.1.1-x86_64-unknown-linux-musl/complete/rg.bash
ripgrep-14.1.1-x86_64-unknown-linux-musl/complete/rg.fish
ripgrep-14.1.1-x86_64-unknown-linux-musl/doc/
ripgrep-14.1.1-x86_64-unknown-linux-musl/doc/rg.1
ripgrep-14.1.1-x86_64-unknown-linux-musl/doc/FAQ.md
ripgrep-14.1.1-x86_64-unknown-linux-musl/doc/GUIDE.md
ripgrep-14.1.1-x86_64-unknown-linux-musl/CHANGELOG.md
ripgrep-14.1.1-x86_64-unknown-linux-musl/LICENSE-MIT
ripgrep-14.1.1-x86_64-unknown-linux-musl/README.md
```
Note: completions are in `complete/` (not `completions/`). Man page is `doc/rg.1`.
### sd 1.1.0 — linux/x86_64 tar.gz
```
sd-v1.1.0-x86_64-unknown-linux-musl/
sd-v1.1.0-x86_64-unknown-linux-musl/sd
sd-v1.1.0-x86_64-unknown-linux-musl/sd.1
sd-v1.1.0-x86_64-unknown-linux-musl/completions/
sd-v1.1.0-x86_64-unknown-linux-musl/completions/sd.bash
sd-v1.1.0-x86_64-unknown-linux-musl/completions/sd.elv
sd-v1.1.0-x86_64-unknown-linux-musl/completions/sd.fish
sd-v1.1.0-x86_64-unknown-linux-musl/completions/_sd
sd-v1.1.0-x86_64-unknown-linux-musl/completions/_sd.ps1
sd-v1.1.0-x86_64-unknown-linux-musl/CHANGELOG.md
sd-v1.1.0-x86_64-unknown-linux-musl/LICENSE
sd-v1.1.0-x86_64-unknown-linux-musl/README.md
```
Note: man page `sd.1` is at subdirectory root. Completions in `completions/`.
### bat 0.26.1 — linux/amd64 tar.gz
```
bat-v0.26.1-x86_64-unknown-linux-musl/
bat-v0.26.1-x86_64-unknown-linux-musl/bat
bat-v0.26.1-x86_64-unknown-linux-musl/bat.1
bat-v0.26.1-x86_64-unknown-linux-musl/autocomplete/
bat-v0.26.1-x86_64-unknown-linux-musl/autocomplete/bat.bash
bat-v0.26.1-x86_64-unknown-linux-musl/autocomplete/bat.fish
bat-v0.26.1-x86_64-unknown-linux-musl/autocomplete/bat.zsh
bat-v0.26.1-x86_64-unknown-linux-musl/LICENSE-APACHE
bat-v0.26.1-x86_64-unknown-linux-musl/LICENSE-MIT
bat-v0.26.1-x86_64-unknown-linux-musl/README.md
```
Note: completions in `autocomplete/` (not `completions/`). Zsh file is `bat.zsh` not `_bat`.
### goreleaser — linux/amd64 tar.gz
```
goreleaser
completions/
completions/goreleaser.bash
completions/goreleaser.fish
completions/goreleaser.zsh
manpages/
manpages/goreleaser.1.gz
LICENSE.md
README.md
```
Note: goreleaser uses Pattern A layout (binary at root, no subdirectory)
but includes completions and a gzipped man page. Set `WEBI_SINGLE=true`;
move completions and man page after the binary.
---
## Pattern D — Binary + shared libraries
### ollama 0.17.7 — linux/amd64 tar.zst
```
bin/
bin/ollama
lib/
lib/ollama/
lib/ollama/libggml-base.so
lib/ollama/libggml-cpu-alderlake.so
lib/ollama/libggml-cpu-haswell.so
lib/ollama/libggml-cpu-icelake.so
lib/ollama/libggml-cpu-sandybridge.so
lib/ollama/libggml-cpu-skylakex.so
lib/ollama/libggml-cpu-sse42.so
lib/ollama/libggml-cpu-x64.so
lib/ollama/cuda_v12/
lib/ollama/cuda_v12/libcublas.so.12
lib/ollama/cuda_v12/libcublasLt.so.12
lib/ollama/cuda_v12/libcudart.so.12
lib/ollama/cuda_v12/libggml-cuda.so
... (66 files total)
```
Extract bin/ and lib/ directories separately or together.
### psql (postgres client) — linux/amd64 tar.gz
```
psql-17.2-linux-x86_64/
psql-17.2-linux-x86_64/bin/
psql-17.2-linux-x86_64/bin/psql
psql-17.2-linux-x86_64/lib/
psql-17.2-linux-x86_64/lib/libpq.so.5
psql-17.2-linux-x86_64/lib/libz.so.1
psql-17.2-linux-x86_64/lib/libzstd.so.1
psql-17.2-linux-x86_64/lib/libssl.so.3
psql-17.2-linux-x86_64/lib/libcrypto.so.3
psql-17.2-linux-x86_64/include/
... (75 files total)
```
Move the entire `psql-{ver}-{triplet}/` directory: `mv ./psql-*/ "$pkg_src_dir"`
---
## Pattern E — FHS layout
### gh 2.67.0 — linux/amd64 tar.gz
```
gh_2.67.0_linux_amd64/
gh_2.67.0_linux_amd64/bin/
gh_2.67.0_linux_amd64/bin/gh
gh_2.67.0_linux_amd64/share/
gh_2.67.0_linux_amd64/share/man/
gh_2.67.0_linux_amd64/share/man/man1/
gh_2.67.0_linux_amd64/share/man/man1/gh-actions-cache-delete.1
gh_2.67.0_linux_amd64/share/man/man1/gh-actions-cache-list.1
... (129 man pages)
gh_2.67.0_linux_amd64/LICENSE
```
Move the entire `gh_*/` directory: `mv ./gh_*/ "$pkg_src_dir"`
---
## Pattern F — Binary needs rename
### yq — linux/amd64 tar.gz (WEBI_SINGLE=true)
```
yq_linux_amd64
yq.1
```
Binary is `yq_linux_amd64` — must rename to `yq` during install.
### pathman 0.6.0 — linux/amd64 tar.gz (WEBI_SINGLE=true)
```
pathman-v0.6.0-linux-amd64_v1
```
Binary name includes the full release tag. Rename to `pathman`.
---
## Pattern G — Full SDK
### node 24.14.0 — linux/amd64 tar.xz
```
node-v24.14.0-linux-x64/
node-v24.14.0-linux-x64/bin/
node-v24.14.0-linux-x64/bin/node
node-v24.14.0-linux-x64/bin/npm -> ../lib/node_modules/npm/bin/npm-cli.js
node-v24.14.0-linux-x64/bin/npx -> ../lib/node_modules/npm/bin/npx-cli.js
node-v24.14.0-linux-x64/include/
node-v24.14.0-linux-x64/lib/
node-v24.14.0-linux-x64/lib/node_modules/
node-v24.14.0-linux-x64/share/
... (thousands of files)
```
Move entire directory: `mv ./node-*/ "$pkg_src_dir"`
### go 1.24.1 — linux/amd64 tar.gz
```
go/
go/bin/
go/bin/go
go/bin/gofmt
go/src/
go/pkg/
... (thousands of files)
```
Note: go's archive root directory is literally `go/` with no version in the name.
---
## Pattern H — .NET runtime bundle
### pwsh 7.4.6 — linux/amd64 tar.gz
```
pwsh
Accessibility.dll
clrcompression.dll
clrjit.dll
createdump
cs/
cs/System.Private.CoreLib.resources.dll
de/
de/System.Private.CoreLib.resources.dll
... (727 files, all in same flat directory)
```
No subdirectory. Move all files into `$pkg_src_bin/`.
---
## Inspecting archives yourself
```sh
# tar.gz / tar.xz / tar.zst — list contents only (no extraction)
curl -fsSL "$URL" | tar -tz | head -20
# zip
curl -fsSL "$URL" -o /tmp/pkg.zip
unzip -l /tmp/pkg.zip | head -20
# For a .zst file when tar doesn't support zstd natively:
curl -fsSL "$URL" -o /tmp/pkg.tar.zst && zstd -dc /tmp/pkg.tar.zst | tar -tz | head -20
```
**What to look for**:
1. Is there a top-level directory? (Pattern B/C/D/E/G) or no directory? (Pattern A/F/H)
2. What is the directory named? Does it contain version? triplet?
3. Are there `completions/`, `autocomplete/`, `complete/` subdirs? (Pattern C)
4. Are there `.so`/`.dylib`/`.dll` files? (Pattern D or H)
5. Does the binary name match the command you want on PATH? (Pattern F if not)
6. Is there a `bin/` directory at the top level? (Pattern E or G)

View File

@@ -0,0 +1,183 @@
# Classification Reference
When to flag classification issues, what the webi classifier does automatically,
and what needs manual annotation.
---
## What the classifier handles automatically
The webi classifier (`internal/classify/classify.go`) parses asset filenames
using regex patterns and produces canonical `os`, `arch`, `libc`, and `ext`
values. It handles the vast majority of packages with no configuration needed.
### OS recognition
Filenames containing these terms are classified automatically:
- `darwin`, `macos`, `osx`, `apple``macos` in legacy cache
- `linux``linux`
- `windows`, `win`, `win32`, `win64``windows`
- `freebsd`, `openbsd`, `netbsd`, `dragonfly` → respective values
- `.deb`, `.rpm`, `.snap``linux` (but dropped from legacy cache)
- `.dmg`, `.app.zip``macos`
### Arch recognition
Filenames containing these terms are classified automatically:
- `x86_64`, `amd64`, `64bit`, `x64``amd64`
- `aarch64`, `arm64``arm64`
- `armv7`, `armv7l`, `armhf`, `gnueabihf``armv7l`
- `armv6`, `armv6l``armv6l`
- `i386`, `i686`, `386`, `x86``x86`
- `universal`, `universal2``amd64` (fat binary; arm64 falls back to this)
### Format recognition
- `.tar.gz`, `.tar.xz`, `.tar.zst`, `.tar.bz2`, `.zip`, `.7z` → compressed archive
- `.pkg`, `.msi`, `.dmg` → platform installer
- `.exe` → either bare binary or GUI installer (see below)
- No extension in filename → bare binary (ext = `exe` in cache)
### Automatically dropped
These asset types are recognised and excluded without any configuration:
- Checksums: `*.sha256`, `*.sha512`, `*.md5`, `*.sha256sum`
- Signatures: `*.asc`, `*.sig`, `*.cosign`, `*.sbom`
- Source archives: files with `source`, `src` in the name but no OS
- Package formats not supported by the Node installer: `.deb`, `.rpm`, `.snap`,
`.AppImage`, `.apk`
---
## When you need to add configuration
### Variant assets
A **variant** is a secondary build that serves the same OS/arch as a baseline
build but requires different hardware or runtime support. The Node.js installer
can't choose between variants — it only knows OS, arch, and libc. Variants
must be tagged and then excluded at export time.
**Common variants and how to identify them**:
| Variant | Filename pattern | Notes |
|---------|-----------------|-------|
| CUDA (GPU) | `*-cuda*`, `*cuda12*` | NVIDIA GPU support |
| ROCm (GPU) | `*-rocm*` | AMD GPU support |
| Vulkan | `*-vulkan*` | Cross-vendor GPU |
| AppImage | `*.AppImage` | Linux sandboxed app |
| .NET fxdependent | `*-fxdependent*` | Requires .NET runtime |
| Windows installer | `*Setup.exe`, `*Install.exe` | GUI installer, not the binary |
**Rule**: if there are multiple assets for the same OS/arch combination and
they serve the same users differently, they need variant tags. The baseline
(most widely compatible) build should be kept; variants should be tagged and
excluded.
**Example**: ollama publishes for linux/amd64:
- `ollama-linux-amd64.tar.zst` — baseline (CPU + any GPU auto-detected)
- `ollama-linux-amd64-rocm.tar.zst` — ROCm variant
- `ollama-linux-amd64-jetpack6.tar.zst` — NVIDIA Jetson variant
Only the baseline is useful via webi. The ROCm and Jetpack builds should be
tagged as variants and excluded.
---
### Windows .exe: bare binary vs GUI installer
`.exe` assets are ambiguous — they could be:
1. A bare binary (the tool itself, run from command line)
2. A GUI installer (runs a setup wizard, not useful for webi)
**How to tell**:
- GUI installer: filename contains `Setup`, `Install`, `Installer`, `inno`, `nsis`
- GUI installer: the tool also has a `.zip` or `.tar.gz` for Windows
- Bare binary: filename matches the tool name with minimal decoration
**When you see both**, the `.zip`/archive build is what webi uses. The `.exe`
installer should be tagged as a variant (`installer`) so it's excluded.
**When there's only a `.exe`** (no archive), it's probably the bare binary.
Test by downloading and running it — a bare binary runs immediately.
---
### Packages with no OS/arch in filenames
Some packages (rare) release with minimal filename decoration. Examples:
- `tool-v1.2.3.tar.gz` — no OS, no arch
- `tool.tar.gz` — version not even in filename
These are usually source archives (not compiled binaries) and should be
dropped entirely from the release list. If they are compiled binaries for a
specific OS, the releases.js config needs an `asset_filter` key to match the
right file, plus OS/arch metadata added.
---
### Non-standard OS naming in filenames
A few upstreams use unusual OS names:
- `sunos` — should map to `solaris` (the webi classifier does this)
- `osx` or `macosx` — recognised as `macos`
- `apple-darwin` (Rust triplet) — recognised as `macos`
If a package uses a genuinely unknown OS string, the classifier will produce
`os = ""` for that asset. Those entries are dropped from the legacy cache.
---
### Asset filter configuration
If GitHub releases for a package include multiple builds that would otherwise
collide (e.g. `extended` vs non-extended for hugo, or specific project builds
in a monorepo), add to the package's `releases.conf`:
```ini
# Only include assets containing "extended" in the name
asset_filter = extended
# Exclude assets containing "legacy" in the name
asset_exclude = legacy
```
These filters run before classification.
---
## Quick checklist when inspecting a new package
1. **Look at the latest 23 releases** on GitHub. Note all asset filenames.
2. **Find the "standard" builds** — the ones a normal user would download for
their OS. Usually there are ≤4 per OS (amd64, arm64, x86, armv7l).
3. **Check for extras**:
- Are there GPU-specific builds for the same OS/arch? → variant
- Are there `.exe` installer files alongside a `.zip`? → variant
- Are there `.deb`/`.rpm`/`.AppImage`? → auto-dropped, no action needed
- Does the Windows build have no archive and only a bare `.exe`? → fine
4. **Check OS/arch naming** — does the filename use standard terms, or
something unusual that might confuse the classifier?
5. **Check format changes** — do old releases use a different archive type
or directory layout than recent ones? The install script may need to
handle both.
---
## Canonical vocabulary reference
All cache output must use exactly these values.
**OS**: `macos`, `linux`, `windows`, `freebsd`, `openbsd`, `netbsd`,
`dragonfly`, `aix`, `illumos`, `plan9`, `solaris`
**Arch**:
- `amd64` (not `x86_64`)
- `arm64` (not `aarch64`)
- `armv7l` (not `armv7` — the `l` stands for little-endian; `uname -m` reports `armv7l`)
- `armv6l` (not `armv6`)
- `x86` (not `i386`, `i686`, `386`)
- `mipsle` (not `mipsel`)
- `mips64le` (not `mips64el`)
- Other: `arm`, `ppc64le`, `ppc64`, `loong64`, `riscv64`, `s390x`, `mips`, `mips64`
**Libc**: `none` (static/Go/Zig — never empty), `gnu`, `musl`, `msvc`
**Ext**: `tar.gz`, `tar.xz`, `zip`, `exe`, `7z`, `pkg`, `msi`
(no leading dot; `exe` for bare binaries with no file extension)

View File

@@ -0,0 +1,388 @@
# Install Patterns Reference
Nine patterns cover the full range of webi packages. Pattern A is by far
the most common. Check `tar -tz $ARCHIVE` before writing any code.
---
## Pattern A — Bare binary at archive root
The archive extracts directly to the current directory with no wrapper
subdirectory. Binary (and optional LICENSE/README) is at the top level.
**Set `WEBI_SINGLE=true`** — tells the framework to link the binary file
directly (`~/.local/bin/cmd → ~/.local/opt/cmd-vX/bin/cmd`) rather than
linking the versioned directory.
Representative packages: caddy, fzf, k9s, terraform, sttr, lf, monorel,
awless, bun, cilium, curlie, dashmsg, dotenv, dotenv-linter, ffuf,
gitdeploy, gprox, grype, hugo, keypairs, koji, ots, runzip, sclient,
sqlc, sqlpkg, uuidv7, xcaddy, deno
**install.sh**:
```sh
pkg_cmd_name="caddy"
WEBI_SINGLE=true
pkg_dst_cmd="$HOME/.local/bin/caddy"
pkg_dst="$pkg_dst_cmd"
pkg_src_cmd="$HOME/.local/opt/caddy-v$WEBI_VERSION/bin/caddy"
pkg_src_dir="$HOME/.local/opt/caddy-v$WEBI_VERSION"
pkg_src="$pkg_src_cmd"
pkg_install() {
mkdir -p "$pkg_src_bin"
mv ./"$pkg_cmd_name"* "$pkg_src_cmd"
chmod a+x "$pkg_src_cmd"
}
pkg_get_current_version() {
caddy version 2>/dev/null | head -n 1 | cut -d' ' -f1 | sed 's:^v::'
}
```
**install.ps1** key lines:
```powershell
# No subdirectory — binary is at the top level of the archive
Move-Item -Path ".\caddy.exe" -Destination "$pkg_src_bin"
```
---
## Pattern B — Binary inside a version/triplet subdirectory
Archive extracts to a single directory named with the version and/or
platform triplet. Binary (and docs) live inside that directory.
Representative packages: delta, hexyl, shellcheck, trip, xsv, kubectx, kubens
**Subdirectory naming conventions seen in the wild**:
- `tool-{ver}-{triplet}/` — most Rust tools (delta, shellcheck, xsv)
- `tool-{ver}/` — simpler version-only dirs
- flat (no dir) — kubectx/kubens use flat archives despite being "B-ish"
**install.sh**:
```sh
pkg_cmd_name="delta"
# WEBI_SINGLE not set (or false)
pkg_dst_cmd="$HOME/.local/bin/delta"
pkg_dst="$pkg_dst_cmd"
pkg_src_cmd="$HOME/.local/opt/delta-v$WEBI_VERSION/bin/delta"
pkg_src_dir="$HOME/.local/opt/delta-v$WEBI_VERSION"
pkg_src="$pkg_src_cmd"
pkg_install() {
mkdir -p "$pkg_src_bin"
mv ./delta-*/delta "$pkg_src_cmd"
chmod a+x "$pkg_src_cmd"
}
pkg_get_current_version() {
delta --version 2>/dev/null | head -n 1 | cut -d' ' -f2
}
```
**install.ps1** key lines:
```powershell
Move-Item -Path ".\delta-*\delta.exe" -Destination "$pkg_src_bin"
```
---
## Pattern C — Subdirectory with binary + completions and/or man pages
Same as B but the archive also contains shell completions and/or man pages
worth installing.
Representative packages: bat, fd, lsd, rg/ripgrep, sd, watchexec, zoxide
Note: goreleaser has a flat archive (Pattern A layout) but with completions at
the archive root. See the goreleaser entry in ARCHIVE-LAYOUTS.md.
**Completion directory name varies by package**:
- `completions/` — sd, watchexec, zoxide
- `autocomplete/` — bat, fd, lsd
- `complete/` — rg/ripgrep
**Completion filename conventions**:
- Bash: `tool.bash`, `tool.bash-completion`, `_tool.bash`
- Fish: `tool.fish`
- Zsh: `_tool`
- PowerShell: `_tool.ps1`, `tool.ps1`
**Man page location varies**:
- `tool.1` at subdirectory root — sd, bat, fd, lsd
- `doc/tool.1` — rg/ripgrep
- `man/man1/tool.1` — zoxide (deepest path)
**install.sh** (rg as example):
```sh
pkg_cmd_name="rg"
pkg_dst_cmd="$HOME/.local/bin/rg"
pkg_dst="$pkg_dst_cmd"
pkg_src_cmd="$HOME/.local/opt/rg-v$WEBI_VERSION/bin/rg"
pkg_src_dir="$HOME/.local/opt/rg-v$WEBI_VERSION"
pkg_src="$pkg_src_cmd"
pkg_install() {
mkdir -p "$pkg_src_bin"
mv ./ripgrep-*/rg "$pkg_src_cmd"
chmod a+x "$pkg_src_cmd"
# bash completion
if test -e ./ripgrep-*/complete/rg.bash; then
mkdir -p "$pkg_src_dir/share/bash-completion/completions"
mv ./ripgrep-*/complete/rg.bash \
"$pkg_src_dir/share/bash-completion/completions/rg"
fi
# fish completion
if test -e ./ripgrep-*/complete/rg.fish; then
mkdir -p "$pkg_src_dir/share/fish/vendor_completions.d"
mv ./ripgrep-*/complete/rg.fish \
"$pkg_src_dir/share/fish/vendor_completions.d/rg.fish"
fi
# zsh completion
if test -e ./ripgrep-*/complete/_rg; then
mkdir -p "$pkg_src_dir/share/zsh/site-functions"
mv ./ripgrep-*/complete/_rg \
"$pkg_src_dir/share/zsh/site-functions/_rg"
fi
# man page
if test -e ./ripgrep-*/doc/rg.1; then
mkdir -p "$pkg_src_dir/share/man/man1"
mv ./ripgrep-*/doc/rg.1 "$pkg_src_dir/share/man/man1/rg.1"
fi
}
pkg_get_current_version() {
rg --version 2>/dev/null | head -n 1 | cut -d' ' -f2
}
```
**Note**: Completion paths in completions/man install are best-effort
— use `if test -e ...` guards so the script still works on older releases
that didn't include them.
---
## Pattern D — Binary + shared libraries
The package bundles shared libraries alongside the binary. The entire
directory tree must be preserved.
Representative packages: ollama (Linux), psql/postgres, sass (Dart VM),
syncthing, xz
**install.sh**:
```sh
pkg_cmd_name="ollama"
pkg_dst_cmd="$HOME/.local/bin/ollama"
pkg_dst="$pkg_dst_cmd"
pkg_src_cmd="$HOME/.local/opt/ollama-v$WEBI_VERSION/bin/ollama"
pkg_src_dir="$HOME/.local/opt/ollama-v$WEBI_VERSION"
pkg_src="$pkg_src_cmd"
pkg_install() {
mkdir -p "$(dirname "$pkg_src_dir")"
# Archive already has bin/ and lib/ layout
mv ./bin "$pkg_src_dir/bin"
mv ./lib "$pkg_src_dir/lib"
}
```
For psql (archive has a `psql-{ver}-{triplet}/` wrapper dir):
```sh
pkg_install() {
mkdir -p "$(dirname "$pkg_src_dir")"
mv ./psql-*/ "$pkg_src_dir"
}
```
---
## Pattern E — FHS-like layout
Archive already follows `bin/`, `share/man/`, `share/doc/` hierarchy.
Extract the whole thing directly into the versioned opt directory.
Representative packages: gh (GitHub CLI), pandoc
**install.sh**:
```sh
pkg_cmd_name="gh"
pkg_dst_cmd="$HOME/.local/bin/gh"
pkg_dst="$pkg_dst_cmd"
pkg_src_cmd="$HOME/.local/opt/gh-v$WEBI_VERSION/bin/gh"
pkg_src_dir="$HOME/.local/opt/gh-v$WEBI_VERSION"
pkg_src="$pkg_src_cmd"
pkg_install() {
mkdir -p "$(dirname "$pkg_src_dir")"
mv ./gh_*/ "$pkg_src_dir"
}
pkg_get_current_version() {
gh --version 2>/dev/null | head -n 1 | cut -d' ' -f3
}
```
No `chmod` needed — binary is already executable inside the archive.
---
## Pattern F — Binary needs rename
Binary in the archive doesn't match the expected command name.
Representative packages: pathman (`pathman-v0.6.0-linux-amd64_v1``pathman`),
yq (`yq_linux_amd64``yq`)
**install.sh**:
```sh
pkg_cmd_name="yq"
WEBI_SINGLE=true
pkg_dst_cmd="$HOME/.local/bin/yq"
pkg_dst="$pkg_dst_cmd"
pkg_src_cmd="$HOME/.local/opt/yq-v$WEBI_VERSION/bin/yq"
pkg_src_dir="$HOME/.local/opt/yq-v$WEBI_VERSION"
pkg_src="$pkg_src_cmd"
pkg_install() {
mkdir -p "$pkg_src_bin"
# Binary is named yq_linux_amd64 (or yq_darwin_amd64 etc)
mv ./yq_* "$pkg_src_cmd"
chmod a+x "$pkg_src_cmd"
}
```
---
## Pattern G — Full SDK / toolchain
Archive contains a complete runtime or SDK (hundreds to thousands of files).
The entire tree goes into opt; multiple binaries are linked from `bin/`.
Representative packages: go, node, zig, flutter, julia, cmake, tinygo
**install.sh** (node as example):
```sh
pkg_cmd_name="node"
# NOTE: pkg_src points to the directory, not a binary
pkg_dst_cmd="$HOME/.local/bin/node"
pkg_dst="$HOME/.local/opt/node" # versioned-dir symlink target
pkg_src_cmd="$HOME/.local/opt/node-v$WEBI_VERSION/bin/node"
pkg_src_dir="$HOME/.local/opt/node-v$WEBI_VERSION"
pkg_src="$pkg_src_dir" # pkg_src = the directory
pkg_install() {
mkdir -p "$(dirname "$pkg_src")"
mv ./node-*/ "$pkg_src"
}
pkg_link() {
rm -f "$pkg_dst"
ln -s "$pkg_src" "$pkg_dst"
}
pkg_get_current_version() {
node --version 2>/dev/null | head -n 1 | sed 's:^v::'
}
```
---
## Pattern H — .NET runtime bundle
Flat directory with one binary and hundreds of `.dll` files. The entire
directory must be preserved. Like Pattern G (SDK) in structure — the
versioned directory is the package root, with the binary directly inside
(no `bin/` subdirectory). A `pkg_link()` creates the unversioned symlink.
Representative packages: pwsh (PowerShell Core)
**install.sh**:
```sh
pkg_cmd_name="pwsh"
# note: binary is at pkg_src_dir root, no bin/ subdirectory
pkg_src_cmd="$HOME/.local/opt/pwsh-v$WEBI_VERSION/pwsh"
pkg_src_dir="$HOME/.local/opt/pwsh-v$WEBI_VERSION"
pkg_src="$pkg_src_dir"
pkg_dst_cmd="$HOME/.local/opt/pwsh/pwsh"
pkg_dst="$HOME/.local/opt/pwsh"
pkg_install() {
# Archive extracts flat — move all contents into the versioned dir
mkdir -p "$pkg_src_dir"
mv ./* "$pkg_src_dir"
chmod a+x "$pkg_src_cmd"
}
pkg_link() {
rm -rf "$pkg_dst"
ln -s "$pkg_src" "$pkg_dst"
}
```
---
## Pattern I — Multi-binary distribution
Archive contains multiple related binaries. Install the primary one and
link only that.
Representative packages: dashcore (dashd + dash-cli + dash-qt + ...),
mutagen (mutagen + mutagen-agents.tar.gz)
**install.sh** (dashcore-style):
```sh
pkg_cmd_name="dashd"
pkg_dst_cmd="$HOME/.local/bin/dashd"
pkg_dst="$pkg_dst_cmd"
pkg_src_cmd="$HOME/.local/opt/dashcore-v$WEBI_VERSION/bin/dashd"
pkg_src_dir="$HOME/.local/opt/dashcore-v$WEBI_VERSION"
pkg_src="$pkg_src_cmd"
pkg_install() {
mkdir -p "$(dirname "$pkg_src_dir")"
mv ./dashcore-*/ "$pkg_src_dir"
}
```
---
## Choosing between patterns
```
Archive root contains a single binary (or binary + docs)?
→ Pattern A (set WEBI_SINGLE=true)
Archive has a named subdirectory wrapping the binary?
├─ Binary only inside subdir? → Pattern B
├─ Binary + completions/man pages? → Pattern C
└─ Binary + shared libraries (.so)? → Pattern D
Archive already has bin/ and share/ layout?
→ Pattern E
Binary name doesn't match the command name?
→ Pattern F (rename during install)
Archive is a full SDK (compiler, runtime, stdlib)?
→ Pattern G (pkg_src = pkg_src_dir)
Flat directory with many DLLs (.NET)?
→ Pattern H
Multiple binaries for a single distributed system?
→ Pattern I
```