Merge pull request #10552 from Bo98/sdk-checks

os/mac/diagnostic: check SDK version in SDKSettings matches the path
This commit is contained in:
Bo Anderson 2021-02-11 12:53:05 +00:00 committed by GitHub
commit 0f4ccd7625
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 319 additions and 56 deletions

View File

@ -51,10 +51,7 @@ class DevelopmentTools
sig { returns(String) }
def installation_instructions
<<~EOS
Install the Command Line Tools:
xcode-select --install
EOS
MacOS::CLT.installation_instructions
end
sig { returns(String) }

View File

@ -64,6 +64,7 @@ module Homebrew
check_clt_minimum_version
check_if_xcode_needs_clt_installed
check_if_supported_sdk_available
check_broken_sdks
].freeze
end
@ -425,7 +426,7 @@ module Homebrew
source = if locator.source == :clt
update_instructions = MacOS::CLT.update_instructions
"CLT"
"Command Line Tools (CLT)"
else
update_instructions = MacOS::Xcode.update_instructions
"Xcode"
@ -438,6 +439,40 @@ module Homebrew
#{update_instructions}
EOS
end
# The CLT 10.x -> 11.x upgrade process on 10.14 contained a bug which broke the SDKs.
# Notably, MacOSX10.14.sdk would indirectly symlink to MacOSX10.15.sdk.
# This diagnostic was introduced to check for this and recommend a full reinstall.
def check_broken_sdks
locator = MacOS.sdk_locator
return if locator.all_sdks.all? do |sdk|
path_version = sdk.path.basename.to_s[MacOS::SDK::VERSIONED_SDK_REGEX, 1]
next true if path_version.blank?
sdk.version == MacOS::Version.new(path_version).strip_patch
end
if locator.source == :clt
source = "Command Line Tools (CLT)"
path_to_remove = MacOS::CLT::PKG_PATH
installation_instructions = MacOS::CLT.installation_instructions
else
source = "Xcode"
path_to_remove = MacOS::Xcode.bundle_path
installation_instructions = MacOS::Xcode.installation_instructions
end
<<~EOS
The contents of the SDKs in your #{source} installation do not match the SDK folder names.
A clean reinstall of #{source} should fix this.
Remove the broken installation before reinstalling:
sudo rm -rf #{path_to_remove}
#{installation_instructions}
EOS
end
end
end
end

View File

