Skip to content

Commit 60632e9

Browse files
authored
Support --debug option on Android (#2351)
1 parent 00d4083 commit 60632e9

File tree

16 files changed

+428
-74
lines changed

16 files changed

+428
-74
lines changed

changes/2351.feature.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added a `--debug` option to the `build` and `run` commands for Android.

docs/en/how-to/debugging/pdb.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ To debug an app in development mode, add `breakpoint()` to your code somewhere t
1010

1111
/// warning | Note
1212

13-
This is currently an **experimental feature** that is only supported on Windows, macOS and iOS.
13+
This is currently an **experimental feature** that is only supported on Windows, macOS, iOS and Android.
1414

1515
///
1616

1717
To debug a bundled app, add `breakpoint()` somewhere in your code where the debugger should halt.
1818

19-
Your app must then be modified to include a bootstrap that will connect to the VS Code debugger. This is done by passing the `--debug debugpy` option to `briefcase build`:
19+
Your app must then be modified to include a bootstrap that will connect to the VS Code debugger. This is done by passing the `--debug pdb` option to `briefcase build`:
2020

2121
```console
2222
$ briefcase build --debug pdb

docs/en/how-to/debugging/vscode.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ To start a debug session, open the debug view in VS Code using the sidebar, sele
4040

4141
/// warning | Experimental feature
4242

43-
This is currently an **experimental feature** that is only supported on Windows, macOS and iOS.
43+
This is currently an **experimental feature** that is only supported on Windows, macOS, iOS and Android.
4444

4545
///
4646

docs/en/reference/commands/build.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ Currently the following debuggers are supported:
105105

106106
If calling only `--debug` without selecting a debugger explicitly, `pdb` is used as default.
107107

108-
This is an **experimental** new feature, that is currently only supported on Windows, macOS and iOS.
108+
This is an **experimental** new feature, that is currently only supported on Windows, macOS, iOS and Android.
109109

110110
This option may slow down the app a little bit.
111111

docs/en/reference/commands/run.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ Currently the following debuggers are supported:
109109

110110
If calling only `--debug` without selecting a debugger explicitly, `pdb` is used as default.
111111

112-
This is an **experimental** new feature, that is currently only supported on Windows, macOS and iOS.
112+
This is an **experimental** new feature, that is currently only supported on Windows, macOS, iOS and Android.
113113

114114
The selected debugger in `run --debug <debugger>` has to match the selected debugger in `build --debug <debugger>`.
115115

src/briefcase/integrations/android_sdk.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1523,7 +1523,9 @@ def force_stop_app(self, package: str):
15231523
f"Unable to force stop app {package} on {self.device}"
15241524
) from e
15251525

1526-
def start_app(self, package: str, activity: str, passthrough: list[str]):
1526+
def start_app(
1527+
self, package: str, activity: str, passthrough: list[str], env: dict[str, str]
1528+
):
15271529
"""Start an app, specified as a package name & activity name.
15281530
15291531
If you have an APK file, and you are not sure of the package or activity
@@ -1533,6 +1535,7 @@ def start_app(self, package: str, activity: str, passthrough: list[str]):
15331535
:param package: The name of the Android package, e.g., com.username.myapp.
15341536
:param activity: The activity of the APK to start.
15351537
:param passthrough: Arguments to pass to the app.
1538+
:param env: Environment variables to pass to the app.
15361539
:returns: `None` on success; raises an exception on failure.
15371540
"""
15381541
try:
@@ -1552,6 +1555,15 @@ def start_app(self, package: str, activity: str, passthrough: list[str]):
15521555
"--es",
15531556
"org.beeware.ARGV",
15541557
shlex.quote(json.dumps(passthrough)), # Protect from Android's shell
1558+
*(
1559+
[
1560+
"--es",
1561+
"org.beeware.ENVIRON",
1562+
shlex.quote(json.dumps(env)), # Protect from Android's shell
1563+
]
1564+
if env
1565+
else []
1566+
),
15551567
)
15561568

15571569
# `adb shell am start` always exits with status zero. We look for error

src/briefcase/platforms/android/gradle.py

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
)
2020
from briefcase.config import AppConfig, parsed_version
2121
from briefcase.console import ANSI_ESC_SEQ_RE_DEF
22+
from briefcase.debuggers.base import (
23+
AppPackagesPathMappings,
24+
DebuggerConnectionMode,
25+
)
2226
from briefcase.exceptions import BriefcaseCommandError
2327
from briefcase.integrations.android_sdk import ADB, AndroidSDK
2428
from briefcase.integrations.subprocess import SubprocessArgT
@@ -69,7 +73,7 @@ def android_log_clean_filter(line):
6973
class GradleMixin:
7074
output_format = "gradle"
7175
platform = "android"
72-
platform_target_version = "0.3.15"
76+
platform_target_version = "0.3.27"
7377

7478
@property
7579
def packaging_formats(self):
@@ -219,13 +223,6 @@ def output_format_template_context(self, app: AppConfig):
219223
return {
220224
"version_code": version_code,
221225
"safe_formal_name": safe_formal_name(app.formal_name),
222-
# Extract test packages to enable features like test discovery and assertion
223-
# rewriting.
224-
"extract_packages": ", ".join(
225-
f'"{name}"'
226-
for path in (app.test_sources or [])
227-
if (name := Path(path).name)
228-
),
229226
"build_gradle_dependencies": {"implementation": dependencies},
230227
}
231228

@@ -314,6 +311,7 @@ def permissions_context(self, app: AppConfig, x_permissions: dict[str, str]):
314311

315312
class GradleUpdateCommand(GradleCreateCommand, UpdateCommand):
316313
description = "Update an existing Android Gradle project."
314+
supports_debugger = True
317315

318316

319317
class GradleOpenCommand(GradleMixin, OpenCommand):
@@ -322,10 +320,14 @@ class GradleOpenCommand(GradleMixin, OpenCommand):
322320

323321
class GradleBuildCommand(GradleMixin, BuildCommand):
324322
description = "Build an Android debug APK."
323+
supports_debugger = True
325324

326325
def metadata_resource_path(self, app: AppConfig):
327326
return self.bundle_path(app) / self.path_index(app, "metadata_resource_path")
328327

328+
def extract_packages_path(self, app: AppConfig):
329+
return self.bundle_path(app) / self.path_index(app, "extract_packages_path")
330+
329331
def update_app_metadata(self, app: AppConfig):
330332
with (
331333
self.console.wait_bar("Setting main module..."),
@@ -341,6 +343,25 @@ def update_app_metadata(self, app: AppConfig):
341343
"""
342344
)
343345

346+
with (
347+
self.console.wait_bar("Setting packages to extract..."),
348+
self.extract_packages_path(app).open("w", encoding="utf-8") as f,
349+
):
350+
if app.debugger:
351+
# In debug mode include the .py files and extract all of them so
352+
# that the debugger can get the source code at runtime. This is
353+
# e.g. necessary for setting breakpoints in VS Code.
354+
extract_packages = ["*"]
355+
else:
356+
# Extract test packages, to enable features like test discovery and
357+
# assertion rewriting.
358+
extract_sources = app.test_sources or []
359+
extract_packages = [
360+
name for path in extract_sources if (name := Path(path).name)
361+
]
362+
363+
f.write("\n".join(extract_packages))
364+
344365
def build_app(self, app: AppConfig, **kwargs):
345366
"""Build an application.
346367
@@ -359,6 +380,7 @@ def build_app(self, app: AppConfig, **kwargs):
359380

360381
class GradleRunCommand(GradleMixin, RunCommand):
361382
description = "Run an Android debug APK on a device (physical or virtual)."
383+
supports_debugger = True
362384

363385
def verify_tools(self):
364386
super().verify_tools()
@@ -404,6 +426,20 @@ def add_options(self, parser):
404426
help="Reverse the specified port from device to host.",
405427
)
406428

429+
def debugger_app_packages_path_mapping(
430+
self, app: AppConfig
431+
) -> AppPackagesPathMappings:
432+
"""Get the path mappings for the app packages.
433+
434+
:param app: The config object for the app
435+
:returns: The path mappings for the app packages
436+
"""
437+
app_packages_path = self.bundle_path(app) / "app/build/python/pip/debug/common"
438+
return AppPackagesPathMappings(
439+
sys_path_regex="requirements$",
440+
host_folder=f"{app_packages_path}",
441+
)
442+
407443
def run_app(
408444
self,
409445
app: AppConfig,
@@ -476,6 +512,17 @@ def run_app(
476512
forward_ports = forward_ports or []
477513
reverse_ports = reverse_ports or []
478514

515+
env = {}
516+
if self.console.is_debug:
517+
env["BRIEFCASE_DEBUG"] = "1"
518+
519+
if app.debugger:
520+
env["BRIEFCASE_DEBUGGER"] = app.debugger.get_env_config(self, app)
521+
if app.debugger.connection_mode == DebuggerConnectionMode.SERVER:
522+
forward_ports.append(app.debugger_port)
523+
else:
524+
reverse_ports.append(app.debugger_port)
525+
479526
# Forward/Reverse requested ports
480527
with self.forward_ports(adb, forward_ports, reverse_ports):
481528
# To start the app, we launch `org.beeware.android.MainActivity`.
@@ -484,7 +531,7 @@ def run_app(
484531
device_start_time = adb.datetime()
485532

486533
adb.start_app(
487-
package, "org.beeware.android.MainActivity", passthrough
534+
package, "org.beeware.android.MainActivity", passthrough, env
488535
)
489536

490537
# Try to get the PID for 5 seconds.

tests/integrations/android_sdk/ADB/test_start_app.py

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def test_start_app_launches_app(adb, capsys, passthrough):
3030

3131
# Invoke start_app
3232
adb.start_app(
33-
"com.example.sample.package", "com.example.sample.activity", passthrough
33+
"com.example.sample.package", "com.example.sample.activity", passthrough, {}
3434
)
3535

3636
# Validate call parameters.
@@ -54,6 +54,45 @@ def test_start_app_launches_app(adb, capsys, passthrough):
5454
assert "normal adb output" not in capsys.readouterr()
5555

5656

57+
@pytest.mark.parametrize(
58+
"env",
59+
[
60+
{"PARAM1": "VALUE1"},
61+
{"BRIEFCASE_DEBUGGER": '{"host": "localhost", "port": 1234}'},
62+
],
63+
)
64+
def test_start_app_launches_app_with_env(adb, capsys, env):
65+
"""Invoking `start_app()` calls `run()` with the appropriate parameters."""
66+
# Mock out the run command on an adb instance
67+
adb.run = MagicMock(return_value="example normal adb output")
68+
69+
# Invoke start_app
70+
adb.start_app("com.example.sample.package", "com.example.sample.activity", [], env)
71+
72+
# Validate call parameters.
73+
adb.run.assert_called_once_with(
74+
"shell",
75+
"am",
76+
"start",
77+
"-n",
78+
"com.example.sample.package/com.example.sample.activity",
79+
"-a",
80+
"android.intent.action.MAIN",
81+
"-c",
82+
"android.intent.category.LAUNCHER",
83+
"--es",
84+
"org.beeware.ARGV",
85+
"'[]'",
86+
"--es",
87+
"org.beeware.ENVIRON",
88+
shlex.quote(json.dumps(env)),
89+
)
90+
91+
# Validate that the normal output of the command was not printed (since there
92+
# was no error).
93+
assert "normal adb output" not in capsys.readouterr()
94+
95+
5796
def test_missing_activity(adb):
5897
"""If the activity doesn't exist, the error is caught."""
5998
# Use real `adb` output from launching an activity that does not exist.
@@ -69,7 +108,9 @@ def test_missing_activity(adb):
69108
)
70109

71110
with pytest.raises(BriefcaseCommandError) as exc_info:
72-
adb.start_app("com.example.sample.package", "com.example.sample.activity", [])
111+
adb.start_app(
112+
"com.example.sample.package", "com.example.sample.activity", [], {}
113+
)
73114

74115
assert "Activity class not found" in str(exc_info.value)
75116

@@ -81,7 +122,9 @@ def test_invalid_device(adb):
81122
adb.run = MagicMock(side_effect=InvalidDeviceError("device", "exampleDevice"))
82123

83124
with pytest.raises(InvalidDeviceError):
84-
adb.start_app("com.example.sample.package", "com.example.sample.activity", [])
125+
adb.start_app(
126+
"com.example.sample.package", "com.example.sample.activity", [], {}
127+
)
85128

86129

87130
def test_unable_to_start(adb):
@@ -92,4 +135,6 @@ def test_unable_to_start(adb):
92135
BriefcaseCommandError,
93136
match=r"Unable to start com.example.sample.package/com.example.sample.activity on exampleDevice",
94137
):
95-
adb.start_app("com.example.sample.package", "com.example.sample.activity", [])
138+
adb.start_app(
139+
"com.example.sample.package", "com.example.sample.activity", [], {}
140+
)

tests/platforms/android/gradle/conftest.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ def first_app_generated(first_app_config, tmp_path):
5050
app_packages_path="app_packages"
5151
support_path="support"
5252
metadata_resource_path="res/briefcase.xml"
53+
extract_packages_path = "app/extract-packages.txt"
5354
""",
5455
)
5556

@@ -64,4 +65,16 @@ def first_app_generated(first_app_config, tmp_path):
6465
/ "briefcase.xml",
6566
"""<resources></resources>""",
6667
)
68+
69+
create_file(
70+
tmp_path
71+
/ "base_path"
72+
/ "build"
73+
/ "first-app"
74+
/ "android"
75+
/ "gradle"
76+
/ "app"
77+
/ "extract-packages.txt",
78+
"something-to-be-overwritten",
79+
)
6780
return first_app_config

0 commit comments

Comments
 (0)