run_loop 1.5.5 → 1.5.6.pre1

Sign up to get free protection for your applications and to get access to all the features.
@@ -42,27 +42,114 @@ module RunLoop
42
42
  debug = RunLoop::Environment.debug?
43
43
 
44
44
  sha = OpenSSL::Digest::SHA256.new
45
- self.recursive_glob_for_entries(path).each do |file|
46
- if File.directory?(file)
47
- # skip directories
48
- elsif !Pathname.new(file).exist?
49
- # skip broken symlinks
50
- else
51
- case File.ftype(file)
52
- when 'fifo'
53
- RunLoop.log_warn("SHA1 SKIPPING FIFO #{file}") if debug
54
- when 'socket'
55
- RunLoop.log_warn("SHA1 SKIPPING SOCKET #{file}") if debug
56
- when 'characterSpecial'
57
- RunLoop.log_warn("SHA1 SKIPPING CHAR SPECIAL #{file}") if debug
58
- when 'blockSpecial'
59
- RunLoop.log_warn("SHA1 SKIPPING BLOCK SPECIAL #{file}") if debug
60
- else
61
- sha << File.read(file)
45
+ entries.each do |file|
46
+ unless self.skip_file?(file, 'SHA1', debug)
47
+ begin
48
+ sha << File.read(file)
49
+ rescue => e
50
+ if debug
51
+ RunLoop.log_warn(%Q{
52
+ RunLoop::Directory.directory_digest raised an error:
53
+
54
+ #{e}
55
+
56
+ while trying to find the SHA of this file:
57
+
58
+ #{file}
59
+
60
+ Please report this here:
61
+
62
+ https://github.com/calabash/run_loop/issues
63
+
64
+ })
65
+ end
62
66
  end
63
67
  end
64
68
  end
65
69
  sha.hexdigest
66
70
  end
71
+
72
+ def self.size(path, format)
73
+
74
+ allowed_formats = [:bytes, :kb, :mb, :gb]
75
+ unless allowed_formats.include?(format)
76
+ raise ArgumentError, "Expected '#{format}' to be one of #{allowed_formats.join(', ')}"
77
+ end
78
+
79
+ unless File.exist?(path)
80
+ raise ArgumentError, "Expected '#{path}' to exist"
81
+ end
82
+
83
+ unless File.directory?(path)
84
+ raise ArgumentError, "Expected '#{path}' to be a directory"
85
+ end
86
+
87
+ entries = self.recursive_glob_for_entries(path)
88
+
89
+ if entries.empty?
90
+ raise ArgumentError, "Expected a non-empty dir at '#{path}' found '#{entries}'"
91
+ end
92
+
93
+ size = self.iterate_for_size(entries)
94
+
95
+ case format
96
+ when :bytes
97
+ size
98
+ when :kb
99
+ size/1000.0
100
+ when :mb
101
+ size/1000.0/1000.0
102
+ when :gb
103
+ size/1000.0/1000.0/1000.0
104
+ else
105
+ # Not expected to reach this.
106
+ size
107
+ end
108
+ end
109
+
110
+ private
111
+
112
+ def self.skip_file?(file, task, debug)
113
+ skip = false
114
+ if File.directory?(file)
115
+ # Skip directories
116
+ skip = true
117
+ elsif !Pathname.new(file).exist?
118
+ # Skip broken symlinks
119
+ skip = true
120
+ elsif !File.exist?(file)
121
+ # Skip files that don't exist
122
+ skip = true
123
+ else
124
+ case File.ftype(file)
125
+ when 'fifo'
126
+ RunLoop.log_warn("#{task} IS SKIPPING FIFO #{file}") if debug
127
+ skip = true
128
+ when 'socket'
129
+ RunLoop.log_warn("#{task} IS SKIPPING SOCKET #{file}") if debug
130
+ skip = true
131
+ when 'characterSpecial'
132
+ RunLoop.log_warn("#{task} IS SKIPPING CHAR SPECIAL #{file}") if debug
133
+ skip = true
134
+ when 'blockSpecial'
135
+ skip = true
136
+ RunLoop.log_warn("#{task} SKIPPING BLOCK SPECIAL #{file}") if debug
137
+ else
138
+
139
+ end
140
+ end
141
+ skip
142
+ end
143
+
144
+ def self.iterate_for_size(entries)
145
+ debug = RunLoop::Environment.debug?
146
+ size = 0
147
+ entries.each do |file|
148
+ unless self.skip_file?(file, "SIZE", debug)
149
+ size = size + File.size(file)
150
+ end
151
+ end
152
+ size
153
+ end
67
154
  end
