Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use versioned Perl shebangs on macOS and support replacing shebangs during relocation #11286

Merged
merged 8 commits into from
May 10, 2021
20 changes: 20 additions & 0 deletions Library/Homebrew/cleaner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ def clean
info_dir_file = @f.info/"dir"
observe_file_removal info_dir_file if info_dir_file.file? && [email protected]_clean?(info_dir_file)

rewrite_shebangs

prune
end

Expand Down Expand Up @@ -118,6 +120,24 @@ def clean_dir(d)
end
end
end

def rewrite_shebangs
require "language/perl"
require "utils/shebang"

basepath = @f.prefix.realpath
basepath.find do |path|
Find.prune if @f.skip_clean? path

next if path.directory? || path.symlink?

begin
Utils::Shebang.rewrite_shebang Language::Perl::Shebang.detected_perl_shebang(@f), path
rescue ShebangDetectionError
break
end
end
end
end

require "extend/os/cleaner"
7 changes: 7 additions & 0 deletions Library/Homebrew/exceptions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -729,3 +729,10 @@ def initialize(version)
super "unknown or unsupported macOS version: #{version.inspect}"
end
end

# Raised when `detected_perl_shebang` etc cannot detect the shebang.
class ShebangDetectionError < RuntimeError
def initialize(type, reason)
super "Cannot detect #{type} shebang: #{reason}."
end
end
1 change: 1 addition & 0 deletions Library/Homebrew/extend/ENV/shared.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def setup_build_environment(formula: nil, cc: nil, build_bottle: false, bottle_a
reset
end
private :setup_build_environment
alias generic_shared_setup_build_environment setup_build_environment

sig { void }
def reset
Expand Down
4 changes: 3 additions & 1 deletion Library/Homebrew/extend/os/linux/keg_relocate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ def relocate_dynamic_linkage(relocation)
# Patching patchelf using itself fails with "Text file busy" or SIGBUS.
return if name == "patchelf"

old_prefix, new_prefix = relocation.replacement_pair_for(:prefix)

elf_files.each do |file|
file.ensure_writable do
change_rpath(file, relocation.old_prefix, relocation.new_prefix)
change_rpath(file, old_prefix, new_prefix)
end
end
end
Expand Down
5 changes: 3 additions & 2 deletions Library/Homebrew/extend/os/mac/development_tools.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,9 @@ def custom_installation_instructions

def build_system_info
build_info = {
"xcode" => MacOS::Xcode.version.to_s.presence,
"clt" => MacOS::CLT.version.to_s.presence,
"xcode" => MacOS::Xcode.version.to_s.presence,
"clt" => MacOS::CLT.version.to_s.presence,
"preferred_perl" => MacOS.preferred_perl_version,
}
generic_build_system_info.merge build_info
end
Expand Down
10 changes: 10 additions & 0 deletions Library/Homebrew/extend/os/mac/extend/ENV/shared.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@
module SharedEnvExtension
extend T::Sig

def setup_build_environment(formula: nil, cc: nil, build_bottle: false, bottle_arch: nil, testing_formula: false)
generic_shared_setup_build_environment(
formula: formula, cc: cc, build_bottle: build_bottle,
bottle_arch: bottle_arch, testing_formula: testing_formula
)

# Normalise the system Perl version used, where multiple may be available
self["VERSIONER_PERL_VERSION"] = MacOS.preferred_perl_version
end

sig { returns(T::Boolean) }
def no_weak_imports_support?
return false unless compiler == :clang
Expand Down
40 changes: 31 additions & 9 deletions Library/Homebrew/extend/os/mac/keg_relocate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,29 +19,32 @@ def file_linked_libraries(file, string)
undef relocate_dynamic_linkage

def relocate_dynamic_linkage(relocation)
old_prefix, new_prefix = relocation.replacement_pair_for(:prefix)
old_cellar, new_cellar = relocation.replacement_pair_for(:cellar)

mach_o_files.each do |file|
file.ensure_writable do
if file.dylib?
id = dylib_id_for(file).sub(relocation.old_prefix, relocation.new_prefix)
id = dylib_id_for(file).sub(old_prefix, new_prefix)
change_dylib_id(id, file)
end

each_install_name_for(file) do |old_name|
if old_name.start_with? relocation.old_cellar
new_name = old_name.sub(relocation.old_cellar, relocation.new_cellar)
elsif old_name.start_with? relocation.old_prefix
new_name = old_name.sub(relocation.old_prefix, relocation.new_prefix)
if old_name.start_with? old_cellar
new_name = old_name.sub(old_cellar, new_cellar)
elsif old_name.start_with? old_prefix
new_name = old_name.sub(old_prefix, new_prefix)
end

change_install_name(old_name, new_name, file) if new_name
end

if ENV["HOMEBREW_RELOCATE_RPATHS"]
each_rpath_for(file) do |old_name|
new_name = if old_name.start_with? relocation.old_cellar
old_name.sub(relocation.old_cellar, relocation.new_cellar)
elsif old_name.start_with? relocation.old_prefix
old_name.sub(relocation.old_prefix, relocation.new_prefix)
new_name = if old_name.start_with? old_cellar
old_name.sub(old_cellar, new_cellar)
elsif old_name.start_with? old_prefix
old_name.sub(old_prefix, new_prefix)
end

change_rpath(old_name, new_name, file) if new_name
Expand Down Expand Up @@ -172,6 +175,25 @@ def mach_o_files
mach_o_files
end

def prepare_relocation_to_locations
relocation = generic_prepare_relocation_to_locations

brewed_perl = runtime_dependencies&.any? { |dep| dep["full_name"] == "perl" && dep["declared_directly"] }
perl_path = if brewed_perl
"#{HOMEBREW_PREFIX}/opt/perl/bin/perl"
else
perl_version = if tab["built_on"].present?
tab["built_on"]["preferred_perl"]
else
MacOS.preferred_perl_version
end
"/usr/bin/perl#{perl_version}"
end
relocation.add_replacement_pair(:perl, PERL_PLACEHOLDER, perl_path)

relocation
end

def recursive_fgrep_args
# Don't recurse into symlinks; the man page says this is the default, but
# it's wrong. -O is a BSD-grep-only option.
Expand Down
2 changes: 1 addition & 1 deletion Library/Homebrew/formula_installer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -805,7 +805,7 @@ def finish
tab = Tab.for_keg(keg)
Tab.clear_cache
f_runtime_deps = formula.runtime_dependencies(read_from_tab: false)
tab.runtime_dependencies = Tab.runtime_deps_hash(f_runtime_deps)
tab.runtime_dependencies = Tab.runtime_deps_hash(formula, f_runtime_deps)
tab.write

# let's reset Utils::Git.available? if we just installed git
Expand Down
103 changes: 68 additions & 35 deletions Library/Homebrew/keg_relocate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,44 @@ class Keg
CELLAR_PLACEHOLDER = "@@HOMEBREW_CELLAR@@"
REPOSITORY_PLACEHOLDER = "@@HOMEBREW_REPOSITORY@@"
LIBRARY_PLACEHOLDER = "@@HOMEBREW_LIBRARY@@"
PERL_PLACEHOLDER = "@@HOMEBREW_PERL@@"

Relocation = Struct.new(:old_prefix, :old_cellar, :old_repository, :old_library,
:new_prefix, :new_cellar, :new_repository, :new_library) do
# Use keyword args instead of positional args for initialization.
def initialize(**kwargs)
super(*members.map { |k| kwargs[k] })
class Relocation
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth having any basic unit tests for this class?

extend T::Sig

def initialize
@replacement_map = {}
end

def freeze
@replacement_map.freeze
super
end

sig { params(key: Symbol, old_value: T.any(String, Regexp), new_value: String).void }
def add_replacement_pair(key, old_value, new_value)
@replacement_map[key] = [old_value, new_value]
end

sig { params(key: Symbol).returns(T::Array[T.any(String, Regexp)]) }
def replacement_pair_for(key)
@replacement_map.fetch(key)
end

sig { params(text: String).void }
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 = false
sorted_keys.each do |key|
changed = text.gsub!(key, replacements[key])
any_changed ||= changed
end
any_changed
end
end

Expand All @@ -36,32 +68,42 @@ def relocate_dynamic_linkage(_relocation)
[]
end

def prepare_relocation_to_placeholders
relocation = Relocation.new
relocation.add_replacement_pair(:prefix, HOMEBREW_PREFIX.to_s, PREFIX_PLACEHOLDER)
relocation.add_replacement_pair(:cellar, HOMEBREW_CELLAR.to_s, CELLAR_PLACEHOLDER)
# 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)
end
relocation.add_replacement_pair(:library, HOMEBREW_LIBRARY.to_s, LIBRARY_PLACEHOLDER)
relocation.add_replacement_pair(:perl,
%r{\A#!(?:/usr/bin/perl\d\.\d+|#{HOMEBREW_PREFIX}/opt/perl/bin/perl)( |$)}o,
"#!#{PERL_PLACEHOLDER}\\1")
relocation
end
alias generic_prepare_relocation_to_placeholders prepare_relocation_to_placeholders

def replace_locations_with_placeholders
relocation = Relocation.new(
old_prefix: HOMEBREW_PREFIX.to_s,
old_cellar: HOMEBREW_CELLAR.to_s,
new_prefix: PREFIX_PLACEHOLDER,
new_cellar: CELLAR_PLACEHOLDER,
old_repository: HOMEBREW_REPOSITORY.to_s,
new_repository: REPOSITORY_PLACEHOLDER,
old_library: HOMEBREW_LIBRARY.to_s,
new_library: LIBRARY_PLACEHOLDER,
)
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")
relocation
end
alias generic_prepare_relocation_to_locations prepare_relocation_to_locations

def replace_placeholders_with_locations(files, skip_linkage: false)
relocation = Relocation.new(
old_prefix: PREFIX_PLACEHOLDER,
old_cellar: CELLAR_PLACEHOLDER,
old_repository: REPOSITORY_PLACEHOLDER,
old_library: LIBRARY_PLACEHOLDER,
new_prefix: HOMEBREW_PREFIX.to_s,
new_cellar: HOMEBREW_CELLAR.to_s,
new_repository: HOMEBREW_REPOSITORY.to_s,
new_library: HOMEBREW_LIBRARY.to_s,
)
relocation = prepare_relocation_to_locations.freeze
relocate_dynamic_linkage(relocation) unless skip_linkage
replace_text_in_files(relocation, files: files)
end
Expand All @@ -73,16 +115,7 @@ def replace_text_in_files(relocation, files: nil)
files.map(&path.method(:join)).group_by { |f| f.stat.ino }.each_value do |first, *rest|
s = first.open("rb", &:read)

replacements = {
relocation.old_prefix => relocation.new_prefix,
relocation.old_cellar => relocation.new_cellar,
relocation.old_library => relocation.new_library,
}.compact
# when HOMEBREW_PREFIX == HOMEBREW_REPOSITORY we should use HOMEBREW_PREFIX for all relocations to avoid
# being unable to differentiate between them.
replacements[relocation.old_repository] = relocation.new_repository if HOMEBREW_PREFIX != HOMEBREW_REPOSITORY
changed = s.gsub!(Regexp.union(replacements.keys.sort_by(&:length).reverse), replacements)
next unless changed
next unless relocation.replace_text(s)

changed_files += [first, *rest].map { |file| file.relative_path_from(path) }

Expand Down
10 changes: 5 additions & 5 deletions Library/Homebrew/language/perl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,17 @@ module Shebang

def detected_perl_shebang(formula = self)
perl_path = if formula.uses_from_macos_elements&.include? "perl"
"/usr/bin/perl"
"/usr/bin/perl#{MacOS.preferred_perl_version}"
elsif formula.deps.map(&:name).include? "perl"
Formula["perl"].opt_bin/"perl"
else
raise "Cannot detect Perl shebang: formula does not depend on Perl."
raise ShebangDetectionError.new("Perl", "formula does not depend on Perl")
end

Utils::Shebang::RewriteInfo.new(
%r{^#! ?/usr/bin/(env )?perl$},
20, # the length of "#! /usr/bin/env perl"
perl_path,
%r{^#! ?/usr/bin/(?:env )?perl( |$)},
21, # the length of "#! /usr/bin/env perl "
"#{perl_path}\\1",
)
end
end
Expand Down
12 changes: 7 additions & 5 deletions Library/Homebrew/language/python.rb
Original file line number Diff line number Diff line change
Expand Up @@ -96,17 +96,19 @@ module Shebang
# @private
def python_shebang_rewrite_info(python_path)
Utils::Shebang::RewriteInfo.new(
%r{^#! ?/usr/bin/(env )?python([23](\.\d{1,2})?)?$},
28, # the length of "#! /usr/bin/env pythonx.yyy$"
python_path,
%r{^#! ?/usr/bin/(?:env )?python(?:[23](?:\.\d{1,2})?)?( |$)},
28, # the length of "#! /usr/bin/env pythonx.yyy "
"#{python_path}\\1",
)
end

def detected_python_shebang(formula = self)
python_deps = formula.deps.map(&:name).grep(/^python(@.*)?$/)

raise "Cannot detect Python shebang: formula does not depend on Python." if python_deps.empty?
raise "Cannot detect Python shebang: formula has multiple Python dependencies." if python_deps.length > 1
raise ShebangDetectionError.new("Python", "formula does not depend on Python") if python_deps.empty?
if python_deps.length > 1
raise ShebangDetectionError.new("Python", "formula has multiple Python dependencies")
end

python_shebang_rewrite_info(Formula[python_deps.first].opt_bin/"python3")
end
Expand Down
11 changes: 11 additions & 0 deletions Library/Homebrew/os/mac.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,17 @@ def prerelease?
version >= "12"
end

sig { returns(String) }
def preferred_perl_version
if version >= :big_sur
"5.30"
elsif version == :catalina
"5.28"
else
"5.18"
end
end

def languages
return @languages if @languages

Expand Down
10 changes: 7 additions & 3 deletions Library/Homebrew/tab.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def self.create(formula, compiler, stdlib)
"compiler" => compiler,
"stdlib" => stdlib,
"aliases" => formula.aliases,
"runtime_dependencies" => Tab.runtime_deps_hash(runtime_deps),
"runtime_dependencies" => Tab.runtime_deps_hash(formula, runtime_deps),
"arch" => Hardware::CPU.arch,
"source" => {
"path" => formula.specified_path.to_s,
Expand Down Expand Up @@ -210,10 +210,14 @@ def self.empty
new(attributes)
end

def self.runtime_deps_hash(deps)
def self.runtime_deps_hash(formula, deps)
deps.map do |dep|
f = dep.to_formula
{ "full_name" => f.full_name, "version" => f.version.to_s }
{
"full_name" => f.full_name,
"version" => f.version.to_s,
"declared_directly" => formula.deps.include?(dep),
}
end
end

Expand Down
Loading