@ -9,6 +9,8 @@ module OS
#
# @api private
class SDK
VERSIONED_SDK_REGEX = /MacOSX(\d+\.\d+)\.sdk$/.freeze
attr_reader :version, :path, :source
def initialize(version, path, source)
@ -25,14 +27,39 @@ module OS
class NoSDKError < StandardError; end
def sdk_for(v)
path = sdk_paths[v]
raise NoSDKError if path.nil?
sdk = all_sdks.find { |s| s.version == v }
raise NoSDKError if sdk.nil?
SDK.new v, path, source
sdk
end
def all_sdks
sdk_paths.map { |v, p| SDK.new v, p, source }
return @all_sdks if @all_sdks
@all_sdks = []
# Bail out if there is no SDK prefix at all
return @all_sdks unless File.directory? sdk_prefix
# Use unversioned SDK path on Big Sur to avoid issues such as:
# https://github.com/Homebrew/homebrew-core/issues/67075
unversioned_sdk_path = Pathname.new("#{sdk_prefix}/MacOSX.sdk")
version = read_sdk_version(unversioned_sdk_path)
if version && version >= :big_sur
unversioned_sdk_version = version
@all_sdks << SDK.new(unversioned_sdk_version, unversioned_sdk_path, source)
end
Dir["#{sdk_prefix}/MacOSX*.sdk"].each do |sdk_path|
next unless sdk_path.match?(SDK::VERSIONED_SDK_REGEX)
version = read_sdk_version(Pathname.new(sdk_path))
next if version.nil? || version == unversioned_sdk_version
@all_sdks << SDK.new(version, sdk_path, source)
end
@all_sdks
end
def sdk_if_applicable(v = nil)
@ -64,43 +91,34 @@ module OS
""
end
def sdk_paths
@sdk_paths ||= begin
# Bail out if there is no SDK prefix at all
if File.directory? sdk_prefix
paths = {}
Dir[File.join(sdk_prefix, "MacOSX*.sdk")].each do |sdk_path|
version = sdk_path[/MacOSX(\d+\.\d+)u?\.sdk$/, 1]
paths[OS::Mac::Version.new(version)] = sdk_path if version.present?
end
# Use unversioned SDK path on Big Sur to avoid issues such as:
# https://github.com/Homebrew/homebrew-core/issues/67075
# This creates an entry in `paths` whose key is the OS major version
sdk_path = Pathname.new("#{sdk_prefix}/MacOSX.sdk")
sdk_settings = sdk_path/"SDKSettings.json"
if sdk_settings.exist? &&
(sdk_settings_string = sdk_settings.read.presence) &&
(sdk_settings_json = JSON.parse(sdk_settings_string).presence) &&
(version_string = sdk_settings_json.fetch("Version", nil).presence) &&
(version = version_string[/(\d+)\./, 1].presence)
paths[OS::Mac::Version.new(version)] = sdk_path
end
paths
else
{}
end
end
def latest_sdk
all_sdks.max_by(&:version)
end
# NOTE: This returns a versioned SDK path, even on Big Sur
def latest_sdk
return if sdk_paths.empty?
def read_sdk_version(sdk_path)
sdk_settings = sdk_path/"SDKSettings.json"
sdk_settings_string = sdk_settings.read if sdk_settings.exist?
v, path = sdk_paths.max { |(v1, _), (v2, _)| v1 <=> v2 }
SDK.new v, path, source
# Pre-10.14 SDKs
sdk_settings = sdk_path/"SDKSettings.plist"
if sdk_settings_string.blank? && sdk_settings.exist?
result = system_command("plutil", args: ["-convert", "json", "-o", "-", sdk_settings])
sdk_settings_string = result.stdout if result.success?
end
return if sdk_settings_string.blank?
sdk_settings_json = JSON.parse(sdk_settings_string)
return if sdk_settings_json.blank?
version_string = sdk_settings_json.fetch("Version", nil)
return if version_string.blank?
begin
OS::Mac::Version.new(version_string).strip_patch
rescue MacOSVersionError
nil
end
end
end
private_constant :BaseSDKLocator

View File

@ -78,17 +78,19 @@ module OS
end
end
sig { returns(T.self_type) }
def strip_patch
# Big Sur is 11.x but Catalina is 10.15.x.
if major >= 11
self.class.new(major.to_s)
else
major_minor
end
end
sig { returns(Symbol) }
def to_sym
@to_sym ||= begin
# Big Sur is 11.x but Catalina is 10.15.
major_macos = if major >= 11
major
else
major_minor
end.to_s
SYMBOLS.invert.fetch(major_macos, :dunno)
end
@to_sym ||= SYMBOLS.invert.fetch(strip_patch.to_s, :dunno)
end
sig { returns(String) }

View File

@ -134,6 +134,19 @@ module OS
sdk(v)&.path
end
def installation_instructions
if OS::Mac.prerelease?
<<~EOS
Xcode can be installed from:
#{Formatter.url("https://developer.apple.com/download/more/")}
EOS
else
<<~EOS
Xcode can be installed from the App Store.
EOS
end
end
sig { returns(String) }
def update_instructions
if OS::Mac.prerelease?
@ -254,6 +267,21 @@ module OS
sdk(v)&.path
end
def installation_instructions
if MacOS.version == "10.14"
# This is not available from `xcode-select`
<<~EOS
Install the Command Line Tools for Xcode 11.3.1 from:
#{Formatter.url("https://developer.apple.com/download/more/")}
EOS
else
<<~EOS
Install the Command Line Tools:
xcode-select --install
EOS
end
end
sig { returns(String) }
def update_instructions
software_update_location = if MacOS.version >= "10.14"

View File

