Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Library/Homebrew/formula.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1746,7 +1746,7 @@ def migration_needed?

sig { params(fetch_head: T::Boolean).returns(T::Array[Keg]) }
def outdated_kegs(fetch_head: false)
raise Migrator::MigrationNeededError.new(oldnames_to_migrate.first, name) if migration_needed?
raise Migrator::MigrationNeededError.new(oldnames_to_migrate.fetch(0), name) if migration_needed?

cache_key = "#{full_name}-#{fetch_head}"
Formula.cache[:outdated_kegs] ||= {}
Expand Down
116 changes: 83 additions & 33 deletions Library/Homebrew/migrator.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true

require "lock_file"
Expand All @@ -14,6 +14,7 @@ class Migrator

# Error for when a migration is necessary.
class MigrationNeededError < RuntimeError
sig { params(oldname: String, newname: String).void }
def initialize(oldname, newname)
super <<~EOS
#{oldname} was renamed to #{newname} and needs to be migrated by running:
Expand All @@ -24,13 +25,15 @@ def initialize(oldname, newname)

# Error for when the old name's path does not exist.
class MigratorNoOldpathError < RuntimeError
sig { params(oldname: String).void }
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
sig { params(formula: Formula, oldname: String, tap: Tap).void }
def initialize(formula, oldname, tap)
msg = if tap.core_tap?
"Please try to use #{oldname} to refer to the formula.\n"
Expand All @@ -39,58 +42,74 @@ def initialize(formula, oldname, tap)
end

super <<~EOS
#{formula.name} from #{formula.tap} is given, but old name #{oldname} was installed from #{tap || "path or url"}.
#{formula.name} from #{formula.tap} is given, but old name #{oldname} was installed from #{tap}.
#{msg}To force migration, run:
brew migrate --force #{oldname}
EOS
end
end

# Instance of renamed formula.
sig { returns(Formula) }
attr_reader :formula

# Old name of the formula.
sig { returns(String) }
attr_reader :oldname

# Path to oldname's Cellar.
sig { returns(Pathname) }
attr_reader :old_cellar

# Path to oldname pin.
sig { returns(Pathname) }
attr_reader :old_pin_record

# Path to oldname opt.
sig { returns(T::Array[Pathname]) }
attr_reader :old_opt_records

# Oldname linked kegs.
sig { returns(T::Array[Keg]) }
attr_reader :old_linked_kegs

# Oldname linked kegs that were fully linked.
sig { returns(T::Array[Keg]) }
attr_reader :old_full_linked_kegs

# Tabs from oldname kegs.
sig { returns(T::Array[Tab]) }
attr_reader :old_tabs

# Tap of the old name.
sig { returns(T.nilable(Tap)) }
attr_reader :old_tap

# Resolved path to oldname pin.
sig { returns(T.nilable(Pathname)) }
attr_reader :old_pin_link_record

# New name of the formula.
sig { returns(String) }
attr_reader :newname

# Path to newname Cellar according to new name.
sig { returns(Pathname) }
attr_reader :new_cellar

# True if new Cellar existed at initialization time.
sig { returns(T::Boolean) }
attr_reader :new_cellar_existed

# Path to newname pin.
sig { returns(Pathname) }
attr_reader :new_pin_record

# Path to newname keg that will be linked if old_linked_keg isn't nil.
sig { returns(T.nilable(Pathname)) }
attr_reader :new_linked_keg_record

sig { params(formula: Formula).returns(T::Array[String]) }
def self.oldnames_needing_migration(formula)
formula.oldnames.select do |oldname|
oldname_rack = HOMEBREW_CELLAR/oldname
Expand All @@ -101,10 +120,12 @@ def self.oldnames_needing_migration(formula)
end
end

sig { params(formula: Formula).returns(T::Boolean) }
def self.needs_migration?(formula)
!oldnames_needing_migration(formula).empty?
end

