3
3
module Fastlane
4
4
module Actions
5
5
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
+
6
13
def self . run ( params )
7
14
# Check input parameters
8
15
api_url = URI ( params [ :api_url ] )
9
16
api_token = params [ :api_token ]
10
17
if ( api_token . nil? || api_token . empty? ) && !api_url . is_a? ( URI ::File )
11
18
UI . user_error! ( 'An API token is required when using an `api_url` with a scheme other than `file://`' )
12
19
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
13
23
14
24
# Build the payload base
15
25
metrics_helper = Fastlane ::Helper ::AppSizeMetricsHelper . new (
@@ -21,28 +31,22 @@ def self.run(params)
21
31
'Build Type' : params [ :build_type ] ,
22
32
Source : params [ :source ]
23
33
)
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?
25
38
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
27
40
if params [ :include_split_sizes ]
28
- check_bundletool_installed!
29
41
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 |
38
44
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 )
43
46
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 )
46
50
end
47
51
end
48
52
@@ -54,26 +58,79 @@ def self.run(params)
54
58
)
55
59
end
56
60
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
63
72
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 }
69
86
end
70
- apkanalyzer_bin || Action . sh ( 'command' , '-v' , 'apkanalyzer' , print_command_output : false ) { |_ | nil }
71
- end
72
87
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
77
134
end
78
135
79
136
#####################################################
@@ -95,6 +152,7 @@ def self.details
95
152
DETAILS
96
153
end
97
154
155
+ # rubocop:disable Metrics/MethodLength
98
156
def self . available_options
99
157
[
100
158
FastlaneCore ::ConfigItem . new (
@@ -165,7 +223,7 @@ def self.available_options
165
223
env_name : 'FL_ANDROID_SEND_APP_SIZE_METRICS_AAB_PATH' ,
166
224
description : 'The path to the .aab to extract size information from' ,
167
225
type : String ,
168
- optional : false ,
226
+ optional : true , # We can have `aab_path` only, or `universal_apk_path` only, or both (but not none)
169
227
verify_block : proc do |value |
170
228
UI . user_error! ( 'You must provide an path to an existing `.aab` file' ) unless File . exist? ( value )
171
229
end
@@ -178,6 +236,16 @@ def self.available_options
178
236
type : FastlaneCore ::Boolean ,
179
237
default_value : true
180
238
) ,
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
+ ) ,
181
249
FastlaneCore ::ConfigItem . new (
182
250
key : :apkanalyzer_binary ,
183
251
env_name : 'FL_ANDROID_SEND_APP_SIZE_METRICS_APKANALYZER_BINARY' ,
@@ -191,6 +259,7 @@ def self.available_options
191
259
) ,
192
260
]
193
261
end
262
+ # rubocop:enable Metrics/MethodLength
194
263
195
264
def self . return_type
196
265
:integer
0 commit comments