Skip to content

Commit 7d3c9e9

Browse files
committed
WIP
1 parent 08793e6 commit 7d3c9e9

File tree

2 files changed

+146
-100
lines changed

2 files changed

+146
-100
lines changed

.github/workflows/ci.yml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -93,14 +93,14 @@ jobs:
9393
9494
if [ "$PLATFORM" = "iPadOS" ]; then
9595
PLATFORM=iOS
96-
FASTLANE_PLATFORM=ipados
96+
SCRIPT_PLATFORM=ipados
9797
else
9898
case "$PLATFORM" in
99-
iOS) FASTLANE_PLATFORM=ios ;;
100-
tvOS) FASTLANE_PLATFORM=tvos ;;
101-
watchOS) FASTLANE_PLATFORM=watchos ;;
102-
visionOS) FASTLANE_PLATFORM=visionos ;;
103-
macOS) FASTLANE_PLATFORM=macos ;;
99+
iOS) SCRIPT_PLATFORM=ios ;;
100+
tvOS) SCRIPT_PLATFORM=tvos ;;
101+
watchOS) SCRIPT_PLATFORM=watchos ;;
102+
visionOS) SCRIPT_PLATFORM=visionos ;;
103+
macOS) SCRIPT_PLATFORM=macos ;;
104104
esac
105105
fi
106106
@@ -110,7 +110,7 @@ jobs:
110110
echo "MAJOR=$MAJOR" >> $GITHUB_ENV
111111
echo "MINOR=$MINOR" >> $GITHUB_ENV
112112
echo "RUNTIME=$RUNTIME" >> $GITHUB_ENV
113-
echo "FASTLANE_PLATFORM=$FASTLANE_PLATFORM" >> $GITHUB_ENV
113+
echo "SCRIPT_PLATFORM=$SCRIPT_PLATFORM" >> $GITHUB_ENV
114114
115115
- if: ${{ env.PLATFORM != 'macOS' }}
116116
name: Check for ${{ env.RUNTIME }} runtime
@@ -153,7 +153,7 @@ jobs:
153153
run: |
154154
set -eo pipefail
155155
xcrun simctl delete all
156-
fastlane create_simulators platform:${{ env.FASTLANE_PLATFORM }} version:${{ env.MAJOR }}
156+
script/create_simulators --platform ${{ env.SCRIPT_PLATFORM }} --version ${{ env.MAJOR }}
157157
158158
- name: List Available Runtimes, Simulators, and Destinations
159159
run: |

script/create_simulators

100644100755
Lines changed: 138 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
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.
311
devices = {
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
149193
end
194+
195+
puts "Summary: created=#{created}, skipped=#{skipped}"

0 commit comments

Comments
 (0)