sig { params(formula: Formula, force: T::Boolean, dry_run: T::Boolean).void }
def self.migrate_if_needed(formula, force:, dry_run: false)
oldnames = Migrator.oldnames_needing_migration(formula)

Expand All @@ -123,40 +144,44 @@ def self.migrate_if_needed(formula, force:, dry_run: false)
end
end

sig { params(formula: Formula, oldname: String, force: T::Boolean).void }
def initialize(formula, oldname, force: false)
@oldname = oldname
@newname = formula.name
@oldname = T.let(oldname, String)
@newname = T.let(formula.name, String)

@formula = formula
@old_cellar = HOMEBREW_CELLAR/oldname
@formula = T.let(formula, Formula)
@old_cellar = T.let(HOMEBREW_CELLAR/oldname, Pathname)
raise MigratorNoOldpathError, oldname unless old_cellar.exist?

@old_tabs = old_cellar.subdirs.map { |d| Keg.new(d).tab }
@old_tap = old_tabs.first.tap
@old_tabs = T.let(old_cellar.subdirs.map { |d| Keg.new(d).tab }, T::Array[Tab])
first_tab = old_tabs.first
@old_tap = T.let(first_tab&.tap, T.nilable(Tap))

raise MigratorDifferentTapsError.new(formula, oldname, old_tap) if !force && !from_same_tap_user?
raise MigratorDifferentTapsError.new(formula, oldname, T.must(old_tap)) if !force && !from_same_tap_user?

@new_cellar = HOMEBREW_CELLAR/formula.name
@new_cellar_existed = @new_cellar.exist?
@new_cellar = T.let(HOMEBREW_CELLAR/formula.name, Pathname)
@new_cellar_existed = T.let(@new_cellar.exist?, T::Boolean)

@old_linked_kegs = linked_old_linked_kegs
@old_full_linked_kegs = []
@old_opt_records = []
@old_linked_kegs = T.let(linked_old_linked_kegs, T::Array[Keg])
@old_full_linked_kegs = T.let([], T::Array[Keg])
@old_opt_records = T.let([], T::Array[Pathname])
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
@new_linked_keg_record = T.let(nil, T.nilable(Pathname))
unless old_linked_kegs.empty?
@new_linked_keg_record = HOMEBREW_CELLAR/"#{newname}/#{File.basename(old_linked_kegs.first)}"
@new_linked_keg_record = HOMEBREW_CELLAR/"#{newname}/#{File.basename(old_linked_kegs.first.to_s)}"
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
@old_pin_record = T.let(HOMEBREW_PINNED_KEGS/oldname, Pathname)
@new_pin_record = T.let(HOMEBREW_PINNED_KEGS/newname, Pathname)
@pinned = T.let(old_pin_record.symlink?, T::Boolean)
@old_pin_link_record = T.let(old_pin_record.symlink? ? old_pin_record.readlink : nil, T.nilable(Pathname))
end

# Fix `INSTALL_RECEIPT`s for tap-migrated formula.
sig { void }
def fix_tabs
old_tabs.each do |tab|
tab.tap = formula.tap
Expand All @@ -166,10 +191,10 @@ def fix_tabs

sig { returns(T::Boolean) }
def from_same_tap_user?
formula_tap_user = formula.tap.user if formula.tap
formula_tap_user = formula.tap&.user
old_tap_user = nil