68
155
  end
@@ -0,0 +1,83 @@
1
+ # A module for managing the ~/.run-loop directory.
2
+ module RunLoop::DotDir
3
+
4
+ def self.directory
5
+ home = RunLoop::Environment.user_home_directory
6
+ dir = File.join(home, ".run-loop")
7
+ if !File.exist?(dir)
8
+ FileUtils.mkdir_p(dir)
9
+ end
10
+ dir
11
+ end
12
+
13
+ def self.make_results_dir
14
+ if RunLoop::Environment.xtc?
15
+ next_results_dir = Dir.mktmpdir("run_loop")
16
+ else
17
+ results_dir = File.join(self.directory, 'results')
18
+ next_results_dir = self.next_timestamped_dirname(results_dir)
19
+ FileUtils.mkdir_p(next_results_dir)
20
+ end
21
+
22
+ next_results_dir
23
+ end
24
+
25
+ def self.rotate_result_directories
26
+ return :xtc if RunLoop::Environment.xtc?
27
+
28
+ start = Time.now
29
+
30
+ glob = "#{self.directory}/results/*"
31
+
32
+ RunLoop.log_debug("Searching for run-loop results with glob: #{glob}")
33
+
34
+ directories = Dir.glob(glob).select do |path|
35
+ File.directory?(path)
36
+ end
37
+
38
+ oldest_first = directories.sort_by { |f| File.mtime(f) }
39
+
40
+ RunLoop.log_debug("Found #{oldest_first.count} previous run-loop results")
41
+ oldest_first.pop(5)
42
+
43
+ RunLoop.log_debug("Will delete #{oldest_first.count} previous run-loop results")
44
+
45
+ oldest_first.each do |path|
46
+ FileUtils.rm_rf(path)
47
+ end
48
+
49
+ elapsed = Time.now - start
50
+
51
+ RunLoop.log_debug("Deleted #{oldest_first.count} previous results in #{elapsed} seconds")
52
+ rescue StandardError => e
53
+ RunLoop.log_error("While rotating previous results, encounterd: #{e}")
54
+ end
55
+
56
+ private
57
+
58
+ def self.timestamped_dirname(plus_seconds = 0)
59
+ (Time.now + plus_seconds).strftime("%Y-%m-%d_%H-%M-%S")
60
+ end
61
+
62
+ def self.next_timestamped_dirname(base_dir)
63
+ dir = File.join(base_dir, self.timestamped_dirname)
64
+ return dir if !File.exist?(dir)
65
+
66
+ # Rather than wait, just increment the second. Per-second accuracy
67
+ # is not important; uniqueness is.
68
+ counter = 0
69
+ loop do
70
+ break if !File.exist?(dir)
71
+ break if counter == 4
72
+ counter = counter + 1
73
+ dir = File.join(base_dir, self.timestamped_dirname(counter))
74
+ end
75
+
76
+ # If all else fails, just return a unique UUID
77
+ if File.exist?(dir)
78
+ dir = File.join(base_dir, SecureRandom.uuid)
79
+ end
80
+ dir
81
+ end
82
+ end
83
+
@@ -1,6 +1,18 @@
1
1
  module RunLoop
2
2
  class Environment
3
3
 
4
+ # Returns the user home directory
5
+ def self.user_home_directory
6
+ if self.xtc?
7
+ home = File.join("./", "tmp", "home")
8
+ FileUtils.mkdir_p(home)
9
+ home
10
+ else
11
+ require 'etc'
12
+ Etc.getpwuid.dir
13
+ end
14
+ end
15
+
4
16
  # Returns true if debugging is enabled.
5
17
  def self.debug?
6
18
  ENV['DEBUG'] == '1'
@@ -51,13 +63,13 @@ module RunLoop
51
63
  if !value || value == ''
52
64
  nil
53
65
  else
54
- value
66
+ File.expand_path(value)
55
67
  end
56
68
  end
57
69
 
58
70
  # Returns the value of DEVELOPER_DIR
59
71
  #
60
- # @note Never call this directly. Always create an XCTool instance
72
+ # @note Never call this directly. Always create an Xcode instance
61
73
  # and allow it to derive the path to the Xcode toolchain.
62
74
  def self.developer_dir
63
75
  value = ENV['DEVELOPER_DIR']
@@ -96,13 +108,20 @@ module RunLoop
96
108
  end
97
109
  end
