mirror of
https://github.com/Homebrew/brew.git
synced 2025-07-14 16:09:03 +08:00

From reading https://github.com/orgs/Homebrew/discussions/3328: I initially thought we should just change "Updated" to "Modified" when appropriate. After conversation with Bo98, though, I thought more and saw that we're already checking for outdated formulae here so, rather than ever traverse through the formula history, look at the outdated formula and list them unless we've set `HOMEBREW_UPDATE_REPORT_ALL_FORMULAE` in which case we show the modifications. While we're here, also do a bit of reformatting and renaming to better clarify intent.
634 lines
17 KiB
Ruby
634 lines
17 KiB
Ruby
# typed: false
|
|
# frozen_string_literal: true
|
|
|
|
require "time"
|
|
|
|
require "utils/analytics"
|
|
require "utils/curl"
|
|
require "utils/fork"
|
|
require "utils/formatter"
|
|
require "utils/gems"
|
|
require "utils/git"
|
|
require "utils/git_repository"
|
|
require "utils/github"
|
|
require "utils/inreplace"
|
|
require "utils/link"
|
|
require "utils/popen"
|
|
require "utils/repology"
|
|
require "utils/svn"
|
|
require "utils/tty"
|
|
require "tap_constants"
|
|
|
|
module Homebrew
|
|
extend Context
|
|
|
|
module_function
|
|
|
|
def _system(cmd, *args, **options)
|
|
pid = fork do
|
|
yield if block_given?
|
|
args.map!(&:to_s)
|
|
begin
|
|
exec(cmd, *args, **options)
|
|
rescue
|
|
nil
|
|
end
|
|
exit! 1 # never gets here unless exec failed
|
|
end
|
|
Process.wait(T.must(pid))
|
|
$CHILD_STATUS.success?
|
|
end
|
|
|
|
def system(cmd, *args, **options)
|
|
if verbose?
|
|
puts "#{cmd} #{args * " "}".gsub(RUBY_PATH, "ruby")
|
|
.gsub($LOAD_PATH.join(File::PATH_SEPARATOR).to_s, "$LOAD_PATH")
|
|
end
|
|
_system(cmd, *args, **options)
|
|
end
|
|
|
|
# rubocop:disable Style/GlobalVars
|
|
def inject_dump_stats!(the_module, pattern)
|
|
@injected_dump_stat_modules ||= {}
|
|
@injected_dump_stat_modules[the_module] ||= []
|
|
injected_methods = @injected_dump_stat_modules[the_module]
|
|
the_module.module_eval do
|
|
instance_methods.grep(pattern).each do |name|
|
|
next if injected_methods.include? name
|
|
|
|
method = instance_method(name)
|
|
define_method(name) do |*args, &block|
|
|
time = Time.now
|
|
|
|
begin
|
|
method.bind(self).call(*args, &block)
|
|
ensure
|
|
$times[name] ||= 0
|
|
$times[name] += Time.now - time
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
return unless $times.nil?
|
|
|
|
$times = {}
|
|
at_exit do
|
|
col_width = [$times.keys.map(&:size).max.to_i + 2, 15].max
|
|
$times.sort_by { |_k, v| v }.each do |method, time|
|
|
puts format("%<method>-#{col_width}s %<time>0.4f sec", method: "#{method}:", time: time)
|
|
end
|
|
end
|
|
end
|
|
# rubocop:enable Style/GlobalVars
|
|
end
|
|
|
|
module Kernel
|
|
extend T::Sig
|
|
|
|
def require?(path)
|
|
return false if path.nil?
|
|
|
|
require path
|
|
true
|
|
rescue LoadError => e
|
|
# we should raise on syntax errors but not if the file doesn't exist.
|
|
raise unless e.message.include?(path)
|
|
end
|
|
|
|
def ohai_title(title)
|
|
verbose = if respond_to?(:verbose?)
|
|
verbose?
|
|
else
|
|
Context.current.verbose?
|
|
end
|
|
|
|
title = Tty.truncate(title) if $stdout.tty? && !verbose
|
|
Formatter.headline(title, color: :blue)
|
|
end
|
|
|
|
def ohai(title, *sput)
|
|
puts ohai_title(title)
|
|
puts sput
|
|
end
|
|
|
|
def odebug(title, *sput, always_display: false)
|
|
debug = if respond_to?(:debug)
|
|
debug?
|
|
else
|
|
Context.current.debug?
|
|
end
|
|
|
|
return if !debug && !always_display
|
|
|
|
puts Formatter.headline(title, color: :magenta)
|
|
puts sput unless sput.empty?
|
|
end
|
|
|
|
def oh1(title, truncate: :auto)
|
|
verbose = if respond_to?(:verbose?)
|
|
verbose?
|
|
else
|
|
Context.current.verbose?
|
|
end
|
|
|
|
title = Tty.truncate(title) if $stdout.tty? && !verbose && truncate == :auto
|
|
puts Formatter.headline(title, color: :green)
|
|
end
|
|
|
|
# Print a message prefixed with "Warning" (do this rarely).
|
|
def opoo(message)
|
|
Tty.with($stderr) do |stderr|
|
|
stderr.puts Formatter.warning(message, label: "Warning")
|
|
end
|
|
end
|
|
|
|
# Print a message prefixed with "Error".
|
|
def onoe(message)
|
|
Tty.with($stderr) do |stderr|
|
|
stderr.puts Formatter.error(message, label: "Error")
|
|
end
|
|
end
|
|
|
|
def ofail(error)
|
|
onoe error
|
|
Homebrew.failed = true
|
|
end
|
|
|
|
def odie(error)
|
|
onoe error
|
|
exit 1
|
|
end
|
|
|
|
def odeprecated(method, replacement = nil,
|
|
disable: false,
|
|
disable_on: nil,
|
|
disable_for_developers: true,
|
|
caller: send(:caller))
|
|
replacement_message = if replacement
|
|
"Use #{replacement} instead."
|
|
else
|
|
"There is no replacement."
|
|
end
|
|
|
|
unless disable_on.nil?
|
|
if disable_on > Time.now
|
|
will_be_disabled_message = " and will be disabled on #{disable_on.strftime("%Y-%m-%d")}"
|
|
else
|
|
disable = true
|
|
end
|
|
end
|
|
|
|
verb = if disable
|
|
"disabled"
|
|
else
|
|
"deprecated#{will_be_disabled_message}"
|
|
end
|
|
|
|
# Try to show the most relevant location in message, i.e. (if applicable):
|
|
# - Location in a formula.
|
|
# - Location outside of 'compat/'.
|
|
# - Location of caller of deprecated method (if all else fails).
|
|
backtrace = caller
|
|
|
|
# Don't throw deprecations at all for cached, .brew or .metadata files.
|
|
return if backtrace.any? do |line|
|
|
next true if line.include?(HOMEBREW_CACHE.to_s)
|
|
next true if line.include?("/.brew/")
|
|
next true if line.include?("/.metadata/")
|
|
|
|
next false unless line.match?(HOMEBREW_TAP_PATH_REGEX)
|
|
|
|
path = Pathname(line.split(":", 2).first)
|
|
next false unless path.file?
|
|
next false unless path.readable?
|
|
|
|
formula_contents = path.read
|
|
formula_contents.include?(" deprecate! ") || formula_contents.include?(" disable! ")
|
|
end
|
|
|
|
tap_message = T.let(nil, T.nilable(String))
|
|
|
|
backtrace.each do |line|
|
|
next unless (match = line.match(HOMEBREW_TAP_PATH_REGEX))
|
|
|
|
tap = Tap.fetch(match[:user], match[:repo])
|
|
tap_message = +"\nPlease report this issue to the #{tap} tap (not Homebrew/brew or Homebrew/core)"
|
|
tap_message += ", or even better, submit a PR to fix it" if replacement
|
|
tap_message << ":\n #{line.sub(/^(.*:\d+):.*$/, '\1')}\n\n"
|
|
break
|
|
end
|
|
|
|
message = +"Calling #{method} is #{verb}! #{replacement_message}"
|
|
message << tap_message if tap_message
|
|
message.freeze
|
|
|
|
disable = true if disable_for_developers && Homebrew::EnvConfig.developer?
|
|
if disable || Homebrew.raise_deprecation_exceptions?
|
|
exception = MethodDeprecatedError.new(message)
|
|
exception.set_backtrace(backtrace)
|
|
raise exception
|
|
elsif !Homebrew.auditing?
|
|
opoo message
|
|
end
|
|
end
|
|
|
|
def odisabled(method, replacement = nil, options = {})
|
|
options = { disable: true, caller: caller }.merge(options)
|
|
odeprecated(method, replacement, options)
|
|
end
|
|
|
|
def pretty_installed(f)
|
|
if !$stdout.tty?
|
|
f.to_s
|
|
elsif Homebrew::EnvConfig.no_emoji?
|
|
Formatter.success("#{Tty.bold}#{f} (installed)#{Tty.reset}")
|
|
else
|
|
"#{Tty.bold}#{f} #{Formatter.success("✔")}#{Tty.reset}"
|
|
end
|
|
end
|
|
|
|
def pretty_outdated(f)
|
|
if !$stdout.tty?
|
|
f.to_s
|
|
elsif Homebrew::EnvConfig.no_emoji?
|
|
Formatter.error("#{Tty.bold}#{f} (outdated)#{Tty.reset}")
|
|
else
|
|
"#{Tty.bold}#{f} #{Formatter.warning("⚠")}#{Tty.reset}"
|
|
end
|
|
end
|
|
|
|
def pretty_uninstalled(f)
|
|
if !$stdout.tty?
|
|
f.to_s
|
|
elsif Homebrew::EnvConfig.no_emoji?
|
|
Formatter.error("#{Tty.bold}#{f} (uninstalled)#{Tty.reset}")
|
|
else
|
|
"#{Tty.bold}#{f} #{Formatter.error("✘")}#{Tty.reset}"
|
|
end
|
|
end
|
|
|
|
def pretty_duration(s)
|
|
s = s.to_i
|
|
res = +""
|
|
|
|
if s > 59
|
|
m = s / 60
|
|
s %= 60
|
|
res = +"#{m} #{"minute".pluralize(m)}"
|
|
return res.freeze if s.zero?
|
|
|
|
res << " "
|
|
end
|
|
|
|
res << "#{s} #{"second".pluralize(s)}"
|
|
res.freeze
|
|
end
|
|
|
|
def interactive_shell(f = nil)
|
|
unless f.nil?
|
|
ENV["HOMEBREW_DEBUG_PREFIX"] = f.prefix
|
|
ENV["HOMEBREW_DEBUG_INSTALL"] = f.full_name
|
|
end
|
|
|
|
if ENV["SHELL"].include?("zsh") && (home = ENV["HOME"])&.start_with?(HOMEBREW_TEMP.resolved_path.to_s)
|
|
FileUtils.mkdir_p home
|
|
FileUtils.touch "#{home}/.zshrc"
|
|
end
|
|
|
|
Process.wait fork { exec ENV.fetch("SHELL") }
|
|
|
|
return if $CHILD_STATUS.success?
|
|
raise "Aborted due to non-zero exit status (#{$CHILD_STATUS.exitstatus})" if $CHILD_STATUS.exited?
|
|
|
|
raise $CHILD_STATUS.inspect
|
|
end
|
|
|
|
def with_homebrew_path(&block)
|
|
with_env(PATH: PATH.new(ENV["HOMEBREW_PATH"]), &block)
|
|
end
|
|
|
|
def with_custom_locale(locale, &block)
|
|
with_env(LC_ALL: locale, &block)
|
|
end
|
|
|
|
# Kernel.system but with exceptions.
|
|
def safe_system(cmd, *args, **options)
|
|
return if Homebrew.system(cmd, *args, **options)
|
|
|
|
raise ErrorDuringExecution.new([cmd, *args], status: $CHILD_STATUS)
|
|
end
|
|
|
|
# Prints no output.
|
|
def quiet_system(cmd, *args)
|
|
Homebrew._system(cmd, *args) do
|
|
# Redirect output streams to `/dev/null` instead of closing as some programs
|
|
# will fail to execute if they can't write to an open stream.
|
|
$stdout.reopen("/dev/null")
|
|
$stderr.reopen("/dev/null")
|
|
end
|
|
end
|
|
|
|
def which(cmd, path = ENV["PATH"])
|
|
PATH.new(path).each do |p|
|
|
begin
|
|
pcmd = File.expand_path(cmd, p)
|
|
rescue ArgumentError
|
|
# File.expand_path will raise an ArgumentError if the path is malformed.
|
|
# See https://github.com/Homebrew/legacy-homebrew/issues/32789
|
|
next
|
|
end
|
|
return Pathname.new(pcmd) if File.file?(pcmd) && File.executable?(pcmd)
|
|
end
|
|
nil
|
|
end
|
|
|
|
def which_all(cmd, path = ENV["PATH"])
|
|
PATH.new(path).map do |p|
|
|
begin
|
|
pcmd = File.expand_path(cmd, p)
|
|
rescue ArgumentError
|
|
# File.expand_path will raise an ArgumentError if the path is malformed.
|
|
# See https://github.com/Homebrew/legacy-homebrew/issues/32789
|
|
next
|
|
end
|
|
Pathname.new(pcmd) if File.file?(pcmd) && File.executable?(pcmd)
|
|
end.compact.uniq
|
|
end
|
|
|
|
def which_editor
|
|
editor = Homebrew::EnvConfig.editor
|
|
return editor if editor
|
|
|
|
# Find Atom, Sublime Text, VS Code, Textmate, BBEdit / TextWrangler, or vim
|
|
editor = %w[atom subl code mate edit vim].find do |candidate|
|
|
candidate if which(candidate, ENV["HOMEBREW_PATH"])
|
|
end
|
|
editor ||= "vim"
|
|
|
|
opoo <<~EOS
|
|
Using #{editor} because no editor was set in the environment.
|
|
This may change in the future, so we recommend setting EDITOR,
|
|
or HOMEBREW_EDITOR to your preferred text editor.
|
|
EOS
|
|
|
|
editor
|
|
end
|
|
|
|
def exec_editor(*args)
|
|
puts "Editing #{args.join "\n"}"
|
|
with_homebrew_path { safe_system(*which_editor.shellsplit, *args) }
|
|
end
|
|
|
|
def exec_browser(*args)
|
|
browser = Homebrew::EnvConfig.browser
|
|
browser ||= OS::PATH_OPEN if defined?(OS::PATH_OPEN)
|
|
return unless browser
|
|
|
|
ENV["DISPLAY"] = Homebrew::EnvConfig.display
|
|
|
|
with_env(DBUS_SESSION_BUS_ADDRESS: ENV["HOMEBREW_DBUS_SESSION_BUS_ADDRESS"]) do
|
|
safe_system(browser, *args)
|
|
end
|
|
end
|
|
|
|
# GZips the given paths, and returns the gzipped paths.
|
|
def gzip(*paths)
|
|
paths.map do |path|
|
|
safe_system "gzip", path
|
|
Pathname.new("#{path}.gz")
|
|
end
|
|
end
|
|
|
|
def ignore_interrupts(_opt = nil)
|
|
# rubocop:disable Style/GlobalVars
|
|
$ignore_interrupts_nesting_level = 0 unless defined?($ignore_interrupts_nesting_level)
|
|
$ignore_interrupts_nesting_level += 1
|
|
|
|
$ignore_interrupts_interrupted = false unless defined?($ignore_interrupts_interrupted)
|
|
old_sigint_handler = trap(:INT) do
|
|
$ignore_interrupts_interrupted = true
|
|
$stderr.print "\n"
|
|
$stderr.puts "One sec, cleaning up..."
|
|
end
|
|
|
|
begin
|
|
yield
|
|
ensure
|
|
trap(:INT, old_sigint_handler)
|
|
|
|
$ignore_interrupts_nesting_level -= 1
|
|
if $ignore_interrupts_nesting_level == 0 && $ignore_interrupts_interrupted
|
|
$ignore_interrupts_interrupted = false
|
|
raise Interrupt
|
|
end
|
|
end
|
|
# rubocop:enable Style/GlobalVars
|
|
end
|
|
|
|
sig { returns(String) }
|
|
def capture_stderr
|
|
old = $stderr
|
|
$stderr = StringIO.new
|
|
yield
|
|
$stderr.string
|
|
ensure
|
|
$stderr = old
|
|
end
|
|
|
|
def nostdout(&block)
|
|
if verbose?
|
|
yield
|
|
else
|
|
redirect_stdout(File::NULL, &block)
|
|
end
|
|
end
|
|
|
|
def redirect_stdout(file)
|
|
out = $stdout.dup
|
|
$stdout.reopen(file)
|
|
yield
|
|
ensure
|
|
$stdout.reopen(out)
|
|
out.close
|
|
end
|
|
|
|
# Ensure the given formula is installed
|
|
# This is useful for installing a utility formula (e.g. `shellcheck` for `brew style`)
|
|
def ensure_formula_installed!(formula_or_name, reason: "", latest: false,
|
|
output_to_stderr: true, quiet: false)
|
|
if output_to_stderr || quiet
|
|
file = if quiet
|
|
File::NULL
|
|
else
|
|
$stderr
|
|
end
|
|
# Call this method itself with redirected stdout
|
|
redirect_stdout(file) do
|
|
return ensure_formula_installed!(formula_or_name, latest: latest,
|
|
reason: reason, output_to_stderr: false)
|
|
end
|
|
end
|
|
|
|
require "formula"
|
|
|
|
formula = if formula_or_name.is_a?(Formula)
|
|
formula_or_name
|
|
else
|
|
Formula[formula_or_name]
|
|
end
|
|
|
|
reason = " for #{reason}" if reason.present?
|
|
|
|
unless formula.any_version_installed?
|
|
ohai "Installing `#{formula.name}`#{reason}..."
|
|
safe_system HOMEBREW_BREW_FILE, "install", "--formula", formula.full_name
|
|
end
|
|
|
|
if latest && !formula.latest_version_installed?
|
|
ohai "Upgrading `#{formula.name}`#{reason}..."
|
|
safe_system HOMEBREW_BREW_FILE, "upgrade", "--formula", formula.full_name
|
|
end
|
|
|
|
formula
|
|
end
|
|
|
|
# Ensure the given executable is exist otherwise install the brewed version
|
|
def ensure_executable!(name, formula_name = nil, reason: "")
|
|
formula_name ||= name
|
|
|
|
executable = [
|
|
which(name),
|
|
which(name, ENV["HOMEBREW_PATH"]),
|
|
HOMEBREW_PREFIX/"bin/#{name}",
|
|
].compact.first
|
|
return executable if executable.exist?
|
|
|
|
ensure_formula_installed!(formula_name, reason: reason).opt_bin/name
|
|
end
|
|
|
|
def paths
|
|
@paths ||= PATH.new(ENV["HOMEBREW_PATH"]).map do |p|
|
|
File.expand_path(p).chomp("/")
|
|
rescue ArgumentError
|
|
onoe "The following PATH component is invalid: #{p}"
|
|
end.uniq.compact
|
|
end
|
|
|
|
def parse_author!(author)
|
|
/^(?<name>[^<]+?)[ \t]*<(?<email>[^>]+?)>$/ =~ author
|
|
raise UsageError, "Unable to parse name and email." if name.blank? && email.blank?
|
|
|
|
{ name: name, email: email }
|
|
end
|
|
|
|
def disk_usage_readable(size_in_bytes)
|
|
if size_in_bytes >= 1_073_741_824
|
|
size = size_in_bytes.to_f / 1_073_741_824
|
|
unit = "GB"
|
|
elsif size_in_bytes >= 1_048_576
|
|
size = size_in_bytes.to_f / 1_048_576
|
|
unit = "MB"
|
|
elsif size_in_bytes >= 1_024
|
|
size = size_in_bytes.to_f / 1_024
|
|
unit = "KB"
|
|
else
|
|
size = size_in_bytes
|
|
unit = "B"
|
|
end
|
|
|
|
# avoid trailing zero after decimal point
|
|
if ((size * 10).to_i % 10).zero?
|
|
"#{size.to_i}#{unit}"
|
|
else
|
|
"#{format("%<size>.1f", size: size)}#{unit}"
|
|
end
|
|
end
|
|
|
|
def number_readable(number)
|
|
numstr = number.to_i.to_s
|
|
(numstr.size - 3).step(1, -3) { |i| numstr.insert(i, ",") }
|
|
numstr
|
|
end
|
|
|
|
# Truncates a text string to fit within a byte size constraint,
|
|
# preserving character encoding validity. The returned string will
|
|
# be not much longer than the specified max_bytes, though the exact
|
|
# shortfall or overrun may vary.
|
|
def truncate_text_to_approximate_size(s, max_bytes, options = {})
|
|
front_weight = options.fetch(:front_weight, 0.5)
|
|
raise "opts[:front_weight] must be between 0.0 and 1.0" if front_weight < 0.0 || front_weight > 1.0
|
|
return s if s.bytesize <= max_bytes
|
|
|
|
glue = "\n[...snip...]\n"
|
|
max_bytes_in = [max_bytes - glue.bytesize, 1].max
|
|
bytes = s.dup.force_encoding("BINARY")
|
|
glue_bytes = glue.encode("BINARY")
|
|
n_front_bytes = (max_bytes_in * front_weight).floor
|
|
n_back_bytes = max_bytes_in - n_front_bytes
|
|
if n_front_bytes.zero?
|
|
front = bytes[1..0]
|
|
back = bytes[-max_bytes_in..]
|
|
elsif n_back_bytes.zero?
|
|
front = bytes[0..(max_bytes_in - 1)]
|
|
back = bytes[1..0]
|
|
else
|
|
front = bytes[0..(n_front_bytes - 1)]
|
|
back = bytes[-n_back_bytes..]
|
|
end
|
|
out = front + glue_bytes + back
|
|
out.force_encoding("UTF-8")
|
|
out.encode!("UTF-16", invalid: :replace)
|
|
out.encode!("UTF-8")
|
|
out
|
|
end
|
|
|
|
# Calls the given block with the passed environment variables
|
|
# added to ENV, then restores ENV afterwards.
|
|
# <pre>with_env(PATH: "/bin") do
|
|
# system "echo $PATH"
|
|
# end</pre>
|
|
#
|
|
# @note This method is *not* thread-safe - other threads
|
|
# which happen to be scheduled during the block will also
|
|
# see these environment variables.
|
|
# @api public
|
|
def with_env(hash)
|
|
old_values = {}
|
|
begin
|
|
hash.each do |key, value|
|
|
key = key.to_s
|
|
old_values[key] = ENV.delete(key)
|
|
ENV[key] = value
|
|
end
|
|
|
|
yield if block_given?
|
|
ensure
|
|
ENV.update(old_values)
|
|
end
|
|
end
|
|
|
|
sig { returns(String) }
|
|
def shell_profile
|
|
Utils::Shell.profile
|
|
end
|
|
|
|
def tap_and_name_comparison
|
|
proc do |a, b|
|
|
if a.include?("/") && b.exclude?("/")
|
|
1
|
|
elsif a.exclude?("/") && b.include?("/")
|
|
-1
|
|
else
|
|
a <=> b
|
|
end
|
|
end
|
|
end
|
|
|
|
def redact_secrets(input, secrets)
|
|
secrets.compact
|
|
.reduce(input) { |str, secret| str.gsub secret, "******" }
|
|
.freeze
|
|
end
|
|
end
|