run_loop 2.1.6 → 2.1.7

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.
@@ -95,21 +95,30 @@ but binary does not exist at that path.
95
95
  "-t", runner.tester,
96
96
  "-d", device.udid]
97
97
 
98
+ code_sign_identity = RunLoop::Environment.code_sign_identity
99
+ if !code_sign_identity
100
+ code_sign_identity = "iPhone Developer"
101
+ end
102
+
98
103
  if device.physical_device?
99
104
  args << "-c"
100
- args << RunLoop::Environment.codesign_identity
105
+ args << code_sign_identity
101
106
  end
102
107
 
103
108
  log_file = IOSDeviceManager.log_file
104
109
  FileUtils.rm_rf(log_file)
105
110
  FileUtils.touch(log_file)
106
111
 
112
+ env = {
113
+ "CLOBBER" => "1"
114
+ }
115
+
107
116
  options = {:out => log_file, :err => log_file}
108
117
  RunLoop.log_unix_cmd("#{cmd} #{args.join(" ")} >& #{log_file}")
109
118
 
110
119
  # Gotta keep the ios_device_manager process alive or the connection
111
120
  # to testmanagerd will fail.
112
- pid = Process.spawn(cmd, *args, options)
121
+ pid = Process.spawn(env, cmd, *args, options)
113
122
  Process.detach(pid)
114
123
 
115
124
  if device.simulator?
@@ -61,7 +61,8 @@ module RunLoop
61
61
  # @!visibility private
62
62
  def xcodebuild
63
63
  env = {
64
- "COMMAND_LINE_BUILD" => "1"
64
+ "COMMAND_LINE_BUILD" => "1",
65
+ "CLOBBER" => "1"
65
66
  }
66
67
 
67
68
  args = [
@@ -72,7 +73,11 @@ module RunLoop
72
73
  "-config", "Debug",
73
74
  "-destination",
74
75
  "id=#{device.udid}",
75
- "clean",
76
+ "CLANG_ENABLE_CODE_COVERAGE=YES",
77
+ "GCC_GENERATE_TEST_COVERAGE_FILES=NO",
78
+ "GCC_INSTRUMENT_PROGRAM_FLOW_ARCS=NO",
79
+ # Scheme setting.
80
+ "-enableCodeCoverage", "YES",
76
81
  "test"
77
82
  ]
78
83
 
@@ -163,8 +163,8 @@ module RunLoop
163
163
  end
164
164
 
165
165
  # Returns the value of CODESIGN_IDENTITY
166
- def self.codesign_identity
167
- value = ENV["CODESIGN_IDENTITY"]
166
+ def self.code_sign_identity
167
+ value = ENV["CODE_SIGN_IDENTITY"]
168
168
  if !value || value == ""
169
169
  nil
170
170
  else
@@ -158,10 +158,13 @@ module RunLoop
158
158
  # @todo Should this raise errors?
159
159
  # @todo Is this jruby compatible?
160
160
  def spawn(automation_template, options, log_file)
161
+ env = {
162
+ "CLOBBER" => "1"
163
+ }
161
164
  splat_args = spawn_arguments(automation_template, options)
162
165
  logger = options[:logger]
163
166
  RunLoop::Logging.log_debug(logger, "xcrun #{splat_args.join(' ')} >& #{log_file}")
164
- pid = Process.spawn('xcrun', *splat_args, {:out => log_file, :err => log_file})
167
+ pid = Process.spawn(env, 'xcrun', *splat_args, {:out => log_file, :err => log_file})
165
168
  Process.detach(pid)
166
169
  pid.to_i
167
170
  end
@@ -5,26 +5,14 @@ module RunLoop
5
5
  class Otool
6
6
 
7
7
  # @!visibility private
8
- attr_reader :path
9
-
10
- # @!visibility private
11
- def initialize(path)
12
- @path = path
13
-
14
- if !Otool.valid_path?(path)
15
- raise ArgumentError,
16
- %Q{File:
17
-
18
- #{path}
19
-
20
- must exist and not be a directory.
21
- }
22
- end
8
+ # @param [RunLoop::Xcode] xcode An instance of Xcode
9
+ def initialize(xcode)
10
+ @xcode = xcode
23
11
  end
24
12
 
25
13
  # @!visibility private
26
14
  def to_s
