-
Notifications
You must be signed in to change notification settings - Fork 9
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
Changes from all commits
d5cd729
f9b2fc0
d27847d
96df190
3dbfd45
dec39d8
16e5050
f99c55e
1f43c4c
0e801e1
b28c1f0
1d566d1
2d499f9
658e8f6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,149 @@ | ||||||||
module Fastlane | ||||||||
module Actions | ||||||||
class AndroidGenerateApkFromAabAction < Action | ||||||||
def self.run(params) | ||||||||
begin | ||||||||
sh('command', '-v', 'bundletool', print_command: false, print_command_output: false) | ||||||||
rescue StandardError | ||||||||
UI.user_error!(MISSING_BUNDLETOOL_ERROR_MESSAGE) | ||||||||
end | ||||||||
|
||||||||
# Parse input parameters | ||||||||
aab_file_path = parse_aab_param(params) | ||||||||
apk_output_file_path = params[:apk_output_file_path] || Pathname(aab_file_path).sub_ext('.apk').to_s | ||||||||
apk_output_file_path = File.join(apk_output_file_path, "#{File.basename(aab_file_path, '.aab')}.apk") if File.directory?(apk_output_file_path) | ||||||||
code_sign_arguments = { | ||||||||
'--ks': params[:keystore_path], | ||||||||
'--ks-pass': params[:keystore_password], | ||||||||
'--ks-key-alias': params[:keystore_key_alias], | ||||||||
'--key-pass': params[:signing_key_password] | ||||||||
}.compact.flatten.map(&:to_s) | ||||||||
|
||||||||
Dir.mktmpdir('a8c-release-toolkit-bundletool-') do |tmpdir| | ||||||||
sh( | ||||||||
'bundletool', 'build-apks', | ||||||||
'--mode', 'universal', | ||||||||
'--bundle', aab_file_path, | ||||||||
'--output-format', 'DIRECTORY', | ||||||||
'--output', tmpdir, | ||||||||
*code_sign_arguments | ||||||||
) | ||||||||
FileUtils.mkdir_p(File.dirname(apk_output_file_path)) # Create destination directory if it doesn't exist yet | ||||||||
FileUtils.mv(File.join(tmpdir, 'universal.apk'), apk_output_file_path) | ||||||||
end | ||||||||
|
||||||||
apk_output_file_path | ||||||||
end | ||||||||
|
||||||||
##################################################### | ||||||||
|
||||||||
def self.parse_aab_param(params) | ||||||||
# If no AAB param was provided, attempt to get it from the lane context | ||||||||
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_ERROR_MESSAGE) | ||||||||
elsif !File.file?(aab_file_path) | ||||||||
UI.user_error!("The file `#{aab_file_path}` was not found. Please provide a path to an existing file.") | ||||||||
end | ||||||||
|
||||||||
aab_file_path | ||||||||
end | ||||||||
|
||||||||
MISSING_BUNDLETOOL_ERROR_MESSAGE = 'bundletool is not installed. Please install it using the instructions at https://developer.android.com/studio/command-line/bundletool.'.freeze | ||||||||
NO_AAB_ERROR_MESSAGE = '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 `gradle` action has been run prior to this action.'.freeze | ||||||||
|
||||||||
##################################################### | ||||||||
# @!group Documentation | ||||||||
##################################################### | ||||||||
|
||||||||
def self.description | ||||||||
'Generates an APK from the specified AAB' | ||||||||
end | ||||||||
|
||||||||
def self.details | ||||||||
'Generates an APK file from the specified AAB file using `bundletool`' | ||||||||
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 specified, 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 | ||||||||
), | ||||||||
FastlaneCore::ConfigItem.new( | ||||||||
key: :apk_output_file_path, | ||||||||
env_name: 'ANDROID_APK_OUTPUT_PATH', | ||||||||
description: 'The path of the output APK file to generate. If not specified, will use the same path and basename as the `aab_file_path` but with an `.apk` file extension', | ||||||||
type: String, | ||||||||
optional: true, | ||||||||
default_value: nil | ||||||||
), | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
|
||||||||
FastlaneCore::ConfigItem.new( | ||||||||
key: :keystore_path, | ||||||||
env_name: 'ANDROID_KEYSTORE_PATH', | ||||||||
description: 'The path to the keystore file (if you want to codesign the APK)', | ||||||||
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 p.nil? || File.file?(p) } | ||||||||
), | ||||||||
FastlaneCore::ConfigItem.new( | ||||||||
key: :keystore_password, | ||||||||
env_name: 'ANDROID_KEYSTORE_PASSWORD', | ||||||||
description: 'The password for the keystore (if you want to codesign the APK)', | ||||||||
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 (if you want to codesign the APK)', | ||||||||
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 (if you want to codesign the APK)', | ||||||||
type: String, | ||||||||
optional: true, | ||||||||
default_value: nil | ||||||||
), | ||||||||
] | ||||||||
end | ||||||||
|
||||||||
def self.return_type | ||||||||
:string | ||||||||
end | ||||||||
|
||||||||
def self.return_value | ||||||||
'The path to the APK that has been generated' | ||||||||
end | ||||||||
|
||||||||
def self.authors | ||||||||
['Automattic'] | ||||||||
end | ||||||||
|
||||||||
def self.is_supported?(platform) | ||||||||
platform == 'android' | ||||||||
end | ||||||||
end | ||||||||
end | ||||||||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
require 'spec_helper' | ||
|
||
describe Fastlane::Actions::AndroidGenerateApkFromAabAction do | ||
let(:aab_file_path) { 'Dev/My App/my-app.aab' } | ||
let(:apk_output_file_path) { 'Dev/My App/build/artifacts/my-universal-app.apk' } | ||
|
||
def expect_bundletool_call(aab, apk, *options) | ||
expect(Fastlane::Action).to receive('sh').with('command', '-v', 'bundletool', { print_command: false, print_command_output: false }) | ||
allow(File).to receive(:file?).with(aab).and_return(true) | ||
expect(Fastlane::Action).to receive('sh').with( | ||
'bundletool', 'build-apks', '--mode', 'universal', '--bundle', aab, | ||
'--output-format', 'DIRECTORY', '--output', anything, | ||
*options | ||
) | ||
expect(FileUtils).to receive(:mkdir_p).with(File.dirname(apk)) | ||
expect(FileUtils).to receive(:mv).with(anything, apk) | ||
end | ||
|
||
context 'when generating a signed APK' do | ||
let(:keystore_path) { 'Dev/My App/secrets/path/to/keystore' } | ||
let(:keystore_password) { 'keystore_password' } | ||
let(:keystore_key_alias) { 'keystore_key_alias' } | ||
let(:signing_key_password) { 'signing_key_password' } | ||
|
||
it 'calls the `bundletool` command with the correct arguments' do | ||
allow(File).to receive(:file?).with(keystore_path).and_return(true) | ||
expect_bundletool_call( | ||
aab_file_path, apk_output_file_path, | ||
'--ks', keystore_path, '--ks-pass', keystore_password, '--ks-key-alias', keystore_key_alias, '--key-pass', signing_key_password | ||
) | ||
|
||
output = 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 | ||
) | ||
|
||
expect(output).to eq(apk_output_file_path) | ||
end | ||
end | ||
|
||
context 'when generating an unsigned APK' do | ||
it 'calls the `bundletool` command with the correct arguments' do | ||
expect_bundletool_call(aab_file_path, apk_output_file_path) | ||
|
||
output = run_described_fastlane_action( | ||
aab_file_path: aab_file_path, | ||
apk_output_file_path: apk_output_file_path | ||
) | ||
|
||
expect(output).to eq(apk_output_file_path) | ||
end | ||
end | ||
|
||
describe 'parameter inference' do | ||
it 'infers the AAB path from lane context if `SharedValues::GRADLE_AAB_OUTPUT_PATH` is set' do | ||
aab_path_from_context = 'path/from/context/my-app.aab' | ||
Fastlane::Actions.lane_context[Fastlane::Actions::SharedValues::GRADLE_AAB_OUTPUT_PATH] = aab_path_from_context | ||
|
||
expect_bundletool_call(aab_path_from_context, apk_output_file_path) | ||
run_described_fastlane_action( | ||
apk_output_file_path: apk_output_file_path | ||
) | ||
|
||
Fastlane::Actions.lane_context[Fastlane::Actions::SharedValues::GRADLE_AAB_OUTPUT_PATH] = nil | ||
end | ||
|
||
it 'infers the AAB path from lane context if `SharedValues::GRADLE_ALL_AAB_OUTPUT_PATHS` is set with only one value' do | ||
aab_paths_from_context = ['first/path/from/context/app.aab'] | ||
Fastlane::Actions.lane_context[Fastlane::Actions::SharedValues::GRADLE_ALL_AAB_OUTPUT_PATHS] = aab_paths_from_context | ||
|
||
expect_bundletool_call(aab_paths_from_context.first, apk_output_file_path) | ||
run_described_fastlane_action( | ||
apk_output_file_path: apk_output_file_path | ||
) | ||
|
||
Fastlane::Actions.lane_context[Fastlane::Actions::SharedValues::GRADLE_ALL_AAB_OUTPUT_PATHS] = nil | ||
end | ||
|
||
it 'does not infer the AAB path from lane context if `SharedValues::GRADLE_AAB_OUTPUT_PATHS` has more than one value' do | ||
aab_paths_from_context = ['first/path/from/context/app.aab', 'second/path/from/context/app.aab'] | ||
Fastlane::Actions.lane_context[Fastlane::Actions::SharedValues::GRADLE_ALL_AAB_OUTPUT_PATHS] = aab_paths_from_context | ||
|
||
expect do | ||
run_described_fastlane_action( | ||
apk_output_file_path: apk_output_file_path | ||
) | ||
end.to raise_error(FastlaneCore::Interface::FastlaneError, described_class::NO_AAB_ERROR_MESSAGE) | ||
|
||
Fastlane::Actions.lane_context[Fastlane::Actions::SharedValues::GRADLE_ALL_AAB_OUTPUT_PATHS] = nil | ||
end | ||
|
||
it 'infers the output path if none is provided' do | ||
inferred_apk_path = File.join(File.dirname(aab_file_path), "#{File.basename(aab_file_path, '.aab')}.apk") | ||
expect_bundletool_call(aab_file_path, inferred_apk_path) | ||
|
||
output = run_described_fastlane_action( | ||
aab_file_path: aab_file_path | ||
) | ||
|
||
expect(output).to eq(inferred_apk_path) | ||
end | ||
|
||
it 'infers the output file name if output path is a directory' do | ||
in_tmp_dir do |output_dir| | ||
inferred_apk_path = File.join(output_dir, "#{File.basename(aab_file_path, '.aab')}.apk") | ||
expect_bundletool_call(aab_file_path, inferred_apk_path) | ||
|
||
output = run_described_fastlane_action( | ||
aab_file_path: aab_file_path, | ||
apk_output_file_path: output_dir | ||
) | ||
|
||
expect(output).to eq(inferred_apk_path) | ||
end | ||
end | ||
end | ||
|
||
describe 'error handling' do | ||
it 'errors if bundletool is not installed' do | ||
allow(Fastlane::Action).to receive('sh').with('command', '-v', 'bundletool', any_args).and_raise | ||
|
||
expect do | ||
run_described_fastlane_action( | ||
aab_file_path: aab_file_path, | ||
apk_output_file_path: apk_output_file_path | ||
) | ||
end.to raise_error(FastlaneCore::Interface::FastlaneError, described_class::MISSING_BUNDLETOOL_ERROR_MESSAGE) | ||
end | ||
|
||
it 'errors if no input AAB file was provided nor can be inferred' do | ||
expect(Fastlane::Action).to receive('sh').with('command', '-v', 'bundletool', any_args) | ||
|
||
expect do | ||
run_described_fastlane_action( | ||
apk_output_file_path: apk_output_file_path | ||
) | ||
end.to raise_error(FastlaneCore::Interface::FastlaneError, described_class::NO_AAB_ERROR_MESSAGE) | ||
end | ||
|
||
it 'errors if the provided input AAB file does not exist' do | ||
expect(Fastlane::Action).to receive('sh').with('command', '-v', 'bundletool', any_args) | ||
allow(File).to receive(:file?).with(aab_file_path).and_return(false) | ||
|
||
expect do | ||
run_described_fastlane_action( | ||
aab_file_path: aab_file_path, | ||
apk_output_file_path: apk_output_file_path | ||
) | ||
end.to raise_error(FastlaneCore::Interface::FastlaneError, "The file `#{aab_file_path}` was not found. Please provide a path to an existing file.") | ||
end | ||
end | ||
end |
Uh oh!
There was an error while loading. Please reload this page.