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

Ever since we started using this at runtime it's been polluting the backtrace output. This makes it harder to debug errors and increases the amount of info users have to paste into the box when filing an issue. This is a very direct approach. Essentially, we strip out everything related to the `sorbet-runtime` gem whenever the top line in the backtrace is unrelated to sorbet-runtime. The hope is that this will allow errors related to sorbet to be diagnosed easily while also reducing the backtrace size for all other types of errors. Sometimes it is useful to see the full backtrace though. For those cases, we include the full backtrace when `--verbose` is passed in and print a warning that the Sorbet lines have been removed from the backtrace the first time they are removed. Note: This requires gems to be set up so that the call to `Gem.paths.home` works correctly. For that reason, it must be included after `utils/gems` which is included in `standalone/load_path` already.
473 lines
13 KiB
Ruby
473 lines
13 KiB
Ruby
# typed: true
|
|
# frozen_string_literal: true
|
|
|
|
require "lock_file"
|
|
require "keg"
|
|
require "tab"
|
|
|
|
# Helper class for migrating a formula from an old to a new name.
|
|
#
|
|
# @api private
|
|
class Migrator
|
|
include Context
|
|
|
|
# Error for when a migration is necessary.
|
|
class MigrationNeededError < RuntimeError
|
|
def initialize(oldname, newname)
|
|
super <<~EOS
|
|
#{oldname} was renamed to #{newname} and needs to be migrated by running:
|
|
brew migrate #{oldname}
|
|
EOS
|
|
end
|
|
end
|
|
|
|
# Error for when the old name's path does not exist.
|
|
class MigratorNoOldpathError < RuntimeError
|
|
def initialize(oldname)
|
|
super "#{HOMEBREW_CELLAR/oldname} doesn't exist."
|
|
end
|
|
end
|
|
|
|
# Error for when a formula is migrated to a different tap without explicitly using its fully-qualified name.
|
|
class MigratorDifferentTapsError < RuntimeError
|
|
def initialize(formula, oldname, tap)
|
|
msg = if tap.core_tap?
|
|
"Please try to use #{oldname} to refer to the formula.\n"
|
|
elsif tap
|
|
"Please try to use fully-qualified #{tap}/#{oldname} to refer to the formula.\n"
|
|
end
|
|
|
|
super <<~EOS
|
|
#{formula.name} from #{formula.tap} is given, but old name #{oldname} was installed from #{tap || "path or url"}.
|
|
#{msg}To force migration, run:
|
|
brew migrate --force #{oldname}
|
|
EOS
|
|
end
|
|
end
|
|
|
|
# Instance of renamed formula.
|
|
attr_reader :formula
|
|
|
|
# Old name of the formula.
|
|
attr_reader :oldname
|
|
|
|
# Path to oldname's cellar.
|
|
attr_reader :old_cellar
|
|
|
|
# Path to oldname pin.
|
|
attr_reader :old_pin_record
|
|
|
|
# Path to oldname opt.
|
|
attr_reader :old_opt_records
|
|
|
|
# Oldname linked kegs.
|
|
attr_reader :old_linked_kegs
|
|
|
|
# Oldname linked kegs that were fully linked.
|
|
attr_reader :old_full_linked_kegs
|
|
|
|
# Tabs from oldname kegs.
|
|
attr_reader :old_tabs
|
|
|
|
# Tap of the old name.
|
|
attr_reader :old_tap
|
|
|
|
# Resolved path to oldname pin.
|
|
attr_reader :old_pin_link_record
|
|
|
|
# New name of the formula.
|
|
attr_reader :newname
|
|
|
|
# Path to newname cellar according to new name.
|
|
attr_reader :new_cellar
|
|
|
|
# True if new cellar existed at initialization time.
|
|
attr_reader :new_cellar_existed
|
|
|
|
# Path to newname pin.
|
|
attr_reader :new_pin_record
|
|
|
|
# Path to newname keg that will be linked if old_linked_keg isn't nil.
|
|
attr_reader :new_linked_keg_record
|
|
|
|
def self.oldnames_needing_migration(formula)
|
|
formula.oldnames.select do |oldname|
|
|
oldname_rack = HOMEBREW_CELLAR/oldname
|
|
next false if oldname_rack.symlink?
|
|
next false unless oldname_rack.directory?
|
|
|
|
true
|
|
end
|
|
end
|
|
|
|
def self.needs_migration?(formula)
|
|
!oldnames_needing_migration(formula).empty?
|
|
end
|
|
|
|
def self.migrate_if_needed(formula, force:, dry_run: false)
|
|
oldnames = Migrator.oldnames_needing_migration(formula)
|
|
|
|
begin
|
|
oldnames.each do |oldname|
|
|
if dry_run
|
|
oh1 "Would migrate formula #{Formatter.identifier(oldname)} to #{Formatter.identifier(formula.name)}"
|
|
next
|
|
end
|
|
|
|
migrator = Migrator.new(formula, oldname, force: force)
|
|
migrator.migrate
|
|
end
|
|
rescue => e
|
|
onoe e
|
|
end
|
|
end
|
|
|
|
def initialize(formula, oldname, force: false)
|
|
@oldname = oldname
|
|
@newname = formula.name
|
|
|
|
@formula = formula
|
|
@old_cellar = HOMEBREW_CELLAR/oldname
|
|
raise MigratorNoOldpathError, oldname unless old_cellar.exist?
|
|
|
|
@old_tabs = old_cellar.subdirs.map { |d| Tab.for_keg(Keg.new(d)) }
|
|
@old_tap = old_tabs.first.tap
|
|
|
|
raise MigratorDifferentTapsError.new(formula, oldname, old_tap) if !force && !from_same_tap_user?
|
|
|
|
@new_cellar = HOMEBREW_CELLAR/formula.name
|
|
@new_cellar_existed = @new_cellar.exist?
|
|
|
|
@old_linked_kegs = linked_old_linked_kegs
|
|
@old_full_linked_kegs = []
|
|
@old_opt_records = []
|
|
old_linked_kegs.each do |old_linked_keg|
|
|
@old_full_linked_kegs << old_linked_keg if old_linked_keg.linked?
|
|
@old_opt_records << old_linked_keg.opt_record if old_linked_keg.optlinked?
|
|
end
|
|
unless old_linked_kegs.empty?
|
|
@new_linked_keg_record = HOMEBREW_CELLAR/"#{newname}/#{File.basename(old_linked_kegs.first)}"
|
|
end
|
|
|
|
@old_pin_record = HOMEBREW_PINNED_KEGS/oldname
|
|
@new_pin_record = HOMEBREW_PINNED_KEGS/newname
|
|
@pinned = old_pin_record.symlink?
|
|
@old_pin_link_record = old_pin_record.readlink if @pinned
|
|
end
|
|
|
|
# Fix `INSTALL_RECEIPT`s for tap-migrated formula.
|
|
def fix_tabs
|
|
old_tabs.each do |tab|
|
|
tab.tap = formula.tap
|
|
tab.write
|
|
end
|
|
end
|
|
|
|
sig { returns(T::Boolean) }
|
|
def from_same_tap_user?
|
|
formula_tap_user = formula.tap.user if formula.tap
|
|
old_tap_user = nil
|
|
|
|
new_tap = if old_tap
|
|
old_tap_user, = old_tap.user
|
|
if (migrate_tap = old_tap.tap_migrations[oldname])
|
|
new_tap_user, new_tap_repo = migrate_tap.split("/")
|
|
"#{new_tap_user}/#{new_tap_repo}"
|
|
end
|
|
end
|
|
|
|
if formula_tap_user == old_tap_user
|
|
true
|
|
# Homebrew didn't use to update tabs while performing tap-migrations,
|
|
# so there can be `INSTALL_RECEIPT`s containing wrong information about tap,
|
|
# so we check if there is an entry about oldname migrated to tap and if
|
|
# newname's tap is the same as tap to which oldname migrated, then we
|
|
# can perform migrations and the taps for oldname and newname are the same.
|
|
elsif formula.tap && old_tap && formula.tap == new_tap
|
|
fix_tabs
|
|
true
|
|
else
|
|
false
|
|
end
|
|
end
|
|
|
|
def linked_old_linked_kegs
|
|
keg_dirs = []
|
|
keg_dirs += new_cellar.subdirs if new_cellar.exist?
|
|
keg_dirs += old_cellar.subdirs
|
|
kegs = keg_dirs.map { |d| Keg.new(d) }
|
|
kegs.select { |keg| keg.linked? || keg.optlinked? }
|
|
end
|
|
|
|
def pinned?
|
|
@pinned
|
|
end
|
|
|
|
def migrate
|
|
oh1 "Migrating formula #{Formatter.identifier(oldname)} to #{Formatter.identifier(newname)}"
|
|
lock
|
|
unlink_oldname
|
|
unlink_newname if new_cellar.exist?
|
|
repin
|
|
move_to_new_directory
|
|
link_oldname_cellar
|
|
link_oldname_opt
|
|
link_newname unless old_linked_kegs.empty?
|
|
update_tabs
|
|
return unless formula.outdated?
|
|
|
|
opoo <<~EOS
|
|
#{Formatter.identifier(newname)} is outdated!
|
|
To avoid broken installations, as soon as possible please run:
|
|
brew upgrade
|
|
Or, if you're OK with a less reliable fix:
|
|
brew upgrade #{newname}
|
|
EOS
|
|
rescue Interrupt
|
|
ignore_interrupts { backup_oldname }
|
|
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
onoe "The migration did not complete successfully."
|
|
puts e
|
|
puts Utils::Backtrace.clean(e) if debug?
|
|
puts "Backing up..."
|
|
ignore_interrupts { backup_oldname }
|
|
ensure
|
|
unlock
|
|
end
|
|
|
|
def remove_conflicts(directory)
|
|
conflicted = T.let(false, T::Boolean)
|
|
|
|
directory.each_child do |c|
|
|
if c.directory? && !c.symlink?
|
|
conflicted ||= remove_conflicts(c)
|
|
else
|
|
next unless (new_cellar/c.relative_path_from(old_cellar)).exist?
|
|
|
|
begin
|
|
FileUtils.rm_rf c
|
|
rescue Errno::EACCES
|
|
conflicted = true
|
|
onoe "#{new_cellar/c.basename} already exists."
|
|
end
|
|
end
|
|
end
|
|
|
|
conflicted
|
|
end
|
|
|
|
def merge_directory(directory)
|
|
directory.each_child do |c|
|
|
new_path = new_cellar/c.relative_path_from(old_cellar)
|
|
|
|
if c.directory? && !c.symlink? && new_path.exist?
|
|
merge_directory(c)
|
|
c.unlink
|
|
else
|
|
FileUtils.mv(c, new_path)
|
|
end
|
|
end
|
|
end
|
|
|
|
# Move everything from `Cellar/oldname` to `Cellar/newname`.
|
|
def move_to_new_directory
|
|
return unless old_cellar.exist?
|
|
|
|
if new_cellar.exist?
|
|
conflicted = remove_conflicts(old_cellar)
|
|
odie "Remove #{new_cellar} and #{old_cellar} manually and run `brew reinstall #{newname}`." if conflicted
|
|
end
|
|
|
|
oh1 "Moving #{Formatter.identifier(oldname)} versions to #{new_cellar}"
|
|
if new_cellar.exist?
|
|
merge_directory(old_cellar)
|
|
else
|
|
FileUtils.mv(old_cellar, new_cellar)
|
|
end
|
|
end
|
|
|
|
def repin
|
|
return unless pinned?
|
|
|
|
# old_pin_record is a relative symlink and when we try to to read it
|
|
# from <dir> we actually try to find file
|
|
# <dir>/../<...>/../Cellar/name/version.
|
|
# To repin formula we need to update the link thus that it points to
|
|
# the right directory.
|
|
# NOTE: old_pin_record.realpath.sub(oldname, newname) is unacceptable
|
|
# here, because it resolves every symlink for old_pin_record and then
|
|
# substitutes oldname with newname. It breaks things like
|
|
# Pathname#make_relative_symlink, where Pathname#relative_path_from
|
|
# is used to find relative path from source to destination parent and
|
|
# it assumes no symlinks.
|
|
src_oldname = (old_pin_record.dirname/old_pin_link_record).expand_path
|
|
new_pin_record.make_relative_symlink(src_oldname.sub(oldname, newname))
|
|
old_pin_record.delete
|
|
end
|
|
|
|
def unlink_oldname
|
|
oh1 "Unlinking #{Formatter.identifier(oldname)}"
|
|
old_cellar.subdirs.each do |d|
|
|
keg = Keg.new(d)
|
|
keg.unlink(verbose: verbose?)
|
|
end
|
|
end
|
|
|
|
def unlink_newname
|
|
oh1 "Temporarily unlinking #{Formatter.identifier(newname)}"
|
|
new_cellar.subdirs.each do |d|
|
|
keg = Keg.new(d)
|
|
keg.unlink(verbose: verbose?)
|
|
end
|
|
end
|
|
|
|
def link_newname
|
|
oh1 "Relinking #{Formatter.identifier(newname)}"
|
|
new_keg = Keg.new(new_linked_keg_record)
|
|
|
|
# If old_keg wasn't linked then we just optlink a keg.
|
|
# If old keg wasn't optlinked and linked, we don't call this method at all.
|
|
# If formula is keg-only we also optlink it.
|
|
if formula.keg_only? || old_full_linked_kegs.empty?
|
|
begin
|
|
new_keg.optlink(verbose: verbose?)
|
|
rescue Keg::LinkError => e
|
|
onoe "Failed to create #{formula.opt_prefix}"
|
|
raise
|
|
end
|
|
return
|
|
end
|
|
|
|
new_keg.remove_linked_keg_record if new_keg.linked?
|
|
|
|
begin
|
|
new_keg.link(overwrite: true, verbose: verbose?)
|
|
rescue Keg::ConflictError => e
|
|
onoe "The `brew link` step did not complete successfully."
|
|
puts e
|
|
puts
|
|
puts "Possible conflicting files are:"
|
|
new_keg.link(dry_run: true, overwrite: true, verbose: verbose?)
|
|
raise
|
|
rescue Keg::LinkError => e
|
|
onoe "The `brew link` step did not complete successfully."
|
|
puts e
|
|
puts
|
|
puts "You can try again using:"
|
|
puts " brew link #{formula.name}"
|
|
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
onoe "An unexpected error occurred during linking"
|
|
puts e
|
|
puts Utils::Backtrace.clean(e) if debug?
|
|
ignore_interrupts { new_keg.unlink(verbose: verbose?) }
|
|
raise
|
|
end
|
|
end
|
|
|
|
# Link keg to opt if it was linked before migrating.
|
|
def link_oldname_opt
|
|
old_opt_records.each do |old_opt_record|
|
|
old_opt_record.delete if old_opt_record.symlink?
|
|
old_opt_record.make_relative_symlink(new_linked_keg_record)
|
|
end
|
|
end
|
|
|
|
# After migration every `INSTALL_RECEIPT.json` has the wrong path to the formula
|
|
# so we must update `INSTALL_RECEIPT`s.
|
|
def update_tabs
|
|
new_tabs = new_cellar.subdirs.map { |d| Tab.for_keg(Keg.new(d)) }
|
|
new_tabs.each do |tab|
|
|
tab.source["path"] = formula.path.to_s if tab.source["path"]
|
|
tab.write
|
|
end
|
|
end
|
|
|
|
# Remove `opt/oldname` link if it belongs to newname.
|
|
def unlink_oldname_opt
|
|
return unless new_linked_keg_record.exist?
|
|
|
|
old_opt_records.each do |old_opt_record|
|
|
next unless old_opt_record.symlink?
|
|
next unless old_opt_record.exist?
|
|
next if new_linked_keg_record.realpath != old_opt_record.realpath
|
|
|
|
old_opt_record.unlink
|
|
old_opt_record.parent.rmdir_if_possible
|
|
end
|
|
end
|
|
|
|
# Remove `Cellar/oldname` if it exists.
|
|
def link_oldname_cellar
|
|
old_cellar.delete if old_cellar.symlink? || old_cellar.exist?
|
|
old_cellar.make_relative_symlink(formula.rack)
|
|
end
|
|
|
|
# Remove `Cellar/oldname` link if it belongs to newname.
|
|
def unlink_oldname_cellar
|
|
if (old_cellar.symlink? && !old_cellar.exist?) || (old_cellar.symlink? \
|
|
&& formula.rack.exist? && formula.rack.realpath == old_cellar.realpath)
|
|
old_cellar.unlink
|
|
end
|
|
end
|
|
|
|
# Backup everything if errors occur while migrating.
|
|
def backup_oldname
|
|
unlink_oldname_opt
|
|
unlink_oldname_cellar
|
|
backup_oldname_cellar
|
|
backup_old_tabs
|
|
|
|
if pinned? && !old_pin_record.symlink?
|
|
src_oldname = (old_pin_record.dirname/old_pin_link_record).expand_path
|
|
old_pin_record.make_relative_symlink(src_oldname)
|
|
new_pin_record.delete
|
|
end
|
|
|
|
if new_cellar.exist?
|
|
new_cellar.subdirs.each do |d|
|
|
newname_keg = Keg.new(d)
|
|
newname_keg.unlink(verbose: verbose?)
|
|
newname_keg.uninstall unless new_cellar_existed
|
|
end
|
|
end
|
|
|
|
return if old_linked_kegs.empty?
|
|
|
|
# The keg used to be linked and when we backup everything we restore
|
|
# Cellar/oldname, the target also gets restored, so we are able to
|
|
# create a keg using its old path
|
|
old_full_linked_kegs.each do |old_linked_keg|
|
|
old_linked_keg.link(verbose: verbose?)
|
|
rescue Keg::LinkError
|
|
old_linked_keg.unlink(verbose: verbose?)
|
|
raise
|
|
rescue Keg::AlreadyLinkedError
|
|
old_linked_keg.unlink(verbose: verbose?)
|
|
retry
|
|
end
|
|
(old_linked_kegs - old_full_linked_kegs).each do |old_linked_keg|
|
|
old_linked_keg.optlink(verbose: verbose?)
|
|
end
|
|
end
|
|
|
|
def backup_oldname_cellar
|
|
FileUtils.mv(new_cellar, old_cellar) unless old_cellar.exist?
|
|
end
|
|
|
|
def backup_old_tabs
|
|
old_tabs.each(&:write)
|
|
end
|
|
|
|
def lock
|
|
@newname_lock = FormulaLock.new newname
|
|
@oldname_lock = FormulaLock.new oldname
|
|
@newname_lock.lock
|
|
@oldname_lock.lock
|
|
end
|
|
|
|
def unlock
|
|
@newname_lock.unlock
|
|
@oldname_lock.unlock
|
|
end
|
|
end
|