new_tap = if old_tap
new_tap = if (old_tap = self.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("/")
Expand All @@ -192,6 +217,7 @@ def from_same_tap_user?
end
end

sig { returns(T::Array[Keg]) }
def linked_old_linked_kegs
keg_dirs = []
keg_dirs += new_cellar.subdirs if new_cellar.exist?
Expand All @@ -200,10 +226,12 @@ def linked_old_linked_kegs
kegs.select { |keg| keg.linked? || keg.optlinked? }
end

sig { returns(T::Boolean) }
def pinned?
@pinned
end

sig { void }
def migrate
oh1 "Migrating formula #{Formatter.identifier(oldname)} to #{Formatter.identifier(newname)}"
lock
Expand Down Expand Up @@ -240,6 +268,7 @@ def migrate
unlock
end

sig { params(directory: T.untyped).returns(T::Boolean) }
def remove_conflicts(directory)
conflicted = T.let(false, T::Boolean)

Expand All @@ -261,6 +290,7 @@ def remove_conflicts(directory)
conflicted
end

sig { params(directory: Pathname).void }
def merge_directory(directory)
directory.each_child do |c|
new_path = new_cellar/c.relative_path_from(old_cellar)
Expand All @@ -275,6 +305,7 @@ def merge_directory(directory)
end

# Move everything from `Cellar/oldname` to `Cellar/newname`.
sig { void }
def move_to_new_directory
return unless old_cellar.exist?

Expand All @@ -291,9 +322,13 @@ def move_to_new_directory
end
end

sig { void }
def repin
return unless pinned?

old_pin_link_record = self.old_pin_link_record
return unless old_pin_link_record

# `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.
Expand All @@ -311,6 +346,7 @@ def repin
old_pin_record.delete
end

sig { void }
def unlink_oldname
oh1 "Unlinking #{Formatter.identifier(oldname)}"
old_cellar.subdirs.each do |d|
Expand All @@ -319,6 +355,7 @@ def unlink_oldname
end
end

sig { void }
def unlink_newname
oh1 "Temporarily unlinking #{Formatter.identifier(newname)}"
new_cellar.subdirs.each do |d|
Expand All @@ -327,9 +364,10 @@ def unlink_newname
end
end

sig { returns(T.nilable(Integer)) }
def link_newname
oh1 "Relinking #{Formatter.identifier(newname)}"
new_keg = Keg.new(new_linked_keg_record)
new_keg = Keg.new(T.must(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.
Expand Down Expand Up @@ -375,15 +413,17 @@ def link_newname
end

# Link keg to opt if it was linked before migrating.
sig { void }
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)
old_opt_record.make_relative_symlink(T.must(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.
sig { void }
def update_tabs
new_tabs = new_cellar.subdirs.map { |d| Keg.new(d).tab }
new_tabs.each do |tab|
Expand All @@ -393,26 +433,29 @@ def update_tabs
end

# Remove `opt/oldname` link if it belongs to newname.
sig { void }
def unlink_oldname_opt
return unless new_linked_keg_record.exist?
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
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.
sig { void }
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.
sig { void }
def unlink_oldname_cellar
if (old_cellar.symlink? && !old_cellar.exist?) ||
(old_cellar.symlink? && formula.rack.exist? && formula.rack.realpath == old_cellar.realpath)
Expand All @@ -421,14 +464,15 @@ def unlink_oldname_cellar
end

# Backup everything if errors occur while migrating.
sig { void }
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
if pinned? && !old_pin_record.symlink? && old_pin_link_record
src_oldname = (old_pin_record.dirname/T.must(old_pin_link_record)).expand_path
old_pin_record.make_relative_symlink(src_oldname)
new_pin_record.delete
end
Expand Down Expand Up @@ -460,23 +504,29 @@ def backup_oldname
end
end

sig { void }
def backup_oldname_cellar
FileUtils.mv(new_cellar, old_cellar) unless old_cellar.exist?
end

sig { void }
def backup_old_tabs
old_tabs.each(&:write)
end

sig { void }
def lock
@newname_lock = FormulaLock.new newname
@oldname_lock = FormulaLock.new oldname
@newname_lock.lock
@oldname_lock.lock
newname_lock = FormulaLock.new(newname)
oldname_lock = FormulaLock.new(oldname)
newname_lock.lock
oldname_lock.lock
@newname_lock = T.let(newname_lock, T.nilable(FormulaLock))
@oldname_lock = T.let(oldname_lock, T.nilable(FormulaLock))
end

sig { void }
def unlock
@newname_lock.unlock
@oldname_lock.unlock
@newname_lock&.unlock
@oldname_lock&.unlock
end
end
Loading