Skip to content

Commit e0b70ab

Browse files
committed
Merge pull request #80 from jmoody/feature/ensure-compatible-arch-before-launching-on-device
Ensure compatible arch before launching on device
2 parents 7d19e70 + 1a91f7f commit e0b70ab

File tree

17 files changed

+426
-31
lines changed

17 files changed

+426
-31
lines changed

.travis.yml

+2
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,7 @@ notifications:
1919
recipients:
2020
- joshuajmoody@gmail.com
2121
- karl.krukow@xamarin.com
22+
- michael.john.kirk@gmail.com
23+
- sam.vevang@gmail.com
2224
on_success: change
2325
on_failure: always

lib/run_loop.rb

+2
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@
55
require 'run_loop/sim_control'
66
require 'run_loop/device'
77
require 'run_loop/instruments'
8+
require 'run_loop/lipo'
9+

lib/run_loop/core.rb

+49-6
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,48 @@ def self.detect_connected_device
8383
nil
8484
end
8585

86+
# Raise an error if the application binary is not compatible with the
87+
# target simulator.
88+
#
89+
# @note This method is implemented for CoreSimulator environments only;
90+
# for Xcode < 6.0 this method does nothing.
91+
#
92+
# @param [Hash] launch_options These options need to contain the app bundle
93+
# path and a udid that corresponds to a simulator name or simulator udid.
94+
# In practical terms: call this after merging the original launch
95+
# options with those options that are discovered.
96+
#
97+
# @param [RunLoop::SimControl] sim_control A simulator control object.
98+
# @raise [RuntimeError] Raises an error if the `launch_options[:udid]`
99+
# cannot be used to find a simulator.
100+
# @raise [RunLoop::IncompatibleArchitecture] Raises an error if the
101+
# application binary is not compatible with the target simulator.
102+
def self.expect_compatible_simulator_architecture(launch_options, sim_control)
103+
if sim_control.xcode_version_gte_6?
104+
sim_identifier = launch_options[:udid]
105+
simulator = sim_control.simulators.find do |simulator|
106+
[simulator.instruments_identifier(sim_control.xctools),
107+
simulator.udid].include?(sim_identifier)
108+
end
109+
110+
if simulator.nil?
111+
raise "Could not find simulator with identifier '#{sim_identifier}'"
112+
end
113+
114+
lipo = RunLoop::Lipo.new(launch_options[:bundle_dir_or_bundle_id])
115+
lipo.expect_compatible_arch(simulator)
116+
if ENV['DEBUG'] == '1'
117+
puts "Simulator instruction set '#{simulator.instruction_set}' is compatible with #{lipo.info}"
118+
end
119+
true
120+
else
121+
if ENV['DEBUG'] == '1'
122+
puts "Xcode #{sim_control.xctools.xcode_version} detected; skipping simulator architecture check."
123+
end
124+
false
125+
end
126+
end
127+
86128
def self.run_with_options(options)
87129
before = Time.now
88130

@@ -91,11 +133,6 @@ def self.run_with_options(options)
91133

92134
RunLoop::Instruments.new.kill_instruments(xctools)
93135

94-
if self.simulator_target?(options, sim_control)
95-
# @todo only enable accessibility on the targeted simulator
96-
sim_control.enable_accessibility_on_sims({:verbose => false})
97-
end
98-
99136
device_target = options[:udid] || options[:device_target] || detect_connected_device || 'simulator'
100137
if device_target && device_target.to_s.downcase == 'device'
101138
device_target = detect_connected_device
@@ -150,7 +187,7 @@ def self.run_with_options(options)
150187
log_file ||= File.join(results_dir, 'run_loop.out')
151188

152189
after = Time.now
153-
if ENV['DEBUG']=='1'
190+
if ENV['DEBUG'] == '1'
154191
puts "Preparation took #{after-before} seconds"
155192
end
156193

@@ -166,6 +203,12 @@ def self.run_with_options(options)
166203
}
167204
merged_options = options.merge(discovered_options)
168205

206+
if self.simulator_target?(merged_options, sim_control)
207+
# @todo only enable accessibility on the targeted simulator
208+
sim_control.enable_accessibility_on_sims({:verbose => false})
209+
self.expect_compatible_simulator_architecture(merged_options, sim_control)
210+
end
211+
169212
self.log_run_loop_options(merged_options, xctools)
170213

171214
cmd = instruments_command(merged_options, xctools)

lib/run_loop/device.rb

+36-12
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,6 @@ def initialize(name, version, udid)
1717
@udid = udid
1818
end
1919

