diff --git a/Dockerfile b/Dockerfile index 9ff76d88..f0f82f08 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,4 +15,4 @@ RUN yes | bubblewrap doctor WORKDIR /app -ENTRYPOINT ["bubblewrap"] + diff --git a/packages/cli/src/lib/cmds/build.ts b/packages/cli/src/lib/cmds/build.ts index b4bfed0c..6d529837 100644 --- a/packages/cli/src/lib/cmds/build.ts +++ b/packages/cli/src/lib/cmds/build.ts @@ -43,7 +43,7 @@ interface SigningKeyPasswords { keyPassword: string; } -class Build { +export class Build { constructor( private args: ParsedArgs, private androidSdkTools: AndroidSdkTools, @@ -111,9 +111,9 @@ class Build { async signApk(signingKey: SigningKeyInfo, passwords: SigningKeyPasswords): Promise { await this.androidSdkTools.apksigner( signingKey.path, - `"${passwords.keystorePassword}"`, + passwords.keystorePassword, signingKey.alias, - `"${passwords.keyPassword}"`, + passwords.keyPassword, APK_ALIGNED_FILE_NAME, // input file path APK_SIGNED_FILE_NAME, ); @@ -126,8 +126,8 @@ class Build { async signAppBundle(signingKey: SigningKeyInfo, passwords: SigningKeyPasswords): Promise { await this.jarSigner.sign( signingKey, - `"${passwords.keystorePassword}"`, - `"${passwords.keyPassword}"`, + passwords.keystorePassword, + passwords.keyPassword, APP_BUNDLE_BUILD_OUTPUT_FILE_NAME, APP_BUNDLE_SIGNED_FILE_NAME); } @@ -189,7 +189,7 @@ class Build { passwords = await this.getPasswords(signingKey); signingKey = { ...signingKey, - ...{path: `"${signingKey.path}"`}, // Wrap path in quotes in case there are spaces + ...{path: signingKey.path}, ...(this.args.signingKeyPath ? {path: this.args.signingKeyPath} : null), ...(this.args.signingKeyAlias ? {alias: this.args.signingKeyAlias} : null), }; diff --git a/packages/cli/src/spec/lib/cmds/BuildSpec.ts b/packages/cli/src/spec/lib/cmds/BuildSpec.ts new file mode 100644 index 00000000..92f6129a --- /dev/null +++ b/packages/cli/src/spec/lib/cmds/BuildSpec.ts @@ -0,0 +1,68 @@ +import mock from 'mock-fs'; +import {build as buildCmd, Build} from '../../../lib/cmds/build'; +import * as core from '@bubblewrap/core'; +import {computeChecksum} from '../../../lib/cmds/shared'; +import {MockPrompt} from '../../mock/MockPrompt'; + +describe('build', () => { + describe('#build', () => { + it('passes unquoted passwords to apksigner and jarsigner', async () => { + // Create a minimal twa-manifest.json + const manifest = JSON.stringify({ + packageId: 'com.example.twa', + host: 'example.com', + name: 'Test', + display: 'standalone', + themeColor: '#FFFFFF', + navigationColor: '#000000', + backgroundColor: '#FFFFFF', + startUrl: '/', + signingKey: {path: './android.keystore', alias: 'android'}, + }); + const checksum = computeChecksum(Buffer.from(manifest)); + + mock({ + 'twa-manifest.json': manifest, + 'manifest-checksum.txt': checksum, + }); + + const mockAndroidSdkTools: any = { + checkBuildTools: async () => true, + installBuildTools: async () => {}, + zipalignOnlyVerification: async (_: string) => {}, + apksigner: async (_ks: any, ksPass: string, _alias: any, keyPass: string, _in: any, _out: any) => { + expect(ksPass).toEqual('mystorepass'); + expect(keyPass).toEqual('mykeypass'); + }, + }; + + // Instead of running the full build flow, instantiate Build directly and test signApk + const mockGradleWrapper: any = { + assembleRelease: async () => {}, + bundleRelease: async () => {}, + }; + const mockKeyTool: any = {}; + const mockJarSigner: any = { + sign: async (_signingKey: any, storepass: string, keypass: string) => { + expect(storepass).toEqual('mystorepass'); + expect(keypass).toEqual('mykeypass'); + }, + }; + + // Set env so we bypass interactive prompts + process.env['BUBBLEWRAP_KEYSTORE_PASSWORD'] = 'mystorepass'; + process.env['BUBBLEWRAP_KEY_PASSWORD'] = 'mykeypass'; + + const args = { manifest: 'twa-manifest.json', directory: '.', skipSigning: false } as any; + const buildInstance = new Build(args, mockAndroidSdkTools, mockKeyTool, mockGradleWrapper, + mockJarSigner, undefined, new MockPrompt() as any); + + await buildInstance.signApk({path: './android.keystore', alias: 'android'} as any, + {keystorePassword: 'mystorepass', keyPassword: 'mykeypass'}); + await buildInstance.signAppBundle({path: './android.keystore', alias: 'android'} as any, + {keystorePassword: 'mystorepass', keyPassword: 'mykeypass'}); + + mock.restore(); + }); + }); +});