98
110
 
99
- def self.with_debugging(&block)
100
- original_value = ENV['DEBUG']
101
- ENV['DEBUG'] = '1'
102
- begin
111
+ # !@visibility private
112
+ def self.with_debugging(debug, &block)
113
+ if debug
114
+ original_value = ENV['DEBUG']
115
+
116
+ begin
117
+ ENV['DEBUG'] = '1'
118
+ block.call
119
+ ensure
120
+ ENV['DEBUG'] = original_value
121
+ end
122
+
123
+ else
103
124
  block.call
104
- ensure
105
- ENV['DEBUG'] = original_value
106
125
  end
107
126
  end
108
127
  end
@@ -7,6 +7,73 @@ module RunLoop
7
7
 
8
8
  include RunLoop::Regex
9
9
 
10
+ # @!visibility private
11
+ #
12
+ # Rotates xrtmp__ directories in /Library/Caches/com.apple.dt.instruments
13
+ # keeping the last 5. On CI systems these can be hundreds of gigabytes.
14
+ def self.rotate_cache_directories
15
+
16
+ # Never run on the XTC
17
+ return :xtc if RunLoop::Environment.xtc?
18
+
19
+ cache = self.library_cache_dir
20
+
21
+ # If the directory does not exist, do nothing.
22
+ return :no_cache if !cache || !File.exist?(cache)
23
+
24
+ start = Time.now
25
+
26
+ glob = "#{cache}/xrtmp__*"
27
+
28
+ RunLoop.log_debug("Searching for instruments caches with glob: #{glob}")
29
+
30
+ directories = Dir.glob(glob).select do |path|
31
+ File.directory?(path)
32
+ end
33
+
34
+ log_progress = false
35
+ if directories.count > 25
36
+ RunLoop.log_info2("Found #{directories.count} instruments caches: ~#{20 * directories.count} Mb")
37
+ RunLoop.log_info2("Deleting them could take a long time.")
38
+ RunLoop.log_info2("This delay will only happen once!")
39
+ RunLoop.log_info2("Please be patient and allow the directories to be deleted")
40
+ log_progress = true
41
+ else
42
+ RunLoop.log_debug("Found #{directories.count} instruments caches")
43
+ end
44
+
45
+ if log_progress
46
+ RunLoop.log_info2("Sorting instruments caches by modification time...")
47
+ end
48
+
49
+ oldest_first = directories.sort_by { |f| File.mtime(f) }
50
+
51
+ oldest_first.pop(5)
52
+
53
+ if log_progress
54
+ RunLoop.log_info2("Will delete #{oldest_first.count} instruments caches...")
55
+ else
56
+ RunLoop.log_debug("Will delete #{oldest_first.count} instruments caches")
57
+ end
58
+
59
+ oldest_first.each do |path|
60
+ FileUtils.rm_rf(path)
61
+ if log_progress
62
+ printf "."
63
+ end
64
+ end
65
+
66
+ elapsed = Time.now - start
67
+
68
+ if log_progress
69
+ puts ""
70
+ RunLoop.log_info2("Deleted #{oldest_first.count} instruments caches in #{elapsed} seconds")
71
+ else
72
+ RunLoop.log_debug("Deleted #{oldest_first.count} instruments caches in #{elapsed} seconds")
73
+ end
74
+ true
75
+ end
76
+
10
77
  attr_reader :xcode
11
78
 
12
79
  def pbuddy
@@ -192,7 +259,8 @@ Please update your sources to pass an instance of RunLoop::Xcode))
192
259
  fetch_devices[:out].chomp.split("\n").map do |line|
193
260
  stripped = line.strip
194
261
  if line_is_simulator?(stripped) &&
195
- !line_is_simulator_paired_with_watch?(stripped)
262
+ !line_is_simulator_paired_with_watch?(stripped) &&
263
+ !line_is_apple_tv?(stripped)
196
264
 
197
265
  version = stripped[VERSION_REGEX, 0]
198
266
 
@@ -402,6 +470,11 @@ Please update your sources to pass an instance of RunLoop::Xcode))
402
470
  line[CORE_SIMULATOR_UDID_REGEX, 0] && line[/Apple Watch/, 0]
403
471
  end
404
472
 
473
+ # @!visibility private
474
+ def line_is_apple_tv?(line)
475
+ line[/Apple TV/, 0]
476
+ end
477
+
405
478
  # @!visibility private
406
479
  def path_to_instruments_app_plist
407
480
  @path_to_instruments_app_plist ||=
