| name | cli-distribution |
| description | Distribution and packaging patterns including shell completions, man pages, cross-compilation, and release automation. Use when preparing CLI tools for distribution. |
CLI Distribution Skill
Patterns and best practices for distributing Rust CLI applications to users.
Shell Completion Generation
Using clap_complete
use clap::{CommandFactory, Parser};
use clap_complete::{generate, Generator, Shell};
use std::io;
#[derive(Parser)]
struct Cli {
/// Generate shell completions
#[arg(long = "generate", value_enum)]
generator: Option<Shell>,
// ... other fields
}
fn print_completions<G: Generator>(gen: G, cmd: &mut clap::Command) {
generate(gen, cmd, cmd.get_name().to_string(), &mut io::stdout());
}
fn main() {
let cli = Cli::parse();
if let Some(generator) = cli.generator {
let mut cmd = Cli::command();
print_completions(generator, &mut cmd);
return;
}
// ... rest of application
}
Installation Instructions by Shell
Bash:
# Generate and save
myapp --generate bash > /etc/bash_completion.d/myapp
# Or add to ~/.bashrc
eval "$(myapp --generate bash)"
Zsh:
# Generate and save
myapp --generate zsh > ~/.zfunc/_myapp
# Add to ~/.zshrc
fpath=(~/.zfunc $fpath)
autoload -Uz compinit && compinit
Fish:
# Generate and save
myapp --generate fish > ~/.config/fish/completions/myapp.fish
# Or load directly
myapp --generate fish | source
PowerShell:
# Add to $PROFILE
Invoke-Expression (& myapp --generate powershell)
Dynamic Completions
For commands with dynamic values (like listing resources):
use clap::CommandFactory;
use clap_complete::{generate, Generator};
pub fn generate_with_values<G: Generator>(
gen: G,
resources: &[String],
) -> String {
let mut cmd = Cli::command();
// Add dynamic values to completion
if let Some(subcommand) = cmd.find_subcommand_mut("get") {
for resource in resources {
subcommand = subcommand.arg(
clap::Arg::new("resource")
.value_parser(clap::builder::PossibleValuesParser::new(resource))
);
}
}
let mut buf = Vec::new();
generate(gen, &mut cmd, "myapp", &mut buf);
String::from_utf8(buf).unwrap()
}
Man Page Generation
Using clap_mangen
[dependencies]
clap_mangen = "0.2"
use clap::CommandFactory;
use clap_mangen::Man;
use std::io;
fn generate_man_page() {
let cmd = Cli::command();
let man = Man::new(cmd);
man.render(&mut io::stdout()).unwrap();
}
Build Script for Man Pages
// build.rs
use clap::CommandFactory;
use clap_mangen::Man;
use std::fs;
use std::path::PathBuf;
include!("src/cli.rs");
fn main() {
let out_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("target/man");
fs::create_dir_all(&out_dir).unwrap();
let cmd = Cli::command();
let man = Man::new(cmd);
let mut buffer = Vec::new();
man.render(&mut buffer).unwrap();
fs::write(out_dir.join("myapp.1"), buffer).unwrap();
}
Install Man Page
# System-wide
sudo cp target/man/myapp.1 /usr/local/share/man/man1/
# User-local
mkdir -p ~/.local/share/man/man1
cp target/man/myapp.1 ~/.local/share/man/man1/
Cross-Compilation
Target Triples
Common targets for CLI distribution:
# Linux
x86_64-unknown-linux-gnu # GNU Linux
x86_64-unknown-linux-musl # MUSL Linux (static)
aarch64-unknown-linux-gnu # ARM64 Linux
# macOS
x86_64-apple-darwin # Intel Mac
aarch64-apple-darwin # Apple Silicon
# Windows
x86_64-pc-windows-msvc # Windows MSVC
x86_64-pc-windows-gnu # Windows GNU
Cross-Compilation with cross
# Install cross
cargo install cross
# Build for Linux from any platform
cross build --release --target x86_64-unknown-linux-gnu
# Build static binary with MUSL
cross build --release --target x86_64-unknown-linux-musl
GitHub Actions for Cross-Compilation
# .github/workflows/release.yml
name: Release
on:
push:
tags:
- 'v*'
jobs:
build:
strategy:
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
artifact_name: myapp
asset_name: myapp-linux-amd64
- os: ubuntu-latest
target: x86_64-unknown-linux-musl
artifact_name: myapp
asset_name: myapp-linux-musl-amd64
- os: macos-latest
target: x86_64-apple-darwin
artifact_name: myapp
asset_name: myapp-macos-amd64
- os: macos-latest
target: aarch64-apple-darwin
artifact_name: myapp
asset_name: myapp-macos-arm64
- os: windows-latest
target: x86_64-pc-windows-msvc
artifact_name: myapp.exe
asset_name: myapp-windows-amd64.exe
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Build
run: cargo build --release --target ${{ matrix.target }}
- name: Upload binaries
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.asset_name }}
path: target/${{ matrix.target }}/release/${{ matrix.artifact_name }}
Binary Size Optimization
Cargo.toml optimizations
[profile.release]
opt-level = "z" # Optimize for size
lto = true # Link-time optimization
codegen-units = 1 # Better optimization
strip = true # Strip symbols
panic = "abort" # Smaller panic handler
Additional size reduction
# Install upx
brew install upx # macOS
apt install upx # Linux
# Compress binary
upx --best --lzma target/release/myapp
Before/After example:
Original: 2.5 MB
Optimized: 1.2 MB (strip = true)
UPX: 400 KB (upx --best --lzma)
Package Distribution
Homebrew (macOS/Linux)
Create a formula:
# Formula/myapp.rb
class Myapp < Formula
desc "Description of your CLI tool"
homepage "https://github.com/username/myapp"
url "https://github.com/username/myapp/archive/v1.0.0.tar.gz"
sha256 "abc123..."
license "MIT"
depends_on "rust" => :build
def install
system "cargo", "install", "--locked", "--root", prefix, "--path", "."
# Install shell completions
generate_completions_from_executable(bin/"myapp", "--generate")
# Install man page
man1.install "target/man/myapp.1"
end
test do
assert_match "myapp 1.0.0", shell_output("#{bin}/myapp --version")
end
end
Debian Package (.deb)
Using cargo-deb:
cargo install cargo-deb
# Create debian package
cargo deb
# Package will be in target/debian/myapp_1.0.0_amd64.deb
Cargo.toml metadata:
[package.metadata.deb]
maintainer = "Your Name <you@example.com>"
copyright = "2024, Your Name"
license-file = ["LICENSE", "4"]
extended-description = """
A longer description of your CLI tool
that spans multiple lines."""
depends = "$auto"
section = "utility"
priority = "optional"
assets = [
["target/release/myapp", "usr/bin/", "755"],
["README.md", "usr/share/doc/myapp/", "644"],
["target/completions/myapp.bash", "usr/share/bash-completion/completions/", "644"],
["target/man/myapp.1", "usr/share/man/man1/", "644"],
]
Docker Distribution
# Dockerfile
FROM rust:1.75 as builder
WORKDIR /app
COPY . .
RUN cargo build --release
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release/myapp /usr/local/bin/myapp
ENTRYPOINT ["myapp"]
Multi-stage with MUSL (smaller image):
FROM rust:1.75-alpine as builder
RUN apk add --no-cache musl-dev
WORKDIR /app
COPY . .
RUN cargo build --release --target x86_64-unknown-linux-musl
FROM scratch
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/myapp /myapp
ENTRYPOINT ["/myapp"]
Cargo-binstall Support
Add metadata for faster installation:
[package.metadata.binstall]
pkg-url = "{ repo }/releases/download/v{ version }/{ name }-{ target }{ binary-ext }"
bin-dir = "{ bin }{ binary-ext }"
pkg-fmt = "bin"
Users can then install with:
cargo binstall myapp
Auto-Update
Using self_update crate
[dependencies]
self_update = "0.39"
use self_update::cargo_crate_version;
fn update() -> Result<()> {
let status = self_update::backends::github::Update::configure()
.repo_owner("username")
.repo_name("myapp")
.bin_name("myapp")
.show_download_progress(true)
.current_version(cargo_crate_version!())
.build()?
.update()?;
println!("Update status: `{}`!", status.version());
Ok(())
}
Update Command
#[derive(Subcommand)]
enum Commands {
/// Update to the latest version
Update,
}
fn handle_update() -> Result<()> {
println!("Checking for updates...");
match update() {
Ok(_) => {
println!("Updated successfully! Please restart the application.");
Ok(())
}
Err(e) => {
eprintln!("Update failed: {}", e);
eprintln!("Download manually: https://github.com/username/myapp/releases");
Err(e)
}
}
}
Release Automation
Cargo-release
cargo install cargo-release
# Dry run
cargo release --dry-run
# Release patch version
cargo release patch --execute
# Release minor version
cargo release minor --execute
GitHub Release Action
# .github/workflows/release.yml
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: taiki-e/create-gh-release-action@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
changelog: CHANGELOG.md
upload-assets:
needs: release
strategy:
matrix:
include:
- target: x86_64-unknown-linux-gnu
- target: x86_64-apple-darwin
- target: x86_64-pc-windows-msvc
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: taiki-e/upload-rust-binary-action@v1
with:
bin: myapp
target: ${{ matrix.target }}
token: ${{ secrets.GITHUB_TOKEN }}
Best Practices
- Provide multiple installation methods - Cargo, Homebrew, apt, etc.
- Generate completions - Essential for good UX
- Create man pages - Professional documentation
- Test cross-platform - Build for all major platforms
- Optimize binary size - Users appreciate smaller downloads
- Automate releases - Use CI/CD for consistent builds
- Version clearly - Semantic versioning
- Sign binaries - Build trust (especially on macOS)
- Provide checksums - Verify download integrity
- Document installation - Clear, platform-specific instructions
Distribution Checklist
- Shell completions generated (bash, zsh, fish, powershell)
- Man pages created
- Cross-compiled for major platforms
- Binary size optimized
- Release artifacts uploaded to GitHub
- Installation instructions in README
- Homebrew formula (if applicable)
- Debian package (if applicable)
- Docker image (if applicable)
- Checksums provided
- Changelog maintained
- Version bumped properly