mirror of
https://github.com/Homebrew/brew.git
synced 2025-07-14 16:09:03 +08:00

This is the pattern we've been adopting for a while and it's a bit cleaner. Let's remove all of the existing usage of the existing pattern to avoid confusion when adopting the new one.
367 lines
12 KiB
Ruby
367 lines
12 KiB
Ruby
# typed: true # rubocop:todo Sorbet/StrictSigil
|
|
# frozen_string_literal: true
|
|
|
|
class Keg
|
|
PREFIX_PLACEHOLDER = "@@HOMEBREW_PREFIX@@"
|
|
CELLAR_PLACEHOLDER = "@@HOMEBREW_CELLAR@@"
|
|
REPOSITORY_PLACEHOLDER = "@@HOMEBREW_REPOSITORY@@"
|
|
LIBRARY_PLACEHOLDER = "@@HOMEBREW_LIBRARY@@"
|
|
PERL_PLACEHOLDER = "@@HOMEBREW_PERL@@"
|
|
JAVA_PLACEHOLDER = "@@HOMEBREW_JAVA@@"
|
|
NULL_BYTE = "\x00"
|
|
NULL_BYTE_STRING = "\\x00"
|
|
|
|
class Relocation
|
|
RELOCATABLE_PATH_REGEX_PREFIX = /(?:(?<=-F|-I|-L|-isystem)|(?<![a-zA-Z0-9]))/
|
|
|
|
def initialize
|
|
@replacement_map = T.let({}, T::Hash[Symbol, [T.any(String, Regexp), String]])
|
|
end
|
|
|
|
def freeze
|
|
@replacement_map.freeze
|
|
super
|
|
end
|
|
|
|
sig { params(key: Symbol, old_value: T.any(String, Regexp), new_value: String, path: T::Boolean).void }
|
|
def add_replacement_pair(key, old_value, new_value, path: false)
|
|
old_value = self.class.path_to_regex(old_value) if path
|
|
@replacement_map[key] = [old_value, new_value]
|
|
end
|
|
|
|
sig { params(key: Symbol).returns([T.any(String, Regexp), String]) }
|
|
def replacement_pair_for(key)
|
|
@replacement_map.fetch(key)
|
|
end
|
|
|
|
sig { params(text: String).returns(T::Boolean) }
|
|
def replace_text(text)
|
|
replacements = @replacement_map.values.to_h
|
|
|
|
sorted_keys = replacements.keys.sort_by do |key|
|
|
key.is_a?(String) ? key.length : 999
|
|
end.reverse
|
|
|
|
any_changed = T.let(nil, T.nilable(String))
|
|
sorted_keys.each do |key|
|
|
changed = text.gsub!(key, replacements.fetch(key))
|
|
any_changed ||= changed
|
|
end
|
|
!any_changed.nil?
|
|
end
|
|
|
|
sig { params(path: T.any(String, Regexp)).returns(Regexp) }
|
|
def self.path_to_regex(path)
|
|
path = case path
|
|
when String
|
|
Regexp.escape(path)
|
|
when Regexp
|
|
path.source
|
|
end
|
|
Regexp.new(RELOCATABLE_PATH_REGEX_PREFIX.source + path)
|
|
end
|
|
end
|
|
|
|
def fix_dynamic_linkage
|
|
symlink_files.each do |file|
|
|
link = file.readlink
|
|
# Don't fix relative symlinks
|
|
next unless link.absolute?
|
|
|
|
link_starts_cellar = link.to_s.start_with?(HOMEBREW_CELLAR.to_s)
|
|
link_starts_prefix = link.to_s.start_with?(HOMEBREW_PREFIX.to_s)
|
|
next if !link_starts_cellar && !link_starts_prefix
|
|
|
|
new_src = link.relative_path_from(file.parent)
|
|
file.unlink
|
|
FileUtils.ln_s(new_src, file)
|
|
end
|
|
end
|
|
|
|
def relocate_dynamic_linkage(_relocation)
|
|
[]
|
|
end
|
|
|
|
JAVA_REGEX = %r{#{HOMEBREW_PREFIX}/opt/openjdk(@\d+(\.\d+)*)?/libexec(/openjdk\.jdk/Contents/Home)?}
|
|
|
|
def prepare_relocation_to_placeholders
|
|
relocation = Relocation.new
|
|
relocation.add_replacement_pair(:prefix, HOMEBREW_PREFIX.to_s, PREFIX_PLACEHOLDER, path: true)
|
|
relocation.add_replacement_pair(:cellar, HOMEBREW_CELLAR.to_s, CELLAR_PLACEHOLDER, path: true)
|
|
# when HOMEBREW_PREFIX == HOMEBREW_REPOSITORY we should use HOMEBREW_PREFIX for all relocations to avoid
|
|
# being unable to differentiate between them.
|
|
if HOMEBREW_PREFIX != HOMEBREW_REPOSITORY
|
|
relocation.add_replacement_pair(:repository, HOMEBREW_REPOSITORY.to_s, REPOSITORY_PLACEHOLDER, path: true)
|
|
end
|
|
relocation.add_replacement_pair(:library, HOMEBREW_LIBRARY.to_s, LIBRARY_PLACEHOLDER, path: true)
|
|
relocation.add_replacement_pair(:perl,
|
|
%r{\A#![ \t]*(?:/usr/bin/perl\d\.\d+|#{HOMEBREW_PREFIX}/opt/perl/bin/perl)( |$)}o,
|
|
"#!#{PERL_PLACEHOLDER}\\1")
|
|
relocation.add_replacement_pair(:java, JAVA_REGEX, JAVA_PLACEHOLDER)
|
|
|
|
relocation
|
|
end
|
|
|
|
def replace_locations_with_placeholders
|
|
relocation = prepare_relocation_to_placeholders.freeze
|
|
relocate_dynamic_linkage(relocation)
|
|
replace_text_in_files(relocation)
|
|
end
|
|
|
|
def prepare_relocation_to_locations
|
|
relocation = Relocation.new
|
|
relocation.add_replacement_pair(:prefix, PREFIX_PLACEHOLDER, HOMEBREW_PREFIX.to_s)
|
|
relocation.add_replacement_pair(:cellar, CELLAR_PLACEHOLDER, HOMEBREW_CELLAR.to_s)
|
|
relocation.add_replacement_pair(:repository, REPOSITORY_PLACEHOLDER, HOMEBREW_REPOSITORY.to_s)
|
|
relocation.add_replacement_pair(:library, LIBRARY_PLACEHOLDER, HOMEBREW_LIBRARY.to_s)
|
|
relocation.add_replacement_pair(:perl, PERL_PLACEHOLDER, "#{HOMEBREW_PREFIX}/opt/perl/bin/perl")
|
|
if (openjdk = openjdk_dep_name_if_applicable)
|
|
relocation.add_replacement_pair(:java, JAVA_PLACEHOLDER, "#{HOMEBREW_PREFIX}/opt/#{openjdk}/libexec")
|
|
end
|
|
|
|
relocation
|
|
end
|
|
|
|
def replace_placeholders_with_locations(files, skip_linkage: false)
|
|
relocation = prepare_relocation_to_locations.freeze
|
|
relocate_dynamic_linkage(relocation) unless skip_linkage
|
|
replace_text_in_files(relocation, files:)
|
|
end
|
|
|
|
def openjdk_dep_name_if_applicable
|
|
deps = runtime_dependencies
|
|
return if deps.blank?
|
|
|
|
dep_names = deps.map { |d| d["full_name"] }
|
|
dep_names.find { |d| d.match? Version.formula_optionally_versioned_regex(:openjdk) }
|
|
end
|
|
|
|
def replace_text_in_files(relocation, files: nil)
|
|
files ||= text_files | libtool_files
|
|
|
|
changed_files = T.let([], Array)
|
|
files.map { path.join(_1) }.group_by { |f| f.stat.ino }.each_value do |first, *rest|
|
|
s = first.open("rb", &:read)
|
|
|
|
next unless relocation.replace_text(s)
|
|
|
|
changed_files += [first, *rest].map { |file| file.relative_path_from(path) }
|
|
|
|
begin
|
|
first.atomic_write(s)
|
|
rescue SystemCallError
|
|
first.ensure_writable do
|
|
first.open("wb") { |f| f.write(s) }
|
|
end
|
|
else
|
|
rest.each { |file| FileUtils.ln(first, file, force: true) }
|
|
end
|
|
end
|
|
changed_files
|
|
end
|
|
|
|
def relocate_build_prefix(keg, old_prefix, new_prefix)
|
|
each_unique_file_matching(old_prefix) do |file|
|
|
# Skip files which are not binary, as they do not need null padding.
|
|
next unless keg.binary_file?(file)
|
|
|
|
# Skip sharballs, which appear to break if patched.
|
|
next if file.text_executable?
|
|
|
|
# Split binary by null characters into array and substitute new prefix for old prefix.
|
|
# Null padding is added if the new string is too short.
|
|
file.ensure_writable do
|
|
binary = File.binread file
|
|
odebug "Replacing build prefix in: #{file}"
|
|
binary_strings = binary.split(/#{NULL_BYTE}/o, -1)
|
|
match_indices = binary_strings.each_index.select { |i| binary_strings.fetch(i).include?(old_prefix) }
|
|
|
|
# Only perform substitution on strings which match prefix regex.
|
|
match_indices.each do |i|
|
|
s = binary_strings.fetch(i)
|
|
binary_strings[i] = s.gsub(old_prefix, new_prefix)
|
|
.ljust(s.size, NULL_BYTE)
|
|
end
|
|
|
|
# Rejoin strings by null bytes.
|
|
patched_binary = binary_strings.join(NULL_BYTE)
|
|
if patched_binary.size != binary.size
|
|
raise <<~EOS
|
|
Patching failed! Original and patched binary sizes do not match.
|
|
Original size: #{binary.size}
|
|
Patched size: #{patched_binary.size}
|
|
EOS
|
|
end
|
|
|
|
file.atomic_write patched_binary
|
|
end
|
|
codesign_patched_binary(file)
|
|
end
|
|
end
|
|
|
|
def detect_cxx_stdlibs(_options = {})
|
|
[]
|
|
end
|
|
|
|
def recursive_fgrep_args
|
|
# for GNU grep; overridden for BSD grep on OS X
|
|
"-lr"
|
|
end
|
|
|
|
def egrep_args
|
|
grep_bin = "grep"
|
|
grep_args = [
|
|
"--files-with-matches",
|
|
"--perl-regexp",
|
|
"--binary-files=text",
|
|
]
|
|
|
|
[grep_bin, grep_args]
|
|
end
|
|
|
|
def each_unique_file_matching(string)
|
|
Utils.popen_read("fgrep", recursive_fgrep_args, string, to_s) do |io|
|
|
hardlinks = Set.new
|
|
|
|
until io.eof?
|
|
file = Pathname.new(io.readline.chomp)
|
|
# Don't return symbolic links.
|
|
next if file.symlink?
|
|
|
|
# To avoid returning hardlinks, only return files with unique inodes.
|
|
# Hardlinks will have the same inode as the file they point to.
|
|
yield file if hardlinks.add? file.stat.ino
|
|
end
|
|
end
|
|
end
|
|
|
|
def binary_file?(file)
|
|
grep_bin, grep_args = egrep_args
|
|
|
|
# We need to pass NULL_BYTE_STRING, the literal string "\x00", to grep
|
|
# rather than NULL_BYTE, a literal null byte, because grep will internally
|
|
# convert the literal string "\x00" to a null byte.
|
|
Utils.popen_read(grep_bin, *grep_args, NULL_BYTE_STRING, file).present?
|
|
end
|
|
|
|
def lib
|
|
path/"lib"
|
|
end
|
|
|
|
def libexec
|
|
path/"libexec"
|
|
end
|
|
|
|
def text_files
|
|
text_files = []
|
|
return text_files if !which("file") || !which("xargs")
|
|
|
|
# file has known issues with reading files on other locales. Has
|
|
# been fixed upstream for some time, but a sufficiently new enough
|
|
# file with that fix is only available in macOS Sierra.
|
|
# https://bugs.gw.com/view.php?id=292
|
|
with_custom_locale("C") do
|
|
files = Set.new path.find.reject { |pn|
|
|
next true if pn.symlink?
|
|
next true if pn.directory?
|
|
next false if pn.basename.to_s == "orig-prefix.txt" # for python virtualenvs
|
|
next true if pn == self/".brew/#{name}.rb"
|
|
|
|
require "metafiles"
|
|
next true if Metafiles::EXTENSIONS.include?(pn.extname)
|
|
|
|
if pn.text_executable?
|
|
text_files << pn
|
|
next true
|
|
end
|
|
false
|
|
}
|
|
output, _status = Open3.capture2("xargs -0 file --no-dereference --print0",
|
|
stdin_data: files.to_a.join("\0"))
|
|
# `file` output sometimes contains data from the file, which may include
|
|
# invalid UTF-8 entities, so tell Ruby this is just a bytestring
|
|
output.force_encoding(Encoding::ASCII_8BIT)
|
|
output.each_line do |line|
|
|
path, info = line.split("\0", 2)
|
|
# `file` sometimes prints more than one line of output per file;
|
|
# subsequent lines do not contain a null-byte separator, so `info`
|
|
# will be `nil` for those lines
|
|
next unless info
|
|
next unless info.include?("text")
|
|
|
|
path = Pathname.new(path)
|
|
next unless files.include?(path)
|
|
|
|
text_files << path
|
|
end
|
|
end
|
|
|
|
text_files
|
|
end
|
|
|
|
def libtool_files
|
|
libtool_files = []
|
|
|
|
path.find do |pn|
|
|
next if pn.symlink? || pn.directory? || Keg::LIBTOOL_EXTENSIONS.exclude?(pn.extname)
|
|
|
|
libtool_files << pn
|
|
end
|
|
libtool_files
|
|
end
|
|
|
|
def symlink_files
|
|
symlink_files = []
|
|
path.find do |pn|
|
|
symlink_files << pn if pn.symlink?
|
|
end
|
|
|
|
symlink_files
|
|
end
|
|
|
|
def self.text_matches_in_file(file, string, ignores, linked_libraries, formula_and_runtime_deps_names)
|
|
text_matches = []
|
|
path_regex = Relocation.path_to_regex(string)
|
|
Utils.popen_read("strings", "-t", "x", "-", file.to_s) do |io|
|
|
until io.eof?
|
|
str = io.readline.chomp
|
|
next if ignores.any? { |i| str.match?(i) }
|
|
next unless str.match? path_regex
|
|
|
|
offset, match = str.split(" ", 2)
|
|
|
|
# Some binaries contain strings with lists of files
|
|
# e.g. `/usr/local/lib/foo:/usr/local/share/foo:/usr/lib/foo`
|
|
# Each item in the list should be checked separately
|
|
match.split(":").each do |sub_match|
|
|
# Not all items in the list may be matches
|
|
next unless sub_match.match? path_regex
|
|
next if linked_libraries.include? sub_match # Don't bother reporting a string if it was found by otool
|
|
|
|
# Do not report matches to files that do not exist.
|
|
next unless File.exist? sub_match
|
|
|
|
# Do not report matches to build dependencies.
|
|
if formula_and_runtime_deps_names.present?
|
|
begin
|
|
keg_name = Keg.for(Pathname.new(sub_match)).name
|
|
next unless formula_and_runtime_deps_names.include? keg_name
|
|
rescue NotAKegError
|
|
nil
|
|
end
|
|
end
|
|
|
|
text_matches << [match, offset] unless text_matches.any? { |text| text.last == offset }
|
|
end
|
|
end
|
|
end
|
|
text_matches
|
|
end
|
|
|
|
def self.file_linked_libraries(_file, _string)
|
|
[]
|
|
end
|
|
end
|
|
|
|
require "extend/os/keg_relocate"
|