-
Notifications
You must be signed in to change notification settings - Fork 41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve how App and Ipa instances detect the Calabash server version #379
Changes from all commits
713108e
e91b183
daf2e76
3576714
aa5afa6
0f80da3
5442d81
21695fa
d5f2fc7
e0e7116
b7ba49d
f88d387
4683a07
c80f86c
843373f
26f030b
54883b3
d865426
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,6 +14,20 @@ class App | |
# @return [RunLoop::App] A instance of App with a path. | ||
def initialize(app_bundle_path) | ||
@path = File.expand_path(app_bundle_path) | ||
|
||
if !App.valid?(app_bundle_path) | ||
raise ArgumentError, | ||
%Q{App does not exist at path or is not an app bundle. | ||
|
||
#{app_bundle_path} | ||
|
||
Bundle must: | ||
|
||
1. be a directory that exists, | ||
2. have a .app extension, | ||
3. and contain an Info.plist. | ||
} | ||
end | ||
end | ||
|
||
# @!visibility private | ||
|
@@ -28,19 +42,23 @@ def inspect | |
|
||
# Is this a valid app? | ||
def valid? | ||
[File.exist?(path), | ||
File.directory?(path), | ||
File.extname(path) == '.app'].all? | ||
App.valid?(path) | ||
end | ||
|
||
# @!visibility private | ||
def self.valid?(app_bundle_path) | ||
return false if app_bundle_path.nil? | ||
|
||
File.exist?(app_bundle_path) && | ||
File.directory?(app_bundle_path) && | ||
File.extname(app_bundle_path) == '.app' && | ||
File.exist?(File.join(app_bundle_path, "Info.plist")) | ||
end | ||
|
||
# Returns the Info.plist path. | ||
# @raise [RuntimeError] If there is no Info.plist. | ||
def info_plist_path | ||
info_plist = File.join(path, 'Info.plist') | ||
unless File.exist?(info_plist) | ||
raise "Expected an Info.plist at '#{path}'" | ||
end | ||
info_plist | ||
@info_plist_path ||= File.join(path, 'Info.plist') | ||
end | ||
|
||
# Inspects the app's Info.plist for the bundle identifier. | ||
|
@@ -69,20 +87,26 @@ def executable_name | |
|
||
# Inspects the app's file for the server version | ||
def calabash_server_version | ||
if valid? | ||
path_to_bin = File.join(path, executable_name) | ||
xcrun ||= RunLoop::Xcrun.new | ||
hash = xcrun.exec(["strings", path_to_bin]) | ||
unless hash.nil? | ||
version_str = hash[:out][/CALABASH VERSION: \d+\.\d+\.\d+/, 0] | ||
unless version_str.nil? || version_str == "" | ||
server_ver = version_str.split(":")[1].delete(' ') | ||
RunLoop::Version.new(server_ver) | ||
end | ||
version = nil | ||
executables.each do |executable| | ||
version = strings(executable).server_version | ||
break if version | ||
end | ||
version | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handles the load-calabash-dylib-at-runtime example. Collect the executable files and use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this a recursive search of executables? I'm not really sure where the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am using It is hard to see the code through the comments.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is that recursive or just two levels down? I thought recursive glob syntax was There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I'm wrong about that, nvm. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wait, not completely wrong - There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
end | ||
|
||
# @!visibility private | ||
# Collects the paths to executables in the bundle. | ||
def executables | ||
executables = [] | ||
Dir.glob("#{path}/**/*") do |file| | ||
next if File.directory?(file) | ||
next if skip_executable_check?(file) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An optimization. Calling |
||
if otool(file).executable? | ||
executables << file | ||
end | ||
else | ||
raise 'Path is not valid' | ||
end | ||
executables | ||
end | ||
|
||
# Returns the sha1 of the application. | ||
|
@@ -92,9 +116,89 @@ def sha1 | |
|
||
private | ||
|
||
# @!visibility private | ||
def plist_buddy | ||
@plist_buddy ||= RunLoop::PlistBuddy.new | ||
end | ||
|
||
# @!visibility private | ||
# An otool factory. | ||
def otool(file) | ||
RunLoop::Otool.new(file) | ||
end | ||
|
||
# @!visibility private | ||
# A strings factory | ||
def strings(file) | ||
RunLoop::Strings.new(file) | ||
end | ||
|
||
# @!visibility private | ||
def skip_executable_check?(file) | ||
image?(file) || | ||
text?(file) || | ||
plist?(file) || | ||
lproj_asset?(file) || | ||
code_signing_asset?(file) || | ||
core_data_asset?(file) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jmoody Why not use a whitelist approach instead? Files that need to be checked for calabash linkage should be just the main app binary and any dylibs There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I thought about pretty seriously. I am tempted to use a whitelist. I was thinking about extensions and a future where we would automate those as well - but the same whitelist rules would apply. I think I will change the approach now to use whitelist. |
||
end | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could use a second pair of eyes on this. Anything else I should skip? Ack! CoreData bundles!!! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not just check the main executable + |
||
|
||
# @!visibility private | ||
def text?(file) | ||
extension = File.extname(file) | ||
|
||
extension == ".txt" || | ||
extension == ".md" || | ||
extension == ".html" || | ||
extension == ".xml" || | ||
extension == ".json" || | ||
extension == ".yaml" || | ||
extension == ".yml" || | ||
extension == ".rtf" || | ||
file[/NOTICE|LICENSE|README|ABOUT/, 0] | ||
end | ||
|
||
# @!visibility private | ||
def image?(file) | ||
file[/jpeg|jpg|gif|png|tiff|svg|pdf|car|iTunesArtwork/, 0] | ||
end | ||
|
||
# @!visibility private | ||
def plist?(file) | ||
File.extname(file) == ".plist" | ||
end | ||
|
||
# @!visibility private | ||
def lproj_asset?(file) | ||
extension = File.extname(file) | ||
|
||
file[/lproj/, 0] || | ||
file[/storyboard/, 0] || | ||
extension == ".strings" || | ||
extension == ".xib" || | ||
extension == ".nib" | ||
end | ||
|
||
# @!visibility private | ||
def code_signing_asset?(file) | ||
name = File.basename(file) | ||
extension = File.extname(file) | ||
|
||
name == "PkgInfo" || | ||
name == "embedded" || | ||
extension == ".mobileprovision" || | ||
extension == ".xcent" || | ||
file[/_CodeSignature/, 0] | ||
end | ||
|
||
# @!visibility private | ||
def core_data_asset?(file) | ||
extension = File.extname(file) | ||
|
||
file[/momd/, 0] || | ||
extension == ".mom" || | ||
extension == ".db" | ||
end | ||
end | ||
end | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,18 +2,11 @@ module RunLoop | |
# A model of the an .ipa - a application binary for iOS devices. | ||
class Ipa | ||
|
||
|
||
# The path to this .ipa. | ||
# @!attribute [r] path | ||
# @return [String] A path to this .ipa. | ||
attr_reader :path | ||
|
||
# The bundle identifier of this ipa. | ||
# @!attribute [r] bundle_identifier | ||
# @return [String] The bundle identifier of this ipa; obtained by inspecting | ||
# the app's Info.plist. | ||
attr_reader :bundle_identifier | ||
|
||
# Create a new ipa instance. | ||
# @param [String] path_to_ipa The path the .ipa file. | ||
# @return [Calabash::Ipa] A new ipa instance. | ||
|
@@ -42,96 +35,59 @@ def inspect | |
|
||
# The bundle identifier of this ipa. | ||
# @return [String] A string representation of this ipa's CFBundleIdentifier | ||
# @raise [RuntimeError] If ipa does not expand into a Payload/<app name>.app | ||
# directory. | ||
# @raise [RuntimeError] If an Info.plist does exist in the .app. | ||
def bundle_identifier | ||
if bundle_dir.nil? || !File.exist?(bundle_dir) | ||
raise "Expected a '#{File.basename(path).split('.').first}.app'\nat path '#{payload_dir}'" | ||
end | ||
|
||
@bundle_identifier ||= lambda { | ||
info_plist_path = File.join(bundle_dir, 'Info.plist') | ||
unless File.exist? info_plist_path | ||
raise "Expected an 'Info.plist' at '#{bundle_dir}'" | ||
end | ||
identifier = plist_buddy.plist_read('CFBundleIdentifier', info_plist_path) | ||
|
||
unless identifier | ||
raise "Expected key 'CFBundleIdentifier' in '#{info_plist_path}'" | ||
end | ||
identifier | ||
}.call | ||
app.bundle_identifier | ||
end | ||
|
||
# Inspects the app's Info.plist for the executable name. | ||
# @return [String] The value of CFBundleExecutable. | ||
# @raise [RuntimeError] If the plist cannot be read or the | ||
# CFBundleExecutable is empty or does not exist. | ||
def executable_name | ||
if bundle_dir.nil? || !File.exist?(bundle_dir) | ||
raise "Expected a '#{File.basename(path).split('.').first}.app'\nat path '#{payload_dir}'" | ||
end | ||
|
||
@executable_name ||= lambda { | ||
info_plist_path = File.join(bundle_dir, 'Info.plist') | ||
unless File.exist? info_plist_path | ||
raise "Expected an 'Info.plist' at '#{bundle_dir}'" | ||
end | ||
name = plist_buddy.plist_read('CFBundleExecutable', info_plist_path) | ||
|
||
unless name | ||
raise "Expected key 'CFBundleExecutable' in '#{info_plist_path}'" | ||
end | ||
name | ||
}.call | ||
app.executable_name | ||
end | ||
|
||
# Inspects the app's file for the server version | ||
# Inspects the app's executables for the server version | ||
# @return[RunLoop::Version] a version instance | ||
def calabash_server_version | ||
if bundle_dir.nil? || !File.exist?(bundle_dir) | ||
raise "Expected a '#{File.basename(path).split('.').first}.app'\nat path '#{payload_dir}'" | ||
else | ||
if !executable_name.nil? && executable_name != '' | ||
path_to_bin = File.join(bundle_dir, executable_name) | ||
xcrun ||= RunLoop::Xcrun.new | ||
hash = xcrun.exec(["strings", path_to_bin]) | ||
unless hash.nil? | ||
version_str = hash[:out][/CALABASH VERSION: \d+\.\d+\.\d+/, 0] | ||
unless version_str.nil? || version_str == "" | ||
server_ver = version_str.split(":")[1].delete(' ') | ||
RunLoop::Version.new(server_ver) | ||
end | ||
end | ||
end | ||
end | ||
app.calabash_server_version | ||
end | ||
|
||
private | ||
|
||
# @!visibility private | ||
def tmpdir | ||
@tmpdir ||= Dir.mktmpdir | ||
end | ||
|
||
# @!visibility private | ||
def payload_dir | ||
@payload_dir ||= lambda { | ||
@payload_dir ||= lambda do | ||
FileUtils.cp(path, tmpdir) | ||
zip_path = File.join(tmpdir, File.basename(path)) | ||
Dir.chdir(tmpdir) do | ||
system('unzip', *['-q', zip_path]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought we were supposed to use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For this operation we are copying a zip file (.ipa) to a temp location. It is safe, I think, to copy with cp. Who knows, maybe FileUtils.cp is implemented with ditto on MacOS. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To clarify, we should use ditto when copying .app (app bundles) directories. I don't think we need to use ditto when copying archive files because the resource forks are included in the archive. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was talking about the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. E.g., for resigning we do: // NOTE: To correctly preserve MACOS metadata we must use ditto instead of zip.
// Failing to do so can make codesign bail out with some very exotic
// error messages in some rare cases.
ShellOrDie("ditto", "-x", "-k", "--sequesterRsrc", filepath, tmpDir) Perhaps it's not relevant here, but I thought it was just generally safer There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (>_O) Of course you were. I will fix this. |
||
end | ||
File.join(tmpdir, 'Payload') | ||
}.call | ||
end.call | ||
end | ||
|
||
# @!visibility private | ||
def bundle_dir | ||
@bundle_dir ||= lambda { | ||
Dir.glob(File.join(payload_dir, '*')).detect {|f| File.directory?(f) && f.end_with?('.app')} | ||
}.call | ||
@bundle_dir ||= lambda do | ||
Dir.glob(File.join(payload_dir, '*')).detect do |f| | ||
File.directory?(f) && f.end_with?('.app') | ||
end | ||
end.call | ||
end | ||
|
||
# @!visibility private | ||
def app | ||
@app ||= RunLoop::App.new(bundle_dir) | ||
end | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎱 We should have been doing this from the beginning. The Payload/My.app is just an app bundle - use the App class so we don't have to duplicate code. |
||
|
||
# @!visibility private | ||
def plist_buddy | ||
@plist_buddy ||= RunLoop::PlistBuddy.new | ||
end | ||
end | ||
end | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Immediately raise an error if the app bundle is invalid. This avoids having to check in each instance method.