From ef46c89aeca29b5b56d8338ebfb18acea372b7dd Mon Sep 17 00:00:00 2001 From: Patrick Linnane Date: Tue, 6 Oct 2026 07:50:24 -0700 Subject: [PATCH] Stop inferring Xcode versions from clang Signed-off-by: Patrick Linnane --- Library/Homebrew/brew.sh | 2 +- .../Homebrew/extend/os/mac/system_config.rb | 8 ++- Library/Homebrew/os/mac/xcode.rb | 64 ++----------------- Library/Homebrew/test/cmd/config_spec.rb | 11 ++++ Library/Homebrew/test/os/mac/xcode_spec.rb | 48 ++++++++------ docs/Xcode.md | 7 +- 6 files changed, 54 insertions(+), 86 deletions(-) diff --git a/Library/Homebrew/brew.sh b/Library/Homebrew/brew.sh index ed94d7684b..b1e854d881 100644 --- a/Library/Homebrew/brew.sh +++ b/Library/Homebrew/brew.sh @@ -333,7 +333,7 @@ HOMEBREW_BOTTLE_DEFAULT_DOMAIN="https://ghcr.io/v2/homebrew/core" # - https://github.com/Homebrew/install/blob/HEAD/install.sh # - Library/Homebrew/os/mac.rb (latest_sdk_version) # - Library/Homebrew/os/mac/xcode.rb (latest_version), (minimum_version) -# - Library/Homebrew/os/mac/xcode.rb (detect_version_from_clang_version), (latest_clang_version) +# - Library/Homebrew/os/mac/xcode.rb (latest_clang_version) # and, if needed: # - MacOSVersion::RELEASES HOMEBREW_MACOS_NEWEST_UNSUPPORTED="28" diff --git a/Library/Homebrew/extend/os/mac/system_config.rb b/Library/Homebrew/extend/os/mac/system_config.rb index b7c5bed29c..30f766e817 100644 --- a/Library/Homebrew/extend/os/mac/system_config.rb +++ b/Library/Homebrew/extend/os/mac/system_config.rb @@ -15,7 +15,7 @@ module OS def initialize super @xcode = T.let(nil, T.nilable(String)) - @clt = T.let(nil, T.nilable(Version)) + @clt = T.let(nil, T.nilable(String)) end sig { returns(String) } @@ -35,9 +35,11 @@ module OS end end - sig { returns(T.nilable(Version)) } + sig { returns(T.nilable(String)) } def clt - @clt ||= MacOS::CLT.version if MacOS::CLT.installed? + @clt ||= if MacOS::CLT.installed? + MacOS::CLT.version.to_s.presence || "unknown" + end end sig { returns(T.nilable(String)) } diff --git a/Library/Homebrew/os/mac/xcode.rb b/Library/Homebrew/os/mac/xcode.rb index 81887352fb..d8451dcad6 100644 --- a/Library/Homebrew/os/mac/xcode.rb +++ b/Library/Homebrew/os/mac/xcode.rb @@ -170,9 +170,6 @@ module OS # @api internal sig { returns(::Version) } def self.version - # may return a version string - # that is guessed based on the compiler, so do not - # use it in order to check if Xcode is installed. if @version ||= T.let(detect_version, T.nilable(String)) ::Version.new @version else @@ -184,8 +181,6 @@ module OS def self.detect_version # This is a separate function as you can't cache the value out of a block # if return is used in the middle, which we do many times in here. - return if !MacOS::Xcode.installed? && !MacOS::CLT.installed? - if (xcode_prefix = prefix) # Fast path that will probably almost always work unless `xcode-select -p` is misconfigured version_plist = xcode_prefix.parent/"version.plist" @@ -210,40 +205,7 @@ module OS end end - detect_version_from_clang_version - end - - sig { params(version: ::Version, build: T.nilable(String)).returns(String) } - def self.detect_version_from_clang_version( - version = ::DevelopmentTools.clang_version, - build = ::DevelopmentTools.clang_version_output&.[](/clang-(\d+(?:\.\d+)+)/, 1) - ) - return "dunno" if version.null? - - # This logic provides a fake Xcode version based on the - # installed CLT version. This is useful as they are packaged - # simultaneously so workarounds need to apply to both based on their - # comparable version. - case version - when "11.0.0" then "11.3.1" - when "11.0.3" then "11.7" - when "12.0.0" then "12.4" - when "12.0.5" then "12.5.1" - when "13.0.0" then "13.2.1" - when "13.1.6" then "13.4.1" - when "14.0.0" then "14.2" - when "14.0.3" then "14.3.1" - when "15.0.0" then "15.4" - when "16.0.0" then "16.2" - when "17.0.0" then "26.3" - else - # Xcode 26.4 onwards ship clang 21.0.0 so map the full build to the newest matching Xcode. - case build - when /\A2100\.0\./ then "26.4.1" - when /\A2100\.1\./ then "26.6" - else "27.0" - end - end + nil end sig { returns(T::Boolean) } @@ -262,7 +224,7 @@ module OS # Returns true even if outdated tools are installed. sig { returns(T::Boolean) } def self.installed? - !version.null? + File.exist?("#{PKG_PATH}/usr/bin/clang") end sig { returns(CLTSDKLocator) } @@ -377,7 +339,7 @@ module OS sig { returns(T::Boolean) } def self.below_minimum_version? - return false unless installed? + return false if version.null? version < minimum_version end @@ -396,18 +358,10 @@ module OS version_output[/clang-(\d+(?:\.\d+)+)/, 1] end - sig { returns(T.nilable(String)) } - def self.detect_version_from_clang_version - clang_build = detect_clang_version - return if clang_build.nil? - - clang_version = clang_build.sub(/\A(\d+)(\d)(\d)\..*/, "\\1.\\2.\\3") - MacOS::Xcode.detect_version_from_clang_version(Version.new(clang_version), clang_build) - end - # Version string (a pretty long one) of the CLT package. # Note that the different ways of installing the CLTs lead to different - # version numbers. + # version numbers. Installations without a package receipt have an + # unknown version (`Version::NULL`). # # @api internal sig { returns(::Version) } @@ -421,13 +375,9 @@ module OS sig { returns(T.nilable(String)) } def self.detect_version - version = T.let(nil, T.nilable(String)) - if File.exist?("#{PKG_PATH}/usr/bin/clang") - version = MacOS.pkgutil_info(EXECUTABLE_PKG_ID)[/version: (.+)$/, 1] - return version if version - end + return unless installed? - detect_version_from_clang_version + MacOS.pkgutil_info(EXECUTABLE_PKG_ID)[/version: (.+)$/, 1] end end end diff --git a/Library/Homebrew/test/cmd/config_spec.rb b/Library/Homebrew/test/cmd/config_spec.rb index d0415aece7..40a4e37cc9 100644 --- a/Library/Homebrew/test/cmd/config_spec.rb +++ b/Library/Homebrew/test/cmd/config_spec.rb @@ -51,6 +51,17 @@ RSpec.describe Homebrew::Cmd::Config do ]) end + it "prints an unknown Command Line Tools version", :needs_macos do + output = StringIO.new + + allow(OS::Mac::CLT).to receive_messages(installed?: true, version: Version::NULL) + allow(SystemConfig).to receive(:config_sections).and_return([:macos_config]) + + SystemConfig.dump_verbose_config(output) + + expect(output.string).to include("CLT: unknown\n") + end + it "reads the Windows version on WSL", :needs_linux do allow(OS).to receive(:wsl?).and_return(true) stub_const("ORIGINAL_PATHS", [windows_cmd.dirname]) diff --git a/Library/Homebrew/test/os/mac/xcode_spec.rb b/Library/Homebrew/test/os/mac/xcode_spec.rb index 7f08764a19..2dea20302e 100644 --- a/Library/Homebrew/test/os/mac/xcode_spec.rb +++ b/Library/Homebrew/test/os/mac/xcode_spec.rb @@ -40,18 +40,11 @@ RSpec.describe OS::Mac::Xcode, :needs_macos do end describe ".detect_version" do - test_each_hash({ "2100.0.123.102" => "26.4.1", "2100.1.1.101" => "26.6", - "2100.3.34.2" => "27.0" }) do |build, xcode| - it "infers Xcode #{xcode} from the clang-#{build} Command Line Tools compiler" do - allow(described_class).to receive_messages(installed?: false, prefix: nil) - allow(OS::Mac::CLT).to receive(:installed?).and_return(true) - allow(DevelopmentTools).to receive_messages( - clang_version: Version.new("21.0.0"), - clang_version_output: "Apple clang version 21.0.0 (clang-#{build})\n", - ) + it "does not infer a version from the Command Line Tools" do + allow(described_class).to receive_messages(installed?: false, prefix: nil) + allow(OS::Mac::CLT).to receive(:installed?).and_return(true) - expect(described_class.detect_version).to eq(xcode) - end + expect(described_class.detect_version).to be_nil end it "loads Plist when version.plist exists" do @@ -73,12 +66,6 @@ RSpec.describe OS::Mac::Xcode, :needs_macos do end end - describe ".detect_version_from_clang_version" do - it "preserves Xcode 26.3 for clang 17" do - expect(described_class.detect_version_from_clang_version(Version.new("17.0.0"))).to eq("26.3") - end - end - describe OS::Mac::CLT do describe ".latest_version" do test_each_hash({ "27" => "27.0", "26" => "27.0", "15" => "26.3", "14" => "16.2", @@ -152,11 +139,30 @@ RSpec.describe OS::Mac::Xcode, :needs_macos do end end - describe ".detect_version_from_clang_version" do - it "infers Xcode 26.6 from the Command Line Tools 26.6 compiler" do - allow(described_class).to receive(:detect_clang_version).and_return("2100.1.1.101") + describe ".installed?" do + it "detects Command Line Tools without a package receipt" do + allow(File).to receive(:exist?).and_call_original + allow(File).to receive(:exist?).with("#{OS::Mac::CLT::PKG_PATH}/usr/bin/clang").and_return(true) + allow(MacOS).to receive(:pkgutil_info).and_return("") - expect(described_class.detect_version_from_clang_version).to eq("26.6") + expect(described_class.installed?).to be true + end + end + + describe ".detect_version" do + it "leaves the version unknown without a package receipt" do + allow(described_class).to receive(:installed?).and_return(true) + allow(MacOS).to receive(:pkgutil_info).and_return("") + + expect(described_class.detect_version).to be_nil + end + end + + describe ".below_minimum_version?" do + it "does not treat an unknown version as below the minimum" do + allow(described_class).to receive_messages(installed?: true, version: Version::NULL) + + expect(described_class.below_minimum_version?).to be false end end diff --git a/docs/Xcode.md b/docs/Xcode.md index 12de5b6349..d9996e474c 100644 --- a/docs/Xcode.md +++ b/docs/Xcode.md @@ -22,10 +22,9 @@ When Apple publishes a new Xcode or Command Line Tools release: 1. Confirm the version, build number, bundled Apple Clang version and supported macOS versions from Apple's release information. 2. Update `OS::Mac::Xcode.latest_version` when the latest Xcode mapping changes. 3. Update `OS::Mac::CLT.latest_clang_version` when the Command Line Tools compiler mapping changes. -4. Update `OS::Mac::Xcode.detect_version_from_clang_version` when Homebrew must infer a new Xcode version from Apple Clang. -5. Review minimum-version checks and diagnostic text in the same file for assumptions affected by the release. -6. Add or update automated coverage for every changed mapping or inference. -7. Verify `brew config` and the relevant `brew doctor` output on an affected macOS runner when one is available. +4. Review minimum-version checks and diagnostic text in the same file for assumptions affected by the release. +5. Add or update automated coverage for every changed mapping. +6. Verify `brew config` and the relevant `brew doctor` output on an affected macOS runner when one is available. Run the repository checks from the Homebrew/brew checkout: