stout-bundle 0.2.1

Brewfile parsing, bundle management, and environment snapshots
Documentation

stout-bundle

Crates.io Docs.rs License: MIT

Brewfile parsing, bundle management, and environment snapshots for declarative package management in Rust.

Keywords: brewfile, bundle, declarative, package-management, snapshot, ruby-parser, rust, homebrew, infrastructure-as-code

Why stout-bundle?

Managing a fleet of developer machines or reproducing an environment shouldn't require running commands one by one. stout-bundle lets you declare your dependencies in a Brewfile and install them with a single command. It also generates Brewfiles from your existing installations and creates named snapshots for rollback.

This crate powers the stout bundle, stout snapshot, and stout lock commands, but it's designed as a general-purpose library for any Rust project that needs to manage sets of packages declaratively.

Features

  • Brewfile Parsing — Parse brew, cask, tap, and mas entries from Brewfiles
  • Brewfile Generation — Generate Brewfiles from currently installed packages
  • Bundle Installation — Install all entries in a Brewfile with dependency resolution
  • Bundle Checking — Verify if a Brewfile's requirements are satisfied
  • Named Snapshots — Create and restore named package snapshots
  • Lockfile Support — Generate lockfiles for reproducible environments
  • Ruby DSL Compatibility — Supports standard Homebrew Brewfile syntax
  • Custom Directives — Extensible for custom entry types

Installation

cargo add stout-bundle

Or in your Cargo.toml:

[dependencies]
stout-bundle = "0.2"

Quick Start

use stout_bundle::Brewfile;

// Parse an existing Brewfile
let brewfile = Brewfile::parse("Brewfile")?;

// Print all entries
for entry in brewfile.entries() {
    println!("{:?}", entry);
}

// Generate from installed packages
let brewfile = Brewfile::from_installed()?;
brewfile.save("Brewfile")?;

Brewfile Syntax

Standard Homebrew Brewfile syntax is fully supported:

# Taps
tap "homebrew/core"
tap "homebrew/cask"
tap "neul-labs/custom", "https://github.com/neul-labs/homebrew-custom"

# Formulas (CLI tools)
brew "jq"
brew "curl"
brew "git"
brew "postgresql@16", restart_service: true
brew "node@20", link: true

# Casks (applications)
cask "visual-studio-code"
cask "firefox"
cask "docker", args: { appdir: "~/Applications" }

# Mac App Store apps
mas "Xcode", id: 497799835

# Custom paths
brew "custom-tool", path: "/path/to/formula.rb"

API Overview

Parsing Brewfiles

use stout_bundle::Brewfile;

// Parse from file
let brewfile = Brewfile::parse("Brewfile")?;

// Parse from string
let brewfile = Brewfile::parse_str(r#"
    tap "homebrew/core"
    brew "jq"
    cask "firefox"
"#)?;

// Parse with custom working directory
let brewfile = Brewfile::parse_with_context("Brewfile", "/path/to/project")?;

Working with Entries

use stout_bundle::BrewfileEntry;

for entry in brewfile.entries() {
    match entry {
        BrewfileEntry::Tap { name, url } => {
            println!("Tap: {} ({})", name, url.as_deref().unwrap_or("default"));
        }
        BrewfileEntry::Brew { name, options } => {
            println!("Formula: {}", name);
            if options.restart_service {
                println!("  → Will restart service");
            }
        }
        BrewfileEntry::Cask { name, options } => {
            println!("Cask: {}", name);
            if let Some(appdir) = &options.appdir {
                println!("  → App directory: {}", appdir);
            }
        }
        BrewfileEntry::Mas { name, id } => {
            println!("Mac App Store: {} (ID: {})", name, id);
        }
    }
}

Generating Brewfiles

use stout_bundle::Brewfile;

// Generate from all installed packages
let brewfile = Brewfile::from_installed()?;

// Generate from specific packages
let brewfile = Brewfile::from_packages(
    &["jq", "curl", "git"],           // formulas
    &["firefox", "visual-studio-code"], // casks
    &[],                                // taps (use defaults)
)?;

// Save to file
brewfile.save("Brewfile")?;

// Get as string
let content = brewfile.to_string()?;

Bundle Operations

use stout_bundle::Bundle;
use stout_index::Index;

let index = Index::open_default()?;
let bundle = Bundle::new(&index);

// Install all entries from a Brewfile
let results = bundle.install("Brewfile").await?;
for result in results {
    match result {
        Ok(name) => println!("Installed: {}", name),
        Err((name, e)) => eprintln!("Failed to install {}: {}", name, e),
    }
}

// Check if Brewfile is satisfied (all packages installed)
let is_satisfied = bundle.check("Brewfile")?;
if !is_satisfied {
    println!("Some packages from the Brewfile are not installed");
}

// List what would be installed (dry run)
let plan = bundle.plan("Brewfile")?;
for entry in plan.to_install {
    println!("Would install: {}", entry);
}
for entry in plan.to_upgrade {
    println!("Would upgrade: {}", entry);
}

Snapshots

use stout_bundle::Snapshot;

// Create a named snapshot
let snapshot = Snapshot::create("before-upgrade").await?;
println!("Snapshot created: {}", snapshot.id);

// List all snapshots
let snapshots = Snapshot::list()?;
for s in snapshots {
    println!("{}{}{}", s.id, s.created_at, s.description);
}

// Restore a snapshot
Snapshot::restore("before-upgrade").await?;

// Delete a snapshot
Snapshot::delete("before-upgrade")?;

// Export snapshot as portable archive
Snapshot::export("before-upgrade", "backup.tar.gz").await?;
Snapshot::import("backup.tar.gz").await?;

Lockfiles

use stout_bundle::Lockfile;

// Generate lockfile from current state
let lockfile = Lockfile::generate()?;
lockfile.save("Brewfile.lock")?;

// Load and verify
let lockfile = Lockfile::load("Brewfile.lock")?;
if lockfile.is_current()? {
    println!("Lockfile matches current state");
} else {
    println!("State has diverged from lockfile");
}

// Restore exact versions from lockfile
lockfile.restore().await?;

Cleanup

use stout_bundle::Bundle;

// Remove packages not in the Brewfile
let removed = Bundle::cleanup("Brewfile").await?;
for pkg in removed {
    println!("Removed: {}", pkg);
}

// Preview what would be removed
let to_remove = Bundle::cleanup_plan("Brewfile")?;
for pkg in to_remove {
    println!("Would remove: {}", pkg);
}

Integration with the Stout Ecosystem

stout-bundle is the automation layer of stout:

  • stout-index provides metadata for resolving Brewfile entries
  • stout-resolve computes installation plans for bundle operations
  • stout-install executes the installations
  • stout-cask handles cask entries
  • stout-state tracks what's installed for generation and checking

You can use stout-bundle standalone for any project that needs Brewfile parsing or declarative package management.

License

MIT License — see the repository root for details.