Skip to content

Commit 0a593f4

Browse files
mlisikbfthymikee
andcommitted
feat: android aab signing (#593)
* feat: android aab signing * feat: android aab signing - docs update * feat: android aab signing - fixes assets path * feat: android aab signing - changeset * feat: android aab signing - missing semicolon * feat: android aab signing - ensures non-empty alias * feat: android aab signing - avoids shadowing node:path * feat: android aab signing - strips jarsigner password input * feat: android aab signing - corrects key-alias message * feat: android aab signing - uses input path for isAab checks * feat: android aab signing - updates changeset * feat: android aab signing - adds note on sign/align sequence * feat: android aab signing - removes aab-specific signing flow * feat: android aab signing - minimizes changes * feat: android aab signing - adds --min-sdk-version arg * feat: android aab signing - adds --min-sdk-version note in the docs * Update packages/platform-android/src/lib/commands/signAndroid/signAndroid.ts Co-authored-by: Michał Pierzchała <[email protected]> * feat: android aab signing - adds applies prettier * feat: android aab signing - moves doc comment * feat: android aab signing - removes reduntant brace * feat: android aab signing - adds comment on default --min-sdk-version * feat: android aab signing - removes --new-sdk-version from sign command --------- Co-authored-by: Michał Pierzchała <[email protected]>
1 parent 0119b27 commit 0a593f4

File tree

4 files changed

+66
-50
lines changed

4 files changed

+66
-50
lines changed

.changeset/whole-tools-occur.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@rock-js/platform-android': patch
3+
---
4+
5+
feat: android aab signing

packages/platform-android/src/lib/commands/signAndroid/command.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { signAndroid } from './signAndroid.js';
33

44
export type SignFlags = {
55
verbose?: boolean;
6-
apk: string;
6+
path: string;
77
output?: string;
88
keystore?: string;
99
keystorePassword?: string;
@@ -16,8 +16,8 @@ export type SignFlags = {
1616

1717
const ARGUMENTS = [
1818
{
19-
name: 'apk',
20-
description: 'APK file path',
19+
name: 'binaryPath',
20+
description: 'Archive (apk or aab) file path',
2121
},
2222
];
2323

@@ -44,7 +44,7 @@ const OPTIONS = [
4444
},
4545
{
4646
name: '--output <string>',
47-
description: 'Path to the output APK file.',
47+
description: 'Path to the output APK/AAB file.',
4848
},
4949
{
5050
name: '--build-jsbundle',
@@ -66,9 +66,9 @@ export const registerSignCommand = (api: PluginApi) => {
6666
description: 'Sign the Android app with modified JS bundle.',
6767
args: ARGUMENTS,
6868
options: OPTIONS,
69-
action: async (apkPath, flags: SignFlags) => {
69+
action: async (binaryPath, flags: SignFlags) => {
7070
await signAndroid({
71-
apkPath,
71+
binaryPath,
7272
keystorePath: flags.keystore,
7373
keystorePassword: flags.keystorePassword,
7474
keyAlias: flags.keyAlias,

packages/platform-android/src/lib/commands/signAndroid/signAndroid.ts

Lines changed: 50 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { findAndroidBuildTool, getAndroidBuildToolsPath } from '../../paths.js';
1616
import { buildJsBundle } from './bundle.js';
1717

1818
export type SignAndroidOptions = {
19-
apkPath: string;
19+
binaryPath: string;
2020
keystorePath?: string;
2121
keystorePassword?: string;
2222
keyAlias?: string;
@@ -30,7 +30,9 @@ export type SignAndroidOptions = {
3030
export async function signAndroid(options: SignAndroidOptions) {
3131
validateOptions(options);
3232

33-
intro(`Modifying APK file`);
33+
const extension = path.extname(options.binaryPath).slice(1);
34+
35+
intro(`Modifying ${extension.toUpperCase()} file`);
3436

3537
const tempPath = getSignOutputPath();
3638
if (fs.existsSync(tempPath)) {
@@ -60,28 +62,28 @@ export async function signAndroid(options: SignAndroidOptions) {
6062
options.jsBundlePath = bundleOutputPath;
6163
}
6264

63-
// 2. Initialize temporary APK file
64-
const tempApkPath = path.join(tempPath, 'output-app.apk');
65+
// 2. Initialize temporary archive file
66+
const tempArchivePath = path.join(tempPath, `output-app.${extension}`);
6567

66-
loader.start('Initializing output APK...');
68+
loader.start(`Initializing output ${extension.toUpperCase()}...`);
6769
try {
68-
const zip = new AdmZip(options.apkPath);
70+
const zip = new AdmZip(options.binaryPath);
6971
// Remove old signature files
7072
zip.deleteFile('META-INF/*');
71-
zip.writeZip(tempApkPath);
73+
zip.writeZip(tempArchivePath);
7274
} catch (error) {
7375
throw new RockError(
74-
`Failed to initialize output APK file: ${options.outputPath}`,
76+
`Failed to initialize output file: ${options.outputPath}`,
7577
{ cause: (error as SubprocessError).stderr },
7678
);
7779
}
78-
loader.stop(`Initialized output APK.`);
80+
loader.stop(`Initialized output ${extension.toUpperCase()}`);
7981

8082
// 3. Replace JS bundle if provided
8183
if (options.jsBundlePath) {
8284
loader.start('Replacing JS bundle...');
8385
await replaceJsBundle({
84-
apkPath: tempApkPath,
86+
archivePath: tempArchivePath,
8587
jsBundlePath: options.jsBundlePath,
8688
});
8789
loader.stop(
@@ -91,32 +93,34 @@ export async function signAndroid(options: SignAndroidOptions) {
9193
);
9294
}
9395

94-
// 4. Align APK file
95-
loader.start('Aligning output APK file...');
96-
const outputApkPath = options.outputPath ?? options.apkPath;
97-
await alignApkFile(tempApkPath, outputApkPath);
96+
// 4. Align archive
97+
loader.start('Aligning output file...');
98+
const outputPath = options.outputPath ?? options.binaryPath;
99+
await alignArchiveFile(tempArchivePath, outputPath);
98100
loader.stop(
99-
`Created output APK file: ${colorLink(relativeToCwd(outputApkPath))}.`,
101+
`Created output ${extension.toUpperCase()} file: ${colorLink(relativeToCwd(outputPath))}.`,
100102
);
101103

102-
// 5. Sign APK file
103-
loader.start('Signing the APK file...');
104+
// 5. Sign archive file
105+
loader.start(`Signing the ${extension.toUpperCase()} file...`);
104106
const keystorePath = options.keystorePath ?? 'android/app/debug.keystore';
105-
await signApkFile({
106-
apkPath: outputApkPath,
107+
await signArchive({
108+
binaryPath: outputPath,
107109
keystorePath,
108110
keystorePassword: options.keystorePassword ?? 'pass:android',
109111
keyAlias: options.keyAlias,
110112
keyPassword: options.keyPassword,
111113
});
112-
loader.stop(`Signed the APK file with keystore: ${colorLink(keystorePath)}.`);
114+
loader.stop(
115+
`Signed the ${extension.toUpperCase()} file with keystore: ${colorLink(keystorePath)}.`,
116+
);
113117

114118
outro('Success 🎉.');
115119
}
116120

117121
function validateOptions(options: SignAndroidOptions) {
118-
if (!fs.existsSync(options.apkPath)) {
119-
throw new RockError(`APK file not found "${options.apkPath}"`);
122+
if (!fs.existsSync(options.binaryPath)) {
123+
throw new RockError(`File not found "${options.binaryPath}"`);
120124
}
121125

122126
if (options.buildJsBundle && options.jsBundlePath) {
@@ -131,22 +135,24 @@ function validateOptions(options: SignAndroidOptions) {
131135
}
132136

133137
type ReplaceJsBundleOptions = {
134-
apkPath: string;
138+
archivePath: string;
135139
jsBundlePath: string;
136140
};
137141

138142
async function replaceJsBundle({
139-
apkPath,
143+
archivePath,
140144
jsBundlePath,
141145
}: ReplaceJsBundleOptions) {
142146
try {
143-
const zip = new AdmZip(apkPath);
144-
zip.deleteFile('assets/index.android.bundle');
145-
zip.addLocalFile(jsBundlePath, 'assets', 'index.android.bundle');
146-
zip.writeZip(apkPath);
147+
const zip = new AdmZip(archivePath);
148+
const assetsPath = isAab(archivePath) ? 'base/assets' : 'assets';
149+
150+
zip.deleteFile(path.join(assetsPath, 'index.android.bundle'));
151+
zip.addLocalFile(jsBundlePath, assetsPath, 'index.android.bundle');
152+
zip.writeZip(archivePath);
147153
} catch (error) {
148154
throw new RockError(
149-
`Failed to replace JS bundle in destination file: ${apkPath}}`,
155+
`Failed to replace JS bundle in destination file: ${archivePath}`,
150156
{ cause: error },
151157
);
152158
}
@@ -159,7 +165,7 @@ function isSdkGTE35(versionString: string) {
159165
return match[1].localeCompare('35.0.0', undefined, { numeric: true }) >= 0;
160166
}
161167

162-
async function alignApkFile(inputApkPath: string, outputApkPath: string) {
168+
async function alignArchiveFile(inputArchivePath: string, outputPath: string) {
163169
const zipAlignPath = findAndroidBuildTool('zipalign');
164170
if (!zipAlignPath) {
165171
throw new RockError(
@@ -177,34 +183,34 @@ Please follow instructions at: https://reactnative.dev/docs/set-up-your-environm
177183
'-f', // Overwrites existing output file.
178184
'-v', // Overwrites existing output file.
179185
'4', // alignment in bytes, e.g. '4' provides 32-bit alignment
180-
inputApkPath,
181-
outputApkPath,
186+
inputArchivePath,
187+
outputPath,
182188
];
183189
try {
184190
await spawn(zipAlignPath, zipalignArgs);
185191
} catch (error) {
186192
throw new RockError(
187-
`Failed to align APK file: ${zipAlignPath} ${zipalignArgs.join(' ')}`,
193+
`Failed to align archive file: ${zipAlignPath} ${zipalignArgs.join(' ')}`,
188194
{ cause: (error as SubprocessError).stderr },
189195
);
190196
}
191197
}
192198

193-
type SignApkOptions = {
194-
apkPath: string;
199+
type SignOptions = {
200+
binaryPath: string;
195201
keystorePath: string;
196202
keystorePassword: string;
197203
keyAlias?: string;
198204
keyPassword?: string;
199205
};
200206

201-
async function signApkFile({
202-
apkPath,
207+
async function signArchive({
208+
binaryPath,
203209
keystorePath,
204210
keystorePassword,
205211
keyAlias,
206212
keyPassword,
207-
}: SignApkOptions) {
213+
}: SignOptions) {
208214
if (!fs.existsSync(keystorePath)) {
209215
throw new RockError(
210216
`Keystore file not found "${keystorePath}". Provide a valid keystore path using the "--keystore" option.`,
@@ -230,7 +236,8 @@ Please follow instructions at: https://reactnative.dev/docs/set-up-your-environm
230236
formatPassword(keystorePassword),
231237
...(keyAlias ? ['--ks-key-alias', keyAlias] : []),
232238
...(keyPassword ? ['--key-pass', formatPassword(keyPassword)] : []),
233-
apkPath,
239+
...(isAab(binaryPath) ? ['--min-sdk-version', '36'] : []),
240+
binaryPath,
234241
];
235242

236243
try {
@@ -264,3 +271,7 @@ function formatPassword(password: string) {
264271
function getSignOutputPath() {
265272
return path.join(getDotRockPath(), 'android/sign');
266273
}
274+
275+
function isAab(filePath: string): boolean {
276+
return path.extname(filePath).toLowerCase() === '.aab';
277+
}

website/src/docs/cli/introduction.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -244,17 +244,17 @@ Same as for `build:android` and:
244244

245245
### `rock sign:android` Options
246246

247-
The `sign:android <binaryPath>` command signs your Android app with a keystore, producing a signed APK file ready for distribution. It allows for replacing the JS bundle with a new version.
247+
The `sign:android <binaryPath>` command signs your Android app with a keystore, producing a signed APK or AAB file ready for distribution. It allows for replacing the JS bundle with a new version.
248248

249-
| Argument | Description |
250-
| :----------- | :------------------- |
251-
| `binaryPath` | Path to the APK file |
249+
| Argument | Description |
250+
| :----------- | :-------------------------- |
251+
| `binaryPath` | Path to the APK or AAB file |
252252

253253
| Option | Description |
254254
| :----------------------------- | :---------------------------------------- |
255255
| `--keystore <string>` | Path to keystore file |
256256
| `--keystore-password <string>` | Password for keystore file |
257-
| `--output <string>` | Path to output APK file |
257+
| `--output <string>` | Path to output APK or AAB file |
258258
| `--build-jsbundle` | Build JS bundle before signing |
259259
| `--jsbundle <string>` | Path to JS bundle to apply before signing |
260260
| `--no-hermes` | Don't use Hermes for JS bundle |

0 commit comments

Comments
 (0)