Skip to content

Add android_generate_apk_from_aab action #467

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 14 commits into from
Closed
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ _None_
### New Features

- Add `ios_get_build_number` action to get the current build number from an `xcconfig` file. [#458]

- Add new `android_generate_apk_from_aab` action to generate an APK from a given AAB.

### Bug Fixes

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
module Fastlane
module Actions
class AndroidGenerateApkFromAabAction < Action
def self.generate_command(aab_file_path, apk_output_file_path, keystore_path, keystore_password, keystore_key_alias, signing_key_password)
command = "bundletool build-apks --mode universal --bundle #{aab_file_path} --output-format DIRECTORY --output #{apk_output_file_path} "
code_sign_arguments = "--ks #{keystore_path} --ks-pass #{keystore_password} --ks-key-alias #{keystore_key_alias} --key-pass #{signing_key_password} "
move_and_cleanup_command = "&& mv #{apk_output_file_path}/universal.apk #{apk_output_file_path}_tmp && rm -rf #{apk_output_file_path} && mv #{apk_output_file_path}_tmp #{apk_output_file_path}"

# Attempt to code sign the APK if a keystore_path parameter is specified
command += code_sign_arguments unless keystore_path.nil?

# Move and rename the universal.apk file to the specified output path and cleanup the directory created by bundletool
command += move_and_cleanup_command

return command
end

def self.run(params)
begin
sh('command -v bundletool > /dev/null')
rescue StandardError
UI.user_error!('bundletool is not installed. Please install it using the instructions at https://developer.android.com/studio/command-line/bundletool.')
raise
end

# If no AAB param was provided, attempt to get it from the lane context
# First GRADLE_ALL_AAB_OUTPUT_PATHS if only one
# Second GRADLE_AAB_OUTPUT_PATH if it is set
# Else use the specified parameter value
if params[:aab_file_path].nil?
all_aab_paths = Actions.lane_context[SharedValues::GRADLE_ALL_AAB_OUTPUT_PATHS] || []
aab_file_path = if all_aab_paths.count == 1
all_aab_paths.first
else
Actions.lane_context[SharedValues::GRADLE_AAB_OUTPUT_PATH]
end
else
aab_file_path = params[:aab_file_path]
end

# If no AAB file path was found, raise an error
if aab_file_path.nil?
UI.user_error!('No AAB file path was specified and none was found in the lane context. Please specify the `aab_file_path` parameter or ensure that the relevant build action has been run prior to this action.')
raise
end

apk_output_file_path = params[:apk_output_file_path]
keystore_path = params[:keystore_path]
keystore_password = params[:keystore_password]
keystore_key_alias = params[:keystore_key_alias]
signing_key_password = params[:signing_key_password]

sh(generate_command(aab_file_path, apk_output_file_path, keystore_path, keystore_password, keystore_key_alias, signing_key_password))
end

#####################################################
# @!group Documentation
#####################################################

def self.description
'Generates an APK from the specified AAB'
end

def self.details
'Generates an APK from the specified AAB'
end

def self.available_options
[
FastlaneCore::ConfigItem.new(
key: :aab_file_path,
env_name: 'ANDROID_AAB_FILE_PATH',
description: 'The path to the AAB file. If not speicified, the action will attempt to read from the lane context using the `SharedValues::GRADLE_ALL_AAB_OUTPUT_PATHS` and `SharedValues::GRADLE_AAB_OUTPUT_PATH` keys',
type: String,
optional: true,
default_value: nil,
verify_block: proc { |p| UI.user_error!("AAB path `#{p}` is not a valid file path.") unless File.file?(p) }
),
FastlaneCore::ConfigItem.new(
key: :apk_output_file_path,
env_name: 'ANDROID_APK_OUTPUT_PATH',
description: 'The output path where the APK file will be generated. The directory will be created if it does not yet exist',
type: String,
optional: false,
default_value: nil
),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we decided to not create intermediate folders if they don't exist, we should add a check to ensure the parent dir exists. Otherwise we'd still have the action run bundletool but only fail afterwards while trying to move the file to the destination, instead of failing early.

Suggested change
),
verify_block: proc { |p| UI.user_error!('The parent folder for the destination does not exist. Please create it first.') unless p.nil? || File.directory?(File.dirname(p)) }
),

