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

.deb / .rpm file

Debian-family and RPM-family Linux

dpkg -i, apt install ./shelf.deb, rpm -i

Optional package signature (nFPM supports deb and rpm signature keys). Installing a loose file gives no repository trust.

apt / dnf repository

Linux

apt install shelf, dnf install shelf

The repository metadata and the user’s key configuration carry the trust; you publish and sign the repository.

Homebrew tap and bottles

macOS, Linux

brew install OWNER/tap/shelf

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

scoop install shelf

The manifest has a hash per URL (SHA-256 by default). No code-signing requirement is stated.

winget manifest

Windows

winget install Publisher.Shelf

InstallerSha256 is required in the manifest; the community repository validates submissions. The installer must support silent installation.

Chocolatey

Windows

choco install shelf

The community repository’s moderation rules ask for checksum validation of downloads.

MSI / MSIX

Windows

Double click, winget, msiexec

MSIX is a signed package format; see MSIX Packaging & Signing.

Snap

Linux with snapd

snap install shelf

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

nix-build, nix profile, or a package in nixpkgs

Source and binary inputs are pinned by a content hash.

Install script

Linux, macOS (Windows with PowerShell)

An sh script fetched over HTTPS

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}.

nfpm.yaml for the shelf release binary
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
Building both package formats
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).

Cargo.toml (fragment)
[package.metadata.deb]
section = "utils"
depends = "$auto"
assets = [
    ["target/release/shelf", "usr/bin/", "755"],
    ["README.md", "usr/share/doc/shelf/README", "644"],
]
Building, and cross-building with a binary produced elsewhere
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.

Producing the metadata of a dnf repository
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.

Creating and publishing a tap
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.

shelf.json in a bucket
{
  "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.

Bookshelf.Shelf.yaml (singleton shape from the documentation, placeholders)
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.

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.

AppxManifest.xml (fragment)
<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.

snapcraft.yaml (fragment, keys named by the reference)
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.

shelf.nix
{ 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.2 to curl.

  • Wrap the whole script in a function or put set -eu first and execute nothing until the file is complete.

  • Download the archive and a checksum file, verify with sha256sum or shasum -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 sudo implicitly: install under $HOME/.local/bin by 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.

install.sh (verified with sh -n only)
#!/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 .deb or .rpm installs but never upgrades; users still need a repository or a manual download.

  • The architecture field differs by ecosystem (amd64 for Debian and nFPM, x86_64 for RPM and most archives, x64 for winget, arm64 or aarch64 elsewhere); 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: choco shims, Scoop bin entries 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.