@@ -412,5 +485,20 @@ Please update your sources to pass an instance of RunLoop::Xcode))
412
485
  'Contents',
413
486
  'Info.plist'))
414
487
  end
488
+
489
+ # @!visibility private
490
+ #
491
+ # Instruments caches files in this directory and it can become quite large
492
+ # over time; particularly on CI system.
493
+ def self.library_cache_dir
494
+ path = "/Library/Caches/com.apple.dt.instruments"
495
+
496
+ if File.exist?(path)
497
+ path
498
+ else
499
+ nil
500
+ end
501
+ end
415
502
  end
416
503
  end
504
+
@@ -59,6 +59,12 @@ module RunLoop
59
59
  end
60
60
  end
61
61
 
62
+ # .log_info is already taken by the XTC logger. (>_O)
63
+ # green
64
+ def self.log_info2(msg)
65
+ puts self.green(" INFO: #{msg}") if msg
66
+ end
67
+
62
68
  # red
63
69
  def self.log_error(msg)
64
70
  puts self.red("ERROR: #{msg}") if msg
@@ -101,4 +107,9 @@ module RunLoop
101
107
  def self.cyan(string)
102
108
  colorize(string, 36)
103
109
  end
110
+
111
+ # @!visibility private
112
+ def self.green(string)
113
+ colorize(string, 32)
114
+ end
104
115
  end
@@ -55,32 +55,22 @@ module RunLoop
55
55
  def kill_process
56
56
  return true unless process_alive?
57
57
 
58
- debug_logging = RunLoop::Environment.debug?
59
58
  begin
60
- if debug_logging
61
- puts "Sending '#{kill_signal}' to #{display_name} process '#{pid}'"
62
- end
59
+ RunLoop.log_debug("Sending '#{kill_signal}' to #{display_name} process '#{pid}'")
63
60
  Process.kill(kill_signal, pid.to_i)
64
61
  # Don't wait.
65
62
  # We might not own this process and a WNOHANG would be a nop.
66
63
  # Process.wait(pid, Process::WNOHANG)
67
64
  rescue Errno::ESRCH
68
- if debug_logging
69
- puts "Process with pid '#{pid}' does not exist; nothing to do."
70
- end
65
+ RunLoop.log_debug("Process with pid '#{pid}' does not exist; nothing to do.")
71
66
  # Return early; there is no need to wait if the process does not exist.
72
67
  return true
73
68
  rescue Errno::EPERM
74
- if debug_logging
75
- puts "Cannot kill process '#{pid}' with '#{kill_signal}'; not a child of this process"
76
- end
69
+ RunLoop.log_debug("Cannot kill process '#{pid}' with '#{kill_signal}'; not a child of this process")
77
70
  rescue SignalException => e
78
71
  raise e.message
79
72
  end
80
-
81
- if debug_logging
82
- puts "Waiting for #{display_name} '#{pid}' to terminate"
83
- end
73
+ RunLoop.log_debug("Waiting for #{display_name} with pid '#{pid}' to terminate")
84
74
  wait_for_process_to_terminate
85
75
  end
86
76
 
@@ -127,9 +117,7 @@ module RunLoop
127
117
  sleep delay
128
118
  end
129
119
 
130
- if RunLoop::Environment.debug?
131
- puts "Waited for #{Time.now - now} seconds for #{display_name} with '#{pid}' to terminate"
132
- end
120
+ RunLoop.log_debug("Waited for #{Time.now - now} seconds for #{display_name} with pid '#{pid}' to terminate")
133
121
 
134
122
  if @options[:raise_on_no_terminate] and !has_terminated
135
123
  raise "Waited #{options[:timeout]} seconds for #{display_name} (#{ps_details}) to terminate"
@@ -52,6 +52,10 @@ module RunLoop
52
52
  xcode.developer_dir
53
53
  end
54
54
 
55
+ def xcrun
56
+ @xcrun ||= RunLoop::Xcrun.new
57
+ end
58
+
55
59
  # Return an instance of PlistBuddy.
56
60
  # @return [RunLoop::PlistBuddy] The plist buddy instance that is used internally.
57
61
  def pbuddy
@@ -1137,55 +1141,61 @@ module RunLoop
1137
1141
  # base sdk version.
1138
1142
  # @see #simctl_list
1139
1143
  def simctl_list_devices
