How to build a Homebrew bottle from a local formula file in GitHub Actions?
I want to use GitHub Actions to build a bottle for a Homebrew formula that lives in the same repository. My current approach is:
- name: build package
run: |
brew install --build-bottle ./Formula/mypackage.rb
brew bottle mypackage
However, this fails with the error:
Error: Homebrew requires formulae to be in a tap, rejecting:
./Formula/mypackage.rb (/Users/runner/work/homebrew-test/homebrew-test/Formula/mypackage.rb)
If I use brew tap owner/mytap and brew install --build-bottle mypackage, it works but uses the main branch instead of the development branch where I’m making changes.
How can I force Homebrew to build the bottle from the local file mypackage.rb in GitHub Actions?
To build a Homebrew bottle from a local formula file in GitHub Actions, you need to create a proper tap structure or use Homebrew’s tap creation commands. Homebrew requires formulae to be within a tap context rather than raw file paths, which is why your current approach fails with the “requires formulae to be in a tap” error.
Contents
- Understanding the Tap Requirement
- Solution 1: Create a Local Tap Structure
- Solution 2: Use brew tap-new
- Solution 3: Use brew bottle with Formula Path
- Complete GitHub Actions Workflow
- Troubleshooting Common Issues
- Best Practices
Understanding the Tap Requirement
Homebrew’s bottle system is designed around taps, which are essentially Git repositories containing formulae. This structure allows Homebrew to manage dependencies, versions, and binary packages more effectively. When you try to install from a local file path like ./Formula/mypackage.rb, Homebrew rejects it because:
- Bottles need to be associated with a specific tap for proper versioning
- The tap structure provides metadata about dependencies and compatibility
- Bottles are stored in tap-specific directories with standardized naming conventions
As Homebrew discussions explain, “Make sure Formula/my_formular.rb lives in a Homebrew tap (e.g. one created with brew tap-new), and refer to it by name instead of using a relative path.”
Solution 1: Create a Local Tap Structure
The most robust solution is to create a proper tap structure within your repository. This approach mimics how official Homebrew taps are organized.
-
Create the tap directory structure:
your-repo/ ├── .github/ │ └── workflows/ │ └── build.yml ├── Formula/ │ └── mypackage.rb └── Homebrew/ └── tap.rb -
Create a simple tap.rb file:
ruby# Homebrew/tap.rb require "tap" class YourTap < Tap url "https://github.com/yourusername/your-repo" sha256 "your_checksum" # You can generate this after the first build end -
Update your GitHub Actions workflow:
yaml- name: build package run: | brew tap-new yourusername/your-repo brew install --build-bottle --formula ./Formula/mypackage.rb brew bottle mypackage
This approach allows you to build from your local formula while maintaining proper tap context.
Solution 2: Use brew tap-new
You can create a temporary tap in the GitHub Actions environment using brew tap-new:
- name: build package
run: |
brew tap-new yourusername/your-repo
brew install --build-bottle --formula ./Formula/mypackage.rb
brew bottle mypackage
This command creates a tap that your local formula can be associated with. The formula will be referenced by its full tap name (yourusername/your-repo/mypackage) rather than the file path.
Solution 3: Use brew bottle with Formula Path
According to the Stack Overflow solution, you can use the --formula flag with the local path:
- name: build package
run: |
brew install --build-bottle --formula ./Formula/mypackage.rb
brew bottle --json --write ./Formula/mypackage.rb
The --json --write options generate the bottle DSL that can be inserted into your formula file. This approach directly addresses your original attempt by adding the necessary flags.
Complete GitHub Actions Workflow
Here’s a comprehensive workflow that addresses all aspects of bottle building:
name: Build Homebrew Bottle
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest, macos-13, macos-12]
steps:
- uses: actions/checkout@v3
- name: Set up Homebrew
uses: Homebrew/actions/setup-homebrew@master
- name: Build bottle
run: |
# Create a temporary tap
brew tap-new yourusername/your-repo
# Build from local formula
brew install --build-bottle --formula ./Formula/mypackage.rb
# Generate bottle
brew bottle mypackage
# Upload artifacts
find . -name "*.bottle.tar.gz" -exec sh -c 'echo "::set-output name=artifact::$(basename "$0")"' {} \ |
while read artifact; do
echo "::upload-artifact::${artifact}"
done
- name: Generate bottle DSL
run: |
brew bottle --json --write ./Formula/mypackage.rb
git add ./Formula/mypackage.rb
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git commit -m "Update bottle DSL" || true
git push
This workflow:
- Supports multiple macOS versions
- Creates a proper tap context
- Builds bottles from your local formula
- Uploads artifacts as GitHub releases
- Updates the formula with bottle metadata
Troubleshooting Common Issues
Dependency Issues: If your formula has dependencies that aren’t available in the standard Homebrew repository, you may need to install them first:
- name: Install dependencies
run: |
brew install dependency1 dependency2
Cellar Issues: Bottles need to be built in the correct cellar location. If you’re using a custom Homebrew installation:
- name: Set up custom Homebrew
run: |
echo '/usr/local/opt/python/libexec/bin' >> $GITHUB_PATH
Bottle Naming: Bottles must follow the naming convention {formula}-{version}-{os}-{arch}.bottle.tar.gz. The brew bottle command handles this automatically.
Best Practices
- Use Multiple OS Versions: Build bottles for different macOS versions to ensure compatibility
- Test Before Building: Always test your formula with
brew install --dry-runbefore building bottles - Update Bottle DSL: After building, update your formula with the generated bottle DSL
- Use GitHub Releases: Upload bottles as GitHub releases for easy distribution
- Handle Dependencies: Ensure all dependencies are properly specified and available
According to Jonathan Chang’s guide, “When a library your formula depends_on has breaking changes (e.g., Boost), you’ll need to increment the revision number; if this isn’t already present in your formula, just add revision 1.”
Sources
- Build Homebrew bottle in GitHub Actions - Stack Overflow
- Homebrew tap with bottles uploaded to GitHub Releases — Homebrew
- Create bottle from local Formula · Homebrew · Discussion #4468
- Maintain your own Homebrew repository, with binary bottles · Jonathan Chang
- Automate build workflow for Homebrew tap bottles (Linux and macOS) · GitHub Gist
- Bottles.md - Homebrew Documentation
Conclusion
Building Homebrew bottles from local formulae in GitHub Actions requires understanding Homebrew’s tap architecture. The key solutions are:
- Create a proper tap structure - This is the most reliable approach that mirrors official Homebrew practices
- Use
brew tap-new- Creates a temporary tap context for your local formula - Use
--formulaflag - Allows building from local paths with proper flags - Generate bottle DSL - Updates your formula with bottle metadata after building
By implementing one of these solutions, you can successfully build bottles from your local formula files while maintaining compatibility with Homebrew’s bottle system. The recommended approach is to create a proper tap structure as it provides the most robust and maintainable solution for continuous integration workflows.