ionic-team / ionic-team/capacitor
[Bug]: `cap build android` fails with cryptic `MinSdkVersionException` / `Missing AndroidManifest.xml` when using `apksigner` with AAB (related to #6909)
- Dominant language
- TypeScript
- Stars
- 16.7k
- Forks
- 1.3k
- Avg merge
- 3d 15h
- Merged PRs (30d)
- 10
Description
### Capacitor Version
💊 Capacitor Doctor 💊
Latest Dependencies:
@capacitor/cli: 8.3.0
@capacitor/core: 8.3.0
@capacitor/android: 8.3.0
@capacitor/ios: 8.3.0
Installed Dependencies:
@capacitor/ios: not installed
@capacitor/android: 8.1.0
@capacitor/cli: 8.1.0
@capacitor/core: 8.1.0
[success] Android looking great! 👌
### Other API Details
```Shell
- Capacitor CLI version: (any version with `signingType` support, i.e. ≥ 5.1.0)
- Platform: Android
- `releaseType`: `'AAB'` (default)
- `signingType`: `'apksigner'`
```
### Platforms Affected
- [ ] iOS
- [x] Android
- [ ] Web
### Current Behavior
**Describe the bug**
When `signingType` is set to `'apksigner'` and `releaseType` is `'AAB'`, `npx cap build android` fails during the signing step with a confusing Java stack trace. The root cause is that `apksigner` does not support the AAB format — it is an APK-only tool — but the CLI passes the `.aab` file to it without any validation.
**Error output**
```
✖ Signing Release - failed!
[error] Exception in thread "main" com.android.apksig.apk.MinSdkVersionException: Failed to determine APK's minimum supported platform version. Use --min-sdk-version to override
at com.android.apksigner.ApkSignerTool.sign(ApkSignerTool.java:431)
at com.android.apksigner.ApkSignerTool.main(ApkSignerTool.java:94)
Caused by: com.android.apksig.apk.MinSdkVersionException: Failed to determine APK's minimum supported Android platform version
at com.android.apksig.ApkSigner.getMinSdkVersionFromApk(ApkSigner.java:1019)
at com.android.apksig.ApkSigner.sign(ApkSigner.java:301)
...
Caused by: com.android.apksig.apk.ApkFormatException: Missing AndroidManifest.xml
at com.android.apksig.ApkSigner.getAndroidManifestFromApk(ApkSigner.java:975)
...
```
**Steps to reproduce**
1. Set `signingType: 'apksigner'` in `capacitor.config.ts` (or pass `--signing-type apksigner` on the CLI), while `releaseType` is `'AAB'` (the default, or explicitly set).
2. Run `npx cap build android --keystorepath ... --keystorepass ...`
3. The Gradle build succeeds, but the signing step fails with the above error.
### Additional context
The inverse problem also exists. The default `signingType` is `'jarsigner'`:
```typescript
// cli/src/tasks/build.ts, line 49
signingtype: buildOptions.signingtype || config.android.buildOptions.signingType || 'jarsigner',
```
`jarsigner` uses `SHA1withRSA` / `SHA1` digest (hardcoded in `signWithJarSigner`):
```typescript
// cli/src/android/build.ts, lines 121–125
const signingArgs = [
'-sigalg', 'SHA1withRSA',
'-digestalg', 'SHA1',
...
```
SHA-1 signatures are rejected by Android for APKs targeting API 30+. So when building an **APK** without specifying a signing type, the default `jarsigner` path produces a binary that modern Android versions will refuse to install. `apksigner` should be the default for APK builds.
### Root cause
The signing dispatch in `cli/src/android/build.ts` is a simple binary branch with no awareness of `releaseType`:
```typescript
// cli/src/android/build.ts, lines 58–62
if (buildOptions.signingtype == 'jarsigner') {
await signWithJarSigner(config, buildOptions, releasePath, signedReleaseName, unsignedReleaseName);
} else {
await signWithApkSigner(config, buildOptions, releasePath, signedReleaseName, unsignedReleaseName);
}
```
The `else` branch routes **any value that is not `'jarsigner'`** — including `'apksigner'` — to `signWithApkSigner`, regardless of whether the output file is an APK or an AAB. When `releaseType` is `'AAB'` (the default), this calls:
```
apksigner sign --in app-release.aab --out app-release-signed.aab ...
```
`apksigner` expects an APK, which is a ZIP with `AndroidManifest.xml` at the root. An AAB stores its manifest at `base/manifest/AndroidManifest.xml`, so `apksigner` cannot find it and throws `ApkFormatException: Missing AndroidManifest.xml`, which surfaces as the misleading `MinSdkVersionException`.
The `releaseType` and `signingType` options are documented and treated as independent, but they are not — `apksigner` is fundamentally incompatible with AAB.
### Expected Behavior
The CLI should either:
- Throw a clear, actionable error before invoking `apksigner`, explaining that `apksigner` does not support AAB files and instructing the user to use `jarsigner` for AAB builds or switch `releaseType` to `'APK'`.
- Or automatically select the correct signing tool based on `releaseType`, with a warning if the user's explicit `signingType` is incompatible.
### Proposed fix
Replace the binary signing dispatch with logic that is aware of `releaseType`:
```typescript
if (releaseTypeIsAAB) {
if (buildOptions.signingtype === 'apksigner') {
throw `apksigner does not support AAB files. Set signingType to 'jarsigner' for AAB builds, or set androidReleaseType to 'APK'.`;
}
await signWithJarSigner(config, buildOptions, releasePath, signedReleaseName, unsignedReleaseName);
} else {
// APK: prefer apksigner; only use jarsigner if explicitly requested
if (buildOptions.signingtype === 'jarsigner') {
await signWithJarSigner(config, buildOptions, releasePath, signedReleaseName, unsignedReleaseName);
} else {
await signWithApkSigner(config, buildOptions, releasePath, signedReleaseName, unsignedReleaseName);
}
}
```
This also requires:
- Removing the hardcoded `|| 'jarsigner'` default from `cli/src/tasks/build.ts` line 49, so `buildAndroid` can choose the correct tool based on `releaseType` when the user has not explicitly set `signingType`.
- Updating the `@default "jarsigner"` JSDoc comment in `cli/src/declarations.ts` line 278 to reflect that the default is now context-dependent (`'jarsigner'` for AAB, `'apksigner'` for APK).
### Additional Information
This issue is related to #6909
Contributor guide
Assessment
This issue has not been assessed yet.