11#!/usr/bin/env ruby
2+ # frozen_string_literal: true
23
4+ require 'json'
5+ require 'set'
6+ require 'optparse'
7+ require 'open3'
8+
9+ # Mapping of desired simulators per OS major to a human-friendly descriptor.
10+ # Keep the descriptor's trailing (version) so we can parse the runtime.
311devices = {
412 "ios" => {
513 15 => "iPhone 13 Pro (15.5)" ,
@@ -23,127 +31,165 @@ devices = {
2331 26 => "Apple TV (26.0)" ,
2432 } ,
2533 "watchos" => {
26- 8 => "Apple Watch Series 7 (45mm) (8.5)" ,
27- 9 => "Apple Watch Series 8 (45mm) (9.4)" ,
34+ 8 => "Apple Watch Series 7 (45mm) (8.5)" ,
35+ 9 => "Apple Watch Series 8 (45mm) (9.4)" ,
2836 10 => "Apple Watch Series 9 (45mm) (10.5)" ,
2937 11 => "Apple Watch Series 10 (42mm) (11.5)" ,
3038 26 => "Apple Watch Series 11 (42mm) (26.0)" ,
3139 } ,
3240 "visionos" => {
33- 1 => "Apple Vision Pro (at 2732x2048) (1.2)" ,
34- 2 => "Apple Vision Pro (at 2732x2048) (2.5)" ,
41+ 1 => "Apple Vision Pro (at 2732x2048) (1.2)" ,
42+ 2 => "Apple Vision Pro (at 2732x2048) (2.5)" ,
3543 26 => "Apple Vision Pro (26.0)" ,
3644 } ,
3745}
3846
39- lane :create_simulators do |options |
40- require 'json'
41- require 'set'
42-
43- # map Fastfile platform keys to display names used by CoreSimulator runtimes
44- platforms_to_os = {
45- "ios" => "iOS" ,
46- "ipados" => "iOS" ,
47- "tvos" => "tvOS" ,
48- "watchos" => "watchOS" ,
49- "visionos" => "visionOS" ,
50- }
51-
52- # Build lookup tables from CoreSimulator for robust name→identifier mapping
53- begin
54- # Build a set of existing simulator name+runtime pairs to prevent duplicates across OS versions
55- devices_json = sh ( "xcrun simctl list -j devices" , log : false )
47+ # Map script platform keys to CoreSimulator OS display names
48+ PLATFORMS_TO_OS = {
49+ "ios" => "iOS" ,
50+ "ipados" => "iOS" ,
51+ "tvos" => "tvOS" ,
52+ "watchos" => "watchOS" ,
53+ "visionos" => "visionOS" ,
54+ }
55+
56+ options = {
57+ platform : nil ,
58+ version : nil ,
59+ }
60+
61+ OptionParser . new do |opts |
62+ opts . banner = "Usage: create_simulators [--platform ios|ipados|tvos|watchos|visionos] [--version N]"
63+ opts . on ( "--platform PLATFORM" , String , "Limit to a single platform key" ) { |v | options [ :platform ] = v . downcase }
64+ opts . on ( "--version N" , Integer , "Limit to a single major version key under the chosen platform" ) { |v | options [ :version ] = v }
65+ opts . on ( "-h" , "--help" , "Show help" ) { puts opts ; exit 0 }
66+ end . parse! ( ARGV )
67+
68+ # Helper to run a shell command and capture stdout. Returns "" on error.
69+ def run ( cmd )
70+ stdout , status = Open3 . capture2e ( cmd )
71+ unless status . success?
72+ warn "Command failed (#{ status . exitstatus } ): #{ cmd } \n #{ stdout } "
73+ end
74+ stdout
75+ rescue => e
76+ warn "Failed to run: #{ cmd } (#{ e } )"
77+ ""
78+ end
79+
80+ # Build a set of existing simulator name+runtime pairs to prevent duplicates across OS versions
81+ existing_pairs = Set . new
82+ begin
83+ devices_json = run ( "xcrun simctl list -j devices" )
84+ if !devices_json . empty?
5685 devices_list = JSON . parse ( devices_json )
57- existing_pairs = Set . new
5886 ( devices_list [ "devices" ] || { } ) . each do |runtime_key , arr |
5987 Array ( arr ) . each do |d |
6088 name = d [ "name" ]
6189 next unless name && runtime_key
6290 existing_pairs . add ( "#{ name } ||#{ runtime_key } " )
6391 end
6492 end
65-
66- list_json = sh ( "xcrun simctl list -j" , log : false )
67- list = JSON . parse ( list_json )
68- devtypes = list [ "devicetypes" ] || [ ]
69- runtimes = list [ "runtimes" ] || [ ]
70- rescue => e
71- UI . message ( "Failed to read simctl lists: #{ e } " )
72- devtypes = [ ]
73- runtimes = [ ]
74- existing_pairs = Set . new
7593 end
94+ rescue => e
95+ warn "Failed to read existing devices: #{ e } "
96+ end
7697
77- device_name_to_id = devtypes . each_with_object ( { } ) do |dt , h |
78- name = dt [ "name" ] ; id = dt [ "identifier" ]
79- h [ name ] = id if name && id
80- end
98+ # Build lookup tables for device types and runtimes
99+ begin
100+ list_json = run ( "xcrun simctl list -j" )
101+ list = list_json . empty? ? { } : JSON . parse ( list_json )
102+ devtypes = list [ "devicetypes" ] || [ ]
103+ runtimes = list [ "runtimes" ] || [ ]
104+ rescue => e
105+ warn "Failed to read simctl lists: #{ e } "
106+ devtypes = [ ]
107+ runtimes = [ ]
108+ end
81109
82- runtime_name_to_id = runtimes . each_with_object ( { } ) do |rt , h |
83- next unless rt [ "isAvailable" ]
84- name = rt [ "name" ] ; id = rt [ "identifier" ]
85- h [ name ] = id if name && id
86- end
110+ device_name_to_id = devtypes . each_with_object ( { } ) do |dt , h |
111+ name = dt [ "name" ] ; id = dt [ "identifier" ]
112+ h [ name ] = id if name && id
113+ end
87114
88- # Fallback builders when exact matches are not present in the lookup tables
89- build_device_type_id = proc do | device_name |
90- s = device_name . gsub ( /[()]/ , '' ) . gsub ( / \s +/ , '-' ) . gsub ( /[^A-Za-z0-9-]/ , '' )
91- "com.apple.CoreSimulator.SimDeviceType. #{ s } "
92- end
115+ runtime_name_to_id = runtimes . each_with_object ( { } ) do | rt , h |
116+ next unless rt [ "isAvailable" ]
117+ name = rt [ "name" ] ; id = rt [ "identifier" ]
118+ h [ name ] = id if name && id
119+ end
93120
94- build_runtime_id = proc do |os_name , version |
95- "com.apple.CoreSimulator.SimRuntime.#{ os_name } -#{ version . tr ( '.' , '-' ) } "
96- end
121+ # Fallback builders when exact matches are not present in the lookup tables
122+ build_device_type_id = proc do |device_name |
123+ s = device_name . gsub ( /[()]/ , '' ) . gsub ( /\s +/ , '-' ) . gsub ( /[^A-Za-z0-9-]/ , '' )
124+ "com.apple.CoreSimulator.SimDeviceType.#{ s } "
125+ end
97126
98- platform_opt = options && options [ :platform ] ? options [ :platform ] . to_s . downcase : nil
99- version_opt = options && options [ :version ] ? options [ :version ] . to_i : nil
127+ build_runtime_id = proc do |os_name , version |
128+ "com.apple.CoreSimulator.SimRuntime.#{ os_name } -#{ version . tr ( '.' , '-' ) } "
129+ end
100130
101- local_devices = if platform_opt && devices . key? ( platform_opt )
102- subset_versions = devices [ platform_opt ]
103- if version_opt && subset_versions . key? ( version_opt )
104- { platform_opt => { version_opt => subset_versions [ version_opt ] } }
105- else
106- { platform_opt => subset_versions }
107- end
131+ # Filter the devices hash according to CLI options
132+ local_devices = if options [ :platform ] && devices . key? ( options [ :platform ] )
133+ subset_versions = devices [ options [ :platform ] ]
134+ if options [ :version ] && subset_versions . key? ( options [ :version ] )
135+ { options [ :platform ] => { options [ :version ] => subset_versions [ options [ :version ] ] } }
108136 else
109- devices
137+ { options [ :platform ] => subset_versions }
110138 end
139+ else
140+ devices
141+ end
142+
143+ created = 0
144+ skipped = 0
145+
146+ local_devices . each do |platform , versions |
147+ os_name = PLATFORMS_TO_OS [ platform ]
148+ next if os_name . nil?
149+
150+ versions . values . each do |descriptor |
151+ begin
152+ # Parse trailing "(x.y)" and derive device name
153+ if descriptor =~ /\s *\( ([^()]+)\) \s *\z /
154+ runtime_version = $1
155+ device_name = descriptor . sub ( /\s *\( [^()]+\) \s *\z / , '' )
156+ else
157+ warn "Could not parse runtime version from '#{ descriptor } ', skipping"
158+ skipped += 1
159+ next
160+ end
161+
162+ runtime_name = "#{ os_name } #{ runtime_version } "
163+
164+ device_type_id = device_name_to_id [ device_name ] || build_device_type_id . call ( device_name )
165+ runtime_id = runtime_name_to_id [ runtime_name ] || build_runtime_id . call ( os_name , runtime_version )
166+
167+ # Use the device name without the version suffix as the simulator name
168+ sim_name = device_name
111169
112- local_devices . each do |platform , versions |
113- os_name = platforms_to_os [ platform ]
114- next if os_name . nil?
115-
116- versions . values . each do |descriptor |
117- # descriptor is a single string like "iPhone 14 Pro (16.4)" or "iPad Pro 11-inch (M4) (18.6)"
118- begin
119- # Parse trailing "(x.y)" and derive device name
120- if descriptor =~ /\s *\( ([^()]+)\) \s *\z /
121- runtime_version = $1
122- device_name = descriptor . sub ( /\s *\( [^()]+\) \s *\z / , '' )
123- else
124- UI . message ( "Could not parse runtime version from '#{ descriptor } ', skipping" )
125- next
126- end
127-
128- runtime_name = "#{ os_name } #{ runtime_version } "
129-
130- device_type_id = device_name_to_id [ device_name ] || build_device_type_id . call ( device_name )
131- runtime_id = runtime_name_to_id [ runtime_name ] || build_runtime_id . call ( os_name , runtime_version )
132-
133- # Use the device name without the version suffix as the simulator name
134- sim_name = device_name
135-
136- pair_key = "#{ sim_name } ||#{ runtime_id } "
137- if existing_pairs . include? ( pair_key )
138- UI . message ( "Already exists: #{ sim_name } (#{ runtime_version } ), skipping" )
139- next
140- end
141-
142- sh ( %(xcrun simctl create "#{ sim_name } " "#{ device_type_id } " "#{ runtime_id } " || true) )
170+ pair_key = "#{ sim_name } ||#{ runtime_id } "
171+ if existing_pairs . include? ( pair_key )
172+ puts "Already exists: #{ sim_name } (#{ runtime_version } ), skipping"
173+ skipped += 1
174+ next
175+ end
176+
177+ cmd = %(xcrun simctl create "#{ sim_name } " "#{ device_type_id } " "#{ runtime_id } ")
178+ out = run ( cmd )
179+ if out . strip . empty?
180+ # simctl returns the new UUID on success; if empty we assume failure was already reported
181+ warn "Failed to create: #{ sim_name } (#{ runtime_version } )"
182+ skipped += 1
183+ else
184+ puts "Created: #{ sim_name } (#{ runtime_version } ) -> #{ out . strip } "
185+ created += 1
143186 existing_pairs . add ( pair_key )
144- rescue => e
145- UI . message ( "Skipping #{ descriptor } : #{ e } " )
146187 end
188+ rescue => e
189+ warn "Skipping #{ descriptor } : #{ e } "
190+ skipped += 1
147191 end
148192 end
149193end
194+
195+ puts "Summary: created=#{ created } , skipped=#{ skipped } "
0 commit comments