27
- "#<OTOOL: #{path}>"
15
+ "#<OTOOL: Xcode #{xcode.version.to_s}>"
28
16
  end
29
17
 
30
18
  # @!visibility private
@@ -33,15 +21,19 @@ must exist and not be a directory.
33
21
  end
34
22
 
35
23
  # @!visibility private
36
- def executable?
37
- !arch_info[/is not an object file/, 0]
24
+ def executable?(path)
25
+ expect_valid_path!(path)
26
+ !arch_info(path)[/is not an object file/, 0]
38
27
  end
39
28
 
40
29
  private
41
30
 
42
31
  # @!visibility private
43
- def arch_info
44
- args = ["otool", "-hv", "-arch", "all", path]
32
+ attr_reader :xcode, :command_name
33
+
34
+ # @!visibility private
35
+ def arch_info(path)
36
+ args = [command_name, "-hv", "-arch", "all", path]
45
37
  opts = { :log_cmd => false }
46
38
 
47
39
  hash = xcrun.run_command_in_context(args, opts)
@@ -60,17 +52,41 @@ exited #{hash[:exit_status]} with the following output:
60
52
  }
61
53
  end
62
54
 
63
- @arch_info = hash[:out]
55
+ hash[:out]
64
56
  end
65
57
 
66
58
  # @!visibility private
67
- def self.valid_path?(path)
68
- File.exist?(path) && !File.directory?(path)
59
+ def expect_valid_path!(path)
60
+ return true if File.exist?(path) && !File.directory?(path)
61
+ raise ArgumentError, %Q[
62
+ File:
63
+
64
+ #{path}
65
+
66
+ must exist and not be a directory.
67
+
68
+ ]
69
69
  end
70
70
 
71
71
  # @!visibility private
72
72
  def xcrun
73
- RunLoop::Xcrun.new
73
+ @xcrun ||= RunLoop::Xcrun.new
74
+ end
75
+
76
+ # @!visibility private
77
+ def xcode
78
+ @xcode
79
+ end
80
+
81
+ # @!visibility private
82
+ def command_name
83
+ @command_name ||= begin
84
+ if xcode.version_gte_8?
85
+ "otool-classic"
86
+ else
87
+ "otool"
88
+ end
89
+ end
74
90
  end
75
91
  end
76
92
  end
@@ -0,0 +1,89 @@
1
+
2
+ module RunLoop
3
+ module PhysicalDevice
4
+
5
+ require "run_loop/physical_device/life_cycle"
6
+ class IOSDeviceManager < RunLoop::PhysicalDevice::LifeCycle
7
+
8
+ # Is the tool installed?
9
+ def self.tool_is_installed?
10
+ File.exist?(IOSDeviceManager.executable_path)
11
+ end
12
+
13
+ # Path to tool.
14
+ def self.executable_path
15
+ RunLoop::DeviceAgent::IOSDeviceManager.ios_device_manager
16
+ end
17
+
18
+ def initialize(device)
19
+ super(device)
20
+
21
+ # Expands the Frameworks.zip if necessary.
22
+ RunLoop::DeviceAgent::Frameworks.instance.install
23
+ end
24
+
25
+ def app_installed?(bundle_id)
26
+ args = [
27
+ IOSDeviceManager.executable_path,
28
+ "is_installed",
29
+ "-d", device.udid,
30
+ "-b", bundle_id
31
+ ]
32
+
33
+ options = { :log_cmd => true }
34
+ hash = run_shell_command(args, options)
35
+
36
+ # TODO: error reporting
37
+ hash[:exit_status] == 0
38
+ end
39
+
40
+ def install_app(app_or_ipa)
41
+ app = app_or_ipa
42
+ if is_ipa?(app)
43
+ app = app_or_ipa.app
44
+ end
45
+
46
+ code_sign_identity = RunLoop::Environment.code_sign_identity
47
+ if !code_sign_identity
48
+ code_sign_identity = "iPhone Developer"
49
+ end
50
+
51
+ args = [
52
+ IOSDeviceManager.executable_path,
53
+ "install",
54
+ "-d", device.udid,
55
+ "-a", app.path,
56
+ "-c", code_sign_identity
57
+ ]
58
+
59
+ options = { :log_cmd => true }
60
+ hash = run_shell_command(args, options)
61
+
62
+ # TODO: error reporting
63
+ if hash[:exit_status] == 0
64
+ true
65
+ else
66
+ puts hash[:out]
67
+ false
68
+ end
69
+ end
70
+
71
+ def uninstall_app(bundle_id)
72
+ return true if !app_installed?(bundle_id)
73
+
74
+ args = [
75
+ IOSDeviceManager.executable_path,
76
+ "uninstall",
77
+ "-d", device.udid,
78
+ "-b", bundle_id
79
+ ]
80
+
81
+ options = { :log_cmd => true }
82
+ hash = run_shell_command(args, options)
83
+
84
+ # TODO: error reporting
85
+ hash[:exit_status] == 0
86
+ end
87
+ end
88
+ end
89
+ end
@@ -13,7 +13,7 @@ module RunLoop
13
13
  DEVICE_UDID_REGEX = /[a-f0-9]{40}/.freeze
