Skip to content

Commit

Permalink
Merge pull request #1377 from epba23/android-apk
Browse files Browse the repository at this point in the history
Android apk #1136
  • Loading branch information
freakboy3742 authored Jul 23, 2023
2 parents 89d3db9 + 06e1880 commit 9c0c752
Show file tree
Hide file tree
Showing 12 changed files with 442 additions and 156 deletions.
1 change: 1 addition & 0 deletions changes/1136.misc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Briefcase can now package Android APKs as a packaging artefact.
7 changes: 7 additions & 0 deletions docs/reference/platforms/android.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
===========

Expand Down
11 changes: 8 additions & 3 deletions docs/reference/platforms/macOS/app.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
===========
Expand Down
11 changes: 8 additions & 3 deletions docs/reference/platforms/macOS/xcode.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
===========
Expand Down
14 changes: 10 additions & 4 deletions docs/reference/platforms/windows/app.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://wixtoolset.org/>`__ to build an MSI
installer for a Windows App. WiX, in turn, requires that .NET Framework 3.5 is
Expand Down
11 changes: 8 additions & 3 deletions docs/reference/platforms/windows/visualstudio.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://wixtoolset.org/>`__ to build an MSI
installer for a Windows App. WiX, in turn, requires that .NET Framework 3.5 is
Expand Down
34 changes: 28 additions & 6 deletions src/briefcase/platforms/android/gradle.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ class GradleMixin:

@property
def packaging_formats(self):
return ["aab"]
return ["aab", "apk", "debug-apk"]

@property
def default_packaging_format(self):
Expand All @@ -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)
Expand All @@ -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 >
Expand Down Expand Up @@ -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

Expand All @@ -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),
)

Expand Down
30 changes: 30 additions & 0 deletions tests/platforms/android/gradle/conftest.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
138 changes: 1 addition & 137 deletions tests/platforms/android/gradle/test_package.py
Original file line number Diff line number Diff line change
@@ -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)
Loading

0 comments on commit 9c0c752

Please sign in to comment.