Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
cbaa713
feat: android aab signing
mlisikbf Sep 29, 2025
3ec1556
feat: android aab signing - docs update
mlisikbf Sep 29, 2025
fc799c9
feat: android aab signing - fixes assets path
mlisikbf Sep 30, 2025
1962250
feat: android aab signing - changeset
mlisikbf Sep 30, 2025
c0410d4
feat: android aab signing - missing semicolon
mlisikbf Sep 30, 2025
18f59fb
feat: android aab signing - ensures non-empty alias
mlisikbf Sep 30, 2025
7a69dd5
feat: android aab signing - avoids shadowing node:path
mlisikbf Sep 30, 2025
6b51b87
feat: android aab signing - strips jarsigner password input
mlisikbf Sep 30, 2025
9f6b227
feat: android aab signing - corrects key-alias message
mlisikbf Sep 30, 2025
c3005d7
feat: android aab signing - uses input path for isAab checks
mlisikbf Sep 30, 2025
4f80eb2
feat: android aab signing - updates changeset
mlisikbf Oct 1, 2025
321b308
feat: android aab signing - adds note on sign/align sequence
mlisikbf Oct 1, 2025
eb5bbc6
Merge branch 'main' into feat/android-aab-signing
mlisikbf Oct 6, 2025
b3d4acf
Merge branch 'callstackincubator:main' into feat/android-aab-signing
mlisikbf Oct 17, 2025
2dd2769
feat: android aab signing - removes aab-specific signing flow
mlisikbf Oct 17, 2025
c617315
feat: android aab signing - minimizes changes
mlisikbf Oct 17, 2025
5863312
feat: android aab signing - adds --min-sdk-version arg
mlisikbf Oct 17, 2025
f7448cb
feat: android aab signing - adds --min-sdk-version note in the docs
mlisikbf Oct 17, 2025
2f5fcf7
Update packages/platform-android/src/lib/commands/signAndroid/signAnd…
mlisikbf Oct 17, 2025
d09a087
feat: android aab signing - adds applies prettier
mlisikbf Oct 17, 2025
6fcdad1
feat: android aab signing - moves doc comment
mlisikbf Oct 17, 2025
7bed7a2
feat: android aab signing - removes reduntant brace
mlisikbf Oct 17, 2025
829f423
feat: android aab signing - adds comment on default --min-sdk-version
mlisikbf Oct 17, 2025
83d7cd4
feat: android aab signing - removes --new-sdk-version from sign command
mlisikbf Oct 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/whole-tools-occur.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rock-js/platform-android': minor
---

feat: android aab signing
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { signAndroid } from './signAndroid.js';

