Skip to content
Open
1 change: 1 addition & 0 deletions changes/2724.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Android apps can now be signed directly by ``briefcase package android``.
1 change: 1 addition & 0 deletions docs/en/reference/commands/package.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,4 @@ The format for the code signing identity is platform specific:
* **On macOS:** The 40-character hex thumbprint of the signing identity; the full name of the certificate (e.g., `Developer ID Application: Jane Smith (ABC12345DE)`); or `-` to use an ad-hoc signature. See the [documentation on macOS code signing for more details](../../how-to/code-signing/macOS.md).

* **On Windows:** The 40-character hex thumbprint of the signing identity; or the subject name of a certificate in the user's certificate store. See the [documentation on Windows code signing for more details](../../how-to/code-signing/windows.md).
* **On Android:** The path to a `.jks` keystore file. For legacy reasons, this is also available as `--identity`. See the [documentation on Android signing](../../reference/platforms/android/gradle.md#signing-of-briefcase-package-artefacts) for more details.
63 changes: 61 additions & 2 deletions docs/en/reference/platforms/android/gradle.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,33 @@ Android allows for some customization of the colors used by your app:

The following options can be provided at the command line when producing Android projects:

### run
### package { #android-package }
Comment thread
mhsmith marked this conversation as resolved.

#### `-i <path>` / `--keystore <path>`

The path to a keystore file to use for signing. While Briefcase defaults to
creating a PKCS12 format keystore (with a `.p12` extension), it can also use
legacy JKS format keystores (with a `.jks` extension).

#### `--keystore-password <password>`

The password for the keystore. If not provided, Briefcase will prompt for it.

#### `--key-alias <alias>`

The alias of the signing key within the keystore. If not provided, Briefcase will prompt for it.

#### `--key-password <password>`

The password for the signing key itself. If not provided, it defaults to the keystore password.

If provided, these options will be used to sign the artefact non-interactively. If any required option is not provided, Briefcase will prompt for it in an interactive session, or fail if input is disabled.

#### `--adhoc-sign`

Create an unsigned release artefact, skipping the keystore signing step entirely. Useful for CI pipelines that handle signing separately, or for testing distribution.

### run { #android-run }

#### `-d <device>` / `--device <device>`

Expand Down Expand Up @@ -376,4 +402,37 @@ For advice on how to deal with this situation, see the [Chaquopy FAQ](https://ch

### Signing of `briefcase package` artefacts

While it is possible to use <span class="title-ref">briefcase package android</span> to produce an APK or AAB file for distribution, the file is *not* usable as-is. It must be signed regardless of whether you're distributing your app through the Play Store, or via loading the APK directly. For details on how to manually sign your code, see the instructions on [signing an Android App Bundle][sign-the-android-app-bundle].
When building an AAB or a Release APK, Briefcase will automatically sign the artefact using a [PKCS12 format](https://en.wikipedia.org/wiki/PKCS_12) keystore (`.p12`) file.

You must supply the path to an existing keystore on the command line:

```console
$ briefcase package android --keystore /path/to/my-release-key.jks
```

If you are using a keystore that has been created with an alias or password, you can also supply those properties at the command line:

```console
$ briefcase package android \
--keystore /path/to/my-release-key.jks \
--key-alias my-key \
--keystore-password s3cr3t
```

If you do not specify a keystore, Briefcase will offer to discover existing keystores or create a new one. If you choose to create a new one, Briefcase will generate a new PKCS12 keystore at `<project-root>/.android/<bundle-identifier>.p12` using `keytool` from your JDK installation.

To produce an *unsigned* artefact (for example, when signing is handled externally in a CI pipeline), use `--adhoc-sign`:

```console
$ briefcase package android --adhoc-sign
```

/// note | Note

Keep your keystore file secure and backed up. If you lose it, you will not be able to publish updates to your app on the Play Store.

///

Debug APKs (produced with `-p debug-apk`) are signed automatically by the [Android SDK debug key](https://developer.android.com/studio/publish/app-signing#debug-mode) and do not go through this signing flow.

If you have an existing project that configures signing via `build_gradle_extra_content`, Briefcase will detect the `signingConfig` block and defer to that configuration. A migration warning will be printed suggesting you switch to the `--keystore` flag instead.
3 changes: 3 additions & 0 deletions docs/spelling_wordlist
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ amongst
APIs
ABIs
APK
APKs
appimage
AppImage
AppImages
Expand Down Expand Up @@ -126,6 +127,7 @@ OSX
passthrough
Passthrough
pbb
PKCS
PDB
PFX
phablet
Expand Down Expand Up @@ -190,6 +192,7 @@ SSL
stylesheet
subdirectories
subdirectory
subfolder
subfolders
submodule
subprocess
Expand Down
219 changes: 219 additions & 0 deletions src/briefcase/integrations/android_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
import shutil
import subprocess
import time
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Any

from briefcase.config import PEP508_NAME_RE
from briefcase.exceptions import (
Expand Down Expand Up @@ -377,6 +379,11 @@ def verify_install(
tools.android_sdk = sdk
return sdk

@property
def signing(self) -> AndroidSigning:
"""Obtain an AndroidSigning instance."""
return AndroidSigning(tools=self.tools)

def exists(self) -> bool:
"""Confirm that the SDK actually exists.

Expand Down Expand Up @@ -1422,6 +1429,218 @@ def start_emulator(
return device, full_name


@dataclass
class AndroidSigningConfig:
keystore_path: Path
key_alias: str
store_password: str
key_password: str
Comment on lines +1434 to +1437

@mhsmith mhsmith Mar 20, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The order and names of these fields should match the documentation of the command-line arguments (see comments in gradle.md).

@mhsmith mhsmith Apr 8, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still hasn't been done.



class AndroidSigning:
def __init__(self, tools: ToolCache):
self.tools = tools

@property
def _keytool(self) -> Path:
"""Path to the keytool executable bundled with the JDK."""
ext = ".exe" if self.tools.host_os == "Windows" else ""
return self.tools.java.java_home / "bin" / f"keytool{ext}"

def _keystore_candidates(self, base_path: Path) -> list[Path]:
"""Find candidate .jks keystore files in standard locations.

Searches the project folder, its .android subfolder, and ~/.android.

:param base_path: The base path (usually the project folder) to search.
"""
search_paths = [
base_path,
base_path / ".android",
self.tools.home_path / ".android",
]
candidates = []
for search_path in search_paths:
if search_path.is_dir():
candidates.extend(sorted(search_path.glob("*.p12")))
candidates.extend(sorted(search_path.glob("*.jks")))
return sorted(set(candidates))

def create_keystore(
self,
app: Any,
base_path: Path,
key_alias: str | None = None,
store_password: str | None = None,
key_password: str | None = None,
) -> AndroidSigningConfig:
"""Create a new keystore for signing Android apps.

The keystore is created at <base_path>/.android/<bundle_identifier>.jks.

:param app: The app being packaged
:param base_path: The project base path
:param key_alias: The key alias; prompted if not provided
:param store_password: The keystore password; prompted if not provided
:returns: An AndroidSigningConfig for the new keystore
"""
keystore_path = base_path / ".android" / f"{app.bundle_identifier}.p12"

if key_alias is None:
key_alias = self.tools.console.text_question(
description="Key alias",
intro="Enter an alias for the signing key.",
default="mykey",
)

if store_password is None:

@mhsmith mhsmith Mar 20, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The question order should match the documentation of the command-line arguments (see comments in gradle.md).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still hasn't been done, either here or in select_keystore.

store_password = self.tools.console.text_question(
description="Keystore password",
intro="Enter a password for the keystore.",
)

# PKCS12 keystores created by keytool must have the same password for
# the keystore and the key.
key_password = store_password

keystore_path.parent.mkdir(parents=True, exist_ok=True)

with self.tools.console.wait_bar("Creating keystore..."):
try:
# Based on the documentation for signing at
# https://developer.android.com/build/building-cmdline#sign_cmdline
self.tools.subprocess.run(
[
self._keytool,
"-genkeypair",
"-v",
"-keystore",
str(keystore_path),
"-storetype",
"PKCS12",
"-alias",
key_alias,
"-keyalg",
"RSA",
"-keysize",
"2048",
Comment thread
mhsmith marked this conversation as resolved.
"-validity",
"10000",
"-storepass",
store_password,
"-keypass",
key_password,
Comment on lines +1529 to +1532

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I experimented with this command, I got the message "Warning: Different store and key passwords not supported for PKCS12 KeyStores. Ignoring user-specified -keypass value." And when I run keytool -list on an older keystore, I get the message "The JKS keystore uses a proprietary format. It is recommended to migrate to PKCS12". So it looks like we should only support key passwords for reading existing keystores, not creating new ones.

It appears that the most common filename extension for PKCS12 files is .p12, and we should pass the -storetype option to make sure the format matches the name.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For PKCS12 keystores, it looks like -keypass is redundant and can be removed.

"-dname",
(
f"CN={app.formal_name}, "
"OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown"
),
],
check=True,
)
except subprocess.CalledProcessError as e:
raise BriefcaseCommandError("Failed to create keystore.") from e

self.tools.console.info(
f"""
A new keystore has been created at:

{keystore_path}

Keep this file secure and backed up. If you lose it, you will not be able to
publish updates to this app.

In future, you can reuse this keystore by running:

$ briefcase package android --keystore {keystore_path}

"""
)
return AndroidSigningConfig(
keystore_path=keystore_path,
key_alias=key_alias,
store_password=store_password,
key_password=key_password,
)

def select_keystore(
self,
app: Any,
base_path: Path,
keystore: str | None = None,
key_alias: str | None = None,
keystore_password: str | None = None,
key_password: str | None = None,
) -> AndroidSigningConfig:
"""Select or create a keystore for signing.

If ``keystore`` is provided it is treated as a path to a .jks keystore
file. Otherwise keystores are discovered from standard locations and
the user is prompted to select one or create a new one.

:param app: The app being packaged
:param base_path: The project base path
:param keystore: Path to a keystore file, or None to discover/create
:param key_alias: The key alias; prompted if not provided
:param keystore_password: The keystore password; prompted if not provided
:param key_password: The key password; defaults to keystore_password
:returns: An AndroidSigningConfig for the selected keystore
"""
if keystore is not None:
keystore_path = Path(keystore)
if not keystore_path.exists():
raise BriefcaseCommandError(
f"Keystore file {str(keystore)!r} does not exist."
)
else:
candidates = self._keystore_candidates(base_path)

_CREATE_NEW = "__create_new__"
options = {_CREATE_NEW: "Create a new keystore"}
for path in candidates:
options[str(path)] = str(path)

selection = self.tools.console.selection_question(
description="Keystore",
intro="Select the keystore to use for signing, or create a new one.",
options=options,
)

if selection == _CREATE_NEW:
return self.create_keystore(
app,
base_path=base_path,
key_alias=key_alias,
store_password=keystore_password,
key_password=key_password,
)

keystore_path = Path(selection)

if key_alias is None:
key_alias = self.tools.console.text_question(
description="Key alias",
intro="Enter the alias of the signing key in the keystore.",
default="mykey",
)

if keystore_password is None:
keystore_password = self.tools.console.text_question(
description="Keystore password",
intro="Enter the password for the keystore.",
)

if key_password is None:
key_password = keystore_password

return AndroidSigningConfig(
keystore_path=keystore_path,
key_alias=key_alias,
store_password=keystore_password,
key_password=key_password,
)


class ADB:
def __init__(self, tools: ToolCache, device: str):
"""An API integration for the Android Debug Bridge (ADB).
Expand Down
Loading