Packaging a Python CLI Tool for Homebrew

To package a Python CLI tool for Homebrew, create a GitHub tap, generate a formula using homebrew-pypi-poet, add tests, and optionally submit to Homebrew core for pre-compiled bottle distributions.

Quick Glance

  • Tap: GitHub repository with homebrew- prefix that gets tapped when users run brew tap
  • Formula: Ruby .rb file in the Formula/ folder defining how to install the package
  • homebrew-pypi-poet: Tool to automatically generate formulas from Python packages
  • Test block: Required to verify basic functionality without user input
  • Bottle: Pre-compiled distributions for faster Homebrew installs

Key Concepts

Concept Definition Context/Example
Tap GitHub repository with homebrew- prefix that users tap with brew tap https://github.com/simonw/homebrew-datasette for brew tap simonw/datasette
Formula Ruby .rb file in the Formula/ folder defining package installation datasette.rb with dependencies, resources, and test block
Bottle Pre-compiled .tar.gz bundles for each macOS version for faster installs Homebrew core provides bottles for all core packages

Creating a Tap

  • Create a GitHub repository with homebrew- prefix
  • Repository URL becomes the tap: https://github.com/simonw/homebrew-datasette
  • Users tap it with brew tap simonw/datasette
  • Repository needs a Formula/ folder containing .rb formula files

Creating the Formula

  • Every dependency must be listed as a resource
  • All dependencies must have sdist packages on PyPI
  • Use homebrew-pypi-poet to generate the formula
  • Must run in a fresh virtual environment to avoid including unrelated packages

Workflow:

  1. Create fresh virtual environment: python -m venv venv and activate
  2. Install tool and poet: pip install strip-tags homebrew-pypi-poet
  3. Generate formula: poet -f strip-tags > strip-tags.rb
  4. Test locally: HOMEBREW_NO_INSTALL_FROM_API=1 brew install --build-from-source --verbose --debug strip-tags.rb
  5. Verify installation: which strip-tags
  6. Add to GitHub repo and test from tap: brew uninstall strip-tags && brew install yourname/yourtap/strip-tags

Implementing the Test Block

  • Tests should not require user input and verify basic functionality
  • Avoid --version and --help tests (widely used but not ideal)
  • Example for Datasette:
test do
    assert_match "15", shell_output("#{bin}/datasette --get '/:memory:.csv?sql=select+3*5'")
    assert_match "<title>Datasette:", shell_output("#{bin}/datasette --get '/'")
  end

Iterating During Development

  • Run brew install package, test, then brew uninstall package
  • Modify .rb file on GitHub
  • Run brew install yourname/yourtap/package again
  • Use brew install package --debug for detailed error information and interactive debugging

Submitting to Homebrew-core

  • If accepted, users can install with brew install packagename
  • Homebrew provides bottles (pre-compiled) for all core packages
  • For Python packages, remember to:
    • Add a license field (e.g., license "Apache 2.0")
    • Run brew audit --new-formula package and fix warnings
    • Submit PR with title format: package X.Y.Z (new formula)

Key Takeaways

  1. Formula Generation: Use homebrew-pypi-poet in a fresh virtual environment to automatically generate Homebrew formulas from Python packages.
  2. Testing Requirements: Always include a test block that verifies basic functionality without user input.
  3. Development Workflow: Use brew install --debug for detailed error information and interactive debugging during development.
  4. Core Submission: For Python packages, add a license field, run brew audit, and submit a PR with the correct title format to get into Homebrew core.
  5. Bottles Matter: Homebrew core provides pre-compiled bottles for faster installs across macOS versions.

Resources