Skip to content

Commit

Permalink
Upgrade: implement linkage repair
Browse files Browse the repository at this point in the history
After upgrading existing kegs, we now search and upgrade their
dependents as well. If any are detected that have broken linkage, they
are reinstalled from source.

If there are any formulae in the dependents tree that are pinned, they
are only reinstalled if they're not outdated; in all cases, a suitable
message is printed detailing the kegs that will be acted upon.
  • Loading branch information
amyspark committed Sep 12, 2018
1 parent da52ba6 commit d442905
Show file tree
Hide file tree
Showing 4 changed files with 270 additions and 56 deletions.
56 changes: 1 addition & 55 deletions Library/Homebrew/cmd/reinstall.rb
Expand Up @@ -7,6 +7,7 @@
require "formula_installer"
require "development_tools"
require "messages"
require "reinstall"

module Homebrew
module_function
Expand All @@ -26,59 +27,4 @@ def reinstall
end
Homebrew.messages.display_messages
end

def reinstall_formula(f)
if f.opt_prefix.directory?
keg = Keg.new(f.opt_prefix.resolved_path)
keg_had_linked_opt = true
keg_was_linked = keg.linked?
backup keg
end

build_options = BuildOptions.new(Options.create(ARGV.flags_only), f.options)
options = build_options.used_options
options |= f.build.used_options
options &= f.options

fi = FormulaInstaller.new(f)
fi.options = options
fi.invalid_option_names = build_options.invalid_option_names
fi.build_bottle = ARGV.build_bottle? || (!f.bottled? && f.build.bottle?)
fi.interactive = ARGV.interactive?
fi.git = ARGV.git?
fi.link_keg ||= keg_was_linked if keg_had_linked_opt
fi.prelude

oh1 "Reinstalling #{Formatter.identifier(f.full_name)} #{options.to_a.join " "}"

fi.install
fi.finish
rescue FormulaInstallationAlreadyAttemptedError
nil
rescue Exception # rubocop:disable Lint/RescueException
ignore_interrupts { restore_backup(keg, keg_was_linked) }
raise
else
backup_path(keg).rmtree if backup_path(keg).exist?
end

def backup(keg)
keg.unlink
keg.rename backup_path(keg)
end

def restore_backup(keg, keg_was_linked)
path = backup_path(keg)

return unless path.directory?

Pathname.new(keg).rmtree if keg.exist?

path.rename keg
keg.link if keg_was_linked
end

def backup_path(path)
Pathname.new "#{path}.reinstall"
end
end
197 changes: 196 additions & 1 deletion Library/Homebrew/cmd/upgrade.rb
Expand Up @@ -21,6 +21,7 @@
#: are pinned; see `pin`, `unpin`).

require "install"
require "reinstall"
require "formula_installer"
require "cleanup"
require "development_tools"
Expand Down Expand Up @@ -80,6 +81,16 @@ def upgrade
puts formulae_upgrades.join(", ")
end

upgrade_formulae(formulae_to_install)

check_dependents(formulae_to_install)

Homebrew.messages.display_messages
end

def upgrade_formulae(formulae_to_install)
return if formulae_to_install.empty?

# Sort keg_only before non-keg_only formulae to avoid any needless conflicts
# with outdated, non-keg_only versions of formulae being upgraded.
formulae_to_install.sort! do |a, b|
Expand All @@ -104,7 +115,6 @@ def upgrade
onoe "#{f}: #{e}"
end
end
Homebrew.messages.display_messages
end

def upgrade_formula(f)
Expand Down Expand Up @@ -171,4 +181,189 @@ def upgrade_formula(f)
nil
end
end

def upgradable_dependents(kegs, formulae)
formulae_to_upgrade = Set.new
formulae_pinned = Set.new

formulae.each do |formula|
descendants = Set.new

dependents = kegs.select do |keg|
keg.runtime_dependencies
.any? { |d| d["full_name"] == formula.full_name }
end

next if dependents.empty?

dependent_formulae = dependents.map(&:to_formula)

dependent_formulae.each do |f|
next if formulae_to_upgrade.include?(f)
next if formulae_pinned.include?(f)

if f.outdated?(fetch_head: ARGV.fetch_head?)
if f.pinned?
formulae_pinned << f
else
formulae_to_upgrade << f
end
end

descendants << f
end

upgradable_descendants, pinned_descendants = upgradable_dependents(kegs, descendants)

formulae_to_upgrade.merge upgradable_descendants
formulae_pinned.merge pinned_descendants
end

[formulae_to_upgrade, formulae_pinned]
end

def broken_dependents(kegs, formulae)
formulae_to_reinstall = Set.new
formulae_pinned_and_outdated = Set.new

CacheStoreDatabase.use(:linkage) do |db|
formulae.each do |formula|
descendants = Set.new

dependents = kegs.select do |keg|
keg.runtime_dependencies
.any? { |d| d["full_name"] == formula.full_name }
end

next if dependents.empty?

dependents.each do |keg|
f = keg.to_formula

next if formulae_to_reinstall.include?(f)
next if formulae_pinned_and_outdated.include?(f)

checker = LinkageChecker.new(keg, cache_db: db)

if checker.broken_library_linkage?
if f.outdated?(fetch_head: ARGV.fetch_head?)
# Outdated formulae = pinned formulae (see function above)
formulae_pinned_and_outdated << f
else
formulae_to_reinstall << f
end
end

descendants << f
end

descendants_to_reinstall, descendants_pinned = broken_dependents(kegs, descendants)

formulae_to_reinstall.merge descendants_to_reinstall
formulae_pinned_and_outdated.merge descendants_pinned
end
end