@ -39,4 +39,79 @@ describe Homebrew::Diagnostic::Checks do
expect(checks.check_ruby_version)
.to match "Ruby version 1.8.6 is unsupported on 10.12"
end
describe "#check_if_supported_sdk_available" do
let(:macos_version) { OS::Mac::Version.new("11") }
before do
allow(DevelopmentTools).to receive(:installed?).and_return(true)
allow(OS::Mac).to receive(:version).and_return(macos_version)
end
it "doesn't trigger when SDK root is not needed" do
allow(OS::Mac).to receive(:sdk_root_needed?).and_return(false)
allow(OS::Mac).to receive(:sdk).and_return(nil)
expect(checks.check_if_supported_sdk_available).to be_nil
end
it "doesn't trigger when a valid SDK is present" do
allow(OS::Mac).to receive(:sdk_root_needed?).and_return(true)
allow(OS::Mac).to receive(:sdk).and_return(OS::Mac::SDK.new(macos_version, "/some/path/MacOSX.sdk", :clt))
expect(checks.check_if_supported_sdk_available).to be_nil
end
it "triggers when a valid SDK is not present on CLT systems" do
allow(OS::Mac).to receive(:sdk_root_needed?).and_return(true)
allow(OS::Mac).to receive(:sdk).and_return(nil)
allow(OS::Mac).to receive(:sdk_locator).and_return(OS::Mac::CLT.sdk_locator)
expect(checks.check_if_supported_sdk_available)
.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(:sdk_root_needed?).and_return(true)
allow(OS::Mac).to receive(:sdk).and_return(nil)
allow(OS::Mac).to receive(:sdk_locator).and_return(OS::Mac::Xcode.sdk_locator)
expect(checks.check_if_supported_sdk_available)
.to include("Your Xcode does not support macOS #{macos_version}")
end
end
describe "#check_broken_sdks" do
it "doesn't trigger when SDK versions are as expected" do
allow(OS::Mac).to receive(:sdk_locator).and_return(OS::Mac::CLT.sdk_locator)
allow_any_instance_of(OS::Mac::CLTSDKLocator).to receive(:all_sdks)
.and_return([
OS::Mac::SDK.new(OS::Mac::Version.new("11"), "/some/path/MacOSX.sdk", :clt),
OS::Mac::SDK.new(OS::Mac::Version.new("10.15"), "/some/path/MacOSX10.15.sdk", :clt),
])
expect(checks.check_broken_sdks).to be_nil
end
it "triggers when the CLT SDK version doesn't match the folder name" do
allow_any_instance_of(OS::Mac::CLTSDKLocator).to receive(:all_sdks)
.and_return([
OS::Mac::SDK.new(OS::Mac::Version.new("10.14"), "/some/path/MacOSX10.15.sdk", :clt),
])
expect(checks.check_broken_sdks)
.to include("SDKs in your Command Line Tools (CLT) installation do not match the SDK folder names")
end
it "triggers when the Xcode SDK version doesn't match the folder name" do
allow(OS::Mac).to receive(:sdk_locator).and_return(OS::Mac::Xcode.sdk_locator)
allow_any_instance_of(OS::Mac::XcodeSDKLocator).to receive(:all_sdks)
.and_return([
OS::Mac::SDK.new(OS::Mac::Version.new("10.14"), "/some/path/MacOSX10.15.sdk", :xcode),
])
expect(checks.check_broken_sdks)
.to include("The contents of the SDKs in your Xcode installation do not match the SDK folder names")
end
end
end

View File