export type SignFlags = {
verbose?: boolean;
apk: string;
path: string;
output?: string;
keystore?: string;
keystorePassword?: string;
Expand All @@ -16,8 +16,8 @@ export type SignFlags = {

const ARGUMENTS = [
{
name: 'apk',
description: 'APK file path',
name: 'binaryPath',
description: 'Archive (apk or aab) file path',
},
];

Expand All @@ -44,7 +44,7 @@ const OPTIONS = [
},
{
name: '--output <string>',
description: 'Path to the output APK file.',
description: 'Path to the output APK/AAB file.',
},
{
name: '--build-jsbundle',
Expand All @@ -66,9 +66,9 @@ export const registerSignCommand = (api: PluginApi) => {
description: 'Sign the Android app with modified JS bundle.',
args: ARGUMENTS,
options: OPTIONS,
action: async (apkPath, flags: SignFlags) => {
action: async (binaryPath, flags: SignFlags) => {
await signAndroid({
apkPath,
binaryPath,
keystorePath: flags.keystore,
keystorePassword: flags.keystorePassword,
keyAlias: flags.keyAlias,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { findAndroidBuildTool, getAndroidBuildToolsPath } from '../../paths.js';
import { buildJsBundle } from './bundle.js';

export type SignAndroidOptions = {
apkPath: string;
binaryPath: string;
keystorePath?: string;
keystorePassword?: string;
keyAlias?: string;
Expand All @@ -30,7 +30,9 @@ export type SignAndroidOptions = {
export async function signAndroid(options: SignAndroidOptions) {
validateOptions(options);

intro(`Modifying APK file`);
const extension = path.extname(options.binaryPath).slice(1);

intro(`Modifying ${extension.toUpperCase()} file`);

const tempPath = getSignOutputPath();
if (fs.existsSync(tempPath)) {
Expand Down Expand Up @@ -60,28 +62,28 @@ export async function signAndroid(options: SignAndroidOptions) {
options.jsBundlePath = bundleOutputPath;
}

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

loader.start('Initializing output APK...');
loader.start(`Initializing output ${extension.toUpperCase()}...`);
try {
const zip = new AdmZip(options.apkPath);
const zip = new AdmZip(options.binaryPath);
// Remove old signature files
zip.deleteFile('META-INF/*');
zip.writeZip(tempApkPath);
zip.writeZip(tempArchivePath);
} catch (error) {
throw new RockError(
`Failed to initialize output APK file: ${options.outputPath}`,
`Failed to initialize output file: ${options.outputPath}`,
{ cause: (error as SubprocessError).stderr },
);
}
loader.stop(`Initialized output APK.`);
loader.stop(`Initialized output ${extension.toUpperCase()}`);

// 3. Replace JS bundle if provided
if (options.jsBundlePath) {
loader.start('Replacing JS bundle...');
await replaceJsBundle({
apkPath: tempApkPath,
archivePath: tempArchivePath,
jsBundlePath: options.jsBundlePath,
});
loader.stop(
Expand All @@ -91,32 +93,34 @@ export async function signAndroid(options: SignAndroidOptions) {
);
}

// 4. Align APK file
loader.start('Aligning output APK file...');
const outputApkPath = options.outputPath ?? options.apkPath;
await alignApkFile(tempApkPath, outputApkPath);
// 4. Align archive
loader.start('Aligning output file...');
const outputPath = options.outputPath ?? options.binaryPath;
await alignArchiveFile(tempArchivePath, outputPath);
loader.stop(
`Created output APK file: ${colorLink(relativeToCwd(outputApkPath))}.`,
`Created output ${extension.toUpperCase()} file: ${colorLink(relativeToCwd(outputPath))}.`,
);

// 5. Sign APK file
loader.start('Signing the APK file...');
// 5. Sign archive file
loader.start(`Signing the ${extension.toUpperCase()} file...`);
const keystorePath = options.keystorePath ?? 'android/app/debug.keystore';
await signApkFile({
apkPath: outputApkPath,
await signArchive({
binaryPath: outputPath,
keystorePath,
keystorePassword: options.keystorePassword ?? 'pass:android',
keyAlias: options.keyAlias,
keyPassword: options.keyPassword,
});
loader.stop(`Signed the APK file with keystore: ${colorLink(keystorePath)}.`);
loader.stop(
`Signed the ${extension.toUpperCase()} file with keystore: ${colorLink(keystorePath)}.`,
);

outro('Success 🎉.');
}

function validateOptions(options: SignAndroidOptions) {
if (!fs.existsSync(options.apkPath)) {
throw new RockError(`APK file not found "${options.apkPath}"`);
if (!fs.existsSync(options.binaryPath)) {
throw new RockError(`File not found "${options.binaryPath}"`);
}

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

type ReplaceJsBundleOptions = {
apkPath: string;
archivePath: string;
jsBundlePath: string;
};

async function replaceJsBundle({
apkPath,
archivePath,
jsBundlePath,
}: ReplaceJsBundleOptions) {
try {
const zip = new AdmZip(apkPath);
zip.deleteFile('assets/index.android.bundle');
zip.addLocalFile(jsBundlePath, 'assets', 'index.android.bundle');
zip.writeZip(apkPath);
const zip = new AdmZip(archivePath);
const assetsPath = isAab(archivePath) ? 'base/assets' : 'assets';

zip.deleteFile(path.join(assetsPath, 'index.android.bundle'));
zip.addLocalFile(jsBundlePath, assetsPath, 'index.android.bundle');
zip.writeZip(archivePath);
} catch (error) {
throw new RockError(
`Failed to replace JS bundle in destination file: ${apkPath}}`,
`Failed to replace JS bundle in destination file: ${archivePath}`,
{ cause: error },
);
}
Expand All @@ -159,7 +165,7 @@ function isSdkGTE35(versionString: string) {
return match[1].localeCompare('35.0.0', undefined, { numeric: true }) >= 0;
}

async function alignApkFile(inputApkPath: string, outputApkPath: string) {
async function alignArchiveFile(inputArchivePath: string, outputPath: string) {
const zipAlignPath = findAndroidBuildTool('zipalign');
if (!zipAlignPath) {
throw new RockError(
Expand All @@ -177,34 +183,34 @@ Please follow instructions at: https://reactnative.dev/docs/set-up-your-environm
'-f', // Overwrites existing output file.
'-v', // Overwrites existing output file.
'4', // alignment in bytes, e.g. '4' provides 32-bit alignment
inputApkPath,
outputApkPath,
inputArchivePath,
outputPath,
];
try {
await spawn(zipAlignPath, zipalignArgs);
} catch (error) {
throw new RockError(
`Failed to align APK file: ${zipAlignPath} ${zipalignArgs.join(' ')}`,
`Failed to align archive file: ${zipAlignPath} ${zipalignArgs.join(' ')}`,
{ cause: (error as SubprocessError).stderr },
);
}
}

type SignApkOptions = {
apkPath: string;
type SignOptions = {
binaryPath: string;
keystorePath: string;
keystorePassword: string;
keyAlias?: string;
keyPassword?: string;
};

async function signApkFile({
apkPath,
async function signArchive({
binaryPath,
keystorePath,
keystorePassword,
keyAlias,
keyPassword,
}: SignApkOptions) {
}: SignOptions) {
if (!fs.existsSync(keystorePath)) {
throw new RockError(
`Keystore file not found "${keystorePath}". Provide a valid keystore path using the "--keystore" option.`,
Expand All @@ -230,7 +236,8 @@ Please follow instructions at: https://reactnative.dev/docs/set-up-your-environm
formatPassword(keystorePassword),
...(keyAlias ? ['--ks-key-alias', keyAlias] : []),
...(keyPassword ? ['--key-pass', formatPassword(keyPassword)] : []),
apkPath,
...(isAab(binaryPath) ? ['--min-sdk-version', '36'] : []),
binaryPath,
];

try {
Expand Down Expand Up @@ -264,3 +271,7 @@ function formatPassword(password: string) {
function getSignOutputPath() {
return path.join(getDotRockPath(), 'android/sign');
}

function isAab(filePath: string): boolean {
return path.extname(filePath).toLowerCase() === '.aab';
}
10 changes: 5 additions & 5 deletions website/src/docs/cli/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,17 +244,17 @@ Same as for `build:android` and:

### `rock sign:android` Options

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.
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.

| Argument | Description |
| :----------- | :------------------- |
| `binaryPath` | Path to the APK file |
| Argument | Description |
| :----------- | :-------------------------- |
| `binaryPath` | Path to the APK or AAB file |

| Option | Description |
| :----------------------------- | :---------------------------------------- |
| `--keystore <string>` | Path to keystore file |
| `--keystore-password <string>` | Password for keystore file |
| `--output <string>` | Path to output APK file |
| `--output <string>` | Path to output APK or AAB file |
| `--build-jsbundle` | Build JS bundle before signing |
| `--jsbundle <string>` | Path to JS bundle to apply before signing |
| `--no-hermes` | Don't use Hermes for JS bundle |
Expand Down
Loading