Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[scan] Filter simulators with version greater than SDK version of active Xcode installation when choosing default #21677

Merged
merged 8 commits into from Dec 14, 2023
19 changes: 12 additions & 7 deletions fastlane_core/lib/fastlane_core/device_manager.rb
Expand Up @@ -7,6 +7,7 @@
module FastlaneCore
class DeviceManager
class << self
attr_reader :runtime_build_os_versions
def all(requested_os_type = "")
return connected_devices(requested_os_type) + simulators(requested_os_type)
end
Expand All @@ -25,13 +26,16 @@ def simulators(requested_os_type = "")
runtime_info = ''
Open3.popen3('xcrun simctl list runtimes') do |stdin, stdout, stderr, wait_thr|
# This regex outputs the version info in the format "<platform> <version><exact version>"
runtime_info = stdout.read.lines.map { |v| v.sub(/(\w+ \S+)\s*\((\S+)\s[\S\s]*/, "\\1 \\2") }.drop(1)
runtime_info = stdout.read
end
exact_versions = Hash.new({})
runtime_info.each do |r|
platform, general, exact = r.split
exact_versions[platform] = {} unless exact_versions.include?(platform)
exact_versions[platform][general] = exact
unless runtime_info.include?("== Runtimes ==")
UI.error("xcrun simctl CLI broken, run `xcrun simctl list devices` and make sure it works")
UI.user_error!("xcrun simctl not working.")
end

@runtime_build_os_versions = runtime_info.lines.drop(1).each_with_object({}) do |line, res|
matches = line.match(/.*\((?<version>\S+)\s-\s(?<build>\S+)\)[\S\s]*/)
res[matches[:build]] = matches[:version]
wuaar1003 marked this conversation as resolved.
Show resolved Hide resolved
end

unless output.include?("== Devices ==")
Expand All @@ -57,7 +61,7 @@ def simulators(requested_os_type = "")

if matches.count && (os_type == requested_os_type || requested_os_type == "")
# This is disabled here because the Device is defined later in the file, and that's a problem for the cop
@devices << Device.new(name: name, os_type: os_type, os_version: (exact_versions[os_type][os_version] || os_version), udid: udid, state: state, is_simulator: true)
@devices << Device.new(name: name, os_type: os_type, os_version: os_version, udid: udid, state: state, is_simulator: true)
end
end
end
Expand Down Expand Up @@ -289,6 +293,7 @@ def disable_slide_to_type(udid: nil, name: nil, os_version: nil)

def clear_cache
@devices = nil
@runtime_build_os_versions = nil
end

def launch(device)
Expand Down
42 changes: 18 additions & 24 deletions fastlane_core/spec/device_manager_spec.rb
Expand Up @@ -30,7 +30,7 @@
expect(response).to receive(:read).and_return(@simctl_output)
expect(Open3).to receive(:popen3).with("xcrun simctl list devices").and_yield(nil, response, nil, nil)
thing = {}
expect(thing).to receive(:read).and_return("line\n")
expect(thing).to receive(:read).and_return("== Runtimes ==\n")
allow(Open3).to receive(:popen3).with("xcrun simctl list runtimes").and_yield(nil, thing, nil, nil)

devices = FastlaneCore::Simulator.all
Expand Down Expand Up @@ -80,7 +80,7 @@
expect(response).to receive(:read).and_return(simctl_output)
expect(Open3).to receive(:popen3).with("xcrun simctl list devices").and_yield(nil, response, nil, nil)
thing = {}
expect(thing).to receive(:read).and_return("line\n")
expect(thing).to receive(:read).and_return("== Runtimes ==\n")
allow(Open3).to receive(:popen3).with("xcrun simctl list runtimes").and_yield(nil, thing, nil, nil)

devices = FastlaneCore::Simulator.all
Expand Down Expand Up @@ -112,7 +112,7 @@
expect(response).to receive(:read).and_return(simctl_output)
expect(Open3).to receive(:popen3).with("xcrun simctl list devices").and_yield(nil, response, nil, nil)
thing = {}
expect(thing).to receive(:read).and_return("line\n")
expect(thing).to receive(:read).and_return("== Runtimes ==\n")
allow(Open3).to receive(:popen3).with("xcrun simctl list runtimes").and_yield(nil, thing, nil, nil)

devices = FastlaneCore::Simulator.all
Expand Down Expand Up @@ -144,7 +144,7 @@
expect(response).to receive(:read).and_return(simctl_output)
expect(Open3).to receive(:popen3).with("xcrun simctl list devices").and_yield(nil, response, nil, nil)
thing = {}
expect(thing).to receive(:read).and_return("line\n")
expect(thing).to receive(:read).and_return("== Runtimes ==\n")
allow(Open3).to receive(:popen3).with("xcrun simctl list runtimes").and_yield(nil, thing, nil, nil)

devices = FastlaneCore::Simulator.all
Expand Down Expand Up @@ -176,7 +176,7 @@
expect(response).to receive(:read).and_return(@simctl_output)
expect(Open3).to receive(:popen3).with("xcrun simctl list devices").and_yield(nil, response, nil, nil)
thing = {}
expect(thing).to receive(:read).and_return("line\n")
expect(thing).to receive(:read).and_return("== Runtimes ==\n")
allow(Open3).to receive(:popen3).with("xcrun simctl list runtimes").and_yield(nil, thing, nil, nil)

devices = FastlaneCore::SimulatorTV.all
Expand All @@ -195,7 +195,7 @@
expect(response).to receive(:read).and_return(@simctl_output)
expect(Open3).to receive(:popen3).with("xcrun simctl list devices").and_yield(nil, response, nil, nil)
thing = {}
expect(thing).to receive(:read).and_return("line\n")
expect(thing).to receive(:read).and_return("== Runtimes ==\n")
allow(Open3).to receive(:popen3).with("xcrun simctl list runtimes").and_yield(nil, thing, nil, nil)

devices = FastlaneCore::SimulatorWatch.all
Expand All @@ -220,7 +220,7 @@
expect(response).to receive(:read).and_return(@simctl_output)
expect(Open3).to receive(:popen3).with("xcrun simctl list devices").and_yield(nil, response, nil, nil)
thing = {}
expect(thing).to receive(:read).and_return("line\n")
expect(thing).to receive(:read).and_return("== Runtimes ==\n")
allow(Open3).to receive(:popen3).with("xcrun simctl list runtimes").and_yield(nil, thing, nil, nil)

devices = FastlaneCore::DeviceManager.simulators
Expand Down Expand Up @@ -276,7 +276,7 @@
expect(response).to receive(:read).and_return(simctl_output)
expect(Open3).to receive(:popen3).with("xcrun simctl list devices").and_yield(nil, response, nil, nil)
thing = {}
expect(thing).to receive(:read).and_return("line\n")
expect(thing).to receive(:read).and_return("== Runtimes ==\n")
allow(Open3).to receive(:popen3).with("xcrun simctl list runtimes").and_yield(nil, thing, nil, nil)

devices = FastlaneCore::DeviceManager.simulators
Expand Down Expand Up @@ -385,7 +385,7 @@
expect(response).to receive(:read).and_return(@simctl_output)
expect(Open3).to receive(:popen3).with("xcrun simctl list devices").and_yield(nil, response, nil, nil)
thing = {}
expect(thing).to receive(:read).and_return("line\n")
expect(thing).to receive(:read).and_return("== Runtimes ==\n")
allow(Open3).to receive(:popen3).with("xcrun simctl list runtimes").and_yield(nil, thing, nil, nil)

devices = FastlaneCore::DeviceManager.all('iOS')
Expand Down Expand Up @@ -440,7 +440,7 @@
expect(response).to receive(:read).and_return(@simctl_output)
expect(Open3).to receive(:popen3).with("xcrun simctl list devices").and_yield(nil, response, nil, nil)
thing = {}
expect(thing).to receive(:read).and_return("line\n")
expect(thing).to receive(:read).and_return("== Runtimes ==\n")
allow(Open3).to receive(:popen3).with("xcrun simctl list runtimes").and_yield(nil, thing, nil, nil)

devices = FastlaneCore::DeviceManager.all('tvOS')
Expand All @@ -460,23 +460,17 @@
)
end

