fastlane-plugin-create_simulator_devices 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +68 -0
- data/lib/fastlane/plugin/create_simulator_devices/actions/create_simulator_devices_action.rb +102 -0
- data/lib/fastlane/plugin/create_simulator_devices/helpers/create_simulator_devices/models/apple_build_version.rb +101 -0
- data/lib/fastlane/plugin/create_simulator_devices/helpers/create_simulator_devices/models/required_device.rb +26 -0
- data/lib/fastlane/plugin/create_simulator_devices/helpers/create_simulator_devices/models/required_runtime.rb +44 -0
- data/lib/fastlane/plugin/create_simulator_devices/helpers/create_simulator_devices/models/simctl/device.rb +45 -0
- data/lib/fastlane/plugin/create_simulator_devices/helpers/create_simulator_devices/models/simctl/device_type.rb +36 -0
- data/lib/fastlane/plugin/create_simulator_devices/helpers/create_simulator_devices/models/simctl/runtime.rb +114 -0
- data/lib/fastlane/plugin/create_simulator_devices/helpers/create_simulator_devices/models/simctl/runtime_supported_device_type.rb +33 -0
- data/lib/fastlane/plugin/create_simulator_devices/helpers/create_simulator_devices/models/xcodebuild/sdk.rb +63 -0
- data/lib/fastlane/plugin/create_simulator_devices/helpers/create_simulator_devices/models.rb +17 -0
- data/lib/fastlane/plugin/create_simulator_devices/helpers/create_simulator_devices/runner.rb +143 -0
- data/lib/fastlane/plugin/create_simulator_devices/helpers/create_simulator_devices/runtime_helper.rb +208 -0
- data/lib/fastlane/plugin/create_simulator_devices/helpers/create_simulator_devices/shell_helper.rb +181 -0
- data/lib/fastlane/plugin/create_simulator_devices/version.rb +7 -0
- data/lib/fastlane/plugin/create_simulator_devices.rb +19 -0
- metadata +60 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 0e9e16ed0de4857cf85fa712f346be0ceda11335896fd9fea73550fdd36c1fde
|
4
|
+
data.tar.gz: 6d1e956f7127169be6bc8505c543313be366be0f41063a417459a512801127ab
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: bd836c8046d442771c35103fcb4501f5a811939f1cc66e9d46f80f55b87e29facaa999fcca5c7841457461eb9f9297a1ca6526b5b2c7a5d5f37e3d5b74a0ae9b
|
7
|
+
data.tar.gz: 9f26874142099e933d8eb96da7d30b51f078fcce9b472501c356b653b2e06f37c9e0a0e8fd5ca36db1ebb25086b3e50acd970cd6de83fc1d9d2179d123831648
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2025 MacPaw Inc.
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
# create_simulator_devices plugin
|
2
|
+
|
3
|
+
[](https://rubygems.org/gems/fastlane-plugin-create_simulator_devices)
|
4
|
+
|
5
|
+
## About create_simulator_devices
|
6
|
+
|
7
|
+
This action creates simulator devices.
|
8
|
+
|
9
|
+
Usage sample:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
available_simulators_list = create_simulator_devices(
|
13
|
+
devices: ['iPhone 15 (17.0)', 'iPhone 16', 'iPhone 14 (16.3)']
|
14
|
+
verbose: false
|
15
|
+
)
|
16
|
+
# or
|
17
|
+
available_simulators_list = create_simulator_devices(
|
18
|
+
devices: 'iPhone 15 (17.0), iPhone 16,iPhone 14 (16.3)'
|
19
|
+
verbose: false
|
20
|
+
)
|
21
|
+
# Then run tests with a available_simulators_list
|
22
|
+
run_tests(
|
23
|
+
devices: available_simulators_list
|
24
|
+
)
|
25
|
+
```
|
26
|
+
|
27
|
+
## Getting Started
|
28
|
+
|
29
|
+
This project is a [_fastlane_](https://github.com/fastlane/fastlane) plugin. To get started with `fastlane-plugin-create_simulator_devices`, add it to your project by running:
|
30
|
+
|
31
|
+
```bash
|
32
|
+
fastlane add_plugin create_simulator_devices
|
33
|
+
```
|
34
|
+
|
35
|
+
## Example
|
36
|
+
|
37
|
+
Check out the [example `Fastfile`](fastlane/Fastfile) to see how to use this plugin. Try it by cloning the repo, running `fastlane install_plugins` and `bundle exec fastlane test`.
|
38
|
+
|
39
|
+
# Contributions
|
40
|
+
|
41
|
+
## Run tests for this plugin
|
42
|
+
|
43
|
+
To run both the tests, and code style validation, run
|
44
|
+
|
45
|
+
```
|
46
|
+
rake
|
47
|
+
```
|
48
|
+
|
49
|
+
To automatically fix many of the styling issues, use
|
50
|
+
```
|
51
|
+
rubocop -a
|
52
|
+
```
|
53
|
+
|
54
|
+
## Issues and Feedback
|
55
|
+
|
56
|
+
For any other issues and feedback about this plugin, please submit it to this repository.
|
57
|
+
|
58
|
+
## Troubleshooting
|
59
|
+
|
60
|
+
If you have trouble using plugins, check out the [Plugins Troubleshooting](https://docs.fastlane.tools/plugins/plugins-troubleshooting/) guide.
|
61
|
+
|
62
|
+
## Using _fastlane_ Plugins
|
63
|
+
|
64
|
+
For more information about how the `fastlane` plugin system works, check out the [Plugins documentation](https://docs.fastlane.tools/plugins/create-plugin/).
|
65
|
+
|
66
|
+
## About _fastlane_
|
67
|
+
|
68
|
+
_fastlane_ is the easiest way to automate beta deployments and releases for your iOS and Android apps. To learn more, check out [fastlane.tools](https://fastlane.tools).
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'fastlane'
|
4
|
+
require 'spaceship'
|
5
|
+
require_relative '../helpers/create_simulator_devices/runner'
|
6
|
+
require_relative '../helpers/create_simulator_devices/models'
|
7
|
+
|
8
|
+
module Fastlane
|
9
|
+
module Actions
|
10
|
+
module SharedValues
|
11
|
+
AVAILABLE_SIMULATOR_DEVICES = :AVAILABLE_SIMULATOR_DEVICES
|
12
|
+
end
|
13
|
+
|
14
|
+
CreateSimulatorDevices = ::Fastlane::CreateSimulatorDevices
|
15
|
+
|
16
|
+
# Create simulator devices.
|
17
|
+
class CreateSimulatorDevicesAction < Fastlane::Action
|
18
|
+
UI = ::Fastlane::UI unless defined?(UI)
|
19
|
+
|
20
|
+
attr_accessor :shell_helper
|
21
|
+
|
22
|
+
def self.run(params)
|
23
|
+
verbose = params[:verbose]
|
24
|
+
params[:devices] = params[:devices].split(',').map(&:strip) if params[:devices].is_a?(String)
|
25
|
+
required_devices = params[:devices]
|
26
|
+
UI.user_error!('No devices specified') if required_devices.nil? || required_devices.empty?
|
27
|
+
|
28
|
+
shell_helper = CreateSimulatorDevices::ShellHelper.new(verbose:, action_context: self)
|
29
|
+
runtime_helper = CreateSimulatorDevices::RuntimeHelper.new(cache_dir: params[:cache_dir], shell_helper:, verbose:)
|
30
|
+
|
31
|
+
runner = CreateSimulatorDevices::Runner.new(
|
32
|
+
runtime_helper: runtime_helper,
|
33
|
+
shell_helper: shell_helper,
|
34
|
+
verbose: verbose
|
35
|
+
)
|
36
|
+
|
37
|
+
available_simulator_devices = runner.run(required_devices)
|
38
|
+
|
39
|
+
Actions.lane_context[SharedValues::AVAILABLE_SIMULATOR_DEVICES] = available_simulator_devices
|
40
|
+
|
41
|
+
available_simulator_devices
|
42
|
+
end
|
43
|
+
|
44
|
+
#####################################################
|
45
|
+
# @!group Documentation
|
46
|
+
#####################################################
|
47
|
+
|
48
|
+
def self.description
|
49
|
+
'Creates simulator devices and installs missing runtimes if needed'
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.details
|
53
|
+
"This action does it best to create simulator devices.
|
54
|
+
|
55
|
+
Usage sample:
|
56
|
+
|
57
|
+
available_simulators_list = create_simulator_devices(
|
58
|
+
devices: ['iPhone 15 (17.0)', 'iPhone 16', 'iPhone 14 (16.3)']
|
59
|
+
verbose: false
|
60
|
+
)"
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.available_options
|
64
|
+
[
|
65
|
+
::FastlaneCore::ConfigItem.new(key: :devices,
|
66
|
+
env_name: 'SCAN_DEVICES',
|
67
|
+
description: 'A list of simulator devices to install (e.g. "iPhone 16")',
|
68
|
+
is_string: false,
|
69
|
+
default_value: 'iPhone 16'),
|
70
|
+
::FastlaneCore::ConfigItem.new(key: :cache_dir,
|
71
|
+
description: 'The directory to cache the simulator runtimes',
|
72
|
+
is_string: true,
|
73
|
+
default_value: "#{Dir.home}/.cache/create_simulator_devices"),
|
74
|
+
::FastlaneCore::ConfigItem.new(key: :verbose,
|
75
|
+
env_name: 'VERBOSE',
|
76
|
+
description: 'Verbose output',
|
77
|
+
is_string: false,
|
78
|
+
default_value: ::FastlaneCore::Globals.verbose?,
|
79
|
+
default_value_dynamic: true)
|
80
|
+
]
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.output
|
84
|
+
[
|
85
|
+
['AVAILABLE_SIMULATOR_DEVICES', 'A list of available simulator devices']
|
86
|
+
]
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.return_value
|
90
|
+
'Returns a list of available simulator devices'
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.authors
|
94
|
+
['nekrich']
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.is_supported?(_platform) # rubocop:disable Naming/PredicatePrefix
|
98
|
+
true
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Fastlane
|
4
|
+
# Create simulator devices.
|
5
|
+
module CreateSimulatorDevices
|
6
|
+
# Compare model for Apple build versions.
|
7
|
+
class AppleBuildVersion
|
8
|
+
def initialize(build_version)
|
9
|
+
return if build_version.nil? || build_version.empty?
|
10
|
+
|
11
|
+
@build_version = build_version
|
12
|
+
end
|
13
|
+
|
14
|
+
def beta?
|
15
|
+
@build_version.length >= 8
|
16
|
+
end
|
17
|
+
|
18
|
+
def <=>(other)
|
19
|
+
lhs_build_version = @build_version
|
20
|
+
rhs_build_version = other.to_s
|
21
|
+
|
22
|
+
return 0 if lhs_build_version == rhs_build_version
|
23
|
+
|
24
|
+
lhs_sdk_version = lhs_build_version[0...3]
|
25
|
+
rhs_sdk_version = rhs_build_version[0...3]
|
26
|
+
|
27
|
+
# Check if the SDK major.minor versions (first 3 characters) are the same.
|
28
|
+
# If they are the same, compare only SDK versions.
|
29
|
+
return lhs_sdk_version <=> rhs_sdk_version if lhs_sdk_version != rhs_sdk_version
|
30
|
+
|
31
|
+
lhs_is_beta = beta?
|
32
|
+
# In case we compare with a string, convert it to the AppleBuildVersion object.
|
33
|
+
rhs_is_beta = AppleBuildVersion.new(rhs_build_version).beta?
|
34
|
+
|
35
|
+
return lhs_is_beta ? -1 : 1 if lhs_is_beta != rhs_is_beta
|
36
|
+
|
37
|
+
# If the build versions are the same length, compare them lexicographically.
|
38
|
+
# Otherwise, compare the length of the build versions.
|
39
|
+
if lhs_build_version.length == rhs_build_version.length
|
40
|
+
lhs_build_version <=> rhs_build_version
|
41
|
+
else
|
42
|
+
# iOS 16.0 Release: 22A3362
|
43
|
+
# iOS 16.0 RC1: 22A348
|
44
|
+
# Trying to compare them lexicographically will return false ("22A3362" < "22A348").
|
45
|
+
# So we need to compare the length of the build versions.
|
46
|
+
lhs_build_version.length <=> rhs_build_version.length
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def almost_equal?(other)
|
51
|
+
return false if other.nil?
|
52
|
+
|
53
|
+
lhs_build_version = @build_version
|
54
|
+
rhs_build_version = other.to_s
|
55
|
+
|
56
|
+
lhs_is_beta = beta?
|
57
|
+
rhs_is_beta = AppleBuildVersion.new(rhs_build_version).beta?
|
58
|
+
|
59
|
+
return self == other unless lhs_is_beta && rhs_is_beta && lhs_build_version.length == rhs_build_version.length
|
60
|
+
|
61
|
+
lhs_build_version.chop == rhs_build_version.chop
|
62
|
+
end
|
63
|
+
|
64
|
+
def <(other)
|
65
|
+
(self <=> other).negative?
|
66
|
+
end
|
67
|
+
|
68
|
+
def >(other)
|
69
|
+
(self <=> other).positive?
|
70
|
+
end
|
71
|
+
|
72
|
+
def <=(other)
|
73
|
+
!(self <=> other).positive?
|
74
|
+
end
|
75
|
+
|
76
|
+
def >=(other)
|
77
|
+
!(self <=> other).negative?
|
78
|
+
end
|
79
|
+
|
80
|
+
def eql?(other)
|
81
|
+
(self <=> other).zero?
|
82
|
+
end
|
83
|
+
|
84
|
+
def eq?(other)
|
85
|
+
eql?(other)
|
86
|
+
end
|
87
|
+
|
88
|
+
def ==(other)
|
89
|
+
eql?(other)
|
90
|
+
end
|
91
|
+
|
92
|
+
def hash
|
93
|
+
@build_version.hash
|
94
|
+
end
|
95
|
+
|
96
|
+
def to_s
|
97
|
+
@build_version
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Fastlane
|
4
|
+
module CreateSimulatorDevices
|
5
|
+
# Represents a required runtime.
|
6
|
+
class RequiredDevice
|
7
|
+
attr_accessor :device_type, :os_name, :required_runtime, :available_runtime, :available_device
|
8
|
+
|
9
|
+
def initialize(device_type:, os_name:, required_runtime:, available_runtime:, available_device:)
|
10
|
+
self.device_type = device_type
|
11
|
+
self.os_name = os_name
|
12
|
+
self.required_runtime = required_runtime
|
13
|
+
self.available_runtime = available_runtime
|
14
|
+
self.available_device = available_device
|
15
|
+
end
|
16
|
+
|
17
|
+
def description
|
18
|
+
if required_runtime
|
19
|
+
"#{device_type.name} (#{required_runtime.product_version})"
|
20
|
+
else
|
21
|
+
device_type.name
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Fastlane
|
4
|
+
module CreateSimulatorDevices
|
5
|
+
# Represents a required runtime.
|
6
|
+
class RequiredRuntime
|
7
|
+
attr_accessor :sdk_platform, :os_name, :product_version, :product_build_version, :is_latest
|
8
|
+
|
9
|
+
def initialize(sdk_platform:, os_name:, product_version:, product_build_version:, is_latest: false)
|
10
|
+
self.sdk_platform = sdk_platform
|
11
|
+
self.os_name = os_name
|
12
|
+
self.product_version = product_version
|
13
|
+
self.product_build_version = product_build_version
|
14
|
+
self.is_latest = is_latest
|
15
|
+
end
|
16
|
+
|
17
|
+
def runtime_name
|
18
|
+
[os_name, product_version].compact.join(' ')
|
19
|
+
end
|
20
|
+
|
21
|
+
def latest?
|
22
|
+
is_latest
|
23
|
+
end
|
24
|
+
|
25
|
+
def beta?
|
26
|
+
return false unless product_build_version
|
27
|
+
|
28
|
+
product_build_version.beta?
|
29
|
+
end
|
30
|
+
|
31
|
+
def eql?(other)
|
32
|
+
other.sdk_platform == sdk_platform && other.os_name == os_name && other.product_version == product_version && other.product_build_version == product_build_version
|
33
|
+
end
|
34
|
+
|
35
|
+
def hash
|
36
|
+
[sdk_platform, os_name, product_version, product_build_version].compact.hash
|
37
|
+
end
|
38
|
+
|
39
|
+
def description
|
40
|
+
"#{os_name} (#{product_version}, #{product_build_version})"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Fastlane
|
4
|
+
module CreateSimulatorDevices
|
5
|
+
module SimCTL
|
6
|
+
# Represents a device.
|
7
|
+
class Device
|
8
|
+
attr_accessor :udid, :name, :device_type_identifier, :available
|
9
|
+
|
10
|
+
def initialize(name:, udid:, device_type_identifier:, available:)
|
11
|
+
self.name = name
|
12
|
+
self.udid = udid
|
13
|
+
self.device_type_identifier = device_type_identifier
|
14
|
+
self.available = available
|
15
|
+
end
|
16
|
+
|
17
|
+
def available?
|
18
|
+
available
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.from_hash(hash)
|
22
|
+
new(
|
23
|
+
name: hash[:name],
|
24
|
+
udid: hash[:udid],
|
25
|
+
device_type_identifier: hash[:deviceTypeIdentifier],
|
26
|
+
available: hash[:isAvailable]
|
27
|
+
)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Example of a device object from `xcrun simctl list devices --json` output:
|
35
|
+
#
|
36
|
+
# {
|
37
|
+
# "dataPath": "/Users/username/Library/Developer/CoreSimulator/Devices/1C1796E3-AB33-41AB-B3EB-9836CF39E57B/data",
|
38
|
+
# "dataPathSize": 4096,
|
39
|
+
# "logPath": "/Users/username/Library/Logs/CoreSimulator/1C1796E3-AB33-41AB-B3EB-9836CF39E57B",
|
40
|
+
# "udid": "1C1796E3-AB33-41AB-B3EB-9836CF39E57B",
|
41
|
+
# "isAvailable": true,
|
42
|
+
# "deviceTypeIdentifier": "com.apple.CoreSimulator.SimDeviceType.Apple-TV-1080p",
|
43
|
+
# "state": "Shutdown",
|
44
|
+
# "name": "Apple TV"
|
45
|
+
# }
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Fastlane
|
4
|
+
module CreateSimulatorDevices
|
5
|
+
module SimCTL
|
6
|
+
# Represents a device type.
|
7
|
+
class DeviceType
|
8
|
+
attr_accessor :identifier, :name, :product_family
|
9
|
+
|
10
|
+
def initialize(identifier:, name:, product_family:)
|
11
|
+
self.identifier = identifier
|
12
|
+
self.name = name
|
13
|
+
self.product_family = product_family
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.from_hash(hash)
|
17
|
+
new(identifier: hash[:identifier], name: hash[:name], product_family: hash[:productFamily])
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Example of a device type object from `xcrun simctl list devicetypes --json` output:
|
25
|
+
#
|
26
|
+
# {
|
27
|
+
# "productFamily" : "iPhone",
|
28
|
+
# "bundlePath" : "/Library/Developer/CoreSimulator/Profiles/DeviceTypes/iPod touch (7th generation).simdevicetype",
|
29
|
+
# "maxRuntimeVersion" : 1048575,
|
30
|
+
# "maxRuntimeVersionString" : "15.255.255",
|
31
|
+
# "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPod-touch--7th-generation-",
|
32
|
+
# "modelIdentifier" : "iPod9,1",
|
33
|
+
# "minRuntimeVersionString" : "12.3.1",
|
34
|
+
# "minRuntimeVersion" : 787201,
|
35
|
+
# "name" : "iPod touch (7th generation)"
|
36
|
+
# }
|
@@ -0,0 +1,114 @@
|
|
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 runtime from `xcrun simctl runtime list --json` output.
|
10
|
+
class RuntimeWithState
|
11
|
+
attr_accessor :identifier, :version, :build, :state, :deletable
|
12
|
+
|
13
|
+
def initialize(identifier:, version:, build:, state:, deletable:)
|
14
|
+
self.identifier = identifier
|
15
|
+
self.version = version
|
16
|
+
self.build = build
|
17
|
+
self.state = state
|
18
|
+
self.deletable = deletable
|
19
|
+
end
|
20
|
+
|
21
|
+
def ready?
|
22
|
+
state == 'Ready'
|
23
|
+
end
|
24
|
+
|
25
|
+
def deletable?
|
26
|
+
deletable
|
27
|
+
end
|
28
|
+
|
29
|
+
def unusable?
|
30
|
+
state == 'Unusable'
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.from_hash(hash)
|
34
|
+
new(
|
35
|
+
identifier: hash[:identifier],
|
36
|
+
version: Gem::Version.new(hash[:version]),
|
37
|
+
build: AppleBuildVersion.new(hash[:build]),
|
38
|
+
state: hash[:state],
|
39
|
+
deletable: hash[:deletable]
|
40
|
+
)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Represents a runtime from `xcrun simctl list runtimes --json` output.
|
45
|
+
class Runtime
|
46
|
+
attr_accessor :identifier, :platform, :version, :supported_device_types, :build_version, :is_available
|
47
|
+
|
48
|
+
def initialize(identifier:, platform:, version:, supported_device_types:, build_version:, is_available:) # rubocop:disable Metrics/ParameterLists
|
49
|
+
self.identifier = identifier
|
50
|
+
self.platform = platform
|
51
|
+
self.version = version
|
52
|
+
self.supported_device_types = supported_device_types
|
53
|
+
self.build_version = build_version
|
54
|
+
self.is_available = is_available
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.from_hash(hash)
|
58
|
+
build_version = AppleBuildVersion.new(hash[:buildversion]) if hash[:buildversion]
|
59
|
+
|
60
|
+
new(
|
61
|
+
identifier: hash[:identifier],
|
62
|
+
platform: hash[:platform],
|
63
|
+
version: Gem::Version.new(hash[:version]),
|
64
|
+
supported_device_types: hash[:supportedDeviceTypes].map { |device_type| Runtime::SupportedDeviceType.from_hash(device_type) },
|
65
|
+
build_version:,
|
66
|
+
is_available: hash[:isAvailable]
|
67
|
+
)
|
68
|
+
end
|
69
|
+
|
70
|
+
def runtime_name
|
71
|
+
"#{[platform, version].join(' ')} (build #{build_version})"
|
72
|
+
end
|
73
|
+
|
74
|
+
def available?
|
75
|
+
is_available
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Example of a Runtime object from `xcrun simctl list runtimes --json` output:
|
83
|
+
#
|
84
|
+
# {
|
85
|
+
# "isAvailable": true,
|
86
|
+
# "version": "17.4",
|
87
|
+
# "isInternal": false,
|
88
|
+
# "buildversion" : "23M5279f",
|
89
|
+
# "supportedArchitectures" : [
|
90
|
+
# "arm64"
|
91
|
+
# ],
|
92
|
+
# "supportedDeviceTypes": [
|
93
|
+
# {
|
94
|
+
# "bundlePath": "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/DeviceTypes/iPhone 15.simdevicetype",
|
95
|
+
# "name": "iPhone 15",
|
96
|
+
# "identifier": "com.apple.CoreSimulator.SimDeviceType.iPhone-15",
|
97
|
+
# "productFamily": "iPhone"
|
98
|
+
# },
|
99
|
+
# {
|
100
|
+
# "bundlePath": "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/DeviceTypes/iPhone 15 Pro.simdevicetype",
|
101
|
+
# "name": "iPhone 15 Pro",
|
102
|
+
# "identifier": "com.apple.CoreSimulator.SimDeviceType.iPhone-15-Pro",
|
103
|
+
# "productFamily": "iPhone"
|
104
|
+
# }
|
105
|
+
# ],
|
106
|
+
# "identifier": "com.apple.CoreSimulator.SimRuntime.iOS-17-4",
|
107
|
+
# "platform": "iOS",
|
108
|
+
# "bundlePath": "",
|
109
|
+
# "runtimeRoot": "",
|
110
|
+
# "lastUsage": {
|
111
|
+
# "arm64" : "0001-01-01T00:00:00Z"
|
112
|
+
# },
|
113
|
+
# "name": "iOS 17.4"
|
114
|
+
# }
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Fastlane
|
4
|
+
module CreateSimulatorDevices
|
5
|
+
module SimCTL
|
6
|
+
class Runtime
|
7
|
+
# Represents a supported device type by a runtime.
|
8
|
+
class SupportedDeviceType
|
9
|
+
attr_accessor :identifier, :name, :product_family
|
10
|
+
|
11
|
+
def initialize(identifier:, name:, product_family:)
|
12
|
+
self.identifier = identifier
|
13
|
+
self.name = name
|
14
|
+
self.product_family = product_family
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.from_hash(hash)
|
18
|
+
new(identifier: hash[:identifier], name: hash[:name], product_family: hash[:productFamily])
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Example of a supported device type object from `xcrun simctl list runtimes --json` output:
|
27
|
+
#
|
28
|
+
# {
|
29
|
+
# "bundlePath": "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/DeviceTypes/iPhone 15.simdevicetype",
|
30
|
+
# "name": "iPhone 15",
|
31
|
+
# "identifier": "com.apple.CoreSimulator.SimDeviceType.iPhone-15",
|
32
|
+
# "productFamily": "iPhone"
|
33
|
+
# }
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../apple_build_version'
|
4
|
+
|
5
|
+
module Fastlane
|
6
|
+
module CreateSimulatorDevices
|
7
|
+
module Xcodebuild
|
8
|
+
# Represents a SDK.
|
9
|
+
class SDK
|
10
|
+
attr_accessor :build_id, :canonical_name, :display_name, :platform, :platform_version, :product_build_version, :sdk_version, :product_name, :product_version
|
11
|
+
|
12
|
+
def initialize(build_id:, canonical_name:, display_name:, platform:, platform_version:, sdk_version:, product_name:, product_version:, product_build_version:) # rubocop:disable Metrics/ParameterLists
|
13
|
+
self.build_id = build_id
|
14
|
+
self.canonical_name = canonical_name
|
15
|
+
self.display_name = display_name
|
16
|
+
self.platform = platform
|
17
|
+
self.platform_version = platform_version
|
18
|
+
self.sdk_version = sdk_version
|
19
|
+
self.product_name = product_name
|
20
|
+
self.product_version = product_version
|
21
|
+
self.product_build_version = product_build_version
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.from_hash(hash)
|
25
|
+
product_version = Gem::Version.new(hash[:productVersion]) if hash[:productVersion]
|
26
|
+
new(
|
27
|
+
build_id: hash[:buildID],
|
28
|
+
canonical_name: hash[:canonicalName],
|
29
|
+
display_name: hash[:displayName],
|
30
|
+
platform: hash[:platform],
|
31
|
+
platform_version: Gem::Version.new(hash[:platformVersion]),
|
32
|
+
sdk_version: Gem::Version.new(hash[:sdkVersion]),
|
33
|
+
product_name: hash[:productName],
|
34
|
+
product_version: product_version,
|
35
|
+
product_build_version: AppleBuildVersion.new(hash[:productBuildVersion])
|
36
|
+
)
|
37
|
+
end
|
38
|
+
|
39
|
+
def simulator?
|
40
|
+
platform.end_with?('simulator')
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Example of a SDK object from `xcrun xcodebuild -showsdks -json` output:
|
48
|
+
#
|
49
|
+
# {
|
50
|
+
# "buildID": "F8821DD8-570E-11F0-99A4-5B96CBB013DB",
|
51
|
+
# "canonicalName": "watchsimulator26.0",
|
52
|
+
# "displayName": "Simulator - watchOS 26.0",
|
53
|
+
# "isBaseSdk": true,
|
54
|
+
# "platform": "watchsimulator",
|
55
|
+
# "platformPath": "/Applications/Xcode_26_beta_3.app/Contents/Developer/Platforms/WatchSimulator.platform",
|
56
|
+
# "platformVersion": "26.0",
|
57
|
+
# "productBuildVersion": "23R5307e",
|
58
|
+
# "productCopyright": "1983-2025 Apple Inc.",
|
59
|
+
# "productName": "Watch OS",
|
60
|
+
# "productVersion": "26.0",
|
61
|
+
# "sdkPath": "/Applications/Xcode_26_beta_3.app/Contents/Developer/Platforms/WatchSimulator.platform/Developer/SDKs/WatchSimulator26.0.sdk",
|
62
|
+
# "sdkVersion": "26.0"
|
63
|
+
# }
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'models/simctl/device_type'
|
4
|
+
require_relative 'models/simctl/device'
|
5
|
+
require_relative 'models/simctl/runtime'
|
6
|
+
require_relative 'models/simctl/runtime_supported_device_type'
|
7
|
+
require_relative 'models/xcodebuild/sdk'
|
8
|
+
require_relative 'models/required_device'
|
9
|
+
require_relative 'models/required_runtime'
|
10
|
+
require_relative 'models/apple_build_version'
|
11
|
+
|
12
|
+
module Fastlane
|
13
|
+
# Create simulator devices.
|
14
|
+
module CreateSimulatorDevices
|
15
|
+
UI = ::Fastlane::UI unless defined?(UI)
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'runtime_helper'
|
4
|
+
|
5
|
+
module Fastlane
|
6
|
+
# Create simulator devices.
|
7
|
+
module CreateSimulatorDevices
|
8
|
+
# Does all the work to create simulator devices.
|
9
|
+
class Runner
|
10
|
+
UI = ::Fastlane::UI unless defined?(UI)
|
11
|
+
|
12
|
+
attr_accessor :shell_helper, :verbose, :runtime_helper
|
13
|
+
|
14
|
+
def initialize(runtime_helper:, shell_helper:, verbose:)
|
15
|
+
self.shell_helper = shell_helper
|
16
|
+
self.verbose = verbose || shell_helper.verbose
|
17
|
+
self.runtime_helper = runtime_helper
|
18
|
+
end
|
19
|
+
|
20
|
+
def run(devices)
|
21
|
+
shell_helper.stop_core_simulator_services
|
22
|
+
|
23
|
+
# Delete unusable runtimes and unavailable devices.
|
24
|
+
runtime_helper.delete_unusable_runtimes
|
25
|
+
delete_unavailable_devices
|
26
|
+
|
27
|
+
# Create distict required devices from a given list of device strings.
|
28
|
+
required_devices = devices
|
29
|
+
.filter_map { |device| required_device_for_device(device) }
|
30
|
+
.uniq { |required_device| [required_device.device_type.name, required_device.required_runtime.product_version] }
|
31
|
+
|
32
|
+
# Install missing runtimes if needed.
|
33
|
+
runtime_helper.install_missing_runtimes(required_devices)
|
34
|
+
|
35
|
+
# Create missing devices for required devices.
|
36
|
+
create_missing_devices(required_devices)
|
37
|
+
|
38
|
+
# Return distinct matched devices strings
|
39
|
+
matched_devices = required_devices
|
40
|
+
.reject { |required_device| required_device.available_device.nil? }
|
41
|
+
.map(&:description)
|
42
|
+
|
43
|
+
UI.user_error!('No available devices found') if matched_devices.empty?
|
44
|
+
|
45
|
+
matched_devices
|
46
|
+
end
|
47
|
+
|
48
|
+
def delete_unavailable_devices
|
49
|
+
return unless shell_helper.available_devices_for_runtimes.values.flatten.any?(&:available?)
|
50
|
+
|
51
|
+
shell_helper.delete_unavailable_devices
|
52
|
+
|
53
|
+
shell_helper.available_devices_for_runtimes(force: true)
|
54
|
+
end
|
55
|
+
|
56
|
+
def available_device_for_required_device(required_device)
|
57
|
+
return nil if required_device.available_runtime.nil?
|
58
|
+
|
59
|
+
available_devices = shell_helper.available_devices_for_runtimes[required_device.available_runtime.identifier.to_sym]
|
60
|
+
|
61
|
+
available_devices.detect { |device| device.device_type_identifier == required_device.device_type.identifier }
|
62
|
+
end
|
63
|
+
|
64
|
+
def create_missing_devices(required_devices)
|
65
|
+
find_runtime_and_device_for_required_devices(required_devices)
|
66
|
+
|
67
|
+
missing_devices = required_devices
|
68
|
+
.select { |required_device| required_device.available_device.nil? }
|
69
|
+
|
70
|
+
return if missing_devices.empty?
|
71
|
+
|
72
|
+
UI.message('Creating missing devices')
|
73
|
+
missing_devices.each do |missing_device|
|
74
|
+
shell_helper.create_device(
|
75
|
+
missing_device.description,
|
76
|
+
missing_device.device_type.identifier,
|
77
|
+
missing_device.available_runtime.identifier
|
78
|
+
)
|
79
|
+
end
|
80
|
+
|
81
|
+
shell_helper.available_devices_for_runtimes(force: true)
|
82
|
+
|
83
|
+
find_runtime_and_device_for_required_devices(missing_devices)
|
84
|
+
end
|
85
|
+
|
86
|
+
def find_runtime_and_device_for_required_devices(required_devices)
|
87
|
+
UI.message('Searching for matching available devices...')
|
88
|
+
required_devices.each do |required_device|
|
89
|
+
required_device.available_runtime = runtime_helper.available_runtime_for_required_device(required_device)
|
90
|
+
required_device.available_device = available_device_for_required_device(required_device)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
PRODUCT_FAMILY_TO_OS_NAME = {
|
95
|
+
'iPhone' => 'iOS',
|
96
|
+
'iPad' => 'iOS',
|
97
|
+
'iPod' => 'iOS',
|
98
|
+
'Apple TV' => 'tvOS',
|
99
|
+
'Apple Watch' => 'watchOS',
|
100
|
+
'Apple Vision' => 'xrOS'
|
101
|
+
}.freeze
|
102
|
+
|
103
|
+
def required_device_for_device(device)
|
104
|
+
available_device_types = shell_helper.available_device_types
|
105
|
+
|
106
|
+
device_type = available_device_types.detect do |available_device_type|
|
107
|
+
# Avoid matching "iPhone 16" for the "iPhone 16e" device.
|
108
|
+
"#{device} ".start_with?("#{available_device_type.name} ")
|
109
|
+
end
|
110
|
+
|
111
|
+
unless device_type
|
112
|
+
UI.important("Device type not found for device #{device}")
|
113
|
+
return nil
|
114
|
+
end
|
115
|
+
|
116
|
+
product_family = device_type.product_family
|
117
|
+
|
118
|
+
os_name = PRODUCT_FAMILY_TO_OS_NAME[product_family]
|
119
|
+
device_os_version = device.delete_prefix(device_type.name).strip
|
120
|
+
device_os_version = device_os_version[/\(([\d.]*?)\)/, 1]
|
121
|
+
|
122
|
+
runtime_version = nil
|
123
|
+
unless device_os_version.nil? || device_os_version.empty?
|
124
|
+
device_os_version += '.0' if device_os_version.scan('.').none?
|
125
|
+
|
126
|
+
runtime_version = Gem::Version.new(device_os_version)
|
127
|
+
end
|
128
|
+
|
129
|
+
required_device = RequiredDevice.new(
|
130
|
+
device_type:,
|
131
|
+
os_name:,
|
132
|
+
required_runtime: nil,
|
133
|
+
available_runtime: nil,
|
134
|
+
available_device: nil
|
135
|
+
)
|
136
|
+
|
137
|
+
required_device.required_runtime = runtime_helper.required_runtime_for_device(required_device, runtime_version)
|
138
|
+
|
139
|
+
required_device.required_runtime.nil? ? nil : required_device
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
data/lib/fastlane/plugin/create_simulator_devices/helpers/create_simulator_devices/runtime_helper.rb
ADDED
@@ -0,0 +1,208 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
require_relative 'shell_helper'
|
5
|
+
|
6
|
+
module Fastlane
|
7
|
+
module CreateSimulatorDevices
|
8
|
+
# Helper class for managing simulator runtimes.
|
9
|
+
class RuntimeHelper
|
10
|
+
UI = ::Fastlane::UI unless defined?(UI)
|
11
|
+
|
12
|
+
attr_accessor :cache_dir, :shell_helper, :verbose
|
13
|
+
|
14
|
+
def initialize(cache_dir:, shell_helper:, verbose:)
|
15
|
+
self.cache_dir = cache_dir
|
16
|
+
self.shell_helper = shell_helper
|
17
|
+
self.verbose = verbose
|
18
|
+
end
|
19
|
+
|
20
|
+
SDK_PLATFORM_TO_OS_NAME = {
|
21
|
+
'iphonesimulator' => 'iOS',
|
22
|
+
'appletvsimulator' => 'tvOS',
|
23
|
+
'watchsimulator' => 'watchOS',
|
24
|
+
'xrsimulator' => 'xrOS'
|
25
|
+
}.freeze
|
26
|
+
|
27
|
+
def delete_unusable_runtimes
|
28
|
+
deletable_runtimes = shell_helper.installed_runtimes_with_state
|
29
|
+
.select { |runtime| runtime.unusable? && runtime.deletable? }
|
30
|
+
|
31
|
+
return if deletable_runtimes.empty?
|
32
|
+
|
33
|
+
deletable_runtimes.each do |runtime|
|
34
|
+
shell_helper.delete_runtime(runtime.identifier)
|
35
|
+
end
|
36
|
+
|
37
|
+
shell_helper.available_runtimes(force: true)
|
38
|
+
end
|
39
|
+
|
40
|
+
def install_missing_runtimes(required_devices)
|
41
|
+
needed_runtimes = required_devices.filter_map(&:required_runtime).uniq
|
42
|
+
|
43
|
+
missing_runtimes = missing_runtimes(needed_runtimes)
|
44
|
+
|
45
|
+
return if missing_runtimes.empty?
|
46
|
+
|
47
|
+
missing_runtimes.each do |missing_runtime|
|
48
|
+
download_and_install_missing_runtime(missing_runtime)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Update available_runtimes after installing the runtimes.
|
52
|
+
shell_helper.installed_runtimes_with_state
|
53
|
+
shell_helper.available_runtimes(force: true)
|
54
|
+
|
55
|
+
# Check if missing runtimes are available after installing
|
56
|
+
missing_runtimes = missing_runtimes(missing_runtimes)
|
57
|
+
|
58
|
+
# List missing runtimes after attempt to install the runtimes.
|
59
|
+
missing_runtimes(missing_runtimes)
|
60
|
+
.each do |missing_runtime|
|
61
|
+
UI.important("Failed to find/download/install runtime #{missing_runtime.runtime_name}")
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def missing_runtimes(needed_runtimes)
|
66
|
+
needed_runtimes.select do |needed_runtime|
|
67
|
+
# Check if available runtimes contain the needed runtime.
|
68
|
+
available_runtime_matching_needed_runtime?(needed_runtime).nil?
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def available_runtime_matching_needed_runtime?(needed_runtime)
|
73
|
+
matching_runtimes = shell_helper.available_runtimes
|
74
|
+
.select do |available_runtime|
|
75
|
+
next false if needed_runtime.os_name != available_runtime.platform ||
|
76
|
+
needed_runtime.product_version != available_runtime.version
|
77
|
+
|
78
|
+
needed_runtime.product_build_version = [needed_runtime.product_build_version, available_runtime.build_version].compact.max
|
79
|
+
|
80
|
+
needed_runtime.product_build_version.almost_equal?(available_runtime.build_version)
|
81
|
+
end
|
82
|
+
|
83
|
+
matching_runtimes.max_by { |available_runtime| [available_runtime.version, available_runtime.build_version] }
|
84
|
+
end
|
85
|
+
|
86
|
+
def available_runtime_for_required_device(required_device)
|
87
|
+
available_runtime = available_runtime_matching_needed_runtime?(required_device.required_runtime)
|
88
|
+
|
89
|
+
if available_runtime.nil?
|
90
|
+
UI.important("Runtime #{required_device.required_runtime.description} not found. Skipping simulator creation for #{required_device.description}...")
|
91
|
+
return nil
|
92
|
+
end
|
93
|
+
|
94
|
+
# Check if the runtime supports the device type.
|
95
|
+
if available_runtime.supported_device_types
|
96
|
+
.none? { |supported_device_type| supported_device_type.identifier == required_device.device_type.identifier }
|
97
|
+
UI.important("Device type #{required_device.device_type.name} is not supported by runtime #{available_runtime.identifier}. Skipping simulator creation for #{required_device.description}...")
|
98
|
+
return nil
|
99
|
+
end
|
100
|
+
|
101
|
+
if available_runtime.nil?
|
102
|
+
UI.important("Runtime #{required_device.required_runtime.description} not found. Skipping simulator creation for #{required_device.description}...")
|
103
|
+
return nil
|
104
|
+
end
|
105
|
+
|
106
|
+
available_runtime
|
107
|
+
end
|
108
|
+
|
109
|
+
def install_missing_runtime(missing_runtime, cached_runtime_file)
|
110
|
+
runtime_name = missing_runtime.runtime_name
|
111
|
+
|
112
|
+
if missing_runtime.product_build_version.nil?
|
113
|
+
UI.important("Failed to find runtime build version for #{runtime_name}")
|
114
|
+
return
|
115
|
+
end
|
116
|
+
|
117
|
+
shell_helper.import_runtime(cached_runtime_file, runtime_name)
|
118
|
+
end
|
119
|
+
|
120
|
+
def download_and_install_missing_runtime(missing_runtime)
|
121
|
+
UI.message("Attempting to install #{missing_runtime.runtime_name} runtime.")
|
122
|
+
|
123
|
+
downloaded_runtime_file = cached_runtime_file(missing_runtime)
|
124
|
+
|
125
|
+
if downloaded_runtime_file.nil?
|
126
|
+
shell_helper.download_runtime(missing_runtime, cache_dir)
|
127
|
+
downloaded_runtime_file = cached_runtime_file(missing_runtime)
|
128
|
+
end
|
129
|
+
|
130
|
+
shell_helper.import_runtime(downloaded_runtime_file, missing_runtime.runtime_name)
|
131
|
+
end
|
132
|
+
|
133
|
+
def runtime_build_version_for_filename(filename)
|
134
|
+
return nil unless filename
|
135
|
+
|
136
|
+
# iphonesimulator_18.4_22E238.dmg
|
137
|
+
# Format: iphonesimulator_VERSION_BUILD.dmg
|
138
|
+
build_version = File.basename(filename, '.dmg').split('_').last
|
139
|
+
|
140
|
+
AppleBuildVersion.new(build_version)
|
141
|
+
end
|
142
|
+
|
143
|
+
def cached_runtime_file(missing_runtime)
|
144
|
+
FileUtils.mkdir_p(cache_dir)
|
145
|
+
|
146
|
+
runtime_dmg_search_pattern = "#{cache_dir}/#{missing_runtime.sdk_platform}_#{missing_runtime.product_version}_"
|
147
|
+
|
148
|
+
# Remove the last character of the build version if it is the latest beta.
|
149
|
+
# Apple can create a new Runtime version and block product build version
|
150
|
+
# shipped with Xcode betas and use the same product version.
|
151
|
+
# E.g. Xcode 26.0 Beta 3 has iOS 26.0 (23A5287e) SDK, but
|
152
|
+
# xcodebuild downloads iphonesimulator_26.0_23A5287g.dmg as latest.
|
153
|
+
runtime_dmg_search_pattern += missing_runtime.product_build_version.to_s.chop if missing_runtime.product_build_version
|
154
|
+
runtime_dmg_search_pattern += '*.dmg'
|
155
|
+
|
156
|
+
runtime_file = Dir
|
157
|
+
.glob(runtime_dmg_search_pattern)
|
158
|
+
.max_by { |filename| runtime_build_version_for_filename(filename) }
|
159
|
+
|
160
|
+
return nil if runtime_file.nil?
|
161
|
+
|
162
|
+
missing_runtime.product_build_version ||= runtime_build_version_for_filename(runtime_file)
|
163
|
+
|
164
|
+
UI.message("Found existing #{missing_runtime.runtime_name} runtime image in #{cache_dir}: #{runtime_file}")
|
165
|
+
|
166
|
+
runtime_file
|
167
|
+
end
|
168
|
+
|
169
|
+
def required_runtime_for_device(required_device, runtime_version)
|
170
|
+
sdk = max_available_simulator_sdks[required_device.os_name]
|
171
|
+
|
172
|
+
# If the runtime version is the same as the SDK version, use the SDK build version.
|
173
|
+
# This will allow to use different runtimes for the same version but different Xcode beta versions.
|
174
|
+
product_build_version = sdk.product_build_version if runtime_version.nil? || sdk.product_version == runtime_version
|
175
|
+
|
176
|
+
if !runtime_version.nil? && runtime_version > sdk.product_version
|
177
|
+
UI.important("Runtime version for #{required_device.device_type.name} (#{runtime_version}) is higher than maximum supported by the Xcode: #{sdk.product_version}")
|
178
|
+
return nil
|
179
|
+
end
|
180
|
+
|
181
|
+
RequiredRuntime.new(
|
182
|
+
sdk_platform: sdk.platform,
|
183
|
+
os_name: required_device.os_name,
|
184
|
+
product_version: runtime_version || sdk.product_version,
|
185
|
+
product_build_version:,
|
186
|
+
is_latest: sdk.product_build_version.almost_equal?(product_build_version)
|
187
|
+
)
|
188
|
+
end
|
189
|
+
|
190
|
+
# Returns a hash where key is platform string and value is sdk version.
|
191
|
+
def max_available_simulator_sdks
|
192
|
+
return @max_available_simulator_sdks unless @max_available_simulator_sdks.nil?
|
193
|
+
|
194
|
+
@max_available_simulator_sdks = shell_helper.available_sdks
|
195
|
+
# Only simulators
|
196
|
+
.filter { |sdk| sdk.platform.include?('simulator') }
|
197
|
+
# Calculate max version for each product name
|
198
|
+
.each_with_object({}) do |sdk, sdk_versions|
|
199
|
+
os_name = SDK_PLATFORM_TO_OS_NAME[sdk.platform]
|
200
|
+
stored_sdk = sdk_versions[os_name]
|
201
|
+
sdk_versions[os_name] = sdk if stored_sdk.nil? || sdk.product_version > stored_sdk.product_version
|
202
|
+
end
|
203
|
+
|
204
|
+
@max_available_simulator_sdks
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
data/lib/fastlane/plugin/create_simulator_devices/helpers/create_simulator_devices/shell_helper.rb
ADDED
@@ -0,0 +1,181 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'models'
|
4
|
+
|
5
|
+
module Fastlane
|
6
|
+
# Create simulator devices.
|
7
|
+
module CreateSimulatorDevices
|
8
|
+
# Shell helper
|
9
|
+
class ShellHelper
|
10
|
+
UI = ::Fastlane::UI unless defined?(UI)
|
11
|
+
|
12
|
+
# Proprty verbose
|
13
|
+
attr_accessor :verbose, :action_context
|
14
|
+
|
15
|
+
def initialize(verbose: false, action_context: nil)
|
16
|
+
self.verbose = verbose
|
17
|
+
self.action_context = action_context
|
18
|
+
end
|
19
|
+
|
20
|
+
def sh(command:, print_command: verbose, print_command_output: verbose)
|
21
|
+
if action_context
|
22
|
+
action_context.sh(command, print_command:, print_command_output:)
|
23
|
+
else
|
24
|
+
# Fallback for testing or direct usage
|
25
|
+
require 'open3'
|
26
|
+
stdout, stderr, status = Open3.capture3(command)
|
27
|
+
|
28
|
+
UI.message command if print_command
|
29
|
+
|
30
|
+
if status.success?
|
31
|
+
UI.message stdout if print_command_output
|
32
|
+
stdout
|
33
|
+
else
|
34
|
+
error_message = "Command failed: #{command}\n#{stderr}"
|
35
|
+
UI.error error_message
|
36
|
+
raise StandardError, error_message
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def stop_core_simulator_services
|
42
|
+
UI.message('Stop CoreSimulator')
|
43
|
+
services_to_stop = [
|
44
|
+
'com.apple.CoreSimulator.CoreSimulatorService',
|
45
|
+
'com.apple.CoreSimulator.SimulatorTrampoline',
|
46
|
+
'com.apple.CoreSimulator.SimLaunchHost-arm64',
|
47
|
+
'com.apple.CoreSimulator.SimLaunchHost-x86'
|
48
|
+
]
|
49
|
+
services_to_stop.each do |service|
|
50
|
+
sh(command: "launchctl remove #{service} || true")
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def available_device_types(force: false)
|
55
|
+
return @available_device_types unless force || @available_device_types.nil?
|
56
|
+
|
57
|
+
UI.message('Fetching available device types...')
|
58
|
+
json = sh(command: 'xcrun simctl list --json --no-escape-slashes devicetypes')
|
59
|
+
|
60
|
+
@available_device_types = JSON
|
61
|
+
.parse(json, symbolize_names: true)[:devicetypes]
|
62
|
+
.map { |device_type| SimCTL::DeviceType.from_hash(device_type) }
|
63
|
+
|
64
|
+
@available_device_types
|
65
|
+
end
|
66
|
+
|
67
|
+
def delete_device(udid)
|
68
|
+
UI.message("Deleting device #{udid}...")
|
69
|
+
sh(command: "xcrun simctl delete #{udid.shellescape}")
|
70
|
+
end
|
71
|
+
|
72
|
+
def delete_unavailable_devices
|
73
|
+
UI.message('Deleting unavailable devices...')
|
74
|
+
sh(command: 'xcrun simctl delete unavailable')
|
75
|
+
end
|
76
|
+
|
77
|
+
def available_devices_for_runtimes(force: false)
|
78
|
+
return @available_devices_for_runtimes unless force || @available_devices_for_runtimes.nil?
|
79
|
+
|
80
|
+
UI.message('Fetching available devices...')
|
81
|
+
json = sh(command: 'xcrun simctl list --json --no-escape-slashes devices')
|
82
|
+
|
83
|
+
@available_devices_for_runtimes = JSON
|
84
|
+
.parse(json, symbolize_names: true)[:devices]
|
85
|
+
.transform_values { |devices| devices.map { |device| SimCTL::Device.from_hash(device) } }
|
86
|
+
|
87
|
+
@available_devices_for_runtimes
|
88
|
+
end
|
89
|
+
|
90
|
+
def available_runtimes(force: false)
|
91
|
+
return @available_runtimes unless force || @available_runtimes.nil?
|
92
|
+
|
93
|
+
UI.message('Fetching available runtimes...')
|
94
|
+
json = sh(command: 'xcrun simctl list --json --no-escape-slashes runtimes')
|
95
|
+
|
96
|
+
@available_runtimes = JSON
|
97
|
+
.parse(json, symbolize_names: true)[:runtimes]
|
98
|
+
.map { |runtime| SimCTL::Runtime.from_hash(runtime) }
|
99
|
+
.select(&:available?)
|
100
|
+
|
101
|
+
@available_runtimes
|
102
|
+
end
|
103
|
+
|
104
|
+
def installed_runtimes_with_state
|
105
|
+
UI.message('Fetching runtimes with state...')
|
106
|
+
json = sh(command: 'xcrun simctl runtime list --json')
|
107
|
+
|
108
|
+
JSON
|
109
|
+
.parse(json, symbolize_names: true)
|
110
|
+
.map { |_, runtime| SimCTL::RuntimeWithState.from_hash(runtime) }
|
111
|
+
end
|
112
|
+
|
113
|
+
def available_sdks(force: false)
|
114
|
+
return @available_sdks unless force || @available_sdks.nil?
|
115
|
+
|
116
|
+
UI.message('Fetching available sdks...')
|
117
|
+
json = sh(command: 'xcrun xcodebuild -showsdks -json')
|
118
|
+
|
119
|
+
@available_sdks = JSON
|
120
|
+
.parse(json, symbolize_names: true)
|
121
|
+
.map { |sdk| Xcodebuild::SDK.from_hash(sdk) }
|
122
|
+
|
123
|
+
@available_sdks
|
124
|
+
end
|
125
|
+
|
126
|
+
def create_device(name, device_type_identifier, runtime_identifier)
|
127
|
+
UI.message("Creating device #{name}")
|
128
|
+
sh(command: "xcrun simctl create #{name.shellescape} #{device_type_identifier.shellescape} #{runtime_identifier.shellescape}")
|
129
|
+
end
|
130
|
+
|
131
|
+
def delete_runtime(runtime_identifier)
|
132
|
+
UI.message("Deleting runtime #{runtime_identifier}...")
|
133
|
+
sh(command: "xcrun simctl runtime delete #{runtime_identifier.shellescape}")
|
134
|
+
end
|
135
|
+
|
136
|
+
def download_runtime(missing_runtime, cache_dir)
|
137
|
+
UI.message("Downloading #{missing_runtime.runtime_name} to #{cache_dir}. This may take a while...")
|
138
|
+
|
139
|
+
command = [
|
140
|
+
'xcrun',
|
141
|
+
'xcodebuild',
|
142
|
+
'-verbose',
|
143
|
+
|
144
|
+
'-exportPath',
|
145
|
+
cache_dir.shellescape,
|
146
|
+
|
147
|
+
'-downloadPlatform',
|
148
|
+
missing_runtime.os_name.shellescape
|
149
|
+
]
|
150
|
+
|
151
|
+
unless missing_runtime.latest?
|
152
|
+
command << '-buildVersion'
|
153
|
+
# Prefer the build version if available, otherwise use the product version.
|
154
|
+
command << (missing_runtime.product_build_version || missing_runtime.product_version.to_s).shellescape
|
155
|
+
end
|
156
|
+
|
157
|
+
sh(command: command.join(' '), print_command: true, print_command_output: true)
|
158
|
+
end
|
159
|
+
|
160
|
+
def import_runtime(runtime_dmg_filename, runtime_name)
|
161
|
+
UI.message("Importing runtime #{runtime_name} image from #{runtime_dmg_filename}...")
|
162
|
+
import_platform_command = "xcrun xcodebuild -verbose -importPlatform #{runtime_dmg_filename.shellescape}"
|
163
|
+
begin
|
164
|
+
sh(command: import_platform_command)
|
165
|
+
rescue StandardError => e
|
166
|
+
UI.important("Failed to import runtime #{runtime_name} with '#{import_platform_command}' :\n#{e}")
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# def add_runtime(runtime_dmg_filename, runtime_name)
|
171
|
+
# UI.message("Adding runtime #{runtime_name}...")
|
172
|
+
# add_runtime_command = "xcrun simctl runtime add #{runtime_dmg_filename.shellescape}"
|
173
|
+
# begin
|
174
|
+
# sh(command: add_runtime_command)
|
175
|
+
# rescue StandardError => e
|
176
|
+
# UI.important("Failed to add runtime #{runtime_name} with '#{add_runtime_command}':\n#{e}")
|
177
|
+
# end
|
178
|
+
# end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'fastlane/plugin/create_simulator_devices/version'
|
4
|
+
|
5
|
+
module Fastlane
|
6
|
+
# Creates missing simulator devices.
|
7
|
+
module CreateSimulatorDevices
|
8
|
+
# Return all .rb files inside the "actions" and "helper" directory
|
9
|
+
def self.all_classes
|
10
|
+
Dir[File.expand_path('**/{actions,helpers}/*.rb', File.dirname(__FILE__))]
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# By default we want to import all available actions and helpers
|
16
|
+
# A plugin can contain any number of actions and plugins
|
17
|
+
Fastlane::CreateSimulatorDevices.all_classes.each do |current|
|
18
|
+
require current
|
19
|
+
end
|
metadata
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fastlane-plugin-create_simulator_devices
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Vitalii Budnik
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-07-17 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description:
|
14
|
+
email: developers@setapp.com
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- LICENSE
|
20
|
+
- README.md
|
21
|
+
- lib/fastlane/plugin/create_simulator_devices.rb
|
22
|
+
- lib/fastlane/plugin/create_simulator_devices/actions/create_simulator_devices_action.rb
|
23
|
+
- lib/fastlane/plugin/create_simulator_devices/helpers/create_simulator_devices/models.rb
|
24
|
+
- lib/fastlane/plugin/create_simulator_devices/helpers/create_simulator_devices/models/apple_build_version.rb
|
25
|
+
- lib/fastlane/plugin/create_simulator_devices/helpers/create_simulator_devices/models/required_device.rb
|
26
|
+
- lib/fastlane/plugin/create_simulator_devices/helpers/create_simulator_devices/models/required_runtime.rb
|
27
|
+
- lib/fastlane/plugin/create_simulator_devices/helpers/create_simulator_devices/models/simctl/device.rb
|
28
|
+
- lib/fastlane/plugin/create_simulator_devices/helpers/create_simulator_devices/models/simctl/device_type.rb
|
29
|
+
- lib/fastlane/plugin/create_simulator_devices/helpers/create_simulator_devices/models/simctl/runtime.rb
|
30
|
+
- lib/fastlane/plugin/create_simulator_devices/helpers/create_simulator_devices/models/simctl/runtime_supported_device_type.rb
|
31
|
+
- lib/fastlane/plugin/create_simulator_devices/helpers/create_simulator_devices/models/xcodebuild/sdk.rb
|
32
|
+
- lib/fastlane/plugin/create_simulator_devices/helpers/create_simulator_devices/runner.rb
|
33
|
+
- lib/fastlane/plugin/create_simulator_devices/helpers/create_simulator_devices/runtime_helper.rb
|
34
|
+
- lib/fastlane/plugin/create_simulator_devices/helpers/create_simulator_devices/shell_helper.rb
|
35
|
+
- lib/fastlane/plugin/create_simulator_devices/version.rb
|
36
|
+
homepage:
|
37
|
+
licenses:
|
38
|
+
- MIT
|
39
|
+
metadata:
|
40
|
+
rubygems_mfa_required: 'true'
|
41
|
+
post_install_message:
|
42
|
+
rdoc_options: []
|
43
|
+
require_paths:
|
44
|
+
- lib
|
45
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: 3.1.1
|
50
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
requirements: []
|
56
|
+
rubygems_version: 3.3.7
|
57
|
+
signing_key:
|
58
|
+
specification_version: 4
|
59
|
+
summary: Fastlane plugin to create simulator devices
|
60
|
+
test_files: []
|