Cross-Compilation
To compile your application for different platforms, you can use the cross
tool:
# Install cross
cargo install cross
# Build for Windows
cross build --target x86_64-pc-windows-gnu --release
# Build for macOS
cross build --target x86_64-apple-darwin --release
# Build for Linux
cross build --target x86_64-unknown-linux-gnu --release
Binary Size Optimization
Rust binaries can be large, but several techniques can reduce their size:
# Cargo.toml
[profile.release]
opt-level = 'z' # Optimize for size
lto = true # Enable Link Time Optimization
codegen-units = 1 # Reduce parallel code generation units
panic = 'abort' # Abort on panic
strip = true # Strip symbols from binary
After building, you can further reduce the size using tools like upx
:
upx --best --lzma target/release/myapp
Distribution via Package Managers
For wider distribution, you can publish your application to package managers:
- Cargo: Publish to crates.io with
cargo publish
- Homebrew: Create a formula for macOS users
- Apt/Yum/Pacman: Create packages for Linux distributions
- Chocolatey/Scoop: Create packages for Windows users
Advanced Techniques
Let’s explore some advanced techniques for building more sophisticated CLI applications:
Interactive User Input
The dialoguer
crate provides utilities for interactive user input:
use dialoguer::{Confirm, Input, MultiSelect, Password, Select};
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
// Simple confirmation
if Confirm::new().with_prompt("Do you want to continue?").interact()? {
println!("Continuing...");
} else {
println!("Aborting.");
return Ok(());
}
// Text input
let name: String = Input::new()
.with_prompt("Enter your name")
.default("User".into())
.interact_text()?;
// Password input
let password = Password::new()
.with_prompt("Enter your password")
.with_confirmation("Confirm password", "Passwords don't match")
.interact()?;
// Selection from a list
let items = vec!["Option 1", "Option 2", "Option 3"];
let selection = Select::new()
.with_prompt("Select an option")
.items(&items)
.default(0)
.interact()?;
// Multiple selection
let selections = MultiSelect::new()
.with_prompt("Select multiple options")
.items(&items)
.interact()?;
println!("Name: {}", name);
println!("Password length: {}", password.len());
println!("Selected option: {}", items[selection]);
println!("Multiple selections: {:?}", selections.iter().map(|i| items[*i]).collect::<Vec<_>>());
Ok(())
}
Persistent State
For applications that need to maintain state between runs, you can use the directories
crate to find appropriate directories for configuration and data:
use directories::ProjectDirs;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
#[derive(Debug, Serialize, Deserialize)]
struct AppState {
last_run: chrono::DateTime<chrono::Utc>,
run_count: u32,
last_file: Option<String>,
}
impl Default for AppState {
fn default() -> Self {
Self {
last_run: chrono::Utc::now(),
run_count: 0,
last_file: None,
}
}
}
fn get_state_file() -> Option<PathBuf> {
ProjectDirs::from("com", "example", "myapp").map(|dirs| {
let data_dir = dirs.data_dir();
fs::create_dir_all(data_dir).ok()?;
Some(data_dir.join("state.json"))
})?
}
fn load_state() -> AppState {
if let Some(state_file) = get_state_file() {
if let Ok(content) = fs::read_to_string(state_file) {
if let Ok(state) = serde_json::from_str(&content) {
return state;
}
}
}
AppState::default()
}
fn save_state(state: &AppState) -> Result<(), Box<dyn std::error::Error>> {
if let Some(state_file) = get_state_file() {
let content = serde_json::to_string_pretty(state)?;
fs::write(state_file, content)?;
}
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Load state
let mut state = load_state();
// Update state
state.run_count += 1;
state.last_run = chrono::Utc::now();
state.last_file = Some("example.txt".to_string());
println!("Run count: {}", state.run_count);
println!("Last run: {}", state.last_run);
// Save state
save_state(&state)?;
Ok(())
}