20-
# Is this device a simulator?
21-
# @return [Boolean] Return true if this device is a simulator.
22-
def simulator?
23-
not physical_device?
24-
end
25-
26-
# Is this is a physical device?
27-
# @return [Boolean] Return true if this is a physical device.
28-
def physical_device?
29-
(self.udid =~ /[a-f0-9]{40}/) == 0
30-
end
31-
3220
# Returns and instruments-ready device identifier that is a suitable value
3321
# for DEVICE_TARGET environment variable.
3422
#
@@ -50,5 +38,41 @@ def instruments_identifier(xcode_tools=RunLoop::XCTools.new)
5038
"#{self.name} (#{version_part} Simulator)"
5139
end
5240
end
41+
42+
# Is this a physical device?
43+
# @return [Boolean] Returns true if this is a device.
44+
def physical_device?
45+
not self.udid[/[a-f0-9]{40}/, 0].nil?
46+
end
47+
48+
# Is this a simulator?
49+
# @return [Boolean] Returns true if this is a simulator.
50+
def simulator?
51+
not self.physical_device?
52+
end
53+
54+
# Return the instruction set for this device.
55+
#
56+
# **Simulator**
57+
# The simulator instruction set will be i386 or x86_64 depending on the
58+
# the (marketing) name of the device.
59+
#
60+
# @note Finding the instruction set of a device requires a third-party tool
61+
# like ideviceinfo. Example:
62+
# `$ ideviceinfo -u 89b59 < snip > ab7ba --key 'CPUArchitecture' => arm64`
63+
#
64+
# @raise [RuntimeError] Raises an error if this device is a physical device.
65+
# @return [String] An instruction set.
66+
def instruction_set
67+
if self.simulator?
68+
if ['iPhone 4s', 'iPhone 5', 'iPad 2', 'iPad Retina'].include?(self.name)
69+
'i386'
70+
else
71+
'x86_64'
72+
end
73+
else
74+
raise 'Finding the instruction set of a device requires a third-party tool like ideviceinfo'
75+
end
76+
end
5377
end
5478
end

lib/run_loop/lipo.rb

+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
require 'open3'
2+
3+
module RunLoop
4+
5+
# An error class for signaling an incompatible architecture.
6+
class IncompatibleArchitecture < StandardError
7+
end
8+
9+
# A class for interacting with the lipo command-line tool to verify that an
10+
# executable is valid for the test target (device or simulator).
11+
#
12+
# @note All lipo commands are run in the context of `xcrun`.
13+
class Lipo
14+
15+
# The path to the application bundle we are inspecting.
16+
# @!attribute [wr] bundle_path
17+
# @return [String] The path to the application bundle (.app).
18+
attr_accessor :bundle_path
19+
20+
def initialize(bundle_path)
21+
@bundle_path = bundle_path
22+
@plist_buddy = RunLoop::PlistBuddy.new
23+
end
24+
25+
# Inspect the `CFBundleExecutable` in the app bundle path with `lipo` and
26+
# compare the result with the target device's instruction set.
27+
#
28+
# **Simulators**
29+
#
30+
# If the target is a simulator and the binary contains an i386 slice, the
31+
# app will launch on the 64-bit simulators.
32+
#
33+
# If the target is a simulator and the binary contains _only_ an x86_64
34+
# slice, the app will not launch on these simulators:
35+
#
36+
# ```
37+
# iPhone 4S, iPad 2, iPhone 5, and iPad Retina.
38+
# ```
39+
#
40+
# All other simulators are 64-bit.
41+
#
42+
# **Devices**
43+
#
44+
# @see {https://www.innerfence.com/howto/apple-ios-devices-dates-versions-instruction-sets}
45+
#
46+
# ```
47+
# armv7 <== 3gs, 4s, iPad 2, iPad mini, iPad 3, iPod 3, iPod 4, iPod 5
48+
# armv7s <== 5, 5c, iPad 4
49+
# arm64 <== 5s, 6, 6 Plus, Air, Air 2, iPad Mini Retina, iPad Mini 3
50+
# ```
51+
#
52+
# @note At the moment, we are focusing on simulator compatibility. Since we
53+
# don't have an automated way of installing an .ipa on local device, we
54+
# don't require an .ipa path. Without an .ipa path, we cannot verify the
55+
# architectures. Further, we would need to adopt a third-party tool like
56+
# ideviceinfo to find the target device's instruction set.
57+
# @param [RunLoop::Device] device The test target.
58+
# @raise [RuntimeError] Raises an error if the device is a physical device.
59+
# @raise [RunLoop::IncompatibleArchitecture] Raises an error if the instruction set of the target
60+
# device is not compatible with the executable in the application.
61+
def expect_compatible_arch(device)
62+
if device.physical_device?
63+
raise 'Ensuring compatible arches for physical devices is NYI'
64+
else
65+
arches = self.info
66+
# An i386 binary will run on any simulator.
67+
return true if arches.include?('i386')
68+
69+
instruction_set = device.instruction_set
70+
unless arches.include?(instruction_set)
71+
raise RunLoop::IncompatibleArchitecture,
72+
['Binary at:',
73+
binary_path,
74+
"does not contain a compatible architecture for target device.",
75+
"Expected '#{instruction_set}' but found #{arches}."].join("\n")
76+
end
77+
end
78+
end
79+
80+
# Returns a list of architecture in the binary.
81+
# @return [Array<String>] A list of architecture.
82+
def info
83+
execute_lipo("-info #{binary_path}") do |stdout, _, _|
84+
output = stdout.read.strip
85+
output.split(':')[-1].strip.split
86+
end
87+
end
88+
89+
private
90+
91+
def execute_lipo(argument)
92+
command = "xcrun lipo #{argument}"
93+
Open3.popen3(command) do |_, stdout, stderr, wait_thr|
94+
yield stdout, stderr, wait_thr
95+
end
96+
end
97+
98+
def plist_path
99+
File.join(@bundle_path, 'Info.plist');
100+
end
101+
102+
def binary_path
103+
binary_relative_path = @plist_buddy.plist_read('CFBundleExecutable', plist_path)
104+
File.join(@bundle_path, binary_relative_path)
105+
end
106+
end
107+
end