FastlaneCore::ConfigItem.new(
key: :keystore_path,
env_name: 'ANDROID_KEYSTORE_PATH',
description: 'The path to the keystore file',
type: String,
optional: true,
default_value: nil,
verify_block: proc { |p| UI.user_error!("Keystore file path `#{p}` is not a valid file path.") unless File.file?(p) || p.nil }
),
FastlaneCore::ConfigItem.new(
key: :keystore_password,
env_name: 'ANDROID_KEYSTORE_PASSWORD',
description: 'The password for the keystore',
type: String,
optional: true,
default_value: nil
),
FastlaneCore::ConfigItem.new(
key: :keystore_key_alias,
env_name: 'ANDROID_KEYSTORE_KEY_ALIAS',
description: 'The alias of the key in the keystore',
type: String,
optional: true,
default_value: nil
),
FastlaneCore::ConfigItem.new(
key: :signing_key_password,
env_name: 'ANDROID_SIGNING_KEY_PASSWORD',
description: 'The password for the signing key',
type: String,
optional: true,
default_value: nil
),
]
end

def self.authors
['Automattic']
end

def self.is_supported?(platform)
true
end
end
end
end
87 changes: 87 additions & 0 deletions spec/android_generate_apk_from_aab_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
require 'spec_helper'

describe Fastlane::Actions::AndroidGenerateApkFromAabAction do
before do
allow(File).to receive(:file?).with(aab_file_path).and_return('mocked file data')
allow(File).to receive(:file?).with(apk_output_file_path).and_return('mocked file data')
allow(File).to receive(:file?).with(keystore_path).and_return('mocked file data')
end

let(:aab_file_path) { 'path/to/app.aab' }
let(:apk_output_file_path) { 'path/to/app.apk' }
let(:keystore_path) { 'path/to/keystore' }
let(:keystore_password) { 'keystore_password' }
let(:keystore_key_alias) { 'keystore_key_alias' }
let(:signing_key_password) { 'signing_key_password' }

def generate_command(apk_output_file_path:, aab_file_path: nil, keystore_path: nil, keystore_password: nil, keystore_key_alias: nil, signing_key_password: nil)
command = "bundletool build-apks --mode universal --bundle #{aab_file_path} --output-format DIRECTORY --output #{apk_output_file_path} "
code_sign_arguments = "--ks #{keystore_path} --ks-pass #{keystore_password} --ks-key-alias #{keystore_key_alias} --key-pass #{signing_key_password} "
move_and_cleanup_command = "&& mv #{apk_output_file_path}/universal.apk #{apk_output_file_path}_tmp && rm -rf #{apk_output_file_path} && mv #{apk_output_file_path}_tmp #{apk_output_file_path}"

# Append the code signing arguments
command += code_sign_arguments unless keystore_path.nil?

# Append the move and cleanup command
command += move_and_cleanup_command
return command
end

describe 'android_generate_apk_from_aab' do
it 'calls the `bundletool` command with the correct arguments when generating a signed APK' do
cmd = run_described_fastlane_action(
aab_file_path: aab_file_path,
apk_output_file_path: apk_output_file_path,
keystore_path: keystore_path,
keystore_password: keystore_password,
keystore_key_alias: keystore_key_alias,
signing_key_password: signing_key_password
)
expected_command = generate_command(aab_file_path: aab_file_path,
apk_output_file_path: apk_output_file_path,
keystore_path: keystore_path,
keystore_password: keystore_password,
keystore_key_alias: keystore_key_alias,
signing_key_password: signing_key_password)
expect(cmd).to eq(expected_command)
end

it 'calls the `bundletool` command with the correct arguments when generating an unsigned APK' do
cmd = run_described_fastlane_action(
aab_file_path: aab_file_path,
apk_output_file_path: apk_output_file_path
)
expected_command = generate_command(aab_file_path: aab_file_path,
apk_output_file_path: apk_output_file_path)
expect(cmd).to eq(expected_command)
end

it 'calls the `bundletool` command with the correct arguments and use the path to the AAB from the lane context if the SharedValues::GRADLE_AAB_OUTPUT_PATH key is set' do
aab_path_from_context = 'path/from/context/app.aab'

Fastlane::Actions.lane_context[Fastlane::Actions::SharedValues::GRADLE_AAB_OUTPUT_PATH] = aab_path_from_context

cmd = run_described_fastlane_action(
apk_output_file_path: apk_output_file_path
)

expected_command = generate_command(aab_file_path: aab_path_from_context,
apk_output_file_path: apk_output_file_path)
expect(cmd).to eq(expected_command)
end

it 'calls the `bundletool` command with the correct arguments and use the path to the AAB from the lane context if the SharedValues::GRADLE_ALL_AAB_OUTPUT_PATHS key is set' do
all_aab_paths_from_context = ['first/path/from/context/app.aab']

Fastlane::Actions.lane_context[Fastlane::Actions::SharedValues::GRADLE_ALL_AAB_OUTPUT_PATHS] = all_aab_paths_from_context

cmd = run_described_fastlane_action(
apk_output_file_path: apk_output_file_path
)

expected_command = generate_command(aab_file_path: all_aab_paths_from_context.first,
apk_output_file_path: apk_output_file_path)
expect(cmd).to eq(expected_command)
end
end
end