Native Packages & Installers
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages. |
A release archive is enough for people who are happy to download, verify and unpack a file. Most users expect to run one
command of the package manager they already use, and to receive upgrades with the rest of their software. This page is
about the native channels that provide that: Linux packages and repositories, Homebrew, Scoop, winget, Chocolatey, MSI and
MSIX, Snap, AppImage and Nix, plus install scripts and self-update. The channel list that is common to every ecosystem
(pipx, npm, JBang, cargo install, dotnet tool, containers, GoReleaser) and the signing and checksum rules for agents are
on Packaging & Distribution and
Security; they are not repeated here. How the binaries themselves are produced for
each operating system is on Native Binaries & Cross-Compilation,
and the signing, notarization and trust mechanics of each operating system are on
Signing, Notarization & OS Trust. Windows installer technologies for
GUI applications are on Distribution Channels and
MSIX Packaging & Signing.
The page was checked against the documentation of nFPM, cargo-deb, Homebrew, Scoop, the Windows Package Manager,
Chocolatey, the Windows packaging extensions, Snapcraft, AppImage and nix.dev on 2026-10-25. The release URLs, owners and
checksums in the snippets are placeholders (example.invalid). The snippets were not run against real package managers
while the page was written, because they would publish or install software; the install script was only syntax-checked
with sh -n. The Bookshelf shelf command is the packaged program; its release archives are assumed to contain a single
executable named shelf.
Channel by operating system by signing
Every channel has a different answer to two questions: who vouches for the artifact, and what must the author sign. The table summarises what the official documentation says. A dash means the documentation consulted does not state a requirement.
| Channel | Operating system | What the user runs | Trust and signing requirement |
|---|---|---|---|
|
Debian-family and RPM-family Linux |
|
Optional package signature (nFPM supports |
apt / dnf repository |
Linux |
|
The repository metadata and the user’s key configuration carry the trust; you publish and sign the repository. |
Homebrew tap and bottles |
macOS, Linux |
|
The formula pins a SHA-256; bottles carry SHA-256 per OS and architecture. Non-official taps need explicit trust by the user. |
Scoop bucket |
Windows |
|
The manifest has a |
winget manifest |
Windows |
|
|
Chocolatey |
Windows |
|
The community repository’s moderation rules ask for checksum validation of downloads. |
MSI / MSIX |
Windows |
Double click, |
MSIX is a signed package format; see MSIX Packaging & Signing. |
Snap |
Linux with snapd |
|
Store review; a classic snap cannot be distributed without approval from the Store team. |
AppImage |
Linux |
Make the file executable and run it |
The documentation consulted does not describe a signing requirement; checksums and HTTPS hosting are your responsibility. |
Nix |
Linux, macOS |
|
Source and binary inputs are pinned by a content |
Install script |
Linux, macOS (Windows with PowerShell) |
An |
None by itself: the script must verify what it downloads. |
Signing a bare executable for macOS Gatekeeper or Windows SmartScreen is a separate topic from the repository and manifest checks above; it is covered on the signing page linked in the introduction.
Linux packages: deb and rpm
A .deb is the Debian and Ubuntu package format and an .rpm the Fedora, RHEL and openSUSE one. Both are archives of files
plus metadata (name, version, architecture, maintainer, dependencies) and maintainer scripts. For a single static binary
the work is mostly mapping a file to a destination such as /usr/bin/shelf.
nfpm
nFPM is a packager written in Go whose documentation lists the formats deb, rpm, apk, ipk, Arch Linux and MSIX. It is
configured with a YAML file and does not need dpkg-deb or rpmbuild. It does not template: environment variables are
expanded in many fields, so a CI job can set SEMVER and write version: ${SEMVER}.
name: shelf
arch: amd64
platform: linux
version: ${SEMVER}
maintainer: Bookshelf Maintainers <maintainers@example.invalid>
description: Command-line catalogue of books and loans
license: MIT
contents:
- src: dist/shelf-linux-amd64
dst: /usr/bin/shelf
- src: packaging/shelf.conf
dst: /etc/shelf.conf
type: config
SEMVER=0.4.0 nfpm package --packager deb --target dist/
SEMVER=0.4.0 nfpm package --packager rpm --target dist/
Source: nFPM — Configuration and
nfpm package. The package command takes -f/--config (default
nfpm.yaml), -p/--packager and -t/--target (a file name, a folder, or empty for the current folder).
Package signing is configured per format with deb: signature: or rpm: signature: blocks holding key_file and
key_id. The passphrase is read from NFPM_DEB_PASSPHRASE or NFPM_RPM_PASSPHRASE, falling back to NFPM_PASSPHRASE;
keep these in CI secrets, never in the file. nFPM documents debsign as the default Debian signing method and warns that
dpkg-sig is not supported by newer Debian versions.
cargo-deb
For a Rust tool, cargo-deb builds the .deb from Cargo.toml metadata and the Cargo build output. It needs Rust 1.76
or later and writes target/debian/<name>_<version>-1_<arch>.deb. Assets default to installing the binary into
/usr/bin/; depends defaults to automatically generated dependencies ($auto).
[package.metadata.deb]
section = "utils"
depends = "$auto"
assets = [
["target/release/shelf", "usr/bin/", "755"],
["README.md", "usr/share/doc/shelf/README", "644"],
]
cargo install cargo-deb
cargo deb # native build
cargo deb --target=aarch64-unknown-linux-gnu --no-build # package an existing cross-built binary
Source: cargo-deb. The project states that cross-compilation cannot target an older Debian release than the host; build on the oldest distribution you support, or link statically as described on Native Binaries & Cross-Compilation.
apt and dnf repositories
A package file only installs once. A repository gives upgrades. The Debian wiki describes an official-archive layout
(supporting pinning and secure APT) and a deprecated "trivial archive" layout, and lists tools for small personal
archives such as reprepro, mini-dinstall and aptly. For RPM, createrepo_c generates the repository metadata
(repomd.xml and friends) for a directory of packages.
mkdir -p repo
cp dist/shelf-0.4.0-1.x86_64.rpm repo/
createrepo_c repo/
Source: createrepo_c and
Debian Wiki — Setting up a Debian repository. The pages consulted do not
show how to sign repository metadata or how users should configure the key (Signed-By for apt; gpgcheck,
repo_gpgcheck and gpgkey in a .repo file for dnf); take those steps from the documentation of the tool you pick and
from Signing, Notarization & OS Trust rather than from memory.
Hosting is plain static files over HTTPS, so GitHub Pages or object storage is enough.
Homebrew taps and bottles
A tap is a Git repository; on GitHub its name starts with homebrew- so that users can use the short form. The
documentation recommends brew tap-new to create the layout, formulae live in a Formula directory, and users install
with brew install OWNER/tap/shelf, which taps automatically. Non-official taps require explicit trust (brew trust --tap
OWNER/tap). The formula syntax itself is shown on
the AI packaging page; the new part is bottles.
A bottle is a gzipped tarball of compiled binaries, one per OS and architecture, whose sha256 Homebrew checks (a missing
or mismatching checksum means the bottle is not used). Build one with brew install --build-bottle and brew bottle; the
second command prints the bottle do block to add to the formula. By default bottles target the oldest CPU generation
supported for the platform, and a bottle built for a newer CPU can fail with an illegal-instruction error on older
hardware. A tap generated by brew tap-new ships GitHub Actions workflows that build bottles for pull requests, and
brew pr-pull publishes them (use --head-sha so the command fails if the pull request changes after review).
brew tap-new --github-packages stores bottles in GitHub Packages instead of GitHub Releases.
brew tap-new OWNER/homebrew-tap
brew create https://example.invalid/shelf/releases/v0.4.0/shelf-0.4.0-source.tar.gz --tap OWNER/homebrew-tap
gh repo create OWNER/homebrew-tap --source "$(brew --repository OWNER/homebrew-tap)" --push
Source: Homebrew — How to Create and Maintain a Tap and Homebrew — Bottles. The Bottles page does not mention cryptographic signing, only SHA-256 checksums. If your formula downloads a prebuilt macOS executable, signing and notarization are your responsibility and are discussed on the signing page.
Windows: Scoop, winget, Chocolatey
Scoop
Scoop installs portable applications from JSON manifests kept in a bucket. The documented fields include version,
description, homepage and license (all required), url, hash (SHA-256 unless prefixed), bin (executables added
to the path), architecture (per 32bit, 64bit, arm64 overrides), checkver and autoupdate.
{
"version": "0.4.0",
"description": "Command-line catalogue of books and loans",
"homepage": "https://example.invalid/shelf",
"license": "MIT",
"architecture": {
"64bit": {
"url": "https://example.invalid/shelf/releases/v0.4.0/shelf-0.4.0-windows-x86_64.zip",
"hash": "<sha256 of the archive>"
},
"arm64": {
"url": "https://example.invalid/shelf/releases/v0.4.0/shelf-0.4.0-windows-aarch64.zip",
"hash": "<sha256 of the archive>"
}
},
"bin": "shelf.exe",
"checkver": "github",
"autoupdate": {
"architecture": {
"64bit": { "url": "https://example.invalid/shelf/releases/v$version/shelf-$version-windows-x86_64.zip" },
"arm64": { "url": "https://example.invalid/shelf/releases/v$version/shelf-$version-windows-aarch64.zip" }
}
}
}
Source: Scoop — App Manifests. The page consulted links to a
separate Buckets page for creating a bucket and does not describe checkver shortcuts or the autoupdate syntax in
detail. The checkver value and the $version substitution in the example come from Scoop’s separate auto-update
documentation, which was not consulted, so check them there before use.
winget
Windows Package Manager manifests are YAML. A singleton manifest is valid only for a package with one installer and one
locale; otherwise a multi-file manifest (version, default locale, installer and optional extra locales) is required.
InstallerSha256 is required, every field is Pascal-cased and may not be duplicated, and the documentation states that
all tools must support a silent install. Create manifests with wingetcreate new (installed with winget install
wingetcreate), which can also submit the pull request to the community repository, or with the YAMLCreate.ps1 script of
the winget-pkgs repository. Komac is a community tool with the same purpose; it is not described by the Microsoft
documentation consulted.
PackageIdentifier: Bookshelf.Shelf
PackageVersion: 0.4.0
PackageLocale: en-US
Publisher: Bookshelf
PackageName: shelf
License: MIT
ShortDescription: Command-line catalogue of books and loans.
Installers:
- Architecture: x64
InstallerType: zip
InstallerUrl: https://example.invalid/shelf/releases/v0.4.0/shelf-0.4.0-windows-x86_64.zip
InstallerSha256: <sha256 of the zip>
NestedInstallerType: portable
NestedInstallerFiles:
- RelativeFilePath: shelf.exe
PortableCommandAlias: shelf
ManifestType: singleton
ManifestVersion: 1.12.0
Source: Microsoft Learn — Create your package
manifest. The page consulted lists exe, msi, msix, inno, wix, nullsoft, appx and font as installer types in
its minimal schema. The zip installer type with the NestedInstallerType, NestedInstallerFiles and
PortableCommandAlias fields used above are not described by that page; take them from the manifest schema in the
winget-pkgs repository and validate the manifest before submitting.
Chocolatey
A Chocolatey package is a NuGet-style package whose only required element is the .nuspec. A self-contained package
simply includes the executables, which Chocolatey shims onto the path automatically; Bob.exe.ignore skips a shim and
Bob.exe.gui marks a GUI program. An installer script chocolateyInstall.ps1 (saved as UTF-8 with BOM) is only needed
when something must be downloaded or run. The documented workflow is choco new, choco pack, a local test with
choco install shelf --debug --verbose --source . (preferably in a virtual machine), then choco push with an API key.
The community repository’s moderation rules include validating downloads with a checksum, a lowercase package identifier
without dots, and a four-segment version.
Source: Chocolatey — Create packages.
MSI, MSIX and app execution aliases
For a console tool, a Windows installer matters when you need to change the machine (add to the system path, register an uninstaller, deploy through enterprise tooling). MSI is the Windows Installer format; MSIX is the modern, signed package format whose creation and signing are covered by the Windows section linked in the introduction, so only the part that is specific to command-line tools is described here.
A packaged app does not put its executable on the path. An app execution alias gives it a command name: the package
manifest declares a windows.appExecutionAlias extension whose alias must end in .exe. Only one alias can be specified
for each application in the package, and when several apps register the same alias the system invokes the last one
registered, so choose a distinctive name.
<Package
xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3"
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
IgnorableNamespaces="uap3 desktop">
<Applications>
<Application>
<Extensions>
<uap3:Extension Category="windows.appExecutionAlias"
Executable="shelf.exe"
EntryPoint="Windows.FullTrustApplication">
<uap3:AppExecutionAlias>
<desktop:ExecutionAlias Alias="shelf.exe" />
</uap3:AppExecutionAlias>
</uap3:Extension>
</Extensions>
</Application>
</Applications>
</Package>
Source: Microsoft Learn — Integrate
your desktop app with Windows using packaging extensions. Users can disable the alias on the App execution aliases
page of Settings. The page does not say how aliases behave for console-only programs run from every shell, so test
shelf from cmd, PowerShell and a non-interactive CI job before relying on it.
Snap, AppImage and Nix
Snap and classic confinement
Snaps are confined by default. Classic confinement is the most permissive level, giving access similar to a traditional
package: no private mount namespace, no device cgroup, AppArmor in complain mode and no seccomp filtering. It exists for
programs that need arbitrary host binaries, which a developer tool such as a CLI that launches compilers or git
often does. The documentation states that a classic snap cannot be distributed without approval from the Store team and
that Canonical audits them. The confinement page does not describe how users install a classic snap, so consult the
Snapcraft documentation for the install command rather than assuming one.
name: shelf
base: core24
version: "0.4.0"
summary: Command-line catalogue of books and loans
description: The shelf CLI.
grade: stable
confinement: classic
apps:
shelf:
command: bin/shelf
parts:
shelf:
plugin: dump
source: dist/shelf-linux-amd64/
Source: Snapcraft — Classic confinement and
snapcraft.yaml reference. The reference page consulted lists the
fields and the three confinement values (strict, devmode, classic) without explaining them; the base value, the
dump plugin options and the build commands should be taken from the current Snapcraft how-to for your release.
AppImage
An AppImage is a single file for Linux that users download, mark executable and run. A type 2 AppImage can be inspected
with my.AppImage --appimage-extract (creates squashfs-root) or --appimage-mount. The distribution guide asks that
the file name not contain linux, that AppImages are not wrapped in .zip or .tar.gz archives (which breaks desktop
integration), that a stable URL without a version number is used for the catalogue, and that the host supports HTTP range
requests when you want AppImageUpdate to work. The pages consulted do not describe a signing requirement, the FUSE
requirement, or how update information is embedded; consult the Packaging Guide sections on optional features before
documenting any of them for your users. AppImage suits GUI programs better than a terminal tool, which has no menu entry
to integrate and is usually installed by a package manager.
Nix
Nix packages are functions that produce derivations. The nix.dev tutorial packages an existing program with
stdenv.mkDerivation and a source fetched with fetchurl, whose hash starts empty: the first nix-build fails with a
mismatch that reports the right value, which you then paste in.
{ stdenv, fetchurl }:
stdenv.mkDerivation {
pname = "shelf";
version = "0.4.0";
src = fetchurl {
url = "https://example.invalid/shelf/releases/v0.4.0/shelf-0.4.0-source.tar.gz";
hash = ""; # replace with the value reported by the first build
};
}
Source: nix.dev — Packaging existing software. The tutorial’s skeleton builds with default phases; a real Rust or Go tool needs its language’s builder from nixpkgs, which the page does not cover. Getting a package into nixpkgs itself is a pull request to that repository and is not described on nix.dev’s tutorial page.
Install scripts and their risks
An install script (curl URL | sh) is popular because it works on every Linux and macOS machine without a package
manager account. It is also the weakest channel: the user executes whatever the server returns, with their own
privileges, and a connection that drops half-way can run a truncated script. No standards body or package manager
documentation defines a script format, so the guidance below is engineering practice, not a specification.
-
Serve over HTTPS only and pass
--proto '=https' --tlsv1.2tocurl. -
Wrap the whole script in a function or put
set -eufirst and execute nothing until the file is complete. -
Download the archive and a checksum file, verify with
sha256sumorshasum -a 256, and abort on mismatch. A checksum fetched from the same server only detects corruption; pair it with a signature or a package-manager channel for integrity against a compromised server. -
Never ask for
sudoimplicitly: install under$HOME/.local/binby default and let the user choose a prefix. -
Support an exact version (
SHELF_VERSION), so automation does not follow "latest". -
Offer the script as a file the user can read first, and publish the same checks in the package channels.
#!/bin/sh
# Install the shelf CLI from a release archive after verifying its checksum.
set -eu
version="${SHELF_VERSION:?set SHELF_VERSION, for example 0.4.0}"
base="https://example.invalid/shelf/releases/v${version}"
dest="${SHELF_PREFIX:-$HOME/.local}/bin"
os=$(uname -s | tr '[:upper:]' '[:lower:]')
arch=$(uname -m)
case "$arch" in
x86_64|amd64) arch=x86_64 ;;
arm64|aarch64) arch=aarch64 ;;
*) echo "unsupported architecture: $arch" >&2; exit 1 ;;
esac
archive="shelf-${version}-${os}-${arch}.tar.gz"
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
curl --proto '=https' --tlsv1.2 -fsSL -o "$tmp/$archive" "$base/$archive"
curl --proto '=https' --tlsv1.2 -fsSL -o "$tmp/SHA256SUMS" "$base/SHA256SUMS"
expected=$(grep " $archive\$" "$tmp/SHA256SUMS" | cut -d' ' -f1)
[ -n "$expected" ] || { echo "no checksum for $archive" >&2; exit 1; }
if command -v sha256sum >/dev/null 2>&1; then
actual=$(sha256sum "$tmp/$archive" | cut -d' ' -f1)
else
actual=$(shasum -a 256 "$tmp/$archive" | cut -d' ' -f1)
fi
[ "$expected" = "$actual" ] || { echo "checksum mismatch" >&2; exit 1; }
mkdir -p "$dest"
tar -xzf "$tmp/$archive" -C "$tmp" shelf
install -m 0755 "$tmp/shelf" "$dest/shelf"
echo "installed $dest/shelf"
Source: curl — man page, where --proto and --tlsv1.2 are listed, and the checksum guidance on
the AI packaging page.
On Windows the equivalent is a PowerShell script fetched with Invoke-WebRequest; prefer winget or Scoop, which verify
InstallerSha256 or hash for you.
Self-update
Some tools update themselves (shelf self-update). The official package-manager documentation consulted does not
recommend or define this, so treat it as a design decision with these trade-offs:
-
When a package manager installed the program, a self-updater fights with it: the manager’s database no longer matches the files, and the binary may live in a root-owned directory. Detect the install source and, for managed installs, print the manager’s upgrade command (
brew upgrade shelf,scoop update shelf,winget upgrade Bookshelf.Shelf) instead of replacing the file. -
Replace atomically: download to a temporary file in the same directory, verify the checksum, set the executable bit and rename over the old file. On Windows a running executable cannot be overwritten, but it can be renamed, so the updater renames the current file, writes the new one and removes the old one at next start.
-
Verify before replacing, exactly as the install script does; an updater that skips verification is a remote code execution channel.
-
Never update silently in non-interactive use, and make it opt-in so that automation stays reproducible, as argued in the AI sub-section.
The Windows rename behaviour is general Win32 file-locking behaviour described on Windows Console & Processes; no package-manager documentation provides a self-update API.
Pitfalls
-
A loose
.debor.rpminstalls but never upgrades; users still need a repository or a manual download. -
The architecture field differs by ecosystem (
amd64for Debian and nFPM,x86_64for RPM and most archives,x64for winget,arm64oraarch64elsewhere); a typo yields a package that installs on no machine or the wrong one. -
Submitting to community repositories (winget, Chocolatey, Snap classic) involves review, so a hotfix release can take longer than a tap or bucket you control.
-
A bottle built on a newer CPU than the oldest supported machine fails with an illegal instruction.
-
Hash fields must be regenerated for every release; automate them in the release workflow.
-
Windows aliases and shims are per installer:
chocoshims, Scoopbinentries and MSIX aliases all place different launchers on the path, and installing the same tool through two channels creates confusing duplicates. -
Package scripts run as root on Linux (
postinst) and elevated on Windows; keep them minimal.