From 50e7811bd8c0db13b4ba683e928c916c70c94fef Mon Sep 17 00:00:00 2001 From: Jaret Bottoms <25535214+jbbottoms@users.noreply.github.com> Date: Sun, 20 Sep 2026 18:22:52 -0500 Subject: [PATCH] fix(cli): default to apksigner for APK builds --- cli/src/declarations.ts | 5 ++- cli/src/index.ts | 8 ++-- cli/src/tasks/build.ts | 8 +++- cli/test/build.signing.spec.ts | 73 ++++++++++++++++++++++++++++++++++ 4 files changed, 86 insertions(+), 8 deletions(-) create mode 100644 cli/test/build.signing.spec.ts diff --git a/cli/src/declarations.ts b/cli/src/declarations.ts index 2cee84996b..9da5543a09 100644 --- a/cli/src/declarations.ts +++ b/cli/src/declarations.ts @@ -272,10 +272,11 @@ export interface CapacitorConfig { releaseType?: 'AAB' | 'APK'; /** - * Program to sign your build with + * Program to sign your build with. Defaults to `apksigner` for APKs and + * `jarsigner` for Android App Bundles. `apksigner` must be on your PATH + * when used. * * @since 5.1.0 - * @default "jarsigner" */ signingType?: 'apksigner' | 'jarsigner'; }; diff --git a/cli/src/index.ts b/cli/src/index.ts index fb70e1039b..5f755df20c 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -150,10 +150,10 @@ export function runProgram(config: Config): void { ]), ) .addOption( - new Option('--signing-type ', 'Program used to sign apps (default: jarsigner)').choices([ - 'apksigner', - 'jarsigner', - ]), + new Option( + '--signing-type ', + 'Program used to sign apps (default: apksigner for APK, jarsigner for AAB)', + ).choices(['apksigner', 'jarsigner']), ) .addOption( new Option('--xcode-team-id ', 'The Developer team to use for building and exporting the archive'), diff --git a/cli/src/tasks/build.ts b/cli/src/tasks/build.ts index c7e6b24159..f33adaff65 100644 --- a/cli/src/tasks/build.ts +++ b/cli/src/tasks/build.ts @@ -38,6 +38,7 @@ export async function buildCommand( ); } + const androidReleaseType = buildOptions.androidreleasetype || config.android.buildOptions.releaseType || 'AAB'; const buildCommandOptions: BuildCommandOptions = { scheme: buildOptions.scheme || config.ios.scheme, flavor: buildOptions.flavor || config.android.flavor, @@ -45,8 +46,11 @@ export async function buildCommand( keystorepass: buildOptions.keystorepass || config.android.buildOptions.keystorePassword, keystorealias: buildOptions.keystorealias || config.android.buildOptions.keystoreAlias, keystorealiaspass: buildOptions.keystorealiaspass || config.android.buildOptions.keystoreAliasPassword, - androidreleasetype: buildOptions.androidreleasetype || config.android.buildOptions.releaseType || 'AAB', - signingtype: buildOptions.signingtype || config.android.buildOptions.signingType || 'jarsigner', + androidreleasetype: androidReleaseType, + signingtype: + buildOptions.signingtype || + config.android.buildOptions.signingType || + (androidReleaseType === 'APK' ? 'apksigner' : 'jarsigner'), configuration: buildOptions.configuration || 'Release', xcodeTeamId: buildOptions.xcodeTeamId || config.ios.buildOptions.teamId, xcodeExportMethod: diff --git a/cli/test/build.signing.spec.ts b/cli/test/build.signing.spec.ts new file mode 100644 index 0000000000..bf1b001bbe --- /dev/null +++ b/cli/test/build.signing.spec.ts @@ -0,0 +1,73 @@ +import { join } from 'path'; + +import type { Config } from '../src/definitions'; +import { buildCommand } from '../src/tasks/build'; +import type { BuildCommandOptions } from '../src/tasks/build'; +import { runCommand } from '../src/util/subprocess'; + +jest.mock('../src/common', () => ({ + selectPlatforms: jest.fn().mockResolvedValue(['android']), + runTask: jest.fn((_title, task) => task()), +})); +jest.mock('../src/log', () => ({ logSuccess: jest.fn() })); +jest.mock('../src/util/subprocess', () => ({ runCommand: jest.fn().mockResolvedValue('') })); + +describe('Android build signing defaults', () => { + beforeEach(() => jest.clearAllMocks()); + + it.each([ + ['default bundle', {}, {}, 'AAB', 'jarsigner'], + ['CLI APK', { androidreleasetype: 'APK' }, {}, 'APK', 'apksigner'], + ['configured APK', {}, { releaseType: 'APK' }, 'APK', 'apksigner'], + ['CLI AAB overrides configured APK', { androidreleasetype: 'AAB' }, { releaseType: 'APK' }, 'AAB', 'jarsigner'], + ['CLI APK overrides configured AAB', { androidreleasetype: 'APK' }, { releaseType: 'AAB' }, 'APK', 'apksigner'], + ['explicit CLI jarsigner', { androidreleasetype: 'APK', signingtype: 'jarsigner' }, {}, 'APK', 'jarsigner'], + ['configured jarsigner', {}, { releaseType: 'APK', signingType: 'jarsigner' }, 'APK', 'jarsigner'], + [ + 'CLI signer overrides configured signer', + { signingtype: 'apksigner' }, + { releaseType: 'APK', signingType: 'jarsigner' }, + 'APK', + 'apksigner', + ], + ] as const)('%s', async (_name, cliOptions, configuredOptions, releaseType, signer) => { + const config = { + android: { + name: 'android', + platformDirAbs: '/test/android', + appDirAbs: '/test/android/app', + buildOptions: { + keystorePath: 'test.jks', + keystorePassword: 'test-password', + keystoreAlias: 'test', + keystoreAliasPassword: 'test-password', + ...configuredOptions, + }, + }, + ios: { name: 'ios', buildOptions: {} }, + } as Config; + + await buildCommand(config, 'android', { configuration: 'Release', ...cliOptions } as BuildCommandOptions); + + expect(runCommand).toHaveBeenCalledTimes(2); + expect(runCommand).toHaveBeenNthCalledWith( + 1, + './gradlew', + [releaseType === 'APK' ? 'assembleRelease' : ':app:bundleRelease'], + { cwd: '/test/android' }, + ); + expect(runCommand).toHaveBeenNthCalledWith( + 2, + signer, + expect.arrayContaining([ + join( + '/test/android/app/build/outputs', + releaseType === 'APK' ? 'apk' : 'bundle', + 'release', + `app-release-signed.${releaseType.toLowerCase()}`, + ), + ]), + { cwd: '/test/android' }, + ); + }); +});