Homebrew packages can be installed, upgraded, and removed using simple commands that interact with its internal database and filesystem.
Let’s see it in action. Imagine you need the htop utility, a much nicer way to view running processes than the standard top.
brew install htop
This command fetches the latest htop formula from Homebrew’s core repository, checks for any dependencies (like readline or ncurses if htop needed them, which it doesn’t in this simple case), downloads the source code or pre-compiled binary, compiles it if necessary, and installs it into Homebrew’s Cellar directory. It then creates symbolic links in /usr/local/bin (or /opt/homebrew/bin on Apple Silicon) so you can run htop directly from your terminal.
Now, let’s say a new, improved version of htop is released, or you want to update all your installed packages.
brew upgrade
This command checks every package you’ve installed against the latest available versions in the Homebrew repositories. If an update is found for any package, it will download, compile (if needed), and install the new version, then update the symbolic links. If you only wanted to upgrade htop, you’d run:
brew upgrade htop
Finally, when you no longer need a package, you can remove it.
brew uninstall htop
This command removes the installed files for htop from the Cellar and, crucially, removes the symbolic links from your PATH, ensuring it no longer appears in your shell.
Homebrew’s core is a Git repository containing "formulae" – Ruby scripts that define how to download, compile, and install specific software. When you run brew install, Homebrew pulls the latest version of the relevant formula, executes its Ruby code, and manages the installation process. The Cellar is where Homebrew keeps the actual installed versions of each package, typically in versioned subdirectories (e.g., /usr/local/Cellar/htop/3.2.1/). Symbolic links in /usr/local/bin or /opt/homebrew/bin point to the executables within the Cellar, making them accessible.
The brew upgrade command is a bit more sophisticated than just downloading new versions. It first runs brew update if you haven’t run it recently, which fetches the latest changes from all your configured Homebrew "taps" (repositories, the main one being homebrew/core). This ensures your local list of available formulae and versions is current. Then, it iterates through your installed packages, comparing their installed version against the latest available version defined in the updated formulae. If a newer version exists, it proceeds with the installation of that new version.
When you uninstall a package, Homebrew doesn’t just delete files; it also cleans up any "dangling" symbolic links. These are links that might have been created by the package for helper scripts or other executables. The brew cleanup command is often run automatically after an upgrade or uninstall to remove old, unlinked versions of packages from the Cellar, saving disk space. You can also run brew cleanup manually to remove old versions of all installed packages, keeping only the latest.
The internal state of Homebrew is managed by a SQLite database, which tracks installed packages, their versions, and their dependencies. While you rarely need to interact with this database directly, it’s what allows Homebrew to quickly determine what’s installed and what needs updating.
Most people think of Homebrew as just a package manager, but it also has a robust system for managing "casks" – GUI applications that don’t typically have command-line executables. Using brew install --cask firefox installs applications like Firefox, Chrome, or VS Code, placing them in the /Applications folder and managing their updates similarly, but without the compilation step. This distinction between formulae (for command-line tools) and casks (for GUI apps) is fundamental to understanding Homebrew’s versatility.
The next common hurdle is dealing with broken dependencies or formulae that haven’t been updated in a long time.