From d9f7e516bac92d320e3994434806e56c6c938ecd Mon Sep 17 00:00:00 2001 From: Sean Molenaar Date: Fri, 29 May 2026 15:05:04 +0200 Subject: [PATCH 1/4] Add JSON output method for doctor --- Library/Homebrew/cmd/doctor.rb | 28 +- Library/Homebrew/diagnostic.rb | 927 +++++++++++------- .../Homebrew/extend/os/linux/diagnostic.rb | 256 +++-- Library/Homebrew/extend/os/mac/diagnostic.rb | 349 ++++--- Library/Homebrew/extend/os/mac/pkgconf.rb | 27 +- Library/Homebrew/extend/os/mac/reinstall.rb | 2 +- Library/Homebrew/install.rb | 2 +- .../sorbet/rbi/dsl/homebrew/cmd/doctor.rbi | 3 + Library/Homebrew/test/cmd/doctor_spec.rb | 9 +- .../Homebrew/test/diagnostic_checks_spec.rb | 54 +- .../Homebrew/test/os/linux/diagnostic_spec.rb | 22 +- .../Homebrew/test/os/mac/diagnostic_spec.rb | 50 +- .../test/support/helper/integration_mocks.rb | 2 +- 13 files changed, 1075 insertions(+), 656 deletions(-) diff --git a/Library/Homebrew/cmd/doctor.rb b/Library/Homebrew/cmd/doctor.rb index a5a8256d3cb60..fd533dc7e67db 100644 --- a/Library/Homebrew/cmd/doctor.rb +++ b/Library/Homebrew/cmd/doctor.rb @@ -4,6 +4,7 @@ require "abstract_command" require "diagnostic" require "cask/caskroom" +require "json" module Homebrew module Cmd @@ -20,6 +21,8 @@ class Doctor < AbstractCommand switch "--list-checks", description: "List all audit methods, which can be run individually " \ "if provided as arguments." + switch "--json", + description: "Print a JSON representation." switch "-D", "--audit-debug", description: "Enable debugging and profiling of audit methods." @@ -48,6 +51,7 @@ def run methods = args.named end + findings = [] first_warning = T.let(true, T::Boolean) methods.each do |method| $stderr.puts Formatter.headline("Checking #{method}", color: :magenta) if args.debug? @@ -56,8 +60,20 @@ def run next end - out = checks.send(method) - next if out.blank? + finding = checks.send(method) + + return_findings = if finding.is_a?(Array) + T.let(finding.compact, T::Array[Diagnostic::Finding]) + else + T.let([finding].compact, T::Array[Diagnostic::Finding]) + end + + next if return_findings.empty? + + if args.json? + findings.concat(return_findings.compact.map(&:to_h)) + next + end if first_warning && !args.quiet? $stderr.puts <<~EOS @@ -68,11 +84,17 @@ def run end $stderr.puts - opoo out + opoo return_findings.each(&:to_s).join("\n") Homebrew.failed = true first_warning = false end + if args.json? + tier = findings.max_by { |f| f[:tier] }&.fetch(:tier, 1) + puts JSON.pretty_generate({ tier: tier, findings: findings }).gsub(/\[\n\n\s*\]/, "[]") + return + end + puts "Your system is ready to brew." if !Homebrew.failed? && !args.quiet? end end diff --git a/Library/Homebrew/diagnostic.rb b/Library/Homebrew/diagnostic.rb index f493938c3a560..cb1a424042a4a 100644 --- a/Library/Homebrew/diagnostic.rb +++ b/Library/Homebrew/diagnostic.rb @@ -21,6 +21,112 @@ module Homebrew module Diagnostic extend Utils::Output::Mixin + class Finding + class Remediation + sig { returns(String) } + attr_reader :text + + sig { returns(T::Array[String]) } + attr_reader :commands + + sig { params(commands: T::Array[String], text: String).void } + def initialize(commands: [], text: "") + @commands = commands + @text = text + end + + sig { returns(String) } + def to_s + return "" if @commands.empty? && @text.empty? + + @text.presence || "You can solve this by running:\n #{@commands.join("\n ")}" + end + + sig { returns(T::Hash[Symbol, T.any(String, T::Array[String])]) } + def to_h + { commands: @commands, text: @text } + end + end + + sig { returns(T.nilable(String)) } + attr_reader :issue + + sig { returns(T.any(Integer, Symbol)) } + attr_reader :tier + + sig { returns(T::Array[String]) } + attr_reader :affects + + sig { returns(T::Array[String]) } + attr_reader :links + + sig { returns(T.nilable(Remediation)) } + attr_reader :remediation + + sig { params(issue: String, tier: T.any(Integer, Symbol), affects: T::Array[String], links: T::Array[String], remediation: T.any(T.nilable(Remediation), String)).void } + def initialize(issue:, tier: 1, affects: [], links: [], remediation: nil) + @issue = issue + @tier = tier + @affects = affects + @links = links + @remediation = T.let(if remediation.is_a?(String) + Remediation.new(text: remediation) + else + remediation + end, T.nilable(Homebrew::Diagnostic::Finding::Remediation)) + end + + sig { + returns(T::Hash[Symbol, + T.any(Integer, Symbol, String, T::Array[String], T.nilable(T::Hash[Symbol, T.any(String, T::Array[String])]))]) + } + def to_h + { + issue: @issue, + tier: @tier, + affects: @affects, + links: @links, + remediation: @remediation.to_h, + } + end + + sig { returns(String) } + def to_s + <<~EOS.rstrip + #{issue} + #{remediation.to_s.strip} + #{support_tier_message(tier: tier)} + EOS + end + + sig { params(tier: T.any(Integer, String, Symbol)).returns(T.nilable(String)) } + def support_tier_message(tier:) + return if tier.to_s == "1" + + if tier == :nix + return <<~EOS + This is a Tier 3 configuration: + #{Formatter.url("https://docs.brew.sh/Support-Tiers#tier-3")} + #{Formatter.bold("Report issues to the upstream Nix project, not Homebrew/* repositories:")} + #{Formatter.url(OS.nix_managed_homebrew_issues_url)} + EOS + end + + tier_title, tier_slug, tier_issues = if tier.to_s == "unsupported" + ["Unsupported", "unsupported", "Do not report any issues"] + else + ["Tier #{tier}", "tier-#{tier.to_s.downcase}", "You can report issues with Tier #{tier} configurations"] + end + + <<~EOS + This is a #{tier_title} configuration: + #{Formatter.url("https://docs.brew.sh/Support-Tiers##{tier_slug}")} + #{Formatter.bold("#{tier_issues} to Homebrew/* repositories!")} + Read the above document before opening any issues or PRs. + EOS + end + end + sig { params(type: Symbol, fatal: T::Boolean).void } def self.checks(type, fatal: true) @checks ||= T.let(Checks.new, T.nilable(Checks)) @@ -31,9 +137,9 @@ def self.checks(type, fatal: true) if fatal failed ||= true - ofail out + ofail out.to_s else - opoo out + opoo out.to_s end end exit 1 if failed && fatal @@ -133,81 +239,68 @@ def build_error_checks supported_configuration_checks + build_from_source_checks end - sig { params(tier: T.any(Integer, String, Symbol)).returns(T.nilable(String)) } - def support_tier_message(tier:) - return if tier.to_s == "1" - - tier_title, tier_slug, tier_issues = if tier.to_s == "unsupported" - ["Unsupported", "unsupported", "Do not report any issues"] - else - ["Tier #{tier}", "tier-#{tier.to_s.downcase}", "You can report issues with Tier #{tier} configurations"] - end - - <<~EOS - This is a #{tier_title} configuration: - #{Formatter.url("https://docs.brew.sh/Support-Tiers##{tier_slug}")} - #{Formatter.bold("#{tier_issues} to Homebrew/* repositories!")} - Read the above document before opening any issues or PRs. - EOS - end - - sig { params(repository_path: GitRepository, desired_origin: String).returns(T.nilable(String)) } + sig { params(repository_path: GitRepository, desired_origin: String).returns(T.nilable(Finding)) } def examine_git_origin(repository_path, desired_origin) return if !Utils::Git.available? || !repository_path.git_repository? current_origin = repository_path.origin_url if current_origin.nil? - <<~EOS - Missing #{desired_origin} git origin remote. - - Without a correctly configured origin, Homebrew won't update - properly. You can solve this by adding the remote: - git -C "#{repository_path}" remote add origin #{Formatter.url(desired_origin)} - EOS + Finding.new( + issue: "Without a correctly configured origin, Homebrew won't update + properly.", + remediation: Finding::Remediation.new(text: "You can solve this by adding the remote", commands: [ + "git -C \"#{repository_path}\" remote add origin #{Formatter.url(desired_origin)}", + ]), + ) elsif !current_origin.match?(%r{#{desired_origin}(\.git|/)?$}i) - <<~EOS - Suspicious #{desired_origin} git origin remote found. + issue = <<~EOS The current git origin is: #{current_origin} With a non-standard origin, Homebrew won't update properly. - You can solve this by setting the origin remote: - git -C "#{repository_path}" remote set-url origin #{Formatter.url(desired_origin)} EOS + Finding.new( + issue: issue, + remediation: Finding::Remediation.new(text: "You can solve this by setting the origin remote", commands: [ + "git -C \"#{repository_path}\" remote set-url origin #{Formatter.url(desired_origin)}", + ]), + ) end end - sig { params(tap: Tap).returns(T.nilable(String)) } + sig { params(tap: Tap).returns(T.nilable(Finding)) } def broken_tap(tap) return unless Utils::Git.available? repo = GitRepository.new(HOMEBREW_REPOSITORY) return unless repo.git_repository? - message = <<~EOS - #{tap.full_name} was not tapped properly! Run: - rm -rf "#{tap.path}" - brew tap #{tap.name} - EOS + finding = Finding.new( + issue: "#{tap.full_name} was not tapped properly!", + remediation: Finding::Remediation.new(text: "You can solve this by tapping again", commands: [ + "rm -rf \"#{tap.path}\"", + "brew tap #{tap.name}", + ]), + ) - return message if tap.remote.blank? + return finding if tap.remote.blank? tap_head = tap.git_head - return message if tap_head.blank? + return finding if tap_head.blank? return if tap_head != repo.head_ref - message + finding end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_for_installed_developer_tools return if DevelopmentTools.installed? - <<~EOS - No developer tools installed. - #{DevelopmentTools.installation_instructions} - EOS + Finding.new( + issue: "", + remediation: Finding::Remediation.new(text: DevelopmentTools.installation_instructions), + ) end sig { params(dir: String, pattern: String, allow_list: T::Array[String], message: String).returns(T.nilable(String)) } @@ -228,7 +321,7 @@ def __check_stray_files(dir, pattern, allow_list, message) inject_file_list(files, message) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_for_stray_dylibs # Dylibs which are generally OK should be added to this list, # with a short description of the software they come with. @@ -254,16 +347,17 @@ def check_for_stray_dylibs "sentinel-*.dylib", # SentinelOne ] - __check_stray_files "/usr/local/lib", "*.dylib", allow_list, <<~EOS + msg = __check_stray_files "/usr/local/lib", "*.dylib", allow_list, <<~EOS Unbrewed dylibs were found in /usr/local/lib. If you didn't put them there on purpose they could cause problems when building Homebrew formulae and may need to be deleted. Unexpected dylibs: EOS + Finding.new(issue: msg) if msg.present? end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_for_stray_static_libs # Static libs which are generally OK should be added to this list, # with a short description of the software they come with. @@ -282,16 +376,18 @@ def check_for_stray_static_libs "libtrustedcomponents.a", # Symantec Endpoint Protection ] - __check_stray_files "/usr/local/lib", "*.a", allow_list, <<~EOS + msg = __check_stray_files "/usr/local/lib", "*.a", allow_list, <<~EOS Unbrewed static libraries were found in /usr/local/lib. If you didn't put them there on purpose they could cause problems when building Homebrew formulae and may need to be deleted. Unexpected static libraries: EOS + + Finding.new(issue: msg) if msg.present? end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_for_stray_pcs # Package-config files which are generally OK should be added to this list, # with a short description of the software they come with. @@ -305,16 +401,18 @@ def check_for_stray_pcs "libublio.pc", # NTFS-3G ] - __check_stray_files "/usr/local/lib/pkgconfig", "*.pc", allow_list, <<~EOS + msg = __check_stray_files("/usr/local/lib/pkgconfig", "*.pc", allow_list, <<~EOS Unbrewed '.pc' files were found in /usr/local/lib/pkgconfig. If you didn't put them there on purpose they could cause problems when building Homebrew formulae and may need to be deleted. Unexpected '.pc' files: EOS + ) + Finding.new(issue: msg) if msg.present? end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_for_stray_las allow_list = [ "libfuse.la", # MacFuse @@ -327,16 +425,18 @@ def check_for_stray_las "libublio.la", # NTFS-3G ] - __check_stray_files "/usr/local/lib", "*.la", allow_list, <<~EOS + msg = __check_stray_files("/usr/local/lib", "*.la", allow_list, <<~EOS Unbrewed '.la' files were found in /usr/local/lib. If you didn't put them there on purpose they could cause problems when building Homebrew formulae and may need to be deleted. Unexpected '.la' files: EOS + ) + Finding.new(issue: msg) if msg.present? end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_for_stray_headers allow_list = [ "fuse.h", # MacFuse @@ -348,16 +448,18 @@ def check_for_stray_headers "ntfs-3g/**/*.h", # NTFS-3G ] - __check_stray_files "/usr/local/include", "**/*.h", allow_list, <<~EOS + msg = __check_stray_files "/usr/local/include", "**/*.h", allow_list, <<~EOS Unbrewed header files were found in /usr/local/include. If you didn't put them there on purpose they could cause problems when building Homebrew formulae and may need to be deleted. Unexpected header files: EOS + + Finding.new(issue: msg) if msg.present? end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_for_broken_symlinks broken_symlinks = [] @@ -370,78 +472,99 @@ def check_for_broken_symlinks end return if broken_symlinks.empty? - inject_file_list broken_symlinks, <<~EOS - Broken symlinks were found. Remove them with `brew cleanup`: - EOS + Finding.new( + issue: inject_file_list(broken_symlinks, <<~EOS + Broken symlinks were found: + EOS + ), + remediation: <<~EOS, + Remove them with `brew cleanup` + EOS + ) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_tmpdir_sticky_bit world_writable = HOMEBREW_TEMP.stat.mode & 0777 == 0777 return if !world_writable || HOMEBREW_TEMP.sticky? - <<~EOS - #{HOMEBREW_TEMP} is world-writable but does not have the sticky bit set. - To set it, run the following command: - sudo chmod +t #{HOMEBREW_TEMP} - EOS + Finding.new( + issue: <<~EOS, + #{HOMEBREW_TEMP} is world-writable but does not have the sticky bit set. + EOS + remediation: <<~EOS, + To set it, run the following command: + sudo chmod +t #{HOMEBREW_TEMP} + EOS + ) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_exist_directories return if HOMEBREW_PREFIX.writable? not_exist_dirs = Keg.must_exist_directories.reject(&:exist?) return if not_exist_dirs.empty? - <<~EOS - The following directories do not exist: - #{not_exist_dirs.join("\n")} - - You should create these directories and change their ownership to your user. - sudo mkdir -p #{not_exist_dirs.join(" ")} - sudo chown -R #{current_user} #{not_exist_dirs.join(" ")} - EOS + Finding.new( + issue: <<~EOS, + The following directories do not exist: + #{not_exist_dirs.join("\n")} + EOS + remediation: <<~EOS, + You should create these directories and change their ownership to your user. + sudo mkdir -p #{not_exist_dirs.join(" ")} + sudo chown -R #{current_user} #{not_exist_dirs.join(" ")} + EOS + ) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_access_directories not_writable_dirs = Keg.must_be_writable_directories.select(&:exist?) .reject(&:writable?) return if not_writable_dirs.empty? - <<~EOS - The following directories are not writable by your user: - #{not_writable_dirs.join("\n")} - - You should change the ownership of these directories to your user. - sudo chown -R #{current_user} #{not_writable_dirs.join(" ")} + Finding.new( + issue: <<~EOS, + The following directories are not writable by your user: + #{not_writable_dirs.join("\n")} + EOS + remediation: <<~EOS, + You should change the ownership of these directories to your user. + sudo chown -R #{current_user} #{not_writable_dirs.join(" ")} - And make sure that your user has write permission. - chmod u+w #{not_writable_dirs.join(" ")} - EOS + And make sure that your user has write permission. + chmod u+w #{not_writable_dirs.join(" ")} + EOS + ) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_multiple_cellars return if HOMEBREW_PREFIX.to_s == HOMEBREW_REPOSITORY.to_s return unless (HOMEBREW_REPOSITORY/"Cellar").exist? return unless (HOMEBREW_PREFIX/"Cellar").exist? - <<~EOS - You have multiple Cellars. - You should delete #{HOMEBREW_REPOSITORY}/Cellar: - rm -rf #{HOMEBREW_REPOSITORY}/Cellar - EOS + Finding.new( + issue: <<~EOS, + You have multiple Cellars. + EOS + remediation: <<~EOS, + You should delete #{HOMEBREW_REPOSITORY}/Cellar: + rm -rf #{HOMEBREW_REPOSITORY}/Cellar + EOS + ) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_user_path_1 @seen_prefix_bin = false @seen_prefix_sbin = false message = "" + remediation = T.let(nil, T.nilable(String)) paths.each do |p| case p @@ -457,12 +580,15 @@ def check_user_path_1 message = inject_file_list conflicts, <<~EOS /usr/bin occurs before #{HOMEBREW_PREFIX}/bin in your PATH. This means that system-provided programs will be used instead of those - provided by Homebrew. Consider setting your PATH so that - #{HOMEBREW_PREFIX}/bin occurs before /usr/bin. Here is a one-liner: - #{Utils::Shell.prepend_path_in_profile("#{HOMEBREW_PREFIX}/bin")} + provided by Homebrew. The following tools exist at both paths: EOS + remediation = <<~EOS + Consider setting your PATH so that + #{HOMEBREW_PREFIX}/bin occurs before /usr/bin. Here is a one-liner: + #{Utils::Shell.prepend_path_in_profile("#{HOMEBREW_PREFIX}/bin")} + EOS end end when "#{HOMEBREW_PREFIX}/bin" @@ -473,22 +599,26 @@ def check_user_path_1 end @user_path_1_done = true - message unless message.empty? + Finding.new(issue: message, remediation: remediation) if message.present? end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_user_path_2 check_user_path_1 unless @user_path_1_done return if @seen_prefix_bin - <<~EOS - Homebrew's "bin" was not found in your PATH. - Consider setting your PATH for example like so: - #{Utils::Shell.prepend_path_in_profile("#{HOMEBREW_PREFIX}/bin")} - EOS + Finding.new( + issue: <<~EOS, + Homebrew's "bin" was not found in your PATH. + EOS + remediation: <<~EOS, + Consider setting your PATH for example like so: + #{Utils::Shell.prepend_path_in_profile("#{HOMEBREW_PREFIX}/bin")} + EOS + ) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_user_path_3 check_user_path_1 unless @user_path_1_done return if @seen_prefix_sbin @@ -499,35 +629,41 @@ def check_user_path_3 return if sbin.children.empty? return if sbin.children.one? && sbin.children.first.basename.to_s == ".keepme" - <<~EOS - Homebrew's "sbin" was not found in your PATH but you have installed - formulae that put executables in #{HOMEBREW_PREFIX}/sbin. - Consider setting your PATH for example like so: - #{Utils::Shell.prepend_path_in_profile("#{HOMEBREW_PREFIX}/sbin")} - EOS + Finding.new( + issue: <<~EOS, + Homebrew's "sbin" was not found in your PATH but you have installed + formulae that put executables in #{HOMEBREW_PREFIX}/sbin. + EOS + remediation: <<~EOS, + Consider setting your PATH for example like so: + #{Utils::Shell.prepend_path_in_profile("#{HOMEBREW_PREFIX}/sbin")} + EOS + ) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_for_symlinked_cellar return unless HOMEBREW_CELLAR.exist? return unless HOMEBREW_CELLAR.symlink? - <<~EOS - Symlinked Cellars can cause problems. - Your Homebrew Cellar is a symlink: #{HOMEBREW_CELLAR} - which resolves to: #{HOMEBREW_CELLAR.realpath} + Finding.new( + issue: <<~EOS, + Symlinked Cellars can cause problems. + Your Homebrew Cellar is a symlink: #{HOMEBREW_CELLAR} + which resolves to: #{HOMEBREW_CELLAR.realpath} - The recommended Homebrew installations are either: - (A) Have Cellar be a real directory inside of your `$HOMEBREW_PREFIX` - (B) Symlink "bin/brew" into your prefix, but don't symlink "Cellar". + The recommended Homebrew installations are either: + (A) Have Cellar be a real directory inside of your `$HOMEBREW_PREFIX` + (B) Symlink "bin/brew" into your prefix, but don't symlink "Cellar". - Older installations of Homebrew may have created a symlinked Cellar, but this can - cause problems when two formulae install to locations that are mapped on top of each - other during the linking step. - EOS + Older installations of Homebrew may have created a symlinked Cellar, but this can + cause problems when two formulae install to locations that are mapped on top of each + other during the linking step. + EOS + ) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_git_version minimum_version = ENV.fetch("HOMEBREW_MINIMUM_GIT_VERSION") return unless Utils::Git.available? @@ -535,46 +671,58 @@ def check_git_version git = Formula["git"] git_upgrade_cmd = git.any_version_installed? ? "upgrade" : "install" - <<~EOS - An outdated version (#{Utils::Git.version}) of Git was detected in your PATH. - Git #{minimum_version} or newer is required for Homebrew. - Please upgrade: - brew #{git_upgrade_cmd} git - EOS + Finding.new( + issue: <<~EOS, + An outdated version (#{Utils::Git.version}) of Git was detected in your PATH. + Git #{minimum_version} or newer is required for Homebrew. + EOS + remediation: <<~EOS, + Please upgrade: + brew #{git_upgrade_cmd} git + EOS + ) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_for_git return if Utils::Git.available? - <<~EOS - Git could not be found in your PATH. - Homebrew uses Git for several internal functions and some formulae use Git - checkouts instead of stable tarballs. You may want to install Git: - brew install git - EOS + Finding.new( + issue: <<~EOS, + Git could not be found in your PATH. + Homebrew uses Git for several internal functions and some formulae use Git + checkouts instead of stable tarballs. + EOS + remediation: <<~EOS, + You may want to install Git: + brew install git + EOS + ) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_git_newline_settings return unless Utils::Git.available? autocrlf = HOMEBREW_REPOSITORY.cd { `git config --get core.autocrlf`.chomp } return if autocrlf != "true" - <<~EOS - Suspicious Git newline settings found. - - The detected Git newline settings will cause checkout problems: - core.autocrlf = #{autocrlf} + Finding.new( + issue: <<~EOS, + Suspicious Git newline settings found. - If you are not routinely dealing with Windows-based projects, - consider removing these by running: - git config --global core.autocrlf input - EOS + The detected Git newline settings will cause checkout problems: + core.autocrlf = #{autocrlf} + EOS + remediation: <<~EOS, + If you are not routinely dealing with Windows-based projects, + consider removing these by running: + git config --global core.autocrlf input + EOS + ) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_homebrew_repository_git_hooks found = T.let([], T::Array[Pathname]) @@ -587,38 +735,39 @@ def check_homebrew_repository_git_hooks found << gitconfig if gitconfig.exist? return if found.empty? - inject_file_list found, <<~EOS - Git hooks or a repository-local `.gitconfig` were found in your Homebrew repository. - Homebrew does not use these, and they can break Homebrew operations. - Remove them with: - rm -rf "#{HOMEBREW_REPOSITORY}/.git/hooks" "#{HOMEBREW_REPOSITORY}/.gitconfig" + Finding.new( + issue: inject_file_list(found, <<~EOS + Git hooks or a repository-local `.gitconfig` were found in your Homebrew repository. + Homebrew does not use these, and they can break Homebrew operations. - Paths found: - EOS + Paths found: + EOS + ), + remediation: <<~EOS, + Remove them with: + rm -rf "#{HOMEBREW_REPOSITORY}/.git/hooks" "#{HOMEBREW_REPOSITORY}/.gitconfig" + EOS + ) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_brew_git_origin repo = GitRepository.new(HOMEBREW_REPOSITORY) examine_git_origin(repo, Homebrew::EnvConfig.brew_git_remote) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_for_nix_homebrew return unless OS.nix_managed_homebrew? - <<~EOS + Finding.new(tier: :nix, issue: <<~EOS, Your Homebrew installation is managed by Nix. Homebrew does not support Nix-managed installations. - - This is a Tier 3 configuration: - #{Formatter.url("https://docs.brew.sh/Support-Tiers#tier-3")} - #{Formatter.bold("Report issues to the upstream Nix project, not Homebrew/* repositories:")} - #{Formatter.url(OS.nix_managed_homebrew_issues_url)} EOS + ) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_coretap_integrity core_tap = CoreTap.instance unless core_tap.installed? @@ -630,7 +779,7 @@ def check_coretap_integrity broken_tap(core_tap) || examine_git_origin(core_tap.git_repository, Homebrew::EnvConfig.core_git_remote) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_casktap_integrity core_cask_tap = CoreCaskTap.instance return unless core_cask_tap.installed? @@ -638,7 +787,7 @@ def check_casktap_integrity broken_tap(core_cask_tap) || examine_git_origin(core_cask_tap.git_repository, T.must(core_cask_tap.remote)) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_tap_git_branch return if ENV["CI"] return unless Utils::Git.available? @@ -669,19 +818,22 @@ def check_tap_git_branch EOS end + remediation = nil if commands.any? message << "\n" if deprecated_master.any? message << <<~EOS - Some taps are not on the default git origin branch and may not receive - updates. If this is a surprise to you, check out the default branch with: + Some taps are not on the default git origin branch and may not receive updates. + EOS + remediation = Finding::Remediation.new(text: <<~EOS, commands: commands) + If this is a surprise to you, check out the default branch with: #{commands.join("\n ")} EOS end - message.presence + Finding.new(issue: message, remediation: remediation) if message.present? end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_deprecated_official_taps tapped_deprecated_taps = Tap.select(&:official?).map(&:repository) & DEPRECATED_OFFICIAL_TAPS @@ -691,14 +843,18 @@ def check_deprecated_official_taps return if tapped_deprecated_taps.empty? - <<~EOS - You have the following deprecated, official taps tapped: - Homebrew/homebrew-#{tapped_deprecated_taps.join("\n Homebrew/homebrew-")} - Untap them with `brew untap`. - EOS + Finding.new( + issue: <<~EOS, + You have the following deprecated, official taps tapped: + Homebrew/homebrew-#{tapped_deprecated_taps.join("\n Homebrew/homebrew-")} + EOS + remediation: <<~EOS, + Untap them with `brew untap`. + EOS + ) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_untrusted_taps return if Homebrew::EnvConfig.no_require_tap_trust? @@ -787,13 +943,15 @@ def check_untrusted_taps #{Formatter.url("https://docs.brew.sh/Tap-Trust")} EOS - <<~EOS - The following taps are not trusted: - #{untrusted_tap_names.join("\n ")} + Finding.new( + issue: <<~EOS, + The following taps are not trusted: + #{untrusted_tap_names.join("\n ")} - Homebrew is currently ignoring formulae, casks and commands from these taps because tap trust is required. - #{trust_messages.join("\n")} - EOS + Homebrew is currently ignoring formulae, casks and commands from these taps because tap trust is required. + EOS + remediation: trust_messages.join("\n"), + ) end sig { params(formula: Formula).returns(T::Boolean) } @@ -810,7 +968,7 @@ def __check_linked_brew!(formula) false end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_for_other_frameworks # Other frameworks that are known to cause problems when present frameworks_to_check = %w[ @@ -823,24 +981,31 @@ def check_for_other_frameworks .select { |framework| File.exist? framework } return if frameworks_found.empty? - inject_file_list frameworks_found, <<~EOS - Some frameworks can be picked up by CMake's build system and will likely - cause the build to fail. To compile CMake, you may wish to move these - out of the way: - EOS + Finding.new( + issue: <<~EOS, + Some frameworks can be picked up by CMake's build system and will likely + cause the build to fail. + EOS + remediation: <<~EOS, + To compile CMake, you may wish to move these out of the way: + #{frameworks_found.join("\n")} + EOS + ) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_tmpdir tmpdir = ENV.fetch("TMPDIR", nil) return if tmpdir.nil? || File.directory?(tmpdir) - <<~EOS - TMPDIR #{tmpdir.inspect} doesn't exist. - EOS + Finding.new( + issue: <<~EOS, + TMPDIR #{tmpdir.inspect} doesn't exist. + EOS + ) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_missing_deps return if !HOMEBREW_CELLAR.exist? && !Cask::Caskroom.path.exist? @@ -850,47 +1015,56 @@ def check_missing_deps end return if missing.empty? - <<~EOS - Some installed formulae or casks are missing dependencies. - You should `brew install` the missing dependencies: - brew install #{missing.sort * " "} - - Run `brew missing` for more details. - EOS + Finding.new( + issue: <<~EOS, + Some installed formulae or casks are missing dependencies. + Run `brew missing` for more details. + EOS + remediation: <<~EOS, + You should `brew install` the missing dependencies: + brew install #{missing.sort * " "} + EOS + ) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_deprecated_disabled return unless HOMEBREW_CELLAR.exist? deprecated_or_disabled = Formula.installed.select { |f| f.deprecated? || f.disabled? } return if deprecated_or_disabled.empty? - <<~EOS - Some installed formulae are deprecated or disabled. - You should find replacements for the following formulae: + Finding.new( + affects: deprecated_or_disabled.map(&:full_name), + issue: "Some installed formulae are deprecated or disabled.", + remediation: <<~EOS, + You should find replacements for the following formulae: #{deprecated_or_disabled.sort_by(&:full_name).uniq * "\n "} - EOS + EOS + ) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_cask_deprecated_disabled deprecated_or_disabled = Cask::Caskroom.casks.select(&:deprecated?) deprecated_or_disabled += Cask::Caskroom.casks.select(&:disabled?) return if deprecated_or_disabled.empty? - <<~EOS - Some installed casks are deprecated or disabled. - You should find replacements for the following casks: + Finding.new( + affects: deprecated_or_disabled.map(&:full_name), + issue: "Some installed casks are deprecated or disabled.", + remediation: <<~EOS, + You should find replacements for the following casks: #{deprecated_or_disabled.sort_by(&:token).uniq * "\n "} - EOS + EOS + ) end - sig { returns(T.nilable(String)) } + sig { returns(T::Array[Finding]) } def check_git_status - return unless Utils::Git.available? + return [] unless Utils::Git.available? - message = T.let(nil, T.nilable(String)) + T.let(nil, T.nilable(String)) repos = { "Homebrew/brew" => HOMEBREW_REPOSITORY, @@ -898,35 +1072,48 @@ def check_git_status "Homebrew/homebrew-cask" => CoreCaskTap.instance.path, } + status = [] repos.each do |name, path| - next unless path.exist? + finding = __tap_git_status(name, path) + status << finding if finding.present? + end - status = path.cd do - `git status --untracked-files=all --porcelain 2>/dev/null` - end - next if status.blank? + status + end + + sig { params(tap: String, path: Pathname).returns(T.nilable(Finding)) } + def __tap_git_status(tap, path) + return unless path.exist? - message ||= "" - message += "\n" unless message.empty? - message += <<~EOS - You have uncommitted modifications to #{name}. + status = path.cd do + `git status --untracked-files=all --porcelain 2>/dev/null` + end + return if status.blank? + + message = <<~EOS + You have uncommitted modifications to #{tap}. + EOS + Finding::Remediation.new( + commands: ["git -C \"#{path}\" stash -u && git -C \"#{path}\" clean -d -f"], + text: <<~EOS, If this is a surprise to you, then you should stash these modifications. Stashing returns Homebrew to a pristine state but can be undone should you later need to do so for some reason. + git -C "#{path}" stash -u && git -C "#{path}" clean -d -f EOS + ) - modified = status.split("\n") - message += inject_file_list modified, <<~EOS + modified = status.split("\n") + message += inject_file_list modified, <<~EOS - Uncommitted files: - EOS - end + Uncommitted files: + EOS - message + Finding.new(issue: message, affects: modified) if message.present? end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_for_non_prefixed_coreutils coreutils = Formula["coreutils"] return unless coreutils.any_version_installed? @@ -934,26 +1121,34 @@ def check_for_non_prefixed_coreutils gnubin = %W[#{coreutils.opt_libexec}/gnubin #{coreutils.libexec}/gnubin] return unless paths.intersect?(gnubin) - <<~EOS - Putting non-prefixed coreutils in your path can cause GMP builds to fail. - EOS + Finding.new( + issue: <<~EOS, + Putting non-prefixed coreutils in your path can cause GMP builds to fail. + EOS + ) rescue FormulaUnavailableError nil end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_for_pydistutils_cfg_in_home return unless File.exist? "#{Dir.home}/.pydistutils.cfg" - <<~EOS - A '.pydistutils.cfg' file was found in $HOME, which may cause Python - builds to fail. See: - #{Formatter.url("https://bugs.python.org/issue6138")} - #{Formatter.url("https://bugs.python.org/issue4655")} - EOS + Finding.new( + links: [ + "https://bugs.python.org/issue6138", + "https://bugs.python.org/issue4655", + ], + issue: <<~EOS, + A '.pydistutils.cfg' file was found in $HOME, which may cause Python + builds to fail. See: + #{Formatter.url("https://bugs.python.org/issue6138")} + #{Formatter.url("https://bugs.python.org/issue4655")} + EOS + ) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_for_unreadable_installed_formula formula_unavailable_exceptions = [] Formula.racks.each do |rack| @@ -966,13 +1161,16 @@ def check_for_unreadable_installed_formula end return if formula_unavailable_exceptions.empty? - <<~EOS - Some installed formulae are not readable: - #{formula_unavailable_exceptions.join("\n\n ")} - EOS + Finding.new( + affects: formula_unavailable_exceptions, + issue: <<~EOS, + Some installed formulae are not readable: + #{formula_unavailable_exceptions.join("\n\n ")} + EOS + ) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_for_unlinked_but_not_keg_only unlinked = Formula.racks.reject do |rack| next true if (HOMEBREW_LINKED_KEGS/rack.basename).directory? @@ -987,14 +1185,21 @@ def check_for_unlinked_but_not_keg_only end.map(&:basename) return if unlinked.empty? - inject_file_list unlinked, <<~EOS - You have unlinked kegs in your Cellar. - Leaving kegs unlinked can lead to build-trouble and cause formulae that depend on - those kegs to fail to run properly once built. Run `brew link` on these: - EOS + Finding.new( + affects: unlinked.map(&:to_s), + issue: <<~EOS, + You have unlinked kegs in your Cellar. + Leaving kegs unlinked can lead to build-trouble and cause formulae that depend on + those kegs to fail to run properly once built. + EOS + remediation: inject_file_list(unlinked, <<~EOS + Run `brew link` on these: + EOS + ), + ) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_for_external_cmd_name_conflict cmds = Commands.tap_cmd_directories.flat_map { |p| Dir["#{p}/brew-*"] }.uniq cmds = cmds.select { |cmd| File.file?(cmd) && File.executable?(cmd) } @@ -1019,10 +1224,10 @@ def check_for_external_cmd_name_conflict EOS end - message + Finding.new(issue: message) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_for_tap_ruby_files_locations bad_tap_files = {} Tap.installed.each do |tap| @@ -1040,30 +1245,33 @@ def check_for_tap_ruby_files_locations end return if bad_tap_files.empty? - bad_tap_files.keys.map do |tap| - <<~EOS - Found Ruby file outside #{tap} tap formula directory. - (#{tap.formula_dir}): - #{bad_tap_files[tap].join("\n ")} - EOS - end.join("\n") + Finding.new( + issue: bad_tap_files.keys.map do |tap| + <<~EOS + Found Ruby file outside #{tap} tap formula directory. + (#{tap.formula_dir}): + #{bad_tap_files[tap].join("\n ")} + EOS + end.join("\n"), + ) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_homebrew_prefix return if Homebrew.default_prefix? - <<~EOS - Your Homebrew's prefix is not #{Homebrew::DEFAULT_PREFIX}. - - Most of Homebrew's bottles (binary packages) can only be used with the default prefix. - Consider uninstalling Homebrew and reinstalling into the default prefix. + Finding.new( + tier: 3, + remediation: "Consider uninstalling Homebrew and reinstalling into the default prefix.", + issue: <<~EOS, + Your Homebrew's prefix is not #{Homebrew::DEFAULT_PREFIX}. - #{support_tier_message(tier: 3)} - EOS + Most of Homebrew's bottles (binary packages) can only be used with the default prefix. + EOS + ) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_deleted_formula kegs = Keg.all @@ -1090,30 +1298,39 @@ def check_deleted_formula return if deleted_formulae.blank? - <<~EOS - Some installed kegs have no formulae! - This means they were either deleted or installed manually. - You should find replacements for the following formulae: - #{deleted_formulae.join("\n ")} - EOS + Finding.new( + affects: deleted_formulae, + issue: <<~EOS, + Some installed kegs have no formulae! + This means they were either deleted or installed manually. + + EOS + remediation: <<~EOS, + You should find replacements for the following formulae: + #{deleted_formulae.join("\n ")} + EOS + ) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_for_unnecessary_core_tap return if Homebrew::EnvConfig.developer? return if Homebrew::EnvConfig.no_install_from_api? return if Homebrew::EnvConfig.devcmdrun? return unless CoreTap.instance.installed? - <<~EOS - You have an unnecessary local Core tap! - This can cause problems installing up-to-date formulae. + remediation = Finding::Remediation.new(text: <<~EOS, commands: ["brew untap #{CoreTap.instance.name}"]) Please remove it by running: brew untap #{CoreTap.instance.name} EOS + Finding.new(remediation: remediation, issue: <<~EOS, + You have an unnecessary local Core tap! + This can cause problems installing up-to-date formulae. + EOS + ) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_for_unnecessary_cask_tap return if Homebrew::EnvConfig.developer? return if Homebrew::EnvConfig.no_install_from_api? @@ -1122,46 +1339,59 @@ def check_for_unnecessary_cask_tap cask_tap = CoreCaskTap.instance return unless cask_tap.installed? - <<~EOS + remediation = Finding::Remediation.new(text: <<~EOS, commands: ["brew untap #{cask_tap.name}"]) + Please remove it by running: + brew untap #{cask_tap.name} + EOS + Finding.new(remediation: remediation, issue: <<~EOS, You have an unnecessary local Cask tap. This can cause problems installing up-to-date casks. - Please remove it by running: - brew untap #{cask_tap.name} EOS + ) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_deprecated_cask_taps tapped_caskroom_taps = ::Tap.select { |t| t.user == "caskroom" || t.name == "phinze/cask" } .map(&:name) return if tapped_caskroom_taps.empty? - <<~EOS + remediation = Finding::Remediation.new(commands: ["brew untap #{tapped_caskroom_taps.join(" ")}"], + text: <<~EOS, + Please remove it by running: + brew untap #{tapped_caskroom_taps.join(" ")} + EOS + ) + Finding.new(remediation: remediation, issue: <<~EOS, You have the following deprecated Cask taps installed: #{tapped_caskroom_taps.join("\n ")} - Please remove them with: - brew untap #{tapped_caskroom_taps.join(" ")} EOS + ) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_cask_software_versions add_info "Homebrew Version", HOMEBREW_VERSION nil end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_cask_install_location locations = Dir.glob(HOMEBREW_CELLAR.join("brew-cask", "*")).reverse return if locations.empty? - locations.map do |l| - "Legacy install at #{l}. Run `brew uninstall --force brew-cask`." - end.join "\n" + Finding.new( + issue: locations.map do |l| + "Legacy install at #{l}." + end.join("\n"), + remediation: <<~EOS, + Run `brew uninstall --force brew-cask`." + EOS + ) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_cask_staging_location # Skip this check when running CI since the staging path is not writable for security reasons return if GitHub::Actions.env_set? @@ -1172,28 +1402,37 @@ def check_cask_staging_location return if !path.exist? || path.writable? - <<~EOS + remediation = Finding::Remediation.new(commands: ["sudo chown -R #{current_user} #{user_tilde(path.to_s)}"], + text: <<~EOS, + To fix, run: + sudo chown -R #{current_user} #{user_tilde(path.to_s)} + EOS + ) + Finding.new(remediation: remediation, issue: <<~EOS, The staging path #{user_tilde(path.to_s)} is not writable by the current user. - To fix, run: - sudo chown -R #{current_user} #{user_tilde(path.to_s)} EOS + ) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_cask_corrupt_dirs corrupt = Cask::Caskroom.corrupt_cask_dirs return if corrupt.empty? - <<~EOS - Some directories in the Caskroom do not have valid metadata. - #{corrupt.map { |token| "#{Cask::Caskroom.path}/#{token}" }.join("\n ")} - The following #{Utils.pluralize("cask", corrupt.count)} cannot be upgraded as-is. - To fix this, run: - #{corrupt.map { |token| "brew reinstall --cask --force #{token}" }.join("\n ")} - EOS + Finding.new( + issue: <<~EOS, + Some directories in the Caskroom do not have valid metadata. + #{corrupt.map { |token| "#{Cask::Caskroom.path}/#{token}" }.join("\n ")} + The following #{Utils.pluralize("cask", corrupt.count)} cannot be upgraded as-is. + EOS + remediation: <<~EOS, + To fix this, run: + #{corrupt.map { |token| "brew reinstall --cask --force #{token}" }.join("\n ")} + EOS + ) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_cask_taps error_tap_paths = [] @@ -1213,19 +1452,21 @@ def check_cask_taps add_info "Homebrew Cask Taps:", taps_info taps_string = Utils.pluralize("tap", error_tap_paths.count) - "Unable to read from cask #{taps_string}: #{error_tap_paths.to_sentence}" if error_tap_paths.present? + return unless error_tap_paths.present? + + Finding.new(issue: "Unable to read from cask #{taps_string}: #{error_tap_paths.to_sentence}") end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_cask_load_path paths = $LOAD_PATH.map { user_tilde(it) } add_info "$LOAD_PATHS", paths.presence || none_string - "$LOAD_PATH is empty" if paths.blank? + Finding.new(issue: "$LOAD_PATH is empty") if paths.blank? end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_cask_environment_variables environment_variables = %w[ RUBYLIB @@ -1253,11 +1494,11 @@ def check_cask_environment_variables nil end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_cask_xattr # If quarantine is not available, a warning is already shown by check_cask_quarantine_support so just return return unless Cask::Quarantine.available? - return "Unable to find `xattr`." unless File.exist?("/usr/bin/xattr") + return Finding.new(issue: "Unable to find `xattr`.") unless File.exist?("/usr/bin/xattr") result = system_command "/usr/bin/xattr", args: ["-h"] @@ -1267,22 +1508,30 @@ def check_cask_xattr result = Utils.popen_read "/usr/bin/python", "--version", err: :out if result.include? "Python 2.7" - <<~EOS - Your Python installation has a broken version of setuptools. - To fix, reinstall macOS or run: - sudo /usr/bin/python -m pip install -I setuptools - EOS + Finding.new( + issue: <<~EOS, + Your Python installation has a broken version of setuptools. + EOS + remediation: <<~EOS, + To fix, reinstall macOS or run: + sudo /usr/bin/python -m pip install -I setuptools + EOS + ) else - <<~EOS - The system Python version is wrong. - To fix, run: - defaults write com.apple.versioner.python Version 2.7 - EOS + Finding.new( + issue: <<~EOS, + The system Python version is wrong. + EOS + remediation: <<~EOS, + To fix, run: + defaults write com.apple.versioner.python Version 2.7 + EOS + ) end elsif result.stderr.include? "pkg_resources.DistributionNotFound" - "Your Python installation is unable to find `xattr`." + Finding.new(issue: "Your Python installation is unable to find `xattr`.") else - "unknown xattr error: #{result.stderr.split("\n").last}" + Finding.new(issue: "unknown xattr error: #{result.stderr.split("\n").last}") end end @@ -1291,7 +1540,7 @@ def non_core_taps @non_core_taps ||= Tap.installed.reject(&:core_tap?).reject(&:core_cask_tap?) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_for_duplicate_formulae return if ENV["HOMEBREW_TEST_BOT"].present? @@ -1312,14 +1561,16 @@ def check_for_duplicate_formulae "Some of these can be resolved with:\n brew untap #{unused_shadowed_formula_tap_names.join(" ")}" end - <<~EOS - The following formulae have the same name as core formulae: - #{shadowed_formula_full_names.join("\n ")} - #{resolution} - EOS + Finding.new( + issue: <<~EOS, + The following formulae have the same name as core formulae: + #{shadowed_formula_full_names.join("\n ")} + EOS + remediation: resolution, + ) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(Finding)) } def check_for_duplicate_casks return if ENV["HOMEBREW_TEST_BOT"].present? @@ -1335,16 +1586,22 @@ def check_for_duplicate_casks unused_shadowed_cask_tap_names = (shadowed_cask_tap_names - installed_cask_tap_names).sort resolution = if unused_shadowed_cask_tap_names.empty? - "Their taps are in use, so you must use these full names throughout Homebrew." + Finding::Remediation.new( + text: "Their taps are in use, so you must use these full names throughout Homebrew.", + ) else - "Some of these can be resolved with:\n brew untap #{unused_shadowed_cask_tap_names.join(" ")}" + Finding::Remediation.new(text: "Some of these can be resolved with:", + commands: ["brew untap #{unused_shadowed_cask_tap_names.join(" ")}"]) end - <<~EOS - The following casks have the same name as core casks: - #{shadowed_cask_full_names.join("\n ")} - #{resolution} - EOS + Finding.new( + issue: <<~EOS, + The following casks have the same name as core casks: + #{shadowed_cask_full_names.join("\n ")} + EOS + affects: shadowed_cask_full_names, + remediation: resolution, + ) end sig { returns(T::Array[String]) } diff --git a/Library/Homebrew/extend/os/linux/diagnostic.rb b/Library/Homebrew/extend/os/linux/diagnostic.rb index b91f761995adc..26a5f5759bb7a 100644 --- a/Library/Homebrew/extend/os/linux/diagnostic.rb +++ b/Library/Homebrew/extend/os/linux/diagnostic.rb @@ -36,7 +36,7 @@ def supported_configuration_checks ].freeze end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) } def check_tmpdir_sticky_bit message = super return if message.nil? @@ -50,7 +50,7 @@ def check_tmpdir_sticky_bit EOS end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) } def check_tmpdir_executable f = Tempfile.new(%w[homebrew_check_tmpdir_executable .sh], HOMEBREW_TEMP) f.write "#!/bin/sh\n" @@ -58,78 +58,95 @@ def check_tmpdir_executable 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 + ::Homebrew::Diagnostic::Finding.new( + issue: <<~EOS, + The directory #{HOMEBREW_TEMP} does not permit executing + programs. It is likely mounted as "noexec". + EOS + remediation: <<~EOS, + 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 - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) } 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 + ::Homebrew::Diagnostic::Finding.new( + issue: <<~EOS, + umask is currently set to 000. Directories created by Homebrew cannot + be world-writable. + EOS + remediation: ::Homebrew::Diagnostic::Finding::Remediation.new( + text: <<~EOS, + This issue can be resolved by adding "umask 002" to + your #{Utils::Shell.profile}: + EOS + commands: ["echo 'umask 002' >> #{Utils::Shell.profile}"], + ), + ) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) } def check_supported_architecture return if ::Hardware::CPU.intel? return if ::Hardware::CPU.arm64? - <<~EOS - Your CPU architecture (#{::Hardware::CPU.arch}) is not supported. We only support - x86_64 or ARM64/AArch64 CPU architectures. You will be unable to use binary packages (bottles). - - #{support_tier_message(tier: 2)} - EOS + ::Homebrew::Diagnostic::Finding.new( + tier: 2, + issue: <<~EOS, + Your CPU architecture (#{::Hardware::CPU.arch}) is not supported. We only support + x86_64 or ARM64/AArch64 CPU architectures. You will be unable to use binary packages (bottles). + EOS + ) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) } 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. - - We recommend updating to a newer version via your distribution's - package manager, upgrading your distribution to the latest version, - or changing distributions. - - #{support_tier_message(tier: :unsupported)} - EOS + ::Homebrew::Diagnostic::Finding.new( + tier: :unsupported, + issue: <<~EOS, + Your system glibc #{OS::Linux::Glibc.system_version} is too old. + We only support glibc #{OS::Linux::Glibc.minimum_version} or later. + EOS + remediation: <<~EOS, + 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 - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) } def check_glibc_version return unless OS::Linux::Glibc.below_ci_version? # We want to bypass this check in some tests. return if ENV["HOMEBREW_GLIBC_TESTING"] - <<~EOS - Your system glibc #{OS::Linux::Glibc.system_version} is too old. - We will need to automatically install a newer version. - - We recommend updating to a newer version via your distribution's - package manager, upgrading your distribution to the latest version, - or changing distributions. - - #{support_tier_message(tier: 2)} - EOS + ::Homebrew::Diagnostic::Finding.new( + tier: 2, + issue: <<~EOS, + Your system glibc #{OS::Linux::Glibc.system_version} is too old. + We will need to automatically install a newer version. + EOS + remediation: <<~EOS, + 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 - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) } def check_glibc_next_version return if OS::LINUX_GLIBC_NEXT_CI_VERSION.blank? return if OS::Linux::Glibc.below_ci_version? @@ -138,34 +155,39 @@ def check_glibc_next_version # We want to bypass this check in some tests. return if ENV["HOMEBREW_GLIBC_TESTING"] || ENV["CI"] || ENV["HOMEBREW_TEST_BOT"].present? - <<~EOS - Your system glibc #{OS::Linux::Glibc.system_version} is older than #{OS::LINUX_GLIBC_NEXT_CI_VERSION}. - An upcoming brew release will automatically install a newer version. - - We recommend updating to a newer version via your distribution's - package manager, upgrading your distribution to the latest version, - or changing distributions. - EOS + ::Homebrew::Diagnostic::Finding.new( + issue: <<~EOS, + Your system glibc #{OS::Linux::Glibc.system_version} is older than #{OS::LINUX_GLIBC_NEXT_CI_VERSION}. + An upcoming brew release will automatically install a newer version. + EOS, + remediation: <<~EOS + 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 - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) } 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). - - We recommend updating to a newer version via your distribution's - package manager, upgrading your distribution to the latest version, - or changing distributions. - - #{support_tier_message(tier: 3)} - EOS + ::Homebrew::Diagnostic::Finding.new( + tier: 3, + issue: <<~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). + EOS, + remediation: <<~EOS + 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 - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) } def check_linux_sandbox return unless Homebrew::EnvConfig.sandbox_linux? return if OS::Linux.inside_docker? @@ -174,7 +196,17 @@ def check_linux_sandbox return if [:disabled, :available].include?(state) reason = ::Sandbox.failure_reason || "The Linux sandbox is not available." - lines = case state + reason_append = case state + when :setuid + "\n\nHomebrew's Linux sandbox requires a rootless `bwrap` executable." + when :unavailable + "\n\nHomebrew's Linux sandbox requires rootless Bubblewrap and unprivileged user namespaces." + else + "" + end + reason += reason_append + + fix_lines = case state when :missing missing_lines = [ reason, @@ -187,9 +219,6 @@ def check_linux_sandbox missing_lines when :setuid [ - reason, - "", - "Homebrew's Linux sandbox requires a rootless `bwrap` executable.", "Install a non-setuid Bubblewrap or put it earlier on `PATH`.", ] when :unavailable @@ -201,63 +230,79 @@ def check_linux_sandbox *::Sandbox.configuration_command_messages, ] else - [reason] + [] end - "#{[ - *lines, - "", - "As a final workaround, disable the Linux sandbox:", - " export HOMEBREW_NO_SANDBOX_LINUX=1", - ].join("\n")}\n" + ::Homebrew::Diagnostic::Finding.new( + issue: reason, + remediation: [ + *fix_lines, + "", + "As a final workaround, disable the Linux sandbox:", + " export HOMEBREW_NO_SANDBOX_LINUX=1", + ].join("\n").to_s, + ) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) } def check_linuxbrew_core return unless Homebrew::EnvConfig.no_install_from_api? return unless CoreTap.instance.linuxbrew_core? - <<~EOS + issue = <<~EOS Your Linux core repository is still linuxbrew-core. You must either unset `$HOMEBREW_NO_INSTALL_FROM_API` or set the repository's remote to homebrew-core to update core formulae. EOS + + ::Homebrew::Diagnostic::Finding.new( + issue: issue, + remediation: <<~EOS, + You can unset `$HOMEBREW_NO_INSTALL_FROM_API` or set + the repository's remote to homebrew-core to update core formulae. + EOS + ) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) } 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 + ::Homebrew::Diagnostic::Finding.new( + remediation: "You can unset `$HOMEBREW_BOTTLE_DOMAIN` or adjust it to not contain \"linuxbrew\".", + issue: <<~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 - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) } def check_for_symlinked_home return unless File.symlink?("/home") - <<~EOS + issue = <<~EOS Your /home directory is a symlink. This is known to cause issues with formula linking, particularly when installing multiple formulae that create symlinks in shared directories. While this may be a standard directory structure in some distributions (e.g. Fedora Silverblue) there are known issues as-is. - - If you encounter linking issues, you may need to manually create conflicting - directories or use `brew link --overwrite` as a workaround. - - We'd welcome a PR to fix this functionality. - See https://github.com/Homebrew/brew/issues/18036 for more context. - - #{support_tier_message(tier: 2)} EOS + ::Homebrew::Diagnostic::Finding.new( + tier: 2, + issue: issue, + links: ["https://github.com/Homebrew/brew/issues/18036"], + remediation: <<~EOS, + If you encounter linking issues, you may need to manually create conflicting + directories or use `brew link --overwrite` as a workaround. + We'd welcome a PR to fix this functionality. + EOS + ) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) } def check_gcc_dependent_linkage gcc_dependents = ::Formula.installed.select do |formula| next false unless formula.tap&.core_tap? @@ -286,15 +331,22 @@ def check_gcc_dependent_linkage 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 + remediation = ::Homebrew::Diagnostic::Finding::Remediation.new( + commands: ["brew reinstall #{badly_linked.join(" ")}"], + ) + ::Homebrew::Diagnostic::Finding.new( + remediation: remediation, + issue: <<~EOS, + Formulae which link to GCC through a versioned path were found. These formulae + are prone to breaking when GCC is updated. + EOS + ) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) } def check_cask_software_versions super add_info "Linux", OS::Linux.os_version diff --git a/Library/Homebrew/extend/os/mac/diagnostic.rb b/Library/Homebrew/extend/os/mac/diagnostic.rb index c36c4bc772a22..13a902d63cb09 100644 --- a/Library/Homebrew/extend/os/mac/diagnostic.rb +++ b/Library/Homebrew/extend/os/mac/diagnostic.rb @@ -107,7 +107,7 @@ def build_from_source_checks ].freeze end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) } def check_for_non_prefixed_findutils findutils = ::Formula["findutils"] return unless findutils.any_version_installed? @@ -116,44 +116,49 @@ def check_for_non_prefixed_findutils 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 + ::Homebrew::Diagnostic::Finding.new( + issue: <<~EOS, + Putting non-prefixed findutils in your path can cause python builds to fail." + EOS + ) rescue FormulaUnavailableError nil end - sig { returns(T.nilable(String)) } + 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)" - <<~EOS.chomp - old version. + remediation = <<~EOS You may have better luck with MacPorts which supports older versions of macOS: - #{Formatter.url("https://www.macports.org")} + #{Formatter.url("https://www.macports.org")} EOS + "old version." end return if what.blank? who.freeze - <<~EOS - You are using macOS #{MacOS.version}. - #{who} do not provide support for this #{what} - - #{support_tier_message(tier:)} - EOS + ::Homebrew::Diagnostic::Finding.new( + remediation: remediation, + tier: tier, + issue: <<~EOS, + You are using macOS #{MacOS.version}. + #{who} do not provide support for this #{what} + EOS + ) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) } def check_for_opencore return if ::Hardware::CPU.physical_cpu_arm64? @@ -174,15 +179,16 @@ def check_for_opencore 3 end - <<~EOS - You have booted macOS using OpenCore Legacy Patcher. - We do not provide support for this configuration. - - #{support_tier_message(tier: oclp_support_tier)} - EOS + ::Homebrew::Diagnostic::Finding.new( + issue: <<~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(String)) } + sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) } def check_xcode_up_to_date return unless MacOS::Xcode.outdated? @@ -195,27 +201,29 @@ def check_xcode_up_to_date # Homebrew/brew is currently using. return if GitHub::Actions.env_set? - message = <<~EOS - Your Xcode (#{MacOS::Xcode.version}) is outdated. + remediation = <<~EOS Please update to Xcode #{MacOS::Xcode.latest_version} (or delete it). #{MacOS::Xcode.update_instructions} - - #{support_tier_message(tier: 2)} EOS if OS::Mac.version.prerelease? current_path = Utils.popen_read("/usr/bin/xcode-select", "-p") - message += <<~EOS + 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 - message + + ::Homebrew::Diagnostic::Finding.new( + tier: 2, + issue: "Your Xcode (#{MacOS::Xcode.version}) is outdated.", + remediation: remediation, + ) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) } def check_clt_up_to_date return unless MacOS::CLT.outdated? @@ -228,73 +236,86 @@ def check_clt_up_to_date # Homebrew/brew is currently using. return if GitHub::Actions.env_set? - <<~EOS - A newer Command Line Tools release is available. - #{MacOS::CLT.update_instructions} - - #{support_tier_message(tier: 2)} - EOS + ::Homebrew::Diagnostic::Finding.new( + issue: "A newer Command Line Tools release is available.", + tier: 2, + remediation: MacOS::CLT.update_instructions, + ) end - sig { returns(T.nilable(String)) } + 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? - <<~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 + ::Homebrew::Diagnostic::Finding.new( + issue: <<~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(String)) } + sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) } 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 + ::Homebrew::Diagnostic::Finding.new( + issue: <<~EOS, + Your Command Line Tools are too outdated. + EOS + remediation: MacOS::CLT.update_instructions, + ) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) } 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 + ::Homebrew::Diagnostic::Finding.new( + issue: <<~EOS, + Xcode alone is not sufficient on #{MacOS.version.pretty_name}. + EOS + remediation: ::DevelopmentTools.installation_instructions, + ) end - sig { returns(T.nilable(String)) } + 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?(" ") - <<~EOS - Xcode is installed to a directory with a space in the name. - This will cause some formulae to fail to build. - EOS + ::Homebrew::Diagnostic::Finding.new( + issue: <<~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(String)) } + sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) } 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: + ::Homebrew::Diagnostic::Finding.new( + issue: <<~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 + EOS + remediation: <<~EOS, + You may need to `xcode-select` the proper path if you have moved Xcode. + EOS + ) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) } def check_xcode_select_path return if MacOS::CLT.installed? return unless MacOS::Xcode.installed? @@ -302,28 +323,43 @@ def check_xcode_select_path 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 + + ::Homebrew::Diagnostic::Finding.new( + issue: <<~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(String)) } + 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? - <<~EOS - You have not agreed to the Xcode license. - Agree to the license by opening Xcode.app or running: - sudo xcodebuild -license - EOS + ::Homebrew::Diagnostic::Finding.new( + issue: <<~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(String)) } + sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) } def check_filesystem_case_sensitive dirs_to_check = [ HOMEBREW_PREFIX, @@ -351,13 +387,15 @@ def check_filesystem_case_sensitive 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 + ::Homebrew::Diagnostic::Finding.new( + issue: <<~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(String)) } + sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) } def check_for_gettext find_relative_paths("lib/libgettextlib.dylib", "lib/libintl.dylib", @@ -386,14 +424,20 @@ def check_for_gettext 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 + ::Homebrew::Diagnostic::Finding.new( + issue: <<~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(" ")} + EOS + ) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) } def check_for_iconv find_relative_paths("lib/libiconv.dylib", "include/iconv.h") return if @found.empty? @@ -405,26 +449,32 @@ def check_for_iconv 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 + ::Homebrew::Diagnostic::Finding.new( + issue: <<~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 + ::Homebrew::Diagnostic::Finding.new( + issue: <<~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(String)) } + sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) } def check_for_multiple_volumes return unless HOMEBREW_CELLAR.exist? @@ -448,19 +498,22 @@ def check_for_multiple_volumes return if where_cellar == where_tmp - <<~EOS + issue = <<~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. - - You should set the `$HOMEBREW_TEMP` environment variable to a suitable - directory on the same volume as your Cellar. - - #{support_tier_message(tier: 2)} EOS + ::Homebrew::Diagnostic::Finding.new( + issue: issue, + 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(String)) } + sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) } def check_if_supported_sdk_available return unless ::DevelopmentTools.installed? return if MacOS.sdk @@ -479,18 +532,22 @@ def check_if_supported_sdk_available "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 + ::Homebrew::Diagnostic::Finding.new( + issue: <<~EOS, + Your #{source} does not support macOS #{MacOS.version}.\nIt 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(String)) } + sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) } def check_broken_sdks locator = MacOS.sdk_locator @@ -511,18 +568,20 @@ def check_broken_sdks 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 + remediation = ::Homebrew::Diagnostic::Finding::Remediation.new( + commands: ["sudo rm -rf #{path_to_remove}"], + text: "Remove the broken installation before reinstalling\n #{installation_instructions}", + ) + ::Homebrew::Diagnostic::Finding.new( + remediation: remediation, + issue: <<~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 + ) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) } def check_cask_software_versions super add_info "macOS", MacOS.full_version @@ -545,7 +604,7 @@ def check_cask_software_versions nil end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) } def check_pkgconf_macos_sdk_mismatch mismatch = Homebrew::Pkgconf.macos_sdk_mismatch return unless mismatch @@ -553,51 +612,57 @@ def check_pkgconf_macos_sdk_mismatch Homebrew::Pkgconf.mismatch_warning_message(mismatch) end - sig { returns(T.nilable(String)) } + sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) } def check_cask_quarantine_support status, check_output = ::Cask::Quarantine.check_quarantine_support - case status + messages = case status when :quarantine_available - nil + [nil, nil] when :xattr_broken - "No Cask quarantine support available: there's no working version of `xattr` on this system." + ["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." + ["No Cask quarantine support available: there's no available version of `swift` on this system.", nil] when :swift_broken_clt - <<~EOS - No Cask quarantine support available: Swift is not working due to missing Command Line Tools. - #{MacOS::CLT.installation_then_reinstall_instructions} - EOS + ["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 - <<~EOS + msg = <<~EOS No Cask quarantine support available: Swift compilation failed. This is usually due to a broken or incompatible Command Line Tools installation. - #{MacOS::CLT.installation_then_reinstall_instructions} EOS + [msg, MacOS::CLT.installation_then_reinstall_instructions] when :swift_runtime_error - <<~EOS + msg = <<~EOS No Cask quarantine support available: Swift runtime error. Your Command Line Tools installation may be broken or incomplete. - #{MacOS::CLT.installation_then_reinstall_instructions} EOS + [msg, MacOS::CLT.installation_then_reinstall_instructions] when :swift_not_executable - <<~EOS + msg = <<~EOS No Cask quarantine support available: Swift is not executable. Your Command Line Tools installation may be incomplete. - #{MacOS::CLT.installation_then_reinstall_instructions} EOS + [msg, MacOS::CLT.installation_then_reinstall_instructions] when :swift_unexpected_error - <<~EOS + msg = <<~EOS No Cask quarantine support available: Swift returned an unexpected error: #{check_output} EOS + [msg, nil] else - <<~EOS + 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( + issue: T.must(messages.first), + remediation: messages.last, + ) end end end diff --git a/Library/Homebrew/extend/os/mac/pkgconf.rb b/Library/Homebrew/extend/os/mac/pkgconf.rb index 9a9f9e38adee5..b3fd3f84a4f6b 100644 --- a/Library/Homebrew/extend/os/mac/pkgconf.rb +++ b/Library/Homebrew/extend/os/mac/pkgconf.rb @@ -31,18 +31,27 @@ def macos_sdk_mismatch [built_on_version, current_version] end - sig { params(mismatch: [String, String]).returns(String) } + sig { params(mismatch: [String, String]).returns(Diagnostic::Finding) } def mismatch_warning_message(mismatch) - <<~EOS - You have pkgconf installed that was built on macOS #{mismatch[0]}, + Diagnostic::Finding.new( + links: [ + "https://github.com/Homebrew/brew/issues/16137", + ], + affects: ["pkgconf"], + remediation: Diagnostic::Finding::Remediation.new( + text: <<~EOS, + To fix this issue, reinstall pkgconf: + brew reinstall pkgconf + EOS + commands: ["brew reinstall pkgconf"], + ), + issue: <<~EOS, + You have pkgconf installed that was built on macOS #{mismatch[0]}, but you are running macOS #{mismatch[1]}. - This can cause issues with packages that depend on system libraries, such as libffi. - To fix this issue, reinstall pkgconf: - brew reinstall pkgconf - - For more information, see: https://github.com/Homebrew/brew/issues/16137 - EOS + This can cause issues with packages that depend on system libraries, such as libffi. + EOS + ) end end end diff --git a/Library/Homebrew/extend/os/mac/reinstall.rb b/Library/Homebrew/extend/os/mac/reinstall.rb index d2b5746185885..55cc7a59ee5fb 100644 --- a/Library/Homebrew/extend/os/mac/reinstall.rb +++ b/Library/Homebrew/extend/os/mac/reinstall.rb @@ -32,7 +32,7 @@ def reinstall_pkgconf_if_needed!(dry_run: false) T.unsafe(self).reinstall_formula(context) ohai "Reinstalled pkgconf due to macOS version mismatch" rescue - ofail Homebrew::Pkgconf.mismatch_warning_message(mismatch) + ofail Homebrew::Pkgconf.mismatch_warning_message(mismatch).to_s end end end diff --git a/Library/Homebrew/install.rb b/Library/Homebrew/install.rb index 16db56b0c5ac0..4e811f5b3e5e9 100644 --- a/Library/Homebrew/install.rb +++ b/Library/Homebrew/install.rb @@ -30,7 +30,7 @@ def perform_preinstall_checks_once(all_fatal: false) def check_cc_argv(cc) return unless cc - @checks ||= T.let(Diagnostic::Checks.new, T.nilable(Homebrew::Diagnostic::Checks)) + @checks ||= T.let(Diagnostic::Finding.new(issue: "", tier: 3), T.nilable(Homebrew::Diagnostic::Finding)) opoo <<~EOS You passed `--cc=#{cc}`. diff --git a/Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/doctor.rbi b/Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/doctor.rbi index 76369fc38b7b2..f04e7cba4c033 100644 --- a/Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/doctor.rbi +++ b/Library/Homebrew/sorbet/rbi/dsl/homebrew/cmd/doctor.rbi @@ -17,6 +17,9 @@ class Homebrew::Cmd::Doctor::Args < Homebrew::CLI::Args sig { returns(T::Boolean) } def audit_debug?; end + sig { returns(T::Boolean) } + def json?; end + sig { returns(T::Boolean) } def list_checks?; end end diff --git a/Library/Homebrew/test/cmd/doctor_spec.rb b/Library/Homebrew/test/cmd/doctor_spec.rb index 18c277c449ac4..01490608aca79 100644 --- a/Library/Homebrew/test/cmd/doctor_spec.rb +++ b/Library/Homebrew/test/cmd/doctor_spec.rb @@ -12,6 +12,13 @@ .to output(/This is an integration test/).to_stderr end + specify "prints json when requested" do + cmd = described_class.new(["--json"]) + + expect { cmd.run } + .to output(/"tier": 1/).to_stdout + end + specify "check_missing_deps reports formula and cask dependencies", :cask do formula = instance_double(Formula, full_name: "needs-foo", missing_dependencies: [instance_double(Dependency, to_s: "foo")]) @@ -25,7 +32,7 @@ allow(Cask::Caskroom).to receive(:casks).and_return([cask]) allow(Cask::Tab).to receive(:for_cask).with(cask).and_return(tab) - expect(Homebrew::Diagnostic::Checks.new.check_missing_deps) + expect(Homebrew::Diagnostic::Checks.new.check_missing_deps&.to_s) .to include( "Some installed formulae or casks are missing dependencies.", "brew install foo local-caffeine unar", diff --git a/Library/Homebrew/test/diagnostic_checks_spec.rb b/Library/Homebrew/test/diagnostic_checks_spec.rb index 3f5045e35f9f1..1f57b69a3d2c7 100644 --- a/Library/Homebrew/test/diagnostic_checks_spec.rb +++ b/Library/Homebrew/test/diagnostic_checks_spec.rb @@ -25,7 +25,7 @@ dirs.each do |dir| modes[dir] = dir.stat.mode & 0777 dir.chmod 0555 - expect(checks.check_access_directories).to match(dir.to_s) + expect(checks.check_access_directories&.to_s).to match(dir.to_s) end ensure modes.each do |dir, mode| @@ -45,7 +45,7 @@ # HOMEBREW_PREFIX/bin/ (bin/File.basename(Dir["/usr/bin/*"].first)).mkpath - expect(checks.check_user_path_1) + expect(checks.check_user_path_1&.to_s) .to match("/usr/bin occurs before #{HOMEBREW_PREFIX}/bin") end @@ -53,8 +53,8 @@ ENV["PATH"] = ENV["PATH"].gsub \ %r{(?:^|#{File::PATH_SEPARATOR})#{HOMEBREW_PREFIX}/bin}o, "" - expect(checks.check_user_path_1).to be_nil - expect(checks.check_user_path_2) + expect(checks.check_user_path_1&.to_s).to be_nil + expect(checks.check_user_path_2&.to_s) .to match("Homebrew's \"bin\" was not found in your PATH.") end @@ -67,9 +67,9 @@ ENV["HOMEBREW_PATH"].gsub(/(?:^|#{Regexp.escape(File::PATH_SEPARATOR)})#{Regexp.escape(sbin)}/, "") stub_const("ORIGINAL_PATHS", PATH.new(homebrew_path).filter_map { |path| Pathname.new(path).expand_path }) - expect(checks.check_user_path_1).to be_nil - expect(checks.check_user_path_2).to be_nil - expect(checks.check_user_path_3) + expect(checks.check_user_path_1&.to_s).to be_nil + expect(checks.check_user_path_2&.to_s).to be_nil + expect(checks.check_user_path_3&.to_s) .to match("Homebrew's \"sbin\" was not found in your PATH") ensure FileUtils.rm_rf(sbin) @@ -81,7 +81,7 @@ mktmpdir do |path| FileUtils.ln_s path, HOMEBREW_CELLAR - expect(checks.check_for_symlinked_cellar).to match(path) + expect(checks.check_for_symlinked_cellar&.to_s).to match(path) end ensure HOMEBREW_CELLAR.unlink @@ -98,15 +98,16 @@ gitconfig = path/".gitconfig" gitconfig.write("[safe]\n") - expect(checks.check_homebrew_repository_git_hooks).to eq <<~EOS + expect(checks.check_homebrew_repository_git_hooks&.to_s).to eq <<~EOS.rstrip Git hooks or a repository-local `.gitconfig` were found in your Homebrew repository. Homebrew does not use these, and they can break Homebrew operations. - Remove them with: - rm -rf "#{path}/.git/hooks" "#{path}/.gitconfig" Paths found: #{hook} #{gitconfig} + + Remove them with: + rm -rf "#{path}/.git/hooks" "#{path}/.gitconfig" EOS end end @@ -119,7 +120,7 @@ hook.dirname.mkpath hook.write("#!/bin/sh\n") - expect(checks.check_homebrew_repository_git_hooks).to be_nil + expect(checks.check_homebrew_repository_git_hooks&.to_s).to be_nil end end @@ -139,13 +140,14 @@ allow(Cask::Caskroom).to receive(:casks).and_return([foo_cask, bar_cask]) with_env(HOMEBREW_REQUIRE_TAP_TRUST: "1") do - check_untrusted_taps = checks.check_untrusted_taps - expect(check_untrusted_taps).to eq <<~EOS + check_untrusted_taps = checks.check_untrusted_taps&.to_s + expect(check_untrusted_taps).to eq <<~EOS.rstrip The following taps are not trusted: thirdparty/foo thirdparty/bar Homebrew is currently ignoring formulae, casks and commands from these taps because tap trust is required. + Prefer trusting only the specific formulae, casks or commands you need. Trust installed formulae from these taps with: brew trust --formula thirdparty/bar/bar-formula @@ -182,12 +184,13 @@ allow(Cask::Caskroom).to receive(:casks).and_return([]) with_env(HOMEBREW_REQUIRE_TAP_TRUST: nil, HOMEBREW_NO_REQUIRE_TAP_TRUST: nil) do - check_untrusted_taps = checks.check_untrusted_taps - expect(check_untrusted_taps).to eq <<~EOS + check_untrusted_taps = checks.check_untrusted_taps&.to_s + expect(check_untrusted_taps).to eq <<~EOS.rstrip The following taps are not trusted: thirdparty/foo Homebrew is currently ignoring formulae, casks and commands from these taps because tap trust is required. + Untap them with: brew untap thirdparty/foo Trust specific formulae, casks and commands with: @@ -213,7 +216,7 @@ allow(Cask::Caskroom).to receive(:casks).and_return([]) with_env(HOMEBREW_NO_ENV_HINTS: "1") do - check_untrusted_taps = checks.check_untrusted_taps + check_untrusted_taps = checks.check_untrusted_taps&.to_s expect(check_untrusted_taps).to include(Formatter.url("https://docs.brew.sh/Tap-Trust")) expect(check_untrusted_taps).not_to include("export HOMEBREW_NO_REQUIRE_TAP_TRUST=1") end @@ -223,19 +226,19 @@ with_env(HOMEBREW_NO_REQUIRE_TAP_TRUST: "1") do expect(Homebrew::Trust).not_to receive(:wholly_untrusted_taps) - expect(checks.check_untrusted_taps).to be_nil + expect(checks.check_untrusted_taps&.to_s).to be_nil end end specify "#check_tmpdir" do ENV["TMPDIR"] = "/i/don/t/exis/t" - expect(checks.check_tmpdir).to match("doesn't exist") + expect(checks.check_tmpdir&.to_s).to match("doesn't exist") end specify "#check_for_nix_homebrew" do stub_const("HOMEBREW_REPOSITORY", HOMEBREW_PREFIX/"Library/.homebrew-is-managed-by-nix") - expect(checks.check_for_nix_homebrew) + expect(checks.check_for_nix_homebrew&.to_s) .to include("This is a Tier 3 configuration", "https://github.com/zhaofengli/nix-homebrew/issues") end @@ -250,7 +253,7 @@ allow(Commands).to receive(:tap_cmd_directories).and_return([path1, path2]) - expect(checks.check_for_external_cmd_name_conflict) + expect(checks.check_for_external_cmd_name_conflict&.to_s) .to match("brew-foo") end end @@ -258,7 +261,7 @@ specify "#check_homebrew_prefix" do allow(Homebrew).to receive(:default_prefix?).and_return(false) - expect(checks.check_homebrew_prefix) + expect(checks.check_homebrew_prefix&.to_s) .to match("Your Homebrew's prefix is not #{Homebrew::DEFAULT_PREFIX}") end @@ -267,7 +270,7 @@ expect_any_instance_of(CoreTap).to receive(:installed?).and_return(true) - expect(checks.check_for_unnecessary_core_tap).to match("You have an unnecessary local Core tap") + expect(checks.check_for_unnecessary_core_tap&.to_s).to match("You have an unnecessary local Core tap") end specify "#check_for_unnecessary_cask_tap" do @@ -275,17 +278,18 @@ expect_any_instance_of(CoreCaskTap).to receive(:installed?).and_return(true) - expect(checks.check_for_unnecessary_cask_tap).to match("unnecessary local Cask tap") + expect(checks.check_for_unnecessary_cask_tap&.to_s).to match("unnecessary local Cask tap") end specify "#check_cask_corrupt_dirs" do allow(Cask::Caskroom).to receive(:corrupt_cask_dirs).and_return(["google-chrome", "docker-desktop"]) - expect(checks.check_cask_corrupt_dirs).to eq <<~EOS + expect(checks.check_cask_corrupt_dirs&.to_s).to eq <<~EOS.rstrip Some directories in the Caskroom do not have valid metadata. #{Cask::Caskroom.path}/google-chrome #{Cask::Caskroom.path}/docker-desktop The following casks cannot be upgraded as-is. + To fix this, run: brew reinstall --cask --force google-chrome brew reinstall --cask --force docker-desktop diff --git a/Library/Homebrew/test/os/linux/diagnostic_spec.rb b/Library/Homebrew/test/os/linux/diagnostic_spec.rb index 543cda2acd354..20c01655f9478 100644 --- a/Library/Homebrew/test/os/linux/diagnostic_spec.rb +++ b/Library/Homebrew/test/os/linux/diagnostic_spec.rb @@ -14,14 +14,14 @@ specify "#check_supported_architecture" do allow(Hardware::CPU).to receive(:type).and_return(:arm64) - expect(checks.check_supported_architecture) + expect(checks.check_supported_architecture&.to_s) .to match(/Your CPU architecture .+ is not supported/) end specify "#check_glibc_minimum_version" do allow(OS::Linux::Glibc).to receive(:below_minimum_version?).and_return(true) - expect(checks.check_glibc_minimum_version) + expect(checks.check_glibc_minimum_version&.to_s) .to match(/Your system glibc .+ is too old/) end @@ -30,14 +30,14 @@ allow(OS::Linux::Glibc).to receive_messages(below_ci_version?: false, system_version: Version.new("2.35")) allow(ENV).to receive(:[]).and_return(nil) - expect(checks.check_glibc_next_version) + expect(checks.check_glibc_next_version&.to_s) .to match("Your system glibc 2.35 is older than 2.39") end specify "#check_kernel_minimum_version" do allow(OS::Linux::Kernel).to receive(:below_minimum_version?).and_return(true) - expect(checks.check_kernel_minimum_version) + expect(checks.check_kernel_minimum_version&.to_s) .to match(/Your Linux kernel .+ is too old/) end @@ -49,7 +49,7 @@ expect(Sandbox).not_to receive(:failure_reason) with_env(HOMEBREW_NO_SANDBOX_LINUX: "1") do - expect(checks.check_linux_sandbox).to be_nil + expect(checks.check_linux_sandbox&.to_s).to be_nil end end @@ -58,7 +58,7 @@ expect(Sandbox).not_to receive(:failure_reason) with_env(HOMEBREW_NO_SANDBOX_LINUX: nil) do - expect(checks.check_linux_sandbox).to be_nil + expect(checks.check_linux_sandbox&.to_s).to be_nil end end @@ -67,7 +67,7 @@ expect(Sandbox).not_to receive(:state) with_env(HOMEBREW_NO_SANDBOX_LINUX: nil) do - expect(checks.check_linux_sandbox).to be_nil + expect(checks.check_linux_sandbox&.to_s).to be_nil end end @@ -79,7 +79,7 @@ ) with_env(HOMEBREW_NO_SANDBOX_LINUX: nil) do - message = checks.check_linux_sandbox.to_s + message = checks.check_linux_sandbox&.to_s expect(message) .to include( @@ -101,7 +101,7 @@ ) with_env(HOMEBREW_NO_SANDBOX_LINUX: nil) do - message = checks.check_linux_sandbox.to_s + message = checks.check_linux_sandbox&.to_s expect(message) .to include( @@ -122,7 +122,7 @@ ) with_env(HOMEBREW_NO_SANDBOX_LINUX: nil) do - message = checks.check_linux_sandbox.to_s + message = checks.check_linux_sandbox&.to_s expect(message) .to include( @@ -143,7 +143,7 @@ specify "#check_for_symlinked_home" do allow(File).to receive(:symlink?).with("/home").and_return(true) - expect(checks.check_for_symlinked_home) + expect(checks.check_for_symlinked_home&.to_s) .to include("Your /home directory is a symlink") end end diff --git a/Library/Homebrew/test/os/mac/diagnostic_spec.rb b/Library/Homebrew/test/os/mac/diagnostic_spec.rb index 8571ccd133d7c..99c4f062b74c9 100644 --- a/Library/Homebrew/test/os/mac/diagnostic_spec.rb +++ b/Library/Homebrew/test/os/mac/diagnostic_spec.rb @@ -13,7 +13,7 @@ allow(OS::Mac).to receive_messages(version: macos_version, full_version: macos_version) allow(OS::Mac.version).to receive_messages(outdated_release?: false, prerelease?: true) - expect(checks.check_for_unsupported_macos) + expect(checks.check_for_unsupported_macos&.to_s) .to match("We do not provide support for this pre-release version.") end @@ -35,21 +35,21 @@ allow(Hardware::CPU).to receive(:features).and_return([:pclmulqdq]) allow(macos_version).to receive(:outdated_release?).and_return(false) - expect(checks.check_for_opencore).to include("This is a Tier 2 configuration") + expect(checks.check_for_opencore&.to_s).to include("This is a Tier 2 configuration") end it "reports Tier 3 on an old CPU" do allow(Hardware::CPU).to receive(:features).and_return([]) allow(macos_version).to receive(:outdated_release?).and_return(false) - expect(checks.check_for_opencore).to include("This is a Tier 3 configuration") + expect(checks.check_for_opencore&.to_s).to include("This is a Tier 3 configuration") end it "reports Tier 3 on a modern CPU running an outdated macOS" do allow(Hardware::CPU).to receive(:features).and_return([:pclmulqdq]) allow(macos_version).to receive(:outdated_release?).and_return(true) - expect(checks.check_for_opencore).to include("This is a Tier 3 configuration") + expect(checks.check_for_opencore&.to_s).to include("This is a Tier 3 configuration") end end @@ -58,7 +58,7 @@ allow(OS::Mac).to receive_messages(version: macos_version, full_version: macos_version) allow(OS::Mac::Xcode).to receive_messages(installed?: true, version: "8.0", without_clt?: true) - expect(checks.check_if_xcode_needs_clt_installed) + expect(checks.check_if_xcode_needs_clt_installed&.to_s) .to match("Xcode alone is not sufficient on Big Sur") end @@ -77,20 +77,20 @@ macos_version, "/some/path/MacOSX.sdk", :clt )) - expect(checks.check_if_supported_sdk_available).to be_nil + expect(checks.check_if_supported_sdk_available&.to_s).to be_nil end it "triggers when a valid SDK is not present on CLT systems" do allow(OS::Mac).to receive_messages(sdk: nil, sdk_locator: OS::Mac::CLT.sdk_locator) - expect(checks.check_if_supported_sdk_available) + expect(checks.check_if_supported_sdk_available&.to_s) .to include("Your Command Line Tools (CLT) does not support macOS #{macos_version}") end it "triggers when a valid SDK is not present on Xcode systems" do allow(OS::Mac).to receive_messages(sdk: nil, sdk_locator: OS::Mac::Xcode.sdk_locator) - expect(checks.check_if_supported_sdk_available) + expect(checks.check_if_supported_sdk_available&.to_s) .to include("Your Xcode does not support macOS #{macos_version}") end end @@ -103,7 +103,7 @@ OS::Mac::SDK.new(MacOSVersion.new("10.15"), "/some/path/MacOSX10.15.sdk", :clt), ]) - expect(checks.check_broken_sdks).to be_nil + expect(checks.check_broken_sdks&.to_s).to be_nil end it "triggers when the CLT SDK version doesn't match the folder name" do @@ -111,7 +111,7 @@ OS::Mac::SDK.new(MacOSVersion.new("10.14"), "/some/path/MacOSX10.15.sdk", :clt), ]) - expect(checks.check_broken_sdks) + expect(checks.check_broken_sdks&.to_s) .to include("SDKs in your Command Line Tools (CLT) installation do not match the SDK folder names") end @@ -121,7 +121,7 @@ OS::Mac::SDK.new(MacOSVersion.new("10.14"), "/some/path/MacOSX10.15.sdk", :xcode), ]) - expect(checks.check_broken_sdks) + expect(checks.check_broken_sdks&.to_s) .to include("The contents of the SDKs in your Xcode installation do not match the SDK folder names") end end @@ -138,87 +138,87 @@ it "doesn't trigger when pkgconf is not installed" do allow(Formula).to receive(:[]).with("pkgconf").and_raise(FormulaUnavailableError.new("pkgconf")) - expect(checks.check_pkgconf_macos_sdk_mismatch).to be_nil + expect(checks.check_pkgconf_macos_sdk_mismatch&.to_s).to be_nil end it "doesn't trigger when no versions are installed" do allow(pkg_config_formula).to receive(:any_version_installed?).and_return(false) - expect(checks.check_pkgconf_macos_sdk_mismatch).to be_nil + expect(checks.check_pkgconf_macos_sdk_mismatch&.to_s).to be_nil end it "doesn't trigger when built_on information is missing" do allow(tab).to receive(:built_on).and_return(nil) - expect(checks.check_pkgconf_macos_sdk_mismatch).to be_nil + expect(checks.check_pkgconf_macos_sdk_mismatch&.to_s).to be_nil end it "doesn't trigger when os_version information is missing" do allow(tab).to receive(:built_on).and_return({ "cpu_family" => "x86_64" }) - expect(checks.check_pkgconf_macos_sdk_mismatch).to be_nil + expect(checks.check_pkgconf_macos_sdk_mismatch&.to_s).to be_nil end it "doesn't trigger when versions match" do current_version = MacOS.version.to_s allow(tab).to receive(:built_on).and_return({ "os_version" => current_version }) - expect(checks.check_pkgconf_macos_sdk_mismatch).to be_nil + expect(checks.check_pkgconf_macos_sdk_mismatch&.to_s).to be_nil end it "triggers when built_on version differs from current macOS version" do allow(MacOS).to receive(:version).and_return(MacOSVersion.new("14")) allow(tab).to receive(:built_on).and_return({ "os_version" => "13" }) - expect(checks.check_pkgconf_macos_sdk_mismatch).to include("brew reinstall pkgconf") + expect(checks.check_pkgconf_macos_sdk_mismatch&.to_s).to include("brew reinstall pkgconf") end end describe "#check_cask_quarantine_support" do it "returns nil when quarantine is available" do allow(Cask::Quarantine).to receive(:check_quarantine_support).and_return([:quarantine_available, nil]) - expect(checks.check_cask_quarantine_support).to be_nil + expect(checks.check_cask_quarantine_support&.to_s).to be_nil end it "returns error when xattr is broken" do allow(Cask::Quarantine).to receive(:check_quarantine_support).and_return([:xattr_broken, nil]) - expect(checks.check_cask_quarantine_support) + expect(checks.check_cask_quarantine_support&.to_s) .to match("there's no working version of `xattr` on this system") end it "returns error when swift is not available" do allow(Cask::Quarantine).to receive(:check_quarantine_support).and_return([:no_swift, nil]) - expect(checks.check_cask_quarantine_support) + expect(checks.check_cask_quarantine_support&.to_s) .to match("there's no available version of `swift` on this system") end it "returns error when swift is broken due to missing CLT" do allow(Cask::Quarantine).to receive(:check_quarantine_support).and_return([:swift_broken_clt, nil]) - expect(checks.check_cask_quarantine_support) + expect(checks.check_cask_quarantine_support&.to_s) .to match("Swift is not working due to missing Command Line Tools") end it "returns error when swift compilation failed" do allow(Cask::Quarantine).to receive(:check_quarantine_support).and_return([:swift_compilation_failed, nil]) - expect(checks.check_cask_quarantine_support) + expect(checks.check_cask_quarantine_support&.to_s) .to match("Swift compilation failed") end it "returns error when swift runtime error occurs" do allow(Cask::Quarantine).to receive(:check_quarantine_support).and_return([:swift_runtime_error, nil]) - expect(checks.check_cask_quarantine_support) + expect(checks.check_cask_quarantine_support&.to_s) .to match("Swift runtime error") end it "returns error when swift is not executable" do allow(Cask::Quarantine).to receive(:check_quarantine_support).and_return([:swift_not_executable, nil]) - expect(checks.check_cask_quarantine_support) + expect(checks.check_cask_quarantine_support&.to_s) .to match("Swift is not executable") end it "returns error when swift returns unexpected error" do allow(Cask::Quarantine).to receive(:check_quarantine_support).and_return([:swift_unexpected_error, "whoopsie"]) - expect(checks.check_cask_quarantine_support) + expect(checks.check_cask_quarantine_support&.to_s) .to match("whoopsie") end end diff --git a/Library/Homebrew/test/support/helper/integration_mocks.rb b/Library/Homebrew/test/support/helper/integration_mocks.rb index 13e19d0116967..78cc863b98458 100644 --- a/Library/Homebrew/test/support/helper/integration_mocks.rb +++ b/Library/Homebrew/test/support/helper/integration_mocks.rb @@ -5,7 +5,7 @@ module Homebrew module Diagnostic class Checks def check_integration_test - "This is an integration test" if ENV["HOMEBREW_INTEGRATION_TEST"] + Finding.new(issue: "This is an integration test") if ENV["HOMEBREW_INTEGRATION_TEST"] end end end From 9ad97b6451898b9a7f7c34d734c033821d1baa66 Mon Sep 17 00:00:00 2001 From: Sean Molenaar Date: Sun, 21 Jun 2026 17:09:26 +0200 Subject: [PATCH 2/4] Rename finding text --- Library/Homebrew/diagnostic.rb | 133 +++++++++--------- .../Homebrew/extend/os/linux/diagnostic.rb | 79 ++++++----- Library/Homebrew/extend/os/mac/diagnostic.rb | 42 +++--- Library/Homebrew/extend/os/mac/pkgconf.rb | 2 +- Library/Homebrew/install.rb | 2 +- .../test/support/helper/integration_mocks.rb | 2 +- 6 files changed, 133 insertions(+), 127 deletions(-) diff --git a/Library/Homebrew/diagnostic.rb b/Library/Homebrew/diagnostic.rb index cb1a424042a4a..1f976bf111c8e 100644 --- a/Library/Homebrew/diagnostic.rb +++ b/Library/Homebrew/diagnostic.rb @@ -49,7 +49,7 @@ def to_h end sig { returns(T.nilable(String)) } - attr_reader :issue + attr_reader :text sig { returns(T.any(Integer, Symbol)) } attr_reader :tier @@ -63,9 +63,9 @@ def to_h sig { returns(T.nilable(Remediation)) } attr_reader :remediation - sig { params(issue: String, tier: T.any(Integer, Symbol), affects: T::Array[String], links: T::Array[String], remediation: T.any(T.nilable(Remediation), String)).void } - def initialize(issue:, tier: 1, affects: [], links: [], remediation: nil) - @issue = issue + sig { params(text: String, tier: T.any(Integer, Symbol), affects: T::Array[String], links: T::Array[String], remediation: T.any(T.nilable(Remediation), String)).void } + def initialize(text:, tier: 1, affects: [], links: [], remediation: nil) + @text = text @tier = tier @affects = affects @links = links @@ -82,7 +82,7 @@ def initialize(issue:, tier: 1, affects: [], links: [], remediation: nil) } def to_h { - issue: @issue, + text: @text, tier: @tier, affects: @affects, links: @links, @@ -93,7 +93,7 @@ def to_h sig { returns(String) } def to_s <<~EOS.rstrip - #{issue} + #{text} #{remediation.to_s.strip} #{support_tier_message(tier: tier)} EOS @@ -247,7 +247,7 @@ def examine_git_origin(repository_path, desired_origin) if current_origin.nil? Finding.new( - issue: "Without a correctly configured origin, Homebrew won't update + text: "Without a correctly configured origin, Homebrew won't update properly.", remediation: Finding::Remediation.new(text: "You can solve this by adding the remote", commands: [ "git -C \"#{repository_path}\" remote add origin #{Formatter.url(desired_origin)}", @@ -261,7 +261,7 @@ def examine_git_origin(repository_path, desired_origin) With a non-standard origin, Homebrew won't update properly. EOS Finding.new( - issue: issue, + text: issue, remediation: Finding::Remediation.new(text: "You can solve this by setting the origin remote", commands: [ "git -C \"#{repository_path}\" remote set-url origin #{Formatter.url(desired_origin)}", ]), @@ -277,7 +277,7 @@ def broken_tap(tap) return unless repo.git_repository? finding = Finding.new( - issue: "#{tap.full_name} was not tapped properly!", + text: "#{tap.full_name} was not tapped properly!", remediation: Finding::Remediation.new(text: "You can solve this by tapping again", commands: [ "rm -rf \"#{tap.path}\"", "brew tap #{tap.name}", @@ -298,7 +298,7 @@ def check_for_installed_developer_tools return if DevelopmentTools.installed? Finding.new( - issue: "", + text: "", remediation: Finding::Remediation.new(text: DevelopmentTools.installation_instructions), ) end @@ -354,7 +354,7 @@ def check_for_stray_dylibs Unexpected dylibs: EOS - Finding.new(issue: msg) if msg.present? + Finding.new(text: msg) if msg.present? end sig { returns(T.nilable(Finding)) } @@ -384,7 +384,7 @@ def check_for_stray_static_libs Unexpected static libraries: EOS - Finding.new(issue: msg) if msg.present? + Finding.new(text: msg) if msg.present? end sig { returns(T.nilable(Finding)) } @@ -409,7 +409,7 @@ def check_for_stray_pcs Unexpected '.pc' files: EOS ) - Finding.new(issue: msg) if msg.present? + Finding.new(text: msg) if msg.present? end sig { returns(T.nilable(Finding)) } @@ -433,7 +433,7 @@ def check_for_stray_las Unexpected '.la' files: EOS ) - Finding.new(issue: msg) if msg.present? + Finding.new(text: msg) if msg.present? end sig { returns(T.nilable(Finding)) } @@ -456,7 +456,7 @@ def check_for_stray_headers Unexpected header files: EOS - Finding.new(issue: msg) if msg.present? + Finding.new(text: msg) if msg.present? end sig { returns(T.nilable(Finding)) } @@ -473,7 +473,7 @@ def check_for_broken_symlinks return if broken_symlinks.empty? Finding.new( - issue: inject_file_list(broken_symlinks, <<~EOS + text: inject_file_list(broken_symlinks, <<~EOS Broken symlinks were found: EOS ), @@ -489,13 +489,16 @@ def check_tmpdir_sticky_bit return if !world_writable || HOMEBREW_TEMP.sticky? Finding.new( - issue: <<~EOS, + text: <<~EOS, #{HOMEBREW_TEMP} is world-writable but does not have the sticky bit set. EOS - remediation: <<~EOS, - To set it, run the following command: - sudo chmod +t #{HOMEBREW_TEMP} - EOS + remediation: Finding::Remediation.new( + text: <<~EOS, + To set it, run the following command: + sudo chmod +t #{HOMEBREW_TEMP} + EOS + commands: ["sudo chmod +t #{HOMEBREW_TEMP}"], + ), ) end @@ -507,7 +510,7 @@ def check_exist_directories return if not_exist_dirs.empty? Finding.new( - issue: <<~EOS, + text: <<~EOS, The following directories do not exist: #{not_exist_dirs.join("\n")} EOS @@ -527,7 +530,7 @@ def check_access_directories return if not_writable_dirs.empty? Finding.new( - issue: <<~EOS, + text: <<~EOS, The following directories are not writable by your user: #{not_writable_dirs.join("\n")} EOS @@ -548,7 +551,7 @@ def check_multiple_cellars return unless (HOMEBREW_PREFIX/"Cellar").exist? Finding.new( - issue: <<~EOS, + text: <<~EOS, You have multiple Cellars. EOS remediation: <<~EOS, @@ -599,7 +602,7 @@ def check_user_path_1 end @user_path_1_done = true - Finding.new(issue: message, remediation: remediation) if message.present? + Finding.new(text: message, remediation: remediation) if message.present? end sig { returns(T.nilable(Finding)) } @@ -608,7 +611,7 @@ def check_user_path_2 return if @seen_prefix_bin Finding.new( - issue: <<~EOS, + text: <<~EOS, Homebrew's "bin" was not found in your PATH. EOS remediation: <<~EOS, @@ -630,7 +633,7 @@ def check_user_path_3 return if sbin.children.one? && sbin.children.first.basename.to_s == ".keepme" Finding.new( - issue: <<~EOS, + text: <<~EOS, Homebrew's "sbin" was not found in your PATH but you have installed formulae that put executables in #{HOMEBREW_PREFIX}/sbin. EOS @@ -647,7 +650,7 @@ def check_for_symlinked_cellar return unless HOMEBREW_CELLAR.symlink? Finding.new( - issue: <<~EOS, + text: <<~EOS, Symlinked Cellars can cause problems. Your Homebrew Cellar is a symlink: #{HOMEBREW_CELLAR} which resolves to: #{HOMEBREW_CELLAR.realpath} @@ -672,7 +675,7 @@ def check_git_version git = Formula["git"] git_upgrade_cmd = git.any_version_installed? ? "upgrade" : "install" Finding.new( - issue: <<~EOS, + text: <<~EOS, An outdated version (#{Utils::Git.version}) of Git was detected in your PATH. Git #{minimum_version} or newer is required for Homebrew. EOS @@ -688,7 +691,7 @@ def check_for_git return if Utils::Git.available? Finding.new( - issue: <<~EOS, + text: <<~EOS, Git could not be found in your PATH. Homebrew uses Git for several internal functions and some formulae use Git checkouts instead of stable tarballs. @@ -708,7 +711,7 @@ def check_git_newline_settings return if autocrlf != "true" Finding.new( - issue: <<~EOS, + text: <<~EOS, Suspicious Git newline settings found. The detected Git newline settings will cause checkout problems: @@ -736,7 +739,7 @@ def check_homebrew_repository_git_hooks return if found.empty? Finding.new( - issue: inject_file_list(found, <<~EOS + text: inject_file_list(found, <<~EOS Git hooks or a repository-local `.gitconfig` were found in your Homebrew repository. Homebrew does not use these, and they can break Homebrew operations. @@ -760,7 +763,7 @@ def check_brew_git_origin def check_for_nix_homebrew return unless OS.nix_managed_homebrew? - Finding.new(tier: :nix, issue: <<~EOS, + Finding.new(tier: :nix, text: <<~EOS, Your Homebrew installation is managed by Nix. Homebrew does not support Nix-managed installations. EOS @@ -830,7 +833,7 @@ def check_tap_git_branch EOS end - Finding.new(issue: message, remediation: remediation) if message.present? + Finding.new(text: message, remediation: remediation) if message.present? end sig { returns(T.nilable(Finding)) } @@ -844,7 +847,7 @@ def check_deprecated_official_taps return if tapped_deprecated_taps.empty? Finding.new( - issue: <<~EOS, + text: <<~EOS, You have the following deprecated, official taps tapped: Homebrew/homebrew-#{tapped_deprecated_taps.join("\n Homebrew/homebrew-")} EOS @@ -944,7 +947,7 @@ def check_untrusted_taps EOS Finding.new( - issue: <<~EOS, + text: <<~EOS, The following taps are not trusted: #{untrusted_tap_names.join("\n ")} @@ -982,7 +985,7 @@ def check_for_other_frameworks return if frameworks_found.empty? Finding.new( - issue: <<~EOS, + text: <<~EOS, Some frameworks can be picked up by CMake's build system and will likely cause the build to fail. EOS @@ -999,7 +1002,7 @@ def check_tmpdir return if tmpdir.nil? || File.directory?(tmpdir) Finding.new( - issue: <<~EOS, + text: <<~EOS, TMPDIR #{tmpdir.inspect} doesn't exist. EOS ) @@ -1016,7 +1019,7 @@ def check_missing_deps return if missing.empty? Finding.new( - issue: <<~EOS, + text: <<~EOS, Some installed formulae or casks are missing dependencies. Run `brew missing` for more details. EOS @@ -1036,7 +1039,7 @@ def check_deprecated_disabled Finding.new( affects: deprecated_or_disabled.map(&:full_name), - issue: "Some installed formulae are deprecated or disabled.", + text: "Some installed formulae are deprecated or disabled.", remediation: <<~EOS, You should find replacements for the following formulae: #{deprecated_or_disabled.sort_by(&:full_name).uniq * "\n "} @@ -1052,7 +1055,7 @@ def check_cask_deprecated_disabled Finding.new( affects: deprecated_or_disabled.map(&:full_name), - issue: "Some installed casks are deprecated or disabled.", + text: "Some installed casks are deprecated or disabled.", remediation: <<~EOS, You should find replacements for the following casks: #{deprecated_or_disabled.sort_by(&:token).uniq * "\n "} @@ -1110,7 +1113,7 @@ def __tap_git_status(tap, path) Uncommitted files: EOS - Finding.new(issue: message, affects: modified) if message.present? + Finding.new(text: message, affects: modified) if message.present? end sig { returns(T.nilable(Finding)) } @@ -1122,7 +1125,7 @@ def check_for_non_prefixed_coreutils return unless paths.intersect?(gnubin) Finding.new( - issue: <<~EOS, + text: <<~EOS, Putting non-prefixed coreutils in your path can cause GMP builds to fail. EOS ) @@ -1139,7 +1142,7 @@ def check_for_pydistutils_cfg_in_home "https://bugs.python.org/issue6138", "https://bugs.python.org/issue4655", ], - issue: <<~EOS, + text: <<~EOS, A '.pydistutils.cfg' file was found in $HOME, which may cause Python builds to fail. See: #{Formatter.url("https://bugs.python.org/issue6138")} @@ -1163,7 +1166,7 @@ def check_for_unreadable_installed_formula Finding.new( affects: formula_unavailable_exceptions, - issue: <<~EOS, + text: <<~EOS, Some installed formulae are not readable: #{formula_unavailable_exceptions.join("\n\n ")} EOS @@ -1187,7 +1190,7 @@ def check_for_unlinked_but_not_keg_only Finding.new( affects: unlinked.map(&:to_s), - issue: <<~EOS, + text: <<~EOS, You have unlinked kegs in your Cellar. Leaving kegs unlinked can lead to build-trouble and cause formulae that depend on those kegs to fail to run properly once built. @@ -1224,7 +1227,7 @@ def check_for_external_cmd_name_conflict EOS end - Finding.new(issue: message) + Finding.new(text: message) end sig { returns(T.nilable(Finding)) } @@ -1246,7 +1249,7 @@ def check_for_tap_ruby_files_locations return if bad_tap_files.empty? Finding.new( - issue: bad_tap_files.keys.map do |tap| + text: bad_tap_files.keys.map do |tap| <<~EOS Found Ruby file outside #{tap} tap formula directory. (#{tap.formula_dir}): @@ -1263,7 +1266,7 @@ def check_homebrew_prefix Finding.new( tier: 3, remediation: "Consider uninstalling Homebrew and reinstalling into the default prefix.", - issue: <<~EOS, + text: <<~EOS, Your Homebrew's prefix is not #{Homebrew::DEFAULT_PREFIX}. Most of Homebrew's bottles (binary packages) can only be used with the default prefix. @@ -1300,7 +1303,7 @@ def check_deleted_formula Finding.new( affects: deleted_formulae, - issue: <<~EOS, + text: <<~EOS, Some installed kegs have no formulae! This means they were either deleted or installed manually. @@ -1323,7 +1326,7 @@ def check_for_unnecessary_core_tap Please remove it by running: brew untap #{CoreTap.instance.name} EOS - Finding.new(remediation: remediation, issue: <<~EOS, + Finding.new(remediation: remediation, text: <<~EOS, You have an unnecessary local Core tap! This can cause problems installing up-to-date formulae. EOS @@ -1343,7 +1346,7 @@ def check_for_unnecessary_cask_tap Please remove it by running: brew untap #{cask_tap.name} EOS - Finding.new(remediation: remediation, issue: <<~EOS, + Finding.new(remediation: remediation, text: <<~EOS, You have an unnecessary local Cask tap. This can cause problems installing up-to-date casks. EOS @@ -1362,7 +1365,7 @@ def check_deprecated_cask_taps brew untap #{tapped_caskroom_taps.join(" ")} EOS ) - Finding.new(remediation: remediation, issue: <<~EOS, + Finding.new(remediation: remediation, text: <<~EOS, You have the following deprecated Cask taps installed: #{tapped_caskroom_taps.join("\n ")} EOS @@ -1382,7 +1385,7 @@ def check_cask_install_location return if locations.empty? Finding.new( - issue: locations.map do |l| + text: locations.map do |l| "Legacy install at #{l}." end.join("\n"), remediation: <<~EOS, @@ -1408,7 +1411,7 @@ def check_cask_staging_location sudo chown -R #{current_user} #{user_tilde(path.to_s)} EOS ) - Finding.new(remediation: remediation, issue: <<~EOS, + Finding.new(remediation: remediation, text: <<~EOS, The staging path #{user_tilde(path.to_s)} is not writable by the current user. EOS ) @@ -1420,7 +1423,7 @@ def check_cask_corrupt_dirs return if corrupt.empty? Finding.new( - issue: <<~EOS, + text: <<~EOS, Some directories in the Caskroom do not have valid metadata. #{corrupt.map { |token| "#{Cask::Caskroom.path}/#{token}" }.join("\n ")} The following #{Utils.pluralize("cask", corrupt.count)} cannot be upgraded as-is. @@ -1454,7 +1457,7 @@ def check_cask_taps taps_string = Utils.pluralize("tap", error_tap_paths.count) return unless error_tap_paths.present? - Finding.new(issue: "Unable to read from cask #{taps_string}: #{error_tap_paths.to_sentence}") + Finding.new(text: "Unable to read from cask #{taps_string}: #{error_tap_paths.to_sentence}") end sig { returns(T.nilable(Finding)) } @@ -1463,7 +1466,7 @@ def check_cask_load_path add_info "$LOAD_PATHS", paths.presence || none_string - Finding.new(issue: "$LOAD_PATH is empty") if paths.blank? + Finding.new(text: "$LOAD_PATH is empty") if paths.blank? end sig { returns(T.nilable(Finding)) } @@ -1498,7 +1501,7 @@ def check_cask_environment_variables def check_cask_xattr # If quarantine is not available, a warning is already shown by check_cask_quarantine_support so just return return unless Cask::Quarantine.available? - return Finding.new(issue: "Unable to find `xattr`.") unless File.exist?("/usr/bin/xattr") + return Finding.new(text: "Unable to find `xattr`.") unless File.exist?("/usr/bin/xattr") result = system_command "/usr/bin/xattr", args: ["-h"] @@ -1509,7 +1512,7 @@ def check_cask_xattr if result.include? "Python 2.7" Finding.new( - issue: <<~EOS, + text: <<~EOS, Your Python installation has a broken version of setuptools. EOS remediation: <<~EOS, @@ -1519,7 +1522,7 @@ def check_cask_xattr ) else Finding.new( - issue: <<~EOS, + text: <<~EOS, The system Python version is wrong. EOS remediation: <<~EOS, @@ -1529,9 +1532,9 @@ def check_cask_xattr ) end elsif result.stderr.include? "pkg_resources.DistributionNotFound" - Finding.new(issue: "Your Python installation is unable to find `xattr`.") + Finding.new(text: "Your Python installation is unable to find `xattr`.") else - Finding.new(issue: "unknown xattr error: #{result.stderr.split("\n").last}") + Finding.new(text: "unknown xattr error: #{result.stderr.split("\n").last}") end end @@ -1562,7 +1565,7 @@ def check_for_duplicate_formulae end Finding.new( - issue: <<~EOS, + text: <<~EOS, The following formulae have the same name as core formulae: #{shadowed_formula_full_names.join("\n ")} EOS @@ -1595,7 +1598,7 @@ def check_for_duplicate_casks end Finding.new( - issue: <<~EOS, + text: <<~EOS, The following casks have the same name as core casks: #{shadowed_cask_full_names.join("\n ")} EOS diff --git a/Library/Homebrew/extend/os/linux/diagnostic.rb b/Library/Homebrew/extend/os/linux/diagnostic.rb index 26a5f5759bb7a..f3ac4e82bdc95 100644 --- a/Library/Homebrew/extend/os/linux/diagnostic.rb +++ b/Library/Homebrew/extend/os/linux/diagnostic.rb @@ -38,10 +38,10 @@ def supported_configuration_checks sig { returns(T.nilable(::Homebrew::Diagnostic::Finding)) } def check_tmpdir_sticky_bit - message = super - return if message.nil? + finding = super + return if finding.nil? - message + <<~EOS + finding.remediation.text += <<~EOS If you don't have administrative privileges on this machine, create a directory and set the `$HOMEBREW_TEMP` environment variable, for example: @@ -59,16 +59,19 @@ def check_tmpdir_executable return if system T.must(f.path) ::Homebrew::Diagnostic::Finding.new( - issue: <<~EOS, + text: <<~EOS, The directory #{HOMEBREW_TEMP} does not permit executing programs. It is likely mounted as "noexec". EOS - remediation: <<~EOS, - 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 + remediation: ::Homebrew::Diagnostic::Finding::Remediation.new( + commands: ["export HOMEBREW_TEMP=~/tmp", "echo 'export HOMEBREW_TEMP=~/tmp' >> #{Utils::Shell.profile}"], + text: <<~EOS, + 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 @@ -79,7 +82,7 @@ def check_umask_not_zero return unless File.umask.zero? ::Homebrew::Diagnostic::Finding.new( - issue: <<~EOS, + text: <<~EOS, umask is currently set to 000. Directories created by Homebrew cannot be world-writable. EOS @@ -99,8 +102,8 @@ def check_supported_architecture return if ::Hardware::CPU.arm64? ::Homebrew::Diagnostic::Finding.new( - tier: 2, - issue: <<~EOS, + tier: 2, + text: <<~EOS, Your CPU architecture (#{::Hardware::CPU.arch}) is not supported. We only support x86_64 or ARM64/AArch64 CPU architectures. You will be unable to use binary packages (bottles). EOS @@ -113,7 +116,7 @@ def check_glibc_minimum_version ::Homebrew::Diagnostic::Finding.new( tier: :unsupported, - issue: <<~EOS, + text: <<~EOS, Your system glibc #{OS::Linux::Glibc.system_version} is too old. We only support glibc #{OS::Linux::Glibc.minimum_version} or later. EOS @@ -134,7 +137,7 @@ def check_glibc_version ::Homebrew::Diagnostic::Finding.new( tier: 2, - issue: <<~EOS, + text: <<~EOS, Your system glibc #{OS::Linux::Glibc.system_version} is too old. We will need to automatically install a newer version. EOS @@ -156,14 +159,14 @@ def check_glibc_next_version return if ENV["HOMEBREW_GLIBC_TESTING"] || ENV["CI"] || ENV["HOMEBREW_TEST_BOT"].present? ::Homebrew::Diagnostic::Finding.new( - issue: <<~EOS, - Your system glibc #{OS::Linux::Glibc.system_version} is older than #{OS::LINUX_GLIBC_NEXT_CI_VERSION}. - An upcoming brew release will automatically install a newer version. - EOS, - remediation: <<~EOS - We recommend updating to a newer version via your distribution's - package manager, upgrading your distribution to the latest version, - or changing distributions. + text: <<~EOS, + Your system glibc #{OS::Linux::Glibc.system_version} is older than #{OS::LINUX_GLIBC_NEXT_CI_VERSION}. + An upcoming brew release will automatically install a newer version. + EOS + remediation: <<~EOS, + 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 @@ -173,16 +176,16 @@ def check_kernel_minimum_version return unless OS::Linux::Kernel.below_minimum_version? ::Homebrew::Diagnostic::Finding.new( - tier: 3, - issue: <<~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). - EOS, - remediation: <<~EOS - We recommend updating to a newer version via your distribution's - package manager, upgrading your distribution to the latest version, - or changing distributions. + tier: 3, + text: <<~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). + EOS + remediation: <<~EOS, + 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 @@ -234,7 +237,7 @@ def check_linux_sandbox end ::Homebrew::Diagnostic::Finding.new( - issue: reason, + text: reason, remediation: [ *fix_lines, "", @@ -256,7 +259,7 @@ def check_linuxbrew_core EOS ::Homebrew::Diagnostic::Finding.new( - issue: issue, + text: issue, remediation: <<~EOS, You can unset `$HOMEBREW_NO_INSTALL_FROM_API` or set the repository's remote to homebrew-core to update core formulae. @@ -270,7 +273,7 @@ def check_linuxbrew_bottle_domain ::Homebrew::Diagnostic::Finding.new( remediation: "You can unset `$HOMEBREW_BOTTLE_DOMAIN` or adjust it to not contain \"linuxbrew\".", - issue: <<~EOS, + text: <<~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). @@ -292,7 +295,7 @@ def check_for_symlinked_home EOS ::Homebrew::Diagnostic::Finding.new( tier: 2, - issue: issue, + text: issue, links: ["https://github.com/Homebrew/brew/issues/18036"], remediation: <<~EOS, If you encounter linking issues, you may need to manually create conflicting @@ -339,7 +342,7 @@ def check_gcc_dependent_linkage ) ::Homebrew::Diagnostic::Finding.new( remediation: remediation, - issue: <<~EOS, + text: <<~EOS, Formulae which link to GCC through a versioned path were found. These formulae are prone to breaking when GCC is updated. EOS diff --git a/Library/Homebrew/extend/os/mac/diagnostic.rb b/Library/Homebrew/extend/os/mac/diagnostic.rb index 13a902d63cb09..3b89072982485 100644 --- a/Library/Homebrew/extend/os/mac/diagnostic.rb +++ b/Library/Homebrew/extend/os/mac/diagnostic.rb @@ -117,7 +117,7 @@ def check_for_non_prefixed_findutils return if !default_names && !paths.intersect?(gnubin) ::Homebrew::Diagnostic::Finding.new( - issue: <<~EOS, + text: <<~EOS, Putting non-prefixed findutils in your path can cause python builds to fail." EOS ) @@ -151,7 +151,7 @@ def check_for_unsupported_macos ::Homebrew::Diagnostic::Finding.new( remediation: remediation, tier: tier, - issue: <<~EOS, + text: <<~EOS, You are using macOS #{MacOS.version}. #{who} do not provide support for this #{what} EOS @@ -180,11 +180,11 @@ def check_for_opencore end ::Homebrew::Diagnostic::Finding.new( - issue: <<~EOS, + text: <<~EOS, You have booted macOS using OpenCore Legacy Patcher. We do not provide support for this configuration. EOS - tier: oclp_support_tier, + tier: oclp_support_tier, ) end @@ -218,7 +218,7 @@ def check_xcode_up_to_date ::Homebrew::Diagnostic::Finding.new( tier: 2, - issue: "Your Xcode (#{MacOS::Xcode.version}) is outdated.", + text: "Your Xcode (#{MacOS::Xcode.version}) is outdated.", remediation: remediation, ) end @@ -237,7 +237,7 @@ def check_clt_up_to_date return if GitHub::Actions.env_set? ::Homebrew::Diagnostic::Finding.new( - issue: "A newer Command Line Tools release is available.", + text: "A newer Command Line Tools release is available.", tier: 2, remediation: MacOS::CLT.update_instructions, ) @@ -251,7 +251,7 @@ def check_xcode_minimum_version xcode += " => #{MacOS::Xcode.prefix}" unless MacOS::Xcode.default_prefix? ::Homebrew::Diagnostic::Finding.new( - issue: <<~EOS, + text: <<~EOS, Your Xcode (#{xcode}) at #{MacOS::Xcode.bundle_path} is too outdated. EOS remediation: <<~EOS, @@ -266,7 +266,7 @@ def check_clt_minimum_version return unless MacOS::CLT.below_minimum_version? ::Homebrew::Diagnostic::Finding.new( - issue: <<~EOS, + text: <<~EOS, Your Command Line Tools are too outdated. EOS remediation: MacOS::CLT.update_instructions, @@ -278,7 +278,7 @@ def check_if_xcode_needs_clt_installed return unless MacOS::Xcode.needs_clt_installed? ::Homebrew::Diagnostic::Finding.new( - issue: <<~EOS, + text: <<~EOS, Xcode alone is not sufficient on #{MacOS.version.pretty_name}. EOS remediation: ::DevelopmentTools.installation_instructions, @@ -292,7 +292,7 @@ def check_xcode_prefix return unless prefix.to_s.include?(" ") ::Homebrew::Diagnostic::Finding.new( - issue: <<~EOS, + text: <<~EOS, Xcode is installed to a directory with a space in the name. This will cause some formulae to fail to build. EOS @@ -305,7 +305,7 @@ def check_xcode_prefix_exists return if prefix.nil? || prefix.exist? ::Homebrew::Diagnostic::Finding.new( - issue: <<~EOS, + text: <<~EOS, The directory Xcode is reportedly installed to doesn't exist: #{prefix} EOS @@ -325,7 +325,7 @@ def check_xcode_select_path path = "/Developer" if path.nil? || !path.directory? ::Homebrew::Diagnostic::Finding.new( - issue: <<~EOS, + text: <<~EOS, Your Xcode is configured with an invalid path. EOS remediation: ::Homebrew::Diagnostic::Finding::Remediation.new( @@ -346,7 +346,7 @@ def check_xcode_license_approved return if $CHILD_STATUS.success? ::Homebrew::Diagnostic::Finding.new( - issue: <<~EOS, + text: <<~EOS, You have not agreed to the Xcode license. EOS remediation: ::Homebrew::Diagnostic::Finding::Remediation.new( @@ -388,7 +388,7 @@ def check_filesystem_case_sensitive case_sensitive_vols.uniq! ::Homebrew::Diagnostic::Finding.new( - issue: <<~EOS, + text: <<~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 @@ -425,7 +425,7 @@ def check_for_gettext end ::Homebrew::Diagnostic::Finding.new( - issue: <<~EOS, + text: <<~EOS, gettext files detected at a system prefix. These files can cause compilation and link failures, especially if they are compiled with improper architectures. @@ -450,7 +450,7 @@ def check_for_iconv if libiconv&.linked_keg&.directory? unless libiconv&.keg_only? ::Homebrew::Diagnostic::Finding.new( - issue: <<~EOS, + text: <<~EOS, A libiconv formula is installed and linked. This will break stuff. For serious. Unlink it. EOS @@ -458,7 +458,7 @@ def check_for_iconv end else ::Homebrew::Diagnostic::Finding.new( - issue: <<~EOS, + text: <<~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 @@ -504,7 +504,7 @@ def check_for_multiple_volumes exists. Formulae known to be affected by this are Git and Narwhal. EOS ::Homebrew::Diagnostic::Finding.new( - issue: issue, + text: issue, tier: 2, remediation: <<~EOS, You should set the `$HOMEBREW_TEMP` environment variable to a suitable @@ -533,7 +533,7 @@ def check_if_supported_sdk_available end ::Homebrew::Diagnostic::Finding.new( - issue: <<~EOS, + text: <<~EOS, Your #{source} does not support macOS #{MacOS.version}.\nIt is either outdated or was modified. EOS remediation: ::Homebrew::Diagnostic::Finding::Remediation.new(text: <<~EOS, @@ -574,7 +574,7 @@ def check_broken_sdks ) ::Homebrew::Diagnostic::Finding.new( remediation: remediation, - issue: <<~EOS, + text: <<~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 @@ -660,7 +660,7 @@ def check_cask_quarantine_support return unless messages.first.present? ::Homebrew::Diagnostic::Finding.new( - issue: T.must(messages.first), + text: T.must(messages.first), remediation: messages.last, ) end diff --git a/Library/Homebrew/extend/os/mac/pkgconf.rb b/Library/Homebrew/extend/os/mac/pkgconf.rb index b3fd3f84a4f6b..210341cc3f0b8 100644 --- a/Library/Homebrew/extend/os/mac/pkgconf.rb +++ b/Library/Homebrew/extend/os/mac/pkgconf.rb @@ -45,7 +45,7 @@ def mismatch_warning_message(mismatch) EOS commands: ["brew reinstall pkgconf"], ), - issue: <<~EOS, + text: <<~EOS, You have pkgconf installed that was built on macOS #{mismatch[0]}, but you are running macOS #{mismatch[1]}. diff --git a/Library/Homebrew/install.rb b/Library/Homebrew/install.rb index 4e811f5b3e5e9..8172653f2d5e7 100644 --- a/Library/Homebrew/install.rb +++ b/Library/Homebrew/install.rb @@ -30,7 +30,7 @@ def perform_preinstall_checks_once(all_fatal: false) def check_cc_argv(cc) return unless cc - @checks ||= T.let(Diagnostic::Finding.new(issue: "", tier: 3), T.nilable(Homebrew::Diagnostic::Finding)) + @checks ||= T.let(Diagnostic::Finding.new(text: "", tier: 3), T.nilable(Homebrew::Diagnostic::Finding)) opoo <<~EOS You passed `--cc=#{cc}`. diff --git a/Library/Homebrew/test/support/helper/integration_mocks.rb b/Library/Homebrew/test/support/helper/integration_mocks.rb index 78cc863b98458..8ef6540264355 100644 --- a/Library/Homebrew/test/support/helper/integration_mocks.rb +++ b/Library/Homebrew/test/support/helper/integration_mocks.rb @@ -5,7 +5,7 @@ module Homebrew module Diagnostic class Checks def check_integration_test - Finding.new(issue: "This is an integration test") if ENV["HOMEBREW_INTEGRATION_TEST"] + Finding.new(text: "This is an integration test") if ENV["HOMEBREW_INTEGRATION_TEST"] end end end From 812f921f90f58e17765d07bbc54376c23171dc3c Mon Sep 17 00:00:00 2001 From: Sean Molenaar Date: Sun, 21 Jun 2026 17:09:39 +0200 Subject: [PATCH 3/4] Add link to flat namespace check --- Library/Homebrew/extend/os/mac/formula_cellar_checks.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/Library/Homebrew/extend/os/mac/formula_cellar_checks.rb b/Library/Homebrew/extend/os/mac/formula_cellar_checks.rb index 60e4e43d2cac6..aab5477c55d90 100644 --- a/Library/Homebrew/extend/os/mac/formula_cellar_checks.rb +++ b/Library/Homebrew/extend/os/mac/formula_cellar_checks.rb @@ -103,6 +103,7 @@ def check_linkage sig { params(formula: ::Formula).returns(T.nilable(String)) } def check_flat_namespace(formula) + # See https://developer.apple.com/forums/thread/689991?answerId=687895022#687895022 return unless formula.prefix.directory? return if formula.tap&.audit_exception(:flat_namespace_allowlist, formula.name) From 800b5e2347003af130025fac17aa7343501c5588 Mon Sep 17 00:00:00 2001 From: Sean Molenaar Date: Sun, 21 Jun 2026 17:14:15 +0200 Subject: [PATCH 4/4] Mark command as failed with JSON --- Library/Homebrew/cmd/doctor.rb | 2 ++ Library/Homebrew/test/os/linux/diagnostic_spec.rb | 12 ++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Library/Homebrew/cmd/doctor.rb b/Library/Homebrew/cmd/doctor.rb index fd533dc7e67db..257bd308e5200 100644 --- a/Library/Homebrew/cmd/doctor.rb +++ b/Library/Homebrew/cmd/doctor.rb @@ -71,6 +71,7 @@ def run next if return_findings.empty? if args.json? + Homebrew.failed = true findings.concat(return_findings.compact.map(&:to_h)) next end @@ -92,6 +93,7 @@ def run if args.json? tier = findings.max_by { |f| f[:tier] }&.fetch(:tier, 1) puts JSON.pretty_generate({ tier: tier, findings: findings }).gsub(/\[\n\n\s*\]/, "[]") + return end diff --git a/Library/Homebrew/test/os/linux/diagnostic_spec.rb b/Library/Homebrew/test/os/linux/diagnostic_spec.rb index 20c01655f9478..cf845e37bd109 100644 --- a/Library/Homebrew/test/os/linux/diagnostic_spec.rb +++ b/Library/Homebrew/test/os/linux/diagnostic_spec.rb @@ -79,7 +79,7 @@ ) with_env(HOMEBREW_NO_SANDBOX_LINUX: nil) do - message = checks.check_linux_sandbox&.to_s + message = checks.check_linux_sandbox&.to_s&.rstrip expect(message) .to include( @@ -90,7 +90,7 @@ "export HOMEBREW_NO_SANDBOX_LINUX=1", ) expect(message).not_to include("sysctl") - expect(message).to end_with(" export HOMEBREW_NO_SANDBOX_LINUX=1\n") + expect(message).to end_with(" export HOMEBREW_NO_SANDBOX_LINUX=1") end end @@ -101,7 +101,7 @@ ) with_env(HOMEBREW_NO_SANDBOX_LINUX: nil) do - message = checks.check_linux_sandbox&.to_s + message = checks.check_linux_sandbox&.to_s&.rstrip expect(message) .to include( @@ -111,7 +111,7 @@ "export HOMEBREW_NO_SANDBOX_LINUX=1", ) expect(message).not_to include("sysctl") - expect(message).to end_with(" export HOMEBREW_NO_SANDBOX_LINUX=1\n") + expect(message).to end_with(" export HOMEBREW_NO_SANDBOX_LINUX=1") end end @@ -122,7 +122,7 @@ ) with_env(HOMEBREW_NO_SANDBOX_LINUX: nil) do - message = checks.check_linux_sandbox&.to_s + message = checks.check_linux_sandbox&.to_s&.rstrip expect(message) .to include( @@ -136,7 +136,7 @@ "Allows unprivileged user namespaces on AppArmor-enabled systems", "export HOMEBREW_NO_SANDBOX_LINUX=1", ) - expect(message).to end_with(" export HOMEBREW_NO_SANDBOX_LINUX=1\n") + expect(message).to end_with(" export HOMEBREW_NO_SANDBOX_LINUX=1") end end