it "parses runtime information properly to get the exact version information" do
response = "response"
expect(response).to receive(:read).and_return(@simctl_output)
expect(Open3).to receive(:popen3).with("xcrun simctl list devices").and_yield(nil, response, nil, nil)
thing = {}
expect(thing).to receive(:read).and_return("== Runtimes ==\ntvOS 9.0 (9.0.1 - 13A345) - com.apple.CoreSimulator.SimRuntime.tvOS-9-0\n")
it 'properly parses `xcrun simctl list runtimes` to associate runtime builds with their exact OS version' do
response = double('xcrun simctl list devices', read: '== Devices ==')
allow(Open3).to receive(:popen3).with('xcrun simctl list devices').and_yield(nil, response, nil, nil)

thing = double('xcrun simctl list runtimes', read: "== Runtimes ==\niOS 17.0 (17.0 - 21A328) - com.apple.CoreSimulator.SimRuntime.iOS-17-0\niOS 17.0 (17.0.1 - 21A342) - com.apple.CoreSimulator.SimRuntime.iOS-17-0")
allow(Open3).to receive(:popen3).with("xcrun simctl list runtimes").and_yield(nil, thing, nil, nil)

devices = FastlaneCore::SimulatorTV.all
expect(devices.count).to eq(1)
devices = FastlaneCore::DeviceManager.simulators

