diff --git a/_skills/installer/SKILL.md b/_skills/installer/SKILL.md new file mode 100644 index 000000000..a07a7eca7 --- /dev/null +++ b/_skills/installer/SKILL.md @@ -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 (A–I), 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 (A–I) cover almost every real-world case. +3. [Write `install.sh`](#3-write-installsh) — POSIX shell, ~20–40 lines. +4. [Write `install.ps1`](#4-write-installps1) — PowerShell, ~40–60 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/` (the symlink) | +| `pkg_dst` | Same as `pkg_dst_cmd` for single-binary packages; `~/.local/opt/` for SDKs | +| `pkg_src_cmd` | Versioned binary: `~/.local/opt/-v/bin/` | +| `pkg_src_dir` | Versioned install dir: `~/.local/opt/-v` | +| `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 B–F), +`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 diff --git a/_skills/installer/references/ARCHIVE-LAYOUTS.md b/_skills/installer/references/ARCHIVE-LAYOUTS.md new file mode 100644 index 000000000..f76e642bf --- /dev/null +++ b/_skills/installer/references/ARCHIVE-LAYOUTS.md @@ -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) diff --git a/_skills/installer/references/CLASSIFICATION.md b/_skills/installer/references/CLASSIFICATION.md new file mode 100644 index 000000000..33180db6c --- /dev/null +++ b/_skills/installer/references/CLASSIFICATION.md @@ -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 2–3 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) diff --git a/_skills/installer/references/PATTERNS.md b/_skills/installer/references/PATTERNS.md new file mode 100644 index 000000000..567d639e1 --- /dev/null +++ b/_skills/installer/references/PATTERNS.md @@ -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 +```