14
14
 
15
15
  # @!visibility private
16
- VERSION_REGEX = /(\d\.\d(\.\d)?)/.freeze
16
+ VERSION_REGEX = /(\d+\.\d+(\.\d+)?)/.freeze
17
17
 
18
18
  end
19
19
  end
@@ -26,6 +26,16 @@ module RunLoop
26
26
  # Raised when shell command times out.
27
27
  class TimeoutError < RuntimeError; end
28
28
 
29
+ def self.run_shell_command(args, options={})
30
+ shell = Class.new do
31
+ include RunLoop::Shell
32
+ def to_s; "#<Anonymous Shell>"; end
33
+ def inspect; to_s; end
34
+ end.new
35
+
36
+ shell.run_shell_command(args, options)
37
+ end
38
+
29
39
  def run_shell_command(args, options={})
30
40
 
31
41
  merged_options = DEFAULT_OPTIONS.merge(options)
@@ -85,19 +95,43 @@ executing this command:
85
95
  }
86
96
  end
87
97
 
98
+ now = Time.now
99
+
88
100
  if hash[:exit_status].nil?
89
- elapsed = "%0.2f" % (Time.now - start_time)
90
- raise TimeoutError,
91
- %Q{Timed out after #{elapsed} seconds executing
101
+ elapsed = "%0.2f" % (now - start_time)
102
+
103
+ if timeout_exceeded?(start_time, timeout)
104
+ raise TimeoutError,
105
+ %Q[
106
+ Timed out after #{elapsed} seconds executing
92
107
 
93
108
  #{cmd}
94
109
 
95
110
  with a timeout of #{timeout}
96
- }
111
+ ]
112
+ else
113
+ raise Error,
114
+ %Q[
115
+ There was an error executing:
116
+
117
+ #{cmd}
118
+
119
+ The command generated this output:
120
+
121
+ #{hash[:out]}
122
+ ]
123
+
124
+ end
97
125
  end
98
126
 
99
127
  hash
100
128
  end
129
+
130
+ private
131
+
132
+ def timeout_exceeded?(start_time, timeout)
133
+ Time.now > start_time + timeout
134
+ end
101
135
  end
102
136
  end
103
137
 
@@ -1136,7 +1136,9 @@ module RunLoop
1136
1136
  # base sdk version.
1137
1137
  # @see #simctl_list
1138
1138
  def simctl_list_devices
1139
- args = ['simctl', 'list', 'devices']
1139
+ # Ensure correct CoreSimulator service is installed.
1140
+ RunLoop::Simctl.new
1141
+ args = ["simctl", 'list', 'devices']
1140
1142
  hash = xcrun.run_command_in_context(args)
1141
1143
 
1142
1144
  current_sdk = nil
@@ -1219,7 +1221,9 @@ module RunLoop
1219
1221
  #
1220
1222
  # @see #simctl_list
1221
1223
  def simctl_list_runtimes
1222
- args = ['simctl', 'list', 'runtimes']
1224
+ # Ensure correct CoreSimulator service is installed.
1225
+ RunLoop::Simctl.new
1226
+ args = ["simctl", 'list', 'runtimes']
1223
1227
  hash = xcrun.run_command_in_context(args)
1224
1228
 
1225
1229
  # Ex.
@@ -15,10 +15,17 @@ module RunLoop
15
15
  # @!visibility private
