From 559ff48d987d3f19e40b5515de7b42a4e42d5a3d Mon Sep 17 00:00:00 2001 From: zhanglangning Date: Tue, 11 Aug 2026 23:48:42 +0800 Subject: [PATCH] fix(cli): reject apksigner for app bundles --- cli/src/android/build.ts | 8 ++- cli/test/build.android.spec.ts | 100 +++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 cli/test/build.android.spec.ts diff --git a/cli/src/android/build.ts b/cli/src/android/build.ts index f5b31a2116..61316adef6 100644 --- a/cli/src/android/build.ts +++ b/cli/src/android/build.ts @@ -10,6 +10,12 @@ import { runCommand } from '../util/subprocess'; export async function buildAndroid(config: Config, buildOptions: BuildCommandOptions): Promise { const releaseType = buildOptions.androidreleasetype ?? 'AAB'; const releaseTypeIsAAB = releaseType === 'AAB'; + const signingType = buildOptions.signingtype ?? 'jarsigner'; + + if (releaseTypeIsAAB && signingType === 'apksigner') { + throw 'apksigner cannot sign Android App Bundles. Use jarsigner by setting android.buildOptions.signingType to "jarsigner" or passing "--signing-type jarsigner". Alternatively, build an APK by setting android.buildOptions.releaseType to "APK" or passing "--androidreleasetype APK".'; + } + const flavor = buildOptions.flavor ?? ''; const arg = releaseTypeIsAAB ? `:app:bundle${flavor}Release` : `assemble${flavor}Release`; const gradleArgs = [arg]; @@ -55,7 +61,7 @@ export async function buildAndroid(config: Config, buildOptions: BuildCommandOpt `-release-signed.${releaseType.toLowerCase()}`, ); - if (buildOptions.signingtype == 'jarsigner') { + if (signingType === 'jarsigner') { await signWithJarSigner(config, buildOptions, releasePath, signedReleaseName, unsignedReleaseName); } else { await signWithApkSigner(config, buildOptions, releasePath, signedReleaseName, unsignedReleaseName); diff --git a/cli/test/build.android.spec.ts b/cli/test/build.android.spec.ts new file mode 100644 index 0000000000..7dd7447a4a --- /dev/null +++ b/cli/test/build.android.spec.ts @@ -0,0 +1,100 @@ +import { buildAndroid } from '../src/android/build'; +import type { AndroidConfig, Config } from '../src/definitions'; +import type { BuildCommandOptions } from '../src/tasks/build'; +import { runCommand } from '../src/util/subprocess'; + +jest.mock('../src/util/subprocess', () => ({ + runCommand: jest.fn(), +})); + +const mockedRunCommand = runCommand as jest.MockedFunction; + +function makeConfig(android: Partial): Config { + return { android } as unknown as Config; +} + +const config = makeConfig({ + platformDirAbs: '/tmp/app/android', + appDirAbs: '/tmp/app/android/app', +}); + +const signingOptions: BuildCommandOptions = { + keystorepath: '/tmp/release.keystore', + keystorepass: 'store-password', + keystorealias: 'release', + keystorealiaspass: 'key-password', + configuration: 'Release', +}; + +describe('buildAndroid', () => { + beforeEach(() => { + mockedRunCommand.mockReset(); + }); + + it('rejects apksigner for AAB before running external commands', async () => { + await expect( + buildAndroid(config, { + ...signingOptions, + signingtype: 'apksigner', + }), + ).rejects.toBe( + 'apksigner cannot sign Android App Bundles. Use jarsigner by setting android.buildOptions.signingType to "jarsigner" or passing "--signing-type jarsigner". Alternatively, build an APK by setting android.buildOptions.releaseType to "APK" or passing "--androidreleasetype APK".', + ); + expect(mockedRunCommand).not.toHaveBeenCalled(); + }); + + it.each<{ + name: string; + options: Partial; + gradleTask: string; + signingTool: 'apksigner' | 'jarsigner'; + unsignedArtifact: string; + signedArtifact: string; + }>([ + { + name: 'uses jarsigner for the default AAB build', + options: {}, + gradleTask: ':app:bundleRelease', + signingTool: 'jarsigner', + unsignedArtifact: '/tmp/app/android/app/build/outputs/bundle/release/app-release.aab', + signedArtifact: '/tmp/app/android/app/build/outputs/bundle/release/app-release-signed.aab', + }, + { + name: 'uses jarsigner for an explicit AAB build', + options: { androidreleasetype: 'AAB', signingtype: 'jarsigner' }, + gradleTask: ':app:bundleRelease', + signingTool: 'jarsigner', + unsignedArtifact: '/tmp/app/android/app/build/outputs/bundle/release/app-release.aab', + signedArtifact: '/tmp/app/android/app/build/outputs/bundle/release/app-release-signed.aab', + }, + { + name: 'uses apksigner for an APK build when requested', + options: { androidreleasetype: 'APK', signingtype: 'apksigner' }, + gradleTask: 'assembleRelease', + signingTool: 'apksigner', + unsignedArtifact: '/tmp/app/android/app/build/outputs/apk/release/app-release-unsigned.apk', + signedArtifact: '/tmp/app/android/app/build/outputs/apk/release/app-release-signed.apk', + }, + { + name: 'uses jarsigner for an APK build when requested', + options: { androidreleasetype: 'APK', signingtype: 'jarsigner' }, + gradleTask: 'assembleRelease', + signingTool: 'jarsigner', + unsignedArtifact: '/tmp/app/android/app/build/outputs/apk/release/app-release-unsigned.apk', + signedArtifact: '/tmp/app/android/app/build/outputs/apk/release/app-release-signed.apk', + }, + ])('$name', async ({ options, gradleTask, signingTool, unsignedArtifact, signedArtifact }) => { + await expect(buildAndroid(config, { ...signingOptions, ...options })).resolves.toBeUndefined(); + + expect(mockedRunCommand).toHaveBeenCalledTimes(2); + expect(mockedRunCommand).toHaveBeenNthCalledWith(1, './gradlew', [gradleTask], { + cwd: '/tmp/app/android', + }); + expect(mockedRunCommand).toHaveBeenNthCalledWith( + 2, + signingTool, + expect.arrayContaining([unsignedArtifact, signedArtifact]), + { cwd: '/tmp/app/android' }, + ); + }); +});