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

For many people `brew edit` makes use of the `EDITOR` variable to pick a sensible editor. With environment filtering enabled unless this editor is found in the default system PATH it'll fall back to e.g. `vim`. Instead, ensure that we export the original, pre-filtering `PATH` as `HOMEBREW_PATH` and use that internally to locate the editor. In future this same approach will likely be used for requirements to be able to find tools, too, and for other variables which we want to expose to Homebrew itself but not other build tools. Note that `HOMEBREW_PATH` is the same as `PATH` when build filtering hasn't been enabled.
520 lines
13 KiB
Ruby
520 lines
13 KiB
Ruby
require "pathname"
|
|
require "emoji"
|
|
require "exceptions"
|
|
require "utils/analytics"
|
|
require "utils/curl"
|
|
require "utils/fork"
|
|
require "utils/formatter"
|
|
require "utils/git"
|
|
require "utils/github"
|
|
require "utils/hash"
|
|
require "utils/inreplace"
|
|
require "utils/link"
|
|
require "utils/popen"
|
|
require "utils/svn"
|
|
require "utils/tty"
|
|
require "time"
|
|
|
|
def ohai(title, *sput)
|
|
title = Tty.truncate(title) if $stdout.tty? && !ARGV.verbose?
|
|
puts Formatter.headline(title, color: :blue)
|
|
puts sput
|
|
end
|
|
|
|
def oh1(title, options = {})
|
|
if $stdout.tty? && !ARGV.verbose? && options.fetch(:truncate, :auto) == :auto
|
|
title = Tty.truncate(title)
|
|
end
|
|
puts Formatter.headline(title, color: :green)
|
|
end
|
|
|
|
# Print a warning (do this rarely)
|
|
def opoo(message)
|
|
$stderr.puts Formatter.warning(message, label: "Warning")
|
|
end
|
|
|
|
def onoe(message)
|
|
$stderr.puts Formatter.error(message, label: "Error")
|
|
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, 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
|
|
tap_message = nil
|
|
caller_message = backtrace.detect do |line|
|
|
next unless line =~ %r{^#{Regexp.escape HOMEBREW_LIBRARY}/Taps/([^/]+/[^/]+)/}
|
|
tap = Tap.fetch $1
|
|
tap_message = "\nPlease report this to the #{tap} tap!"
|
|
true
|
|
end
|
|
caller_message ||= backtrace.detect do |line|
|
|
!line.start_with?("#{HOMEBREW_LIBRARY_PATH}/compat/")
|
|
end
|
|
caller_message ||= backtrace[1]
|
|
|
|
message = <<-EOS.undent
|
|
Calling #{method} is #{verb}!
|
|
#{replacement_message}
|
|
#{caller_message}#{tap_message}
|
|
EOS
|
|
|
|
if ARGV.homebrew_developer? || disable ||
|
|
Homebrew.raise_deprecation_exceptions?
|
|
raise MethodDeprecatedError, message
|
|
else
|
|
opoo "#{message}\n"
|
|
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 Emoji.enabled?
|
|
"#{Tty.bold}#{f} #{Formatter.success("✔")}#{Tty.reset}"
|
|
else
|
|
Formatter.success("#{Tty.bold}#{f} (installed)#{Tty.reset}")
|
|
end
|
|
end
|
|
|
|
def pretty_uninstalled(f)
|
|
if !$stdout.tty?
|
|
f.to_s
|
|
elsif Emoji.enabled?
|
|
"#{Tty.bold}#{f} #{Formatter.error("✘")}#{Tty.reset}"
|
|
else
|
|
Formatter.error("#{Tty.bold}#{f} (uninstalled)#{Tty.reset}")
|
|
end
|
|
end
|
|
|
|
def pretty_duration(s)
|
|
s = s.to_i
|
|
res = ""
|
|
|
|
if s > 59
|
|
m = s / 60
|
|
s %= 60
|
|
res = Formatter.pluralize(m, "minute")
|
|
return res if s.zero?
|
|
res << " "
|
|
end
|
|
|
|
res << Formatter.pluralize(s, "second")
|
|
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") && ENV["HOME"].start_with?(HOMEBREW_TEMP.resolved_path.to_s)
|
|
FileUtils.touch "#{ENV["HOME"]}/.zshrc"
|
|
end
|
|
|
|
Process.wait fork { exec ENV["SHELL"] }
|
|
|
|
return if $?.success?
|
|
raise "Aborted due to non-zero exit status (#{$?.exitstatus})" if $?.exited?
|
|
raise $?.inspect
|
|
end
|
|
|
|
module Homebrew
|
|
module_function
|
|
|
|
def _system(cmd, *args)
|
|
pid = fork do
|
|
yield if block_given?
|
|
args.collect!(&:to_s)
|
|
begin
|
|
exec(cmd, *args)
|
|
rescue
|
|
nil
|
|
end
|
|
exit! 1 # never gets here unless exec failed
|
|
end
|
|
Process.wait(pid)
|
|
$?.success?
|
|
end
|
|
|
|
def system(cmd, *args)
|
|
puts "#{cmd} #{args*" "}" if ARGV.verbose?
|
|
_system(cmd, *args)
|
|
end
|
|
|
|
def install_gem_setup_path!(name, version = nil, executable = name)
|
|
# Respect user's preferences for where gems should be installed.
|
|
ENV["GEM_HOME"] = ENV["GEM_OLD_HOME"].to_s
|
|
ENV["GEM_HOME"] = Gem.user_dir if ENV["GEM_HOME"].empty?
|
|
ENV["GEM_PATH"] = ENV["GEM_OLD_PATH"] unless ENV["GEM_OLD_PATH"].to_s.empty?
|
|
|
|
# Make rubygems notice env changes.
|
|
Gem.clear_paths
|
|
Gem::Specification.reset
|
|
|
|
# Add Gem binary directory and (if missing) Ruby binary directory to PATH.
|
|
path = ENV["PATH"].split(File::PATH_SEPARATOR)
|
|
path.unshift(RUBY_BIN) if which("ruby") != RUBY_PATH
|
|
path.unshift(Gem.bindir)
|
|
ENV["PATH"] = path.join(File::PATH_SEPARATOR)
|
|
|
|
if Gem::Specification.find_all_by_name(name, version).empty?
|
|
ohai "Installing or updating '#{name}' gem"
|
|
install_args = %W[--no-ri --no-rdoc #{name}]
|
|
install_args << "--version" << version if version
|
|
|
|
# Do `gem install [...]` without having to spawn a separate process or
|
|
# having to find the right `gem` binary for the running Ruby interpreter.
|
|
require "rubygems/commands/install_command"
|
|
install_cmd = Gem::Commands::InstallCommand.new
|
|
install_cmd.handle_options(install_args)
|
|
exit_code = 1 # Should not matter as `install_cmd.execute` always throws.
|
|
begin
|
|
install_cmd.execute
|
|
rescue Gem::SystemExitException => e
|
|
exit_code = e.exit_code
|
|
end
|
|
odie "Failed to install/update the '#{name}' gem." if exit_code.nonzero?
|
|
end
|
|
|
|
return if which(executable)
|
|
odie <<-EOS.undent
|
|
The '#{name}' gem is installed but couldn't find '#{executable}' in the PATH:
|
|
#{ENV["PATH"]}
|
|
EOS
|
|
end
|
|
|
|
# Hash of Module => Set(method_names)
|
|
@injected_dump_stat_modules = {}
|
|
|
|
def inject_dump_stats!(the_module, pattern)
|
|
@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|
|
|
begin
|
|
time = Time.now
|
|
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 + 2, 15].max
|
|
$times.sort_by { |_k, v| v }.each do |method, time|
|
|
puts format("%-*s %0.4f sec", col_width, "#{method}:", time)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def with_system_path
|
|
old_path = ENV["PATH"]
|
|
ENV["PATH"] = "/usr/bin:/bin"
|
|
yield
|
|
ensure
|
|
ENV["PATH"] = old_path
|
|
end
|
|
|
|
def with_custom_locale(locale)
|
|
old_locale = ENV["LC_ALL"]
|
|
ENV["LC_ALL"] = locale
|
|
yield
|
|
ensure
|
|
ENV["LC_ALL"] = old_locale
|
|
end
|
|
|
|
def run_as_not_developer(&_block)
|
|
old = ENV.delete "HOMEBREW_DEVELOPER"
|
|
yield
|
|
ensure
|
|
ENV["HOMEBREW_DEVELOPER"] = old
|
|
end
|
|
|
|
# Kernel.system but with exceptions
|
|
def safe_system(cmd, *args)
|
|
Homebrew.system(cmd, *args) || raise(ErrorDuringExecution.new(cmd, args))
|
|
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.split(File::PATH_SEPARATOR).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.to_s.split(File::PATH_SEPARATOR).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 = ENV.values_at("HOMEBREW_EDITOR", "VISUAL").compact.first
|
|
return which(editor, ENV["HOMEBREW_PATH"]) unless editor.nil?
|
|
|
|
# Find Textmate
|
|
editor = "mate" if which "mate"
|
|
# Find BBEdit / TextWrangler
|
|
editor ||= "edit" if which "edit"
|
|
# Find vim
|
|
editor ||= "vim" if which "vim"
|
|
# Default to standard vim
|
|
editor ||= "/usr/bin/vim"
|
|
|
|
opoo <<-EOS.undent
|
|
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"}"
|
|
safe_exec(which_editor, *args)
|
|
end
|
|
|
|
def exec_browser(*args)
|
|
browser = ENV["HOMEBREW_BROWSER"] || ENV["BROWSER"]
|
|
browser ||= OS::PATH_OPEN if defined?(OS::PATH_OPEN)
|
|
return unless browser
|
|
safe_exec(browser, *args)
|
|
end
|
|
|
|
def safe_exec(cmd, *args)
|
|
# This buys us proper argument quoting and evaluation
|
|
# of environment variables in the cmd parameter.
|
|
exec "/bin/sh", "-c", "#{cmd} \"$@\"", "--", *args
|
|
end
|
|
|
|
# GZips the given paths, and returns the gzipped paths
|
|
def gzip(*paths)
|
|
paths.collect do |path|
|
|
with_system_path { safe_system "gzip", path }
|
|
Pathname.new("#{path}.gz")
|
|
end
|
|
end
|
|
|
|
# Returns array of architectures that the given command or library is built for.
|
|
def archs_for_command(cmd)
|
|
cmd = which(cmd) unless Pathname.new(cmd).absolute?
|
|
Pathname.new(cmd).archs
|
|
end
|
|
|
|
def ignore_interrupts(opt = nil)
|
|
std_trap = trap("INT") do
|
|
puts "One sec, just cleaning up" unless opt == :quietly
|
|
end
|
|
yield
|
|
ensure
|
|
trap("INT", std_trap)
|
|
end
|
|
|
|
def capture_stderr
|
|
old = $stderr
|
|
$stderr = StringIO.new
|
|
yield
|
|
$stderr.string
|
|
ensure
|
|
$stderr = old
|
|
end
|
|
|
|
def nostdout
|
|
if ARGV.verbose?
|
|
yield
|
|
else
|
|
begin
|
|
out = $stdout.dup
|
|
$stdout.reopen("/dev/null")
|
|
yield
|
|
ensure
|
|
$stdout.reopen(out)
|
|
out.close
|
|
end
|
|
end
|
|
end
|
|
|
|
def paths
|
|
@paths ||= ENV["PATH"].split(File::PATH_SEPARATOR).collect do |p|
|
|
begin
|
|
File.expand_path(p).chomp("/")
|
|
rescue ArgumentError
|
|
onoe "The following PATH component is invalid: #{p}"
|
|
end
|
|
end.uniq.compact
|
|
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("%.1f", 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)
|
|
if front_weight < 0.0 || front_weight > 1.0
|
|
raise "opts[:front_weight] must be between 0.0 and 1.0"
|
|
end
|
|
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..-1]
|
|
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..-1]
|
|
end
|
|
out = front + glue_bytes + back
|
|
out.force_encoding("UTF-8")
|
|
out.encode!("UTF-16", invalid: :replace)
|
|
out.encode!("UTF-8")
|
|
out
|
|
end
|
|
|
|
def migrate_legacy_keg_symlinks_if_necessary
|
|
legacy_linked_kegs = HOMEBREW_LIBRARY/"LinkedKegs"
|
|
return unless legacy_linked_kegs.directory?
|
|
|
|
HOMEBREW_LINKED_KEGS.mkpath unless legacy_linked_kegs.children.empty?
|
|
legacy_linked_kegs.children.each do |link|
|
|
name = link.basename.to_s
|
|
src = begin
|
|
link.realpath
|
|
rescue Errno::ENOENT
|
|
begin
|
|
(HOMEBREW_PREFIX/"opt/#{name}").realpath
|
|
rescue Errno::ENOENT
|
|
begin
|
|
Formulary.factory(name).installed_prefix
|
|
rescue
|
|
next
|
|
end
|
|
end
|
|
end
|
|
dst = HOMEBREW_LINKED_KEGS/name
|
|
dst.unlink if dst.exist?
|
|
FileUtils.ln_sf(src.relative_path_from(dst.parent), dst)
|
|
end
|
|
FileUtils.rm_rf legacy_linked_kegs
|
|
|
|
legacy_pinned_kegs = HOMEBREW_LIBRARY/"PinnedKegs"
|
|
return unless legacy_pinned_kegs.directory?
|
|
|
|
HOMEBREW_PINNED_KEGS.mkpath unless legacy_pinned_kegs.children.empty?
|
|
legacy_pinned_kegs.children.each do |link|
|
|
name = link.basename.to_s
|
|
src = link.realpath
|
|
dst = HOMEBREW_PINNED_KEGS/name
|
|
FileUtils.ln_sf(src.relative_path_from(dst.parent), dst)
|
|
end
|
|
FileUtils.rm_rf legacy_pinned_kegs
|
|
end
|