spec/integration/run_device_spec.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
path = install_hash[:path]
6565
physical_devices.each do |device|
6666
if Resources.shared.incompatible_xcode_ios_version(device.version, version)
67-
it "Skipping #{device.name} iOS #{device.version} Xcode #{version}- combination not supported" do
67+
it "Skipping #{device.name} iOS #{device.version} Xcode #{version} - combination not supported" do
6868
expect(true).to be == true
6969
end
7070
else
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
describe 'Simulator/Binary Compatibility Check' do
2+
3+
let(:sim_control) {
4+
obj = RunLoop::SimControl.new
5+
obj.reset_sim_content_and_settings
6+
obj
7+
}
8+
9+
describe 'can launch if library is FAT' do
10+
it 'can launch if libraries are compatible' do
11+
sim_control = RunLoop::SimControl.new
12+
sim_control.reset_sim_content_and_settings
13+
14+
options =
15+
{
16+
:app => Resources.shared.cal_app_bundle_path,
17+
:device_target => 'simulator',
18+
:sim_control => sim_control
19+
}
20+
21+
hash = nil
22+
Retriable.retriable({:tries => Resources.shared.launch_retries}) do
23+
hash = RunLoop.run(options)
24+
end
25+
expect(hash).not_to be nil
26+
end
27+
28+
it 'targeting x86_64 simulator with binary that contains only a i386 slice' do
29+
# The latest iPad Air
30+
air = sim_control.simulators.select do |device|
31+
device.name == 'iPad Air'
32+
end[-1]
33+
34+
expect(air).not_to be == nil
35+
options =
36+
{
37+
:app => Resources.shared.app_bundle_path_i386,
38+
:device_target => air.instruments_identifier,
39+
:sim_control => sim_control
40+
}
41+
42+
hash = nil
43+
Retriable.retriable({:tries => Resources.shared.launch_retries}) do
44+
hash = RunLoop.run(options)
45+
end
46+
expect(hash).not_to be nil
47+
end
48+
end
49+
50+
describe 'raises an error if libraries are not compatible' do
51+
it 'target only has arm slices' do
52+
options =
53+
{
54+
:app => Resources.shared.app_bundle_path_arm_FAT,
55+
:device_target => 'simulator',
56+
:sim_control => sim_control
57+
}
58+
59+
expect { RunLoop.run(options) }.to raise_error RunLoop::IncompatibleArchitecture
60+
end
61+
62+
if RunLoop::XCTools.new.xcode_version_gte_6?
63+
it 'targeting i386 simulator with binary that contains only a x86_64 slice' do
64+
# The latest iPad 2; will eventually fail when the iPad 2 is no longer supported. :(
65+
ipad2 = sim_control.simulators.select do |device|
66+
device.name == 'iPad 2'
67+
end[-1]
68+
69+
expect(ipad2).not_to be == nil
70+
options =
71+
{
72+
:app => Resources.shared.app_bundle_path_x86_64,
73+
:device_target => ipad2.instruments_identifier,
74+
:sim_control => sim_control
75+
}
76+
77+
expect { RunLoop.run(options) }.to raise_error RunLoop::IncompatibleArchitecture
78+
end
79+
end
80+
end
81+
end

0 commit comments

Comments
 (0)