493 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
			
		
		
	
	
			493 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
| # typed: true # rubocop:disable Sorbet/StrictSigil
 | |
| # frozen_string_literal: true
 | |
| 
 | |
| module OS
 | |
|   module Mac
 | |
|     module Diagnostic
 | |
|       class Volumes
 | |
|         def initialize
 | |
|           @volumes = get_mounts
 | |
|         end
 | |
| 
 | |
|         def which(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
 | |
| 
 | |
|         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
 | |
| 
 | |
|           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 }
 | |
| 
 | |
|         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
 | |
| 
 | |
|         def fatal_setup_build_environment_checks
 | |
|           %w[
 | |
|             check_xcode_minimum_version
 | |
|             check_clt_minimum_version
 | |
|             check_if_supported_sdk_available
 | |
|           ].freeze
 | |
|         end
 | |
| 
 | |
|         def supported_configuration_checks
 | |
|           %w[
 | |
|             check_for_unsupported_macos
 | |
|           ].freeze
 | |
|         end
 | |
| 
 | |
|         def build_from_source_checks
 | |
|           %w[
 | |
|             check_for_installed_developer_tools
 | |
|             check_xcode_up_to_date
 | |
|             check_clt_up_to_date
 | |
|           ].freeze
 | |
|         end
 | |
| 
 | |
|         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"
 | |
|           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
 | |
| 
 | |
|         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
 | |
| 
 | |
|           <<~EOS
 | |
|             You have booted macOS using OpenCore Legacy Patcher.
 | |
|             We do not provide support for this configuration.
 | |
|             #{please_create_pull_requests}
 | |
|           EOS
 | |
|         end
 | |
| 
 | |
|         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?
 | |
| 
 | |
|           # 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"]
 | |
| 
 | |
|           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
 | |
| 
 | |
|           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
 | |
|           message
 | |
|         end
 | |
| 
 | |
|         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
 | |
| 
 | |
|         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?
 | |
| 
 | |
|           <<~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.
 | |
|               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.
 | |
| 
 | |
|               tl;dr: delete these files:
 | |
|             EOS
 | |
|           end
 | |
|         end
 | |
| 
 | |
|         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
 | |
| 
 | |
|         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?
 | |
| 
 | |
|           <<~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"
 | |
|           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)
 | 