expect(devices[0]).to have_attributes(
name: "Apple TV 1080p", os_type: "tvOS", os_version: "9.0.1",
udid: "D239A51B-A61C-4B60-B4D6-B7EC16595128",
state: "Shutdown",
is_simulator: true
)
expect(FastlaneCore::DeviceManager.runtime_build_os_versions['21A328']).to eq('17.0')
expect(FastlaneCore::DeviceManager.runtime_build_os_versions['21A342']).to eq('17.0.1')
end

describe FastlaneCore::DeviceManager::Device do
Expand Down
12 changes: 12 additions & 0 deletions fastlane_core/spec/simulator_spec.rb
Expand Up @@ -71,6 +71,10 @@
expect(response).to receive(:read).and_return(@valid_simulators)
expect(Open3).to receive(:popen3).with("xcrun simctl list devices").and_yield(nil, response, nil, nil)

thing = {}
expect(thing).to receive(:read).and_return("== Runtimes ==\n")
allow(Open3).to receive(:popen3).with("xcrun simctl list runtimes").and_yield(nil, thing, nil, nil)

devices = FastlaneCore::Simulator.all
expect(devices.count).to eq(4)

Expand Down Expand Up @@ -101,6 +105,10 @@
expect(response).to receive(:read).and_return(@valid_simulators)
expect(Open3).to receive(:popen3).with("xcrun simctl list devices").and_yield(nil, response, nil, nil)

thing = {}
expect(thing).to receive(:read).and_return("== Runtimes ==\n")
allow(Open3).to receive(:popen3).with("xcrun simctl list runtimes").and_yield(nil, thing, nil, nil)

devices = FastlaneCore::SimulatorTV.all
expect(devices.count).to eq(1)

Expand All @@ -116,6 +124,10 @@
expect(response).to receive(:read).and_return(@valid_simulators)
expect(Open3).to receive(:popen3).with("xcrun simctl list devices").and_yield(nil, response, nil, nil)

thing = {}
expect(thing).to receive(:read).and_return("== Runtimes ==\n")
allow(Open3).to receive(:popen3).with("xcrun simctl list runtimes").and_yield(nil, thing, nil, nil)

devices = FastlaneCore::SimulatorWatch.all
expect(devices.count).to eq(2)

Expand Down
92 changes: 73 additions & 19 deletions scan/lib/scan/detect_values.rb
@@ -1,10 +1,19 @@
require 'fastlane_core/device_manager'
require 'fastlane_core/project'
require 'pathname'
require 'set'
require_relative 'module'

