Stop inferring Xcode versions from clang

Signed-off-by: Patrick Linnane <patrick@linnane.io>
This commit is contained in:
Patrick Linnane committed 2026-10-06 10:19:24 -07:00
1 parent 1b0b73c84b
commit ef46c89aec
6 files changed
+54 -86

No files matched your search

+1 -1
View File
@@ -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"
@@ -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)) }
+7 -57
View File
@@ -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
+11
View File
@@ -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])
+27 -21
View File
@@ -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
+3 -4
View File
@@ -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: