Files
brew/Library/Homebrew/extend/os/mac/diagnostic.rb
T
Mike McQuaid 0a88a9467d Use codesign to sign patched binaries on Intel
- Since the switch to `MachO.codesign!` in ruby-macho 6.0, Intel macOS
  rejects the ad-hoc signatures on larger relocated binaries (e.g.
  `libruby`, the Python framework) and kills hardened-runtime programs
  such as MacVim's `vim` at launch with `CODESIGNING, Invalid Page`:
  https://github.com/Homebrew/brew/issues/23418
- The signatures have correct page hashes and pass `codesign --verify`
  on newer macOS, so this looks like an Intel macOS verifier quirk
  rather than simple corruption.
- Restore the pre-6.0 behaviour on Intel: leave unsigned binaries
  unsigned and use `codesign` to re-sign only the binaries whose
  existing signature our modifications have just broken, e.g. MacVim's
  Xcode-ad-hoc-signed hardened-runtime `vim`.
- Restore the fatal preinstall developer tools check, now skipped on
  Apple Silicon rather than gated to it, as `codesign` requires the
  Command Line Tools while ruby-macho does not.
- Keep `MachO.codesign!` on Apple Silicon, where it is required,
  proven and avoids a `codesign` subprocess per relocated file.
2026-08-04 13:12:07 +01:00

682 lines
24 KiB
Ruby

# typed: strict
# frozen_string_literal: true
require "extend/os/mac/pkgconf"
module OS
module Mac
module Diagnostic
class Volumes
sig { void }
def initialize
@volumes = T.let(get_mounts, T::Array[String])
end
sig { params(path: T.nilable(::Pathname)).returns(Integer) }
def index_of(path)
vols = get_mounts path
# 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
end
sig { params(path: T.nilable(::Pathname)).returns(T::Array[String]) }
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.to_s 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)
end
end
end
vols
end
end
module Checks
extend T::Helpers
requires_ancestor { Homebrew::Diagnostic::Checks }
sig { params(verbose: T::Boolean).void }
def initialize(verbose: true)
super
@found = T.let([], T::Array[String])
end
sig { returns(T::Array[String]) }
def fatal_preinstall_checks
checks = %w[
check_access_directories
]
# We need the developer tools for `codesign` on Intel:
# https://github.com/Homebrew/brew/issues/23418
checks << "check_for_installed_developer_tools" unless ::Hardware::CPU.arm?
checks.freeze
end
sig { returns(T::Array[String]) }
def fatal_build_from_source_checks
%w[
check_for_installed_developer_tools
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
sig { returns(T::Array[String]) }
def fatal_setup_build_environment_checks
%w[
check_xcode_minimum_version
check_clt_minimum_version
check_if_supported_sdk_available
].freeze
end
sig { returns(T::Array[String]) }
def supported_configuration_checks
%w[
check_for_unsupported_macos
].freeze
end
sig { returns(T::Array[String]) }
def build_from_source_checks
%w[
check_for_installed_developer_tools
check_xcode_up_to_date
check_clt_up_to_date
].freeze
end
sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) }
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)
::Homebrew::Diagnostic::Finding.new(
<<~EOS,
Putting non-prefixed findutils in your path can cause python builds to fail.
EOS
)
rescue FormulaUnavailableError
nil
end
sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) }
def check_for_unsupported_macos
return if Homebrew::EnvConfig.developer?
return if ENV["HOMEBREW_INTEGRATION_TEST"]
tier = 2
who = +"We"
remediation = nil
what = if OS::Mac.version.prerelease?
"pre-release version."
elsif OS::Mac.version.outdated_release?
tier = 3
who << " (and Apple)"
remediation = <<~EOS
You may have better luck with MacPorts which supports older versions of macOS:
#{Formatter.url("https://www.macports.org")}
EOS
"old version."
end
return if what.blank?
who.freeze
::Homebrew::Diagnostic::Finding.new(
<<~EOS,
You are using macOS #{MacOS.version}.
#{who} do not provide support for this #{what}
EOS
remediation:,
tier:,
)
end
sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) }
def check_for_opencore
return if ::Hardware::CPU.physical_cpu_arm64?
# https://dortania.github.io/OpenCore-Legacy-Patcher/UPDATE.html#checking-oclp-and-opencore-versions
begin
opencore_version = Utils.safe_popen_read("/usr/sbin/nvram",
"4D1FDA02-38C7-4A6A-9CC6-4BCCA8B30102:opencore-version").split[1]
oclp_version = Utils.safe_popen_read("/usr/sbin/nvram",
"4D1FDA02-38C7-4A6A-9CC6-4BCCA8B30102:OCLP-Version").split[1]
return if opencore_version.blank? || oclp_version.blank?
rescue ErrorDuringExecution
return
end
oclp_support_tier = if ::Hardware::CPU.features.include?(:pclmulqdq) && !OS::Mac.version.outdated_release?
2
else
3
end
::Homebrew::Diagnostic::Finding.new(
<<~EOS,
You have booted macOS using OpenCore Legacy Patcher.
We do not provide support for this configuration.
EOS
tier: oclp_support_tier,
)
end
sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) }
def check_xcode_up_to_date
return unless MacOS::Xcode.outdated?
# 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?
remediation = <<~EOS
Please update to Xcode #{MacOS::Xcode.latest_version} (or delete it).
#{MacOS::Xcode.update_instructions}
EOS
if OS::Mac.version.prerelease?
current_path = Utils.popen_read("/usr/bin/xcode-select", "-p")
remediation += <<~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
::Homebrew::Diagnostic::Finding.new(
"Your Xcode (#{MacOS::Xcode.version}) is outdated.",
tier: 2,
remediation:,
)
end
sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) }
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?
::Homebrew::Diagnostic::Finding.new(
"A newer Command Line Tools release is available.",
tier: 2,
remediation: MacOS::CLT.update_instructions,
)
end
sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) }
def check_xcode_minimum_version
return unless MacOS::Xcode.below_minimum_version?
xcode = MacOS::Xcode.version.to_s
xcode += " => #{MacOS::Xcode.prefix}" unless MacOS::Xcode.default_prefix?
::Homebrew::Diagnostic::Finding.new(
<<~EOS,
Your Xcode (#{xcode}) at #{MacOS::Xcode.bundle_path} is too outdated.
EOS
remediation: <<~EOS,
Please update to Xcode #{MacOS::Xcode.latest_version} (or delete it).
#{MacOS::Xcode.update_instructions}
EOS
)
end
sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) }
def check_clt_minimum_version
return unless MacOS::CLT.below_minimum_version?
::Homebrew::Diagnostic::Finding.new(
<<~EOS,
Your Command Line Tools are too outdated.
EOS
remediation: MacOS::CLT.update_instructions,
)
end
sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) }
def check_if_xcode_needs_clt_installed
return unless MacOS::Xcode.needs_clt_installed?
::Homebrew::Diagnostic::Finding.new(
<<~EOS,
Xcode alone is not sufficient on #{MacOS.version.pretty_name}.
EOS
remediation: ::DevelopmentTools.installation_instructions,
)
end
sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) }
def check_xcode_prefix
prefix = MacOS::Xcode.prefix
return if prefix.nil?
return unless prefix.to_s.include?(" ")
::Homebrew::Diagnostic::Finding.new(
<<~EOS,
Xcode is installed to a directory with a space in the name.
This will cause some formulae to fail to build.
EOS
)
end
sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) }
def check_xcode_prefix_exists
prefix = MacOS::Xcode.prefix
return if prefix.nil? || prefix.exist?
::Homebrew::Diagnostic::Finding.new(
<<~EOS,
The directory Xcode is reportedly installed to doesn't exist:
#{prefix}
EOS
remediation: <<~EOS,
You may need to `xcode-select` the proper path if you have moved Xcode.
EOS
)
end
sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) }
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?
::Homebrew::Diagnostic::Finding.new(
<<~EOS,
Your Xcode is configured with an invalid path.
EOS
remediation: ::Homebrew::Diagnostic::Finding::Remediation.new(
commands: ["sudo xcode-select --switch #{path}"],
text: <<~EOS,
You should change it to the correct path:
sudo xcode-select --switch #{path}
EOS
),
)
end
sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) }
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 --find clang 2>&1`.include?("license")
return if $CHILD_STATUS.success?
::Homebrew::Diagnostic::Finding.new(
<<~EOS,
You have not agreed to the Xcode license.
EOS
remediation: ::Homebrew::Diagnostic::Finding::Remediation.new(
commands: ["sudo xcodebuild -license"],
text: <<~EOS,
Agree to the license by opening Xcode.app or running:
sudo xcodebuild -license
EOS
),
)
end
sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) }
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!
::Homebrew::Diagnostic::Finding.new(
<<~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
sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) }
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
realpath.start_with?(*allowlist)
end
end
::Homebrew::Diagnostic::Finding.new(
<<~EOS,
gettext files detected at a system prefix.
These files can cause compilation and link failures, especially if they
are compiled with improper architectures.
EOS
remediation: <<~EOS,
Consider removing these files:
#{@found.join("\n ")}
EOS
)
end
sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) }
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?
::Homebrew::Diagnostic::Finding.new(
<<~EOS,
A libiconv formula is installed and linked.
This will break stuff. For serious. Unlink it.
EOS
)
end
else
::Homebrew::Diagnostic::Finding.new(
<<~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.
EOS
remediation: <<~EOS,
tl;dr: delete these files:
#{@found.join("\n")}
EOS
)
end
end
sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) }
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.index_of real_cellar
begin
tmp = ::Pathname.new(Dir.mktmpdir("doctor", HOMEBREW_TEMP))
begin
real_tmp = tmp.realpath.parent
where_tmp = volumes.index_of real_tmp
ensure
Dir.delete tmp.to_s
end
rescue
return
end
return if where_cellar == where_tmp
::Homebrew::Diagnostic::Finding.new(
<<~EOS,
Your Cellar and TEMP directories are on different volumes.
macOS won't move relative symlinks across volumes unless the target file already
exists. Formulae known to be affected by this are Git and Narwhal.
EOS
tier: 2,
remediation: <<~EOS,
You should set the `$HOMEBREW_TEMP` environment variable to a suitable
directory on the same volume as your Cellar.
EOS
)
end
sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) }
def check_if_supported_sdk_available
return unless ::DevelopmentTools.installed?
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
::Homebrew::Diagnostic::Finding.new(
<<~EOS,
Your #{source} does not support macOS #{MacOS.version}.
It is either outdated or was modified.
EOS
remediation: ::Homebrew::Diagnostic::Finding::Remediation.new(
text: <<~EOS,
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.
sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) }
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
remediation = ::Homebrew::Diagnostic::Finding::Remediation.new(
commands: ["sudo rm -rf #{path_to_remove}"],
text: <<~EOS,
Remove the broken installation before reinstalling
#{installation_instructions}
EOS
)
::Homebrew::Diagnostic::Finding.new(
<<~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.
EOS
remediation:,
)
end
sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) }
def check_cask_software_versions
super
add_info "macOS", MacOS.full_version
add_info "SIP", begin
csrutil = "/usr/bin/csrutil"
if File.executable?(csrutil)
Open3.capture2(csrutil, "status")
.first
.gsub("This is an unsupported configuration, likely to break in " \
"the future and leave your machine in an unknown state.", "")
.gsub("System Integrity Protection status: ", "")
.delete("\t.")
.capitalize
.strip
else
"N/A"
end
end
nil
end
sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) }
def check_pkgconf_macos_sdk_mismatch
mismatch = Homebrew::Pkgconf.macos_sdk_mismatch
return unless mismatch
::Homebrew::Diagnostic::Finding.new(
Homebrew::Pkgconf.mismatch_warning_message(mismatch),
)
end
sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) }
def check_cask_quarantine_support
status, check_output = ::Cask::Quarantine.check_quarantine_support
messages = case status
when :quarantine_available
[nil, nil]
when :xattr_broken
["No Cask quarantine support available: there's no working version of `xattr` on this system.", nil]
when :no_swift
["No Cask quarantine support available: there's no available version of `swift` on this system.", nil]
when :swift_broken_clt
["No Cask quarantine support available: Swift is not working due to missing Command Line Tools.", MacOS::CLT.installation_then_reinstall_instructions]
when :swift_compilation_failed
msg = <<~EOS
No Cask quarantine support available: Swift compilation failed.
This is usually due to a broken or incompatible Command Line Tools installation.
EOS
[msg, MacOS::CLT.installation_then_reinstall_instructions]
when :swift_runtime_error
msg = <<~EOS
No Cask quarantine support available: Swift runtime error.
Your Command Line Tools installation may be broken or incomplete.
EOS
[msg, MacOS::CLT.installation_then_reinstall_instructions]
when :swift_not_executable
msg = <<~EOS
No Cask quarantine support available: Swift is not executable.
Your Command Line Tools installation may be incomplete.
EOS
[msg, MacOS::CLT.installation_then_reinstall_instructions]
when :swift_unexpected_error
msg = <<~EOS
No Cask quarantine support available: Swift returned an unexpected error:
#{check_output}
EOS
[msg, nil]
else
msg = <<~EOS
No Cask quarantine support available: unknown reason: #{status.inspect}:
#{check_output}
EOS
[msg, nil]
end
return unless messages.first.present?
::Homebrew::Diagnostic::Finding.new(
T.must(messages.first),
remediation: messages.last,
)
end
end
end
end
end
Homebrew::Diagnostic::Checks.prepend(OS::Mac::Diagnostic::Checks)