16
16
  SIMCTL_PLIST_DIR = lambda {
17
17
  dirname = File.dirname(__FILE__)
18
- joined = File.join(dirname, '..', '..', 'plists', 'simctl')
18
+ joined = File.join(dirname, "..", "..", "plists", "simctl")
19
19
  File.expand_path(joined)
20
20
  }.call
21
21
 
22
+ # @!visibility private
23
+ SIM_STATES = {
24
+ "Shutdown" => 1,
25
+ "Shutting Down" => 2,
26
+ "Booted" => 3,
27
+ }.freeze
28
+
22
29
  # @!visibility private
23
30
  def self.uia_automation_plist
24
31
  File.join(SIMCTL_PLIST_DIR, 'com.apple.UIAutomation.plist')
@@ -29,6 +36,28 @@ module RunLoop
29
36
  File.join(SIMCTL_PLIST_DIR, 'com.apple.UIAutomationPlugIn.plist')
30
37
  end
31
38
 
39
+ # @!visibility private
40
+ def self.ensure_valid_core_simulator_service
41
+ require "run_loop/shell"
42
+ args = ["xcrun", "simctl", "help"]
43
+
44
+ max_tries = 3
45
+ 3.times do |try|
46
+ hash = {}
47
+ begin
48
+ hash = Shell.run_shell_command(args)
49
+ if hash[:exit_status] != 0
50
+ RunLoop.log_debug("Invalid CoreSimulator service for active Xcode: try #{try + 1} of #{max_tries}")
51
+ else
52
+ return true
53
+ end
54
+ rescue RunLoop::Shell::Error => _
55
+ RunLoop.log_debug("Invalid CoreSimulator service for active Xcode, retrying #{try + 1} of #{max_tries}")
56
+ end
57
+ end
58
+ false
59
+ end
60
+
32
61
  # @!visibility private
33
62
  attr_reader :device
34
63
 
@@ -37,6 +66,7 @@ module RunLoop
37
66
  @ios_devices = []
38
67
  @tvos_devices = []
39
68
  @watchos_devices = []
69
+ Simctl.ensure_valid_core_simulator_service
40
70
  end
41
71
 
42
72
  # @!visibility private
@@ -76,7 +106,7 @@ module RunLoop
76
106
  def app_container(device, bundle_id)
77
107
  return nil if !xcode.version_gte_7?
78
108
  cmd = ["simctl", "get_app_container", device.udid, bundle_id]
79
- hash = execute(cmd, DEFAULTS)
109
+ hash = shell_out_with_xcrun(cmd, DEFAULTS)
80
110
 
81
111
  exit_status = hash[:exit_status]
82
112
  if exit_status != 0
@@ -86,6 +116,253 @@ module RunLoop
86
116
  end
87
117
  end
88
118
 
