Skip to content

Commit d804711

Browse files
authored
Merge pull request #365 from wordpress-mobile/app-size-metrics+android-universal-apk
App Size Metrics: add support for Android Universal APKs
2 parents fbb5f61 + 4a15e07 commit d804711

File tree

7 files changed

+364
-123
lines changed

7 files changed

+364
-123
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ _None_
1010

1111
### New Features
1212

13-
* Introduce new `ios_send_app_size_metrics` and `android_send_app_size_metrics` actions. [#364]
13+
* Introduce new `ios_send_app_size_metrics` and `android_send_app_size_metrics` actions. [#364] [#365]
1414

1515
### Bug Fixes
1616

lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_send_app_size_metrics.rb

Lines changed: 104 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,23 @@
33
module Fastlane
44
module Actions
55
class AndroidSendAppSizeMetricsAction < Action
6+
# Keys used by the metrics payload
7+
AAB_FILE_SIZE_KEY = 'AAB File Size'.freeze # value from `File.size` of the `.aab`
8+
UNIVERSAL_APK_FILE_SIZE_KEY = 'Universal APK File Size'.freeze # value from `File.size` of the Universal `.apk`
9+
UNIVERSAL_APK_SPLIT_NAME = 'Universal'.freeze # pseudo-name of the split representing the Universal `.apk`
10+
APK_OPTIMIZED_FILE_SIZE_KEY = 'Optimized APK File Size'.freeze # value from `apkanalyzer apk file-size`
11+
APK_OPTIMIZED_DOWNLOAD_SIZE_KEY = 'Download Size'.freeze # value from `apkanalyzer apk download-size`
12+
613
def self.run(params)
714
# Check input parameters
815
api_url = URI(params[:api_url])
916
api_token = params[:api_token]
1017
if (api_token.nil? || api_token.empty?) && !api_url.is_a?(URI::File)
1118
UI.user_error!('An API token is required when using an `api_url` with a scheme other than `file://`')
1219
end
20+
if params[:aab_path].nil? && params[:universal_apk_path].nil?
21+
UI.user_error!('You must provide at least an `aab_path` or an `universal_apk_path`, or both')
22+
end
1323

1424
# Build the payload base
1525
metrics_helper = Fastlane::Helper::AppSizeMetricsHelper.new(
@@ -21,28 +31,22 @@ def self.run(params)
2131
'Build Type': params[:build_type],
2232
Source: params[:source]
2333
)
24-
metrics_helper.add_metric(name: 'AAB File Size', value: File.size(params[:aab_path]))
34+
# Add AAB file size
35+
metrics_helper.add_metric(name: AAB_FILE_SIZE_KEY, value: File.size(params[:aab_path])) unless params[:aab_path].nil?
36+
# Add Universal APK file size
37+
metrics_helper.add_metric(name: UNIVERSAL_APK_FILE_SIZE_KEY, value: File.size(params[:universal_apk_path])) unless params[:universal_apk_path].nil?
2538

26-
# Add device-specific 'splits' metrics to the payload if a `:include_split_sizes` is enabled
39+
# Add optimized file and download sizes for each split `.apk` metrics to the payload if a `:include_split_sizes` is enabled
2740
if params[:include_split_sizes]
28-
check_bundletool_installed!
2941
apkanalyzer_bin = params[:apkanalyzer_binary] || find_apkanalyzer_binary!
30-
UI.message("[App Size Metrics] Generating the various APK splits from #{params[:aab_path]}...")
31-
Dir.mktmpdir('release-toolkit-android-app-size-metrics') do |tmp_dir|
32-
Action.sh('bundletool', 'build-apks', '--bundle', params[:aab_path], '--output-format', 'DIRECTORY', '--output', tmp_dir)
33-
apks = Dir.glob('splits/*.apk', base: tmp_dir).map { |f| File.join(tmp_dir, f) }
34-
UI.message("[App Size Metrics] Generated #{apks.length} APKs.")
35-
36-
apks.each do |apk|
37-
UI.message("[App Size Metrics] Computing file and download size of #{File.basename(apk)}...")
42+
unless params[:aab_path].nil?
43+
generate_split_apks(aab_path: params[:aab_path]) do |apk|
3844
split_name = File.basename(apk, '.apk')
39-
file_size = Action.sh(apkanalyzer_bin, 'apk', 'file-size', apk, print_command: false, print_command_output: false).chomp.to_i
40-
download_size = Action.sh(apkanalyzer_bin, 'apk', 'download-size', apk, print_command: false, print_command_output: false).chomp.to_i
41-
metrics_helper.add_metric(name: 'APK File Size', value: file_size, metadata: { split: split_name })
42-
metrics_helper.add_metric(name: 'Download Size', value: download_size, metadata: { split: split_name })
45+
add_apk_size_metrics(helper: metrics_helper, apkanalyzer_bin: apkanalyzer_bin, apk: apk, split_name: split_name)
4346
end
44-
45-
UI.message('[App Size Metrics] Done computing splits sizes.')
47+
end
48+
unless params[:universal_apk_path].nil?
49+
add_apk_size_metrics(helper: metrics_helper, apkanalyzer_bin: apkanalyzer_bin, apk: params[:universal_apk_path], split_name: UNIVERSAL_APK_SPLIT_NAME)
4650
end
4751
end
4852

@@ -54,26 +58,79 @@ def self.run(params)
5458
)
5559
end
5660

57-
def self.check_bundletool_installed!
58-
Action.sh('command', '-v', 'bundletool', print_command: false, print_command_output: false)
59-
rescue StandardError
60-
UI.user_error!('bundletool is required to build the split APKs. Install it with `brew install bundletool`')
61-
raise
62-
end
61+
#####################################################
62+
# @!group Small helper methods
63+
#####################################################
64+
class << self
65+
# @raise if `bundletool` can not be found in `$PATH`
66+
def check_bundletool_installed!
67+
Action.sh('command', '-v', 'bundletool', print_command: false, print_command_output: false)
68+
rescue StandardError
69+
UI.user_error!('`bundletool` is required to build the split APKs. Install it with `brew install bundletool`')
70+
raise
71+
end
6372

64-
def self.find_apkanalyzer_binary
65-
sdk_root = ENV['ANDROID_SDK_ROOT'] || ENV['ANDROID_HOME']
66-
if sdk_root
67-
pattern = File.join(sdk_root, 'cmdline-tools', '{latest,tools}', 'bin', 'apkanalyzer')
68-
apkanalyzer_bin = Dir.glob(pattern).find { |path| File.executable?(path) }
73+
# The path where the `apkanalyzer` binary was found, after searching it:
74+
# - in priority in `$ANDROID_SDK_ROOT` (or `$ANDROID_HOME` for legacy setups), under `cmdline-tools/latest/bin/` or `cmdline-tools/tools/bin`
75+
# - and falling back by trying to find it in `$PATH`
76+
#
77+
# @return [String,Nil] The path to `apkanalyzer`, or `nil` if it wasn't found in any of the above tested paths.
78+
#
79+
def find_apkanalyzer_binary
80+
sdk_root = ENV['ANDROID_SDK_ROOT'] || ENV['ANDROID_HOME']
81+
if sdk_root
82+
pattern = File.join(sdk_root, 'cmdline-tools', '{latest,tools}', 'bin', 'apkanalyzer')
83+
apkanalyzer_bin = Dir.glob(pattern).find { |path| File.executable?(path) }
84+
end
85+
apkanalyzer_bin || Action.sh('command', '-v', 'apkanalyzer', print_command_output: false) { |_| nil }
6986
end
70-
apkanalyzer_bin || Action.sh('command', '-v', 'apkanalyzer', print_command_output: false) { |_| nil }
71-
end
7287

73-
def self.find_apkanalyzer_binary!
74-
apkanalyzer_bin = find_apkanalyzer_binary
75-
UI.user_error!('Unable to find `apkanalyzer` executable in `$PATH` nor `$ANDROID_SDK_ROOT`. Make sure you installed the Android SDK Command-line Tools') if apkanalyzer_bin.nil?
76-
apkanalyzer_bin
88+
# The path where the `apkanalyzer` binary was found, after searching it:
89+
# - in priority in `$ANDROID_SDK_ROOT` (or `$ANDROID_HOME` for legacy setups), under `cmdline-tools/latest/bin/` or `cmdline-tools/tools/bin`
90+
# - and falling back by trying to find it in `$PATH`
91+
#
92+
# @return [String] The path to `apkanalyzer`
93+
# @raise [FastlaneCore::Interface::FastlaneError] if it wasn't found in any of the above tested paths.
94+
#
95+
def find_apkanalyzer_binary!
96+
apkanalyzer_bin = find_apkanalyzer_binary
97+
UI.user_error!('Unable to find `apkanalyzer` executable in either `$PATH` or `$ANDROID_SDK_ROOT`. Make sure you installed the Android SDK Command-line Tools') if apkanalyzer_bin.nil?
98+
apkanalyzer_bin
99+
end
100+
101+
# Add the `file-size` and `download-size` values of an APK to the helper, as reported by the corresponding `apkanalyzer apk …` commands
102+
#
103+
# @param [Fastlane::Helper::AppSizeMetricsHelper] helper The helper to add the metrics to
104+
# @param [String] apkanalyzer_bin The path to the `apkanalyzer` binary to use to extract those file and download sizes from the `.apk`
105+
# @param [String] apk The path to the `.apk` file to extract the sizes from
106+
# @param [String] split_name The name to use for the value of the `split` metadata key in the metrics being added
107+
#
108+
def add_apk_size_metrics(helper:, apkanalyzer_bin:, apk:, split_name:)
109+
UI.message("[App Size Metrics] Computing file and download size of #{File.basename(apk)}...")
110+
file_size = Action.sh(apkanalyzer_bin, 'apk', 'file-size', apk, print_command: false, print_command_output: false).chomp.to_i
111+
download_size = Action.sh(apkanalyzer_bin, 'apk', 'download-size', apk, print_command: false, print_command_output: false).chomp.to_i
112+
helper.add_metric(name: APK_OPTIMIZED_FILE_SIZE_KEY, value: file_size, metadata: { split: split_name })
113+
helper.add_metric(name: APK_OPTIMIZED_DOWNLOAD_SIZE_KEY, value: download_size, metadata: { split: split_name })
114+
end
115+
116+
# Generates all the split `.apk` files (typically one per device architecture) from a given `.aab` file, then yield for each apk produced.
117+
#
118+
# @note The split `.apk` files are generated in a temporary directory and are thus all deleted after each of them has been `yield`ed to the provided block.
119+
# @param [String] aab_path The path to the `.aab` file to generate split `.apk` files for
120+
# @yield [apk] Calls the provided block once for each split `.apk` that was generated from the `.aab`
121+
# @yieldparam apk [String] The path to one of the split `.apk` temporary file generated from the `.aab`
122+
#
123+
def generate_split_apks(aab_path:, &block)
124+
check_bundletool_installed!
125+
UI.message("[App Size Metrics] Generating the various APK splits from #{aab_path}...")
126+
Dir.mktmpdir('release-toolkit-android-app-size-metrics') do |tmp_dir|
127+
Action.sh('bundletool', 'build-apks', '--bundle', aab_path, '--output-format', 'DIRECTORY', '--output', tmp_dir)
128+
apks = Dir.glob('splits/*.apk', base: tmp_dir).map { |f| File.join(tmp_dir, f) }
129+
UI.message("[App Size Metrics] Generated #{apks.length} APKs.")
130+
apks.each(&block)
131+
UI.message('[App Size Metrics] Done computing splits sizes.')
132+
end
133+
end
77134
end
78135

79136
#####################################################
@@ -95,6 +152,7 @@ def self.details
95152
DETAILS
96153
end
97154

155+
# rubocop:disable Metrics/MethodLength
98156
def self.available_options
99157
[
100158
FastlaneCore::ConfigItem.new(
@@ -165,7 +223,7 @@ def self.available_options
165223
env_name: 'FL_ANDROID_SEND_APP_SIZE_METRICS_AAB_PATH',
166224
description: 'The path to the .aab to extract size information from',
167225
type: String,
168-
optional: false,
226+
optional: true, # We can have `aab_path` only, or `universal_apk_path` only, or both (but not none)
169227
verify_block: proc do |value|
170228
UI.user_error!('You must provide an path to an existing `.aab` file') unless File.exist?(value)
171229
end
@@ -178,6 +236,16 @@ def self.available_options
178236
type: FastlaneCore::Boolean,
179237
default_value: true
180238
),
239+
FastlaneCore::ConfigItem.new(
240+
key: :universal_apk_path,
241+
env_name: 'FL_ANDROID_SEND_APP_SIZE_METRICS_UNIVERSAL_APK_PATH',
242+
description: 'The path to the Universal `.apk` to extract size information from',
243+
type: String,
244+
optional: true, # We can have `aab_path` only, or `universal_apk_path` only, or both (but not none)
245+
verify_block: proc do |value|
246+
UI.user_error!('You must provide a path to an existing `.apk` file') unless File.exist?(value)
247+
end
248+
),
181249
FastlaneCore::ConfigItem.new(
182250
key: :apkanalyzer_binary,
183251
env_name: 'FL_ANDROID_SEND_APP_SIZE_METRICS_APKANALYZER_BINARY',
@@ -191,6 +259,7 @@ def self.available_options
191259
),
192260
]
193261
end
262+
# rubocop:enable Metrics/MethodLength
194263

195264
def self.return_type
196265
:integer

lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_send_app_size_metrics.rb

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44
module Fastlane
55
module Actions
66
class IosSendAppSizeMetricsAction < Action
7+
# Keys used by the metrics payload
8+
IPA_FILE_SIZE_KEY = 'File Size'.freeze # value from `File.size` of the Universal `.ipa`
9+
IPA_DOWNLOAD_SIZE_KEY = 'Download Size'.freeze # value from `app-thinning.plist`
10+
IPA_INSTALL_SIZE_KEY = 'Install Size'.freeze # value from `app-thinning.plist`
11+
712
def self.run(params)
813
# Check input parameters
914
api_url = URI(params[:api_url])
@@ -20,7 +25,7 @@ def self.run(params)
2025
'Build Type': params[:build_type],
2126
Source: params[:source]
2227
)
23-
metrics_helper.add_metric(name: 'File Size', value: File.size(params[:ipa_path]))
28+
metrics_helper.add_metric(name: IPA_FILE_SIZE_KEY, value: File.size(params[:ipa_path]))
2429

2530
# Add app-thinning metrics to the payload if a `.plist` is provided
2631
app_thinning_plist_path = params[:app_thinning_plist_path] || File.join(File.dirname(params[:ipa_path]), 'app-thinning.plist')
@@ -30,8 +35,8 @@ def self.run(params)
3035
variant_descriptors = variant['variantDescriptors'] || [{ 'device' => 'Universal' }]
3136
variant_descriptors.each do |desc|
3237
variant_metadata = { device: desc['device'], 'OS Version': desc['os-version'] }
33-
metrics_helper.add_metric(name: 'Download Size', value: variant['sizeCompressedApp'], metadata: variant_metadata)
34-
metrics_helper.add_metric(name: 'Install Size', value: variant['sizeUncompressedApp'], metadata: variant_metadata)
38+
metrics_helper.add_metric(name: IPA_DOWNLOAD_SIZE_KEY, value: variant['sizeCompressedApp'], metadata: variant_metadata)
39+
metrics_helper.add_metric(name: IPA_INSTALL_SIZE_KEY, value: variant['sizeUncompressedApp'], metadata: variant_metadata)
3540
end
3641
end
3742
end

0 commit comments

Comments
 (0)