-
-
Notifications
You must be signed in to change notification settings - Fork 535
feat: Add code signing for Android release artefacts #2724
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
base: main
Are you sure you want to change the base?
Changes from all commits
0cd811d
ce78e0e
47a05c9
01b1277
0934f76
70f2b75
ce4b950
533c286
969e8ea
1dcfbd0
25c8ba9
6033b96
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Android apps can now be signed directly by ``briefcase package android``. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 ( | ||
|
|
@@ -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. | ||
|
|
||
|
|
@@ -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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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).
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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).
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This still hasn't been done, either here or in |
||
| 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", | ||
|
mhsmith marked this conversation as resolved.
|
||
| "-validity", | ||
| "10000", | ||
| "-storepass", | ||
| store_password, | ||
| "-keypass", | ||
| key_password, | ||
|
Comment on lines
+1529
to
+1532
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 It appears that the most common filename extension for PKCS12 files is .p12, and we should pass the
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For PKCS12 keystores, it looks like |
||
| "-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). | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.