brew/Library/Homebrew/cmd/update-report.rb

666 lines
21 KiB
Ruby
Raw Normal View History

#: @hide_from_man_page
#: * `update_report` [`--preinstall`]:
#: The Ruby implementation of `brew update`. Never called manually.
require "formula_versions"
require "migrator"
require "formulary"
require "descriptions"
require "cleanup"
module Homebrew
2016-09-26 01:44:51 +02:00
module_function
def update_preinstall_header
2018-03-07 16:14:55 +00:00
@update_preinstall_header ||= begin
ohai "Auto-updated Homebrew!" if ARGV.include?("--preinstall")
true
end
end
def update_report
HOMEBREW_REPOSITORY.cd do
analytics_message_displayed =
Utils.popen_read("git", "config", "--local", "--get", "homebrew.analyticsmessage").chuzzle
analytics_disabled =
Utils.popen_read("git", "config", "--local", "--get", "homebrew.analyticsdisabled").chuzzle
if analytics_message_displayed != "true" &&
analytics_disabled != "true" &&
!ENV["HOMEBREW_NO_ANALYTICS"] &&
!ENV["HOMEBREW_NO_ANALYTICS_MESSAGE_OUTPUT"]
ENV["HOMEBREW_NO_ANALYTICS_THIS_RUN"] = "1"
# Use the shell's audible bell.
print "\a"
# Use an extra newline and bold to avoid this being missed.
ohai "Homebrew has enabled anonymous aggregate user behaviour analytics."
2017-10-15 02:28:32 +02:00
puts <<~EOS
#{Tty.bold}Read the analytics documentation (and how to opt-out) here:
#{Formatter.url("https://docs.brew.sh/Analytics")}#{Tty.reset}
EOS
# Consider the message possibly missed if not a TTY.
if $stdout.tty?
safe_system "git", "config", "--local", "--replace-all", "homebrew.analyticsmessage", "true"
end
end
donation_message_displayed =
Utils.popen_read("git", "config", "--local", "--get", "homebrew.donationmessage").chuzzle
if donation_message_displayed != "true"
ohai "Homebrew is run entirely by unpaid volunteers. Please consider donating:"
puts " #{Formatter.url("https://github.com/Homebrew/brew#donations")}\n"
# Consider the message possibly missed if not a TTY.
if $stdout.tty?
safe_system "git", "config", "--local", "--replace-all", "homebrew.donationmessage", "true"
end
end
end
install_core_tap_if_necessary
hub = ReporterHub.new
updated = false
initial_revision = ENV["HOMEBREW_UPDATE_BEFORE"].to_s
current_revision = ENV["HOMEBREW_UPDATE_AFTER"].to_s
if initial_revision.empty? || current_revision.empty?
odie "update-report should not be called directly!"
end
if initial_revision != current_revision
update_preinstall_header
puts "Updated Homebrew from #{shorten_revision(initial_revision)} to #{shorten_revision(current_revision)}."
updated = true
end
out, _, status = system_command("git",
args: ["describe", "--tags", "--abbrev=0", initial_revision],
chdir: HOMEBREW_REPOSITORY,
print_stderr: false)
initial_version = Version.new(out) if status.success?
updated_taps = []
Tap.each do |tap|
2016-01-20 19:42:55 +08:00
next unless tap.git?
begin
reporter = Reporter.new(tap)
rescue Reporter::ReporterRevisionUnsetError => e
onoe "#{e.message}\n#{e.backtrace.join "\n"}" if ARGV.homebrew_developer?
next
end
if reporter.updated?
updated_taps << tap.name
hub.add(reporter)
end
end
unless updated_taps.empty?
update_preinstall_header
2017-03-11 11:33:12 +01:00
puts "Updated #{Formatter.pluralize(updated_taps.size, "tap")} " \
"(#{updated_taps.join(", ")})."
updated = true
end
migrate_legacy_cache_if_necessary
migrate_cache_entries_to_double_dashes(initial_version)
migrate_legacy_keg_symlinks_if_necessary
if !updated
if !ARGV.include?("--preinstall") && !ENV["HOMEBREW_UPDATE_FAILED"]
puts "Already up-to-date."
end
else
if hub.empty?
puts "No changes to formulae."
else
hub.dump
hub.reporters.each(&:migrate_tap_migration)
hub.reporters.each(&:migrate_formula_rename)
Descriptions.update_cache(hub)
end
puts if ARGV.include?("--preinstall")
end
link_completions_manpages_and_docs
Tap.each(&:link_completions_and_manpages)
Homebrew.failed = true if ENV["HOMEBREW_UPDATE_FAILED"]
# This should always be the last thing to run (but skip on auto-update).
if !ARGV.include?("--preinstall") ||
ENV["HOMEBREW_ENABLE_AUTO_UPDATE_MIGRATION"]
migrate_legacy_repository_if_necessary
end
end
def shorten_revision(revision)
Utils.popen_read("git", "-C", HOMEBREW_REPOSITORY, "rev-parse", "--short", revision).chomp
end
def install_core_tap_if_necessary
return if ENV["HOMEBREW_UPDATE_TEST"]
core_tap = CoreTap.instance
return if core_tap.installed?
CoreTap.ensure_installed!
revision = core_tap.git_head
ENV["HOMEBREW_UPDATE_BEFORE_HOMEBREW_HOMEBREW_CORE"] = revision
ENV["HOMEBREW_UPDATE_AFTER_HOMEBREW_HOMEBREW_CORE"] = revision
end
def migrate_legacy_cache_if_necessary
legacy_cache = Pathname.new "/Library/Caches/Homebrew"
return if HOMEBREW_CACHE.to_s == legacy_cache.to_s
return unless legacy_cache.directory?
return unless legacy_cache.readable_real?
migration_attempted_file = legacy_cache/".migration_attempted"
return if migration_attempted_file.exist?
return unless legacy_cache.writable_real?
FileUtils.touch migration_attempted_file
# This directory could have been compromised if it's world-writable/
# a symlink/owned by another user so don't copy files in those cases.
2016-06-01 09:49:50 +01:00
world_writable = legacy_cache.stat.mode & 0777 == 0777
return if world_writable
return if legacy_cache.symlink?
return if !legacy_cache.owned? && legacy_cache.lstat.uid.nonzero?
ohai "Migrating #{legacy_cache} to #{HOMEBREW_CACHE}..."
HOMEBREW_CACHE.mkpath
legacy_cache.cd do
legacy_cache.entries.each do |f|
next if [".", "..", ".migration_attempted"].include? f.to_s
begin
FileUtils.cp_r f, HOMEBREW_CACHE
rescue
@migration_failed ||= true
end
end
end
if @migration_failed
2017-10-15 02:28:32 +02:00
opoo <<~EOS
Failed to migrate #{legacy_cache} to
#{HOMEBREW_CACHE}. Please do so manually.
EOS
else
ohai "Deleting #{legacy_cache}..."
FileUtils.rm_rf legacy_cache
if legacy_cache.exist?
FileUtils.touch migration_attempted_file
2017-10-15 02:28:32 +02:00
opoo <<~EOS
Failed to delete #{legacy_cache}.
Please do so manually.
EOS
end
end
end
def migrate_cache_entries_to_double_dashes(initial_version)
return if initial_version && initial_version > "1.7.1"
return if ENV.key?("HOMEBREW_DISABLE_LOAD_FORMULA")
ohai "Migrating cache entries..."
Formula.each do |formula|
specs = [*formula.stable, *formula.devel, *formula.head]
resources = [*formula.bottle&.resource] + specs.flat_map do |spec|
[
spec,
*spec.resources.values,
*spec.patches.select(&:external?).map(&:resource),
]
end
2018-08-06 12:23:37 +02:00
resources.each do |resource|
downloader = resource.downloader
name = resource.download_name
version = resource.version
new_location = downloader.cached_location
extname = new_location.extname
old_location = downloader.cached_location.dirname/"#{name}-#{version}#{extname}"
next unless old_location.file?
if new_location.exist?
begin
FileUtils.rm_rf old_location
rescue Errno::EACCES
opoo "Could not remove #{old_location}, please do so manually."
end
else
begin
FileUtils.mv old_location, new_location
rescue Errno::EACCES
opoo "Could not move #{old_location} to #{new_location}, please do so manually."
end
end
end
end
end
def migrate_legacy_repository_if_necessary
return unless HOMEBREW_PREFIX.to_s == "/usr/local"
return unless HOMEBREW_REPOSITORY.to_s == "/usr/local"
ohai "Migrating HOMEBREW_REPOSITORY (please wait)..."
unless HOMEBREW_PREFIX.writable_real?
2017-10-15 02:28:32 +02:00
ofail <<~EOS
#{HOMEBREW_PREFIX} is not writable.
You should change the ownership and permissions of #{HOMEBREW_PREFIX}
temporarily back to your user account so we can complete the Homebrew
repository migration:
sudo chown -R $(whoami) #{HOMEBREW_PREFIX}
EOS
return
end
new_homebrew_repository = Pathname.new "/usr/local/Homebrew"
new_homebrew_repository.rmdir_if_possible
if new_homebrew_repository.exist?
2017-10-15 02:28:32 +02:00
ofail <<~EOS
#{new_homebrew_repository} already exists.
Please remove it manually or uninstall and reinstall Homebrew into a new
location as the migration cannot be done automatically.
EOS
return
end
new_homebrew_repository.mkpath
repo_files = HOMEBREW_REPOSITORY.cd do
Utils.popen_read("git ls-files").lines.map(&:chomp)
end
unless Utils.popen_read("git status --untracked-files=all --porcelain").empty?
HOMEBREW_REPOSITORY.cd do
quiet_system "git", "merge", "--abort"
quiet_system "git", "rebase", "--abort"
quiet_system "git", "reset", "--mixed"
safe_system "git", "-c", "user.email=brew-update@localhost",
"-c", "user.name=brew update",
"stash", "save", "--include-untracked"
end
stashed = true
end
FileUtils.cp_r "#{HOMEBREW_REPOSITORY}/.git", "#{new_homebrew_repository}/.git"
new_homebrew_repository.cd do
safe_system "git", "checkout", "--force", "."
safe_system "git", "stash", "pop" if stashed
end
if (HOMEBREW_REPOSITORY/"Library/Locks").exist?
FileUtils.cp_r "#{HOMEBREW_REPOSITORY}/Library/Locks", "#{new_homebrew_repository}/Library/Locks"
end
if (HOMEBREW_REPOSITORY/"Library/Taps").exist?
FileUtils.cp_r "#{HOMEBREW_REPOSITORY}/Library/Taps", "#{new_homebrew_repository}/Library/Taps"
end
unremovable_paths = []
extra_remove_paths = [".git", "Library/Locks", "Library/Taps",
"Library/Homebrew/cask", "Library/Homebrew/test"]
(repo_files + extra_remove_paths).each do |file|
path = Pathname.new "#{HOMEBREW_REPOSITORY}/#{file}"
begin
FileUtils.rm_rf path
rescue Errno::EACCES
unremovable_paths << path
end
quiet_system "rmdir", "-p", path.parent if path.parent.exist?
end
unless unremovable_paths.empty?
2017-10-15 02:28:32 +02:00
ofail <<~EOS
Could not remove old HOMEBREW_REPOSITORY paths!
Please do this manually with:
sudo rm -rf #{unremovable_paths.join " "}
EOS
end
(Keg::ALL_TOP_LEVEL_DIRECTORIES + ["Cellar"]).each do |dir|
FileUtils.mkdir_p "#{HOMEBREW_PREFIX}/#{dir}"
end
src = Pathname.new("#{new_homebrew_repository}/bin/brew")
dst = Pathname.new("#{HOMEBREW_PREFIX}/bin/brew")
begin
FileUtils.ln_s(src.relative_path_from(dst.parent), dst)
rescue Errno::EACCES, Errno::ENOENT
2017-10-15 02:28:32 +02:00
ofail <<~EOS
Could not create symlink at #{dst}!
Please do this manually with:
sudo ln -sf #{src} #{dst}
sudo chown $(whoami) #{dst}
EOS
end
link_completions_manpages_and_docs(new_homebrew_repository)
ohai "Migrated HOMEBREW_REPOSITORY to #{new_homebrew_repository}!"
2017-10-15 02:28:32 +02:00
puts <<~EOS
Homebrew no longer needs to have ownership of /usr/local. If you wish you can
return /usr/local to its default ownership with:
sudo chown root:wheel #{HOMEBREW_PREFIX}
EOS
rescue => e
2017-10-15 02:28:32 +02:00
ofail <<~EOS
2016-08-26 16:04:47 +02:00
#{Tty.bold}Failed to migrate HOMEBREW_REPOSITORY to #{new_homebrew_repository}!#{Tty.reset}
The error was:
#{e}
Please try to resolve this error yourself and then run `brew update` again to
complete the migration. If you need help please +1 an existing error or comment
with your new error in issue:
2016-09-17 03:21:51 +02:00
#{Formatter.url("https://github.com/Homebrew/brew/issues/987")}
EOS
$stderr.puts e.backtrace
end
def link_completions_manpages_and_docs(repository = HOMEBREW_REPOSITORY)
command = "brew update"
Utils::Link.link_completions(repository, command)
Utils::Link.link_manpages(repository, command)
Utils::Link.link_docs(repository, command)
rescue => e
2017-10-15 02:28:32 +02:00
ofail <<~EOS
Failed to link all completions, docs and manpages:
#{e}
EOS
end
end
class Reporter
class ReporterRevisionUnsetError < RuntimeError
def initialize(var_name)
super "#{var_name} is unset!"
end
end
attr_reader :tap, :initial_revision, :current_revision
def initialize(tap)
@tap = tap
2018-05-25 16:21:37 +02:00
initial_revision_var = "HOMEBREW_UPDATE_BEFORE#{tap.repo_var}"
@initial_revision = ENV[initial_revision_var].to_s
raise ReporterRevisionUnsetError, initial_revision_var if @initial_revision.empty?
2018-05-25 16:21:37 +02:00
current_revision_var = "HOMEBREW_UPDATE_AFTER#{tap.repo_var}"
@current_revision = ENV[current_revision_var].to_s
raise ReporterRevisionUnsetError, current_revision_var if @current_revision.empty?
end
def report
return @report if @report
@report = Hash.new { |h, k| h[k] = [] }
return @report unless updated?
diff.each_line do |line|
status, *paths = line.split
src = Pathname.new paths.first
dst = Pathname.new paths.last
next unless dst.extname == ".rb"
if paths.any? { |p| tap.cask_file?(p) }
# Currently only need to handle Cask deletion/migration.
if status == "D"
# Have a dedicated report array for deleted casks.
@report[:DC] << tap.formula_file_to_name(src)
end
end
next unless paths.any? { |p| tap.formula_file?(p) }
case status
when "A", "D"
full_name = tap.formula_file_to_name(src)
name = full_name.split("/").last
new_tap = tap.tap_migrations[name]
@report[status.to_sym] << full_name unless new_tap
when "M"
begin
formula = Formulary.factory(tap.path/src)
new_version = formula.pkg_version
old_version = FormulaVersions.new(formula).formula_at_revision(@initial_revision, &:pkg_version)
next if new_version == old_version
rescue Exception => e # rubocop:disable Lint/RescueException
onoe "#{e.message}\n#{e.backtrace.join "\n"}" if ARGV.homebrew_developer?
end
@report[:M] << tap.formula_file_to_name(src)
when /^R\d{0,3}/
src_full_name = tap.formula_file_to_name(src)
dst_full_name = tap.formula_file_to_name(dst)
# Don't report formulae that are moved within a tap but not renamed
next if src_full_name == dst_full_name
@report[:D] << src_full_name
@report[:A] << dst_full_name
end
end
renamed_formulae = Set.new
@report[:D].each do |old_full_name|
old_name = old_full_name.split("/").last
new_name = tap.formula_renames[old_name]
next unless new_name
if tap.core_tap?
new_full_name = new_name
else
new_full_name = "#{tap}/#{new_name}"
end
renamed_formulae << [old_full_name, new_full_name] if @report[:A].include? new_full_name
end
@report[:A].each do |new_full_name|
new_name = new_full_name.split("/").last
old_name = tap.formula_renames.key(new_name)
next unless old_name
if tap.core_tap?
old_full_name = old_name
else
old_full_name = "#{tap}/#{old_name}"
end
renamed_formulae << [old_full_name, new_full_name]
end
unless renamed_formulae.empty?
@report[:A] -= renamed_formulae.map(&:last)
@report[:D] -= renamed_formulae.map(&:first)
@report[:R] = renamed_formulae.to_a
end
@report
end
def updated?
initial_revision != current_revision
end
def migrate_tap_migration
(report[:D] + report[:DC]).each do |full_name|
name = full_name.split("/").last
new_tap_name = tap.tap_migrations[name]
next if new_tap_name.nil? # skip if not in tap_migrations list.
new_tap_user, new_tap_repo, new_tap_new_name = new_tap_name.split("/")
new_name = if new_tap_new_name
new_full_name = new_tap_new_name
new_tap_name = "#{new_tap_user}/#{new_tap_repo}"
new_tap_new_name
else
new_full_name = "#{new_tap_name}/#{name}"
name
end
# This means it is a Cask
if report[:DC].include? full_name
next unless (HOMEBREW_PREFIX/"Caskroom"/new_name).exist?
new_tap = Tap.fetch(new_tap_name)
new_tap.install unless new_tap.installed?
2017-10-15 02:28:32 +02:00
ohai "#{name} has been moved to Homebrew.", <<~EOS
To uninstall the cask run:
brew cask uninstall --force #{name}
EOS
next if (HOMEBREW_CELLAR/new_name.split("/").last).directory?
ohai "Installing #{new_name}..."
system HOMEBREW_BREW_FILE, "install", new_full_name
begin
unless Formulary.factory(new_full_name).keg_only?
system HOMEBREW_BREW_FILE, "link", new_full_name, "--overwrite"
end
rescue Exception => e # rubocop:disable Lint/RescueException
onoe "#{e.message}\n#{e.backtrace.join "\n"}" if ARGV.homebrew_developer?
end
next
end
next unless (dir = HOMEBREW_CELLAR/name).exist? # skip if formula is not installed.
tabs = dir.subdirs.map { |d| Tab.for_keg(Keg.new(d)) }
next unless tabs.first.tap == tap # skip if installed formula is not from this tap.
new_tap = Tap.fetch(new_tap_name)
# For formulae migrated to cask: Auto-install cask or provide install instructions.
2018-05-25 18:03:16 +02:00
if new_tap_name.start_with?("homebrew/cask")
if new_tap.installed? && (HOMEBREW_PREFIX/"Caskroom").directory?
ohai "#{name} has been moved to Homebrew-Cask."
ohai "brew unlink #{name}"
system HOMEBREW_BREW_FILE, "unlink", name
ohai "brew prune"
system HOMEBREW_BREW_FILE, "prune"
ohai "brew cask install #{new_name}"
system HOMEBREW_BREW_FILE, "cask", "install", new_name
2017-10-15 02:28:32 +02:00
ohai <<~EOS
#{name} has been moved to Homebrew-Cask.
The existing keg has been unlinked.
Please uninstall the formula when convenient by running:
brew uninstall --force #{name}
EOS
else
2017-10-15 02:28:32 +02:00
ohai "#{name} has been moved to Homebrew-Cask.", <<~EOS
To uninstall the formula and install the cask run:
brew uninstall --force #{name}
2018-05-25 18:03:16 +02:00
brew tap #{new_tap_name}
brew cask install #{new_name}
EOS
end
else
new_tap.install unless new_tap.installed?
# update tap for each Tab
tabs.each { |tab| tab.tap = new_tap }
tabs.each(&:write)
end
end
end
def migrate_formula_rename
Formula.installed.each do |formula|
next unless Migrator.needs_migration?(formula)
oldname = formula.oldname
oldname_rack = HOMEBREW_CELLAR/oldname
if oldname_rack.subdirs.empty?
oldname_rack.rmdir_if_possible
next
end
new_name = tap.formula_renames[oldname]
next unless new_name
new_full_name = "#{tap}/#{new_name}"
begin
f = Formulary.factory(new_full_name)
rescue Exception => e # rubocop:disable Lint/RescueException
onoe "#{e.message}\n#{e.backtrace.join "\n"}" if ARGV.homebrew_developer?
next
end
Migrator.migrate_if_needed(f)
end
end
private
def diff
Utils.popen_read(
"git", "-C", tap.path, "diff-tree", "-r", "--name-status", "--diff-filter=AMDR",
"-M85%", initial_revision, current_revision
)
end
end
class ReporterHub
extend Forwardable
attr_reader :reporters
def initialize
@hash = {}
@reporters = []
end
def select_formula(key)
@hash.fetch(key, [])
end
def add(reporter)
@reporters << reporter
report = reporter.report.delete_if { |_k, v| v.empty? }
@hash.update(report) { |_key, oldval, newval| oldval.concat(newval) }
end
delegate :empty? => :@hash
def dump
# Key Legend: Added (A), Copied (C), Deleted (D), Modified (M), Renamed (R)
dump_formula_report :A, "New Formulae"
dump_formula_report :M, "Updated Formulae"
dump_formula_report :R, "Renamed Formulae"
dump_formula_report :D, "Deleted Formulae"
end
private
def dump_formula_report(key, title)
formulae = select_formula(key).sort.map do |name, new_name|
# Format list items of renamed formulae
case key
when :R
name = pretty_installed(name) if installed?(name)
new_name = pretty_installed(new_name) if installed?(new_name)
"#{name} -> #{new_name}"
when :A
name unless installed?(name)
else
installed?(name) ? pretty_installed(name) : name
end
end.compact
2016-09-23 22:02:23 +02:00
return if formulae.empty?
# Dump formula list.
ohai title
puts Formatter.columns(formulae.sort)
end
def installed?(formula)
(HOMEBREW_CELLAR/formula.split("/").last).directory?
end
end