module Scan
# This class detects all kinds of default values
class DetectValues
PLATFORMS = {
'iOS' => { simulator: 'iphonesimulator', name: 'com.apple.platform.iphoneos' },
'tvOS' => { simulator: 'appletvsimulator', name: 'com.apple.platform.appletvos' },
'watchOS' => { simulator: 'watchsimulator', name: 'com.apple.platform.watchos' },
'visionOS' => { simulator: 'xrsimulator', name: 'com.apple.platform.xros' }
}.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 +109,63 @@ def self.filter_simulators(simulators, operator = :greater_than_or_equal, deploy
end
end

def self.default_os_version(os_type)
@os_versions ||= {}
@os_versions[os_type] ||= begin
UI.crash!("Unknown platform: #{os_type}") unless PLATFORMS.key?(os_type)
platform = PLATFORMS[os_type]

# list SDK version for currently running Xcode
sdks_output, = Open3.capture3('xcodebuild -showsdks -json')
sdk_version = begin
JSON.parse(sdks_output).find { |e| e['platform'] == platform[:simulator] }['sdkVersion']
rescue StandardError => e
UI.error(e)
UI.error("xcodebuild CLI broken, please run `xcodebuild` and make sure it works")
UI.user_error!("xcodebuild not working")
end
wuaar1003 marked this conversation as resolved.
Show resolved Hide resolved

# Get runtime build from SDK version
runtime_output, = Open3.capture3('xcrun simctl runtime match list -j')
runtime_build = begin
JSON.parse(runtime_output).values.find { |elem| elem['platform'] == platform[:name] && elem['sdkVersion'] == sdk_version }['chosenRuntimeBuild']
rescue StandardError => e
wuaar1003 marked this conversation as resolved.
Show resolved Hide resolved
UI.error(e)
UI.error("xcrun simctl runtime broken, please verify that `xcrun simctl runtime match list` and `xcrun simctl runtime list` work")
UI.user_error!("xcrun simctl runtime not working")
end

# Get OS version corresponding to build
Gem::Version.new(FastlaneCore::DeviceManager.runtime_build_os_versions[runtime_build])
end
end

def self.compatibility_constraint
@compability_constraint ||= begin
_, error, = Open3.capture3('xcrun simctl runtime -h')
unless error.include?('Usage: simctl runtime <operation> <arguments>')
UI.error("xcrun simctl runtime broken, run 'xcrun simctl runtime' and make sure it works")
UI.user_error!("xcrun simctl runtime not working.")
end

# `match list` subcommand added in Xcode 15
if error.include?('match list')
->(name, sim) { sim.name == name && Gem::Version.new(sim.os_version) <= default_os_version(sim.os_type) }
else
->(name, sim) { sim.name == name }
end
end
end

def self.highest_compatible_simulator(simulators, device_name)
constraint = compatibility_constraint.curry[device_name]
simulators
.select(&constraint)
.reverse
.sort_by! { |sim| Gem::Version.new(sim.os_version) }
.last
end

def self.regular_expression_for_split_on_whitespace_followed_by_parenthesized_version
# %r{
# \s # a whitespace character
Expand All @@ -114,8 +180,6 @@ def self.regular_expression_for_split_on_whitespace_followed_by_parenthesized_ve
end

def self.detect_simulator(devices, requested_os_type, deployment_target_key, default_device_name, simulator_type_descriptor)
require 'set'

deployment_target_version = get_deployment_target_version(deployment_target_key)

simulators = filter_simulators(
Expand All @@ -134,29 +198,26 @@ def self.detect_simulator(devices, requested_os_type, deployment_target_key, def

# At this point we have all simulators for the given deployment target (or higher)

# Clear out cached values
@os_versions = nil
@compability_constraint = nil

# 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)
.reverse # more efficient, because `simctl` prints higher versions first
.sort_by! { |sim| Gem::Version.new(sim.os_version) }
.pop(1)
[ highest_compatible_simulator(simulators, pieces.first) ]
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 +229,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 @@ -185,13 +246,7 @@ def self.detect_simulator(devices, requested_os_type, deployment_target_key, def
default = lambda do
UI.error("Couldn't find any matching simulators for '#{devices}' - falling back to default simulator") if (devices || []).count > 0

result = Array(
simulators
.select { |sim| sim.name == default_device_name }
.reverse # more efficient, because `simctl` prints higher versions first
.sort_by! { |sim| Gem::Version.new(sim.os_version) }
.last || simulators.first
)
result = [ highest_compatible_simulator(simulators, default_device_name) || simulators.first ]

UI.message("Found simulator \"#{result.first.name} (#{result.first.os_version})\"") if result.first

Expand Down Expand Up @@ -248,7 +303,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