diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 868bec85..c5573984 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -93,14 +93,14 @@ jobs: if [ "$PLATFORM" = "iPadOS" ]; then PLATFORM=iOS - FASTLANE_PLATFORM=ipados + SCRIPT_PLATFORM=ipados else case "$PLATFORM" in - iOS) FASTLANE_PLATFORM=ios ;; - tvOS) FASTLANE_PLATFORM=tvos ;; - watchOS) FASTLANE_PLATFORM=watchos ;; - visionOS) FASTLANE_PLATFORM=visionos ;; - macOS) FASTLANE_PLATFORM=macos ;; + iOS) SCRIPT_PLATFORM=ios ;; + tvOS) SCRIPT_PLATFORM=tvos ;; + watchOS) SCRIPT_PLATFORM=watchos ;; + visionOS) SCRIPT_PLATFORM=visionos ;; + macOS) SCRIPT_PLATFORM=macos ;; esac fi @@ -110,7 +110,7 @@ jobs: echo "MAJOR=$MAJOR" >> $GITHUB_ENV echo "MINOR=$MINOR" >> $GITHUB_ENV echo "RUNTIME=$RUNTIME" >> $GITHUB_ENV - echo "FASTLANE_PLATFORM=$FASTLANE_PLATFORM" >> $GITHUB_ENV + echo "SCRIPT_PLATFORM=$SCRIPT_PLATFORM" >> $GITHUB_ENV - if: ${{ env.PLATFORM != 'macOS' }} name: Check for ${{ env.RUNTIME }} runtime @@ -128,20 +128,12 @@ jobs: action: none verbosity: xcbeautify - - if: ${{ env.PLATFORM != 'macOS' }} - name: Download Required Runtime - uses: nick-fields/retry@v3 - with: - timeout_minutes: 15 - max_attempts: 3 - command: xcodebuild -downloadPlatform ${{ env.PLATFORM }} - - if: env.has_runtime == 'false' name: List Downloadable Runtimes run: xcodes runtimes --include-betas - if: env.has_runtime == 'false' - name: Install Required Runtime (${{ env.RUNTIME }}) + name: Download Required Runtime (${{ env.RUNTIME }}) uses: nick-fields/retry@v3 with: timeout_minutes: 15 @@ -153,7 +145,12 @@ jobs: run: | set -eo pipefail xcrun simctl delete all - fastlane create_simulators platform:${{ env.FASTLANE_PLATFORM }} version:${{ env.MAJOR }} + OUTPUT=$(script/create_simulators --platform ${{ env.SCRIPT_PLATFORM }} --version ${{ env.MAJOR }} | tee /dev/stderr) + FIRST_UDID=$(printf "%s\n" "$OUTPUT" | awk '/^(Created:|Already exists:)/{print $NF}' | head -n1) + if [ -n "$FIRST_UDID" ]; then + echo "SIM_UDID=$FIRST_UDID" >> "$GITHUB_ENV" + echo "Captured SIM_UDID=$FIRST_UDID" + fi - name: List Available Runtimes, Simulators, and Destinations run: | @@ -162,11 +159,12 @@ jobs: - if: ${{ env.PLATFORM != 'watchOS' }} name: Build Showcase - uses: mxcl/xcodebuild@v3 + uses: davdroman/xcodebuild@destination with: xcode: ~26.0 platform: ${{ env.PLATFORM }} platform-version: ~${{ env.MAJOR }}.${{ env.MINOR }} + destination: ${{ env.SIM_UDID }} action: build scheme: Showcase configuration: Debug @@ -174,11 +172,12 @@ jobs: - if: ${{ env.PLATFORM == 'watchOS' }} name: Build Library - uses: mxcl/xcodebuild@v3 + uses: davdroman/xcodebuild@destination with: xcode: ~26.0 platform: ${{ env.PLATFORM }} platform-version: ~${{ env.MAJOR }}.${{ env.MINOR }} + destination: ${{ env.SIM_UDID }} action: build scheme: SwiftUIIntrospect configuration: Debug @@ -186,11 +185,12 @@ jobs: - if: ${{ env.PLATFORM != 'watchOS' }} name: Run Tests - uses: mxcl/xcodebuild@v3 + uses: davdroman/xcodebuild@destination with: xcode: ~26.0 platform: ${{ env.PLATFORM }} platform-version: ~${{ env.MAJOR }}.${{ env.MINOR }} + destination: ${{ env.SIM_UDID }} action: test scheme: SwiftUIIntrospectTests configuration: Debug diff --git a/Tests/Tests/WeakTests.swift b/Tests/Tests/WeakTests.swift index 7fead49c..8de3b597 100644 --- a/Tests/Tests/WeakTests.swift +++ b/Tests/Tests/WeakTests.swift @@ -7,49 +7,49 @@ struct WeakTests { var strongFoo: Foo? = Foo() - @Test func Init_nil() { + @Test func init_nil() { @Weak var weakFoo: Foo? #expect(weakFoo == nil) } - @Test func Init_nonNil() { + @Test func init_nonNil() { @Weak var weakFoo: Foo? = strongFoo #expect(weakFoo === strongFoo) } - @Test func Assignment_nilToNil() { + @Test func assignment_nilToNil() { @Weak var weakFoo: Foo? weakFoo = nil #expect(weakFoo == nil) } - @Test func Assignment_nilToNonNil() { + @Test func assignment_nilToNonNil() { @Weak var weakFoo: Foo? let otherFoo = Foo() weakFoo = otherFoo #expect(weakFoo === otherFoo) } - @Test func Assignment_nonNilToNil() { + @Test func assignment_nonNilToNil() { @Weak var weakFoo: Foo? = strongFoo weakFoo = nil #expect(weakFoo == nil) } - @Test func Assignment_nonNilToNonNil() { + @Test func assignment_nonNilToNonNil() { @Weak var weakFoo: Foo? = strongFoo let otherFoo = Foo() weakFoo = otherFoo #expect(weakFoo === otherFoo) } - @Test mutating func IndirectAssignment_nonNilToNil() { + @Test mutating func indirectAssignment_nonNilToNil() { @Weak var weakFoo: Foo? = strongFoo strongFoo = nil #expect(weakFoo == nil) } - @Test mutating func IndirectAssignment_nonNilToNonNil() { + @Test mutating func indirectAssignment_nonNilToNonNil() { @Weak var weakFoo: Foo? = strongFoo strongFoo = Foo() #expect(weakFoo == nil) diff --git a/fastlane/Fastfile b/fastlane/Fastfile deleted file mode 100644 index 9dc02ff4..00000000 --- a/fastlane/Fastfile +++ /dev/null @@ -1,149 +0,0 @@ -skip_docs - -devices = { - "ios" => { - 15 => "iPhone 13 Pro (15.5)", - 16 => "iPhone 14 Pro (16.4)", - 17 => "iPhone 15 Pro (17.5)", - 18 => "iPhone 16 Pro (18.6)", - 26 => "iPhone 17 Pro (26.0)", - }, - "ipados" => { - 15 => "iPad Pro (11-inch) (3rd generation) (15.5)", - 16 => "iPad Pro (11-inch) (4th generation) (16.4)", - 17 => "iPad Pro 11-inch (M4) (17.5)", - 18 => "iPad Pro 11-inch (M4) (18.6)", - 26 => "iPad Pro 11-inch (M4) (26.0)", - }, - "tvos" => { - 15 => "Apple TV (15.4)", - 16 => "Apple TV (16.4)", - 17 => "Apple TV (17.5)", - 18 => "Apple TV (18.5)", - 26 => "Apple TV (26.0)", - }, - "watchos" => { - 8 => "Apple Watch Series 7 (45mm) (8.5)", - 9 => "Apple Watch Series 8 (45mm) (9.4)", - 10 => "Apple Watch Series 9 (45mm) (10.5)", - 11 => "Apple Watch Series 10 (42mm) (11.5)", - 26 => "Apple Watch Series 11 (42mm) (26.0)", - }, - "visionos" => { - 1 => "Apple Vision Pro (at 2732x2048) (1.2)", - 2 => "Apple Vision Pro (at 2732x2048) (2.5)", - 26 => "Apple Vision Pro (26.0)", - }, -} - -lane :create_simulators do |options| - require 'json' - require 'set' - - # map Fastfile platform keys to display names used by CoreSimulator runtimes - platforms_to_os = { - "ios" => "iOS", - "ipados" => "iOS", - "tvos" => "tvOS", - "watchos" => "watchOS", - "visionos" => "visionOS", - } - - # Build lookup tables from CoreSimulator for robust name→identifier mapping - begin - # Build a set of existing simulator name+runtime pairs to prevent duplicates across OS versions - devices_json = sh("xcrun simctl list -j devices", log: false) - devices_list = JSON.parse(devices_json) - existing_pairs = Set.new - (devices_list["devices"] || {}).each do |runtime_key, arr| - Array(arr).each do |d| - name = d["name"] - next unless name && runtime_key - existing_pairs.add("#{name}||#{runtime_key}") - end - end - - list_json = sh("xcrun simctl list -j", log: false) - list = JSON.parse(list_json) - devtypes = list["devicetypes"] || [] - runtimes = list["runtimes"] || [] - rescue => e - UI.message("Failed to read simctl lists: #{e}") - devtypes = [] - runtimes = [] - existing_pairs = Set.new - end - - device_name_to_id = devtypes.each_with_object({}) do |dt, h| - name = dt["name"]; id = dt["identifier"] - h[name] = id if name && id - end - - runtime_name_to_id = runtimes.each_with_object({}) do |rt, h| - next unless rt["isAvailable"] - name = rt["name"]; id = rt["identifier"] - h[name] = id if name && id - end - - # Fallback builders when exact matches are not present in the lookup tables - build_device_type_id = proc do |device_name| - s = device_name.gsub(/[()]/, '').gsub(/\s+/, '-').gsub(/[^A-Za-z0-9-]/, '') - "com.apple.CoreSimulator.SimDeviceType.#{s}" - end - - build_runtime_id = proc do |os_name, version| - "com.apple.CoreSimulator.SimRuntime.#{os_name}-#{version.tr('.', '-')}" - end - - platform_opt = options && options[:platform] ? options[:platform].to_s.downcase : nil - version_opt = options && options[:version] ? options[:version].to_i : nil - - local_devices = if platform_opt && devices.key?(platform_opt) - subset_versions = devices[platform_opt] - if version_opt && subset_versions.key?(version_opt) - { platform_opt => { version_opt => subset_versions[version_opt] } } - else - { platform_opt => subset_versions } - end - else - devices - end - - local_devices.each do |platform, versions| - os_name = platforms_to_os[platform] - next if os_name.nil? - - versions.values.each do |descriptor| - # descriptor is a single string like "iPhone 14 Pro (16.4)" or "iPad Pro 11-inch (M4) (18.6)" - begin - # Parse trailing "(x.y)" and derive device name - if descriptor =~ /\s*\(([^()]+)\)\s*\z/ - runtime_version = $1 - device_name = descriptor.sub(/\s*\([^()]+\)\s*\z/, '') - else - UI.message("Could not parse runtime version from '#{descriptor}', skipping") - next - end - - runtime_name = "#{os_name} #{runtime_version}" - - device_type_id = device_name_to_id[device_name] || build_device_type_id.call(device_name) - runtime_id = runtime_name_to_id[runtime_name] || build_runtime_id.call(os_name, runtime_version) - - # Use the device name without the version suffix as the simulator name - sim_name = device_name - - pair_key = "#{sim_name}||#{runtime_id}" - if existing_pairs.include?(pair_key) - UI.message("Already exists: #{sim_name} (#{runtime_version}), skipping") - next - end - - sh(%(xcrun simctl create "#{sim_name}" "#{device_type_id}" "#{runtime_id}" || true)) - existing_pairs.add(pair_key) - rescue => e - UI.message("Skipping #{descriptor}: #{e}") - end - end - end -end diff --git a/script/create_simulators b/script/create_simulators new file mode 100755 index 00000000..050e956d --- /dev/null +++ b/script/create_simulators @@ -0,0 +1,197 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require 'json' +require 'set' +require 'optparse' +require 'open3' + +# Mapping of desired simulators per OS major to a human-friendly descriptor. +# Keep the descriptor's trailing (version) so we can parse the runtime. +devices = { + "ios" => { + 15 => "iPhone 13 Pro (15.5)", + 16 => "iPhone 14 Pro (16.4)", + 17 => "iPhone 15 Pro (17.5)", + 18 => "iPhone 16 Pro (18.6)", + 26 => "iPhone 17 Pro (26.0)", + }, + "ipados" => { + 15 => "iPad Pro (11-inch) (3rd generation) (15.5)", + 16 => "iPad Pro (11-inch) (4th generation) (16.4)", + 17 => "iPad Pro 11-inch (M4) (17.5)", + 18 => "iPad Pro 11-inch (M4) (18.6)", + 26 => "iPad Pro 11-inch (M4) (26.0)", + }, + "tvos" => { + 15 => "Apple TV (15.4)", + 16 => "Apple TV (16.4)", + 17 => "Apple TV (17.5)", + 18 => "Apple TV (18.5)", + 26 => "Apple TV (26.0)", + }, + "watchos" => { + 8 => "Apple Watch Series 7 (45mm) (8.5)", + 9 => "Apple Watch Series 8 (45mm) (9.4)", + 10 => "Apple Watch Series 9 (45mm) (10.5)", + 11 => "Apple Watch Series 10 (42mm) (11.5)", + 26 => "Apple Watch Series 11 (42mm) (26.0)", + }, + "visionos" => { + 1 => "Apple Vision Pro (at 2732x2048) (1.2)", + 2 => "Apple Vision Pro (at 2732x2048) (2.5)", + 26 => "Apple Vision Pro (26.0)", + }, +} + +# Map script platform keys to CoreSimulator OS display names +PLATFORMS_TO_OS = { + "ios" => "iOS", + "ipados" => "iOS", + "tvos" => "tvOS", + "watchos" => "watchOS", + "visionos" => "visionOS", +} + +options = { + platform: nil, + version: nil, +} + +OptionParser.new do |opts| + opts.banner = "Usage: create_simulators [--platform ios|ipados|tvos|watchos|visionos] [--version N]" + opts.on("--platform PLATFORM", String, "Limit to a single platform key") { |v| options[:platform] = v.downcase } + opts.on("--version N", Integer, "Limit to a single major version key under the chosen platform") { |v| options[:version] = v } + opts.on("-h", "--help", "Show help") { puts opts; exit 0 } +end.parse!(ARGV) + +# Helper to run a shell command and capture stdout. Returns "" on error. +def run(cmd) + stdout, status = Open3.capture2e(cmd) + unless status.success? + warn "Command failed (#{status.exitstatus}): #{cmd}\n#{stdout}" + end + stdout +rescue => e + warn "Failed to run: #{cmd} (#{e})" + "" +end + +# Build a set of existing simulator name+runtime pairs to prevent duplicates across OS versions +existing_pairs = Set.new +existing_index = {} +begin + devices_json = run("xcrun simctl list -j devices") + if !devices_json.empty? + devices_list = JSON.parse(devices_json) + (devices_list["devices"] || {}).each do |runtime_key, arr| + Array(arr).each do |d| + name = d["name"] + next unless name && runtime_key + existing_pairs.add("#{name}||#{runtime_key}") + existing_index["#{name}||#{runtime_key}"] = d["udid"] if d["udid"] + end + end + end +rescue => e + warn "Failed to read existing devices: #{e}" +end + +# Build lookup tables for device types and runtimes +begin + list_json = run("xcrun simctl list -j") + list = list_json.empty? ? {} : JSON.parse(list_json) + devtypes = list["devicetypes"] || [] + runtimes = list["runtimes"] || [] +rescue => e + warn "Failed to read simctl lists: #{e}" + devtypes = [] + runtimes = [] +end + +device_name_to_id = devtypes.each_with_object({}) do |dt, h| + name = dt["name"]; id = dt["identifier"] + h[name] = id if name && id +end + +runtime_name_to_id = runtimes.each_with_object({}) do |rt, h| + next unless rt["isAvailable"] + name = rt["name"]; id = rt["identifier"] + h[name] = id if name && id +end + +# Fallback builders when exact matches are not present in the lookup tables +build_device_type_id = proc do |device_name| + s = device_name.gsub(/[()]/, '').gsub(/\s+/, '-').gsub(/[^A-Za-z0-9-]/, '') + "com.apple.CoreSimulator.SimDeviceType.#{s}" +end + +build_runtime_id = proc do |os_name, version| + "com.apple.CoreSimulator.SimRuntime.#{os_name}-#{version.tr('.', '-')}" +end + +# Filter the devices hash according to CLI options +local_devices = if options[:platform] && devices.key?(options[:platform]) + subset_versions = devices[options[:platform]] + if options[:version] && subset_versions.key?(options[:version]) + { options[:platform] => { options[:version] => subset_versions[options[:version]] } } + else + { options[:platform] => subset_versions } + end +else + devices +end + +created = 0 +skipped = 0 + +local_devices.each do |platform, versions| + os_name = PLATFORMS_TO_OS[platform] + next if os_name.nil? + + versions.values.each do |descriptor| + begin + # Parse trailing "(x.y)" and derive device name + if descriptor =~ /\s*\(([^()]+)\)\s*\z/ + runtime_version = $1 + device_name = descriptor.sub(/\s*\([^()]+\)\s*\z/, '') + else + warn "Could not parse runtime version from '#{descriptor}', skipping" + skipped += 1 + next + end + + runtime_name = "#{os_name} #{runtime_version}" + + device_type_id = device_name_to_id[device_name] || build_device_type_id.call(device_name) + runtime_id = runtime_name_to_id[runtime_name] || build_runtime_id.call(os_name, runtime_version) + + # Use the device name without the version suffix as the simulator name + sim_name = device_name + + pair_key = "#{sim_name}||#{runtime_id}" + if existing_pairs.include?(pair_key) + puts "Already exists: #{sim_name} (#{runtime_version}) -> #{existing_index[pair_key]}" + skipped += 1 + next + end + + cmd = %(xcrun simctl create "#{sim_name}" "#{device_type_id}" "#{runtime_id}") + out = run(cmd) + if out.strip.empty? + # simctl returns the new UUID on success; if empty we assume failure was already reported + warn "Failed to create: #{sim_name} (#{runtime_version})" + skipped += 1 + else + puts "Created: #{sim_name} (#{runtime_version}) -> #{out.strip}" + created += 1 + existing_pairs.add(pair_key) + end + rescue => e + warn "Skipping #{descriptor}: #{e}" + skipped += 1 + end + end +end + +puts "Summary: created=#{created}, skipped=#{skipped}"