appium_instrumenter 0.1.0

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.
@@ -0,0 +1,123 @@
1
+ module Calabash
2
+ module Android
3
+ # @!visibility private
4
+ class Environment
5
+ # @!visibility private
6
+ class InvalidEnvironmentError < RuntimeError; end
7
+ # @!visibility private
8
+ class InvalidJavaSDKHome < RuntimeError; end
9
+
10
+ # @!visibility private
11
+ # Returns true if running on Windows
12
+ def self.windows?
13
+ RbConfig::CONFIG['host_os'][/mswin|mingw|cygwin/, 0] != nil
14
+ end
15
+
16
+ # @!visibility private
17
+ # Returns the user home directory
18
+ def self.user_home_directory
19
+ if self.xtc?
20
+ home = File.join("./", "tmp", "home")
21
+ FileUtils.mkdir_p(home)
22
+ home
23
+ else
24
+ if self.windows?
25
+ # http://stackoverflow.com/questions/4190930/cross-platform-means-of-getting-users-home-directory-in-ruby
26
+ home = ENV["HOME"] || ENV["USERPROFILE"]
27
+ else
28
+ require "etc"
29
+ home = Etc.getpwuid.dir
30
+ end
31
+
32
+ unless File.exist?(home)
33
+ home = File.join("./", "tmp", "home")
34
+ FileUtils.mkdir_p(home)
35
+ end
36
+
37
+ home
38
+ end
39
+ end
40
+
41
+ # @!visibility private
42
+ # Returns true if debugging is enabled.
43
+ def self.debug?
44
+ ENV['DEBUG'] == '1' ||
45
+ ARGV.include?("-v") ||
46
+ ARGV.include?("--verbose")
47
+ end
48
+
49
+ # @!visibility private
50
+ # Returns true if we are running on the XTC
51
+ def self.xtc?
52
+ ENV['XAMARIN_TEST_CLOUD'] == '1'
53
+ end
54
+
55
+ # @!visibility private
56
+ # Returns true if running in Jenkins CI
57
+ #
58
+ # Checks the value of JENKINS_HOME
59
+ def self.jenkins?
60
+ value = ENV["JENKINS_HOME"]
61
+ !!value && value != ''
62
+ end
63
+
64
+ # @!visibility private
65
+ # Returns true if running in Travis CI
66
+ #
67
+ # Checks the value of TRAVIS
68
+ def self.travis?
69
+ value = ENV["TRAVIS"]
70
+ !!value && value != ''
71
+ end
72
+
73
+ # @!visibility private
74
+ # Returns true if running in Circle CI
75
+ #
76
+ # Checks the value of CIRCLECI
77
+ def self.circle_ci?
78
+ value = ENV["CIRCLECI"]
79
+ !!value && value != ''
80
+ end
81
+
82
+ # @!visibility private
83
+ # Returns true if running in Teamcity
84
+ #
85
+ # Checks the value of TEAMCITY_PROJECT_NAME
86
+ def self.teamcity?
87
+ value = ENV["TEAMCITY_PROJECT_NAME"]
88
+ !!value && value != ''
89
+ end
90
+
91
+ # @!visibility private
92
+ # Returns true if running in Teamcity
93
+ #
94
+ # Checks the value of GITLAB_CI
95
+ def self.gitlab?
96
+ value = ENV["GITLAB_CI"]
97
+ !!value && value != ''
98
+ end
99
+
100
+ # @!visibility private
101
+ # Returns true if running in a CI environment
102
+ def self.ci?
103
+ [
104
+ self.ci_var_defined?,
105
+ self.travis?,
106
+ self.jenkins?,
107
+ self.circle_ci?,
108
+ self.teamcity?,
109
+ self.gitlab?
110
+ ].any?
111
+ end
112
+
113
+ private
114
+
115
+ # !@visibility private
116
+ def self.ci_var_defined?
117
+ value = ENV["CI"]
118
+ !!value && value != ''
119
+ end
120
+ end
121
+ end
122
+ end
123
+
@@ -0,0 +1,220 @@
1
+ require "stringio"
2
+ require 'zip'
3
+ require 'tempfile'
4
+ require 'escape'
5
+ require 'rbconfig'
6
+ require_relative 'utils'
7
+ require_relative 'dependencies'
8
+ require_relative 'environment'
9
+
10
+ def package_name(app)
11
+ unless File.exist?(app)
12
+ raise "Application '#{app}' does not exist"
13
+ end
14
+
15
+ package_line = aapt_dump(app, "package").first
16
+ raise "'package' not found in aapt output" unless package_line
17
+ m = package_line.match(/name='([^']+)'/)
18
+ raise "Unexpected output from aapt: #{package_line}" unless m
19
+ m[1]
20
+ end
21
+
22
+ def main_activity(app)
23
+ unless File.exist?(app)
24
+ raise "Application '#{app}' does not exist"
25
+ end
26
+
27
+ begin
28
+ log("Trying to find launchable activity")
29
+ launchable_activity_line = aapt_dump(app, "launchable-activity").first
30
+ raise "'launchable-activity' not found in aapt output" unless launchable_activity_line
31
+ m = launchable_activity_line.match(/name='([^']+)'/)
32
+ raise "Unexpected output from aapt: #{launchable_activity_line}" unless m
33
+ log("Found launchable activity '#{m[1]}'")
34
+ m[1]
35
+ rescue => e
36
+ log("Could not find launchable activity, trying to parse raw AndroidManifest. #{e.message}")
37
+
38
+ manifest_data = `"#{Calabash::Android::Dependencies.aapt_path}" dump xmltree "#{app}" AndroidManifest.xml`
39
+ regex = /^\s*A:[\s*]android:name\(\w+\)\=\"android.intent.category.LAUNCHER\"/
40
+ lines = manifest_data.lines.collect(&:strip)
41
+ indicator_line = nil
42
+
43
+ lines.each_with_index do |line, index|
44
+ match = line.match(regex)
45
+
46
+ unless match.nil?
47
+ raise 'More than one launchable activity in AndroidManifest' unless indicator_line.nil?
48
+ indicator_line = index
49
+ end
50
+ end
51
+
52
+ raise 'No launchable activity found in AndroidManifest' unless indicator_line
53
+
54
+ intent_filter_found = false
55
+
56
+ (0..indicator_line).reverse_each do |index|
57
+ if intent_filter_found
58
+ match = lines[index].match(/\s*E:\s*activity-alias/)
59
+
60
+ raise 'Could not find target activity in activity alias' if match
61
+
62
+ match = lines[index].match(/^\s*A:\s*android:targetActivity\(\w*\)\=\"([^\"]+)/){$1}
63
+
64
+ if match
65
+ log("Found launchable activity '#{match}'")
66
+
67
+ return match
68
+ end
69
+ else
70
+ unless lines[index].match(/\s*E: intent-filter/).nil?
71
+ log("Read intent filter")
72
+ intent_filter_found = true
73
+ end
74
+ end
75
+ end
76
+
77
+ raise 'Could not find launchable activity'
78
+ end
79
+ end
80
+
81
+ def aapt_dump(app, key)
82
+ lines = `"#{Calabash::Android::Dependencies.aapt_path}" dump badging "#{app}"`.lines.collect(&:strip)
83
+ lines.select { |l| l.start_with?("#{key}:") }
84
+ end
85
+
86
+ def checksum(file_path)
87
+ require 'digest/md5'
88
+ Digest::MD5.file(file_path).hexdigest
89
+ end
90
+
91
+ def test_server_path(apk_file_path)
92
+ "test_servers/#{checksum(apk_file_path)}_#{Calabash::Android::VERSION}.apk"
93
+ end
94
+
95
+ def build_test_server_if_needed(app_path)
96
+ unless File.exist?(test_server_path(app_path))
97
+ if ARGV.include? "--no-build"
98
+ puts "No test server found for this combination of app and calabash version. Exiting!"
99
+ exit 1
100
+ else
101
+ puts "No test server found for this combination of app and calabash version. Recreating test server."
102
+ require 'calabash-android/operations'
103
+ require File.join(File.dirname(__FILE__), '..', '..', 'bin', 'calabash-android-build')
104
+ calabash_build(app_path)
105
+ end
106
+ end
107
+ end
108
+
109
+ def resign_apk(app_path)
110
+ Dir.mktmpdir do |tmp_dir|
111
+ log "Resign apk"
112
+ unsigned_path = File.join(tmp_dir, 'unsigned.apk')
113
+ unaligned_path = File.join(tmp_dir, 'unaligned.apk')
114
+ FileUtils.cp(app_path, unsigned_path)
115
+ unsign_apk(unsigned_path)
116
+ sign_apk(unsigned_path, unaligned_path)
117
+ zipalign_apk(unaligned_path, app_path)
118
+ end
119
+ end
120
+
121
+ def unsign_apk(path)
122
+ meta_files = `"#{Calabash::Android::Dependencies.aapt_path}" list "#{path}"`.lines.collect(&:strip).grep(/^META-INF\//)
123
+
124
+ signing_file_names = ['.mf', '.rsa', '.dsa', '.ec', '.sf']
125
+
126
+ files_to_remove = meta_files.select do |file|
127
+ # other will be:
128
+ # META-INF/foo/bar
129
+ # other #=> bar
130
+ directory, file_name, other = file.split('/')
131
+
132
+ if other != nil || file_name.nil?
133
+ false
134
+ else
135
+ if signing_file_names.include?(File.extname(file_name).downcase)
136
+ true
137
+ end
138
+ end
139
+ end
140
+
141
+ if files_to_remove.empty?
142
+ log "App wasn't signed. Will not try to unsign it."
143
+ else
144
+ system("\"#{Calabash::Android::Dependencies.aapt_path}\" remove \"#{path}\" #{files_to_remove.join(" ")}")
145
+ end
146
+ end
147
+
148
+ def zipalign_apk(inpath, outpath)
149
+ cmd = %Q("#{Calabash::Android::Dependencies.zipalign_path}" -f 4 "#{inpath}" "#{outpath}")
150
+ log "Zipaligning using: #{cmd}"
151
+ system(cmd)
152
+ end
153
+
154
+ def sign_apk(app_path, dest_path)
155
+ java_keystore = JavaKeystore.get_keystores.first
156
+
157
+ if java_keystore.nil?
158
+ raise 'No keystores found. You can specify the keystore location and credentials using calabash-android setup'
159
+ end
160
+
161
+ java_keystore.sign_apk(app_path, dest_path)
162
+ end
163
+
164
+ def fingerprint_from_apk(app_path)
165
+ app_path = File.expand_path(app_path)
166
+ Dir.mktmpdir do |tmp_dir|
167
+ Dir.chdir(tmp_dir) do
168
+ FileUtils.cp(app_path, "app.apk")
169
+ FileUtils.mkdir("META-INF")
170
+
171
+ Calabash::Utils.with_silent_zip do
172
+ Zip::File.foreach("app.apk") do |z|
173
+ z.extract if /^META-INF\/\w+.(rsa|dsa)/i =~ z.name
174
+ end
175
+ end
176
+
177
+ signature_files = Dir["#{tmp_dir}/META-INF/*"]
178
+
179
+ log 'Signature files:'
180
+
181
+ signature_files.each do |signature_file|
182
+ log signature_file
183
+ end
184
+
185
+ raise "No signature files found in META-INF. Cannot proceed." if signature_files.empty?
186
+ raise "More than one signature file (DSA or RSA) found in META-INF. Cannot proceed." if signature_files.length > 1
187
+
188
+ cmd = "\"#{Calabash::Android::Dependencies.keytool_path}\" -v -printcert -J\"-Dfile.encoding=utf-8\" -file \"#{signature_files.first}\""
189
+ log cmd
190
+ fingerprints = `#{cmd}`
191
+ md5_fingerprint = extract_sha1_fingerprint(fingerprints)
192
+ log "SHA1 fingerprint for signing cert (#{app_path}): #{md5_fingerprint}"
193
+ md5_fingerprint
194
+ end
195
+ end
196
+ end
197
+
198
+ def extract_md5_fingerprint(fingerprints)
199
+ m = fingerprints.scan(/MD5.*((?:[a-fA-F\d]{2}:){15}[a-fA-F\d]{2})/).flatten
200
+ raise "No MD5 fingerprint found:\n #{fingerprints}" if m.empty?
201
+ m.first
202
+ end
203
+
204
+ def extract_sha1_fingerprint(fingerprints)
205
+ m = fingerprints.scan(/SHA1.*((?:[a-fA-F\d]{2}:){15}[a-fA-F\d]{2})/).flatten
206
+ raise "No SHA1 fingerprint found:\n #{fingerprints}" if m.empty?
207
+ m.first
208
+ end
209
+
210
+ def extract_signature_algorithm_name(fingerprints)
211
+ m = fingerprints.scan(/Signature algorithm name: (.*)/).flatten
212
+ raise "No signature algorithm names found:\n #{fingerprints}" if m.empty?
213
+ m.first
214
+ end
215
+
216
+ def log(message, error = false)
217
+ if error or ARGV.include? "-v" or ARGV.include? "--verbose" or ENV["DEBUG"] == "1"
218
+ $stdout.puts "#{Time.now.strftime("%Y-%m-%d %H:%M:%S")} - #{message}"
219
+ end
220
+ end
@@ -0,0 +1,125 @@
1
+ require 'json'
2
+ class JavaKeystore
3
+ attr_reader :errors, :location, :keystore_alias, :password, :fingerprint
4
+ attr_reader :signature_algorithm_name
5
+
6
+ def initialize(location, keystore_alias, password)
7
+ raise "No such keystore file '#{location}'" unless File.exists?(File.expand_path(location))
8
+ log "Reading keystore data from keystore file '#{File.expand_path(location)}'"
9
+
10
+ keystore_data = system_with_stdout_on_success(Calabash::Android::Dependencies.keytool_path, '-list', '-v', '-alias', keystore_alias, '-keystore', location, '-storepass', password, '-J"-Dfile.encoding=utf-8"', '-J"-Duser.language=en-US"')
11
+
12
+ if keystore_data.nil?
13
+ if keystore_alias.empty?
14
+ log "Could not obtain keystore data. Will try to extract alias automatically"
15
+
16
+ keystore_data = system_with_stdout_on_success(Calabash::Android::Dependencies.keytool_path, '-list', '-v', '-keystore', location, '-storepass', password, '-J"-Dfile.encoding=utf-8"', '-J"-Duser.language=en-US"')
17
+ aliases = keystore_data.scan(/Alias name\:\s*(.*)/).flatten
18
+
19
+ if aliases.length == 0
20
+ raise 'Could not extract alias automatically. Please specify alias using calabash-android setup'
21
+ elsif aliases.length > 1
22
+ raise 'Multiple aliases found in keystore. Please specify alias using calabash-android setup'
23
+ else
24
+ keystore_alias = aliases.first
25
+ log "Extracted keystore alias '#{keystore_alias}'. Continuing"
26
+
27
+ return initialize(location, keystore_alias, password)
28
+ end
29
+ else
30
+ error = "Could not list certificates in keystore. Probably because the password was incorrect."
31
+ @errors = [{:message => error}]
32
+ log error
33
+ raise error
34
+ end
35
+ end
36
+
37
+ @location = location
38
+ @keystore_alias = keystore_alias
39
+ @password = password
40
+ log "Key store data:"
41
+ log keystore_data
42
+ @fingerprint = extract_sha1_fingerprint(keystore_data)
43
+ @signature_algorithm_name = extract_signature_algorithm_name(keystore_data)
44
+ log "Fingerprint: #{fingerprint}"
45
+ log "Signature algorithm name: #{signature_algorithm_name}"
46
+ end
47
+
48
+ def sign_apk(apk_path, dest_path)
49
+ raise "Cannot sign with a miss configured keystore" if errors
50
+ raise "No such file: #{apk_path}" unless File.exists?(apk_path)
51
+
52
+ # E.g. MD5withRSA or MD5withRSAandMGF1
53
+ encryption = signature_algorithm_name.split('with')[1].split('and')[0]
54
+ signing_algorithm = "SHA1with#{encryption}"
55
+ digest_algorithm = 'SHA1'
56
+
57
+ log "Signing using the signature algorithm: '#{signing_algorithm}'"
58
+ log "Signing using the digest algorithm: '#{digest_algorithm}'"
59
+
60
+ unless system_with_stdout_on_success(Calabash::Android::Dependencies.jarsigner_path, '-sigfile', 'CERT', '-sigalg', signing_algorithm, '-digestalg', digest_algorithm, '-signedjar', dest_path, '-storepass', password, '-keystore', location, apk_path, keystore_alias)
61
+ raise "Could not sign app: #{apk_path}"
62
+ end
63
+ end
64
+
65
+ def system_with_stdout_on_success(cmd, *args)
66
+ a = Escape.shell_command(args)
67
+ cmd = "\"#{cmd}\" #{a.gsub("'", '"')}"
68
+ log cmd
69
+ out = `#{cmd}`
70
+ if $?.exitstatus == 0
71
+ out
72
+ else
73
+ nil
74
+ end
75
+ end
76
+
77
+ def self.read_keystore_with_default_password_and_alias(path)
78
+ path = File.expand_path path
79
+
80
+ if File.exists? path
81
+ keystore = JavaKeystore.new(path, 'androiddebugkey', 'android')
82
+ if keystore.errors
83
+ log "Trying to "
84
+ nil
85
+ else
86
+ log "Unlocked keystore at #{path} - fingerprint: #{keystore.fingerprint}"
87
+ keystore
88
+ end
89
+ else
90
+ log "Trying to read keystore from: #{path} - no such file"
91
+ nil
92
+ end
93
+ end
94
+
95
+ def self.get_keystores
96
+ if keystore = keystore_from_settings
97
+ p "found calabash_settings file"
98
+ [ keystore ]
99
+ else
100
+ [
101
+ read_keystore_with_default_password_and_alias(File.join(ENV["HOME"], "/.android/debug.keystore")),
102
+ read_keystore_with_default_password_and_alias("debug.keystore"),
103
+ read_keystore_with_default_password_and_alias(File.join(ENV["HOME"], ".local/share/Xamarin/Mono\\ for\\ Android/debug.keystore")),
104
+ read_keystore_with_default_password_and_alias(File.join(ENV["HOME"], "AppData/Local/Xamarin/Mono for Android/debug.keystore")),
105
+ ].compact
106
+ end
107
+ end
108
+
109
+ def self.keystore_from_settings
110
+ keystore = JSON.parse(IO.read(".calabash_settings")) if File.exist? ".calabash_settings"
111
+ keystore = JSON.parse(IO.read("calabash_settings")) if File.exist? "calabash_settings"
112
+ return unless keystore
113
+ fail_if_key_missing(keystore, "keystore_location")
114
+ fail_if_key_missing(keystore, "keystore_password")
115
+ fail_if_key_missing(keystore, "keystore_alias")
116
+ keystore["keystore_location"] = File.expand_path(keystore["keystore_location"])
117
+ log("Keystore location specified in #{File.exist?(".calabash_settings") ? ".calabash_settings" : "calabash_settings"}.")
118
+ JavaKeystore.new(keystore["keystore_location"], keystore["keystore_alias"], keystore["keystore_password"])
119
+ end
120
+
121
+ def self.fail_if_key_missing(map, key)
122
+ raise "Found .calabash_settings but no #{key} defined." unless map[key]
123
+ end
124
+
125
+ end