Merge pull request #18378 from Homebrew/no-undefs

Move remaining `undef` use in OS extensions to `prepend`
This commit is contained in:
Douglas Eichelberger 2024-10-05 12:54:02 -07:00 committed by GitHub
commit 3c51d0c8d3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 1219 additions and 1162 deletions

View File

@ -177,7 +177,6 @@ class DevelopmentTools
"cpu_family" => Hardware::CPU.family.to_s,
}
end
alias generic_build_system_info build_system_info
end
end

View File

@ -1,101 +1,103 @@
# typed: true # rubocop:disable Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
require "os/linux/glibc"
class DependencyCollector
undef gcc_dep_if_needed
undef glibc_dep_if_needed
undef init_global_dep_tree_if_needed!
module OS
module Linux
module DependencyCollector
sig { params(related_formula_names: T::Set[String]).returns(T.nilable(Dependency)) }
def gcc_dep_if_needed(related_formula_names)
# gcc is required for libgcc_s.so.1 if glibc or gcc are too old
return unless ::DevelopmentTools.needs_build_formulae?
return if building_global_dep_tree?
return if related_formula_names.include?(GCC)
return if global_dep_tree[GCC]&.intersect?(related_formula_names)
return unless formula_for(GCC)
sig { params(related_formula_names: T::Set[String]).returns(T.nilable(Dependency)) }
def gcc_dep_if_needed(related_formula_names)
# gcc is required for libgcc_s.so.1 if glibc or gcc are too old
return unless DevelopmentTools.needs_build_formulae?
return if building_global_dep_tree?
return if related_formula_names.include?(GCC)
return if global_dep_tree[GCC]&.intersect?(related_formula_names)
return unless formula_for(GCC)
Dependency.new(GCC, [:implicit])
end
Dependency.new(GCC, [:implicit])
end
sig { params(related_formula_names: T::Set[String]).returns(T.nilable(Dependency)) }
def glibc_dep_if_needed(related_formula_names)
return unless ::DevelopmentTools.needs_libc_formula?
return if building_global_dep_tree?
return if related_formula_names.include?(GLIBC)
return if global_dep_tree[GLIBC]&.intersect?(related_formula_names)
return unless formula_for(GLIBC)
sig { params(related_formula_names: T::Set[String]).returns(T.nilable(Dependency)) }
def glibc_dep_if_needed(related_formula_names)
return unless DevelopmentTools.needs_libc_formula?
return if building_global_dep_tree?
return if related_formula_names.include?(GLIBC)
return if global_dep_tree[GLIBC]&.intersect?(related_formula_names)
return unless formula_for(GLIBC)
Dependency.new(GLIBC, [:implicit])
end
Dependency.new(GLIBC, [:implicit])
end
private
private
GLIBC = "glibc"
GCC = OS::LINUX_PREFERRED_GCC_RUNTIME_FORMULA
GLIBC = "glibc"
GCC = OS::LINUX_PREFERRED_GCC_RUNTIME_FORMULA
sig { void }
def init_global_dep_tree_if_needed!
return unless ::DevelopmentTools.needs_build_formulae?
return if building_global_dep_tree?
return unless global_dep_tree.empty?
sig { void }
def init_global_dep_tree_if_needed!
return unless DevelopmentTools.needs_build_formulae?
return if building_global_dep_tree?
return unless global_dep_tree.empty?
building_global_dep_tree!
global_dep_tree[GLIBC] = Set.new(global_deps_for(GLIBC))
# gcc depends on glibc
global_dep_tree[GCC] = Set.new([*global_deps_for(GCC), GLIBC, *@@global_dep_tree[GLIBC]])
built_global_dep_tree!
end
building_global_dep_tree!
global_dep_tree[GLIBC] = Set.new(global_deps_for(GLIBC))
# gcc depends on glibc
global_dep_tree[GCC] = Set.new([*global_deps_for(GCC), GLIBC, *@@global_dep_tree[GLIBC]])
built_global_dep_tree!
end
sig { params(name: String).returns(T.nilable(Formula)) }
def formula_for(name)
@formula_for ||= T.let({}, T.nilable(T::Hash[String, Formula]))
@formula_for[name] ||= ::Formula[name]
rescue FormulaUnavailableError
nil
end
sig { params(name: String).returns(T.nilable(Formula)) }
def formula_for(name)
@formula_for ||= {}
@formula_for[name] ||= Formula[name]
rescue FormulaUnavailableError
nil
end
sig { params(name: String).returns(T::Array[String]) }
def global_deps_for(name)
@global_deps_for ||= T.let({}, T.nilable(T::Hash[String, T::Array[String]]))
# Always strip out glibc and gcc from all parts of dependency tree when
# we're calculating their dependency trees. Other parts of Homebrew will
# catch any circular dependencies.
@global_deps_for[name] ||= if (formula = formula_for(name))
formula.deps.map(&:name).flat_map do |dep|
[dep, *global_deps_for(dep)].compact
end.uniq
else
[]
end
end
sig { params(name: String).returns(T::Array[String]) }
def global_deps_for(name)
@global_deps_for ||= {}
# Always strip out glibc and gcc from all parts of dependency tree when
# we're calculating their dependency trees. Other parts of Homebrew will
# catch any circular dependencies.
@global_deps_for[name] ||= if (formula = formula_for(name))
formula.deps.map(&:name).flat_map do |dep|
[dep, *global_deps_for(dep)].compact
end.uniq
else
[]
# Use class variables to avoid this expensive logic needing to be done more
# than once.
# rubocop:disable Style/ClassVars
@@global_dep_tree = T.let({}, T::Hash[String, T::Set[String]])
@@building_global_dep_tree = T.let(false, T::Boolean)
sig { returns(T::Hash[String, T::Set[String]]) }
def global_dep_tree
@@global_dep_tree
end
sig { void }
def building_global_dep_tree!
@@building_global_dep_tree = true
end
sig { void }
def built_global_dep_tree!
@@building_global_dep_tree = false
end
sig { returns(T::Boolean) }
def building_global_dep_tree?
@@building_global_dep_tree.present?
end
# rubocop:enable Style/ClassVars
end
end
# Use class variables to avoid this expensive logic needing to be done more
# than once.
# rubocop:disable Style/ClassVars
@@global_dep_tree = {}
@@building_global_dep_tree = false
sig { returns(T::Hash[String, T::Set[String]]) }
def global_dep_tree
@@global_dep_tree
end
sig { void }
def building_global_dep_tree!
@@building_global_dep_tree = true
end
sig { void }
def built_global_dep_tree!
@@building_global_dep_tree = false
end
sig { returns(T::Boolean) }
def building_global_dep_tree?
@@building_global_dep_tree.present?
end
# rubocop:enable Style/ClassVars
end
DependencyCollector.prepend(OS::Linux::DependencyCollector)

View File

@ -1,54 +1,64 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
class DevelopmentTools
class << self
sig { params(tool: T.any(String, Symbol)).returns(T.nilable(Pathname)) }
def locate(tool)
(@locate ||= {}).fetch(tool) do |key|
@locate[key] = if needs_build_formulae? &&
(binutils_path = HOMEBREW_PREFIX/"opt/binutils/bin/#{tool}").executable?
binutils_path
elsif needs_build_formulae? && (glibc_path = HOMEBREW_PREFIX/"opt/glibc/bin/#{tool}").executable?
glibc_path
elsif (homebrew_path = HOMEBREW_PREFIX/"bin/#{tool}").executable?
homebrew_path
elsif File.executable?((system_path = "/usr/bin/#{tool}"))
Pathname.new system_path
module OS
module Linux
module DevelopmentTools
extend T::Helpers
requires_ancestor { ::DevelopmentTools }
sig { params(tool: T.any(String, Symbol)).returns(T.nilable(Pathname)) }
def locate(tool)
@locate ||= T.let({}, T.nilable(T::Hash[T.any(String, Symbol), Pathname]))
@locate.fetch(tool) do |key|
@locate[key] = if ::DevelopmentTools.needs_build_formulae? &&
(binutils_path = HOMEBREW_PREFIX/"opt/binutils/bin/#{tool}").executable?
binutils_path
elsif ::DevelopmentTools.needs_build_formulae? &&
(glibc_path = HOMEBREW_PREFIX/"opt/glibc/bin/#{tool}").executable?
glibc_path
elsif (homebrew_path = HOMEBREW_PREFIX/"bin/#{tool}").executable?
homebrew_path
elsif File.executable?((system_path = "/usr/bin/#{tool}"))
Pathname.new system_path
end
end
end
end
sig { returns(Symbol) }
def default_compiler
:gcc
end
sig { returns(Symbol) }
def default_compiler = :gcc
sig { returns(T::Boolean) }
def needs_libc_formula?
return @needs_libc_formula if defined? @needs_libc_formula
sig { returns(T::Boolean) }
def needs_libc_formula?
return @needs_libc_formula unless @needs_libc_formula.nil?
@needs_libc_formula = OS::Linux::Glibc.below_ci_version?
end
sig { returns(T::Boolean) }
def needs_compiler_formula?
return @needs_compiler_formula if defined? @needs_compiler_formula
gcc = "/usr/bin/gcc"
@needs_compiler_formula = if File.exist?(gcc)
gcc_version(gcc) < OS::LINUX_GCC_CI_VERSION
else
true
@needs_libc_formula = T.let(OS::Linux::Glibc.below_ci_version?, T.nilable(T::Boolean))
@needs_libc_formula = !!@needs_libc_formula
end
end
sig { returns(T::Hash[String, T.nilable(String)]) }
def build_system_info
generic_build_system_info.merge({
"glibc_version" => OS::Linux::Glibc.version.to_s.presence,
"oldest_cpu_family" => Hardware.oldest_cpu.to_s,
})
sig { returns(T::Boolean) }
def needs_compiler_formula?
return @needs_compiler_formula unless @needs_compiler_formula.nil?
gcc = "/usr/bin/gcc"
@needs_compiler_formula = T.let(if File.exist?(gcc)
::DevelopmentTools.gcc_version(gcc) < OS::LINUX_GCC_CI_VERSION
else
true
end, T.nilable(T::Boolean))
!!@needs_compiler_formula
end
sig { returns(T::Hash[String, T.nilable(String)]) }
def build_system_info
super.merge({
"glibc_version" => OS::Linux::Glibc.version.to_s.presence,
"oldest_cpu_family" => Hardware.oldest_cpu.to_s,
})
end
end
end
end
DevelopmentTools.singleton_class.prepend(OS::Linux::DevelopmentTools)

View File

