2025-06-26 23:21:54 +01:00
|
|
|
# typed: true
|
2025-03-18 17:38:37 +00:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2025-04-10 13:04:01 +01:00
|
|
|
require "English"
|
2025-03-18 17:38:37 +00:00
|
|
|
require "exceptions"
|
|
|
|
require "extend/ENV"
|
|
|
|
require "utils"
|
|
|
|
require "PATH"
|
|
|
|
|
|
|
|
module Homebrew
|
|
|
|
module Bundle
|
|
|
|
module Commands
|
|
|
|
module Exec
|
|
|
|
PATH_LIKE_ENV_REGEX = /.+#{File::PATH_SEPARATOR}/
|
|
|
|
|
2025-06-26 23:21:54 +01:00
|
|
|
sig {
|
|
|
|
params(
|
|
|
|
args: String,
|
|
|
|
global: T::Boolean,
|
|
|
|
file: T.nilable(String),
|
|
|
|
subcommand: String,
|
|
|
|
services: T::Boolean,
|
|
|
|
check: T::Boolean,
|
|
|
|
).void
|
|
|
|
}
|
2025-03-28 17:21:28 +08:00
|
|
|
def self.run(*args, global: false, file: nil, subcommand: "", services: false, check: false)
|
|
|
|
if check
|
|
|
|
require "bundle/commands/check"
|
|
|
|
Homebrew::Bundle::Commands::Check.run(global:, file:, quiet: true)
|
|
|
|
end
|
|
|
|
|
2025-03-25 13:47:44 +00:00
|
|
|
# Store the old environment so we can check if things were already set
|
|
|
|
# before we start mutating it.
|
|
|
|
old_env = ENV.to_h
|
2025-06-26 23:21:54 +01:00
|
|
|
new_env = T.cast(ENV, Superenv)
|
2025-03-25 13:47:44 +00:00
|
|
|
|
2025-03-18 17:38:37 +00:00
|
|
|
# Setup Homebrew's ENV extensions
|
|
|
|
ENV.activate_extensions!
|
|
|
|
|
|
|
|
command = args.first
|
2025-06-26 23:21:54 +01:00
|
|
|
raise UsageError, "No command to execute was specified!" if command.blank?
|
2025-03-18 17:38:37 +00:00
|
|
|
|
2025-03-24 21:55:47 +08:00
|
|
|
require "bundle/brewfile"
|
2025-03-18 17:38:37 +00:00
|
|
|
@dsl = Brewfile.read(global:, file:)
|
|
|
|
|
|
|
|
require "formula"
|
|
|
|
require "formulary"
|
|
|
|
|
2025-06-26 23:21:54 +01:00
|
|
|
new_env.deps = @dsl.entries.filter_map do |entry|
|
2025-03-18 17:38:37 +00:00
|
|
|
next if entry.type != :brew
|
|
|
|
|
|
|
|
Formulary.factory(entry.name)
|
|
|
|
end
|
|
|
|
|
|
|
|
# Allow setting all dependencies to be keg-only
|
|
|
|
# (i.e. should be explicitly in HOMEBREW_*PATHs ahead of HOMEBREW_PREFIX)
|
2025-06-26 23:21:54 +01:00
|
|
|
new_env.keg_only_deps = if ENV["HOMEBREW_BUNDLE_EXEC_ALL_KEG_ONLY_DEPS"].present?
|
2025-03-18 17:38:37 +00:00
|
|
|
ENV.delete("HOMEBREW_BUNDLE_EXEC_ALL_KEG_ONLY_DEPS")
|
2025-06-26 23:21:54 +01:00
|
|
|
new_env.deps
|
2025-03-18 17:38:37 +00:00
|
|
|
else
|
2025-06-26 23:21:54 +01:00
|
|
|
new_env.deps.select(&:keg_only?)
|
2025-03-18 17:38:37 +00:00
|
|
|
end
|
2025-06-26 23:21:54 +01:00
|
|
|
new_env.setup_build_environment
|
2025-03-18 17:38:37 +00:00
|
|
|
|
|
|
|
# Enable compiler flag filtering
|
|
|
|
ENV.refurbish_args
|
|
|
|
|
|
|
|
# Set up `nodenv`, `pyenv` and `rbenv` if present.
|
|
|
|
env_formulae = %w[nodenv pyenv rbenv]
|
2025-06-26 23:21:54 +01:00
|
|
|
new_env.deps.each do |dep|
|
2025-03-18 17:38:37 +00:00
|
|
|
dep_name = dep.name
|
|
|
|
next unless env_formulae.include?(dep_name)
|
|
|
|
|
|
|
|
dep_root = ENV.fetch("HOMEBREW_#{dep_name.upcase}_ROOT", "#{Dir.home}/.#{dep_name}")
|
|
|
|
ENV.prepend_path "PATH", Pathname.new(dep_root)/"shims"
|
|
|
|
end
|
|
|
|
|
2025-06-09 19:06:16 +01:00
|
|
|
# Setup pkgconf, if needed, to help locate packages
|
|
|
|
Bundle.prepend_pkgconf_path_if_needed!
|
2025-03-18 17:38:37 +00:00
|
|
|
|
2025-03-19 21:27:45 +08:00
|
|
|
# For commands which aren't either absolute or relative
|
|
|
|
# Add the command directory to PATH, since it may get blown away by superenv
|
|
|
|
if command.exclude?("/") && (which_command = which(command)).present?
|
|
|
|
ENV.prepend_path "PATH", which_command.dirname.to_s
|
|
|
|
end
|
2025-03-18 17:38:37 +00:00
|
|
|
|
|
|
|
# Replace the formula versions from the environment variables
|
2025-06-26 23:21:54 +01:00
|
|
|
new_env.deps.each do |formula|
|
2025-04-03 11:05:07 +01:00
|
|
|
formula_name = formula.name
|
|
|
|
formula_version = Bundle.formula_versions_from_env(formula_name)
|
|
|
|
next unless formula_version
|
|
|
|
|
2025-03-18 17:38:37 +00:00
|
|
|
ENV.each do |key, value|
|
|
|
|
opt = %r{/opt/#{formula_name}([/:$])}
|
|
|
|
next unless value.match(opt)
|
|
|
|
|
|
|
|
cellar = "/Cellar/#{formula_name}/#{formula_version}\\1"
|
|
|
|
|
|
|
|
# Look for PATH-like environment variables
|
|
|
|
ENV[key] = if key.include?("PATH") && value.match?(PATH_LIKE_ENV_REGEX)
|
|
|
|
rejected_opts = []
|
|
|
|
path = PATH.new(ENV.fetch("PATH"))
|
2025-03-26 11:00:11 -07:00
|
|
|
.reject do |path_value|
|
2025-06-26 23:21:54 +01:00
|
|
|
if path_value.match?(opt)
|
|
|
|
rejected_opts << path_value
|
|
|
|
true
|
|
|
|
else
|
|
|
|
false
|
|
|
|
end
|
2025-03-18 17:38:37 +00:00
|
|
|
end
|
2025-03-26 11:00:11 -07:00
|
|
|
rejected_opts.each do |path_value|
|
|
|
|
path.prepend(path_value.gsub(opt, cellar))
|
2025-03-18 17:38:37 +00:00
|
|
|
end
|
|
|
|
path.to_s
|
|
|
|
else
|
|
|
|
value.gsub(opt, cellar)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2025-04-25 11:53:23 +01:00
|
|
|
# Ensure brew bundle exec/sh/env commands have access to other tools in the PATH
|
|
|
|
if (homebrew_path = ENV.fetch("HOMEBREW_PATH", nil))
|
2025-03-18 17:38:37 +00:00
|
|
|
ENV.append_path "PATH", homebrew_path
|
|
|
|
end
|
|
|
|
|
2025-03-19 21:27:45 +08:00
|
|
|
# For commands which aren't either absolute or relative
|
|
|
|
raise "command was not found in your PATH: #{command}" if command.exclude?("/") && which(command).nil?
|
|
|
|
|
2025-04-14 19:10:58 +01:00
|
|
|
%w[HOMEBREW_TEMP TMPDIR HOMEBREW_TMPDIR].each do |var|
|
|
|
|
value = ENV.fetch(var, nil)
|
|
|
|
next if value.blank?
|
|
|
|
next if File.writable?(value)
|
|
|
|
|
|
|
|
ENV.delete(var)
|
|
|
|
end
|
|
|
|
|
2025-04-25 11:53:23 +01:00
|
|
|
ENV.each do |key, value|
|
|
|
|
# Look for PATH-like environment variables
|
|
|
|
next if key.exclude?("PATH") || !value.match?(PATH_LIKE_ENV_REGEX)
|
|
|
|
|
|
|
|
# Exclude Homebrew shims from the PATH as they don't work
|
|
|
|
# without all Homebrew environment variables and can interfere with
|
|
|
|
# non-Homebrew builds.
|
|
|
|
ENV[key] = PATH.new(value)
|
|
|
|
.reject do |path_value|
|
|
|
|
path_value.include?("/Homebrew/shims/")
|
|
|
|
end.to_s
|
|
|
|
end
|
|
|
|
|
2025-03-18 17:38:37 +00:00
|
|
|
if subcommand == "env"
|
2025-03-25 13:47:44 +00:00
|
|
|
ENV.sort.each do |key, value|
|
2025-04-10 16:16:05 +01:00
|
|
|
# Skip exporting Homebrew internal variables that won't be used by other tools.
|
|
|
|
# Those Homebrew needs have already been set to global constants and/or are exported again later.
|
|
|
|
# Setting these globally can interfere with nested Homebrew invocations/environments.
|
2025-04-25 11:53:23 +01:00
|
|
|
if key.start_with?("HOMEBREW_", "PORTABLE_RUBY_")
|
|
|
|
ENV.delete(key)
|
|
|
|
next
|
|
|
|
end
|
|
|
|
|
|
|
|
# No need to export empty values.
|
|
|
|
next if value.blank?
|
2025-04-10 16:16:05 +01:00
|
|
|
|
2025-04-10 15:23:02 +01:00
|
|
|
# Skip exporting things that were the same in the old environment.
|
|
|
|
old_value = old_env[key]
|
|
|
|
next if old_value == value
|
2025-03-25 13:47:44 +00:00
|
|
|
|
2025-04-10 15:23:02 +01:00
|
|
|
# Look for PATH-like environment variables
|
|
|
|
if key.include?("PATH") && value.match?(PATH_LIKE_ENV_REGEX)
|
|
|
|
old_values = old_value.to_s.split(File::PATH_SEPARATOR)
|
|
|
|
path = PATH.new(value)
|
|
|
|
.reject do |path_value|
|
|
|
|
# Exclude existing/old values as they've already been exported.
|
2025-04-25 11:53:23 +01:00
|
|
|
old_values.include?(path_value)
|
2025-04-10 15:23:02 +01:00
|
|
|
end
|
|
|
|
next if path.blank?
|
|
|
|
|
|
|
|
puts "export #{key}=\"#{Utils::Shell.sh_quote(path.to_s)}:${#{key}:-}\""
|
|
|
|
else
|
|
|
|
puts "export #{key}=\"#{Utils::Shell.sh_quote(value)}\""
|
|
|
|
end
|
2025-03-18 17:38:37 +00:00
|
|
|
end
|
|
|
|
return
|
2025-06-05 15:43:34 +01:00
|
|
|
elsif subcommand == "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
|
|
|
|
args = [Utils::Shell.shell_with_prompt("brew bundle", preferred_path:, notice:)]
|
2025-03-18 17:38:37 +00:00
|
|
|
end
|
|
|
|
|
2025-03-27 06:12:10 +00:00
|
|
|
if services
|
2025-03-28 05:39:46 +00:00
|
|
|
require "bundle/brew_services"
|
2025-03-27 06:12:10 +00:00
|
|
|
|
2025-06-26 23:21:54 +01:00
|
|
|
exit_code = T.let(0, Integer)
|
2025-03-28 05:39:46 +00:00
|
|
|
run_services(@dsl.entries) do
|
2025-03-27 06:12:10 +00:00
|
|
|
Kernel.system(*args)
|
2025-05-21 16:31:30 +01:00
|
|
|
if (system_exit_code = $CHILD_STATUS&.exitstatus)
|
|
|
|
exit_code = system_exit_code
|
|
|
|
end
|
2025-03-27 06:12:10 +00:00
|
|
|
end
|
|
|
|
exit!(exit_code)
|
|
|
|
else
|
|
|
|
exec(*args)
|
|
|
|
end
|
2025-03-18 17:38:37 +00:00
|
|
|
end
|
2025-03-28 05:39:46 +00:00
|
|
|
|
|
|
|
sig {
|
|
|
|
params(
|
|
|
|
entries: T::Array[Homebrew::Bundle::Dsl::Entry],
|
|
|
|
_block: T.proc.params(
|
2025-04-07 14:10:42 +01:00
|
|
|
entry: Homebrew::Bundle::Dsl::Entry,
|
2025-06-26 23:21:54 +01:00
|
|
|
info: T::Hash[String, T.untyped],
|
2025-03-28 05:39:46 +00:00
|
|
|
service_file: Pathname,
|
2025-06-26 23:21:54 +01:00
|
|
|
conflicting_services: T::Array[T::Hash[String, T.untyped]],
|
2025-03-28 05:39:46 +00:00
|
|
|
).void,
|
|
|
|
).void
|
|
|
|
}
|
|
|
|
private_class_method def self.map_service_info(entries, &_block)
|
|
|
|
entries_formulae = entries.filter_map do |entry|
|
|
|
|
next if entry.type != :brew
|
|
|
|
|
|
|
|
formula = Formula[entry.name]
|
|
|
|
next unless formula.any_version_installed?
|
|
|
|
|
|
|
|
[entry, formula]
|
|
|
|
end.to_h
|
|
|
|
|
2025-04-07 14:12:41 +01:00
|
|
|
return if entries_formulae.empty?
|
|
|
|
|
2025-04-02 07:30:14 +01:00
|
|
|
conflicts = entries_formulae.to_h do |entry, formula|
|
|
|
|
[
|
|
|
|
entry,
|
|
|
|
(
|
|
|
|
formula.versioned_formulae_names +
|
|
|
|
formula.conflicts.map(&:name) +
|
|
|
|
Array(entry.options[:conflicts_with])
|
|
|
|
).uniq,
|
|
|
|
]
|
|
|
|
end
|
|
|
|
|
2025-03-28 05:39:46 +00:00
|
|
|
# The formula + everything that could possible conflict with the service
|
|
|
|
names_to_query = entries_formulae.flat_map do |entry, formula|
|
|
|
|
[
|
|
|
|
formula.name,
|
2025-04-02 07:30:14 +01:00
|
|
|
*conflicts.fetch(entry),
|
2025-03-28 05:39:46 +00:00
|
|
|
]
|
|
|
|
end
|
|
|
|
|
|
|
|
# We parse from a command invocation so that brew wrappers can invoke special actions
|
|
|
|
# for the elevated nature of `brew services`
|
|
|
|
services_info = JSON.parse(
|
|
|
|
Utils.safe_popen_read(HOMEBREW_BREW_FILE, "services", "info", "--json", *names_to_query),
|
|
|
|
)
|
|
|
|
|
|
|
|
entries_formulae.filter_map do |entry, formula|
|
|
|
|
service_file = Bundle::BrewServices.versioned_service_file(entry.name)
|
|
|
|
|
|
|
|
unless service_file&.file?
|
|
|
|
prefix = formula.any_installed_prefix
|
|
|
|
next if prefix.nil?
|
|
|
|
|
|
|
|
service_file = if Homebrew::Services::System.launchctl?
|
|
|
|
prefix/"#{formula.plist_name}.plist"
|
|
|
|
else
|
|
|
|
prefix/"#{formula.service_name}.service"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
next unless service_file.file?
|
|
|
|
|
|
|
|
info = services_info.find { |candidate| candidate["name"] == formula.name }
|
|
|
|
conflicting_services = services_info.select do |candidate|
|
|
|
|
next unless candidate["running"]
|
|
|
|
|
2025-04-02 07:30:14 +01:00
|
|
|
conflicts.fetch(entry).include?(candidate["name"])
|
2025-03-28 05:39:46 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
raise "Failed to get service info for #{entry.name}" if info.nil?
|
|
|
|
|
2025-04-07 14:10:42 +01:00
|
|
|
yield entry, info, service_file, conflicting_services
|
2025-03-28 05:39:46 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
sig { params(entries: T::Array[Homebrew::Bundle::Dsl::Entry], _block: T.nilable(T.proc.void)).void }
|
|
|
|
private_class_method def self.run_services(entries, &_block)
|
2025-04-07 14:10:42 +01:00
|
|
|
entries_to_stop = []
|
2025-03-28 05:39:46 +00:00
|
|
|
services_to_restart = []
|
|
|
|
|
2025-04-07 14:10:42 +01:00
|
|
|
map_service_info(entries) do |entry, info, service_file, conflicting_services|
|
|
|
|
# Don't restart if already running this version
|
|
|
|
loaded_file = Pathname.new(info["loaded_file"].to_s)
|
2025-06-26 23:21:54 +01:00
|
|
|
next if info["running"] && loaded_file.file? && loaded_file.realpath == service_file.realpath
|
2025-04-07 14:10:42 +01:00
|
|
|
|
2025-03-28 05:39:46 +00:00
|
|
|
if info["running"] && !Bundle::BrewServices.stop(info["name"], keep: true)
|
|
|
|
opoo "Failed to stop #{info["name"]} service"
|
|
|
|
end
|
|
|
|
|
|
|
|
conflicting_services.each do |conflict|
|
|
|
|
if Bundle::BrewServices.stop(conflict["name"], keep: true)
|
|
|
|
services_to_restart << conflict["name"] if conflict["registered"]
|
|
|
|
else
|
|
|
|
opoo "Failed to stop #{conflict["name"]} service"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
unless Bundle::BrewServices.run(info["name"], file: service_file)
|
|
|
|
opoo "Failed to start #{info["name"]} service"
|
|
|
|
end
|
2025-04-07 14:10:42 +01:00
|
|
|
|
|
|
|
entries_to_stop << entry
|
2025-03-28 05:39:46 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
return unless block_given?
|
|
|
|
|
|
|
|
begin
|
|
|
|
yield
|
|
|
|
ensure
|
|
|
|
# Do a full re-evaluation of services instead state has changed
|
2025-04-07 14:10:42 +01:00
|
|
|
stop_services(entries_to_stop)
|
2025-03-28 05:39:46 +00:00
|
|
|
|
|
|
|
services_to_restart.each do |service|
|
|
|
|
next if Bundle::BrewServices.run(service)
|
|
|
|
|
|
|
|
opoo "Failed to restart #{service} service"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
sig { params(entries: T::Array[Homebrew::Bundle::Dsl::Entry]).void }
|
|
|
|
private_class_method def self.stop_services(entries)
|
2025-04-07 14:10:42 +01:00
|
|
|
map_service_info(entries) do |_, info, _, _|
|
2025-03-28 05:39:46 +00:00
|
|
|
next unless info["loaded"]
|
|
|
|
|
|
|
|
# Try avoid services not started by `brew bundle services`
|
|
|
|
next if Homebrew::Services::System.launchctl? && info["registered"]
|
|
|
|
|
|
|
|
if info["running"] && !Bundle::BrewServices.stop(info["name"], keep: true)
|
|
|
|
opoo "Failed to stop #{info["name"]} service"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2025-03-18 17:38:37 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|