diff --git a/changes/1136.misc.rst b/changes/1136.misc.rst
new file mode 100644
index 000000000..082db4e98
--- /dev/null
+++ b/changes/1136.misc.rst
@@ -0,0 +1 @@
+Briefcase can now package Android APKs as a packaging artefact.
diff --git a/docs/reference/platforms/android.rst b/docs/reference/platforms/android.rst
index 4123ddafd..17ac5259e 100644
--- a/docs/reference/platforms/android.rst
+++ b/docs/reference/platforms/android.rst
@@ -19,6 +19,13 @@ existing JDK install.
If the above methods fail to find an Android SDK or Java JDK, Briefcase will
download and install an isolated copy in its data directory.
+Briefcase supports three packaging formats for an Android app:
+
+1. An AAB bundle (the default output of ``briefcase package android``, or by using
+ ``briefcase package android -p aab``); or
+2. A Release APK (by using ``briefcase package android -p apk``); or
+3. A Debug APK (by using ``briefcase package android -p debug-apk``).
+
Icon format
===========
diff --git a/docs/reference/platforms/macOS/app.rst b/docs/reference/platforms/macOS/app.rst
index 0fac08574..e547187ab 100644
--- a/docs/reference/platforms/macOS/app.rst
+++ b/docs/reference/platforms/macOS/app.rst
@@ -11,9 +11,14 @@ also be compressed to reduce their size for transport.
By default, apps will be both signed and notarized when they are packaged.
-The ``.app`` bundle is a distributable artefact. Alternatively, the ``.app``
-bundle can be packaged as a ``.dmg`` that contains the ``.app`` bundle. The
-default packaging format is ``.dmg``.
+Packaging format
+================
+
+Briefcase supports two packaging formats for a macOS ``.app`` bundle:
+
+1. A DMG that contains the ``.app`` bundle (the default output of ``briefcase package
+ macOS``, or by using ``briefcase package macOS -p dmg``); or
+2. A zipped ``.app`` folder (using ``briefcase package macOS -p app``).
Icon format
===========
diff --git a/docs/reference/platforms/macOS/xcode.rst b/docs/reference/platforms/macOS/xcode.rst
index 37a81c174..2820184e9 100644
--- a/docs/reference/platforms/macOS/xcode.rst
+++ b/docs/reference/platforms/macOS/xcode.rst
@@ -8,9 +8,14 @@ command or directly from Xcode.
By default, apps will be both signed and notarized when they are packaged.
-The Xcode project will produce a ``.app`` bundle is a distributable artefact.
-Alternatively, this ``.app`` bundle can be packaged as a ``.dmg`` that contains
-the ``.app`` bundle. The default packaging format is ``.dmg``.
+Packaging format
+================
+
+Briefcase supports two packaging formats for a macOS Xcode project:
+
+1. A DMG that contains the ``.app`` bundle (the default output of ``briefcase package
+ macOS Xcode``, or by using ``briefcase package macOS Xcode -p dmg``); or
+2. A zipped ``.app`` folder (using ``briefcase package macOS Xcode -p app``).
Icon format
===========
diff --git a/docs/reference/platforms/windows/app.rst b/docs/reference/platforms/windows/app.rst
index 42e4b1ee0..c9696e2ad 100644
--- a/docs/reference/platforms/windows/app.rst
+++ b/docs/reference/platforms/windows/app.rst
@@ -3,11 +3,17 @@ Windows App
===========
A Windows app is a stub binary, allow with a collection of folders that contain
-the Python code for the app and the Python runtime libraries. Briefcase supports
-two packaging formats for a Windows app:
+the Python code for the app and the Python runtime libraries.
-1. As an MSI installer
-2. As a ZIP file containing all files needed to run the app
+Packaging format
+================
+
+Briefcase supports two packaging formats for a Windows app:
+
+1. As an MSI installer (the default output of ``briefcase package windows``, or by using
+ ``briefcase package windows -p msi``); or
+2. As a ZIP file containing all files needed to run the app (by using ``briefcase
+ package windows -p zip``).
Briefcase uses the `WiX Toolset `__ to build an MSI
installer for a Windows App. WiX, in turn, requires that .NET Framework 3.5 is
diff --git a/docs/reference/platforms/windows/visualstudio.rst b/docs/reference/platforms/windows/visualstudio.rst
index a760884e7..ae1d46092 100644
--- a/docs/reference/platforms/windows/visualstudio.rst
+++ b/docs/reference/platforms/windows/visualstudio.rst
@@ -28,10 +28,15 @@ ensure that you have installed the following:
- All default packages
- C++/CLI support for v143 build tools
-Briefcase supports two packaging formats for a Windows App:
+Packaging format
+================
-1. As an MSI installer
-2. As a ZIP file containing all files needed to run the app
+Briefcase supports two packaging formats for a Windows app:
+
+1. As an MSI installer (the default output of ``briefcase package windows
+ VisualStudio``, or by using ``briefcase package windows VisualStudio -p msi``); or
+2. As a ZIP file containing all files needed to run the app (by using ``briefcase
+ package windows VisualStudio -p zip``).
Briefcase uses the `WiX Toolset `__ to build an MSI
installer for a Windows App. WiX, in turn, requires that .NET Framework 3.5 is
diff --git a/src/briefcase/platforms/android/gradle.py b/src/briefcase/platforms/android/gradle.py
index 4788e198f..471172c3a 100644
--- a/src/briefcase/platforms/android/gradle.py
+++ b/src/briefcase/platforms/android/gradle.py
@@ -64,7 +64,7 @@ class GradleMixin:
@property
def packaging_formats(self):
- return ["aab"]
+ return ["aab", "apk", "debug-apk"]
@property
def default_packaging_format(self):
@@ -76,6 +76,22 @@ def __init__(self, *args, **kwargs):
def project_path(self, app):
return self.bundle_path(app)
+ def package_name(self, app) -> Path:
+ package_name_dict = {
+ "aab": Path("bundle") / "release" / "app-release.aab",
+ "apk": Path("apk") / "release" / "app-release-unsigned.apk",
+ "debug-apk": Path("apk") / "debug" / "app-debug.apk",
+ }
+ return package_name_dict[app.packaging_format]
+
+ def build_command(self, app):
+ command_dict = {
+ "aab": "bundleRelease",
+ "apk": "assembleRelease",
+ "debug-apk": "assembleDebug",
+ }
+ return command_dict[app.packaging_format]
+
def binary_path(self, app):
return (
self.bundle_path(app)
@@ -88,7 +104,15 @@ def binary_path(self, app):
)
def distribution_path(self, app):
- return self.dist_path / f"{app.formal_name}-{app.version}.aab"
+ packaging_format_extension = {
+ "aab": "aab",
+ "apk": "apk",
+ "debug-apk": "apk",
+ }
+ return (
+ self.dist_path
+ / f"{app.formal_name}-{app.version}.{packaging_format_extension[app.packaging_format]}"
+ )
def run_gradle(self, app, args):
# Gradle may install the emulator via the dependency chain build-tools > tools >
@@ -372,7 +396,7 @@ def package_app(self, app: BaseConfig, **kwargs):
)
with self.input.wait_bar("Bundling..."):
try:
- self.run_gradle(app, ["bundleRelease"])
+ self.run_gradle(app, [self.build_command(app)])
except subprocess.CalledProcessError as e:
raise BriefcaseCommandError("Error while building project.") from e
@@ -382,9 +406,7 @@ def package_app(self, app: BaseConfig, **kwargs):
/ "app"
/ "build"
/ "outputs"
- / "bundle"
- / "release"
- / "app-release.aab",
+ / f"{self.package_name(app)}",
self.distribution_path(app),
)
diff --git a/tests/platforms/android/gradle/conftest.py b/tests/platforms/android/gradle/conftest.py
index b3b212374..b9de86b07 100644
--- a/tests/platforms/android/gradle/conftest.py
+++ b/tests/platforms/android/gradle/conftest.py
@@ -1,8 +1,38 @@
+import os
+import sys
+from unittest.mock import MagicMock
+
import pytest
+import requests
+
+from briefcase.console import Console, Log
+from briefcase.integrations.android_sdk import AndroidSDK
+from briefcase.integrations.subprocess import Subprocess
+from briefcase.platforms.android.gradle import GradlePackageCommand
from ....utils import create_file
+@pytest.fixture
+def package_command(tmp_path, first_app_config):
+ command = GradlePackageCommand(
+ logger=Log(),
+ console=Console(),
+ base_path=tmp_path / "base_path",
+ data_path=tmp_path / "briefcase",
+ )
+ command.tools.android_sdk = MagicMock(spec_set=AndroidSDK)
+ command.tools.os = MagicMock(spec_set=os)
+ command.tools.os.environ = {}
+ command.tools.sys = MagicMock(spec_set=sys)
+ command.tools.requests = MagicMock(spec_set=requests)
+ command.tools.subprocess = MagicMock(spec_set=Subprocess)
+
+ # Make sure the dist folder exists
+ (tmp_path / "base_path" / "dist").mkdir(parents=True)
+ return command
+
+
@pytest.fixture
def first_app_generated(first_app_config, tmp_path):
# Create the briefcase.toml file
diff --git a/tests/platforms/android/gradle/test_package.py b/tests/platforms/android/gradle/test_package.py
index e89ea5c78..0a039c7a2 100644
--- a/tests/platforms/android/gradle/test_package.py
+++ b/tests/platforms/android/gradle/test_package.py
@@ -1,142 +1,6 @@
-import os
-import sys
-from subprocess import CalledProcessError
-from unittest.mock import MagicMock
-
-import pytest
-import requests
-
-from briefcase.console import Console, Log
-from briefcase.exceptions import BriefcaseCommandError
-from briefcase.integrations.android_sdk import AndroidSDK
-from briefcase.integrations.subprocess import Subprocess
-from briefcase.platforms.android.gradle import GradlePackageCommand
-
-from ....utils import create_file
-
-
-@pytest.fixture
-def package_command(tmp_path, first_app_config):
- command = GradlePackageCommand(
- logger=Log(),
- console=Console(),
- base_path=tmp_path / "base_path",
- data_path=tmp_path / "briefcase",
- )
- command.tools.android_sdk = MagicMock(spec_set=AndroidSDK)
- command.tools.os = MagicMock(spec_set=os)
- command.tools.os.environ = {}
- command.tools.sys = MagicMock(spec_set=sys)
- command.tools.requests = MagicMock(spec_set=requests)
- command.tools.subprocess = MagicMock(spec_set=Subprocess)
-
- # Make sure the dist folder exists
- (tmp_path / "base_path" / "dist").mkdir(parents=True)
- return command
-
-
-def test_unsupported_template_version(package_command, first_app_generated):
- """Error raised if template's target version is not supported."""
- # Mock the build command previously called
- create_file(package_command.binary_path(first_app_generated), content="")
-
- package_command.verify_app = MagicMock(wraps=package_command.verify_app)
-
- package_command._briefcase_toml.update(
- {first_app_generated: {"briefcase": {"target_epoch": "0.3.16"}}}
- )
-
- with pytest.raises(
- BriefcaseCommandError,
- match="The app template used to generate this app is not compatible",
- ):
- package_command(first_app_generated, packaging_format="aab")
-
- package_command.verify_app.assert_called_once_with(first_app_generated)
-
-
def test_packaging_formats(package_command):
- assert package_command.packaging_formats == ["aab"]
+ assert package_command.packaging_formats == ["aab", "apk", "debug-apk"]
def test_default_packaging_format(package_command):
assert package_command.default_packaging_format == "aab"
-
-
-def test_distribution_path(package_command, first_app_config, tmp_path):
- assert (
- package_command.distribution_path(first_app_config)
- == tmp_path / "base_path" / "dist" / "First App-0.0.1.aab"
- )
-
-
-@pytest.mark.parametrize(
- "host_os,gradlew_name",
- [("Windows", "gradlew.bat"), ("NonWindows", "gradlew")],
-)
-def test_execute_gradle(
- package_command,
- first_app_config,
- host_os,
- gradlew_name,
- tmp_path,
-):
- """Validate that package_app() will launch `gradlew bundleRelease` with the
- appropriate environment & cwd, and that it will use `gradlew.bat` on Windows but
- `gradlew` elsewhere."""
- # Mock out `host_os` so we can validate which name is used for gradlew.
- package_command.tools.host_os = host_os
-
- # Set up a side effect of invoking gradle to create a bundle
- def create_bundle(*args, **kwargs):
- create_file(
- tmp_path
- / "base_path"
- / "build"
- / "first-app"
- / "android"
- / "gradle"
- / "app"
- / "build"
- / "outputs"
- / "bundle"
- / "release"
- / "app-release.aab",
- "Android release",
- )
-
- package_command.tools.subprocess.run.side_effect = create_bundle
-
- # Create mock environment with `key`, which we expect to be preserved, and
- # `ANDROID_SDK_ROOT`, which we expect to be overwritten.
- package_command.tools.os.environ = {"ANDROID_SDK_ROOT": "somewhere", "key": "value"}
-
- package_command.package_app(first_app_config)
-
- package_command.tools.android_sdk.verify_emulator.assert_called_once_with()
- package_command.tools.subprocess.run.assert_called_once_with(
- [
- package_command.bundle_path(first_app_config) / gradlew_name,
- "bundleRelease",
- "--console",
- "plain",
- ],
- cwd=package_command.bundle_path(first_app_config),
- env=package_command.tools.android_sdk.env,
- check=True,
- )
-
- # The release asset has been moved into the dist folder
- assert (tmp_path / "base_path" / "dist" / "First App-0.0.1.aab").exists()
-
-
-def test_print_gradle_errors(package_command, first_app_config):
- """Validate that package_app() will convert stderr/stdout from the process into
- exception text."""
- # Create a mock subprocess that crashes, printing text partly in non-ASCII.
- package_command.tools.subprocess.run.side_effect = CalledProcessError(
- returncode=1,
- cmd=["ignored"],
- )
- with pytest.raises(BriefcaseCommandError):
- package_command.package_app(first_app_config)
diff --git a/tests/platforms/android/gradle/test_package__aab.py b/tests/platforms/android/gradle/test_package__aab.py
new file mode 100644
index 000000000..5cbee4231
--- /dev/null
+++ b/tests/platforms/android/gradle/test_package__aab.py
@@ -0,0 +1,113 @@
+from subprocess import CalledProcessError
+from unittest.mock import MagicMock
+
+import pytest
+
+from briefcase.exceptions import BriefcaseCommandError
+
+from ....utils import create_file
+
+
+@pytest.fixture
+def first_app_aab(first_app_config):
+ first_app_config.packaging_format = "aab"
+ return first_app_config
+
+
+def test_unsupported_template_version(package_command, first_app_generated):
+ """Error raised if template's target version is not supported."""
+ # Mock the build command previously called
+ create_file(package_command.binary_path(first_app_generated), content="")
+
+ package_command.verify_app = MagicMock(wraps=package_command.verify_app)
+
+ package_command._briefcase_toml.update(
+ {first_app_generated: {"briefcase": {"target_epoch": "0.3.16"}}}
+ )
+
+ with pytest.raises(
+ BriefcaseCommandError,
+ match="The app template used to generate this app is not compatible",
+ ):
+ package_command(first_app_generated, packaging_format="aab")
+
+ package_command.verify_app.assert_called_once_with(first_app_generated)
+
+
+def test_distribution_path(package_command, first_app_aab, tmp_path):
+ assert (
+ package_command.distribution_path(first_app_aab)
+ == tmp_path / "base_path" / "dist" / "First App-0.0.1.aab"
+ )
+
+
+@pytest.mark.parametrize(
+ "host_os,gradlew_name",
+ [("Windows", "gradlew.bat"), ("NonWindows", "gradlew")],
+)
+def test_execute_gradle(
+ package_command,
+ first_app_aab,
+ host_os,
+ gradlew_name,
+ tmp_path,
+):
+ """Validate that package_app() will launch `gradlew bundleRelease` with the
+ appropriate environment & cwd, and that it will use `gradlew.bat` on Windows but
+ `gradlew` elsewhere."""
+ # Mock out `host_os` so we can validate which name is used for gradlew.
+ package_command.tools.host_os = host_os
+
+ # Set up a side effect of invoking gradle to create a bundle
+ def create_bundle(*args, **kwargs):
+ create_file(
+ tmp_path
+ / "base_path"
+ / "build"
+ / "first-app"
+ / "android"
+ / "gradle"
+ / "app"
+ / "build"
+ / "outputs"
+ / "bundle"
+ / "release"
+ / "app-release.aab",
+ "Android release",
+ )
+
+ package_command.tools.subprocess.run.side_effect = create_bundle
+
+ # Create mock environment with `key`, which we expect to be preserved, and
+ # `ANDROID_SDK_ROOT`, which we expect to be overwritten.
+ package_command.tools.os.environ = {"ANDROID_SDK_ROOT": "somewhere", "key": "value"}
+
+ package_command.package_app(first_app_aab)
+
+ package_command.tools.android_sdk.verify_emulator.assert_called_once_with()
+ package_command.tools.subprocess.run.assert_called_once_with(
+ [
+ package_command.bundle_path(first_app_aab) / gradlew_name,
+ "bundleRelease",
+ "--console",
+ "plain",
+ ],
+ cwd=package_command.bundle_path(first_app_aab),
+ env=package_command.tools.android_sdk.env,
+ check=True,
+ )
+
+ # The release asset has been moved into the dist folder
+ assert (tmp_path / "base_path" / "dist" / "First App-0.0.1.aab").exists()
+
+
+def test_print_gradle_errors(package_command, first_app_aab):
+ """Validate that package_app() will convert stderr/stdout from the process into
+ exception text."""
+ # Create a mock subprocess that crashes, printing text partly in non-ASCII.
+ package_command.tools.subprocess.run.side_effect = CalledProcessError(
+ returncode=1,
+ cmd=["ignored"],
+ )
+ with pytest.raises(BriefcaseCommandError):
+ package_command.package_app(first_app_aab)
diff --git a/tests/platforms/android/gradle/test_package__apk.py b/tests/platforms/android/gradle/test_package__apk.py
new file mode 100644
index 000000000..9614e267b
--- /dev/null
+++ b/tests/platforms/android/gradle/test_package__apk.py
@@ -0,0 +1,114 @@
+from subprocess import CalledProcessError
+from unittest.mock import MagicMock
+
+import pytest
+
+from briefcase.exceptions import BriefcaseCommandError
+
+from ....utils import create_file
+
+
+@pytest.fixture
+def first_app_apk(first_app_config):
+ first_app_config.packaging_format = "apk"
+ return first_app_config
+
+
+def test_unsupported_template_version(package_command, first_app_generated):
+ """Error raised if template's target version is not supported."""
+ # Mock the build command previously called
+ create_file(package_command.binary_path(first_app_generated), content="")
+
+ package_command.verify_app = MagicMock(wraps=package_command.verify_app)
+
+ package_command._briefcase_toml.update(
+ {first_app_generated: {"briefcase": {"target_epoch": "0.3.16"}}}
+ )
+
+ with pytest.raises(
+ BriefcaseCommandError,
+ match="The app template used to generate this app is not compatible",
+ ):
+ package_command(first_app_generated, packaging_format="apk")
+
+ package_command.verify_app.assert_called_once_with(first_app_generated)
+
+
+def test_distribution_path(package_command, first_app_apk, tmp_path):
+ print(package_command.packaging_formats)
+ assert (
+ package_command.distribution_path(first_app_apk)
+ == tmp_path / "base_path" / "dist" / "First App-0.0.1.apk"
+ )
+
+
+@pytest.mark.parametrize(
+ "host_os,gradlew_name",
+ [("Windows", "gradlew.bat"), ("NonWindows", "gradlew")],
+)
+def test_execute_gradle(
+ package_command,
+ first_app_apk,
+ host_os,
+ gradlew_name,
+ tmp_path,
+):
+ """Validate that package_app() will launch `gradlew bundleRelease` with the
+ appropriate environment & cwd, and that it will use `gradlew.bat` on Windows but
+ `gradlew` elsewhere."""
+ # Mock out `host_os` so we can validate which name is used for gradlew.
+ package_command.tools.host_os = host_os
+
+ # Set up a side effect of invoking gradle to create a bundle
+ def create_bundle(*args, **kwargs):
+ create_file(
+ tmp_path
+ / "base_path"
+ / "build"
+ / "first-app"
+ / "android"
+ / "gradle"
+ / "app"
+ / "build"
+ / "outputs"
+ / "apk"
+ / "release"
+ / "app-release-unsigned.apk",
+ "Android release",
+ )
+
+ package_command.tools.subprocess.run.side_effect = create_bundle
+
+ # Create mock environment with `key`, which we expect to be preserved, and
+ # `ANDROID_SDK_ROOT`, which we expect to be overwritten.
+ package_command.tools.os.environ = {"ANDROID_SDK_ROOT": "somewhere", "key": "value"}
+
+ package_command.package_app(first_app_apk)
+
+ package_command.tools.android_sdk.verify_emulator.assert_called_once_with()
+ package_command.tools.subprocess.run.assert_called_once_with(
+ [
+ package_command.bundle_path(first_app_apk) / gradlew_name,
+ "assembleRelease",
+ "--console",
+ "plain",
+ ],
+ cwd=package_command.bundle_path(first_app_apk),
+ env=package_command.tools.android_sdk.env,
+ check=True,
+ )
+
+ # The release asset has been moved into the dist folder
+ assert (tmp_path / "base_path" / "dist" / "First App-0.0.1.apk").exists()
+
+
+def test_print_gradle_errors(package_command, first_app_apk):
+ """Validate that package_app() will convert stderr/stdout from the process into
+ exception text."""
+ # Create a mock subprocess that crashes, printing text partly in non-ASCII.
+ package_command.tools.subprocess.run.side_effect = CalledProcessError(
+ returncode=1,
+ cmd=["ignored"],
+ )
+ with pytest.raises(BriefcaseCommandError):
+ package_command.package_app(first_app_apk)
diff --git a/tests/platforms/android/gradle/test_package__debug_apk.py b/tests/platforms/android/gradle/test_package__debug_apk.py
new file mode 100644
index 000000000..aeb9de34b
--- /dev/null
+++ b/tests/platforms/android/gradle/test_package__debug_apk.py
@@ -0,0 +1,114 @@
+from subprocess import CalledProcessError
+from unittest.mock import MagicMock
+
+import pytest
+
+from briefcase.exceptions import BriefcaseCommandError
+
+from ....utils import create_file
+
+
+@pytest.fixture
+def first_app_apk(first_app_config):
+ first_app_config.packaging_format = "debug-apk"
+ return first_app_config
+
+
+def test_unsupported_template_version(package_command, first_app_generated):
+ """Error raised if template's target version is not supported."""
+ # Mock the build command previously called
+ create_file(package_command.binary_path(first_app_generated), content="")
+
+ package_command.verify_app = MagicMock(wraps=package_command.verify_app)
+
+ package_command._briefcase_toml.update(
+ {first_app_generated: {"briefcase": {"target_epoch": "0.3.16"}}}
+ )
+
+ with pytest.raises(
+ BriefcaseCommandError,
+ match="The app template used to generate this app is not compatible",
+ ):
+ package_command(first_app_generated, packaging_format="debug-apk")
+
+ package_command.verify_app.assert_called_once_with(first_app_generated)
+
+
+def test_distribution_path(package_command, first_app_apk, tmp_path):
+ print(package_command.packaging_formats)
+ assert (
+ package_command.distribution_path(first_app_apk)
+ == tmp_path / "base_path" / "dist" / "First App-0.0.1.apk"
+ )
+
+
+@pytest.mark.parametrize(
+ "host_os,gradlew_name",
+ [("Windows", "gradlew.bat"), ("NonWindows", "gradlew")],
+)
+def test_execute_gradle(
+ package_command,
+ first_app_apk,
+ host_os,
+ gradlew_name,
+ tmp_path,
+):
+ """Validate that package_app() will launch `gradlew bundleRelease` with the
+ appropriate environment & cwd, and that it will use `gradlew.bat` on Windows but
+ `gradlew` elsewhere."""
+ # Mock out `host_os` so we can validate which name is used for gradlew.
+ package_command.tools.host_os = host_os
+
+ # Set up a side effect of invoking gradle to create a bundle
+ def create_bundle(*args, **kwargs):
+ create_file(
+ tmp_path
+ / "base_path"
+ / "build"
+ / "first-app"
+ / "android"
+ / "gradle"
+ / "app"
+ / "build"
+ / "outputs"
+ / "apk"
+ / "debug"
+ / "app-debug.apk",
+ "Android release",
+ )
+
+ package_command.tools.subprocess.run.side_effect = create_bundle
+
+ # Create mock environment with `key`, which we expect to be preserved, and
+ # `ANDROID_SDK_ROOT`, which we expect to be overwritten.
+ package_command.tools.os.environ = {"ANDROID_SDK_ROOT": "somewhere", "key": "value"}
+
+ package_command.package_app(first_app_apk)
+
+ package_command.tools.android_sdk.verify_emulator.assert_called_once_with()
+ package_command.tools.subprocess.run.assert_called_once_with(
+ [
+ package_command.bundle_path(first_app_apk) / gradlew_name,
+ "assembleDebug",
+ "--console",
+ "plain",
+ ],
+ cwd=package_command.bundle_path(first_app_apk),
+ env=package_command.tools.android_sdk.env,
+ check=True,
+ )
+
+ # The release asset has been moved into the dist folder
+ assert (tmp_path / "base_path" / "dist" / "First App-0.0.1.apk").exists()
+
+
+def test_print_gradle_errors(package_command, first_app_apk):
+ """Validate that package_app() will convert stderr/stdout from the process into
+ exception text."""
+ # Create a mock subprocess that crashes, printing text partly in non-ASCII.
+ package_command.tools.subprocess.run.side_effect = CalledProcessError(
+ returncode=1,
+ cmd=["ignored"],
+ )
+ with pytest.raises(BriefcaseCommandError):
+ package_command.package_app(first_app_apk)