Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 60 additions & 6 deletions .beans/df-g2t5--new-feature-brew.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,75 @@
---
# df-g2t5
title: 'new feature: Brew'
status: draft
title: 'new feature: Homebrew'
status: done
type: task
created_at: 2026-03-12T15:40:26Z
updated_at: 2026-03-12T15:40:26Z
---
Read these instructions carefully. If further clarification is required, append questions to the body of this bean and set the status to draft (so the user can review and update). When the user provides the information to continue, they will reset the bean status to todo.

For a recap of how to use beans, read the result of `beans prime`

# Changes required

The following brief has been implemented, however the following changes have been made. Read the brief for context about the original implementation

Changes:

- test/homebrew/scenarios.json: jq is not a good tool choice for testing, as it is likely already available on the debian system

# Brief

Implement a new devcontainer feature in src/homebrew that should install homebrew (https://brew.sh/), and optionally, a brew package

This feature has already been started in src/homebrew, but it may need reimplementing

feature options:
- brewPackage: Name of Brew package to install
- brewPackageVersion: Minimum required version of brew package to install
- brewArgs: argument string to pass to brew install command

containerEnv:
- "PATH": "/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin:${PATH}"

install.sh:
1. Check dependencies are installed: bash, curl. Install them if not
1. check if brew is installed. if not, install with brew.sh script below
1. verify brew is installed correctly
1. if a brewPackage is not specified, exit here
1. if a brewpackage is specified, ensure we can use brew install, add to path if necessary
1. check if that package is installed, and check that the required brewPackageVersion is installed, if a sufficient version of the required package is not found, install with brew install
1. verify that the brew package is installed correctly

non-functional requirements:
1. Read other features in the src folder and follow similar implementation patterns where relevant
1. should work on alpine, ubuntu and debian images (may have to be a posix compliant script). bash may need to be installed on alpine
1. Use util functions where appropriate (see ./test/util-test/sample-feature/util.sh)
1. write test scenarios for the feature - run `test/test-builds.sh --help` for scenario file structure
1. When running tests, use test/heartbeat.sh to wrap the test command, to avoid polluting context with test logs. Use grep or tail to read the test logs.

Install script from homebrew: https://brew.sh/
NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

Note it provides instructions at the end on how to add brew to the PATH. We should provide an option in devcontainer.json to do this automatically (so that brew power users can override it manually).
Documentation: add to the NOTES.md file the command to uninstall homebrew at https://github.com/homebrew/install#uninstall-homebrew

# Implementation comments/notes from agent:

Should first check if the required version of brew is already installed. only install brew if a sufficient version of brew cannot be found.
## Implementation

Add an option to install a brew package immediately after brew installs. Must be able to pass brew args. Note we can only install one package this way. can we find a workaround for this?
Reimplemented `src/homebrew` with:

Add an option to remove brew after package install to keep the image small? This is just for the purpose of installing a package with brew
- **`devcontainer-feature.json`**: Replaced single `package` option with `brewPackage`, `brewPackageVersion`, `brewArgs`. Kept `containerEnv` PATH entries.
- **`install.sh`**: Full rewrite using `util.sh` patterns:
- Skips gracefully on Alpine (Homebrew requires glibc; Alpine uses musl libc)
- Installs `curl`/`bash` dependencies if missing
- Installs Homebrew via official `brew.sh` script as remote user
- Verifies brew binary at `/home/linuxbrew/.linuxbrew/bin/brew`
- Exits early if no `brewPackage` specified
- Checks if package + `brewPackageVersion` requirement is already satisfied before installing (uses `semver_gte`)
- Verifies package post-install
- **`util.sh`**: Copied from `test/util-test/sample-feature/util.sh` (v0.1.4)
- **`NOTES.md`**: Documents supported OS, usage examples, and Homebrew uninstall command from https://github.com/homebrew/install#uninstall-homebrew
- **`test/homebrew/scenarios.json`**: 5 scenarios — alpine (warns + exits 0), ubuntu, debian, ubuntu-with-package (hello), ubuntu-with-package-version (hello >= 2.10). Replaced `jq` with `hello` (GNU Hello) as `jq` is pre-installed on Debian/Ubuntu and would not validate brew actually installed it.


4 changes: 3 additions & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
{
// node needed for devcontainers cli
"image": "mcr.microsoft.com/devcontainers/javascript-node:1-20-bookworm",
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
"ghcr.io/stu-bell/devcontainer-features/geminicli": {},
"ghcr.io/stu-bell/devcontainer-features/claude-code": {},
// "ghcr.io/stu-bell/devcontainer-features/geminicli": {},
"ghcr.io/stu-bell/devcontainer-features/neovim": {
"CONFIG_GIT_URL": "https://github.com/stu-bell/nvim.git"
}
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ jobs:
- claude-code
- cursor-cli
- gemini-cli
- homebrew
- neovim
- node
- open-code
Expand Down
40 changes: 40 additions & 0 deletions src/homebrew/NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Supported OS

Tested on Debian and Ubuntu. Alpine Linux is not supported (Homebrew requires glibc; Alpine uses musl libc).

# Get Started

Add the feature to your devcontainer.json:

```devcontainer.json
{
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
"features": {
"ghcr.io/stu-bell/devcontainer-features/homebrew": {}
}
}
```

To also install a brew package:

```devcontainer.json
{
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
"features": {
"ghcr.io/stu-bell/devcontainer-features/homebrew": {
"brewPackage": "jq",
"brewPackageVersion": "1.6"
}
}
}
```

# Uninstall Homebrew

To uninstall Homebrew from within your container, run:

```sh
NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/uninstall.sh)"
```

See https://github.com/homebrew/install#uninstall-homebrew for full uninstall instructions.
29 changes: 29 additions & 0 deletions src/homebrew/devcontainer-feature.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"id": "homebrew",
"version": "1.0.0",
"name": "Homebrew",
"description": "Installs the Homebrew package manager and optionally a brew package",
"options": {
"brewPackage": {
"type": "string",
"default": "",
"description": "Name of Brew package to install (e.g. 'hmans/beans/beans')"
},
"brewPackageVersion": {
"type": "string",
"default": "",
"description": "Minimum required version of brew package to install (e.g. '1.2.0')"
},
"brewArgs": {
"type": "string",
"default": "",
"description": "Argument string to pass to the brew install command (e.g. '--build-from-source')"
}
},
"containerEnv": {
"PATH": "/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin:${PATH}"
},
"installsAfter": [
"ghcr.io/devcontainers/features/common-utils"
]
}
77 changes: 77 additions & 0 deletions src/homebrew/install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/bin/sh
# Copyright (c) 2026 Stuart Bell
# Licensed under the MIT License. See https://github.com/stu-bell/devcontainer-features/blob/main/LICENSE for license information.
set -e
. ./util.sh

BREW_PACKAGE="${BREWPACKAGE:-}"
BREW_PACKAGE_VERSION="${BREWPACKAGEVERSION:-}"
BREW_ARGS="${BREWARGS:-}"
BREW_BIN="/home/linuxbrew/.linuxbrew/bin/brew"

# Homebrew requires glibc and does not support Alpine (musl libc)
if os_alpine; then
echoyel "WARNING: Homebrew does not support Alpine Linux (musl libc). Skipping installation."
exit 0
fi

# 1. Check dependencies are installed: bash, curl. Install them if not.
has_command curl || {
echo "curl not found, installing..."
apt_get_install curl ca-certificates
}
has_command bash || {
echo "bash not found, installing..."
apt_get_install bash
}

# 2. Check if brew is installed. If not, install with brew.sh script.
if [ -x "$BREW_BIN" ]; then
echo "Homebrew is already installed."
else
echo "Installing Homebrew..."
remote_user_run 'NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'
fi

# 3. Verify brew is installed correctly.
if ! [ -x "$BREW_BIN" ]; then
echored "ERROR: Homebrew not found at $BREW_BIN. Installation may have failed."
exit 1
fi
echogrn "Homebrew installed: $($BREW_BIN --version | head -1)"

# 4. If a brewPackage is not specified, exit here.
[ -z "$BREW_PACKAGE" ] && exit 0

# 5. If a brewPackage is specified, ensure we can use brew install, add to PATH if necessary.
export PATH="/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin:$PATH"

# 6. Check if that package is installed and if the required brewPackageVersion is met.
NEEDS_INSTALL=true
if remote_user_run 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" && brew list --formula '"$BREW_PACKAGE"' > /dev/null 2>&1'; then
echo "Package '$BREW_PACKAGE' is already installed."
if [ -n "$BREW_PACKAGE_VERSION" ]; then
installed_ver=$(remote_user_run 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" && brew list --versions '"$BREW_PACKAGE" | awk '{print $2}' | cut -d'_' -f1)
if semver_gte "$installed_ver" "$BREW_PACKAGE_VERSION"; then
echo "Installed version $installed_ver meets minimum required version $BREW_PACKAGE_VERSION."
NEEDS_INSTALL=false
else
echo "Installed version $installed_ver does not meet minimum required $BREW_PACKAGE_VERSION, reinstalling."
fi
else
NEEDS_INSTALL=false
fi
fi

if [ "$NEEDS_INSTALL" = "true" ]; then
echo "Installing brew package: $BREW_PACKAGE"
remote_user_run 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" && brew install '"$BREW_ARGS $BREW_PACKAGE"
fi

# 7. Verify that the brew package is installed correctly.
if remote_user_run 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" && brew list '"$BREW_PACKAGE"' > /dev/null 2>&1'; then
echogrn "Package '$BREW_PACKAGE' installed successfully."
else
echored "ERROR: Package '$BREW_PACKAGE' installation could not be verified."
exit 1
fi
Loading
Loading