Skip to content

Commit 24fc427

Browse files
authored
fix: better matched runtime build detection (#43)
* fix: apple build version * fix: better matched runtime build detection
1 parent 7e6d06e commit 24fc427

File tree

6 files changed

+93
-8
lines changed

6 files changed

+93
-8
lines changed

lib/fastlane/plugin/create_simulator_devices/helpers/create_simulator_devices/models.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
require_relative 'models/required_runtime'
1010
require_relative 'models/apple_build_version'
1111
require_relative 'models/device_naming_style'
12+
require_relative 'models/simctl/matched_runtime'
1213
require 'fastlane'
1314

1415
module Fastlane

lib/fastlane/plugin/create_simulator_devices/helpers/create_simulator_devices/models/apple_build_version.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,15 +62,15 @@ def almost_equal?(other)
6262
end
6363

6464
def major
65-
@build_version.match(/^([0-9]+)([A-Z][0-9]{1,3})([0-9]+)/)[1]
65+
@build_version.match(/^([0-9]+)([A-Z])([0-9]+)/)[1]
6666
end
6767

6868
def minor
69-
@build_version.match(/^([0-9]+)([A-Z][0-9]{1,3})([0-9]+)/)[2]
69+
@build_version.match(/^([0-9]+)([A-Z])([0-9]+)/)[2]
7070
end
7171

7272
def patch
73-
@build_version.match(/^([0-9]+)([A-Z][0-9]{1,3})([0-9]+)/)[3]
73+
@build_version.match(/^([0-9]+)([A-Z])([0-9]+)/)[3]
7474
end
7575

7676
def <(other)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# frozen_string_literal: true
2+
3+
require_relative 'runtime_supported_device_type'
4+
require_relative '../apple_build_version'
5+
6+
module Fastlane
7+
module CreateSimulatorDevices
8+
module SimCTL
9+
# Represents a matched runtime from `xcrun simctl runtime match list --json` output.
10+
class MatchedRuntime
11+
attr_accessor :identifier, :default_build, :chosen_runtime_build, :sdk_build, :sdk_version, :platform
12+
13+
def initialize(identifier:, default_build:, chosen_runtime_build:, sdk_build:, sdk_version:, platform:) # rubocop:disable Metrics/ParameterLists
14+
self.identifier = identifier
15+
self.default_build = default_build
16+
self.chosen_runtime_build = chosen_runtime_build
17+
self.sdk_build = sdk_build
18+
self.sdk_version = sdk_version
19+
self.platform = platform
20+
end
21+
22+
def self.from_hash(hash, identifier:)
23+
new(
24+
identifier: identifier.to_s,
25+
default_build: AppleBuildVersion.new(hash[:defaultBuild]),
26+
chosen_runtime_build: AppleBuildVersion.new(hash[:chosenRuntimeBuild]),
27+
sdk_build: AppleBuildVersion.new(hash[:sdkBuild]),
28+
sdk_version: Gem::Version.new(hash[:sdkVersion]),
29+
platform: hash[:platform].to_s
30+
)
31+
end
32+
end
33+
end
34+
end
35+
end
36+
37+
# Example of a Runtime object from `xcrun simctl list runtimes --json` output:
38+
#
39+
# {"iphoneos26.1" : {
40+
# "chosenRuntimeBuild" : "23B80",
41+
# "defaultBuild" : "23B77",
42+
# "platform" : "com.apple.platform.iphoneos",
43+
# "sdkBuild" : "23B77",
44+
# "sdkDirectory" : "\/Applications\/Xcode-26.1.1.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Developer\/SDKs\/iPhoneOS26.1.sdk",
45+
# "sdkVersion" : "26.1"
46+
# }

lib/fastlane/plugin/create_simulator_devices/helpers/create_simulator_devices/runtime_helper.rb

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ def initialize(cache_dir:, shell_helper:, verbose:)
2525
'xrsimulator' => 'xrOS'
2626
}.freeze
2727

28+
OS_NAME_TO_MATCHED_RUNTIME_IDENTIFIER = {
29+
'iOS' => 'iphoneos',
30+
'tvOS' => 'appletvos',
31+
'watchOS' => 'watchos',
32+
'xrOS' => 'xros'
33+
}.freeze
34+
2835
def delete_unusable_runtimes
2936
deletable_runtimes = shell_helper.installed_runtimes_with_state
3037
.select { |runtime| runtime.unusable? && runtime.deletable? }
@@ -153,6 +160,16 @@ def runtime_build_version_for_filename(filename)
153160
AppleBuildVersion.new(build_version)
154161
end
155162

