brew/Library/Homebrew/update_migrator.rb
Shaun Jackman f6093961ef Add Homebrew::DEFAULT_PREFIX for Linux
The default prefix is /usr/local on macOS
and /home/linuxbrew/.linuxbrew on Linux.
2018-09-11 09:57:08 -07:00

383 lines
12 KiB
Ruby

require "cask/cask_loader"
require "cask/download"
module UpdateMigrator
class << self
def formula_resources(formula)
specs = [formula.stable, formula.devel, formula.head].compact
[*formula.bottle&.resource] + specs.flat_map do |spec|
[
spec,
*spec.resources.values,
*spec.patches.select(&:external?).map(&:resource),
]
end
end
private :formula_resources
def parse_extname(url)
uri_path = if URI::DEFAULT_PARSER.make_regexp =~ url
uri = URI(url)
uri.query ? "#{uri.path}?#{uri.query}" : uri.path
else
url
end
# Given a URL like https://example.com/download.php?file=foo-1.0.tar.gz
# the extension we want is ".tar.gz", not ".php".
Pathname.new(uri_path).ascend do |path|
ext = path.extname[/[^?&]+/]
return ext if ext
end
nil
end
private :parse_extname
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")
return unless HOMEBREW_CACHE.directory?
return if HOMEBREW_CACHE.children.empty?
ohai "Migrating cache entries..."
Formula.each do |formula|
formula_resources(formula).each do |resource|
downloader = resource.downloader
url = downloader.url
name = resource.download_name
version = resource.version
extname = parse_extname(url)
old_location = downloader.cache/"#{name}-#{version}#{extname}"
new_location = downloader.cache/"#{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_cache_entries_to_symlinks(initial_version)
return if initial_version && initial_version > "1.7.2"
return if ENV.key?("HOMEBREW_DISABLE_LOAD_FORMULA")
return unless HOMEBREW_CACHE.directory?
return if HOMEBREW_CACHE.children.empty?
ohai "Migrating cache entries..."
cache_entries = lambda do |path|
if path.directory?
path.children
.reject(&:symlink?)
.select(&:file?)
.map { |child| child.basename.to_s }
.select { |basename| basename.include?("--") }
.map { |basename| basename.sub(/\-\-.*/, "") }
.uniq
else
[]
end
end
load_formula = lambda do |formula|
begin
Formula[formula]
rescue FormulaUnavailableError
nil
end
end
load_cask = lambda do |cask|
begin
Cask::CaskLoader.load(cask)
rescue Cask::CaskUnavailableError
nil
end
end
formula_downloaders =
cache_entries.call(HOMEBREW_CACHE)
.map(&load_formula)
.compact
.flat_map { |formula| formula_resources(formula) }
.map { |resource| [resource.downloader, resource.download_name, resource.version] }
cask_downloaders =
cache_entries.call(HOMEBREW_CACHE/"Cask")
.map(&load_cask)
.compact
.map { |cask| [Cask::Download.new(cask).downloader, cask.token, cask.version] }
downloaders = formula_downloaders + cask_downloaders
downloaders.each do |downloader, name, version|
next unless downloader.respond_to?(:symlink_location)
url = downloader.url
extname = parse_extname(url)
old_location = downloader.cache/"#{name}--#{version}#{extname}"
next unless old_location.file?
new_symlink_location = downloader.symlink_location
new_location = downloader.cached_location
if new_location.exist? && new_symlink_location.symlink?
begin
FileUtils.rm_rf old_location unless old_location == new_symlink_location
rescue Errno::EACCES
opoo "Could not remove #{old_location}, please do so manually."
end
else
begin
new_location.dirname.mkpath
if new_location.exist?
FileUtils.rm_rf old_location
else
FileUtils.mv old_location, new_location
end
symlink_target = new_location.relative_path_from(new_symlink_location.dirname)
new_symlink_location.dirname.mkpath
FileUtils.ln_s symlink_target, new_symlink_location, force: true
rescue Errno::EACCES
opoo "Could not move #{old_location} to #{new_location}, please do so manually."
end
end
end
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.
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
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
Failed to delete #{legacy_cache}.
Please do so manually.
EOS
end
end
end
def migrate_legacy_keg_symlinks_if_necessary
legacy_linked_kegs = HOMEBREW_LIBRARY/"LinkedKegs"
return unless legacy_linked_kegs.directory?
HOMEBREW_LINKED_KEGS.mkpath unless legacy_linked_kegs.children.empty?
legacy_linked_kegs.children.each do |link|
name = link.basename.to_s
src = begin
link.realpath
rescue Errno::ENOENT
begin
(HOMEBREW_PREFIX/"opt/#{name}").realpath
rescue Errno::ENOENT
begin
Formulary.factory(name).installed_prefix
rescue
next
end
end
end
dst = HOMEBREW_LINKED_KEGS/name
dst.unlink if dst.exist?
FileUtils.ln_sf(src.relative_path_from(dst.parent), dst)
end
FileUtils.rm_rf legacy_linked_kegs
legacy_pinned_kegs = HOMEBREW_LIBRARY/"PinnedKegs"
return unless legacy_pinned_kegs.directory?
HOMEBREW_PINNED_KEGS.mkpath unless legacy_pinned_kegs.children.empty?
legacy_pinned_kegs.children.each do |link|
name = link.basename.to_s
src = link.realpath
dst = HOMEBREW_PINNED_KEGS/name
FileUtils.ln_sf(src.relative_path_from(dst.parent), dst)
end
FileUtils.rm_rf legacy_pinned_kegs
end
def migrate_legacy_repository_if_necessary
return unless Homebrew.default_prefix?
return unless Homebrew.default_prefix?(HOMEBREW_REPOSITORY)
ohai "Migrating HOMEBREW_REPOSITORY (please wait)..."
unless HOMEBREW_PREFIX.writable_real?
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 "#{HOMEBREW_PREFIX}/Homebrew"
new_homebrew_repository.rmdir_if_possible
if new_homebrew_repository.exist?
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?
ofail <<~EOS
Could not remove old HOMEBREW_REPOSITORY paths!
Please do this manually with:
sudo rm -rf #{unremovable_paths.join " "}
EOS
end
Keg::MUST_EXIST_DIRECTORIES.each { |dir| FileUtils.mkdir_p dir }
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
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}!"
if HOMEBREW_PREFIX == "/usr/local"
puts <<~EOS
Homebrew no longer needs to have ownership of #{HOMEBREW_PREFIX}. If you wish you can
return #{HOMEBREW_PREFIX} to its default ownership with:
sudo chown root:wheel #{HOMEBREW_PREFIX}
EOS
end
rescue => e
ofail <<~EOS
#{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
end
end