# typed: strict # frozen_string_literal: true require "abstract_command" module Homebrew module Cmd class Bundle < AbstractCommand cmd_args do usage_banner <<~EOS `bundle` [] Bundler for non-Ruby dependencies from Homebrew, Homebrew Cask, Mac App Store, Whalebrew and Visual Studio Code (and forks/variants). `brew bundle` [`install`]: Install and upgrade (by default) all dependencies from the `Brewfile`. You can specify the `Brewfile` location using `--file` or by setting the `$HOMEBREW_BUNDLE_FILE` environment variable. You can skip the installation of dependencies by adding space-separated values to one or more of the following environment variables: `$HOMEBREW_BUNDLE_BREW_SKIP`, `$HOMEBREW_BUNDLE_CASK_SKIP`, `$HOMEBREW_BUNDLE_MAS_SKIP`, `$HOMEBREW_BUNDLE_WHALEBREW_SKIP`, `$HOMEBREW_BUNDLE_TAP_SKIP`. `brew bundle upgrade`: Shorthand for `brew bundle install --upgrade`. `brew bundle dump`: Write all installed casks/formulae/images/taps into a `Brewfile` in the current directory or to a custom file specified with the `--file` option. `brew bundle cleanup`: Uninstall all dependencies not present in the `Brewfile`. This workflow is useful for maintainers or testers who regularly install lots of formulae. Unless `--force` is passed, this returns a 1 exit code if anything would be removed. `brew bundle check`: Check if all dependencies present in the `Brewfile` are installed. This provides a successful exit code if everything is up-to-date, making it useful for scripting. `brew bundle list`: List all dependencies present in the `Brewfile`. By default, only Homebrew formula dependencies are listed. `brew bundle edit`: Edit the `Brewfile` in your editor. `brew bundle add` [...]: Add entries to your `Brewfile`. Adds formulae by default. Use `--cask`, `--tap`, `--whalebrew` or `--vscode` to add the corresponding entry instead. `brew bundle remove` [...]: Remove entries that match `name` from your `Brewfile`. Use `--formula`, `--cask`, `--tap`, `--mas`, `--whalebrew` or `--vscode` to remove only entries of the corresponding type. Passing `--formula` also removes matches against formula aliases and old formula names. `brew bundle exec` : Run an external command in an isolated build environment based on the `Brewfile` dependencies. This sanitized build environment ignores unrequested dependencies, which makes sure that things you didn't specify in your `Brewfile` won't get picked up by commands like `bundle install`, `npm install`, etc. It will also add compiler flags which will help with finding keg-only dependencies like `openssl`, `icu4c`, etc. `brew bundle sh`: Run your shell in a `brew bundle exec` environment. `brew bundle env`: Print the environment variables that would be set in a `brew bundle exec` environment. EOS flag "--file=", description: "Read from or write to the `Brewfile` from this location. " \ "Use `--file=-` to pipe to stdin/stdout." switch "--global", description: "Read from or write to the `Brewfile` from `$HOMEBREW_BUNDLE_FILE_GLOBAL` (if set), " \ "`${XDG_CONFIG_HOME}/homebrew/Brewfile` (if `$XDG_CONFIG_HOME` is set), " \ "`~/.homebrew/Brewfile` or `~/.Brewfile` otherwise." switch "-v", "--verbose", description: "`install` prints output from commands as they are run. " \ "`check` lists all missing dependencies." switch "--no-upgrade", env: :bundle_no_upgrade, description: "`install` does not run `brew upgrade` on outdated dependencies. " \ "`check` does not check for outdated dependencies. " \ "Note they may still be upgraded by `brew install` if needed. " \ "This is enabled by default if `$HOMEBREW_BUNDLE_NO_UPGRADE` is set." switch "--upgrade", description: "`install` runs `brew upgrade` on outdated dependencies, " \ "even if `$HOMEBREW_BUNDLE_NO_UPGRADE` is set." flag "--upgrade-formulae=", "--upgrade-formula=", description: "`install` runs `brew upgrade` on any of these comma-separated formulae, " \ "even if `$HOMEBREW_BUNDLE_NO_UPGRADE` is set." switch "--install", description: "Run `install` before continuing to other operations e.g. `exec`." switch "--services", env: :bundle_services, description: "Temporarily start services while running the `exec` or `sh` command. " \ "This is enabled by default if `$HOMEBREW_BUNDLE_SERVICES` is set." switch "-f", "--force", description: "`install` runs with `--force`/`--overwrite`. " \ "`dump` overwrites an existing `Brewfile`. " \ "`cleanup` actually performs its cleanup operations." switch "--cleanup", env: :bundle_install_cleanup, description: "`install` performs cleanup operation, same as running `cleanup --force`. " \ "This is enabled by default if `$HOMEBREW_BUNDLE_INSTALL_CLEANUP` is set and " \ "`--global` is passed." switch "--all", description: "`list` all dependencies." switch "--formula", "--brews", description: "`list` or `dump` Homebrew formula dependencies." switch "--cask", "--casks", description: "`list` or `dump` Homebrew cask dependencies." switch "--tap", "--taps", description: "`list` or `dump` Homebrew tap dependencies." switch "--mas", description: "`list` or `dump` Mac App Store dependencies." switch "--whalebrew", description: "`list` or `dump` Whalebrew dependencies." switch "--vscode", description: "`list` or `dump` VSCode (and forks/variants) extensions." switch "--no-vscode", env: :bundle_dump_no_vscode, description: "`dump` without VSCode (and forks/variants) extensions. " \ "This is enabled by default if `$HOMEBREW_BUNDLE_DUMP_NO_VSCODE` is set." switch "--describe", env: :bundle_dump_describe, description: "`dump` adds a description comment above each line, unless the " \ "dependency does not have a description. " \ "This is enabled by default if `$HOMEBREW_BUNDLE_DUMP_DESCRIBE` is set." switch "--no-restart", description: "`dump` does not add `restart_service` to formula lines." switch "--zap", description: "`cleanup` casks using the `zap` command instead of `uninstall`." conflicts "--all", "--no-vscode" conflicts "--vscode", "--no-vscode" conflicts "--install", "--upgrade" named_args %w[install dump cleanup check exec list sh env edit] end sig { override.void } def run # Keep this inside `run` to keep --help fast. require "bundle" subcommand = args.named.first.presence if %w[exec add remove].exclude?(subcommand) && args.named.size > 1 raise UsageError, "This command does not take more than 1 subcommand argument." end global = args.global? file = args.file args.zap? no_upgrade = if args.upgrade? || subcommand == "upgrade" false else args.no_upgrade? end verbose = args.verbose? force = args.force? zap = args.zap? Homebrew::Bundle.upgrade_formulae = args.upgrade_formulae no_type_args = !args.brews? && !args.casks? && !args.taps? && !args.mas? && !args.whalebrew? && !args.vscode? if args.install? if [nil, "install", "upgrade"].include?(subcommand) raise UsageError, "`--install` cannot be used with `install`, `upgrade` or no subcommand." end require "bundle/commands/install" redirect_stdout($stderr) do Homebrew::Bundle::Commands::Install.run(global:, file:, no_upgrade:, verbose:, force:, quiet: true) end end case subcommand when nil, "install", "upgrade" require "bundle/commands/install" Homebrew::Bundle::Commands::Install.run(global:, file:, no_upgrade:, verbose:, force:, quiet: args.quiet?) cleanup = if ENV.fetch("HOMEBREW_BUNDLE_INSTALL_CLEANUP", nil) args.global? else args.cleanup? end if cleanup require "bundle/commands/cleanup" Homebrew::Bundle::Commands::Cleanup.run( global:, file:, zap:, force: true, dsl: Homebrew::Bundle::Commands::Install.dsl ) end when "dump" vscode = if args.no_vscode? false elsif args.vscode? true else no_type_args end require "bundle/commands/dump" Homebrew::Bundle::Commands::Dump.run( global:, file:, force:, describe: args.describe?, no_restart: args.no_restart?, taps: args.taps? || no_type_args, brews: args.brews? || no_type_args, casks: args.casks? || no_type_args, mas: args.mas? || no_type_args, whalebrew: args.whalebrew? || no_type_args, vscode: ) when "edit" require "bundle/brewfile" exec_editor(Homebrew::Bundle::Brewfile.path(global:, file:)) when "cleanup" require "bundle/commands/cleanup" Homebrew::Bundle::Commands::Cleanup.run(global:, file:, force:, zap:) when "check" require "bundle/commands/check" Homebrew::Bundle::Commands::Check.run(global:, file:, no_upgrade:, verbose:) when "exec", "sh", "env" named_args = case subcommand when "exec" _subcommand, *named_args = args.named named_args when "sh" preferred_path = Utils::Shell.preferred_path(default: "/bin/bash") notice = unless Homebrew::EnvConfig.no_env_hints? <<~EOS Your shell has been configured to use a build environment from your `Brewfile`. This should help you build stuff. Hide these hints with HOMEBREW_NO_ENV_HINTS (see `man brew`). When done, type `exit`. EOS end ENV["HOMEBREW_FORCE_API_AUTO_UPDATE"] = nil [Utils::Shell.shell_with_prompt("brew bundle", preferred_path:, notice:)] when "env" ["env"] end require "bundle/commands/exec" Homebrew::Bundle::Commands::Exec.run(*named_args, global:, file:, subcommand:, services: args.services?) when "list" require "bundle/commands/list" Homebrew::Bundle::Commands::List.run( global:, file:, brews: args.brews? || args.all? || no_type_args, casks: args.casks? || args.all?, taps: args.taps? || args.all?, mas: args.mas? || args.all?, whalebrew: args.whalebrew? || args.all?, vscode: args.vscode? || args.all?, ) when "add", "remove" # We intentionally omit the `s` from `brews`, `casks`, and `taps` for ease of handling later. type_hash = { brew: args.brews?, cask: args.casks?, tap: args.taps?, mas: args.mas?, whalebrew: args.whalebrew?, vscode: args.vscode?, none: no_type_args, } selected_types = type_hash.select { |_, v| v }.keys raise UsageError, "`#{subcommand}` supports only one type of entry at a time." if selected_types.count != 1 _, *named_args = args.named if subcommand == "add" type = case (t = selected_types.first) when :none then :brew when :mas then raise UsageError, "`add` does not support `--mas`." else t end require "bundle/commands/add" Homebrew::Bundle::Commands::Add.run(*named_args, type:, global:, file:) else require "bundle/commands/remove" Homebrew::Bundle::Commands::Remove.run(*named_args, type: selected_types.first, global:, file:) end else raise UsageError, "unknown subcommand: #{subcommand}" end end end end end