[formulae_to_reinstall, formulae_pinned_and_outdated]
end

# @private
def depends_on(a, b)
if a.opt_or_installed_prefix_keg
.runtime_dependencies
.any? { |d| d["full_name"] == b.full_name }
1
else
a <=> b
end
end

# @private
def formulae_with_runtime_dependencies
Formula.installed
.map(&:opt_or_installed_prefix_keg)
.reject(&:nil?)
.reject { |f| f.runtime_dependencies.to_a.empty? }
end

def check_dependents(formulae)
return if formulae.empty?

# First find all the outdated dependents.
kegs = formulae_with_runtime_dependencies

return if kegs.empty?

oh1 "Checking dependents for outdated formulae" if ARGV.verbose?
upgradable, pinned = upgradable_dependents(kegs, formulae).map(&:to_a)

upgradable.sort! { |a, b| depends_on(a, b) }

pinned.sort! { |a, b| depends_on(a, b) }

# Print the pinned dependents.
unless pinned.empty?
ohai "Not upgrading #{Formatter.pluralize(pinned.length, "pinned dependent")}:"
puts pinned.map { |f| "#{f.full_specified_name} #{f.pkg_version}" } * ", "
end

# Print the upgradable dependents.
if upgradable.empty?
ohai "No dependents to upgrade" if ARGV.verbose?
else
ohai "Upgrading #{Formatter.pluralize(upgradable.length, "dependent")}:"
formulae_upgrades = upgradable.map do |f|
if f.optlinked?
"#{f.full_specified_name} #{Keg.new(f.opt_prefix).version} -> #{f.pkg_version}"
else
"#{f.full_specified_name} #{f.pkg_version}"
end
end
puts formulae_upgrades.join(", ")
end

upgrade_formulae(upgradable)

# Assess the dependents tree again.
kegs = formulae_with_runtime_dependencies

oh1 "Checking dependents for broken library links" if ARGV.verbose?
reinstallable, pinned = broken_dependents(kegs, formulae).map(&:to_a)

reinstallable.sort! { |a, b| depends_on(a, b) }

pinned.sort! { |a, b| depends_on(a, b) }

# Print the pinned dependents.
unless pinned.empty?
onoe "Not reinstalling #{Formatter.pluralize(pinned.length, "broken and outdated, but pinned dependent")}:"
$stderr.puts pinned.map { |f| "#{f.full_specified_name} #{f.pkg_version}" } * ", "
end

# Print the broken dependents.
if reinstallable.empty?
ohai "No broken dependents to reinstall" if ARGV.verbose?
else
ohai "Reinstalling #{Formatter.pluralize(reinstallable.length, "broken dependent")} from source:"
puts reinstallable.map(&:full_specified_name).join(", ")
end

reinstallable.each do |f|
begin
reinstall_formula(f, build_from_source: true)
rescue FormulaInstallationAlreadyAttemptedError
# We already attempted to reinstall f as part of the dependency tree of
# another formula. In that case, don't generate an error, just move on.
nil
rescue CannotInstallFormulaError => e
ofail e
rescue BuildError => e
e.dump
puts
Homebrew.failed = true
rescue DownloadError => e
ofail e
end
end
end
end
63 changes: 63 additions & 0 deletions Library/Homebrew/reinstall.rb
@@ -0,0 +1,63 @@
require "formula_installer"
require "development_tools"
require "messages"

module Homebrew
module_function

def reinstall_formula(f, build_from_source: false)
if f.opt_prefix.directory?
keg = Keg.new(f.opt_prefix.resolved_path)
keg_had_linked_opt = true
keg_was_linked = keg.linked?
backup keg
end

build_options = BuildOptions.new(Options.create(ARGV.flags_only), f.options)
options = build_options.used_options
options |= f.build.used_options
options &= f.options

fi = FormulaInstaller.new(f)
fi.options = options
fi.invalid_option_names = build_options.invalid_option_names
fi.build_bottle = ARGV.build_bottle? || (!f.bottled? && f.build.bottle?)
fi.interactive = ARGV.interactive?
fi.git = ARGV.git?
fi.link_keg ||= keg_was_linked if keg_had_linked_opt
fi.build_from_source = true if build_from_source
fi.prelude

oh1 "Reinstalling #{Formatter.identifier(f.full_name)} #{options.to_a.join " "}"

fi.install
fi.finish
rescue FormulaInstallationAlreadyAttemptedError
nil
rescue Exception # rubocop:disable Lint/RescueException
ignore_interrupts { restore_backup(keg, keg_was_linked) }
raise
else
backup_path(keg).rmtree if backup_path(keg).exist?
end

def backup(keg)
keg.unlink
keg.rename backup_path(keg)
end

def restore_backup(keg, keg_was_linked)
path = backup_path(keg)

return unless path.directory?

Pathname.new(keg).rmtree if keg.exist?

path.rename keg
keg.link if keg_was_linked
end

def backup_path(path)
Pathname.new "#{path}.reinstall"
end
end
10 changes: 10 additions & 0 deletions Library/Homebrew/test/cmd/upgrade_spec.rb
Expand Up @@ -7,4 +7,14 @@

expect(HOMEBREW_CELLAR/"testball/0.1").to be_a_directory
end

it "upgrades a Formula and cleans up old versions" do
setup_test_formula "testball"
(HOMEBREW_CELLAR/"testball/0.0.1/foo").mkpath

expect { brew "upgrade", "--cleanup" }.to be_a_success

expect(HOMEBREW_CELLAR/"testball/0.1").to be_a_directory
expect(HOMEBREW_CELLAR/"testball/0.0.1").not_to exist
end
end

0 comments on commit d442905

Please sign in to comment.