1140
- args = 'simctl list devices'.split(' ')
1141
- Open3.popen3('xcrun', *args) do |_, stdout, stderr, _|
1142
- out = stdout.read.strip
1143
- err = stderr.read.strip
1144
- if ENV['DEBUG_UNIX_CALLS'] == '1'
1145
- cmd = "xcrun #{args.join(' ')}"
1146
- puts "#{cmd} => stdout: '#{out}' | stderr: '#{err}'"
1147
- end
1144
+ args = ['simctl', 'list', 'devices']
1145
+ hash = xcrun.exec(args)
1148
1146
 
1149
- current_sdk = nil
1150
- res = {}
1151
- out.split("\n").each do |line|
1147
+ current_sdk = nil
1148
+ simulators = {}
1152
1149
 
1153
- possible_sdk = line[VERSION_REGEX,0]
1154
- if possible_sdk
1155
- current_sdk = possible_sdk
1156
- res[current_sdk] = []
1157
- next
1158
- end
1150
+ out = hash[:out]
1159
1151
 
1160
- unavailable_sdk = line[/Unavailable/, 0]
1161
- if unavailable_sdk
1162
- current_sdk = nil
1163
- next
1164
- end
1152
+ out.split("\n").each do |line|
1165
1153
 
1166
- watch_os = line[/watchOS/, 0]
1167
- if watch_os
1168
- current_sdk = nil
1169
- next
1170
- end
1154
+ possible_sdk = line[VERSION_REGEX,0]
1155
+ if possible_sdk
1156
+ current_sdk = possible_sdk
1157
+ simulators[current_sdk] = []
1158
+ next
1159
+ end
1171
1160
 
1172
- watch = line[/Apple Watch/, 0]
1173
- if watch
1174
- current_sdk = nil
1175
- next
1176
- end
1161
+ unavailable_sdk = line[/Unavailable/, 0]
1162
+ if unavailable_sdk
1163
+ current_sdk = nil
1164
+ next
1165
+ end
1177
1166
 
1178
- if current_sdk
1179
- unless line[/unavailable/,0]
1180
- name = line.split('(').first.strip
1181
- udid = line[CORE_SIMULATOR_UDID_REGEX,0]
1182
- state = line[/(Booted|Shutdown)/,0]
1183
- res[current_sdk] << {:name => name, :udid => udid, :state => state}
1184
- end
1167
+ watch_os = line[/watchOS/, 0]
1168
+ if watch_os
1169
+ current_sdk = nil
1170
+ next
1171
+ end
1172
+
1173
+ watch = line[/Apple Watch/, 0]
1174
+ if watch
1175
+ current_sdk = nil
1176
+ next
1177
+ end
1178
+
1179
+ tv = line[/Apple TV/, 0]
1180
+ if tv
1181
+ current_sdk = nil
1182
+ next
1183
+ end
1184
+
1185
+ if current_sdk
1186
+ unless line[/unavailable/,0]
1187
+ name = line.split('(').first.strip
1188
+ udid = line[CORE_SIMULATOR_UDID_REGEX,0]
1189
+ state = line[/(Booted|Shutdown)/,0]
1190
+ simulators[current_sdk] << {
1191
+ :name => name,
1192
+ :udid => udid,
1193
+ :state => state
1194
+ }
1185
1195
  end
1186
1196
  end
1187
- res
1188
1197
  end
1198
+ simulators
1189
1199
  end
1190
1200
 
1191
1201
  # @!visibility private
@@ -1193,48 +1203,75 @@ module RunLoop
1193
1203
  #
1194
1204
  # @example
1195
1205
  # RunLoop::SimControl.new.simctl_list :runtimes
1196
- # {
1197
- # "7.0" => {
1198
- # :sdk => "7.0.3",
1199
- # :runtime => "com.apple.CoreSimulator.SimRuntime.iOS-7-0"
1200
- # },
1201
- # "7.1" => {
1202
- # :sdk => "7.1",
1203
- # :runtime => "com.apple.CoreSimulator.SimRuntime.iOS-7-1"
1206
+ # :iOS => {
1207
+ # <Version 8.1> => {
1208
+ # :name => "iOS",
1209
+ # :runtime => "com.apple.CoreSimulator.SimRuntime.iOS-8-1",
1210
+ # :complete => "iOS 8.1 (8.1 - 12B411) (com.apple.CoreSimulator.SimRuntime.iOS-8-1)"
1204
1211
  # },
