Distribution and packaging patterns including shell completions, man pages, cross-compilation, and release automation. Use when preparing CLI tools for distribution.
Patterns and best practices for distributing Rust CLI applications to users.
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
}
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)
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()
}
[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.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();
}
# 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/
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
# 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/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 }}
[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
# 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)
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
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"],
]
# 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"]
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
[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(())
}
#[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)
}
}
}
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/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 }}
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer