brew/Library/Homebrew/migrator.rb

359 lines
10 KiB
Ruby
Raw Normal View History

require "formula"
2015-08-17 22:49:57 +08:00
require "formula_lock"
require "keg"
require "tab"
class Migrator
class MigrationNeededError < RuntimeError
def initialize(formula)
super <<-EOS.undent
#{formula.oldname} was renamed to #{formula.name} and needs to be migrated.
Please run `brew migrate #{formula.oldname}`
EOS
end
end
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)
2016-03-08 21:53:29 +08:00
msg = if tap.core_tap?
"Please try to use #{formula.oldname} to refer the formula.\n"
elsif tap
2015-12-06 22:50:21 +08:00
"Please try to use fully-qualified #{tap}/#{formula.oldname} to refer the formula.\n"
end
super <<-EOS.undent
#{formula.name} from #{formula.tap} is given, but old name #{formula.oldname} was installed from #{tap ? tap : "path or url"}.
#{msg}To force migrate use `brew migrate --force #{formula.oldname}`.
EOS
end
end
# instance of new name 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
# 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 initialize(formula)
@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
if !ARGV.force? && !from_same_taps?
raise MigratorDifferentTapsError.new(formula, old_tap)
end
@new_cellar = HOMEBREW_CELLAR/formula.name
if @old_linked_keg = get_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_RECEIPTS for tap-migrated formula.
def fix_tabs
old_tabs.each do |tab|
2015-12-06 22:50:21 +08:00
tab.tap = formula.tap
tab.write
end
end
def from_same_taps?
if 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,
# 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 == old_tap.tap_migrations[formula.oldname]
fix_tabs
true
2015-12-06 22:50:21 +08:00
else
false
end
end
def get_linked_old_linked_keg
kegs = old_cellar.subdirs.map { |d| Keg.new(d) }
kegs.detect(&:linked?) || kegs.detect(&:optlinked?)
end
def pinned?
@pinned
end
def migrate
if new_cellar.exist?
onoe "#{new_cellar} 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}"
2015-08-17 22:49:57 +08:00
lock
unlink_oldname
move_to_new_directory
repin
link_oldname_cellar
link_oldname_opt
link_newname unless old_linked_keg.nil?
update_tabs
rescue Interrupt
ignore_interrupts { backup_oldname }
rescue Exception => e
2015-08-15 20:37:24 +08:00
onoe "Error occured while migrating."
puts e
puts e.backtrace if ARGV.debug?
puts "Backuping..."
ignore_interrupts { backup_oldname }
2015-08-17 22:49:57 +08:00
ensure
unlock
end
end
# move everything from Cellar/oldname to Cellar/newname
def move_to_new_directory
puts "Moving to: #{new_cellar}"
FileUtils.mv(old_cellar, new_cellar)
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}"
old_cellar.subdirs.each do |d|
keg = Keg.new(d)
keg.unlink
end
end
def link_newname
oh1 "Linking #{Tty.green}#{newname}#{Tty.reset}"
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_linked_keg_record
begin
new_keg.optlink
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
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)
new_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
2015-08-15 20:37:24 +08:00
puts e.backtrace if ARGV.debug?
ignore_interrupts { new_keg.unlink }
2015-08-15 20:37:24 +08:00
raise
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.make_relative_symlink(new_linked_keg_record)
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 = 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 old_opt_record
if old_opt_record.symlink? && old_opt_record.exist? \
&& new_linked_keg_record.exist? \
&& new_linked_keg_record.realpath == old_opt_record.realpath
old_opt_record.unlink
old_opt_record.parent.rmdir_if_possible
end
end
# Remove old_cellar 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 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 new_cellar.exist?
new_cellar.subdirs.each do |d|
newname_keg = Keg.new(d)
newname_keg.unlink
newname_keg.uninstall
end
end
unless old_linked_keg.nil?
# 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
if old_linked_keg_record
begin
old_linked_keg.link
rescue Keg::LinkError
old_linked_keg.unlink
raise
rescue Keg::AlreadyLinkedError
old_linked_keg.unlink
retry
end
else
old_linked_keg.optlink
end
end
end
def backup_oldname_cellar
2016-09-23 11:01:40 +02:00
FileUtils.mv(new_cellar, old_cellar) unless old_cellar.exist?
end
def backup_old_tabs
old_tabs.each(&:write)
end
2015-08-17 22:49:57 +08:00
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