1205
- # "8.0" => {
1206
- # :sdk => "8.0",
1207
- # :runtime => "com.apple.CoreSimulator.SimRuntime.iOS-8-0"
1208
- # }
1209
- # }
1212
+ # ...
1213
+ # },
1214
+ #
1215
+ # :tvOS => {
1216
+ # <Version 9.0> => {
1217
+ # :name => "tvOS",
1218
+ # :runtime => "com.apple.CoreSimulator.SimRuntime.tvOS-9-0",
1219
+ # :complete => "tvOS 9.0 (9.0 - 13T5365h) (com.apple.CoreSimulator.SimRuntime.tvOS-9-0)"
1220
+ # },
1221
+ # ...
1222
+ # },
1223
+ #
1224
+ # :watchOS => {
1225
+ # <Version 2.0> => {
1226
+ # :name => "watchOS",
1227
+ # :runtime => "com.apple.CoreSimulator.SimRuntime.watchOS-2-0",
1228
+ # :complete => "watchOS 2.0 (2.0 - 13S343) (com.apple.CoreSimulator.SimRuntime.watchOS-2-0)"
1229
+ # },
1230
+ # ...
1231
+ # }
1232
+ #
1210
1233
  # @see #simctl_list
1211
1234
  def simctl_list_runtimes
1212
- # The 'com.apple.CoreSimulator.SimRuntime.iOS-7-0' is the runtime-id,
1213
- # which can be used to create devices.
1214
- args = 'simctl list runtimes'.split(' ')
1215
- Open3.popen3('xcrun', *args) do |_, stdout, stderr, _|
1216
- out = stdout.read.strip
1217
- err = stderr.read.strip
1218
- if ENV['DEBUG_UNIX_CALLS'] == '1'
1219
- cmd = "xcrun #{args.join(' ')}"
1220
- puts "#{cmd} => stdout: '#{out}' | stderr: '#{err}'"
1221
- end
1222
- # Ex.
1223
- # == Runtimes ==
1224
- # iOS 7.0 (7.0.3 - 11B507) (com.apple.CoreSimulator.SimRuntime.iOS-7-0)
1225
- # iOS 7.1 (7.1 - 11D167) (com.apple.CoreSimulator.SimRuntime.iOS-7-1)
1226
- # iOS 8.0 (8.0 - 12A4331d) (com.apple.CoreSimulator.SimRuntime.iOS-8-0)
1227
- lines = out.split("\n").delete_if { |line| not line =~ /com.apple.CoreSimulator.SimRuntime/ }
1228
- res = {}
1229
- lines.each do |line|
1230
- key = line[/iOS (\d.\d)/,1]
1231
- sdk_version = line[/(\d.\d)(.\d)?\s-/, 0].tr(' -','')
1232
- runtime = line[/com.apple.CoreSimulator.SimRuntime.iOS-\d-\d/,0]
1233
- value = {:sdk => sdk_version, :runtime => runtime}
1234
- res[key] = value
1235
+ args = ['simctl', 'list', 'runtimes']
1236
+ hash = xcrun.exec(args)
1237
+
1238
+ # Ex.
1239
+ # == Runtimes ==
1240
+ # iOS 7.0 (7.0.3 - 11B507) (com.apple.CoreSimulator.SimRuntime.iOS-7-0)
1241
+ # iOS 7.1 (7.1 - 11D167) (com.apple.CoreSimulator.SimRuntime.iOS-7-1)
1242
+ # iOS 8.0 (8.0 - 12A4331d) (com.apple.CoreSimulator.SimRuntime.iOS-8-0)
1243
+
1244
+ out = hash[:out]
1245
+
1246
+ runtimes = {}
1247
+
1248
+ out.split("\n").each do |line|
1249
+ next if line[/unavailable/, 0]
1250
+ next if !line[/com.apple.CoreSimulator.SimRuntime/,0]
1251
+
1252
+ tokens = line.split(' ')
1253
+
1254
+ name = tokens.first
1255
+
1256
+ key = name.to_sym
1257
+
1258
+ unless runtimes[key]
1259
+ runtimes[key] = {}
1235
1260
  end
1236
- res
1261
+
1262
+ version_str = tokens[1]
1263
+ version = RunLoop::Version.new(version_str)
1264
+
1265
+ runtime = line[/com.apple.CoreSimulator.SimRuntime.*/, 0].chomp(')')
1266
+
1267
+ runtimes[key][version] =
1268
+ {
1269
+ :name => name,
1270
+ :runtime => runtime,
1271
+ :complete => line
1272
+ }
1237
1273
  end
1274
+ runtimes
1238
1275
  end
1239
1276
  end
1240
1277
  end