-
Notifications
You must be signed in to change notification settings - Fork 36
feat: android aab signing #593
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
thymikee
merged 24 commits into
callstackincubator:main
from
mlisikbf:feat/android-aab-signing
Oct 20, 2025
Merged
Changes from 23 commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
cbaa713
feat: android aab signing
mlisikbf 3ec1556
feat: android aab signing - docs update
mlisikbf fc799c9
feat: android aab signing - fixes assets path
mlisikbf 1962250
feat: android aab signing - changeset
mlisikbf c0410d4
feat: android aab signing - missing semicolon
mlisikbf 18f59fb
feat: android aab signing - ensures non-empty alias
mlisikbf 7a69dd5
feat: android aab signing - avoids shadowing node:path
mlisikbf 6b51b87
feat: android aab signing - strips jarsigner password input
mlisikbf 9f6b227
feat: android aab signing - corrects key-alias message
mlisikbf c3005d7
feat: android aab signing - uses input path for isAab checks
mlisikbf 4f80eb2
feat: android aab signing - updates changeset
mlisikbf 321b308
feat: android aab signing - adds note on sign/align sequence
mlisikbf eb5bbc6
Merge branch 'main' into feat/android-aab-signing
mlisikbf b3d4acf
Merge branch 'callstackincubator:main' into feat/android-aab-signing
mlisikbf 2dd2769
feat: android aab signing - removes aab-specific signing flow
mlisikbf c617315
feat: android aab signing - minimizes changes
mlisikbf 5863312
feat: android aab signing - adds --min-sdk-version arg
mlisikbf f7448cb
feat: android aab signing - adds --min-sdk-version note in the docs
mlisikbf 2f5fcf7
Update packages/platform-android/src/lib/commands/signAndroid/signAnd…
mlisikbf d09a087
feat: android aab signing - adds applies prettier
mlisikbf 6fcdad1
feat: android aab signing - moves doc comment
mlisikbf 7bed7a2
feat: android aab signing - removes reduntant brace
mlisikbf 829f423
feat: android aab signing - adds comment on default --min-sdk-version
mlisikbf 83d7cd4
feat: android aab signing - removes --new-sdk-version from sign command
mlisikbf File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@rock-js/platform-android': minor | ||
| --- | ||
|
|
||
| feat: android aab signing |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -25,12 +25,15 @@ export type SignAndroidOptions = { | |
| buildJsBundle?: boolean; | ||
| jsBundlePath?: string; | ||
| useHermes?: boolean; | ||
| minSdkVersion?: string; | ||
| }; | ||
|
|
||
| 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)) { | ||
|
|
@@ -60,28 +63,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( | ||
|
|
@@ -91,32 +94,35 @@ 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, | ||
| minSdkVersion: options.minSdkVersion, | ||
| }); | ||
| 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) { | ||
|
|
@@ -131,22 +137,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'; | ||
thymikee marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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 }, | ||
| ); | ||
| } | ||
|
|
@@ -159,7 +167,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( | ||
|
|
@@ -177,34 +185,36 @@ 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; | ||
| minSdkVersion?: string; | ||
| }; | ||
|
|
||
| async function signApkFile({ | ||
| apkPath, | ||
| async function signArchive({ | ||
| binaryPath, | ||
| keystorePath, | ||
| keystorePassword, | ||
| keyAlias, | ||
| keyPassword, | ||
| }: SignApkOptions) { | ||
| minSdkVersion, | ||
| }: SignOptions) { | ||
| if (!fs.existsSync(keystorePath)) { | ||
| throw new RockError( | ||
| `Keystore file not found "${keystorePath}". Provide a valid keystore path using the "--keystore" option.`, | ||
|
|
@@ -230,7 +240,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, | ||
| ...getSdkVersionArgs(isAab(binaryPath), minSdkVersion), | ||
| binaryPath, | ||
| ]; | ||
|
|
||
| try { | ||
|
|
@@ -264,3 +275,17 @@ function formatPassword(password: string) { | |
| function getSignOutputPath() { | ||
| return path.join(getDotRockPath(), 'android/sign'); | ||
| } | ||
|
|
||
| function getSdkVersionArgs(aab?: boolean, minSdkVersion?: string) { | ||
| if (!aab && !minSdkVersion) { | ||
| return []; | ||
| } | ||
|
|
||
| // the default here will only be applied for AABs | ||
| // we use a higher value as it is irrelevant for AAB verification, but allows apksigner to use better signing algorithms | ||
| return ['--min-sdk-version', minSdkVersion || '36']; | ||
thymikee marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| function isAab(filePath: string): boolean { | ||
| return path.extname(filePath).toLowerCase() === '.aab'; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Missing Type Specifier for
--min-sdk-versionThe
--min-sdk-versionoption for thesign:androidcommand is missing its<string>type specifier. This can cause the CLI framework to misinterpret the argument, potentially treating it as a boolean instead of the expected string value.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cursor is right, should be
--min-sdk-version <string>There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please finish setting up background agents. Go to Cursor