@ -7,168 +7,174 @@ require "hardware"
require "os/linux/glibc"
require "os/linux/kernel"
module Homebrew
module Diagnostic
class Checks
undef fatal_preinstall_checks, supported_configuration_checks
module OS
module Linux
module Diagnostic
module Checks
extend T::Helpers
def fatal_preinstall_checks
%w[
check_access_directories
check_linuxbrew_core
check_linuxbrew_bottle_domain
].freeze
end
requires_ancestor { Homebrew::Diagnostic::Checks }
def supported_configuration_checks
%w[
check_glibc_minimum_version
check_kernel_minimum_version
check_supported_architecture
].freeze
end
def check_tmpdir_sticky_bit
message = generic_check_tmpdir_sticky_bit
return if message.nil?
message + <<~EOS
If you don't have administrative privileges on this machine,
create a directory and set the HOMEBREW_TEMP environment variable,
for example:
install -d -m 1755 ~/tmp
#{Utils::Shell.set_variable_in_profile("HOMEBREW_TEMP", "~/tmp")}
EOS
end
def check_tmpdir_executable
f = Tempfile.new(%w[homebrew_check_tmpdir_executable .sh], HOMEBREW_TEMP)
f.write "#!/bin/sh\n"
f.chmod 0700
f.close
return if system T.must(f.path)
<<~EOS
The directory #{HOMEBREW_TEMP} does not permit executing
programs. It is likely mounted as "noexec". Please set HOMEBREW_TEMP
in your #{Utils::Shell.profile} to a different directory, for example:
export HOMEBREW_TEMP=~/tmp
echo 'export HOMEBREW_TEMP=~/tmp' >> #{Utils::Shell.profile}
EOS
ensure
f&.unlink
end
def check_xdg_data_dirs
xdg_data_dirs = ENV.fetch("HOMEBREW_XDG_DATA_DIRS", nil)
return if xdg_data_dirs.blank?
return if xdg_data_dirs.split(":").include?("#{HOMEBREW_PREFIX}/share")
<<~EOS
Homebrew's share was not found in your XDG_DATA_DIRS but you have
this variable set to include other locations.
Some programs like `vapigen` may not work correctly.
Consider adding Homebrew's share directory to XDG_DATA_DIRS like so:
echo 'export XDG_DATA_DIRS="#{HOMEBREW_PREFIX}/share:$XDG_DATA_DIRS"' >> #{Utils::Shell.profile}
EOS
end
def check_umask_not_zero
return unless File.umask.zero?
<<~EOS
umask is currently set to 000. Directories created by Homebrew cannot
be world-writable. This issue can be resolved by adding "umask 002" to
your #{Utils::Shell.profile}:
echo 'umask 002' >> #{Utils::Shell.profile}
EOS
end
def check_supported_architecture
return if Hardware::CPU.arch == :x86_64
<<~EOS
Your CPU architecture (#{Hardware::CPU.arch}) is not supported. We only support
x86_64 CPU architectures. You will be unable to use binary packages (bottles).
#{please_create_pull_requests}
EOS
end
def check_glibc_minimum_version
return unless OS::Linux::Glibc.below_minimum_version?
<<~EOS
Your system glibc #{OS::Linux::Glibc.system_version} is too old.
We only support glibc #{OS::Linux::Glibc.minimum_version} or later.
#{please_create_pull_requests}
We recommend updating to a newer version via your distribution's
package manager, upgrading your distribution to the latest version,
or changing distributions.
EOS
end
def check_kernel_minimum_version
return unless OS::Linux::Kernel.below_minimum_version?
<<~EOS
Your Linux kernel #{OS.kernel_version} is too old.
We only support kernel #{OS::Linux::Kernel.minimum_version} or later.
You will be unable to use binary packages (bottles).
#{please_create_pull_requests}
We recommend updating to a newer version via your distribution's
package manager, upgrading your distribution to the latest version,
or changing distributions.
EOS
end
def check_linuxbrew_core
return unless Homebrew::EnvConfig.no_install_from_api?
return unless CoreTap.instance.linuxbrew_core?
<<~EOS
Your Linux core repository is still linuxbrew-core.
You must `brew update` to update to homebrew-core.
EOS
end
def check_linuxbrew_bottle_domain
return unless Homebrew::EnvConfig.bottle_domain.include?("linuxbrew")
<<~EOS
Your HOMEBREW_BOTTLE_DOMAIN still contains "linuxbrew".
You must unset it (or adjust it to not contain linuxbrew
e.g. by using homebrew instead).
EOS
end
def check_gcc_dependent_linkage
gcc_dependents = Formula.installed.select do |formula|
next false unless formula.tap&.core_tap?
# FIXME: This includes formulae that have no runtime dependency on GCC.
formula.recursive_dependencies.map(&:name).include? "gcc"
rescue TapFormulaUnavailableError
false
def fatal_preinstall_checks
%w[
check_access_directories
check_linuxbrew_core
check_linuxbrew_bottle_domain
].freeze
end
return if gcc_dependents.empty?
badly_linked = gcc_dependents.select do |dependent|
keg = Keg.new(dependent.prefix)
keg.binary_executable_or_library_files.any? do |binary|
paths = binary.rpaths
versioned_linkage = paths.any? { |path| path.match?(%r{lib/gcc/\d+$}) }
unversioned_linkage = paths.any? { |path| path.match?(%r{lib/gcc/current$}) }
def supported_configuration_checks
%w[
check_glibc_minimum_version
check_kernel_minimum_version
check_supported_architecture
].freeze
end
versioned_linkage && !unversioned_linkage
def check_tmpdir_sticky_bit
message = generic_check_tmpdir_sticky_bit
return if message.nil?
message + <<~EOS
If you don't have administrative privileges on this machine,
create a directory and set the HOMEBREW_TEMP environment variable,
for example:
install -d -m 1755 ~/tmp
#{Utils::Shell.set_variable_in_profile("HOMEBREW_TEMP", "~/tmp")}
EOS
end
def check_tmpdir_executable
f = Tempfile.new(%w[homebrew_check_tmpdir_executable .sh], HOMEBREW_TEMP)
f.write "#!/bin/sh\n"
f.chmod 0700
f.close
return if system T.must(f.path)
<<~EOS
The directory #{HOMEBREW_TEMP} does not permit executing
programs. It is likely mounted as "noexec". Please set HOMEBREW_TEMP
in your #{Utils::Shell.profile} to a different directory, for example:
export HOMEBREW_TEMP=~/tmp
echo 'export HOMEBREW_TEMP=~/tmp' >> #{Utils::Shell.profile}
EOS
ensure
f&.unlink
end
def check_xdg_data_dirs
xdg_data_dirs = ENV.fetch("HOMEBREW_XDG_DATA_DIRS", nil)
return if xdg_data_dirs.blank?
return if xdg_data_dirs.split(":").include?("#{HOMEBREW_PREFIX}/share")
<<~EOS
Homebrew's share was not found in your XDG_DATA_DIRS but you have
this variable set to include other locations.
Some programs like `vapigen` may not work correctly.
Consider adding Homebrew's share directory to XDG_DATA_DIRS like so:
echo 'export XDG_DATA_DIRS="#{HOMEBREW_PREFIX}/share:$XDG_DATA_DIRS"' >> #{Utils::Shell.profile}
EOS
end
def check_umask_not_zero
return unless File.umask.zero?
<<~EOS
umask is currently set to 000. Directories created by Homebrew cannot
be world-writable. This issue can be resolved by adding "umask 002" to
your #{Utils::Shell.profile}:
echo 'umask 002' >> #{Utils::Shell.profile}
EOS
end
def check_supported_architecture
return if Hardware::CPU.arch == :x86_64
<<~EOS
Your CPU architecture (#{Hardware::CPU.arch}) is not supported. We only support
x86_64 CPU architectures. You will be unable to use binary packages (bottles).
#{please_create_pull_requests}
EOS
end
def check_glibc_minimum_version
return unless OS::Linux::Glibc.below_minimum_version?
<<~EOS
Your system glibc #{OS::Linux::Glibc.system_version} is too old.
We only support glibc #{OS::Linux::Glibc.minimum_version} or later.
#{please_create_pull_requests}
We recommend updating to a newer version via your distribution's
package manager, upgrading your distribution to the latest version,
or changing distributions.
EOS
end
def check_kernel_minimum_version
return unless OS::Linux::Kernel.below_minimum_version?
<<~EOS
Your Linux kernel #{OS.kernel_version} is too old.
We only support kernel #{OS::Linux::Kernel.minimum_version} or later.
You will be unable to use binary packages (bottles).
#{please_create_pull_requests}
We recommend updating to a newer version via your distribution's
package manager, upgrading your distribution to the latest version,
or changing distributions.
EOS
end
def check_linuxbrew_core
return unless Homebrew::EnvConfig.no_install_from_api?
return unless CoreTap.instance.linuxbrew_core?
<<~EOS
Your Linux core repository is still linuxbrew-core.
You must `brew update` to update to homebrew-core.
EOS
end
def check_linuxbrew_bottle_domain
return unless Homebrew::EnvConfig.bottle_domain.include?("linuxbrew")
<<~EOS
Your HOMEBREW_BOTTLE_DOMAIN still contains "linuxbrew".
You must unset it (or adjust it to not contain linuxbrew
e.g. by using homebrew instead).
EOS
end
def check_gcc_dependent_linkage
gcc_dependents = ::Formula.installed.select do |formula|
next false unless formula.tap&.core_tap?
# FIXME: This includes formulae that have no runtime dependency on GCC.
formula.recursive_dependencies.map(&:name).include? "gcc"
rescue TapFormulaUnavailableError
false
end
end
return if badly_linked.empty?
return if gcc_dependents.empty?
inject_file_list badly_linked, <<~EOS
Formulae which link to GCC through a versioned path were found. These formulae
are prone to breaking when GCC is updated. You should `brew reinstall` these formulae:
EOS
badly_linked = gcc_dependents.select do |dependent|
keg = Keg.new(dependent.prefix)
keg.binary_executable_or_library_files.any? do |binary|
paths = binary.rpaths
versioned_linkage = paths.any? { |path| path.match?(%r{lib/gcc/\d+$}) }
unversioned_linkage = paths.any? { |path| path.match?(%r{lib/gcc/current$}) }
versioned_linkage && !unversioned_linkage
end
end
return if badly_linked.empty?
inject_file_list badly_linked, <<~EOS
Formulae which link to GCC through a versioned path were found. These formulae
are prone to breaking when GCC is updated. You should `brew reinstall` these formulae:
EOS
end
end
end
end
end
Homebrew::Diagnostic::Checks.prepend(OS::Linux::Diagnostic::Checks)

View File

@ -28,7 +28,7 @@ module OS
sig { params(spec: SoftwareSpec).void }
def add_global_deps_to_spec(spec)
return unless DevelopmentTools.needs_build_formulae?
return unless ::DevelopmentTools.needs_build_formulae?
@global_deps ||= begin
dependency_collector = spec.dependency_collector

View File

@ -1,15 +1,12 @@
# typed: true # rubocop:disable Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
module Homebrew
class SimulateSystem
class << self
undef os
undef simulating_or_running_on_linux?
undef current_os
module OS
module Linux
module SimulateSystem
sig { returns(T.nilable(Symbol)) }
def os
@os ||= T.let(nil, T.nilable(Symbol))
return :macos if @os.blank? && Homebrew::EnvConfig.simulate_macos_on_linux?
@os
@ -27,3 +24,5 @@ module Homebrew
end
end
end
Homebrew::SimulateSystem.singleton_class.prepend(OS::Linux::SimulateSystem)

View File

@ -8,7 +8,7 @@ module OS
def use_system_ruby?
return false if Homebrew::EnvConfig.force_vendor_ruby?
Homebrew::EnvConfig.developer? && ENV["HOMEBREW_USE_RUBY_FROM_PATH"].present?
::Homebrew::EnvConfig.developer? && ENV["HOMEBREW_USE_RUBY_FROM_PATH"].present?
end
end
end

View File

@ -1,23 +1,26 @@
# typed: true # rubocop:disable Sorbet/StrictSigil
# frozen_string_literal: true
class DependencyCollector
undef git_dep_if_needed, subversion_dep_if_needed, cvs_dep_if_needed,
xz_dep_if_needed, unzip_dep_if_needed, bzip2_dep_if_needed
module OS
module Mac
module DependencyCollector
def git_dep_if_needed(tags); end
def git_dep_if_needed(tags); end
def subversion_dep_if_needed(tags)
Dependency.new("subversion", [*tags, :implicit])
end
def subversion_dep_if_needed(tags)
Dependency.new("subversion", [*tags, :implicit])
def cvs_dep_if_needed(tags)
Dependency.new("cvs", [*tags, :implicit])
end
def xz_dep_if_needed(tags); end
def unzip_dep_if_needed(tags); end
def bzip2_dep_if_needed(tags); end
end
end
def cvs_dep_if_needed(tags)
Dependency.new("cvs", [*tags, :implicit])
end
def xz_dep_if_needed(tags); end
def unzip_dep_if_needed(tags); end
def bzip2_dep_if_needed(tags); end
end
DependencyCollector.prepend(OS::Mac::DependencyCollector)

View File

@ -1,86 +1,91 @@
# typed: true # rubocop:disable Sorbet/StrictSigil
# typed: strict
# frozen_string_literal: true
require "os/mac/xcode"
class DevelopmentTools
class << self
alias generic_locate locate
undef installed?, default_compiler, curl_handles_most_https_certificates?,
subversion_handles_most_https_certificates?
module OS
module Mac
module DevelopmentTools
extend T::Helpers
sig { params(tool: T.any(String, Symbol)).returns(T.nilable(Pathname)) }
def locate(tool)
(@locate ||= {}).fetch(tool) do |key|
@locate[key] = if (located_tool = generic_locate(tool))
located_tool
else
path = Utils.popen_read("/usr/bin/xcrun", "-no-cache", "-find", tool, err: :close).chomp
Pathname.new(path) if File.executable?(path)
requires_ancestor { ::DevelopmentTools }
sig { params(tool: T.any(String, Symbol)).returns(T.nilable(Pathname)) }
def locate(tool)
@locate ||= T.let({}, T.nilable(T::Hash[T.any(String, Symbol), Pathname]))
@locate.fetch(tool) do |key|
@locate[key] = if (located_tool = super(tool))
located_tool
else
path = Utils.popen_read("/usr/bin/xcrun", "-no-cache", "-find", tool, err: :close).chomp
Pathname.new(path) if File.executable?(path)
end
end
end
end
# Checks if the user has any developer tools installed, either via Xcode
# or the CLT. Convenient for guarding against formula builds when building
# is impossible.
sig { returns(T::Boolean) }
def installed?
MacOS::Xcode.installed? || MacOS::CLT.installed?
end
sig { returns(Symbol) }
def default_compiler
:clang
end
sig { returns(Version) }
def ld64_version
@ld64_version ||= begin
json = Utils.popen_read("/usr/bin/ld", "-version_details")
if $CHILD_STATUS.success?
Version.parse(JSON.parse(json)["version"])
else
Version::NULL
end
# Checks if the user has any developer tools installed, either via Xcode
# or the CLT. Convenient for guarding against formula builds when building
# is impossible.
sig { returns(T::Boolean) }
def installed?
MacOS::Xcode.installed? || MacOS::CLT.installed?
end
end
sig { returns(T::Boolean) }
def curl_handles_most_https_certificates?
# The system Curl is too old for some modern HTTPS certificates on
# older macOS versions.
ENV["HOMEBREW_SYSTEM_CURL_TOO_OLD"].nil?
end
sig { returns(Symbol) }
def default_compiler
:clang
end
sig { returns(T::Boolean) }
def subversion_handles_most_https_certificates?
# The system Subversion is too old for some HTTPS certificates on
# older macOS versions.
MacOS.version >= :sierra
end
sig { returns(Version) }
def self.ld64_version
@ld64_version ||= T.let(begin
json = Utils.popen_read("/usr/bin/ld", "-version_details")
if $CHILD_STATUS.success?
Version.parse(JSON.parse(json)["version"])
else
Version::NULL
end
end, T.nilable(Version))
end
sig { returns(String) }
def installation_instructions
MacOS::CLT.installation_instructions
end
sig { returns(T::Boolean) }
def curl_handles_most_https_certificates?
# The system Curl is too old for some modern HTTPS certificates on
# older macOS versions.
ENV["HOMEBREW_SYSTEM_CURL_TOO_OLD"].nil?
end
sig { returns(String) }
def custom_installation_instructions
<<~EOS
Install GNU's GCC:
brew install gcc
EOS
end
sig { returns(T::Boolean) }
def subversion_handles_most_https_certificates?
# The system Subversion is too old for some HTTPS certificates on
# older macOS versions.
MacOS.version >= :sierra
end
sig { returns(T::Hash[String, T.nilable(String)]) }
def build_system_info
build_info = {
"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
sig { returns(String) }
def installation_instructions
MacOS::CLT.installation_instructions
end
sig { returns(String) }
def custom_installation_instructions
<<~EOS
Install GNU's GCC:
brew install gcc
EOS
end
sig { returns(T::Hash[String, T.nilable(String)]) }
def build_system_info
build_info = {
"xcode" => MacOS::Xcode.version.to_s.presence,
"clt" => MacOS::CLT.version.to_s.presence,
"preferred_perl" => MacOS.preferred_perl_version,
}
super.merge build_info
end
end
end
end
DevelopmentTools.singleton_class.prepend(OS::Mac::DevelopmentTools)

View File

@ -1,467 +1,471 @@
# typed: true # rubocop:disable Sorbet/StrictSigil
# frozen_string_literal: true
module Homebrew
module Diagnostic
class Volumes
def initialize
@volumes = get_mounts
end
module OS
module Mac
module Diagnostic
class Volumes
def initialize
@volumes = get_mounts
end
def which(path)
vols = get_mounts path
def which(path)
vols = get_mounts path
# no volume found
return -1 if vols.empty?
# no volume found
return -1 if vols.empty?
vol_index = @volumes.index(vols[0])
# volume not found in volume list
return -1 if vol_index.nil?
vol_index = @volumes.index(vols[0])
# volume not found in volume list
return -1 if vol_index.nil?
vol_index
end
vol_index
end
def get_mounts(path = nil)
vols = []
# get the volume of path, if path is nil returns all volumes
def get_mounts(path = nil)
vols = []
# get the volume of path, if path is nil returns all volumes
args = %w[/bin/df -P]
args << path if path
args = %w[/bin/df -P]
args << path if path
Utils.popen_read(*args) do |io|
io.each_line do |line|
case line.chomp
# regex matches: /dev/disk0s2 489562928 440803616 48247312 91% /
when /^.+\s+[0-9]+\s+[0-9]+\s+[0-9]+\s+[0-9]{1,3}%\s+(.+)/
vols << Regexp.last_match(1)
Utils.popen_read(*args) do |io|
io.each_line do |line|
case line.chomp
# regex matches: /dev/disk0s2 489562928 440803616 48247312 91% /
when /^.+\s+[0-9]+\s+[0-9]+\s+[0-9]+\s+[0-9]{1,3}%\s+(.+)/
vols << Regexp.last_match(1)
end
end
end
vols
end
vols
end
end
class Checks
undef fatal_preinstall_checks, fatal_build_from_source_checks,
fatal_setup_build_environment_checks, supported_configuration_checks,
build_from_source_checks
def fatal_preinstall_checks
checks = %w[
check_access_directories
]
# We need the developer tools for `codesign`.
checks << "check_for_installed_developer_tools" if Hardware::CPU.arm?
checks.freeze
end
def fatal_build_from_source_checks
%w[
check_xcode_license_approved
check_xcode_minimum_version
check_clt_minimum_version
check_if_xcode_needs_clt_installed
check_if_supported_sdk_available
check_broken_sdks
].freeze
end
module Checks
extend T::Helpers
def fatal_setup_build_environment_checks
%w[
check_xcode_minimum_version
check_clt_minimum_version
check_if_supported_sdk_available
].freeze
end
requires_ancestor { Homebrew::Diagnostic::Checks }
def supported_configuration_checks
%w[
check_for_unsupported_macos
].freeze
end
def fatal_preinstall_checks
checks = %w[
check_access_directories
]
def build_from_source_checks
%w[
check_for_installed_developer_tools
check_xcode_up_to_date
check_clt_up_to_date
].freeze
end
# We need the developer tools for `codesign`.
checks << "check_for_installed_developer_tools" if ::Hardware::CPU.arm?
def check_for_non_prefixed_findutils
findutils = Formula["findutils"]
return unless findutils.any_version_installed?
gnubin = %W[#{findutils.opt_libexec}/gnubin #{findutils.libexec}/gnubin]
default_names = Tab.for_name("findutils").with? "default-names"
return if !default_names && !paths.intersect?(gnubin)
<<~EOS
Putting non-prefixed findutils in your path can cause python builds to fail.
EOS
rescue FormulaUnavailableError
nil
end
def check_for_unsupported_macos
return if Homebrew::EnvConfig.developer?
return if ENV["HOMEBREW_INTEGRATION_TEST"]
who = +"We"
what = if OS::Mac.version.prerelease?
"pre-release version"
elsif OS::Mac.version.outdated_release?
who << " (and Apple)"
"old version"
checks.freeze
end
return if what.blank?
who.freeze
def fatal_build_from_source_checks
%w[
check_xcode_license_approved
check_xcode_minimum_version
check_clt_minimum_version
check_if_xcode_needs_clt_installed
check_if_supported_sdk_available
check_broken_sdks
].freeze
end
<<~EOS
You are using macOS #{MacOS.version}.
#{who} do not provide support for this #{what}.
#{please_create_pull_requests(what)}
EOS
end
def fatal_setup_build_environment_checks
%w[
check_xcode_minimum_version
check_clt_minimum_version
check_if_supported_sdk_available
].freeze
end
def check_xcode_up_to_date
return unless MacOS::Xcode.outdated?
def supported_configuration_checks
%w[
check_for_unsupported_macos
].freeze
end
# avoid duplicate very similar messages
return if MacOS::Xcode.below_minimum_version?
def build_from_source_checks
%w[
check_for_installed_developer_tools
check_xcode_up_to_date
check_clt_up_to_date
].freeze
end
# CI images are going to end up outdated so don't complain when
# `brew test-bot` runs `brew doctor` in the CI for the Homebrew/brew
# repository. This only needs to support whatever CI providers
# Homebrew/brew is currently using.
return if GitHub::Actions.env_set?
def check_for_non_prefixed_findutils
findutils = ::Formula["findutils"]
return unless findutils.any_version_installed?
# With fake El Capitan for Portable Ruby, we are intentionally not using Xcode 8.
# This is because we are not using the CLT and Xcode 8 has the 10.12 SDK.
return if ENV["HOMEBREW_FAKE_MACOS"]
gnubin = %W[#{findutils.opt_libexec}/gnubin #{findutils.libexec}/gnubin]
default_names = Tab.for_name("findutils").with? "default-names"
return if !default_names && !paths.intersect?(gnubin)
message = <<~EOS
Your Xcode (#{MacOS::Xcode.version}) is outdated.
Please update to Xcode #{MacOS::Xcode.latest_version} (or delete it).
#{MacOS::Xcode.update_instructions}
EOS
<<~EOS
Putting non-prefixed findutils in your path can cause python builds to fail.
EOS
rescue FormulaUnavailableError
nil
end
if OS::Mac.version.prerelease?
current_path = Utils.popen_read("/usr/bin/xcode-select", "-p")
message += <<~EOS
If #{MacOS::Xcode.latest_version} is installed, you may need to:
sudo xcode-select --switch /Applications/Xcode.app
Current developer directory is:
#{current_path}
def check_for_unsupported_macos
return if Homebrew::EnvConfig.developer?
return if ENV["HOMEBREW_INTEGRATION_TEST"]
who = +"We"
what = if OS::Mac.version.prerelease?
"pre-release version"
elsif OS::Mac.version.outdated_release?
who << " (and Apple)"
"old version"
end
return if what.blank?
who.freeze
<<~EOS
You are using macOS #{MacOS.version}.
#{who} do not provide support for this #{what}.
#{please_create_pull_requests(what)}
EOS
end
message
end
def check_clt_up_to_date
return unless MacOS::CLT.outdated?
def check_xcode_up_to_date
return unless MacOS::Xcode.outdated?
# avoid duplicate very similar messages
return if MacOS::CLT.below_minimum_version?
# avoid duplicate very similar messages
return if MacOS::Xcode.below_minimum_version?
# CI images are going to end up outdated so don't complain when
# `brew test-bot` runs `brew doctor` in the CI for the Homebrew/brew
# repository. This only needs to support whatever CI providers
# Homebrew/brew is currently using.
return if GitHub::Actions.env_set?
# CI images are going to end up outdated so don't complain when
# `brew test-bot` runs `brew doctor` in the CI for the Homebrew/brew
# repository. This only needs to support whatever CI providers
# Homebrew/brew is currently using.
return if GitHub::Actions.env_set?
<<~EOS
A newer Command Line Tools release is available.
#{MacOS::CLT.update_instructions}
EOS
end
# With fake El Capitan for Portable Ruby, we are intentionally not using Xcode 8.
# This is because we are not using the CLT and Xcode 8 has the 10.12 SDK.
return if ENV["HOMEBREW_FAKE_MACOS"]
def check_xcode_minimum_version
return unless MacOS::Xcode.below_minimum_version?
message = <<~EOS
Your Xcode (#{MacOS::Xcode.version}) is outdated.
Please update to Xcode #{MacOS::Xcode.latest_version} (or delete it).
#{MacOS::Xcode.update_instructions}
EOS
xcode = MacOS::Xcode.version.to_s
xcode += " => #{MacOS::Xcode.prefix}" unless MacOS::Xcode.default_prefix?
<<~EOS
Your Xcode (#{xcode}) at #{MacOS::Xcode.bundle_path} is too outdated.
Please update to Xcode #{MacOS::Xcode.latest_version} (or delete it).
#{MacOS::Xcode.update_instructions}
EOS
end
def check_clt_minimum_version
return unless MacOS::CLT.below_minimum_version?
<<~EOS
Your Command Line Tools are too outdated.
#{MacOS::CLT.update_instructions}
EOS
end
def check_if_xcode_needs_clt_installed
return unless MacOS::Xcode.needs_clt_installed?
<<~EOS
Xcode alone is not sufficient on #{MacOS.version.pretty_name}.
#{DevelopmentTools.installation_instructions}
EOS
end
def check_xcode_prefix
prefix = MacOS::Xcode.prefix
return if prefix.nil?
return unless prefix.to_s.include?(" ")
<<~EOS
Xcode is installed to a directory with a space in the name.
This will cause some formulae to fail to build.
EOS
end
def check_xcode_prefix_exists
prefix = MacOS::Xcode.prefix
return if prefix.nil? || prefix.exist?
<<~EOS
The directory Xcode is reportedly installed to doesn't exist:
#{prefix}
You may need to `xcode-select` the proper path if you have moved Xcode.
EOS
end
def check_xcode_select_path
return if MacOS::CLT.installed?
return unless MacOS::Xcode.installed?
return if File.file?("#{MacOS.active_developer_dir}/usr/bin/xcodebuild")
path = MacOS::Xcode.bundle_path
path = "/Developer" if path.nil? || !path.directory?
<<~EOS
Your Xcode is configured with an invalid path.
You should change it to the correct path:
sudo xcode-select --switch #{path}
EOS
end
def check_xcode_license_approved
# If the user installs Xcode-only, they have to approve the
# license or no "xc*" tool will work.
return unless `/usr/bin/xcrun clang 2>&1`.include?("license")
return if $CHILD_STATUS.success?
<<~EOS
You have not agreed to the Xcode license.
Agree to the license by opening Xcode.app or running:
sudo xcodebuild -license
EOS
end
def check_filesystem_case_sensitive
dirs_to_check = [
HOMEBREW_PREFIX,
HOMEBREW_REPOSITORY,
HOMEBREW_CELLAR,
HOMEBREW_TEMP,
]
case_sensitive_dirs = dirs_to_check.select do |dir|
# We select the dir as being case-sensitive if either the UPCASED or the
# downcased variant is missing.
# Of course, on a case-insensitive fs, both exist because the os reports so.
# In the rare situation when the user has indeed a downcased and an upcased
# dir (e.g. /TMP and /tmp) this check falsely thinks it is case-insensitive
# but we don't care because: 1. there is more than one dir checked, 2. the
# check is not vital and 3. we would have to touch files otherwise.
upcased = Pathname.new(dir.to_s.upcase)
downcased = Pathname.new(dir.to_s.downcase)
dir.exist? && !(upcased.exist? && downcased.exist?)
end
return if case_sensitive_dirs.empty?
volumes = Volumes.new
case_sensitive_vols = case_sensitive_dirs.map do |case_sensitive_dir|
volumes.get_mounts(case_sensitive_dir)
end
case_sensitive_vols.uniq!
<<~EOS
The filesystem on #{case_sensitive_vols.join(",")} appears to be case-sensitive.
The default macOS filesystem is case-insensitive. Please report any apparent problems.
EOS
end
def check_for_gettext
find_relative_paths("lib/libgettextlib.dylib",
"lib/libintl.dylib",
"include/libintl.h")
return if @found.empty?
# Our gettext formula will be caught by check_linked_keg_only_brews
gettext = begin
Formulary.factory("gettext")
rescue
nil
end
if gettext&.linked_keg&.directory?
allowlist = ["#{HOMEBREW_CELLAR}/gettext"]
if Hardware::CPU.physical_cpu_arm64?
allowlist += %W[
#{HOMEBREW_MACOS_ARM_DEFAULT_PREFIX}/Cellar/gettext
#{HOMEBREW_DEFAULT_PREFIX}/Cellar/gettext
]
end
return if @found.all? do |path|
realpath = Pathname.new(path).realpath.to_s
allowlist.any? { |rack| realpath.start_with?(rack) }
end
end
inject_file_list @found, <<~EOS
gettext files detected at a system prefix.
These files can cause compilation and link failures, especially if they
are compiled with improper architectures. Consider removing these files:
EOS
end
def check_for_iconv
find_relative_paths("lib/libiconv.dylib", "include/iconv.h")
return if @found.empty?
libiconv = begin
Formulary.factory("libiconv")
rescue
nil
end
if libiconv&.linked_keg&.directory?
unless libiconv&.keg_only?
<<~EOS
A libiconv formula is installed and linked.
This will break stuff. For serious. Unlink it.
if OS::Mac.version.prerelease?
current_path = Utils.popen_read("/usr/bin/xcode-select", "-p")
message += <<~EOS
If #{MacOS::Xcode.latest_version} is installed, you may need to:
sudo xcode-select --switch /Applications/Xcode.app
Current developer directory is:
#{current_path}
EOS
end
else
inject_file_list @found, <<~EOS
libiconv files detected at a system prefix other than /usr.
Homebrew doesn't provide a libiconv formula and expects to link against
the system version in /usr. libiconv in other prefixes can cause
compile or link failure, especially if compiled with improper
architectures. macOS itself never installs anything to /usr/local so
it was either installed by a user or some other third party software.
message
end
tl;dr: delete these files:
def check_clt_up_to_date
return unless MacOS::CLT.outdated?
# avoid duplicate very similar messages
return if MacOS::CLT.below_minimum_version?
# CI images are going to end up outdated so don't complain when
# `brew test-bot` runs `brew doctor` in the CI for the Homebrew/brew
# repository. This only needs to support whatever CI providers
# Homebrew/brew is currently using.
return if GitHub::Actions.env_set?
<<~EOS
A newer Command Line Tools release is available.
#{MacOS::CLT.update_instructions}
EOS
end
end
def check_for_multiple_volumes
return unless HOMEBREW_CELLAR.exist?
def check_xcode_minimum_version
return unless MacOS::Xcode.below_minimum_version?
volumes = Volumes.new
xcode = MacOS::Xcode.version.to_s
xcode += " => #{MacOS::Xcode.prefix}" unless MacOS::Xcode.default_prefix?
# Find the volumes for the TMP folder & HOMEBREW_CELLAR
real_cellar = HOMEBREW_CELLAR.realpath
where_cellar = volumes.which real_cellar
<<~EOS
Your Xcode (#{xcode}) at #{MacOS::Xcode.bundle_path} is too outdated.
Please update to Xcode #{MacOS::Xcode.latest_version} (or delete it).
#{MacOS::Xcode.update_instructions}
EOS
end
begin
tmp = Pathname.new(Dir.mktmpdir("doctor", HOMEBREW_TEMP))
begin
real_tmp = tmp.realpath.parent
where_tmp = volumes.which real_tmp
ensure
Dir.delete tmp.to_s
def check_clt_minimum_version
return unless MacOS::CLT.below_minimum_version?
<<~EOS
Your Command Line Tools are too outdated.
#{MacOS::CLT.update_instructions}
EOS
end
def check_if_xcode_needs_clt_installed
return unless MacOS::Xcode.needs_clt_installed?
<<~EOS
Xcode alone is not sufficient on #{MacOS.version.pretty_name}.
#{::DevelopmentTools.installation_instructions}
EOS
end
def check_xcode_prefix
prefix = MacOS::Xcode.prefix
return if prefix.nil?
return unless prefix.to_s.include?(" ")
<<~EOS
Xcode is installed to a directory with a space in the name.
This will cause some formulae to fail to build.
EOS
end
def check_xcode_prefix_exists
prefix = MacOS::Xcode.prefix
return if prefix.nil? || prefix.exist?
<<~EOS
The directory Xcode is reportedly installed to doesn't exist:
#{prefix}
You may need to `xcode-select` the proper path if you have moved Xcode.
EOS
end
def check_xcode_select_path
return if MacOS::CLT.installed?
return unless MacOS::Xcode.installed?
return if File.file?("#{MacOS.active_developer_dir}/usr/bin/xcodebuild")
path = MacOS::Xcode.bundle_path
path = "/Developer" if path.nil? || !path.directory?
<<~EOS
Your Xcode is configured with an invalid path.
You should change it to the correct path:
sudo xcode-select --switch #{path}
EOS
end
def check_xcode_license_approved
# If the user installs Xcode-only, they have to approve the
# license or no "xc*" tool will work.
return unless `/usr/bin/xcrun clang 2>&1`.include?("license")
return if $CHILD_STATUS.success?
<<~EOS
You have not agreed to the Xcode license.
Agree to the license by opening Xcode.app or running:
sudo xcodebuild -license
EOS
end
def check_filesystem_case_sensitive
dirs_to_check = [
HOMEBREW_PREFIX,
HOMEBREW_REPOSITORY,
HOMEBREW_CELLAR,
HOMEBREW_TEMP,
]
case_sensitive_dirs = dirs_to_check.select do |dir|
# We select the dir as being case-sensitive if either the UPCASED or the
# downcased variant is missing.
# Of course, on a case-insensitive fs, both exist because the os reports so.
# In the rare situation when the user has indeed a downcased and an upcased
# dir (e.g. /TMP and /tmp) this check falsely thinks it is case-insensitive
# but we don't care because: 1. there is more than one dir checked, 2. the
# check is not vital and 3. we would have to touch files otherwise.
upcased = Pathname.new(dir.to_s.upcase)
downcased = Pathname.new(dir.to_s.downcase)
dir.exist? && !(upcased.exist? && downcased.exist?)
end
rescue
return
return if case_sensitive_dirs.empty?
volumes = Volumes.new
case_sensitive_vols = case_sensitive_dirs.map do |case_sensitive_dir|
volumes.get_mounts(case_sensitive_dir)
end
case_sensitive_vols.uniq!
<<~EOS
The filesystem on #{case_sensitive_vols.join(",")} appears to be case-sensitive.
The default macOS filesystem is case-insensitive. Please report any apparent problems.
EOS
end
return if where_cellar == where_tmp
def check_for_gettext
find_relative_paths("lib/libgettextlib.dylib",
"lib/libintl.dylib",
"include/libintl.h")
return if @found.empty?
<<~EOS
Your Cellar and TEMP directories are on different volumes.
macOS won't move relative symlinks across volumes unless the target file already
exists. Brews known to be affected by this are Git and Narwhal.
# Our gettext formula will be caught by check_linked_keg_only_brews
gettext = begin
Formulary.factory("gettext")
rescue
nil
end
You should set the "HOMEBREW_TEMP" environment variable to a suitable
directory on the same volume as your Cellar.
EOS
end
if gettext&.linked_keg&.directory?
allowlist = ["#{HOMEBREW_CELLAR}/gettext"]
if ::Hardware::CPU.physical_cpu_arm64?
allowlist += %W[
#{HOMEBREW_MACOS_ARM_DEFAULT_PREFIX}/Cellar/gettext
#{HOMEBREW_DEFAULT_PREFIX}/Cellar/gettext
]
end
def check_deprecated_caskroom_taps
tapped_caskroom_taps = Tap.select { |t| t.user == "caskroom" || t.name == "phinze/cask" }
.map(&:name)
return if tapped_caskroom_taps.empty?
return if @found.all? do |path|
realpath = Pathname.new(path).realpath.to_s
allowlist.any? { |rack| realpath.start_with?(rack) }
end
end
<<~EOS
You have the following deprecated, cask taps tapped:
#{tapped_caskroom_taps.join("\n ")}
Untap them with `brew untap`.
EOS
end
def check_if_supported_sdk_available
return unless DevelopmentTools.installed?
return unless MacOS.sdk_root_needed?
return if MacOS.sdk
locator = MacOS.sdk_locator
source = if locator.source == :clt
return if MacOS::CLT.below_minimum_version? # Handled by other diagnostics.
update_instructions = MacOS::CLT.update_instructions
"Command Line Tools (CLT)"
else
return if MacOS::Xcode.below_minimum_version? # Handled by other diagnostics.
update_instructions = MacOS::Xcode.update_instructions
"Xcode"
inject_file_list @found, <<~EOS
gettext files detected at a system prefix.
These files can cause compilation and link failures, especially if they
are compiled with improper architectures. Consider removing these files:
EOS
end
<<~EOS
Your #{source} does not support macOS #{MacOS.version}.
It is either outdated or was modified.
Please update your #{source} or delete it if no updates are available.
#{update_instructions}
EOS
end
def check_for_iconv
find_relative_paths("lib/libiconv.dylib", "include/iconv.h")
return if @found.empty?
# The CLT 10.x -> 11.x upgrade process on 10.14 contained a bug which broke the SDKs.
# Notably, MacOSX10.14.sdk would indirectly symlink to MacOSX10.15.sdk.
# This diagnostic was introduced to check for this and recommend a full reinstall.
def check_broken_sdks
locator = MacOS.sdk_locator
libiconv = begin
Formulary.factory("libiconv")
rescue
nil
end
if libiconv&.linked_keg&.directory?
unless libiconv&.keg_only?
<<~EOS
A libiconv formula is installed and linked.
This will break stuff. For serious. Unlink it.
EOS
end
else
inject_file_list @found, <<~EOS
libiconv files detected at a system prefix other than /usr.
Homebrew doesn't provide a libiconv formula and expects to link against
the system version in /usr. libiconv in other prefixes can cause
compile or link failure, especially if compiled with improper
architectures. macOS itself never installs anything to /usr/local so
it was either installed by a user or some other third party software.
return if locator.all_sdks.all? do |sdk|
path_version = sdk.path.basename.to_s[MacOS::SDK::VERSIONED_SDK_REGEX, 1]
next true if path_version.blank?
sdk.version == MacOSVersion.new(path_version).strip_patch
tl;dr: delete these files:
EOS
end
end
if locator.source == :clt
source = "Command Line Tools (CLT)"
path_to_remove = MacOS::CLT::PKG_PATH
installation_instructions = MacOS::CLT.installation_instructions
else
source = "Xcode"
path_to_remove = MacOS::Xcode.bundle_path
installation_instructions = MacOS::Xcode.installation_instructions
def check_for_multiple_volumes
return unless HOMEBREW_CELLAR.exist?
volumes = Volumes.new
# Find the volumes for the TMP folder & HOMEBREW_CELLAR
real_cellar = HOMEBREW_CELLAR.realpath
where_cellar = volumes.which real_cellar
begin
tmp = Pathname.new(Dir.mktmpdir("doctor", HOMEBREW_TEMP))
begin
real_tmp = tmp.realpath.parent
where_tmp = volumes.which real_tmp
ensure
Dir.delete tmp.to_s
end
rescue
return
end
return if where_cellar == where_tmp
<<~EOS
Your Cellar and TEMP directories are on different volumes.
macOS won't move relative symlinks across volumes unless the target file already
exists. Brews known to be affected by this are Git and Narwhal.
You should set the "HOMEBREW_TEMP" environment variable to a suitable
directory on the same volume as your Cellar.
EOS
end
<<~EOS
The contents of the SDKs in your #{source} installation do not match the SDK folder names.
A clean reinstall of #{source} should fix this.
def check_deprecated_caskroom_taps
tapped_caskroom_taps = Tap.select { |t| t.user == "caskroom" || t.name == "phinze/cask" }
.map(&:name)
return if tapped_caskroom_taps.empty?
Remove the broken installation before reinstalling:
sudo rm -rf #{path_to_remove}
<<~EOS
You have the following deprecated, cask taps tapped:
#{tapped_caskroom_taps.join("\n ")}
Untap them with `brew untap`.
EOS
end
#{installation_instructions}
EOS
def check_if_supported_sdk_available
return unless ::DevelopmentTools.installed?
return unless MacOS.sdk_root_needed?
return if MacOS.sdk
locator = MacOS.sdk_locator
source = if locator.source == :clt
return if MacOS::CLT.below_minimum_version? # Handled by other diagnostics.
update_instructions = MacOS::CLT.update_instructions
"Command Line Tools (CLT)"
else
return if MacOS::Xcode.below_minimum_version? # Handled by other diagnostics.
update_instructions = MacOS::Xcode.update_instructions
"Xcode"
end
<<~EOS
Your #{source} does not support macOS #{MacOS.version}.
It is either outdated or was modified.
Please update your #{source} or delete it if no updates are available.
#{update_instructions}
EOS
end
# The CLT 10.x -> 11.x upgrade process on 10.14 contained a bug which broke the SDKs.
# Notably, MacOSX10.14.sdk would indirectly symlink to MacOSX10.15.sdk.
# This diagnostic was introduced to check for this and recommend a full reinstall.
def check_broken_sdks
locator = MacOS.sdk_locator
return if locator.all_sdks.all? do |sdk|
path_version = sdk.path.basename.to_s[MacOS::SDK::VERSIONED_SDK_REGEX, 1]
next true if path_version.blank?
sdk.version == MacOSVersion.new(path_version).strip_patch
end
if locator.source == :clt
source = "Command Line Tools (CLT)"
path_to_remove = MacOS::CLT::PKG_PATH
installation_instructions = MacOS::CLT.installation_instructions
else
source = "Xcode"
path_to_remove = MacOS::Xcode.bundle_path
installation_instructions = MacOS::Xcode.installation_instructions
end
<<~EOS
The contents of the SDKs in your #{source} installation do not match the SDK folder names.
A clean reinstall of #{source} should fix this.
Remove the broken installation before reinstalling:
sudo rm -rf #{path_to_remove}
#{installation_instructions}
EOS
end
end
end
end
end
Homebrew::Diagnostic::Checks.prepend(OS::Mac::Diagnostic::Checks)

View File

@ -37,6 +37,6 @@ module SharedEnvExtension
# This is supported starting Xcode 13, which ships ld64-711.
# https://developer.apple.com/documentation/xcode-release-notes/xcode-13-release-notes
# https://en.wikipedia.org/wiki/Xcode#Xcode_11.0_-_14.x_(since_SwiftUI_framework)_2
DevelopmentTools.ld64_version >= 711
OS::Mac::DevelopmentTools.ld64_version >= 711
end
end

View File

@ -152,11 +152,13 @@ module Superenv
no_fixup_chains
# Strip build prefixes from linker where supported, for deterministic builds.
append_to_cccfg "o" if DevelopmentTools.ld64_version >= 512
append_to_cccfg "o" if OS::Mac::DevelopmentTools.ld64_version >= 512
# Pass `-ld_classic` whenever the linker is invoked with `-dead_strip_dylibs`
# on `ld` versions that don't properly handle that option.
append_to_cccfg "c" if DevelopmentTools.ld64_version >= "1015.7" && DevelopmentTools.ld64_version <= "1022.1"
if OS::Mac::DevelopmentTools.ld64_version >= "1015.7" && OS::Mac::DevelopmentTools.ld64_version <= "1022.1"
append_to_cccfg "c"
end
end
def no_weak_imports

View File

@ -97,7 +97,7 @@ module FormulaCellarChecks
return unless formula.prefix.directory?
return if formula.tap&.audit_exception(:flat_namespace_allowlist, formula.name)
keg = Keg.new(formula.prefix)
keg = ::Keg.new(formula.prefix)
flat_namespace_files = keg.mach_o_files.reject do |file|
next true unless file.dylib?

View File

@ -10,7 +10,7 @@ module OS
sig { params(formula: Formula).returns(T.nilable(T::Boolean)) }
def fresh_install?(formula)
!Homebrew::EnvConfig.developer? && !OS::Mac.version.outdated_release? &&
!::Homebrew::EnvConfig.developer? && !OS::Mac.version.outdated_release? &&
(!installed_as_dependency? || !formula.any_version_installed?)
end
end

View File

@ -3,54 +3,64 @@
require "macho"
module OS
module Mac
module Hardware
module CPU
extend T::Helpers
# These methods use info spewed out by sysctl.
# Look in <mach/machine.h> for decoding info.
def type
case ::Hardware::CPU.sysctl_int("hw.cputype")
when MachO::Headers::CPU_TYPE_I386
:intel
when MachO::Headers::CPU_TYPE_ARM64
:arm
else
:dunno
end
end
def family
if ::Hardware::CPU.arm?
::Hardware::CPU.arm_family
elsif ::Hardware::CPU.intel?
::Hardware::CPU.intel_family
else
:dunno
end
end
# True when running under an Intel-based shell via Rosetta 2 on an
# Apple Silicon Mac. This can be detected via seeing if there's a
# conflict between what `uname` reports and the underlying `sysctl` flags,
# since the `sysctl` flags don't change behaviour under Rosetta 2.
def in_rosetta2?
::Hardware::CPU.sysctl_bool("sysctl.proc_translated")
end
def features
@features ||= ::Hardware::CPU.sysctl_n(
"machdep.cpu.features",
"machdep.cpu.extfeatures",
"machdep.cpu.leaf7_features",
).split.map { |s| s.downcase.to_sym }
end
def sse4?
::Hardware::CPU.sysctl_bool("hw.optional.sse4_1")
end
end
end
end
end
Hardware::CPU.singleton_class.prepend(OS::Mac::Hardware::CPU)
module Hardware
class CPU
class << self
undef type, family, features, sse4?
# These methods use info spewed out by sysctl.
# Look in <mach/machine.h> for decoding info.
def type
case sysctl_int("hw.cputype")
when MachO::Headers::CPU_TYPE_I386
:intel
when MachO::Headers::CPU_TYPE_ARM64
:arm
else
:dunno
end
end
def family
if arm?
arm_family
elsif intel?
intel_family
else
:dunno
end
end
# True when running under an Intel-based shell via Rosetta 2 on an
# Apple Silicon Mac. This can be detected via seeing if there's a
# conflict between what `uname` reports and the underlying `sysctl` flags,
# since the `sysctl` flags don't change behaviour under Rosetta 2.
def in_rosetta2?
sysctl_bool("sysctl.proc_translated")
end
def features
@features ||= sysctl_n(
"machdep.cpu.features",
"machdep.cpu.extfeatures",
"machdep.cpu.leaf7_features",
).split.map { |s| s.downcase.to_sym }
end
def sse4?
sysctl_bool("hw.optional.sse4_1")
end
def extmodel
sysctl_int("machdep.cpu.extmodel")
end
@ -93,8 +103,6 @@ module Hardware
sysctl_bool("kern.hv_vmm_present")
end
private
def arm_family
case sysctl_int("hw.cpufamily")
when 0x2c91a47e # ARMv8.0-A (Typhoon)

View File

@ -6,6 +6,7 @@ require "system_command"
class Keg
include SystemCommand::Mixin
# TODO: re-implement these as functions, so that we aren't modifying constants:
GENERIC_KEG_LINK_DIRECTORIES = (remove_const :KEG_LINK_DIRECTORIES).freeze
KEG_LINK_DIRECTORIES = (GENERIC_KEG_LINK_DIRECTORIES + ["Frameworks"]).freeze
GENERIC_MUST_EXIST_SUBDIRECTORIES = (remove_const :MUST_EXIST_SUBDIRECTORIES).freeze
@ -23,101 +24,101 @@ class Keg
GENERIC_MUST_BE_WRITABLE_DIRECTORIES +
[HOMEBREW_PREFIX/"Frameworks"]
).sort.uniq.freeze
end
undef binary_executable_or_library_files
module OS
module Mac
module Keg
def binary_executable_or_library_files = mach_o_files
def binary_executable_or_library_files
mach_o_files
end
def codesign_patched_binary(file)
return if MacOS.version < :big_sur
def codesign_patched_binary(file)
return if MacOS.version < :big_sur
unless ::Hardware::CPU.arm?
result = system_command("codesign", args: ["--verify", file], print_stderr: false)
return unless result.stderr.match?(/invalid signature/i)
end
unless Hardware::CPU.arm?
result = system_command("codesign", args: ["--verify", file], print_stderr: false)
return unless result.stderr.match?(/invalid signature/i)
end
odebug "Codesigning #{file}"
prepare_codesign_writable_files(file) do
# Use quiet_system to squash notifications about resigning binaries
# which already have valid signatures.
return if quiet_system("codesign", "--sign", "-", "--force",
"--preserve-metadata=entitlements,requirements,flags,runtime",
file)
odebug "Codesigning #{file}"
prepare_codesign_writable_files(file) do
# Use quiet_system to squash notifications about resigning binaries
# which already have valid signatures.
return if quiet_system("codesign", "--sign", "-", "--force",
"--preserve-metadata=entitlements,requirements,flags,runtime",
file)
# If the codesigning fails, it may be a bug in Apple's codesign utility
# A known workaround is to copy the file to another inode, then move it back
# erasing the previous file. Then sign again.
#
# TODO: remove this once the bug in Apple's codesign utility is fixed
Dir::Tmpname.create("workaround") do |tmppath|
FileUtils.cp file, tmppath
FileUtils.mv tmppath, file, force: true
end
# If the codesigning fails, it may be a bug in Apple's codesign utility
# A known workaround is to copy the file to another inode, then move it back
# erasing the previous file. Then sign again.
#
# TODO: remove this once the bug in Apple's codesign utility is fixed
Dir::Tmpname.create("workaround") do |tmppath|
FileUtils.cp file, tmppath
FileUtils.mv tmppath, file, force: true
# Try signing again
odebug "Codesigning (2nd try) #{file}"
result = system_command("codesign", args: [
"--sign", "-", "--force",
"--preserve-metadata=entitlements,requirements,flags,runtime",
file
], print_stderr: false)
return if result.success?
# If it fails again, error out
onoe <<~EOS
Failed applying an ad-hoc signature to #{file}:
#{result.stderr}
EOS
end
end
# Try signing again
odebug "Codesigning (2nd try) #{file}"
result = system_command("codesign", args: [
"--sign", "-", "--force",
"--preserve-metadata=entitlements,requirements,flags,runtime",
file
], print_stderr: false)
return if result.success?
def prepare_codesign_writable_files(file)
result = system_command("codesign", args: [
"--display", "--file-list", "-", file
], print_stderr: false)
return unless result.success?
# If it fails again, error out
onoe <<~EOS
Failed applying an ad-hoc signature to #{file}:
#{result.stderr}
EOS
end
end
def prepare_codesign_writable_files(file)
result = system_command("codesign", args: [
"--display", "--file-list", "-", file
], print_stderr: false)
return unless result.success?
files = result.stdout.lines.map { |f| Pathname(f.chomp) }
saved_perms = {}
files.each do |f|
unless f.writable?
saved_perms[f] = f.stat.mode
FileUtils.chmod "u+rw", f.to_path
files = result.stdout.lines.map { |f| Pathname(f.chomp) }
saved_perms = {}
files.each do |f|
unless f.writable?
saved_perms[f] = f.stat.mode
FileUtils.chmod "u+rw", f.to_path
end
end
yield
ensure
saved_perms&.each do |f, p|
f.chmod p if p
end
end
end
yield
ensure
saved_perms&.each do |f, p|
f.chmod p if p
end
end
undef prepare_debug_symbols
def prepare_debug_symbols
binary_executable_or_library_files.each do |file|
odebug "Extracting symbols #{file}"
def prepare_debug_symbols
binary_executable_or_library_files.each do |file|
odebug "Extracting symbols #{file}"
result = system_command("dsymutil", args: [file], print_stderr: false)
next if result.success?
result = system_command("dsymutil", args: [file], print_stderr: false)
next if result.success?
# If it fails again, error out
ofail <<~EOS
Failed to extract symbols from #{file}:
#{result.stderr}
EOS
end
end
# If it fails again, error out
ofail <<~EOS
Failed to extract symbols from #{file}:
#{result.stderr}
EOS
end
end
undef consistent_reproducible_symlink_permissions!
# Needed to make symlink permissions consistent on macOS and Linux for
# reproducible bottles.
def consistent_reproducible_symlink_permissions!
path.find do |file|
File.lchmod 0777, file if file.symlink?
# Needed to make symlink permissions consistent on macOS and Linux for
# reproducible bottles.
def consistent_reproducible_symlink_permissions!
path.find do |file|
File.lchmod 0777, file if file.symlink?
end
end
end
end
end
Keg.prepend(OS::Mac::Keg)

View File

@ -1,255 +1,262 @@
# typed: true # rubocop:disable Sorbet/StrictSigil
# frozen_string_literal: true
class Keg
class << self
undef file_linked_libraries
module OS
module Mac
module Keg
extend T::Helpers
def file_linked_libraries(file, string)
# Check dynamic library linkage. Importantly, do not perform for static
# libraries, which will falsely report "linkage" to themselves.
if file.mach_o_executable? || file.dylib? || file.mach_o_bundle?
file.dynamically_linked_libraries.select { |lib| lib.include? string }
else
[]
end
end
end
requires_ancestor { ::Keg }
undef relocate_dynamic_linkage
def relocate_dynamic_linkage(relocation)
mach_o_files.each do |file|
file.ensure_writable do
modified = T.let(false, T::Boolean)
needs_codesigning = T.let(false, T::Boolean)
if file.dylib?
id = relocated_name_for(file.dylib_id, relocation)
modified = change_dylib_id(id, file)
needs_codesigning ||= modified
end
each_linkage_for(file, :dynamically_linked_libraries) do |old_name|
new_name = relocated_name_for(old_name, relocation)
modified = change_install_name(old_name, new_name, file) if new_name
needs_codesigning ||= modified
end
each_linkage_for(file, :rpaths) do |old_name|
new_name = relocated_name_for(old_name, relocation)
modified = change_rpath(old_name, new_name, file) if new_name
needs_codesigning ||= modified
end
# codesign the file if needed
codesign_patched_binary(file) if needs_codesigning
end
end
end
def fix_dynamic_linkage
mach_o_files.each do |file|
file.ensure_writable do
modified = T.let(false, T::Boolean)
needs_codesigning = T.let(false, T::Boolean)
modified = change_dylib_id(dylib_id_for(file), file) if file.dylib?
needs_codesigning ||= modified
each_linkage_for(file, :dynamically_linked_libraries) do |bad_name|
# Don't fix absolute paths unless they are rooted in the build directory.
new_name = if bad_name.start_with?("/") && !rooted_in_build_directory?(bad_name)
bad_name
module ClassMethods
def file_linked_libraries(file, string)
# Check dynamic library linkage. Importantly, do not perform for static
# libraries, which will falsely report "linkage" to themselves.
if file.mach_o_executable? || file.dylib? || file.mach_o_bundle?
file.dynamically_linked_libraries.select { |lib| lib.include? string }
else
fixed_name(file, bad_name)
[]
end
end
end
def relocate_dynamic_linkage(relocation)
mach_o_files.each do |file|
file.ensure_writable do
modified = T.let(false, T::Boolean)
needs_codesigning = T.let(false, T::Boolean)
if file.dylib?
id = relocated_name_for(file.dylib_id, relocation)
modified = change_dylib_id(id, file)
needs_codesigning ||= modified
end
each_linkage_for(file, :dynamically_linked_libraries) do |old_name|
new_name = relocated_name_for(old_name, relocation)
modified = change_install_name(old_name, new_name, file) if new_name
needs_codesigning ||= modified
end
each_linkage_for(file, :rpaths) do |old_name|
new_name = relocated_name_for(old_name, relocation)
modified = change_rpath(old_name, new_name, file) if new_name
needs_codesigning ||= modified
end
# codesign the file if needed
codesign_patched_binary(file) if needs_codesigning
end
end
end
def fix_dynamic_linkage
mach_o_files.each do |file|
file.ensure_writable do
modified = T.let(false, T::Boolean)
needs_codesigning = T.let(false, T::Boolean)
modified = change_dylib_id(dylib_id_for(file), file) if file.dylib?
needs_codesigning ||= modified
each_linkage_for(file, :dynamically_linked_libraries) do |bad_name|
# Don't fix absolute paths unless they are rooted in the build directory.
new_name = if bad_name.start_with?("/") && !rooted_in_build_directory?(bad_name)
bad_name
else
fixed_name(file, bad_name)
end
loader_name = loader_name_for(file, new_name)
modified = change_install_name(bad_name, loader_name, file) if loader_name != bad_name
needs_codesigning ||= modified
end
each_linkage_for(file, :rpaths) do |bad_name|
new_name = opt_name_for(bad_name)
loader_name = loader_name_for(file, new_name)
next if loader_name == bad_name
modified = change_rpath(bad_name, loader_name, file)
needs_codesigning ||= modified
end
# Strip duplicate rpaths and rpaths rooted in the build directory.
# We do this separately from the rpath relocation above to avoid
# failing to relocate an rpath whose variable duplicate we deleted.
each_linkage_for(file, :rpaths, resolve_variable_references: true) do |bad_name|
next if !rooted_in_build_directory?(bad_name) && file.rpaths.count(bad_name) == 1
modified = delete_rpath(bad_name, file)
needs_codesigning ||= modified
end
# codesign the file if needed
codesign_patched_binary(file) if needs_codesigning
end
loader_name = loader_name_for(file, new_name)
modified = change_install_name(bad_name, loader_name, file) if loader_name != bad_name
needs_codesigning ||= modified
end
each_linkage_for(file, :rpaths) do |bad_name|
new_name = opt_name_for(bad_name)
loader_name = loader_name_for(file, new_name)
next if loader_name == bad_name
generic_fix_dynamic_linkage
end
modified = change_rpath(bad_name, loader_name, file)
needs_codesigning ||= modified
def loader_name_for(file, target)
# Use @loader_path-relative install names for other Homebrew-installed binaries.
if ENV["HOMEBREW_RELOCATABLE_INSTALL_NAMES"] && target.start_with?(HOMEBREW_PREFIX)
dylib_suffix = find_dylib_suffix_from(target)
target_dir = Pathname.new(target.delete_suffix(dylib_suffix)).cleanpath
"@loader_path/#{target_dir.relative_path_from(file.dirname)/dylib_suffix}"
else
target
end
end
# If file is a dylib or bundle itself, look for the dylib named by
# bad_name relative to the lib directory, so that we can skip the more
# expensive recursive search if possible.
def fixed_name(file, bad_name)
if bad_name.start_with? ::Keg::PREFIX_PLACEHOLDER
bad_name.sub(::Keg::PREFIX_PLACEHOLDER, HOMEBREW_PREFIX)
elsif bad_name.start_with? ::Keg::CELLAR_PLACEHOLDER
bad_name.sub(::Keg::CELLAR_PLACEHOLDER, HOMEBREW_CELLAR)
elsif (file.dylib? || file.mach_o_bundle?) && (file.dirname/bad_name).exist?
"@loader_path/#{bad_name}"
elsif file.mach_o_executable? && (lib/bad_name).exist?
"#{lib}/#{bad_name}"
elsif file.mach_o_executable? && (libexec/"lib"/bad_name).exist?
"#{libexec}/lib/#{bad_name}"
elsif (abs_name = find_dylib(bad_name)) && abs_name.exist?
abs_name.to_s
else
opoo "Could not fix #{bad_name} in #{file}"
bad_name
end
end
VARIABLE_REFERENCE_RX = /^@(loader_|executable_|r)path/
def each_linkage_for(file, linkage_type, resolve_variable_references: false, &block)
file.public_send(linkage_type, resolve_variable_references:)
.grep_v(VARIABLE_REFERENCE_RX)
.each(&block)
end
def dylib_id_for(file)
# The new dylib ID should have the same basename as the old dylib ID, not
# the basename of the file itself.
basename = File.basename(file.dylib_id)
relative_dirname = file.dirname.relative_path_from(path)
(opt_record/relative_dirname/basename).to_s
end
def relocated_name_for(old_name, relocation)
old_prefix, new_prefix = relocation.replacement_pair_for(:prefix)
old_cellar, new_cellar = relocation.replacement_pair_for(:cellar)
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
end
# Matches framework references like `XXX.framework/Versions/YYY/XXX` and
# `XXX.framework/XXX`, both with or without a slash-delimited prefix.
FRAMEWORK_RX = %r{(?:^|/)(([^/]+)\.framework/(?:Versions/[^/]+/)?\2)$}
def find_dylib_suffix_from(bad_name)
if (framework = bad_name.match(FRAMEWORK_RX))
framework[1]
else
File.basename(bad_name)
end
end
def find_dylib(bad_name)
return unless lib.directory?
suffix = "/#{find_dylib_suffix_from(bad_name)}"
lib.find { |pn| break pn if pn.to_s.end_with?(suffix) }
end
def mach_o_files
hardlinks = Set.new
mach_o_files = []
path.find do |pn|
next if pn.symlink? || pn.directory?
next if !pn.dylib? && !pn.mach_o_bundle? && !pn.mach_o_executable?
# if we've already processed a file, ignore its hardlinks (which have the same dev ID and inode)
# this prevents relocations from being performed on a binary more than once
next unless hardlinks.add? [pn.stat.dev, pn.stat.ino]
mach_o_files << pn
end
# Strip duplicate rpaths and rpaths rooted in the build directory.
# We do this separately from the rpath relocation above to avoid
# failing to relocate an rpath whose variable duplicate we deleted.
each_linkage_for(file, :rpaths, resolve_variable_references: true) do |bad_name|
next if !rooted_in_build_directory?(bad_name) && file.rpaths.count(bad_name) == 1
mach_o_files
end
modified = delete_rpath(bad_name, file)
needs_codesigning ||= modified
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 || name == "perl"
"#{HOMEBREW_PREFIX}/opt/perl/bin/perl"
elsif tab.built_on.present?
perl_path = "/usr/bin/perl#{tab.built_on["preferred_perl"]}"
# For `:all` bottles, we could have built this bottle with a Perl we don't have.
# Such bottles typically don't have strict version requirements.
perl_path = "/usr/bin/perl#{MacOS.preferred_perl_version}" unless File.exist?(perl_path)
perl_path
else
"/usr/bin/perl#{MacOS.preferred_perl_version}"
end
relocation.add_replacement_pair(:perl, ::Keg::PERL_PLACEHOLDER, perl_path)
if (openjdk = openjdk_dep_name_if_applicable)
openjdk_path = HOMEBREW_PREFIX/"opt"/openjdk/"libexec/openjdk.jdk/Contents/Home"
relocation.add_replacement_pair(:java, ::Keg::JAVA_PLACEHOLDER, openjdk_path.to_s)
end
# codesign the file if needed
codesign_patched_binary(file) if needs_codesigning
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.
"-lrO"
end
def egrep_args
grep_bin = "egrep"
grep_args = "--files-with-matches"
[grep_bin, grep_args]
end
private
CELLAR_RX = %r{\A#{HOMEBREW_CELLAR}/(?<formula_name>[^/]+)/[^/]+}
# Replace HOMEBREW_CELLAR references with HOMEBREW_PREFIX/opt references
# if the Cellar reference is to a different keg.
def opt_name_for(filename)
return filename unless filename.start_with?(HOMEBREW_PREFIX.to_s)
return filename if filename.start_with?(path.to_s)
return filename if (matches = CELLAR_RX.match(filename)).blank?
filename.sub(CELLAR_RX, "#{HOMEBREW_PREFIX}/opt/#{matches[:formula_name]}")
end
def rooted_in_build_directory?(filename)
# CMake normalises `/private/tmp` to `/tmp`.
# https://gitlab.kitware.com/cmake/cmake/-/issues/23251
return true if HOMEBREW_TEMP.to_s == "/private/tmp" && filename.start_with?("/tmp/")
filename.start_with?(HOMEBREW_TEMP.to_s) || filename.start_with?(HOMEBREW_TEMP.realpath.to_s)
end
end
generic_fix_dynamic_linkage
end
def loader_name_for(file, target)
# Use @loader_path-relative install names for other Homebrew-installed binaries.
if ENV["HOMEBREW_RELOCATABLE_INSTALL_NAMES"] && target.start_with?(HOMEBREW_PREFIX)
dylib_suffix = find_dylib_suffix_from(target)
target_dir = Pathname.new(target.delete_suffix(dylib_suffix)).cleanpath
"@loader_path/#{target_dir.relative_path_from(file.dirname)/dylib_suffix}"
else
target
end
end
# If file is a dylib or bundle itself, look for the dylib named by
# bad_name relative to the lib directory, so that we can skip the more
# expensive recursive search if possible.
def fixed_name(file, bad_name)
if bad_name.start_with? PREFIX_PLACEHOLDER
bad_name.sub(PREFIX_PLACEHOLDER, HOMEBREW_PREFIX)
elsif bad_name.start_with? CELLAR_PLACEHOLDER
bad_name.sub(CELLAR_PLACEHOLDER, HOMEBREW_CELLAR)
elsif (file.dylib? || file.mach_o_bundle?) && (file.dirname/bad_name).exist?
"@loader_path/#{bad_name}"
elsif file.mach_o_executable? && (lib/bad_name).exist?
"#{lib}/#{bad_name}"
elsif file.mach_o_executable? && (libexec/"lib"/bad_name).exist?
"#{libexec}/lib/#{bad_name}"
elsif (abs_name = find_dylib(bad_name)) && abs_name.exist?
abs_name.to_s
else
opoo "Could not fix #{bad_name} in #{file}"
bad_name
end
end
VARIABLE_REFERENCE_RX = /^@(loader_|executable_|r)path/
def each_linkage_for(file, linkage_type, resolve_variable_references: false, &block)
file.public_send(linkage_type, resolve_variable_references:)
.grep_v(VARIABLE_REFERENCE_RX)
.each(&block)
end
def dylib_id_for(file)
# The new dylib ID should have the same basename as the old dylib ID, not
# the basename of the file itself.
basename = File.basename(file.dylib_id)
relative_dirname = file.dirname.relative_path_from(path)
(opt_record/relative_dirname/basename).to_s
end
def relocated_name_for(old_name, relocation)
old_prefix, new_prefix = relocation.replacement_pair_for(:prefix)
old_cellar, new_cellar = relocation.replacement_pair_for(:cellar)
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
end
# Matches framework references like `XXX.framework/Versions/YYY/XXX` and
# `XXX.framework/XXX`, both with or without a slash-delimited prefix.
FRAMEWORK_RX = %r{(?:^|/)(([^/]+)\.framework/(?:Versions/[^/]+/)?\2)$}
def find_dylib_suffix_from(bad_name)
if (framework = bad_name.match(FRAMEWORK_RX))
framework[1]
else
File.basename(bad_name)
end
end
def find_dylib(bad_name)
return unless lib.directory?
suffix = "/#{find_dylib_suffix_from(bad_name)}"
lib.find { |pn| break pn if pn.to_s.end_with?(suffix) }
end
def mach_o_files
hardlinks = Set.new
mach_o_files = []
path.find do |pn|
next if pn.symlink? || pn.directory?
next if !pn.dylib? && !pn.mach_o_bundle? && !pn.mach_o_executable?
# if we've already processed a file, ignore its hardlinks (which have the same dev ID and inode)
# this prevents relocations from being performed on a binary more than once
next unless hardlinks.add? [pn.stat.dev, pn.stat.ino]
mach_o_files << pn
end
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 || name == "perl"
"#{HOMEBREW_PREFIX}/opt/perl/bin/perl"
elsif tab.built_on.present?
perl_path = "/usr/bin/perl#{tab.built_on["preferred_perl"]}"
# For `:all` bottles, we could have built this bottle with a Perl we don't have.
# Such bottles typically don't have strict version requirements.
perl_path = "/usr/bin/perl#{MacOS.preferred_perl_version}" unless File.exist?(perl_path)
perl_path
else
"/usr/bin/perl#{MacOS.preferred_perl_version}"
end
relocation.add_replacement_pair(:perl, PERL_PLACEHOLDER, perl_path)
if (openjdk = openjdk_dep_name_if_applicable)
openjdk_path = HOMEBREW_PREFIX/"opt"/openjdk/"libexec/openjdk.jdk/Contents/Home"
relocation.add_replacement_pair(:java, JAVA_PLACEHOLDER, openjdk_path.to_s)
end
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.
"-lrO"
end
def egrep_args
grep_bin = "egrep"
grep_args = "--files-with-matches"
[grep_bin, grep_args]
end
private
CELLAR_RX = %r{\A#{HOMEBREW_CELLAR}/(?<formula_name>[^/]+)/[^/]+}
# Replace HOMEBREW_CELLAR references with HOMEBREW_PREFIX/opt references
# if the Cellar reference is to a different keg.
def opt_name_for(filename)
return filename unless filename.start_with?(HOMEBREW_PREFIX.to_s)
return filename if filename.start_with?(path.to_s)
return filename if (matches = CELLAR_RX.match(filename)).blank?
filename.sub(CELLAR_RX, "#{HOMEBREW_PREFIX}/opt/#{matches[:formula_name]}")
end
def rooted_in_build_directory?(filename)
# CMake normalises `/private/tmp` to `/tmp`.
# https://gitlab.kitware.com/cmake/cmake/-/issues/23251
return true if HOMEBREW_TEMP.to_s == "/private/tmp" && filename.start_with?("/tmp/")
filename.start_with?(HOMEBREW_TEMP.to_s) || filename.start_with?(HOMEBREW_TEMP.realpath.to_s)
end
end
Keg.singleton_class.prepend(OS::Mac::Keg::ClassMethods)
Keg.prepend(OS::Mac::Keg)

View File

@ -9,7 +9,7 @@ module OS
requires_ancestor { Kernel }
sig { params(tap: Tap, os_name: T.nilable(Symbol), arch: T.nilable(Symbol)).returns(T::Boolean) }
def valid_casks?(tap, os_name: nil, arch: Hardware::CPU.type)
def valid_casks?(tap, os_name: nil, arch: ::Hardware::CPU.type)
return true if os_name == :linux
current_macos_version = if os_name.is_a?(Symbol)

View File

@ -13,7 +13,7 @@ module OS
sig { returns(Symbol) }
def current_os
Homebrew::SimulateSystem.os || MacOS.version.to_sym
::Homebrew::SimulateSystem.os || MacOS.version.to_sym
end
end
end

View File

@ -3,20 +3,26 @@
require "system_command"
module OS
module Mac
module SystemConfig
sig { returns(String) }
def describe_clang
return "N/A" if ::SystemConfig.clang.null?
clang_build_info = ::SystemConfig.clang_build.null? ? "(parse error)" : ::SystemConfig.clang_build
"#{::SystemConfig.clang} build #{clang_build_info}"
end
end
end
end
SystemConfig.prepend(OS::Mac::SystemConfig)
module SystemConfig
class << self
include SystemCommand::Mixin
undef describe_clang
sig { returns(String) }
def describe_clang
return "N/A" if clang.null?
clang_build_info = clang_build.null? ? "(parse error)" : clang_build
"#{clang} build #{clang_build_info}"
end
def xcode
@xcode ||= if MacOS::Xcode.installed?
xcode = MacOS::Xcode.version.to_s

6
Library/Homebrew/keg.rbi Normal file
View File

@ -0,0 +1,6 @@
# typed: strict
class Keg
# This is only true under MacOS, so can lead to true negative type errors
include OS::Mac::Keg
end

View File

@ -210,7 +210,6 @@ class Keg
# for GNU grep; overridden for BSD grep on OS X
"-lr"
end
alias generic_recursive_fgrep_args recursive_fgrep_args
def egrep_args
grep_bin = "grep"

View File

@ -151,7 +151,7 @@ module OS
# Xcode.prefix is pretty smart, so let's look inside to find the sdk
sdk_prefix = "#{Xcode.prefix}/Platforms/MacOSX.platform/Developer/SDKs"
# Finally query Xcode itself (this is slow, so check it last)
sdk_platform_path = Utils.popen_read(DevelopmentTools.locate("xcrun"), "--show-sdk-platform-path").chomp
sdk_platform_path = Utils.popen_read(::DevelopmentTools.locate("xcrun"), "--show-sdk-platform-path").chomp
sdk_prefix = File.join(sdk_platform_path, "Developer", "SDKs") unless File.directory? sdk_prefix
sdk_prefix

View File

@ -227,7 +227,7 @@ module OS
sig { returns(String) }
def self.detect_version_from_clang_version
version = DevelopmentTools.clang_version
version = ::DevelopmentTools.clang_version
return "dunno" if version.null?

View File

@ -87,7 +87,7 @@ class AbstractTab
"tap" => nil,
"tap_git_head" => nil,
},
"built_on" => DevelopmentTools.generic_build_system_info,
"built_on" => DevelopmentTools.build_system_info,
}
new(attributes)