Skip to content

Commit

Permalink
Merge pull request #365 from wordpress-mobile/app-size-metrics+androi…
Browse files Browse the repository at this point in the history
…d-universal-apk

App Size Metrics: add support for Android Universal APKs
  • Loading branch information
AliSoftware authored May 31, 2022
2 parents fbb5f61 + 4a15e07 commit d804711
Show file tree
Hide file tree
Showing 7 changed files with 364 additions and 123 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ _None_

### New Features

* Introduce new `ios_send_app_size_metrics` and `android_send_app_size_metrics` actions. [#364]
* Introduce new `ios_send_app_size_metrics` and `android_send_app_size_metrics` actions. [#364] [#365]

### Bug Fixes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,23 @@
module Fastlane
module Actions
class AndroidSendAppSizeMetricsAction < Action
# Keys used by the metrics payload
AAB_FILE_SIZE_KEY = 'AAB File Size'.freeze # value from `File.size` of the `.aab`
UNIVERSAL_APK_FILE_SIZE_KEY = 'Universal APK File Size'.freeze # value from `File.size` of the Universal `.apk`
UNIVERSAL_APK_SPLIT_NAME = 'Universal'.freeze # pseudo-name of the split representing the Universal `.apk`
APK_OPTIMIZED_FILE_SIZE_KEY = 'Optimized APK File Size'.freeze # value from `apkanalyzer apk file-size`
APK_OPTIMIZED_DOWNLOAD_SIZE_KEY = 'Download Size'.freeze # value from `apkanalyzer apk download-size`

def self.run(params)
# Check input parameters
api_url = URI(params[:api_url])
api_token = params[:api_token]
if (api_token.nil? || api_token.empty?) && !api_url.is_a?(URI::File)
UI.user_error!('An API token is required when using an `api_url` with a scheme other than `file://`')
end
if params[:aab_path].nil? && params[:universal_apk_path].nil?
UI.user_error!('You must provide at least an `aab_path` or an `universal_apk_path`, or both')
end

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

# Add device-specific 'splits' metrics to the payload if a `:include_split_sizes` is enabled
# Add optimized file and download sizes for each split `.apk` metrics to the payload if a `:include_split_sizes` is enabled
if params[:include_split_sizes]
check_bundletool_installed!
apkanalyzer_bin = params[:apkanalyzer_binary] || find_apkanalyzer_binary!
UI.message("[App Size Metrics] Generating the various APK splits from #{params[:aab_path]}...")
Dir.mktmpdir('release-toolkit-android-app-size-metrics') do |tmp_dir|
Action.sh('bundletool', 'build-apks', '--bundle', params[:aab_path], '--output-format', 'DIRECTORY', '--output', tmp_dir)
apks = Dir.glob('splits/*.apk', base: tmp_dir).map { |f| File.join(tmp_dir, f) }
UI.message("[App Size Metrics] Generated #{apks.length} APKs.")

apks.each do |apk|
UI.message("[App Size Metrics] Computing file and download size of #{File.basename(apk)}...")
unless params[:aab_path].nil?
generate_split_apks(aab_path: params[:aab_path]) do |apk|
split_name = File.basename(apk, '.apk')
file_size = Action.sh(apkanalyzer_bin, 'apk', 'file-size', apk, print_command: false, print_command_output: false).chomp.to_i
download_size = Action.sh(apkanalyzer_bin, 'apk', 'download-size', apk, print_command: false, print_command_output: false).chomp.to_i
metrics_helper.add_metric(name: 'APK File Size', value: file_size, metadata: { split: split_name })
metrics_helper.add_metric(name: 'Download Size', value: download_size, metadata: { split: split_name })
add_apk_size_metrics(helper: metrics_helper, apkanalyzer_bin: apkanalyzer_bin, apk: apk, split_name: split_name)
end

UI.message('[App Size Metrics] Done computing splits sizes.')
end
unless params[:universal_apk_path].nil?
add_apk_size_metrics(helper: metrics_helper, apkanalyzer_bin: apkanalyzer_bin, apk: params[:universal_apk_path], split_name: UNIVERSAL_APK_SPLIT_NAME)
end
end

Expand All @@ -54,26 +58,79 @@ def self.run(params)
)
end

def self.check_bundletool_installed!
Action.sh('command', '-v', 'bundletool', print_command: false, print_command_output: false)
rescue StandardError
UI.user_error!('bundletool is required to build the split APKs. Install it with `brew install bundletool`')
raise
end
#####################################################
# @!group Small helper methods
#####################################################
class << self
# @raise if `bundletool` can not be found in `$PATH`
def check_bundletool_installed!
Action.sh('command', '-v', 'bundletool', print_command: false, print_command_output: false)
rescue StandardError
UI.user_error!('`bundletool` is required to build the split APKs. Install it with `brew install bundletool`')
raise
end

def self.find_apkanalyzer_binary
sdk_root = ENV['ANDROID_SDK_ROOT'] || ENV['ANDROID_HOME']
if sdk_root
pattern = File.join(sdk_root, 'cmdline-tools', '{latest,tools}', 'bin', 'apkanalyzer')
apkanalyzer_bin = Dir.glob(pattern).find { |path| File.executable?(path) }
# The path where the `apkanalyzer` binary was found, after searching it:
# - in priority in `$ANDROID_SDK_ROOT` (or `$ANDROID_HOME` for legacy setups), under `cmdline-tools/latest/bin/` or `cmdline-tools/tools/bin`
# - and falling back by trying to find it in `$PATH`
#
# @return [String,Nil] The path to `apkanalyzer`, or `nil` if it wasn't found in any of the above tested paths.
#
def find_apkanalyzer_binary
sdk_root = ENV['ANDROID_SDK_ROOT'] || ENV['ANDROID_HOME']
if sdk_root
pattern = File.join(sdk_root, 'cmdline-tools', '{latest,tools}', 'bin', 'apkanalyzer')
apkanalyzer_bin = Dir.glob(pattern).find { |path| File.executable?(path) }
end
apkanalyzer_bin || Action.sh('command', '-v', 'apkanalyzer', print_command_output: false) { |_| nil }
end
apkanalyzer_bin || Action.sh('command', '-v', 'apkanalyzer', print_command_output: false) { |_| nil }
end

def self.find_apkanalyzer_binary!
apkanalyzer_bin = find_apkanalyzer_binary
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?
apkanalyzer_bin
# The path where the `apkanalyzer` binary was found, after searching it:
# - in priority in `$ANDROID_SDK_ROOT` (or `$ANDROID_HOME` for legacy setups), under `cmdline-tools/latest/bin/` or `cmdline-tools/tools/bin`
# - and falling back by trying to find it in `$PATH`
#
# @return [String] The path to `apkanalyzer`
# @raise [FastlaneCore::Interface::FastlaneError] if it wasn't found in any of the above tested paths.
#
def find_apkanalyzer_binary!
apkanalyzer_bin = find_apkanalyzer_binary
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?
apkanalyzer_bin
end

# Add the `file-size` and `download-size` values of an APK to the helper, as reported by the corresponding `apkanalyzer apk …` commands
#
# @param [Fastlane::Helper::AppSizeMetricsHelper] helper The helper to add the metrics to
# @param [String] apkanalyzer_bin The path to the `apkanalyzer` binary to use to extract those file and download sizes from the `.apk`
# @param [String] apk The path to the `.apk` file to extract the sizes from
# @param [String] split_name The name to use for the value of the `split` metadata key in the metrics being added
#
def add_apk_size_metrics(helper:, apkanalyzer_bin:, apk:, split_name:)
UI.message("[App Size Metrics] Computing file and download size of #{File.basename(apk)}...")
file_size = Action.sh(apkanalyzer_bin, 'apk', 'file-size', apk, print_command: false, print_command_output: false).chomp.to_i
download_size = Action.sh(apkanalyzer_bin, 'apk', 'download-size', apk, print_command: false, print_command_output: false).chomp.to_i
helper.add_metric(name: APK_OPTIMIZED_FILE_SIZE_KEY, value: file_size, metadata: { split: split_name })
helper.add_metric(name: APK_OPTIMIZED_DOWNLOAD_SIZE_KEY, value: download_size, metadata: { split: split_name })
end

# Generates all the split `.apk` files (typically one per device architecture) from a given `.aab` file, then yield for each apk produced.
#
# @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.
# @param [String] aab_path The path to the `.aab` file to generate split `.apk` files for
# @yield [apk] Calls the provided block once for each split `.apk` that was generated from the `.aab`
# @yieldparam apk [String] The path to one of the split `.apk` temporary file generated from the `.aab`
#
def generate_split_apks(aab_path:, &block)
check_bundletool_installed!
UI.message("[App Size Metrics] Generating the various APK splits from #{aab_path}...")
Dir.mktmpdir('release-toolkit-android-app-size-metrics') do |tmp_dir|
Action.sh('bundletool', 'build-apks', '--bundle', aab_path, '--output-format', 'DIRECTORY', '--output', tmp_dir)
apks = Dir.glob('splits/*.apk', base: tmp_dir).map { |f| File.join(tmp_dir, f) }
UI.message("[App Size Metrics] Generated #{apks.length} APKs.")
apks.each(&block)
UI.message('[App Size Metrics] Done computing splits sizes.')
end
end
end

#####################################################
Expand All @@ -95,6 +152,7 @@ def self.details
DETAILS
end

# rubocop:disable Metrics/MethodLength
def self.available_options
[
FastlaneCore::ConfigItem.new(
Expand Down Expand Up @@ -165,7 +223,7 @@ def self.available_options
env_name: 'FL_ANDROID_SEND_APP_SIZE_METRICS_AAB_PATH',
description: 'The path to the .aab to extract size information from',
type: String,
optional: false,
optional: true, # We can have `aab_path` only, or `universal_apk_path` only, or both (but not none)
verify_block: proc do |value|
UI.user_error!('You must provide an path to an existing `.aab` file') unless File.exist?(value)
end
Expand All @@ -178,6 +236,16 @@ def self.available_options
type: FastlaneCore::Boolean,
default_value: true
),
FastlaneCore::ConfigItem.new(
key: :universal_apk_path,
env_name: 'FL_ANDROID_SEND_APP_SIZE_METRICS_UNIVERSAL_APK_PATH',
description: 'The path to the Universal `.apk` to extract size information from',
type: String,
optional: true, # We can have `aab_path` only, or `universal_apk_path` only, or both (but not none)
verify_block: proc do |value|
UI.user_error!('You must provide a path to an existing `.apk` file') unless File.exist?(value)
end
),
FastlaneCore::ConfigItem.new(
key: :apkanalyzer_binary,
env_name: 'FL_ANDROID_SEND_APP_SIZE_METRICS_APKANALYZER_BINARY',
Expand All @@ -191,6 +259,7 @@ def self.available_options
),
]
end
# rubocop:enable Metrics/MethodLength

def self.return_type
:integer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
module Fastlane
module Actions
class IosSendAppSizeMetricsAction < Action
# Keys used by the metrics payload
IPA_FILE_SIZE_KEY = 'File Size'.freeze # value from `File.size` of the Universal `.ipa`
IPA_DOWNLOAD_SIZE_KEY = 'Download Size'.freeze # value from `app-thinning.plist`
IPA_INSTALL_SIZE_KEY = 'Install Size'.freeze # value from `app-thinning.plist`

def self.run(params)
# Check input parameters
api_url = URI(params[:api_url])
Expand All @@ -20,7 +25,7 @@ def self.run(params)
'Build Type': params[:build_type],
Source: params[:source]
)
metrics_helper.add_metric(name: 'File Size', value: File.size(params[:ipa_path]))
metrics_helper.add_metric(name: IPA_FILE_SIZE_KEY, value: File.size(params[:ipa_path]))

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

0 comments on commit d804711

Please sign in to comment.