@ -0,0 +1,97 @@
# typed: false
# frozen_string_literal: true
describe OS::Mac::CLTSDKLocator do
subject(:locator) { described_class.new }
let(:big_sur_sdk) { OS::Mac::SDK.new(OS::Mac::Version.new("11"), "/some/path/MacOSX.sdk", :clt) }
let(:catalina_sdk) { OS::Mac::SDK.new(OS::Mac::Version.new("10.15"), "/some/path/MacOSX10.15.sdk", :clt) }
specify "#sdk_for" do
allow(locator).to receive(:all_sdks).and_return([big_sur_sdk, catalina_sdk])
expect(locator.sdk_for(OS::Mac::Version.new("11"))).to eq(big_sur_sdk)
expect(locator.sdk_for(OS::Mac::Version.new("10.15"))).to eq(catalina_sdk)
expect { locator.sdk_for(OS::Mac::Version.new("10.14")) }.to raise_error(described_class::NoSDKError)
end
describe "#sdk_if_applicable" do
before do
allow(locator).to receive(:all_sdks).and_return([big_sur_sdk, catalina_sdk])
end
it "returns the requested SDK" do
expect(locator.sdk_if_applicable(OS::Mac::Version.new("11"))).to eq(big_sur_sdk)
expect(locator.sdk_if_applicable(OS::Mac::Version.new("10.15"))).to eq(catalina_sdk)
end
it "returns the latest SDK if the requested version is not found" do
expect(locator.sdk_if_applicable(OS::Mac::Version.new("10.14"))).to eq(big_sur_sdk)
expect(locator.sdk_if_applicable(OS::Mac::Version.new("12"))).to eq(big_sur_sdk)
end
it "returns the SDK matching the OS version if no version is specified" do
allow(OS::Mac).to receive(:version).and_return(OS::Mac::Version.new("10.15"))
expect(locator.sdk_if_applicable).to eq(catalina_sdk)
end
it "returns the latest SDK on older OS versions when there's no matching SDK" do
allow(OS::Mac).to receive(:version).and_return(OS::Mac::Version.new("10.14"))
expect(locator.sdk_if_applicable).to eq(big_sur_sdk)
end
it "returns nil if the OS is newer than all SDKs" do
allow(OS::Mac).to receive(:version).and_return(OS::Mac::Version.new("12"))
expect(locator.sdk_if_applicable).to be_nil
end
end
describe "#all_sdks" do
let(:big_sur_sdk_prefix) { TEST_FIXTURE_DIR/"sdks/big_sur" }
let(:mojave_broken_sdk_prefix) { TEST_FIXTURE_DIR/"sdks/mojave_broken" }
let(:high_sierra_sdk_prefix) { TEST_FIXTURE_DIR/"sdks/high_sierra" }
let(:malformed_sdk_prefix) { TEST_FIXTURE_DIR/"sdks/malformed" }
it "reads the SDKSettings.json version of unversioned SDKs folders" do
allow(locator).to receive(:sdk_prefix).and_return(big_sur_sdk_prefix.to_s)
sdks = locator.all_sdks
expect(sdks.count).to eq(1)
sdk = sdks.first
expect(sdk.path).to eq(big_sur_sdk_prefix/"MacOSX.sdk")
expect(sdk.version).to eq(OS::Mac::Version.new("11"))
expect(sdk.source).to eq(:clt)
end
it "reads the SDKSettings.json version of versioned SDKs folders" do
allow(locator).to receive(:sdk_prefix).and_return(mojave_broken_sdk_prefix.to_s)
sdks = locator.all_sdks
expect(sdks.count).to eq(1)
sdk = sdks.first
expect(sdk.path).to eq(mojave_broken_sdk_prefix/"MacOSX10.14.sdk")
expect(sdk.version).to eq(OS::Mac::Version.new("10.15"))
expect(sdk.source).to eq(:clt)
end
it "reads the SDKSettings.plist version" do
allow(locator).to receive(:sdk_prefix).and_return(high_sierra_sdk_prefix.to_s)
sdks = locator.all_sdks
expect(sdks.count).to eq(1)
sdk = sdks.first
expect(sdk.path).to eq(high_sierra_sdk_prefix/"MacOSX10.13.sdk")
expect(sdk.version).to eq(OS::Mac::Version.new("10.13"))
expect(sdk.source).to eq(:clt)
end
it "rejects malformed sdks" do
allow(locator).to receive(:sdk_prefix).and_return(malformed_sdk_prefix.to_s)
expect(locator.all_sdks).to be_empty
end
end
end

View File

@ -0,0 +1 @@
{"Version":"11.1"}

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Version</key>
<string>10.13</string>
</dict>
</plist>

View File

@ -0,0 +1 @@
{"Version":"broken"}

View File

@ -0,0 +1 @@
{"Version":"10.15.6"}

View File

@ -589,15 +589,15 @@ class Version
end
# @api public
sig { returns(Version) }
sig { returns(T.self_type) }
def major_minor
Version.new([major, minor].compact.join("."))
self.class.new([major, minor].compact.join("."))
end
# @api public
sig { returns(Version) }
sig { returns(T.self_type) }
def major_minor_patch
Version.new([major, minor, patch].compact.join("."))
self.class.new([major, minor, patch].compact.join("."))
end
sig { returns(T::Boolean) }