33module 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
0 commit comments