# typed: false # 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 extend T::Sig include Context # Error for when a migration is necessary. class MigrationNeededError < RuntimeError def initialize(formula) super <<~EOS #{formula.oldname} was renamed to #{formula.name} and needs to be migrated. Please run `brew migrate #{formula.oldname}` EOS end end # Error for when a formula does not replace another formula. class MigratorNoOldnameError < RuntimeError def initialize(formula) super "#{formula.name} doesn't replace any formula." end end # Error for when the old name's path does not exist. class MigratorNoOldpathError < RuntimeError def initialize(formula) super "#{HOMEBREW_CELLAR/formula.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, tap) msg = if tap.core_tap? "Please try to use #{formula.oldname} to refer to the formula.\n" elsif tap "Please try to use fully-qualified #{tap}/#{formula.oldname} to refer to the formula.\n" end super <<~EOS #{formula.name} from #{formula.tap} is given, but old name #{formula.oldname} was installed from #{tap || "path or url"}. #{msg}To force migration use `brew migrate --force #{formula.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_record # Oldname linked keg. attr_reader :old_linked_keg # Path to oldname's linked keg. attr_reader :old_linked_keg_record # 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.needs_migration?(formula) oldname = formula.oldname return false unless oldname oldname_rack = HOMEBREW_CELLAR/oldname return false if oldname_rack.symlink? return false unless oldname_rack.directory? true end def self.migrate_if_needed(formula, force:) return unless Migrator.needs_migration?(formula) begin migrator = Migrator.new(formula, force: force) migrator.migrate rescue => e onoe e end end def initialize(formula, force: false) @oldname = formula.oldname @newname = formula.name raise MigratorNoOldnameError, formula unless oldname @formula = formula @old_cellar = HOMEBREW_CELLAR/formula.oldname raise MigratorNoOldpathError, formula 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, old_tap) if !force && !from_same_tap_user? @new_cellar = HOMEBREW_CELLAR/formula.name @new_cellar_existed = @new_cellar.exist? if @old_linked_keg = linked_old_linked_keg @old_linked_keg_record = old_linked_keg.linked_keg_record if old_linked_keg.linked? @old_opt_record = old_linked_keg.opt_record if old_linked_keg.optlinked? @new_linked_keg_record = HOMEBREW_CELLAR/"#{newname}/#{File.basename(old_linked_keg)}" 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[formula.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_keg 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.find(&:linked?) || kegs.find(&:optlinked?) end def pinned? @pinned end def migrate oh1 "Processing #{Formatter.identifier(oldname)} formula rename 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_keg.nil? 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 "Error occurred while migrating." puts e puts e.backtrace if debug? puts "Backing up..." ignore_interrupts { backup_oldname } ensure unlock end # Move everything from `Cellar/oldname` to `Cellar/newname`. def move_to_new_directory return unless old_cellar.exist? if new_cellar.exist? conflicted = T.let(false, T::Boolean) old_cellar.each_child do |c| next unless (new_cellar/c.basename).exist? begin FileUtils.rm_rf c rescue Errno::EACCES conflicted = true onoe "#{new_cellar/c.basename} already exists." end end odie "Remove #{new_cellar} manually and run `brew migrate #{oldname}`." if conflicted end oh1 "Moving #{Formatter.identifier(oldname)} versions to #{new_cellar}" if new_cellar.exist? FileUtils.mv(old_cellar.children, new_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