119
+ # @!visibility private
120
+ def simulator_state_as_int(device)
121
+ plist = device.simulator_device_plist
122
+ pbuddy.plist_read("state", plist).to_i
123
+ end
124
+
125
+ # @!visibility private
126
+ def simulator_state_as_string(device)
127
+ string_for_sim_state(simulator_state_as_int(device))
128
+ end
129
+
130
+ # @!visibility private
131
+ def shutdown(device)
132
+ if simulator_state_as_int(device) == SIM_STATES["Shutdown"]
133
+ RunLoop.log_debug("Simulator is already shutdown")
134
+ true
135
+ else
136
+ cmd = ["simctl", "shutdown", device.udid]
137
+ hash = shell_out_with_xcrun(cmd, DEFAULTS)
138
+
139
+ exit_status = hash[:exit_status]
140
+ if exit_status != 0
141
+
142
+ if simulator_state_as_int(device) == SIM_STATES["Shutdown"]
143
+ RunLoop.log_debug("simctl shutdown called when state is 'Shutdown'; ignoring error")
144
+ else
145
+ raise RuntimeError,
146
+ %Q[Could not shutdown the simulator:
147
+
148
+ command: xcrun #{cmd.join(" ")}
149
+ simulator: #{device}
150
+
151
+ #{hash[:out]}
152
+
153
+ This usually means your CoreSimulator processes need to be restarted.
154
+
155
+ You can restart the CoreSimulator processes with this command:
156
+
157
+ $ bundle exec run-loop simctl manage-processes
158
+
159
+ ]
160
+ end
161
+ end
162
+ true
163
+ end
164
+ end
165
+
166
+ # @!visibility private
167
+ #
168
+ # Waiting for anything but 'Shutdown' is not advised. The simulator reports
169
+ # that it is "Booted" long before it is ready to receive commands.
170
+ #
171
+ # Waiting for 'Shutdown' is required for erasing the simulator and launching
172
+ # launching the simulator with iOSDeviceManager.
173
+ def wait_for_shutdown(device, timeout, delay)
174
+ now = Time.now
175
+ poll_until = now + timeout
176
+ in_state = false
177
+
178
+ state = nil
179
+
180
+ while Time.now < poll_until
181
+ state = simulator_state_as_int(device)
182
+ in_state = state == SIM_STATES["Shutdown"]
183
+ break if in_state
184
+ sleep delay if delay != 0
185
+ end
186
+
187
+ elapsed = Time.now - now
188
+ RunLoop.log_debug("Waited for #{elapsed} seconds for device to have state: 'Shutdown'.")
189
+
190
+ unless in_state
191
+ string = string_for_sim_state(state)
192
+ raise "Expected 'Shutdown' state but found '#{string}' after waiting for #{elapsed} seconds."
193
+ end
194
+ in_state
195
+ end
196
+
197
+ # @!visibility private
198
+ # Erases the simulator.
199
+ #
200
+ # @param [RunLoop::Device] device The simulator to erase.
201
+ # @param [Numeric] wait_timeout How long to wait for the simulator to have
202
+ # state "Shutdown"; passed to #wait_for_shutdown.
203
+ # @param [Numeric] wait_delay How long to wait between calls to
204
+ # #simulator_state_as_int while waiting for the simulator have to state "Shutdown";
205
+ # passed to #wait_for_shutdown
206
+ def erase(device, wait_timeout, wait_delay)
207
+ require "run_loop/core_simulator"
208
+ CoreSimulator.quit_simulator
209
+
210
+ shutdown(device)
211
+ wait_for_shutdown(device, wait_timeout, wait_delay)
212
+
213
+ cmd = ["simctl", "erase", device.udid]
214
+ hash = shell_out_with_xcrun(cmd, DEFAULTS)
215
+
216
+ exit_status = hash[:exit_status]
217
+ if exit_status != 0
218
+ raise RuntimeError,
219
+ %Q[Could not erase the simulator:
220
+
221
+ command: xcrun #{cmd.join(" ")}
222
+ simulator: #{device}
223
+
224
+ #{hash[:out]}
225
+
226
+ This usually means your CoreSimulator processes need to be restarted.
227
+
228
+ You can restart the CoreSimulator processes with this command:
229
+
230
+ $ bundle exec run-loop simctl manage-processes
231
+
232
+ ]
233
+ end
234
+ true
235
+ end
236
+
237
+ # @!visibility private
238
+ #
239
+ # Launches the app on on the device.
240
+ #
241
+ # Caller is responsible for the following:
242
+ #
243
+ # 1. Launching the simulator.
244
+ # 2. Installing the application.
245
+ #
246
+ # No checks are made.
247
+ #
248
+ # @param [RunLoop::Device] device The simulator to launch on.
249
+ # @param [RunLoop::App] app The app to launch.
250
+ # @param [Numeric] timeout How long to wait for simctl to complete.
251
+ def launch(device, app, timeout)
252
+ cmd = ["simctl", "launch", device.udid, app.bundle_identifier]
253
+ options = DEFAULTS.dup
254
+ options[:timeout] = timeout
255
+
256
+ hash = shell_out_with_xcrun(cmd, options)
257
+
258
+ exit_status = hash[:exit_status]
259
+ if exit_status != 0
260
+ raise RuntimeError,
261
+ %Q[Could not launch app on simulator:
262
+
263
+ command: xcrun #{cmd.join(" ")}
264
+ simulator: #{device}
265
+ app: #{app}
266
+
267
+ #{hash[:out]}
268
+
269
+ This usually means your CoreSimulator processes need to be restarted.
270
+
271
+ You can restart the CoreSimulator processes with this command:
272
+
273
+ $ bundle exec run-loop simctl manage-processes
274
+
275
+ ]
276
+ end
277
+ true
278
+ end
279
+
280
+ # @!visibility private
281
+ #
282
+ # Launches the app on on the device.
283
+ #
284
+ # Caller is responsible for the following:
285
+ #
286
+ # 1. Launching the simulator.
287
+ # 2. That the application is installed; simctl uninstall will fail if app
288
+ # is installed.
289
+ #
290
+ # No checks are made.
291
+ #
292
+ # @param [RunLoop::Device] device The simulator to launch on.
293
+ # @param [RunLoop::App] app The app to launch.
294
+ # @param [Numeric] timeout How long to wait for simctl to complete.
295
+ def uninstall(device, app, timeout)
296
+ cmd = ["simctl", "uninstall", device.udid, app.bundle_identifier]
297
+ options = DEFAULTS.dup
298
+ options[:timeout] = timeout
299
+
300
+ hash = shell_out_with_xcrun(cmd, options)
301
+
302
+ exit_status = hash[:exit_status]
303
+ if exit_status != 0
304
+ raise RuntimeError,
305
+ %Q[Could not uninstall app from simulator:
306
+
307
+ command: xcrun #{cmd.join(" ")}
308
+ simulator: #{device}
309
+ app: #{app}
310
+
311
+ #{hash[:out]}
312
+
313
+ This usually means your CoreSimulator processes need to be restarted.
314
+
315
+ You can restart the CoreSimulator processes with this command:
316
+
317
+ $ bundle exec run-loop simctl manage-processes
318
+
319
+ ]
320
+ end
321
+ true
322
+ end
323
+
324
+ # @!visibility private
325
+ #
326
+ # Launches the app on on the device.
327
+ #
328
+ # Caller is responsible for the following:
329
+ #
330
+ # 1. Launching the simulator.
331
+ #
332
+ # No checks are made.
333
+ #
334
+ # @param [RunLoop::Device] device The simulator to launch on.
335
+ # @param [RunLoop::App] app The app to launch.
336
+ # @param [Numeric] timeout How long to wait for simctl to complete.
337
+ def install(device, app, timeout)
338
+ cmd = ["simctl", "install", device.udid, app.path]
339
+ options = DEFAULTS.dup
340
+ options[:timeout] = timeout
341
+
342
+ hash = shell_out_with_xcrun(cmd, options)
343
+
344
+ exit_status = hash[:exit_status]
345
+ if exit_status != 0
346
+ raise RuntimeError,
347
+ %Q[Could not install app on simulator:
348
+
349
+ command: xcrun #{cmd.join(" ")}
350
+ simulator: #{device}
351
+ app: #{app}
352
+
353
+ #{hash[:out]}
354
+
355
+ This usually means your CoreSimulator processes need to be restarted.
356
+
357
+ You can restart the CoreSimulator processes with this command:
358
+
359
+ $ bundle exec run-loop simctl manage-processes
360
+
361
+ ]
362
+ end
363
+ true
364
+ end
365
+
89
366
  # @!visibility private