163+
def matched_runtime_for_missing_runtime(missing_runtime)
164+
matched_runtimes = shell_helper.simctl_matched_runtimes(force: true)
165+
166+
matched_runtime_identifier = "#{OS_NAME_TO_MATCHED_RUNTIME_IDENTIFIER[missing_runtime.os_name]}#{missing_runtime.product_version}"
167+
168+
matched_runtimes = matched_runtimes.select { |matched_runtime| matched_runtime.identifier == matched_runtime_identifier && matched_runtime.sdk_build == missing_runtime.product_build_version }
169+
170+
matched_runtimes.max_by(&:chosen_runtime_build)
171+
end
172+
156173
def cached_runtime_file(missing_runtime)
157174
FileUtils.mkdir_p(cache_dir)
158175

@@ -164,10 +181,10 @@ def cached_runtime_file(missing_runtime)
164181
# E.g. Xcode 26.0 Beta 3 has iOS 26.0 (23A5287e) SDK, but
165182
# xcodebuild downloads iphonesimulator_26.0_23A5287g.dmg as latest.
166183
product_build_version = missing_runtime.product_build_version
184+
167185
if product_build_version
168186
runtime_dmg_search_pattern += product_build_version.major.to_s
169187
runtime_dmg_search_pattern += product_build_version.minor.to_s
170-
runtime_dmg_search_pattern += product_build_version.patch.to_s
171188
end
172189
runtime_dmg_search_pattern += '*.dmg'
173190

@@ -177,9 +194,17 @@ def cached_runtime_file(missing_runtime)
177194
UI.message("Available files with pattern: #{Dir.glob(runtime_dmg_search_pattern)}")
178195
end
179196

180-
runtime_file = Dir
197+
runtime_file = nil
198+
runtime_files = Dir
181199
.glob(runtime_dmg_search_pattern)
182-
.max_by { |filename| runtime_build_version_for_filename(filename) }
200+
201+
return nil if runtime_files.empty?
202+
203+
matched_runtime = matched_runtime_for_missing_runtime(missing_runtime)
204+
205+
runtime_file = runtime_files.detect { |filename| filename.end_with?("_#{matched_runtime.chosen_runtime_build}.dmg") } unless matched_runtime.nil?
206+
207+
runtime_file ||= runtime_files.max_by { |filename| runtime_build_version_for_filename(filename) }
183208

184209
return nil if runtime_file.nil?
185210

lib/fastlane/plugin/create_simulator_devices/helpers/create_simulator_devices/shell_helper.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,17 @@ def simctl_runtimes(force: false)
104104
@simctl_runtimes
105105
end
106106

107+
def simctl_matched_runtimes(force: false)
108+
return @simctl_matched_runtimes unless force || @simctl_matched_runtimes.nil?
109+
110+
UI.message('Fetching matched runtimes...')
111+
json = sh(command: 'xcrun simctl runtime match list --json')
112+
113+
@simctl_matched_runtimes = JSON
114+
.parse(json, symbolize_names: true)
115+
.map { |identifier, runtime| SimCTL::MatchedRuntime.from_hash(runtime, identifier: identifier) }
116+
end
117+
107118
def installed_runtimes_with_state
108119
UI.message('Fetching runtimes with state...')
109120
json = sh(command: 'xcrun simctl runtime list --json')

spec/create_simulator_devices/runtime_helper_spec.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
RSpec.describe Fastlane::CreateSimulatorDevices::RuntimeHelper do
88
AppleBuildVersion = Fastlane::CreateSimulatorDevices::AppleBuildVersion
99
RequiredRuntime = Fastlane::CreateSimulatorDevices::RequiredRuntime
10+
MatchedRuntime = Fastlane::CreateSimulatorDevices::SimCTL::MatchedRuntime
1011
RequiredDevice = Fastlane::CreateSimulatorDevices::RequiredDevice
1112
SimCTL = Fastlane::CreateSimulatorDevices::SimCTL
1213
Xcodebuild = Fastlane::CreateSimulatorDevices::Xcodebuild
@@ -242,12 +243,13 @@
242243
product_build_version: AppleBuildVersion.new('21A326'),
243244
runtime_name: 'iOS 17.0')
244245

245-
existing_file = '/tmp/test_cache/iphonesimulator_17.0.1_21A326.dmg'
246+
existing_file = '/tmp/test_cache/iphonesimulator_17.0.1_21A327.dmg'
246247

247248
allow(FileUtils).to receive(:mkdir_p).with(cache_dir)
248-
allow(Dir).to receive(:glob).with('/tmp/test_cache/iphonesimulator_17.0*_21A326*.dmg').and_return([existing_file])
249+
allow(Dir).to receive(:glob).with('/tmp/test_cache/iphonesimulator_17.0*_21A*.dmg').and_return([existing_file])
249250
allow(sut).to receive(:runtime_build_version_for_filename).and_return(AppleBuildVersion.new('21A326'))
250251
allow(Fastlane::UI).to receive(:message)
252+
allow(sut).to receive(:matched_runtime_for_missing_runtime).and_return(instance_double(MatchedRuntime, chosen_runtime_build: '21A327'))
251253

252254
# WHEN: Finding cached runtime file
253255
result = sut.cached_runtime_file(missing_runtime)

0 commit comments

Comments
 (0)