diff --git a/Library/Homebrew/migrator.rb b/Library/Homebrew/migrator.rb new file mode 100644 index 0000000000000000000000000000000000000000..5bd3c6c57aa5800789bd1dbdab5cfbd056e11402 --- /dev/null +++ b/Library/Homebrew/migrator.rb @@ -0,0 +1,303 @@ +require "formula" +require "keg" +require "tab" +require "tap_migrations" + +class Migrator + class MigratorNoOldnameError < RuntimeError + def initialize(formula) + super "#{formula.name} doesn't replace any formula." + end + end + + class MigratorNoOldpathError < RuntimeError + def initialize(formula) + super "#{HOMEBREW_CELLAR/formula.oldname} doesn't exist." + end + end + + class MigratorDifferentTapsError < RuntimeError + def initialize(formula, tap) + if tap.nil? + super <<-EOS.undent + #{formula.name} from #{formula.tap} is given, but old name #{formula.oldname} wasn't installed from taps or core formulae + + You can try `brew migrate --force #{formula.oldname}`. + EOS + else + user, repo = tap.split("/") + repo.sub!("homebrew-", "") + name = "fully-qualified #{user}/#{repo}/#{formula.oldname}" + name = formula.oldname if tap == "Homebrew/homebrew" + super <<-EOS.undent + #{formula.name} from #{formula.tap} is given, but old name #{formula.oldname} was installed from #{tap} + + Please try to use #{name} to refer the formula + EOS + end + end + end + + attr_reader :formula + attr_reader :oldname, :oldpath, :old_pin_record, :old_opt_record + attr_reader :old_linked_keg_record, :oldkeg, :old_tabs, :old_tap + attr_reader :newname, :newpath, :new_pin_record + attr_reader :old_pin_link_record + + def initialize(formula) + @oldname = formula.oldname + @newname = formula.name + raise MigratorNoOldnameError.new(formula) unless oldname + + @formula = formula + @oldpath = HOMEBREW_CELLAR/formula.oldname + raise MigratorNoOldpathError.new(formula) unless oldpath.exist? + + @old_tabs = oldpath.subdirs.each.map { |d| Tab.for_keg(Keg.new(d)) } + @old_tap = old_tabs.first.tap + raise MigratorDifferentTapsError.new(formula, old_tap) unless from_same_taps? + + @newpath = HOMEBREW_CELLAR/formula.name + + if @oldkeg = get_linked_oldkeg + @old_linked_keg_record = oldkeg.linked_keg_record if oldkeg.linked? + @old_opt_record = oldkeg.opt_record if oldkeg.optlinked? + end + + @old_pin_record = HOMEBREW_LIBRARY/"PinnedKegs"/oldname + @new_pin_record = HOMEBREW_LIBRARY/"PinnedKegs"/newname + @pinned = old_pin_record.symlink? + @old_pin_link_record = old_pin_record.readlink if @pinned + end + + # Fix INSTALL_RECEIPTS for tap-migrated formula. + def fix_tabs + old_tabs.each do |tab| + tab.source["tap"] = formula.tap + tab.write + end + end + + def from_same_taps? + if old_tap == nil && formula.core_formula? && ARGV.force? + true + elsif formula.tap == old_tap + true + # Homebrew didn't use to update tabs while performing tap-migrations, + # so there can be INSTALL_RECEIPT's containing wrong information about + # tap (tap is Homebrew/homebrew if installed formula migrates to a 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 TAP_MIGRATIONS && (rec = TAP_MIGRATIONS[formula.oldname]) \ + && rec == formula.tap.sub("homebrew-", "") + fix_tabs + true + elsif formula.tap + false + end + end + + def get_linked_oldkeg + kegs = oldpath.subdirs.map { |d| Keg.new(d) } + kegs.detect(&:linked?) || kegs.detect(&:optlinked?) + end + + def pinned? + @pinned + end + + def oldkeg_linked? + !!oldkeg + end + + def migrate + if newpath.exist? + onoe "#{newpath} already exists; remove it manually and run brew migrate #{oldname}." + return + end + + begin + oh1 "Migrating #{Tty.green}#{oldname}#{Tty.white} to #{Tty.green}#{newname}#{Tty.reset}" + unlink_oldname + move_to_new_directory + repin + link_newname + link_oldname_opt + link_oldname_cellar + update_tabs + rescue Interrupt + ignore_interrupts { backup_oldname } + rescue Exception => e + onoe "error occured while migrating." + puts e if ARGV.debug? + puts "Backuping..." + ignore_interrupts { backup_oldname } + end + end + + # move everything from Cellar/oldname to Cellar/newname + def move_to_new_directory + puts "Moving to: #{newpath}" + FileUtils.mv(oldpath, newpath) + end + + def repin + if 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.join(old_pin_link_record).expand_path + new_pin_record.make_relative_symlink(src_oldname.sub(oldname, newname)) + old_pin_record.delete + end + end + + def unlink_oldname + oh1 "Unlinking #{Tty.green}#{oldname}#{Tty.reset}" + oldpath.subdirs.each do |d| + keg = Keg.new(d) + keg.unlink + end + end + + def link_newname + oh1 "Linking #{Tty.green}#{newname}#{Tty.reset}" + keg = Keg.new(formula.installed_prefix) + + if formula.keg_only? + begin + keg.optlink + rescue Keg::LinkError => e + onoe "Failed to create #{formula.opt_prefix}" + puts e + raise + end + return + end + + keg.remove_linked_keg_record if keg.linked? + + begin + keg.link + rescue Keg::ConflictError => e + onoe "Error while executing `brew link` step on #{newname}" + puts e + puts + puts "Possible conflicting files are:" + mode = OpenStruct.new(:dry_run => true, :overwrite => true) + keg.link(mode) + raise + rescue Keg::LinkError => e + onoe "Error while linking" + puts e + puts + puts "You can try again using:" + puts " brew link #{formula.name}" + rescue Exception => e + onoe "An unexpected error occurred during linking" + puts e + puts e.backtrace + ignore_interrupts { keg.unlink } + raise e + end + end + + # Link keg to opt if it was linked before migrating. + def link_oldname_opt + if old_opt_record + old_opt_record.delete if old_opt_record.symlink? || old_opt_record.exist? + old_opt_record.make_relative_symlink(formula.installed_prefix) + end + end + + # After migtaion every INSTALL_RECEIPT.json has wrong path to the formula + # so we must update INSTALL_RECEIPTs + def update_tabs + new_tabs = newpath.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 old_opt_record + if old_opt_record.symlink? && formula.installed_prefix.exist? \ + && formula.installed_prefix.realpath == old_opt_record.realpath + old_opt_record.unlink + old_opt_record.parent.rmdir_if_possible + end + end + + # Remove oldpath if it exists + def link_oldname_cellar + oldpath.delete if oldpath.symlink? || oldpath.exist? + oldpath.make_relative_symlink(formula.rack) + end + + # Remove Cellar/oldname link if it belongs to newname. + def unlink_oldname_cellar + if (oldpath.symlink? && !oldpath.exist?) || (oldpath.symlink? \ + && formula.rack.exist? && formula.rack.realpath == oldpath.realpath) + oldpath.unlink + end + end + + # Backup everything if errors occured 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.join(old_pin_link_record).expand_path + old_pin_record.make_relative_symlink(src_oldname) + new_pin_record.delete + end + + if newpath.exist? + newpath.subdirs.each do |d| + newname_keg = Keg.new(d) + newname_keg.unlink + newname_keg.uninstall + end + end + + if oldkeg_linked? + begin + # 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 + keg = Keg.new(Pathname.new(oldkeg.to_s)) + keg.link + rescue Keg::LinkError + keg.unlink + raise + rescue Keg::AlreadyLinkedError + keg.unlink + retry + end + end + end + + def backup_oldname_cellar + unless oldpath.exist? + FileUtils.mv(newpath, oldpath) + end + end + + def backup_old_tabs + old_tabs.each(&:write) + end +end