#: @hide_from_man_page #: * `update_report`: #: The Ruby implementation of `brew update`. Never called manually. require "formula_versions" require "migrator" require "formulary" require "descriptions" require "cleanup" require "utils" module Homebrew def update_preinstall_header @header_already_printed ||= 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_THIS_RUN"] = "1" ohai "Homebrew has enabled anonymous aggregate user behaviour analytics" puts "Read the analytics documentation (and how to opt-out) here:" puts " https://git.io/brew-analytics" # Consider the message possibly missed if not a TTY. if $stdout.tty? safe_system "git", "config", "--local", "--replace-all", "homebrew.analyticsmessage", "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 updated_taps = [] Tap.each do |tap| 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 puts "Updated #{updated_taps.size} tap#{plural(updated_taps.size)} " \ "(#{updated_taps.join(", ")})." updated = true end migrate_legacy_cache_if_necessary 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_and_docs Tap.each(&:link_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 private def shorten_revision(revision) Utils.popen_read("git", "-C", HOMEBREW_REPOSITORY, "rev-parse", "--short", revision).chomp end def install_core_tap_if_necessary core_tap = CoreTap.instance return if core_tap.installed? CoreTap.ensure_installed! quiet: false 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 # Cleanup to avoid copying files unnecessarily ohai "Cleaning up #{legacy_cache}..." Cleanup.cleanup_cache legacy_cache # 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. 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 opoo <<-EOS.undent 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 opoo <<-EOS.undent Failed to delete #{legacy_cache}. Please do so manually. EOS 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? ofail <<-EOS.undent #{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" if new_homebrew_repository.exist? ofail <<-EOS.undent #{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? ofail <<-EOS.undent 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 ofail <<-EOS.undent Could not create symlink at #{dst}! Please do this manually with: sudo ln -sf #{src} #{dst} sudo chown $(whoami) #{dst} EOS end link_completions_and_docs(new_homebrew_repository) ohai "Migrated HOMEBREW_REPOSITORY to #{new_homebrew_repository}!" puts <<-EOS.undent 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 ofail <<-EOS.undent #{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: #{Formatter.url("https://github.com/Homebrew/brew/issues/987")} EOS $stderr.puts e.backtrace end def link_completions_and_docs(repository = HOMEBREW_REPOSITORY) command = "brew update" link_src_dst_dirs(repository/"completions/bash", HOMEBREW_PREFIX/"etc/bash_completion.d", command) link_src_dst_dirs(repository/"docs", HOMEBREW_PREFIX/"share/doc/homebrew", command, link_dir: true) link_src_dst_dirs(repository/"completions/zsh", HOMEBREW_PREFIX/"share/zsh/site-functions", command) link_src_dst_dirs(repository/"manpages", HOMEBREW_PREFIX/"share/man/man1", command) rescue => e ofail <<-EOS.undent 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 initial_revision_var = "HOMEBREW_UPDATE_BEFORE#{repo_var}" @initial_revision = ENV[initial_revision_var].to_s raise ReporterRevisionUnsetError, initial_revision_var if @initial_revision.empty? current_revision_var = "HOMEBREW_UPDATE_AFTER#{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" @report[status.to_sym] << tap.formula_file_to_name(src) 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 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 = [] @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 unless renamed_formulae.empty? @report[:A] -= renamed_formulae.map(&:last) @report[:D] -= renamed_formulae.map(&:first) @report[:R] = renamed_formulae 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. # This means it is a Cask if report[:DC].include? full_name next unless (HOMEBREW_REPOSITORY/"Caskroom"/name).exist? new_tap = Tap.fetch(new_tap_name) new_tap.install unless new_tap.installed? ohai "#{name} has been moved to Homebrew.", <<-EOS.undent To uninstall the cask run: brew cask uninstall --force #{name} EOS new_full_name = "#{new_tap_name}/#{name}" next if (HOMEBREW_CELLAR/name.split("/").last).directory? ohai "Installing #{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 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. if new_tap_name == "caskroom/cask" if new_tap.installed? && (HOMEBREW_REPOSITORY/"Caskroom").directory? ohai "#{name} has been moved to Homebrew Cask." ohai "brew uninstall --force #{name}" system HOMEBREW_BREW_FILE, "uninstall", "--force", name ohai "brew prune" system HOMEBREW_BREW_FILE, "prune" ohai "brew cask install #{name}" system HOMEBREW_BREW_FILE, "cask", "install", name else ohai "#{name} has been moved to Homebrew Cask.", <<-EOS.undent To uninstall the formula and install the cask run: brew uninstall --force #{name} brew cask install #{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 report[:R].each do |old_full_name, new_full_name| old_name = old_full_name.split("/").last next unless (dir = HOMEBREW_CELLAR/old_name).directory? && !dir.subdirs.empty? begin f = Formulary.factory(new_full_name) rescue Exception => e onoe "#{e.message}\n#{e.backtrace.join "\n"}" if ARGV.homebrew_developer? next end begin migrator = Migrator.new(f) migrator.migrate rescue Migrator::MigratorDifferentTapsError rescue Exception => e onoe e end end end private def repo_var @repo_var ||= tap.path.to_s .strip_prefix(Tap::TAP_DIRECTORY.to_s) .tr("^A-Za-z0-9", "_") .upcase end 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 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 def empty? @hash.empty? end 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 if key == :R name = pretty_installed(name) if installed?(name) new_name = pretty_installed(new_name) if installed?(new_name) "#{name} -> #{new_name}" else installed?(name) ? pretty_installed(name) : name end end return if formulae.empty? # Dump formula list. ohai title puts_columns(formulae) end def installed?(formula) (HOMEBREW_CELLAR/formula.split("/").last).directory? end end