90
367
  #
91
368
  # SimControl compatibility
@@ -110,10 +387,26 @@ module RunLoop
110
387
  private
111
388
 
112
389
  # @!visibility private
113
- attr_reader :ios_devices, :tvos_devices, :watchos_devices
390
+ attr_reader :ios_devices, :tvos_devices, :watchos_devices, :pbuddy
391
+
392
+ # @!visibility private
393
+ def pbuddy
394
+ @pbuddy ||= RunLoop::PlistBuddy.new
395
+ end
396
+
397
+ # @!visibility private
398
+ def string_for_sim_state(integer)
399
+ SIM_STATES.each do |key, value|
400
+ if value == integer
401
+ return key
402
+ end
403
+ end
404
+
405
+ raise ArgumentError, "Could not find state for #{integer}"
406
+ end
114
407
 
115
408
  # @!visibility private
116
- def execute(array, options)
409
+ def shell_out_with_xcrun(array, options)
117
410
  merged = DEFAULTS.merge(options)
118
411
  xcrun.run_command_in_context(array, merged)
119
412
  end
@@ -143,7 +436,7 @@ module RunLoop
143
436
  @watchos_devices = []
144
437
 
145
438
  cmd = ["simctl", "list", "devices", "--json"]
146
- hash = execute(cmd, DEFAULTS)
439
+ hash = shell_out_with_xcrun(cmd, DEFAULTS)
147
440
 
148
441
  out = hash[:out]
149
442
  exit_status = hash[:exit_status]