Skip to content

Commit

Permalink
[scan] Filter simulators with version greater than currently installe…
Browse files Browse the repository at this point in the history
…d SDK
  • Loading branch information
wuaar1003 committed Nov 29, 2023
1 parent 142cfba commit aacdbca
Show file tree
Hide file tree
Showing 4 changed files with 225 additions and 8 deletions.
47 changes: 39 additions & 8 deletions scan/lib/scan/detect_values.rb
@@ -1,10 +1,18 @@
require 'fastlane_core/device_manager'
require 'fastlane_core/project'
require 'pathname'
require_relative 'module'

module Scan
# This class detects all kinds of default values
class DetectValues
PLATFORM_SIMULATOR_NAME = {
'iOS' => 'iPhoneSimulator',
'tvOS' => 'AppleTVSimulator',
'watchOS' => 'WatchSimulator',
'visionOS' => 'XRSimulator'
}.freeze

# This is needed as these are more complex default values
# Returns the finished config object
def self.set_additional_default_values
Expand Down Expand Up @@ -100,6 +108,33 @@ def self.filter_simulators(simulators, operator = :greater_than_or_equal, deploy
end
end

def self.detect_sdk_version(platform)
UI.crash!("Unknown platform: #{platform}") unless PLATFORM_SIMULATOR_NAME.key?(platform)
simulator_name = PLATFORM_SIMULATOR_NAME[platform]
@sdk_versions ||= {}
@sdk_versions[platform] ||= begin
platform_path = Pathname.new("Platforms/#{simulator_name}.platform/Developer/SDKs/")
sdks_path = Pathname.new(FastlaneCore::Helper.xcode_path).join(platform_path)
default_sdk_path = sdks_path.join("#{simulator_name}.sdk")
sdk_path = sdks_path.children.find { |child| child.symlink? && child.realpath == default_sdk_path }
UI.crash!("Unable to find default #{simulator_name} SDK version from SDKs: #{sdks_path.children}") unless sdk_path

version = /#{Regexp.quote(simulator_name)}(?<val>.*)\.sdk/.match(sdk_path.basename.to_s)
UI.crash!("Could not determine SDK version from #{sdk_path}") unless version && version[:val] != ''

begin
Gem::Version.new(version[:val])
rescue ArgumentError => e
UI.crash!("Could not parse SDK version: #{e.message}")
end
end
end

def self.compatible_with_sdk(sim)
default_sdk_version = detect_sdk_version(sim.os_type)
Gem::Version.new(sim.os_version) <= default_sdk_version || sim.os_version.start_with?(default_sdk_version.version)
end

def self.regular_expression_for_split_on_whitespace_followed_by_parenthesized_version
# %r{
# \s # a whitespace character
Expand Down Expand Up @@ -137,26 +172,23 @@ def self.detect_simulator(devices, requested_os_type, deployment_target_key, def
# We create 2 lambdas, which we iterate over later on
# If the first lambda `matches` found a simulator to use
# we'll never call the second one

matches = lambda do
set_of_simulators = devices.inject(
Set.new # of simulators
) do |set, device_string|
pieces = device_string.split(regular_expression_for_split_on_whitespace_followed_by_parenthesized_version)

selector = ->(sim) { pieces.count > 0 && sim.name == pieces.first }

display_device = "'#{device_string}'"

set + (
if pieces.count == 0
[] # empty array
elsif pieces.count == 1
simulators
.select(&selector)
.select { |sim| sim.name == pieces.first && compatible_with_sdk(sim) }
.reverse # more efficient, because `simctl` prints higher versions first
.sort_by! { |sim| Gem::Version.new(sim.os_version) }
.pop(1)
.pop(1) # Select the "highest versioned" simulator if none specified
else # pieces.count == 2 -- mathematically, because of the 'end of line' part of our regular expression
version = pieces[1].tr('()', '')
display_device = "'#{pieces[0]}' with version #{version}"
Expand All @@ -168,7 +200,7 @@ def self.detect_simulator(devices, requested_os_type, deployment_target_key, def
"of deployment target (#{deployment_target_version})")
end
end
filter_simulators(simulators, :equal, version).tap(&potential_emptiness_error).select(&selector)
filter_simulators(simulators, :equal, version).tap(&potential_emptiness_error).select { |sim| sim.name == pieces.first }
end
).tap do |array|
if array.empty?
Expand All @@ -187,7 +219,7 @@ def self.detect_simulator(devices, requested_os_type, deployment_target_key, def

result = Array(
simulators
.select { |sim| sim.name == default_device_name }
.select { |sim| sim.name == default_device_name && compatible_with_sdk(sim) }
.reverse # more efficient, because `simctl` prints higher versions first
.sort_by! { |sim| Gem::Version.new(sim.os_version) }
.last || simulators.first
Expand Down Expand Up @@ -248,7 +280,6 @@ def self.get_deployment_target_version(deployment_target_key)
version = Scan.config[:deployment_target_version]
version ||= Scan.project.build_settings(key: deployment_target_key) if Scan.project
version ||= 0

return version
end
end
Expand Down
150 changes: 150 additions & 0 deletions scan/spec/detect_values_spec.rb
Expand Up @@ -51,6 +51,156 @@
end
end

describe "#detect_sdk_version" do
it 'informs users of unknown platform name' do
expect do
Scan::DetectValues.detect_sdk_version('test')
end.to raise_error(FastlaneCore::Interface::FastlaneCrash, "Unknown platform: test")
end
%w[iOS tvOS watchOS].each do |platform|
simulator_name = Scan::DetectValues::PLATFORM_SIMULATOR_NAME[platform]

it "returns an error if there is no default #{platform} SDK symlink" do
default_path = double("path/to/default.sdk")
platform_path = double("path/to/sdks")
sdks_path = double("full/path/to/sdks",
children: [
double("path/to/other.sdk", symlink?: false),
double("path/to/some.sdk", symlink?: false),
double("path/to/default.sdk", symlink?: false)
])

allow(Pathname).to receive(:new).with("Platforms/#{simulator_name}.platform/Developer/SDKs/").and_return(platform_path)
allow(FastlaneCore::Helper).to receive(:xcode_path).and_return('mock')
allow(Pathname).to receive(:new).with('mock').and_return(sdks_path)
allow(sdks_path).to receive(:join).with(platform_path).and_return(sdks_path)
allow(sdks_path).to receive(:join).with("#{simulator_name}.sdk").and_return(default_path)

expect do
Scan::DetectValues.detect_sdk_version(platform)
end.to raise_error(FastlaneCore::Interface::FastlaneCrash, "Unable to find default #{simulator_name} SDK version from SDKs: #{sdks_path.children}")
end
it "returns an error on failure to determine #{platform} SDK version from filename" do
default_path = double("path/to/default.sdk")
platform_path = double("path/to/sdks")
some_path = double("path/to/some.sdk", symlink?: true, realpath: default_path, basename: "#{simulator_name}.sdk")
sdks_path = double("full/path/to/sdks",
children: [
double("path/to/other.sdk", symlink?: false),
some_path,
double("path/to/default.sdk", symlink?: false)
])

allow(Pathname).to receive(:new).with("Platforms/#{simulator_name}.platform/Developer/SDKs/").and_return(platform_path)
allow(FastlaneCore::Helper).to receive(:xcode_path).and_return('mock')
allow(Pathname).to receive(:new).with('mock').and_return(sdks_path)
allow(sdks_path).to receive(:join).with(platform_path).and_return(sdks_path)
allow(sdks_path).to receive(:join).with("#{simulator_name}.sdk").and_return(default_path)

expect do
Scan::DetectValues.detect_sdk_version(platform)
end.to raise_error(FastlaneCore::Interface::FastlaneCrash, "Could not determine SDK version from #{some_path}")
end
it "returns an error on failure to parse #{platform} SDK version from filename" do
default_path = double("path/to/default.sdk")
platform_path = double("path/to/sdks")
some_path = double("path/to/some.sdk", symlink?: true, realpath: default_path, basename: "#{simulator_name}asdf17g.sdk")
sdks_path = double("full/path/to/sdks",
children: [
double("path/to/other.sdk", symlink?: false),
some_path,
double("path/to/default.sdk", symlink?: false)
])

allow(Pathname).to receive(:new).with("Platforms/#{simulator_name}.platform/Developer/SDKs/").and_return(platform_path)
allow(FastlaneCore::Helper).to receive(:xcode_path).and_return('mock')
allow(Pathname).to receive(:new).with('mock').and_return(sdks_path)
allow(sdks_path).to receive(:join).with(platform_path).and_return(sdks_path)
allow(sdks_path).to receive(:join).with("#{simulator_name}.sdk").and_return(default_path)

expect do
Scan::DetectValues.detect_sdk_version(platform)
end.to raise_error(FastlaneCore::Interface::FastlaneCrash, "Could not parse SDK version: Malformed version number string asdf17g")
end
it "detects the expected default for #{platform}" do
default_path = double("path/to/default.sdk")
platform_path = double("path/to/sdks")
target_version = "17.0"
sdks_path = double("full/path/to/sdks",
children: [
double("path/to/other.sdk", symlink?: false),
double("path/to/some.sdk", symlink?: true, realpath: default_path, basename: "#{simulator_name}#{target_version}.sdk"),
double("path/to/default.sdk", symlink?: false)
])

allow(Pathname).to receive(:new).with("Platforms/#{simulator_name}.platform/Developer/SDKs/").and_return(platform_path)
allow(FastlaneCore::Helper).to receive(:xcode_path).and_return('mock')
allow(Pathname).to receive(:new).with('mock').and_return(sdks_path)
allow(sdks_path).to receive(:join).with(platform_path).and_return(sdks_path)
allow(sdks_path).to receive(:join).with("#{simulator_name}.sdk").and_return(default_path)

expect(Scan::DetectValues.detect_sdk_version(platform.to_s)).to equal(Gem::Version.new(target_version))
end
end
end

describe "#detect_simulator" do
it 'returns simulators for requested devices' do
simctl_device_output = double("simctl device output", read: File.read('./scan/spec/fixtures/DeviceManagerSimctlOutputXcode15'))
expect(Open3).to receive(:popen3).with("xcrun simctl list devices").and_yield(nil, simctl_device_output, nil, nil)

simctl_runtime_output = double("simctl runtime output", read: "line\n")
allow(Open3).to receive(:popen3).with("xcrun simctl list runtimes").and_yield(nil, simctl_runtime_output, nil, nil)

allow(Scan::DetectValues).to receive(:detect_sdk_version).with('iOS').and_return(Gem::Version.new('17.0'))
allow(Scan::DetectValues).to receive(:detect_sdk_version).with('tvOS').and_return(Gem::Version.new('17.0'))
allow(Scan::DetectValues).to receive(:detect_sdk_version).with('watchOS').and_return(Gem::Version.new('10.0'))

devices = ['iPhone 14 Pro Max', 'Apple TV 4K (3rd generation)', 'Apple Watch Ultra (49mm)']
simulators = Scan::DetectValues.detect_simulator(devices, '', '', '', nil)

expect(simulators.count).to eq(3)
expect(simulators[0]).to have_attributes(
name: "iPhone 14 Pro Max", os_type: "iOS", os_version: "17.0"
)
expect(simulators[1]).to have_attributes(
name: "Apple TV 4K (3rd generation)", os_type: "tvOS", os_version: "17.0"
)
expect(simulators[2]).to have_attributes(
name: "Apple Watch Ultra (49mm)", os_type: "watchOS", os_version: "10.0"
)
end

it 'filters out simulators newer than what the current Xcode SDK supports' do
simctl_device_output = double("simctl device output", read: File.read('./scan/spec/fixtures/DeviceManagerSimctlOutputXcode14'))
expect(Open3).to receive(:popen3).with("xcrun simctl list devices").and_yield(nil, simctl_device_output, nil, nil)

simctl_runtime_output = double("simctl runtime output", read: "line\n")
allow(Open3).to receive(:popen3).with("xcrun simctl list runtimes").and_yield(nil, simctl_runtime_output, nil, nil)

allow(Scan::DetectValues).to receive(:detect_sdk_version).with('iOS').and_return(Gem::Version.new('16.4'))
allow(Scan::DetectValues).to receive(:detect_sdk_version).with('tvOS').and_return(Gem::Version.new('16.4'))
allow(Scan::DetectValues).to receive(:detect_sdk_version).with('watchOS').and_return(Gem::Version.new('9.4'))

devices = ['iPhone 14 Pro Max', 'iPad Pro (12.9-inch) (6th generation) (16.1)', 'Apple TV 4K (3rd generation)', 'Apple Watch Ultra (49mm)']
simulators = Scan::DetectValues.detect_simulator(devices, '', '', '', nil)

expect(simulators.count).to eq(4)
expect(simulators[0]).to have_attributes(
name: "iPhone 14 Pro Max", os_type: "iOS", os_version: "16.4"
)
expect(simulators[1]).to have_attributes(
name: "iPad Pro (12.9-inch) (6th generation)", os_type: "iOS", os_version: "16.1"
)
expect(simulators[2]).to have_attributes(
name: "Apple TV 4K (3rd generation)", os_type: "tvOS", os_version: "16.4"
)
expect(simulators[3]).to have_attributes(
name: "Apple Watch Ultra (49mm)", os_type: "watchOS", os_version: "9.4"
)
end
end

describe "#detect_destination" do
it "ios", requires_xcodebuild: true do
options = { project: "./scan/examples/standard/app.xcodeproj" }
Expand Down
25 changes: 25 additions & 0 deletions scan/spec/fixtures/DeviceManagerSimctlOutputXcode14
@@ -0,0 +1,25 @@
== Devices ==
-- iOS 16.1 --
iPhone 14 Pro Max (01F8450A-7C9A-474B-BC8C-1F75F9671CC1) (Shutdown)
iPad Pro (12.9-inch) (6th generation) (D4A359E0-A0A7-407B-8909-217ED5C0E27C) (Shutdown)
-- iOS 16.4 --
iPhone 14 Pro Max (0200E1C9-D6D7-4C1E-AF67-40DD0598EEFF) (Shutdown)
iPad Pro (12.9-inch) (6th generation) (D4A359E0-A0A7-407B-8909-217ED5C0E27C) (Shutdown)
-- iOS 17.0 --
iPhone 14 Pro Max (530756ED-F705-4A8A-AA0E-704F72B7FEE5) (Shutdown)
iPad Pro (12.9-inch) (6th generation) (AF0DB812-330D-4030-813E-F7437F1F2492) (Shutdown)
iPhone 15 (E7A4A3FF-51A8-4AB5-9515-2D07E48E15CE) (Shutdown) (unavailable, device type profile not found)
iPhone 15 Plus (CA203CC7-C896-4467-8595-3A3432BD2F98) (Shutdown) (unavailable, device type profile not found)
iPhone 15 Pro (80A21777-0BC1-478E-9C0E-BAFACF126BE3) (Shutdown) (unavailable, device type profile not found)
iPhone 15 Pro Max (20952C59-B21A-49D8-A09F-4DB0983018C5) (Shutdown) (unavailable, device type profile not found)
-- tvOS 16.4 --
Apple TV 4K (3rd generation) (16B3AE21-BB25-463E-8392-1224DFFE776D) (Shutdown)
-- tvOS 17.0 --
Apple TV 4K (3rd generation) (AB921238-6D71-4AB3-A3AD-F64266AE66DD) (Shutdown)
-- watchOS 9.4 --
Apple Watch Ultra (49mm) (8EB51A76-1821-47F0-91C4-53F2D1634375) (Shutdown)
-- watchOS 10.0 --
Apple Watch Ultra (49mm) (64F04E58-1FE2-455B-8438-A7C662AA0CE4) (Shutdown)
Apple Watch Series 9 (41mm) (FAC17A8B-0DD4-4980-BC58-914E02DB2973) (Shutdown) (unavailable, device type profile not found)
Apple Watch Series 9 (45mm) (986D9C9F-705A-405F-B6F1-0E3C9FC04923) (Shutdown) (unavailable, device type profile not found)
Apple Watch Ultra 2 (49mm) (7E89824D-0A5A-464B-8B08-E0CBE3297DFA) (Shutdown) (unavailable, device type profile not found)
11 changes: 11 additions & 0 deletions scan/spec/fixtures/DeviceManagerSimctlOutputXcode15
@@ -0,0 +1,11 @@
== Devices ==
-- iOS 17.0 --
iPhone 14 Pro Max (530756ED-F705-4A8A-AA0E-704F72B7FEE5) (Shutdown)
-- tvOS 16.4 --
Apple TV 4K (3rd generation) (16B3AE21-BB25-463E-8392-1224DFFE776D) (Shutdown)
-- tvOS 17.0 --
Apple TV 4K (3rd generation) (AB921238-6D71-4AB3-A3AD-F64266AE66DD) (Shutdown)
-- watchOS 10.0 --
Apple Watch Ultra (49mm) (64F04E58-1FE2-455B-8438-A7C662AA0CE4) (Shutdown)
-- Unavailable: com.apple.CoreSimulator.SimRuntime.iOS-16-4 --
iPhone 14 Pro Max (0200E1C9-D6D7-4C1E-AF67-40DD0598EEFF) (Shutdown) (unavailable, runtime profile not found using "System" match policy)

0 comments on commit aacdbca

Please sign in to comment.