From c19ea849fd681a4f066bf18a8acaa5f7db17e324 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sun, 23 Feb 2025 20:32:10 +0100 Subject: [PATCH 001/131] Added initial remote debugging support --- .gitignore | 1 + changes/###.added.rst | 1 + docs/reference/commands/run.rst | 62 +++++ src/briefcase/commands/base.py | 10 +- src/briefcase/commands/build.py | 2 +- src/briefcase/commands/create.py | 53 +++- src/briefcase/commands/run.py | 2 +- src/briefcase/commands/update.py | 11 +- src/briefcase/config.py | 8 + src/briefcase/debugger.py | 273 ++++++++++++++++++++ src/briefcase/integrations/android_sdk.py | 78 ++++++ src/briefcase/platforms/android/gradle.py | 51 +++- src/briefcase/platforms/iOS/xcode.py | 31 +++ src/briefcase/platforms/macOS/__init__.py | 26 ++ src/briefcase/platforms/windows/__init__.py | 26 ++ 15 files changed, 622 insertions(+), 13 deletions(-) create mode 100644 changes/###.added.rst create mode 100644 src/briefcase/debugger.py diff --git a/.gitignore b/.gitignore index 9ae452b64..ecdf734b4 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ distribute-* .coverage.* coverage.xml venv*/ +.venv*/ .vscode/ .eggs/ .tox/ diff --git a/changes/###.added.rst b/changes/###.added.rst new file mode 100644 index 000000000..9fa952bca --- /dev/null +++ b/changes/###.added.rst @@ -0,0 +1 @@ +Added debugging support in bundled app through ``run --debug``. diff --git a/docs/reference/commands/run.rst b/docs/reference/commands/run.rst index 86912bb9c..915ad3bd8 100644 --- a/docs/reference/commands/run.rst +++ b/docs/reference/commands/run.rst @@ -34,6 +34,60 @@ corresponding to test suite completion. Briefcase has built-in support for other test frameworks can be added using the ``test_success_regex`` and ``test_failure_regex`` settings. +Debug mode +---------- + +The debug mode can be used to (remote) debug an bundled app. The debugger to +use can be configured via ``pyproject.toml`` an can then be activated through +``run --debug``. + +This is incredible useful when developing an iOS or Android app that can't be +debugged via ``briefcase dev``. + +To debug an bundled app you need a socket connection from your host system to +the device running your bundled app. Depending on the debugger selected the +bundled app is either a socket server or socket client. + +This feature needs to inject some code into your app to create the socket +connection. So you have to add ``import __briefcase_startup__`` to your +``__main__.py`` to get this running. +TODO: Better use .pth Files for this, so no modification of the app is needed. + But using this currently creates an import error on android... + +Some debuggers (like ``debugpy``) also support the mapping of the source code +from your bundled app to your local copy of the apps source code in the +``build`` folder. + +The configuration is done via ``pyproject.toml``: + +- ``debugger``: + - ``pdb`` (default): + This is used for debugging via console. + Currently it only supports ``debgger_mode=server``. + It creates a socket server that streams all stdout/stderr/stdin to + the host PC. The host PC can connect to it via: + - ``telnet localhost 5678`` + - ``nc -C localhost 5678`` + - ``socat readline tcp:localhost:5678``. + The app will start after the connection is established. + - ``debugpy``: + This is used for debugging via VSCode: + - If ``debgger_mode=server``: The bundled app creates an socket server an VSCode can connect to it. + - If ``debgger_mode=client``: This is used for debugging via VSCode. VSCode has to create an socket server and the bundled app connects to VSCode. + - ``debugpy-client``: + This is used for debugging via VSCode. VSCode has to create an socket server and the bundled app connects to VSCode. +- ``debugger_mode``: + - ``server`` (default): + The bundled app creates an socket server and the host system connects to it. + - ``client``: + The bundled app creates an socket client and the host system creates an socket server. + Note, that not all debuggers support all modes. +- ``debugger_ip``: + Specifies the IP of the socket connection (defaults to ``localhost``) +- ``debugger_port``: + Specifies the Port of the socket connection (defaults to ``5678``) + + Usage ===== @@ -137,6 +191,14 @@ contains the most recent test code. To prevent this update and build, use the Prevent the automated update and build of app code that is performed when specifying by the ``--test`` option. +``--debug`` +---------- + +Run the app in debug mode in the bundled app environment. This option can be used +standalone or in combination with the test mode ``run --test --debug``. + +On Android this also forwards the ``debugger_port`` from the android device to the host pc via adb. + Passthrough arguments --------------------- diff --git a/src/briefcase/commands/base.py b/src/briefcase/commands/base.py index 4a19b58ce..8151195e6 100644 --- a/src/briefcase/commands/base.py +++ b/src/briefcase/commands/base.py @@ -864,8 +864,8 @@ def _add_update_options( help=f"Prevent any automated update{context_label}", ) - def _add_test_options(self, parser, context_label): - """Internal utility method for adding common test-related options. + def _add_test_and_debug_options(self, parser, context_label): + """Internal utility method for adding common test- and debug-related options. :param parser: The parser to which options should be added. :param context_label: Label text for commands; the capitalized action being @@ -877,6 +877,12 @@ def _add_test_options(self, parser, context_label): action="store_true", help=f"{context_label} the app in test mode", ) + parser.add_argument( + "--debug", + dest="debug_mode", + action="store_true", + help=f"{context_label} the app in debug mode", + ) def add_options(self, parser): """Add any options that this command needs to parse from the command line. diff --git a/src/briefcase/commands/build.py b/src/briefcase/commands/build.py index a649910c0..819f8a758 100644 --- a/src/briefcase/commands/build.py +++ b/src/briefcase/commands/build.py @@ -14,7 +14,7 @@ class BuildCommand(BaseCommand): def add_options(self, parser): self._add_update_options(parser, context_label=" before building") - self._add_test_options(parser, context_label="Build") + self._add_test_and_debug_options(parser, context_label="Build") parser.add_argument( "-a", diff --git a/src/briefcase/commands/create.py b/src/briefcase/commands/create.py index 28a12d0d9..72c7356ea 100644 --- a/src/briefcase/commands/create.py +++ b/src/briefcase/commands/create.py @@ -13,6 +13,7 @@ import briefcase from briefcase.config import AppConfig +from briefcase.debugger import DebuggerConfig, write_debugger_startup_file from briefcase.exceptions import ( BriefcaseCommandError, InvalidStubBinary, @@ -666,7 +667,9 @@ def _install_app_requirements( else: self.console.info("No application requirements.") - def install_app_requirements(self, app: AppConfig, test_mode: bool): + def install_app_requirements( + self, app: AppConfig, test_mode: bool, debug_mode: bool + ): """Handle requirements for the app. This will result in either (in preferential order): @@ -687,6 +690,10 @@ def install_app_requirements(self, app: AppConfig, test_mode: bool): if test_mode and app.test_requires: requires.extend(app.test_requires) + if debug_mode: + debugger_config = DebuggerConfig.from_app(app) + requires.extend(debugger_config.additional_requirements) + try: requirements_path = self.app_requirements_path(app) except KeyError: @@ -713,11 +720,12 @@ def install_app_requirements(self, app: AppConfig, test_mode: bool): "`app_requirements_path` or `app_packages_path`" ) from e - def install_app_code(self, app: AppConfig, test_mode: bool): + def install_app_code(self, app: AppConfig, test_mode: bool, debug_mode: bool): """Install the application code into the bundle. :param app: The config object for the app :param test_mode: Should the application test code also be installed? + :param debug_mode: Should the application debug code also be installed? """ # Remove existing app folder if it exists app_path = self.app_path(app) @@ -746,6 +754,23 @@ def install_app_code(self, app: AppConfig, test_mode: bool): else: self.console.info(f"No sources defined for {app.app_name}.") + if debug_mode: + with self.console.wait_bar("Writing debugger startup files..."): + # TODO: How to get the "pth_folder_path"? + # - On windows "app_path.parent" is working + # - On android "app_path" is working, but then when running "__briefcase_startup__.py" via .pth file + # importing socket raises an error... + # + # As long as it is not clear for all platforms we have to call "import __briefcase_startup__" manually + # in "main.py" + + write_debugger_startup_file( + app_path=app_path, + pth_folder_path=None, # TODO: see above + app=app, + path_mappings=self.debugger_path_mappings(app, sources), + ) + # Write the dist-info folder for the application. write_dist_info( app=app, @@ -753,6 +778,16 @@ def install_app_code(self, app: AppConfig, test_mode: bool): / f"{app.module_name}-{app.version}.dist-info", ) + def debugger_path_mappings(self, app: AppConfig, app_sources: list[str]) -> str: + """Path mappings for enhanced debugger support + + :param app: The config object for the app + :param app_sources: All source files of the app + :return: A code snippet, that adds all path mappings to the + 'path_mappings' variable. + """ + return "" + def install_image(self, role, variant, size, source, target): """Install an icon/image of the requested size at a target location, using the source images defined by the app config. @@ -908,7 +943,13 @@ def cleanup_app_content(self, app: AppConfig): self.console.verbose(f"Removing {relative_path}") path.unlink() - def create_app(self, app: AppConfig, test_mode: bool = False, **options): + def create_app( + self, + app: AppConfig, + test_mode: bool = False, + debug_mode: bool = False, + **options, + ): """Create an application bundle. :param app: The config object for the app @@ -953,10 +994,12 @@ def create_app(self, app: AppConfig, test_mode: bool = False, **options): self.verify_app(app) self.console.info("Installing application code...", prefix=app.app_name) - self.install_app_code(app=app, test_mode=test_mode) + self.install_app_code(app=app, test_mode=test_mode, debug_mode=debug_mode) self.console.info("Installing requirements...", prefix=app.app_name) - self.install_app_requirements(app=app, test_mode=test_mode) + self.install_app_requirements( + app=app, test_mode=test_mode, debug_mode=debug_mode + ) self.console.info("Installing application resources...", prefix=app.app_name) self.install_app_resources(app=app) diff --git a/src/briefcase/commands/run.py b/src/briefcase/commands/run.py index 9df195b48..b8ce9b3af 100644 --- a/src/briefcase/commands/run.py +++ b/src/briefcase/commands/run.py @@ -218,7 +218,7 @@ def add_options(self, parser): ) self._add_update_options(parser, context_label=" before running") - self._add_test_options(parser, context_label="Run") + self._add_test_and_debug_options(parser, context_label="Run") def _prepare_app_kwargs(self, app: AppConfig, test_mode: bool): """Prepare the kwargs for running an app as a log stream. diff --git a/src/briefcase/commands/update.py b/src/briefcase/commands/update.py index 27673b326..819d401f1 100644 --- a/src/briefcase/commands/update.py +++ b/src/briefcase/commands/update.py @@ -15,7 +15,7 @@ class UpdateCommand(CreateCommand): def add_options(self, parser): self._add_update_options(parser, update=False) - self._add_test_options(parser, context_label="Update") + self._add_test_and_debug_options(parser, context_label="Update") parser.add_argument( "-a", @@ -33,6 +33,7 @@ def update_app( update_support: bool, update_stub: bool, test_mode: bool, + debug_mode: bool, **options, ) -> dict | None: """Update an existing application bundle. @@ -43,6 +44,7 @@ def update_app( :param update_support: Should app support be updated? :param update_stub: Should stub binary be updated? :param test_mode: Should the app be updated in test mode? + :param debug_mode: Should the app be updated in debug mode? """ if not self.bundle_path(app).exists(): @@ -54,11 +56,13 @@ def update_app( self.verify_app(app) self.console.info("Updating application code...", prefix=app.app_name) - self.install_app_code(app=app, test_mode=test_mode) + self.install_app_code(app=app, test_mode=test_mode, debug_mode=debug_mode) if update_requirements: self.console.info("Updating requirements...", prefix=app.app_name) - self.install_app_requirements(app=app, test_mode=test_mode) + self.install_app_requirements( + app=app, test_mode=test_mode, debug_mode=debug_mode + ) if update_resources: self.console.info("Updating application resources...", prefix=app.app_name) @@ -96,6 +100,7 @@ def __call__( update_support: bool = False, update_stub: bool = False, test_mode: bool = False, + debug_mode: bool = False, **options, ) -> dict | None: # Confirm host compatibility, that all required tools are available, diff --git a/src/briefcase/config.py b/src/briefcase/config.py index 607f889de..f8e51d0f9 100644 --- a/src/briefcase/config.py +++ b/src/briefcase/config.py @@ -281,6 +281,10 @@ def __init__( template_branch=None, test_sources=None, test_requires=None, + debugger=None, + debugger_mode=None, + debugger_ip=None, + debugger_port=None, supported=True, long_description=None, console_app=False, @@ -310,6 +314,10 @@ def __init__( self.template_branch = template_branch self.test_sources = test_sources self.test_requires = test_requires + self.debugger = debugger + self.debugger_mode = debugger_mode + self.debugger_ip = debugger_ip + self.debugger_port = debugger_port self.supported = supported self.long_description = long_description self.license = license diff --git a/src/briefcase/debugger.py b/src/briefcase/debugger.py new file mode 100644 index 000000000..1032f8df4 --- /dev/null +++ b/src/briefcase/debugger.py @@ -0,0 +1,273 @@ +import dataclasses +import enum +import textwrap +from datetime import datetime +from pathlib import Path +from typing import TextIO + +from briefcase.config import AppConfig + + +class Debugger(enum.StrEnum): + PDB = "pdb" + DEBUGPY = "debugpy" + + +class DebuggerMode(enum.StrEnum): + SERVER = "server" + CLIENT = "client" + + +_DEBGGER_REQUIREMENTS_MAPPING = { + Debugger.PDB: [], + Debugger.DEBUGPY: ["debugpy~=1.8.12"], +} + + +@dataclasses.dataclass +class DebuggerConfig: + debugger: Debugger + debugger_mode: DebuggerMode + ip: str + port: int + + @property + def additional_requirements(self) -> list[str]: + return _DEBGGER_REQUIREMENTS_MAPPING[self.debugger] + + @staticmethod + def from_app(app: AppConfig) -> "DebuggerConfig": + debugger = app.debugger or Debugger.PDB + debugger_mode = app.debugger_mode or DebuggerMode.SERVER + debugger_ip = app.debugger_ip or "localhost" + debugger_port = app.debugger_port or 5678 + + try: + debugger = Debugger(debugger) + except Exception: + raise ValueError("debugger has a wrong value") + + try: + debugger_port = int(debugger_port) + except Exception: + raise ValueError("debugger_port has to be an integer") + + return DebuggerConfig( + debugger, + debugger_mode, + debugger_ip, + debugger_port, + ) + + +def write_debugger_startup_file( + app_path: Path, + pth_folder_path: Path | None, + app: AppConfig, + path_mappings: str, +): + debugger_cfg = DebuggerConfig.from_app(app) + + startup_modul = "__briefcase_startup__" + startup_code_path = app_path / f"{startup_modul}.py" + + if debugger_cfg.debugger == Debugger.PDB: + if debugger_cfg.debugger_mode != DebuggerMode.SERVER: + raise ValueError( + f"{debugger_cfg.debugger_mode} not supported by {debugger_cfg.debugger}" + ) + + with startup_code_path.open("w", encoding="utf-8") as f: + create_remote_pdb_startup_file( + f, + debugger_cfg, + ) + elif debugger_cfg.debugger == Debugger.DEBUGPY: + if debugger_cfg.debugger_mode not in (DebuggerMode.SERVER, DebuggerMode.CLIENT): + raise ValueError( + f"{debugger_cfg.debugger_mode} not supported by {debugger_cfg.debugger}" + ) + + with startup_code_path.open("w", encoding="utf-8") as f: + create_debugpy_startup_file( + f, + debugger_cfg, + path_mappings, + ) + else: + raise ValueError(f"debugger '{debugger_cfg.debugger}' not found") + + if pth_folder_path: + startup_pth_path = pth_folder_path / f"{startup_modul}.pth" + with startup_pth_path.open("w", encoding="utf-8") as f: + f.write(f"import {startup_modul}") + + +def create_remote_pdb_startup_file( + file: TextIO, + debugger_cfg: DebuggerConfig, +) -> str: + file.write( + f""" +# Generated {datetime.now()} + +import socket +import sys +import re + +NEWLINE_REGEX = re.compile("\\r?\\n") + +class SocketFileWrapper(object): + def __init__(self, connection: socket.socket): + self.connection = connection + self.stream = connection.makefile('rw') + + self.read = self.stream.read + self.readline = self.stream.readline + self.readlines = self.stream.readlines + self.close = self.stream.close + self.isatty = self.stream.isatty + self.flush = self.stream.flush + self.fileno = lambda: -1 + self.__iter__ = self.stream.__iter__ + + @property + def encoding(self): + return self.stream.encoding + + def write(self, data): + data = NEWLINE_REGEX.sub("\\r\\n", data) + self.connection.sendall(data.encode(self.stream.encoding)) + + def writelines(self, lines): + for line in lines: + self.write(line) + +def redirect_stdio(): + f'''Open a socket server and stream all stdio via the connection bidirectional.''' + ip = "{debugger_cfg.ip}" + port = {debugger_cfg.port} + print(f''' +Stdio redirector server opened at {{ip}}:{{port}}, waiting for connection... +To connect to stdio redirector use eg.: + - telnet {{ip}} {{port}} + - nc -C {{ip}} {{port}} + - socat readline tcp:{{ip}}:{{port}} +''') + + listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True) + listen_socket.bind((ip, port)) + listen_socket.listen(1) + connection, address = listen_socket.accept() + print(f"Stdio redirector accepted connection from {{repr(address)}}.") + + file_wrapper = SocketFileWrapper(connection) + + sys.stderr = file_wrapper + sys.stdout = file_wrapper + sys.stdin = file_wrapper + sys.__stderr__ = file_wrapper + sys.__stdout__ = file_wrapper + sys.__stdin__ = file_wrapper + +redirect_stdio() +""" + ) + + +def create_debugpy_startup_file( + file: TextIO, + debugger_cfg: DebuggerConfig, + path_mappings: str, +) -> str: + """Create the code that is necessary to start the debugger""" + file.write( + f""" +# Generated {datetime.now()} + +import os +import sys +from pathlib import Path + +def start_debugger(): + ip = "{debugger_cfg.ip}" + port = {debugger_cfg.port} + path_mappings = [] + {textwrap.indent(path_mappings, " ")} + + # When an app is bundled with briefcase "os.__file__" is not set at runtime + # on some platforms (eg. windows). But debugpy accesses it internally, so it + # has to be set or an Exception is raised from debugpy. + if not hasattr(os, "__file__"): + os.__file__ = "" + +""" + ) + if debugger_cfg.debugger_mode == DebuggerMode.CLIENT: + file.write( + """ + print(f''' +Connecting to debugpy server at {ip}:{port}... +To create the debugpy server using VSCode add the following configuration to launch.json and start the debugger: +{{ + "version": "0.2.0", + "configurations": [ + {{ + "name": "Briefcase: Attach (Listen)", + "type": "debugpy", + "request": "attach", + "listen": {{ + "host": "{ip}", + "port": {port} + }} + }} + ] +}} +''') + import debugpy + try: + debugpy.connect((ip, port)) + except ConnectionRefusedError as e: + print("Could not connect to debugpy server. Is it already started? We continue with the app...") + return +""" + ) + elif debugger_cfg.debugger_mode == DebuggerMode.SERVER: + file.write( + """ + print(f''' +The debugpy server started at {ip}:{port}, waiting for connection... +To connect to debugpy using VSCode add the following configuration to launch.json: +{{ + "version": "0.2.0", + "configurations": [ + {{ + "name": "Briefcase: Attach (Connect)", + "type": "debugpy", + "request": "attach", + "connect": {{ + "host": "{ip}", + "port": {port} + }} + }} + ] +}} +''') + import debugpy + debugpy.listen((ip, port), in_process_debug_adapter=True) +""" + ) + + file.write( + """ + if (len(path_mappings) > 0): + # path_mappings has to be applied after connection is established. If no connection is + # established this import will fail. + import pydevd_file_utils + + pydevd_file_utils.setup_client_server_paths(path_mappings) + +start_debugger() +""" + ) diff --git a/src/briefcase/integrations/android_sdk.py b/src/briefcase/integrations/android_sdk.py index e0ea3fe42..ebf45e561 100644 --- a/src/briefcase/integrations/android_sdk.py +++ b/src/briefcase/integrations/android_sdk.py @@ -1628,6 +1628,84 @@ def logcat_tail(self, since: datetime): except subprocess.CalledProcessError as e: raise BriefcaseCommandError("Error starting ADB logcat.") from e + def forward(self, host_port: int, device_port: int): + """Use the forward command to set up arbitrary port forwarding, which + forwards requests on a specific host port to a different port on a device. + + :param host_port: The port on the host that should be forwarded to the device + :param device_port: The port on the device + """ + try: + # TODO: This prints the port to the terminal. How to remove the output? + + # If the port we are forwarding to the device is also reversed to the host, + # it has happened that adb hangs. So we remove the reversed port first. + self.tools.subprocess.run( + [ + self.tools.android_sdk.adb_path, + "-s", + self.device, + "reverse", + "--remove", + f"tcp:{host_port}", + ], + env=self.tools.android_sdk.env, + check=False, # if the port is not in use an error is returned, but we dont care + ) + self.tools.subprocess.run( + [ + self.tools.android_sdk.adb_path, + "-s", + self.device, + "forward", + f"tcp:{host_port}", + f"tcp:{device_port}", + ], + env=self.tools.android_sdk.env, + check=True, + ) + except subprocess.CalledProcessError as e: + raise BriefcaseCommandError("Error starting ADB forward.") from e + + def reverse(self, device_port: int, host_port: int): + """Use the reverse command to set up arbitrary port forwarding, which + forwards requests on a specific device port to a different port on the host. + + :param device_port: The port on the device that should be forwarded to the host + :param host_port: The port on the host + """ + try: + # TODO: This prints the port to the terminal. How to remove the output? + + # If the port we are reversing to the host is also forwarded to the device, + # it has happened that adb hangs. So we remove the forwarded port first. + self.tools.subprocess.run( + [ + self.tools.android_sdk.adb_path, + "-s", + self.device, + "forward", + "--remove", + f"tcp:{host_port}", + ], + env=self.tools.android_sdk.env, + check=False, # if the port is not in use an error is returned, but we dont care + ) + self.tools.subprocess.run( + [ + self.tools.android_sdk.adb_path, + "-s", + self.device, + "reverse", + f"tcp:{device_port}", + f"tcp:{host_port}", + ], + env=self.tools.android_sdk.env, + check=True, + ) + except subprocess.CalledProcessError as e: + raise BriefcaseCommandError("Error starting ADB reverse.") from e + def pidof(self, package: str, **kwargs) -> str | None: """Obtain the PID of a running app by package name. diff --git a/src/briefcase/platforms/android/gradle.py b/src/briefcase/platforms/android/gradle.py index 9a041edcb..f011b6727 100644 --- a/src/briefcase/platforms/android/gradle.py +++ b/src/briefcase/platforms/android/gradle.py @@ -17,8 +17,9 @@ ) from briefcase.config import AppConfig, parsed_version from briefcase.console import ANSI_ESC_SEQ_RE_DEF +from briefcase.debugger import DebuggerConfig, DebuggerMode from briefcase.exceptions import BriefcaseCommandError -from briefcase.integrations.android_sdk import AndroidSDK +from briefcase.integrations.android_sdk import ADB, AndroidSDK from briefcase.integrations.subprocess import SubprocessArgT @@ -282,6 +283,36 @@ def permissions_context(self, app: AppConfig, x_permissions: dict[str, str]): "features": features, } + def debugger_path_mappings(self, app: AppConfig, app_sources: list[str]): + """Path mappings for enhanced debugger support + + :param app: The config object for the app + :param app_sources: All source files of the app + :return: A list of code snippets that add a path mapping to the + 'path_mappings' variable. + """ + path_mappings = """ +device_app_folder = list(filter(lambda p: True if "AssetFinder/app" in p else False, sys.path)) +if len(device_app_folder) > 0: + pass +""" + for src in app_sources: + original = self.base_path / src + path_mappings += f""" + path_mappings.append((r"{original.absolute()}", str(Path(device_app_folder[0]) / "{original.name}"))) +""" + + host_requirements_folder = ( + self.bundle_path(app) / "app/build/python/pip/debug/common" + ) + path_mappings += f""" +device_requirements_folder = list(filter(lambda p: True if "AssetFinder/requirements" in p else False, sys.path)) +if len(device_requirements_folder) > 0: + path_mappings.append((r"{host_requirements_folder.absolute()}", device_requirements_folder[0])) +""" + + return path_mappings + class GradleUpdateCommand(GradleCreateCommand, UpdateCommand): description = "Update an existing Android Gradle project." @@ -364,6 +395,7 @@ def run_app( self, app: AppConfig, test_mode: bool, + debug_mode: bool, passthrough: list[str], device_or_avd=None, extra_emulator_args=None, @@ -374,6 +406,7 @@ def run_app( :param app: The config object for the app :param test_mode: Boolean; Is the app running in test mode? + :param debug_mode: Boolean; Is the app running in debug mode? :param passthrough: The list of arguments to pass to the app :param device_or_avd: The device to target. If ``None``, the user will be asked to re-run the command selecting a specific device. @@ -427,6 +460,10 @@ def run_app( with self.console.wait_bar("Installing new app version..."): adb.install_apk(self.binary_path(app)) + if debug_mode: + with self.console.wait_bar("Establishing debugger connection..."): + self.establish_debugger_connection(app, adb) + # To start the app, we launch `org.beeware.android.MainActivity`. with self.console.wait_bar(f"Launching {label}..."): # capture the earliest time for device logging in case PID not found @@ -477,6 +514,18 @@ def run_app( with self.tools.console.wait_bar("Stopping emulator..."): adb.kill() + def establish_debugger_connection(self, app: AppConfig, adb: ADB): + """Forward/Reverse the ports necessary for remote debugging. + + :param app: The config object for the app + :param adb: Access to the adb + """ + debugger_cfg = DebuggerConfig.from_app(app) + if debugger_cfg.debugger_mode == DebuggerMode.SERVER: + adb.forward(debugger_cfg.port, debugger_cfg.port) + elif debugger_cfg.port == DebuggerMode.CLIENT: + adb.reverse(debugger_cfg.port, debugger_cfg.port) + class GradlePackageCommand(GradleMixin, PackageCommand): description = "Create an Android App Bundle and APK in release mode." diff --git a/src/briefcase/platforms/iOS/xcode.py b/src/briefcase/platforms/iOS/xcode.py index b44516ca5..71809ed98 100644 --- a/src/briefcase/platforms/iOS/xcode.py +++ b/src/briefcase/platforms/iOS/xcode.py @@ -394,6 +394,37 @@ def _install_app_requirements( }, ) + def debugger_path_mappings(self, app: AppConfig, app_sources: list[str]): + """Path mappings for enhanced debugger support + + :param app: The config object for the app + :param app_sources: All source files of the app + :return: A list of code snippets that add a path mapping to the + 'path_mappings' variable. + """ + path_mappings = """ +device_app_folder = list(filter(lambda p: True if p.endswith("app") else False, sys.path)) +if len(device_app_folder) > 0: + pass +""" + for src in app_sources: + original = self.base_path / src + path_mappings += f""" + path_mappings.append((r"{original.absolute()}", str(Path(device_app_folder[0]) / "{original.name}"))) +""" + + host_requirements_folder = self.app_packages_path(app) + path_mappings += f""" +import platform +host_requirements_folder = "{host_requirements_folder.absolute()}" +host_requirements_folder += ".iphonesimulator" if platform.ios_ver().is_simulator else ".iphoneos" +device_requirements_folder = list(filter(lambda p: True if p.endswith("app_packages") else False, sys.path)) +if len(device_requirements_folder) > 0: + path_mappings.append((host_requirements_folder, device_requirements_folder[0])) +""" + + return path_mappings + class iOSXcodeUpdateCommand(iOSXcodeCreateCommand, UpdateCommand): description = "Update an existing iOS Xcode project." diff --git a/src/briefcase/platforms/macOS/__init__.py b/src/briefcase/platforms/macOS/__init__.py index fc37e4260..452bbb3a3 100644 --- a/src/briefcase/platforms/macOS/__init__.py +++ b/src/briefcase/platforms/macOS/__init__.py @@ -289,6 +289,32 @@ def permissions_context(self, app: AppConfig, cross_platform: dict[str, str]): "entitlements": entitlements, } + def debugger_path_mappings(self, app: AppConfig, app_sources: list[str]): + """Path mappings for enhanced debugger support + + :param app: The config object for the app + :param app_sources: All source files of the app + :return: A list of code snippets that add a path mapping to the + 'path_mappings' variable. + """ + # Normally app & requirements are automatically found, because + # developing an macOS app also requires a macOs host. But the app + # path is pointing to a copy of the source in some temporary folder, + # so we redirect it to the original source. + + path_mappings = """ +device_app_folder = list(filter(lambda p: True if p.endswith("app") else False, sys.path)) +if len(device_app_folder) > 0: + pass +""" + for src in app_sources: + original = self.base_path / src + path_mappings += f""" + path_mappings.append((r"{original.absolute()}", str(Path(device_app_folder[0]) / "{original.name}"))) +""" + + return path_mappings + class macOSRunMixin: def run_app( diff --git a/src/briefcase/platforms/windows/__init__.py b/src/briefcase/platforms/windows/__init__.py index b110782f7..ba110dea8 100644 --- a/src/briefcase/platforms/windows/__init__.py +++ b/src/briefcase/platforms/windows/__init__.py @@ -124,6 +124,32 @@ def _cleanup_app_support_package(self, support_path): """ ) + def debugger_path_mappings(self, app: AppConfig, app_sources: list[str]): + """Path mappings for enhanced debugger support + + :param app: The config object for the app + :param app_sources: All source files of the app + :return: A list of code snippets that add a path mapping to the + 'path_mappings' variable. + """ + # Normally app & requirements are automatically found, because + # developing an windows app also requires a windows host. But the app + # path is pointing to a copy of the source in some temporary folder, + # so we redirect it to the original source. + + path_mappings = """ +device_app_folder = list(filter(lambda p: True if p.endswith("app") else False, sys.path)) +if len(device_app_folder) > 0: + pass +""" + for src in app_sources: + original = self.base_path / src + path_mappings += f""" + path_mappings.append((r"{original.absolute()}", str(Path(device_app_folder[0]) / "{original.name}"))) +""" + + return path_mappings + class WindowsRunCommand(RunCommand): def run_app( From f138e7affc77ee4187f8af5b37c9237e43c412a9 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Mon, 10 Mar 2025 22:25:34 +0100 Subject: [PATCH 002/131] - added plugin system for debugger - changed command line from `--debug` to `--remote-debugger="REMOTE-DEBUGGER-CONFIG"` - removed configuration via pyproject.toml --- changes/###.added.rst | 1 - changes/2147.feature.rst | 1 + pyproject.toml | 4 + src/briefcase/commands/base.py | 31 ++- src/briefcase/commands/create.py | 36 +-- src/briefcase/commands/update.py | 16 +- src/briefcase/config.py | 8 - src/briefcase/debugger.py | 273 ---------------------- src/briefcase/debuggers/__init__.py | 22 ++ src/briefcase/debuggers/base.py | 134 +++++++++++ src/briefcase/debuggers/debugpy.py | 109 +++++++++ src/briefcase/debuggers/pdb.py | 81 +++++++ src/briefcase/platforms/android/gradle.py | 23 +- 13 files changed, 421 insertions(+), 318 deletions(-) delete mode 100644 changes/###.added.rst create mode 100644 changes/2147.feature.rst delete mode 100644 src/briefcase/debugger.py create mode 100644 src/briefcase/debuggers/__init__.py create mode 100644 src/briefcase/debuggers/base.py create mode 100644 src/briefcase/debuggers/debugpy.py create mode 100644 src/briefcase/debuggers/pdb.py diff --git a/changes/###.added.rst b/changes/###.added.rst deleted file mode 100644 index 9fa952bca..000000000 --- a/changes/###.added.rst +++ /dev/null @@ -1 +0,0 @@ -Added debugging support in bundled app through ``run --debug``. diff --git a/changes/2147.feature.rst b/changes/2147.feature.rst new file mode 100644 index 000000000..ec9a8b0ef --- /dev/null +++ b/changes/2147.feature.rst @@ -0,0 +1 @@ +Added debugging support in bundled app through ``run --remote-debugger``. diff --git a/pyproject.toml b/pyproject.toml index 5f1ed32b6..75279cca2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -134,6 +134,10 @@ Console = "briefcase.bootstraps.console:ConsoleBootstrap" PySide6 = "briefcase.bootstraps.pyside6:PySide6GuiBootstrap" Pygame = "briefcase.bootstraps.pygame:PygameGuiBootstrap" +[project.entry-points."briefcase.debuggers"] +pdb = "briefcase.debuggers.pdb:PdbDebugger" +debugpy = "briefcase.debuggers.debugpy:DebugpyDebugger" + [project.entry-points."briefcase.platforms"] android = "briefcase.platforms.android" iOS = "briefcase.platforms.iOS" diff --git a/src/briefcase/commands/base.py b/src/briefcase/commands/base.py index 8151195e6..913fd02a6 100644 --- a/src/briefcase/commands/base.py +++ b/src/briefcase/commands/base.py @@ -20,6 +20,12 @@ from packaging.version import Version from platformdirs import PlatformDirs +from briefcase.debuggers import DEFAULT_DEBUGGER, get_debuggers +from briefcase.debuggers.base import ( + BaseDebugger, + remote_debugger_config_from_string, +) + if sys.version_info >= (3, 11): # pragma: no-cover-if-lt-py311 import tomllib else: # pragma: no-cover-if-gte-py311 @@ -690,6 +696,20 @@ def verify_required_python(self, app: AppConfig): version_specifier=requires_python, running_version=running_version ) + def create_debugger(self, remote_debugger: str | None) -> BaseDebugger | None: + """Select and instantiate a debugger for the new project. + + :returns: An instance of the debugger that the user has selected. + """ + if remote_debugger is None: + return None + + debugger, config = remote_debugger_config_from_string(remote_debugger) + + debuggers = get_debuggers() + debugger_class = debuggers[debugger] if debugger else DEFAULT_DEBUGGER + return debugger_class(console=self.console, config=config) + def parse_options(self, extra): """Parse the command line arguments for the Command. @@ -878,10 +898,13 @@ def _add_test_and_debug_options(self, parser, context_label): help=f"{context_label} the app in test mode", ) parser.add_argument( - "--debug", - dest="debug_mode", - action="store_true", - help=f"{context_label} the app in debug mode", + "--remote-debugger", + dest="remote_debugger", + nargs="?", + default=None, + const="", + metavar="REMOTE-DEBUGGER-CONFIG", + help=f"{context_label} the app with remote debugger enabled", ) def add_options(self, parser): diff --git a/src/briefcase/commands/create.py b/src/briefcase/commands/create.py index 72c7356ea..7c8873f87 100644 --- a/src/briefcase/commands/create.py +++ b/src/briefcase/commands/create.py @@ -13,7 +13,7 @@ import briefcase from briefcase.config import AppConfig -from briefcase.debugger import DebuggerConfig, write_debugger_startup_file +from briefcase.debuggers import BaseDebugger from briefcase.exceptions import ( BriefcaseCommandError, InvalidStubBinary, @@ -668,7 +668,7 @@ def _install_app_requirements( self.console.info("No application requirements.") def install_app_requirements( - self, app: AppConfig, test_mode: bool, debug_mode: bool + self, app: AppConfig, test_mode: bool, debugger: BaseDebugger | None ): """Handle requirements for the app. @@ -680,19 +680,22 @@ def install_app_requirements( If ``test_mode`` is True, the test requirements will also be installed. + If ``debugger`` is provided, the debugger's additional requirements will also + be installed. + If the path index doesn't specify either of the path index entries, an error is raised. :param app: The config object for the app :param test_mode: Should the test requirements be installed? + :param debugger: Debugger to be used or None if no debugger should be used. """ requires = app.requires.copy() if app.requires else [] if test_mode and app.test_requires: requires.extend(app.test_requires) - if debug_mode: - debugger_config = DebuggerConfig.from_app(app) - requires.extend(debugger_config.additional_requirements) + if debugger: + requires.extend(debugger.additional_requirements) try: requirements_path = self.app_requirements_path(app) @@ -720,12 +723,14 @@ def install_app_requirements( "`app_requirements_path` or `app_packages_path`" ) from e - def install_app_code(self, app: AppConfig, test_mode: bool, debug_mode: bool): + def install_app_code( + self, app: AppConfig, test_mode: bool, debugger: BaseDebugger | None + ): """Install the application code into the bundle. :param app: The config object for the app :param test_mode: Should the application test code also be installed? - :param debug_mode: Should the application debug code also be installed? + :param debugger: Debugger to be used or None if no debugger should be used. """ # Remove existing app folder if it exists app_path = self.app_path(app) @@ -754,7 +759,7 @@ def install_app_code(self, app: AppConfig, test_mode: bool, debug_mode: bool): else: self.console.info(f"No sources defined for {app.app_name}.") - if debug_mode: + if debugger: with self.console.wait_bar("Writing debugger startup files..."): # TODO: How to get the "pth_folder_path"? # - On windows "app_path.parent" is working @@ -763,11 +768,9 @@ def install_app_code(self, app: AppConfig, test_mode: bool, debug_mode: bool): # # As long as it is not clear for all platforms we have to call "import __briefcase_startup__" manually # in "main.py" - - write_debugger_startup_file( + debugger.write_startup_file( app_path=app_path, pth_folder_path=None, # TODO: see above - app=app, path_mappings=self.debugger_path_mappings(app, sources), ) @@ -947,13 +950,14 @@ def create_app( self, app: AppConfig, test_mode: bool = False, - debug_mode: bool = False, + remote_debugger: str | None = None, **options, ): """Create an application bundle. :param app: The config object for the app :param test_mode: Should the app be updated in test mode? (default: False) + :param remote_debugger: Remote debugger that should be used. (default: None) """ if not app.supported: raise UnsupportedPlatform(self.platform) @@ -992,14 +996,14 @@ def create_app( # Verify the app after the app template and support package # are in place since the app tools may be dependent on them. self.verify_app(app) + self.console.print(f"{remote_debugger=}") + debugger = self.create_debugger(remote_debugger) if remote_debugger else None self.console.info("Installing application code...", prefix=app.app_name) - self.install_app_code(app=app, test_mode=test_mode, debug_mode=debug_mode) + self.install_app_code(app=app, test_mode=test_mode, debugger=debugger) self.console.info("Installing requirements...", prefix=app.app_name) - self.install_app_requirements( - app=app, test_mode=test_mode, debug_mode=debug_mode - ) + self.install_app_requirements(app=app, test_mode=test_mode, debugger=debugger) self.console.info("Installing application resources...", prefix=app.app_name) self.install_app_resources(app=app) diff --git a/src/briefcase/commands/update.py b/src/briefcase/commands/update.py index 819d401f1..6ba012574 100644 --- a/src/briefcase/commands/update.py +++ b/src/briefcase/commands/update.py @@ -33,7 +33,7 @@ def update_app( update_support: bool, update_stub: bool, test_mode: bool, - debug_mode: bool, + remote_debugger: str | None, **options, ) -> dict | None: """Update an existing application bundle. @@ -44,7 +44,7 @@ def update_app( :param update_support: Should app support be updated? :param update_stub: Should stub binary be updated? :param test_mode: Should the app be updated in test mode? - :param debug_mode: Should the app be updated in debug mode? + :param remote_debugger: Remote debugger that should be used. """ if not self.bundle_path(app).exists(): @@ -54,14 +54,20 @@ def update_app( return self.verify_app(app) + self.console.print(f"{remote_debugger=}") + debugger = ( + self.create_debugger(remote_debugger) + if remote_debugger is not None + else None + ) self.console.info("Updating application code...", prefix=app.app_name) - self.install_app_code(app=app, test_mode=test_mode, debug_mode=debug_mode) + self.install_app_code(app=app, test_mode=test_mode, debugger=debugger) if update_requirements: self.console.info("Updating requirements...", prefix=app.app_name) self.install_app_requirements( - app=app, test_mode=test_mode, debug_mode=debug_mode + app=app, test_mode=test_mode, debugger=debugger ) if update_resources: @@ -100,7 +106,7 @@ def __call__( update_support: bool = False, update_stub: bool = False, test_mode: bool = False, - debug_mode: bool = False, + remote_debugger: str | None = None, **options, ) -> dict | None: # Confirm host compatibility, that all required tools are available, diff --git a/src/briefcase/config.py b/src/briefcase/config.py index f8e51d0f9..607f889de 100644 --- a/src/briefcase/config.py +++ b/src/briefcase/config.py @@ -281,10 +281,6 @@ def __init__( template_branch=None, test_sources=None, test_requires=None, - debugger=None, - debugger_mode=None, - debugger_ip=None, - debugger_port=None, supported=True, long_description=None, console_app=False, @@ -314,10 +310,6 @@ def __init__( self.template_branch = template_branch self.test_sources = test_sources self.test_requires = test_requires - self.debugger = debugger - self.debugger_mode = debugger_mode - self.debugger_ip = debugger_ip - self.debugger_port = debugger_port self.supported = supported self.long_description = long_description self.license = license diff --git a/src/briefcase/debugger.py b/src/briefcase/debugger.py deleted file mode 100644 index 1032f8df4..000000000 --- a/src/briefcase/debugger.py +++ /dev/null @@ -1,273 +0,0 @@ -import dataclasses -import enum -import textwrap -from datetime import datetime -from pathlib import Path -from typing import TextIO - -from briefcase.config import AppConfig - - -class Debugger(enum.StrEnum): - PDB = "pdb" - DEBUGPY = "debugpy" - - -class DebuggerMode(enum.StrEnum): - SERVER = "server" - CLIENT = "client" - - -_DEBGGER_REQUIREMENTS_MAPPING = { - Debugger.PDB: [], - Debugger.DEBUGPY: ["debugpy~=1.8.12"], -} - - -@dataclasses.dataclass -class DebuggerConfig: - debugger: Debugger - debugger_mode: DebuggerMode - ip: str - port: int - - @property - def additional_requirements(self) -> list[str]: - return _DEBGGER_REQUIREMENTS_MAPPING[self.debugger] - - @staticmethod - def from_app(app: AppConfig) -> "DebuggerConfig": - debugger = app.debugger or Debugger.PDB - debugger_mode = app.debugger_mode or DebuggerMode.SERVER - debugger_ip = app.debugger_ip or "localhost" - debugger_port = app.debugger_port or 5678 - - try: - debugger = Debugger(debugger) - except Exception: - raise ValueError("debugger has a wrong value") - - try: - debugger_port = int(debugger_port) - except Exception: - raise ValueError("debugger_port has to be an integer") - - return DebuggerConfig( - debugger, - debugger_mode, - debugger_ip, - debugger_port, - ) - - -def write_debugger_startup_file( - app_path: Path, - pth_folder_path: Path | None, - app: AppConfig, - path_mappings: str, -): - debugger_cfg = DebuggerConfig.from_app(app) - - startup_modul = "__briefcase_startup__" - startup_code_path = app_path / f"{startup_modul}.py" - - if debugger_cfg.debugger == Debugger.PDB: - if debugger_cfg.debugger_mode != DebuggerMode.SERVER: - raise ValueError( - f"{debugger_cfg.debugger_mode} not supported by {debugger_cfg.debugger}" - ) - - with startup_code_path.open("w", encoding="utf-8") as f: - create_remote_pdb_startup_file( - f, - debugger_cfg, - ) - elif debugger_cfg.debugger == Debugger.DEBUGPY: - if debugger_cfg.debugger_mode not in (DebuggerMode.SERVER, DebuggerMode.CLIENT): - raise ValueError( - f"{debugger_cfg.debugger_mode} not supported by {debugger_cfg.debugger}" - ) - - with startup_code_path.open("w", encoding="utf-8") as f: - create_debugpy_startup_file( - f, - debugger_cfg, - path_mappings, - ) - else: - raise ValueError(f"debugger '{debugger_cfg.debugger}' not found") - - if pth_folder_path: - startup_pth_path = pth_folder_path / f"{startup_modul}.pth" - with startup_pth_path.open("w", encoding="utf-8") as f: - f.write(f"import {startup_modul}") - - -def create_remote_pdb_startup_file( - file: TextIO, - debugger_cfg: DebuggerConfig, -) -> str: - file.write( - f""" -# Generated {datetime.now()} - -import socket -import sys -import re - -NEWLINE_REGEX = re.compile("\\r?\\n") - -class SocketFileWrapper(object): - def __init__(self, connection: socket.socket): - self.connection = connection - self.stream = connection.makefile('rw') - - self.read = self.stream.read - self.readline = self.stream.readline - self.readlines = self.stream.readlines - self.close = self.stream.close - self.isatty = self.stream.isatty - self.flush = self.stream.flush - self.fileno = lambda: -1 - self.__iter__ = self.stream.__iter__ - - @property - def encoding(self): - return self.stream.encoding - - def write(self, data): - data = NEWLINE_REGEX.sub("\\r\\n", data) - self.connection.sendall(data.encode(self.stream.encoding)) - - def writelines(self, lines): - for line in lines: - self.write(line) - -def redirect_stdio(): - f'''Open a socket server and stream all stdio via the connection bidirectional.''' - ip = "{debugger_cfg.ip}" - port = {debugger_cfg.port} - print(f''' -Stdio redirector server opened at {{ip}}:{{port}}, waiting for connection... -To connect to stdio redirector use eg.: - - telnet {{ip}} {{port}} - - nc -C {{ip}} {{port}} - - socat readline tcp:{{ip}}:{{port}} -''') - - listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True) - listen_socket.bind((ip, port)) - listen_socket.listen(1) - connection, address = listen_socket.accept() - print(f"Stdio redirector accepted connection from {{repr(address)}}.") - - file_wrapper = SocketFileWrapper(connection) - - sys.stderr = file_wrapper - sys.stdout = file_wrapper - sys.stdin = file_wrapper - sys.__stderr__ = file_wrapper - sys.__stdout__ = file_wrapper - sys.__stdin__ = file_wrapper - -redirect_stdio() -""" - ) - - -def create_debugpy_startup_file( - file: TextIO, - debugger_cfg: DebuggerConfig, - path_mappings: str, -) -> str: - """Create the code that is necessary to start the debugger""" - file.write( - f""" -# Generated {datetime.now()} - -import os -import sys -from pathlib import Path - -def start_debugger(): - ip = "{debugger_cfg.ip}" - port = {debugger_cfg.port} - path_mappings = [] - {textwrap.indent(path_mappings, " ")} - - # When an app is bundled with briefcase "os.__file__" is not set at runtime - # on some platforms (eg. windows). But debugpy accesses it internally, so it - # has to be set or an Exception is raised from debugpy. - if not hasattr(os, "__file__"): - os.__file__ = "" - -""" - ) - if debugger_cfg.debugger_mode == DebuggerMode.CLIENT: - file.write( - """ - print(f''' -Connecting to debugpy server at {ip}:{port}... -To create the debugpy server using VSCode add the following configuration to launch.json and start the debugger: -{{ - "version": "0.2.0", - "configurations": [ - {{ - "name": "Briefcase: Attach (Listen)", - "type": "debugpy", - "request": "attach", - "listen": {{ - "host": "{ip}", - "port": {port} - }} - }} - ] -}} -''') - import debugpy - try: - debugpy.connect((ip, port)) - except ConnectionRefusedError as e: - print("Could not connect to debugpy server. Is it already started? We continue with the app...") - return -""" - ) - elif debugger_cfg.debugger_mode == DebuggerMode.SERVER: - file.write( - """ - print(f''' -The debugpy server started at {ip}:{port}, waiting for connection... -To connect to debugpy using VSCode add the following configuration to launch.json: -{{ - "version": "0.2.0", - "configurations": [ - {{ - "name": "Briefcase: Attach (Connect)", - "type": "debugpy", - "request": "attach", - "connect": {{ - "host": "{ip}", - "port": {port} - }} - }} - ] -}} -''') - import debugpy - debugpy.listen((ip, port), in_process_debug_adapter=True) -""" - ) - - file.write( - """ - if (len(path_mappings) > 0): - # path_mappings has to be applied after connection is established. If no connection is - # established this import will fail. - import pydevd_file_utils - - pydevd_file_utils.setup_client_server_paths(path_mappings) - -start_debugger() -""" - ) diff --git a/src/briefcase/debuggers/__init__.py b/src/briefcase/debuggers/__init__.py new file mode 100644 index 000000000..475e37875 --- /dev/null +++ b/src/briefcase/debuggers/__init__.py @@ -0,0 +1,22 @@ +import sys + +if sys.version_info >= (3, 10): # pragma: no-cover-if-lt-py310 + from importlib.metadata import entry_points +else: # pragma: no-cover-if-gte-py310 + # Before Python 3.10, entry_points did not support the group argument; + # so, the backport package must be used on older versions. + from importlib_metadata import entry_points + +from briefcase.debuggers.base import BaseDebugger +from briefcase.debuggers.debugpy import DebugpyDebugger # noqa: F401 +from briefcase.debuggers.pdb import PdbDebugger # noqa: F401 + +DEFAULT_DEBUGGER = PdbDebugger + + +def get_debuggers() -> dict[str, type[BaseDebugger]]: + """Loads built-in and third-party debuggers.""" + return { + entry_point.name: entry_point.load() + for entry_point in entry_points(group="briefcase.debuggers") + } diff --git a/src/briefcase/debuggers/base.py b/src/briefcase/debuggers/base.py new file mode 100644 index 000000000..c34577245 --- /dev/null +++ b/src/briefcase/debuggers/base.py @@ -0,0 +1,134 @@ +from __future__ import annotations + +import dataclasses +import enum +from abc import ABC, abstractmethod +from pathlib import Path +from typing import ClassVar, TextIO + +from briefcase.console import Console +from briefcase.exceptions import BriefcaseCommandError, BriefcaseConfigError + + +class DebuggerMode(enum.StrEnum): + SERVER = "server" + CLIENT = "client" + + +@dataclasses.dataclass +class DebuggerConfig: + mode: str | None + ip: str | None + port: int | None + + +def remote_debugger_config_from_string( + remote_debugger_config: str, +) -> tuple[str, DebuggerConfig]: + """ + Convert a remote debugger config string into a DebuggerConfig object. + + The config string is expected to be in the form: + "[DEBUGGER[,[IP:]PORT][,MODE]]" + + Config examples: + "" + "pdb" + "pdb,5678" + "pdb,,server" + "pdb,localhost:5678,server" + """ + debugger = ip_and_port = ip = port = mode = None + parts = remote_debugger_config.split(",") + if len(parts) == 1: + debugger = parts[0] + elif len(parts) == 2: + debugger, ip_and_port = parts + elif len(parts) == 3: + debugger, ip_and_port, mode = parts + else: + raise BriefcaseCommandError( + f"Invalid remote debugger specification: {remote_debugger_config}" + ) + + if ip_and_port is not None: + parts = ip_and_port.split(":") + if len(parts) == 1: + port = parts[0] + if port == "": + port = None + elif len(parts) == 2: + ip = parts[0] + port = parts[1] + else: + raise BriefcaseCommandError( + f"Invalid remote debugger specification: {remote_debugger_config}" + ) + + if port is not None: + try: + port = int(port) + except ValueError: + raise BriefcaseCommandError(f"Invalid remote debugger port: {port}") + + return debugger, DebuggerConfig(mode=mode, ip=ip, port=port) + + +STARTUP_MODULE = "_briefcase" + + +class BaseDebugger(ABC): + """Definition for a plugin that defines a new Briefcase debugger.""" + + supported_modes: ClassVar[list[DebuggerMode]] + default_mode: ClassVar[DebuggerMode] + default_ip: ClassVar[str] = "localhost" + default_port: ClassVar[int] = 5678 + + def __init__(self, console: Console, config: DebuggerConfig) -> None: + self.console = console + self.mode: DebuggerMode = DebuggerMode(config.mode or self.default_mode) + self.ip: str = config.ip or self.default_ip + self.port: int = config.port or self.default_port + + if self.mode not in self.supported_modes: + raise BriefcaseConfigError( + f"Unsupported debugger mode: {self.mode} for {self.__class__.__name__}" + ) + + @property + def additional_requirements(self) -> list[str]: + """Return a list of additional requirements for the debugger.""" + return [] + + @abstractmethod + def create_startup_file(self, file: TextIO, path_mappings: str) -> None: + """ + Create the code that is necessary to start the debugger. + + :param file: The file to write the startup code to. + :param path_mappings: The path mappings that should be used in the startup file. + """ + raise NotImplementedError() + + def write_startup_file( + self, + app_path: Path, + pth_folder_path: Path | None, + path_mappings: str, + ): + """ + Write the debugger startup file and create a .pth file to import it automatically at startup. + + :param app_path: The path to the application folder. + :param pth_folder_path: The path to the folder where the .pth file should be created. + :param path_mappings: The path mappings that should be used in the startup file. + """ + startup_code_path = app_path / f"{STARTUP_MODULE}.py" + with startup_code_path.open("w", encoding="utf-8") as f: + self.create_startup_file(f, path_mappings) + + if pth_folder_path: + startup_pth_path = pth_folder_path / f"{STARTUP_MODULE}.pth" + with startup_pth_path.open("w", encoding="utf-8") as f: + f.write(f"import {STARTUP_MODULE}") diff --git a/src/briefcase/debuggers/debugpy.py b/src/briefcase/debuggers/debugpy.py new file mode 100644 index 000000000..0307c2afa --- /dev/null +++ b/src/briefcase/debuggers/debugpy.py @@ -0,0 +1,109 @@ +import textwrap +from datetime import datetime +from typing import TextIO + +from briefcase.debuggers.base import BaseDebugger, DebuggerMode + + +class DebugpyDebugger(BaseDebugger): + """Definition for a plugin that defines a new Briefcase debugger.""" + + supported_modes = [DebuggerMode.SERVER, DebuggerMode.CLIENT] + default_mode = DebuggerMode.SERVER + + @property + def additional_requirements(self) -> list[str]: + """Return a list of additional requirements for the debugger.""" + return ["debugpy~=1.8.12"] + + def create_startup_file(self, file: TextIO, path_mappings: str) -> None: + """Create the code that is necessary to start the debugger""" + file.write( + f"""\ +# Generated {datetime.now()} + +import os +import sys +from pathlib import Path + +def start_debugger(): + ip = "{self.ip}" + port = {self.port} + path_mappings = [] + {textwrap.indent(path_mappings, " ")} + + # When an app is bundled with briefcase "os.__file__" is not set at runtime + # on some platforms (eg. windows). But debugpy accesses it internally, so it + # has to be set or an Exception is raised from debugpy. + if not hasattr(os, "__file__"): + os.__file__ = "" + +""" + ) + if self.mode == DebuggerMode.CLIENT: + file.write( + """\ + print(f''' +Connecting to debugpy server at {ip}:{port}... +To create the debugpy server using VSCode add the following configuration to launch.json and start the debugger: +{{ + "version": "0.2.0", + "configurations": [ + {{ + "name": "Briefcase: Attach (Listen)", + "type": "debugpy", + "request": "attach", + "listen": {{ + "host": "{ip}", + "port": {port} + }} + }} + ] +}} +''') + import debugpy + try: + debugpy.connect((ip, port)) + except ConnectionRefusedError as e: + print("Could not connect to debugpy server. Is it already started? We continue with the app...") + return +""" + ) + elif self.mode == DebuggerMode.SERVER: + file.write( + """\ + print(f''' +The debugpy server started at {ip}:{port}, waiting for connection... +To connect to debugpy using VSCode add the following configuration to launch.json: +{{ + "version": "0.2.0", + "configurations": [ + {{ + "name": "Briefcase: Attach (Connect)", + "type": "debugpy", + "request": "attach", + "connect": {{ + "host": "{ip}", + "port": {port} + }} + }} + ] +}} +''') + import debugpy + debugpy.listen((ip, port), in_process_debug_adapter=True) +""" + ) + + file.write( + """\ + if (len(path_mappings) > 0): + # path_mappings has to be applied after connection is established. If no connection is + # established this import will fail. + import pydevd_file_utils + + pydevd_file_utils.setup_client_server_paths(path_mappings) + +start_debugger() +""" + ) diff --git a/src/briefcase/debuggers/pdb.py b/src/briefcase/debuggers/pdb.py new file mode 100644 index 000000000..6e4531770 --- /dev/null +++ b/src/briefcase/debuggers/pdb.py @@ -0,0 +1,81 @@ +from datetime import datetime +from typing import TextIO + +from briefcase.debuggers.base import BaseDebugger, DebuggerMode + + +class PdbDebugger(BaseDebugger): + """Definition for a plugin that defines a new Briefcase debugger.""" + + supported_modes = [DebuggerMode.SERVER] + default_mode = DebuggerMode.SERVER + + def create_startup_file(self, file: TextIO, path_mappings: str) -> None: + """Create the code that is necessary to start the debugger""" + file.write( + f"""\ +# Generated {datetime.now()} + +import socket +import sys +import re + +NEWLINE_REGEX = re.compile("\\r?\\n") + +class SocketFileWrapper(object): + def __init__(self, connection: socket.socket): + self.connection = connection + self.stream = connection.makefile('rw') + + self.read = self.stream.read + self.readline = self.stream.readline + self.readlines = self.stream.readlines + self.close = self.stream.close + self.isatty = self.stream.isatty + self.flush = self.stream.flush + self.fileno = lambda: -1 + self.__iter__ = self.stream.__iter__ + + @property + def encoding(self): + return self.stream.encoding + + def write(self, data): + data = NEWLINE_REGEX.sub("\\r\\n", data) + self.connection.sendall(data.encode(self.stream.encoding)) + + def writelines(self, lines): + for line in lines: + self.write(line) + +def redirect_stdio(): + f'''Open a socket server and stream all stdio via the connection bidirectional.''' + ip = "{self.ip}" + port = {self.port} + print(f''' +Stdio redirector server opened at {{ip}}:{{port}}, waiting for connection... +To connect to stdio redirector use eg.: + - telnet {{ip}} {{port}} + - nc -C {{ip}} {{port}} + - socat readline tcp:{{ip}}:{{port}} +''') + + listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True) + listen_socket.bind((ip, port)) + listen_socket.listen(1) + connection, address = listen_socket.accept() + print(f"Stdio redirector accepted connection from {{repr(address)}}.") + + file_wrapper = SocketFileWrapper(connection) + + sys.stderr = file_wrapper + sys.stdout = file_wrapper + sys.stdin = file_wrapper + sys.__stderr__ = file_wrapper + sys.__stdout__ = file_wrapper + sys.__stdin__ = file_wrapper + +redirect_stdio() +""" + ) diff --git a/src/briefcase/platforms/android/gradle.py b/src/briefcase/platforms/android/gradle.py index f011b6727..ec2bfe541 100644 --- a/src/briefcase/platforms/android/gradle.py +++ b/src/briefcase/platforms/android/gradle.py @@ -17,7 +17,7 @@ ) from briefcase.config import AppConfig, parsed_version from briefcase.console import ANSI_ESC_SEQ_RE_DEF -from briefcase.debugger import DebuggerConfig, DebuggerMode +from briefcase.debuggers.base import BaseDebugger, DebuggerMode from briefcase.exceptions import BriefcaseCommandError from briefcase.integrations.android_sdk import ADB, AndroidSDK from briefcase.integrations.subprocess import SubprocessArgT @@ -395,7 +395,7 @@ def run_app( self, app: AppConfig, test_mode: bool, - debug_mode: bool, + remote_debugger: str | None, passthrough: list[str], device_or_avd=None, extra_emulator_args=None, @@ -406,7 +406,7 @@ def run_app( :param app: The config object for the app :param test_mode: Boolean; Is the app running in test mode? - :param debug_mode: Boolean; Is the app running in debug mode? + :param remote_debugger: Remote debugger that should be used. :param passthrough: The list of arguments to pass to the app :param device_or_avd: The device to target. If ``None``, the user will be asked to re-run the command selecting a specific device. @@ -437,6 +437,8 @@ def run_app( avd, extra_emulator_args ) + debugger = self.create_debugger(remote_debugger) + try: label = "test suite" if test_mode else "app" @@ -460,9 +462,9 @@ def run_app( with self.console.wait_bar("Installing new app version..."): adb.install_apk(self.binary_path(app)) - if debug_mode: + if debugger: with self.console.wait_bar("Establishing debugger connection..."): - self.establish_debugger_connection(app, adb) + self.establish_debugger_connection(adb, debugger) # To start the app, we launch `org.beeware.android.MainActivity`. with self.console.wait_bar(f"Launching {label}..."): @@ -514,17 +516,16 @@ def run_app( with self.tools.console.wait_bar("Stopping emulator..."): adb.kill() - def establish_debugger_connection(self, app: AppConfig, adb: ADB): + def establish_debugger_connection(self, adb: ADB, debugger: BaseDebugger): """Forward/Reverse the ports necessary for remote debugging. :param app: The config object for the app :param adb: Access to the adb """ - debugger_cfg = DebuggerConfig.from_app(app) - if debugger_cfg.debugger_mode == DebuggerMode.SERVER: - adb.forward(debugger_cfg.port, debugger_cfg.port) - elif debugger_cfg.port == DebuggerMode.CLIENT: - adb.reverse(debugger_cfg.port, debugger_cfg.port) + if debugger.mode == DebuggerMode.SERVER: + adb.forward(debugger.port, debugger.port) + elif debugger.port == DebuggerMode.CLIENT: + adb.reverse(debugger.port, debugger.port) class GradlePackageCommand(GradleMixin, PackageCommand): From fb79869983b7b3e422593f23ef0ca7085427354d Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sat, 15 Mar 2025 14:38:10 +0100 Subject: [PATCH 003/131] some cleanups --- src/briefcase/commands/create.py | 30 +++++++++++++++++++++++------- src/briefcase/commands/update.py | 6 +----- src/briefcase/debuggers/base.py | 2 +- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/briefcase/commands/create.py b/src/briefcase/commands/create.py index 7c8873f87..560acd3da 100644 --- a/src/briefcase/commands/create.py +++ b/src/briefcase/commands/create.py @@ -762,15 +762,30 @@ def install_app_code( if debugger: with self.console.wait_bar("Writing debugger startup files..."): # TODO: How to get the "pth_folder_path"? - # - On windows "app_path.parent" is working - # - On android "app_path" is working, but then when running "__briefcase_startup__.py" via .pth file - # importing socket raises an error... - # - # As long as it is not clear for all platforms we have to call "import __briefcase_startup__" manually + # As long as it is not clear for all platforms we have to call "import _briefcase" manually # in "main.py" + # This should be placed somewhere else, when it is clear for all platforms. + if self.platform == "windows": + # this is working + pth_folder_path = app_path.parent + elif self.platform == "android": + # this is working, but when running "_briefcase.py" via .pth file importing socket raises + # an error... + pth_folder_path = app_path + elif self.platform == "macos": + pth_folder_path = None # TODO: find it out... + elif self.platform == "ios": + pth_folder_path = None # TODO: find it out... + elif self.platform == "linux": + pth_folder_path = None # TODO: find it out... + elif self.platform == "web": + pth_folder_path = None # TODO: find it out... + else: + pth_folder_path = None + debugger.write_startup_file( app_path=app_path, - pth_folder_path=None, # TODO: see above + pth_folder_path=pth_folder_path, path_mappings=self.debugger_path_mappings(app, sources), ) @@ -997,7 +1012,8 @@ def create_app( # are in place since the app tools may be dependent on them. self.verify_app(app) self.console.print(f"{remote_debugger=}") - debugger = self.create_debugger(remote_debugger) if remote_debugger else None + + debugger = self.create_debugger(remote_debugger) self.console.info("Installing application code...", prefix=app.app_name) self.install_app_code(app=app, test_mode=test_mode, debugger=debugger) diff --git a/src/briefcase/commands/update.py b/src/briefcase/commands/update.py index 6ba012574..9851b6c60 100644 --- a/src/briefcase/commands/update.py +++ b/src/briefcase/commands/update.py @@ -55,11 +55,7 @@ def update_app( self.verify_app(app) self.console.print(f"{remote_debugger=}") - debugger = ( - self.create_debugger(remote_debugger) - if remote_debugger is not None - else None - ) + debugger = self.create_debugger(remote_debugger) self.console.info("Updating application code...", prefix=app.app_name) self.install_app_code(app=app, test_mode=test_mode, debugger=debugger) diff --git a/src/briefcase/debuggers/base.py b/src/briefcase/debuggers/base.py index c34577245..cc160071a 100644 --- a/src/briefcase/debuggers/base.py +++ b/src/briefcase/debuggers/base.py @@ -10,7 +10,7 @@ from briefcase.exceptions import BriefcaseCommandError, BriefcaseConfigError -class DebuggerMode(enum.StrEnum): +class DebuggerMode(str, enum.Enum): SERVER = "server" CLIENT = "client" From 70b290ed32e231c5bea2e6176b2347d683c04b5f Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sun, 16 Mar 2025 01:17:26 +0100 Subject: [PATCH 004/131] added "pth_folder_path" for iOS and Linux --- src/briefcase/commands/create.py | 71 +++++++++++++++++++++++++------- 1 file changed, 57 insertions(+), 14 deletions(-) diff --git a/src/briefcase/commands/create.py b/src/briefcase/commands/create.py index 560acd3da..4e9642d70 100644 --- a/src/briefcase/commands/create.py +++ b/src/briefcase/commands/create.py @@ -762,24 +762,67 @@ def install_app_code( if debugger: with self.console.wait_bar("Writing debugger startup files..."): # TODO: How to get the "pth_folder_path"? - # As long as it is not clear for all platforms we have to call "import _briefcase" manually - # in "main.py" - # This should be placed somewhere else, when it is clear for all platforms. - if self.platform == "windows": - # this is working + # As long as it is not clear for all platforms we have to call : + # + # try: + # import _briefcase + # except Exception: + # pass + # + # manually in "__main__.py" + + # TODO: This should be placed somewhere else, when it is clear for all platforms. + if self.platform == "windows" and self.output_format == "app": + # This is working. pth_folder_path = app_path.parent + + elif ( + self.platform == "windows" and self.output_format == "VisualStudio" + ): + # TODO: Not tested yet + pth_folder_path = None + elif self.platform == "android": - # this is working, but when running "_briefcase.py" via .pth file importing socket raises - # an error... + # This is working, but when running "_briefcase.py" via .pth file importing socket raises + # an error... See: https://github.com/chaquo/chaquopy/issues/1338 pth_folder_path = app_path - elif self.platform == "macos": - pth_folder_path = None # TODO: find it out... - elif self.platform == "ios": - pth_folder_path = None # TODO: find it out... - elif self.platform == "linux": - pth_folder_path = None # TODO: find it out... + + elif self.platform == "macOS" and self.output_format == "app": + # TODO: Not tested yet + pth_folder_path = None + + elif self.platform == "macOS" and self.output_format == "Xcode": + # TODO: Not tested yet + pth_folder_path = None + + elif self.platform == "iOS" and self.output_format == "Xcode": + # This is working, but makes problems only if `std-nslog` is removed from the dependencies. + pth_folder_path = self.support_path(app) / ( + "Python.xcframework/ios-arm64_x86_64-simulator/" + f"lib/python{self.python_version_tag}/site-packages" + ) + + elif self.platform == "linux" and self.output_format == "system": + # I could not find a way to get the path of the folder where the .pth file should be located. + # I guess it is not even possible because `config.site_import = 0;` See: + # https://github.com/beeware/briefcase-linux-system-template/blob/a7e1407c15a1c3a3246d10db53f638bf48e1bb26/%7B%7B%20cookiecutter.format%20%7D%7D/bootstrap/main.c#L68C5-L68C28 + pth_folder_path = None + + elif self.platform == "linux" and self.output_format == "flatpak": + # This is working. + pth_folder_path = ( + self.support_path(app) + / f"python/lib/python{self.python_version_tag}/site-packages" + ) + + elif self.platform == "linux" and self.output_format == "appimage": + # TODO: Not tested yet + pth_folder_path = None + elif self.platform == "web": - pth_folder_path = None # TODO: find it out... + # TODO: Not tested yet + pth_folder_path = None + else: pth_folder_path = None From 8746452a17460afdeb2961171c17de04582477d2 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Wed, 19 Mar 2025 20:26:29 +0100 Subject: [PATCH 005/131] - use `BRIEFCASE_MAIN_MODULE` environment variable to start the remote debugger instead of a .pth file - moved `remote_debugger` to AppConfig --- src/briefcase/commands/base.py | 34 ++--- src/briefcase/commands/build.py | 3 +- src/briefcase/commands/create.py | 145 ++++++++-------------- src/briefcase/commands/run.py | 7 +- src/briefcase/commands/update.py | 14 +-- src/briefcase/config.py | 13 +- src/briefcase/debuggers/base.py | 42 ++----- src/briefcase/debuggers/debugpy.py | 31 ++--- src/briefcase/debuggers/pdb.py | 22 ++-- src/briefcase/platforms/android/gradle.py | 8 +- 10 files changed, 121 insertions(+), 198 deletions(-) diff --git a/src/briefcase/commands/base.py b/src/briefcase/commands/base.py index 913fd02a6..156a95e80 100644 --- a/src/briefcase/commands/base.py +++ b/src/briefcase/commands/base.py @@ -21,10 +21,7 @@ from platformdirs import PlatformDirs from briefcase.debuggers import DEFAULT_DEBUGGER, get_debuggers -from briefcase.debuggers.base import ( - BaseDebugger, - remote_debugger_config_from_string, -) +from briefcase.debuggers.base import parse_remote_debugger_cfg if sys.version_info >= (3, 11): # pragma: no-cover-if-lt-py311 import tomllib @@ -601,7 +598,9 @@ def finalize_app_config(self, app: AppConfig): :param app: The app configuration to finalize. """ - def finalize(self, app: AppConfig | None = None): + def finalize( + self, app: AppConfig | None = None, remote_debugger_cfg: str | None = None + ): """Finalize Briefcase configuration. This will: @@ -621,10 +620,12 @@ def finalize(self, app: AppConfig | None = None): if app is None: for app in self.apps.values(): if hasattr(app, "__draft__"): + self.create_remote_debugger(app, remote_debugger_cfg) self.finalize_app_config(app) delattr(app, "__draft__") else: if hasattr(app, "__draft__"): + self.create_remote_debugger(app, remote_debugger_cfg) self.finalize_app_config(app) delattr(app, "__draft__") @@ -696,19 +697,18 @@ def verify_required_python(self, app: AppConfig): version_specifier=requires_python, running_version=running_version ) - def create_debugger(self, remote_debugger: str | None) -> BaseDebugger | None: - """Select and instantiate a debugger for the new project. - - :returns: An instance of the debugger that the user has selected. - """ - if remote_debugger is None: + def create_remote_debugger(self, app: AppConfig, remote_debugger_cfg: str | None): + """Select and instantiate a debugger for the project.""" + if remote_debugger_cfg is None: return None - debugger, config = remote_debugger_config_from_string(remote_debugger) + with self.console.wait_bar("Loading remote debugger config..."): + debugger, config = parse_remote_debugger_cfg(remote_debugger_cfg) - debuggers = get_debuggers() - debugger_class = debuggers[debugger] if debugger else DEFAULT_DEBUGGER - return debugger_class(console=self.console, config=config) + debuggers = get_debuggers() + debugger_class = debuggers[debugger] if debugger else DEFAULT_DEBUGGER + app.remote_debugger = debugger_class(console=self.console, config=config) + self.console.info(f"Using '{app.remote_debugger}'") def parse_options(self, extra): """Parse the command line arguments for the Command. @@ -898,8 +898,8 @@ def _add_test_and_debug_options(self, parser, context_label): help=f"{context_label} the app in test mode", ) parser.add_argument( - "--remote-debugger", - dest="remote_debugger", + "--remote-debug", + dest="remote_debugger_cfg", nargs="?", default=None, const="", diff --git a/src/briefcase/commands/build.py b/src/briefcase/commands/build.py index 819f8a758..155eed801 100644 --- a/src/briefcase/commands/build.py +++ b/src/briefcase/commands/build.py @@ -104,6 +104,7 @@ def __call__( update_stub: bool = False, no_update: bool = False, test_mode: bool = False, + remote_debugger_cfg: str | None = None, **options, ) -> dict | None: # Has the user requested an invalid set of options? @@ -132,7 +133,7 @@ def __call__( # Confirm host compatibility, that all required tools are available, # and that the app configuration is finalized. - self.finalize(app) + self.finalize(app, remote_debugger_cfg) if app_name: try: diff --git a/src/briefcase/commands/create.py b/src/briefcase/commands/create.py index 4e9642d70..4562902a0 100644 --- a/src/briefcase/commands/create.py +++ b/src/briefcase/commands/create.py @@ -13,7 +13,6 @@ import briefcase from briefcase.config import AppConfig -from briefcase.debuggers import BaseDebugger from briefcase.exceptions import ( BriefcaseCommandError, InvalidStubBinary, @@ -82,6 +81,46 @@ def write_dist_info(app: AppConfig, dist_info_path: Path): f.write(f"{app.module_name}\n") +def write_launcher_module( + launcher_path: Path, + main_module: str, + start_remote_debugger_fct: str, +): + """Install the launcher folder for the application. + + :param app: The config object for the app + :param launcher_path: The path into which the launcher folder should be written. + :main_module: Name of the real main module to run. + :param start_remote_debugger_fct: A string to a code snippet, that at least + defines a "start_remote_debugger()" function. + """ + # Create launcher folder, and write a minimal launcher file + with launcher_path.open("w", encoding="utf-8") as f: + f.write( + f"""\ +# Generated {datetime.now()} + +# #################### REMOTE DEBUGGER CODE - START ########################### +{start_remote_debugger_fct} +# #################### REMOTE DEBUGGER CODE - END ############################# + +def main(): + print("Launcher started") + + print("Starting remote debugger...") + start_remote_debugger() + + # Run main module + print(f"Starting main module '{main_module}'...") + import runpy + runpy._run_module_as_main("{main_module}") + +if __name__ == "__main__": + main() +""" + ) + + class CreateCommand(BaseCommand): command = "create" description = "Create a new app for a target platform." @@ -667,9 +706,7 @@ def _install_app_requirements( else: self.console.info("No application requirements.") - def install_app_requirements( - self, app: AppConfig, test_mode: bool, debugger: BaseDebugger | None - ): + def install_app_requirements(self, app: AppConfig, test_mode: bool): """Handle requirements for the app. This will result in either (in preferential order): @@ -680,9 +717,6 @@ def install_app_requirements( If ``test_mode`` is True, the test requirements will also be installed. - If ``debugger`` is provided, the debugger's additional requirements will also - be installed. - If the path index doesn't specify either of the path index entries, an error is raised. @@ -694,8 +728,8 @@ def install_app_requirements( if test_mode and app.test_requires: requires.extend(app.test_requires) - if debugger: - requires.extend(debugger.additional_requirements) + if app.remote_debugger: + requires.extend(app.remote_debugger.additional_requirements) try: requirements_path = self.app_requirements_path(app) @@ -723,14 +757,11 @@ def install_app_requirements( "`app_requirements_path` or `app_packages_path`" ) from e - def install_app_code( - self, app: AppConfig, test_mode: bool, debugger: BaseDebugger | None - ): + def install_app_code(self, app: AppConfig, test_mode: bool): """Install the application code into the bundle. :param app: The config object for the app :param test_mode: Should the application test code also be installed? - :param debugger: Debugger to be used or None if no debugger should be used. """ # Remove existing app folder if it exists app_path = self.app_path(app) @@ -759,77 +790,14 @@ def install_app_code( else: self.console.info(f"No sources defined for {app.app_name}.") - if debugger: - with self.console.wait_bar("Writing debugger startup files..."): - # TODO: How to get the "pth_folder_path"? - # As long as it is not clear for all platforms we have to call : - # - # try: - # import _briefcase - # except Exception: - # pass - # - # manually in "__main__.py" - - # TODO: This should be placed somewhere else, when it is clear for all platforms. - if self.platform == "windows" and self.output_format == "app": - # This is working. - pth_folder_path = app_path.parent - - elif ( - self.platform == "windows" and self.output_format == "VisualStudio" - ): - # TODO: Not tested yet - pth_folder_path = None - - elif self.platform == "android": - # This is working, but when running "_briefcase.py" via .pth file importing socket raises - # an error... See: https://github.com/chaquo/chaquopy/issues/1338 - pth_folder_path = app_path - - elif self.platform == "macOS" and self.output_format == "app": - # TODO: Not tested yet - pth_folder_path = None - - elif self.platform == "macOS" and self.output_format == "Xcode": - # TODO: Not tested yet - pth_folder_path = None - - elif self.platform == "iOS" and self.output_format == "Xcode": - # This is working, but makes problems only if `std-nslog` is removed from the dependencies. - pth_folder_path = self.support_path(app) / ( - "Python.xcframework/ios-arm64_x86_64-simulator/" - f"lib/python{self.python_version_tag}/site-packages" - ) - - elif self.platform == "linux" and self.output_format == "system": - # I could not find a way to get the path of the folder where the .pth file should be located. - # I guess it is not even possible because `config.site_import = 0;` See: - # https://github.com/beeware/briefcase-linux-system-template/blob/a7e1407c15a1c3a3246d10db53f638bf48e1bb26/%7B%7B%20cookiecutter.format%20%7D%7D/bootstrap/main.c#L68C5-L68C28 - pth_folder_path = None - - elif self.platform == "linux" and self.output_format == "flatpak": - # This is working. - pth_folder_path = ( - self.support_path(app) - / f"python/lib/python{self.python_version_tag}/site-packages" - ) - - elif self.platform == "linux" and self.output_format == "appimage": - # TODO: Not tested yet - pth_folder_path = None - - elif self.platform == "web": - # TODO: Not tested yet - pth_folder_path = None - - else: - pth_folder_path = None - - debugger.write_startup_file( - app_path=app_path, - pth_folder_path=pth_folder_path, - path_mappings=self.debugger_path_mappings(app, sources), + if app.remote_debugger: + with self.console.wait_bar("Writing launcher files..."): + write_launcher_module( + launcher_path=self.app_path(app) / "_briefcase_launcher.py", + main_module=app.main_module(test_mode, include_launcher=False), + start_remote_debugger_fct=app.remote_debugger.generate_startup_code( + self.debugger_path_mappings(app, sources) + ), ) # Write the dist-info folder for the application. @@ -1008,14 +976,12 @@ def create_app( self, app: AppConfig, test_mode: bool = False, - remote_debugger: str | None = None, **options, ): """Create an application bundle. :param app: The config object for the app :param test_mode: Should the app be updated in test mode? (default: False) - :param remote_debugger: Remote debugger that should be used. (default: None) """ if not app.supported: raise UnsupportedPlatform(self.platform) @@ -1054,15 +1020,12 @@ def create_app( # Verify the app after the app template and support package # are in place since the app tools may be dependent on them. self.verify_app(app) - self.console.print(f"{remote_debugger=}") - - debugger = self.create_debugger(remote_debugger) self.console.info("Installing application code...", prefix=app.app_name) - self.install_app_code(app=app, test_mode=test_mode, debugger=debugger) + self.install_app_code(app=app, test_mode=test_mode) self.console.info("Installing requirements...", prefix=app.app_name) - self.install_app_requirements(app=app, test_mode=test_mode, debugger=debugger) + self.install_app_requirements(app=app, test_mode=test_mode) self.console.info("Installing application resources...", prefix=app.app_name) self.install_app_resources(app=app) diff --git a/src/briefcase/commands/run.py b/src/briefcase/commands/run.py index b8ce9b3af..715b126f0 100644 --- a/src/briefcase/commands/run.py +++ b/src/briefcase/commands/run.py @@ -237,8 +237,8 @@ def _prepare_app_kwargs(self, app: AppConfig, test_mode: bool): if self.console.is_debug: env["BRIEFCASE_DEBUG"] = "1" - if test_mode: - # In test mode, set a BRIEFCASE_MAIN_MODULE environment variable + if test_mode or app.remote_debugger: + # In test or with remote debugger mode, set a BRIEFCASE_MAIN_MODULE environment variable # to override the module at startup env["BRIEFCASE_MAIN_MODULE"] = app.main_module(test_mode) self.console.info("Starting test_suite...", prefix=app.app_name) @@ -268,6 +268,7 @@ def __call__( update_stub: bool = False, no_update: bool = False, test_mode: bool = False, + remote_debugger_cfg: str | None = None, passthrough: list[str] | None = None, **options, ) -> dict | None: @@ -290,7 +291,7 @@ def __call__( # Confirm host compatibility, that all required tools are available, # and that the app configuration is finalized. - self.finalize(app) + self.finalize(app, remote_debugger_cfg) template_file = self.bundle_path(app) exec_file = self.binary_executable_path(app) diff --git a/src/briefcase/commands/update.py b/src/briefcase/commands/update.py index 9851b6c60..bc08300b4 100644 --- a/src/briefcase/commands/update.py +++ b/src/briefcase/commands/update.py @@ -33,7 +33,6 @@ def update_app( update_support: bool, update_stub: bool, test_mode: bool, - remote_debugger: str | None, **options, ) -> dict | None: """Update an existing application bundle. @@ -44,7 +43,6 @@ def update_app( :param update_support: Should app support be updated? :param update_stub: Should stub binary be updated? :param test_mode: Should the app be updated in test mode? - :param remote_debugger: Remote debugger that should be used. """ if not self.bundle_path(app).exists(): @@ -54,17 +52,13 @@ def update_app( return self.verify_app(app) - self.console.print(f"{remote_debugger=}") - debugger = self.create_debugger(remote_debugger) self.console.info("Updating application code...", prefix=app.app_name) - self.install_app_code(app=app, test_mode=test_mode, debugger=debugger) + self.install_app_code(app=app, test_mode=test_mode) if update_requirements: self.console.info("Updating requirements...", prefix=app.app_name) - self.install_app_requirements( - app=app, test_mode=test_mode, debugger=debugger - ) + self.install_app_requirements(app=app, test_mode=test_mode) if update_resources: self.console.info("Updating application resources...", prefix=app.app_name) @@ -102,12 +96,12 @@ def __call__( update_support: bool = False, update_stub: bool = False, test_mode: bool = False, - remote_debugger: str | None = None, + remote_debugger_cfg: str | None = None, **options, ) -> dict | None: # Confirm host compatibility, that all required tools are available, # and that the app configuration is finalized. - self.finalize(app) + self.finalize(app, remote_debugger_cfg) if app_name: try: diff --git a/src/briefcase/config.py b/src/briefcase/config.py index 607f889de..00d40a30e 100644 --- a/src/briefcase/config.py +++ b/src/briefcase/config.py @@ -13,6 +13,7 @@ else: # pragma: no-cover-if-gte-py311 import tomli as tomllib +from briefcase.debuggers.base import BaseDebugger from briefcase.platforms import get_output_formats, get_platforms from .constants import RESERVED_WORDS @@ -317,6 +318,7 @@ def __init__( self.requirement_installer_args = ( [] if requirement_installer_args is None else requirement_installer_args ) + self.remote_debugger: BaseDebugger | None = None if not is_valid_app_name(self.app_name): raise BriefcaseConfigError( @@ -421,7 +423,7 @@ def PYTHONPATH(self, test_mode): paths.append(path) return paths - def main_module(self, test_mode: bool): + def main_module(self, test_mode: bool, include_launcher: bool = True): """The path to the main module for the app. In normal operation, this is ``app.module_name``; however, @@ -430,9 +432,14 @@ def main_module(self, test_mode: bool): :param test_mode: Are we running in test mode? """ if test_mode: - return f"tests.{self.module_name}" + module_name = f"tests.{self.module_name}" else: - return self.module_name + module_name = self.module_name + + if self.remote_debugger and include_launcher: + return "_briefcase_launcher" + else: + return module_name def merge_config(config, data): diff --git a/src/briefcase/debuggers/base.py b/src/briefcase/debuggers/base.py index cc160071a..08fd9c18c 100644 --- a/src/briefcase/debuggers/base.py +++ b/src/briefcase/debuggers/base.py @@ -3,8 +3,7 @@ import dataclasses import enum from abc import ABC, abstractmethod -from pathlib import Path -from typing import ClassVar, TextIO +from typing import ClassVar from briefcase.console import Console from briefcase.exceptions import BriefcaseCommandError, BriefcaseConfigError @@ -22,8 +21,8 @@ class DebuggerConfig: port: int | None -def remote_debugger_config_from_string( - remote_debugger_config: str, +def parse_remote_debugger_cfg( + remote_debugger_cfg: str, ) -> tuple[str, DebuggerConfig]: """ Convert a remote debugger config string into a DebuggerConfig object. @@ -39,7 +38,7 @@ def remote_debugger_config_from_string( "pdb,localhost:5678,server" """ debugger = ip_and_port = ip = port = mode = None - parts = remote_debugger_config.split(",") + parts = remote_debugger_cfg.split(",") if len(parts) == 1: debugger = parts[0] elif len(parts) == 2: @@ -48,7 +47,7 @@ def remote_debugger_config_from_string( debugger, ip_and_port, mode = parts else: raise BriefcaseCommandError( - f"Invalid remote debugger specification: {remote_debugger_config}" + f"Invalid remote debugger specification: {remote_debugger_cfg}" ) if ip_and_port is not None: @@ -62,7 +61,7 @@ def remote_debugger_config_from_string( port = parts[1] else: raise BriefcaseCommandError( - f"Invalid remote debugger specification: {remote_debugger_config}" + f"Invalid remote debugger specification: {remote_debugger_cfg}" ) if port is not None: @@ -74,9 +73,6 @@ def remote_debugger_config_from_string( return debugger, DebuggerConfig(mode=mode, ip=ip, port=port) -STARTUP_MODULE = "_briefcase" - - class BaseDebugger(ABC): """Definition for a plugin that defines a new Briefcase debugger.""" @@ -102,33 +98,11 @@ def additional_requirements(self) -> list[str]: return [] @abstractmethod - def create_startup_file(self, file: TextIO, path_mappings: str) -> None: + def generate_startup_code(self, path_mappings: str) -> str: """ - Create the code that is necessary to start the debugger. + Generate the code that is necessary to start the debugger. :param file: The file to write the startup code to. :param path_mappings: The path mappings that should be used in the startup file. """ raise NotImplementedError() - - def write_startup_file( - self, - app_path: Path, - pth_folder_path: Path | None, - path_mappings: str, - ): - """ - Write the debugger startup file and create a .pth file to import it automatically at startup. - - :param app_path: The path to the application folder. - :param pth_folder_path: The path to the folder where the .pth file should be created. - :param path_mappings: The path mappings that should be used in the startup file. - """ - startup_code_path = app_path / f"{STARTUP_MODULE}.py" - with startup_code_path.open("w", encoding="utf-8") as f: - self.create_startup_file(f, path_mappings) - - if pth_folder_path: - startup_pth_path = pth_folder_path / f"{STARTUP_MODULE}.pth" - with startup_pth_path.open("w", encoding="utf-8") as f: - f.write(f"import {STARTUP_MODULE}") diff --git a/src/briefcase/debuggers/debugpy.py b/src/briefcase/debuggers/debugpy.py index 0307c2afa..8d98b8010 100644 --- a/src/briefcase/debuggers/debugpy.py +++ b/src/briefcase/debuggers/debugpy.py @@ -1,6 +1,4 @@ import textwrap -from datetime import datetime -from typing import TextIO from briefcase.debuggers.base import BaseDebugger, DebuggerMode @@ -16,17 +14,18 @@ def additional_requirements(self) -> list[str]: """Return a list of additional requirements for the debugger.""" return ["debugpy~=1.8.12"] - def create_startup_file(self, file: TextIO, path_mappings: str) -> None: - """Create the code that is necessary to start the debugger""" - file.write( - f"""\ -# Generated {datetime.now()} + def generate_startup_code(self, path_mappings: str) -> str: + """ + Generate the code that is necessary to start the debugger. + :param path_mappings: The path mappings that should be used in the startup file. + """ + code = f"""\ import os import sys from pathlib import Path -def start_debugger(): +def start_remote_debugger(): ip = "{self.ip}" port = {self.port} path_mappings = [] @@ -39,10 +38,8 @@ def start_debugger(): os.__file__ = "" """ - ) if self.mode == DebuggerMode.CLIENT: - file.write( - """\ + code += """\ print(f''' Connecting to debugpy server at {ip}:{port}... To create the debugpy server using VSCode add the following configuration to launch.json and start the debugger: @@ -68,10 +65,8 @@ def start_debugger(): print("Could not connect to debugpy server. Is it already started? We continue with the app...") return """ - ) elif self.mode == DebuggerMode.SERVER: - file.write( - """\ + code += """\ print(f''' The debugpy server started at {ip}:{port}, waiting for connection... To connect to debugpy using VSCode add the following configuration to launch.json: @@ -93,17 +88,13 @@ def start_debugger(): import debugpy debugpy.listen((ip, port), in_process_debug_adapter=True) """ - ) - file.write( - """\ + code += """\ if (len(path_mappings) > 0): # path_mappings has to be applied after connection is established. If no connection is # established this import will fail. import pydevd_file_utils pydevd_file_utils.setup_client_server_paths(path_mappings) - -start_debugger() """ - ) + return code diff --git a/src/briefcase/debuggers/pdb.py b/src/briefcase/debuggers/pdb.py index 6e4531770..b763adda4 100644 --- a/src/briefcase/debuggers/pdb.py +++ b/src/briefcase/debuggers/pdb.py @@ -1,6 +1,3 @@ -from datetime import datetime -from typing import TextIO - from briefcase.debuggers.base import BaseDebugger, DebuggerMode @@ -10,12 +7,13 @@ class PdbDebugger(BaseDebugger): supported_modes = [DebuggerMode.SERVER] default_mode = DebuggerMode.SERVER - def create_startup_file(self, file: TextIO, path_mappings: str) -> None: - """Create the code that is necessary to start the debugger""" - file.write( - f"""\ -# Generated {datetime.now()} + def generate_startup_code(self, path_mappings: str) -> str: + """ + Generate the code that is necessary to start the debugger. + :param path_mappings: The path mappings that should be used in the startup file. + """ + code = f"""\ import socket import sys import re @@ -48,8 +46,8 @@ def writelines(self, lines): for line in lines: self.write(line) -def redirect_stdio(): - f'''Open a socket server and stream all stdio via the connection bidirectional.''' +def start_remote_debugger(): + '''Open a socket server and stream all stdio via the connection bidirectional.''' ip = "{self.ip}" port = {self.port} print(f''' @@ -75,7 +73,5 @@ def redirect_stdio(): sys.__stderr__ = file_wrapper sys.__stdout__ = file_wrapper sys.__stdin__ = file_wrapper - -redirect_stdio() """ - ) + return code diff --git a/src/briefcase/platforms/android/gradle.py b/src/briefcase/platforms/android/gradle.py index ec2bfe541..19d2575e2 100644 --- a/src/briefcase/platforms/android/gradle.py +++ b/src/briefcase/platforms/android/gradle.py @@ -395,7 +395,6 @@ def run_app( self, app: AppConfig, test_mode: bool, - remote_debugger: str | None, passthrough: list[str], device_or_avd=None, extra_emulator_args=None, @@ -406,7 +405,6 @@ def run_app( :param app: The config object for the app :param test_mode: Boolean; Is the app running in test mode? - :param remote_debugger: Remote debugger that should be used. :param passthrough: The list of arguments to pass to the app :param device_or_avd: The device to target. If ``None``, the user will be asked to re-run the command selecting a specific device. @@ -437,8 +435,6 @@ def run_app( avd, extra_emulator_args ) - debugger = self.create_debugger(remote_debugger) - try: label = "test suite" if test_mode else "app" @@ -462,9 +458,9 @@ def run_app( with self.console.wait_bar("Installing new app version..."): adb.install_apk(self.binary_path(app)) - if debugger: + if app.remote_debugger: with self.console.wait_bar("Establishing debugger connection..."): - self.establish_debugger_connection(adb, debugger) + self.establish_debugger_connection(adb, app.remote_debugger) # To start the app, we launch `org.beeware.android.MainActivity`. with self.console.wait_bar(f"Launching {label}..."): From aad75652bff75aa662b15f385c203dad81a0d5ab Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Wed, 19 Mar 2025 20:54:13 +0100 Subject: [PATCH 006/131] fix for linux flatpak --- src/briefcase/platforms/linux/flatpak.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/briefcase/platforms/linux/flatpak.py b/src/briefcase/platforms/linux/flatpak.py index 05675057f..2d7cb04c4 100644 --- a/src/briefcase/platforms/linux/flatpak.py +++ b/src/briefcase/platforms/linux/flatpak.py @@ -217,7 +217,7 @@ def run_app( # Starting a flatpak has slightly different startup arguments; however, # the rest of the app startup process is the same. Transform the output # of the "default" behavior to be in flatpak format. - if test_mode: + if test_mode or app.remote_debugger: kwargs = {"main_module": kwargs["env"]["BRIEFCASE_MAIN_MODULE"]} else: kwargs = {} From 6d86c6c5bcafc406b143dc9c49a432c767f3dfd2 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Mon, 24 Mar 2025 21:22:55 +0100 Subject: [PATCH 007/131] change "_briefcase_launcher" approach to an approach with an separate package "briefcase-remote-debugger". --- src/briefcase/commands/create.py | 64 +------------- src/briefcase/commands/run.py | 74 +++++++++++++++- src/briefcase/config.py | 22 +++-- src/briefcase/debuggers/base.py | 35 +++++--- src/briefcase/debuggers/debugpy.py | 93 ++------------------- src/briefcase/debuggers/pdb.py | 71 +--------------- src/briefcase/platforms/android/gradle.py | 30 ------- src/briefcase/platforms/iOS/xcode.py | 93 ++++++++++----------- src/briefcase/platforms/linux/flatpak.py | 2 +- src/briefcase/platforms/macOS/__init__.py | 26 ------ src/briefcase/platforms/windows/__init__.py | 33 ++------ 11 files changed, 173 insertions(+), 370 deletions(-) diff --git a/src/briefcase/commands/create.py b/src/briefcase/commands/create.py index 4562902a0..90237a8b1 100644 --- a/src/briefcase/commands/create.py +++ b/src/briefcase/commands/create.py @@ -81,46 +81,6 @@ def write_dist_info(app: AppConfig, dist_info_path: Path): f.write(f"{app.module_name}\n") -def write_launcher_module( - launcher_path: Path, - main_module: str, - start_remote_debugger_fct: str, -): - """Install the launcher folder for the application. - - :param app: The config object for the app - :param launcher_path: The path into which the launcher folder should be written. - :main_module: Name of the real main module to run. - :param start_remote_debugger_fct: A string to a code snippet, that at least - defines a "start_remote_debugger()" function. - """ - # Create launcher folder, and write a minimal launcher file - with launcher_path.open("w", encoding="utf-8") as f: - f.write( - f"""\ -# Generated {datetime.now()} - -# #################### REMOTE DEBUGGER CODE - START ########################### -{start_remote_debugger_fct} -# #################### REMOTE DEBUGGER CODE - END ############################# - -def main(): - print("Launcher started") - - print("Starting remote debugger...") - start_remote_debugger() - - # Run main module - print(f"Starting main module '{main_module}'...") - import runpy - runpy._run_module_as_main("{main_module}") - -if __name__ == "__main__": - main() -""" - ) - - class CreateCommand(BaseCommand): command = "create" description = "Create a new app for a target platform." @@ -769,9 +729,7 @@ def install_app_code(self, app: AppConfig, test_mode: bool): self.tools.shutil.rmtree(app_path) self.tools.os.mkdir(app_path) - sources = app.sources.copy() if app.sources else [] - if test_mode and app.test_sources: - sources.extend(app.test_sources) + sources = app.all_sources(test_mode) # Install app code. if sources: @@ -790,16 +748,6 @@ def install_app_code(self, app: AppConfig, test_mode: bool): else: self.console.info(f"No sources defined for {app.app_name}.") - if app.remote_debugger: - with self.console.wait_bar("Writing launcher files..."): - write_launcher_module( - launcher_path=self.app_path(app) / "_briefcase_launcher.py", - main_module=app.main_module(test_mode, include_launcher=False), - start_remote_debugger_fct=app.remote_debugger.generate_startup_code( - self.debugger_path_mappings(app, sources) - ), - ) - # Write the dist-info folder for the application. write_dist_info( app=app, @@ -807,16 +755,6 @@ def install_app_code(self, app: AppConfig, test_mode: bool): / f"{app.module_name}-{app.version}.dist-info", ) - def debugger_path_mappings(self, app: AppConfig, app_sources: list[str]) -> str: - """Path mappings for enhanced debugger support - - :param app: The config object for the app - :param app_sources: All source files of the app - :return: A code snippet, that adds all path mappings to the - 'path_mappings' variable. - """ - return "" - def install_image(self, role, variant, size, source, target): """Install an icon/image of the requested size at a target location, using the source images defined by the app config. diff --git a/src/briefcase/commands/run.py b/src/briefcase/commands/run.py index 715b126f0..5882158bd 100644 --- a/src/briefcase/commands/run.py +++ b/src/briefcase/commands/run.py @@ -1,11 +1,18 @@ from __future__ import annotations +import json import re import subprocess from abc import abstractmethod from contextlib import suppress +from pathlib import Path from briefcase.config import AppConfig +from briefcase.debuggers.base import ( + AppPackagesPathMappings, + AppPathMappings, + RemoteDebuggerConfig, +) from briefcase.exceptions import BriefcaseCommandError, BriefcaseTestSuiteFailure from briefcase.integrations.subprocess import StopStreaming @@ -220,6 +227,63 @@ def add_options(self, parser): self._add_update_options(parser, context_label=" before running") self._add_test_and_debug_options(parser, context_label="Run") + def remote_debugger_app_path_mappings( + self, app: AppConfig, test_mode: bool + ) -> AppPathMappings: + """ + Get the path mappings for the app code. + + :param app: The config object for the app + :param test_mode: Is the test mode enabled? + :returns: The path mappings for the app code + """ + device_subfolders = [] + host_folders = [] + for src in app.all_sources(test_mode): + original = Path(self.base_path / src) + device_subfolders.append(original.name) + host_folders.append(f"{original.absolute()}") + return AppPathMappings( + device_sys_path_regex="app$", + device_subfolders=device_subfolders, + host_folders=host_folders, + ) + + def remote_debugger_app_packages_path_mapping( + self, app: AppConfig + ) -> AppPackagesPathMappings: + """ + Get the path mappings for the app packages. + + :param app: The config object for the app + :returns: The path mappings for the app packages + """ + app_packages_path = self.app_packages_path(app) + return AppPackagesPathMappings( + sys_path_regex="app_packages$", + host_folder=f"{app_packages_path}", + ) + + def remote_debugger_config(self, app: AppConfig, test_mode: bool) -> str: + """ + Create the remote debugger configuration that should be saved as environment variable for this run. + + :param app: The app to be debugged + :param test_mode: Is the test mode enabled? + :returns: The remote debugger configuration + """ + app_path_mappings = self.remote_debugger_app_path_mappings(app, test_mode) + app_packages_path_mappings = self.remote_debugger_app_packages_path_mapping(app) + config = RemoteDebuggerConfig( + debugger=app.remote_debugger.name, + mode=app.remote_debugger.mode, + ip=app.remote_debugger.ip, + port=app.remote_debugger.port, + app_path_mappings=app_path_mappings, + app_packages_path_mappings=app_packages_path_mappings, + ) + return json.dumps(config) + def _prepare_app_kwargs(self, app: AppConfig, test_mode: bool): """Prepare the kwargs for running an app as a log stream. @@ -237,8 +301,14 @@ def _prepare_app_kwargs(self, app: AppConfig, test_mode: bool): if self.console.is_debug: env["BRIEFCASE_DEBUG"] = "1" - if test_mode or app.remote_debugger: - # In test or with remote debugger mode, set a BRIEFCASE_MAIN_MODULE environment variable + # If we're in remote debug mode, save the remote debugger config + if app.remote_debugger: + env["BRIEFCASE_REMOTE_DEBUGGER"] = self.remote_debugger_config( + app, test_mode + ) + + if test_mode: + # In test mode, set a BRIEFCASE_MAIN_MODULE environment variable # to override the module at startup env["BRIEFCASE_MAIN_MODULE"] = app.main_module(test_mode) self.console.info("Starting test_suite...", prefix=app.app_name) diff --git a/src/briefcase/config.py b/src/briefcase/config.py index 00d40a30e..2bb489c66 100644 --- a/src/briefcase/config.py +++ b/src/briefcase/config.py @@ -423,7 +423,18 @@ def PYTHONPATH(self, test_mode): paths.append(path) return paths - def main_module(self, test_mode: bool, include_launcher: bool = True): + def all_sources(self, test_mode: bool) -> list[str]: + """Get all sources of the application that should be copied to the app. + + :param test_mode: Is the test mode enabled? + :returns: The Path to the dist-info folder. + """ + sources = self.sources.copy() if self.sources else [] + if test_mode and self.test_sources: + sources.extend(self.test_sources) + return sources + + def main_module(self, test_mode: bool): """The path to the main module for the app. In normal operation, this is ``app.module_name``; however, @@ -432,14 +443,9 @@ def main_module(self, test_mode: bool, include_launcher: bool = True): :param test_mode: Are we running in test mode? """ if test_mode: - module_name = f"tests.{self.module_name}" - else: - module_name = self.module_name - - if self.remote_debugger and include_launcher: - return "_briefcase_launcher" + return f"tests.{self.module_name}" else: - return module_name + return self.module_name def merge_config(config, data): diff --git a/src/briefcase/debuggers/base.py b/src/briefcase/debuggers/base.py index 08fd9c18c..3367644ea 100644 --- a/src/briefcase/debuggers/base.py +++ b/src/briefcase/debuggers/base.py @@ -2,13 +2,33 @@ import dataclasses import enum -from abc import ABC, abstractmethod -from typing import ClassVar +from abc import ABC +from typing import ClassVar, TypedDict from briefcase.console import Console from briefcase.exceptions import BriefcaseCommandError, BriefcaseConfigError +class AppPathMappings(TypedDict): + device_sys_path_regex: str + device_subfolders: list[str] + host_folders: list[str] + + +class AppPackagesPathMappings(TypedDict): + sys_path_regex: str + host_folder: str + + +class RemoteDebuggerConfig(TypedDict): + debugger: str + mode: str # client / server + ip: str + port: int + app_path_mappings: AppPathMappings | None + app_packages_path_mappings: AppPackagesPathMappings | None + + class DebuggerMode(str, enum.Enum): SERVER = "server" CLIENT = "client" @@ -76,6 +96,7 @@ def parse_remote_debugger_cfg( class BaseDebugger(ABC): """Definition for a plugin that defines a new Briefcase debugger.""" + name: str supported_modes: ClassVar[list[DebuggerMode]] default_mode: ClassVar[DebuggerMode] default_ip: ClassVar[str] = "localhost" @@ -96,13 +117,3 @@ def __init__(self, console: Console, config: DebuggerConfig) -> None: def additional_requirements(self) -> list[str]: """Return a list of additional requirements for the debugger.""" return [] - - @abstractmethod - def generate_startup_code(self, path_mappings: str) -> str: - """ - Generate the code that is necessary to start the debugger. - - :param file: The file to write the startup code to. - :param path_mappings: The path mappings that should be used in the startup file. - """ - raise NotImplementedError() diff --git a/src/briefcase/debuggers/debugpy.py b/src/briefcase/debuggers/debugpy.py index 8d98b8010..2e0564314 100644 --- a/src/briefcase/debuggers/debugpy.py +++ b/src/briefcase/debuggers/debugpy.py @@ -1,100 +1,17 @@ -import textwrap - from briefcase.debuggers.base import BaseDebugger, DebuggerMode class DebugpyDebugger(BaseDebugger): """Definition for a plugin that defines a new Briefcase debugger.""" + name = "debugpy" supported_modes = [DebuggerMode.SERVER, DebuggerMode.CLIENT] default_mode = DebuggerMode.SERVER @property def additional_requirements(self) -> list[str]: """Return a list of additional requirements for the debugger.""" - return ["debugpy~=1.8.12"] - - def generate_startup_code(self, path_mappings: str) -> str: - """ - Generate the code that is necessary to start the debugger. - - :param path_mappings: The path mappings that should be used in the startup file. - """ - code = f"""\ -import os -import sys -from pathlib import Path - -def start_remote_debugger(): - ip = "{self.ip}" - port = {self.port} - path_mappings = [] - {textwrap.indent(path_mappings, " ")} - - # When an app is bundled with briefcase "os.__file__" is not set at runtime - # on some platforms (eg. windows). But debugpy accesses it internally, so it - # has to be set or an Exception is raised from debugpy. - if not hasattr(os, "__file__"): - os.__file__ = "" - -""" - if self.mode == DebuggerMode.CLIENT: - code += """\ - print(f''' -Connecting to debugpy server at {ip}:{port}... -To create the debugpy server using VSCode add the following configuration to launch.json and start the debugger: -{{ - "version": "0.2.0", - "configurations": [ - {{ - "name": "Briefcase: Attach (Listen)", - "type": "debugpy", - "request": "attach", - "listen": {{ - "host": "{ip}", - "port": {port} - }} - }} - ] -}} -''') - import debugpy - try: - debugpy.connect((ip, port)) - except ConnectionRefusedError as e: - print("Could not connect to debugpy server. Is it already started? We continue with the app...") - return -""" - elif self.mode == DebuggerMode.SERVER: - code += """\ - print(f''' -The debugpy server started at {ip}:{port}, waiting for connection... -To connect to debugpy using VSCode add the following configuration to launch.json: -{{ - "version": "0.2.0", - "configurations": [ - {{ - "name": "Briefcase: Attach (Connect)", - "type": "debugpy", - "request": "attach", - "connect": {{ - "host": "{ip}", - "port": {port} - }} - }} - ] -}} -''') - import debugpy - debugpy.listen((ip, port), in_process_debug_adapter=True) -""" - - code += """\ - if (len(path_mappings) > 0): - # path_mappings has to be applied after connection is established. If no connection is - # established this import will fail. - import pydevd_file_utils - - pydevd_file_utils.setup_client_server_paths(path_mappings) -""" - return code + return [ + "git+https://github.com/timrid/briefcase-remote-debugger@main", + "debugpy~=1.8.12", + ] diff --git a/src/briefcase/debuggers/pdb.py b/src/briefcase/debuggers/pdb.py index b763adda4..f234eb162 100644 --- a/src/briefcase/debuggers/pdb.py +++ b/src/briefcase/debuggers/pdb.py @@ -4,74 +4,11 @@ class PdbDebugger(BaseDebugger): """Definition for a plugin that defines a new Briefcase debugger.""" + name = "pdb" supported_modes = [DebuggerMode.SERVER] default_mode = DebuggerMode.SERVER - def generate_startup_code(self, path_mappings: str) -> str: - """ - Generate the code that is necessary to start the debugger. - - :param path_mappings: The path mappings that should be used in the startup file. - """ - code = f"""\ -import socket -import sys -import re - -NEWLINE_REGEX = re.compile("\\r?\\n") - -class SocketFileWrapper(object): - def __init__(self, connection: socket.socket): - self.connection = connection - self.stream = connection.makefile('rw') - - self.read = self.stream.read - self.readline = self.stream.readline - self.readlines = self.stream.readlines - self.close = self.stream.close - self.isatty = self.stream.isatty - self.flush = self.stream.flush - self.fileno = lambda: -1 - self.__iter__ = self.stream.__iter__ - @property - def encoding(self): - return self.stream.encoding - - def write(self, data): - data = NEWLINE_REGEX.sub("\\r\\n", data) - self.connection.sendall(data.encode(self.stream.encoding)) - - def writelines(self, lines): - for line in lines: - self.write(line) - -def start_remote_debugger(): - '''Open a socket server and stream all stdio via the connection bidirectional.''' - ip = "{self.ip}" - port = {self.port} - print(f''' -Stdio redirector server opened at {{ip}}:{{port}}, waiting for connection... -To connect to stdio redirector use eg.: - - telnet {{ip}} {{port}} - - nc -C {{ip}} {{port}} - - socat readline tcp:{{ip}}:{{port}} -''') - - listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True) - listen_socket.bind((ip, port)) - listen_socket.listen(1) - connection, address = listen_socket.accept() - print(f"Stdio redirector accepted connection from {{repr(address)}}.") - - file_wrapper = SocketFileWrapper(connection) - - sys.stderr = file_wrapper - sys.stdout = file_wrapper - sys.stdin = file_wrapper - sys.__stderr__ = file_wrapper - sys.__stdout__ = file_wrapper - sys.__stdin__ = file_wrapper -""" - return code + def additional_requirements(self) -> list[str]: + """Return a list of additional requirements for the debugger.""" + return ["git+https://github.com/timrid/briefcase-remote-debugger@main"] diff --git a/src/briefcase/platforms/android/gradle.py b/src/briefcase/platforms/android/gradle.py index 19d2575e2..e9917a956 100644 --- a/src/briefcase/platforms/android/gradle.py +++ b/src/briefcase/platforms/android/gradle.py @@ -283,36 +283,6 @@ def permissions_context(self, app: AppConfig, x_permissions: dict[str, str]): "features": features, } - def debugger_path_mappings(self, app: AppConfig, app_sources: list[str]): - """Path mappings for enhanced debugger support - - :param app: The config object for the app - :param app_sources: All source files of the app - :return: A list of code snippets that add a path mapping to the - 'path_mappings' variable. - """ - path_mappings = """ -device_app_folder = list(filter(lambda p: True if "AssetFinder/app" in p else False, sys.path)) -if len(device_app_folder) > 0: - pass -""" - for src in app_sources: - original = self.base_path / src - path_mappings += f""" - path_mappings.append((r"{original.absolute()}", str(Path(device_app_folder[0]) / "{original.name}"))) -""" - - host_requirements_folder = ( - self.bundle_path(app) / "app/build/python/pip/debug/common" - ) - path_mappings += f""" -device_requirements_folder = list(filter(lambda p: True if "AssetFinder/requirements" in p else False, sys.path)) -if len(device_requirements_folder) > 0: - path_mappings.append((r"{host_requirements_folder.absolute()}", device_requirements_folder[0])) -""" - - return path_mappings - class GradleUpdateCommand(GradleCreateCommand, UpdateCommand): description = "Update an existing Android Gradle project." diff --git a/src/briefcase/platforms/iOS/xcode.py b/src/briefcase/platforms/iOS/xcode.py index 71809ed98..5ca82b074 100644 --- a/src/briefcase/platforms/iOS/xcode.py +++ b/src/briefcase/platforms/iOS/xcode.py @@ -18,6 +18,7 @@ UpdateCommand, ) from briefcase.config import AppConfig +from briefcase.debuggers.base import AppPackagesPathMappings from briefcase.exceptions import ( BriefcaseCommandError, InputDisabled, @@ -52,6 +53,28 @@ def binary_path(self, app): / f"{app.formal_name}.app" ) + def device_and_simulator_platform_site(self, app: AppConfig) -> Path: + # Feb 2025: The platform-site was moved into the xcframework as + # `platform-config`. Look for the new location; fall back to the old location. + device_platform_site = ( + self.support_path(app) + / "Python.xcframework/ios-arm64/platform-config/arm64-iphoneos" + ) + simulator_platform_site = ( + self.support_path(app) + / "Python.xcframework/ios-arm64_x86_64-simulator" + / f"platform-config/{self.tools.host_arch}-iphonesimulator" + ) + if not device_platform_site.exists(): + device_platform_site = ( + self.support_path(app) / "platform-site/iphoneos.arm64" + ) + simulator_platform_site = ( + self.support_path(app) + / f"platform-site/iphonesimulator.{self.tools.host_arch}" + ) + return device_platform_site, simulator_platform_site + def distribution_path(self, app): # This path won't ever be *generated*, as distribution artefacts # can't be generated on iOS. @@ -341,25 +364,9 @@ def _install_app_requirements( ios_min_tag = str(ios_min_version).replace(".", "_") - # Feb 2025: The platform-site was moved into the xcframework as - # `platform-config`. Look for the new location; fall back to the old location. - device_platform_site = ( - self.support_path(app) - / "Python.xcframework/ios-arm64/platform-config/arm64-iphoneos" - ) - simulator_platform_site = ( - self.support_path(app) - / "Python.xcframework/ios-arm64_x86_64-simulator" - / f"platform-config/{self.tools.host_arch}-iphonesimulator" + device_platform_site, simulator_platform_site = ( + self.device_and_simulator_platform_site(app) ) - if not device_platform_site.exists(): - device_platform_site = ( - self.support_path(app) / "platform-site/iphoneos.arm64" - ) - simulator_platform_site = ( - self.support_path(app) - / f"platform-site/iphonesimulator.{self.tools.host_arch}" - ) # Perform the initial install pass targeting the "iphoneos" platform super()._install_app_requirements( @@ -394,37 +401,6 @@ def _install_app_requirements( }, ) - def debugger_path_mappings(self, app: AppConfig, app_sources: list[str]): - """Path mappings for enhanced debugger support - - :param app: The config object for the app - :param app_sources: All source files of the app - :return: A list of code snippets that add a path mapping to the - 'path_mappings' variable. - """ - path_mappings = """ -device_app_folder = list(filter(lambda p: True if p.endswith("app") else False, sys.path)) -if len(device_app_folder) > 0: - pass -""" - for src in app_sources: - original = self.base_path / src - path_mappings += f""" - path_mappings.append((r"{original.absolute()}", str(Path(device_app_folder[0]) / "{original.name}"))) -""" - - host_requirements_folder = self.app_packages_path(app) - path_mappings += f""" -import platform -host_requirements_folder = "{host_requirements_folder.absolute()}" -host_requirements_folder += ".iphonesimulator" if platform.ios_ver().is_simulator else ".iphoneos" -device_requirements_folder = list(filter(lambda p: True if p.endswith("app_packages") else False, sys.path)) -if len(device_requirements_folder) > 0: - path_mappings.append((host_requirements_folder, device_requirements_folder[0])) -""" - - return path_mappings - class iOSXcodeUpdateCommand(iOSXcodeCreateCommand, UpdateCommand): description = "Update an existing iOS Xcode project." @@ -506,6 +482,25 @@ def __init__(self, *args, **kwargs): # This is abstracted to enable testing without patching. self.get_device_state = get_device_state + def remote_debugger_app_packages_path_mapping( + self, app: AppConfig + ) -> AppPackagesPathMappings: + """ + Get the path mappings for the app packages. + + :param app: The config object for the app + :returns: The path mappings for the app packages + """ + # TODO: Add handling to switch between simulator and real device. Currently we only + # support simulator. + device_platform_site, simulator_platform_site = ( + self.device_and_simulator_platform_site(app) + ) + return AppPackagesPathMappings( + sys_path_regex="app_packages$", + host_folder=simulator_platform_site, + ) + def run_app( self, app: AppConfig, diff --git a/src/briefcase/platforms/linux/flatpak.py b/src/briefcase/platforms/linux/flatpak.py index 2d7cb04c4..05675057f 100644 --- a/src/briefcase/platforms/linux/flatpak.py +++ b/src/briefcase/platforms/linux/flatpak.py @@ -217,7 +217,7 @@ def run_app( # Starting a flatpak has slightly different startup arguments; however, # the rest of the app startup process is the same. Transform the output # of the "default" behavior to be in flatpak format. - if test_mode or app.remote_debugger: + if test_mode: kwargs = {"main_module": kwargs["env"]["BRIEFCASE_MAIN_MODULE"]} else: kwargs = {} diff --git a/src/briefcase/platforms/macOS/__init__.py b/src/briefcase/platforms/macOS/__init__.py index 452bbb3a3..fc37e4260 100644 --- a/src/briefcase/platforms/macOS/__init__.py +++ b/src/briefcase/platforms/macOS/__init__.py @@ -289,32 +289,6 @@ def permissions_context(self, app: AppConfig, cross_platform: dict[str, str]): "entitlements": entitlements, } - def debugger_path_mappings(self, app: AppConfig, app_sources: list[str]): - """Path mappings for enhanced debugger support - - :param app: The config object for the app - :param app_sources: All source files of the app - :return: A list of code snippets that add a path mapping to the - 'path_mappings' variable. - """ - # Normally app & requirements are automatically found, because - # developing an macOS app also requires a macOs host. But the app - # path is pointing to a copy of the source in some temporary folder, - # so we redirect it to the original source. - - path_mappings = """ -device_app_folder = list(filter(lambda p: True if p.endswith("app") else False, sys.path)) -if len(device_app_folder) > 0: - pass -""" - for src in app_sources: - original = self.base_path / src - path_mappings += f""" - path_mappings.append((r"{original.absolute()}", str(Path(device_app_folder[0]) / "{original.name}"))) -""" - - return path_mappings - class macOSRunMixin: def run_app( diff --git a/src/briefcase/platforms/windows/__init__.py b/src/briefcase/platforms/windows/__init__.py index ba110dea8..416ba6219 100644 --- a/src/briefcase/platforms/windows/__init__.py +++ b/src/briefcase/platforms/windows/__init__.py @@ -124,34 +124,19 @@ def _cleanup_app_support_package(self, support_path): """ ) - def debugger_path_mappings(self, app: AppConfig, app_sources: list[str]): - """Path mappings for enhanced debugger support - :param app: The config object for the app - :param app_sources: All source files of the app - :return: A list of code snippets that add a path mapping to the - 'path_mappings' variable. +class WindowsRunCommand(RunCommand): + def remote_debugger_app_packages_path_mapping(self, app: AppConfig) -> None: """ - # Normally app & requirements are automatically found, because - # developing an windows app also requires a windows host. But the app - # path is pointing to a copy of the source in some temporary folder, - # so we redirect it to the original source. - - path_mappings = """ -device_app_folder = list(filter(lambda p: True if p.endswith("app") else False, sys.path)) -if len(device_app_folder) > 0: - pass -""" - for src in app_sources: - original = self.base_path / src - path_mappings += f""" - path_mappings.append((r"{original.absolute()}", str(Path(device_app_folder[0]) / "{original.name}"))) -""" - - return path_mappings + Get the path mappings for the app packages. + :param app: The config object for the app + :returns: The path mappings for the app packages + """ + # No path mapping is required. The paths are automatically found, because + # developing an windows app also requires a windows host. + return None -class WindowsRunCommand(RunCommand): def run_app( self, app: AppConfig, From dd28eff83aa65e0bd6118b93ac760737ac63bcdd Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Thu, 27 Mar 2025 22:22:17 +0100 Subject: [PATCH 008/131] add enviroment variable on android --- src/briefcase/integrations/android_sdk.py | 7 ++++++- src/briefcase/platforms/android/gradle.py | 25 +++++++++++++++++++++-- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/briefcase/integrations/android_sdk.py b/src/briefcase/integrations/android_sdk.py index ebf45e561..259f7631d 100644 --- a/src/briefcase/integrations/android_sdk.py +++ b/src/briefcase/integrations/android_sdk.py @@ -1514,7 +1514,9 @@ def force_stop_app(self, package: str): f"Unable to force stop app {package} on {self.device}" ) from e - def start_app(self, package: str, activity: str, passthrough: list[str]): + def start_app( + self, package: str, activity: str, passthrough: list[str], env: dict[str, str] + ): """Start an app, specified as a package name & activity name. If you have an APK file, and you are not sure of the package or activity @@ -1543,6 +1545,9 @@ def start_app(self, package: str, activity: str, passthrough: list[str]): "--es", "org.beeware.ARGV", shlex.quote(json.dumps(passthrough)), # Protect from Android's shell + "--es", + "org.beeware.ENV", + shlex.quote(json.dumps(env)), # Protect from Android's shell ) # `adb shell am start` always exits with status zero. We look for error diff --git a/src/briefcase/platforms/android/gradle.py b/src/briefcase/platforms/android/gradle.py index e9917a956..61f0e7ec1 100644 --- a/src/briefcase/platforms/android/gradle.py +++ b/src/briefcase/platforms/android/gradle.py @@ -17,7 +17,7 @@ ) from briefcase.config import AppConfig, parsed_version from briefcase.console import ANSI_ESC_SEQ_RE_DEF -from briefcase.debuggers.base import BaseDebugger, DebuggerMode +from briefcase.debuggers.base import AppPackagesPathMappings, BaseDebugger, DebuggerMode from briefcase.exceptions import BriefcaseCommandError from briefcase.integrations.android_sdk import ADB, AndroidSDK from briefcase.integrations.subprocess import SubprocessArgT @@ -361,6 +361,21 @@ def add_options(self, parser): required=False, ) + def remote_debugger_app_packages_path_mapping( + self, app: AppConfig + ) -> AppPackagesPathMappings: + """ + Get the path mappings for the app packages. + + :param app: The config object for the app + :returns: The path mappings for the app packages + """ + app_packages_path = self.bundle_path(app) / "app/build/python/pip/debug/common" + return AppPackagesPathMappings( + sys_path_regex="app_packages$", + host_folder=f"{app_packages_path}", + ) + def run_app( self, app: AppConfig, @@ -428,16 +443,22 @@ def run_app( with self.console.wait_bar("Installing new app version..."): adb.install_apk(self.binary_path(app)) + env = {} if app.remote_debugger: with self.console.wait_bar("Establishing debugger connection..."): self.establish_debugger_connection(adb, app.remote_debugger) + env["BRIEFCASE_REMOTE_DEBUGGER"] = self.remote_debugger_config( + app, test_mode + ) # To start the app, we launch `org.beeware.android.MainActivity`. with self.console.wait_bar(f"Launching {label}..."): # capture the earliest time for device logging in case PID not found device_start_time = adb.datetime() - adb.start_app(package, "org.beeware.android.MainActivity", passthrough) + adb.start_app( + package, "org.beeware.android.MainActivity", passthrough, env + ) # Try to get the PID for 5 seconds. pid = None From 1946e31f5a0074098d64f1c140f9a93b20387505 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Fri, 28 Mar 2025 22:17:51 +0100 Subject: [PATCH 009/131] set environment variables on ios --- src/briefcase/platforms/iOS/xcode.py | 33 +++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/src/briefcase/platforms/iOS/xcode.py b/src/briefcase/platforms/iOS/xcode.py index 5ca82b074..4875420bd 100644 --- a/src/briefcase/platforms/iOS/xcode.py +++ b/src/briefcase/platforms/iOS/xcode.py @@ -53,7 +53,7 @@ def binary_path(self, app): / f"{app.formal_name}.app" ) - def device_and_simulator_platform_site(self, app: AppConfig) -> Path: + def device_and_simulator_platform_site(self, app: AppConfig) -> tuple[Path, Path]: # Feb 2025: The platform-site was moved into the xcframework as # `platform-config`. Look for the new location; fall back to the old location. device_platform_site = ( @@ -493,12 +493,11 @@ def remote_debugger_app_packages_path_mapping( """ # TODO: Add handling to switch between simulator and real device. Currently we only # support simulator. - device_platform_site, simulator_platform_site = ( - self.device_and_simulator_platform_site(app) - ) return AppPackagesPathMappings( sys_path_regex="app_packages$", - host_folder=simulator_platform_site, + host_folder=str( + self.app_packages_path(app).parent / "app_packages.iphonesimulator" + ), ) def run_app( @@ -653,6 +652,30 @@ def run_app( # Wait for the log stream start up time.sleep(0.25) + # Add additional environment variables + env = {} + if app.remote_debugger: + env["BRIEFCASE_REMOTE_DEBUGGER"] = self.remote_debugger_config( + app, test_mode + ) + + # Install additional environment variables + if env: + with self.console.wait_bar("Setting environment variables..."): + for env_key, env_value in env.items(): + output = self.tools.subprocess.check_output( + [ + "xcrun", + "simctl", + "spawn", + udid, + "launchctl", + "setenv", + f"{env_key}", + f"{env_value}", + ] + ) + try: self.console.info(f"Starting {label}...", prefix=app.app_name) with self.console.wait_bar(f"Launching {label}..."): From bb238d62e933af88c3c2f729983115e4fdd982f9 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sat, 29 Mar 2025 21:46:35 +0100 Subject: [PATCH 010/131] fix path mapping for android --- src/briefcase/platforms/android/gradle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/briefcase/platforms/android/gradle.py b/src/briefcase/platforms/android/gradle.py index 61f0e7ec1..c2f2d6718 100644 --- a/src/briefcase/platforms/android/gradle.py +++ b/src/briefcase/platforms/android/gradle.py @@ -372,7 +372,7 @@ def remote_debugger_app_packages_path_mapping( """ app_packages_path = self.bundle_path(app) / "app/build/python/pip/debug/common" return AppPackagesPathMappings( - sys_path_regex="app_packages$", + sys_path_regex="requirements$", host_folder=f"{app_packages_path}", ) From aa1df93c63f4db634443c0dc75e1419ef8f28788 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sat, 29 Mar 2025 21:47:19 +0100 Subject: [PATCH 011/131] add remote debug support via environment variable for flatpak --- src/briefcase/integrations/flatpak.py | 18 +----------------- src/briefcase/platforms/linux/flatpak.py | 24 ++++++++++++++++-------- 2 files changed, 17 insertions(+), 25 deletions(-) diff --git a/src/briefcase/integrations/flatpak.py b/src/briefcase/integrations/flatpak.py index ad8f340f9..0e107109e 100644 --- a/src/briefcase/integrations/flatpak.py +++ b/src/briefcase/integrations/flatpak.py @@ -254,36 +254,20 @@ def run( self, bundle_identifier: str, args: list[SubprocessArgT] | None = None, - main_module: str | None = None, stream_output: bool = True, + **kwargs, ) -> subprocess.Popen[str]: """Run a Flatpak in a way that allows for log streaming. :param bundle_identifier: The bundle identifier for the app being built. :param args: (Optional) The list of arguments to pass to the app - :param main_module: (Optional) The main module to run. Only required if you want - to override the default main module for the app. :param stream_output: Should output be streamed? :returns: A Popen object for the running app; or ``None`` if the app isn't streaming """ - if main_module: - # Set a BRIEFCASE_MAIN_MODULE environment variable - # to override the module at startup - kwargs = { - "env": { - "BRIEFCASE_MAIN_MODULE": main_module, - } - } - else: - kwargs = {} - flatpak_run_cmd = ["flatpak", "run", bundle_identifier] flatpak_run_cmd.extend([] if args is None else args) - if self.tools.console.is_debug: - kwargs.setdefault("env", {})["BRIEFCASE_DEBUG"] = "1" - if self.tools.console.is_deep_debug: # Must come before bundle identifier; otherwise, it's passed as an arg to app flatpak_run_cmd.insert(2, "--verbose") diff --git a/src/briefcase/platforms/linux/flatpak.py b/src/briefcase/platforms/linux/flatpak.py index 05675057f..3f4e27495 100644 --- a/src/briefcase/platforms/linux/flatpak.py +++ b/src/briefcase/platforms/linux/flatpak.py @@ -10,6 +10,7 @@ UpdateCommand, ) from briefcase.config import AppConfig +from briefcase.debuggers.base import AppPackagesPathMappings from briefcase.exceptions import BriefcaseConfigError from briefcase.integrations.flatpak import Flatpak from briefcase.platforms.linux import LinuxMixin @@ -198,6 +199,21 @@ def build_app(self, app: AppConfig, **kwargs): class LinuxFlatpakRunCommand(LinuxFlatpakMixin, RunCommand): description = "Run a Linux Flatpak." + def remote_debugger_app_packages_path_mapping( + self, app: AppConfig + ) -> AppPackagesPathMappings: + """ + Get the path mappings for the app packages. + + :param app: The config object for the app + :returns: The path mappings for the app packages + """ + app_packages_path = self.bundle_path(app) / "build/files/briefcase/app_packages" + return AppPackagesPathMappings( + sys_path_regex="app_packages$", + host_folder=f"{app_packages_path}", + ) + def run_app( self, app: AppConfig, @@ -214,14 +230,6 @@ def run_app( # Set up the log stream kwargs = self._prepare_app_kwargs(app=app, test_mode=test_mode) - # Starting a flatpak has slightly different startup arguments; however, - # the rest of the app startup process is the same. Transform the output - # of the "default" behavior to be in flatpak format. - if test_mode: - kwargs = {"main_module": kwargs["env"]["BRIEFCASE_MAIN_MODULE"]} - else: - kwargs = {} - # Console apps must operate in non-streaming mode so that console input can # be handled correctly. However, if we're in test mode, we *must* stream so # that we can see the test exit sentinel From ea5f3f7174ec82b4c5779960fb585c86496b962e Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sun, 30 Mar 2025 13:35:55 +0200 Subject: [PATCH 012/131] revert unnesessary changes on xcode --- src/briefcase/platforms/iOS/xcode.py | 42 ++++++++++++---------------- 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/src/briefcase/platforms/iOS/xcode.py b/src/briefcase/platforms/iOS/xcode.py index 4875420bd..0c59a9e28 100644 --- a/src/briefcase/platforms/iOS/xcode.py +++ b/src/briefcase/platforms/iOS/xcode.py @@ -53,28 +53,6 @@ def binary_path(self, app): / f"{app.formal_name}.app" ) - def device_and_simulator_platform_site(self, app: AppConfig) -> tuple[Path, Path]: - # Feb 2025: The platform-site was moved into the xcframework as - # `platform-config`. Look for the new location; fall back to the old location. - device_platform_site = ( - self.support_path(app) - / "Python.xcframework/ios-arm64/platform-config/arm64-iphoneos" - ) - simulator_platform_site = ( - self.support_path(app) - / "Python.xcframework/ios-arm64_x86_64-simulator" - / f"platform-config/{self.tools.host_arch}-iphonesimulator" - ) - if not device_platform_site.exists(): - device_platform_site = ( - self.support_path(app) / "platform-site/iphoneos.arm64" - ) - simulator_platform_site = ( - self.support_path(app) - / f"platform-site/iphonesimulator.{self.tools.host_arch}" - ) - return device_platform_site, simulator_platform_site - def distribution_path(self, app): # This path won't ever be *generated*, as distribution artefacts # can't be generated on iOS. @@ -364,9 +342,25 @@ def _install_app_requirements( ios_min_tag = str(ios_min_version).replace(".", "_") - device_platform_site, simulator_platform_site = ( - self.device_and_simulator_platform_site(app) + # Feb 2025: The platform-site was moved into the xcframework as + # `platform-config`. Look for the new location; fall back to the old location. + device_platform_site = ( + self.support_path(app) + / "Python.xcframework/ios-arm64/platform-config/arm64-iphoneos" ) + simulator_platform_site = ( + self.support_path(app) + / "Python.xcframework/ios-arm64_x86_64-simulator" + / f"platform-config/{self.tools.host_arch}-iphonesimulator" + ) + if not device_platform_site.exists(): + device_platform_site = ( + self.support_path(app) / "platform-site/iphoneos.arm64" + ) + simulator_platform_site = ( + self.support_path(app) + / f"platform-site/iphonesimulator.{self.tools.host_arch}" + ) # Perform the initial install pass targeting the "iphoneos" platform super()._install_app_requirements( From 5d0232a536f0dc9202bea8fd5d5f2b0150399693 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sun, 30 Mar 2025 13:42:13 +0200 Subject: [PATCH 013/131] changed "org.beeware.ENV" to "org.beeware.ENVIRON" --- src/briefcase/integrations/android_sdk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/briefcase/integrations/android_sdk.py b/src/briefcase/integrations/android_sdk.py index 259f7631d..bff5cd7d5 100644 --- a/src/briefcase/integrations/android_sdk.py +++ b/src/briefcase/integrations/android_sdk.py @@ -1546,7 +1546,7 @@ def start_app( "org.beeware.ARGV", shlex.quote(json.dumps(passthrough)), # Protect from Android's shell "--es", - "org.beeware.ENV", + "org.beeware.ENVIRON", shlex.quote(json.dumps(env)), # Protect from Android's shell ) From dc3eb351dbd374c5362e081c88b294cf5fed8709 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sun, 30 Mar 2025 16:38:30 +0200 Subject: [PATCH 014/131] add BRIEFCASE_DEBUG to android --- src/briefcase/platforms/android/gradle.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/briefcase/platforms/android/gradle.py b/src/briefcase/platforms/android/gradle.py index c2f2d6718..bdfcaede0 100644 --- a/src/briefcase/platforms/android/gradle.py +++ b/src/briefcase/platforms/android/gradle.py @@ -451,6 +451,9 @@ def run_app( app, test_mode ) + if self.console.is_debug: + env["BRIEFCASE_DEBUG"] = "1" + # To start the app, we launch `org.beeware.android.MainActivity`. with self.console.wait_bar(f"Launching {label}..."): # capture the earliest time for device logging in case PID not found From 97b9a9b86a0e60b4a10b4f5f40f9d73b29073fb4 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sun, 30 Mar 2025 21:33:39 +0200 Subject: [PATCH 015/131] Add sources to the extract_packages so that the debugger can get the source code at runtime (eg. via 'll' in pdb). --- src/briefcase/platforms/android/gradle.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/briefcase/platforms/android/gradle.py b/src/briefcase/platforms/android/gradle.py index bdfcaede0..b3d02cac1 100644 --- a/src/briefcase/platforms/android/gradle.py +++ b/src/briefcase/platforms/android/gradle.py @@ -215,15 +215,19 @@ def output_format_template_context(self, app: AppConfig): "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0", ] + extract_sources = app.test_sources or [] + if app.remote_debugger: + # Add sources to the extract_packages so that the debugger can + # get the source code at runtime (eg. via 'll' in pdb). + extract_sources.extend(app.sources) + return { "version_code": version_code, "safe_formal_name": safe_formal_name(app.formal_name), # Extract test packages, to enable features like test discovery and assertion # rewriting. "extract_packages": ", ".join( - f'"{name}"' - for path in (app.test_sources or []) - if (name := Path(path).name) + [f'"{name}"' for path in extract_sources if (name := Path(path).name)] ), "build_gradle_dependencies": {"implementation": dependencies}, } From e6b516729cec0d2df6fdfcd68bcb5db6e947d9da Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sun, 30 Mar 2025 22:11:21 +0200 Subject: [PATCH 016/131] remove port forwarding on android when "briefcase run" stops --- src/briefcase/integrations/android_sdk.py | 50 +++++++++++++++-------- src/briefcase/platforms/android/gradle.py | 14 +++++++ 2 files changed, 46 insertions(+), 18 deletions(-) diff --git a/src/briefcase/integrations/android_sdk.py b/src/briefcase/integrations/android_sdk.py index bff5cd7d5..a393a04cb 100644 --- a/src/briefcase/integrations/android_sdk.py +++ b/src/briefcase/integrations/android_sdk.py @@ -1642,35 +1642,42 @@ def forward(self, host_port: int, device_port: int): """ try: # TODO: This prints the port to the terminal. How to remove the output? - - # If the port we are forwarding to the device is also reversed to the host, - # it has happened that adb hangs. So we remove the reversed port first. self.tools.subprocess.run( [ self.tools.android_sdk.adb_path, "-s", self.device, - "reverse", - "--remove", + "forward", f"tcp:{host_port}", + f"tcp:{device_port}", ], env=self.tools.android_sdk.env, - check=False, # if the port is not in use an error is returned, but we dont care + check=True, ) + except subprocess.CalledProcessError as e: + raise BriefcaseCommandError("Error starting 'adb forward'.") from e + + def forward_remove(self, host_port: int): + """Remove forwarded port. + + :param host_port: The port on the host that should be forwarded to the device + """ + try: + # TODO: This prints the port to the terminal. How to remove the output? self.tools.subprocess.run( [ self.tools.android_sdk.adb_path, "-s", self.device, "forward", + "--remove", f"tcp:{host_port}", - f"tcp:{device_port}", ], env=self.tools.android_sdk.env, - check=True, + check=False, # if the port is not in use an error is returned, but we dont care ) except subprocess.CalledProcessError as e: - raise BriefcaseCommandError("Error starting ADB forward.") from e + raise BriefcaseCommandError("Error starting 'adb forward --remove'.") from e def reverse(self, device_port: int, host_port: int): """Use the reverse command to set up arbitrary port forwarding, which @@ -1681,35 +1688,42 @@ def reverse(self, device_port: int, host_port: int): """ try: # TODO: This prints the port to the terminal. How to remove the output? - - # If the port we are reversing to the host is also forwarded to the device, - # it has happened that adb hangs. So we remove the forwarded port first. self.tools.subprocess.run( [ self.tools.android_sdk.adb_path, "-s", self.device, - "forward", - "--remove", + "reverse", + f"tcp:{device_port}", f"tcp:{host_port}", ], env=self.tools.android_sdk.env, - check=False, # if the port is not in use an error is returned, but we dont care + check=True, ) + except subprocess.CalledProcessError as e: + raise BriefcaseCommandError("Error starting 'adb reverse'.") from e + + def reverse_remove(self, device_port: int): + """Remove reversed port. + + :param device_port: The port on the device that should be forwarded to the host + """ + try: + # TODO: This prints the port to the terminal. How to remove the output? self.tools.subprocess.run( [ self.tools.android_sdk.adb_path, "-s", self.device, "reverse", + "--remove", f"tcp:{device_port}", - f"tcp:{host_port}", ], env=self.tools.android_sdk.env, - check=True, + check=False, # if the port is not in use an error is returned, but we dont care ) except subprocess.CalledProcessError as e: - raise BriefcaseCommandError("Error starting ADB reverse.") from e + raise BriefcaseCommandError("Error starting 'adb reverse --remove'.") from e def pidof(self, package: str, **kwargs) -> str | None: """Obtain the PID of a running app by package name. diff --git a/src/briefcase/platforms/android/gradle.py b/src/briefcase/platforms/android/gradle.py index b3d02cac1..3f2e6476c 100644 --- a/src/briefcase/platforms/android/gradle.py +++ b/src/briefcase/platforms/android/gradle.py @@ -506,6 +506,9 @@ def run_app( raise BriefcaseCommandError(f"Problem starting app {app.app_name!r}") finally: + if app.remote_debugger: + with self.console.wait_bar("Stopping debugger connection..."): + self.remove_debugger_connection(adb, app.remote_debugger) if shutdown_on_exit: with self.tools.console.wait_bar("Stopping emulator..."): adb.kill() @@ -521,6 +524,17 @@ def establish_debugger_connection(self, adb: ADB, debugger: BaseDebugger): elif debugger.port == DebuggerMode.CLIENT: adb.reverse(debugger.port, debugger.port) + def remove_debugger_connection(self, adb: ADB, debugger: BaseDebugger): + """Remove Forward/Reverse of the ports necessary for remote debugging. + + :param app: The config object for the app + :param adb: Access to the adb + """ + if debugger.mode == DebuggerMode.SERVER: + adb.forward_remove(debugger.port) + elif debugger.port == DebuggerMode.CLIENT: + adb.reverse_remove(debugger.port) + class GradlePackageCommand(GradleMixin, PackageCommand): description = "Create an Android App Bundle and APK in release mode." From 14fec610c7163da34331471387d6fad3cd4a7fc9 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Mon, 31 Mar 2025 20:28:49 +0200 Subject: [PATCH 017/131] removed unnessesary output to the console --- src/briefcase/integrations/android_sdk.py | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/briefcase/integrations/android_sdk.py b/src/briefcase/integrations/android_sdk.py index a393a04cb..7c0457877 100644 --- a/src/briefcase/integrations/android_sdk.py +++ b/src/briefcase/integrations/android_sdk.py @@ -1641,8 +1641,7 @@ def forward(self, host_port: int, device_port: int): :param device_port: The port on the device """ try: - # TODO: This prints the port to the terminal. How to remove the output? - self.tools.subprocess.run( + self.tools.subprocess.check_output( [ self.tools.android_sdk.adb_path, "-s", @@ -1652,7 +1651,6 @@ def forward(self, host_port: int, device_port: int): f"tcp:{device_port}", ], env=self.tools.android_sdk.env, - check=True, ) except subprocess.CalledProcessError as e: raise BriefcaseCommandError("Error starting 'adb forward'.") from e @@ -1660,11 +1658,10 @@ def forward(self, host_port: int, device_port: int): def forward_remove(self, host_port: int): """Remove forwarded port. - :param host_port: The port on the host that should be forwarded to the device + :param host_port: The port on the host that should be removed """ try: - # TODO: This prints the port to the terminal. How to remove the output? - self.tools.subprocess.run( + self.tools.subprocess.check_output( [ self.tools.android_sdk.adb_path, "-s", @@ -1674,7 +1671,6 @@ def forward_remove(self, host_port: int): f"tcp:{host_port}", ], env=self.tools.android_sdk.env, - check=False, # if the port is not in use an error is returned, but we dont care ) except subprocess.CalledProcessError as e: raise BriefcaseCommandError("Error starting 'adb forward --remove'.") from e @@ -1687,8 +1683,7 @@ def reverse(self, device_port: int, host_port: int): :param host_port: The port on the host """ try: - # TODO: This prints the port to the terminal. How to remove the output? - self.tools.subprocess.run( + self.tools.subprocess.check_output( [ self.tools.android_sdk.adb_path, "-s", @@ -1698,7 +1693,6 @@ def reverse(self, device_port: int, host_port: int): f"tcp:{host_port}", ], env=self.tools.android_sdk.env, - check=True, ) except subprocess.CalledProcessError as e: raise BriefcaseCommandError("Error starting 'adb reverse'.") from e @@ -1706,11 +1700,10 @@ def reverse(self, device_port: int, host_port: int): def reverse_remove(self, device_port: int): """Remove reversed port. - :param device_port: The port on the device that should be forwarded to the host + :param device_port: The port on the device that should be removed """ try: - # TODO: This prints the port to the terminal. How to remove the output? - self.tools.subprocess.run( + self.tools.subprocess.check_output( [ self.tools.android_sdk.adb_path, "-s", @@ -1720,7 +1713,6 @@ def reverse_remove(self, device_port: int): f"tcp:{device_port}", ], env=self.tools.android_sdk.env, - check=False, # if the port is not in use an error is returned, but we dont care ) except subprocess.CalledProcessError as e: raise BriefcaseCommandError("Error starting 'adb reverse --remove'.") from e From cf137bf0317d75053b7d2c287b2a1aff543b390e Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sat, 17 May 2025 16:19:10 +0200 Subject: [PATCH 018/131] - Added option "--debug" for build, run and update command. - Added "--debugger-host" and "--debugger-port" for run command. - Removed multiple modes (client/server) for one debugger. It was to complicated. - Added "debug_reqires" to pyproject.toml for debug requirements - changed to "https://github.com/timrid/briefcase-debugadapter" --- src/briefcase/commands/base.py | 60 +++++----- src/briefcase/commands/build.py | 26 ++++- src/briefcase/commands/create.py | 27 +++-- src/briefcase/commands/run.py | 72 +++++++++--- src/briefcase/commands/update.py | 14 ++- src/briefcase/config.py | 4 +- src/briefcase/debuggers/__init__.py | 12 +- src/briefcase/debuggers/base.py | 122 +++++++------------- src/briefcase/debuggers/debugpy.py | 10 +- src/briefcase/debuggers/pdb.py | 11 +- src/briefcase/platforms/android/gradle.py | 13 ++- src/briefcase/platforms/iOS/xcode.py | 9 +- src/briefcase/platforms/linux/appimage.py | 16 ++- src/briefcase/platforms/linux/flatpak.py | 13 ++- src/briefcase/platforms/linux/system.py | 15 ++- src/briefcase/platforms/macOS/__init__.py | 31 ++++- src/briefcase/platforms/web/static.py | 5 +- src/briefcase/platforms/windows/__init__.py | 13 ++- tests/commands/create/conftest.py | 2 +- 19 files changed, 300 insertions(+), 175 deletions(-) diff --git a/src/briefcase/commands/base.py b/src/briefcase/commands/base.py index 156a95e80..18ecbad60 100644 --- a/src/briefcase/commands/base.py +++ b/src/briefcase/commands/base.py @@ -20,8 +20,7 @@ from packaging.version import Version from platformdirs import PlatformDirs -from briefcase.debuggers import DEFAULT_DEBUGGER, get_debuggers -from briefcase.debuggers.base import parse_remote_debugger_cfg +from briefcase.debuggers import get_debugger, get_debuggers if sys.version_info >= (3, 11): # pragma: no-cover-if-lt-py311 import tomllib @@ -598,9 +597,7 @@ def finalize_app_config(self, app: AppConfig): :param app: The app configuration to finalize. """ - def finalize( - self, app: AppConfig | None = None, remote_debugger_cfg: str | None = None - ): + def finalize(self, app: AppConfig | None = None, debug_mode: str | None = None): """Finalize Briefcase configuration. This will: @@ -620,15 +617,27 @@ def finalize( if app is None: for app in self.apps.values(): if hasattr(app, "__draft__"): - self.create_remote_debugger(app, remote_debugger_cfg) + self.finalze_debug_mode(app, debug_mode) self.finalize_app_config(app) delattr(app, "__draft__") else: if hasattr(app, "__draft__"): - self.create_remote_debugger(app, remote_debugger_cfg) + self.finalze_debug_mode(app, debug_mode) self.finalize_app_config(app) delattr(app, "__draft__") + def finalze_debug_mode(self, app: AppConfig, debug_mode: str | None = None): + """Finalize the debugger configuration. + + This will ensure that the debugger is available and that the app + configuration is valid. + + :param app: The app configuration to finalize. + """ + if debug_mode and debug_mode != "": + debugger = get_debugger(debug_mode) + app.debug_requires.extend(debugger.additional_requirements) + def verify_app(self, app: AppConfig): """Verify the app is compatible and the app tools are available. @@ -697,19 +706,6 @@ def verify_required_python(self, app: AppConfig): version_specifier=requires_python, running_version=running_version ) - def create_remote_debugger(self, app: AppConfig, remote_debugger_cfg: str | None): - """Select and instantiate a debugger for the project.""" - if remote_debugger_cfg is None: - return None - - with self.console.wait_bar("Loading remote debugger config..."): - debugger, config = parse_remote_debugger_cfg(remote_debugger_cfg) - - debuggers = get_debuggers() - debugger_class = debuggers[debugger] if debugger else DEFAULT_DEBUGGER - app.remote_debugger = debugger_class(console=self.console, config=config) - self.console.info(f"Using '{app.remote_debugger}'") - def parse_options(self, extra): """Parse the command line arguments for the Command. @@ -884,8 +880,8 @@ def _add_update_options( help=f"Prevent any automated update{context_label}", ) - def _add_test_and_debug_options(self, parser, context_label): - """Internal utility method for adding common test- and debug-related options. + def _add_test_options(self, parser, context_label): + """Internal utility method for adding common test-related options. :param parser: The parser to which options should be added. :param context_label: Label text for commands; the capitalized action being @@ -897,14 +893,26 @@ def _add_test_and_debug_options(self, parser, context_label): action="store_true", help=f"{context_label} the app in test mode", ) + + def _add_debug_options(self, parser, context_label): + """Internal utility method for adding common debug-related options. + + :param parser: The parser to which options should be added. + :param context_label: Label text for commands; the capitalized action being + performed (e.g., "Build", "Run",...) + """ + debuggers = get_debuggers() + debugger_names = list(reversed(debuggers.keys())) + parser.add_argument( - "--remote-debug", - dest="remote_debugger_cfg", + "--debug", + dest="debug_mode", nargs="?", default=None, const="", - metavar="REMOTE-DEBUGGER-CONFIG", - help=f"{context_label} the app with remote debugger enabled", + choices=["", *debugger_names], + metavar="DEBUGGER", + help=f"{context_label} the app with the specified debugger ({', '.join(debugger_names)})", ) def add_options(self, parser): diff --git a/src/briefcase/commands/build.py b/src/briefcase/commands/build.py index 155eed801..d92d288ac 100644 --- a/src/briefcase/commands/build.py +++ b/src/briefcase/commands/build.py @@ -14,7 +14,8 @@ class BuildCommand(BaseCommand): def add_options(self, parser): self._add_update_options(parser, context_label=" before building") - self._add_test_and_debug_options(parser, context_label="Build") + self._add_test_options(parser, context_label="Build") + self._add_debug_options(parser, context_label="Build") parser.add_argument( "-a", @@ -41,6 +42,7 @@ def _build_app( update_stub: bool, no_update: bool, test_mode: bool, + debug_mode: str | None, **options, ) -> dict | None: """Internal method to invoke a build on a single app. Ensures the app exists, @@ -57,9 +59,12 @@ def _build_app( :param update_stub: Should the stub binary be updated? :param no_update: Should automated updates be disabled? :param test_mode: Is the app being build in test mode? + :param debug_mode: Is the app being build in debug mode? """ if not self.bundle_path(app).exists(): - state = self.create_command(app, test_mode=test_mode, **options) + state = self.create_command( + app, test_mode=test_mode, debug_mode=debug_mode, **options + ) elif ( update # An explicit update has been requested or update_requirements # An explicit update of requirements has been requested @@ -69,6 +74,9 @@ def _build_app( or ( test_mode and not no_update ) # Test mode, but updates have not been disabled + or ( + debug_mode and not no_update + ) # Debug mode, but updates have not been disabled ): state = self.update_command( app, @@ -77,6 +85,7 @@ def _build_app( update_support=update_support, update_stub=update_stub, test_mode=test_mode, + debug_mode=debug_mode, **options, ) else: @@ -84,9 +93,15 @@ def _build_app( self.verify_app(app) - state = self.build_app(app, test_mode=test_mode, **full_options(state, options)) + state = self.build_app( + app, + test_mode=test_mode, + debug_mode=debug_mode, + **full_options(state, options), + ) qualifier = " (test mode)" if test_mode else "" + qualifier += " (debug mode)" if debug_mode else "" self.console.info( f"Built {self.binary_path(app).relative_to(self.base_path)}{qualifier}", prefix=app.app_name, @@ -104,7 +119,7 @@ def __call__( update_stub: bool = False, no_update: bool = False, test_mode: bool = False, - remote_debugger_cfg: str | None = None, + debug_mode: str | None = None, **options, ) -> dict | None: # Has the user requested an invalid set of options? @@ -133,7 +148,7 @@ def __call__( # Confirm host compatibility, that all required tools are available, # and that the app configuration is finalized. - self.finalize(app, remote_debugger_cfg) + self.finalize(app, debug_mode) if app_name: try: @@ -158,6 +173,7 @@ def __call__( update_stub=update_stub, no_update=no_update, test_mode=test_mode, + debug_mode=debug_mode, **full_options(state, options), ) diff --git a/src/briefcase/commands/create.py b/src/briefcase/commands/create.py index 90237a8b1..e7820e452 100644 --- a/src/briefcase/commands/create.py +++ b/src/briefcase/commands/create.py @@ -207,14 +207,14 @@ def permissions_context(self, app: AppConfig, x_permissions: dict[str, str]): """ return {} - def output_format_template_context(self, app: AppConfig): + def output_format_template_context(self, app: AppConfig, debug_mode: bool = False): """Additional template context required by the output format. :param app: The config object for the app """ return {} - def generate_app_template(self, app: AppConfig): + def generate_app_template(self, app: AppConfig, debug_mode: bool = False): """Create an application bundle. :param app: The config object for the app @@ -258,7 +258,9 @@ def generate_app_template(self, app: AppConfig): extra_context.update(self.permissions_context(app, self._x_permissions(app))) # Add in any extra template context required by the output format. - extra_context.update(self.output_format_template_context(app)) + extra_context.update( + self.output_format_template_context(app, debug_mode=debug_mode) + ) # Create the platform directory (if it doesn't already exist) output_path = self.bundle_path(app).parent @@ -666,7 +668,9 @@ def _install_app_requirements( else: self.console.info("No application requirements.") - def install_app_requirements(self, app: AppConfig, test_mode: bool): + def install_app_requirements( + self, app: AppConfig, test_mode: bool, debug_mode: str | None + ): """Handle requirements for the app. This will result in either (in preferential order): @@ -676,20 +680,21 @@ def install_app_requirements(self, app: AppConfig, test_mode: bool): by the ``app_packages_path`` in the template path index. If ``test_mode`` is True, the test requirements will also be installed. + If ``debug_mode`` is True, the debug requirements will also be installed. If the path index doesn't specify either of the path index entries, an error is raised. :param app: The config object for the app :param test_mode: Should the test requirements be installed? - :param debugger: Debugger to be used or None if no debugger should be used. + :param debug_mode: Should the debug requirements be installed? """ requires = app.requires.copy() if app.requires else [] if test_mode and app.test_requires: requires.extend(app.test_requires) - if app.remote_debugger: - requires.extend(app.remote_debugger.additional_requirements) + if debug_mode: + requires.extend(app.debug_requires) try: requirements_path = self.app_requirements_path(app) @@ -914,12 +919,14 @@ def create_app( self, app: AppConfig, test_mode: bool = False, + debug_mode: bool = False, **options, ): """Create an application bundle. :param app: The config object for the app :param test_mode: Should the app be updated in test mode? (default: False) + :param debug_mode: Should the app be updated in debug mode? (default: False) """ if not app.supported: raise UnsupportedPlatform(self.platform) @@ -939,7 +946,7 @@ def create_app( self.tools.shutil.rmtree(bundle_path) self.console.info("Generating application template...", prefix=app.app_name) - self.generate_app_template(app=app) + self.generate_app_template(app=app, debug_mode=debug_mode) self.console.info("Installing support package...", prefix=app.app_name) self.install_app_support_package(app=app) @@ -963,7 +970,9 @@ def create_app( self.install_app_code(app=app, test_mode=test_mode) self.console.info("Installing requirements...", prefix=app.app_name) - self.install_app_requirements(app=app, test_mode=test_mode) + self.install_app_requirements( + app=app, test_mode=test_mode, debug_mode=debug_mode + ) self.console.info("Installing application resources...", prefix=app.app_name) self.install_app_resources(app=app) diff --git a/src/briefcase/commands/run.py b/src/briefcase/commands/run.py index 5882158bd..599f9ee79 100644 --- a/src/briefcase/commands/run.py +++ b/src/briefcase/commands/run.py @@ -11,7 +11,7 @@ from briefcase.debuggers.base import ( AppPackagesPathMappings, AppPathMappings, - RemoteDebuggerConfig, + DebuggerConfig, ) from briefcase.exceptions import BriefcaseCommandError, BriefcaseTestSuiteFailure from briefcase.integrations.subprocess import StopStreaming @@ -225,7 +225,23 @@ def add_options(self, parser): ) self._add_update_options(parser, context_label=" before running") - self._add_test_and_debug_options(parser, context_label="Run") + self._add_test_options(parser, context_label="Run") + self._add_debug_options(parser, context_label="Run") + + parser.add_argument( + "--debugger-host", + default="localhost", + help="The host on which to run the debug server (default: localhost)", + required=False, + ) + parser.add_argument( + "-dp", + "--debugger-port", + default=5678, + type=int, + help="The port on which to run the debug server (default: 8080)", + required=False, + ) def remote_debugger_app_path_mappings( self, app: AppConfig, test_mode: bool @@ -264,7 +280,13 @@ def remote_debugger_app_packages_path_mapping( host_folder=f"{app_packages_path}", ) - def remote_debugger_config(self, app: AppConfig, test_mode: bool) -> str: + def remote_debugger_config( + self, + app: AppConfig, + test_mode: bool, + debugger_host: str, + debugger_port: int, + ) -> str: """ Create the remote debugger configuration that should be saved as environment variable for this run. @@ -274,17 +296,22 @@ def remote_debugger_config(self, app: AppConfig, test_mode: bool) -> str: """ app_path_mappings = self.remote_debugger_app_path_mappings(app, test_mode) app_packages_path_mappings = self.remote_debugger_app_packages_path_mapping(app) - config = RemoteDebuggerConfig( - debugger=app.remote_debugger.name, - mode=app.remote_debugger.mode, - ip=app.remote_debugger.ip, - port=app.remote_debugger.port, + config = DebuggerConfig( + host=debugger_host, + port=debugger_port, app_path_mappings=app_path_mappings, app_packages_path_mappings=app_packages_path_mappings, ) return json.dumps(config) - def _prepare_app_kwargs(self, app: AppConfig, test_mode: bool): + def _prepare_app_kwargs( + self, + app: AppConfig, + test_mode: bool, + debug_mode: bool, + debugger_host: str | None, + debugger_port: int | None, + ): """Prepare the kwargs for running an app as a log stream. This won't be used by every backend; but it's a sufficiently common default that @@ -302,9 +329,9 @@ def _prepare_app_kwargs(self, app: AppConfig, test_mode: bool): env["BRIEFCASE_DEBUG"] = "1" # If we're in remote debug mode, save the remote debugger config - if app.remote_debugger: - env["BRIEFCASE_REMOTE_DEBUGGER"] = self.remote_debugger_config( - app, test_mode + if debug_mode: + env["BRIEFCASE_DEBUGGER"] = self.remote_debugger_config( + app, test_mode, debugger_host, debugger_port ) if test_mode: @@ -322,7 +349,16 @@ def _prepare_app_kwargs(self, app: AppConfig, test_mode: bool): return args @abstractmethod - def run_app(self, app: AppConfig, **options) -> dict | None: + def run_app( + self, + app: AppConfig, + test_mode: bool, + debug_mode: bool, + debugger_host: str | None, + debugger_port: int | None, + passthrough: list[str], + **options, + ) -> dict | None: """Start an application. :param app: The application to start @@ -338,7 +374,9 @@ def __call__( update_stub: bool = False, no_update: bool = False, test_mode: bool = False, - remote_debugger_cfg: str | None = None, + debug_mode: str | None = None, + debugger_host: str | None = None, + debugger_port: int | None = None, passthrough: list[str] | None = None, **options, ) -> dict | None: @@ -361,7 +399,7 @@ def __call__( # Confirm host compatibility, that all required tools are available, # and that the app configuration is finalized. - self.finalize(app, remote_debugger_cfg) + self.finalize(app, debug_mode) template_file = self.bundle_path(app) exec_file = self.binary_executable_path(app) @@ -386,6 +424,7 @@ def __call__( update_stub=update_stub, no_update=no_update, test_mode=test_mode, + debug_mode=debug_mode, **options, ) else: @@ -396,6 +435,9 @@ def __call__( state = self.run_app( app, test_mode=test_mode, + debug_mode=debug_mode, + debugger_host=debugger_host, + debugger_port=debugger_port, passthrough=[] if passthrough is None else passthrough, **full_options(state, options), ) diff --git a/src/briefcase/commands/update.py b/src/briefcase/commands/update.py index bc08300b4..fe8d2c2b2 100644 --- a/src/briefcase/commands/update.py +++ b/src/briefcase/commands/update.py @@ -15,7 +15,8 @@ class UpdateCommand(CreateCommand): def add_options(self, parser): self._add_update_options(parser, update=False) - self._add_test_and_debug_options(parser, context_label="Update") + self._add_test_options(parser, context_label="Update") + self._add_debug_options(parser, context_label="Update") parser.add_argument( "-a", @@ -33,6 +34,7 @@ def update_app( update_support: bool, update_stub: bool, test_mode: bool, + debug_mode: bool, **options, ) -> dict | None: """Update an existing application bundle. @@ -43,6 +45,7 @@ def update_app( :param update_support: Should app support be updated? :param update_stub: Should stub binary be updated? :param test_mode: Should the app be updated in test mode? + :param debug_mode: Should the app be updated in debug mode? """ if not self.bundle_path(app).exists(): @@ -58,7 +61,9 @@ def update_app( if update_requirements: self.console.info("Updating requirements...", prefix=app.app_name) - self.install_app_requirements(app=app, test_mode=test_mode) + self.install_app_requirements( + app=app, test_mode=test_mode, debug_mode=debug_mode + ) if update_resources: self.console.info("Updating application resources...", prefix=app.app_name) @@ -96,12 +101,12 @@ def __call__( update_support: bool = False, update_stub: bool = False, test_mode: bool = False, - remote_debugger_cfg: str | None = None, + debug_mode: str | None = None, **options, ) -> dict | None: # Confirm host compatibility, that all required tools are available, # and that the app configuration is finalized. - self.finalize(app, remote_debugger_cfg) + self.finalize(app, debug_mode) if app_name: try: @@ -124,6 +129,7 @@ def __call__( update_support=update_support, update_stub=update_stub, test_mode=test_mode, + debug_mode=debug_mode, **full_options(state, options), ) diff --git a/src/briefcase/config.py b/src/briefcase/config.py index 2bb489c66..a099cf9f6 100644 --- a/src/briefcase/config.py +++ b/src/briefcase/config.py @@ -13,7 +13,6 @@ else: # pragma: no-cover-if-gte-py311 import tomli as tomllib -from briefcase.debuggers.base import BaseDebugger from briefcase.platforms import get_output_formats, get_platforms from .constants import RESERVED_WORDS @@ -282,6 +281,7 @@ def __init__( template_branch=None, test_sources=None, test_requires=None, + debug_requires=None, supported=True, long_description=None, console_app=False, @@ -311,6 +311,7 @@ def __init__( self.template_branch = template_branch self.test_sources = test_sources self.test_requires = test_requires + self.debug_requires = debug_requires self.supported = supported self.long_description = long_description self.license = license @@ -318,7 +319,6 @@ def __init__( self.requirement_installer_args = ( [] if requirement_installer_args is None else requirement_installer_args ) - self.remote_debugger: BaseDebugger | None = None if not is_valid_app_name(self.app_name): raise BriefcaseConfigError( diff --git a/src/briefcase/debuggers/__init__.py b/src/briefcase/debuggers/__init__.py index 475e37875..0574b988a 100644 --- a/src/briefcase/debuggers/__init__.py +++ b/src/briefcase/debuggers/__init__.py @@ -1,5 +1,7 @@ import sys +from briefcase.exceptions import BriefcaseCommandError + if sys.version_info >= (3, 10): # pragma: no-cover-if-lt-py310 from importlib.metadata import entry_points else: # pragma: no-cover-if-gte-py310 @@ -11,8 +13,6 @@ from briefcase.debuggers.debugpy import DebugpyDebugger # noqa: F401 from briefcase.debuggers.pdb import PdbDebugger # noqa: F401 -DEFAULT_DEBUGGER = PdbDebugger - def get_debuggers() -> dict[str, type[BaseDebugger]]: """Loads built-in and third-party debuggers.""" @@ -20,3 +20,11 @@ def get_debuggers() -> dict[str, type[BaseDebugger]]: entry_point.name: entry_point.load() for entry_point in entry_points(group="briefcase.debuggers") } + + +def get_debugger(name: str) -> BaseDebugger: + """Get a debugger by name.""" + debuggers = get_debuggers() + if name not in debuggers: + raise BriefcaseCommandError(f"Unknown debugger: {name}") + return debuggers[name]() diff --git a/src/briefcase/debuggers/base.py b/src/briefcase/debuggers/base.py index 3367644ea..f1bcd267f 100644 --- a/src/briefcase/debuggers/base.py +++ b/src/briefcase/debuggers/base.py @@ -1,12 +1,8 @@ from __future__ import annotations -import dataclasses import enum -from abc import ABC -from typing import ClassVar, TypedDict - -from briefcase.console import Console -from briefcase.exceptions import BriefcaseCommandError, BriefcaseConfigError +from abc import ABC, abstractmethod +from typing import TypedDict class AppPathMappings(TypedDict): @@ -20,10 +16,8 @@ class AppPackagesPathMappings(TypedDict): host_folder: str -class RemoteDebuggerConfig(TypedDict): - debugger: str - mode: str # client / server - ip: str +class DebuggerConfig(TypedDict): + host: str port: int app_path_mappings: AppPathMappings | None app_packages_path_mappings: AppPackagesPathMappings | None @@ -34,86 +28,48 @@ class DebuggerMode(str, enum.Enum): CLIENT = "client" -@dataclasses.dataclass -class DebuggerConfig: - mode: str | None - ip: str | None - port: int | None - - -def parse_remote_debugger_cfg( - remote_debugger_cfg: str, -) -> tuple[str, DebuggerConfig]: - """ - Convert a remote debugger config string into a DebuggerConfig object. - - The config string is expected to be in the form: - "[DEBUGGER[,[IP:]PORT][,MODE]]" - - Config examples: - "" - "pdb" - "pdb,5678" - "pdb,,server" - "pdb,localhost:5678,server" - """ - debugger = ip_and_port = ip = port = mode = None - parts = remote_debugger_cfg.split(",") - if len(parts) == 1: - debugger = parts[0] - elif len(parts) == 2: - debugger, ip_and_port = parts - elif len(parts) == 3: - debugger, ip_and_port, mode = parts - else: - raise BriefcaseCommandError( - f"Invalid remote debugger specification: {remote_debugger_cfg}" - ) - - if ip_and_port is not None: - parts = ip_and_port.split(":") - if len(parts) == 1: - port = parts[0] - if port == "": - port = None - elif len(parts) == 2: - ip = parts[0] - port = parts[1] - else: - raise BriefcaseCommandError( - f"Invalid remote debugger specification: {remote_debugger_cfg}" - ) - - if port is not None: - try: - port = int(port) - except ValueError: - raise BriefcaseCommandError(f"Invalid remote debugger port: {port}") - - return debugger, DebuggerConfig(mode=mode, ip=ip, port=port) +# @dataclasses.dataclass +# class DebuggerOptions: +# mode: str +# host: str +# port: int class BaseDebugger(ABC): """Definition for a plugin that defines a new Briefcase debugger.""" name: str - supported_modes: ClassVar[list[DebuggerMode]] - default_mode: ClassVar[DebuggerMode] - default_ip: ClassVar[str] = "localhost" - default_port: ClassVar[int] = 5678 - - def __init__(self, console: Console, config: DebuggerConfig) -> None: - self.console = console - self.mode: DebuggerMode = DebuggerMode(config.mode or self.default_mode) - self.ip: str = config.ip or self.default_ip - self.port: int = config.port or self.default_port - - if self.mode not in self.supported_modes: - raise BriefcaseConfigError( - f"Unsupported debugger mode: {self.mode} for {self.__class__.__name__}" - ) @property + @abstractmethod def additional_requirements(self) -> list[str]: """Return a list of additional requirements for the debugger.""" - return [] + raise NotImplementedError + + @property + @abstractmethod + def debugger_mode(self) -> DebuggerMode: + """Return the mode of the debugger.""" + raise NotImplementedError + + # def validate_run_options(self, mode: str | None, host: str | None, port: int | None) -> DebuggerOptions: + # """Validate the run options for the debugger.""" + # if mode is None: + # mode = self.default_mode.value + # elif mode not in [m.value for m in self.supported_modes]: + # raise BriefcaseCommandError( + # f"Invalid mode '{mode}'. Supported modes are: {', '.join([m.value for m in self.supported_modes])}." + # ) + + # if host is None: + # host = self.default_host + + # if port is None: + # port = self.default_port + + # return DebuggerOptions(mode=mode, host=host, port=port) + + # @abstractmethod + # def get_env(self) -> dict[str, str]: + # """Return environment variables to set before running the debugger.""" + # return {} diff --git a/src/briefcase/debuggers/debugpy.py b/src/briefcase/debuggers/debugpy.py index 2e0564314..4cebd0388 100644 --- a/src/briefcase/debuggers/debugpy.py +++ b/src/briefcase/debuggers/debugpy.py @@ -5,13 +5,15 @@ class DebugpyDebugger(BaseDebugger): """Definition for a plugin that defines a new Briefcase debugger.""" name = "debugpy" - supported_modes = [DebuggerMode.SERVER, DebuggerMode.CLIENT] - default_mode = DebuggerMode.SERVER @property def additional_requirements(self) -> list[str]: """Return a list of additional requirements for the debugger.""" return [ - "git+https://github.com/timrid/briefcase-remote-debugger@main", - "debugpy~=1.8.12", + "git+https://github.com/timrid/briefcase-debugadapter#subdirectory=briefcase-debugpy-debugadapter" ] + + @property + def debugger_mode(self) -> DebuggerMode: + """Return the mode of the debugger.""" + return DebuggerMode.SERVER diff --git a/src/briefcase/debuggers/pdb.py b/src/briefcase/debuggers/pdb.py index f234eb162..104353135 100644 --- a/src/briefcase/debuggers/pdb.py +++ b/src/briefcase/debuggers/pdb.py @@ -5,10 +5,15 @@ class PdbDebugger(BaseDebugger): """Definition for a plugin that defines a new Briefcase debugger.""" name = "pdb" - supported_modes = [DebuggerMode.SERVER] - default_mode = DebuggerMode.SERVER @property def additional_requirements(self) -> list[str]: """Return a list of additional requirements for the debugger.""" - return ["git+https://github.com/timrid/briefcase-remote-debugger@main"] + return [ + "git+https://github.com/timrid/briefcase-debugadapter#subdirectory=briefcase-pdb-debugadapter" + ] + + @property + def debugger_mode(self) -> DebuggerMode: + """Return the mode of the debugger.""" + return DebuggerMode.SERVER diff --git a/src/briefcase/platforms/android/gradle.py b/src/briefcase/platforms/android/gradle.py index 3f2e6476c..20f27c25d 100644 --- a/src/briefcase/platforms/android/gradle.py +++ b/src/briefcase/platforms/android/gradle.py @@ -159,7 +159,7 @@ def support_package_filename(self, support_revision): f"Python-{self.python_version_tag}-Android-support.b{support_revision}.zip" ) - def output_format_template_context(self, app: AppConfig): + def output_format_template_context(self, app: AppConfig, debug_mode: bool = False): """Additional template context required by the output format. :param app: The config object for the app @@ -216,7 +216,7 @@ def output_format_template_context(self, app: AppConfig): ] extract_sources = app.test_sources or [] - if app.remote_debugger: + if debug_mode: # Add sources to the extract_packages so that the debugger can # get the source code at runtime (eg. via 'll' in pdb). extract_sources.extend(app.sources) @@ -384,6 +384,9 @@ def run_app( self, app: AppConfig, test_mode: bool, + debug_mode: bool, + debugger_host: str | None, + debugger_port: int | None, passthrough: list[str], device_or_avd=None, extra_emulator_args=None, @@ -448,11 +451,11 @@ def run_app( adb.install_apk(self.binary_path(app)) env = {} - if app.remote_debugger: + if debug_mode: with self.console.wait_bar("Establishing debugger connection..."): self.establish_debugger_connection(adb, app.remote_debugger) - env["BRIEFCASE_REMOTE_DEBUGGER"] = self.remote_debugger_config( - app, test_mode + env["BRIEFCASE_DEBUGGER"] = self.remote_debugger_config( + app, test_mode, debugger_host, debugger_port ) if self.console.is_debug: diff --git a/src/briefcase/platforms/iOS/xcode.py b/src/briefcase/platforms/iOS/xcode.py index 0c59a9e28..18a8f7b40 100644 --- a/src/briefcase/platforms/iOS/xcode.py +++ b/src/briefcase/platforms/iOS/xcode.py @@ -498,6 +498,9 @@ def run_app( self, app: AppConfig, test_mode: bool, + debug_mode: bool, + debugger_host: str | None, + debugger_port: int | None, passthrough: list[str], udid=None, **kwargs, @@ -648,9 +651,9 @@ def run_app( # Add additional environment variables env = {} - if app.remote_debugger: - env["BRIEFCASE_REMOTE_DEBUGGER"] = self.remote_debugger_config( - app, test_mode + if debug_mode: + env["BRIEFCASE_DEBUGGER"] = self.remote_debugger_config( + app, test_mode, debugger_host, debugger_port ) # Install additional environment variables diff --git a/src/briefcase/platforms/linux/appimage.py b/src/briefcase/platforms/linux/appimage.py index 745efae87..38db5f404 100644 --- a/src/briefcase/platforms/linux/appimage.py +++ b/src/briefcase/platforms/linux/appimage.py @@ -184,8 +184,8 @@ class LinuxAppImageCreateCommand( ): description = "Create and populate a Linux AppImage." - def output_format_template_context(self, app: AppConfig): - context = super().output_format_template_context(app) + def output_format_template_context(self, app: AppConfig, debug_mode: bool = False): + context = super().output_format_template_context(app, debug_mode) try: manylinux_arch = { @@ -370,6 +370,9 @@ def run_app( self, app: AppConfig, test_mode: bool, + debug_mode: bool, + debugger_host: str | None, + debugger_port: int | None, passthrough: list[str], **kwargs, ): @@ -380,7 +383,14 @@ def run_app( :param passthrough: The list of arguments to pass to the app """ # Set up the log stream - kwargs = self._prepare_app_kwargs(app=app, test_mode=test_mode) + kwargs = self._prepare_app_kwargs( + app=app, + test_mode=test_mode, + debug_mode=debug_mode, + debugger_host=debugger_host, + debugger_port=debugger_port, + **kwargs, + ) # Console apps must operate in non-streaming mode so that console input can # be handled correctly. However, if we're in test mode, we *must* stream so diff --git a/src/briefcase/platforms/linux/flatpak.py b/src/briefcase/platforms/linux/flatpak.py index 3f4e27495..2813a872e 100644 --- a/src/briefcase/platforms/linux/flatpak.py +++ b/src/briefcase/platforms/linux/flatpak.py @@ -106,7 +106,7 @@ class LinuxFlatpakCreateCommand(LinuxFlatpakMixin, CreateCommand): description = "Create and populate a Linux Flatpak." hidden_app_properties = {"permission", "finish_arg"} - def output_format_template_context(self, app: AppConfig): + def output_format_template_context(self, app: AppConfig, debug_mode: bool = False): """Add flatpak runtime/SDK details to the app template.""" return { "flatpak_runtime": self.flatpak_runtime(app), @@ -218,6 +218,9 @@ def run_app( self, app: AppConfig, test_mode: bool, + debug_mode: bool, + debugger_host: str | None, + debugger_port: int | None, passthrough: list[str], **kwargs, ): @@ -228,7 +231,13 @@ def run_app( :param passthrough: The list of arguments to pass to the app """ # Set up the log stream - kwargs = self._prepare_app_kwargs(app=app, test_mode=test_mode) + kwargs = self._prepare_app_kwargs( + app=app, + test_mode=test_mode, + debug_mode=debug_mode, + debugger_host=debugger_host, + debugger_port=debugger_port, + ) # Console apps must operate in non-streaming mode so that console input can # be handled correctly. However, if we're in test mode, we *must* stream so diff --git a/src/briefcase/platforms/linux/system.py b/src/briefcase/platforms/linux/system.py index a1fe5357c..71e832b9f 100644 --- a/src/briefcase/platforms/linux/system.py +++ b/src/briefcase/platforms/linux/system.py @@ -639,8 +639,8 @@ def verify_host(self): class LinuxSystemCreateCommand(LinuxSystemMixin, LocalRequirementsMixin, CreateCommand): description = "Create and populate a Linux system project." - def output_format_template_context(self, app: AppConfig): - context = super().output_format_template_context(app) + def output_format_template_context(self, app: AppConfig, debug_mode: bool = False): + context = super().output_format_template_context(app, debug_mode) # Linux system templates use the target codename, rather than # the format "system" as the leaf of the bundle path @@ -840,6 +840,9 @@ def run_app( self, app: AppConfig, test_mode: bool, + debug_mode: bool, + debugger_host: str | None, + debugger_port: int | None, passthrough: list[str], **kwargs, ): @@ -850,7 +853,13 @@ def run_app( :param passthrough: The list of arguments to pass to the app """ # Set up the log stream - kwargs = self._prepare_app_kwargs(app=app, test_mode=test_mode) + kwargs = self._prepare_app_kwargs( + app=app, + test_mode=test_mode, + debug_mode=debug_mode, + debugger_host=debugger_host, + debugger_port=debugger_port, + ) with self.tools[app].app_context.run_app_context(kwargs) as kwargs: # Console apps must operate in non-streaming mode so that console input can diff --git a/src/briefcase/platforms/macOS/__init__.py b/src/briefcase/platforms/macOS/__init__.py index fc37e4260..ee9f21ce6 100644 --- a/src/briefcase/platforms/macOS/__init__.py +++ b/src/briefcase/platforms/macOS/__init__.py @@ -295,6 +295,9 @@ def run_app( self, app: AppConfig, test_mode: bool, + debug_mode: bool, + debugger_host: str | None, + debugger_port: int | None, passthrough: list[str], **kwargs, ): @@ -311,6 +314,9 @@ def run_app( self.run_console_app( app, test_mode=test_mode, + debug_mode=debug_mode, + debugger_host=debugger_host, + debugger_port=debugger_port, passthrough=passthrough, **kwargs, ) @@ -318,6 +324,9 @@ def run_app( self.run_gui_app( app, test_mode=test_mode, + debug_mode=debug_mode, + debugger_host=debugger_host, + debugger_port=debugger_port, passthrough=passthrough, **kwargs, ) @@ -326,6 +335,9 @@ def run_console_app( self, app: AppConfig, test_mode: bool, + debug_mode: bool, + debugger_host: str | None, + debugger_port: int | None, passthrough: list[str], **kwargs, ): @@ -335,7 +347,13 @@ def run_console_app( :param test_mode: Boolean; Is the app running in test mode? :param passthrough: The list of arguments to pass to the app """ - sub_kwargs = self._prepare_app_kwargs(app=app, test_mode=test_mode) + sub_kwargs = self._prepare_app_kwargs( + app=app, + test_mode=test_mode, + debug_mode=debug_mode, + debugger_host=debugger_host, + debugger_port=debugger_port, + ) cmdline = [self.binary_path(app) / f"Contents/MacOS/{app.formal_name}"] cmdline.extend(passthrough) @@ -375,6 +393,9 @@ def run_gui_app( self, app: AppConfig, test_mode: bool, + debug_mode: bool, + debugger_host: str | None, + debugger_port: int | None, passthrough: list[str], **kwargs, ): @@ -423,7 +444,13 @@ def run_gui_app( app_pid = None try: # Set up the log stream - sub_kwargs = self._prepare_app_kwargs(app=app, test_mode=test_mode) + sub_kwargs = self._prepare_app_kwargs( + app=app, + test_mode=test_mode, + debug_mode=debug_mode, + debugger_host=debugger_host, + debugger_port=debugger_port, + ) # Start the app in a way that lets us stream the logs self.tools.subprocess.run( diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index 5cecc44dc..ab4660631 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -50,7 +50,7 @@ def distribution_path(self, app): class StaticWebCreateCommand(StaticWebMixin, CreateCommand): description = "Create and populate a static web project." - def output_format_template_context(self, app: AppConfig): + def output_format_template_context(self, app: AppConfig, debug_mode: bool = False): """Add style framework details to the app template.""" return { "style_framework": getattr(app, "style_framework", "None"), @@ -311,6 +311,9 @@ def run_app( self, app: AppConfig, test_mode: bool, + debug_mode: bool, + debugger_host: str | None, + debugger_port: int | None, passthrough: list[str], host, port, diff --git a/src/briefcase/platforms/windows/__init__.py b/src/briefcase/platforms/windows/__init__.py index 416ba6219..942eb0243 100644 --- a/src/briefcase/platforms/windows/__init__.py +++ b/src/briefcase/platforms/windows/__init__.py @@ -59,7 +59,7 @@ def support_package_url(self, support_revision): f"{self.support_package_filename(support_revision)}" ) - def output_format_template_context(self, app: AppConfig): + def output_format_template_context(self, app: AppConfig, debug_mode: bool = False): """Additional template context required by the output format. :param app: The config object for the app @@ -141,6 +141,9 @@ def run_app( self, app: AppConfig, test_mode: bool, + debug_mode: bool, + debugger_host: str | None, + debugger_port: int | None, passthrough: list[str], **kwargs, ): @@ -151,7 +154,13 @@ def run_app( :param passthrough: The list of arguments to pass to the app """ # Set up the log stream - kwargs = self._prepare_app_kwargs(app=app, test_mode=test_mode) + kwargs = self._prepare_app_kwargs( + app=app, + test_mode=test_mode, + debug_mode=debug_mode, + debugger_host=debugger_host, + debugger_port=debugger_port, + ) # Console apps must operate in non-streaming mode so that console input can # be handled correctly. However, if we're in test mode, we *must* stream so diff --git a/tests/commands/create/conftest.py b/tests/commands/create/conftest.py index 4c723920e..2e1086a10 100644 --- a/tests/commands/create/conftest.py +++ b/tests/commands/create/conftest.py @@ -86,7 +86,7 @@ def python_version_tag(self): return "3.X" # Define output format-specific template context. - def output_format_template_context(self, app): + def output_format_template_context(self, app: AppConfig, debug_mode: bool = False): return {"output_format": "dummy"} # Handle platform-specific permissions. From 2c14a4cb828bd26cdb14b5ab3bb677a0228f3536 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sat, 17 May 2025 17:05:34 +0200 Subject: [PATCH 019/131] make android working and remove unnecessary code --- src/briefcase/debuggers/base.py | 37 ++--------------------- src/briefcase/debuggers/debugpy.py | 10 +++--- src/briefcase/debuggers/pdb.py | 10 +++--- src/briefcase/platforms/android/gradle.py | 36 +++++++++++++--------- 4 files changed, 33 insertions(+), 60 deletions(-) diff --git a/src/briefcase/debuggers/base.py b/src/briefcase/debuggers/base.py index f1bcd267f..8a4ff6f9d 100644 --- a/src/briefcase/debuggers/base.py +++ b/src/briefcase/debuggers/base.py @@ -23,23 +23,14 @@ class DebuggerConfig(TypedDict): app_packages_path_mappings: AppPackagesPathMappings | None -class DebuggerMode(str, enum.Enum): +class DebuggerConnectionMode(str, enum.Enum): SERVER = "server" CLIENT = "client" -# @dataclasses.dataclass -# class DebuggerOptions: -# mode: str -# host: str -# port: int - - class BaseDebugger(ABC): """Definition for a plugin that defines a new Briefcase debugger.""" - name: str - @property @abstractmethod def additional_requirements(self) -> list[str]: @@ -48,28 +39,6 @@ def additional_requirements(self) -> list[str]: @property @abstractmethod - def debugger_mode(self) -> DebuggerMode: - """Return the mode of the debugger.""" + def connection_mode(self) -> DebuggerConnectionMode: + """Return the connection mode of the debugger.""" raise NotImplementedError - - # def validate_run_options(self, mode: str | None, host: str | None, port: int | None) -> DebuggerOptions: - # """Validate the run options for the debugger.""" - # if mode is None: - # mode = self.default_mode.value - # elif mode not in [m.value for m in self.supported_modes]: - # raise BriefcaseCommandError( - # f"Invalid mode '{mode}'. Supported modes are: {', '.join([m.value for m in self.supported_modes])}." - # ) - - # if host is None: - # host = self.default_host - - # if port is None: - # port = self.default_port - - # return DebuggerOptions(mode=mode, host=host, port=port) - - # @abstractmethod - # def get_env(self) -> dict[str, str]: - # """Return environment variables to set before running the debugger.""" - # return {} diff --git a/src/briefcase/debuggers/debugpy.py b/src/briefcase/debuggers/debugpy.py index 4cebd0388..1a19d1935 100644 --- a/src/briefcase/debuggers/debugpy.py +++ b/src/briefcase/debuggers/debugpy.py @@ -1,11 +1,9 @@ -from briefcase.debuggers.base import BaseDebugger, DebuggerMode +from briefcase.debuggers.base import BaseDebugger, DebuggerConnectionMode class DebugpyDebugger(BaseDebugger): """Definition for a plugin that defines a new Briefcase debugger.""" - name = "debugpy" - @property def additional_requirements(self) -> list[str]: """Return a list of additional requirements for the debugger.""" @@ -14,6 +12,6 @@ def additional_requirements(self) -> list[str]: ] @property - def debugger_mode(self) -> DebuggerMode: - """Return the mode of the debugger.""" - return DebuggerMode.SERVER + def connection_mode(self) -> DebuggerConnectionMode: + """Return the connection mode of the debugger.""" + return DebuggerConnectionMode.SERVER diff --git a/src/briefcase/debuggers/pdb.py b/src/briefcase/debuggers/pdb.py index 104353135..6bd4bebd0 100644 --- a/src/briefcase/debuggers/pdb.py +++ b/src/briefcase/debuggers/pdb.py @@ -1,11 +1,9 @@ -from briefcase.debuggers.base import BaseDebugger, DebuggerMode +from briefcase.debuggers.base import BaseDebugger, DebuggerConnectionMode class PdbDebugger(BaseDebugger): """Definition for a plugin that defines a new Briefcase debugger.""" - name = "pdb" - @property def additional_requirements(self) -> list[str]: """Return a list of additional requirements for the debugger.""" @@ -14,6 +12,6 @@ def additional_requirements(self) -> list[str]: ] @property - def debugger_mode(self) -> DebuggerMode: - """Return the mode of the debugger.""" - return DebuggerMode.SERVER + def connection_mode(self) -> DebuggerConnectionMode: + """Return the connection mode of the debugger.""" + return DebuggerConnectionMode.SERVER diff --git a/src/briefcase/platforms/android/gradle.py b/src/briefcase/platforms/android/gradle.py index 20f27c25d..4406c7065 100644 --- a/src/briefcase/platforms/android/gradle.py +++ b/src/briefcase/platforms/android/gradle.py @@ -17,7 +17,11 @@ ) from briefcase.config import AppConfig, parsed_version from briefcase.console import ANSI_ESC_SEQ_RE_DEF -from briefcase.debuggers.base import AppPackagesPathMappings, BaseDebugger, DebuggerMode +from briefcase.debuggers import get_debugger +from briefcase.debuggers.base import ( + AppPackagesPathMappings, + DebuggerConnectionMode, +) from briefcase.exceptions import BriefcaseCommandError from briefcase.integrations.android_sdk import ADB, AndroidSDK from briefcase.integrations.subprocess import SubprocessArgT @@ -453,7 +457,7 @@ def run_app( env = {} if debug_mode: with self.console.wait_bar("Establishing debugger connection..."): - self.establish_debugger_connection(adb, app.remote_debugger) + self.establish_debugger_connection(adb, debug_mode, debugger_port) env["BRIEFCASE_DEBUGGER"] = self.remote_debugger_config( app, test_mode, debugger_host, debugger_port ) @@ -509,34 +513,38 @@ def run_app( raise BriefcaseCommandError(f"Problem starting app {app.app_name!r}") finally: - if app.remote_debugger: + if debug_mode: with self.console.wait_bar("Stopping debugger connection..."): - self.remove_debugger_connection(adb, app.remote_debugger) + self.remove_debugger_connection(adb, debug_mode, debugger_port) if shutdown_on_exit: with self.tools.console.wait_bar("Stopping emulator..."): adb.kill() - def establish_debugger_connection(self, adb: ADB, debugger: BaseDebugger): + def establish_debugger_connection( + self, adb: ADB, debug_mode: str, debugger_port: int + ): """Forward/Reverse the ports necessary for remote debugging. :param app: The config object for the app :param adb: Access to the adb """ - if debugger.mode == DebuggerMode.SERVER: - adb.forward(debugger.port, debugger.port) - elif debugger.port == DebuggerMode.CLIENT: - adb.reverse(debugger.port, debugger.port) + debugger = get_debugger(debug_mode) + if debugger.connection_mode == DebuggerConnectionMode.SERVER: + adb.forward(debugger_port, debugger_port) + elif debugger.connection_mode == DebuggerConnectionMode.CLIENT: + adb.reverse(debugger_port, debugger_port) - def remove_debugger_connection(self, adb: ADB, debugger: BaseDebugger): + def remove_debugger_connection(self, adb: ADB, debug_mode: str, debugger_port: int): """Remove Forward/Reverse of the ports necessary for remote debugging. :param app: The config object for the app :param adb: Access to the adb """ - if debugger.mode == DebuggerMode.SERVER: - adb.forward_remove(debugger.port) - elif debugger.port == DebuggerMode.CLIENT: - adb.reverse_remove(debugger.port) + debugger = get_debugger(debug_mode) + if debugger.connection_mode == DebuggerConnectionMode.SERVER: + adb.forward_remove(debugger_port) + elif debugger.connection_mode == DebuggerConnectionMode.CLIENT: + adb.reverse_remove(debugger_port) class GradlePackageCommand(GradleMixin, PackageCommand): From cd99d80063a17836d011103cdbe0d946e205e6fd Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sun, 18 May 2025 15:59:02 +0200 Subject: [PATCH 020/131] fixed all unit tests --- src/briefcase/commands/create.py | 8 +- src/briefcase/integrations/android_sdk.py | 12 +- src/briefcase/platforms/android/gradle.py | 6 +- src/briefcase/platforms/linux/appimage.py | 4 +- src/briefcase/platforms/linux/flatpak.py | 4 +- src/briefcase/platforms/linux/system.py | 4 +- src/briefcase/platforms/web/static.py | 4 +- src/briefcase/platforms/windows/__init__.py | 4 +- tests/commands/build/conftest.py | 3 + tests/commands/build/test_call.py | 242 +++++++++++++++--- tests/commands/create/conftest.py | 8 +- .../create/test_generate_app_template.py | 1 + .../create/test_install_app_requirements.py | 50 ++-- tests/commands/run/conftest.py | 1 + tests/commands/run/test_call.py | 200 +++++++++++++-- tests/commands/update/conftest.py | 2 +- tests/commands/update/test_update_app.py | 10 + .../android_sdk/ADB/test_start_app.py | 14 +- .../integrations/flatpak/test_Flatpak__run.py | 13 +- tests/platforms/android/gradle/test_run.py | 55 +++- tests/platforms/iOS/xcode/test_create.py | 12 +- tests/platforms/iOS/xcode/test_run.py | 126 ++++++++- tests/platforms/iOS/xcode/test_update.py | 4 +- tests/platforms/linux/appimage/test_run.py | 54 +++- tests/platforms/linux/flatpak/test_run.py | 60 ++++- tests/platforms/linux/system/test_run.py | 96 ++++++- tests/platforms/macOS/app/test_create.py | 24 +- tests/platforms/macOS/app/test_run.py | 78 +++++- tests/platforms/macOS/xcode/test_run.py | 24 +- tests/platforms/web/static/test_run.py | 30 +++ tests/platforms/windows/app/test_run.py | 54 +++- .../windows/visualstudio/test_run.py | 24 +- tests/test_cmdline.py | 3 + tests/test_mainline.py | 8 +- 34 files changed, 1067 insertions(+), 175 deletions(-) diff --git a/src/briefcase/commands/create.py b/src/briefcase/commands/create.py index e7820e452..973cfe31a 100644 --- a/src/briefcase/commands/create.py +++ b/src/briefcase/commands/create.py @@ -207,14 +207,16 @@ def permissions_context(self, app: AppConfig, x_permissions: dict[str, str]): """ return {} - def output_format_template_context(self, app: AppConfig, debug_mode: bool = False): + def output_format_template_context( + self, app: AppConfig, debug_mode: str | None = None + ): """Additional template context required by the output format. :param app: The config object for the app """ return {} - def generate_app_template(self, app: AppConfig, debug_mode: bool = False): + def generate_app_template(self, app: AppConfig, debug_mode: str | None = None): """Create an application bundle. :param app: The config object for the app @@ -919,7 +921,7 @@ def create_app( self, app: AppConfig, test_mode: bool = False, - debug_mode: bool = False, + debug_mode: str | None = None, **options, ): """Create an application bundle. diff --git a/src/briefcase/integrations/android_sdk.py b/src/briefcase/integrations/android_sdk.py index 7c0457877..cf4bccefc 100644 --- a/src/briefcase/integrations/android_sdk.py +++ b/src/briefcase/integrations/android_sdk.py @@ -1545,9 +1545,15 @@ def start_app( "--es", "org.beeware.ARGV", shlex.quote(json.dumps(passthrough)), # Protect from Android's shell - "--es", - "org.beeware.ENVIRON", - shlex.quote(json.dumps(env)), # Protect from Android's shell + *( + [ + "--es", + "org.beeware.ENVIRON", + shlex.quote(json.dumps(env)), # Protect from Android's shell + ] + if env + else [] + ), ) # `adb shell am start` always exits with status zero. We look for error diff --git a/src/briefcase/platforms/android/gradle.py b/src/briefcase/platforms/android/gradle.py index 4406c7065..0f98bb738 100644 --- a/src/briefcase/platforms/android/gradle.py +++ b/src/briefcase/platforms/android/gradle.py @@ -163,7 +163,9 @@ def support_package_filename(self, support_revision): f"Python-{self.python_version_tag}-Android-support.b{support_revision}.zip" ) - def output_format_template_context(self, app: AppConfig, debug_mode: bool = False): + def output_format_template_context( + self, app: AppConfig, debug_mode: str | None = None + ): """Additional template context required by the output format. :param app: The config object for the app @@ -388,7 +390,7 @@ def run_app( self, app: AppConfig, test_mode: bool, - debug_mode: bool, + debug_mode: str | None, debugger_host: str | None, debugger_port: int | None, passthrough: list[str], diff --git a/src/briefcase/platforms/linux/appimage.py b/src/briefcase/platforms/linux/appimage.py index 38db5f404..1bb2e86b8 100644 --- a/src/briefcase/platforms/linux/appimage.py +++ b/src/briefcase/platforms/linux/appimage.py @@ -184,7 +184,9 @@ class LinuxAppImageCreateCommand( ): description = "Create and populate a Linux AppImage." - def output_format_template_context(self, app: AppConfig, debug_mode: bool = False): + def output_format_template_context( + self, app: AppConfig, debug_mode: str | None = None + ): context = super().output_format_template_context(app, debug_mode) try: diff --git a/src/briefcase/platforms/linux/flatpak.py b/src/briefcase/platforms/linux/flatpak.py index 2813a872e..dce1d69d1 100644 --- a/src/briefcase/platforms/linux/flatpak.py +++ b/src/briefcase/platforms/linux/flatpak.py @@ -106,7 +106,9 @@ class LinuxFlatpakCreateCommand(LinuxFlatpakMixin, CreateCommand): description = "Create and populate a Linux Flatpak." hidden_app_properties = {"permission", "finish_arg"} - def output_format_template_context(self, app: AppConfig, debug_mode: bool = False): + def output_format_template_context( + self, app: AppConfig, debug_mode: str | None = None + ): """Add flatpak runtime/SDK details to the app template.""" return { "flatpak_runtime": self.flatpak_runtime(app), diff --git a/src/briefcase/platforms/linux/system.py b/src/briefcase/platforms/linux/system.py index 71e832b9f..5be194d40 100644 --- a/src/briefcase/platforms/linux/system.py +++ b/src/briefcase/platforms/linux/system.py @@ -639,7 +639,9 @@ def verify_host(self): class LinuxSystemCreateCommand(LinuxSystemMixin, LocalRequirementsMixin, CreateCommand): description = "Create and populate a Linux system project." - def output_format_template_context(self, app: AppConfig, debug_mode: bool = False): + def output_format_template_context( + self, app: AppConfig, debug_mode: str | None = None + ): context = super().output_format_template_context(app, debug_mode) # Linux system templates use the target codename, rather than diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index ab4660631..607dcbaf4 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -50,7 +50,9 @@ def distribution_path(self, app): class StaticWebCreateCommand(StaticWebMixin, CreateCommand): description = "Create and populate a static web project." - def output_format_template_context(self, app: AppConfig, debug_mode: bool = False): + def output_format_template_context( + self, app: AppConfig, debug_mode: str | None = None + ): """Add style framework details to the app template.""" return { "style_framework": getattr(app, "style_framework", "None"), diff --git a/src/briefcase/platforms/windows/__init__.py b/src/briefcase/platforms/windows/__init__.py index 942eb0243..bc912bacd 100644 --- a/src/briefcase/platforms/windows/__init__.py +++ b/src/briefcase/platforms/windows/__init__.py @@ -59,7 +59,9 @@ def support_package_url(self, support_revision): f"{self.support_package_filename(support_revision)}" ) - def output_format_template_context(self, app: AppConfig, debug_mode: bool = False): + def output_format_template_context( + self, app: AppConfig, debug_mode: str | None = None + ): """Additional template context required by the output format. :param app: The config object for the app diff --git a/tests/commands/build/conftest.py b/tests/commands/build/conftest.py index 2caeae40e..381abc3b2 100644 --- a/tests/commands/build/conftest.py +++ b/tests/commands/build/conftest.py @@ -60,6 +60,7 @@ def build_app(self, app, **kwargs): kwargs.pop("update_stub", None) kwargs.pop("no_update", None) kwargs.pop("test_mode", None) + kwargs.pop("debug_mode", None) return full_options({"build_state": app.app_name}, kwargs) # These commands override the default behavior, simply tracking that @@ -69,6 +70,7 @@ def create_command(self, app, **kwargs): self.actions.append(("create", app.app_name, kwargs.copy())) # Remove arguments consumed by the underlying call to create_app() kwargs.pop("test_mode", None) + kwargs.pop("debug_mode", None) return full_options({"create_state": app.app_name}, kwargs) def update_command(self, app, **kwargs): @@ -79,6 +81,7 @@ def update_command(self, app, **kwargs): kwargs.pop("update_support", None) kwargs.pop("update_stub", None) kwargs.pop("test_mode", None) + kwargs.pop("debug_mode", None) return full_options({"update_state": app.app_name}, kwargs) diff --git a/tests/commands/build/test_call.py b/tests/commands/build/test_call.py index f70be9f2a..6171bdc67 100644 --- a/tests/commands/build/test_call.py +++ b/tests/commands/build/test_call.py @@ -30,7 +30,7 @@ def test_specific_app(build_command, first_app, second_app): # App tools are verified for app ("verify-app-tools", "first"), # Build the first app; no state - ("build", "first", {"test_mode": False}), + ("build", "first", {"test_mode": False, "debug_mode": None}), ] @@ -62,13 +62,17 @@ def test_multiple_apps(build_command, first_app, second_app): # App tools are verified for first app ("verify-app-tools", "first"), # Build the first app; no state - ("build", "first", {"test_mode": False}), + ("build", "first", {"test_mode": False, "debug_mode": None}), # App template is verified for second app ("verify-app-template", "second"), # App tools are verified for second app ("verify-app-tools", "second"), # Build the second apps; state from previous build. - ("build", "second", {"build_state": "first", "test_mode": False}), + ( + "build", + "second", + {"build_state": "first", "test_mode": False, "debug_mode": None}, + ), ] @@ -96,12 +100,16 @@ def test_non_existent(build_command, first_app_config, second_app): ("finalize-app-config", "first"), ("finalize-app-config", "second"), # First App doesn't exist, so it will be created, then built - ("create", "first", {"test_mode": False}), + ("create", "first", {"test_mode": False, "debug_mode": None}), # App template is verified for first app ("verify-app-template", "first"), # App tools are verified for first app ("verify-app-tools", "first"), - ("build", "first", {"create_state": "first", "test_mode": False}), + ( + "build", + "first", + {"create_state": "first", "test_mode": False, "debug_mode": None}, + ), # App template is verified for second app ("verify-app-template", "second"), # App tools are verified for second app @@ -110,7 +118,12 @@ def test_non_existent(build_command, first_app_config, second_app): ( "build", "second", - {"create_state": "first", "build_state": "first", "test_mode": False}, + { + "create_state": "first", + "build_state": "first", + "test_mode": False, + "debug_mode": None, + }, ), ] @@ -144,13 +157,17 @@ def test_unbuilt(build_command, first_app_unbuilt, second_app): # App tools are verified for first app ("verify-app-tools", "first"), # First App exists, but hasn't been built; it will be built. - ("build", "first", {"test_mode": False}), + ("build", "first", {"test_mode": False, "debug_mode": None}), # App template is verified for second app ("verify-app-template", "second"), # App tools are verified for second app ("verify-app-tools", "second"), # Second app has been built before; it will be built again. - ("build", "second", {"build_state": "first", "test_mode": False}), + ( + "build", + "second", + {"build_state": "first", "test_mode": False, "debug_mode": None}, + ), ] @@ -183,6 +200,7 @@ def test_update_app(build_command, first_app, second_app): "first", { "test_mode": False, + "debug_mode": None, "update_requirements": False, "update_resources": False, "update_support": False, @@ -193,7 +211,11 @@ def test_update_app(build_command, first_app, second_app): ("verify-app-template", "first"), # App tools are verified for first app ("verify-app-tools", "first"), - ("build", "first", {"update_state": "first", "test_mode": False}), + ( + "build", + "first", + {"update_state": "first", "test_mode": False, "debug_mode": None}, + ), # Update then build the second app ( "update", @@ -202,6 +224,7 @@ def test_update_app(build_command, first_app, second_app): "update_state": "first", "build_state": "first", "test_mode": False, + "debug_mode": None, "update_requirements": False, "update_resources": False, "update_support": False, @@ -215,7 +238,12 @@ def test_update_app(build_command, first_app, second_app): ( "build", "second", - {"update_state": "second", "build_state": "first", "test_mode": False}, + { + "update_state": "second", + "build_state": "first", + "test_mode": False, + "debug_mode": None, + }, ), ] @@ -249,6 +277,7 @@ def test_update_app_requirements(build_command, first_app, second_app): "first", { "test_mode": False, + "debug_mode": None, "update_requirements": True, "update_resources": False, "update_support": False, @@ -259,7 +288,11 @@ def test_update_app_requirements(build_command, first_app, second_app): ("verify-app-template", "first"), # App tools are verified for first app ("verify-app-tools", "first"), - ("build", "first", {"update_state": "first", "test_mode": False}), + ( + "build", + "first", + {"update_state": "first", "test_mode": False, "debug_mode": None}, + ), # Update then build the second app ( "update", @@ -268,6 +301,7 @@ def test_update_app_requirements(build_command, first_app, second_app): "update_state": "first", "build_state": "first", "test_mode": False, + "debug_mode": None, "update_requirements": True, "update_resources": False, "update_support": False, @@ -281,7 +315,12 @@ def test_update_app_requirements(build_command, first_app, second_app): ( "build", "second", - {"update_state": "second", "build_state": "first", "test_mode": False}, + { + "update_state": "second", + "build_state": "first", + "test_mode": False, + "debug_mode": None, + }, ), ] @@ -315,6 +354,7 @@ def test_update_app_support(build_command, first_app, second_app): "first", { "test_mode": False, + "debug_mode": None, "update_requirements": False, "update_resources": False, "update_support": True, @@ -325,7 +365,11 @@ def test_update_app_support(build_command, first_app, second_app): ("verify-app-template", "first"), # App tools are verified for first app ("verify-app-tools", "first"), - ("build", "first", {"update_state": "first", "test_mode": False}), + ( + "build", + "first", + {"update_state": "first", "test_mode": False, "debug_mode": None}, + ), # Update then build the second app ( "update", @@ -334,6 +378,7 @@ def test_update_app_support(build_command, first_app, second_app): "update_state": "first", "build_state": "first", "test_mode": False, + "debug_mode": None, "update_requirements": False, "update_resources": False, "update_support": True, @@ -347,7 +392,12 @@ def test_update_app_support(build_command, first_app, second_app): ( "build", "second", - {"update_state": "second", "build_state": "first", "test_mode": False}, + { + "update_state": "second", + "build_state": "first", + "test_mode": False, + "debug_mode": None, + }, ), ] @@ -381,6 +431,7 @@ def test_update_app_stub(build_command, first_app, second_app): "first", { "test_mode": False, + "debug_mode": None, "update_requirements": False, "update_resources": False, "update_support": False, @@ -391,7 +442,11 @@ def test_update_app_stub(build_command, first_app, second_app): ("verify-app-template", "first"), # App tools are verified for first app ("verify-app-tools", "first"), - ("build", "first", {"update_state": "first", "test_mode": False}), + ( + "build", + "first", + {"update_state": "first", "test_mode": False, "debug_mode": None}, + ), # Update then build the second app ( "update", @@ -400,6 +455,7 @@ def test_update_app_stub(build_command, first_app, second_app): "update_state": "first", "build_state": "first", "test_mode": False, + "debug_mode": None, "update_requirements": False, "update_resources": False, "update_support": False, @@ -413,7 +469,12 @@ def test_update_app_stub(build_command, first_app, second_app): ( "build", "second", - {"update_state": "second", "build_state": "first", "test_mode": False}, + { + "update_state": "second", + "build_state": "first", + "test_mode": False, + "debug_mode": None, + }, ), ] @@ -447,6 +508,7 @@ def test_update_app_resources(build_command, first_app, second_app): "first", { "test_mode": False, + "debug_mode": None, "update_requirements": False, "update_resources": True, "update_support": False, @@ -457,7 +519,11 @@ def test_update_app_resources(build_command, first_app, second_app): ("verify-app-template", "first"), # App tools are verified for first app ("verify-app-tools", "first"), - ("build", "first", {"update_state": "first", "test_mode": False}), + ( + "build", + "first", + {"update_state": "first", "test_mode": False, "debug_mode": None}, + ), # Update then build the second app ( "update", @@ -466,6 +532,7 @@ def test_update_app_resources(build_command, first_app, second_app): "update_state": "first", "build_state": "first", "test_mode": False, + "debug_mode": None, "update_requirements": False, "update_resources": True, "update_support": False, @@ -479,7 +546,12 @@ def test_update_app_resources(build_command, first_app, second_app): ( "build", "second", - {"update_state": "second", "build_state": "first", "test_mode": False}, + { + "update_state": "second", + "build_state": "first", + "test_mode": False, + "debug_mode": None, + }, ), ] @@ -508,12 +580,16 @@ def test_update_non_existent(build_command, first_app_config, second_app): ("finalize-app-config", "first"), ("finalize-app-config", "second"), # First App doesn't exist, so it will be created, then built - ("create", "first", {"test_mode": False}), + ("create", "first", {"test_mode": False, "debug_mode": None}), # App template is verified for first app ("verify-app-template", "first"), # App tools are verified for first app ("verify-app-tools", "first"), - ("build", "first", {"create_state": "first", "test_mode": False}), + ( + "build", + "first", + {"create_state": "first", "test_mode": False, "debug_mode": None}, + ), # Second app *does* exist, so it will be updated, then built ( "update", @@ -522,6 +598,7 @@ def test_update_non_existent(build_command, first_app_config, second_app): "create_state": "first", "build_state": "first", "test_mode": False, + "debug_mode": None, "update_requirements": False, "update_resources": False, "update_support": False, @@ -540,6 +617,7 @@ def test_update_non_existent(build_command, first_app_config, second_app): "build_state": "first", "update_state": "second", "test_mode": False, + "debug_mode": None, }, ), ] @@ -574,6 +652,7 @@ def test_update_unbuilt(build_command, first_app_unbuilt, second_app): "first", { "test_mode": False, + "debug_mode": None, "update_requirements": False, "update_resources": False, "update_support": False, @@ -584,7 +663,11 @@ def test_update_unbuilt(build_command, first_app_unbuilt, second_app): ("verify-app-template", "first"), # App tools are verified for first app ("verify-app-tools", "first"), - ("build", "first", {"update_state": "first", "test_mode": False}), + ( + "build", + "first", + {"update_state": "first", "test_mode": False, "debug_mode": None}, + ), # Second app has been built before; it will be built again. ( "update", @@ -593,6 +676,7 @@ def test_update_unbuilt(build_command, first_app_unbuilt, second_app): "update_state": "first", "build_state": "first", "test_mode": False, + "debug_mode": None, "update_requirements": False, "update_resources": False, "update_support": False, @@ -606,7 +690,12 @@ def test_update_unbuilt(build_command, first_app_unbuilt, second_app): ( "build", "second", - {"update_state": "second", "build_state": "first", "test_mode": False}, + { + "update_state": "second", + "build_state": "first", + "test_mode": False, + "debug_mode": None, + }, ), ] @@ -640,6 +729,7 @@ def test_build_test(build_command, first_app, second_app): "first", { "test_mode": True, + "debug_mode": None, "update_requirements": False, "update_resources": False, "update_support": False, @@ -650,7 +740,11 @@ def test_build_test(build_command, first_app, second_app): ("verify-app-template", "first"), # App tools are verified for first app ("verify-app-tools", "first"), - ("build", "first", {"update_state": "first", "test_mode": True}), + ( + "build", + "first", + {"update_state": "first", "test_mode": True, "debug_mode": None}, + ), # Update then build the second app ( "update", @@ -659,6 +753,7 @@ def test_build_test(build_command, first_app, second_app): "update_state": "first", "build_state": "first", "test_mode": True, + "debug_mode": None, "update_requirements": False, "update_resources": False, "update_support": False, @@ -672,7 +767,12 @@ def test_build_test(build_command, first_app, second_app): ( "build", "second", - {"update_state": "second", "build_state": "first", "test_mode": True}, + { + "update_state": "second", + "build_state": "first", + "test_mode": True, + "debug_mode": None, + }, ), ] @@ -706,7 +806,7 @@ def test_build_test_no_update(build_command, first_app, second_app): ("verify-app-template", "first"), # App tools are verified for first app ("verify-app-tools", "first"), - ("build", "first", {"test_mode": True}), + ("build", "first", {"test_mode": True, "debug_mode": None}), # No update of the second app # App template is verified for second app ("verify-app-template", "second"), @@ -715,7 +815,7 @@ def test_build_test_no_update(build_command, first_app, second_app): ( "build", "second", - {"build_state": "first", "test_mode": True}, + {"build_state": "first", "test_mode": True, "debug_mode": None}, ), ] @@ -750,6 +850,7 @@ def test_build_test_update_dependencies(build_command, first_app, second_app): "first", { "test_mode": True, + "debug_mode": None, "update_requirements": True, "update_resources": False, "update_support": False, @@ -760,7 +861,11 @@ def test_build_test_update_dependencies(build_command, first_app, second_app): ("verify-app-template", "first"), # App tools are verified for first app ("verify-app-tools", "first"), - ("build", "first", {"update_state": "first", "test_mode": True}), + ( + "build", + "first", + {"update_state": "first", "test_mode": True, "debug_mode": None}, + ), # Update then build the second app ( "update", @@ -769,6 +874,7 @@ def test_build_test_update_dependencies(build_command, first_app, second_app): "update_state": "first", "build_state": "first", "test_mode": True, + "debug_mode": None, "update_requirements": True, "update_resources": False, "update_support": False, @@ -782,7 +888,12 @@ def test_build_test_update_dependencies(build_command, first_app, second_app): ( "build", "second", - {"update_state": "second", "build_state": "first", "test_mode": True}, + { + "update_state": "second", + "build_state": "first", + "test_mode": True, + "debug_mode": None, + }, ), ] @@ -817,6 +928,7 @@ def test_build_test_update_resources(build_command, first_app, second_app): "first", { "test_mode": True, + "debug_mode": None, "update_requirements": False, "update_resources": True, "update_support": False, @@ -827,7 +939,11 @@ def test_build_test_update_resources(build_command, first_app, second_app): ("verify-app-template", "first"), # App tools are verified for first app ("verify-app-tools", "first"), - ("build", "first", {"update_state": "first", "test_mode": True}), + ( + "build", + "first", + {"update_state": "first", "test_mode": True, "debug_mode": None}, + ), # Update then build the second app ( "update", @@ -836,6 +952,7 @@ def test_build_test_update_resources(build_command, first_app, second_app): "update_state": "first", "build_state": "first", "test_mode": True, + "debug_mode": None, "update_requirements": False, "update_resources": True, "update_support": False, @@ -849,7 +966,12 @@ def test_build_test_update_resources(build_command, first_app, second_app): ( "build", "second", - {"update_state": "second", "build_state": "first", "test_mode": True}, + { + "update_state": "second", + "build_state": "first", + "test_mode": True, + "debug_mode": None, + }, ), ] @@ -884,6 +1006,7 @@ def test_build_test_update_support(build_command, first_app, second_app): "first", { "test_mode": True, + "debug_mode": None, "update_requirements": False, "update_resources": False, "update_support": True, @@ -894,7 +1017,11 @@ def test_build_test_update_support(build_command, first_app, second_app): ("verify-app-template", "first"), # App tools are verified for first app ("verify-app-tools", "first"), - ("build", "first", {"update_state": "first", "test_mode": True}), + ( + "build", + "first", + {"update_state": "first", "test_mode": True, "debug_mode": None}, + ), # Update then build the second app ( "update", @@ -903,6 +1030,7 @@ def test_build_test_update_support(build_command, first_app, second_app): "update_state": "first", "build_state": "first", "test_mode": True, + "debug_mode": None, "update_requirements": False, "update_resources": False, "update_support": True, @@ -916,7 +1044,12 @@ def test_build_test_update_support(build_command, first_app, second_app): ( "build", "second", - {"update_state": "second", "build_state": "first", "test_mode": True}, + { + "update_state": "second", + "build_state": "first", + "test_mode": True, + "debug_mode": None, + }, ), ] @@ -951,6 +1084,7 @@ def test_build_test_update_stub(build_command, first_app, second_app): "first", { "test_mode": True, + "debug_mode": None, "update_requirements": False, "update_resources": False, "update_support": False, @@ -961,7 +1095,11 @@ def test_build_test_update_stub(build_command, first_app, second_app): ("verify-app-template", "first"), # App tools are verified for first app ("verify-app-tools", "first"), - ("build", "first", {"update_state": "first", "test_mode": True}), + ( + "build", + "first", + {"update_state": "first", "test_mode": True, "debug_mode": None}, + ), # Update then build the second app ( "update", @@ -970,6 +1108,7 @@ def test_build_test_update_stub(build_command, first_app, second_app): "update_state": "first", "build_state": "first", "test_mode": True, + "debug_mode": None, "update_requirements": False, "update_resources": False, "update_support": False, @@ -983,7 +1122,12 @@ def test_build_test_update_stub(build_command, first_app, second_app): ( "build", "second", - {"update_state": "second", "build_state": "first", "test_mode": True}, + { + "update_state": "second", + "build_state": "first", + "test_mode": True, + "debug_mode": None, + }, ), ] @@ -1111,12 +1255,16 @@ def test_test_app_non_existent(build_command, first_app_config, second_app): ("finalize-app-config", "first"), ("finalize-app-config", "second"), # First App doesn't exist, so it will be created, then built - ("create", "first", {"test_mode": True}), + ("create", "first", {"test_mode": True, "debug_mode": None}), # App template is verified for first app ("verify-app-template", "first"), # App tools are verified for first app ("verify-app-tools", "first"), - ("build", "first", {"create_state": "first", "test_mode": True}), + ( + "build", + "first", + {"create_state": "first", "test_mode": True, "debug_mode": None}, + ), # Second app *does* exist, so it will be updated, then built ( "update", @@ -1125,6 +1273,7 @@ def test_test_app_non_existent(build_command, first_app_config, second_app): "create_state": "first", "build_state": "first", "test_mode": True, + "debug_mode": None, "update_requirements": False, "update_resources": False, "update_support": False, @@ -1143,6 +1292,7 @@ def test_test_app_non_existent(build_command, first_app_config, second_app): "build_state": "first", "update_state": "second", "test_mode": True, + "debug_mode": None, }, ), ] @@ -1178,6 +1328,7 @@ def test_test_app_unbuilt(build_command, first_app_unbuilt, second_app): "first", { "test_mode": True, + "debug_mode": None, "update_requirements": False, "update_resources": False, "update_support": False, @@ -1191,7 +1342,7 @@ def test_test_app_unbuilt(build_command, first_app_unbuilt, second_app): ( "build", "first", - {"update_state": "first", "test_mode": True}, + {"update_state": "first", "test_mode": True, "debug_mode": None}, ), # Second app has been built before; it will be built again. ( @@ -1201,6 +1352,7 @@ def test_test_app_unbuilt(build_command, first_app_unbuilt, second_app): "update_state": "first", "build_state": "first", "test_mode": True, + "debug_mode": None, "update_requirements": False, "update_resources": False, "update_support": False, @@ -1214,7 +1366,12 @@ def test_test_app_unbuilt(build_command, first_app_unbuilt, second_app): ( "build", "second", - {"update_state": "second", "build_state": "first", "test_mode": True}, + { + "update_state": "second", + "build_state": "first", + "test_mode": True, + "debug_mode": None, + }, ), ] @@ -1249,7 +1406,7 @@ def test_build_app_single(build_command, first_app, second_app, app_flags): # App tools are verified for first app ("verify-app-tools", "first"), # Build the first app - ("build", "first", {"test_mode": False}), + ("build", "first", {"test_mode": False, "debug_mode": None}), ] @@ -1328,6 +1485,7 @@ def test_build_app_all_flags(build_command, first_app, second_app): "first", { "test_mode": True, + "debug_mode": None, "update_requirements": True, "update_resources": True, "update_support": True, @@ -1339,5 +1497,9 @@ def test_build_app_all_flags(build_command, first_app, second_app): # App tools are verified for first app ("verify-app-tools", "first"), # First app is built in test mode - ("build", "first", {"update_state": "first", "test_mode": True}), + ( + "build", + "first", + {"update_state": "first", "test_mode": True, "debug_mode": None}, + ), ] diff --git a/tests/commands/create/conftest.py b/tests/commands/create/conftest.py index 2e1086a10..46102dfe0 100644 --- a/tests/commands/create/conftest.py +++ b/tests/commands/create/conftest.py @@ -86,7 +86,9 @@ def python_version_tag(self): return "3.X" # Define output format-specific template context. - def output_format_template_context(self, app: AppConfig, debug_mode: bool = False): + def output_format_template_context( + self, app: AppConfig, debug_mode: str | None = None + ): return {"output_format": "dummy"} # Handle platform-specific permissions. @@ -152,7 +154,7 @@ def verify_app_tools(self, app): # Override all the body methods of a CreateCommand # with versions that we can use to track actions performed. - def generate_app_template(self, app): + def generate_app_template(self, app, debug_mode=None): self.actions.append(("generate", app.app_name)) # A mock version of template generation. @@ -161,7 +163,7 @@ def generate_app_template(self, app): def install_app_support_package(self, app): self.actions.append(("support", app.app_name)) - def install_app_requirements(self, app, test_mode): + def install_app_requirements(self, app, test_mode, debug_mode): self.actions.append(("requirements", app.app_name, test_mode)) def install_app_code(self, app, test_mode): diff --git a/tests/commands/create/test_generate_app_template.py b/tests/commands/create/test_generate_app_template.py index 7b2abc6f0..d2df9ca9e 100644 --- a/tests/commands/create/test_generate_app_template.py +++ b/tests/commands/create/test_generate_app_template.py @@ -31,6 +31,7 @@ def full_context(): "sources": ["src/my_app"], "test_sources": None, "test_requires": None, + "debug_requires": None, "url": "https://example.com", "author": "First Last", "author_email": "first@example.com", diff --git a/tests/commands/create/test_install_app_requirements.py b/tests/commands/create/test_install_app_requirements.py index 8acf13fd6..c8fb21da7 100644 --- a/tests/commands/create/test_install_app_requirements.py +++ b/tests/commands/create/test_install_app_requirements.py @@ -80,7 +80,7 @@ def test_bad_path_index(create_command, myapp, bundle_path, app_requirements_pat BriefcaseCommandError, match=r"Application path index file does not define `app_requirements_path` or `app_packages_path`", ): - create_command.install_app_requirements(myapp, test_mode=False) + create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) # pip wasn't invoked create_command.tools[myapp].app_context.run.assert_not_called() @@ -102,7 +102,7 @@ def test_app_packages_no_requires( """If an app has no requirements, install_app_requirements is a no-op.""" myapp.requires = None - create_command.install_app_requirements(myapp, test_mode=False) + create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) # No request was made to install requirements create_command.tools[myapp].app_context.run.assert_not_called() @@ -117,7 +117,7 @@ def test_app_packages_empty_requires( """If an app has an empty requirements list, install_app_requirements is a no-op.""" myapp.requires = [] - create_command.install_app_requirements(myapp, test_mode=False) + create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) # No request was made to install requirements create_command.tools[myapp].app_context.run.assert_not_called() @@ -132,7 +132,7 @@ def test_app_packages_valid_requires( """If an app has a valid list of requirements, pip is invoked.""" myapp.requires = ["first", "second==1.2.3", "third>=3.2.1"] - create_command.install_app_requirements(myapp, test_mode=False) + create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) # A request was made to install requirements create_command.tools[myapp].app_context.run.assert_called_with( @@ -173,7 +173,7 @@ def test_app_packages_requirement_installer_args_no_paths( myapp.requirement_installer_args = ["--no-cache"] myapp.requires = ["package"] - create_command.install_app_requirements(myapp, test_mode=False) + create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) # A request was made to install requirements create_command.tools[myapp].app_context.run.assert_called_with( @@ -210,7 +210,7 @@ def test_app_packages_requirement_installer_args_path_transformed( myapp.requirement_installer_args = ["--extra-index-url", "./packages"] myapp.requires = ["package"] - create_command.install_app_requirements(myapp, test_mode=False) + create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) # A request was made to install requirements create_command.tools[myapp].app_context.run.assert_called_with( @@ -248,7 +248,7 @@ def test_app_packages_requirement_installer_args_coincidental_path_not_transform myapp.requirement_installer_args = ["-f./wheels"] myapp.requires = ["package"] - create_command.install_app_requirements(myapp, test_mode=False) + create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) # A request was made to install requirements create_command.tools[myapp].app_context.run.assert_called_with( @@ -285,7 +285,7 @@ def test_app_packages_requirement_installer_args_path_not_transformed( myapp.requirement_installer_args = ["--extra-index-url", "./packages"] myapp.requires = ["package"] - create_command.install_app_requirements(myapp, test_mode=False) + create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) # A request was made to install requirements create_command.tools[myapp].app_context.run.assert_called_with( @@ -323,7 +323,7 @@ def test_app_packages_requirement_installer_args_combined_argument_not_transform myapp.requirement_installer_args = ["--extra-index-url=./packages"] myapp.requires = ["package"] - create_command.install_app_requirements(myapp, test_mode=False) + create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) # A request was made to install requirements create_command.tools[myapp].app_context.run.assert_called_with( @@ -362,7 +362,7 @@ def test_app_packages_valid_requires_no_support_package( "paths": {"app_packages_path": "path/to/app_packages"} } - create_command.install_app_requirements(myapp, test_mode=False) + create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) # A request was made to install requirements create_command.tools[myapp].app_context.run.assert_called_with( @@ -410,7 +410,7 @@ def test_app_packages_invalid_requires( ) with pytest.raises(RequirementsInstallError): - create_command.install_app_requirements(myapp, test_mode=False) + create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) # But the request to install was still made create_command.tools[myapp].app_context.run.assert_called_with( @@ -456,7 +456,7 @@ def test_app_packages_offline( ) with pytest.raises(RequirementsInstallError): - create_command.install_app_requirements(myapp, test_mode=False) + create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) # But the request to install was still made create_command.tools[myapp].app_context.run.assert_called_with( @@ -506,7 +506,7 @@ def test_app_packages_install_requirements( ) # Install the requirements - create_command.install_app_requirements(myapp, test_mode=False) + create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) # The request to install was made create_command.tools[myapp].app_context.run.assert_called_with( @@ -561,7 +561,7 @@ def test_app_packages_replace_existing_requirements( ) # Install the requirements - create_command.install_app_requirements(myapp, test_mode=False) + create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) # The request to install was still made create_command.tools[myapp].app_context.run.assert_called_with( @@ -613,7 +613,7 @@ def test_app_requirements_no_requires( myapp.requires = None # Install requirements into the bundle - create_command.install_app_requirements(myapp, test_mode=False) + create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) # requirements.txt doesn't exist either assert app_requirements_path.exists() @@ -637,7 +637,7 @@ def test_app_requirements_empty_requires( myapp.requires = [] # Install requirements into the bundle - create_command.install_app_requirements(myapp, test_mode=False) + create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) # requirements.txt doesn't exist either assert app_requirements_path.exists() @@ -661,7 +661,7 @@ def test_app_requirements_requires( myapp.requires = ["first", "second==1.2.3", "third>=3.2.1"] # Install requirements into the bundle - create_command.install_app_requirements(myapp, test_mode=False) + create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) # requirements.txt doesn't exist either assert app_requirements_path.exists() @@ -688,7 +688,7 @@ def test_app_requirements_requirement_installer_args_no_template_support( myapp.requires = ["my-favourite-package"] # Install requirements into the bundle - create_command.install_app_requirements(myapp, test_mode=False) + create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) # requirements.txt exists either assert app_requirements_path.exists() @@ -715,7 +715,7 @@ def test_app_requirements_requirement_installer_args_with_template_support( myapp.requires = ["my-favourite-package"] # Install requirements into the bundle - create_command.install_app_requirements(myapp, test_mode=False) + create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) # requirements.txt exists either assert app_requirements_path.exists() @@ -749,7 +749,7 @@ def test_app_requirements_requirement_installer_args_without_requires_no_templat myapp.requires = [] # Install requirements into the bundle - create_command.install_app_requirements(myapp, test_mode=False) + create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) # requirements.txt exists either assert app_requirements_path.exists() @@ -779,7 +779,7 @@ def test_app_requirements_requirement_installer_args_without_requires_with_templ myapp.requires = [] # Install requirements into the bundle - create_command.install_app_requirements(myapp, test_mode=False) + create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) # requirements.txt exists either assert app_requirements_path.exists() @@ -833,7 +833,7 @@ def _test_app_requirements_paths( converted = requirement myapp.requires = ["first", requirement, "third"] - create_command.install_app_requirements(myapp, test_mode=False) + create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) with app_requirements_path.open(encoding="utf-8") as f: assert f.read() == ( "\n".join( @@ -972,7 +972,7 @@ def test_app_packages_test_requires( myapp.requires = ["first", "second==1.2.3", "third>=3.2.1"] myapp.test_requires = ["pytest", "pytest-tldr"] - create_command.install_app_requirements(myapp, test_mode=False) + create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) # A request was made to install requirements create_command.tools[myapp].app_context.run.assert_called_with( @@ -1011,7 +1011,7 @@ def test_app_packages_test_requires_test_mode( myapp.requires = ["first", "second==1.2.3", "third>=3.2.1"] myapp.test_requires = ["pytest", "pytest-tldr"] - create_command.install_app_requirements(myapp, test_mode=True) + create_command.install_app_requirements(myapp, test_mode=True, debug_mode=None) # A request was made to install requirements create_command.tools[myapp].app_context.run.assert_called_with( @@ -1053,7 +1053,7 @@ def test_app_packages_only_test_requires_test_mode( myapp.requires = None myapp.test_requires = ["pytest", "pytest-tldr"] - create_command.install_app_requirements(myapp, test_mode=True) + create_command.install_app_requirements(myapp, test_mode=True, debug_mode=None) # A request was made to install requirements create_command.tools[myapp].app_context.run.assert_called_with( diff --git a/tests/commands/run/conftest.py b/tests/commands/run/conftest.py index f08fe84ae..c1d1add85 100644 --- a/tests/commands/run/conftest.py +++ b/tests/commands/run/conftest.py @@ -93,6 +93,7 @@ def build_command(self, app, **kwargs): kwargs.pop("update_stub", None) kwargs.pop("no_update", None) kwargs.pop("test_mode", None) + kwargs.pop("debug_mode", None) return full_options({"build_state": app.app_name}, kwargs) diff --git a/tests/commands/run/test_call.py b/tests/commands/run/test_call.py index fce9130e9..cf06d1860 100644 --- a/tests/commands/run/test_call.py +++ b/tests/commands/run/test_call.py @@ -29,7 +29,17 @@ def test_no_args_one_app(run_command, first_app): # App tools are verified ("verify-app-tools", "first"), # Run the first app - ("run", "first", {"test_mode": False, "passthrough": []}), + ( + "run", + "first", + { + "test_mode": False, + "debug_mode": None, + "debugger_host": "localhost", + "debugger_port": 5678, + "passthrough": [], + }, + ), ] @@ -60,7 +70,17 @@ def test_no_args_one_app_with_passthrough(run_command, first_app): # App tools have been verified ("verify-app-tools", "first"), # Run the first app - ("run", "first", {"test_mode": False, "passthrough": ["foo", "--bar"]}), + ( + "run", + "first", + { + "test_mode": False, + "debug_mode": None, + "debugger_host": "localhost", + "debugger_port": 5678, + "passthrough": ["foo", "--bar"], + }, + ), ] @@ -109,7 +129,17 @@ def test_with_arg_one_app(run_command, first_app): # App tools are verified ("verify-app-tools", "first"), # Run the first app - ("run", "first", {"test_mode": False, "passthrough": []}), + ( + "run", + "first", + { + "test_mode": False, + "debug_mode": None, + "debugger_host": "localhost", + "debugger_port": 5678, + "passthrough": [], + }, + ), ] @@ -140,7 +170,17 @@ def test_with_arg_two_apps(run_command, first_app, second_app): # App tools have been verified ("verify-app-tools", "second"), # Run the second app - ("run", "second", {"test_mode": False, "passthrough": []}), + ( + "run", + "second", + { + "test_mode": False, + "debug_mode": None, + "debugger_host": "localhost", + "debugger_port": 5678, + "passthrough": [], + }, + ), ] @@ -192,6 +232,7 @@ def test_create_app_before_start(run_command, first_app_config): "first", { "test_mode": False, + "debug_mode": None, "update": False, "update_requirements": False, "update_resources": False, @@ -208,7 +249,14 @@ def test_create_app_before_start(run_command, first_app_config): ( "run", "first", - {"build_state": "first", "test_mode": False, "passthrough": []}, + { + "build_state": "first", + "test_mode": False, + "debug_mode": None, + "debugger_host": "localhost", + "debugger_port": 5678, + "passthrough": [], + }, ), ] @@ -240,6 +288,7 @@ def test_build_app_before_start(run_command, first_app_unbuilt): "first", { "test_mode": False, + "debug_mode": None, "update": False, "update_requirements": False, "update_resources": False, @@ -256,7 +305,14 @@ def test_build_app_before_start(run_command, first_app_unbuilt): ( "run", "first", - {"build_state": "first", "test_mode": False, "passthrough": []}, + { + "build_state": "first", + "test_mode": False, + "debug_mode": None, + "debugger_host": "localhost", + "debugger_port": 5678, + "passthrough": [], + }, ), ] @@ -288,6 +344,7 @@ def test_update_app(run_command, first_app): "first", { "test_mode": False, + "debug_mode": None, "update": True, "update_requirements": False, "update_resources": False, @@ -304,7 +361,14 @@ def test_update_app(run_command, first_app): ( "run", "first", - {"build_state": "first", "test_mode": False, "passthrough": []}, + { + "build_state": "first", + "test_mode": False, + "debug_mode": None, + "debugger_host": "localhost", + "debugger_port": 5678, + "passthrough": [], + }, ), ] @@ -336,6 +400,7 @@ def test_update_app_requirements(run_command, first_app): "first", { "test_mode": False, + "debug_mode": None, "update": False, "update_requirements": True, "update_resources": False, @@ -352,7 +417,14 @@ def test_update_app_requirements(run_command, first_app): ( "run", "first", - {"build_state": "first", "test_mode": False, "passthrough": []}, + { + "build_state": "first", + "test_mode": False, + "debug_mode": None, + "debugger_host": "localhost", + "debugger_port": 5678, + "passthrough": [], + }, ), ] @@ -384,6 +456,7 @@ def test_update_app_resources(run_command, first_app): "first", { "test_mode": False, + "debug_mode": None, "update": False, "update_requirements": False, "update_resources": True, @@ -400,7 +473,14 @@ def test_update_app_resources(run_command, first_app): ( "run", "first", - {"build_state": "first", "test_mode": False, "passthrough": []}, + { + "build_state": "first", + "test_mode": False, + "debug_mode": None, + "debugger_host": "localhost", + "debugger_port": 5678, + "passthrough": [], + }, ), ] @@ -432,6 +512,7 @@ def test_update_app_support(run_command, first_app): "first", { "test_mode": False, + "debug_mode": None, "update": False, "update_requirements": False, "update_resources": False, @@ -448,7 +529,14 @@ def test_update_app_support(run_command, first_app): ( "run", "first", - {"build_state": "first", "test_mode": False, "passthrough": []}, + { + "build_state": "first", + "test_mode": False, + "debug_mode": None, + "debugger_host": "localhost", + "debugger_port": 5678, + "passthrough": [], + }, ), ] @@ -480,6 +568,7 @@ def test_update_app_stub(run_command, first_app): "first", { "test_mode": False, + "debug_mode": None, "update": False, "update_requirements": False, "update_resources": False, @@ -496,7 +585,14 @@ def test_update_app_stub(run_command, first_app): ( "run", "first", - {"build_state": "first", "test_mode": False, "passthrough": []}, + { + "build_state": "first", + "test_mode": False, + "debug_mode": None, + "debugger_host": "localhost", + "debugger_port": 5678, + "passthrough": [], + }, ), ] @@ -529,6 +625,7 @@ def test_update_unbuilt_app(run_command, first_app_unbuilt): "first", { "test_mode": False, + "debug_mode": None, "update": True, "update_requirements": False, "update_resources": False, @@ -545,7 +642,14 @@ def test_update_unbuilt_app(run_command, first_app_unbuilt): ( "run", "first", - {"build_state": "first", "test_mode": False, "passthrough": []}, + { + "build_state": "first", + "test_mode": False, + "debug_mode": None, + "debugger_host": "localhost", + "debugger_port": 5678, + "passthrough": [], + }, ), ] @@ -578,6 +682,7 @@ def test_update_non_existent(run_command, first_app_config): "first", { "test_mode": False, + "debug_mode": None, "update": True, "update_requirements": False, "update_resources": False, @@ -594,7 +699,14 @@ def test_update_non_existent(run_command, first_app_config): ( "run", "first", - {"build_state": "first", "test_mode": False, "passthrough": []}, + { + "build_state": "first", + "test_mode": False, + "debug_mode": None, + "debugger_host": "localhost", + "debugger_port": 5678, + "passthrough": [], + }, ), ] @@ -626,6 +738,7 @@ def test_test_mode_existing_app(run_command, first_app): "first", { "test_mode": True, + "debug_mode": None, "update": False, "update_requirements": False, "update_resources": False, @@ -642,7 +755,14 @@ def test_test_mode_existing_app(run_command, first_app): ( "run", "first", - {"build_state": "first", "test_mode": True, "passthrough": []}, + { + "build_state": "first", + "test_mode": True, + "debug_mode": None, + "debugger_host": "localhost", + "debugger_port": 5678, + "passthrough": [], + }, ), ] @@ -674,6 +794,7 @@ def test_test_mode_existing_app_with_passthrough(run_command, first_app): "first", { "test_mode": True, + "debug_mode": None, "update": False, "update_requirements": False, "update_resources": False, @@ -693,6 +814,9 @@ def test_test_mode_existing_app_with_passthrough(run_command, first_app): { "build_state": "first", "test_mode": True, + "debug_mode": None, + "debugger_host": "localhost", + "debugger_port": 5678, "passthrough": ["foo", "--bar"], }, ), @@ -729,7 +853,13 @@ def test_test_mode_existing_app_no_update(run_command, first_app): ( "run", "first", - {"test_mode": True, "passthrough": []}, + { + "test_mode": True, + "debug_mode": None, + "debugger_host": "localhost", + "debugger_port": 5678, + "passthrough": [], + }, ), ] @@ -761,6 +891,7 @@ def test_test_mode_existing_app_update_requirements(run_command, first_app): "first", { "test_mode": True, + "debug_mode": None, "update": False, "update_requirements": True, "update_resources": False, @@ -777,7 +908,14 @@ def test_test_mode_existing_app_update_requirements(run_command, first_app): ( "run", "first", - {"build_state": "first", "test_mode": True, "passthrough": []}, + { + "build_state": "first", + "test_mode": True, + "debug_mode": None, + "debugger_host": "localhost", + "debugger_port": 5678, + "passthrough": [], + }, ), ] @@ -809,6 +947,7 @@ def test_test_mode_existing_app_update_resources(run_command, first_app): "first", { "test_mode": True, + "debug_mode": None, "update": False, "update_requirements": False, "update_resources": True, @@ -825,7 +964,14 @@ def test_test_mode_existing_app_update_resources(run_command, first_app): ( "run", "first", - {"build_state": "first", "test_mode": True, "passthrough": []}, + { + "build_state": "first", + "test_mode": True, + "debug_mode": None, + "debugger_host": "localhost", + "debugger_port": 5678, + "passthrough": [], + }, ), ] @@ -857,6 +1003,7 @@ def test_test_mode_update_existing_app(run_command, first_app): "first", { "test_mode": True, + "debug_mode": None, "update": True, "update_requirements": False, "update_resources": False, @@ -873,7 +1020,14 @@ def test_test_mode_update_existing_app(run_command, first_app): ( "run", "first", - {"build_state": "first", "test_mode": True, "passthrough": []}, + { + "build_state": "first", + "test_mode": True, + "debug_mode": None, + "debugger_host": "localhost", + "debugger_port": 5678, + "passthrough": [], + }, ), ] @@ -905,6 +1059,7 @@ def test_test_mode_non_existent(run_command, first_app_config): "first", { "test_mode": True, + "debug_mode": None, "update": False, "update_requirements": False, "update_resources": False, @@ -921,6 +1076,13 @@ def test_test_mode_non_existent(run_command, first_app_config): ( "run", "first", - {"build_state": "first", "test_mode": True, "passthrough": []}, + { + "build_state": "first", + "test_mode": True, + "debug_mode": None, + "debugger_host": "localhost", + "debugger_port": 5678, + "passthrough": [], + }, ), ] diff --git a/tests/commands/update/conftest.py b/tests/commands/update/conftest.py index aa6c5b766..84a47cf11 100644 --- a/tests/commands/update/conftest.py +++ b/tests/commands/update/conftest.py @@ -52,7 +52,7 @@ def verify_app_tools(self, app): # Override all the body methods of a UpdateCommand # with versions that we can use to track actions performed. - def install_app_requirements(self, app, test_mode): + def install_app_requirements(self, app, test_mode, debug_mode): self.actions.append(("requirements", app.app_name, test_mode)) create_file(self.bundle_path(app) / "requirements", "app requirements") diff --git a/tests/commands/update/test_update_app.py b/tests/commands/update/test_update_app.py index 111137147..ba5748587 100644 --- a/tests/commands/update/test_update_app.py +++ b/tests/commands/update/test_update_app.py @@ -7,6 +7,7 @@ def test_update_app(update_command, first_app, tmp_path): update_support=False, update_stub=False, test_mode=False, + debug_mode=None, ) # The right sequence of things will be done @@ -40,6 +41,7 @@ def test_update_non_existing_app(update_command, tmp_path): update_support=False, update_stub=False, test_mode=False, + debug_mode=None, ) # No app creation actions will be performed @@ -59,6 +61,7 @@ def test_update_app_with_requirements(update_command, first_app, tmp_path): update_support=False, update_stub=False, test_mode=False, + debug_mode=None, ) # The right sequence of things will be done @@ -92,6 +95,7 @@ def test_update_app_with_resources(update_command, first_app, tmp_path): update_support=False, update_stub=False, test_mode=False, + debug_mode=None, ) # The right sequence of things will be done @@ -125,6 +129,7 @@ def test_update_app_with_support_package(update_command, first_app, tmp_path): update_support=True, update_stub=False, test_mode=False, + debug_mode=None, ) # The right sequence of things will be done @@ -164,6 +169,7 @@ def test_update_app_with_stub(update_command, first_app, tmp_path): update_support=False, update_stub=True, test_mode=False, + debug_mode=None, ) # The right sequence of things will be done @@ -199,6 +205,7 @@ def test_update_app_stub_without_stub(update_command, first_app, tmp_path): update_support=False, update_stub=True, test_mode=False, + debug_mode=None, ) # The right sequence of things will be done @@ -232,6 +239,7 @@ def test_update_app_test_mode(update_command, first_app, tmp_path): update_resources=False, update_support=False, update_stub=False, + debug_mode=None, ) # The right sequence of things will be done @@ -265,6 +273,7 @@ def test_update_app_test_mode_requirements(update_command, first_app, tmp_path): update_resources=False, update_support=False, update_stub=False, + debug_mode=None, ) # The right sequence of things will be done @@ -299,6 +308,7 @@ def test_update_app_test_mode_resources(update_command, first_app, tmp_path): update_resources=True, update_support=False, update_stub=False, + debug_mode=None, ) # The right sequence of things will be done diff --git a/tests/integrations/android_sdk/ADB/test_start_app.py b/tests/integrations/android_sdk/ADB/test_start_app.py index 17788a0dc..a3601f403 100644 --- a/tests/integrations/android_sdk/ADB/test_start_app.py +++ b/tests/integrations/android_sdk/ADB/test_start_app.py @@ -30,7 +30,7 @@ def test_start_app_launches_app(adb, capsys, passthrough): # Invoke start_app adb.start_app( - "com.example.sample.package", "com.example.sample.activity", passthrough + "com.example.sample.package", "com.example.sample.activity", passthrough, {} ) # Validate call parameters. @@ -69,7 +69,9 @@ def test_missing_activity(adb): ) with pytest.raises(BriefcaseCommandError) as exc_info: - adb.start_app("com.example.sample.package", "com.example.sample.activity", []) + adb.start_app( + "com.example.sample.package", "com.example.sample.activity", [], {} + ) assert "Activity class not found" in str(exc_info.value) @@ -81,7 +83,9 @@ def test_invalid_device(adb): adb.run = MagicMock(side_effect=InvalidDeviceError("device", "exampleDevice")) with pytest.raises(InvalidDeviceError): - adb.start_app("com.example.sample.package", "com.example.sample.activity", []) + adb.start_app( + "com.example.sample.package", "com.example.sample.activity", [], {} + ) def test_unable_to_start(adb): @@ -92,4 +96,6 @@ def test_unable_to_start(adb): BriefcaseCommandError, match=r"Unable to start com.example.sample.package/com.example.sample.activity on exampleDevice", ): - adb.start_app("com.example.sample.package", "com.example.sample.activity", []) + adb.start_app( + "com.example.sample.package", "com.example.sample.activity", [], {} + ) diff --git a/tests/integrations/flatpak/test_Flatpak__run.py b/tests/integrations/flatpak/test_Flatpak__run.py index f67621618..3a216db3c 100644 --- a/tests/integrations/flatpak/test_Flatpak__run.py +++ b/tests/integrations/flatpak/test_Flatpak__run.py @@ -18,7 +18,10 @@ def test_run(flatpak, tool_debug_mode): flatpak.tools.subprocess.Popen.return_value = log_popen # Call run() - result = flatpak.run(bundle_identifier="com.example.my-app") + result = flatpak.run( + bundle_identifier="com.example.my-app", + **({"env": {"BRIEFCASE_DEBUG": "1"}} if tool_debug_mode else {}), + ) # The expected call was made flatpak.tools.subprocess.Popen.assert_called_once_with( @@ -53,6 +56,7 @@ def test_run_with_args(flatpak, tool_debug_mode): result = flatpak.run( bundle_identifier="com.example.my-app", args=["foo", "bar"], + **({"env": {"BRIEFCASE_DEBUG": "1"}} if tool_debug_mode else {}), ) # The expected call was made @@ -86,6 +90,7 @@ def test_run_non_streaming(flatpak, tool_debug_mode): bundle_identifier="com.example.my-app", args=["foo", "bar"], stream_output=False, + **({"env": {"BRIEFCASE_DEBUG": "1"}} if tool_debug_mode else {}), ) # The expected call was made @@ -117,7 +122,11 @@ def test_main_module_override(flatpak, tool_debug_mode): # Call run() result = flatpak.run( bundle_identifier="com.example.my-app", - main_module="org.beeware.test-case", + env=( + {"BRIEFCASE_MAIN_MODULE": "org.beeware.test-case", "BRIEFCASE_DEBUG": "1"} + if tool_debug_mode + else {"BRIEFCASE_MAIN_MODULE": "org.beeware.test-case"} + ), ) # The expected call was made diff --git a/tests/platforms/android/gradle/test_run.py b/tests/platforms/android/gradle/test_run.py index 9b5346301..bac2b91c9 100644 --- a/tests/platforms/android/gradle/test_run.py +++ b/tests/platforms/android/gradle/test_run.py @@ -90,6 +90,9 @@ def test_device_option(run_command): "update_stub": False, "no_update": False, "test_mode": False, + "debug_mode": None, + "debugger_host": "localhost", + "debugger_port": 5678, "passthrough": [], "extra_emulator_args": None, "shutdown_on_exit": False, @@ -113,6 +116,9 @@ def test_extra_emulator_args_option(run_command): "update_stub": False, "no_update": False, "test_mode": False, + "debug_mode": None, + "debugger_host": "localhost", + "debugger_port": 5678, "passthrough": [], "extra_emulator_args": ["-no-window", "-no-audio"], "shutdown_on_exit": False, @@ -134,6 +140,9 @@ def test_shutdown_on_exit_option(run_command): "update_stub": False, "no_update": False, "test_mode": False, + "debug_mode": None, + "debugger_host": "localhost", + "debugger_port": 5678, "passthrough": [], "extra_emulator_args": None, "shutdown_on_exit": True, @@ -193,6 +202,9 @@ def mock_stream_output(app, stop_func, **kwargs): first_app_config, device_or_avd="exampleDevice", test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, passthrough=[], ) @@ -216,6 +228,7 @@ def mock_stream_output(app, stop_func, **kwargs): f"{first_app_config.package_name}.{first_app_config.module_name}", "org.beeware.android.MainActivity", [], + {}, ) run_command.tools.mock_adb.pidof.assert_called_once_with( @@ -265,6 +278,9 @@ def mock_stream_output(app, stop_func, **kwargs): first_app_config, device_or_avd="exampleDevice", test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, passthrough=["foo", "--bar"], ) @@ -288,6 +304,7 @@ def mock_stream_output(app, stop_func, **kwargs): f"{first_app_config.package_name}.{first_app_config.module_name}", "org.beeware.android.MainActivity", ["foo", "--bar"], + {}, ) run_command.tools.mock_adb.pidof.assert_called_once_with( @@ -328,6 +345,9 @@ def test_run_slow_start(run_command, first_app_config, monkeypatch): first_app_config, device_or_avd="exampleDevice", test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, passthrough=[], ) @@ -382,6 +402,9 @@ def test_run_crash_at_start(run_command, first_app_config, monkeypatch): first_app_config, device_or_avd="exampleDevice", test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, passthrough=[], ) @@ -419,7 +442,14 @@ def test_run_created_emulator(run_command, first_app_config): run_command.tools.mock_adb.logcat.return_value = log_popen # Invoke run_app - run_command.run_app(first_app_config, test_mode=False, passthrough=[]) + run_command.run_app( + first_app_config, + test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # A new emulator was created run_command.tools.android_sdk.create_emulator.assert_called_once_with() @@ -448,6 +478,7 @@ def test_run_created_emulator(run_command, first_app_config): f"{first_app_config.package_name}.{first_app_config.module_name}", "org.beeware.android.MainActivity", [], + {}, ) run_command.tools.mock_adb.logcat.assert_called_once_with(pid="777") @@ -482,7 +513,14 @@ def test_run_idle_device(run_command, first_app_config): run_command.tools.mock_adb.logcat.return_value = log_popen # Invoke run_app - run_command.run_app(first_app_config, test_mode=False, passthrough=[]) + run_command.run_app( + first_app_config, + test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # No attempt was made to create a new emulator run_command.tools.android_sdk.create_emulator.assert_not_called() @@ -510,6 +548,7 @@ def test_run_idle_device(run_command, first_app_config): f"{first_app_config.package_name}.{first_app_config.module_name}", "org.beeware.android.MainActivity", [], + {}, ) run_command.tools.mock_adb.logcat.assert_called_once_with(pid="777") @@ -587,6 +626,9 @@ def mock_stream_output(app, stop_func, **kwargs): first_app_config, device_or_avd="exampleDevice", test_mode=True, + debug_mode=None, + debugger_host=None, + debugger_port=None, passthrough=[], shutdown_on_exit=True, ) @@ -611,6 +653,7 @@ def mock_stream_output(app, stop_func, **kwargs): f"{first_app_config.package_name}.{first_app_config.module_name}", "org.beeware.android.MainActivity", [], + {}, ) run_command.tools.mock_adb.pidof.assert_called_once_with( @@ -660,6 +703,9 @@ def mock_stream_output(app, stop_func, **kwargs): first_app_config, device_or_avd="exampleDevice", test_mode=True, + debug_mode=None, + debugger_host=None, + debugger_port=None, passthrough=["foo", "--bar"], shutdown_on_exit=True, ) @@ -684,6 +730,7 @@ def mock_stream_output(app, stop_func, **kwargs): f"{first_app_config.package_name}.{first_app_config.module_name}", "org.beeware.android.MainActivity", ["foo", "--bar"], + {}, ) run_command.tools.mock_adb.pidof.assert_called_once_with( @@ -728,6 +775,9 @@ def test_run_test_mode_created_emulator(run_command, first_app_config): run_command.run_app( first_app_config, test_mode=True, + debug_mode=None, + debugger_host=None, + debugger_port=None, passthrough=[], extra_emulator_args=["-no-window", "-no-audio"], shutdown_on_exit=True, @@ -761,6 +811,7 @@ def test_run_test_mode_created_emulator(run_command, first_app_config): f"{first_app_config.package_name}.{first_app_config.module_name}", "org.beeware.android.MainActivity", [], + {}, ) run_command.tools.mock_adb.logcat.assert_called_once_with(pid="777") diff --git a/tests/platforms/iOS/xcode/test_create.py b/tests/platforms/iOS/xcode/test_create.py index 0989a4785..3665a7712 100644 --- a/tests/platforms/iOS/xcode/test_create.py +++ b/tests/platforms/iOS/xcode/test_create.py @@ -72,7 +72,9 @@ def test_extra_pip_args( spec_set=Subprocess ) - create_command.install_app_requirements(first_app_generated, test_mode=False) + create_command.install_app_requirements( + first_app_generated, test_mode=False, debug_mode=None + ) bundle_path = tmp_path / "base_path/build/first-app/ios/xcode" assert create_command.tools[first_app_generated].app_context.run.mock_calls == [ @@ -155,7 +157,9 @@ def test_min_os_version(create_command, first_app_generated, tmp_path): spec_set=Subprocess ) - create_command.install_app_requirements(first_app_generated, test_mode=False) + create_command.install_app_requirements( + first_app_generated, test_mode=False, debug_mode=None + ) bundle_path = tmp_path / "base_path/build/first-app/ios/xcode" assert create_command.tools[first_app_generated].app_context.run.mock_calls == [ @@ -244,7 +248,9 @@ def test_incompatible_min_os_version(create_command, first_app_generated, tmp_pa r"but the support package only supports 12.0" ), ): - create_command.install_app_requirements(first_app_generated, test_mode=False) + create_command.install_app_requirements( + first_app_generated, test_mode=False, debug_mode=None + ) create_command.tools[first_app_generated].app_context.run.assert_not_called() diff --git a/tests/platforms/iOS/xcode/test_run.py b/tests/platforms/iOS/xcode/test_run.py index 1fd150be7..89260e113 100644 --- a/tests/platforms/iOS/xcode/test_run.py +++ b/tests/platforms/iOS/xcode/test_run.py @@ -46,6 +46,9 @@ def test_device_option(run_command): "update_stub": False, "no_update": False, "test_mode": False, + "debug_mode": None, + "debugger_host": "localhost", + "debugger_port": 5678, "passthrough": [], "appname": None, } @@ -72,7 +75,14 @@ def test_run_multiple_devices_input_disabled(run_command, first_app_config): BriefcaseCommandError, match=r"Input has been disabled; can't select a device to target.", ): - run_command.run_app(first_app_config, test_mode=False, passthrough=[]) + run_command.run_app( + first_app_config, + test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) @pytest.mark.usefixtures("sleep_zero") @@ -106,7 +116,14 @@ def test_run_app_simulator_booted(run_command, first_app_config, tmp_path): ] # Run the app - run_command.run_app(first_app_config, test_mode=False, passthrough=[]) + run_command.run_app( + first_app_config, + test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # The correct sequence of commands was issued. run_command.tools.subprocess.run.assert_has_calls( @@ -238,7 +255,14 @@ def test_run_app_simulator_booted_underscore( ] # Run the app - run_command.run_app(underscore_app_config, test_mode=False, passthrough=[]) + run_command.run_app( + underscore_app_config, + test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # slept 4 times for uninstall/install and 1 time for log stream start assert time.sleep.call_count == 4 + 1 @@ -365,6 +389,9 @@ def test_run_app_with_passthrough(run_command, first_app_config, tmp_path): run_command.run_app( first_app_config, test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, passthrough=["foo", "--bar"], ) @@ -498,7 +525,14 @@ def test_run_app_simulator_shut_down( ] # Run the app - run_command.run_app(first_app_config, test_mode=False, passthrough=[]) + run_command.run_app( + first_app_config, + test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # slept 4 times for uninstall/install and 1 time for log stream start assert time.sleep.call_count == 4 + 1 @@ -640,7 +674,14 @@ def test_run_app_simulator_shutting_down(run_command, first_app_config, tmp_path ] # Run the app - run_command.run_app(first_app_config, test_mode=False, passthrough=[]) + run_command.run_app( + first_app_config, + test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # We should have slept 4 times for shutting down and 4 time for uninstall/install assert time.sleep.call_count == 4 + 4 @@ -757,7 +798,14 @@ def test_run_app_simulator_boot_failure(run_command, first_app_config): # Run the app with pytest.raises(BriefcaseCommandError): - run_command.run_app(first_app_config, test_mode=False, passthrough=[]) + run_command.run_app( + first_app_config, + test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # No sleeps assert time.sleep.call_count == 0 @@ -801,7 +849,14 @@ def test_run_app_simulator_open_failure(run_command, first_app_config): # Run the app with pytest.raises(BriefcaseCommandError): - run_command.run_app(first_app_config, test_mode=False, passthrough=[]) + run_command.run_app( + first_app_config, + test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # No sleeps assert time.sleep.call_count == 0 @@ -852,7 +907,14 @@ def test_run_app_simulator_uninstall_failure(run_command, first_app_config): # Run the app with pytest.raises(BriefcaseCommandError): - run_command.run_app(first_app_config, test_mode=False, passthrough=[]) + run_command.run_app( + first_app_config, + test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # Sleep twice for uninstall failure assert time.sleep.call_count == 2 @@ -924,7 +986,14 @@ def test_run_app_simulator_install_failure(run_command, first_app_config, tmp_pa # Run the app with pytest.raises(BriefcaseCommandError): - run_command.run_app(first_app_config, test_mode=False, passthrough=[]) + run_command.run_app( + first_app_config, + test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # Sleep twice for uninstall and twice for install failure assert time.sleep.call_count == 4 @@ -1017,7 +1086,14 @@ def test_run_app_simulator_launch_failure(run_command, first_app_config, tmp_pat # Run the app with pytest.raises(BriefcaseCommandError): - run_command.run_app(first_app_config, test_mode=False, passthrough=[]) + run_command.run_app( + first_app_config, + test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # Sleep four times for uninstall/install and once for log stream start assert time.sleep.call_count == 4 + 1 @@ -1138,7 +1214,14 @@ def test_run_app_simulator_no_pid(run_command, first_app_config, tmp_path): # Run the app with pytest.raises(BriefcaseCommandError): - run_command.run_app(first_app_config, test_mode=False, passthrough=[]) + run_command.run_app( + first_app_config, + test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # Sleep four times for uninstall/install and once for log stream start assert time.sleep.call_count == 4 + 1 @@ -1261,7 +1344,14 @@ def test_run_app_simulator_non_integer_pid(run_command, first_app_config, tmp_pa # Run the app with pytest.raises(BriefcaseCommandError): - run_command.run_app(first_app_config, test_mode=False, passthrough=[]) + run_command.run_app( + first_app_config, + test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # Sleep four times for uninstall/install and once for log stream start assert time.sleep.call_count == 4 + 1 @@ -1383,7 +1473,14 @@ def test_run_app_test_mode(run_command, first_app_config, tmp_path): ] # Run the app - run_command.run_app(first_app_config, test_mode=True, passthrough=[]) + run_command.run_app( + first_app_config, + test_mode=True, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # Sleep four times for uninstall/install and once for log stream start assert time.sleep.call_count == 4 + 1 @@ -1496,6 +1593,9 @@ def test_run_app_test_mode_with_passthrough(run_command, first_app_config, tmp_p run_command.run_app( first_app_config, test_mode=True, + debug_mode=None, + debugger_host=None, + debugger_port=None, passthrough=["foo", "--bar"], ) diff --git a/tests/platforms/iOS/xcode/test_update.py b/tests/platforms/iOS/xcode/test_update.py index ce2daf5c6..97155eafb 100644 --- a/tests/platforms/iOS/xcode/test_update.py +++ b/tests/platforms/iOS/xcode/test_update.py @@ -59,7 +59,9 @@ def test_extra_pip_args( spec_set=Subprocess ) - update_command.install_app_requirements(first_app_generated, test_mode=False) + update_command.install_app_requirements( + first_app_generated, test_mode=False, debug_mode=None + ) bundle_path = tmp_path / "base_path/build/first-app/ios/xcode" assert update_command.tools[first_app_generated].app_context.run.mock_calls == [ diff --git a/tests/platforms/linux/appimage/test_run.py b/tests/platforms/linux/appimage/test_run.py index e4e1eb454..5e7aa8996 100644 --- a/tests/platforms/linux/appimage/test_run.py +++ b/tests/platforms/linux/appimage/test_run.py @@ -49,7 +49,14 @@ def test_run_gui_app(run_command, first_app_config, tmp_path): run_command.tools.subprocess.Popen.return_value = log_popen # Run the app - run_command.run_app(first_app_config, test_mode=False, passthrough=[]) + run_command.run_app( + first_app_config, + test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # The process was started run_command.tools.subprocess.Popen.assert_called_with( @@ -84,6 +91,9 @@ def test_run_gui_app_with_passthrough(run_command, first_app_config, tmp_path): run_command.run_app( first_app_config, test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, passthrough=["foo", "--bar"], ) @@ -116,7 +126,14 @@ def test_run_gui_app_failed(run_command, first_app_config, tmp_path): run_command.tools.subprocess.Popen.side_effect = OSError with pytest.raises(OSError): - run_command.run_app(first_app_config, test_mode=False, passthrough=[]) + run_command.run_app( + first_app_config, + test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # The run command was still invoked run_command.tools.subprocess.Popen.assert_called_with( @@ -139,7 +156,14 @@ def test_run_console_app(run_command, first_app_config, tmp_path): first_app_config.console_app = True # Run the app - run_command.run_app(first_app_config, test_mode=False, passthrough=[]) + run_command.run_app( + first_app_config, + test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # The process was started run_command.tools.subprocess.run.assert_called_with( @@ -166,6 +190,9 @@ def test_run_console_app_with_passthrough(run_command, first_app_config, tmp_pat run_command.run_app( first_app_config, test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, passthrough=["foo", "--bar"], ) @@ -194,7 +221,14 @@ def test_run_console_app_failed(run_command, first_app_config, tmp_path): run_command.tools.subprocess.run.side_effect = OSError with pytest.raises(OSError): - run_command.run_app(first_app_config, test_mode=False, passthrough=[]) + run_command.run_app( + first_app_config, + test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # The run command was still invoked run_command.tools.subprocess.run.assert_called_with( @@ -222,7 +256,14 @@ def test_run_app_test_mode(run_command, first_app_config, is_console_app, tmp_pa run_command.tools.subprocess.Popen.return_value = log_popen # Run the app - run_command.run_app(first_app_config, test_mode=True, passthrough=[]) + run_command.run_app( + first_app_config, + test_mode=True, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # The process was started run_command.tools.subprocess.Popen.assert_called_with( @@ -265,6 +306,9 @@ def test_run_app_test_mode_with_args( run_command.run_app( first_app_config, test_mode=True, + debug_mode=None, + debugger_host=None, + debugger_port=None, passthrough=["foo", "--bar"], ) diff --git a/tests/platforms/linux/flatpak/test_run.py b/tests/platforms/linux/flatpak/test_run.py index 9b59389f5..b45a0269b 100644 --- a/tests/platforms/linux/flatpak/test_run.py +++ b/tests/platforms/linux/flatpak/test_run.py @@ -30,7 +30,14 @@ def test_run_gui_app(run_command, first_app_config): run_command.tools.flatpak.run.return_value = log_popen # Run the app - run_command.run_app(first_app_config, test_mode=False, passthrough=[]) + run_command.run_app( + first_app_config, + test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # App is executed run_command.tools.flatpak.run.assert_called_once_with( @@ -60,6 +67,9 @@ def test_run_gui_app_with_passthrough(run_command, first_app_config): run_command.run_app( first_app_config, test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, passthrough=["foo", "--bar"], ) @@ -68,6 +78,7 @@ def test_run_gui_app_with_passthrough(run_command, first_app_config): bundle_identifier="com.example.first-app", args=["foo", "--bar"], stream_output=True, + env={"BRIEFCASE_DEBUG": "1"}, ) # The streamer was started @@ -84,7 +95,14 @@ def test_run_gui_app_failed(run_command, first_app_config, tmp_path): run_command.tools.flatpak.run.side_effect = OSError with pytest.raises(OSError): - run_command.run_app(first_app_config, test_mode=False, passthrough=[]) + run_command.run_app( + first_app_config, + test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # The run command was still invoked run_command.tools.flatpak.run.assert_called_once_with( @@ -102,7 +120,14 @@ def test_run_console_app(run_command, first_app_config): first_app_config.console_app = True # Run the app - run_command.run_app(first_app_config, test_mode=False, passthrough=[]) + run_command.run_app( + first_app_config, + test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # App is executed run_command.tools.flatpak.run.assert_called_once_with( @@ -124,6 +149,9 @@ def test_run_console_app_with_passthrough(run_command, first_app_config): run_command.run_app( first_app_config, test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, passthrough=["foo", "--bar"], ) @@ -132,6 +160,7 @@ def test_run_console_app_with_passthrough(run_command, first_app_config): bundle_identifier="com.example.first-app", args=["foo", "--bar"], stream_output=False, + env={"BRIEFCASE_DEBUG": "1"}, ) # No attempt to stream was made @@ -145,7 +174,14 @@ def test_run_console_app_failed(run_command, first_app_config, tmp_path): run_command.tools.flatpak.run.side_effect = OSError with pytest.raises(OSError): - run_command.run_app(first_app_config, test_mode=False, passthrough=[]) + run_command.run_app( + first_app_config, + test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # The run command was still invoked run_command.tools.flatpak.run.assert_called_once_with( @@ -169,14 +205,21 @@ def test_run_test_mode(run_command, first_app_config, is_console_app): run_command.tools.flatpak.run.return_value = log_popen # Run the app - run_command.run_app(first_app_config, test_mode=True, passthrough=[]) + run_command.run_app( + first_app_config, + test_mode=True, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # App is executed run_command.tools.flatpak.run.assert_called_once_with( bundle_identifier="com.example.first-app", args=[], - main_module="tests.first_app", stream_output=True, + env={"BRIEFCASE_MAIN_MODULE": "tests.first_app"}, ) # The streamer was started @@ -202,6 +245,9 @@ def test_run_test_mode_with_args(run_command, first_app_config, is_console_app): run_command.run_app( first_app_config, test_mode=True, + debug_mode=None, + debugger_host=None, + debugger_port=None, passthrough=["foo", "--bar"], ) @@ -209,8 +255,8 @@ def test_run_test_mode_with_args(run_command, first_app_config, is_console_app): run_command.tools.flatpak.run.assert_called_once_with( bundle_identifier="com.example.first-app", args=["foo", "--bar"], - main_module="tests.first_app", stream_output=True, + env={"BRIEFCASE_MAIN_MODULE": "tests.first_app"}, ) # The streamer was started diff --git a/tests/platforms/linux/system/test_run.py b/tests/platforms/linux/system/test_run.py index 829954709..16b557966 100644 --- a/tests/platforms/linux/system/test_run.py +++ b/tests/platforms/linux/system/test_run.py @@ -245,7 +245,14 @@ def test_run_gui_app(run_command, first_app, sub_kw, tmp_path): ) # Run the app - run_command.run_app(first_app, test_mode=False, passthrough=[]) + run_command.run_app( + first_app, + test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # The process was started run_command.tools.subprocess._subprocess.Popen.assert_called_with( @@ -285,7 +292,14 @@ def test_run_gui_app_passthrough(run_command, first_app, sub_kw, tmp_path): ) # Run the app - run_command.run_app(first_app, test_mode=False, passthrough=["foo", "--bar"]) + run_command.run_app( + first_app, + test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=["foo", "--bar"], + ) # The process was started run_command.tools.subprocess._subprocess.Popen.assert_called_with( @@ -327,7 +341,14 @@ def test_run_gui_app_failed(run_command, first_app, sub_kw, tmp_path): run_command.tools.subprocess._subprocess.Popen.side_effect = OSError with pytest.raises(OSError): - run_command.run_app(first_app, test_mode=False, passthrough=[]) + run_command.run_app( + first_app, + test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # The run command was still invoked run_command.tools.subprocess._subprocess.Popen.assert_called_with( @@ -356,7 +377,14 @@ def test_run_console_app(run_command, first_app, tmp_path): run_command.verify_app_tools(app=first_app) # Run the app - run_command.run_app(first_app, test_mode=False, passthrough=[]) + run_command.run_app( + first_app, + test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # The process was started run_command.tools.subprocess.run.mock_calls == [ @@ -385,7 +413,14 @@ def test_run_console_app_passthrough(run_command, first_app, tmp_path): run_command.verify_app_tools(app=first_app) # Run the app - run_command.run_app(first_app, test_mode=False, passthrough=["foo", "--bar"]) + run_command.run_app( + first_app, + test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=["foo", "--bar"], + ) # The process was started run_command.tools.subprocess.run.mock_calls == [ @@ -417,7 +452,14 @@ def test_run_console_app_failed(run_command, first_app, sub_kw, tmp_path): run_command.tools.subprocess.run.side_effect = OSError with pytest.raises(OSError): - run_command.run_app(first_app, test_mode=False, passthrough=[]) + run_command.run_app( + first_app, + test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # The run command was still invoked run_command.tools.subprocess.run.mock_calls == [ @@ -459,7 +501,14 @@ def test_run_app_docker(run_command, first_app, sub_kw, tmp_path, monkeypatch): ) # Run the app - run_command.run_app(first_app, test_mode=False, passthrough=[]) + run_command.run_app( + first_app, + test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # The process was started run_command.tools.subprocess._subprocess.Popen.assert_called_with( @@ -520,7 +569,14 @@ def test_run_app_failed_docker(run_command, first_app, sub_kw, tmp_path, monkeyp run_command.tools.subprocess._subprocess.Popen.side_effect = OSError with pytest.raises(OSError): - run_command.run_app(first_app, test_mode=False, passthrough=[]) + run_command.run_app( + first_app, + test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # The run command was still invoked run_command.tools.subprocess._subprocess.Popen.assert_called_with( @@ -580,7 +636,14 @@ def test_run_app_test_mode( monkeypatch.setattr(run_command.tools.os, "environ", {"ENVVAR": "Value"}) # Run the app - run_command.run_app(first_app, test_mode=True, passthrough=[]) + run_command.run_app( + first_app, + test_mode=True, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # The process was started run_command.tools.subprocess._subprocess.Popen.assert_called_with( @@ -639,7 +702,14 @@ def test_run_app_test_mode_docker( ) # Run the app - run_command.run_app(first_app, test_mode=True, passthrough=[]) + run_command.run_app( + first_app, + test_mode=True, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # The process was started run_command.tools.subprocess._subprocess.Popen.assert_called_with( @@ -709,6 +779,9 @@ def test_run_app_test_mode_with_args( run_command.run_app( first_app, test_mode=True, + debug_mode=None, + debugger_host=None, + debugger_port=None, passthrough=["foo", "--bar"], ) @@ -774,6 +847,9 @@ def test_run_app_test_mode_with_args_docker( run_command.run_app( first_app, test_mode=True, + debug_mode=None, + debugger_host=None, + debugger_port=None, passthrough=["foo", "--bar"], ) diff --git a/tests/platforms/macOS/app/test_create.py b/tests/platforms/macOS/app/test_create.py index de41ab30f..610353c66 100644 --- a/tests/platforms/macOS/app/test_create.py +++ b/tests/platforms/macOS/app/test_create.py @@ -349,7 +349,9 @@ def test_install_app_packages( # Mock the merge command so we can confirm it was invoked. create_command.merge_app_packages = mock.Mock() - create_command.install_app_requirements(first_app_templated, test_mode=False) + create_command.install_app_requirements( + first_app_templated, test_mode=False, debug_mode=None + ) # We looked for binary packages in the host app_packages create_command.find_binary_packages.assert_called_once_with( @@ -461,7 +463,9 @@ def test_min_os_version(create_command, first_app_templated, tmp_path): # Mock the merge command so we can confirm it was invoked. create_command.merge_app_packages = mock.Mock() - create_command.install_app_requirements(first_app_templated, test_mode=False) + create_command.install_app_requirements( + first_app_templated, test_mode=False, debug_mode=None + ) # We looked for binary packages in the host app_packages create_command.find_binary_packages.assert_called_once_with( @@ -562,7 +566,9 @@ def test_invalid_min_os_version(create_command, first_app_templated): r"but the support package only supports 10.12" ), ): - create_command.install_app_requirements(first_app_templated, test_mode=False) + create_command.install_app_requirements( + first_app_templated, test_mode=False, debug_mode=None + ) # No request was made to install requirements create_command.tools[first_app_templated].app_context.run.assert_not_called() @@ -600,7 +606,9 @@ def test_install_app_packages_no_binary( # Mock the merge command so we can confirm it was invoked. create_command.merge_app_packages = mock.Mock() - create_command.install_app_requirements(first_app_templated, test_mode=False) + create_command.install_app_requirements( + first_app_templated, test_mode=False, debug_mode=None + ) # We looked for binary packages in the host app_packages create_command.find_binary_packages.assert_called_once_with( @@ -700,7 +708,9 @@ def test_install_app_packages_failure(create_command, first_app_templated, tmp_p r"to the PyPI server.\n" ), ): - create_command.install_app_requirements(first_app_templated, test_mode=False) + create_command.install_app_requirements( + first_app_templated, test_mode=False, debug_mode=None + ) # We looked for binary packages in the host app_packages create_command.find_binary_packages.assert_called_once_with( @@ -804,7 +814,9 @@ def test_install_app_packages_non_universal( # Mock the merge command so we can confirm it wasn't invoked. create_command.merge_app_packages = mock.Mock() - create_command.install_app_requirements(first_app_templated, test_mode=False) + create_command.install_app_requirements( + first_app_templated, test_mode=False, debug_mode=None + ) # We didn't search for binary packages create_command.find_binary_packages.assert_not_called() diff --git a/tests/platforms/macOS/app/test_run.py b/tests/platforms/macOS/app/test_run.py index dcb69dace..ad0d61ce3 100644 --- a/tests/platforms/macOS/app/test_run.py +++ b/tests/platforms/macOS/app/test_run.py @@ -44,7 +44,14 @@ def test_run_gui_app(run_command, first_app_config, sleep_zero, tmp_path, monkey "briefcase.platforms.macOS.get_process_id_by_command", lambda *a, **kw: 100 ) - run_command.run_app(first_app_config, test_mode=False, passthrough=[]) + run_command.run_app( + first_app_config, + test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # Calls were made to start the app and to start a log stream. bin_path = run_command.binary_path(first_app_config) @@ -108,6 +115,9 @@ def test_run_gui_app_with_passthrough( run_command.run_app( first_app_config, test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, passthrough=["foo", "--bar"], ) @@ -160,7 +170,14 @@ def test_run_gui_app_failed(run_command, first_app_config, sleep_zero, tmp_path) ) with pytest.raises(BriefcaseCommandError): - run_command.run_app(first_app_config, test_mode=False, passthrough=[]) + run_command.run_app( + first_app_config, + test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # Calls were made to start the app and to start a log stream. bin_path = run_command.binary_path(first_app_config) @@ -207,7 +224,14 @@ def test_run_gui_app_find_pid_failed( ) with pytest.raises(BriefcaseCommandError) as exc_info: - run_command.run_app(first_app_config, test_mode=False, passthrough=[]) + run_command.run_app( + first_app_config, + test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # Calls were made to start the app and to start a log stream. bin_path = run_command.binary_path(first_app_config) @@ -258,7 +282,14 @@ def test_run_gui_app_test_mode( "briefcase.platforms.macOS.get_process_id_by_command", lambda *a, **kw: 100 ) - run_command.run_app(first_app_config, test_mode=True, passthrough=[]) + run_command.run_app( + first_app_config, + test_mode=True, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # Calls were made to start the app and to start a log stream. bin_path = run_command.binary_path(first_app_config) @@ -305,7 +336,14 @@ def test_run_console_app(run_command, first_app_config, tmp_path): # Set the app to be a console app first_app_config.console_app = True - run_command.run_app(first_app_config, test_mode=False, passthrough=[]) + run_command.run_app( + first_app_config, + test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # Calls were made to start the app and to start a log stream. bin_path = run_command.binary_path(first_app_config) @@ -335,6 +373,9 @@ def test_run_console_app_with_passthrough( run_command.run_app( first_app_config, test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, passthrough=["foo", "--bar"], ) @@ -360,7 +401,14 @@ def test_run_console_app_test_mode(run_command, first_app_config, sleep_zero, tm app_process = mock.MagicMock(spec_set=subprocess.Popen) run_command.tools.subprocess.Popen.return_value = app_process - run_command.run_app(first_app_config, test_mode=True, passthrough=[]) + run_command.run_app( + first_app_config, + test_mode=True, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # Calls were made to start the app and to start a log stream. bin_path = run_command.binary_path(first_app_config) @@ -397,7 +445,14 @@ def test_run_console_app_test_mode_with_passthrough( app_process = mock.MagicMock(spec_set=subprocess.Popen) run_command.tools.subprocess.Popen.return_value = app_process - run_command.run_app(first_app_config, test_mode=True, passthrough=["foo", "--bar"]) + run_command.run_app( + first_app_config, + test_mode=True, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=["foo", "--bar"], + ) # Calls were made to start the app and to start a log stream. bin_path = run_command.binary_path(first_app_config) @@ -431,7 +486,14 @@ def test_run_console_app_failed(run_command, first_app_config, sleep_zero, tmp_p # Although the command raises an error, this could be because the script itself # raised an error. - run_command.run_app(first_app_config, test_mode=False, passthrough=[]) + run_command.run_app( + first_app_config, + test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # Calls were made to start the app and to start a log stream. bin_path = run_command.binary_path(first_app_config) diff --git a/tests/platforms/macOS/xcode/test_run.py b/tests/platforms/macOS/xcode/test_run.py index cba4ccbd5..ebd51d143 100644 --- a/tests/platforms/macOS/xcode/test_run.py +++ b/tests/platforms/macOS/xcode/test_run.py @@ -46,7 +46,14 @@ def test_run_app(run_command, first_app_config, sleep_zero, tmp_path, monkeypatc "briefcase.platforms.macOS.get_process_id_by_command", lambda *a, **kw: 100 ) - run_command.run_app(first_app_config, test_mode=False, passthrough=[]) + run_command.run_app( + first_app_config, + test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # Calls were made to start the app and to start a log stream. bin_path = run_command.binary_path(first_app_config) @@ -108,6 +115,9 @@ def test_run_app_with_passthrough( run_command.run_app( first_app_config, test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, passthrough=["foo", "--bar"], ) @@ -167,7 +177,14 @@ def test_run_app_test_mode( "briefcase.platforms.macOS.get_process_id_by_command", lambda *a, **kw: 100 ) - run_command.run_app(first_app_config, test_mode=True, passthrough=[]) + run_command.run_app( + first_app_config, + test_mode=True, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # Calls were made to start the app and to start a log stream. bin_path = run_command.binary_path(first_app_config) @@ -230,6 +247,9 @@ def test_run_app_test_mode_with_passthrough( run_command.run_app( first_app_config, test_mode=True, + debug_mode=None, + debugger_host=None, + debugger_port=None, passthrough=["foo", "--bar"], ) diff --git a/tests/platforms/web/static/test_run.py b/tests/platforms/web/static/test_run.py index 3b897c00b..f89ed8892 100644 --- a/tests/platforms/web/static/test_run.py +++ b/tests/platforms/web/static/test_run.py @@ -46,6 +46,9 @@ def test_default_options(run_command): "update_stub": False, "no_update": False, "test_mode": False, + "debug_mode": None, + "debugger_host": "localhost", + "debugger_port": 5678, "passthrough": [], "host": "localhost", "port": 8080, @@ -69,6 +72,9 @@ def test_options(run_command): "update_stub": False, "no_update": False, "test_mode": False, + "debug_mode": None, + "debugger_host": "localhost", + "debugger_port": 5678, "passthrough": [], "host": "myhost", "port": 1234, @@ -108,6 +114,9 @@ def test_run(monkeypatch, run_command, first_app_built): run_command.run_app( first_app_built, test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, passthrough=[], host="localhost", port=8080, @@ -173,6 +182,9 @@ def test_run_with_fallback_port( run_command.run_app( first_app_built, test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, passthrough=[], host="localhost", port=8080, @@ -227,6 +239,9 @@ def test_run_with_args(monkeypatch, run_command, first_app_built): run_command.run_app( first_app_built, test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, passthrough=["foo", "--bar"], host="localhost", port=8080, @@ -322,6 +337,9 @@ def test_cleanup_server_error( run_command.run_app( first_app_built, test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, passthrough=[], host=host, port=port, @@ -371,6 +389,9 @@ def test_cleanup_runtime_server_error(monkeypatch, run_command, first_app_built) run_command.run_app( first_app_built, test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, passthrough=[], host="localhost", port=8080, @@ -421,6 +442,9 @@ def test_run_without_browser(monkeypatch, run_command, first_app_built): run_command.run_app( first_app_built, test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, passthrough=[], host="localhost", port=8080, @@ -472,6 +496,9 @@ def test_run_autoselect_port(monkeypatch, run_command, first_app_built): run_command.run_app( first_app_built, test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, passthrough=[], host="localhost", port=0, @@ -569,6 +596,9 @@ def test_test_mode(run_command, first_app_built): run_command.run_app( first_app_built, test_mode=True, + debug_mode=None, + debugger_host=None, + debugger_port=None, passthrough=[], host="localhost", port=8080, diff --git a/tests/platforms/windows/app/test_run.py b/tests/platforms/windows/app/test_run.py index 8fea5137d..cfa5054e7 100644 --- a/tests/platforms/windows/app/test_run.py +++ b/tests/platforms/windows/app/test_run.py @@ -30,7 +30,14 @@ def test_run_gui_app(run_command, first_app_config, tmp_path): run_command.tools.subprocess.Popen.return_value = log_popen # Run the app - run_command.run_app(first_app_config, test_mode=False, passthrough=[]) + run_command.run_app( + first_app_config, + test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # The process was started run_command.tools.subprocess.Popen.assert_called_with( @@ -63,6 +70,9 @@ def test_run_gui_app_with_passthrough(run_command, first_app_config, tmp_path): run_command.run_app( first_app_config, test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, passthrough=["foo", "--bar"], ) @@ -96,7 +106,14 @@ def test_run_gui_app_failed(run_command, first_app_config, tmp_path): run_command.tools.subprocess.Popen.side_effect = OSError with pytest.raises(OSError): - run_command.run_app(first_app_config, test_mode=False, passthrough=[]) + run_command.run_app( + first_app_config, + test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # Popen was still invoked, though run_command.tools.subprocess.Popen.assert_called_with( @@ -121,7 +138,14 @@ def test_run_console_app(run_command, first_app_config, tmp_path): run_command.tools.subprocess.Popen.return_value = log_popen # Run the app - run_command.run_app(first_app_config, test_mode=False, passthrough=[]) + run_command.run_app( + first_app_config, + test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # The process was started run_command.tools.subprocess.run.assert_called_with( @@ -146,6 +170,9 @@ def test_run_console_app_with_passthrough(run_command, first_app_config, tmp_pat run_command.run_app( first_app_config, test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, passthrough=["foo", "--bar"], ) @@ -174,7 +201,14 @@ def test_run_console_app_failed(run_command, first_app_config, tmp_path): run_command.tools.subprocess.run.side_effect = OSError with pytest.raises(OSError): - run_command.run_app(first_app_config, test_mode=False, passthrough=[]) + run_command.run_app( + first_app_config, + test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # Popen was still invoked, though run_command.tools.subprocess.run.assert_called_with( @@ -200,7 +234,14 @@ def test_run_app_test_mode(run_command, first_app_config, is_console_app, tmp_pa run_command.tools.subprocess.Popen.return_value = log_popen # Run the app - run_command.run_app(first_app_config, test_mode=True, passthrough=[]) + run_command.run_app( + first_app_config, + test_mode=True, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # The process was started exe_name = "first-app" if is_console_app else "First App" @@ -242,6 +283,9 @@ def test_run_app_test_mode_with_passthrough( run_command.run_app( first_app_config, test_mode=True, + debug_mode=None, + debugger_host=None, + debugger_port=None, passthrough=["foo", "--bar"], ) diff --git a/tests/platforms/windows/visualstudio/test_run.py b/tests/platforms/windows/visualstudio/test_run.py index 9e3950e9d..7ff241346 100644 --- a/tests/platforms/windows/visualstudio/test_run.py +++ b/tests/platforms/windows/visualstudio/test_run.py @@ -34,7 +34,14 @@ def test_run_app(run_command, first_app_config, tmp_path): run_command.tools.subprocess.Popen.return_value = log_popen # Run the app - run_command.run_app(first_app_config, test_mode=False, passthrough=[]) + run_command.run_app( + first_app_config, + test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # Popen was called run_command.tools.subprocess.Popen.assert_called_with( @@ -69,6 +76,9 @@ def test_run_app_with_args(run_command, first_app_config, tmp_path): run_command.run_app( first_app_config, test_mode=False, + debug_mode=None, + debugger_host=None, + debugger_port=None, passthrough=["foo", "--bar"], ) @@ -104,7 +114,14 @@ def test_run_app_test_mode(run_command, first_app_config, tmp_path): run_command.tools.subprocess.Popen.return_value = log_popen # Run the app in test mode - run_command.run_app(first_app_config, test_mode=True, passthrough=[]) + run_command.run_app( + first_app_config, + test_mode=True, + debug_mode=None, + debugger_host=None, + debugger_port=None, + passthrough=[], + ) # Popen was called run_command.tools.subprocess.Popen.assert_called_with( @@ -140,6 +157,9 @@ def test_run_app_test_mode_with_args(run_command, first_app_config, tmp_path): run_command.run_app( first_app_config, test_mode=True, + debug_mode=None, + debugger_host=None, + debugger_port=None, passthrough=["foo", "--bar"], ) diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index 71e95f4a6..4d3dc89ce 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -280,6 +280,9 @@ def test_run_command( "update_stub": False, "no_update": False, "test_mode": False, + "debug_mode": None, + "debugger_host": "localhost", + "debugger_port": 5678, "passthrough": [], **expected_options, } diff --git a/tests/test_mainline.py b/tests/test_mainline.py index 2169b924c..75aa465a1 100644 --- a/tests/test_mainline.py +++ b/tests/test_mainline.py @@ -91,7 +91,7 @@ def test_command_warning(monkeypatch, pyproject_toml, tmp_path, capsys): monkeypatch.setattr(sys, "argv", ["briefcase", "create"]) # Monkeypatch a warning into the create command - def sort_of_bad_generate_app_template(self, app): + def sort_of_bad_generate_app_template(self, app, debug_mode=None): raise BriefcaseWarning(error_code=0, msg="This is bad, but not *really* bad") monkeypatch.setattr( @@ -134,7 +134,7 @@ def test_unknown_command_error(monkeypatch, pyproject_toml, capsys): monkeypatch.setattr(sys, "argv", ["briefcase", "create"]) # Monkeypatch an error into the create command - def bad_generate_app_template(self, app): + def bad_generate_app_template(self, app, debug_mode=None): raise ValueError("Bad value") monkeypatch.setattr( @@ -151,7 +151,7 @@ def test_interrupted_command(monkeypatch, pyproject_toml, tmp_path, capsys): monkeypatch.setattr(sys, "argv", ["briefcase", "create"]) # Monkeypatch a keyboard interrupt into the create command - def interrupted_generate_app_template(self, app): + def interrupted_generate_app_template(self, app, debug_mode=None): raise KeyboardInterrupt() monkeypatch.setattr( @@ -173,7 +173,7 @@ def test_interrupted_command_with_log(monkeypatch, pyproject_toml, tmp_path, cap monkeypatch.setattr(sys, "argv", ["briefcase", "create", "--log"]) # Monkeypatch a keyboard interrupt into the create command - def interrupted_generate_app_template(self, app): + def interrupted_generate_app_template(self, app, debug_mode=None): raise KeyboardInterrupt() monkeypatch.setattr( From e612e452b0a7d13bc0a75e9832482adfb728b12f Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sun, 18 May 2025 17:41:59 +0200 Subject: [PATCH 021/131] Differentiate between "debug_mode" and "debugger". The selected debugger is saved inside the AppConfig. The "debug_mode" is now a boolean. --- src/briefcase/commands/base.py | 28 ++-- src/briefcase/commands/build.py | 16 +- src/briefcase/commands/create.py | 12 +- src/briefcase/commands/run.py | 15 +- src/briefcase/commands/update.py | 5 +- src/briefcase/config.py | 1 + src/briefcase/platforms/android/gradle.py | 25 +-- src/briefcase/platforms/iOS/xcode.py | 3 + src/briefcase/platforms/linux/appimage.py | 4 +- src/briefcase/platforms/linux/flatpak.py | 4 +- src/briefcase/platforms/linux/system.py | 7 +- src/briefcase/platforms/macOS/__init__.py | 3 + src/briefcase/platforms/web/static.py | 7 +- src/briefcase/platforms/windows/__init__.py | 4 +- tests/commands/build/conftest.py | 4 +- tests/commands/build/test_call.py | 145 +++++++++--------- tests/commands/create/conftest.py | 4 +- .../create/test_generate_app_template.py | 1 + tests/commands/run/test_call.py | 85 +++++----- tests/platforms/android/gradle/test_run.py | 6 +- tests/platforms/iOS/xcode/test_run.py | 2 +- tests/platforms/web/static/test_run.py | 4 +- tests/test_cmdline.py | 2 +- 23 files changed, 211 insertions(+), 176 deletions(-) diff --git a/src/briefcase/commands/base.py b/src/briefcase/commands/base.py index 18ecbad60..dcbd92fe8 100644 --- a/src/briefcase/commands/base.py +++ b/src/briefcase/commands/base.py @@ -597,7 +597,7 @@ def finalize_app_config(self, app: AppConfig): :param app: The app configuration to finalize. """ - def finalize(self, app: AppConfig | None = None, debug_mode: str | None = None): + def finalize(self, app: AppConfig | None = None, debugger: str | None = None): """Finalize Briefcase configuration. This will: @@ -614,19 +614,14 @@ def finalize(self, app: AppConfig | None = None, debug_mode: str | None = None): self.verify_host() self.verify_tools() - if app is None: - for app in self.apps.values(): - if hasattr(app, "__draft__"): - self.finalze_debug_mode(app, debug_mode) - self.finalize_app_config(app) - delattr(app, "__draft__") - else: + apps = self.apps.values() if app is None else [app] + for app in apps: if hasattr(app, "__draft__"): - self.finalze_debug_mode(app, debug_mode) + self.finalze_debugger(app, debugger) self.finalize_app_config(app) delattr(app, "__draft__") - def finalze_debug_mode(self, app: AppConfig, debug_mode: str | None = None): + def finalze_debugger(self, app: AppConfig, debugger: str | None = None): """Finalize the debugger configuration. This will ensure that the debugger is available and that the app @@ -634,9 +629,10 @@ def finalze_debug_mode(self, app: AppConfig, debug_mode: str | None = None): :param app: The app configuration to finalize. """ - if debug_mode and debug_mode != "": - debugger = get_debugger(debug_mode) + if debugger and debugger != "": + debugger = get_debugger(debugger) app.debug_requires.extend(debugger.additional_requirements) + app.debugger = debugger def verify_app(self, app: AppConfig): """Verify the app is compatible and the app tools are available. @@ -903,16 +899,18 @@ def _add_debug_options(self, parser, context_label): """ debuggers = get_debuggers() debugger_names = list(reversed(debuggers.keys())) + choices = ["", *debugger_names] + choices_help = [f"'{choice}'" for choice in choices] parser.add_argument( "--debug", - dest="debug_mode", + dest="debugger", nargs="?", default=None, const="", - choices=["", *debugger_names], + choices=choices, metavar="DEBUGGER", - help=f"{context_label} the app with the specified debugger ({', '.join(debugger_names)})", + help=f"{context_label} the app with the specified debugger ({', '.join(choices_help)})", ) def add_options(self, parser): diff --git a/src/briefcase/commands/build.py b/src/briefcase/commands/build.py index d92d288ac..a1a64aa84 100644 --- a/src/briefcase/commands/build.py +++ b/src/briefcase/commands/build.py @@ -42,7 +42,7 @@ def _build_app( update_stub: bool, no_update: bool, test_mode: bool, - debug_mode: str | None, + debugger: str | None, **options, ) -> dict | None: """Internal method to invoke a build on a single app. Ensures the app exists, @@ -59,17 +59,17 @@ def _build_app( :param update_stub: Should the stub binary be updated? :param no_update: Should automated updates be disabled? :param test_mode: Is the app being build in test mode? - :param debug_mode: Is the app being build in debug mode? """ + debug_mode = debugger is not None if not self.bundle_path(app).exists(): state = self.create_command( - app, test_mode=test_mode, debug_mode=debug_mode, **options + app, test_mode=test_mode, debugger=debugger, **options ) elif ( update # An explicit update has been requested or update_requirements # An explicit update of requirements has been requested or update_resources # An explicit update of resources has been requested - or update_support # An explicit update of app support has been requested + or update_support # An explicit update of app support has been rdebuggerequested or update_stub # An explicit update of the stub binary has been requested or ( test_mode and not no_update @@ -85,7 +85,7 @@ def _build_app( update_support=update_support, update_stub=update_stub, test_mode=test_mode, - debug_mode=debug_mode, + debugger=debugger, **options, ) else: @@ -119,7 +119,7 @@ def __call__( update_stub: bool = False, no_update: bool = False, test_mode: bool = False, - debug_mode: str | None = None, + debugger: str | None = None, **options, ) -> dict | None: # Has the user requested an invalid set of options? @@ -148,7 +148,7 @@ def __call__( # Confirm host compatibility, that all required tools are available, # and that the app configuration is finalized. - self.finalize(app, debug_mode) + self.finalize(app, debugger) if app_name: try: @@ -173,7 +173,7 @@ def __call__( update_stub=update_stub, no_update=no_update, test_mode=test_mode, - debug_mode=debug_mode, + debugger=debugger, **full_options(state, options), ) diff --git a/src/briefcase/commands/create.py b/src/briefcase/commands/create.py index 973cfe31a..11b9f176b 100644 --- a/src/briefcase/commands/create.py +++ b/src/briefcase/commands/create.py @@ -207,16 +207,14 @@ def permissions_context(self, app: AppConfig, x_permissions: dict[str, str]): """ return {} - def output_format_template_context( - self, app: AppConfig, debug_mode: str | None = None - ): + def output_format_template_context(self, app: AppConfig, debug_mode: bool = False): """Additional template context required by the output format. :param app: The config object for the app """ return {} - def generate_app_template(self, app: AppConfig, debug_mode: str | None = None): + def generate_app_template(self, app: AppConfig, debug_mode: bool = False): """Create an application bundle. :param app: The config object for the app @@ -671,7 +669,7 @@ def _install_app_requirements( self.console.info("No application requirements.") def install_app_requirements( - self, app: AppConfig, test_mode: bool, debug_mode: str | None + self, app: AppConfig, test_mode: bool, debug_mode: bool ): """Handle requirements for the app. @@ -695,7 +693,7 @@ def install_app_requirements( if test_mode and app.test_requires: requires.extend(app.test_requires) - if debug_mode: + if debug_mode and app.debug_requires: requires.extend(app.debug_requires) try: @@ -921,7 +919,7 @@ def create_app( self, app: AppConfig, test_mode: bool = False, - debug_mode: str | None = None, + debug_mode: bool = False, **options, ): """Create an application bundle. diff --git a/src/briefcase/commands/run.py b/src/briefcase/commands/run.py index 599f9ee79..9af183f19 100644 --- a/src/briefcase/commands/run.py +++ b/src/briefcase/commands/run.py @@ -319,6 +319,9 @@ def _prepare_app_kwargs( :param app: The app to be launched :param test_mode: Are we launching in test mode? + :param debug_mode: Are we launching in debug mode? + :param debugger_host: The host on which to run the debug server + :param debugger_port: The port on which to run the debug server :returns: A dictionary of additional arguments to pass to the Popen """ args = {} @@ -362,6 +365,11 @@ def run_app( """Start an application. :param app: The application to start + :param test_mode: Is the test mode enabled? + :param debug_mode: Is the debug mode enabled? + :param debugger_host: The host on which to run the debug server + :param debugger_port: The port on which to run the debug server + :param passthrough: Any passthrough arguments """ def __call__( @@ -374,7 +382,7 @@ def __call__( update_stub: bool = False, no_update: bool = False, test_mode: bool = False, - debug_mode: str | None = None, + debugger: str | None = None, debugger_host: str | None = None, debugger_port: int | None = None, passthrough: list[str] | None = None, @@ -399,7 +407,8 @@ def __call__( # Confirm host compatibility, that all required tools are available, # and that the app configuration is finalized. - self.finalize(app, debug_mode) + self.finalize(app, debugger) + debug_mode = debugger is not None template_file = self.bundle_path(app) exec_file = self.binary_executable_path(app) @@ -424,7 +433,7 @@ def __call__( update_stub=update_stub, no_update=no_update, test_mode=test_mode, - debug_mode=debug_mode, + debugger=debugger, **options, ) else: diff --git a/src/briefcase/commands/update.py b/src/briefcase/commands/update.py index fe8d2c2b2..1b3921b45 100644 --- a/src/briefcase/commands/update.py +++ b/src/briefcase/commands/update.py @@ -101,12 +101,13 @@ def __call__( update_support: bool = False, update_stub: bool = False, test_mode: bool = False, - debug_mode: str | None = None, + debugger: str | None = None, **options, ) -> dict | None: # Confirm host compatibility, that all required tools are available, # and that the app configuration is finalized. - self.finalize(app, debug_mode) + self.finalize(app, debugger) + debug_mode = debugger is not None if app_name: try: diff --git a/src/briefcase/config.py b/src/briefcase/config.py index a099cf9f6..44014abe1 100644 --- a/src/briefcase/config.py +++ b/src/briefcase/config.py @@ -319,6 +319,7 @@ def __init__( self.requirement_installer_args = ( [] if requirement_installer_args is None else requirement_installer_args ) + self.debugger = None if not is_valid_app_name(self.app_name): raise BriefcaseConfigError( diff --git a/src/briefcase/platforms/android/gradle.py b/src/briefcase/platforms/android/gradle.py index 0f98bb738..b0cafd28c 100644 --- a/src/briefcase/platforms/android/gradle.py +++ b/src/briefcase/platforms/android/gradle.py @@ -163,9 +163,7 @@ def support_package_filename(self, support_revision): f"Python-{self.python_version_tag}-Android-support.b{support_revision}.zip" ) - def output_format_template_context( - self, app: AppConfig, debug_mode: str | None = None - ): + def output_format_template_context(self, app: AppConfig, debug_mode: bool = False): """Additional template context required by the output format. :param app: The config object for the app @@ -390,7 +388,7 @@ def run_app( self, app: AppConfig, test_mode: bool, - debug_mode: str | None, + debug_mode: bool, debugger_host: str | None, debugger_port: int | None, passthrough: list[str], @@ -403,6 +401,9 @@ def run_app( :param app: The config object for the app :param test_mode: Boolean; Is the app running in test mode? + :param debug_mode: Boolean; Is the app running in debug mode? + :param debugger_host: The host to use for the debugger + :param debugger_port: The port to use for the debugger :param passthrough: The list of arguments to pass to the app :param device_or_avd: The device to target. If ``None``, the user will be asked to re-run the command selecting a specific device. @@ -459,7 +460,7 @@ def run_app( env = {} if debug_mode: with self.console.wait_bar("Establishing debugger connection..."): - self.establish_debugger_connection(adb, debug_mode, debugger_port) + self.establish_debugger_connection(adb, app.debugger, debugger_port) env["BRIEFCASE_DEBUGGER"] = self.remote_debugger_config( app, test_mode, debugger_host, debugger_port ) @@ -517,32 +518,36 @@ def run_app( finally: if debug_mode: with self.console.wait_bar("Stopping debugger connection..."): - self.remove_debugger_connection(adb, debug_mode, debugger_port) + self.remove_debugger_connection(adb, app.debugger, debugger_port) if shutdown_on_exit: with self.tools.console.wait_bar("Stopping emulator..."): adb.kill() def establish_debugger_connection( - self, adb: ADB, debug_mode: str, debugger_port: int + self, adb: ADB, debugger: str, debugger_port: int ): """Forward/Reverse the ports necessary for remote debugging. :param app: The config object for the app :param adb: Access to the adb + :param debugger: The debugger to use + :param debugger_port: The port to forward/reverse for the debugger """ - debugger = get_debugger(debug_mode) + debugger = get_debugger(debugger) if debugger.connection_mode == DebuggerConnectionMode.SERVER: adb.forward(debugger_port, debugger_port) elif debugger.connection_mode == DebuggerConnectionMode.CLIENT: adb.reverse(debugger_port, debugger_port) - def remove_debugger_connection(self, adb: ADB, debug_mode: str, debugger_port: int): + def remove_debugger_connection(self, adb: ADB, debugger: str, debugger_port: int): """Remove Forward/Reverse of the ports necessary for remote debugging. :param app: The config object for the app :param adb: Access to the adb + :param debugger: The debugger to use + :param debugger_port: The port to forward/reverse for the debugger """ - debugger = get_debugger(debug_mode) + debugger = get_debugger(debugger) if debugger.connection_mode == DebuggerConnectionMode.SERVER: adb.forward_remove(debugger_port) elif debugger.connection_mode == DebuggerConnectionMode.CLIENT: diff --git a/src/briefcase/platforms/iOS/xcode.py b/src/briefcase/platforms/iOS/xcode.py index 18a8f7b40..f53f33b6d 100644 --- a/src/briefcase/platforms/iOS/xcode.py +++ b/src/briefcase/platforms/iOS/xcode.py @@ -509,6 +509,9 @@ def run_app( :param app: The config object for the app :param test_mode: Boolean; Is the app running in test mode? + :param debug_mode: Boolean; Is the app running in debug mode? + :param debugger_host: The host to use for the debugger + :param debugger_port: The port to use for the debugger :param passthrough: The list of arguments to pass to the app :param udid: The device UDID to target. If ``None``, the user will be asked to select a device at runtime. diff --git a/src/briefcase/platforms/linux/appimage.py b/src/briefcase/platforms/linux/appimage.py index 1bb2e86b8..38db5f404 100644 --- a/src/briefcase/platforms/linux/appimage.py +++ b/src/briefcase/platforms/linux/appimage.py @@ -184,9 +184,7 @@ class LinuxAppImageCreateCommand( ): description = "Create and populate a Linux AppImage." - def output_format_template_context( - self, app: AppConfig, debug_mode: str | None = None - ): + def output_format_template_context(self, app: AppConfig, debug_mode: bool = False): context = super().output_format_template_context(app, debug_mode) try: diff --git a/src/briefcase/platforms/linux/flatpak.py b/src/briefcase/platforms/linux/flatpak.py index dce1d69d1..2813a872e 100644 --- a/src/briefcase/platforms/linux/flatpak.py +++ b/src/briefcase/platforms/linux/flatpak.py @@ -106,9 +106,7 @@ class LinuxFlatpakCreateCommand(LinuxFlatpakMixin, CreateCommand): description = "Create and populate a Linux Flatpak." hidden_app_properties = {"permission", "finish_arg"} - def output_format_template_context( - self, app: AppConfig, debug_mode: str | None = None - ): + def output_format_template_context(self, app: AppConfig, debug_mode: bool = False): """Add flatpak runtime/SDK details to the app template.""" return { "flatpak_runtime": self.flatpak_runtime(app), diff --git a/src/briefcase/platforms/linux/system.py b/src/briefcase/platforms/linux/system.py index 5be194d40..8731bc48e 100644 --- a/src/briefcase/platforms/linux/system.py +++ b/src/briefcase/platforms/linux/system.py @@ -639,9 +639,7 @@ def verify_host(self): class LinuxSystemCreateCommand(LinuxSystemMixin, LocalRequirementsMixin, CreateCommand): description = "Create and populate a Linux system project." - def output_format_template_context( - self, app: AppConfig, debug_mode: str | None = None - ): + def output_format_template_context(self, app: AppConfig, debug_mode: bool = False): context = super().output_format_template_context(app, debug_mode) # Linux system templates use the target codename, rather than @@ -852,6 +850,9 @@ def run_app( :param app: The config object for the app :param test_mode: Boolean; Is the app running in test mode? + :param debug_mode: Boolean; Is the app running in debug mode? + :param debugger_host: The host to use for the debugger + :param debugger_port: The port to use for the debugger :param passthrough: The list of arguments to pass to the app """ # Set up the log stream diff --git a/src/briefcase/platforms/macOS/__init__.py b/src/briefcase/platforms/macOS/__init__.py index ee9f21ce6..6227912ef 100644 --- a/src/briefcase/platforms/macOS/__init__.py +++ b/src/briefcase/platforms/macOS/__init__.py @@ -305,6 +305,9 @@ def run_app( :param app: The config object for the app :param test_mode: Boolean; Is the app running in test mode? + :param debug_mode: Boolean; Is the app running in debug mode? + :param debugger_host: The host to use for the debugger + :param debugger_port: The port to use for the debugger :param passthrough: The list of arguments to pass to the app """ # Console apps must operate in non-streaming mode so that console input can diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index 607dcbaf4..5cfca9085 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -50,9 +50,7 @@ def distribution_path(self, app): class StaticWebCreateCommand(StaticWebMixin, CreateCommand): description = "Create and populate a static web project." - def output_format_template_context( - self, app: AppConfig, debug_mode: str | None = None - ): + def output_format_template_context(self, app: AppConfig, debug_mode: bool = False): """Add style framework details to the app template.""" return { "style_framework": getattr(app, "style_framework", "None"), @@ -326,6 +324,7 @@ def run_app( :param app: The config object for the app :param test_mode: Boolean; Is the app running in test mode? + :param debug_mode: Boolean; Is the app running in debug mode? :param passthrough: The list of arguments to pass to the app :param host: The host on which to run the server :param port: The port on which to run the server @@ -333,6 +332,8 @@ def run_app( """ if test_mode: raise BriefcaseCommandError("Briefcase can't run web apps in test mode.") + if debug_mode: + raise BriefcaseCommandError("Briefcase can't run web apps in debug mode.") self.console.info("Starting web server...", prefix=app.app_name) diff --git a/src/briefcase/platforms/windows/__init__.py b/src/briefcase/platforms/windows/__init__.py index bc912bacd..942eb0243 100644 --- a/src/briefcase/platforms/windows/__init__.py +++ b/src/briefcase/platforms/windows/__init__.py @@ -59,9 +59,7 @@ def support_package_url(self, support_revision): f"{self.support_package_filename(support_revision)}" ) - def output_format_template_context( - self, app: AppConfig, debug_mode: str | None = None - ): + def output_format_template_context(self, app: AppConfig, debug_mode: bool = False): """Additional template context required by the output format. :param app: The config object for the app diff --git a/tests/commands/build/conftest.py b/tests/commands/build/conftest.py index 381abc3b2..3e893f6ee 100644 --- a/tests/commands/build/conftest.py +++ b/tests/commands/build/conftest.py @@ -70,7 +70,7 @@ def create_command(self, app, **kwargs): self.actions.append(("create", app.app_name, kwargs.copy())) # Remove arguments consumed by the underlying call to create_app() kwargs.pop("test_mode", None) - kwargs.pop("debug_mode", None) + kwargs.pop("debugger", None) return full_options({"create_state": app.app_name}, kwargs) def update_command(self, app, **kwargs): @@ -81,7 +81,7 @@ def update_command(self, app, **kwargs): kwargs.pop("update_support", None) kwargs.pop("update_stub", None) kwargs.pop("test_mode", None) - kwargs.pop("debug_mode", None) + kwargs.pop("debugger", None) return full_options({"update_state": app.app_name}, kwargs) diff --git a/tests/commands/build/test_call.py b/tests/commands/build/test_call.py index 6171bdc67..3a1d2e023 100644 --- a/tests/commands/build/test_call.py +++ b/tests/commands/build/test_call.py @@ -30,7 +30,7 @@ def test_specific_app(build_command, first_app, second_app): # App tools are verified for app ("verify-app-tools", "first"), # Build the first app; no state - ("build", "first", {"test_mode": False, "debug_mode": None}), + ("build", "first", {"test_mode": False, "debug_mode": False}), ] @@ -62,7 +62,7 @@ def test_multiple_apps(build_command, first_app, second_app): # App tools are verified for first app ("verify-app-tools", "first"), # Build the first app; no state - ("build", "first", {"test_mode": False, "debug_mode": None}), + ("build", "first", {"test_mode": False, "debug_mode": False}), # App template is verified for second app ("verify-app-template", "second"), # App tools are verified for second app @@ -71,7 +71,7 @@ def test_multiple_apps(build_command, first_app, second_app): ( "build", "second", - {"build_state": "first", "test_mode": False, "debug_mode": None}, + {"build_state": "first", "test_mode": False, "debug_mode": False}, ), ] @@ -100,7 +100,7 @@ def test_non_existent(build_command, first_app_config, second_app): ("finalize-app-config", "first"), ("finalize-app-config", "second"), # First App doesn't exist, so it will be created, then built - ("create", "first", {"test_mode": False, "debug_mode": None}), + ("create", "first", {"test_mode": False, "debugger": None}), # App template is verified for first app ("verify-app-template", "first"), # App tools are verified for first app @@ -108,7 +108,7 @@ def test_non_existent(build_command, first_app_config, second_app): ( "build", "first", - {"create_state": "first", "test_mode": False, "debug_mode": None}, + {"create_state": "first", "test_mode": False, "debug_mode": False}, ), # App template is verified for second app ("verify-app-template", "second"), @@ -122,7 +122,7 @@ def test_non_existent(build_command, first_app_config, second_app): "create_state": "first", "build_state": "first", "test_mode": False, - "debug_mode": None, + "debug_mode": False, }, ), ] @@ -157,7 +157,7 @@ def test_unbuilt(build_command, first_app_unbuilt, second_app): # App tools are verified for first app ("verify-app-tools", "first"), # First App exists, but hasn't been built; it will be built. - ("build", "first", {"test_mode": False, "debug_mode": None}), + ("build", "first", {"test_mode": False, "debug_mode": False}), # App template is verified for second app ("verify-app-template", "second"), # App tools are verified for second app @@ -166,7 +166,7 @@ def test_unbuilt(build_command, first_app_unbuilt, second_app): ( "build", "second", - {"build_state": "first", "test_mode": False, "debug_mode": None}, + {"build_state": "first", "test_mode": False, "debug_mode": False}, ), ] @@ -200,7 +200,7 @@ def test_update_app(build_command, first_app, second_app): "first", { "test_mode": False, - "debug_mode": None, + "debugger": None, "update_requirements": False, "update_resources": False, "update_support": False, @@ -214,7 +214,7 @@ def test_update_app(build_command, first_app, second_app): ( "build", "first", - {"update_state": "first", "test_mode": False, "debug_mode": None}, + {"update_state": "first", "test_mode": False, "debug_mode": False}, ), # Update then build the second app ( @@ -224,7 +224,7 @@ def test_update_app(build_command, first_app, second_app): "update_state": "first", "build_state": "first", "test_mode": False, - "debug_mode": None, + "debugger": None, "update_requirements": False, "update_resources": False, "update_support": False, @@ -242,7 +242,7 @@ def test_update_app(build_command, first_app, second_app): "update_state": "second", "build_state": "first", "test_mode": False, - "debug_mode": None, + "debug_mode": False, }, ), ] @@ -277,7 +277,7 @@ def test_update_app_requirements(build_command, first_app, second_app): "first", { "test_mode": False, - "debug_mode": None, + "debugger": None, "update_requirements": True, "update_resources": False, "update_support": False, @@ -291,7 +291,7 @@ def test_update_app_requirements(build_command, first_app, second_app): ( "build", "first", - {"update_state": "first", "test_mode": False, "debug_mode": None}, + {"update_state": "first", "test_mode": False, "debug_mode": False}, ), # Update then build the second app ( @@ -301,7 +301,7 @@ def test_update_app_requirements(build_command, first_app, second_app): "update_state": "first", "build_state": "first", "test_mode": False, - "debug_mode": None, + "debugger": None, "update_requirements": True, "update_resources": False, "update_support": False, @@ -319,7 +319,7 @@ def test_update_app_requirements(build_command, first_app, second_app): "update_state": "second", "build_state": "first", "test_mode": False, - "debug_mode": None, + "debug_mode": False, }, ), ] @@ -354,7 +354,7 @@ def test_update_app_support(build_command, first_app, second_app): "first", { "test_mode": False, - "debug_mode": None, + "debugger": None, "update_requirements": False, "update_resources": False, "update_support": True, @@ -368,7 +368,7 @@ def test_update_app_support(build_command, first_app, second_app): ( "build", "first", - {"update_state": "first", "test_mode": False, "debug_mode": None}, + {"update_state": "first", "test_mode": False, "debug_mode": False}, ), # Update then build the second app ( @@ -378,7 +378,7 @@ def test_update_app_support(build_command, first_app, second_app): "update_state": "first", "build_state": "first", "test_mode": False, - "debug_mode": None, + "debugger": None, "update_requirements": False, "update_resources": False, "update_support": True, @@ -396,7 +396,7 @@ def test_update_app_support(build_command, first_app, second_app): "update_state": "second", "build_state": "first", "test_mode": False, - "debug_mode": None, + "debug_mode": False, }, ), ] @@ -431,7 +431,7 @@ def test_update_app_stub(build_command, first_app, second_app): "first", { "test_mode": False, - "debug_mode": None, + "debugger": None, "update_requirements": False, "update_resources": False, "update_support": False, @@ -445,7 +445,7 @@ def test_update_app_stub(build_command, first_app, second_app): ( "build", "first", - {"update_state": "first", "test_mode": False, "debug_mode": None}, + {"update_state": "first", "test_mode": False, "debug_mode": False}, ), # Update then build the second app ( @@ -455,7 +455,7 @@ def test_update_app_stub(build_command, first_app, second_app): "update_state": "first", "build_state": "first", "test_mode": False, - "debug_mode": None, + "debugger": None, "update_requirements": False, "update_resources": False, "update_support": False, @@ -473,7 +473,7 @@ def test_update_app_stub(build_command, first_app, second_app): "update_state": "second", "build_state": "first", "test_mode": False, - "debug_mode": None, + "debug_mode": False, }, ), ] @@ -508,7 +508,7 @@ def test_update_app_resources(build_command, first_app, second_app): "first", { "test_mode": False, - "debug_mode": None, + "debugger": None, "update_requirements": False, "update_resources": True, "update_support": False, @@ -522,7 +522,7 @@ def test_update_app_resources(build_command, first_app, second_app): ( "build", "first", - {"update_state": "first", "test_mode": False, "debug_mode": None}, + {"update_state": "first", "test_mode": False, "debug_mode": False}, ), # Update then build the second app ( @@ -532,7 +532,7 @@ def test_update_app_resources(build_command, first_app, second_app): "update_state": "first", "build_state": "first", "test_mode": False, - "debug_mode": None, + "debugger": None, "update_requirements": False, "update_resources": True, "update_support": False, @@ -550,7 +550,7 @@ def test_update_app_resources(build_command, first_app, second_app): "update_state": "second", "build_state": "first", "test_mode": False, - "debug_mode": None, + "debug_mode": False, }, ), ] @@ -580,7 +580,14 @@ def test_update_non_existent(build_command, first_app_config, second_app): ("finalize-app-config", "first"), ("finalize-app-config", "second"), # First App doesn't exist, so it will be created, then built - ("create", "first", {"test_mode": False, "debug_mode": None}), + ( + "create", + "first", + { + "test_mode": False, + "debugger": None, + }, + ), # App template is verified for first app ("verify-app-template", "first"), # App tools are verified for first app @@ -588,7 +595,7 @@ def test_update_non_existent(build_command, first_app_config, second_app): ( "build", "first", - {"create_state": "first", "test_mode": False, "debug_mode": None}, + {"create_state": "first", "test_mode": False, "debug_mode": False}, ), # Second app *does* exist, so it will be updated, then built ( @@ -598,7 +605,7 @@ def test_update_non_existent(build_command, first_app_config, second_app): "create_state": "first", "build_state": "first", "test_mode": False, - "debug_mode": None, + "debugger": None, "update_requirements": False, "update_resources": False, "update_support": False, @@ -617,7 +624,7 @@ def test_update_non_existent(build_command, first_app_config, second_app): "build_state": "first", "update_state": "second", "test_mode": False, - "debug_mode": None, + "debug_mode": False, }, ), ] @@ -652,7 +659,7 @@ def test_update_unbuilt(build_command, first_app_unbuilt, second_app): "first", { "test_mode": False, - "debug_mode": None, + "debugger": None, "update_requirements": False, "update_resources": False, "update_support": False, @@ -666,7 +673,7 @@ def test_update_unbuilt(build_command, first_app_unbuilt, second_app): ( "build", "first", - {"update_state": "first", "test_mode": False, "debug_mode": None}, + {"update_state": "first", "test_mode": False, "debug_mode": False}, ), # Second app has been built before; it will be built again. ( @@ -676,7 +683,7 @@ def test_update_unbuilt(build_command, first_app_unbuilt, second_app): "update_state": "first", "build_state": "first", "test_mode": False, - "debug_mode": None, + "debugger": None, "update_requirements": False, "update_resources": False, "update_support": False, @@ -694,7 +701,7 @@ def test_update_unbuilt(build_command, first_app_unbuilt, second_app): "update_state": "second", "build_state": "first", "test_mode": False, - "debug_mode": None, + "debug_mode": False, }, ), ] @@ -729,7 +736,7 @@ def test_build_test(build_command, first_app, second_app): "first", { "test_mode": True, - "debug_mode": None, + "debugger": None, "update_requirements": False, "update_resources": False, "update_support": False, @@ -743,7 +750,7 @@ def test_build_test(build_command, first_app, second_app): ( "build", "first", - {"update_state": "first", "test_mode": True, "debug_mode": None}, + {"update_state": "first", "test_mode": True, "debug_mode": False}, ), # Update then build the second app ( @@ -753,7 +760,7 @@ def test_build_test(build_command, first_app, second_app): "update_state": "first", "build_state": "first", "test_mode": True, - "debug_mode": None, + "debugger": None, "update_requirements": False, "update_resources": False, "update_support": False, @@ -771,7 +778,7 @@ def test_build_test(build_command, first_app, second_app): "update_state": "second", "build_state": "first", "test_mode": True, - "debug_mode": None, + "debug_mode": False, }, ), ] @@ -806,7 +813,7 @@ def test_build_test_no_update(build_command, first_app, second_app): ("verify-app-template", "first"), # App tools are verified for first app ("verify-app-tools", "first"), - ("build", "first", {"test_mode": True, "debug_mode": None}), + ("build", "first", {"test_mode": True, "debug_mode": False}), # No update of the second app # App template is verified for second app ("verify-app-template", "second"), @@ -815,7 +822,7 @@ def test_build_test_no_update(build_command, first_app, second_app): ( "build", "second", - {"build_state": "first", "test_mode": True, "debug_mode": None}, + {"build_state": "first", "test_mode": True, "debug_mode": False}, ), ] @@ -850,7 +857,7 @@ def test_build_test_update_dependencies(build_command, first_app, second_app): "first", { "test_mode": True, - "debug_mode": None, + "debugger": None, "update_requirements": True, "update_resources": False, "update_support": False, @@ -864,7 +871,7 @@ def test_build_test_update_dependencies(build_command, first_app, second_app): ( "build", "first", - {"update_state": "first", "test_mode": True, "debug_mode": None}, + {"update_state": "first", "test_mode": True, "debug_mode": False}, ), # Update then build the second app ( @@ -874,7 +881,7 @@ def test_build_test_update_dependencies(build_command, first_app, second_app): "update_state": "first", "build_state": "first", "test_mode": True, - "debug_mode": None, + "debugger": None, "update_requirements": True, "update_resources": False, "update_support": False, @@ -892,7 +899,7 @@ def test_build_test_update_dependencies(build_command, first_app, second_app): "update_state": "second", "build_state": "first", "test_mode": True, - "debug_mode": None, + "debug_mode": False, }, ), ] @@ -928,7 +935,7 @@ def test_build_test_update_resources(build_command, first_app, second_app): "first", { "test_mode": True, - "debug_mode": None, + "debugger": None, "update_requirements": False, "update_resources": True, "update_support": False, @@ -942,7 +949,7 @@ def test_build_test_update_resources(build_command, first_app, second_app): ( "build", "first", - {"update_state": "first", "test_mode": True, "debug_mode": None}, + {"update_state": "first", "test_mode": True, "debug_mode": False}, ), # Update then build the second app ( @@ -952,7 +959,7 @@ def test_build_test_update_resources(build_command, first_app, second_app): "update_state": "first", "build_state": "first", "test_mode": True, - "debug_mode": None, + "debugger": None, "update_requirements": False, "update_resources": True, "update_support": False, @@ -970,7 +977,7 @@ def test_build_test_update_resources(build_command, first_app, second_app): "update_state": "second", "build_state": "first", "test_mode": True, - "debug_mode": None, + "debug_mode": False, }, ), ] @@ -1006,7 +1013,7 @@ def test_build_test_update_support(build_command, first_app, second_app): "first", { "test_mode": True, - "debug_mode": None, + "debugger": None, "update_requirements": False, "update_resources": False, "update_support": True, @@ -1020,7 +1027,7 @@ def test_build_test_update_support(build_command, first_app, second_app): ( "build", "first", - {"update_state": "first", "test_mode": True, "debug_mode": None}, + {"update_state": "first", "test_mode": True, "debug_mode": False}, ), # Update then build the second app ( @@ -1030,7 +1037,7 @@ def test_build_test_update_support(build_command, first_app, second_app): "update_state": "first", "build_state": "first", "test_mode": True, - "debug_mode": None, + "debugger": None, "update_requirements": False, "update_resources": False, "update_support": True, @@ -1048,7 +1055,7 @@ def test_build_test_update_support(build_command, first_app, second_app): "update_state": "second", "build_state": "first", "test_mode": True, - "debug_mode": None, + "debug_mode": False, }, ), ] @@ -1084,7 +1091,7 @@ def test_build_test_update_stub(build_command, first_app, second_app): "first", { "test_mode": True, - "debug_mode": None, + "debugger": None, "update_requirements": False, "update_resources": False, "update_support": False, @@ -1098,7 +1105,7 @@ def test_build_test_update_stub(build_command, first_app, second_app): ( "build", "first", - {"update_state": "first", "test_mode": True, "debug_mode": None}, + {"update_state": "first", "test_mode": True, "debug_mode": False}, ), # Update then build the second app ( @@ -1108,7 +1115,7 @@ def test_build_test_update_stub(build_command, first_app, second_app): "update_state": "first", "build_state": "first", "test_mode": True, - "debug_mode": None, + "debugger": None, "update_requirements": False, "update_resources": False, "update_support": False, @@ -1126,7 +1133,7 @@ def test_build_test_update_stub(build_command, first_app, second_app): "update_state": "second", "build_state": "first", "test_mode": True, - "debug_mode": None, + "debug_mode": False, }, ), ] @@ -1255,7 +1262,7 @@ def test_test_app_non_existent(build_command, first_app_config, second_app): ("finalize-app-config", "first"), ("finalize-app-config", "second"), # First App doesn't exist, so it will be created, then built - ("create", "first", {"test_mode": True, "debug_mode": None}), + ("create", "first", {"test_mode": True, "debugger": None}), # App template is verified for first app ("verify-app-template", "first"), # App tools are verified for first app @@ -1263,7 +1270,7 @@ def test_test_app_non_existent(build_command, first_app_config, second_app): ( "build", "first", - {"create_state": "first", "test_mode": True, "debug_mode": None}, + {"create_state": "first", "test_mode": True, "debug_mode": False}, ), # Second app *does* exist, so it will be updated, then built ( @@ -1273,7 +1280,7 @@ def test_test_app_non_existent(build_command, first_app_config, second_app): "create_state": "first", "build_state": "first", "test_mode": True, - "debug_mode": None, + "debugger": None, "update_requirements": False, "update_resources": False, "update_support": False, @@ -1292,7 +1299,7 @@ def test_test_app_non_existent(build_command, first_app_config, second_app): "build_state": "first", "update_state": "second", "test_mode": True, - "debug_mode": None, + "debug_mode": False, }, ), ] @@ -1328,7 +1335,7 @@ def test_test_app_unbuilt(build_command, first_app_unbuilt, second_app): "first", { "test_mode": True, - "debug_mode": None, + "debugger": None, "update_requirements": False, "update_resources": False, "update_support": False, @@ -1342,7 +1349,7 @@ def test_test_app_unbuilt(build_command, first_app_unbuilt, second_app): ( "build", "first", - {"update_state": "first", "test_mode": True, "debug_mode": None}, + {"update_state": "first", "test_mode": True, "debug_mode": False}, ), # Second app has been built before; it will be built again. ( @@ -1352,7 +1359,7 @@ def test_test_app_unbuilt(build_command, first_app_unbuilt, second_app): "update_state": "first", "build_state": "first", "test_mode": True, - "debug_mode": None, + "debugger": None, "update_requirements": False, "update_resources": False, "update_support": False, @@ -1370,7 +1377,7 @@ def test_test_app_unbuilt(build_command, first_app_unbuilt, second_app): "update_state": "second", "build_state": "first", "test_mode": True, - "debug_mode": None, + "debug_mode": False, }, ), ] @@ -1406,7 +1413,7 @@ def test_build_app_single(build_command, first_app, second_app, app_flags): # App tools are verified for first app ("verify-app-tools", "first"), # Build the first app - ("build", "first", {"test_mode": False, "debug_mode": None}), + ("build", "first", {"test_mode": False, "debug_mode": False}), ] @@ -1485,7 +1492,7 @@ def test_build_app_all_flags(build_command, first_app, second_app): "first", { "test_mode": True, - "debug_mode": None, + "debugger": None, "update_requirements": True, "update_resources": True, "update_support": True, @@ -1500,6 +1507,6 @@ def test_build_app_all_flags(build_command, first_app, second_app): ( "build", "first", - {"update_state": "first", "test_mode": True, "debug_mode": None}, + {"update_state": "first", "test_mode": True, "debug_mode": False}, ), ] diff --git a/tests/commands/create/conftest.py b/tests/commands/create/conftest.py index 46102dfe0..65373b8ac 100644 --- a/tests/commands/create/conftest.py +++ b/tests/commands/create/conftest.py @@ -86,9 +86,7 @@ def python_version_tag(self): return "3.X" # Define output format-specific template context. - def output_format_template_context( - self, app: AppConfig, debug_mode: str | None = None - ): + def output_format_template_context(self, app: AppConfig, debug_mode: bool = False): return {"output_format": "dummy"} # Handle platform-specific permissions. diff --git a/tests/commands/create/test_generate_app_template.py b/tests/commands/create/test_generate_app_template.py index d2df9ca9e..0ded81faa 100644 --- a/tests/commands/create/test_generate_app_template.py +++ b/tests/commands/create/test_generate_app_template.py @@ -32,6 +32,7 @@ def full_context(): "test_sources": None, "test_requires": None, "debug_requires": None, + "debugger": None, "url": "https://example.com", "author": "First Last", "author_email": "first@example.com", diff --git a/tests/commands/run/test_call.py b/tests/commands/run/test_call.py index cf06d1860..c38dbcb0d 100644 --- a/tests/commands/run/test_call.py +++ b/tests/commands/run/test_call.py @@ -34,7 +34,7 @@ def test_no_args_one_app(run_command, first_app): "first", { "test_mode": False, - "debug_mode": None, + "debug_mode": False, "debugger_host": "localhost", "debugger_port": 5678, "passthrough": [], @@ -75,7 +75,7 @@ def test_no_args_one_app_with_passthrough(run_command, first_app): "first", { "test_mode": False, - "debug_mode": None, + "debug_mode": False, "debugger_host": "localhost", "debugger_port": 5678, "passthrough": ["foo", "--bar"], @@ -134,7 +134,7 @@ def test_with_arg_one_app(run_command, first_app): "first", { "test_mode": False, - "debug_mode": None, + "debug_mode": False, "debugger_host": "localhost", "debugger_port": 5678, "passthrough": [], @@ -175,7 +175,7 @@ def test_with_arg_two_apps(run_command, first_app, second_app): "second", { "test_mode": False, - "debug_mode": None, + "debug_mode": False, "debugger_host": "localhost", "debugger_port": 5678, "passthrough": [], @@ -232,7 +232,7 @@ def test_create_app_before_start(run_command, first_app_config): "first", { "test_mode": False, - "debug_mode": None, + "debugger": None, "update": False, "update_requirements": False, "update_resources": False, @@ -252,7 +252,8 @@ def test_create_app_before_start(run_command, first_app_config): { "build_state": "first", "test_mode": False, - "debug_mode": None, + "debug_mode": False, + "debugger": None, "debugger_host": "localhost", "debugger_port": 5678, "passthrough": [], @@ -288,7 +289,7 @@ def test_build_app_before_start(run_command, first_app_unbuilt): "first", { "test_mode": False, - "debug_mode": None, + "debugger": None, "update": False, "update_requirements": False, "update_resources": False, @@ -308,7 +309,8 @@ def test_build_app_before_start(run_command, first_app_unbuilt): { "build_state": "first", "test_mode": False, - "debug_mode": None, + "debug_mode": False, + "debugger": None, "debugger_host": "localhost", "debugger_port": 5678, "passthrough": [], @@ -344,7 +346,7 @@ def test_update_app(run_command, first_app): "first", { "test_mode": False, - "debug_mode": None, + "debugger": None, "update": True, "update_requirements": False, "update_resources": False, @@ -364,7 +366,8 @@ def test_update_app(run_command, first_app): { "build_state": "first", "test_mode": False, - "debug_mode": None, + "debug_mode": False, + "debugger": None, "debugger_host": "localhost", "debugger_port": 5678, "passthrough": [], @@ -400,7 +403,7 @@ def test_update_app_requirements(run_command, first_app): "first", { "test_mode": False, - "debug_mode": None, + "debugger": None, "update": False, "update_requirements": True, "update_resources": False, @@ -420,7 +423,8 @@ def test_update_app_requirements(run_command, first_app): { "build_state": "first", "test_mode": False, - "debug_mode": None, + "debug_mode": False, + "debugger": None, "debugger_host": "localhost", "debugger_port": 5678, "passthrough": [], @@ -456,7 +460,7 @@ def test_update_app_resources(run_command, first_app): "first", { "test_mode": False, - "debug_mode": None, + "debugger": None, "update": False, "update_requirements": False, "update_resources": True, @@ -476,7 +480,8 @@ def test_update_app_resources(run_command, first_app): { "build_state": "first", "test_mode": False, - "debug_mode": None, + "debug_mode": False, + "debugger": None, "debugger_host": "localhost", "debugger_port": 5678, "passthrough": [], @@ -512,7 +517,7 @@ def test_update_app_support(run_command, first_app): "first", { "test_mode": False, - "debug_mode": None, + "debugger": None, "update": False, "update_requirements": False, "update_resources": False, @@ -532,7 +537,8 @@ def test_update_app_support(run_command, first_app): { "build_state": "first", "test_mode": False, - "debug_mode": None, + "debug_mode": False, + "debugger": None, "debugger_host": "localhost", "debugger_port": 5678, "passthrough": [], @@ -568,7 +574,7 @@ def test_update_app_stub(run_command, first_app): "first", { "test_mode": False, - "debug_mode": None, + "debugger": None, "update": False, "update_requirements": False, "update_resources": False, @@ -588,7 +594,8 @@ def test_update_app_stub(run_command, first_app): { "build_state": "first", "test_mode": False, - "debug_mode": None, + "debug_mode": False, + "debugger": None, "debugger_host": "localhost", "debugger_port": 5678, "passthrough": [], @@ -625,7 +632,7 @@ def test_update_unbuilt_app(run_command, first_app_unbuilt): "first", { "test_mode": False, - "debug_mode": None, + "debugger": None, "update": True, "update_requirements": False, "update_resources": False, @@ -645,7 +652,8 @@ def test_update_unbuilt_app(run_command, first_app_unbuilt): { "build_state": "first", "test_mode": False, - "debug_mode": None, + "debug_mode": False, + "debugger": None, "debugger_host": "localhost", "debugger_port": 5678, "passthrough": [], @@ -682,7 +690,7 @@ def test_update_non_existent(run_command, first_app_config): "first", { "test_mode": False, - "debug_mode": None, + "debugger": None, "update": True, "update_requirements": False, "update_resources": False, @@ -702,7 +710,8 @@ def test_update_non_existent(run_command, first_app_config): { "build_state": "first", "test_mode": False, - "debug_mode": None, + "debug_mode": False, + "debugger": None, "debugger_host": "localhost", "debugger_port": 5678, "passthrough": [], @@ -738,7 +747,7 @@ def test_test_mode_existing_app(run_command, first_app): "first", { "test_mode": True, - "debug_mode": None, + "debugger": None, "update": False, "update_requirements": False, "update_resources": False, @@ -758,7 +767,8 @@ def test_test_mode_existing_app(run_command, first_app): { "build_state": "first", "test_mode": True, - "debug_mode": None, + "debug_mode": False, + "debugger": None, "debugger_host": "localhost", "debugger_port": 5678, "passthrough": [], @@ -794,7 +804,7 @@ def test_test_mode_existing_app_with_passthrough(run_command, first_app): "first", { "test_mode": True, - "debug_mode": None, + "debugger": None, "update": False, "update_requirements": False, "update_resources": False, @@ -814,7 +824,8 @@ def test_test_mode_existing_app_with_passthrough(run_command, first_app): { "build_state": "first", "test_mode": True, - "debug_mode": None, + "debug_mode": False, + "debugger": None, "debugger_host": "localhost", "debugger_port": 5678, "passthrough": ["foo", "--bar"], @@ -855,7 +866,7 @@ def test_test_mode_existing_app_no_update(run_command, first_app): "first", { "test_mode": True, - "debug_mode": None, + "debug_mode": False, "debugger_host": "localhost", "debugger_port": 5678, "passthrough": [], @@ -891,7 +902,7 @@ def test_test_mode_existing_app_update_requirements(run_command, first_app): "first", { "test_mode": True, - "debug_mode": None, + "debugger": None, "update": False, "update_requirements": True, "update_resources": False, @@ -911,7 +922,8 @@ def test_test_mode_existing_app_update_requirements(run_command, first_app): { "build_state": "first", "test_mode": True, - "debug_mode": None, + "debug_mode": False, + "debugger": None, "debugger_host": "localhost", "debugger_port": 5678, "passthrough": [], @@ -947,7 +959,7 @@ def test_test_mode_existing_app_update_resources(run_command, first_app): "first", { "test_mode": True, - "debug_mode": None, + "debugger": None, "update": False, "update_requirements": False, "update_resources": True, @@ -967,7 +979,8 @@ def test_test_mode_existing_app_update_resources(run_command, first_app): { "build_state": "first", "test_mode": True, - "debug_mode": None, + "debug_mode": False, + "debugger": None, "debugger_host": "localhost", "debugger_port": 5678, "passthrough": [], @@ -1003,7 +1016,7 @@ def test_test_mode_update_existing_app(run_command, first_app): "first", { "test_mode": True, - "debug_mode": None, + "debugger": None, "update": True, "update_requirements": False, "update_resources": False, @@ -1023,7 +1036,8 @@ def test_test_mode_update_existing_app(run_command, first_app): { "build_state": "first", "test_mode": True, - "debug_mode": None, + "debug_mode": False, + "debugger": None, "debugger_host": "localhost", "debugger_port": 5678, "passthrough": [], @@ -1059,7 +1073,7 @@ def test_test_mode_non_existent(run_command, first_app_config): "first", { "test_mode": True, - "debug_mode": None, + "debugger": None, "update": False, "update_requirements": False, "update_resources": False, @@ -1079,7 +1093,8 @@ def test_test_mode_non_existent(run_command, first_app_config): { "build_state": "first", "test_mode": True, - "debug_mode": None, + "debug_mode": False, + "debugger": None, "debugger_host": "localhost", "debugger_port": 5678, "passthrough": [], diff --git a/tests/platforms/android/gradle/test_run.py b/tests/platforms/android/gradle/test_run.py index bac2b91c9..7a6806e50 100644 --- a/tests/platforms/android/gradle/test_run.py +++ b/tests/platforms/android/gradle/test_run.py @@ -90,7 +90,7 @@ def test_device_option(run_command): "update_stub": False, "no_update": False, "test_mode": False, - "debug_mode": None, + "debugger": None, "debugger_host": "localhost", "debugger_port": 5678, "passthrough": [], @@ -116,7 +116,7 @@ def test_extra_emulator_args_option(run_command): "update_stub": False, "no_update": False, "test_mode": False, - "debug_mode": None, + "debugger": None, "debugger_host": "localhost", "debugger_port": 5678, "passthrough": [], @@ -140,7 +140,7 @@ def test_shutdown_on_exit_option(run_command): "update_stub": False, "no_update": False, "test_mode": False, - "debug_mode": None, + "debugger": None, "debugger_host": "localhost", "debugger_port": 5678, "passthrough": [], diff --git a/tests/platforms/iOS/xcode/test_run.py b/tests/platforms/iOS/xcode/test_run.py index 89260e113..7478c9d5b 100644 --- a/tests/platforms/iOS/xcode/test_run.py +++ b/tests/platforms/iOS/xcode/test_run.py @@ -46,7 +46,7 @@ def test_device_option(run_command): "update_stub": False, "no_update": False, "test_mode": False, - "debug_mode": None, + "debugger": None, "debugger_host": "localhost", "debugger_port": 5678, "passthrough": [], diff --git a/tests/platforms/web/static/test_run.py b/tests/platforms/web/static/test_run.py index f89ed8892..214065e4f 100644 --- a/tests/platforms/web/static/test_run.py +++ b/tests/platforms/web/static/test_run.py @@ -46,7 +46,7 @@ def test_default_options(run_command): "update_stub": False, "no_update": False, "test_mode": False, - "debug_mode": None, + "debugger": None, "debugger_host": "localhost", "debugger_port": 5678, "passthrough": [], @@ -72,7 +72,7 @@ def test_options(run_command): "update_stub": False, "no_update": False, "test_mode": False, - "debug_mode": None, + "debugger": None, "debugger_host": "localhost", "debugger_port": 5678, "passthrough": [], diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index 4d3dc89ce..b177ca1a5 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -280,7 +280,7 @@ def test_run_command( "update_stub": False, "no_update": False, "test_mode": False, - "debug_mode": None, + "debugger": None, "debugger_host": "localhost", "debugger_port": 5678, "passthrough": [], From 4446968931bfdfddcc5336ca4f46c8e982b86b43 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sun, 18 May 2025 17:55:17 +0200 Subject: [PATCH 022/131] make android debugging working again --- src/briefcase/platforms/android/gradle.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/briefcase/platforms/android/gradle.py b/src/briefcase/platforms/android/gradle.py index b0cafd28c..58442b71d 100644 --- a/src/briefcase/platforms/android/gradle.py +++ b/src/briefcase/platforms/android/gradle.py @@ -17,9 +17,9 @@ ) from briefcase.config import AppConfig, parsed_version from briefcase.console import ANSI_ESC_SEQ_RE_DEF -from briefcase.debuggers import get_debugger from briefcase.debuggers.base import ( AppPackagesPathMappings, + BaseDebugger, DebuggerConnectionMode, ) from briefcase.exceptions import BriefcaseCommandError @@ -524,7 +524,7 @@ def run_app( adb.kill() def establish_debugger_connection( - self, adb: ADB, debugger: str, debugger_port: int + self, adb: ADB, debugger: BaseDebugger, debugger_port: int ): """Forward/Reverse the ports necessary for remote debugging. @@ -533,13 +533,14 @@ def establish_debugger_connection( :param debugger: The debugger to use :param debugger_port: The port to forward/reverse for the debugger """ - debugger = get_debugger(debugger) if debugger.connection_mode == DebuggerConnectionMode.SERVER: adb.forward(debugger_port, debugger_port) elif debugger.connection_mode == DebuggerConnectionMode.CLIENT: adb.reverse(debugger_port, debugger_port) - def remove_debugger_connection(self, adb: ADB, debugger: str, debugger_port: int): + def remove_debugger_connection( + self, adb: ADB, debugger: BaseDebugger, debugger_port: int + ): """Remove Forward/Reverse of the ports necessary for remote debugging. :param app: The config object for the app @@ -547,7 +548,6 @@ def remove_debugger_connection(self, adb: ADB, debugger: str, debugger_port: int :param debugger: The debugger to use :param debugger_port: The port to forward/reverse for the debugger """ - debugger = get_debugger(debugger) if debugger.connection_mode == DebuggerConnectionMode.SERVER: adb.forward_remove(debugger_port) elif debugger.connection_mode == DebuggerConnectionMode.CLIENT: From 88b71a7099759a893da39136e7f28dc8a94e68c8 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sun, 18 May 2025 18:20:29 +0200 Subject: [PATCH 023/131] Only forward port on android when "debugger_host" is "localhost" --- src/briefcase/platforms/android/gradle.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/briefcase/platforms/android/gradle.py b/src/briefcase/platforms/android/gradle.py index 58442b71d..a826e01d0 100644 --- a/src/briefcase/platforms/android/gradle.py +++ b/src/briefcase/platforms/android/gradle.py @@ -434,6 +434,7 @@ def run_app( avd, extra_emulator_args ) + debugger_connection_established = False try: label = "test suite" if test_mode else "app" @@ -459,8 +460,12 @@ def run_app( env = {} if debug_mode: - with self.console.wait_bar("Establishing debugger connection..."): - self.establish_debugger_connection(adb, app.debugger, debugger_port) + if debugger_host == "localhost": + with self.console.wait_bar("Establishing debugger connection..."): + self.establish_debugger_connection( + adb, app.debugger, debugger_port + ) + debugger_connection_established = True env["BRIEFCASE_DEBUGGER"] = self.remote_debugger_config( app, test_mode, debugger_host, debugger_port ) @@ -516,7 +521,7 @@ def run_app( raise BriefcaseCommandError(f"Problem starting app {app.app_name!r}") finally: - if debug_mode: + if debugger_connection_established: with self.console.wait_bar("Stopping debugger connection..."): self.remove_debugger_connection(adb, app.debugger, debugger_port) if shutdown_on_exit: From e7fe385384fb02faecda6b036a65ca1938e25741 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sun, 18 May 2025 18:51:44 +0200 Subject: [PATCH 024/131] added documentation --- docs/reference/commands/build.rst | 18 ++++++ docs/reference/commands/run.rst | 98 +++++++++++++++---------------- 2 files changed, 67 insertions(+), 49 deletions(-) diff --git a/docs/reference/commands/build.rst b/docs/reference/commands/build.rst index b1fd77dc4..4245cf504 100644 --- a/docs/reference/commands/build.rst +++ b/docs/reference/commands/build.rst @@ -113,6 +113,24 @@ If you have previously run the app in "normal" mode, you may need to pass ``-r`` / ``--update-requirements`` the first time you build in test mode to ensure that your testing requirements are present in the test app. +``--debug `` +---------- + +Build the app in debug mode in the bundled app environment and establish an +debugger connection via a socket. This installs the selected debugger in the +bundled app. + +Currently the following debuggers are supported: + + - ``'pdb'``: This is used for debugging via console. + - ``'debugpy'``: This is used for debugging via VSCode. + +It also optimizes the app for debugging. E.g. on android it ensures, that all +`.py` files are extracted from the apk and are accessible for the debugger. + +This option may slow down the app a little bit. + + ``--no-update`` --------------- diff --git a/docs/reference/commands/run.rst b/docs/reference/commands/run.rst index 915ad3bd8..e4e84a8d8 100644 --- a/docs/reference/commands/run.rst +++ b/docs/reference/commands/run.rst @@ -39,53 +39,26 @@ Debug mode The debug mode can be used to (remote) debug an bundled app. The debugger to use can be configured via ``pyproject.toml`` an can then be activated through -``run --debug``. +``briefcase run --debug ``. -This is incredible useful when developing an iOS or Android app that can't be -debugged via ``briefcase dev``. +This is useful when developing an iOS or Android app that can't be debugged +via ``briefcase dev``. To debug an bundled app you need a socket connection from your host system to -the device running your bundled app. Depending on the debugger selected the -bundled app is either a socket server or socket client. - -This feature needs to inject some code into your app to create the socket -connection. So you have to add ``import __briefcase_startup__`` to your -``__main__.py`` to get this running. -TODO: Better use .pth Files for this, so no modification of the app is needed. - But using this currently creates an import error on android... - -Some debuggers (like ``debugpy``) also support the mapping of the source code -from your bundled app to your local copy of the apps source code in the -``build`` folder. - -The configuration is done via ``pyproject.toml``: - -- ``debugger``: - - ``pdb`` (default): - This is used for debugging via console. - Currently it only supports ``debgger_mode=server``. - It creates a socket server that streams all stdout/stderr/stdin to - the host PC. The host PC can connect to it via: - - ``telnet localhost 5678`` - - ``nc -C localhost 5678`` - - ``socat readline tcp:localhost:5678``. - The app will start after the connection is established. - - ``debugpy``: - This is used for debugging via VSCode: - - If ``debgger_mode=server``: The bundled app creates an socket server an VSCode can connect to it. - - If ``debgger_mode=client``: This is used for debugging via VSCode. VSCode has to create an socket server and the bundled app connects to VSCode. - - ``debugpy-client``: - This is used for debugging via VSCode. VSCode has to create an socket server and the bundled app connects to VSCode. -- ``debugger_mode``: - - ``server`` (default): - The bundled app creates an socket server and the host system connects to it. - - ``client``: - The bundled app creates an socket client and the host system creates an socket server. - Note, that not all debuggers support all modes. -- ``debugger_ip``: - Specifies the IP of the socket connection (defaults to ``localhost``) -- ``debugger_port``: - Specifies the Port of the socket connection (defaults to ``5678``) +the device running your bundled app. For the iOS simulator the host pc and the +iOS simulator share the same network. For Android briefcase ensures that the +port is forwarded from the android device to the host pc via adb. + +Currently the following debuggers are supported: + +- ``pdb``: This is used for debugging via console. After starting the app + you can connect to it depending on your host system via + - ``telnet localhost 5678`` + - ``nc -C localhost 5678`` + - ``socat readline tcp:localhost:5678`` + The app will start after the connection is established. + +- ``debugpy``: This is used for debugging via VSCode (see TODO) Usage @@ -191,13 +164,40 @@ contains the most recent test code. To prevent this update and build, use the Prevent the automated update and build of app code that is performed when specifying by the ``--test`` option. -``--debug`` ----------- +``--debug `` +---------------------- + +Run the app in debug mode in the bundled app environment and establish an +debugger connection via a socket. + +Currently the following debuggers are supported: + + - ``'pdb'``: This is used for debugging via console. + - ``'debugpy'``: This is used for debugging via VSCode. + +For ``debugpy`` there is also a mapping of the source code from your bundled +app to your local copy of the apps source code in the ``build`` folder. This +is useful for devices like iOS and Android, where the running source code is +not available on the host system. + +``--debugger-host `` +-------------------------- + +Specifies the host of the socket connection for the debugger. This +option is only used when the ``--debug `` option is specified. The +default value is ``localhost``. + +``--debugger-port `` +-------------------------- + +Specifies the port of the socket connection for the debugger. This +option is only used when the ``--debug `` option is specified. The +default value is ``5678``. + +On Android this also forwards the port from the android device to the host pc +via adb if the port is ``localhost``. -Run the app in debug mode in the bundled app environment. This option can be used -standalone or in combination with the test mode ``run --test --debug``. -On Android this also forwards the ``debugger_port`` from the android device to the host pc via adb. Passthrough arguments --------------------- From 3d1e776312dc81f2275f7a83496120a348912e78 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sun, 18 May 2025 19:05:35 +0200 Subject: [PATCH 025/131] add a small how to for debugging with vscode --- docs/how-to/debugging_vscode.rst | 82 ++++++++++++++++++++++++++++++++ docs/how-to/index.rst | 1 + 2 files changed, 83 insertions(+) create mode 100644 docs/how-to/debugging_vscode.rst diff --git a/docs/how-to/debugging_vscode.rst b/docs/how-to/debugging_vscode.rst new file mode 100644 index 000000000..dffbb4649 --- /dev/null +++ b/docs/how-to/debugging_vscode.rst @@ -0,0 +1,82 @@ +Debugging with VSCode +===================== +Debugging is possible at different stages in your development process. It is +different to debug a development app via `briefcase dev` than an bundled app +that is build via `briefcase build` and run via `briefcase run`. + +Development +----------- +During development on your host system you should use `briefcase dev`. To +attach VSCode debugger you can simply create a configuration like this, +that runs `briefcase dev` for you and attaches a debugger. + +``` +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Briefcase: Dev", + "type": "debugpy", + "request": "launch", + "module": "briefcase", + "args": [ + "dev", + ], + "justMyCode": false + }, + ] +} +``` + +Bundled App +----------- +It is also possible to debug a bundled app. This is the only way to debug your +app on a mobile device (iOS/Android). + +For this you need to embed a remote debugger into your app. This is done via: + +``` +briefcase build --debug debugpy +``` + +This will build your app in debug mode and add the `debugpy` package to your +app. Additionally it will optimize the app for debugging. This means e.g. that +all `.py` files are accessible on the device. + +Then it is time to run your app. You can do this via: + +``` +briefcase run --debug debugpy +``` + +Running the app in debug mode will automatically start the `debugpy` debugger +and listen for incoming connections. By default it will listen on `localhost` +and port `5678`. You can then connect your VSCode debugger to the app by +creating a configuration like this in the `launch.json` file: + +``` +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Briefcase: Attach", + "type": "debugpy", + "request": "attach", + "connect": { + "host": "localhost", + "port": 5678 + } + } + ] +} + +Note, that you need an network connection to the device your are trying to +debug. If your bundled app is running on your host system this is no problem. +But when debugging a mobile device your app is running on another device. +Running an iOS app in simulator is also no problem, because the simulator +shares the same network stack as your host. But on Android there is a separate +network stack. That's why briefcase will automatically forward the port from +your host to the android device via `adb` (Android Debug Bridge). + +Now you are ready to debug your app. You can set breakpoints in your code, use +the "Debug Console" and all other debugging features of VSCode :) diff --git a/docs/how-to/index.rst b/docs/how-to/index.rst index a905a6696..9d793d372 100644 --- a/docs/how-to/index.rst +++ b/docs/how-to/index.rst @@ -21,3 +21,4 @@ stand alone. upgrade-from-v0.2 contribute/index internal/index + debugging_vscode From fbb0c33fbba3b2d4e11ca9a45267156e6f3d7efe Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Mon, 19 May 2025 19:42:58 +0200 Subject: [PATCH 026/131] fixed a few pytest errors --- src/briefcase/platforms/web/static.py | 4 +++- src/briefcase/platforms/windows/__init__.py | 2 ++ .../linux/test_LocalRequirementsMixin.py | 24 ++++++++++++++----- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index 5cfca9085..ee83a0b8c 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import errno import subprocess import sys @@ -238,7 +240,7 @@ def build_app(self, app: AppConfig, **kwargs): class HTTPHandler(SimpleHTTPRequestHandler): """Convert any HTTP request into a path request on the static content folder.""" - server: "LocalHTTPServer" + server: LocalHTTPServer def translate_path(self, path): return str(self.server.base_path / path[1:]) diff --git a/src/briefcase/platforms/windows/__init__.py b/src/briefcase/platforms/windows/__init__.py index 942eb0243..5853f9406 100644 --- a/src/briefcase/platforms/windows/__init__.py +++ b/src/briefcase/platforms/windows/__init__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import re import subprocess import uuid diff --git a/tests/platforms/linux/test_LocalRequirementsMixin.py b/tests/platforms/linux/test_LocalRequirementsMixin.py index ee6633c78..ae6b39ef1 100644 --- a/tests/platforms/linux/test_LocalRequirementsMixin.py +++ b/tests/platforms/linux/test_LocalRequirementsMixin.py @@ -152,7 +152,9 @@ def test_install_app_requirements_in_docker(create_command, first_app_config, tm """If Docker is in use, a docker context is used to invoke pip.""" # Install requirements - create_command.install_app_requirements(first_app_config, test_mode=False) + create_command.install_app_requirements( + first_app_config, test_mode=False, debug_mode=False + ) # pip was invoked inside docker. create_command.tools.subprocess.run.assert_called_once_with( @@ -204,7 +206,9 @@ def test_install_app_requirements_no_docker( no_docker_create_command.verify_app_tools(first_app_config) # Install requirements - no_docker_create_command.install_app_requirements(first_app_config, test_mode=False) + no_docker_create_command.install_app_requirements( + first_app_config, test_mode=False, debug_mode=False + ) # Docker is not verified. assert not hasattr(no_docker_create_command.tools, "docker") @@ -282,7 +286,9 @@ def build_sdist(*args, **kwargs): create_command.tools.subprocess.check_output.side_effect = build_sdist # Install requirements - create_command.install_app_requirements(first_app_config, test_mode=False) + create_command.install_app_requirements( + first_app_config, test_mode=False, debug_mode=False + ) # An sdist was built for the local package create_command.tools.subprocess.check_output.assert_called_once_with( @@ -383,7 +389,9 @@ def test_install_app_requirements_with_bad_local( BriefcaseCommandError, match=r"Unable to build sdist for .*/local/first", ): - create_command.install_app_requirements(first_app_config, test_mode=False) + create_command.install_app_requirements( + first_app_config, test_mode=False, debug_mode=False + ) # An attempt to build the sdist was made create_command.tools.subprocess.check_output.assert_called_once_with( @@ -428,7 +436,9 @@ def test_install_app_requirements_with_missing_local_build( BriefcaseCommandError, match=r"Unable to find local requirement .*/local/first", ): - create_command.install_app_requirements(first_app_config, test_mode=False) + create_command.install_app_requirements( + first_app_config, test_mode=False, debug_mode=False + ) # No attempt to build the sdist was made create_command.tools.subprocess.check_output.assert_not_called() @@ -460,7 +470,9 @@ def test_install_app_requirements_with_bad_local_file( BriefcaseCommandError, match=r"Unable to find local requirement .*/local/missing-2.3.4.tar.gz", ): - create_command.install_app_requirements(first_app_config, test_mode=False) + create_command.install_app_requirements( + first_app_config, test_mode=False, debug_mode=False + ) # An attempt was made to copy the package create_command.tools.shutil.copy.assert_called_once_with( From ddaf6a16ed771d39d4aa9ffe221687b7bf8d379e Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Mon, 19 May 2025 20:04:55 +0200 Subject: [PATCH 027/131] debug_mode is bool --- tests/commands/create/conftest.py | 2 +- .../create/test_install_app_requirements.py | 56 ++++++++++--------- tests/commands/update/test_update_app.py | 20 +++---- tests/platforms/android/gradle/test_run.py | 18 +++--- tests/platforms/iOS/xcode/test_create.py | 6 +- tests/platforms/iOS/xcode/test_run.py | 30 +++++----- tests/platforms/iOS/xcode/test_update.py | 2 +- tests/platforms/linux/appimage/test_run.py | 16 +++--- tests/platforms/linux/flatpak/test_run.py | 16 +++--- tests/platforms/linux/system/test_run.py | 24 ++++---- tests/platforms/macOS/app/test_create.py | 12 ++-- tests/platforms/macOS/app/test_run.py | 20 +++---- tests/platforms/macOS/xcode/test_run.py | 8 +-- tests/platforms/web/static/test_run.py | 16 +++--- tests/platforms/windows/app/test_run.py | 16 +++--- .../windows/visualstudio/test_run.py | 8 +-- tests/test_mainline.py | 8 +-- 17 files changed, 142 insertions(+), 136 deletions(-) diff --git a/tests/commands/create/conftest.py b/tests/commands/create/conftest.py index 65373b8ac..9d118fdba 100644 --- a/tests/commands/create/conftest.py +++ b/tests/commands/create/conftest.py @@ -152,7 +152,7 @@ def verify_app_tools(self, app): # Override all the body methods of a CreateCommand # with versions that we can use to track actions performed. - def generate_app_template(self, app, debug_mode=None): + def generate_app_template(self, app, debug_mode=False): self.actions.append(("generate", app.app_name)) # A mock version of template generation. diff --git a/tests/commands/create/test_install_app_requirements.py b/tests/commands/create/test_install_app_requirements.py index c8fb21da7..401a26f0a 100644 --- a/tests/commands/create/test_install_app_requirements.py +++ b/tests/commands/create/test_install_app_requirements.py @@ -80,7 +80,9 @@ def test_bad_path_index(create_command, myapp, bundle_path, app_requirements_pat BriefcaseCommandError, match=r"Application path index file does not define `app_requirements_path` or `app_packages_path`", ): - create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) + create_command.install_app_requirements( + myapp, test_mode=False, debug_mode=False + ) # pip wasn't invoked create_command.tools[myapp].app_context.run.assert_not_called() @@ -102,7 +104,7 @@ def test_app_packages_no_requires( """If an app has no requirements, install_app_requirements is a no-op.""" myapp.requires = None - create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) + create_command.install_app_requirements(myapp, test_mode=False, debug_mode=False) # No request was made to install requirements create_command.tools[myapp].app_context.run.assert_not_called() @@ -117,7 +119,7 @@ def test_app_packages_empty_requires( """If an app has an empty requirements list, install_app_requirements is a no-op.""" myapp.requires = [] - create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) + create_command.install_app_requirements(myapp, test_mode=False, debug_mode=False) # No request was made to install requirements create_command.tools[myapp].app_context.run.assert_not_called() @@ -132,7 +134,7 @@ def test_app_packages_valid_requires( """If an app has a valid list of requirements, pip is invoked.""" myapp.requires = ["first", "second==1.2.3", "third>=3.2.1"] - create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) + create_command.install_app_requirements(myapp, test_mode=False, debug_mode=False) # A request was made to install requirements create_command.tools[myapp].app_context.run.assert_called_with( @@ -173,7 +175,7 @@ def test_app_packages_requirement_installer_args_no_paths( myapp.requirement_installer_args = ["--no-cache"] myapp.requires = ["package"] - create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) + create_command.install_app_requirements(myapp, test_mode=False, debug_mode=False) # A request was made to install requirements create_command.tools[myapp].app_context.run.assert_called_with( @@ -210,7 +212,7 @@ def test_app_packages_requirement_installer_args_path_transformed( myapp.requirement_installer_args = ["--extra-index-url", "./packages"] myapp.requires = ["package"] - create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) + create_command.install_app_requirements(myapp, test_mode=False, debug_mode=False) # A request was made to install requirements create_command.tools[myapp].app_context.run.assert_called_with( @@ -248,7 +250,7 @@ def test_app_packages_requirement_installer_args_coincidental_path_not_transform myapp.requirement_installer_args = ["-f./wheels"] myapp.requires = ["package"] - create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) + create_command.install_app_requirements(myapp, test_mode=False, debug_mode=False) # A request was made to install requirements create_command.tools[myapp].app_context.run.assert_called_with( @@ -285,7 +287,7 @@ def test_app_packages_requirement_installer_args_path_not_transformed( myapp.requirement_installer_args = ["--extra-index-url", "./packages"] myapp.requires = ["package"] - create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) + create_command.install_app_requirements(myapp, test_mode=False, debug_mode=False) # A request was made to install requirements create_command.tools[myapp].app_context.run.assert_called_with( @@ -323,7 +325,7 @@ def test_app_packages_requirement_installer_args_combined_argument_not_transform myapp.requirement_installer_args = ["--extra-index-url=./packages"] myapp.requires = ["package"] - create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) + create_command.install_app_requirements(myapp, test_mode=False, debug_mode=False) # A request was made to install requirements create_command.tools[myapp].app_context.run.assert_called_with( @@ -362,7 +364,7 @@ def test_app_packages_valid_requires_no_support_package( "paths": {"app_packages_path": "path/to/app_packages"} } - create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) + create_command.install_app_requirements(myapp, test_mode=False, debug_mode=False) # A request was made to install requirements create_command.tools[myapp].app_context.run.assert_called_with( @@ -410,7 +412,9 @@ def test_app_packages_invalid_requires( ) with pytest.raises(RequirementsInstallError): - create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) + create_command.install_app_requirements( + myapp, test_mode=False, debug_mode=False + ) # But the request to install was still made create_command.tools[myapp].app_context.run.assert_called_with( @@ -456,7 +460,9 @@ def test_app_packages_offline( ) with pytest.raises(RequirementsInstallError): - create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) + create_command.install_app_requirements( + myapp, test_mode=False, debug_mode=False + ) # But the request to install was still made create_command.tools[myapp].app_context.run.assert_called_with( @@ -506,7 +512,7 @@ def test_app_packages_install_requirements( ) # Install the requirements - create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) + create_command.install_app_requirements(myapp, test_mode=False, debug_mode=False) # The request to install was made create_command.tools[myapp].app_context.run.assert_called_with( @@ -561,7 +567,7 @@ def test_app_packages_replace_existing_requirements( ) # Install the requirements - create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) + create_command.install_app_requirements(myapp, test_mode=False, debug_mode=False) # The request to install was still made create_command.tools[myapp].app_context.run.assert_called_with( @@ -613,7 +619,7 @@ def test_app_requirements_no_requires( myapp.requires = None # Install requirements into the bundle - create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) + create_command.install_app_requirements(myapp, test_mode=False, debug_mode=False) # requirements.txt doesn't exist either assert app_requirements_path.exists() @@ -637,7 +643,7 @@ def test_app_requirements_empty_requires( myapp.requires = [] # Install requirements into the bundle - create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) + create_command.install_app_requirements(myapp, test_mode=False, debug_mode=False) # requirements.txt doesn't exist either assert app_requirements_path.exists() @@ -661,7 +667,7 @@ def test_app_requirements_requires( myapp.requires = ["first", "second==1.2.3", "third>=3.2.1"] # Install requirements into the bundle - create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) + create_command.install_app_requirements(myapp, test_mode=False, debug_mode=False) # requirements.txt doesn't exist either assert app_requirements_path.exists() @@ -688,7 +694,7 @@ def test_app_requirements_requirement_installer_args_no_template_support( myapp.requires = ["my-favourite-package"] # Install requirements into the bundle - create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) + create_command.install_app_requirements(myapp, test_mode=False, debug_mode=False) # requirements.txt exists either assert app_requirements_path.exists() @@ -715,7 +721,7 @@ def test_app_requirements_requirement_installer_args_with_template_support( myapp.requires = ["my-favourite-package"] # Install requirements into the bundle - create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) + create_command.install_app_requirements(myapp, test_mode=False, debug_mode=False) # requirements.txt exists either assert app_requirements_path.exists() @@ -749,7 +755,7 @@ def test_app_requirements_requirement_installer_args_without_requires_no_templat myapp.requires = [] # Install requirements into the bundle - create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) + create_command.install_app_requirements(myapp, test_mode=False, debug_mode=False) # requirements.txt exists either assert app_requirements_path.exists() @@ -779,7 +785,7 @@ def test_app_requirements_requirement_installer_args_without_requires_with_templ myapp.requires = [] # Install requirements into the bundle - create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) + create_command.install_app_requirements(myapp, test_mode=False, debug_mode=False) # requirements.txt exists either assert app_requirements_path.exists() @@ -833,7 +839,7 @@ def _test_app_requirements_paths( converted = requirement myapp.requires = ["first", requirement, "third"] - create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) + create_command.install_app_requirements(myapp, test_mode=False, debug_mode=False) with app_requirements_path.open(encoding="utf-8") as f: assert f.read() == ( "\n".join( @@ -972,7 +978,7 @@ def test_app_packages_test_requires( myapp.requires = ["first", "second==1.2.3", "third>=3.2.1"] myapp.test_requires = ["pytest", "pytest-tldr"] - create_command.install_app_requirements(myapp, test_mode=False, debug_mode=None) + create_command.install_app_requirements(myapp, test_mode=False, debug_mode=False) # A request was made to install requirements create_command.tools[myapp].app_context.run.assert_called_with( @@ -1011,7 +1017,7 @@ def test_app_packages_test_requires_test_mode( myapp.requires = ["first", "second==1.2.3", "third>=3.2.1"] myapp.test_requires = ["pytest", "pytest-tldr"] - create_command.install_app_requirements(myapp, test_mode=True, debug_mode=None) + create_command.install_app_requirements(myapp, test_mode=True, debug_mode=False) # A request was made to install requirements create_command.tools[myapp].app_context.run.assert_called_with( @@ -1053,7 +1059,7 @@ def test_app_packages_only_test_requires_test_mode( myapp.requires = None myapp.test_requires = ["pytest", "pytest-tldr"] - create_command.install_app_requirements(myapp, test_mode=True, debug_mode=None) + create_command.install_app_requirements(myapp, test_mode=True, debug_mode=False) # A request was made to install requirements create_command.tools[myapp].app_context.run.assert_called_with( diff --git a/tests/commands/update/test_update_app.py b/tests/commands/update/test_update_app.py index ba5748587..02d1b17b0 100644 --- a/tests/commands/update/test_update_app.py +++ b/tests/commands/update/test_update_app.py @@ -7,7 +7,7 @@ def test_update_app(update_command, first_app, tmp_path): update_support=False, update_stub=False, test_mode=False, - debug_mode=None, + debug_mode=False, ) # The right sequence of things will be done @@ -41,7 +41,7 @@ def test_update_non_existing_app(update_command, tmp_path): update_support=False, update_stub=False, test_mode=False, - debug_mode=None, + debug_mode=False, ) # No app creation actions will be performed @@ -61,7 +61,7 @@ def test_update_app_with_requirements(update_command, first_app, tmp_path): update_support=False, update_stub=False, test_mode=False, - debug_mode=None, + debug_mode=False, ) # The right sequence of things will be done @@ -95,7 +95,7 @@ def test_update_app_with_resources(update_command, first_app, tmp_path): update_support=False, update_stub=False, test_mode=False, - debug_mode=None, + debug_mode=False, ) # The right sequence of things will be done @@ -129,7 +129,7 @@ def test_update_app_with_support_package(update_command, first_app, tmp_path): update_support=True, update_stub=False, test_mode=False, - debug_mode=None, + debug_mode=False, ) # The right sequence of things will be done @@ -169,7 +169,7 @@ def test_update_app_with_stub(update_command, first_app, tmp_path): update_support=False, update_stub=True, test_mode=False, - debug_mode=None, + debug_mode=False, ) # The right sequence of things will be done @@ -205,7 +205,7 @@ def test_update_app_stub_without_stub(update_command, first_app, tmp_path): update_support=False, update_stub=True, test_mode=False, - debug_mode=None, + debug_mode=False, ) # The right sequence of things will be done @@ -239,7 +239,7 @@ def test_update_app_test_mode(update_command, first_app, tmp_path): update_resources=False, update_support=False, update_stub=False, - debug_mode=None, + debug_mode=False, ) # The right sequence of things will be done @@ -273,7 +273,7 @@ def test_update_app_test_mode_requirements(update_command, first_app, tmp_path): update_resources=False, update_support=False, update_stub=False, - debug_mode=None, + debug_mode=False, ) # The right sequence of things will be done @@ -308,7 +308,7 @@ def test_update_app_test_mode_resources(update_command, first_app, tmp_path): update_resources=True, update_support=False, update_stub=False, - debug_mode=None, + debug_mode=False, ) # The right sequence of things will be done diff --git a/tests/platforms/android/gradle/test_run.py b/tests/platforms/android/gradle/test_run.py index 7a6806e50..97d30eb0a 100644 --- a/tests/platforms/android/gradle/test_run.py +++ b/tests/platforms/android/gradle/test_run.py @@ -202,7 +202,7 @@ def mock_stream_output(app, stop_func, **kwargs): first_app_config, device_or_avd="exampleDevice", test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -278,7 +278,7 @@ def mock_stream_output(app, stop_func, **kwargs): first_app_config, device_or_avd="exampleDevice", test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=["foo", "--bar"], @@ -345,7 +345,7 @@ def test_run_slow_start(run_command, first_app_config, monkeypatch): first_app_config, device_or_avd="exampleDevice", test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -402,7 +402,7 @@ def test_run_crash_at_start(run_command, first_app_config, monkeypatch): first_app_config, device_or_avd="exampleDevice", test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -445,7 +445,7 @@ def test_run_created_emulator(run_command, first_app_config): run_command.run_app( first_app_config, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -516,7 +516,7 @@ def test_run_idle_device(run_command, first_app_config): run_command.run_app( first_app_config, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -626,7 +626,7 @@ def mock_stream_output(app, stop_func, **kwargs): first_app_config, device_or_avd="exampleDevice", test_mode=True, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -703,7 +703,7 @@ def mock_stream_output(app, stop_func, **kwargs): first_app_config, device_or_avd="exampleDevice", test_mode=True, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=["foo", "--bar"], @@ -775,7 +775,7 @@ def test_run_test_mode_created_emulator(run_command, first_app_config): run_command.run_app( first_app_config, test_mode=True, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], diff --git a/tests/platforms/iOS/xcode/test_create.py b/tests/platforms/iOS/xcode/test_create.py index 3665a7712..beb8e6521 100644 --- a/tests/platforms/iOS/xcode/test_create.py +++ b/tests/platforms/iOS/xcode/test_create.py @@ -73,7 +73,7 @@ def test_extra_pip_args( ) create_command.install_app_requirements( - first_app_generated, test_mode=False, debug_mode=None + first_app_generated, test_mode=False, debug_mode=False ) bundle_path = tmp_path / "base_path/build/first-app/ios/xcode" @@ -158,7 +158,7 @@ def test_min_os_version(create_command, first_app_generated, tmp_path): ) create_command.install_app_requirements( - first_app_generated, test_mode=False, debug_mode=None + first_app_generated, test_mode=False, debug_mode=False ) bundle_path = tmp_path / "base_path/build/first-app/ios/xcode" @@ -249,7 +249,7 @@ def test_incompatible_min_os_version(create_command, first_app_generated, tmp_pa ), ): create_command.install_app_requirements( - first_app_generated, test_mode=False, debug_mode=None + first_app_generated, test_mode=False, debug_mode=False ) create_command.tools[first_app_generated].app_context.run.assert_not_called() diff --git a/tests/platforms/iOS/xcode/test_run.py b/tests/platforms/iOS/xcode/test_run.py index 7478c9d5b..a5882fbfb 100644 --- a/tests/platforms/iOS/xcode/test_run.py +++ b/tests/platforms/iOS/xcode/test_run.py @@ -78,7 +78,7 @@ def test_run_multiple_devices_input_disabled(run_command, first_app_config): run_command.run_app( first_app_config, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -119,7 +119,7 @@ def test_run_app_simulator_booted(run_command, first_app_config, tmp_path): run_command.run_app( first_app_config, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -258,7 +258,7 @@ def test_run_app_simulator_booted_underscore( run_command.run_app( underscore_app_config, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -389,7 +389,7 @@ def test_run_app_with_passthrough(run_command, first_app_config, tmp_path): run_command.run_app( first_app_config, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=["foo", "--bar"], @@ -528,7 +528,7 @@ def test_run_app_simulator_shut_down( run_command.run_app( first_app_config, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -677,7 +677,7 @@ def test_run_app_simulator_shutting_down(run_command, first_app_config, tmp_path run_command.run_app( first_app_config, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -801,7 +801,7 @@ def test_run_app_simulator_boot_failure(run_command, first_app_config): run_command.run_app( first_app_config, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -852,7 +852,7 @@ def test_run_app_simulator_open_failure(run_command, first_app_config): run_command.run_app( first_app_config, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -910,7 +910,7 @@ def test_run_app_simulator_uninstall_failure(run_command, first_app_config): run_command.run_app( first_app_config, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -989,7 +989,7 @@ def test_run_app_simulator_install_failure(run_command, first_app_config, tmp_pa run_command.run_app( first_app_config, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -1089,7 +1089,7 @@ def test_run_app_simulator_launch_failure(run_command, first_app_config, tmp_pat run_command.run_app( first_app_config, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -1217,7 +1217,7 @@ def test_run_app_simulator_no_pid(run_command, first_app_config, tmp_path): run_command.run_app( first_app_config, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -1347,7 +1347,7 @@ def test_run_app_simulator_non_integer_pid(run_command, first_app_config, tmp_pa run_command.run_app( first_app_config, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -1476,7 +1476,7 @@ def test_run_app_test_mode(run_command, first_app_config, tmp_path): run_command.run_app( first_app_config, test_mode=True, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -1593,7 +1593,7 @@ def test_run_app_test_mode_with_passthrough(run_command, first_app_config, tmp_p run_command.run_app( first_app_config, test_mode=True, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=["foo", "--bar"], diff --git a/tests/platforms/iOS/xcode/test_update.py b/tests/platforms/iOS/xcode/test_update.py index 97155eafb..860f152a6 100644 --- a/tests/platforms/iOS/xcode/test_update.py +++ b/tests/platforms/iOS/xcode/test_update.py @@ -60,7 +60,7 @@ def test_extra_pip_args( ) update_command.install_app_requirements( - first_app_generated, test_mode=False, debug_mode=None + first_app_generated, test_mode=False, debug_mode=False ) bundle_path = tmp_path / "base_path/build/first-app/ios/xcode" diff --git a/tests/platforms/linux/appimage/test_run.py b/tests/platforms/linux/appimage/test_run.py index 5e7aa8996..9755ee77b 100644 --- a/tests/platforms/linux/appimage/test_run.py +++ b/tests/platforms/linux/appimage/test_run.py @@ -52,7 +52,7 @@ def test_run_gui_app(run_command, first_app_config, tmp_path): run_command.run_app( first_app_config, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -91,7 +91,7 @@ def test_run_gui_app_with_passthrough(run_command, first_app_config, tmp_path): run_command.run_app( first_app_config, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=["foo", "--bar"], @@ -129,7 +129,7 @@ def test_run_gui_app_failed(run_command, first_app_config, tmp_path): run_command.run_app( first_app_config, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -159,7 +159,7 @@ def test_run_console_app(run_command, first_app_config, tmp_path): run_command.run_app( first_app_config, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -190,7 +190,7 @@ def test_run_console_app_with_passthrough(run_command, first_app_config, tmp_pat run_command.run_app( first_app_config, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=["foo", "--bar"], @@ -224,7 +224,7 @@ def test_run_console_app_failed(run_command, first_app_config, tmp_path): run_command.run_app( first_app_config, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -259,7 +259,7 @@ def test_run_app_test_mode(run_command, first_app_config, is_console_app, tmp_pa run_command.run_app( first_app_config, test_mode=True, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -306,7 +306,7 @@ def test_run_app_test_mode_with_args( run_command.run_app( first_app_config, test_mode=True, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=["foo", "--bar"], diff --git a/tests/platforms/linux/flatpak/test_run.py b/tests/platforms/linux/flatpak/test_run.py index b45a0269b..52a17df9f 100644 --- a/tests/platforms/linux/flatpak/test_run.py +++ b/tests/platforms/linux/flatpak/test_run.py @@ -33,7 +33,7 @@ def test_run_gui_app(run_command, first_app_config): run_command.run_app( first_app_config, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -67,7 +67,7 @@ def test_run_gui_app_with_passthrough(run_command, first_app_config): run_command.run_app( first_app_config, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=["foo", "--bar"], @@ -98,7 +98,7 @@ def test_run_gui_app_failed(run_command, first_app_config, tmp_path): run_command.run_app( first_app_config, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -123,7 +123,7 @@ def test_run_console_app(run_command, first_app_config): run_command.run_app( first_app_config, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -149,7 +149,7 @@ def test_run_console_app_with_passthrough(run_command, first_app_config): run_command.run_app( first_app_config, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=["foo", "--bar"], @@ -177,7 +177,7 @@ def test_run_console_app_failed(run_command, first_app_config, tmp_path): run_command.run_app( first_app_config, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -208,7 +208,7 @@ def test_run_test_mode(run_command, first_app_config, is_console_app): run_command.run_app( first_app_config, test_mode=True, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -245,7 +245,7 @@ def test_run_test_mode_with_args(run_command, first_app_config, is_console_app): run_command.run_app( first_app_config, test_mode=True, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=["foo", "--bar"], diff --git a/tests/platforms/linux/system/test_run.py b/tests/platforms/linux/system/test_run.py index 16b557966..3a53c2d44 100644 --- a/tests/platforms/linux/system/test_run.py +++ b/tests/platforms/linux/system/test_run.py @@ -248,7 +248,7 @@ def test_run_gui_app(run_command, first_app, sub_kw, tmp_path): run_command.run_app( first_app, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -295,7 +295,7 @@ def test_run_gui_app_passthrough(run_command, first_app, sub_kw, tmp_path): run_command.run_app( first_app, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=["foo", "--bar"], @@ -344,7 +344,7 @@ def test_run_gui_app_failed(run_command, first_app, sub_kw, tmp_path): run_command.run_app( first_app, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -380,7 +380,7 @@ def test_run_console_app(run_command, first_app, tmp_path): run_command.run_app( first_app, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -416,7 +416,7 @@ def test_run_console_app_passthrough(run_command, first_app, tmp_path): run_command.run_app( first_app, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=["foo", "--bar"], @@ -455,7 +455,7 @@ def test_run_console_app_failed(run_command, first_app, sub_kw, tmp_path): run_command.run_app( first_app, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -504,7 +504,7 @@ def test_run_app_docker(run_command, first_app, sub_kw, tmp_path, monkeypatch): run_command.run_app( first_app, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -572,7 +572,7 @@ def test_run_app_failed_docker(run_command, first_app, sub_kw, tmp_path, monkeyp run_command.run_app( first_app, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -639,7 +639,7 @@ def test_run_app_test_mode( run_command.run_app( first_app, test_mode=True, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -705,7 +705,7 @@ def test_run_app_test_mode_docker( run_command.run_app( first_app, test_mode=True, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -779,7 +779,7 @@ def test_run_app_test_mode_with_args( run_command.run_app( first_app, test_mode=True, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=["foo", "--bar"], @@ -847,7 +847,7 @@ def test_run_app_test_mode_with_args_docker( run_command.run_app( first_app, test_mode=True, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=["foo", "--bar"], diff --git a/tests/platforms/macOS/app/test_create.py b/tests/platforms/macOS/app/test_create.py index 610353c66..e22843072 100644 --- a/tests/platforms/macOS/app/test_create.py +++ b/tests/platforms/macOS/app/test_create.py @@ -350,7 +350,7 @@ def test_install_app_packages( create_command.merge_app_packages = mock.Mock() create_command.install_app_requirements( - first_app_templated, test_mode=False, debug_mode=None + first_app_templated, test_mode=False, debug_mode=False ) # We looked for binary packages in the host app_packages @@ -464,7 +464,7 @@ def test_min_os_version(create_command, first_app_templated, tmp_path): create_command.merge_app_packages = mock.Mock() create_command.install_app_requirements( - first_app_templated, test_mode=False, debug_mode=None + first_app_templated, test_mode=False, debug_mode=False ) # We looked for binary packages in the host app_packages @@ -567,7 +567,7 @@ def test_invalid_min_os_version(create_command, first_app_templated): ), ): create_command.install_app_requirements( - first_app_templated, test_mode=False, debug_mode=None + first_app_templated, test_mode=False, debug_mode=False ) # No request was made to install requirements @@ -607,7 +607,7 @@ def test_install_app_packages_no_binary( create_command.merge_app_packages = mock.Mock() create_command.install_app_requirements( - first_app_templated, test_mode=False, debug_mode=None + first_app_templated, test_mode=False, debug_mode=False ) # We looked for binary packages in the host app_packages @@ -709,7 +709,7 @@ def test_install_app_packages_failure(create_command, first_app_templated, tmp_p ), ): create_command.install_app_requirements( - first_app_templated, test_mode=False, debug_mode=None + first_app_templated, test_mode=False, debug_mode=False ) # We looked for binary packages in the host app_packages @@ -815,7 +815,7 @@ def test_install_app_packages_non_universal( create_command.merge_app_packages = mock.Mock() create_command.install_app_requirements( - first_app_templated, test_mode=False, debug_mode=None + first_app_templated, test_mode=False, debug_mode=False ) # We didn't search for binary packages diff --git a/tests/platforms/macOS/app/test_run.py b/tests/platforms/macOS/app/test_run.py index ad0d61ce3..b861b836d 100644 --- a/tests/platforms/macOS/app/test_run.py +++ b/tests/platforms/macOS/app/test_run.py @@ -47,7 +47,7 @@ def test_run_gui_app(run_command, first_app_config, sleep_zero, tmp_path, monkey run_command.run_app( first_app_config, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -115,7 +115,7 @@ def test_run_gui_app_with_passthrough( run_command.run_app( first_app_config, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=["foo", "--bar"], @@ -173,7 +173,7 @@ def test_run_gui_app_failed(run_command, first_app_config, sleep_zero, tmp_path) run_command.run_app( first_app_config, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -227,7 +227,7 @@ def test_run_gui_app_find_pid_failed( run_command.run_app( first_app_config, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -285,7 +285,7 @@ def test_run_gui_app_test_mode( run_command.run_app( first_app_config, test_mode=True, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -339,7 +339,7 @@ def test_run_console_app(run_command, first_app_config, tmp_path): run_command.run_app( first_app_config, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -373,7 +373,7 @@ def test_run_console_app_with_passthrough( run_command.run_app( first_app_config, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=["foo", "--bar"], @@ -404,7 +404,7 @@ def test_run_console_app_test_mode(run_command, first_app_config, sleep_zero, tm run_command.run_app( first_app_config, test_mode=True, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -448,7 +448,7 @@ def test_run_console_app_test_mode_with_passthrough( run_command.run_app( first_app_config, test_mode=True, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=["foo", "--bar"], @@ -489,7 +489,7 @@ def test_run_console_app_failed(run_command, first_app_config, sleep_zero, tmp_p run_command.run_app( first_app_config, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], diff --git a/tests/platforms/macOS/xcode/test_run.py b/tests/platforms/macOS/xcode/test_run.py index ebd51d143..f495ae42c 100644 --- a/tests/platforms/macOS/xcode/test_run.py +++ b/tests/platforms/macOS/xcode/test_run.py @@ -49,7 +49,7 @@ def test_run_app(run_command, first_app_config, sleep_zero, tmp_path, monkeypatc run_command.run_app( first_app_config, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -115,7 +115,7 @@ def test_run_app_with_passthrough( run_command.run_app( first_app_config, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=["foo", "--bar"], @@ -180,7 +180,7 @@ def test_run_app_test_mode( run_command.run_app( first_app_config, test_mode=True, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -247,7 +247,7 @@ def test_run_app_test_mode_with_passthrough( run_command.run_app( first_app_config, test_mode=True, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=["foo", "--bar"], diff --git a/tests/platforms/web/static/test_run.py b/tests/platforms/web/static/test_run.py index 214065e4f..265cea89f 100644 --- a/tests/platforms/web/static/test_run.py +++ b/tests/platforms/web/static/test_run.py @@ -114,7 +114,7 @@ def test_run(monkeypatch, run_command, first_app_built): run_command.run_app( first_app_built, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -182,7 +182,7 @@ def test_run_with_fallback_port( run_command.run_app( first_app_built, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -239,7 +239,7 @@ def test_run_with_args(monkeypatch, run_command, first_app_built): run_command.run_app( first_app_built, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=["foo", "--bar"], @@ -337,7 +337,7 @@ def test_cleanup_server_error( run_command.run_app( first_app_built, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -389,7 +389,7 @@ def test_cleanup_runtime_server_error(monkeypatch, run_command, first_app_built) run_command.run_app( first_app_built, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -442,7 +442,7 @@ def test_run_without_browser(monkeypatch, run_command, first_app_built): run_command.run_app( first_app_built, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -496,7 +496,7 @@ def test_run_autoselect_port(monkeypatch, run_command, first_app_built): run_command.run_app( first_app_built, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -596,7 +596,7 @@ def test_test_mode(run_command, first_app_built): run_command.run_app( first_app_built, test_mode=True, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], diff --git a/tests/platforms/windows/app/test_run.py b/tests/platforms/windows/app/test_run.py index cfa5054e7..71d51a9f6 100644 --- a/tests/platforms/windows/app/test_run.py +++ b/tests/platforms/windows/app/test_run.py @@ -33,7 +33,7 @@ def test_run_gui_app(run_command, first_app_config, tmp_path): run_command.run_app( first_app_config, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -70,7 +70,7 @@ def test_run_gui_app_with_passthrough(run_command, first_app_config, tmp_path): run_command.run_app( first_app_config, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=["foo", "--bar"], @@ -109,7 +109,7 @@ def test_run_gui_app_failed(run_command, first_app_config, tmp_path): run_command.run_app( first_app_config, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -141,7 +141,7 @@ def test_run_console_app(run_command, first_app_config, tmp_path): run_command.run_app( first_app_config, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -170,7 +170,7 @@ def test_run_console_app_with_passthrough(run_command, first_app_config, tmp_pat run_command.run_app( first_app_config, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=["foo", "--bar"], @@ -204,7 +204,7 @@ def test_run_console_app_failed(run_command, first_app_config, tmp_path): run_command.run_app( first_app_config, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -237,7 +237,7 @@ def test_run_app_test_mode(run_command, first_app_config, is_console_app, tmp_pa run_command.run_app( first_app_config, test_mode=True, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -283,7 +283,7 @@ def test_run_app_test_mode_with_passthrough( run_command.run_app( first_app_config, test_mode=True, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=["foo", "--bar"], diff --git a/tests/platforms/windows/visualstudio/test_run.py b/tests/platforms/windows/visualstudio/test_run.py index 7ff241346..807e1be72 100644 --- a/tests/platforms/windows/visualstudio/test_run.py +++ b/tests/platforms/windows/visualstudio/test_run.py @@ -37,7 +37,7 @@ def test_run_app(run_command, first_app_config, tmp_path): run_command.run_app( first_app_config, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -76,7 +76,7 @@ def test_run_app_with_args(run_command, first_app_config, tmp_path): run_command.run_app( first_app_config, test_mode=False, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=["foo", "--bar"], @@ -117,7 +117,7 @@ def test_run_app_test_mode(run_command, first_app_config, tmp_path): run_command.run_app( first_app_config, test_mode=True, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=[], @@ -157,7 +157,7 @@ def test_run_app_test_mode_with_args(run_command, first_app_config, tmp_path): run_command.run_app( first_app_config, test_mode=True, - debug_mode=None, + debug_mode=False, debugger_host=None, debugger_port=None, passthrough=["foo", "--bar"], diff --git a/tests/test_mainline.py b/tests/test_mainline.py index 75aa465a1..45ed6a1e4 100644 --- a/tests/test_mainline.py +++ b/tests/test_mainline.py @@ -91,7 +91,7 @@ def test_command_warning(monkeypatch, pyproject_toml, tmp_path, capsys): monkeypatch.setattr(sys, "argv", ["briefcase", "create"]) # Monkeypatch a warning into the create command - def sort_of_bad_generate_app_template(self, app, debug_mode=None): + def sort_of_bad_generate_app_template(self, app, debug_mode=False): raise BriefcaseWarning(error_code=0, msg="This is bad, but not *really* bad") monkeypatch.setattr( @@ -134,7 +134,7 @@ def test_unknown_command_error(monkeypatch, pyproject_toml, capsys): monkeypatch.setattr(sys, "argv", ["briefcase", "create"]) # Monkeypatch an error into the create command - def bad_generate_app_template(self, app, debug_mode=None): + def bad_generate_app_template(self, app, debug_mode=False): raise ValueError("Bad value") monkeypatch.setattr( @@ -151,7 +151,7 @@ def test_interrupted_command(monkeypatch, pyproject_toml, tmp_path, capsys): monkeypatch.setattr(sys, "argv", ["briefcase", "create"]) # Monkeypatch a keyboard interrupt into the create command - def interrupted_generate_app_template(self, app, debug_mode=None): + def interrupted_generate_app_template(self, app, debug_mode=False): raise KeyboardInterrupt() monkeypatch.setattr( @@ -173,7 +173,7 @@ def test_interrupted_command_with_log(monkeypatch, pyproject_toml, tmp_path, cap monkeypatch.setattr(sys, "argv", ["briefcase", "create", "--log"]) # Monkeypatch a keyboard interrupt into the create command - def interrupted_generate_app_template(self, app, debug_mode=None): + def interrupted_generate_app_template(self, app, debug_mode=False): raise KeyboardInterrupt() monkeypatch.setattr( From b67ac3f98dae13e82db8ee892a4b49cea457e2ec Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Tue, 20 May 2025 22:17:10 +0200 Subject: [PATCH 028/131] env is not needed for adb forward/reverse --- src/briefcase/integrations/android_sdk.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/briefcase/integrations/android_sdk.py b/src/briefcase/integrations/android_sdk.py index cf4bccefc..2487be355 100644 --- a/src/briefcase/integrations/android_sdk.py +++ b/src/briefcase/integrations/android_sdk.py @@ -1656,7 +1656,6 @@ def forward(self, host_port: int, device_port: int): f"tcp:{host_port}", f"tcp:{device_port}", ], - env=self.tools.android_sdk.env, ) except subprocess.CalledProcessError as e: raise BriefcaseCommandError("Error starting 'adb forward'.") from e @@ -1676,7 +1675,6 @@ def forward_remove(self, host_port: int): "--remove", f"tcp:{host_port}", ], - env=self.tools.android_sdk.env, ) except subprocess.CalledProcessError as e: raise BriefcaseCommandError("Error starting 'adb forward --remove'.") from e @@ -1698,7 +1696,6 @@ def reverse(self, device_port: int, host_port: int): f"tcp:{device_port}", f"tcp:{host_port}", ], - env=self.tools.android_sdk.env, ) except subprocess.CalledProcessError as e: raise BriefcaseCommandError("Error starting 'adb reverse'.") from e @@ -1718,7 +1715,6 @@ def reverse_remove(self, device_port: int): "--remove", f"tcp:{device_port}", ], - env=self.tools.android_sdk.env, ) except subprocess.CalledProcessError as e: raise BriefcaseCommandError("Error starting 'adb reverse --remove'.") from e From 21463e95848516202addb9b62f3e636918e59d7f Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Tue, 20 May 2025 22:22:14 +0200 Subject: [PATCH 029/131] added unittests --- docs/reference/commands/run.rst | 2 +- src/briefcase/config.py | 2 +- src/briefcase/platforms/android/gradle.py | 12 +- src/briefcase/platforms/linux/appimage.py | 12 + tests/commands/build/test_call.py | 77 ++++++ .../create/test_generate_app_template.py | 2 +- .../create/test_install_app_requirements.py | 40 +++ tests/debuggers/__init__.py | 0 tests/debuggers/test_base.py | 42 +++ .../android_sdk/ADB/test_forward_reverse.py | 117 +++++++++ .../android_sdk/ADB/test_start_app.py | 39 +++ tests/platforms/android/gradle/test_create.py | 9 + tests/platforms/android/gradle/test_run.py | 242 +++++++++++++++++- tests/platforms/iOS/xcode/test_run.py | 152 +++++++++++ tests/platforms/linux/appimage/test_run.py | 52 ++++ tests/platforms/linux/flatpak/test_run.py | 53 ++++ tests/platforms/linux/system/conftest.py | 8 + tests/platforms/linux/system/test_run.py | 72 ++++++ tests/platforms/web/static/test_run.py | 20 ++ tests/platforms/windows/app/test_run.py | 50 ++++ .../windows/visualstudio/test_run.py | 54 ++++ 21 files changed, 1047 insertions(+), 10 deletions(-) create mode 100644 tests/debuggers/__init__.py create mode 100644 tests/debuggers/test_base.py create mode 100644 tests/integrations/android_sdk/ADB/test_forward_reverse.py diff --git a/docs/reference/commands/run.rst b/docs/reference/commands/run.rst index e4e84a8d8..1d8c7da71 100644 --- a/docs/reference/commands/run.rst +++ b/docs/reference/commands/run.rst @@ -58,7 +58,7 @@ Currently the following debuggers are supported: - ``socat readline tcp:localhost:5678`` The app will start after the connection is established. -- ``debugpy``: This is used for debugging via VSCode (see TODO) +- ``debugpy``: This is used for debugging via VSCode (see :doc:`Debugging with VSCode `) Usage diff --git a/src/briefcase/config.py b/src/briefcase/config.py index 44014abe1..23fdde19d 100644 --- a/src/briefcase/config.py +++ b/src/briefcase/config.py @@ -311,7 +311,7 @@ def __init__( self.template_branch = template_branch self.test_sources = test_sources self.test_requires = test_requires - self.debug_requires = debug_requires + self.debug_requires = [] if debug_requires is None else debug_requires self.supported = supported self.long_description = long_description self.license = license diff --git a/src/briefcase/platforms/android/gradle.py b/src/briefcase/platforms/android/gradle.py index a826e01d0..8b5299298 100644 --- a/src/briefcase/platforms/android/gradle.py +++ b/src/briefcase/platforms/android/gradle.py @@ -219,17 +219,17 @@ def output_format_template_context(self, app: AppConfig, debug_mode: bool = Fals "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0", ] + # Extract test packages, to enable features like test discovery and assertion rewriting. extract_sources = app.test_sources or [] + + # In debug mode extract all source packages so that the debugger can get the source code + # at runtime (eg. via 'll' in pdb). if debug_mode: - # Add sources to the extract_packages so that the debugger can - # get the source code at runtime (eg. via 'll' in pdb). extract_sources.extend(app.sources) return { "version_code": version_code, "safe_formal_name": safe_formal_name(app.formal_name), - # Extract test packages, to enable features like test discovery and assertion - # rewriting. "extract_packages": ", ".join( [f'"{name}"' for path in extract_sources if (name := Path(path).name)] ), @@ -540,7 +540,7 @@ def establish_debugger_connection( """ if debugger.connection_mode == DebuggerConnectionMode.SERVER: adb.forward(debugger_port, debugger_port) - elif debugger.connection_mode == DebuggerConnectionMode.CLIENT: + else: adb.reverse(debugger_port, debugger_port) def remove_debugger_connection( @@ -555,7 +555,7 @@ def remove_debugger_connection( """ if debugger.connection_mode == DebuggerConnectionMode.SERVER: adb.forward_remove(debugger_port) - elif debugger.connection_mode == DebuggerConnectionMode.CLIENT: + else: adb.reverse_remove(debugger_port) diff --git a/src/briefcase/platforms/linux/appimage.py b/src/briefcase/platforms/linux/appimage.py index 38db5f404..9e7695815 100644 --- a/src/briefcase/platforms/linux/appimage.py +++ b/src/briefcase/platforms/linux/appimage.py @@ -12,6 +12,7 @@ UpdateCommand, ) from briefcase.config import AppConfig +from briefcase.debuggers.base import AppPackagesPathMappings from briefcase.exceptions import ( BriefcaseCommandError, BriefcaseConfigError, @@ -366,6 +367,17 @@ class LinuxAppImageRunCommand(LinuxAppImagePassiveMixin, RunCommand): supported_host_os = {"Linux"} supported_host_os_reason = "Linux AppImages can only be executed on Linux." + def remote_debugger_app_packages_path_mapping( + self, app: AppConfig + ) -> AppPackagesPathMappings: + """ + Get the path mappings for the app packages. + + :param app: The config object for the app + :returns: The path mappings for the app packages + """ + return None # TODO: Where are the app packages located? + def run_app( self, app: AppConfig, diff --git a/tests/commands/build/test_call.py b/tests/commands/build/test_call.py index 3a1d2e023..c9ce3eaf9 100644 --- a/tests/commands/build/test_call.py +++ b/tests/commands/build/test_call.py @@ -1139,6 +1139,83 @@ def test_build_test_update_stub(build_command, first_app, second_app): ] +def test_build_debug(build_command, first_app, second_app): + """If the user builds a debug app, app is updated before build.""" + # Add two apps + build_command.apps = { + "first": first_app, + "second": second_app, + } + + # Configure command line options + options, _ = build_command.parse_options(["--debug=pdb"]) + + # Run the build command + build_command(**options) + + # The right sequence of things will be done + assert build_command.actions == [ + # Host OS is verified + ("verify-host",), + # Tools are verified + ("verify-tools",), + # App configs have been finalized + ("finalize-app-config", "first"), + ("finalize-app-config", "second"), + # Update then build the first app + ( + "update", + "first", + { + "test_mode": False, + "debugger": "pdb", + "update_requirements": False, + "update_resources": False, + "update_support": False, + "update_stub": False, + }, + ), + # App template is verified for first app + ("verify-app-template", "first"), + # App tools are verified for first app + ("verify-app-tools", "first"), + ( + "build", + "first", + {"update_state": "first", "test_mode": False, "debug_mode": True}, + ), + # Update then build the second app + ( + "update", + "second", + { + "update_state": "first", + "build_state": "first", + "test_mode": False, + "debugger": "pdb", + "update_requirements": False, + "update_resources": False, + "update_support": False, + "update_stub": False, + }, + ), + # App template is verified for second app + ("verify-app-template", "second"), + # App tools are verified for second app + ("verify-app-tools", "second"), + ( + "build", + "second", + { + "update_state": "second", + "build_state": "first", + "test_mode": False, + "debug_mode": True, + }, + ), + ] + + def test_build_invalid_update(build_command, first_app, second_app): """If the user requests a build with update and no-update, an error is raised.""" # Add two apps diff --git a/tests/commands/create/test_generate_app_template.py b/tests/commands/create/test_generate_app_template.py index 0ded81faa..843169d7f 100644 --- a/tests/commands/create/test_generate_app_template.py +++ b/tests/commands/create/test_generate_app_template.py @@ -31,7 +31,7 @@ def full_context(): "sources": ["src/my_app"], "test_sources": None, "test_requires": None, - "debug_requires": None, + "debug_requires": [], "debugger": None, "url": "https://example.com", "author": "First Last", diff --git a/tests/commands/create/test_install_app_requirements.py b/tests/commands/create/test_install_app_requirements.py index 401a26f0a..a56499ec4 100644 --- a/tests/commands/create/test_install_app_requirements.py +++ b/tests/commands/create/test_install_app_requirements.py @@ -1085,3 +1085,43 @@ def test_app_packages_only_test_requires_test_mode( # Original app definitions haven't changed assert myapp.requires is None assert myapp.test_requires == ["pytest", "pytest-tldr"] + + +def test_app_packages_debug_requires_debug_mode( + create_command, + myapp, + app_packages_path, + app_packages_path_index, +): + """If an app has debug requirements and we're in debug mode, they are installed.""" + myapp.requires = ["first", "second==1.2.3", "third>=3.2.1"] + myapp.debug_requires = ["debugpy"] + + create_command.install_app_requirements(myapp, test_mode=False, debug_mode=True) + + # A request was made to install requirements + create_command.tools[myapp].app_context.run.assert_called_with( + [ + sys.executable, + "-u", + "-X", + "utf8", + "-m", + "pip", + "install", + "--disable-pip-version-check", + "--upgrade", + "--no-user", + f"--target={app_packages_path}", + "first", + "second==1.2.3", + "third>=3.2.1", + "debugpy", + ], + check=True, + encoding="UTF-8", + ) + + # Original app definitions haven't changed + assert myapp.requires == ["first", "second==1.2.3", "third>=3.2.1"] + assert myapp.debug_requires == ["debugpy"] diff --git a/tests/debuggers/__init__.py b/tests/debuggers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/debuggers/test_base.py b/tests/debuggers/test_base.py new file mode 100644 index 000000000..d17236776 --- /dev/null +++ b/tests/debuggers/test_base.py @@ -0,0 +1,42 @@ +from briefcase.debuggers import ( + DebugpyDebugger, + PdbDebugger, + get_debugger, + get_debuggers, +) +from briefcase.debuggers.base import DebuggerConnectionMode +from briefcase.exceptions import BriefcaseCommandError + + +def test_get_debuggers(): + debuggers = get_debuggers() + assert isinstance(debuggers, dict) + assert debuggers["pdb"] is PdbDebugger + assert debuggers["debugpy"] is DebugpyDebugger + + +def test_get_debugger(): + assert isinstance(get_debugger("pdb"), PdbDebugger) + assert isinstance(get_debugger("debugpy"), DebugpyDebugger) + + # Test with an unknown debugger name + try: + get_debugger("unknown") + except BriefcaseCommandError as e: + assert str(e) == "Unknown debugger: unknown" + + +def test_pdb(): + debugger = PdbDebugger() + debugger.additional_requirements == [ + "git+https://github.com/timrid/briefcase-debugadapter#subdirectory=briefcase-pdb-debugadapter" + ] + debugger.connection_mode == DebuggerConnectionMode.SERVER + + +def test_debugpy(): + debugger = DebugpyDebugger() + debugger.additional_requirements == [ + "git+https://github.com/timrid/briefcase-debugadapter#subdirectory=briefcase-debugpy-debugadapter" + ] + debugger.connection_mode == DebuggerConnectionMode.SERVER diff --git a/tests/integrations/android_sdk/ADB/test_forward_reverse.py b/tests/integrations/android_sdk/ADB/test_forward_reverse.py new file mode 100644 index 000000000..3d05cb54c --- /dev/null +++ b/tests/integrations/android_sdk/ADB/test_forward_reverse.py @@ -0,0 +1,117 @@ +import subprocess + +import pytest + +from briefcase.exceptions import BriefcaseCommandError + + +def test_forward(mock_tools, adb): + """An port forwarding.""" + # Invoke forward + adb.forward(5555, 6666) + + # Validate call parameters. + mock_tools.subprocess.check_output.assert_called_once_with( + [ + mock_tools.android_sdk.adb_path, + "-s", + "exampleDevice", + "forward", + "tcp:5555", + "tcp:6666", + ], + ) + + +def test_forward_failure(adb, mock_tools): + """If port forwarding fails, the error is caught.""" + # Mock out the run command on an adb instance + mock_tools.subprocess.check_output.side_effect = subprocess.CalledProcessError( + returncode=1, cmd="" + ) + with pytest.raises(BriefcaseCommandError): + adb.forward(5555, 6666) + + +def test_forward_remove(mock_tools, adb): + """An port forwarding removing.""" + # Invoke forward remove + adb.forward_remove(5555) + + # Validate call parameters. + mock_tools.subprocess.check_output.assert_called_once_with( + [ + mock_tools.android_sdk.adb_path, + "-s", + "exampleDevice", + "forward", + "--remove", + "tcp:5555", + ], + ) + + +def test_forward_remove_failure(adb, mock_tools): + """If port forwarding removing fails, the error is caught.""" + # Mock out the run command on an adb instance + mock_tools.subprocess.check_output.side_effect = subprocess.CalledProcessError( + returncode=1, cmd="" + ) + with pytest.raises(BriefcaseCommandError): + adb.forward_remove(5555) + + +def test_reverse(mock_tools, adb): + """An port reversing.""" + # Invoke reverse + adb.reverse(5555, 6666) + + # Validate call parameters. + mock_tools.subprocess.check_output.assert_called_once_with( + [ + mock_tools.android_sdk.adb_path, + "-s", + "exampleDevice", + "reverse", + "tcp:5555", + "tcp:6666", + ], + ) + + +def test_reverse_failure(adb, mock_tools): + """If port reversing fails, the error is caught.""" + # Mock out the run command on an adb instance + mock_tools.subprocess.check_output.side_effect = subprocess.CalledProcessError( + returncode=1, cmd="" + ) + with pytest.raises(BriefcaseCommandError): + adb.reverse(5555, 6666) + + +def test_reverse_remove(mock_tools, adb): + """An port reversing removing.""" + # Invoke reverse remove + adb.reverse_remove(5555) + + # Validate call parameters. + mock_tools.subprocess.check_output.assert_called_once_with( + [ + mock_tools.android_sdk.adb_path, + "-s", + "exampleDevice", + "reverse", + "--remove", + "tcp:5555", + ], + ) + + +def test_reverse_remove_failure(adb, mock_tools): + """If port reversing removing fails, the error is caught.""" + # Mock out the run command on an adb instance + mock_tools.subprocess.check_output.side_effect = subprocess.CalledProcessError( + returncode=1, cmd="" + ) + with pytest.raises(BriefcaseCommandError): + adb.reverse_remove(5555) diff --git a/tests/integrations/android_sdk/ADB/test_start_app.py b/tests/integrations/android_sdk/ADB/test_start_app.py index a3601f403..1c6a0d525 100644 --- a/tests/integrations/android_sdk/ADB/test_start_app.py +++ b/tests/integrations/android_sdk/ADB/test_start_app.py @@ -54,6 +54,45 @@ def test_start_app_launches_app(adb, capsys, passthrough): assert "normal adb output" not in capsys.readouterr() +@pytest.mark.parametrize( + "env", + [ + {"PARAM1": "VALUE1"}, + {"BRIEFCASE_DEBUGGER": '{"host": "localhost", "port": 1234}'}, + ], +) +def test_start_app_launches_app_with_env(adb, capsys, env): + """Invoking `start_app()` calls `run()` with the appropriate parameters.""" + # Mock out the run command on an adb instance + adb.run = MagicMock(return_value="example normal adb output") + + # Invoke start_app + adb.start_app("com.example.sample.package", "com.example.sample.activity", [], env) + + # Validate call parameters. + adb.run.assert_called_once_with( + "shell", + "am", + "start", + "-n", + "com.example.sample.package/com.example.sample.activity", + "-a", + "android.intent.action.MAIN", + "-c", + "android.intent.category.LAUNCHER", + "--es", + "org.beeware.ARGV", + "'[]'", + "--es", + "org.beeware.ENVIRON", + shlex.quote(json.dumps(env)), + ) + + # Validate that the normal output of the command was not printed (since there + # was no error). + assert "normal adb output" not in capsys.readouterr() + + def test_missing_activity(adb): """If the activity doesn't exist, the error is caught.""" # Use real `adb` output from launching an activity that does not exist. diff --git a/tests/platforms/android/gradle/test_create.py b/tests/platforms/android/gradle/test_create.py index 4a3b1b133..106a2b0c7 100644 --- a/tests/platforms/android/gradle/test_create.py +++ b/tests/platforms/android/gradle/test_create.py @@ -201,6 +201,15 @@ def test_extract_packages(create_command, first_app_config, test_sources, expect assert context["extract_packages"] == expected +def test_extract_packages_debug_mode(create_command, first_app_config): + first_app_config.test_sources = ["one", "two", "three"] + first_app_config.sources = ["four", "five", "six"] + context = create_command.output_format_template_context( + first_app_config, debug_mode=True + ) + assert context["extract_packages"] == '"one", "two", "three", "four", "five", "six"' + + @pytest.mark.parametrize( "permissions, features, context", [ diff --git a/tests/platforms/android/gradle/test_run.py b/tests/platforms/android/gradle/test_run.py index 97d30eb0a..32e22faa8 100644 --- a/tests/platforms/android/gradle/test_run.py +++ b/tests/platforms/android/gradle/test_run.py @@ -1,4 +1,5 @@ import datetime +import json import os import platform import sys @@ -9,7 +10,8 @@ import httpx import pytest -from briefcase.console import Console +from briefcase.console import Console, LogLevel +from briefcase.debuggers.base import BaseDebugger, DebuggerConnectionMode from briefcase.exceptions import BriefcaseCommandError from briefcase.integrations.android_sdk import ADB, AndroidSDK from briefcase.integrations.java import JDK @@ -828,3 +830,241 @@ def test_run_test_mode_created_emulator(run_command, first_app_config): # The emulator was killed at the end of the test run_command.tools.mock_adb.kill.assert_called_once_with() + + +def test_run_debug_mode(run_command, first_app_config, tmp_path): + """An app can be run in debug mode.""" + # Set up device selection to return a running physical device. + run_command.tools.android_sdk.select_target_device = mock.MagicMock( + return_value=("exampleDevice", "ExampleDevice", None) + ) + + # Set up the log streamer to return a known stream + log_popen = mock.MagicMock() + run_command.tools.mock_adb.logcat.return_value = log_popen + + # To satisfy coverage, the stop function must be invoked at least once + # when invoking stream_output. + def mock_stream_output(app, stop_func, **kwargs): + stop_func() + + run_command._stream_app_logs.side_effect = mock_stream_output + + # Set up app config to have a `-` in the `bundle`, to ensure it gets + # normalized into a `_` via `package_name`. + first_app_config.bundle = "com.ex-ample" + + # Invoke run_app with args. + run_command.run_app( + first_app_config, + device_or_avd="exampleDevice", + test_mode=False, + debug_mode=True, + debugger_host="somehost", + debugger_port=9999, + passthrough=[], + ) + + # select_target_device was invoked with a specific device + run_command.tools.android_sdk.select_target_device.assert_called_once_with( + "exampleDevice" + ) + + # The ADB wrapper is created + run_command.tools.android_sdk.adb.assert_called_once_with(device="exampleDevice") + + # The adb wrapper is invoked with the expected arguments + run_command.tools.mock_adb.install_apk.assert_called_once_with( + run_command.binary_path(first_app_config) + ) + run_command.tools.mock_adb.force_stop_app.assert_called_once_with( + f"{first_app_config.package_name}.{first_app_config.module_name}", + ) + + run_command.tools.mock_adb.start_app.assert_called_once_with( + f"{first_app_config.package_name}.{first_app_config.module_name}", + "org.beeware.android.MainActivity", + [], + { + "BRIEFCASE_DEBUGGER": json.dumps( + { + "host": "somehost", + "port": 9999, + "app_path_mappings": { + "device_sys_path_regex": "app$", + "device_subfolders": ["first_app"], + "host_folders": [str(tmp_path / "base_path/src/first_app")], + }, + "app_packages_path_mappings": { + "sys_path_regex": "requirements$", + "host_folder": str( + tmp_path + / "base_path/build/first-app/android/gradle/app/build/python/pip/debug/common" + ), + }, + } + ) + }, + ) + + run_command.tools.mock_adb.pidof.assert_called_once_with( + f"{first_app_config.package_name}.{first_app_config.module_name}", + quiet=2, + ) + run_command.tools.mock_adb.logcat.assert_called_once_with(pid="777") + + run_command._stream_app_logs.assert_called_once_with( + first_app_config, + popen=log_popen, + test_mode=False, + clean_filter=android_log_clean_filter, + clean_output=False, + stop_func=mock.ANY, + log_stream=True, + ) + + # The emulator was not killed at the end of the test + run_command.tools.mock_adb.kill.assert_not_called() + + +class ServerDebugger(BaseDebugger): + def additional_requirements(self) -> list[str]: + raise NotImplementedError + + @property + def connection_mode(self) -> DebuggerConnectionMode: + return DebuggerConnectionMode.SERVER + + +class ClientDebugger(BaseDebugger): + def additional_requirements(self) -> list[str]: + raise NotImplementedError + + @property + def connection_mode(self) -> DebuggerConnectionMode: + return DebuggerConnectionMode.CLIENT + + +@pytest.mark.parametrize( + "debugger", + [ + ServerDebugger(), + ClientDebugger(), + ], +) +def test_run_debug_mode_localhost(run_command, first_app_config, tmp_path, debugger): + """An app can be run in debug mode.""" + run_command.console.verbosity = LogLevel.DEBUG + + # Set up device selection to return a running physical device. + run_command.tools.android_sdk.select_target_device = mock.MagicMock( + return_value=("exampleDevice", "ExampleDevice", None) + ) + + # Set up the log streamer to return a known stream + log_popen = mock.MagicMock() + run_command.tools.mock_adb.logcat.return_value = log_popen + + # To satisfy coverage, the stop function must be invoked at least once + # when invoking stream_output. + def mock_stream_output(app, stop_func, **kwargs): + stop_func() + + run_command._stream_app_logs.side_effect = mock_stream_output + + # Set up app config to have a `-` in the `bundle`, to ensure it gets + # normalized into a `_` via `package_name`. + first_app_config.bundle = "com.ex-ample" + + # Set up the debugger + first_app_config.debugger = debugger + + # Invoke run_app with args. + run_command.run_app( + first_app_config, + device_or_avd="exampleDevice", + test_mode=False, + debug_mode=True, + debugger_host="localhost", + debugger_port=9999, + passthrough=[], + ) + + # select_target_device was invoked with a specific device + run_command.tools.android_sdk.select_target_device.assert_called_once_with( + "exampleDevice" + ) + + # The ADB wrapper is created + run_command.tools.android_sdk.adb.assert_called_once_with(device="exampleDevice") + + # The adb wrapper is invoked with the expected arguments + run_command.tools.mock_adb.install_apk.assert_called_once_with( + run_command.binary_path(first_app_config) + ) + run_command.tools.mock_adb.force_stop_app.assert_called_once_with( + f"{first_app_config.package_name}.{first_app_config.module_name}", + ) + + run_command.tools.mock_adb.start_app.assert_called_once_with( + f"{first_app_config.package_name}.{first_app_config.module_name}", + "org.beeware.android.MainActivity", + [], + { + "BRIEFCASE_DEBUG": "1", + "BRIEFCASE_DEBUGGER": json.dumps( + { + "host": "localhost", + "port": 9999, + "app_path_mappings": { + "device_sys_path_regex": "app$", + "device_subfolders": ["first_app"], + "host_folders": [str(tmp_path / "base_path/src/first_app")], + }, + "app_packages_path_mappings": { + "sys_path_regex": "requirements$", + "host_folder": str( + tmp_path + / "base_path/build/first-app/android/gradle/app/build/python/pip/debug/common" + ), + }, + } + ), + }, + ) + + run_command.tools.mock_adb.pidof.assert_called_once_with( + f"{first_app_config.package_name}.{first_app_config.module_name}", + quiet=2, + ) + run_command.tools.mock_adb.logcat.assert_called_once_with(pid="777") + + if isinstance(debugger, ServerDebugger): + run_command.tools.mock_adb.forward.assert_called_once_with( + 9999, + 9999, + ) + run_command.tools.mock_adb.forward_remove.assert_called_once_with( + 9999, + ) + elif isinstance(debugger, ClientDebugger): + run_command.tools.mock_adb.reverse.assert_called_once_with( + 9999, + 9999, + ) + run_command.tools.mock_adb.reverse_remove.assert_called_once_with( + 9999, + ) + + run_command._stream_app_logs.assert_called_once_with( + first_app_config, + popen=log_popen, + test_mode=False, + clean_filter=android_log_clean_filter, + clean_output=False, + stop_func=mock.ANY, + log_stream=True, + ) + + # The emulator was not killed at the end of the test + run_command.tools.mock_adb.kill.assert_not_called() diff --git a/tests/platforms/iOS/xcode/test_run.py b/tests/platforms/iOS/xcode/test_run.py index a5882fbfb..62e0347d7 100644 --- a/tests/platforms/iOS/xcode/test_run.py +++ b/tests/platforms/iOS/xcode/test_run.py @@ -1,3 +1,4 @@ +import json import subprocess import time from unittest import mock @@ -1676,3 +1677,154 @@ def test_run_app_test_mode_with_passthrough(run_command, first_app_config, tmp_p stop_func=mock.ANY, log_stream=True, ) + + +@pytest.mark.usefixtures("sleep_zero") +def test_run_app_debug_mode(run_command, first_app_generated, tmp_path): + """An iOS App can be started in debug mode.""" + # A valid target device will be selected. + run_command.select_target_device = mock.MagicMock( + return_value=("2D3503A3-6EB9-4B37-9B17-C7EFEF2FA32D", "13.2", "iPhone 11") + ) + + # Simulator is already booted + run_command.get_device_state = mock.MagicMock(return_value=DeviceState.BOOTED) + + # Mock a process ID for the app + run_command.tools.subprocess.check_output.return_value = ( + "com.example.first-app: 1234\n" + ) + + # Mock the uninstall, install, and log stream Popen processes + uninstall_popen = mock.MagicMock(spec_set=subprocess.Popen) + uninstall_popen.__enter__.return_value = uninstall_popen + uninstall_popen.poll.side_effect = [None, None, 0] + install_popen = mock.MagicMock(spec_set=subprocess.Popen) + install_popen.__enter__.return_value = install_popen + install_popen.poll.side_effect = [None, None, 0] + log_stream_process = mock.MagicMock(spec_set=subprocess.Popen) + run_command.tools.subprocess.Popen.side_effect = [ + uninstall_popen, + install_popen, + log_stream_process, + ] + + # Run the app + run_command.run_app( + first_app_generated, + test_mode=False, + debug_mode=True, + debugger_host="somehost", + debugger_port=9999, + passthrough=[], + ) + + # Sleep four times for uninstall/install and once for log stream start + assert time.sleep.call_count == 4 + 1 + + # Set the environment variables for the debugger and launch the app + run_command.tools.subprocess.check_output.assert_has_calls( + [ + # Set the environment variables for the debugger + mock.call( + [ + "xcrun", + "simctl", + "spawn", + "2D3503A3-6EB9-4B37-9B17-C7EFEF2FA32D", + "launchctl", + "setenv", + "BRIEFCASE_DEBUGGER", + json.dumps( + { + "host": "somehost", + "port": 9999, + "app_path_mappings": { + "device_sys_path_regex": "app$", + "device_subfolders": ["first_app"], + "host_folders": [ + str(tmp_path / "base_path/src/first_app") + ], + }, + "app_packages_path_mappings": { + "sys_path_regex": "app_packages$", + "host_folder": str( + tmp_path + / "base_path/build/first-app/ios/xcode/app_packages.iphonesimulator" + ), + }, + } + ), + ], + ), + # Launch the new app + mock.call( + [ + "xcrun", + "simctl", + "launch", + "2D3503A3-6EB9-4B37-9B17-C7EFEF2FA32D", + "com.example.first-app", + ], + ), + ] + ) + + # Start the uninstall, install, and log stream + run_command.tools.subprocess.Popen.assert_has_calls( + [ + # Uninstall the old app + mock.call( + [ + "xcrun", + "simctl", + "uninstall", + "2D3503A3-6EB9-4B37-9B17-C7EFEF2FA32D", + "com.example.first-app", + ], + ), + # Install the new app + mock.call( + [ + "xcrun", + "simctl", + "install", + "2D3503A3-6EB9-4B37-9B17-C7EFEF2FA32D", + tmp_path + / "base_path/build/first-app/ios/xcode/build/Debug-iphonesimulator/First App.app", + ], + ), + mock.call( + [ + "xcrun", + "simctl", + "spawn", + "2D3503A3-6EB9-4B37-9B17-C7EFEF2FA32D", + "log", + "stream", + "--style", + "compact", + "--predicate", + 'senderImagePath ENDSWITH "/First App"' + ' OR (processImagePath ENDSWITH "/First App"' + ' AND (senderImagePath ENDSWITH "-iphonesimulator.so"' + ' OR senderImagePath ENDSWITH "-iphonesimulator.dylib"' + ' OR senderImagePath ENDSWITH "_ctypes.framework/_ctypes"))', + ], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + bufsize=1, + ), + ] + ) + + # Log stream monitoring was started + run_command._stream_app_logs.assert_called_with( + first_app_generated, + popen=log_stream_process, + test_mode=False, + clean_filter=macOS_log_clean_filter, + clean_output=True, + stop_func=mock.ANY, + log_stream=True, + ) diff --git a/tests/platforms/linux/appimage/test_run.py b/tests/platforms/linux/appimage/test_run.py index 9755ee77b..608469ba4 100644 --- a/tests/platforms/linux/appimage/test_run.py +++ b/tests/platforms/linux/appimage/test_run.py @@ -1,3 +1,4 @@ +import json import subprocess from unittest import mock @@ -334,3 +335,54 @@ def test_run_app_test_mode_with_args( test_mode=True, clean_output=False, ) + + +def test_run_app_debug_mode(run_command, first_app_config, tmp_path): + """A linux App can be started in debug mode.""" + # Set up the log streamer to return a known stream + log_popen = mock.MagicMock() + run_command.tools.subprocess.Popen.return_value = log_popen + + # Run the app + run_command.run_app( + first_app_config, + test_mode=False, + debug_mode=True, + debugger_host="somehost", + debugger_port=9999, + passthrough=[], + ) + + # The process was started + run_command.tools.subprocess.Popen.assert_called_with( + [ + tmp_path + / "base_path/build/first-app/linux/appimage/First_App-0.0.1-x86_64.AppImage" + ], + cwd=tmp_path / "home", + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + bufsize=1, + env={ + "BRIEFCASE_DEBUGGER": json.dumps( + { + "host": "somehost", + "port": 9999, + "app_path_mappings": { + "device_sys_path_regex": "app$", + "device_subfolders": ["first_app"], + "host_folders": [str(tmp_path / "base_path/src/first_app")], + }, + "app_packages_path_mappings": None, + } + ) + }, + ) + + # The streamer was started + run_command._stream_app_logs.assert_called_once_with( + first_app_config, + popen=log_popen, + test_mode=False, + clean_output=False, + ) diff --git a/tests/platforms/linux/flatpak/test_run.py b/tests/platforms/linux/flatpak/test_run.py index 52a17df9f..9e20392b6 100644 --- a/tests/platforms/linux/flatpak/test_run.py +++ b/tests/platforms/linux/flatpak/test_run.py @@ -1,3 +1,4 @@ +import json from unittest import mock import pytest @@ -266,3 +267,55 @@ def test_run_test_mode_with_args(run_command, first_app_config, is_console_app): test_mode=True, clean_output=False, ) + + +def test_run_debug_mode(run_command, first_app_config, tmp_path): + """A flatpak can be executed in debug mode.""" + # Set up the log streamer to return a known stream and a good return code + log_popen = mock.MagicMock() + run_command.tools.flatpak.run.return_value = log_popen + + # Run the app + run_command.run_app( + first_app_config, + test_mode=False, + debug_mode=True, + debugger_host="somehost", + debugger_port=9999, + passthrough=[], + ) + + # App is executed + run_command.tools.flatpak.run.assert_called_once_with( + bundle_identifier="com.example.first-app", + args=[], + stream_output=True, + env={ + "BRIEFCASE_DEBUGGER": json.dumps( + { + "host": "somehost", + "port": 9999, + "app_path_mappings": { + "device_sys_path_regex": "app$", + "device_subfolders": ["first_app"], + "host_folders": [str(tmp_path / "base_path/src/first_app")], + }, + "app_packages_path_mappings": { + "sys_path_regex": "app_packages$", + "host_folder": str( + tmp_path + / "base_path/build/first-app/linux/flatpak/build/files/briefcase/app_packages" + ), + }, + } + ) + }, + ) + + # The streamer was started + run_command._stream_app_logs.assert_called_once_with( + first_app_config, + popen=log_popen, + test_mode=False, + clean_output=False, + ) diff --git a/tests/platforms/linux/system/conftest.py b/tests/platforms/linux/system/conftest.py index 3d23e7e61..d2218c817 100644 --- a/tests/platforms/linux/system/conftest.py +++ b/tests/platforms/linux/system/conftest.py @@ -34,6 +34,14 @@ def first_app(first_app_config, tmp_path): bundle_dir = tmp_path / "base_path/build/first-app/somevendor/surprising" create_file(bundle_dir / "first-app.1", "First App manpage") + create_file( + bundle_dir / "briefcase.toml", + """ +[paths] +app_path = "first-app-0.0.1/usr/lib/first-app/app" +app_packages_path = "first-app-0.0.1/usr/lib/first-app/app_packages" +""", + ) lib_dir = bundle_dir / "first-app-0.0.1/usr/lib/first-app" (lib_dir / "app").mkdir(parents=True, exist_ok=True) diff --git a/tests/platforms/linux/system/test_run.py b/tests/platforms/linux/system/test_run.py index 3a53c2d44..0fb1bbc17 100644 --- a/tests/platforms/linux/system/test_run.py +++ b/tests/platforms/linux/system/test_run.py @@ -1,3 +1,4 @@ +import json import os import subprocess import sys @@ -369,6 +370,77 @@ def test_run_gui_app_failed(run_command, first_app, sub_kw, tmp_path): run_command._stream_app_logs.assert_not_called() +def test_run_gui_app_debug_mode(run_command, first_app, sub_kw, tmp_path, monkeypatch): + """A bootstrap binary for a GUI app can be started in debug mode.""" + + # Set up tool cache + run_command.verify_app_tools(app=first_app) + + # Set up the log streamer to return a known stream + log_popen = mock.MagicMock() + run_command.tools.subprocess._subprocess.Popen = mock.MagicMock( + return_value=log_popen + ) + + # Mock out the environment + monkeypatch.setattr(run_command.tools.os, "environ", {"ENVVAR": "Value"}) + + # Run the app + run_command.run_app( + first_app, + test_mode=False, + debug_mode=True, + debugger_host="somehost", + debugger_port=9999, + passthrough=[], + ) + + # The process was started + run_command.tools.subprocess._subprocess.Popen.assert_called_with( + [ + os.fsdecode( + tmp_path + / "base_path/build/first-app/somevendor/surprising/first-app-0.0.1/usr/bin/first-app" + ) + ], + cwd=os.fsdecode(tmp_path / "home"), + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + bufsize=1, + env={ + "ENVVAR": "Value", + "BRIEFCASE_DEBUGGER": json.dumps( + { + "host": "somehost", + "port": 9999, + "app_path_mappings": { + "device_sys_path_regex": "app$", + "device_subfolders": ["first_app"], + "host_folders": [str(tmp_path / "base_path/src/first_app")], + }, + "app_packages_path_mappings": { + "sys_path_regex": "app_packages$", + "host_folder": str( + tmp_path + / "base_path/build/first-app/somevendor/surprising/first-app-0.0.1/usr/" + "lib/first-app/app_packages" + ), + }, + } + ), + }, + **sub_kw, + ) + + # The streamer was started + run_command._stream_app_logs.assert_called_once_with( + first_app, + popen=log_popen, + test_mode=False, + clean_output=False, + ) + + def test_run_console_app(run_command, first_app, tmp_path): """A bootstrap binary for a console app can be started.""" first_app.console_app = True diff --git a/tests/platforms/web/static/test_run.py b/tests/platforms/web/static/test_run.py index 265cea89f..b49b928d7 100644 --- a/tests/platforms/web/static/test_run.py +++ b/tests/platforms/web/static/test_run.py @@ -604,3 +604,23 @@ def test_test_mode(run_command, first_app_built): port=8080, open_browser=True, ) + + +def test_debug_mode(run_command, first_app_built): + """Debug mode raises an error (at least for now).""" + # Run the app + with pytest.raises( + BriefcaseCommandError, + match=r"Briefcase can't run web apps in debug mode.", + ): + run_command.run_app( + first_app_built, + test_mode=False, + debug_mode=True, + debugger_host="somehost", + debugger_port=9999, + passthrough=[], + host="localhost", + port=8080, + open_browser=True, + ) diff --git a/tests/platforms/windows/app/test_run.py b/tests/platforms/windows/app/test_run.py index 71d51a9f6..3fc45a7b2 100644 --- a/tests/platforms/windows/app/test_run.py +++ b/tests/platforms/windows/app/test_run.py @@ -1,3 +1,4 @@ +import json import subprocess from unittest import mock @@ -312,3 +313,52 @@ def test_run_app_test_mode_with_passthrough( test_mode=True, clean_output=False, ) + + +def test_run_gui_app_debug_mode(run_command, first_app_config, tmp_path): + """A Windows app can be started in debug mode.""" + # Set up the log streamer to return a known stream + log_popen = mock.MagicMock() + run_command.tools.subprocess.Popen.return_value = log_popen + + # Run the app + run_command.run_app( + first_app_config, + test_mode=False, + debug_mode=True, + debugger_host="somehost", + debugger_port=9999, + passthrough=[], + ) + + # The process was started + run_command.tools.subprocess.Popen.assert_called_with( + [tmp_path / "base_path/build/first-app/windows/app/src/First App.exe"], + cwd=tmp_path / "home", + encoding="UTF-8", + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + bufsize=1, + env={ + "BRIEFCASE_DEBUGGER": json.dumps( + { + "host": "somehost", + "port": 9999, + "app_path_mappings": { + "device_sys_path_regex": "app$", + "device_subfolders": ["first_app"], + "host_folders": [str(tmp_path / "base_path/src/first_app")], + }, + "app_packages_path_mappings": None, + } + ) + }, + ) + + # The streamer was started + run_command._stream_app_logs.assert_called_once_with( + first_app_config, + popen=log_popen, + test_mode=False, + clean_output=False, + ) diff --git a/tests/platforms/windows/visualstudio/test_run.py b/tests/platforms/windows/visualstudio/test_run.py index 807e1be72..bcddd77ba 100644 --- a/tests/platforms/windows/visualstudio/test_run.py +++ b/tests/platforms/windows/visualstudio/test_run.py @@ -1,6 +1,7 @@ # The run command inherits most of its behavior from the common base # implementation. Do a surface-level verification here, but the app # tests provide the actual test coverage. +import json import subprocess from unittest import mock @@ -186,3 +187,56 @@ def test_run_app_test_mode_with_args(run_command, first_app_config, tmp_path): test_mode=True, clean_output=False, ) + + +def test_run_app_debug_mode(run_command, first_app_config, tmp_path): + """A windows Visual Studio project app can be started in debug mode.""" + + # Set up the log streamer to return a known stream with a good returncode + log_popen = mock.MagicMock() + run_command.tools.subprocess.Popen.return_value = log_popen + + # Run the app in test mode + run_command.run_app( + first_app_config, + test_mode=False, + debug_mode=True, + debugger_host="somehost", + debugger_port=9999, + passthrough=[], + ) + + # Popen was called + run_command.tools.subprocess.Popen.assert_called_with( + [ + tmp_path + / "base_path/build/first-app/windows/visualstudio/x64/Release/First App.exe" + ], + cwd=tmp_path / "home", + encoding="UTF-8", + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + bufsize=1, + env={ + "BRIEFCASE_DEBUGGER": json.dumps( + { + "host": "somehost", + "port": 9999, + "app_path_mappings": { + "device_sys_path_regex": "app$", + "device_subfolders": ["first_app"], + "host_folders": [str(tmp_path / "base_path/src/first_app")], + }, + "app_packages_path_mappings": None, + } + ) + }, + ) + + # The streamer was started + run_command._stream_app_logs.assert_called_once_with( + first_app_config, + popen=log_popen, + test_mode=False, + clean_output=False, + ) From 84ec2b042e3d2992b0c4bcfaa1639361322fbc93 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Thu, 22 May 2025 19:56:46 +0200 Subject: [PATCH 030/131] fixed test case and corrected changelog entry --- changes/2147.feature.rst | 2 +- tests/debuggers/test_base.py | 44 ++++++++++++++++++++++++------------ 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/changes/2147.feature.rst b/changes/2147.feature.rst index ec9a8b0ef..667d33cf0 100644 --- a/changes/2147.feature.rst +++ b/changes/2147.feature.rst @@ -1 +1 @@ -Added debugging support in bundled app through ``run --remote-debugger``. +Added debugging support in bundled app through ``briefcase run --debug ``. diff --git a/tests/debuggers/test_base.py b/tests/debuggers/test_base.py index d17236776..0e6af2e45 100644 --- a/tests/debuggers/test_base.py +++ b/tests/debuggers/test_base.py @@ -1,3 +1,5 @@ +import pytest + from briefcase.debuggers import ( DebugpyDebugger, PdbDebugger, @@ -26,17 +28,31 @@ def test_get_debugger(): assert str(e) == "Unknown debugger: unknown" -def test_pdb(): - debugger = PdbDebugger() - debugger.additional_requirements == [ - "git+https://github.com/timrid/briefcase-debugadapter#subdirectory=briefcase-pdb-debugadapter" - ] - debugger.connection_mode == DebuggerConnectionMode.SERVER - - -def test_debugpy(): - debugger = DebugpyDebugger() - debugger.additional_requirements == [ - "git+https://github.com/timrid/briefcase-debugadapter#subdirectory=briefcase-debugpy-debugadapter" - ] - debugger.connection_mode == DebuggerConnectionMode.SERVER +@pytest.mark.parametrize( + "debugger_name, expected_class, additional_requirements, connection_mode", + [ + ( + "pdb", + PdbDebugger, + [ + "git+https://github.com/timrid/briefcase-debugadapter#subdirectory=briefcase-pdb-debugadapter" + ], + DebuggerConnectionMode.SERVER, + ), + ( + "debugpy", + DebugpyDebugger, + [ + "git+https://github.com/timrid/briefcase-debugadapter#subdirectory=briefcase-debugpy-debugadapter" + ], + DebuggerConnectionMode.SERVER, + ), + ], +) +def test_debugger( + debugger_name, expected_class, additional_requirements, connection_mode +): + debugger = get_debugger(debugger_name) + assert isinstance(debugger, expected_class) + assert debugger.additional_requirements == additional_requirements + assert debugger.connection_mode == connection_mode From 68128abc148a415694ae41e28e17426363a53058 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Thu, 22 May 2025 20:09:19 +0200 Subject: [PATCH 031/131] fixed typo --- src/briefcase/commands/run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/briefcase/commands/run.py b/src/briefcase/commands/run.py index 9af183f19..f04ebcdd8 100644 --- a/src/briefcase/commands/run.py +++ b/src/briefcase/commands/run.py @@ -239,7 +239,7 @@ def add_options(self, parser): "--debugger-port", default=5678, type=int, - help="The port on which to run the debug server (default: 8080)", + help="The port on which to run the debug server (default: 5678)", required=False, ) From f5242f7c582e05305a1b62166be7dfcf890f398c Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Thu, 22 May 2025 22:11:34 +0200 Subject: [PATCH 032/131] remove iOS environment variables when stopped --- src/briefcase/platforms/iOS/xcode.py | 16 ++++++++++++++++ tests/platforms/iOS/xcode/test_run.py | 12 ++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/briefcase/platforms/iOS/xcode.py b/src/briefcase/platforms/iOS/xcode.py index f53f33b6d..bb5c203d8 100644 --- a/src/briefcase/platforms/iOS/xcode.py +++ b/src/briefcase/platforms/iOS/xcode.py @@ -710,6 +710,22 @@ def run_app( raise BriefcaseCommandError( f"Unable to launch {label} {app.app_name}." ) from e + finally: + # Remove additional environment variables + if env: + with self.console.wait_bar("Setting environment variables..."): + for env_key in env.keys(): + output = self.tools.subprocess.check_output( + [ + "xcrun", + "simctl", + "spawn", + udid, + "launchctl", + "unsetenv", + f"{env_key}", + ] + ) # Preserve the device selection as state. return {"udid": udid} diff --git a/tests/platforms/iOS/xcode/test_run.py b/tests/platforms/iOS/xcode/test_run.py index 62e0347d7..d8ac74af5 100644 --- a/tests/platforms/iOS/xcode/test_run.py +++ b/tests/platforms/iOS/xcode/test_run.py @@ -1767,6 +1767,18 @@ def test_run_app_debug_mode(run_command, first_app_generated, tmp_path): "com.example.first-app", ], ), + # Remove the environment variables for the debugger + mock.call( + [ + "xcrun", + "simctl", + "spawn", + "2D3503A3-6EB9-4B37-9B17-C7EFEF2FA32D", + "launchctl", + "unsetenv", + "BRIEFCASE_DEBUGGER", + ], + ), ] ) From 5c80cf50afd086d2a891685c3d34bbedc0d67199 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Thu, 5 Jun 2025 20:55:01 +0200 Subject: [PATCH 033/131] fixed some unit tests --- tests/commands/create/conftest.py | 4 +++- tests/commands/create/test_call.py | 10 +++++----- tests/commands/create/test_create_app.py | 6 +++--- tests/commands/update/conftest.py | 2 +- tests/commands/update/test_call.py | 6 +++--- tests/commands/update/test_update_app.py | 4 ++-- 6 files changed, 17 insertions(+), 15 deletions(-) diff --git a/tests/commands/create/conftest.py b/tests/commands/create/conftest.py index 1800baec3..88db61300 100644 --- a/tests/commands/create/conftest.py +++ b/tests/commands/create/conftest.py @@ -169,7 +169,9 @@ def install_app_support_package(self, app): self.actions.append(("support", app.app_name)) def install_app_requirements(self, app): - self.actions.append(("requirements", app.app_name, app.test_mode, app.debugger)) + self.actions.append( + ("requirements", app.app_name, app.test_mode, app.debugger is not None) + ) def install_app_code(self, app): self.actions.append(("code", app.app_name, app.test_mode)) diff --git a/tests/commands/create/test_call.py b/tests/commands/create/test_call.py index ff53d25f4..1ebe6ed4c 100644 --- a/tests/commands/create/test_call.py +++ b/tests/commands/create/test_call.py @@ -39,7 +39,7 @@ def test_create(tracking_create_command, tmp_path): ("verify-app-template", "first"), ("verify-app-tools", "first"), ("code", "first", False), - ("requirements", "first", False), + ("requirements", "first", False, False), ("resources", "first"), ("cleanup", "first"), # Create the second app @@ -48,7 +48,7 @@ def test_create(tracking_create_command, tmp_path): ("verify-app-template", "second"), ("verify-app-tools", "second"), ("code", "second", False), - ("requirements", "second", False), + ("requirements", "second", False, False), ("resources", "second"), ("cleanup", "second"), ] @@ -76,7 +76,7 @@ def test_create_single(tracking_create_command, tmp_path): ("verify-app-template", "first"), ("verify-app-tools", "first"), ("code", "first", False), - ("requirements", "first", False), + ("requirements", "first", False, False), ("resources", "first"), ("cleanup", "first"), ] @@ -111,7 +111,7 @@ def test_create_app_single(tracking_create_command, app_flags): ("verify-app-template", "first"), ("verify-app-tools", "first"), ("code", "first", False), - ("requirements", "first", False), + ("requirements", "first", False, False), ("resources", "first"), ("cleanup", "first"), ] @@ -176,7 +176,7 @@ def test_create_app_all_flags(tracking_create_command): ("verify-app-template", "first"), ("verify-app-tools", "first"), ("code", "first", False), - ("requirements", "first", False), + ("requirements", "first", False, False), ("resources", "first"), ("cleanup", "first"), ] diff --git a/tests/commands/create/test_create_app.py b/tests/commands/create/test_create_app.py index a91f63d48..bf92ea278 100644 --- a/tests/commands/create/test_create_app.py +++ b/tests/commands/create/test_create_app.py @@ -18,7 +18,7 @@ def test_create_app(tracking_create_command, tmp_path): ("verify-app-template", "first"), ("verify-app-tools", "first"), ("code", "first", False), - ("requirements", "first", False), + ("requirements", "first", False, False), ("resources", "first"), ("cleanup", "first"), ] @@ -58,7 +58,7 @@ def test_create_existing_app_overwrite(tracking_create_command, tmp_path): ("verify-app-template", "first"), ("verify-app-tools", "first"), ("code", "first", False), - ("requirements", "first", False), + ("requirements", "first", False, False), ("resources", "first"), ("cleanup", "first"), ] @@ -191,7 +191,7 @@ def test_create_app_with_stub(tracking_create_command, tmp_path): ("verify-app-template", "first"), ("verify-app-tools", "first"), ("code", "first", False), - ("requirements", "first", False), + ("requirements", "first", False, False), ("resources", "first"), ("cleanup", "first"), ] diff --git a/tests/commands/update/conftest.py b/tests/commands/update/conftest.py index e7f0cd566..705472b85 100644 --- a/tests/commands/update/conftest.py +++ b/tests/commands/update/conftest.py @@ -54,7 +54,7 @@ def verify_app_tools(self, app): # with versions that we can use to track actions performed. def install_app_requirements(self, app): self.actions.append( - ("requirements", app.app_name, app.debugger is not None, app.test_mode) + ("requirements", app.app_name, app.test_mode, app.debugger is not None) ) create_file(self.bundle_path(app) / "requirements", "app requirements") diff --git a/tests/commands/update/test_call.py b/tests/commands/update/test_call.py index 0c44a0328..df15f9636 100644 --- a/tests/commands/update/test_call.py +++ b/tests/commands/update/test_call.py @@ -91,13 +91,13 @@ def test_update_with_requirements(update_command, first_app, second_app): ("verify-app-template", "first"), ("verify-app-tools", "first"), ("code", "first", False), - ("requirements", "first", False), + ("requirements", "first", False, False), ("cleanup", "first"), # Update the second app ("verify-app-template", "second"), ("verify-app-tools", "second"), ("code", "second", False), - ("requirements", "second", False), + ("requirements", "second", False, False), ("cleanup", "second"), ] @@ -265,7 +265,7 @@ def test_update_app_all_flags(update_command, first_app, second_app): ("verify-app-template", "first"), ("verify-app-tools", "first"), ("code", "first", False), - ("requirements", "first", False), + ("requirements", "first", False, False), ("resources", "first"), ("cleanup-support", "first"), ("support", "first"), diff --git a/tests/commands/update/test_update_app.py b/tests/commands/update/test_update_app.py index c5720a8bd..f8d7de6fc 100644 --- a/tests/commands/update/test_update_app.py +++ b/tests/commands/update/test_update_app.py @@ -63,7 +63,7 @@ def test_update_app_with_requirements(update_command, first_app, tmp_path): ("verify-app-template", "first"), ("verify-app-tools", "first"), ("code", "first", False), - ("requirements", "first", False), + ("requirements", "first", False, False), ("cleanup", "first"), ] @@ -267,7 +267,7 @@ def test_update_app_test_mode_requirements(update_command, first_app, tmp_path): ("verify-app-template", "first"), ("verify-app-tools", "first"), ("code", "first", True), - ("requirements", "first", True), + ("requirements", "first", True, False), ("cleanup", "first"), ] From 9314998f7d8eff496d21a19d58d7a685a24b7ee4 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Thu, 5 Jun 2025 21:13:15 +0200 Subject: [PATCH 034/131] added `debug_mode` in `AppConfig` and fixed unit tests --- src/briefcase/commands/base.py | 1 + src/briefcase/commands/build.py | 6 +++--- src/briefcase/commands/create.py | 2 +- src/briefcase/commands/run.py | 2 +- src/briefcase/config.py | 1 + src/briefcase/platforms/android/gradle.py | 4 ++-- src/briefcase/platforms/iOS/xcode.py | 2 +- src/briefcase/platforms/web/static.py | 2 +- tests/commands/build/conftest.py | 6 +++--- tests/commands/create/conftest.py | 2 +- .../create/test_generate_app_template.py | 1 + .../create/test_install_app_requirements.py | 13 +------------ tests/commands/run/conftest.py | 4 ++-- tests/commands/update/conftest.py | 2 +- tests/platforms/android/gradle/test_create.py | 1 + tests/platforms/android/gradle/test_run.py | 16 ++-------------- tests/platforms/iOS/xcode/test_run.py | 14 +------------- tests/platforms/linux/appimage/test_run.py | 14 +------------- tests/platforms/linux/flatpak/test_run.py | 14 +------------- tests/platforms/linux/system/test_run.py | 14 +------------- tests/platforms/web/static/test_run.py | 13 +------------ tests/platforms/windows/app/test_run.py | 14 +------------- tests/platforms/windows/visualstudio/test_run.py | 13 +------------ 23 files changed, 30 insertions(+), 131 deletions(-) diff --git a/src/briefcase/commands/base.py b/src/briefcase/commands/base.py index 19a79ec6a..34f7b8254 100644 --- a/src/briefcase/commands/base.py +++ b/src/briefcase/commands/base.py @@ -674,6 +674,7 @@ def finalize_debugger(self, app: AppConfig, debugger_name: str | None = None): if debugger_name and debugger_name != "": debugger = get_debugger(debugger_name) app.debug_requires.extend(debugger.additional_requirements) + app.debug_mode = True app.debugger = debugger def verify_app(self, app: AppConfig): diff --git a/src/briefcase/commands/build.py b/src/briefcase/commands/build.py index 1d69ec15f..12af07e19 100644 --- a/src/briefcase/commands/build.py +++ b/src/briefcase/commands/build.py @@ -63,13 +63,13 @@ def _build_app( update # An explicit update has been requested or update_requirements # An explicit update of requirements has been requested or update_resources # An explicit update of resources has been requested - or update_support # An explicit update of app support has been rdebuggerequested + or update_support # An explicit update of app support has been requested or update_stub # An explicit update of the stub binary has been requested or ( app.test_mode and not no_update ) # Test mode, but updates have not been disabled or ( - app.debugger and not no_update + app.debug_mode and not no_update ) # Debug mode, but updates have not been disabled ): state = self.update_command( @@ -88,7 +88,7 @@ def _build_app( state = self.build_app(app, **full_options(state, options)) qualifier = " (test mode)" if app.test_mode else "" - qualifier += " (debug mode)" if app.debugger else "" + qualifier += " (debug mode)" if app.debug_mode else "" self.console.info( f"Built {self.binary_path(app).relative_to(self.base_path)}{qualifier}", prefix=app.app_name, diff --git a/src/briefcase/commands/create.py b/src/briefcase/commands/create.py index 78d378dd8..704424e6b 100644 --- a/src/briefcase/commands/create.py +++ b/src/briefcase/commands/create.py @@ -688,7 +688,7 @@ def install_app_requirements(self, app: AppConfig): if app.test_mode and app.test_requires: requires.extend(app.test_requires) - if app.debugger and app.debug_requires: + if app.debug_mode and app.debug_requires: requires.extend(app.debug_requires) try: diff --git a/src/briefcase/commands/run.py b/src/briefcase/commands/run.py index 68685ff09..668d3d710 100644 --- a/src/briefcase/commands/run.py +++ b/src/briefcase/commands/run.py @@ -318,7 +318,7 @@ def _prepare_app_kwargs( env["BRIEFCASE_DEBUG"] = "1" # If we're in remote debug mode, save the remote debugger config - if app.debugger: + if app.debug_mode: env["BRIEFCASE_DEBUGGER"] = self.remote_debugger_config( app, debugger_host, debugger_port ) diff --git a/src/briefcase/config.py b/src/briefcase/config.py index 050da1394..83cc7f2fd 100644 --- a/src/briefcase/config.py +++ b/src/briefcase/config.py @@ -347,6 +347,7 @@ def __init__( [] if requirement_installer_args is None else requirement_installer_args ) self.test_mode: bool = False + self.debug_mode: bool = False self.debugger: BaseDebugger | None = None if not is_valid_app_name(self.app_name): diff --git a/src/briefcase/platforms/android/gradle.py b/src/briefcase/platforms/android/gradle.py index d71804ba1..c75a4372e 100644 --- a/src/briefcase/platforms/android/gradle.py +++ b/src/briefcase/platforms/android/gradle.py @@ -224,7 +224,7 @@ def output_format_template_context(self, app: AppConfig): # In debug mode extract all source packages so that the debugger can get the source code # at runtime (eg. via 'll' in pdb). - if app.debugger: + if app.debug_mode: extract_sources.extend(app.sources) return { @@ -454,7 +454,7 @@ def run_app( adb.install_apk(self.binary_path(app)) env = {} - if app.debugger: + if app.debug_mode: if debugger_host == "localhost": with self.console.wait_bar("Establishing debugger connection..."): self.establish_debugger_connection( diff --git a/src/briefcase/platforms/iOS/xcode.py b/src/briefcase/platforms/iOS/xcode.py index e95f1607e..fccfd0298 100644 --- a/src/briefcase/platforms/iOS/xcode.py +++ b/src/briefcase/platforms/iOS/xcode.py @@ -663,7 +663,7 @@ def run_app( # Add additional environment variables env = {} - if app.debugger: + if app.debug_mode: env["BRIEFCASE_DEBUGGER"] = self.remote_debugger_config( app, debugger_host, debugger_port ) diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index 12ca89c92..9d32542d9 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -330,7 +330,7 @@ def run_app( """ if app.test_mode: raise BriefcaseCommandError("Briefcase can't run web apps in test mode.") - if app.debugger: + if app.debug_mode: raise BriefcaseCommandError("Briefcase can't run web apps in debug mode.") self.console.info("Starting web server...", prefix=app.app_name) diff --git a/tests/commands/build/conftest.py b/tests/commands/build/conftest.py index e893a9af8..650a0388d 100644 --- a/tests/commands/build/conftest.py +++ b/tests/commands/build/conftest.py @@ -57,7 +57,7 @@ def build_app(self, app, **kwargs): "build", app.app_name, app.test_mode, - app.debugger is not None, + app.debug_mode, kwargs.copy(), ) ) @@ -79,7 +79,7 @@ def create_command(self, app, **kwargs): "create", app.app_name, app.test_mode, - app.debugger is not None, + app.debug_mode, kwargs.copy(), ) ) @@ -92,7 +92,7 @@ def update_command(self, app, **kwargs): "update", app.app_name, app.test_mode, - app.debugger is not None, + app.debug_mode, kwargs.copy(), ) ) diff --git a/tests/commands/create/conftest.py b/tests/commands/create/conftest.py index 88db61300..e475804b9 100644 --- a/tests/commands/create/conftest.py +++ b/tests/commands/create/conftest.py @@ -170,7 +170,7 @@ def install_app_support_package(self, app): def install_app_requirements(self, app): self.actions.append( - ("requirements", app.app_name, app.test_mode, app.debugger is not None) + ("requirements", app.app_name, app.test_mode, app.debug_mode) ) def install_app_code(self, app): diff --git a/tests/commands/create/test_generate_app_template.py b/tests/commands/create/test_generate_app_template.py index 9d9fb96ce..a727cef0a 100644 --- a/tests/commands/create/test_generate_app_template.py +++ b/tests/commands/create/test_generate_app_template.py @@ -47,6 +47,7 @@ def full_context(): "license": {"file": "LICENSE"}, "requirement_installer_args": [], "test_mode": False, + "debug_mode": False, # Properties of the generating environment "python_version": platform.python_version(), "host_arch": "gothic", diff --git a/tests/commands/create/test_install_app_requirements.py b/tests/commands/create/test_install_app_requirements.py index 5f47f0085..58bb6404f 100644 --- a/tests/commands/create/test_install_app_requirements.py +++ b/tests/commands/create/test_install_app_requirements.py @@ -10,7 +10,6 @@ import briefcase from briefcase.commands.create import _is_local_path from briefcase.console import LogLevel -from briefcase.debuggers.base import BaseDebugger, DebuggerConnectionMode from briefcase.exceptions import BriefcaseCommandError, RequirementsInstallError from briefcase.integrations.subprocess import Subprocess @@ -1088,16 +1087,6 @@ def test_app_packages_only_test_requires_test_mode( assert myapp.test_requires == ["pytest", "pytest-tldr"] -class DummyDebugger(BaseDebugger): - @property - def additional_requirements(self) -> list[str]: - raise NotImplementedError - - @property - def connection_mode(self) -> DebuggerConnectionMode: - raise NotImplementedError - - def test_app_packages_debug_requires_debug_mode( create_command, myapp, @@ -1107,7 +1096,7 @@ def test_app_packages_debug_requires_debug_mode( """If an app has debug requirements and we're in debug mode, they are installed.""" myapp.requires = ["first", "second==1.2.3", "third>=3.2.1"] myapp.debug_requires = ["debugpy"] - myapp.debugger = DummyDebugger() + myapp.debug_mode = True create_command.install_app_requirements(myapp) diff --git a/tests/commands/run/conftest.py b/tests/commands/run/conftest.py index 078a70198..8bd27db6d 100644 --- a/tests/commands/run/conftest.py +++ b/tests/commands/run/conftest.py @@ -57,7 +57,7 @@ def run_app(self, app, **kwargs): "run", app.app_name, app.test_mode, - app.debugger is not None, + app.debug_mode, kwargs.copy(), ) ) @@ -94,7 +94,7 @@ def build_command(self, app, **kwargs): "build", app.app_name, app.test_mode, - app.debugger is not None, + app.debug_mode, kwargs.copy(), ) ) diff --git a/tests/commands/update/conftest.py b/tests/commands/update/conftest.py index 705472b85..21935b264 100644 --- a/tests/commands/update/conftest.py +++ b/tests/commands/update/conftest.py @@ -54,7 +54,7 @@ def verify_app_tools(self, app): # with versions that we can use to track actions performed. def install_app_requirements(self, app): self.actions.append( - ("requirements", app.app_name, app.test_mode, app.debugger is not None) + ("requirements", app.app_name, app.test_mode, app.debug_mode) ) create_file(self.bundle_path(app) / "requirements", "app requirements") diff --git a/tests/platforms/android/gradle/test_create.py b/tests/platforms/android/gradle/test_create.py index 7b151a1b4..e464b9551 100644 --- a/tests/platforms/android/gradle/test_create.py +++ b/tests/platforms/android/gradle/test_create.py @@ -204,6 +204,7 @@ def test_extract_packages(create_command, first_app_config, test_sources, expect def test_extract_packages_debug_mode(create_command, first_app_config): first_app_config.test_sources = ["one", "two", "three"] first_app_config.sources = ["four", "five", "six"] + first_app_config.debug_mode = True context = create_command.output_format_template_context(first_app_config) assert context["extract_packages"] == '"one", "two", "three", "four", "five", "six"' diff --git a/tests/platforms/android/gradle/test_run.py b/tests/platforms/android/gradle/test_run.py index f4d52a6d1..37c478f26 100644 --- a/tests/platforms/android/gradle/test_run.py +++ b/tests/platforms/android/gradle/test_run.py @@ -806,16 +806,6 @@ def test_run_test_mode_created_emulator(run_command, first_app_config): run_command.tools.mock_adb.kill.assert_called_once_with() -class DummyDebugger(BaseDebugger): - @property - def additional_requirements(self) -> list[str]: - raise NotImplementedError - - @property - def connection_mode(self) -> DebuggerConnectionMode: - raise NotImplementedError - - def test_run_debug_mode(run_command, first_app_config, tmp_path): """An app can be run in debug mode.""" # Set up device selection to return a running physical device. @@ -837,7 +827,7 @@ def mock_stream_output(app, stop_func, **kwargs): # Set up app config to have a `-` in the `bundle`, to ensure it gets # normalized into a `_` via `package_name`. first_app_config.bundle = "com.ex-ample" - first_app_config.debugger = DummyDebugger() + first_app_config.debug_mode = True # Invoke run_app with args. run_command.run_app( @@ -899,7 +889,6 @@ def mock_stream_output(app, stop_func, **kwargs): run_command._stream_app_logs.assert_called_once_with( first_app_config, popen=log_popen, - test_mode=False, clean_filter=android_log_clean_filter, clean_output=False, stop_func=mock.ANY, @@ -960,8 +949,8 @@ def mock_stream_output(app, stop_func, **kwargs): first_app_config.bundle = "com.ex-ample" # Set up the debugger + first_app_config.debug_mode = True first_app_config.debugger = debugger - first_app_config.debugger = DummyDebugger() # Invoke run_app with args. run_command.run_app( @@ -1041,7 +1030,6 @@ def mock_stream_output(app, stop_func, **kwargs): run_command._stream_app_logs.assert_called_once_with( first_app_config, popen=log_popen, - test_mode=False, clean_filter=android_log_clean_filter, clean_output=False, stop_func=mock.ANY, diff --git a/tests/platforms/iOS/xcode/test_run.py b/tests/platforms/iOS/xcode/test_run.py index 623ca8644..f5da40460 100644 --- a/tests/platforms/iOS/xcode/test_run.py +++ b/tests/platforms/iOS/xcode/test_run.py @@ -6,7 +6,6 @@ import pytest from briefcase.console import Console -from briefcase.debuggers.base import BaseDebugger, DebuggerConnectionMode from briefcase.exceptions import BriefcaseCommandError from briefcase.integrations.subprocess import Subprocess from briefcase.integrations.xcode import DeviceState @@ -1608,16 +1607,6 @@ def test_run_app_test_mode_with_passthrough(run_command, first_app_config, tmp_p ) -class DummyDebugger(BaseDebugger): - @property - def additional_requirements(self) -> list[str]: - raise NotImplementedError - - @property - def connection_mode(self) -> DebuggerConnectionMode: - raise NotImplementedError - - @pytest.mark.usefixtures("sleep_zero") def test_run_app_debug_mode(run_command, first_app_generated, tmp_path): """An iOS App can be started in debug mode.""" @@ -1648,7 +1637,7 @@ def test_run_app_debug_mode(run_command, first_app_generated, tmp_path): log_stream_process, ] - first_app_generated.debugger = DummyDebugger() + first_app_generated.debug_mode = True # Run the app run_command.run_app( @@ -1773,7 +1762,6 @@ def test_run_app_debug_mode(run_command, first_app_generated, tmp_path): run_command._stream_app_logs.assert_called_with( first_app_generated, popen=log_stream_process, - test_mode=False, clean_filter=macOS_log_clean_filter, clean_output=True, stop_func=mock.ANY, diff --git a/tests/platforms/linux/appimage/test_run.py b/tests/platforms/linux/appimage/test_run.py index 70f5903f3..5c8ef0130 100644 --- a/tests/platforms/linux/appimage/test_run.py +++ b/tests/platforms/linux/appimage/test_run.py @@ -5,7 +5,6 @@ import pytest from briefcase.console import Console, LogLevel -from briefcase.debuggers.base import BaseDebugger, DebuggerConnectionMode from briefcase.exceptions import UnsupportedHostError from briefcase.integrations.subprocess import Subprocess from briefcase.platforms.linux.appimage import LinuxAppImageRunCommand @@ -305,23 +304,13 @@ def test_run_app_test_mode_with_args( ) -class DummyDebugger(BaseDebugger): - @property - def additional_requirements(self) -> list[str]: - raise NotImplementedError - - @property - def connection_mode(self) -> DebuggerConnectionMode: - raise NotImplementedError - - def test_run_app_debug_mode(run_command, first_app_config, tmp_path): """A linux App can be started in debug mode.""" # Set up the log streamer to return a known stream log_popen = mock.MagicMock() run_command.tools.subprocess.Popen.return_value = log_popen - first_app_config.debugger = DummyDebugger() + first_app_config.debug_mode = True # Run the app run_command.run_app( @@ -361,6 +350,5 @@ def test_run_app_debug_mode(run_command, first_app_config, tmp_path): run_command._stream_app_logs.assert_called_once_with( first_app_config, popen=log_popen, - test_mode=False, clean_output=False, ) diff --git a/tests/platforms/linux/flatpak/test_run.py b/tests/platforms/linux/flatpak/test_run.py index 726867a3e..412fa269b 100644 --- a/tests/platforms/linux/flatpak/test_run.py +++ b/tests/platforms/linux/flatpak/test_run.py @@ -4,7 +4,6 @@ import pytest from briefcase.console import Console, LogLevel -from briefcase.debuggers.base import BaseDebugger, DebuggerConnectionMode from briefcase.integrations.flatpak import Flatpak from briefcase.integrations.subprocess import Subprocess from briefcase.platforms.linux.flatpak import LinuxFlatpakRunCommand @@ -237,23 +236,13 @@ def test_run_test_mode_with_args(run_command, first_app_config, is_console_app): ) -class DummyDebugger(BaseDebugger): - @property - def additional_requirements(self) -> list[str]: - raise NotImplementedError - - @property - def connection_mode(self) -> DebuggerConnectionMode: - raise NotImplementedError - - def test_run_debug_mode(run_command, first_app_config, tmp_path): """A flatpak can be executed in debug mode.""" # Set up the log streamer to return a known stream and a good return code log_popen = mock.MagicMock() run_command.tools.flatpak.run.return_value = log_popen - first_app_config.debugger = DummyDebugger() + first_app_config.debug_mode = True # Run the app run_command.run_app( @@ -294,6 +283,5 @@ def test_run_debug_mode(run_command, first_app_config, tmp_path): run_command._stream_app_logs.assert_called_once_with( first_app_config, popen=log_popen, - test_mode=False, clean_output=False, ) diff --git a/tests/platforms/linux/system/test_run.py b/tests/platforms/linux/system/test_run.py index cf3b00ce8..259dc1872 100644 --- a/tests/platforms/linux/system/test_run.py +++ b/tests/platforms/linux/system/test_run.py @@ -8,7 +8,6 @@ import pytest from briefcase.console import Console, LogLevel -from briefcase.debuggers.base import BaseDebugger, DebuggerConnectionMode from briefcase.exceptions import UnsupportedHostError from briefcase.integrations.docker import Docker from briefcase.integrations.subprocess import Subprocess @@ -352,16 +351,6 @@ def test_run_gui_app_failed(run_command, first_app, sub_kw, tmp_path): run_command._stream_app_logs.assert_not_called() -class DummyDebugger(BaseDebugger): - @property - def additional_requirements(self) -> list[str]: - raise NotImplementedError - - @property - def connection_mode(self) -> DebuggerConnectionMode: - raise NotImplementedError - - def test_run_gui_app_debug_mode(run_command, first_app, sub_kw, tmp_path, monkeypatch): """A bootstrap binary for a GUI app can be started in debug mode.""" @@ -377,7 +366,7 @@ def test_run_gui_app_debug_mode(run_command, first_app, sub_kw, tmp_path, monkey # Mock out the environment monkeypatch.setattr(run_command.tools.os, "environ", {"ENVVAR": "Value"}) - first_app.debugger = DummyDebugger() + first_app.debug_mode = True # Run the app run_command.run_app( @@ -428,7 +417,6 @@ def test_run_gui_app_debug_mode(run_command, first_app, sub_kw, tmp_path, monkey run_command._stream_app_logs.assert_called_once_with( first_app, popen=log_popen, - test_mode=False, clean_output=False, ) diff --git a/tests/platforms/web/static/test_run.py b/tests/platforms/web/static/test_run.py index 563faed1f..2a1b04100 100644 --- a/tests/platforms/web/static/test_run.py +++ b/tests/platforms/web/static/test_run.py @@ -6,7 +6,6 @@ import pytest from briefcase.console import Console -from briefcase.debuggers.base import BaseDebugger, DebuggerConnectionMode from briefcase.exceptions import BriefcaseCommandError from briefcase.platforms.web.static import ( HTTPHandler, @@ -593,19 +592,9 @@ def test_test_mode(run_command, first_app_built): ) -class DummyDebugger(BaseDebugger): - @property - def additional_requirements(self) -> list[str]: - raise NotImplementedError - - @property - def connection_mode(self) -> DebuggerConnectionMode: - raise NotImplementedError - - def test_debug_mode(run_command, first_app_built): """Debug mode raises an error (at least for now).""" - first_app_built.debugger = DummyDebugger() + first_app_built.debug_mode = True # Run the app with pytest.raises( diff --git a/tests/platforms/windows/app/test_run.py b/tests/platforms/windows/app/test_run.py index 520c20496..cc68cfb48 100644 --- a/tests/platforms/windows/app/test_run.py +++ b/tests/platforms/windows/app/test_run.py @@ -5,7 +5,6 @@ import pytest from briefcase.console import Console, LogLevel -from briefcase.debuggers.base import BaseDebugger, DebuggerConnectionMode from briefcase.integrations.subprocess import Subprocess from briefcase.platforms.windows.app import WindowsAppRunCommand @@ -283,23 +282,13 @@ def test_run_app_test_mode_with_passthrough( ) -class DummyDebugger(BaseDebugger): - @property - def additional_requirements(self) -> list[str]: - raise NotImplementedError - - @property - def connection_mode(self) -> DebuggerConnectionMode: - raise NotImplementedError - - def test_run_gui_app_debug_mode(run_command, first_app_config, tmp_path): """A Windows app can be started in debug mode.""" # Set up the log streamer to return a known stream log_popen = mock.MagicMock() run_command.tools.subprocess.Popen.return_value = log_popen - first_app_config.debugger = DummyDebugger() + first_app_config.debug_mode = True # Run the app run_command.run_app( @@ -337,6 +326,5 @@ def test_run_gui_app_debug_mode(run_command, first_app_config, tmp_path): run_command._stream_app_logs.assert_called_once_with( first_app_config, popen=log_popen, - test_mode=False, clean_output=False, ) diff --git a/tests/platforms/windows/visualstudio/test_run.py b/tests/platforms/windows/visualstudio/test_run.py index 7129ff359..f912d3e83 100644 --- a/tests/platforms/windows/visualstudio/test_run.py +++ b/tests/platforms/windows/visualstudio/test_run.py @@ -8,7 +8,6 @@ import pytest from briefcase.console import Console -from briefcase.debuggers.base import BaseDebugger, DebuggerConnectionMode from briefcase.integrations.subprocess import Subprocess from briefcase.platforms.windows.visualstudio import WindowsVisualStudioRunCommand @@ -174,18 +173,9 @@ def test_run_app_test_mode_with_args(run_command, first_app_config, tmp_path): ) -class DummyDebugger(BaseDebugger): - @property - def additional_requirements(self) -> list[str]: - raise NotImplementedError - - @property - def connection_mode(self) -> DebuggerConnectionMode: - raise NotImplementedError - - def test_run_app_debug_mode(run_command, first_app_config, tmp_path): """A windows Visual Studio project app can be started in debug mode.""" + first_app_config.debug_mode = True # Set up the log streamer to return a known stream with a good returncode log_popen = mock.MagicMock() @@ -230,6 +220,5 @@ def test_run_app_debug_mode(run_command, first_app_config, tmp_path): run_command._stream_app_logs.assert_called_once_with( first_app_config, popen=log_popen, - test_mode=False, clean_output=False, ) From cb5a32ea24876c6c8858a6587ba704fe268b9a71 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Thu, 5 Jun 2025 22:02:39 +0200 Subject: [PATCH 035/131] add context manager for android debugging connection --- src/briefcase/platforms/android/gradle.py | 169 +++++++++++----------- 1 file changed, 87 insertions(+), 82 deletions(-) diff --git a/src/briefcase/platforms/android/gradle.py b/src/briefcase/platforms/android/gradle.py index c75a4372e..c3b7adc4c 100644 --- a/src/briefcase/platforms/android/gradle.py +++ b/src/briefcase/platforms/android/gradle.py @@ -1,5 +1,6 @@ from __future__ import annotations +import contextlib import datetime import re import subprocess @@ -19,7 +20,6 @@ from briefcase.console import ANSI_ESC_SEQ_RE_DEF from briefcase.debuggers.base import ( AppPackagesPathMappings, - BaseDebugger, DebuggerConnectionMode, ) from briefcase.exceptions import BriefcaseCommandError @@ -429,7 +429,6 @@ def run_app( avd, extra_emulator_args ) - debugger_connection_established = False try: label = "test suite" if app.test_mode else "app" @@ -454,103 +453,109 @@ def run_app( adb.install_apk(self.binary_path(app)) env = {} - if app.debug_mode: - if debugger_host == "localhost": - with self.console.wait_bar("Establishing debugger connection..."): - self.establish_debugger_connection( - adb, app.debugger, debugger_port - ) - debugger_connection_established = True - env["BRIEFCASE_DEBUGGER"] = self.remote_debugger_config( - app, debugger_host, debugger_port - ) - if self.console.is_debug: env["BRIEFCASE_DEBUG"] = "1" - # To start the app, we launch `org.beeware.android.MainActivity`. - with self.console.wait_bar(f"Launching {label}..."): - # capture the earliest time for device logging in case PID not found - device_start_time = adb.datetime() - - adb.start_app( - package, "org.beeware.android.MainActivity", passthrough, env - ) - - # Try to get the PID for 5 seconds. - pid = None - fail_time = datetime.datetime.now() + datetime.timedelta(seconds=5) - while not pid and datetime.datetime.now() < fail_time: - # Try to get the PID; run in quiet mode because we may - # need to do this a lot in the next 5 seconds. - pid = adb.pidof(package, quiet=2) - if not pid: - time.sleep(0.01) - - if pid: - self.console.info( - "Following device log output (type CTRL-C to stop log)...", - prefix=app.app_name, - ) - # Start adb's logcat in a way that lets us stream the logs - log_popen = adb.logcat(pid=pid) - - # Stream the app logs. - self._stream_app_logs( - app, - popen=log_popen, - clean_filter=android_log_clean_filter, - clean_output=False, - # Check for the PID in quiet mode so logs aren't corrupted. - stop_func=lambda: not adb.pid_exists(pid=pid, quiet=2), - log_stream=True, + if app.debug_mode: + env["BRIEFCASE_DEBUGGER"] = self.remote_debugger_config( + app, debugger_host, debugger_port ) - else: - self.console.error("Unable to find PID for app", prefix=app.app_name) - self.console.error("Logs for launch attempt follow...") - self.console.error("=" * 75) - # Show the log from the start time of the app - adb.logcat_tail(since=device_start_time) + # Forward port for debugger connection if configured. Else this is a no-op. + with self.forward_port_for_debugger(app, debugger_host, debugger_port, adb): + # To start the app, we launch `org.beeware.android.MainActivity`. + with self.console.wait_bar(f"Launching {label}..."): + # capture the earliest time for device logging in case PID not found + device_start_time = adb.datetime() + + adb.start_app( + package, "org.beeware.android.MainActivity", passthrough, env + ) + + # Try to get the PID for 5 seconds. + pid = None + fail_time = datetime.datetime.now() + datetime.timedelta(seconds=5) + while not pid and datetime.datetime.now() < fail_time: + # Try to get the PID; run in quiet mode because we may + # need to do this a lot in the next 5 seconds. + pid = adb.pidof(package, quiet=2) + if not pid: + time.sleep(0.01) + + if pid: + self.console.info( + "Following device log output (type CTRL-C to stop log)...", + prefix=app.app_name, + ) + # Start adb's logcat in a way that lets us stream the logs + log_popen = adb.logcat(pid=pid) + + # Stream the app logs. + self._stream_app_logs( + app, + popen=log_popen, + clean_filter=android_log_clean_filter, + clean_output=False, + # Check for the PID in quiet mode so logs aren't corrupted. + stop_func=lambda: not adb.pid_exists(pid=pid, quiet=2), + log_stream=True, + ) + else: + self.console.error( + "Unable to find PID for app", prefix=app.app_name + ) + self.console.error("Logs for launch attempt follow...") + self.console.error("=" * 75) + + # Show the log from the start time of the app + adb.logcat_tail(since=device_start_time) + + raise BriefcaseCommandError( + f"Problem starting app {app.app_name!r}" + ) - raise BriefcaseCommandError(f"Problem starting app {app.app_name!r}") finally: - if debugger_connection_established: - with self.console.wait_bar("Stopping debugger connection..."): - self.remove_debugger_connection(adb, app.debugger, debugger_port) if shutdown_on_exit: with self.tools.console.wait_bar("Stopping emulator..."): adb.kill() - def establish_debugger_connection( - self, adb: ADB, debugger: BaseDebugger, debugger_port: int + @contextlib.contextmanager + def forward_port_for_debugger( + self, app: AppConfig, debugger_host: str, debugger_port: int, adb: ADB ): - """Forward/Reverse the ports necessary for remote debugging. - + """Establish a port forwarding for the debugger connection. :param app: The config object for the app - :param adb: Access to the adb - :param debugger: The debugger to use - :param debugger_port: The port to forward/reverse for the debugger + :param debugger_host: The host to use for the debugger + :param debugger_port: The port to use for the debugger + :param adb: The ADB wrapper for the device """ - if debugger.connection_mode == DebuggerConnectionMode.SERVER: - adb.forward(debugger_port, debugger_port) - else: - adb.reverse(debugger_port, debugger_port) + if app.debug_mode and app.debugger and debugger_host == "localhost": + if app.debugger.connection_mode == DebuggerConnectionMode.SERVER: + with self.console.wait_bar( + f"Start forwarding port '{debugger_port}' from host to device for debugger connection..." + ): + adb.forward(debugger_port, debugger_port) + + yield + + with self.console.wait_bar( + f"Stop forwarding port '{debugger_port}' from host to device for debugger connection..." + ): + adb.forward_remove(debugger_port) + else: + with self.console.wait_bar( + f"Start reversing port '{debugger_port}' from device to host for debugger connection..." + ): + adb.reverse(debugger_port, debugger_port) - def remove_debugger_connection( - self, adb: ADB, debugger: BaseDebugger, debugger_port: int - ): - """Remove Forward/Reverse of the ports necessary for remote debugging. + yield - :param app: The config object for the app - :param adb: Access to the adb - :param debugger: The debugger to use - :param debugger_port: The port to forward/reverse for the debugger - """ - if debugger.connection_mode == DebuggerConnectionMode.SERVER: - adb.forward_remove(debugger_port) + with self.console.wait_bar( + f"Stop reversing port '{debugger_port}' from device to host for debugger connection..." + ): + adb.reverse_remove(debugger_port) else: - adb.reverse_remove(debugger_port) + yield # no-op if no debugger is configured class GradlePackageCommand(GradleMixin, PackageCommand): From b9d9d4b8b8d9391525b00ba9ba34d25d81c44b1e Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sun, 8 Jun 2025 16:53:18 +0200 Subject: [PATCH 036/131] add context manager for iOS environment variables --- src/briefcase/platforms/iOS/xcode.py | 121 +++++++++++++++------------ 1 file changed, 67 insertions(+), 54 deletions(-) diff --git a/src/briefcase/platforms/iOS/xcode.py b/src/briefcase/platforms/iOS/xcode.py index fccfd0298..66c4b6ef5 100644 --- a/src/briefcase/platforms/iOS/xcode.py +++ b/src/briefcase/platforms/iOS/xcode.py @@ -1,5 +1,6 @@ from __future__ import annotations +import contextlib import plistlib import subprocess import time @@ -668,11 +669,56 @@ def run_app( app, debugger_host, debugger_port ) - # Install additional environment variables + # Set additional environment variables while the app is running (no-op if no environment variables are set). + with self.setup_env(env, udid): + try: + self.console.info(f"Starting {label}...", prefix=app.app_name) + with self.console.wait_bar(f"Launching {label}..."): + output = self.tools.subprocess.check_output( + ["xcrun", "simctl", "launch", udid, app.bundle_identifier] + + passthrough + ) + try: + app_pid = int(output.split(":")[1].strip()) + except (IndexError, ValueError) as e: + raise BriefcaseCommandError( + f"Unable to determine PID of {label} {app.app_name}." + ) from e + + # Start streaming logs for the app. + self.console.info( + "Following simulator log output (type CTRL-C to stop log)...", + prefix=app.app_name, + ) + + # Stream the app logs, + self._stream_app_logs( + app, + popen=simulator_log_popen, + clean_filter=macOS_log_clean_filter, + clean_output=True, + stop_func=lambda: is_process_dead(app_pid), + log_stream=True, + ) + except subprocess.CalledProcessError as e: + raise BriefcaseCommandError( + f"Unable to launch {label} {app.app_name}." + ) from e + + # Preserve the device selection as state. + return {"udid": udid} + + @contextlib.contextmanager + def setup_env( + self, + env: dict[str, str] | None, + udid: str, + ): + """Context manager to set up the environment for the command.""" if env: - with self.console.wait_bar("Setting environment variables..."): + with self.console.wait_bar("Setting environment variables in simulator..."): for env_key, env_value in env.items(): - output = self.tools.subprocess.check_output( + self.tools.subprocess.check_output( [ "xcrun", "simctl", @@ -685,58 +731,25 @@ def run_app( ] ) - try: - self.console.info(f"Starting {label}...", prefix=app.app_name) - with self.console.wait_bar(f"Launching {label}..."): - output = self.tools.subprocess.check_output( - ["xcrun", "simctl", "launch", udid, app.bundle_identifier] - + passthrough - ) - try: - app_pid = int(output.split(":")[1].strip()) - except (IndexError, ValueError) as e: - raise BriefcaseCommandError( - f"Unable to determine PID of {label} {app.app_name}." - ) from e - - # Start streaming logs for the app. - self.console.info( - "Following simulator log output (type CTRL-C to stop log)...", - prefix=app.app_name, - ) + yield - # Stream the app logs, - self._stream_app_logs( - app, - popen=simulator_log_popen, - clean_filter=macOS_log_clean_filter, - clean_output=True, - stop_func=lambda: is_process_dead(app_pid), - log_stream=True, - ) - except subprocess.CalledProcessError as e: - raise BriefcaseCommandError( - f"Unable to launch {label} {app.app_name}." - ) from e - finally: - # Remove additional environment variables - if env: - with self.console.wait_bar("Setting environment variables..."): - for env_key in env.keys(): - output = self.tools.subprocess.check_output( - [ - "xcrun", - "simctl", - "spawn", - udid, - "launchctl", - "unsetenv", - f"{env_key}", - ] - ) - - # Preserve the device selection as state. - return {"udid": udid} + with self.console.wait_bar( + "Removing environment variables from simulator..." + ): + for env_key in env.keys(): + self.tools.subprocess.check_output( + [ + "xcrun", + "simctl", + "spawn", + udid, + "launchctl", + "unsetenv", + f"{env_key}", + ] + ) + else: + yield # no-op if no environment variables are set class iOSXcodePackageCommand(iOSXcodeMixin, PackageCommand): From 478eb56f7bb1073f25e85c42f55dbdaa4893cb1f Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Tue, 10 Jun 2025 21:09:41 +0200 Subject: [PATCH 037/131] removed "debug_requires" and "debug_mode". --- src/briefcase/commands/base.py | 2 -- src/briefcase/commands/build.py | 4 ++-- src/briefcase/commands/create.py | 4 ++-- src/briefcase/commands/run.py | 2 +- src/briefcase/config.py | 4 +--- src/briefcase/platforms/android/gradle.py | 6 +++--- src/briefcase/platforms/iOS/xcode.py | 2 +- src/briefcase/platforms/web/static.py | 6 ++++-- tests/commands/build/conftest.py | 6 +++--- tests/commands/create/conftest.py | 2 +- .../create/test_generate_app_template.py | 2 -- .../create/test_install_app_requirements.py | 20 +++++++++++++++---- tests/commands/run/conftest.py | 4 ++-- tests/commands/update/conftest.py | 2 +- tests/platforms/android/gradle/test_create.py | 4 ++-- tests/platforms/android/gradle/test_run.py | 7 +++---- tests/platforms/conftest.py | 19 ++++++++++++++++++ tests/platforms/iOS/xcode/test_run.py | 4 ++-- tests/platforms/linux/appimage/test_run.py | 4 ++-- tests/platforms/linux/flatpak/test_run.py | 4 ++-- tests/platforms/linux/system/test_run.py | 6 ++++-- tests/platforms/web/static/test_run.py | 6 +++--- tests/platforms/windows/app/test_run.py | 4 ++-- .../windows/visualstudio/test_run.py | 4 ++-- 24 files changed, 78 insertions(+), 50 deletions(-) diff --git a/src/briefcase/commands/base.py b/src/briefcase/commands/base.py index 34f7b8254..d848368ff 100644 --- a/src/briefcase/commands/base.py +++ b/src/briefcase/commands/base.py @@ -673,8 +673,6 @@ def finalize_debugger(self, app: AppConfig, debugger_name: str | None = None): """ if debugger_name and debugger_name != "": debugger = get_debugger(debugger_name) - app.debug_requires.extend(debugger.additional_requirements) - app.debug_mode = True app.debugger = debugger def verify_app(self, app: AppConfig): diff --git a/src/briefcase/commands/build.py b/src/briefcase/commands/build.py index 12af07e19..8a7f393cb 100644 --- a/src/briefcase/commands/build.py +++ b/src/briefcase/commands/build.py @@ -69,7 +69,7 @@ def _build_app( app.test_mode and not no_update ) # Test mode, but updates have not been disabled or ( - app.debug_mode and not no_update + app.debugger and not no_update ) # Debug mode, but updates have not been disabled ): state = self.update_command( @@ -88,7 +88,7 @@ def _build_app( state = self.build_app(app, **full_options(state, options)) qualifier = " (test mode)" if app.test_mode else "" - qualifier += " (debug mode)" if app.debug_mode else "" + qualifier += " (debug mode)" if app.debugger else "" self.console.info( f"Built {self.binary_path(app).relative_to(self.base_path)}{qualifier}", prefix=app.app_name, diff --git a/src/briefcase/commands/create.py b/src/briefcase/commands/create.py index 704424e6b..a3718d936 100644 --- a/src/briefcase/commands/create.py +++ b/src/briefcase/commands/create.py @@ -688,8 +688,8 @@ def install_app_requirements(self, app: AppConfig): if app.test_mode and app.test_requires: requires.extend(app.test_requires) - if app.debug_mode and app.debug_requires: - requires.extend(app.debug_requires) + if app.debugger: + requires.extend(app.debugger.additional_requirements) try: requirements_path = self.app_requirements_path(app) diff --git a/src/briefcase/commands/run.py b/src/briefcase/commands/run.py index 668d3d710..68685ff09 100644 --- a/src/briefcase/commands/run.py +++ b/src/briefcase/commands/run.py @@ -318,7 +318,7 @@ def _prepare_app_kwargs( env["BRIEFCASE_DEBUG"] = "1" # If we're in remote debug mode, save the remote debugger config - if app.debug_mode: + if app.debugger: env["BRIEFCASE_DEBUGGER"] = self.remote_debugger_config( app, debugger_host, debugger_port ) diff --git a/src/briefcase/config.py b/src/briefcase/config.py index 83cc7f2fd..70846a6a3 100644 --- a/src/briefcase/config.py +++ b/src/briefcase/config.py @@ -308,7 +308,6 @@ def __init__( template_branch=None, test_sources=None, test_requires=None, - debug_requires=None, supported=True, long_description=None, console_app=False, @@ -338,7 +337,6 @@ def __init__( self.template_branch = template_branch self.test_sources = test_sources self.test_requires = test_requires - self.debug_requires = [] if debug_requires is None else debug_requires self.supported = supported self.long_description = long_description self.license = license @@ -347,7 +345,7 @@ def __init__( [] if requirement_installer_args is None else requirement_installer_args ) self.test_mode: bool = False - self.debug_mode: bool = False + self.debugger: BaseDebugger | None = None if not is_valid_app_name(self.app_name): diff --git a/src/briefcase/platforms/android/gradle.py b/src/briefcase/platforms/android/gradle.py index c3b7adc4c..7bd9b9372 100644 --- a/src/briefcase/platforms/android/gradle.py +++ b/src/briefcase/platforms/android/gradle.py @@ -224,7 +224,7 @@ def output_format_template_context(self, app: AppConfig): # In debug mode extract all source packages so that the debugger can get the source code # at runtime (eg. via 'll' in pdb). - if app.debug_mode: + if app.debugger: extract_sources.extend(app.sources) return { @@ -456,7 +456,7 @@ def run_app( if self.console.is_debug: env["BRIEFCASE_DEBUG"] = "1" - if app.debug_mode: + if app.debugger: env["BRIEFCASE_DEBUGGER"] = self.remote_debugger_config( app, debugger_host, debugger_port ) @@ -529,7 +529,7 @@ def forward_port_for_debugger( :param debugger_port: The port to use for the debugger :param adb: The ADB wrapper for the device """ - if app.debug_mode and app.debugger and debugger_host == "localhost": + if app.debugger and debugger_host == "localhost": if app.debugger.connection_mode == DebuggerConnectionMode.SERVER: with self.console.wait_bar( f"Start forwarding port '{debugger_port}' from host to device for debugger connection..." diff --git a/src/briefcase/platforms/iOS/xcode.py b/src/briefcase/platforms/iOS/xcode.py index 66c4b6ef5..dffe24580 100644 --- a/src/briefcase/platforms/iOS/xcode.py +++ b/src/briefcase/platforms/iOS/xcode.py @@ -664,7 +664,7 @@ def run_app( # Add additional environment variables env = {} - if app.debug_mode: + if app.debugger: env["BRIEFCASE_DEBUGGER"] = self.remote_debugger_config( app, debugger_host, debugger_port ) diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index 9d32542d9..9c09603b5 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -330,8 +330,10 @@ def run_app( """ if app.test_mode: raise BriefcaseCommandError("Briefcase can't run web apps in test mode.") - if app.debug_mode: - raise BriefcaseCommandError("Briefcase can't run web apps in debug mode.") + if app.debugger: + raise BriefcaseCommandError( + "Briefcase can't run web apps with an debugger." + ) self.console.info("Starting web server...", prefix=app.app_name) diff --git a/tests/commands/build/conftest.py b/tests/commands/build/conftest.py index 650a0388d..e893a9af8 100644 --- a/tests/commands/build/conftest.py +++ b/tests/commands/build/conftest.py @@ -57,7 +57,7 @@ def build_app(self, app, **kwargs): "build", app.app_name, app.test_mode, - app.debug_mode, + app.debugger is not None, kwargs.copy(), ) ) @@ -79,7 +79,7 @@ def create_command(self, app, **kwargs): "create", app.app_name, app.test_mode, - app.debug_mode, + app.debugger is not None, kwargs.copy(), ) ) @@ -92,7 +92,7 @@ def update_command(self, app, **kwargs): "update", app.app_name, app.test_mode, - app.debug_mode, + app.debugger is not None, kwargs.copy(), ) ) diff --git a/tests/commands/create/conftest.py b/tests/commands/create/conftest.py index e475804b9..88db61300 100644 --- a/tests/commands/create/conftest.py +++ b/tests/commands/create/conftest.py @@ -170,7 +170,7 @@ def install_app_support_package(self, app): def install_app_requirements(self, app): self.actions.append( - ("requirements", app.app_name, app.test_mode, app.debug_mode) + ("requirements", app.app_name, app.test_mode, app.debugger is not None) ) def install_app_code(self, app): diff --git a/tests/commands/create/test_generate_app_template.py b/tests/commands/create/test_generate_app_template.py index a727cef0a..42152a655 100644 --- a/tests/commands/create/test_generate_app_template.py +++ b/tests/commands/create/test_generate_app_template.py @@ -31,7 +31,6 @@ def full_context(): "sources": ["src/my_app"], "test_sources": None, "test_requires": None, - "debug_requires": [], "debugger": None, "url": "https://example.com", "author": "First Last", @@ -47,7 +46,6 @@ def full_context(): "license": {"file": "LICENSE"}, "requirement_installer_args": [], "test_mode": False, - "debug_mode": False, # Properties of the generating environment "python_version": platform.python_version(), "host_arch": "gothic", diff --git a/tests/commands/create/test_install_app_requirements.py b/tests/commands/create/test_install_app_requirements.py index 58bb6404f..387e9c5b7 100644 --- a/tests/commands/create/test_install_app_requirements.py +++ b/tests/commands/create/test_install_app_requirements.py @@ -10,6 +10,7 @@ import briefcase from briefcase.commands.create import _is_local_path from briefcase.console import LogLevel +from briefcase.debuggers.base import BaseDebugger, DebuggerConnectionMode from briefcase.exceptions import BriefcaseCommandError, RequirementsInstallError from briefcase.integrations.subprocess import Subprocess @@ -1087,7 +1088,19 @@ def test_app_packages_only_test_requires_test_mode( assert myapp.test_requires == ["pytest", "pytest-tldr"] -def test_app_packages_debug_requires_debug_mode( +class DummyDebugger(BaseDebugger): + @property + def additional_requirements(self) -> list[str]: + """Return a list of additional requirements for the debugger.""" + return ["debugpy"] + + @property + def connection_mode(self) -> DebuggerConnectionMode: + """Return the connection mode of the debugger.""" + raise NotImplementedError + + +def test_app_packages_debugger_debugger( create_command, myapp, app_packages_path, @@ -1095,8 +1108,7 @@ def test_app_packages_debug_requires_debug_mode( ): """If an app has debug requirements and we're in debug mode, they are installed.""" myapp.requires = ["first", "second==1.2.3", "third>=3.2.1"] - myapp.debug_requires = ["debugpy"] - myapp.debug_mode = True + myapp.debugger = DummyDebugger() create_command.install_app_requirements(myapp) @@ -1125,4 +1137,4 @@ def test_app_packages_debug_requires_debug_mode( # Original app definitions haven't changed assert myapp.requires == ["first", "second==1.2.3", "third>=3.2.1"] - assert myapp.debug_requires == ["debugpy"] + assert isinstance(myapp.debugger, DummyDebugger) diff --git a/tests/commands/run/conftest.py b/tests/commands/run/conftest.py index 8bd27db6d..078a70198 100644 --- a/tests/commands/run/conftest.py +++ b/tests/commands/run/conftest.py @@ -57,7 +57,7 @@ def run_app(self, app, **kwargs): "run", app.app_name, app.test_mode, - app.debug_mode, + app.debugger is not None, kwargs.copy(), ) ) @@ -94,7 +94,7 @@ def build_command(self, app, **kwargs): "build", app.app_name, app.test_mode, - app.debug_mode, + app.debugger is not None, kwargs.copy(), ) ) diff --git a/tests/commands/update/conftest.py b/tests/commands/update/conftest.py index 21935b264..705472b85 100644 --- a/tests/commands/update/conftest.py +++ b/tests/commands/update/conftest.py @@ -54,7 +54,7 @@ def verify_app_tools(self, app): # with versions that we can use to track actions performed. def install_app_requirements(self, app): self.actions.append( - ("requirements", app.app_name, app.test_mode, app.debug_mode) + ("requirements", app.app_name, app.test_mode, app.debugger is not None) ) create_file(self.bundle_path(app) / "requirements", "app requirements") diff --git a/tests/platforms/android/gradle/test_create.py b/tests/platforms/android/gradle/test_create.py index e464b9551..e4472e03f 100644 --- a/tests/platforms/android/gradle/test_create.py +++ b/tests/platforms/android/gradle/test_create.py @@ -201,10 +201,10 @@ def test_extract_packages(create_command, first_app_config, test_sources, expect assert context["extract_packages"] == expected -def test_extract_packages_debug_mode(create_command, first_app_config): +def test_extract_packages_debugger(create_command, first_app_config, dummy_debugger): first_app_config.test_sources = ["one", "two", "three"] first_app_config.sources = ["four", "five", "six"] - first_app_config.debug_mode = True + first_app_config.debugger = dummy_debugger context = create_command.output_format_template_context(first_app_config) assert context["extract_packages"] == '"one", "two", "three", "four", "five", "six"' diff --git a/tests/platforms/android/gradle/test_run.py b/tests/platforms/android/gradle/test_run.py index 37c478f26..d69f4503c 100644 --- a/tests/platforms/android/gradle/test_run.py +++ b/tests/platforms/android/gradle/test_run.py @@ -806,7 +806,7 @@ def test_run_test_mode_created_emulator(run_command, first_app_config): run_command.tools.mock_adb.kill.assert_called_once_with() -def test_run_debug_mode(run_command, first_app_config, tmp_path): +def test_run_debugger(run_command, first_app_config, tmp_path, dummy_debugger): """An app can be run in debug mode.""" # Set up device selection to return a running physical device. run_command.tools.android_sdk.select_target_device = mock.MagicMock( @@ -827,7 +827,7 @@ def mock_stream_output(app, stop_func, **kwargs): # Set up app config to have a `-` in the `bundle`, to ensure it gets # normalized into a `_` via `package_name`. first_app_config.bundle = "com.ex-ample" - first_app_config.debug_mode = True + first_app_config.debugger = dummy_debugger # Invoke run_app with args. run_command.run_app( @@ -924,7 +924,7 @@ def connection_mode(self) -> DebuggerConnectionMode: ClientDebugger(), ], ) -def test_run_debug_mode_localhost(run_command, first_app_config, tmp_path, debugger): +def test_run_debugger_mode_localhost(run_command, first_app_config, tmp_path, debugger): """An app can be run in debug mode.""" run_command.console.verbosity = LogLevel.DEBUG @@ -949,7 +949,6 @@ def mock_stream_output(app, stop_func, **kwargs): first_app_config.bundle = "com.ex-ample" # Set up the debugger - first_app_config.debug_mode = True first_app_config.debugger = debugger # Invoke run_app with args. diff --git a/tests/platforms/conftest.py b/tests/platforms/conftest.py index 4b975e68b..ccf36830a 100644 --- a/tests/platforms/conftest.py +++ b/tests/platforms/conftest.py @@ -1,6 +1,7 @@ import pytest from briefcase.config import AppConfig +from briefcase.debuggers.base import BaseDebugger, DebuggerConnectionMode @pytest.fixture @@ -54,3 +55,21 @@ def underscore_app_config(first_app_config): requires=["foo==1.2.3", "bar>=4.5"], test_requires=["pytest"], ) + + +class DummyDebugger(BaseDebugger): + @property + def additional_requirements(self) -> list[str]: + """Return a list of additional requirements for the debugger.""" + raise NotImplementedError + + @property + def connection_mode(self) -> DebuggerConnectionMode: + """Return the connection mode of the debugger.""" + raise NotImplementedError + + +@pytest.fixture +def dummy_debugger(): + """A dummy debugger for testing purposes.""" + return DummyDebugger() diff --git a/tests/platforms/iOS/xcode/test_run.py b/tests/platforms/iOS/xcode/test_run.py index f5da40460..21e5d1bb0 100644 --- a/tests/platforms/iOS/xcode/test_run.py +++ b/tests/platforms/iOS/xcode/test_run.py @@ -1608,7 +1608,7 @@ def test_run_app_test_mode_with_passthrough(run_command, first_app_config, tmp_p @pytest.mark.usefixtures("sleep_zero") -def test_run_app_debug_mode(run_command, first_app_generated, tmp_path): +def test_run_app_debugger(run_command, first_app_generated, tmp_path, dummy_debugger): """An iOS App can be started in debug mode.""" # A valid target device will be selected. run_command.select_target_device = mock.MagicMock( @@ -1637,7 +1637,7 @@ def test_run_app_debug_mode(run_command, first_app_generated, tmp_path): log_stream_process, ] - first_app_generated.debug_mode = True + first_app_generated.debugger = dummy_debugger # Run the app run_command.run_app( diff --git a/tests/platforms/linux/appimage/test_run.py b/tests/platforms/linux/appimage/test_run.py index 5c8ef0130..d1ebb464a 100644 --- a/tests/platforms/linux/appimage/test_run.py +++ b/tests/platforms/linux/appimage/test_run.py @@ -304,13 +304,13 @@ def test_run_app_test_mode_with_args( ) -def test_run_app_debug_mode(run_command, first_app_config, tmp_path): +def test_run_app_debugger(run_command, first_app_config, tmp_path, dummy_debugger): """A linux App can be started in debug mode.""" # Set up the log streamer to return a known stream log_popen = mock.MagicMock() run_command.tools.subprocess.Popen.return_value = log_popen - first_app_config.debug_mode = True + first_app_config.debugger = dummy_debugger # Run the app run_command.run_app( diff --git a/tests/platforms/linux/flatpak/test_run.py b/tests/platforms/linux/flatpak/test_run.py index 412fa269b..6e3c7a508 100644 --- a/tests/platforms/linux/flatpak/test_run.py +++ b/tests/platforms/linux/flatpak/test_run.py @@ -236,13 +236,13 @@ def test_run_test_mode_with_args(run_command, first_app_config, is_console_app): ) -def test_run_debug_mode(run_command, first_app_config, tmp_path): +def test_run_debugger(run_command, first_app_config, tmp_path, dummy_debugger): """A flatpak can be executed in debug mode.""" # Set up the log streamer to return a known stream and a good return code log_popen = mock.MagicMock() run_command.tools.flatpak.run.return_value = log_popen - first_app_config.debug_mode = True + first_app_config.debugger = dummy_debugger # Run the app run_command.run_app( diff --git a/tests/platforms/linux/system/test_run.py b/tests/platforms/linux/system/test_run.py index 259dc1872..91cd46f04 100644 --- a/tests/platforms/linux/system/test_run.py +++ b/tests/platforms/linux/system/test_run.py @@ -351,7 +351,9 @@ def test_run_gui_app_failed(run_command, first_app, sub_kw, tmp_path): run_command._stream_app_logs.assert_not_called() -def test_run_gui_app_debug_mode(run_command, first_app, sub_kw, tmp_path, monkeypatch): +def test_run_gui_app_debugger( + run_command, first_app, sub_kw, tmp_path, monkeypatch, dummy_debugger +): """A bootstrap binary for a GUI app can be started in debug mode.""" # Set up tool cache @@ -366,7 +368,7 @@ def test_run_gui_app_debug_mode(run_command, first_app, sub_kw, tmp_path, monkey # Mock out the environment monkeypatch.setattr(run_command.tools.os, "environ", {"ENVVAR": "Value"}) - first_app.debug_mode = True + first_app.debugger = dummy_debugger # Run the app run_command.run_app( diff --git a/tests/platforms/web/static/test_run.py b/tests/platforms/web/static/test_run.py index 2a1b04100..212f32bb5 100644 --- a/tests/platforms/web/static/test_run.py +++ b/tests/platforms/web/static/test_run.py @@ -592,14 +592,14 @@ def test_test_mode(run_command, first_app_built): ) -def test_debug_mode(run_command, first_app_built): +def test_debugger(run_command, first_app_built, dummy_debugger): """Debug mode raises an error (at least for now).""" - first_app_built.debug_mode = True + first_app_built.debugger = dummy_debugger # Run the app with pytest.raises( BriefcaseCommandError, - match=r"Briefcase can't run web apps in debug mode.", + match=r"Briefcase can't run web apps with an debugger.", ): run_command.run_app( first_app_built, diff --git a/tests/platforms/windows/app/test_run.py b/tests/platforms/windows/app/test_run.py index cc68cfb48..202e30ef7 100644 --- a/tests/platforms/windows/app/test_run.py +++ b/tests/platforms/windows/app/test_run.py @@ -282,13 +282,13 @@ def test_run_app_test_mode_with_passthrough( ) -def test_run_gui_app_debug_mode(run_command, first_app_config, tmp_path): +def test_run_gui_app_debugger(run_command, first_app_config, tmp_path, dummy_debugger): """A Windows app can be started in debug mode.""" # Set up the log streamer to return a known stream log_popen = mock.MagicMock() run_command.tools.subprocess.Popen.return_value = log_popen - first_app_config.debug_mode = True + first_app_config.debugger = dummy_debugger # Run the app run_command.run_app( diff --git a/tests/platforms/windows/visualstudio/test_run.py b/tests/platforms/windows/visualstudio/test_run.py index f912d3e83..816bff0dc 100644 --- a/tests/platforms/windows/visualstudio/test_run.py +++ b/tests/platforms/windows/visualstudio/test_run.py @@ -173,9 +173,9 @@ def test_run_app_test_mode_with_args(run_command, first_app_config, tmp_path): ) -def test_run_app_debug_mode(run_command, first_app_config, tmp_path): +def test_run_app_debugger(run_command, first_app_config, tmp_path, dummy_debugger): """A windows Visual Studio project app can be started in debug mode.""" - first_app_config.debug_mode = True + first_app_config.debugger = dummy_debugger # Set up the log streamer to return a known stream with a good returncode log_popen = mock.MagicMock() From 1a7cd372c70f309b10438ca78281a6ccdebd034e Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Thu, 12 Jun 2025 19:58:06 +0200 Subject: [PATCH 038/131] embed debugger support package into briefcase --- src/briefcase/commands/create.py | 26 +++- src/briefcase/debuggers/base.py | 90 ++++++++++++- src/briefcase/debuggers/debugpy.py | 196 +++++++++++++++++++++++++++-- src/briefcase/debuggers/pdb.py | 133 ++++++++++++++++++-- 4 files changed, 424 insertions(+), 21 deletions(-) diff --git a/src/briefcase/commands/create.py b/src/briefcase/commands/create.py index a3718d936..715131163 100644 --- a/src/briefcase/commands/create.py +++ b/src/briefcase/commands/create.py @@ -689,7 +689,8 @@ def install_app_requirements(self, app: AppConfig): requires.extend(app.test_requires) if app.debugger: - requires.extend(app.debugger.additional_requirements) + debugger_support_pkg = self.create_debugger_support_pkg(app) + requires.append(str(debugger_support_pkg.absolute())) try: requirements_path = self.app_requirements_path(app) @@ -754,6 +755,29 @@ def install_app_code(self, app: AppConfig): / f"{app.module_name}-{app.version}.dist-info", ) + def create_debugger_support_pkg(self, app: AppConfig) -> Path: + """ + Create the debugger support package. + + This package is used to inject debugger support into the app when it is + run in debug mode. It is necessary to create this as own package, because + the code is automatically started via an .pth file and the .pth file + has to be located in the app's site-packages directory, that it is executed + correctly. + """ + if app.debugger is None: + return + + debugger_support_path = self.bundle_path(app) / ".debugger_support_package" + if debugger_support_path.exists(): + self.tools.shutil.rmtree(debugger_support_path) + self.tools.os.mkdir(debugger_support_path) + + # Create files for the debugger support package + app.debugger.create_debugger_support_pkg(debugger_support_path) + + return debugger_support_path + def install_image(self, role, variant, size, source, target): """Install an icon/image of the requested size at a target location, using the source images defined by the app config. diff --git a/src/briefcase/debuggers/base.py b/src/briefcase/debuggers/base.py index 8a4ff6f9d..291903d9b 100644 --- a/src/briefcase/debuggers/base.py +++ b/src/briefcase/debuggers/base.py @@ -2,6 +2,7 @@ import enum from abc import ABC, abstractmethod +from pathlib import Path from typing import TypedDict @@ -31,14 +32,91 @@ class DebuggerConnectionMode(str, enum.Enum): class BaseDebugger(ABC): """Definition for a plugin that defines a new Briefcase debugger.""" - @property - @abstractmethod - def additional_requirements(self) -> list[str]: - """Return a list of additional requirements for the debugger.""" - raise NotImplementedError - @property @abstractmethod def connection_mode(self) -> DebuggerConnectionMode: """Return the connection mode of the debugger.""" raise NotImplementedError + + @abstractmethod + def create_debugger_support_pkg(self, dir: Path) -> None: + """Create the support package for the debugger. + This package will be installed inside the packaged app bundle. + + :param dir: Directory where the support package should be created. + """ + + def _create_debugger_support_pkg_base( + self, dir: Path, dependencies: list[str] + ) -> None: + """Create the base for the support package for the debugger. + + :param dir: Directory where the support package should be created. + :param dependencies: List of dependencies to include in the package. + """ + pyproject = dir / "pyproject.toml" + setup = dir / "setup.py" + + pyproject.write_text( + f"""\ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "briefcase-debugger-support" +version = "0.1.0" +description = "Add-on for briefcase to add remote debugging." +license = {{ file = "MIT" }} +dependencies = {dependencies} +""", + encoding="utf-8", + ) + + setup.write_text( + '''\ +import os +import setuptools +from setuptools.command.install import install + +# Copied from setuptools: +# (https://github.com/pypa/setuptools/blob/7c859e017368360ba66c8cc591279d8964c031bc/setup.py#L40C6-L82) +class install_with_pth(install): + """ + Custom install command to install a .pth file for distutils patching. + + This hack is necessary because there's no standard way to install behavior + on startup (and it's debatable if there should be one). This hack (ab)uses + the `extra_path` behavior in Setuptools to install a `.pth` file with + implicit behavior on startup to give higher precedence to the local version + of `distutils` over the version from the standard library. + + Please do not replicate this behavior. + """ + + _pth_name = 'briefcase_debugger_support' + _pth_contents = "import briefcase_debugger_support" + + def initialize_options(self): + install.initialize_options(self) + self.extra_path = self._pth_name, self._pth_contents + + def finalize_options(self): + install.finalize_options(self) + self._restore_install_lib() + + def _restore_install_lib(self): + """ + Undo secondary effect of `extra_path` adding to `install_lib` + """ + suffix = os.path.relpath(self.install_lib, self.install_libbase) + + if suffix.strip() == self._pth_contents.strip(): + self.install_lib = self.install_libbase + +setuptools.setup( + cmdclass={'install': install_with_pth}, +) +''', + encoding="utf-8", + ) diff --git a/src/briefcase/debuggers/debugpy.py b/src/briefcase/debuggers/debugpy.py index 1a19d1935..fabe9ae4e 100644 --- a/src/briefcase/debuggers/debugpy.py +++ b/src/briefcase/debuggers/debugpy.py @@ -1,17 +1,199 @@ +from pathlib import Path + from briefcase.debuggers.base import BaseDebugger, DebuggerConnectionMode class DebugpyDebugger(BaseDebugger): """Definition for a plugin that defines a new Briefcase debugger.""" - @property - def additional_requirements(self) -> list[str]: - """Return a list of additional requirements for the debugger.""" - return [ - "git+https://github.com/timrid/briefcase-debugadapter#subdirectory=briefcase-debugpy-debugadapter" - ] - @property def connection_mode(self) -> DebuggerConnectionMode: """Return the connection mode of the debugger.""" return DebuggerConnectionMode.SERVER + + def create_debugger_support_pkg(self, dir: Path) -> None: + """Create the support package for the debugger. + This package will be installed inside the packaged app bundle. + + :param dir: Directory where the support package should be created. + """ + self._create_debugger_support_pkg_base( + dir, + dependencies=["debugpy>=1.8.14,<2.0.0"], + ) + + debugger_support = dir / "briefcase_debugger_support.py" + debugger_support.write_text( + '''\ +import json +import os +import re +import sys +import traceback +from pathlib import Path +from typing import List, Optional, Tuple, TypedDict + +import debugpy + +REMOTE_DEBUGGER_STARTED = False + +class AppPathMappings(TypedDict): + device_sys_path_regex: str + device_subfolders: list[str] + host_folders: list[str] + + +class AppPackagesPathMappings(TypedDict): + sys_path_regex: str + host_folder: str + + +class DebuggerConfig(TypedDict): + host: str + port: int + app_path_mappings: AppPathMappings | None + app_packages_path_mappings: AppPackagesPathMappings | None + + +def _load_path_mappings(config: DebuggerConfig, verbose: bool) -> List[Tuple[str, str]]: + app_path_mappings = config.get("app_path_mappings", None) + app_packages_path_mappings = config.get("app_packages_path_mappings", None) + + mappings_list = [] + if app_path_mappings: + device_app_folder = next( + ( + p + for p in sys.path + if re.search(app_path_mappings["device_sys_path_regex"], p) + ), + None, + ) + if device_app_folder: + for app_subfolder_device, app_subfolder_host in zip( + app_path_mappings["device_subfolders"], + app_path_mappings["host_folders"], + ): + mappings_list.append( + ( + app_subfolder_host, + str(Path(device_app_folder) / app_subfolder_device), + ) + ) + if app_packages_path_mappings: + device_app_packages_folder = next( + ( + p + for p in sys.path + if re.search(app_packages_path_mappings["sys_path_regex"], p) + ), + None, + ) + if device_app_packages_folder: + mappings_list.append( + ( + app_packages_path_mappings["host_folder"], + str(Path(device_app_packages_folder)), + ) + ) + + if verbose: + print("Extracted path mappings:") + for idx, p in enumerate(mappings_list): + print(f"[{idx}] host = {p[0]}") + print(f"[{idx}] device = {p[1]}") + + return mappings_list + + +def _start_debugpy(config_str: str, verbose: bool): + # Parsing config json + debugger_config: dict = json.loads(config_str) + + host = debugger_config["host"] + port = debugger_config["port"] + path_mappings = _load_path_mappings(debugger_config, verbose) + + # When an app is bundled with briefcase "os.__file__" is not set at runtime + # on some platforms (eg. windows). But debugpy accesses it internally, so it + # has to be set or an Exception is raised from debugpy. + if not hasattr(os, "__file__"): + if verbose: + print("'os.__file__' not available. Patching it...") + os.__file__ = "" + + # Starting remote debugger... + print(f"Starting debugpy in server mode at {host}:{port}...") + debugpy.listen((host, port), in_process_debug_adapter=True) + + if len(path_mappings) > 0: + if verbose: + print("Adding path mappings...") + + import pydevd_file_utils + + pydevd_file_utils.setup_client_server_paths(path_mappings) + + print("The debugpy server started. Waiting for debugger to attach...") + print( + f""" +To connect to debugpy using VSCode add the following configuration to launch.json: +{{ +"version": "0.2.0", +"configurations": [ + {{ + "name": "Briefcase: Attach (Connect)", + "type": "debugpy", + "request": "attach", + "connect": {{ + "host": "{host}", + "port": {port} + }} + }} +] +}} +""" + ) + debugpy.wait_for_client() + + print("Debugger attached.") + + +def start_remote_debugger(): + global REMOTE_DEBUGGER_STARTED + REMOTE_DEBUGGER_STARTED = True + + # check verbose output + verbose = True if os.environ.get("BRIEFCASE_DEBUG", "0") == "1" else False + + # reading config + config_str = os.environ.get("BRIEFCASE_DEBUGGER", None) + + # skip debugger if no config is set + if config_str is None: + if verbose: + print("No 'BRIEFCASE_DEBUGGER' environment variable found. Debugger not starting.") + return # If BRIEFCASE_DEBUGGER is not set, this packages does nothing... + + if verbose: + print(f"'BRIEFCASE_DEBUGGER'={config_str}") + + # start debugger + print("Starting remote debugger...") + _start_debugpy(config_str, verbose) + + +def autostart_remote_debugger(): + try: + start_remote_debugger() + except Exception: + # Show exception and stop the whole application when an error occurs + print(traceback.format_exc()) + sys.exit(-1) + + +# only start remote debugger on the first import +if REMOTE_DEBUGGER_STARTED == False: + autostart_remote_debugger() +''' + ) diff --git a/src/briefcase/debuggers/pdb.py b/src/briefcase/debuggers/pdb.py index 6bd4bebd0..3279ab6e8 100644 --- a/src/briefcase/debuggers/pdb.py +++ b/src/briefcase/debuggers/pdb.py @@ -1,17 +1,136 @@ +from pathlib import Path + from briefcase.debuggers.base import BaseDebugger, DebuggerConnectionMode class PdbDebugger(BaseDebugger): """Definition for a plugin that defines a new Briefcase debugger.""" - @property - def additional_requirements(self) -> list[str]: - """Return a list of additional requirements for the debugger.""" - return [ - "git+https://github.com/timrid/briefcase-debugadapter#subdirectory=briefcase-pdb-debugadapter" - ] - @property def connection_mode(self) -> DebuggerConnectionMode: """Return the connection mode of the debugger.""" return DebuggerConnectionMode.SERVER + + def create_debugger_support_pkg(self, dir: Path) -> None: + """Create the support package for the debugger. + This package will be installed inside the packaged app bundle. + + :param dir: Directory where the support package should be created. + """ + self._create_debugger_support_pkg_base( + dir, + dependencies=[], + ) + + debugger_support = dir / "briefcase_debugger_support.py" + debugger_support.write_text(''' +import json +import os +import platform +import re +import socket +import sys +import traceback + +REMOTE_DEBUGGER_STARTED = False + +NEWLINE_REGEX = re.compile("\\r?\\n") + +class SocketFileWrapper(object): + def __init__(self, connection: socket.socket): + self.connection = connection + self.stream = connection.makefile('rw') + + self.read = self.stream.read + self.readline = self.stream.readline + self.readlines = self.stream.readlines + self.close = self.stream.close + self.isatty = self.stream.isatty + self.flush = self.stream.flush + self.fileno = lambda: -1 + self.__iter__ = self.stream.__iter__ + + @property + def encoding(self): + return self.stream.encoding + + def write(self, data): + data = NEWLINE_REGEX.sub("\\r\\n", data) + self.connection.sendall(data.encode(self.stream.encoding)) + + def writelines(self, lines): + for line in lines: + self.write(line) + +def _start_pdb(config_str: str, verbose: bool): + """Open a socket server and stream all stdio via the connection bidirectional.""" + debugger_config: dict = json.loads(config_str) + + # Parsing host/port + host = debugger_config["host"] + port = debugger_config["port"] + + print( + f""" +Stdio redirector server opened at {host}:{port}, waiting for connection... +To connect to stdio redirector use eg.: + - telnet {host} {port} + - nc -C {host} {port} + - socat readline tcp:{host}:{port} +""" + ) + + listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True) + listen_socket.bind((host, port)) + listen_socket.listen(1) + connection, address = listen_socket.accept() + print(f"Stdio redirector accepted connection from {{repr(address)}}.") + + file_wrapper = SocketFileWrapper(connection) + + sys.stderr = file_wrapper + sys.stdout = file_wrapper + sys.stdin = file_wrapper + sys.__stderr__ = file_wrapper + sys.__stdout__ = file_wrapper + sys.__stdin__ = file_wrapper + + +def start_remote_debugger(): + global REMOTE_DEBUGGER_STARTED + REMOTE_DEBUGGER_STARTED = True + + # check verbose output + verbose = True if os.environ.get("BRIEFCASE_DEBUG", "0") == "1" else False + + # reading config + config_str = os.environ.get("BRIEFCASE_DEBUGGER", None) + + # skip debugger if no config is set + if config_str is None: + if verbose: + print("No 'BRIEFCASE_DEBUGGER' environment variable found. Debugger not starting.") + return # If BRIEFCASE_DEBUGGER is not set, this packages does nothing... + + if verbose: + print(f"'BRIEFCASE_DEBUGGER'={config_str}") + + # start debugger + print("Starting remote debugger...") + _start_pdb(config_str, verbose) + + +def autostart_remote_debugger(): + try: + start_remote_debugger() + except Exception: + # Show exception and stop the whole application when an error occurs + print(traceback.format_exc()) + sys.exit(-1) + + +# only start remote debugger on the first import +if REMOTE_DEBUGGER_STARTED == False: + autostart_remote_debugger() +''') From ec47b9f69d42a88595ae7be4baee7b8e545edbaa Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sat, 14 Jun 2025 15:02:30 +0200 Subject: [PATCH 039/131] "pdb" is now the default if no debugger is specified --- src/briefcase/commands/base.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/briefcase/commands/base.py b/src/briefcase/commands/base.py index d848368ff..85d5a373c 100644 --- a/src/briefcase/commands/base.py +++ b/src/briefcase/commands/base.py @@ -940,18 +940,17 @@ def _add_debug_options(self, parser, context_label): """ debuggers = get_debuggers() debugger_names = list(reversed(debuggers.keys())) - choices = ["", *debugger_names] - choices_help = [f"'{choice}'" for choice in choices] + choices_help = [f"'{choice}'" for choice in debugger_names] parser.add_argument( "--debug", dest="debugger", nargs="?", default=None, - const="", - choices=choices, + const="pdb", + choices=debugger_names, metavar="DEBUGGER", - help=f"{context_label} the app with the specified debugger ({', '.join(choices_help)})", + help=f"{context_label} the app with the specified debugger. One of {', '.join(choices_help)} (default: pdb)", ) def add_options(self, parser): From f9198eb2890ac3ae1284c983bce077008a5f1b9a Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sat, 14 Jun 2025 21:21:19 +0200 Subject: [PATCH 040/131] corrected formatting --- src/briefcase/debuggers/debugpy.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/briefcase/debuggers/debugpy.py b/src/briefcase/debuggers/debugpy.py index fabe9ae4e..53858c264 100644 --- a/src/briefcase/debuggers/debugpy.py +++ b/src/briefcase/debuggers/debugpy.py @@ -139,18 +139,18 @@ def _start_debugpy(config_str: str, verbose: bool): f""" To connect to debugpy using VSCode add the following configuration to launch.json: {{ -"version": "0.2.0", -"configurations": [ - {{ - "name": "Briefcase: Attach (Connect)", - "type": "debugpy", - "request": "attach", - "connect": {{ - "host": "{host}", - "port": {port} + "version": "0.2.0", + "configurations": [ + {{ + "name": "Briefcase: Attach (Connect)", + "type": "debugpy", + "request": "attach", + "connect": {{ + "host": "{host}", + "port": {port} + }} }} - }} -] + ] }} """ ) From 123e93225989a2ccd088ad49e6f828d4aaeec3b9 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sat, 14 Jun 2025 21:22:31 +0200 Subject: [PATCH 041/131] use "remote-pdb" --- src/briefcase/debuggers/pdb.py | 66 ++++++++-------------------------- 1 file changed, 15 insertions(+), 51 deletions(-) diff --git a/src/briefcase/debuggers/pdb.py b/src/briefcase/debuggers/pdb.py index 3279ab6e8..cc07870b3 100644 --- a/src/briefcase/debuggers/pdb.py +++ b/src/briefcase/debuggers/pdb.py @@ -19,51 +19,22 @@ def create_debugger_support_pkg(self, dir: Path) -> None: """ self._create_debugger_support_pkg_base( dir, - dependencies=[], + dependencies=["remote-pdb>=2.1.0,<3.0.0"], ) debugger_support = dir / "briefcase_debugger_support.py" debugger_support.write_text(''' import json import os -import platform -import re -import socket import sys import traceback +from remote_pdb import RemotePdb REMOTE_DEBUGGER_STARTED = False -NEWLINE_REGEX = re.compile("\\r?\\n") - -class SocketFileWrapper(object): - def __init__(self, connection: socket.socket): - self.connection = connection - self.stream = connection.makefile('rw') - - self.read = self.stream.read - self.readline = self.stream.readline - self.readlines = self.stream.readlines - self.close = self.stream.close - self.isatty = self.stream.isatty - self.flush = self.stream.flush - self.fileno = lambda: -1 - self.__iter__ = self.stream.__iter__ - - @property - def encoding(self): - return self.stream.encoding - - def write(self, data): - data = NEWLINE_REGEX.sub("\\r\\n", data) - self.connection.sendall(data.encode(self.stream.encoding)) - - def writelines(self, lines): - for line in lines: - self.write(line) def _start_pdb(config_str: str, verbose: bool): - """Open a socket server and stream all stdio via the connection bidirectional.""" + """Start remote PDB server.""" debugger_config: dict = json.loads(config_str) # Parsing host/port @@ -72,29 +43,20 @@ def _start_pdb(config_str: str, verbose: bool): print( f""" -Stdio redirector server opened at {host}:{port}, waiting for connection... -To connect to stdio redirector use eg.: - - telnet {host} {port} - - nc -C {host} {port} - - socat readline tcp:{host}:{port} +Remote PDB server opened at {host}:{port}, waiting for connection... +To connect to remote PDB use eg.: + - telnet {host} {port} (Windows) + - rlwrap socat - tcp:{host}:{port} (Linux, macOS) """ ) - listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True) - listen_socket.bind((host, port)) - listen_socket.listen(1) - connection, address = listen_socket.accept() - print(f"Stdio redirector accepted connection from {{repr(address)}}.") + # Create a RemotePdb instance + remote_pdb = RemotePdb(host, port, quiet=True) - file_wrapper = SocketFileWrapper(connection) + # Connect the remote PDB with the "breakpoint()" function + sys.breakpointhook = remote_pdb.set_trace - sys.stderr = file_wrapper - sys.stdout = file_wrapper - sys.stdin = file_wrapper - sys.__stderr__ = file_wrapper - sys.__stdout__ = file_wrapper - sys.__stdin__ = file_wrapper + print("Debugger client attached.") def start_remote_debugger(): @@ -110,7 +72,9 @@ def start_remote_debugger(): # skip debugger if no config is set if config_str is None: if verbose: - print("No 'BRIEFCASE_DEBUGGER' environment variable found. Debugger not starting.") + print( + "No 'BRIEFCASE_DEBUGGER' environment variable found. Debugger not starting." + ) return # If BRIEFCASE_DEBUGGER is not set, this packages does nothing... if verbose: From 1786d7aa26619ccb9123ff340b99ddec6a9cf22a Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sat, 14 Jun 2025 21:43:28 +0200 Subject: [PATCH 042/131] removed duplicate code --- src/briefcase/debuggers/base.py | 47 +++++++++++++++++++++++++++ src/briefcase/debuggers/debugpy.py | 46 ++------------------------ src/briefcase/debuggers/pdb.py | 52 +++--------------------------- 3 files changed, 54 insertions(+), 91 deletions(-) diff --git a/src/briefcase/debuggers/base.py b/src/briefcase/debuggers/base.py index 291903d9b..30ec64c59 100644 --- a/src/briefcase/debuggers/base.py +++ b/src/briefcase/debuggers/base.py @@ -56,6 +56,8 @@ def _create_debugger_support_pkg_base( """ pyproject = dir / "pyproject.toml" setup = dir / "setup.py" + debugger_support = dir / "briefcase_debugger_support" / "__init__.py" + debugger_support.parent.mkdir(parents=True) pyproject.write_text( f"""\ @@ -120,3 +122,48 @@ def _restore_install_lib(self): ''', encoding="utf-8", ) + + debugger_support.write_text(""" +import json +import os +import sys +import traceback +from briefcase_debugger_support._remote_debugger import _start_remote_debugger + +REMOTE_DEBUGGER_STARTED = False + +def start_remote_debugger(): + global REMOTE_DEBUGGER_STARTED + REMOTE_DEBUGGER_STARTED = True + + # check verbose output + verbose = True if os.environ.get("BRIEFCASE_DEBUG", "0") == "1" else False + + # reading config + config_str = os.environ.get("BRIEFCASE_DEBUGGER", None) + + # skip debugger if no config is set + if config_str is None: + if verbose: + print( + "No 'BRIEFCASE_DEBUGGER' environment variable found. Debugger not starting." + ) + return # If BRIEFCASE_DEBUGGER is not set, this packages does nothing... + + if verbose: + print(f"'BRIEFCASE_DEBUGGER'={config_str}") + + # start debugger + print("Starting remote debugger...") + _start_remote_debugger(config_str, verbose) + + +# only start remote debugger on the first import +if REMOTE_DEBUGGER_STARTED == False: + try: + start_remote_debugger() + except Exception: + # Show exception and stop the whole application when an error occurs + print(traceback.format_exc()) + sys.exit(-1) +""") diff --git a/src/briefcase/debuggers/debugpy.py b/src/briefcase/debuggers/debugpy.py index 53858c264..069e07d23 100644 --- a/src/briefcase/debuggers/debugpy.py +++ b/src/briefcase/debuggers/debugpy.py @@ -22,8 +22,8 @@ def create_debugger_support_pkg(self, dir: Path) -> None: dependencies=["debugpy>=1.8.14,<2.0.0"], ) - debugger_support = dir / "briefcase_debugger_support.py" - debugger_support.write_text( + remote_debugger = dir / "briefcase_debugger_support" / "_remote_debugger.py" + remote_debugger.write_text( '''\ import json import os @@ -35,8 +35,6 @@ def create_debugger_support_pkg(self, dir: Path) -> None: import debugpy -REMOTE_DEBUGGER_STARTED = False - class AppPathMappings(TypedDict): device_sys_path_regex: str device_subfolders: list[str] @@ -106,7 +104,7 @@ def _load_path_mappings(config: DebuggerConfig, verbose: bool) -> List[Tuple[str return mappings_list -def _start_debugpy(config_str: str, verbose: bool): +def _start_remote_debugger(config_str: str, verbose: bool): # Parsing config json debugger_config: dict = json.loads(config_str) @@ -157,43 +155,5 @@ def _start_debugpy(config_str: str, verbose: bool): debugpy.wait_for_client() print("Debugger attached.") - - -def start_remote_debugger(): - global REMOTE_DEBUGGER_STARTED - REMOTE_DEBUGGER_STARTED = True - - # check verbose output - verbose = True if os.environ.get("BRIEFCASE_DEBUG", "0") == "1" else False - - # reading config - config_str = os.environ.get("BRIEFCASE_DEBUGGER", None) - - # skip debugger if no config is set - if config_str is None: - if verbose: - print("No 'BRIEFCASE_DEBUGGER' environment variable found. Debugger not starting.") - return # If BRIEFCASE_DEBUGGER is not set, this packages does nothing... - - if verbose: - print(f"'BRIEFCASE_DEBUGGER'={config_str}") - - # start debugger - print("Starting remote debugger...") - _start_debugpy(config_str, verbose) - - -def autostart_remote_debugger(): - try: - start_remote_debugger() - except Exception: - # Show exception and stop the whole application when an error occurs - print(traceback.format_exc()) - sys.exit(-1) - - -# only start remote debugger on the first import -if REMOTE_DEBUGGER_STARTED == False: - autostart_remote_debugger() ''' ) diff --git a/src/briefcase/debuggers/pdb.py b/src/briefcase/debuggers/pdb.py index cc07870b3..980b8556b 100644 --- a/src/briefcase/debuggers/pdb.py +++ b/src/briefcase/debuggers/pdb.py @@ -22,18 +22,14 @@ def create_debugger_support_pkg(self, dir: Path) -> None: dependencies=["remote-pdb>=2.1.0,<3.0.0"], ) - debugger_support = dir / "briefcase_debugger_support.py" - debugger_support.write_text(''' + remote_debugger = dir / "briefcase_debugger_support" / "_remote_debugger.py" + remote_debugger.write_text(''' import json -import os import sys -import traceback -from remote_pdb import RemotePdb - -REMOTE_DEBUGGER_STARTED = False +from remote_pdb import RemotePdb -def _start_pdb(config_str: str, verbose: bool): +def _start_remote_debugger(config_str: str, verbose: bool): """Start remote PDB server.""" debugger_config: dict = json.loads(config_str) @@ -57,44 +53,4 @@ def _start_pdb(config_str: str, verbose: bool): sys.breakpointhook = remote_pdb.set_trace print("Debugger client attached.") - - -def start_remote_debugger(): - global REMOTE_DEBUGGER_STARTED - REMOTE_DEBUGGER_STARTED = True - - # check verbose output - verbose = True if os.environ.get("BRIEFCASE_DEBUG", "0") == "1" else False - - # reading config - config_str = os.environ.get("BRIEFCASE_DEBUGGER", None) - - # skip debugger if no config is set - if config_str is None: - if verbose: - print( - "No 'BRIEFCASE_DEBUGGER' environment variable found. Debugger not starting." - ) - return # If BRIEFCASE_DEBUGGER is not set, this packages does nothing... - - if verbose: - print(f"'BRIEFCASE_DEBUGGER'={config_str}") - - # start debugger - print("Starting remote debugger...") - _start_pdb(config_str, verbose) - - -def autostart_remote_debugger(): - try: - start_remote_debugger() - except Exception: - # Show exception and stop the whole application when an error occurs - print(traceback.format_exc()) - sys.exit(-1) - - -# only start remote debugger on the first import -if REMOTE_DEBUGGER_STARTED == False: - autostart_remote_debugger() ''') From 4372e215f4a63f249aea008c36431d9c8d2e6c9f Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sat, 14 Jun 2025 21:47:02 +0200 Subject: [PATCH 043/131] telnet is also available on linux --- src/briefcase/debuggers/pdb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/briefcase/debuggers/pdb.py b/src/briefcase/debuggers/pdb.py index 980b8556b..86c8c6c44 100644 --- a/src/briefcase/debuggers/pdb.py +++ b/src/briefcase/debuggers/pdb.py @@ -41,7 +41,7 @@ def _start_remote_debugger(config_str: str, verbose: bool): f""" Remote PDB server opened at {host}:{port}, waiting for connection... To connect to remote PDB use eg.: - - telnet {host} {port} (Windows) + - telnet {host} {port} (Windows, Linux) - rlwrap socat - tcp:{host}:{port} (Linux, macOS) """ ) From 582b57dd5235712f5177f9ace9fdb201d06ab91d Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sat, 14 Jun 2025 22:08:21 +0200 Subject: [PATCH 044/131] fixed unit tests --- src/briefcase/debuggers/base.py | 1 - .../create/test_install_app_requirements.py | 21 ++++++++++----- tests/debuggers/test_base.py | 26 +++++++++++-------- tests/platforms/conftest.py | 9 +++---- 4 files changed, 33 insertions(+), 24 deletions(-) diff --git a/src/briefcase/debuggers/base.py b/src/briefcase/debuggers/base.py index 30ec64c59..d8f13cdb2 100644 --- a/src/briefcase/debuggers/base.py +++ b/src/briefcase/debuggers/base.py @@ -36,7 +36,6 @@ class BaseDebugger(ABC): @abstractmethod def connection_mode(self) -> DebuggerConnectionMode: """Return the connection mode of the debugger.""" - raise NotImplementedError @abstractmethod def create_debugger_support_pkg(self, dir: Path) -> None: diff --git a/tests/commands/create/test_install_app_requirements.py b/tests/commands/create/test_install_app_requirements.py index 387e9c5b7..5b779283b 100644 --- a/tests/commands/create/test_install_app_requirements.py +++ b/tests/commands/create/test_install_app_requirements.py @@ -2,6 +2,7 @@ import os import subprocess import sys +from pathlib import Path from unittest import mock import pytest @@ -1089,16 +1090,15 @@ def test_app_packages_only_test_requires_test_mode( class DummyDebugger(BaseDebugger): - @property - def additional_requirements(self) -> list[str]: - """Return a list of additional requirements for the debugger.""" - return ["debugpy"] + debugger_support_pkg_dir = None @property def connection_mode(self) -> DebuggerConnectionMode: - """Return the connection mode of the debugger.""" raise NotImplementedError + def create_debugger_support_pkg(self, dir: Path) -> None: + self.debugger_support_pkg_dir = dir + def test_app_packages_debugger_debugger( create_command, @@ -1112,6 +1112,8 @@ def test_app_packages_debugger_debugger( create_command.install_app_requirements(myapp) + bundle_path = create_command.bundle_path(myapp) + # A request was made to install requirements create_command.tools[myapp].app_context.run.assert_called_with( [ @@ -1129,7 +1131,7 @@ def test_app_packages_debugger_debugger( "first", "second==1.2.3", "third>=3.2.1", - "debugpy", + f"{bundle_path / '.debugger_support_package'}", ], check=True, encoding="UTF-8", @@ -1137,4 +1139,9 @@ def test_app_packages_debugger_debugger( # Original app definitions haven't changed assert myapp.requires == ["first", "second==1.2.3", "third>=3.2.1"] - assert isinstance(myapp.debugger, DummyDebugger) + + # The debugger support package directory was created + assert ( + myapp.debugger.debugger_support_pkg_dir + == bundle_path / ".debugger_support_package" + ) diff --git a/tests/debuggers/test_base.py b/tests/debuggers/test_base.py index 0e6af2e45..f35823f42 100644 --- a/tests/debuggers/test_base.py +++ b/tests/debuggers/test_base.py @@ -1,3 +1,6 @@ +from pathlib import Path +from tempfile import TemporaryDirectory + import pytest from briefcase.debuggers import ( @@ -29,30 +32,31 @@ def test_get_debugger(): @pytest.mark.parametrize( - "debugger_name, expected_class, additional_requirements, connection_mode", + "debugger_name, expected_class, connection_mode", [ ( "pdb", PdbDebugger, - [ - "git+https://github.com/timrid/briefcase-debugadapter#subdirectory=briefcase-pdb-debugadapter" - ], DebuggerConnectionMode.SERVER, ), ( "debugpy", DebugpyDebugger, - [ - "git+https://github.com/timrid/briefcase-debugadapter#subdirectory=briefcase-debugpy-debugadapter" - ], DebuggerConnectionMode.SERVER, ), ], ) -def test_debugger( - debugger_name, expected_class, additional_requirements, connection_mode -): +def test_debugger(debugger_name, expected_class, connection_mode): debugger = get_debugger(debugger_name) assert isinstance(debugger, expected_class) - assert debugger.additional_requirements == additional_requirements assert debugger.connection_mode == connection_mode + + with TemporaryDirectory() as tmp_path: + tmp_path = Path(tmp_path) + debugger.create_debugger_support_pkg(tmp_path) + assert (tmp_path / "pyproject.toml").exists() + assert (tmp_path / "setup.py").exists() + assert (tmp_path / "briefcase_debugger_support" / "__init__.py").exists() + assert ( + tmp_path / "briefcase_debugger_support" / "_remote_debugger.py" + ).exists() diff --git a/tests/platforms/conftest.py b/tests/platforms/conftest.py index ccf36830a..36dd139bd 100644 --- a/tests/platforms/conftest.py +++ b/tests/platforms/conftest.py @@ -1,3 +1,5 @@ +from pathlib import Path + import pytest from briefcase.config import AppConfig @@ -59,13 +61,10 @@ def underscore_app_config(first_app_config): class DummyDebugger(BaseDebugger): @property - def additional_requirements(self) -> list[str]: - """Return a list of additional requirements for the debugger.""" + def connection_mode(self) -> DebuggerConnectionMode: raise NotImplementedError - @property - def connection_mode(self) -> DebuggerConnectionMode: - """Return the connection mode of the debugger.""" + def create_debugger_support_pkg(self, dir: Path) -> None: raise NotImplementedError From ca55ef772cd57906f737abddc5ef08a6259aefa1 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sat, 14 Jun 2025 22:24:10 +0200 Subject: [PATCH 045/131] fixed more unit tests --- tests/platforms/android/gradle/test_run.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/platforms/android/gradle/test_run.py b/tests/platforms/android/gradle/test_run.py index d69f4503c..6b262743e 100644 --- a/tests/platforms/android/gradle/test_run.py +++ b/tests/platforms/android/gradle/test_run.py @@ -900,22 +900,22 @@ def mock_stream_output(app, stop_func, **kwargs): class ServerDebugger(BaseDebugger): - def additional_requirements(self) -> list[str]: - raise NotImplementedError - @property def connection_mode(self) -> DebuggerConnectionMode: return DebuggerConnectionMode.SERVER - -class ClientDebugger(BaseDebugger): - def additional_requirements(self) -> list[str]: + def create_debugger_support_pkg(self, dir: Path) -> None: raise NotImplementedError + +class ClientDebugger(BaseDebugger): @property def connection_mode(self) -> DebuggerConnectionMode: return DebuggerConnectionMode.CLIENT + def create_debugger_support_pkg(self, dir: Path) -> None: + raise NotImplementedError + @pytest.mark.parametrize( "debugger", From 5bd4257cd7873bb708f2ebaad9b850bbb67fbd3a Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sat, 14 Jun 2025 22:24:32 +0200 Subject: [PATCH 046/131] fixed docs --- docs/reference/commands/build.rst | 8 ++++---- docs/reference/commands/run.rst | 11 +++++------ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/docs/reference/commands/build.rst b/docs/reference/commands/build.rst index 4245cf504..893b50eb9 100644 --- a/docs/reference/commands/build.rst +++ b/docs/reference/commands/build.rst @@ -114,16 +114,16 @@ If you have previously run the app in "normal" mode, you may need to pass ``-r`` your testing requirements are present in the test app. ``--debug `` ----------- +---------------------- Build the app in debug mode in the bundled app environment and establish an debugger connection via a socket. This installs the selected debugger in the bundled app. -Currently the following debuggers are supported: +Currently the following debuggers are supported (default is ``pdb``): - - ``'pdb'``: This is used for debugging via console. - - ``'debugpy'``: This is used for debugging via VSCode. + - ``pdb``: This is used for debugging via console. + - ``debugpy``: This is used for debugging via VSCode. It also optimizes the app for debugging. E.g. on android it ensures, that all `.py` files are extracted from the apk and are accessible for the debugger. diff --git a/docs/reference/commands/run.rst b/docs/reference/commands/run.rst index 1d8c7da71..2f8150926 100644 --- a/docs/reference/commands/run.rst +++ b/docs/reference/commands/run.rst @@ -53,9 +53,8 @@ Currently the following debuggers are supported: - ``pdb``: This is used for debugging via console. After starting the app you can connect to it depending on your host system via - - ``telnet localhost 5678`` - - ``nc -C localhost 5678`` - - ``socat readline tcp:localhost:5678`` + - ``telnet localhost 5678`` (Windows, Linux) + - ``rlwrap socat - tcp:localhost:5678`` (Linux, macOS) The app will start after the connection is established. - ``debugpy``: This is used for debugging via VSCode (see :doc:`Debugging with VSCode `) @@ -170,10 +169,10 @@ specifying by the ``--test`` option. Run the app in debug mode in the bundled app environment and establish an debugger connection via a socket. -Currently the following debuggers are supported: +Currently the following debuggers are supported (default is ``pdb``): - - ``'pdb'``: This is used for debugging via console. - - ``'debugpy'``: This is used for debugging via VSCode. + - ``pdb``: This is used for debugging via console. + - ``debugpy``: This is used for debugging via VSCode. For ``debugpy`` there is also a mapping of the source code from your bundled app to your local copy of the apps source code in the ``build`` folder. This From d59b2ff1fa34911d0206a1ec46083d95d10657cc Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sun, 15 Jun 2025 20:41:57 +0200 Subject: [PATCH 047/131] removed unnecessary branch --- src/briefcase/commands/create.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/briefcase/commands/create.py b/src/briefcase/commands/create.py index 715131163..ce2e9a1ca 100644 --- a/src/briefcase/commands/create.py +++ b/src/briefcase/commands/create.py @@ -765,9 +765,6 @@ def create_debugger_support_pkg(self, app: AppConfig) -> Path: has to be located in the app's site-packages directory, that it is executed correctly. """ - if app.debugger is None: - return - debugger_support_path = self.bundle_path(app) / ".debugger_support_package" if debugger_support_path.exists(): self.tools.shutil.rmtree(debugger_support_path) From 4259f4ae1a9f9d5756bb83bc0e9f5063609b20b9 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sun, 15 Jun 2025 21:09:02 +0200 Subject: [PATCH 048/131] fixed more CI errors --- src/briefcase/commands/create.py | 1 + src/briefcase/debuggers/base.py | 7 +++++-- src/briefcase/debuggers/debugpy.py | 3 ++- src/briefcase/debuggers/pdb.py | 7 +++++-- .../commands/create/test_install_app_requirements.py | 12 ++++++++++-- 5 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/briefcase/commands/create.py b/src/briefcase/commands/create.py index ce2e9a1ca..11c8e2774 100644 --- a/src/briefcase/commands/create.py +++ b/src/briefcase/commands/create.py @@ -765,6 +765,7 @@ def create_debugger_support_pkg(self, app: AppConfig) -> Path: has to be located in the app's site-packages directory, that it is executed correctly. """ + # Remove existing debugger support folder if it exists debugger_support_path = self.bundle_path(app) / ".debugger_support_package" if debugger_support_path.exists(): self.tools.shutil.rmtree(debugger_support_path) diff --git a/src/briefcase/debuggers/base.py b/src/briefcase/debuggers/base.py index d8f13cdb2..dff3b4f1e 100644 --- a/src/briefcase/debuggers/base.py +++ b/src/briefcase/debuggers/base.py @@ -122,7 +122,8 @@ def _restore_install_lib(self): encoding="utf-8", ) - debugger_support.write_text(""" + debugger_support.write_text( + """\ import json import os import sys @@ -165,4 +166,6 @@ def start_remote_debugger(): # Show exception and stop the whole application when an error occurs print(traceback.format_exc()) sys.exit(-1) -""") +""", + encoding="utf-8", + ) diff --git a/src/briefcase/debuggers/debugpy.py b/src/briefcase/debuggers/debugpy.py index 069e07d23..3fe9869bf 100644 --- a/src/briefcase/debuggers/debugpy.py +++ b/src/briefcase/debuggers/debugpy.py @@ -155,5 +155,6 @@ def _start_remote_debugger(config_str: str, verbose: bool): debugpy.wait_for_client() print("Debugger attached.") -''' +''', + encoding="utf-8", ) diff --git a/src/briefcase/debuggers/pdb.py b/src/briefcase/debuggers/pdb.py index 86c8c6c44..6d9a1b125 100644 --- a/src/briefcase/debuggers/pdb.py +++ b/src/briefcase/debuggers/pdb.py @@ -23,7 +23,8 @@ def create_debugger_support_pkg(self, dir: Path) -> None: ) remote_debugger = dir / "briefcase_debugger_support" / "_remote_debugger.py" - remote_debugger.write_text(''' + remote_debugger.write_text( + '''\ import json import sys @@ -53,4 +54,6 @@ def _start_remote_debugger(config_str: str, verbose: bool): sys.breakpointhook = remote_pdb.set_trace print("Debugger client attached.") -''') +''', + encoding="utf-8", + ) diff --git a/tests/commands/create/test_install_app_requirements.py b/tests/commands/create/test_install_app_requirements.py index 5b779283b..9dfb4b99c 100644 --- a/tests/commands/create/test_install_app_requirements.py +++ b/tests/commands/create/test_install_app_requirements.py @@ -1110,9 +1110,14 @@ def test_app_packages_debugger_debugger( myapp.requires = ["first", "second==1.2.3", "third>=3.2.1"] myapp.debugger = DummyDebugger() - create_command.install_app_requirements(myapp) - + # create dummy debugger support package directory, that should be cleared bundle_path = create_command.bundle_path(myapp) + (bundle_path / ".debugger_support_package").mkdir(parents=True, exist_ok=True) + (bundle_path / ".debugger_support_package" / "dummy.txt").write_text( + "dummy content" + ) + + create_command.install_app_requirements(myapp) # A request was made to install requirements create_command.tools[myapp].app_context.run.assert_called_with( @@ -1145,3 +1150,6 @@ def test_app_packages_debugger_debugger( myapp.debugger.debugger_support_pkg_dir == bundle_path / ".debugger_support_package" ) + + # Check that the debugger support package directory is empty + assert len(os.listdir(myapp.debugger.debugger_support_pkg_dir)) == 0 From 68179ec8780ee0ebb83d7e1cdc537ecf22827371 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sun, 15 Jun 2025 21:46:24 +0200 Subject: [PATCH 049/131] corrected the docs a little --- docs/reference/commands/run.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/reference/commands/run.rst b/docs/reference/commands/run.rst index 2f8150926..790c35e49 100644 --- a/docs/reference/commands/run.rst +++ b/docs/reference/commands/run.rst @@ -37,9 +37,9 @@ other test frameworks can be added using the ``test_success_regex`` and Debug mode ---------- -The debug mode can be used to (remote) debug an bundled app. The debugger to -use can be configured via ``pyproject.toml`` an can then be activated through -``briefcase run --debug ``. +The debug mode can be used to (remote) debug an bundled app. The debugger has +to be specified with the ``--debug `` option during ``briefcase build`` +and ``briefcase run``. This is useful when developing an iOS or Android app that can't be debugged via ``briefcase dev``. From a382fbb7078573612c0878be2c931515300c6f11 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sun, 15 Jun 2025 22:28:23 +0200 Subject: [PATCH 050/131] - removed all platform specific code - added `supports_debugger` flag to the commands, so that an error is raised on not supported platforms --- src/briefcase/commands/base.py | 7 + src/briefcase/commands/run.py | 5 +- src/briefcase/integrations/android_sdk.py | 93 +----- src/briefcase/platforms/android/gradle.py | 178 +++--------- src/briefcase/platforms/iOS/xcode.py | 138 ++------- src/briefcase/platforms/linux/appimage.py | 20 +- src/briefcase/platforms/linux/flatpak.py | 24 +- src/briefcase/platforms/linux/system.py | 10 +- src/briefcase/platforms/macOS/__init__.py | 24 +- src/briefcase/platforms/web/static.py | 10 +- src/briefcase/platforms/windows/__init__.py | 21 +- .../android_sdk/ADB/test_forward_reverse.py | 117 -------- .../android_sdk/ADB/test_start_app.py | 53 +--- tests/platforms/android/gradle/test_create.py | 8 - tests/platforms/android/gradle/test_run.py | 275 +----------------- tests/platforms/conftest.py | 18 -- tests/platforms/iOS/xcode/test_run.py | 222 +------------- tests/platforms/linux/appimage/test_run.py | 77 +---- tests/platforms/linux/flatpak/test_run.py | 78 +---- tests/platforms/linux/system/conftest.py | 8 - tests/platforms/linux/system/test_run.py | 117 +------- tests/platforms/macOS/app/test_run.py | 39 +-- tests/platforms/macOS/xcode/test_run.py | 12 +- tests/platforms/web/static/test_run.py | 42 --- tests/platforms/windows/app/test_run.py | 75 +---- .../windows/visualstudio/test_run.py | 64 +--- 26 files changed, 154 insertions(+), 1581 deletions(-) delete mode 100644 tests/integrations/android_sdk/ADB/test_forward_reverse.py diff --git a/src/briefcase/commands/base.py b/src/briefcase/commands/base.py index 85d5a373c..e7e1fab9e 100644 --- a/src/briefcase/commands/base.py +++ b/src/briefcase/commands/base.py @@ -136,6 +136,8 @@ class BaseCommand(ABC): output_format: str # supports passing extra command line arguments to subprocess allows_passthrough = False + # supports remote debugging + supports_debugger = False # if specified for a platform, then any template for that platform must declare # compatibility with that version epoch. An epoch begins when a breaking change is # introduced for a platform such that older versions of a template are incompatible @@ -671,6 +673,11 @@ def finalize_debugger(self, app: AppConfig, debugger_name: str | None = None): :param app: The app configuration to finalize. """ + if not self.supports_debugger and (debugger_name is not None): + raise BriefcaseCommandError( + f"The {self.command} command for the {self.platform} {self.output_format} format does not support debugging." + ) + if debugger_name and debugger_name != "": debugger = get_debugger(debugger_name) app.debugger = debugger diff --git a/src/briefcase/commands/run.py b/src/briefcase/commands/run.py index 68685ff09..9fac0f5d2 100644 --- a/src/briefcase/commands/run.py +++ b/src/briefcase/commands/run.py @@ -298,7 +298,10 @@ def remote_debugger_config( return json.dumps(config) def _prepare_app_kwargs( - self, app: AppConfig, debugger_host: str | None, debugger_port: int | None + self, + app: AppConfig, + debugger_host: str | None = None, + debugger_port: int | None = None, ): """Prepare the kwargs for running an app as a log stream. diff --git a/src/briefcase/integrations/android_sdk.py b/src/briefcase/integrations/android_sdk.py index 2487be355..e0ea3fe42 100644 --- a/src/briefcase/integrations/android_sdk.py +++ b/src/briefcase/integrations/android_sdk.py @@ -1514,9 +1514,7 @@ def force_stop_app(self, package: str): f"Unable to force stop app {package} on {self.device}" ) from e - def start_app( - self, package: str, activity: str, passthrough: list[str], env: dict[str, str] - ): + def start_app(self, package: str, activity: str, passthrough: list[str]): """Start an app, specified as a package name & activity name. If you have an APK file, and you are not sure of the package or activity @@ -1545,15 +1543,6 @@ def start_app( "--es", "org.beeware.ARGV", shlex.quote(json.dumps(passthrough)), # Protect from Android's shell - *( - [ - "--es", - "org.beeware.ENVIRON", - shlex.quote(json.dumps(env)), # Protect from Android's shell - ] - if env - else [] - ), ) # `adb shell am start` always exits with status zero. We look for error @@ -1639,86 +1628,6 @@ def logcat_tail(self, since: datetime): except subprocess.CalledProcessError as e: raise BriefcaseCommandError("Error starting ADB logcat.") from e - def forward(self, host_port: int, device_port: int): - """Use the forward command to set up arbitrary port forwarding, which - forwards requests on a specific host port to a different port on a device. - - :param host_port: The port on the host that should be forwarded to the device - :param device_port: The port on the device - """ - try: - self.tools.subprocess.check_output( - [ - self.tools.android_sdk.adb_path, - "-s", - self.device, - "forward", - f"tcp:{host_port}", - f"tcp:{device_port}", - ], - ) - except subprocess.CalledProcessError as e: - raise BriefcaseCommandError("Error starting 'adb forward'.") from e - - def forward_remove(self, host_port: int): - """Remove forwarded port. - - :param host_port: The port on the host that should be removed - """ - try: - self.tools.subprocess.check_output( - [ - self.tools.android_sdk.adb_path, - "-s", - self.device, - "forward", - "--remove", - f"tcp:{host_port}", - ], - ) - except subprocess.CalledProcessError as e: - raise BriefcaseCommandError("Error starting 'adb forward --remove'.") from e - - def reverse(self, device_port: int, host_port: int): - """Use the reverse command to set up arbitrary port forwarding, which - forwards requests on a specific device port to a different port on the host. - - :param device_port: The port on the device that should be forwarded to the host - :param host_port: The port on the host - """ - try: - self.tools.subprocess.check_output( - [ - self.tools.android_sdk.adb_path, - "-s", - self.device, - "reverse", - f"tcp:{device_port}", - f"tcp:{host_port}", - ], - ) - except subprocess.CalledProcessError as e: - raise BriefcaseCommandError("Error starting 'adb reverse'.") from e - - def reverse_remove(self, device_port: int): - """Remove reversed port. - - :param device_port: The port on the device that should be removed - """ - try: - self.tools.subprocess.check_output( - [ - self.tools.android_sdk.adb_path, - "-s", - self.device, - "reverse", - "--remove", - f"tcp:{device_port}", - ], - ) - except subprocess.CalledProcessError as e: - raise BriefcaseCommandError("Error starting 'adb reverse --remove'.") from e - def pidof(self, package: str, **kwargs) -> str | None: """Obtain the PID of a running app by package name. diff --git a/src/briefcase/platforms/android/gradle.py b/src/briefcase/platforms/android/gradle.py index 7bd9b9372..d32f30a11 100644 --- a/src/briefcase/platforms/android/gradle.py +++ b/src/briefcase/platforms/android/gradle.py @@ -1,6 +1,5 @@ from __future__ import annotations -import contextlib import datetime import re import subprocess @@ -18,12 +17,8 @@ ) from briefcase.config import AppConfig, parsed_version from briefcase.console import ANSI_ESC_SEQ_RE_DEF -from briefcase.debuggers.base import ( - AppPackagesPathMappings, - DebuggerConnectionMode, -) from briefcase.exceptions import BriefcaseCommandError -from briefcase.integrations.android_sdk import ADB, AndroidSDK +from briefcase.integrations.android_sdk import AndroidSDK from briefcase.integrations.subprocess import SubprocessArgT @@ -219,19 +214,15 @@ def output_format_template_context(self, app: AppConfig): "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0", ] - # Extract test packages, to enable features like test discovery and assertion rewriting. - extract_sources = app.test_sources or [] - - # In debug mode extract all source packages so that the debugger can get the source code - # at runtime (eg. via 'll' in pdb). - if app.debugger: - extract_sources.extend(app.sources) - return { "version_code": version_code, "safe_formal_name": safe_formal_name(app.formal_name), + # Extract test packages, to enable features like test discovery and assertion + # rewriting. "extract_packages": ", ".join( - [f'"{name}"' for path in extract_sources if (name := Path(path).name)] + f'"{name}"' + for path in (app.test_sources or []) + if (name := Path(path).name) ), "build_gradle_dependencies": {"implementation": dependencies}, } @@ -368,26 +359,9 @@ def add_options(self, parser): required=False, ) - def remote_debugger_app_packages_path_mapping( - self, app: AppConfig - ) -> AppPackagesPathMappings: - """ - Get the path mappings for the app packages. - - :param app: The config object for the app - :returns: The path mappings for the app packages - """ - app_packages_path = self.bundle_path(app) / "app/build/python/pip/debug/common" - return AppPackagesPathMappings( - sys_path_regex="requirements$", - host_folder=f"{app_packages_path}", - ) - def run_app( self, app: AppConfig, - debugger_host: str | None, - debugger_port: int | None, passthrough: list[str], device_or_avd=None, extra_emulator_args=None, @@ -397,8 +371,6 @@ def run_app( """Start the application. :param app: The config object for the app - :param debugger_host: The host to use for the debugger - :param debugger_port: The port to use for the debugger :param passthrough: The list of arguments to pass to the app :param device_or_avd: The device to target. If ``None``, the user will be asked to re-run the command selecting a specific device. @@ -452,111 +424,55 @@ def run_app( with self.console.wait_bar("Installing new app version..."): adb.install_apk(self.binary_path(app)) - env = {} - if self.console.is_debug: - env["BRIEFCASE_DEBUG"] = "1" - - if app.debugger: - env["BRIEFCASE_DEBUGGER"] = self.remote_debugger_config( - app, debugger_host, debugger_port + # To start the app, we launch `org.beeware.android.MainActivity`. + with self.console.wait_bar(f"Launching {label}..."): + # capture the earliest time for device logging in case PID not found + device_start_time = adb.datetime() + + adb.start_app(package, "org.beeware.android.MainActivity", passthrough) + + # Try to get the PID for 5 seconds. + pid = None + fail_time = datetime.datetime.now() + datetime.timedelta(seconds=5) + while not pid and datetime.datetime.now() < fail_time: + # Try to get the PID; run in quiet mode because we may + # need to do this a lot in the next 5 seconds. + pid = adb.pidof(package, quiet=2) + if not pid: + time.sleep(0.01) + + if pid: + self.console.info( + "Following device log output (type CTRL-C to stop log)...", + prefix=app.app_name, + ) + # Start adb's logcat in a way that lets us stream the logs + log_popen = adb.logcat(pid=pid) + + # Stream the app logs. + self._stream_app_logs( + app, + popen=log_popen, + clean_filter=android_log_clean_filter, + clean_output=False, + # Check for the PID in quiet mode so logs aren't corrupted. + stop_func=lambda: not adb.pid_exists(pid=pid, quiet=2), + log_stream=True, ) + else: + self.console.error("Unable to find PID for app", prefix=app.app_name) + self.console.error("Logs for launch attempt follow...") + self.console.error("=" * 75) - # Forward port for debugger connection if configured. Else this is a no-op. - with self.forward_port_for_debugger(app, debugger_host, debugger_port, adb): - # To start the app, we launch `org.beeware.android.MainActivity`. - with self.console.wait_bar(f"Launching {label}..."): - # capture the earliest time for device logging in case PID not found - device_start_time = adb.datetime() - - adb.start_app( - package, "org.beeware.android.MainActivity", passthrough, env - ) - - # Try to get the PID for 5 seconds. - pid = None - fail_time = datetime.datetime.now() + datetime.timedelta(seconds=5) - while not pid and datetime.datetime.now() < fail_time: - # Try to get the PID; run in quiet mode because we may - # need to do this a lot in the next 5 seconds. - pid = adb.pidof(package, quiet=2) - if not pid: - time.sleep(0.01) - - if pid: - self.console.info( - "Following device log output (type CTRL-C to stop log)...", - prefix=app.app_name, - ) - # Start adb's logcat in a way that lets us stream the logs - log_popen = adb.logcat(pid=pid) - - # Stream the app logs. - self._stream_app_logs( - app, - popen=log_popen, - clean_filter=android_log_clean_filter, - clean_output=False, - # Check for the PID in quiet mode so logs aren't corrupted. - stop_func=lambda: not adb.pid_exists(pid=pid, quiet=2), - log_stream=True, - ) - else: - self.console.error( - "Unable to find PID for app", prefix=app.app_name - ) - self.console.error("Logs for launch attempt follow...") - self.console.error("=" * 75) - - # Show the log from the start time of the app - adb.logcat_tail(since=device_start_time) - - raise BriefcaseCommandError( - f"Problem starting app {app.app_name!r}" - ) + # Show the log from the start time of the app + adb.logcat_tail(since=device_start_time) + raise BriefcaseCommandError(f"Problem starting app {app.app_name!r}") finally: if shutdown_on_exit: with self.tools.console.wait_bar("Stopping emulator..."): adb.kill() - @contextlib.contextmanager - def forward_port_for_debugger( - self, app: AppConfig, debugger_host: str, debugger_port: int, adb: ADB - ): - """Establish a port forwarding for the debugger connection. - :param app: The config object for the app - :param debugger_host: The host to use for the debugger - :param debugger_port: The port to use for the debugger - :param adb: The ADB wrapper for the device - """ - if app.debugger and debugger_host == "localhost": - if app.debugger.connection_mode == DebuggerConnectionMode.SERVER: - with self.console.wait_bar( - f"Start forwarding port '{debugger_port}' from host to device for debugger connection..." - ): - adb.forward(debugger_port, debugger_port) - - yield - - with self.console.wait_bar( - f"Stop forwarding port '{debugger_port}' from host to device for debugger connection..." - ): - adb.forward_remove(debugger_port) - else: - with self.console.wait_bar( - f"Start reversing port '{debugger_port}' from device to host for debugger connection..." - ): - adb.reverse(debugger_port, debugger_port) - - yield - - with self.console.wait_bar( - f"Stop reversing port '{debugger_port}' from device to host for debugger connection..." - ): - adb.reverse_remove(debugger_port) - else: - yield # no-op if no debugger is configured - class GradlePackageCommand(GradleMixin, PackageCommand): description = "Create an Android App Bundle and APK in release mode." diff --git a/src/briefcase/platforms/iOS/xcode.py b/src/briefcase/platforms/iOS/xcode.py index dffe24580..c7ad4f31a 100644 --- a/src/briefcase/platforms/iOS/xcode.py +++ b/src/briefcase/platforms/iOS/xcode.py @@ -1,6 +1,5 @@ from __future__ import annotations -import contextlib import plistlib import subprocess import time @@ -19,7 +18,6 @@ UpdateCommand, ) from briefcase.config import AppConfig -from briefcase.debuggers.base import AppPackagesPathMappings from briefcase.exceptions import ( BriefcaseCommandError, InputDisabled, @@ -490,29 +488,9 @@ def __init__(self, *args, **kwargs): # This is abstracted to enable testing without patching. self.get_device_state = get_device_state - def remote_debugger_app_packages_path_mapping( - self, app: AppConfig - ) -> AppPackagesPathMappings: - """ - Get the path mappings for the app packages. - - :param app: The config object for the app - :returns: The path mappings for the app packages - """ - # TODO: Add handling to switch between simulator and real device. Currently we only - # support simulator. - return AppPackagesPathMappings( - sys_path_regex="app_packages$", - host_folder=str( - self.app_packages_path(app).parent / "app_packages.iphonesimulator" - ), - ) - def run_app( self, app: AppConfig, - debugger_host: str | None, - debugger_port: int | None, passthrough: list[str], udid=None, **kwargs, @@ -520,8 +498,6 @@ def run_app( """Start the application. :param app: The config object for the app - :param debugger_host: The host to use for the debugger - :param debugger_port: The port to use for the debugger :param passthrough: The list of arguments to pass to the app :param udid: The device UDID to target. If ``None``, the user will be asked to select a device at runtime. @@ -662,95 +638,43 @@ def run_app( # Wait for the log stream start up time.sleep(0.25) - # Add additional environment variables - env = {} - if app.debugger: - env["BRIEFCASE_DEBUGGER"] = self.remote_debugger_config( - app, debugger_host, debugger_port - ) - - # Set additional environment variables while the app is running (no-op if no environment variables are set). - with self.setup_env(env, udid): - try: - self.console.info(f"Starting {label}...", prefix=app.app_name) - with self.console.wait_bar(f"Launching {label}..."): - output = self.tools.subprocess.check_output( - ["xcrun", "simctl", "launch", udid, app.bundle_identifier] - + passthrough - ) - try: - app_pid = int(output.split(":")[1].strip()) - except (IndexError, ValueError) as e: - raise BriefcaseCommandError( - f"Unable to determine PID of {label} {app.app_name}." - ) from e - - # Start streaming logs for the app. - self.console.info( - "Following simulator log output (type CTRL-C to stop log)...", - prefix=app.app_name, + try: + self.console.info(f"Starting {label}...", prefix=app.app_name) + with self.console.wait_bar(f"Launching {label}..."): + output = self.tools.subprocess.check_output( + ["xcrun", "simctl", "launch", udid, app.bundle_identifier] + + passthrough ) + try: + app_pid = int(output.split(":")[1].strip()) + except (IndexError, ValueError) as e: + raise BriefcaseCommandError( + f"Unable to determine PID of {label} {app.app_name}." + ) from e + + # Start streaming logs for the app. + self.console.info( + "Following simulator log output (type CTRL-C to stop log)...", + prefix=app.app_name, + ) - # Stream the app logs, - self._stream_app_logs( - app, - popen=simulator_log_popen, - clean_filter=macOS_log_clean_filter, - clean_output=True, - stop_func=lambda: is_process_dead(app_pid), - log_stream=True, - ) - except subprocess.CalledProcessError as e: - raise BriefcaseCommandError( - f"Unable to launch {label} {app.app_name}." - ) from e + # Stream the app logs, + self._stream_app_logs( + app, + popen=simulator_log_popen, + clean_filter=macOS_log_clean_filter, + clean_output=True, + stop_func=lambda: is_process_dead(app_pid), + log_stream=True, + ) + except subprocess.CalledProcessError as e: + raise BriefcaseCommandError( + f"Unable to launch {label} {app.app_name}." + ) from e # Preserve the device selection as state. return {"udid": udid} - @contextlib.contextmanager - def setup_env( - self, - env: dict[str, str] | None, - udid: str, - ): - """Context manager to set up the environment for the command.""" - if env: - with self.console.wait_bar("Setting environment variables in simulator..."): - for env_key, env_value in env.items(): - self.tools.subprocess.check_output( - [ - "xcrun", - "simctl", - "spawn", - udid, - "launchctl", - "setenv", - f"{env_key}", - f"{env_value}", - ] - ) - - yield - - with self.console.wait_bar( - "Removing environment variables from simulator..." - ): - for env_key in env.keys(): - self.tools.subprocess.check_output( - [ - "xcrun", - "simctl", - "spawn", - udid, - "launchctl", - "unsetenv", - f"{env_key}", - ] - ) - else: - yield # no-op if no environment variables are set - class iOSXcodePackageCommand(iOSXcodeMixin, PackageCommand): description = "Package an iOS app." diff --git a/src/briefcase/platforms/linux/appimage.py b/src/briefcase/platforms/linux/appimage.py index 6cc07658c..b6aa93d33 100644 --- a/src/briefcase/platforms/linux/appimage.py +++ b/src/briefcase/platforms/linux/appimage.py @@ -12,7 +12,6 @@ UpdateCommand, ) from briefcase.config import AppConfig -from briefcase.debuggers.base import AppPackagesPathMappings from briefcase.exceptions import ( BriefcaseCommandError, BriefcaseConfigError, @@ -367,22 +366,9 @@ class LinuxAppImageRunCommand(LinuxAppImagePassiveMixin, RunCommand): supported_host_os = {"Linux"} supported_host_os_reason = "Linux AppImages can only be executed on Linux." - def remote_debugger_app_packages_path_mapping( - self, app: AppConfig - ) -> AppPackagesPathMappings: - """ - Get the path mappings for the app packages. - - :param app: The config object for the app - :returns: The path mappings for the app packages - """ - return None # TODO: Where are the app packages located? - def run_app( self, app: AppConfig, - debugger_host: str | None, - debugger_port: int | None, passthrough: list[str], **kwargs, ): @@ -392,11 +378,7 @@ def run_app( :param passthrough: The list of arguments to pass to the app """ # Set up the log stream - kwargs = self._prepare_app_kwargs( - app=app, - debugger_host=debugger_host, - debugger_port=debugger_port, - ) + kwargs = self._prepare_app_kwargs(app=app) # Console apps must operate in non-streaming mode so that console input can # be handled correctly. However, if we're in test mode, we *must* stream so diff --git a/src/briefcase/platforms/linux/flatpak.py b/src/briefcase/platforms/linux/flatpak.py index 4019451d8..c4b2c3a44 100644 --- a/src/briefcase/platforms/linux/flatpak.py +++ b/src/briefcase/platforms/linux/flatpak.py @@ -10,7 +10,6 @@ UpdateCommand, ) from briefcase.config import AppConfig -from briefcase.debuggers.base import AppPackagesPathMappings from briefcase.exceptions import BriefcaseConfigError from briefcase.integrations.flatpak import Flatpak from briefcase.platforms.linux import LinuxMixin @@ -199,26 +198,9 @@ def build_app(self, app: AppConfig, **kwargs): class LinuxFlatpakRunCommand(LinuxFlatpakMixin, RunCommand): description = "Run a Linux Flatpak." - def remote_debugger_app_packages_path_mapping( - self, app: AppConfig - ) -> AppPackagesPathMappings: - """ - Get the path mappings for the app packages. - - :param app: The config object for the app - :returns: The path mappings for the app packages - """ - app_packages_path = self.bundle_path(app) / "build/files/briefcase/app_packages" - return AppPackagesPathMappings( - sys_path_regex="app_packages$", - host_folder=f"{app_packages_path}", - ) - def run_app( self, app: AppConfig, - debugger_host: str | None, - debugger_port: int | None, passthrough: list[str], **kwargs, ): @@ -228,11 +210,7 @@ def run_app( :param passthrough: The list of arguments to pass to the app """ # Set up the log stream - kwargs = self._prepare_app_kwargs( - app=app, - debugger_host=debugger_host, - debugger_port=debugger_port, - ) + kwargs = self._prepare_app_kwargs(app=app) # Console apps must operate in non-streaming mode so that console input can # be handled correctly. However, if we're in test mode, we *must* stream so diff --git a/src/briefcase/platforms/linux/system.py b/src/briefcase/platforms/linux/system.py index 3f030ed41..e637fff44 100644 --- a/src/briefcase/platforms/linux/system.py +++ b/src/briefcase/platforms/linux/system.py @@ -838,24 +838,16 @@ class LinuxSystemRunCommand(LinuxSystemMixin, RunCommand): def run_app( self, app: AppConfig, - debugger_host: str | None, - debugger_port: int | None, passthrough: list[str], **kwargs, ): """Start the application. :param app: The config object for the app - :param debugger_host: The host to use for the debugger - :param debugger_port: The port to use for the debugger :param passthrough: The list of arguments to pass to the app """ # Set up the log stream - kwargs = self._prepare_app_kwargs( - app=app, - debugger_host=debugger_host, - debugger_port=debugger_port, - ) + kwargs = self._prepare_app_kwargs(app=app) with self.tools[app].app_context.run_app_context(kwargs) as kwargs: # Console apps must operate in non-streaming mode so that console input can diff --git a/src/briefcase/platforms/macOS/__init__.py b/src/briefcase/platforms/macOS/__init__.py index ad255f6ac..5ec52e66a 100644 --- a/src/briefcase/platforms/macOS/__init__.py +++ b/src/briefcase/platforms/macOS/__init__.py @@ -301,16 +301,12 @@ class macOSRunMixin: def run_app( self, app: AppConfig, - debugger_host: str | None, - debugger_port: int | None, passthrough: list[str], **kwargs, ): """Start the application. :param app: The config object for the app - :param debugger_host: The host to use for the debugger - :param debugger_port: The port to use for the debugger :param passthrough: The list of arguments to pass to the app """ # Console apps must operate in non-streaming mode so that console input can @@ -319,16 +315,12 @@ def run_app( if app.console_app: self.run_console_app( app, - debugger_host=debugger_host, - debugger_port=debugger_port, passthrough=passthrough, **kwargs, ) else: self.run_gui_app( app, - debugger_host=debugger_host, - debugger_port=debugger_port, passthrough=passthrough, **kwargs, ) @@ -336,8 +328,6 @@ def run_app( def run_console_app( self, app: AppConfig, - debugger_host: str | None, - debugger_port: int | None, passthrough: list[str], **kwargs, ): @@ -346,11 +336,7 @@ def run_console_app( :param app: The config object for the app :param passthrough: The list of arguments to pass to the app """ - sub_kwargs = self._prepare_app_kwargs( - app=app, - debugger_host=debugger_host, - debugger_port=debugger_port, - ) + sub_kwargs = self._prepare_app_kwargs(app=app) cmdline = [self.binary_path(app) / f"Contents/MacOS/{app.formal_name}"] cmdline.extend(passthrough) @@ -389,8 +375,6 @@ def run_console_app( def run_gui_app( self, app: AppConfig, - debugger_host: str | None, - debugger_port: int | None, passthrough: list[str], **kwargs, ): @@ -438,11 +422,7 @@ def run_gui_app( app_pid = None try: # Set up the log stream - sub_kwargs = self._prepare_app_kwargs( - app=app, - debugger_host=debugger_host, - debugger_port=debugger_port, - ) + sub_kwargs = self._prepare_app_kwargs(app=app) # Start the app in a way that lets us stream the logs self.tools.subprocess.run( diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index 9c09603b5..90e44d1c3 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import errno import subprocess import sys @@ -240,7 +238,7 @@ def build_app(self, app: AppConfig, **kwargs): class HTTPHandler(SimpleHTTPRequestHandler): """Convert any HTTP request into a path request on the static content folder.""" - server: LocalHTTPServer + server: "LocalHTTPServer" def translate_path(self, path): return str(self.server.base_path / path[1:]) @@ -312,8 +310,6 @@ def add_options(self, parser): def run_app( self, app: AppConfig, - debugger_host: str | None, - debugger_port: int | None, passthrough: list[str], host, port, @@ -330,10 +326,6 @@ def run_app( """ if app.test_mode: raise BriefcaseCommandError("Briefcase can't run web apps in test mode.") - if app.debugger: - raise BriefcaseCommandError( - "Briefcase can't run web apps with an debugger." - ) self.console.info("Starting web server...", prefix=app.app_name) diff --git a/src/briefcase/platforms/windows/__init__.py b/src/briefcase/platforms/windows/__init__.py index f8e40f19a..41f5802d4 100644 --- a/src/briefcase/platforms/windows/__init__.py +++ b/src/briefcase/platforms/windows/__init__.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import re import subprocess import uuid @@ -128,22 +126,9 @@ def _cleanup_app_support_package(self, support_path): class WindowsRunCommand(RunCommand): - def remote_debugger_app_packages_path_mapping(self, app: AppConfig) -> None: - """ - Get the path mappings for the app packages. - - :param app: The config object for the app - :returns: The path mappings for the app packages - """ - # No path mapping is required. The paths are automatically found, because - # developing an windows app also requires a windows host. - return None - def run_app( self, app: AppConfig, - debugger_host: str | None, - debugger_port: int | None, passthrough: list[str], **kwargs, ): @@ -153,11 +138,7 @@ def run_app( :param passthrough: The list of arguments to pass to the app """ # Set up the log stream - kwargs = self._prepare_app_kwargs( - app=app, - debugger_host=debugger_host, - debugger_port=debugger_port, - ) + kwargs = self._prepare_app_kwargs(app=app) # Console apps must operate in non-streaming mode so that console input can # be handled correctly. However, if we're in test mode, we *must* stream so diff --git a/tests/integrations/android_sdk/ADB/test_forward_reverse.py b/tests/integrations/android_sdk/ADB/test_forward_reverse.py deleted file mode 100644 index 3d05cb54c..000000000 --- a/tests/integrations/android_sdk/ADB/test_forward_reverse.py +++ /dev/null @@ -1,117 +0,0 @@ -import subprocess - -import pytest - -from briefcase.exceptions import BriefcaseCommandError - - -def test_forward(mock_tools, adb): - """An port forwarding.""" - # Invoke forward - adb.forward(5555, 6666) - - # Validate call parameters. - mock_tools.subprocess.check_output.assert_called_once_with( - [ - mock_tools.android_sdk.adb_path, - "-s", - "exampleDevice", - "forward", - "tcp:5555", - "tcp:6666", - ], - ) - - -def test_forward_failure(adb, mock_tools): - """If port forwarding fails, the error is caught.""" - # Mock out the run command on an adb instance - mock_tools.subprocess.check_output.side_effect = subprocess.CalledProcessError( - returncode=1, cmd="" - ) - with pytest.raises(BriefcaseCommandError): - adb.forward(5555, 6666) - - -def test_forward_remove(mock_tools, adb): - """An port forwarding removing.""" - # Invoke forward remove - adb.forward_remove(5555) - - # Validate call parameters. - mock_tools.subprocess.check_output.assert_called_once_with( - [ - mock_tools.android_sdk.adb_path, - "-s", - "exampleDevice", - "forward", - "--remove", - "tcp:5555", - ], - ) - - -def test_forward_remove_failure(adb, mock_tools): - """If port forwarding removing fails, the error is caught.""" - # Mock out the run command on an adb instance - mock_tools.subprocess.check_output.side_effect = subprocess.CalledProcessError( - returncode=1, cmd="" - ) - with pytest.raises(BriefcaseCommandError): - adb.forward_remove(5555) - - -def test_reverse(mock_tools, adb): - """An port reversing.""" - # Invoke reverse - adb.reverse(5555, 6666) - - # Validate call parameters. - mock_tools.subprocess.check_output.assert_called_once_with( - [ - mock_tools.android_sdk.adb_path, - "-s", - "exampleDevice", - "reverse", - "tcp:5555", - "tcp:6666", - ], - ) - - -def test_reverse_failure(adb, mock_tools): - """If port reversing fails, the error is caught.""" - # Mock out the run command on an adb instance - mock_tools.subprocess.check_output.side_effect = subprocess.CalledProcessError( - returncode=1, cmd="" - ) - with pytest.raises(BriefcaseCommandError): - adb.reverse(5555, 6666) - - -def test_reverse_remove(mock_tools, adb): - """An port reversing removing.""" - # Invoke reverse remove - adb.reverse_remove(5555) - - # Validate call parameters. - mock_tools.subprocess.check_output.assert_called_once_with( - [ - mock_tools.android_sdk.adb_path, - "-s", - "exampleDevice", - "reverse", - "--remove", - "tcp:5555", - ], - ) - - -def test_reverse_remove_failure(adb, mock_tools): - """If port reversing removing fails, the error is caught.""" - # Mock out the run command on an adb instance - mock_tools.subprocess.check_output.side_effect = subprocess.CalledProcessError( - returncode=1, cmd="" - ) - with pytest.raises(BriefcaseCommandError): - adb.reverse_remove(5555) diff --git a/tests/integrations/android_sdk/ADB/test_start_app.py b/tests/integrations/android_sdk/ADB/test_start_app.py index 1c6a0d525..17788a0dc 100644 --- a/tests/integrations/android_sdk/ADB/test_start_app.py +++ b/tests/integrations/android_sdk/ADB/test_start_app.py @@ -30,7 +30,7 @@ def test_start_app_launches_app(adb, capsys, passthrough): # Invoke start_app adb.start_app( - "com.example.sample.package", "com.example.sample.activity", passthrough, {} + "com.example.sample.package", "com.example.sample.activity", passthrough ) # Validate call parameters. @@ -54,45 +54,6 @@ def test_start_app_launches_app(adb, capsys, passthrough): assert "normal adb output" not in capsys.readouterr() -@pytest.mark.parametrize( - "env", - [ - {"PARAM1": "VALUE1"}, - {"BRIEFCASE_DEBUGGER": '{"host": "localhost", "port": 1234}'}, - ], -) -def test_start_app_launches_app_with_env(adb, capsys, env): - """Invoking `start_app()` calls `run()` with the appropriate parameters.""" - # Mock out the run command on an adb instance - adb.run = MagicMock(return_value="example normal adb output") - - # Invoke start_app - adb.start_app("com.example.sample.package", "com.example.sample.activity", [], env) - - # Validate call parameters. - adb.run.assert_called_once_with( - "shell", - "am", - "start", - "-n", - "com.example.sample.package/com.example.sample.activity", - "-a", - "android.intent.action.MAIN", - "-c", - "android.intent.category.LAUNCHER", - "--es", - "org.beeware.ARGV", - "'[]'", - "--es", - "org.beeware.ENVIRON", - shlex.quote(json.dumps(env)), - ) - - # Validate that the normal output of the command was not printed (since there - # was no error). - assert "normal adb output" not in capsys.readouterr() - - def test_missing_activity(adb): """If the activity doesn't exist, the error is caught.""" # Use real `adb` output from launching an activity that does not exist. @@ -108,9 +69,7 @@ def test_missing_activity(adb): ) with pytest.raises(BriefcaseCommandError) as exc_info: - adb.start_app( - "com.example.sample.package", "com.example.sample.activity", [], {} - ) + adb.start_app("com.example.sample.package", "com.example.sample.activity", []) assert "Activity class not found" in str(exc_info.value) @@ -122,9 +81,7 @@ def test_invalid_device(adb): adb.run = MagicMock(side_effect=InvalidDeviceError("device", "exampleDevice")) with pytest.raises(InvalidDeviceError): - adb.start_app( - "com.example.sample.package", "com.example.sample.activity", [], {} - ) + adb.start_app("com.example.sample.package", "com.example.sample.activity", []) def test_unable_to_start(adb): @@ -135,6 +92,4 @@ def test_unable_to_start(adb): BriefcaseCommandError, match=r"Unable to start com.example.sample.package/com.example.sample.activity on exampleDevice", ): - adb.start_app( - "com.example.sample.package", "com.example.sample.activity", [], {} - ) + adb.start_app("com.example.sample.package", "com.example.sample.activity", []) diff --git a/tests/platforms/android/gradle/test_create.py b/tests/platforms/android/gradle/test_create.py index e4472e03f..4a3b1b133 100644 --- a/tests/platforms/android/gradle/test_create.py +++ b/tests/platforms/android/gradle/test_create.py @@ -201,14 +201,6 @@ def test_extract_packages(create_command, first_app_config, test_sources, expect assert context["extract_packages"] == expected -def test_extract_packages_debugger(create_command, first_app_config, dummy_debugger): - first_app_config.test_sources = ["one", "two", "three"] - first_app_config.sources = ["four", "five", "six"] - first_app_config.debugger = dummy_debugger - context = create_command.output_format_template_context(first_app_config) - assert context["extract_packages"] == '"one", "two", "three", "four", "five", "six"' - - @pytest.mark.parametrize( "permissions, features, context", [ diff --git a/tests/platforms/android/gradle/test_run.py b/tests/platforms/android/gradle/test_run.py index 6b262743e..2f6fb6485 100644 --- a/tests/platforms/android/gradle/test_run.py +++ b/tests/platforms/android/gradle/test_run.py @@ -1,5 +1,4 @@ import datetime -import json import os import platform import sys @@ -10,8 +9,7 @@ import httpx import pytest -from briefcase.console import Console, LogLevel -from briefcase.debuggers.base import BaseDebugger, DebuggerConnectionMode +from briefcase.console import Console from briefcase.exceptions import BriefcaseCommandError from briefcase.integrations.android_sdk import ADB, AndroidSDK from briefcase.integrations.java import JDK @@ -92,9 +90,6 @@ def test_device_option(run_command): "update_stub": False, "no_update": False, "test_mode": False, - "debugger": None, - "debugger_host": "localhost", - "debugger_port": 5678, "passthrough": [], "extra_emulator_args": None, "shutdown_on_exit": False, @@ -118,9 +113,6 @@ def test_extra_emulator_args_option(run_command): "update_stub": False, "no_update": False, "test_mode": False, - "debugger": None, - "debugger_host": "localhost", - "debugger_port": 5678, "passthrough": [], "extra_emulator_args": ["-no-window", "-no-audio"], "shutdown_on_exit": False, @@ -142,9 +134,6 @@ def test_shutdown_on_exit_option(run_command): "update_stub": False, "no_update": False, "test_mode": False, - "debugger": None, - "debugger_host": "localhost", - "debugger_port": 5678, "passthrough": [], "extra_emulator_args": None, "shutdown_on_exit": True, @@ -203,8 +192,6 @@ def mock_stream_output(app, stop_func, **kwargs): run_command.run_app( first_app_config, device_or_avd="exampleDevice", - debugger_host=None, - debugger_port=None, passthrough=[], ) @@ -228,7 +215,6 @@ def mock_stream_output(app, stop_func, **kwargs): f"{first_app_config.package_name}.{first_app_config.module_name}", "org.beeware.android.MainActivity", [], - {}, ) run_command.tools.mock_adb.pidof.assert_called_once_with( @@ -276,8 +262,6 @@ def mock_stream_output(app, stop_func, **kwargs): run_command.run_app( first_app_config, device_or_avd="exampleDevice", - debugger_host=None, - debugger_port=None, passthrough=["foo", "--bar"], ) @@ -301,7 +285,6 @@ def mock_stream_output(app, stop_func, **kwargs): f"{first_app_config.package_name}.{first_app_config.module_name}", "org.beeware.android.MainActivity", ["foo", "--bar"], - {}, ) run_command.tools.mock_adb.pidof.assert_called_once_with( @@ -340,8 +323,6 @@ def test_run_slow_start(run_command, first_app_config, monkeypatch): run_command.run_app( first_app_config, device_or_avd="exampleDevice", - debugger_host=None, - debugger_port=None, passthrough=[], ) @@ -394,8 +375,6 @@ def test_run_crash_at_start(run_command, first_app_config, monkeypatch): run_command.run_app( first_app_config, device_or_avd="exampleDevice", - debugger_host=None, - debugger_port=None, passthrough=[], ) @@ -433,9 +412,7 @@ def test_run_created_emulator(run_command, first_app_config): run_command.tools.mock_adb.logcat.return_value = log_popen # Invoke run_app - run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app_config, passthrough=[]) # A new emulator was created run_command.tools.android_sdk.create_emulator.assert_called_once_with() @@ -464,7 +441,6 @@ def test_run_created_emulator(run_command, first_app_config): f"{first_app_config.package_name}.{first_app_config.module_name}", "org.beeware.android.MainActivity", [], - {}, ) run_command.tools.mock_adb.logcat.assert_called_once_with(pid="777") @@ -498,9 +474,7 @@ def test_run_idle_device(run_command, first_app_config): run_command.tools.mock_adb.logcat.return_value = log_popen # Invoke run_app - run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app_config, passthrough=[]) # No attempt was made to create a new emulator run_command.tools.android_sdk.create_emulator.assert_not_called() @@ -528,7 +502,6 @@ def test_run_idle_device(run_command, first_app_config): f"{first_app_config.package_name}.{first_app_config.module_name}", "org.beeware.android.MainActivity", [], - {}, ) run_command.tools.mock_adb.logcat.assert_called_once_with(pid="777") @@ -606,8 +579,6 @@ def mock_stream_output(app, stop_func, **kwargs): run_command.run_app( first_app_config, device_or_avd="exampleDevice", - debugger_host=None, - debugger_port=None, passthrough=[], shutdown_on_exit=True, ) @@ -632,7 +603,6 @@ def mock_stream_output(app, stop_func, **kwargs): f"{first_app_config.package_name}.{first_app_config.module_name}", "org.beeware.android.MainActivity", [], - {}, ) run_command.tools.mock_adb.pidof.assert_called_once_with( @@ -682,8 +652,6 @@ def mock_stream_output(app, stop_func, **kwargs): run_command.run_app( first_app_config, device_or_avd="exampleDevice", - debugger_host=None, - debugger_port=None, passthrough=["foo", "--bar"], shutdown_on_exit=True, ) @@ -708,7 +676,6 @@ def mock_stream_output(app, stop_func, **kwargs): f"{first_app_config.package_name}.{first_app_config.module_name}", "org.beeware.android.MainActivity", ["foo", "--bar"], - {}, ) run_command.tools.mock_adb.pidof.assert_called_once_with( @@ -753,8 +720,6 @@ def test_run_test_mode_created_emulator(run_command, first_app_config): # Invoke run_app run_command.run_app( first_app_config, - debugger_host=None, - debugger_port=None, passthrough=[], extra_emulator_args=["-no-window", "-no-audio"], shutdown_on_exit=True, @@ -788,7 +753,6 @@ def test_run_test_mode_created_emulator(run_command, first_app_config): f"{first_app_config.package_name}.{first_app_config.module_name}", "org.beeware.android.MainActivity", [], - {}, ) run_command.tools.mock_adb.logcat.assert_called_once_with(pid="777") @@ -804,236 +768,3 @@ def test_run_test_mode_created_emulator(run_command, first_app_config): # The emulator was killed at the end of the test run_command.tools.mock_adb.kill.assert_called_once_with() - - -def test_run_debugger(run_command, first_app_config, tmp_path, dummy_debugger): - """An app can be run in debug mode.""" - # Set up device selection to return a running physical device. - run_command.tools.android_sdk.select_target_device = mock.MagicMock( - return_value=("exampleDevice", "ExampleDevice", None) - ) - - # Set up the log streamer to return a known stream - log_popen = mock.MagicMock() - run_command.tools.mock_adb.logcat.return_value = log_popen - - # To satisfy coverage, the stop function must be invoked at least once - # when invoking stream_output. - def mock_stream_output(app, stop_func, **kwargs): - stop_func() - - run_command._stream_app_logs.side_effect = mock_stream_output - - # Set up app config to have a `-` in the `bundle`, to ensure it gets - # normalized into a `_` via `package_name`. - first_app_config.bundle = "com.ex-ample" - first_app_config.debugger = dummy_debugger - - # Invoke run_app with args. - run_command.run_app( - first_app_config, - device_or_avd="exampleDevice", - debugger_host="somehost", - debugger_port=9999, - passthrough=[], - ) - - # select_target_device was invoked with a specific device - run_command.tools.android_sdk.select_target_device.assert_called_once_with( - "exampleDevice" - ) - - # The ADB wrapper is created - run_command.tools.android_sdk.adb.assert_called_once_with(device="exampleDevice") - - # The adb wrapper is invoked with the expected arguments - run_command.tools.mock_adb.install_apk.assert_called_once_with( - run_command.binary_path(first_app_config) - ) - run_command.tools.mock_adb.force_stop_app.assert_called_once_with( - f"{first_app_config.package_name}.{first_app_config.module_name}", - ) - - run_command.tools.mock_adb.start_app.assert_called_once_with( - f"{first_app_config.package_name}.{first_app_config.module_name}", - "org.beeware.android.MainActivity", - [], - { - "BRIEFCASE_DEBUGGER": json.dumps( - { - "host": "somehost", - "port": 9999, - "app_path_mappings": { - "device_sys_path_regex": "app$", - "device_subfolders": ["first_app"], - "host_folders": [str(tmp_path / "base_path/src/first_app")], - }, - "app_packages_path_mappings": { - "sys_path_regex": "requirements$", - "host_folder": str( - tmp_path - / "base_path/build/first-app/android/gradle/app/build/python/pip/debug/common" - ), - }, - } - ) - }, - ) - - run_command.tools.mock_adb.pidof.assert_called_once_with( - f"{first_app_config.package_name}.{first_app_config.module_name}", - quiet=2, - ) - run_command.tools.mock_adb.logcat.assert_called_once_with(pid="777") - - run_command._stream_app_logs.assert_called_once_with( - first_app_config, - popen=log_popen, - clean_filter=android_log_clean_filter, - clean_output=False, - stop_func=mock.ANY, - log_stream=True, - ) - - # The emulator was not killed at the end of the test - run_command.tools.mock_adb.kill.assert_not_called() - - -class ServerDebugger(BaseDebugger): - @property - def connection_mode(self) -> DebuggerConnectionMode: - return DebuggerConnectionMode.SERVER - - def create_debugger_support_pkg(self, dir: Path) -> None: - raise NotImplementedError - - -class ClientDebugger(BaseDebugger): - @property - def connection_mode(self) -> DebuggerConnectionMode: - return DebuggerConnectionMode.CLIENT - - def create_debugger_support_pkg(self, dir: Path) -> None: - raise NotImplementedError - - -@pytest.mark.parametrize( - "debugger", - [ - ServerDebugger(), - ClientDebugger(), - ], -) -def test_run_debugger_mode_localhost(run_command, first_app_config, tmp_path, debugger): - """An app can be run in debug mode.""" - run_command.console.verbosity = LogLevel.DEBUG - - # Set up device selection to return a running physical device. - run_command.tools.android_sdk.select_target_device = mock.MagicMock( - return_value=("exampleDevice", "ExampleDevice", None) - ) - - # Set up the log streamer to return a known stream - log_popen = mock.MagicMock() - run_command.tools.mock_adb.logcat.return_value = log_popen - - # To satisfy coverage, the stop function must be invoked at least once - # when invoking stream_output. - def mock_stream_output(app, stop_func, **kwargs): - stop_func() - - run_command._stream_app_logs.side_effect = mock_stream_output - - # Set up app config to have a `-` in the `bundle`, to ensure it gets - # normalized into a `_` via `package_name`. - first_app_config.bundle = "com.ex-ample" - - # Set up the debugger - first_app_config.debugger = debugger - - # Invoke run_app with args. - run_command.run_app( - first_app_config, - device_or_avd="exampleDevice", - debugger_host="localhost", - debugger_port=9999, - passthrough=[], - ) - - # select_target_device was invoked with a specific device - run_command.tools.android_sdk.select_target_device.assert_called_once_with( - "exampleDevice" - ) - - # The ADB wrapper is created - run_command.tools.android_sdk.adb.assert_called_once_with(device="exampleDevice") - - # The adb wrapper is invoked with the expected arguments - run_command.tools.mock_adb.install_apk.assert_called_once_with( - run_command.binary_path(first_app_config) - ) - run_command.tools.mock_adb.force_stop_app.assert_called_once_with( - f"{first_app_config.package_name}.{first_app_config.module_name}", - ) - - run_command.tools.mock_adb.start_app.assert_called_once_with( - f"{first_app_config.package_name}.{first_app_config.module_name}", - "org.beeware.android.MainActivity", - [], - { - "BRIEFCASE_DEBUG": "1", - "BRIEFCASE_DEBUGGER": json.dumps( - { - "host": "localhost", - "port": 9999, - "app_path_mappings": { - "device_sys_path_regex": "app$", - "device_subfolders": ["first_app"], - "host_folders": [str(tmp_path / "base_path/src/first_app")], - }, - "app_packages_path_mappings": { - "sys_path_regex": "requirements$", - "host_folder": str( - tmp_path - / "base_path/build/first-app/android/gradle/app/build/python/pip/debug/common" - ), - }, - } - ), - }, - ) - - run_command.tools.mock_adb.pidof.assert_called_once_with( - f"{first_app_config.package_name}.{first_app_config.module_name}", - quiet=2, - ) - run_command.tools.mock_adb.logcat.assert_called_once_with(pid="777") - - if isinstance(debugger, ServerDebugger): - run_command.tools.mock_adb.forward.assert_called_once_with( - 9999, - 9999, - ) - run_command.tools.mock_adb.forward_remove.assert_called_once_with( - 9999, - ) - elif isinstance(debugger, ClientDebugger): - run_command.tools.mock_adb.reverse.assert_called_once_with( - 9999, - 9999, - ) - run_command.tools.mock_adb.reverse_remove.assert_called_once_with( - 9999, - ) - - run_command._stream_app_logs.assert_called_once_with( - first_app_config, - popen=log_popen, - clean_filter=android_log_clean_filter, - clean_output=False, - stop_func=mock.ANY, - log_stream=True, - ) - - # The emulator was not killed at the end of the test - run_command.tools.mock_adb.kill.assert_not_called() diff --git a/tests/platforms/conftest.py b/tests/platforms/conftest.py index 36dd139bd..4b975e68b 100644 --- a/tests/platforms/conftest.py +++ b/tests/platforms/conftest.py @@ -1,9 +1,6 @@ -from pathlib import Path - import pytest from briefcase.config import AppConfig -from briefcase.debuggers.base import BaseDebugger, DebuggerConnectionMode @pytest.fixture @@ -57,18 +54,3 @@ def underscore_app_config(first_app_config): requires=["foo==1.2.3", "bar>=4.5"], test_requires=["pytest"], ) - - -class DummyDebugger(BaseDebugger): - @property - def connection_mode(self) -> DebuggerConnectionMode: - raise NotImplementedError - - def create_debugger_support_pkg(self, dir: Path) -> None: - raise NotImplementedError - - -@pytest.fixture -def dummy_debugger(): - """A dummy debugger for testing purposes.""" - return DummyDebugger() diff --git a/tests/platforms/iOS/xcode/test_run.py b/tests/platforms/iOS/xcode/test_run.py index 21e5d1bb0..755613e77 100644 --- a/tests/platforms/iOS/xcode/test_run.py +++ b/tests/platforms/iOS/xcode/test_run.py @@ -1,4 +1,3 @@ -import json import subprocess import time from unittest import mock @@ -47,9 +46,6 @@ def test_device_option(run_command): "update_stub": False, "no_update": False, "test_mode": False, - "debugger": None, - "debugger_host": "localhost", - "debugger_port": 5678, "passthrough": [], "appname": None, } @@ -76,9 +72,7 @@ def test_run_multiple_devices_input_disabled(run_command, first_app_config): BriefcaseCommandError, match=r"Input has been disabled; can't select a device to target.", ): - run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app_config, passthrough=[]) @pytest.mark.usefixtures("sleep_zero") @@ -112,9 +106,7 @@ def test_run_app_simulator_booted(run_command, first_app_config, tmp_path): ] # Run the app - run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app_config, passthrough=[]) # The correct sequence of commands was issued. run_command.tools.subprocess.run.assert_has_calls( @@ -245,9 +237,7 @@ def test_run_app_simulator_booted_underscore( ] # Run the app - run_command.run_app( - underscore_app_config, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(underscore_app_config, passthrough=[]) # slept 4 times for uninstall/install and 1 time for log stream start assert time.sleep.call_count == 4 + 1 @@ -372,8 +362,6 @@ def test_run_app_with_passthrough(run_command, first_app_config, tmp_path): # Run the app with passthrough args. run_command.run_app( first_app_config, - debugger_host=None, - debugger_port=None, passthrough=["foo", "--bar"], ) @@ -506,9 +494,7 @@ def test_run_app_simulator_shut_down( ] # Run the app - run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app_config, passthrough=[]) # slept 4 times for uninstall/install and 1 time for log stream start assert time.sleep.call_count == 4 + 1 @@ -649,9 +635,7 @@ def test_run_app_simulator_shutting_down(run_command, first_app_config, tmp_path ] # Run the app - run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app_config, passthrough=[]) # We should have slept 4 times for shutting down and 4 time for uninstall/install assert time.sleep.call_count == 4 + 4 @@ -767,9 +751,7 @@ def test_run_app_simulator_boot_failure(run_command, first_app_config): # Run the app with pytest.raises(BriefcaseCommandError): - run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app_config, passthrough=[]) # No sleeps assert time.sleep.call_count == 0 @@ -813,9 +795,7 @@ def test_run_app_simulator_open_failure(run_command, first_app_config): # Run the app with pytest.raises(BriefcaseCommandError): - run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app_config, passthrough=[]) # No sleeps assert time.sleep.call_count == 0 @@ -866,9 +846,7 @@ def test_run_app_simulator_uninstall_failure(run_command, first_app_config): # Run the app with pytest.raises(BriefcaseCommandError): - run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app_config, passthrough=[]) # Sleep twice for uninstall failure assert time.sleep.call_count == 2 @@ -940,9 +918,7 @@ def test_run_app_simulator_install_failure(run_command, first_app_config, tmp_pa # Run the app with pytest.raises(BriefcaseCommandError): - run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app_config, passthrough=[]) # Sleep twice for uninstall and twice for install failure assert time.sleep.call_count == 4 @@ -1035,9 +1011,7 @@ def test_run_app_simulator_launch_failure(run_command, first_app_config, tmp_pat # Run the app with pytest.raises(BriefcaseCommandError): - run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app_config, passthrough=[]) # Sleep four times for uninstall/install and once for log stream start assert time.sleep.call_count == 4 + 1 @@ -1158,9 +1132,7 @@ def test_run_app_simulator_no_pid(run_command, first_app_config, tmp_path): # Run the app with pytest.raises(BriefcaseCommandError): - run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app_config, passthrough=[]) # Sleep four times for uninstall/install and once for log stream start assert time.sleep.call_count == 4 + 1 @@ -1283,9 +1255,7 @@ def test_run_app_simulator_non_integer_pid(run_command, first_app_config, tmp_pa # Run the app with pytest.raises(BriefcaseCommandError): - run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app_config, passthrough=[]) # Sleep four times for uninstall/install and once for log stream start assert time.sleep.call_count == 4 + 1 @@ -1409,9 +1379,7 @@ def test_run_app_test_mode(run_command, first_app_config, tmp_path): ] # Run the app - run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app_config, passthrough=[]) # Sleep four times for uninstall/install and once for log stream start assert time.sleep.call_count == 4 + 1 @@ -1524,8 +1492,6 @@ def test_run_app_test_mode_with_passthrough(run_command, first_app_config, tmp_p # Run the app with args. run_command.run_app( first_app_config, - debugger_host=None, - debugger_port=None, passthrough=["foo", "--bar"], ) @@ -1605,165 +1571,3 @@ def test_run_app_test_mode_with_passthrough(run_command, first_app_config, tmp_p stop_func=mock.ANY, log_stream=True, ) - - -@pytest.mark.usefixtures("sleep_zero") -def test_run_app_debugger(run_command, first_app_generated, tmp_path, dummy_debugger): - """An iOS App can be started in debug mode.""" - # A valid target device will be selected. - run_command.select_target_device = mock.MagicMock( - return_value=("2D3503A3-6EB9-4B37-9B17-C7EFEF2FA32D", "13.2", "iPhone 11") - ) - - # Simulator is already booted - run_command.get_device_state = mock.MagicMock(return_value=DeviceState.BOOTED) - - # Mock a process ID for the app - run_command.tools.subprocess.check_output.return_value = ( - "com.example.first-app: 1234\n" - ) - - # Mock the uninstall, install, and log stream Popen processes - uninstall_popen = mock.MagicMock(spec_set=subprocess.Popen) - uninstall_popen.__enter__.return_value = uninstall_popen - uninstall_popen.poll.side_effect = [None, None, 0] - install_popen = mock.MagicMock(spec_set=subprocess.Popen) - install_popen.__enter__.return_value = install_popen - install_popen.poll.side_effect = [None, None, 0] - log_stream_process = mock.MagicMock(spec_set=subprocess.Popen) - run_command.tools.subprocess.Popen.side_effect = [ - uninstall_popen, - install_popen, - log_stream_process, - ] - - first_app_generated.debugger = dummy_debugger - - # Run the app - run_command.run_app( - first_app_generated, - debugger_host="somehost", - debugger_port=9999, - passthrough=[], - ) - - # Sleep four times for uninstall/install and once for log stream start - assert time.sleep.call_count == 4 + 1 - - # Set the environment variables for the debugger and launch the app - run_command.tools.subprocess.check_output.assert_has_calls( - [ - # Set the environment variables for the debugger - mock.call( - [ - "xcrun", - "simctl", - "spawn", - "2D3503A3-6EB9-4B37-9B17-C7EFEF2FA32D", - "launchctl", - "setenv", - "BRIEFCASE_DEBUGGER", - json.dumps( - { - "host": "somehost", - "port": 9999, - "app_path_mappings": { - "device_sys_path_regex": "app$", - "device_subfolders": ["first_app"], - "host_folders": [ - str(tmp_path / "base_path/src/first_app") - ], - }, - "app_packages_path_mappings": { - "sys_path_regex": "app_packages$", - "host_folder": str( - tmp_path - / "base_path/build/first-app/ios/xcode/app_packages.iphonesimulator" - ), - }, - } - ), - ], - ), - # Launch the new app - mock.call( - [ - "xcrun", - "simctl", - "launch", - "2D3503A3-6EB9-4B37-9B17-C7EFEF2FA32D", - "com.example.first-app", - ], - ), - # Remove the environment variables for the debugger - mock.call( - [ - "xcrun", - "simctl", - "spawn", - "2D3503A3-6EB9-4B37-9B17-C7EFEF2FA32D", - "launchctl", - "unsetenv", - "BRIEFCASE_DEBUGGER", - ], - ), - ] - ) - - # Start the uninstall, install, and log stream - run_command.tools.subprocess.Popen.assert_has_calls( - [ - # Uninstall the old app - mock.call( - [ - "xcrun", - "simctl", - "uninstall", - "2D3503A3-6EB9-4B37-9B17-C7EFEF2FA32D", - "com.example.first-app", - ], - ), - # Install the new app - mock.call( - [ - "xcrun", - "simctl", - "install", - "2D3503A3-6EB9-4B37-9B17-C7EFEF2FA32D", - tmp_path - / "base_path/build/first-app/ios/xcode/build/Debug-iphonesimulator/First App.app", - ], - ), - mock.call( - [ - "xcrun", - "simctl", - "spawn", - "2D3503A3-6EB9-4B37-9B17-C7EFEF2FA32D", - "log", - "stream", - "--style", - "compact", - "--predicate", - 'senderImagePath ENDSWITH "/First App"' - ' OR (processImagePath ENDSWITH "/First App"' - ' AND (senderImagePath ENDSWITH "-iphonesimulator.so"' - ' OR senderImagePath ENDSWITH "-iphonesimulator.dylib"' - ' OR senderImagePath ENDSWITH "_ctypes.framework/_ctypes"))', - ], - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - bufsize=1, - ), - ] - ) - - # Log stream monitoring was started - run_command._stream_app_logs.assert_called_with( - first_app_generated, - popen=log_stream_process, - clean_filter=macOS_log_clean_filter, - clean_output=True, - stop_func=mock.ANY, - log_stream=True, - ) diff --git a/tests/platforms/linux/appimage/test_run.py b/tests/platforms/linux/appimage/test_run.py index d1ebb464a..e208686df 100644 --- a/tests/platforms/linux/appimage/test_run.py +++ b/tests/platforms/linux/appimage/test_run.py @@ -1,4 +1,3 @@ -import json import subprocess from unittest import mock @@ -50,9 +49,7 @@ def test_run_gui_app(run_command, first_app_config, tmp_path): run_command.tools.subprocess.Popen.return_value = log_popen # Run the app - run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app_config, passthrough=[]) # The process was started run_command.tools.subprocess.Popen.assert_called_with( @@ -85,8 +82,6 @@ def test_run_gui_app_with_passthrough(run_command, first_app_config, tmp_path): # Run the app with args run_command.run_app( first_app_config, - debugger_host=None, - debugger_port=None, passthrough=["foo", "--bar"], ) @@ -118,9 +113,7 @@ def test_run_gui_app_failed(run_command, first_app_config, tmp_path): run_command.tools.subprocess.Popen.side_effect = OSError with pytest.raises(OSError): - run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app_config, passthrough=[]) # The run command was still invoked run_command.tools.subprocess.Popen.assert_called_with( @@ -143,9 +136,7 @@ def test_run_console_app(run_command, first_app_config, tmp_path): first_app_config.console_app = True # Run the app - run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app_config, passthrough=[]) # The process was started run_command.tools.subprocess.run.assert_called_with( @@ -171,8 +162,6 @@ def test_run_console_app_with_passthrough(run_command, first_app_config, tmp_pat # Run the app with args run_command.run_app( first_app_config, - debugger_host=None, - debugger_port=None, passthrough=["foo", "--bar"], ) @@ -201,9 +190,7 @@ def test_run_console_app_failed(run_command, first_app_config, tmp_path): run_command.tools.subprocess.run.side_effect = OSError with pytest.raises(OSError): - run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app_config, passthrough=[]) # The run command was still invoked run_command.tools.subprocess.run.assert_called_with( @@ -232,9 +219,7 @@ def test_run_app_test_mode(run_command, first_app_config, is_console_app, tmp_pa run_command.tools.subprocess.Popen.return_value = log_popen # Run the app - run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app_config, passthrough=[]) # The process was started run_command.tools.subprocess.Popen.assert_called_with( @@ -276,8 +261,6 @@ def test_run_app_test_mode_with_args( # Run the app with args run_command.run_app( first_app_config, - debugger_host=None, - debugger_port=None, passthrough=["foo", "--bar"], ) @@ -302,53 +285,3 @@ def test_run_app_test_mode_with_args( popen=log_popen, clean_output=False, ) - - -def test_run_app_debugger(run_command, first_app_config, tmp_path, dummy_debugger): - """A linux App can be started in debug mode.""" - # Set up the log streamer to return a known stream - log_popen = mock.MagicMock() - run_command.tools.subprocess.Popen.return_value = log_popen - - first_app_config.debugger = dummy_debugger - - # Run the app - run_command.run_app( - first_app_config, - debugger_host="somehost", - debugger_port=9999, - passthrough=[], - ) - - # The process was started - run_command.tools.subprocess.Popen.assert_called_with( - [ - tmp_path - / "base_path/build/first-app/linux/appimage/First_App-0.0.1-x86_64.AppImage" - ], - cwd=tmp_path / "home", - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - bufsize=1, - env={ - "BRIEFCASE_DEBUGGER": json.dumps( - { - "host": "somehost", - "port": 9999, - "app_path_mappings": { - "device_sys_path_regex": "app$", - "device_subfolders": ["first_app"], - "host_folders": [str(tmp_path / "base_path/src/first_app")], - }, - "app_packages_path_mappings": None, - } - ) - }, - ) - - # The streamer was started - run_command._stream_app_logs.assert_called_once_with( - first_app_config, - popen=log_popen, - clean_output=False, - ) diff --git a/tests/platforms/linux/flatpak/test_run.py b/tests/platforms/linux/flatpak/test_run.py index 6e3c7a508..bfb5a0e1b 100644 --- a/tests/platforms/linux/flatpak/test_run.py +++ b/tests/platforms/linux/flatpak/test_run.py @@ -1,4 +1,3 @@ -import json from unittest import mock import pytest @@ -31,9 +30,7 @@ def test_run_gui_app(run_command, first_app_config): run_command.tools.flatpak.run.return_value = log_popen # Run the app - run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app_config, passthrough=[]) # App is executed run_command.tools.flatpak.run.assert_called_once_with( @@ -61,8 +58,6 @@ def test_run_gui_app_with_passthrough(run_command, first_app_config): # Run the app with args run_command.run_app( first_app_config, - debugger_host=None, - debugger_port=None, passthrough=["foo", "--bar"], ) @@ -87,9 +82,7 @@ def test_run_gui_app_failed(run_command, first_app_config, tmp_path): run_command.tools.flatpak.run.side_effect = OSError with pytest.raises(OSError): - run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app_config, passthrough=[]) # The run command was still invoked run_command.tools.flatpak.run.assert_called_once_with( @@ -107,9 +100,7 @@ def test_run_console_app(run_command, first_app_config): first_app_config.console_app = True # Run the app - run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app_config, passthrough=[]) # App is executed run_command.tools.flatpak.run.assert_called_once_with( @@ -130,8 +121,6 @@ def test_run_console_app_with_passthrough(run_command, first_app_config): # Run the app with args run_command.run_app( first_app_config, - debugger_host=None, - debugger_port=None, passthrough=["foo", "--bar"], ) @@ -154,9 +143,7 @@ def test_run_console_app_failed(run_command, first_app_config, tmp_path): run_command.tools.flatpak.run.side_effect = OSError with pytest.raises(OSError): - run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app_config, passthrough=[]) # The run command was still invoked run_command.tools.flatpak.run.assert_called_once_with( @@ -181,9 +168,7 @@ def test_run_test_mode(run_command, first_app_config, is_console_app): run_command.tools.flatpak.run.return_value = log_popen # Run the app - run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app_config, passthrough=[]) # App is executed run_command.tools.flatpak.run.assert_called_once_with( @@ -215,8 +200,6 @@ def test_run_test_mode_with_args(run_command, first_app_config, is_console_app): # Run the app with args run_command.run_app( first_app_config, - debugger_host=None, - debugger_port=None, passthrough=["foo", "--bar"], ) @@ -234,54 +217,3 @@ def test_run_test_mode_with_args(run_command, first_app_config, is_console_app): popen=log_popen, clean_output=False, ) - - -def test_run_debugger(run_command, first_app_config, tmp_path, dummy_debugger): - """A flatpak can be executed in debug mode.""" - # Set up the log streamer to return a known stream and a good return code - log_popen = mock.MagicMock() - run_command.tools.flatpak.run.return_value = log_popen - - first_app_config.debugger = dummy_debugger - - # Run the app - run_command.run_app( - first_app_config, - debugger_host="somehost", - debugger_port=9999, - passthrough=[], - ) - - # App is executed - run_command.tools.flatpak.run.assert_called_once_with( - bundle_identifier="com.example.first-app", - args=[], - stream_output=True, - env={ - "BRIEFCASE_DEBUGGER": json.dumps( - { - "host": "somehost", - "port": 9999, - "app_path_mappings": { - "device_sys_path_regex": "app$", - "device_subfolders": ["first_app"], - "host_folders": [str(tmp_path / "base_path/src/first_app")], - }, - "app_packages_path_mappings": { - "sys_path_regex": "app_packages$", - "host_folder": str( - tmp_path - / "base_path/build/first-app/linux/flatpak/build/files/briefcase/app_packages" - ), - }, - } - ) - }, - ) - - # The streamer was started - run_command._stream_app_logs.assert_called_once_with( - first_app_config, - popen=log_popen, - clean_output=False, - ) diff --git a/tests/platforms/linux/system/conftest.py b/tests/platforms/linux/system/conftest.py index d2218c817..3d23e7e61 100644 --- a/tests/platforms/linux/system/conftest.py +++ b/tests/platforms/linux/system/conftest.py @@ -34,14 +34,6 @@ def first_app(first_app_config, tmp_path): bundle_dir = tmp_path / "base_path/build/first-app/somevendor/surprising" create_file(bundle_dir / "first-app.1", "First App manpage") - create_file( - bundle_dir / "briefcase.toml", - """ -[paths] -app_path = "first-app-0.0.1/usr/lib/first-app/app" -app_packages_path = "first-app-0.0.1/usr/lib/first-app/app_packages" -""", - ) lib_dir = bundle_dir / "first-app-0.0.1/usr/lib/first-app" (lib_dir / "app").mkdir(parents=True, exist_ok=True) diff --git a/tests/platforms/linux/system/test_run.py b/tests/platforms/linux/system/test_run.py index 91cd46f04..a8f6f87a0 100644 --- a/tests/platforms/linux/system/test_run.py +++ b/tests/platforms/linux/system/test_run.py @@ -1,4 +1,3 @@ -import json import os import subprocess import sys @@ -244,9 +243,7 @@ def test_run_gui_app(run_command, first_app, sub_kw, tmp_path): ) # Run the app - run_command.run_app( - first_app, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app, passthrough=[]) # The process was started run_command.tools.subprocess._subprocess.Popen.assert_called_with( @@ -285,9 +282,7 @@ def test_run_gui_app_passthrough(run_command, first_app, sub_kw, tmp_path): ) # Run the app - run_command.run_app( - first_app, debugger_host=None, debugger_port=None, passthrough=["foo", "--bar"] - ) + run_command.run_app(first_app, passthrough=["foo", "--bar"]) # The process was started run_command.tools.subprocess._subprocess.Popen.assert_called_with( @@ -328,9 +323,7 @@ def test_run_gui_app_failed(run_command, first_app, sub_kw, tmp_path): run_command.tools.subprocess._subprocess.Popen.side_effect = OSError with pytest.raises(OSError): - run_command.run_app( - first_app, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app, passthrough=[]) # The run command was still invoked run_command.tools.subprocess._subprocess.Popen.assert_called_with( @@ -351,78 +344,6 @@ def test_run_gui_app_failed(run_command, first_app, sub_kw, tmp_path): run_command._stream_app_logs.assert_not_called() -def test_run_gui_app_debugger( - run_command, first_app, sub_kw, tmp_path, monkeypatch, dummy_debugger -): - """A bootstrap binary for a GUI app can be started in debug mode.""" - - # Set up tool cache - run_command.verify_app_tools(app=first_app) - - # Set up the log streamer to return a known stream - log_popen = mock.MagicMock() - run_command.tools.subprocess._subprocess.Popen = mock.MagicMock( - return_value=log_popen - ) - - # Mock out the environment - monkeypatch.setattr(run_command.tools.os, "environ", {"ENVVAR": "Value"}) - - first_app.debugger = dummy_debugger - - # Run the app - run_command.run_app( - first_app, - debugger_host="somehost", - debugger_port=9999, - passthrough=[], - ) - - # The process was started - run_command.tools.subprocess._subprocess.Popen.assert_called_with( - [ - os.fsdecode( - tmp_path - / "base_path/build/first-app/somevendor/surprising/first-app-0.0.1/usr/bin/first-app" - ) - ], - cwd=os.fsdecode(tmp_path / "home"), - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - bufsize=1, - env={ - "ENVVAR": "Value", - "BRIEFCASE_DEBUGGER": json.dumps( - { - "host": "somehost", - "port": 9999, - "app_path_mappings": { - "device_sys_path_regex": "app$", - "device_subfolders": ["first_app"], - "host_folders": [str(tmp_path / "base_path/src/first_app")], - }, - "app_packages_path_mappings": { - "sys_path_regex": "app_packages$", - "host_folder": str( - tmp_path - / "base_path/build/first-app/somevendor/surprising/first-app-0.0.1/usr/" - "lib/first-app/app_packages" - ), - }, - } - ), - }, - **sub_kw, - ) - - # The streamer was started - run_command._stream_app_logs.assert_called_once_with( - first_app, - popen=log_popen, - clean_output=False, - ) - - def test_run_console_app(run_command, first_app, tmp_path): """A bootstrap binary for a console app can be started.""" first_app.console_app = True @@ -431,9 +352,7 @@ def test_run_console_app(run_command, first_app, tmp_path): run_command.verify_app_tools(app=first_app) # Run the app - run_command.run_app( - first_app, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app, passthrough=[]) # The process was started assert run_command.tools.subprocess.run.mock_calls == [ @@ -462,9 +381,7 @@ def test_run_console_app_passthrough(run_command, first_app, tmp_path): run_command.verify_app_tools(app=first_app) # Run the app - run_command.run_app( - first_app, debugger_host=None, debugger_port=None, passthrough=["foo", "--bar"] - ) + run_command.run_app(first_app, passthrough=["foo", "--bar"]) # The process was started assert run_command.tools.subprocess.run.mock_calls == [ @@ -496,9 +413,7 @@ def test_run_console_app_failed(run_command, first_app, sub_kw, tmp_path): run_command.tools.subprocess.run.side_effect = OSError with pytest.raises(OSError): - run_command.run_app( - first_app, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app, passthrough=[]) # The run command was still invoked assert run_command.tools.subprocess.run.mock_calls == [ @@ -540,9 +455,7 @@ def test_run_app_docker(run_command, first_app, sub_kw, tmp_path, monkeypatch): ) # Run the app - run_command.run_app( - first_app, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app, passthrough=[]) # The process was started run_command.tools.subprocess._subprocess.Popen.assert_called_with( @@ -602,9 +515,7 @@ def test_run_app_failed_docker(run_command, first_app, sub_kw, tmp_path, monkeyp run_command.tools.subprocess._subprocess.Popen.side_effect = OSError with pytest.raises(OSError): - run_command.run_app( - first_app, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app, passthrough=[]) # The run command was still invoked run_command.tools.subprocess._subprocess.Popen.assert_called_with( @@ -665,9 +576,7 @@ def test_run_app_test_mode( monkeypatch.setattr(run_command.tools.os, "environ", {"ENVVAR": "Value"}) # Run the app - run_command.run_app( - first_app, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app, passthrough=[]) # The process was started run_command.tools.subprocess._subprocess.Popen.assert_called_with( @@ -726,9 +635,7 @@ def test_run_app_test_mode_docker( ) # Run the app - run_command.run_app( - first_app, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app, passthrough=[]) # The process was started run_command.tools.subprocess._subprocess.Popen.assert_called_with( @@ -797,8 +704,6 @@ def test_run_app_test_mode_with_args( # Run the app with args run_command.run_app( first_app, - debugger_host=None, - debugger_port=None, passthrough=["foo", "--bar"], ) @@ -863,8 +768,6 @@ def test_run_app_test_mode_with_args_docker( # Run the app with args run_command.run_app( first_app, - debugger_host=None, - debugger_port=None, passthrough=["foo", "--bar"], ) diff --git a/tests/platforms/macOS/app/test_run.py b/tests/platforms/macOS/app/test_run.py index 5061ecedb..7ae0413ec 100644 --- a/tests/platforms/macOS/app/test_run.py +++ b/tests/platforms/macOS/app/test_run.py @@ -44,9 +44,7 @@ def test_run_gui_app(run_command, first_app_config, sleep_zero, tmp_path, monkey "briefcase.platforms.macOS.get_process_id_by_command", lambda *a, **kw: 100 ) - run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app_config, passthrough=[]) # Calls were made to start the app and to start a log stream. bin_path = run_command.binary_path(first_app_config) @@ -108,8 +106,6 @@ def test_run_gui_app_with_passthrough( # Run the app with args run_command.run_app( first_app_config, - debugger_host=None, - debugger_port=None, passthrough=["foo", "--bar"], ) @@ -161,9 +157,7 @@ def test_run_gui_app_failed(run_command, first_app_config, sleep_zero, tmp_path) ) with pytest.raises(BriefcaseCommandError): - run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app_config, passthrough=[]) # Calls were made to start the app and to start a log stream. bin_path = run_command.binary_path(first_app_config) @@ -210,9 +204,7 @@ def test_run_gui_app_find_pid_failed( ) with pytest.raises(BriefcaseCommandError) as exc_info: - run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app_config, passthrough=[]) # Calls were made to start the app and to start a log stream. bin_path = run_command.binary_path(first_app_config) @@ -265,9 +257,7 @@ def test_run_gui_app_test_mode( "briefcase.platforms.macOS.get_process_id_by_command", lambda *a, **kw: 100 ) - run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app_config, passthrough=[]) # Calls were made to start the app and to start a log stream. bin_path = run_command.binary_path(first_app_config) @@ -313,9 +303,7 @@ def test_run_console_app(run_command, first_app_config, tmp_path): # Set the app to be a console app first_app_config.console_app = True - run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app_config, passthrough=[]) # Calls were made to start the app and to start a log stream. bin_path = run_command.binary_path(first_app_config) @@ -344,8 +332,6 @@ def test_run_console_app_with_passthrough( # Run the app with args run_command.run_app( first_app_config, - debugger_host=None, - debugger_port=None, passthrough=["foo", "--bar"], ) @@ -372,9 +358,7 @@ def test_run_console_app_test_mode(run_command, first_app_config, sleep_zero, tm app_process = mock.MagicMock(spec_set=subprocess.Popen) run_command.tools.subprocess.Popen.return_value = app_process - run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app_config, passthrough=[]) # Calls were made to start the app and to start a log stream. bin_path = run_command.binary_path(first_app_config) @@ -411,12 +395,7 @@ def test_run_console_app_test_mode_with_passthrough( app_process = mock.MagicMock(spec_set=subprocess.Popen) run_command.tools.subprocess.Popen.return_value = app_process - run_command.run_app( - first_app_config, - debugger_host=None, - debugger_port=None, - passthrough=["foo", "--bar"], - ) + run_command.run_app(first_app_config, passthrough=["foo", "--bar"]) # Calls were made to start the app and to start a log stream. bin_path = run_command.binary_path(first_app_config) @@ -449,9 +428,7 @@ def test_run_console_app_failed(run_command, first_app_config, sleep_zero, tmp_p # Although the command raises an error, this could be because the script itself # raised an error. - run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app_config, passthrough=[]) # Calls were made to start the app and to start a log stream. bin_path = run_command.binary_path(first_app_config) diff --git a/tests/platforms/macOS/xcode/test_run.py b/tests/platforms/macOS/xcode/test_run.py index 34d71e105..fc5e10b94 100644 --- a/tests/platforms/macOS/xcode/test_run.py +++ b/tests/platforms/macOS/xcode/test_run.py @@ -46,9 +46,7 @@ def test_run_app(run_command, first_app_config, sleep_zero, tmp_path, monkeypatc "briefcase.platforms.macOS.get_process_id_by_command", lambda *a, **kw: 100 ) - run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app_config, passthrough=[]) # Calls were made to start the app and to start a log stream. bin_path = run_command.binary_path(first_app_config) @@ -108,8 +106,6 @@ def test_run_app_with_passthrough( # Run the app with args run_command.run_app( first_app_config, - debugger_host=None, - debugger_port=None, passthrough=["foo", "--bar"], ) @@ -170,9 +166,7 @@ def test_run_app_test_mode( "briefcase.platforms.macOS.get_process_id_by_command", lambda *a, **kw: 100 ) - run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app_config, passthrough=[]) # Calls were made to start the app and to start a log stream. bin_path = run_command.binary_path(first_app_config) @@ -235,8 +229,6 @@ def test_run_app_test_mode_with_passthrough( # Run app in test mode with args run_command.run_app( first_app_config, - debugger_host=None, - debugger_port=None, passthrough=["foo", "--bar"], ) diff --git a/tests/platforms/web/static/test_run.py b/tests/platforms/web/static/test_run.py index 212f32bb5..aec751382 100644 --- a/tests/platforms/web/static/test_run.py +++ b/tests/platforms/web/static/test_run.py @@ -46,9 +46,6 @@ def test_default_options(run_command): "update_stub": False, "no_update": False, "test_mode": False, - "debugger": None, - "debugger_host": "localhost", - "debugger_port": 5678, "passthrough": [], "host": "localhost", "port": 8080, @@ -72,9 +69,6 @@ def test_options(run_command): "update_stub": False, "no_update": False, "test_mode": False, - "debugger": None, - "debugger_host": "localhost", - "debugger_port": 5678, "passthrough": [], "host": "myhost", "port": 1234, @@ -113,8 +107,6 @@ def test_run(monkeypatch, run_command, first_app_built): # Run the app run_command.run_app( first_app_built, - debugger_host=None, - debugger_port=None, passthrough=[], host="localhost", port=8080, @@ -179,8 +171,6 @@ def test_run_with_fallback_port( # Run the app run_command.run_app( first_app_built, - debugger_host=None, - debugger_port=None, passthrough=[], host="localhost", port=8080, @@ -234,8 +224,6 @@ def test_run_with_args(monkeypatch, run_command, first_app_built): # Run the app run_command.run_app( first_app_built, - debugger_host=None, - debugger_port=None, passthrough=["foo", "--bar"], host="localhost", port=8080, @@ -330,8 +318,6 @@ def test_cleanup_server_error( with pytest.raises(BriefcaseCommandError, match=message): run_command.run_app( first_app_built, - debugger_host=None, - debugger_port=None, passthrough=[], host=host, port=port, @@ -380,8 +366,6 @@ def test_cleanup_runtime_server_error(monkeypatch, run_command, first_app_built) with pytest.raises(ValueError): run_command.run_app( first_app_built, - debugger_host=None, - debugger_port=None, passthrough=[], host="localhost", port=8080, @@ -431,8 +415,6 @@ def test_run_without_browser(monkeypatch, run_command, first_app_built): # Run the app run_command.run_app( first_app_built, - debugger_host=None, - debugger_port=None, passthrough=[], host="localhost", port=8080, @@ -483,8 +465,6 @@ def test_run_autoselect_port(monkeypatch, run_command, first_app_built): # Run the app on an autoselected port run_command.run_app( first_app_built, - debugger_host=None, - debugger_port=None, passthrough=[], host="localhost", port=0, @@ -583,28 +563,6 @@ def test_test_mode(run_command, first_app_built): ): run_command.run_app( first_app_built, - debugger_host=None, - debugger_port=None, - passthrough=[], - host="localhost", - port=8080, - open_browser=True, - ) - - -def test_debugger(run_command, first_app_built, dummy_debugger): - """Debug mode raises an error (at least for now).""" - first_app_built.debugger = dummy_debugger - - # Run the app - with pytest.raises( - BriefcaseCommandError, - match=r"Briefcase can't run web apps with an debugger.", - ): - run_command.run_app( - first_app_built, - debugger_host="somehost", - debugger_port=9999, passthrough=[], host="localhost", port=8080, diff --git a/tests/platforms/windows/app/test_run.py b/tests/platforms/windows/app/test_run.py index 202e30ef7..94ef24e9b 100644 --- a/tests/platforms/windows/app/test_run.py +++ b/tests/platforms/windows/app/test_run.py @@ -1,4 +1,3 @@ -import json import subprocess from unittest import mock @@ -31,9 +30,7 @@ def test_run_gui_app(run_command, first_app_config, tmp_path): run_command.tools.subprocess.Popen.return_value = log_popen # Run the app - run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app_config, passthrough=[]) # The process was started run_command.tools.subprocess.Popen.assert_called_with( @@ -64,8 +61,6 @@ def test_run_gui_app_with_passthrough(run_command, first_app_config, tmp_path): # Run the app with args run_command.run_app( first_app_config, - debugger_host=None, - debugger_port=None, passthrough=["foo", "--bar"], ) @@ -98,9 +93,7 @@ def test_run_gui_app_failed(run_command, first_app_config, tmp_path): run_command.tools.subprocess.Popen.side_effect = OSError with pytest.raises(OSError): - run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app_config, passthrough=[]) # Popen was still invoked, though run_command.tools.subprocess.Popen.assert_called_with( @@ -125,9 +118,7 @@ def test_run_console_app(run_command, first_app_config, tmp_path): run_command.tools.subprocess.Popen.return_value = log_popen # Run the app - run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app_config, passthrough=[]) # The process was started run_command.tools.subprocess.run.assert_called_with( @@ -151,8 +142,6 @@ def test_run_console_app_with_passthrough(run_command, first_app_config, tmp_pat # Run the app with args run_command.run_app( first_app_config, - debugger_host=None, - debugger_port=None, passthrough=["foo", "--bar"], ) @@ -181,9 +170,7 @@ def test_run_console_app_failed(run_command, first_app_config, tmp_path): run_command.tools.subprocess.run.side_effect = OSError with pytest.raises(OSError): - run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app_config, passthrough=[]) # Popen was still invoked, though run_command.tools.subprocess.run.assert_called_with( @@ -210,9 +197,7 @@ def test_run_app_test_mode(run_command, first_app_config, is_console_app, tmp_pa run_command.tools.subprocess.Popen.return_value = log_popen # Run the app - run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app_config, passthrough=[]) # The process was started exe_name = "first-app" if is_console_app else "First App" @@ -253,8 +238,6 @@ def test_run_app_test_mode_with_passthrough( # Run the app with args run_command.run_app( first_app_config, - debugger_host=None, - debugger_port=None, passthrough=["foo", "--bar"], ) @@ -280,51 +263,3 @@ def test_run_app_test_mode_with_passthrough( popen=log_popen, clean_output=False, ) - - -def test_run_gui_app_debugger(run_command, first_app_config, tmp_path, dummy_debugger): - """A Windows app can be started in debug mode.""" - # Set up the log streamer to return a known stream - log_popen = mock.MagicMock() - run_command.tools.subprocess.Popen.return_value = log_popen - - first_app_config.debugger = dummy_debugger - - # Run the app - run_command.run_app( - first_app_config, - debugger_host="somehost", - debugger_port=9999, - passthrough=[], - ) - - # The process was started - run_command.tools.subprocess.Popen.assert_called_with( - [tmp_path / "base_path/build/first-app/windows/app/src/First App.exe"], - cwd=tmp_path / "home", - encoding="UTF-8", - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - bufsize=1, - env={ - "BRIEFCASE_DEBUGGER": json.dumps( - { - "host": "somehost", - "port": 9999, - "app_path_mappings": { - "device_sys_path_regex": "app$", - "device_subfolders": ["first_app"], - "host_folders": [str(tmp_path / "base_path/src/first_app")], - }, - "app_packages_path_mappings": None, - } - ) - }, - ) - - # The streamer was started - run_command._stream_app_logs.assert_called_once_with( - first_app_config, - popen=log_popen, - clean_output=False, - ) diff --git a/tests/platforms/windows/visualstudio/test_run.py b/tests/platforms/windows/visualstudio/test_run.py index 816bff0dc..2700e9191 100644 --- a/tests/platforms/windows/visualstudio/test_run.py +++ b/tests/platforms/windows/visualstudio/test_run.py @@ -1,7 +1,6 @@ # The run command inherits most of its behavior from the common base # implementation. Do a surface-level verification here, but the app # tests provide the actual test coverage. -import json import subprocess from unittest import mock @@ -35,9 +34,7 @@ def test_run_app(run_command, first_app_config, tmp_path): run_command.tools.subprocess.Popen.return_value = log_popen # Run the app - run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app_config, passthrough=[]) # Popen was called run_command.tools.subprocess.Popen.assert_called_with( @@ -70,8 +67,6 @@ def test_run_app_with_args(run_command, first_app_config, tmp_path): # Run the app with args run_command.run_app( first_app_config, - debugger_host=None, - debugger_port=None, passthrough=["foo", "--bar"], ) @@ -107,9 +102,7 @@ def test_run_app_test_mode(run_command, first_app_config, tmp_path): run_command.tools.subprocess.Popen.return_value = log_popen # Run the app in test mode - run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] - ) + run_command.run_app(first_app_config, passthrough=[]) # Popen was called run_command.tools.subprocess.Popen.assert_called_with( @@ -144,8 +137,6 @@ def test_run_app_test_mode_with_args(run_command, first_app_config, tmp_path): # Run the app with args run_command.run_app( first_app_config, - debugger_host=None, - debugger_port=None, passthrough=["foo", "--bar"], ) @@ -171,54 +162,3 @@ def test_run_app_test_mode_with_args(run_command, first_app_config, tmp_path): popen=log_popen, clean_output=False, ) - - -def test_run_app_debugger(run_command, first_app_config, tmp_path, dummy_debugger): - """A windows Visual Studio project app can be started in debug mode.""" - first_app_config.debugger = dummy_debugger - - # Set up the log streamer to return a known stream with a good returncode - log_popen = mock.MagicMock() - run_command.tools.subprocess.Popen.return_value = log_popen - - # Run the app in test mode - run_command.run_app( - first_app_config, - debugger_host="somehost", - debugger_port=9999, - passthrough=[], - ) - - # Popen was called - run_command.tools.subprocess.Popen.assert_called_with( - [ - tmp_path - / "base_path/build/first-app/windows/visualstudio/x64/Release/First App.exe" - ], - cwd=tmp_path / "home", - encoding="UTF-8", - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - bufsize=1, - env={ - "BRIEFCASE_DEBUGGER": json.dumps( - { - "host": "somehost", - "port": 9999, - "app_path_mappings": { - "device_sys_path_regex": "app$", - "device_subfolders": ["first_app"], - "host_folders": [str(tmp_path / "base_path/src/first_app")], - }, - "app_packages_path_mappings": None, - } - ) - }, - ) - - # The streamer was started - run_command._stream_app_logs.assert_called_once_with( - first_app_config, - popen=log_popen, - clean_output=False, - ) From 40b3e884520a7ea8bccd18d3084ee401e55d3400 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sun, 22 Jun 2025 12:07:08 +0200 Subject: [PATCH 051/131] fixed unit tests --- src/briefcase/commands/base.py | 5 - src/briefcase/commands/build.py | 4 +- src/briefcase/commands/run.py | 109 +++------------ src/briefcase/commands/update.py | 4 +- tests/commands/build/test_call.py | 18 ++- .../create/test_install_app_requirements.py | 63 ++++++++- tests/commands/run/test_call.py | 126 +++++++++++++----- tests/commands/update/test_call.py | 33 +++++ tests/debuggers/test_base.py | 23 +++- tests/test_cmdline.py | 3 - 10 files changed, 240 insertions(+), 148 deletions(-) diff --git a/src/briefcase/commands/base.py b/src/briefcase/commands/base.py index e7e1fab9e..a3d8d4cf3 100644 --- a/src/briefcase/commands/base.py +++ b/src/briefcase/commands/base.py @@ -673,11 +673,6 @@ def finalize_debugger(self, app: AppConfig, debugger_name: str | None = None): :param app: The app configuration to finalize. """ - if not self.supports_debugger and (debugger_name is not None): - raise BriefcaseCommandError( - f"The {self.command} command for the {self.platform} {self.output_format} format does not support debugging." - ) - if debugger_name and debugger_name != "": debugger = get_debugger(debugger_name) app.debugger = debugger diff --git a/src/briefcase/commands/build.py b/src/briefcase/commands/build.py index 8a7f393cb..766f4326b 100644 --- a/src/briefcase/commands/build.py +++ b/src/briefcase/commands/build.py @@ -15,7 +15,9 @@ class BuildCommand(BaseCommand): def add_options(self, parser): self._add_update_options(parser, context_label=" before building") self._add_test_options(parser, context_label="Build") - self._add_debug_options(parser, context_label="Build") + + if self.supports_debugger: + self._add_debug_options(parser, context_label="Build") parser.add_argument( "-a", diff --git a/src/briefcase/commands/run.py b/src/briefcase/commands/run.py index 9fac0f5d2..09a835f1a 100644 --- a/src/briefcase/commands/run.py +++ b/src/briefcase/commands/run.py @@ -1,18 +1,11 @@ from __future__ import annotations -import json import re import subprocess from abc import abstractmethod from contextlib import suppress -from pathlib import Path from briefcase.config import AppConfig -from briefcase.debuggers.base import ( - AppPackagesPathMappings, - AppPathMappings, - DebuggerConfig, -) from briefcase.exceptions import BriefcaseCommandError, BriefcaseTestSuiteFailure from briefcase.integrations.subprocess import StopStreaming @@ -224,93 +217,31 @@ def add_options(self, parser): self._add_update_options(parser, context_label=" before running") self._add_test_options(parser, context_label="Run") - self._add_debug_options(parser, context_label="Run") - parser.add_argument( - "--debugger-host", - default="localhost", - help="The host on which to run the debug server (default: localhost)", - required=False, - ) - parser.add_argument( - "-dp", - "--debugger-port", - default=5678, - type=int, - help="The port on which to run the debug server (default: 5678)", - required=False, - ) - - def remote_debugger_app_path_mappings(self, app: AppConfig) -> AppPathMappings: - """ - Get the path mappings for the app code. - - :param app: The config object for the app - :returns: The path mappings for the app code - """ - device_subfolders = [] - host_folders = [] - for src in app.all_sources(): - original = Path(self.base_path / src) - device_subfolders.append(original.name) - host_folders.append(f"{original.absolute()}") - return AppPathMappings( - device_sys_path_regex="app$", - device_subfolders=device_subfolders, - host_folders=host_folders, - ) - - def remote_debugger_app_packages_path_mapping( - self, app: AppConfig - ) -> AppPackagesPathMappings: - """ - Get the path mappings for the app packages. - - :param app: The config object for the app - :returns: The path mappings for the app packages - """ - app_packages_path = self.app_packages_path(app) - return AppPackagesPathMappings( - sys_path_regex="app_packages$", - host_folder=f"{app_packages_path}", - ) - - def remote_debugger_config( - self, - app: AppConfig, - debugger_host: str, - debugger_port: int, - ) -> str: - """ - Create the remote debugger configuration that should be saved as environment variable for this run. - - :param app: The app to be debugged - :returns: The remote debugger configuration - """ - app_path_mappings = self.remote_debugger_app_path_mappings(app) - app_packages_path_mappings = self.remote_debugger_app_packages_path_mapping(app) - config = DebuggerConfig( - host=debugger_host, - port=debugger_port, - app_path_mappings=app_path_mappings, - app_packages_path_mappings=app_packages_path_mappings, - ) - return json.dumps(config) + if self.supports_debugger: + self._add_debug_options(parser, context_label="Run") + parser.add_argument( + "--debugger-host", + default="localhost", + help="The host on which to run the debug server (default: localhost)", + required=False, + ) + parser.add_argument( + "-dp", + "--debugger-port", + default=5678, + type=int, + help="The port on which to run the debug server (default: 5678)", + required=False, + ) - def _prepare_app_kwargs( - self, - app: AppConfig, - debugger_host: str | None = None, - debugger_port: int | None = None, - ): + def _prepare_app_kwargs(self, app: AppConfig): """Prepare the kwargs for running an app as a log stream. This won't be used by every backend; but it's a sufficiently common default that it's been factored out. :param app: The app to be launched - :param debugger_host: The host on which to run the debug server - :param debugger_port: The port on which to run the debug server :returns: A dictionary of additional arguments to pass to the Popen """ args = {} @@ -320,12 +251,6 @@ def _prepare_app_kwargs( if self.console.is_debug: env["BRIEFCASE_DEBUG"] = "1" - # If we're in remote debug mode, save the remote debugger config - if app.debugger: - env["BRIEFCASE_DEBUGGER"] = self.remote_debugger_config( - app, debugger_host, debugger_port - ) - if app.test_mode: # In test mode, set a BRIEFCASE_MAIN_MODULE environment variable # to override the module at startup diff --git a/src/briefcase/commands/update.py b/src/briefcase/commands/update.py index b602b5bb0..6e20611d3 100644 --- a/src/briefcase/commands/update.py +++ b/src/briefcase/commands/update.py @@ -16,7 +16,9 @@ class UpdateCommand(CreateCommand): def add_options(self, parser): self._add_update_options(parser, update=False) self._add_test_options(parser, context_label="Update") - self._add_debug_options(parser, context_label="Update") + + if self.supports_debugger: + self._add_debug_options(parser, context_label="Update") parser.add_argument( "-a", diff --git a/tests/commands/build/test_call.py b/tests/commands/build/test_call.py index 52daf2000..ac77d7918 100644 --- a/tests/commands/build/test_call.py +++ b/tests/commands/build/test_call.py @@ -1039,13 +1039,16 @@ def test_build_test_update_stub(build_command, first_app, second_app): def test_build_debug(build_command, first_app, second_app): - """If the user builds a debug app, app is updated before build.""" + """The update command can be called with debug option.""" # Add two apps build_command.apps = { "first": first_app, "second": second_app, } + # Emulate debugger support + build_command.supports_debugger = True + # Configure command line options options, _ = build_command.parse_options(["--debug=pdb"]) @@ -1117,6 +1120,19 @@ def test_build_debug(build_command, first_app, second_app): ] +def test_build_debug_unsupported(build_command, first_app, second_app): + """If the user requests a build with update and no-update, an error is raised.""" + # Add two apps + build_command.apps = { + "first": first_app, + "second": second_app, + } + + # Configure command line options + with pytest.raises(SystemExit): + options, _ = build_command.parse_options(["--debug=pdb"]) + + def test_build_invalid_update(build_command, first_app, second_app): """If the user requests a build with update and no-update, an error is raised.""" # Add two apps diff --git a/tests/commands/create/test_install_app_requirements.py b/tests/commands/create/test_install_app_requirements.py index 9dfb4b99c..3a78c784e 100644 --- a/tests/commands/create/test_install_app_requirements.py +++ b/tests/commands/create/test_install_app_requirements.py @@ -1098,11 +1098,63 @@ def connection_mode(self) -> DebuggerConnectionMode: def create_debugger_support_pkg(self, dir: Path) -> None: self.debugger_support_pkg_dir = dir + (dir / "dummy.py").write_text("# Dummy", encoding="utf8") -def test_app_packages_debugger_debugger( +def test_app_packages_debugger( create_command, myapp, + bundle_path, + app_packages_path, + app_packages_path_index, +): + """If an app has debug requirements and we're in debug mode, they are installed.""" + myapp.requires = ["first", "second==1.2.3", "third>=3.2.1"] + myapp.debugger = DummyDebugger() + + create_command.install_app_requirements(myapp) + + # A request was made to install requirements + create_command.tools[myapp].app_context.run.assert_called_with( + [ + sys.executable, + "-u", + "-X", + "utf8", + "-m", + "pip", + "install", + "--disable-pip-version-check", + "--upgrade", + "--no-user", + f"--target={app_packages_path}", + "first", + "second==1.2.3", + "third>=3.2.1", + f"{bundle_path / '.debugger_support_package'}", + ], + check=True, + encoding="UTF-8", + ) + + # Original app definitions haven't changed + assert myapp.requires == ["first", "second==1.2.3", "third>=3.2.1"] + + # The debugger support package directory was created + assert ( + myapp.debugger.debugger_support_pkg_dir + == bundle_path / ".debugger_support_package" + ) + + # Check that the debugger support package exists + assert (bundle_path / ".debugger_support_package").exists() + assert (bundle_path / ".debugger_support_package" / "dummy.py").exists() + + +def test_app_packages_debugger_clear_old_package( + create_command, + myapp, + bundle_path, app_packages_path, app_packages_path_index, ): @@ -1111,10 +1163,9 @@ def test_app_packages_debugger_debugger( myapp.debugger = DummyDebugger() # create dummy debugger support package directory, that should be cleared - bundle_path = create_command.bundle_path(myapp) (bundle_path / ".debugger_support_package").mkdir(parents=True, exist_ok=True) - (bundle_path / ".debugger_support_package" / "dummy.txt").write_text( - "dummy content" + (bundle_path / ".debugger_support_package" / "some_old_file.py").write_text( + "# some old file content", encoding="utf8" ) create_command.install_app_requirements(myapp) @@ -1151,5 +1202,5 @@ def test_app_packages_debugger_debugger( == bundle_path / ".debugger_support_package" ) - # Check that the debugger support package directory is empty - assert len(os.listdir(myapp.debugger.debugger_support_pkg_dir)) == 0 + # Check that "some_old_file.py" got deleted + assert os.listdir(bundle_path / ".debugger_support_package") == ["dummy.py"] diff --git a/tests/commands/run/test_call.py b/tests/commands/run/test_call.py index 698efdaae..0ef647526 100644 --- a/tests/commands/run/test_call.py +++ b/tests/commands/run/test_call.py @@ -34,7 +34,7 @@ def test_no_args_one_app(run_command, first_app): "first", False, False, - {"debugger_host": "localhost", "debugger_port": 5678, "passthrough": []}, + {"debugger_host": None, "debugger_port": None, "passthrough": []}, ), ] @@ -72,8 +72,8 @@ def test_no_args_one_app_with_passthrough(run_command, first_app): False, False, { - "debugger_host": "localhost", - "debugger_port": 5678, + "debugger_host": None, + "debugger_port": None, "passthrough": ["foo", "--bar"], }, ), @@ -130,7 +130,7 @@ def test_with_arg_one_app(run_command, first_app): "first", False, False, - {"debugger_host": "localhost", "debugger_port": 5678, "passthrough": []}, + {"debugger_host": None, "debugger_port": None, "passthrough": []}, ), ] @@ -167,7 +167,7 @@ def test_with_arg_two_apps(run_command, first_app, second_app): "second", False, False, - {"debugger_host": "localhost", "debugger_port": 5678, "passthrough": []}, + {"debugger_host": None, "debugger_port": None, "passthrough": []}, ), ] @@ -241,8 +241,8 @@ def test_create_app_before_start(run_command, first_app_config): False, { "build_state": "first", - "debugger_host": "localhost", - "debugger_port": 5678, + "debugger_host": None, + "debugger_port": None, "passthrough": [], }, ), @@ -297,8 +297,8 @@ def test_build_app_before_start(run_command, first_app_unbuilt): False, { "build_state": "first", - "debugger_host": "localhost", - "debugger_port": 5678, + "debugger_host": None, + "debugger_port": None, "passthrough": [], }, ), @@ -353,8 +353,8 @@ def test_update_app(run_command, first_app): False, { "build_state": "first", - "debugger_host": "localhost", - "debugger_port": 5678, + "debugger_host": None, + "debugger_port": None, "passthrough": [], }, ), @@ -409,8 +409,8 @@ def test_update_app_requirements(run_command, first_app): False, { "build_state": "first", - "debugger_host": "localhost", - "debugger_port": 5678, + "debugger_host": None, + "debugger_port": None, "passthrough": [], }, ), @@ -465,8 +465,8 @@ def test_update_app_resources(run_command, first_app): False, { "build_state": "first", - "debugger_host": "localhost", - "debugger_port": 5678, + "debugger_host": None, + "debugger_port": None, "passthrough": [], }, ), @@ -521,8 +521,8 @@ def test_update_app_support(run_command, first_app): False, { "build_state": "first", - "debugger_host": "localhost", - "debugger_port": 5678, + "debugger_host": None, + "debugger_port": None, "passthrough": [], }, ), @@ -577,8 +577,8 @@ def test_update_app_stub(run_command, first_app): False, { "build_state": "first", - "debugger_host": "localhost", - "debugger_port": 5678, + "debugger_host": None, + "debugger_port": None, "passthrough": [], }, ), @@ -634,8 +634,8 @@ def test_update_unbuilt_app(run_command, first_app_unbuilt): False, { "build_state": "first", - "debugger_host": "localhost", - "debugger_port": 5678, + "debugger_host": None, + "debugger_port": None, "passthrough": [], }, ), @@ -691,8 +691,8 @@ def test_update_non_existent(run_command, first_app_config): False, { "build_state": "first", - "debugger_host": "localhost", - "debugger_port": 5678, + "debugger_host": None, + "debugger_port": None, "passthrough": [], }, ), @@ -747,8 +747,8 @@ def test_test_mode_existing_app(run_command, first_app): False, { "build_state": "first", - "debugger_host": "localhost", - "debugger_port": 5678, + "debugger_host": None, + "debugger_port": None, "passthrough": [], }, ), @@ -803,8 +803,8 @@ def test_test_mode_existing_app_with_passthrough(run_command, first_app): False, { "build_state": "first", - "debugger_host": "localhost", - "debugger_port": 5678, + "debugger_host": None, + "debugger_port": None, "passthrough": ["foo", "--bar"], }, ), @@ -843,7 +843,7 @@ def test_test_mode_existing_app_no_update(run_command, first_app): "first", True, False, - {"debugger_host": "localhost", "debugger_port": 5678, "passthrough": []}, + {"debugger_host": None, "debugger_port": None, "passthrough": []}, ), ] @@ -896,8 +896,8 @@ def test_test_mode_existing_app_update_requirements(run_command, first_app): False, { "build_state": "first", - "debugger_host": "localhost", - "debugger_port": 5678, + "debugger_host": None, + "debugger_port": None, "passthrough": [], }, ), @@ -952,8 +952,8 @@ def test_test_mode_existing_app_update_resources(run_command, first_app): False, { "build_state": "first", - "debugger_host": "localhost", - "debugger_port": 5678, + "debugger_host": None, + "debugger_port": None, "passthrough": [], }, ), @@ -1008,8 +1008,8 @@ def test_test_mode_update_existing_app(run_command, first_app): False, { "build_state": "first", - "debugger_host": "localhost", - "debugger_port": 5678, + "debugger_host": None, + "debugger_port": None, "passthrough": [], }, ), @@ -1062,6 +1062,64 @@ def test_test_mode_non_existent(run_command, first_app_config): "first", True, False, + { + "build_state": "first", + "debugger_host": None, + "debugger_port": None, + "passthrough": [], + }, + ), + ] + + +def test_debug(run_command, first_app_config): + """Requesting a debugger.""" + # Add a single app, using the 'config only' fixture + run_command.apps = { + "first": first_app_config, + } + + run_command.supports_debugger = True + + # Configure a test option + options, _ = run_command.parse_options(["--debug=pdb"]) + + # Run the run command + run_command(**options) + + # The right sequence of things will be done + assert run_command.actions == [ + # Host OS is verified + ("verify-host",), + # Tools are verified + ("verify-tools",), + # App config has been finalized + ("finalize-app-config", "first"), + # App will be built with debugger + ( + "build", + "first", + False, + True, + { + "update": False, + "update_requirements": False, + "update_resources": False, + "update_support": False, + "update_stub": False, + "no_update": False, + }, + ), + # App template is verified + ("verify-app-template", "first"), + # App tools are verified + ("verify-app-tools", "first"), + # Then, it will be started + ( + "run", + "first", + False, + True, { "build_state": "first", "debugger_host": "localhost", diff --git a/tests/commands/update/test_call.py b/tests/commands/update/test_call.py index df15f9636..9ec6ab909 100644 --- a/tests/commands/update/test_call.py +++ b/tests/commands/update/test_call.py @@ -271,3 +271,36 @@ def test_update_app_all_flags(update_command, first_app, second_app): ("support", "first"), ("cleanup", "first"), ] + + +def test_update_debug_with_requirements(update_command, first_app, second_app): + """The update command can be called, requesting a requirements update.""" + update_command.supports_debugger = True + + # Configure no command line options + options, _ = update_command.parse_options(["-r", "--debug=pdb"]) + + update_command(**options) + + # The right sequence of things will be done + assert update_command.actions == [ + # Host OS is verified + ("verify-host",), + # Tools are verified + ("verify-tools",), + # App configs have been finalized + ("finalize-app-config", "first"), + ("finalize-app-config", "second"), + # Update the first app + ("verify-app-template", "first"), + ("verify-app-tools", "first"), + ("code", "first", False), + ("requirements", "first", False, True), + ("cleanup", "first"), + # Update the second app + ("verify-app-template", "second"), + ("verify-app-tools", "second"), + ("code", "second", False), + ("requirements", "second", False, True), + ("cleanup", "second"), + ] diff --git a/tests/debuggers/test_base.py b/tests/debuggers/test_base.py index f35823f42..d095d3e78 100644 --- a/tests/debuggers/test_base.py +++ b/tests/debuggers/test_base.py @@ -1,7 +1,9 @@ +import py_compile from pathlib import Path from tempfile import TemporaryDirectory import pytest +import tomllib from briefcase.debuggers import ( DebugpyDebugger, @@ -54,9 +56,20 @@ def test_debugger(debugger_name, expected_class, connection_mode): with TemporaryDirectory() as tmp_path: tmp_path = Path(tmp_path) debugger.create_debugger_support_pkg(tmp_path) - assert (tmp_path / "pyproject.toml").exists() - assert (tmp_path / "setup.py").exists() - assert (tmp_path / "briefcase_debugger_support" / "__init__.py").exists() + + # Try to parse pyproject.toml to check for toml-format errors + with (tmp_path / "pyproject.toml").open("rb") as f: + tomllib.load(f) + + # try to compile to check existence and for syntax errors + assert py_compile.compile(tmp_path / "setup.py") is not None + assert ( + py_compile.compile(tmp_path / "briefcase_debugger_support" / "__init__.py") + is not None + ) assert ( - tmp_path / "briefcase_debugger_support" / "_remote_debugger.py" - ).exists() + py_compile.compile( + tmp_path / "briefcase_debugger_support" / "_remote_debugger.py" + ) + is not None + ) diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index b177ca1a5..71e95f4a6 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -280,9 +280,6 @@ def test_run_command( "update_stub": False, "no_update": False, "test_mode": False, - "debugger": None, - "debugger_host": "localhost", - "debugger_port": 5678, "passthrough": [], **expected_options, } From 87e675addb97cee13673678635384a8657ec10f4 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sun, 22 Jun 2025 17:18:47 +0200 Subject: [PATCH 052/131] fixed unit tests in python 3.9 & 3.10 --- tests/debuggers/test_base.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/debuggers/test_base.py b/tests/debuggers/test_base.py index d095d3e78..d121fe2bb 100644 --- a/tests/debuggers/test_base.py +++ b/tests/debuggers/test_base.py @@ -1,9 +1,14 @@ import py_compile +import sys from pathlib import Path from tempfile import TemporaryDirectory import pytest -import tomllib + +if sys.version_info >= (3, 11): # pragma: no-cover-if-lt-py311 + import tomllib +else: # pragma: no-cover-if-gte-py311 + import tomli as tomllib from briefcase.debuggers import ( DebugpyDebugger, From 7eb8e4b7f361c810ef5b431258d425dc4c9772cd Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sun, 22 Jun 2025 17:19:19 +0200 Subject: [PATCH 053/131] Updated docs --- docs/how-to/debugging/console.rst | 74 +++++++++++++++++++++++++++ docs/how-to/debugging/index.rst | 25 ++++++++++ docs/how-to/debugging/vscode.rst | 83 +++++++++++++++++++++++++++++++ docs/how-to/debugging_vscode.rst | 82 ------------------------------ docs/how-to/index.rst | 2 +- docs/reference/commands/build.rst | 8 +-- docs/reference/commands/run.rst | 36 ++------------ 7 files changed, 191 insertions(+), 119 deletions(-) create mode 100644 docs/how-to/debugging/console.rst create mode 100644 docs/how-to/debugging/index.rst create mode 100644 docs/how-to/debugging/vscode.rst delete mode 100644 docs/how-to/debugging_vscode.rst diff --git a/docs/how-to/debugging/console.rst b/docs/how-to/debugging/console.rst new file mode 100644 index 000000000..791f4961c --- /dev/null +++ b/docs/how-to/debugging/console.rst @@ -0,0 +1,74 @@ +================= +Debug via Console +================= + +Debugging an app on the console is normally done via `PDB `_. +It is possible to debug a briefcase app at different stages in your development +process. You can debug a development app via ``briefcase dev``, but also an bundled +app that is build via ``briefcase build`` and run via ``briefcase run``. + + +Development +----------- +Debugging an development app is quiet easy. Just add ``breakpoint()`` inside +your code and start the app via ``briefcase dev``. When the breakpoint got hit +the pdb console opens on your console and you can debug your app. + + +Bundled App +----------- +It is also possible to debug a bundled app. This is the only way to debug your +app on a mobile device (iOS/Android). Note that there are some :ref:`limitations ` +when debugging an bundled app. + +For this you need to embed a remote debugger into your app. This is done via: + +.. code-block:: console + + $ briefcase build --debug pdb + +This will build your app in debug mode and add `remote-pdb `_ +together with a package that automatically starts ``remote-pdb`` at the +startup of your bundled app. Additionally it will optimize the app for +debugging. This means e.g. that all ``.py`` files are accessible on the device. + +Then it is time to run your app. You can do this via: + +.. code-block:: console + + $ briefcase run --debug pdb + +Running the app in debug mode will automatically start the ``remote-pdb`` debugger +and wait for incoming connections. By default it will listen on ``localhost`` +and port ``5678``. + +Then it is time to create a new console window on your host system and connect +to your bundled app by calling: + +.. tabs:: + + .. group-tab:: Windows + + .. code-block:: console + + $ telnet localhost 5678 + + .. group-tab:: Linux + + .. code-block:: console + + $ telnet localhost 5678 + + or + + .. code-block:: console + + $ rlwrap socat - tcp:localhost:5678 + + .. group-tab:: macOS + + .. code-block:: console + + $ rlwrap socat - tcp:localhost:5678 + +The app will start after the connection is established. diff --git a/docs/how-to/debugging/index.rst b/docs/how-to/debugging/index.rst new file mode 100644 index 000000000..36bd6f624 --- /dev/null +++ b/docs/how-to/debugging/index.rst @@ -0,0 +1,25 @@ +============== +Debug your app +============== + +If you get stuck when programming your app, it is time to debug your app. The +following sections describe how you can debug your app with or without an IDE. + +.. toctree:: + :maxdepth: 1 + + console + vscode + + +.. _debugging_limitations: + +Limitations when debugging bundled apps +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +To debug an bundle app you need an network connection from your host system to +the device your are trying to debug. If your bundled app is also running on +your host system this is no problem. But when debugging a mobile device your +app is running on another device. Running an iOS app in simulator is also no +problem, because the simulator shares the same network stack as your host. +But on Android there is a separate network stack. That's why briefcase will +automatically forward the port from your host to the Android device via ADB. diff --git a/docs/how-to/debugging/vscode.rst b/docs/how-to/debugging/vscode.rst new file mode 100644 index 000000000..0722ac9e1 --- /dev/null +++ b/docs/how-to/debugging/vscode.rst @@ -0,0 +1,83 @@ +================ +Debug via VSCode +================ + +Debugging is possible at different stages in your development process. It is +different to debug a development app via ``briefcase dev`` than an bundled app +that is build via ``briefcase build`` and run via ``briefcase run``. + +Development +----------- +During development on your host system you should use ``briefcase dev``. To +attach VSCode debugger you can simply create a configuration like this, +that runs ``briefcase dev`` for you and attaches a debugger. + +.. code-block:: JSON + + { + "version": "0.2.0", + "configurations": [ + { + "name": "Briefcase: Dev", + "type": "debugpy", + "request": "launch", + "module": "briefcase", + "args": [ + "dev", + ], + "justMyCode": false + }, + ] + } + + +Bundled App +----------- +It is also possible to debug a bundled app. This is the only way to debug your +app on a mobile device (iOS/Android). Note that there are some :ref:`limitations ` +when debugging an bundled app. + +For this you need to embed a remote debugger into your app. This is done via: + +.. code-block:: console + + $ briefcase build --debug debugpy + +This will build your app in debug mode and add the `debugpy `_ together with a +package that automatically starts ``debugpy`` at the startup of your bundled app. +Additionally it will optimize the app for debugging. This means e.g. that all +``.py`` files are accessible on the device. + +Then it is time to run your app. You can do this via: + +.. code-block:: console + + $ briefcase run --debug debugpy + +Running the app in debug mode will automatically start the ``debugpy`` debugger +and listen for incoming connections. By default it will listen on ``localhost`` +and port ``5678``. You can then connect your VSCode debugger to the app by +creating a configuration like this in the ``launch.json`` file: + +.. code-block:: JSON + + { + "version": "0.2.0", + "configurations": [ + { + "name": "Briefcase: Attach", + "type": "debugpy", + "request": "attach", + "connect": { + "host": "localhost", + "port": 5678 + } + } + ] + } + +The app will not start until you attach the debugger. Once you attached the +VSCode debugger you are ready to debug your app. You can set `breakpoints `_ +, use the `data inspection `_ +, use the `debug console REPL `_ +and all other debugging features of VSCode :) diff --git a/docs/how-to/debugging_vscode.rst b/docs/how-to/debugging_vscode.rst deleted file mode 100644 index dffbb4649..000000000 --- a/docs/how-to/debugging_vscode.rst +++ /dev/null @@ -1,82 +0,0 @@ -Debugging with VSCode -===================== -Debugging is possible at different stages in your development process. It is -different to debug a development app via `briefcase dev` than an bundled app -that is build via `briefcase build` and run via `briefcase run`. - -Development ------------ -During development on your host system you should use `briefcase dev`. To -attach VSCode debugger you can simply create a configuration like this, -that runs `briefcase dev` for you and attaches a debugger. - -``` -{ - "version": "0.2.0", - "configurations": [ - { - "name": "Briefcase: Dev", - "type": "debugpy", - "request": "launch", - "module": "briefcase", - "args": [ - "dev", - ], - "justMyCode": false - }, - ] -} -``` - -Bundled App ------------ -It is also possible to debug a bundled app. This is the only way to debug your -app on a mobile device (iOS/Android). - -For this you need to embed a remote debugger into your app. This is done via: - -``` -briefcase build --debug debugpy -``` - -This will build your app in debug mode and add the `debugpy` package to your -app. Additionally it will optimize the app for debugging. This means e.g. that -all `.py` files are accessible on the device. - -Then it is time to run your app. You can do this via: - -``` -briefcase run --debug debugpy -``` - -Running the app in debug mode will automatically start the `debugpy` debugger -and listen for incoming connections. By default it will listen on `localhost` -and port `5678`. You can then connect your VSCode debugger to the app by -creating a configuration like this in the `launch.json` file: - -``` -{ - "version": "0.2.0", - "configurations": [ - { - "name": "Briefcase: Attach", - "type": "debugpy", - "request": "attach", - "connect": { - "host": "localhost", - "port": 5678 - } - } - ] -} - -Note, that you need an network connection to the device your are trying to -debug. If your bundled app is running on your host system this is no problem. -But when debugging a mobile device your app is running on another device. -Running an iOS app in simulator is also no problem, because the simulator -shares the same network stack as your host. But on Android there is a separate -network stack. That's why briefcase will automatically forward the port from -your host to the android device via `adb` (Android Debug Bridge). - -Now you are ready to debug your app. You can set breakpoints in your code, use -the "Debug Console" and all other debugging features of VSCode :) diff --git a/docs/how-to/index.rst b/docs/how-to/index.rst index 6c6133c5c..d206f3c03 100644 --- a/docs/how-to/index.rst +++ b/docs/how-to/index.rst @@ -17,9 +17,9 @@ stand alone. ci cli-apps x11passthrough + debugging/index external-apps publishing/index contribute/index internal/index upgrade-from-v0.2 - debugging_vscode diff --git a/docs/reference/commands/build.rst b/docs/reference/commands/build.rst index 893b50eb9..d69b66696 100644 --- a/docs/reference/commands/build.rst +++ b/docs/reference/commands/build.rst @@ -122,11 +122,11 @@ bundled app. Currently the following debuggers are supported (default is ``pdb``): - - ``pdb``: This is used for debugging via console. - - ``debugpy``: This is used for debugging via VSCode. +- ``pdb``: This is used for debugging via console (see :doc:`Debug via Console `) +- ``debugpy``: This is used for debugging via VSCode (see :doc:`Debug via VSCode `) -It also optimizes the app for debugging. E.g. on android it ensures, that all -`.py` files are extracted from the apk and are accessible for the debugger. +It also optimizes the app for debugging. E.g. on Android it ensures, that all +`.py` files are extracted from the APK and are accessible for the debugger. This option may slow down the app a little bit. diff --git a/docs/reference/commands/run.rst b/docs/reference/commands/run.rst index c2a218115..2ecf7644f 100644 --- a/docs/reference/commands/run.rst +++ b/docs/reference/commands/run.rst @@ -34,32 +34,6 @@ corresponding to test suite completion. Briefcase has built-in support for `pyte frameworks can be added using the :attr:`test_success_regex` and :attr:`test_failure_regex` settings. -Debug mode ----------- - -The debug mode can be used to (remote) debug an bundled app. The debugger has -to be specified with the ``--debug `` option during ``briefcase build`` -and ``briefcase run``. - -This is useful when developing an iOS or Android app that can't be debugged -via ``briefcase dev``. - -To debug an bundled app you need a socket connection from your host system to -the device running your bundled app. For the iOS simulator the host pc and the -iOS simulator share the same network. For Android briefcase ensures that the -port is forwarded from the android device to the host pc via adb. - -Currently the following debuggers are supported: - -- ``pdb``: This is used for debugging via console. After starting the app - you can connect to it depending on your host system via - - ``telnet localhost 5678`` (Windows, Linux) - - ``rlwrap socat - tcp:localhost:5678`` (Linux, macOS) - The app will start after the connection is established. - -- ``debugpy``: This is used for debugging via VSCode (see :doc:`Debugging with VSCode `) - - Usage ===== @@ -171,8 +145,8 @@ debugger connection via a socket. Currently the following debuggers are supported (default is ``pdb``): - - ``pdb``: This is used for debugging via console. - - ``debugpy``: This is used for debugging via VSCode. +- ``pdb``: This is used for debugging via console (see :doc:`Debug via Console `) +- ``debugpy``: This is used for debugging via VSCode (see :doc:`Debug via VSCode `) For ``debugpy`` there is also a mapping of the source code from your bundled app to your local copy of the apps source code in the ``build`` folder. This @@ -193,10 +167,8 @@ Specifies the port of the socket connection for the debugger. This option is only used when the ``--debug `` option is specified. The default value is ``5678``. -On Android this also forwards the port from the android device to the host pc -via adb if the port is ``localhost``. - - +On Android this also forwards the port from the Android device to the host pc +via ADB if the port is ``localhost``. Passthrough arguments --------------------- From a3d15f3d3b165bac0cc379bb58932d7093fce4b1 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sun, 22 Jun 2025 17:32:45 +0200 Subject: [PATCH 054/131] removed how-to guides, as they are not working yet. Removed Android specific documentation that is not part of this PR --- docs/how-to/debugging/console.rst | 74 --------------------------- docs/how-to/debugging/index.rst | 25 ---------- docs/how-to/debugging/vscode.rst | 83 ------------------------------- docs/how-to/index.rst | 1 - docs/reference/commands/build.rst | 9 +--- docs/reference/commands/run.rst | 12 +---- 6 files changed, 4 insertions(+), 200 deletions(-) delete mode 100644 docs/how-to/debugging/console.rst delete mode 100644 docs/how-to/debugging/index.rst delete mode 100644 docs/how-to/debugging/vscode.rst diff --git a/docs/how-to/debugging/console.rst b/docs/how-to/debugging/console.rst deleted file mode 100644 index 791f4961c..000000000 --- a/docs/how-to/debugging/console.rst +++ /dev/null @@ -1,74 +0,0 @@ -================= -Debug via Console -================= - -Debugging an app on the console is normally done via `PDB `_. -It is possible to debug a briefcase app at different stages in your development -process. You can debug a development app via ``briefcase dev``, but also an bundled -app that is build via ``briefcase build`` and run via ``briefcase run``. - - -Development ------------ -Debugging an development app is quiet easy. Just add ``breakpoint()`` inside -your code and start the app via ``briefcase dev``. When the breakpoint got hit -the pdb console opens on your console and you can debug your app. - - -Bundled App ------------ -It is also possible to debug a bundled app. This is the only way to debug your -app on a mobile device (iOS/Android). Note that there are some :ref:`limitations ` -when debugging an bundled app. - -For this you need to embed a remote debugger into your app. This is done via: - -.. code-block:: console - - $ briefcase build --debug pdb - -This will build your app in debug mode and add `remote-pdb `_ -together with a package that automatically starts ``remote-pdb`` at the -startup of your bundled app. Additionally it will optimize the app for -debugging. This means e.g. that all ``.py`` files are accessible on the device. - -Then it is time to run your app. You can do this via: - -.. code-block:: console - - $ briefcase run --debug pdb - -Running the app in debug mode will automatically start the ``remote-pdb`` debugger -and wait for incoming connections. By default it will listen on ``localhost`` -and port ``5678``. - -Then it is time to create a new console window on your host system and connect -to your bundled app by calling: - -.. tabs:: - - .. group-tab:: Windows - - .. code-block:: console - - $ telnet localhost 5678 - - .. group-tab:: Linux - - .. code-block:: console - - $ telnet localhost 5678 - - or - - .. code-block:: console - - $ rlwrap socat - tcp:localhost:5678 - - .. group-tab:: macOS - - .. code-block:: console - - $ rlwrap socat - tcp:localhost:5678 - -The app will start after the connection is established. diff --git a/docs/how-to/debugging/index.rst b/docs/how-to/debugging/index.rst deleted file mode 100644 index 36bd6f624..000000000 --- a/docs/how-to/debugging/index.rst +++ /dev/null @@ -1,25 +0,0 @@ -============== -Debug your app -============== - -If you get stuck when programming your app, it is time to debug your app. The -following sections describe how you can debug your app with or without an IDE. - -.. toctree:: - :maxdepth: 1 - - console - vscode - - -.. _debugging_limitations: - -Limitations when debugging bundled apps -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -To debug an bundle app you need an network connection from your host system to -the device your are trying to debug. If your bundled app is also running on -your host system this is no problem. But when debugging a mobile device your -app is running on another device. Running an iOS app in simulator is also no -problem, because the simulator shares the same network stack as your host. -But on Android there is a separate network stack. That's why briefcase will -automatically forward the port from your host to the Android device via ADB. diff --git a/docs/how-to/debugging/vscode.rst b/docs/how-to/debugging/vscode.rst deleted file mode 100644 index 0722ac9e1..000000000 --- a/docs/how-to/debugging/vscode.rst +++ /dev/null @@ -1,83 +0,0 @@ -================ -Debug via VSCode -================ - -Debugging is possible at different stages in your development process. It is -different to debug a development app via ``briefcase dev`` than an bundled app -that is build via ``briefcase build`` and run via ``briefcase run``. - -Development ------------ -During development on your host system you should use ``briefcase dev``. To -attach VSCode debugger you can simply create a configuration like this, -that runs ``briefcase dev`` for you and attaches a debugger. - -.. code-block:: JSON - - { - "version": "0.2.0", - "configurations": [ - { - "name": "Briefcase: Dev", - "type": "debugpy", - "request": "launch", - "module": "briefcase", - "args": [ - "dev", - ], - "justMyCode": false - }, - ] - } - - -Bundled App ------------ -It is also possible to debug a bundled app. This is the only way to debug your -app on a mobile device (iOS/Android). Note that there are some :ref:`limitations ` -when debugging an bundled app. - -For this you need to embed a remote debugger into your app. This is done via: - -.. code-block:: console - - $ briefcase build --debug debugpy - -This will build your app in debug mode and add the `debugpy `_ together with a -package that automatically starts ``debugpy`` at the startup of your bundled app. -Additionally it will optimize the app for debugging. This means e.g. that all -``.py`` files are accessible on the device. - -Then it is time to run your app. You can do this via: - -.. code-block:: console - - $ briefcase run --debug debugpy - -Running the app in debug mode will automatically start the ``debugpy`` debugger -and listen for incoming connections. By default it will listen on ``localhost`` -and port ``5678``. You can then connect your VSCode debugger to the app by -creating a configuration like this in the ``launch.json`` file: - -.. code-block:: JSON - - { - "version": "0.2.0", - "configurations": [ - { - "name": "Briefcase: Attach", - "type": "debugpy", - "request": "attach", - "connect": { - "host": "localhost", - "port": 5678 - } - } - ] - } - -The app will not start until you attach the debugger. Once you attached the -VSCode debugger you are ready to debug your app. You can set `breakpoints `_ -, use the `data inspection `_ -, use the `debug console REPL `_ -and all other debugging features of VSCode :) diff --git a/docs/how-to/index.rst b/docs/how-to/index.rst index d206f3c03..1e60be3e9 100644 --- a/docs/how-to/index.rst +++ b/docs/how-to/index.rst @@ -17,7 +17,6 @@ stand alone. ci cli-apps x11passthrough - debugging/index external-apps publishing/index contribute/index diff --git a/docs/reference/commands/build.rst b/docs/reference/commands/build.rst index d69b66696..0d956d47e 100644 --- a/docs/reference/commands/build.rst +++ b/docs/reference/commands/build.rst @@ -122,13 +122,8 @@ bundled app. Currently the following debuggers are supported (default is ``pdb``): -- ``pdb``: This is used for debugging via console (see :doc:`Debug via Console `) -- ``debugpy``: This is used for debugging via VSCode (see :doc:`Debug via VSCode `) - -It also optimizes the app for debugging. E.g. on Android it ensures, that all -`.py` files are extracted from the APK and are accessible for the debugger. - -This option may slow down the app a little bit. +- ``pdb``: This is used for debugging via console. +- ``debugpy``: This is used for debugging via VSCode. ``--no-update`` diff --git a/docs/reference/commands/run.rst b/docs/reference/commands/run.rst index 2ecf7644f..02963c115 100644 --- a/docs/reference/commands/run.rst +++ b/docs/reference/commands/run.rst @@ -145,13 +145,8 @@ debugger connection via a socket. Currently the following debuggers are supported (default is ``pdb``): -- ``pdb``: This is used for debugging via console (see :doc:`Debug via Console `) -- ``debugpy``: This is used for debugging via VSCode (see :doc:`Debug via VSCode `) - -For ``debugpy`` there is also a mapping of the source code from your bundled -app to your local copy of the apps source code in the ``build`` folder. This -is useful for devices like iOS and Android, where the running source code is -not available on the host system. +- ``pdb``: This is used for debugging via console. +- ``debugpy``: This is used for debugging via VSCode. ``--debugger-host `` -------------------------- @@ -167,9 +162,6 @@ Specifies the port of the socket connection for the debugger. This option is only used when the ``--debug `` option is specified. The default value is ``5678``. -On Android this also forwards the port from the Android device to the host pc -via ADB if the port is ``localhost``. - Passthrough arguments --------------------- From 5ff73eaf2e054d1ad2965efb1e65095797b5a48f Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sun, 22 Jun 2025 17:33:56 +0200 Subject: [PATCH 055/131] corrected changelog --- changes/2147.feature.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changes/2147.feature.rst b/changes/2147.feature.rst index 667d33cf0..b1f0974b9 100644 --- a/changes/2147.feature.rst +++ b/changes/2147.feature.rst @@ -1 +1 @@ -Added debugging support in bundled app through ``briefcase run --debug ``. +Added basic functions for an ``--debug `` option. From 8843ab309c29fd61530b52bb1fb2fa8f35378936 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Wed, 25 Jun 2025 21:32:03 +0200 Subject: [PATCH 056/131] change from dynamic creation of debugger-support packages to a static package --- .gitignore | 2 + .../README.md | 7 + .../pyproject.toml | 19 +++ .../setup.py | 45 +++++ .../__init__.py | 43 +++++ .../_remote_debugger.py | 130 +++++++++++++++ .../briefcase-pdb-debugger-support/README.md | 7 + .../pyproject.toml | 19 +++ .../briefcase-pdb-debugger-support/setup.py | 45 +++++ .../__init__.py | 43 +++++ .../_remote_debugger.py | 30 ++++ src/briefcase/commands/create.py | 24 +-- src/briefcase/debuggers/base.py | 135 +-------------- src/briefcase/debuggers/debugpy.py | 154 +----------------- src/briefcase/debuggers/pdb.py | 53 +----- .../create/test_install_app_requirements.py | 75 +-------- tests/debuggers/test_base.py | 34 +--- 17 files changed, 412 insertions(+), 453 deletions(-) create mode 100644 debugger-support/briefcase-debugpy-debugger-support/README.md create mode 100644 debugger-support/briefcase-debugpy-debugger-support/pyproject.toml create mode 100644 debugger-support/briefcase-debugpy-debugger-support/setup.py create mode 100644 debugger-support/briefcase-debugpy-debugger-support/src/briefcase_debugpy_debugger_support/__init__.py create mode 100644 debugger-support/briefcase-debugpy-debugger-support/src/briefcase_debugpy_debugger_support/_remote_debugger.py create mode 100644 debugger-support/briefcase-pdb-debugger-support/README.md create mode 100644 debugger-support/briefcase-pdb-debugger-support/pyproject.toml create mode 100644 debugger-support/briefcase-pdb-debugger-support/setup.py create mode 100644 debugger-support/briefcase-pdb-debugger-support/src/briefcase_pdb_debugger_support/__init__.py create mode 100644 debugger-support/briefcase-pdb-debugger-support/src/briefcase_pdb_debugger_support/_remote_debugger.py diff --git a/.gitignore b/.gitignore index ecdf734b4..0796b826d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ /dist /build automation/build +debugger-support/briefcase-debugpy-debugger-support/build +debugger-support/briefcase-pdb-debugger-support/build docs/_build/ distribute-* .DS_Store diff --git a/debugger-support/briefcase-debugpy-debugger-support/README.md b/debugger-support/briefcase-debugpy-debugger-support/README.md new file mode 100644 index 000000000..0cf8ffbec --- /dev/null +++ b/debugger-support/briefcase-debugpy-debugger-support/README.md @@ -0,0 +1,7 @@ +# Briefcase Debugpy Debugger Support +This package contains the debugger support package for the debugpy debugger. + +It starts the remote debugger automatically at startup through an .pth file, if a `BRIEFCASE_DEBUGGER` environment variable is set. + +## Installation +Normally you do not need to install this package manually, because it is done automatically by briefcase using the `--debug=debugpy` option. diff --git a/debugger-support/briefcase-debugpy-debugger-support/pyproject.toml b/debugger-support/briefcase-debugpy-debugger-support/pyproject.toml new file mode 100644 index 000000000..5bdbdc236 --- /dev/null +++ b/debugger-support/briefcase-debugpy-debugger-support/pyproject.toml @@ -0,0 +1,19 @@ +[build-system] +requires = [ + # keep versions in sync with automation/pyproject.toml and ../pyproject.toml + "setuptools==80.9.0", + "setuptools_scm==8.3.1", +] +build-backend = "setuptools.build_meta" + +[project] +name = "briefcase-debugpy-debugger-support" +description = "Add-on for briefcase to add remote debugging via debugpy." +license = "BSD-3-Clause" +dependencies = [ + "debugpy>=1.8.14,<2.0.0" +] +dynamic = ["version"] + +[tool.setuptools_scm] +root = "../../" diff --git a/debugger-support/briefcase-debugpy-debugger-support/setup.py b/debugger-support/briefcase-debugpy-debugger-support/setup.py new file mode 100644 index 000000000..6b726c528 --- /dev/null +++ b/debugger-support/briefcase-debugpy-debugger-support/setup.py @@ -0,0 +1,45 @@ +import os + +import setuptools +from setuptools.command.install import install + + +# Copied from setuptools: +# (https://github.com/pypa/setuptools/blob/7c859e017368360ba66c8cc591279d8964c031bc/setup.py#L40C6-L82) +class install_with_pth(install): + """ + Custom install command to install a .pth file for distutils patching. + + This hack is necessary because there's no standard way to install behavior + on startup (and it's debatable if there should be one). This hack (ab)uses + the `extra_path` behavior in Setuptools to install a `.pth` file with + implicit behavior on startup to give higher precedence to the local version + of `distutils` over the version from the standard library. + + Please do not replicate this behavior. + """ + + _pth_name = "briefcase_debugpy_debugger_support" + _pth_contents = "import briefcase_debugpy_debugger_support" + + def initialize_options(self): + install.initialize_options(self) + self.extra_path = self._pth_name, self._pth_contents + + def finalize_options(self): + install.finalize_options(self) + self._restore_install_lib() + + def _restore_install_lib(self): + """ + Undo secondary effect of `extra_path` adding to `install_lib` + """ + suffix = os.path.relpath(self.install_lib, self.install_libbase) + + if suffix.strip() == self._pth_contents.strip(): + self.install_lib = self.install_libbase + + +setuptools.setup( + cmdclass={"install": install_with_pth}, +) diff --git a/debugger-support/briefcase-debugpy-debugger-support/src/briefcase_debugpy_debugger_support/__init__.py b/debugger-support/briefcase-debugpy-debugger-support/src/briefcase_debugpy_debugger_support/__init__.py new file mode 100644 index 000000000..3af2699b4 --- /dev/null +++ b/debugger-support/briefcase-debugpy-debugger-support/src/briefcase_debugpy_debugger_support/__init__.py @@ -0,0 +1,43 @@ +import os +import sys +import traceback + +from ._remote_debugger import start_debugpy + +REMOTE_DEBUGGER_STARTED = False + + +def start_remote_debugger(): + global REMOTE_DEBUGGER_STARTED + REMOTE_DEBUGGER_STARTED = True + + # check verbose output + verbose = os.environ.get("BRIEFCASE_DEBUG", "0") == "1" + + # reading config + config_str = os.environ.get("BRIEFCASE_DEBUGGER", None) + + # skip debugger if no config is set + if config_str is None: + if verbose: + print( + "No 'BRIEFCASE_DEBUGGER' environment variable found. Debugger not starting." + ) + return # If BRIEFCASE_DEBUGGER is not set, this packages does nothing... + + if verbose: + print(f"'BRIEFCASE_DEBUGGER'={config_str}") + + # start debugger + print("Starting remote debugger...") + start_debugpy(config_str, verbose) + + +# only start remote debugger on the first import +if not REMOTE_DEBUGGER_STARTED: + try: + start_remote_debugger() + except Exception: + # Show exception and stop the whole application when an error occurs + print(traceback.format_exc()) + sys.exit(-1) diff --git a/debugger-support/briefcase-debugpy-debugger-support/src/briefcase_debugpy_debugger_support/_remote_debugger.py b/debugger-support/briefcase-debugpy-debugger-support/src/briefcase_debugpy_debugger_support/_remote_debugger.py new file mode 100644 index 000000000..f6b20b055 --- /dev/null +++ b/debugger-support/briefcase-debugpy-debugger-support/src/briefcase_debugpy_debugger_support/_remote_debugger.py @@ -0,0 +1,130 @@ +import json +import os +import re +import sys +from pathlib import Path +from typing import TypedDict + +import debugpy + + +class AppPathMappings(TypedDict): + device_sys_path_regex: str + device_subfolders: list[str] + host_folders: list[str] + + +class AppPackagesPathMappings(TypedDict): + sys_path_regex: str + host_folder: str + + +class DebuggerConfig(TypedDict): + host: str + port: int + app_path_mappings: AppPathMappings | None + app_packages_path_mappings: AppPackagesPathMappings | None + + +def _load_path_mappings(config: DebuggerConfig, verbose: bool) -> list[tuple[str, str]]: + app_path_mappings = config.get("app_path_mappings", None) + app_packages_path_mappings = config.get("app_packages_path_mappings", None) + + mappings_list = [] + if app_path_mappings: + device_app_folder = next( + ( + p + for p in sys.path + if re.search(app_path_mappings["device_sys_path_regex"], p) + ), + None, + ) + if device_app_folder: + for app_subfolder_device, app_subfolder_host in zip( + app_path_mappings["device_subfolders"], + app_path_mappings["host_folders"], + ): + mappings_list.append( + ( + app_subfolder_host, + str(Path(device_app_folder) / app_subfolder_device), + ) + ) + if app_packages_path_mappings: + device_app_packages_folder = next( + ( + p + for p in sys.path + if re.search(app_packages_path_mappings["sys_path_regex"], p) + ), + None, + ) + if device_app_packages_folder: + mappings_list.append( + ( + app_packages_path_mappings["host_folder"], + str(Path(device_app_packages_folder)), + ) + ) + + if verbose: + print("Extracted path mappings:") + for idx, p in enumerate(mappings_list): + print(f"[{idx}] host = {p[0]}") + print(f"[{idx}] device = {p[1]}") + + return mappings_list + + +def start_debugpy(config_str: str, verbose: bool): + # Parsing config json + debugger_config: dict = json.loads(config_str) + + host = debugger_config["host"] + port = debugger_config["port"] + path_mappings = _load_path_mappings(debugger_config, verbose) + + # When an app is bundled with briefcase "os.__file__" is not set at runtime + # on some platforms (eg. windows). But debugpy accesses it internally, so it + # has to be set or an Exception is raised from debugpy. + if not hasattr(os, "__file__"): + if verbose: + print("'os.__file__' not available. Patching it...") + os.__file__ = "" + + # Starting remote debugger... + print(f"Starting debugpy in server mode at {host}:{port}...") + debugpy.listen((host, port), in_process_debug_adapter=True) + + if len(path_mappings) > 0: + if verbose: + print("Adding path mappings...") + + import pydevd_file_utils + + pydevd_file_utils.setup_client_server_paths(path_mappings) + + print("The debugpy server started. Waiting for debugger to attach...") + print( + f""" +To connect to debugpy using VSCode add the following configuration to launch.json: +{{ + "version": "0.2.0", + "configurations": [ + {{ + "name": "Briefcase: Attach (Connect)", + "type": "debugpy", + "request": "attach", + "connect": {{ + "host": "{host}", + "port": {port} + }} + }} + ] +}} +""" + ) + debugpy.wait_for_client() + + print("Debugger attached.") diff --git a/debugger-support/briefcase-pdb-debugger-support/README.md b/debugger-support/briefcase-pdb-debugger-support/README.md new file mode 100644 index 000000000..fbc945b44 --- /dev/null +++ b/debugger-support/briefcase-pdb-debugger-support/README.md @@ -0,0 +1,7 @@ +# Briefcase Pdb Debugger Support +This package contains the debugger support package for the pdb debugger. + +It starts the remote debugger automatically at startup through an .pth file, if a `BRIEFCASE_DEBUGGER` environment variable is set. + +## Installation +Normally you do not need to install this package manually, because it is done automatically by briefcase using the `--debug=pdb` option. diff --git a/debugger-support/briefcase-pdb-debugger-support/pyproject.toml b/debugger-support/briefcase-pdb-debugger-support/pyproject.toml new file mode 100644 index 000000000..f1721eaa9 --- /dev/null +++ b/debugger-support/briefcase-pdb-debugger-support/pyproject.toml @@ -0,0 +1,19 @@ +[build-system] +requires = [ + # keep versions in sync with automation/pyproject.toml and ../pyproject.toml + "setuptools==80.9.0", + "setuptools_scm==8.3.1", +] +build-backend = "setuptools.build_meta" + +[project] +name = "briefcase-pdb-debugger-support" +description = "Add-on for briefcase to add remote debugging via pdb." +license = "BSD-3-Clause" +dependencies = [ + "remote-pdb>=2.1.0,<3.0.0" +] +dynamic = ["version"] + +[tool.setuptools_scm] +root = "../../" diff --git a/debugger-support/briefcase-pdb-debugger-support/setup.py b/debugger-support/briefcase-pdb-debugger-support/setup.py new file mode 100644 index 000000000..9130f3249 --- /dev/null +++ b/debugger-support/briefcase-pdb-debugger-support/setup.py @@ -0,0 +1,45 @@ +import os + +import setuptools +from setuptools.command.install import install + + +# Copied from setuptools: +# (https://github.com/pypa/setuptools/blob/7c859e017368360ba66c8cc591279d8964c031bc/setup.py#L40C6-L82) +class install_with_pth(install): + """ + Custom install command to install a .pth file for distutils patching. + + This hack is necessary because there's no standard way to install behavior + on startup (and it's debatable if there should be one). This hack (ab)uses + the `extra_path` behavior in Setuptools to install a `.pth` file with + implicit behavior on startup to give higher precedence to the local version + of `distutils` over the version from the standard library. + + Please do not replicate this behavior. + """ + + _pth_name = "briefcase_pdb_debugger_support" + _pth_contents = "import briefcase_pdb_debugger_support" + + def initialize_options(self): + install.initialize_options(self) + self.extra_path = self._pth_name, self._pth_contents + + def finalize_options(self): + install.finalize_options(self) + self._restore_install_lib() + + def _restore_install_lib(self): + """ + Undo secondary effect of `extra_path` adding to `install_lib` + """ + suffix = os.path.relpath(self.install_lib, self.install_libbase) + + if suffix.strip() == self._pth_contents.strip(): + self.install_lib = self.install_libbase + + +setuptools.setup( + cmdclass={"install": install_with_pth}, +) diff --git a/debugger-support/briefcase-pdb-debugger-support/src/briefcase_pdb_debugger_support/__init__.py b/debugger-support/briefcase-pdb-debugger-support/src/briefcase_pdb_debugger_support/__init__.py new file mode 100644 index 000000000..2faaa462a --- /dev/null +++ b/debugger-support/briefcase-pdb-debugger-support/src/briefcase_pdb_debugger_support/__init__.py @@ -0,0 +1,43 @@ +import os +import sys +import traceback + +from ._remote_debugger import start_pdb + +REMOTE_DEBUGGER_STARTED = False + + +def start_remote_debugger(): + global REMOTE_DEBUGGER_STARTED + REMOTE_DEBUGGER_STARTED = True + + # check verbose output + verbose = os.environ.get("BRIEFCASE_DEBUG", "0") == "1" + + # reading config + config_str = os.environ.get("BRIEFCASE_DEBUGGER", None) + + # skip debugger if no config is set + if config_str is None: + if verbose: + print( + "No 'BRIEFCASE_DEBUGGER' environment variable found. Debugger not starting." + ) + return # If BRIEFCASE_DEBUGGER is not set, this packages does nothing... + + if verbose: + print(f"'BRIEFCASE_DEBUGGER'={config_str}") + + # start debugger + print("Starting remote debugger...") + start_pdb(config_str, verbose) + + +# only start remote debugger on the first import +if not REMOTE_DEBUGGER_STARTED: + try: + start_remote_debugger() + except Exception: + # Show exception and stop the whole application when an error occurs + print(traceback.format_exc()) + sys.exit(-1) diff --git a/debugger-support/briefcase-pdb-debugger-support/src/briefcase_pdb_debugger_support/_remote_debugger.py b/debugger-support/briefcase-pdb-debugger-support/src/briefcase_pdb_debugger_support/_remote_debugger.py new file mode 100644 index 000000000..7d7e3f424 --- /dev/null +++ b/debugger-support/briefcase-pdb-debugger-support/src/briefcase_pdb_debugger_support/_remote_debugger.py @@ -0,0 +1,30 @@ +import json +import sys + +from remote_pdb import RemotePdb + + +def start_pdb(config_str: str, verbose: bool): + """Start remote PDB server.""" + debugger_config: dict = json.loads(config_str) + + # Parsing host/port + host = debugger_config["host"] + port = debugger_config["port"] + + print( + f""" +Remote PDB server opened at {host}:{port}, waiting for connection... +To connect to remote PDB use eg.: + - telnet {host} {port} (Windows, Linux) + - rlwrap socat - tcp:{host}:{port} (Linux, macOS) +""" + ) + + # Create a RemotePdb instance + remote_pdb = RemotePdb(host, port, quiet=True) + + # Connect the remote PDB with the "breakpoint()" function + sys.breakpointhook = remote_pdb.set_trace + + print("Debugger client attached.") diff --git a/src/briefcase/commands/create.py b/src/briefcase/commands/create.py index d650fa23a..f3a645021 100644 --- a/src/briefcase/commands/create.py +++ b/src/briefcase/commands/create.py @@ -689,8 +689,7 @@ def install_app_requirements(self, app: AppConfig): requires.extend(app.test_requires) if app.debugger: - debugger_support_pkg = self.create_debugger_support_pkg(app) - requires.append(str(debugger_support_pkg.absolute())) + requires.append(app.debugger.debugger_support_pkg) try: requirements_path = self.app_requirements_path(app) @@ -755,27 +754,6 @@ def install_app_code(self, app: AppConfig): / f"{app.module_name}-{app.version}.dist-info", ) - def create_debugger_support_pkg(self, app: AppConfig) -> Path: - """ - Create the debugger support package. - - This package is used to inject debugger support into the app when it is - run in debug mode. It is necessary to create this as own package, because - the code is automatically started via an .pth file and the .pth file - has to be located in the app's site-packages directory, that it is executed - correctly. - """ - # Remove existing debugger support folder if it exists - debugger_support_path = self.bundle_path(app) / ".debugger_support_package" - if debugger_support_path.exists(): - self.tools.shutil.rmtree(debugger_support_path) - self.tools.os.mkdir(debugger_support_path) - - # Create files for the debugger support package - app.debugger.create_debugger_support_pkg(debugger_support_path) - - return debugger_support_path - def install_image(self, role, variant, size, source, target): """Install an icon/image of the requested size at a target location, using the source images defined by the app config. diff --git a/src/briefcase/debuggers/base.py b/src/briefcase/debuggers/base.py index dff3b4f1e..c53479458 100644 --- a/src/briefcase/debuggers/base.py +++ b/src/briefcase/debuggers/base.py @@ -2,7 +2,6 @@ import enum from abc import ABC, abstractmethod -from pathlib import Path from typing import TypedDict @@ -37,135 +36,7 @@ class BaseDebugger(ABC): def connection_mode(self) -> DebuggerConnectionMode: """Return the connection mode of the debugger.""" + @property @abstractmethod - def create_debugger_support_pkg(self, dir: Path) -> None: - """Create the support package for the debugger. - This package will be installed inside the packaged app bundle. - - :param dir: Directory where the support package should be created. - """ - - def _create_debugger_support_pkg_base( - self, dir: Path, dependencies: list[str] - ) -> None: - """Create the base for the support package for the debugger. - - :param dir: Directory where the support package should be created. - :param dependencies: List of dependencies to include in the package. - """ - pyproject = dir / "pyproject.toml" - setup = dir / "setup.py" - debugger_support = dir / "briefcase_debugger_support" / "__init__.py" - debugger_support.parent.mkdir(parents=True) - - pyproject.write_text( - f"""\ -[build-system] -requires = ["setuptools>=61.0", "wheel"] -build-backend = "setuptools.build_meta" - -[project] -name = "briefcase-debugger-support" -version = "0.1.0" -description = "Add-on for briefcase to add remote debugging." -license = {{ file = "MIT" }} -dependencies = {dependencies} -""", - encoding="utf-8", - ) - - setup.write_text( - '''\ -import os -import setuptools -from setuptools.command.install import install - -# Copied from setuptools: -# (https://github.com/pypa/setuptools/blob/7c859e017368360ba66c8cc591279d8964c031bc/setup.py#L40C6-L82) -class install_with_pth(install): - """ - Custom install command to install a .pth file for distutils patching. - - This hack is necessary because there's no standard way to install behavior - on startup (and it's debatable if there should be one). This hack (ab)uses - the `extra_path` behavior in Setuptools to install a `.pth` file with - implicit behavior on startup to give higher precedence to the local version - of `distutils` over the version from the standard library. - - Please do not replicate this behavior. - """ - - _pth_name = 'briefcase_debugger_support' - _pth_contents = "import briefcase_debugger_support" - - def initialize_options(self): - install.initialize_options(self) - self.extra_path = self._pth_name, self._pth_contents - - def finalize_options(self): - install.finalize_options(self) - self._restore_install_lib() - - def _restore_install_lib(self): - """ - Undo secondary effect of `extra_path` adding to `install_lib` - """ - suffix = os.path.relpath(self.install_lib, self.install_libbase) - - if suffix.strip() == self._pth_contents.strip(): - self.install_lib = self.install_libbase - -setuptools.setup( - cmdclass={'install': install_with_pth}, -) -''', - encoding="utf-8", - ) - - debugger_support.write_text( - """\ -import json -import os -import sys -import traceback -from briefcase_debugger_support._remote_debugger import _start_remote_debugger - -REMOTE_DEBUGGER_STARTED = False - -def start_remote_debugger(): - global REMOTE_DEBUGGER_STARTED - REMOTE_DEBUGGER_STARTED = True - - # check verbose output - verbose = True if os.environ.get("BRIEFCASE_DEBUG", "0") == "1" else False - - # reading config - config_str = os.environ.get("BRIEFCASE_DEBUGGER", None) - - # skip debugger if no config is set - if config_str is None: - if verbose: - print( - "No 'BRIEFCASE_DEBUGGER' environment variable found. Debugger not starting." - ) - return # If BRIEFCASE_DEBUGGER is not set, this packages does nothing... - - if verbose: - print(f"'BRIEFCASE_DEBUGGER'={config_str}") - - # start debugger - print("Starting remote debugger...") - _start_remote_debugger(config_str, verbose) - - -# only start remote debugger on the first import -if REMOTE_DEBUGGER_STARTED == False: - try: - start_remote_debugger() - except Exception: - # Show exception and stop the whole application when an error occurs - print(traceback.format_exc()) - sys.exit(-1) -""", - encoding="utf-8", - ) + def debugger_support_pkg(self) -> str: + """Get the name of the debugger support package""" diff --git a/src/briefcase/debuggers/debugpy.py b/src/briefcase/debuggers/debugpy.py index 3fe9869bf..7aa5b5d21 100644 --- a/src/briefcase/debuggers/debugpy.py +++ b/src/briefcase/debuggers/debugpy.py @@ -1,5 +1,4 @@ -from pathlib import Path - +import briefcase from briefcase.debuggers.base import BaseDebugger, DebuggerConnectionMode @@ -11,150 +10,7 @@ def connection_mode(self) -> DebuggerConnectionMode: """Return the connection mode of the debugger.""" return DebuggerConnectionMode.SERVER - def create_debugger_support_pkg(self, dir: Path) -> None: - """Create the support package for the debugger. - This package will be installed inside the packaged app bundle. - - :param dir: Directory where the support package should be created. - """ - self._create_debugger_support_pkg_base( - dir, - dependencies=["debugpy>=1.8.14,<2.0.0"], - ) - - remote_debugger = dir / "briefcase_debugger_support" / "_remote_debugger.py" - remote_debugger.write_text( - '''\ -import json -import os -import re -import sys -import traceback -from pathlib import Path -from typing import List, Optional, Tuple, TypedDict - -import debugpy - -class AppPathMappings(TypedDict): - device_sys_path_regex: str - device_subfolders: list[str] - host_folders: list[str] - - -class AppPackagesPathMappings(TypedDict): - sys_path_regex: str - host_folder: str - - -class DebuggerConfig(TypedDict): - host: str - port: int - app_path_mappings: AppPathMappings | None - app_packages_path_mappings: AppPackagesPathMappings | None - - -def _load_path_mappings(config: DebuggerConfig, verbose: bool) -> List[Tuple[str, str]]: - app_path_mappings = config.get("app_path_mappings", None) - app_packages_path_mappings = config.get("app_packages_path_mappings", None) - - mappings_list = [] - if app_path_mappings: - device_app_folder = next( - ( - p - for p in sys.path - if re.search(app_path_mappings["device_sys_path_regex"], p) - ), - None, - ) - if device_app_folder: - for app_subfolder_device, app_subfolder_host in zip( - app_path_mappings["device_subfolders"], - app_path_mappings["host_folders"], - ): - mappings_list.append( - ( - app_subfolder_host, - str(Path(device_app_folder) / app_subfolder_device), - ) - ) - if app_packages_path_mappings: - device_app_packages_folder = next( - ( - p - for p in sys.path - if re.search(app_packages_path_mappings["sys_path_regex"], p) - ), - None, - ) - if device_app_packages_folder: - mappings_list.append( - ( - app_packages_path_mappings["host_folder"], - str(Path(device_app_packages_folder)), - ) - ) - - if verbose: - print("Extracted path mappings:") - for idx, p in enumerate(mappings_list): - print(f"[{idx}] host = {p[0]}") - print(f"[{idx}] device = {p[1]}") - - return mappings_list - - -def _start_remote_debugger(config_str: str, verbose: bool): - # Parsing config json - debugger_config: dict = json.loads(config_str) - - host = debugger_config["host"] - port = debugger_config["port"] - path_mappings = _load_path_mappings(debugger_config, verbose) - - # When an app is bundled with briefcase "os.__file__" is not set at runtime - # on some platforms (eg. windows). But debugpy accesses it internally, so it - # has to be set or an Exception is raised from debugpy. - if not hasattr(os, "__file__"): - if verbose: - print("'os.__file__' not available. Patching it...") - os.__file__ = "" - - # Starting remote debugger... - print(f"Starting debugpy in server mode at {host}:{port}...") - debugpy.listen((host, port), in_process_debug_adapter=True) - - if len(path_mappings) > 0: - if verbose: - print("Adding path mappings...") - - import pydevd_file_utils - - pydevd_file_utils.setup_client_server_paths(path_mappings) - - print("The debugpy server started. Waiting for debugger to attach...") - print( - f""" -To connect to debugpy using VSCode add the following configuration to launch.json: -{{ - "version": "0.2.0", - "configurations": [ - {{ - "name": "Briefcase: Attach (Connect)", - "type": "debugpy", - "request": "attach", - "connect": {{ - "host": "{host}", - "port": {port} - }} - }} - ] -}} -""" - ) - debugpy.wait_for_client() - - print("Debugger attached.") -''', - encoding="utf-8", - ) + @property + def debugger_support_pkg(self) -> str: + """Get the name of the debugger support package""" + return f"briefcase-debugpy-debugger-support=={briefcase.__version__}" diff --git a/src/briefcase/debuggers/pdb.py b/src/briefcase/debuggers/pdb.py index 6d9a1b125..4fb74f67b 100644 --- a/src/briefcase/debuggers/pdb.py +++ b/src/briefcase/debuggers/pdb.py @@ -1,5 +1,4 @@ -from pathlib import Path - +import briefcase from briefcase.debuggers.base import BaseDebugger, DebuggerConnectionMode @@ -11,49 +10,7 @@ def connection_mode(self) -> DebuggerConnectionMode: """Return the connection mode of the debugger.""" return DebuggerConnectionMode.SERVER - def create_debugger_support_pkg(self, dir: Path) -> None: - """Create the support package for the debugger. - This package will be installed inside the packaged app bundle. - - :param dir: Directory where the support package should be created. - """ - self._create_debugger_support_pkg_base( - dir, - dependencies=["remote-pdb>=2.1.0,<3.0.0"], - ) - - remote_debugger = dir / "briefcase_debugger_support" / "_remote_debugger.py" - remote_debugger.write_text( - '''\ -import json -import sys - -from remote_pdb import RemotePdb - -def _start_remote_debugger(config_str: str, verbose: bool): - """Start remote PDB server.""" - debugger_config: dict = json.loads(config_str) - - # Parsing host/port - host = debugger_config["host"] - port = debugger_config["port"] - - print( - f""" -Remote PDB server opened at {host}:{port}, waiting for connection... -To connect to remote PDB use eg.: - - telnet {host} {port} (Windows, Linux) - - rlwrap socat - tcp:{host}:{port} (Linux, macOS) -""" - ) - - # Create a RemotePdb instance - remote_pdb = RemotePdb(host, port, quiet=True) - - # Connect the remote PDB with the "breakpoint()" function - sys.breakpointhook = remote_pdb.set_trace - - print("Debugger client attached.") -''', - encoding="utf-8", - ) + @property + def debugger_support_pkg(self) -> str: + """Get the name of the debugger support package""" + return f"briefcase-pdb-debugger-support=={briefcase.__version__}" diff --git a/tests/commands/create/test_install_app_requirements.py b/tests/commands/create/test_install_app_requirements.py index 3a78c784e..50980a730 100644 --- a/tests/commands/create/test_install_app_requirements.py +++ b/tests/commands/create/test_install_app_requirements.py @@ -2,7 +2,6 @@ import os import subprocess import sys -from pathlib import Path from unittest import mock import pytest @@ -1096,9 +1095,10 @@ class DummyDebugger(BaseDebugger): def connection_mode(self) -> DebuggerConnectionMode: raise NotImplementedError - def create_debugger_support_pkg(self, dir: Path) -> None: - self.debugger_support_pkg_dir = dir - (dir / "dummy.py").write_text("# Dummy", encoding="utf8") + @property + def debugger_support_pkg(self) -> str: + """Get the name of the debugger support package""" + return "briefcase-dummy-debugger-support" def test_app_packages_debugger( @@ -1131,7 +1131,7 @@ def test_app_packages_debugger( "first", "second==1.2.3", "third>=3.2.1", - f"{bundle_path / '.debugger_support_package'}", + "briefcase-dummy-debugger-support", ], check=True, encoding="UTF-8", @@ -1139,68 +1139,3 @@ def test_app_packages_debugger( # Original app definitions haven't changed assert myapp.requires == ["first", "second==1.2.3", "third>=3.2.1"] - - # The debugger support package directory was created - assert ( - myapp.debugger.debugger_support_pkg_dir - == bundle_path / ".debugger_support_package" - ) - - # Check that the debugger support package exists - assert (bundle_path / ".debugger_support_package").exists() - assert (bundle_path / ".debugger_support_package" / "dummy.py").exists() - - -def test_app_packages_debugger_clear_old_package( - create_command, - myapp, - bundle_path, - app_packages_path, - app_packages_path_index, -): - """If an app has debug requirements and we're in debug mode, they are installed.""" - myapp.requires = ["first", "second==1.2.3", "third>=3.2.1"] - myapp.debugger = DummyDebugger() - - # create dummy debugger support package directory, that should be cleared - (bundle_path / ".debugger_support_package").mkdir(parents=True, exist_ok=True) - (bundle_path / ".debugger_support_package" / "some_old_file.py").write_text( - "# some old file content", encoding="utf8" - ) - - create_command.install_app_requirements(myapp) - - # A request was made to install requirements - create_command.tools[myapp].app_context.run.assert_called_with( - [ - sys.executable, - "-u", - "-X", - "utf8", - "-m", - "pip", - "install", - "--disable-pip-version-check", - "--upgrade", - "--no-user", - f"--target={app_packages_path}", - "first", - "second==1.2.3", - "third>=3.2.1", - f"{bundle_path / '.debugger_support_package'}", - ], - check=True, - encoding="UTF-8", - ) - - # Original app definitions haven't changed - assert myapp.requires == ["first", "second==1.2.3", "third>=3.2.1"] - - # The debugger support package directory was created - assert ( - myapp.debugger.debugger_support_pkg_dir - == bundle_path / ".debugger_support_package" - ) - - # Check that "some_old_file.py" got deleted - assert os.listdir(bundle_path / ".debugger_support_package") == ["dummy.py"] diff --git a/tests/debuggers/test_base.py b/tests/debuggers/test_base.py index d121fe2bb..2b65859ae 100644 --- a/tests/debuggers/test_base.py +++ b/tests/debuggers/test_base.py @@ -1,15 +1,5 @@ -import py_compile -import sys -from pathlib import Path -from tempfile import TemporaryDirectory - import pytest -if sys.version_info >= (3, 11): # pragma: no-cover-if-lt-py311 - import tomllib -else: # pragma: no-cover-if-gte-py311 - import tomli as tomllib - from briefcase.debuggers import ( DebugpyDebugger, PdbDebugger, @@ -57,24 +47,6 @@ def test_debugger(debugger_name, expected_class, connection_mode): debugger = get_debugger(debugger_name) assert isinstance(debugger, expected_class) assert debugger.connection_mode == connection_mode - - with TemporaryDirectory() as tmp_path: - tmp_path = Path(tmp_path) - debugger.create_debugger_support_pkg(tmp_path) - - # Try to parse pyproject.toml to check for toml-format errors - with (tmp_path / "pyproject.toml").open("rb") as f: - tomllib.load(f) - - # try to compile to check existence and for syntax errors - assert py_compile.compile(tmp_path / "setup.py") is not None - assert ( - py_compile.compile(tmp_path / "briefcase_debugger_support" / "__init__.py") - is not None - ) - assert ( - py_compile.compile( - tmp_path / "briefcase_debugger_support" / "_remote_debugger.py" - ) - is not None - ) + assert ( + f"briefcase-{debugger_name}-debugger-support" in debugger.debugger_support_pkg + ) From 7772c79558feb766ca376f2c6fca92e4e08e8e78 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Wed, 25 Jun 2025 21:56:16 +0200 Subject: [PATCH 057/131] Added Windows debugging support --- src/briefcase/commands/run.py | 74 +++++++++++++++++- src/briefcase/platforms/windows/__init__.py | 23 +++++- src/briefcase/platforms/windows/app.py | 2 + .../platforms/windows/visualstudio.py | 2 + .../create/test_install_app_requirements.py | 2 - tests/platforms/conftest.py | 17 +++++ tests/platforms/windows/app/test_run.py | 75 +++++++++++++++++-- .../windows/visualstudio/test_run.py | 64 +++++++++++++++- 8 files changed, 248 insertions(+), 11 deletions(-) diff --git a/src/briefcase/commands/run.py b/src/briefcase/commands/run.py index e6306e423..bd1e9b18a 100644 --- a/src/briefcase/commands/run.py +++ b/src/briefcase/commands/run.py @@ -1,11 +1,18 @@ from __future__ import annotations +import json import re import subprocess from abc import abstractmethod from contextlib import suppress +from pathlib import Path from briefcase.config import AppConfig +from briefcase.debuggers.base import ( + AppPackagesPathMappings, + AppPathMappings, + DebuggerConfig, +) from briefcase.exceptions import BriefcaseCommandError, BriefcaseTestSuiteFailure from briefcase.integrations.subprocess import StopStreaming @@ -235,13 +242,72 @@ def add_options(self, parser): required=False, ) - def _prepare_app_kwargs(self, app: AppConfig): + def _debugger_app_path_mappings(self, app: AppConfig) -> AppPathMappings: + """ + Get the path mappings for the app code. + + :param app: The config object for the app + :returns: The path mappings for the app code + """ + device_subfolders = [] + host_folders = [] + for src in app.all_sources(): + original = Path(self.base_path / src) + device_subfolders.append(original.name) + host_folders.append(f"{original.absolute()}") + return AppPathMappings( + device_sys_path_regex="app$", + device_subfolders=device_subfolders, + host_folders=host_folders, + ) + + def _debugger_app_packages_path_mapping( + self, app: AppConfig + ) -> AppPackagesPathMappings: + """ + Get the path mappings for the app packages. + + :param app: The config object for the app + :returns: The path mappings for the app packages + """ + raise NotImplementedError + + def debugger_config( + self, + app: AppConfig, + debugger_host: str, + debugger_port: int, + ) -> str: + """ + Create the remote debugger configuration that should be saved as environment variable for this run. + + :param app: The app to be debugged + :returns: The remote debugger configuration + """ + app_path_mappings = self._debugger_app_path_mappings(app) + app_packages_path_mappings = self._debugger_app_packages_path_mapping(app) + config = DebuggerConfig( + host=debugger_host, + port=debugger_port, + app_path_mappings=app_path_mappings, + app_packages_path_mappings=app_packages_path_mappings, + ) + return json.dumps(config) + + def _prepare_app_kwargs( + self, + app: AppConfig, + debugger_host: str | None = None, + debugger_port: int | None = None, + ): """Prepare the kwargs for running an app as a log stream. This won't be used by every backend; but it's a sufficiently common default that it's been factored out. :param app: The app to be launched + :param debugger_host: The host on which to run the debug server + :param debugger_port: The port on which to run the debug server :returns: A dictionary of additional arguments to pass to the Popen """ args = {} @@ -251,6 +317,12 @@ def _prepare_app_kwargs(self, app: AppConfig): if self.console.is_debug: env["BRIEFCASE_DEBUG"] = "1" + # If we're in remote debug mode, save the remote debugger config + if app.debugger and debugger_host and debugger_port: + env["BRIEFCASE_DEBUGGER"] = self.debugger_config( + app, debugger_host, debugger_port + ) + if app.test_mode: # In test mode, set a BRIEFCASE_MAIN_MODULE environment variable # to override the module at startup diff --git a/src/briefcase/platforms/windows/__init__.py b/src/briefcase/platforms/windows/__init__.py index c0d821f71..5f978481d 100644 --- a/src/briefcase/platforms/windows/__init__.py +++ b/src/briefcase/platforms/windows/__init__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import re import subprocess import uuid @@ -131,9 +133,24 @@ def _cleanup_app_support_package(self, support_path): class WindowsRunCommand(RunCommand): + supports_debugger = True + + def _debugger_app_packages_path_mapping(self, app: AppConfig) -> None: + """ + Get the path mappings for the app packages. + + :param app: The config object for the app + :returns: The path mappings for the app packages + """ + # No path mapping is required. The paths are automatically found, because + # developing an windows app also requires a windows host. + return None + def run_app( self, app: AppConfig, + debugger_host: str | None, + debugger_port: int | None, passthrough: list[str], **kwargs, ): @@ -143,7 +160,11 @@ def run_app( :param passthrough: The list of arguments to pass to the app """ # Set up the log stream - kwargs = self._prepare_app_kwargs(app=app) + kwargs = self._prepare_app_kwargs( + app=app, + debugger_host=debugger_host, + debugger_port=debugger_port, + ) # Console apps must operate in non-streaming mode so that console input can # be handled correctly. However, if we're in test mode, we *must* stream so diff --git a/src/briefcase/platforms/windows/app.py b/src/briefcase/platforms/windows/app.py index 1de179d80..58cf70fc6 100644 --- a/src/briefcase/platforms/windows/app.py +++ b/src/briefcase/platforms/windows/app.py @@ -30,6 +30,7 @@ class WindowsAppCreateCommand(WindowsAppMixin, WindowsCreateCommand): class WindowsAppUpdateCommand(WindowsAppCreateCommand, UpdateCommand): description = "Update an existing Windows app." + supports_debugger = True class WindowsAppOpenCommand(WindowsAppMixin, OpenCommand): @@ -38,6 +39,7 @@ class WindowsAppOpenCommand(WindowsAppMixin, OpenCommand): class WindowsAppBuildCommand(WindowsAppMixin, BuildCommand): description = "Build a Windows app." + supports_debugger = True def verify_tools(self): super().verify_tools() diff --git a/src/briefcase/platforms/windows/visualstudio.py b/src/briefcase/platforms/windows/visualstudio.py index 25369949f..96401f370 100644 --- a/src/briefcase/platforms/windows/visualstudio.py +++ b/src/briefcase/platforms/windows/visualstudio.py @@ -27,6 +27,7 @@ class WindowsVisualStudioCreateCommand(WindowsVisualStudioMixin, WindowsCreateCo class WindowsVisualStudioUpdateCommand(WindowsVisualStudioCreateCommand, UpdateCommand): description = "Update an existing Visual Studio project." + supports_debugger = True class WindowsVisualStudioOpenCommand(WindowsVisualStudioMixin, OpenCommand): @@ -35,6 +36,7 @@ class WindowsVisualStudioOpenCommand(WindowsVisualStudioMixin, OpenCommand): class WindowsVisualStudioBuildCommand(WindowsVisualStudioMixin, BuildCommand): description = "Build a Visual Studio project." + supports_debugger = True def verify_tools(self): super().verify_tools() diff --git a/tests/commands/create/test_install_app_requirements.py b/tests/commands/create/test_install_app_requirements.py index 50980a730..17e3f44cb 100644 --- a/tests/commands/create/test_install_app_requirements.py +++ b/tests/commands/create/test_install_app_requirements.py @@ -1089,8 +1089,6 @@ def test_app_packages_only_test_requires_test_mode( class DummyDebugger(BaseDebugger): - debugger_support_pkg_dir = None - @property def connection_mode(self) -> DebuggerConnectionMode: raise NotImplementedError diff --git a/tests/platforms/conftest.py b/tests/platforms/conftest.py index 4b975e68b..b33ec1a70 100644 --- a/tests/platforms/conftest.py +++ b/tests/platforms/conftest.py @@ -1,6 +1,7 @@ import pytest from briefcase.config import AppConfig +from briefcase.debuggers.base import BaseDebugger, DebuggerConnectionMode @pytest.fixture @@ -54,3 +55,19 @@ def underscore_app_config(first_app_config): requires=["foo==1.2.3", "bar>=4.5"], test_requires=["pytest"], ) + + +class DummyDebugger(BaseDebugger): + @property + def connection_mode(self) -> DebuggerConnectionMode: + raise NotImplementedError + + @property + def debugger_support_pkg(self) -> str: + raise NotImplementedError + + +@pytest.fixture +def dummy_debugger(): + """A dummy debugger for testing purposes.""" + return DummyDebugger() diff --git a/tests/platforms/windows/app/test_run.py b/tests/platforms/windows/app/test_run.py index 94ef24e9b..202e30ef7 100644 --- a/tests/platforms/windows/app/test_run.py +++ b/tests/platforms/windows/app/test_run.py @@ -1,3 +1,4 @@ +import json import subprocess from unittest import mock @@ -30,7 +31,9 @@ def test_run_gui_app(run_command, first_app_config, tmp_path): run_command.tools.subprocess.Popen.return_value = log_popen # Run the app - run_command.run_app(first_app_config, passthrough=[]) + run_command.run_app( + first_app_config, debugger_host=None, debugger_port=None, passthrough=[] + ) # The process was started run_command.tools.subprocess.Popen.assert_called_with( @@ -61,6 +64,8 @@ def test_run_gui_app_with_passthrough(run_command, first_app_config, tmp_path): # Run the app with args run_command.run_app( first_app_config, + debugger_host=None, + debugger_port=None, passthrough=["foo", "--bar"], ) @@ -93,7 +98,9 @@ def test_run_gui_app_failed(run_command, first_app_config, tmp_path): run_command.tools.subprocess.Popen.side_effect = OSError with pytest.raises(OSError): - run_command.run_app(first_app_config, passthrough=[]) + run_command.run_app( + first_app_config, debugger_host=None, debugger_port=None, passthrough=[] + ) # Popen was still invoked, though run_command.tools.subprocess.Popen.assert_called_with( @@ -118,7 +125,9 @@ def test_run_console_app(run_command, first_app_config, tmp_path): run_command.tools.subprocess.Popen.return_value = log_popen # Run the app - run_command.run_app(first_app_config, passthrough=[]) + run_command.run_app( + first_app_config, debugger_host=None, debugger_port=None, passthrough=[] + ) # The process was started run_command.tools.subprocess.run.assert_called_with( @@ -142,6 +151,8 @@ def test_run_console_app_with_passthrough(run_command, first_app_config, tmp_pat # Run the app with args run_command.run_app( first_app_config, + debugger_host=None, + debugger_port=None, passthrough=["foo", "--bar"], ) @@ -170,7 +181,9 @@ def test_run_console_app_failed(run_command, first_app_config, tmp_path): run_command.tools.subprocess.run.side_effect = OSError with pytest.raises(OSError): - run_command.run_app(first_app_config, passthrough=[]) + run_command.run_app( + first_app_config, debugger_host=None, debugger_port=None, passthrough=[] + ) # Popen was still invoked, though run_command.tools.subprocess.run.assert_called_with( @@ -197,7 +210,9 @@ def test_run_app_test_mode(run_command, first_app_config, is_console_app, tmp_pa run_command.tools.subprocess.Popen.return_value = log_popen # Run the app - run_command.run_app(first_app_config, passthrough=[]) + run_command.run_app( + first_app_config, debugger_host=None, debugger_port=None, passthrough=[] + ) # The process was started exe_name = "first-app" if is_console_app else "First App" @@ -238,6 +253,8 @@ def test_run_app_test_mode_with_passthrough( # Run the app with args run_command.run_app( first_app_config, + debugger_host=None, + debugger_port=None, passthrough=["foo", "--bar"], ) @@ -263,3 +280,51 @@ def test_run_app_test_mode_with_passthrough( popen=log_popen, clean_output=False, ) + + +def test_run_gui_app_debugger(run_command, first_app_config, tmp_path, dummy_debugger): + """A Windows app can be started in debug mode.""" + # Set up the log streamer to return a known stream + log_popen = mock.MagicMock() + run_command.tools.subprocess.Popen.return_value = log_popen + + first_app_config.debugger = dummy_debugger + + # Run the app + run_command.run_app( + first_app_config, + debugger_host="somehost", + debugger_port=9999, + passthrough=[], + ) + + # The process was started + run_command.tools.subprocess.Popen.assert_called_with( + [tmp_path / "base_path/build/first-app/windows/app/src/First App.exe"], + cwd=tmp_path / "home", + encoding="UTF-8", + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + bufsize=1, + env={ + "BRIEFCASE_DEBUGGER": json.dumps( + { + "host": "somehost", + "port": 9999, + "app_path_mappings": { + "device_sys_path_regex": "app$", + "device_subfolders": ["first_app"], + "host_folders": [str(tmp_path / "base_path/src/first_app")], + }, + "app_packages_path_mappings": None, + } + ) + }, + ) + + # The streamer was started + run_command._stream_app_logs.assert_called_once_with( + first_app_config, + popen=log_popen, + clean_output=False, + ) diff --git a/tests/platforms/windows/visualstudio/test_run.py b/tests/platforms/windows/visualstudio/test_run.py index 2700e9191..816bff0dc 100644 --- a/tests/platforms/windows/visualstudio/test_run.py +++ b/tests/platforms/windows/visualstudio/test_run.py @@ -1,6 +1,7 @@ # The run command inherits most of its behavior from the common base # implementation. Do a surface-level verification here, but the app # tests provide the actual test coverage. +import json import subprocess from unittest import mock @@ -34,7 +35,9 @@ def test_run_app(run_command, first_app_config, tmp_path): run_command.tools.subprocess.Popen.return_value = log_popen # Run the app - run_command.run_app(first_app_config, passthrough=[]) + run_command.run_app( + first_app_config, debugger_host=None, debugger_port=None, passthrough=[] + ) # Popen was called run_command.tools.subprocess.Popen.assert_called_with( @@ -67,6 +70,8 @@ def test_run_app_with_args(run_command, first_app_config, tmp_path): # Run the app with args run_command.run_app( first_app_config, + debugger_host=None, + debugger_port=None, passthrough=["foo", "--bar"], ) @@ -102,7 +107,9 @@ def test_run_app_test_mode(run_command, first_app_config, tmp_path): run_command.tools.subprocess.Popen.return_value = log_popen # Run the app in test mode - run_command.run_app(first_app_config, passthrough=[]) + run_command.run_app( + first_app_config, debugger_host=None, debugger_port=None, passthrough=[] + ) # Popen was called run_command.tools.subprocess.Popen.assert_called_with( @@ -137,6 +144,8 @@ def test_run_app_test_mode_with_args(run_command, first_app_config, tmp_path): # Run the app with args run_command.run_app( first_app_config, + debugger_host=None, + debugger_port=None, passthrough=["foo", "--bar"], ) @@ -162,3 +171,54 @@ def test_run_app_test_mode_with_args(run_command, first_app_config, tmp_path): popen=log_popen, clean_output=False, ) + + +def test_run_app_debugger(run_command, first_app_config, tmp_path, dummy_debugger): + """A windows Visual Studio project app can be started in debug mode.""" + first_app_config.debugger = dummy_debugger + + # Set up the log streamer to return a known stream with a good returncode + log_popen = mock.MagicMock() + run_command.tools.subprocess.Popen.return_value = log_popen + + # Run the app in test mode + run_command.run_app( + first_app_config, + debugger_host="somehost", + debugger_port=9999, + passthrough=[], + ) + + # Popen was called + run_command.tools.subprocess.Popen.assert_called_with( + [ + tmp_path + / "base_path/build/first-app/windows/visualstudio/x64/Release/First App.exe" + ], + cwd=tmp_path / "home", + encoding="UTF-8", + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + bufsize=1, + env={ + "BRIEFCASE_DEBUGGER": json.dumps( + { + "host": "somehost", + "port": 9999, + "app_path_mappings": { + "device_sys_path_regex": "app$", + "device_subfolders": ["first_app"], + "host_folders": [str(tmp_path / "base_path/src/first_app")], + }, + "app_packages_path_mappings": None, + } + ) + }, + ) + + # The streamer was started + run_command._stream_app_logs.assert_called_once_with( + first_app_config, + popen=log_popen, + clean_output=False, + ) From 6ffb51cd3a9029052ec1bbe802bb48ec79aeac77 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Thu, 26 Jun 2025 21:15:17 +0200 Subject: [PATCH 058/131] Added macOS debugging support --- src/briefcase/platforms/macOS/__init__.py | 35 ++++++++++++++++++-- src/briefcase/platforms/macOS/app.py | 3 ++ src/briefcase/platforms/macOS/xcode.py | 3 ++ tests/platforms/macOS/app/test_run.py | 39 ++++++++++++++++++----- tests/platforms/macOS/xcode/test_run.py | 12 +++++-- tests/test_cmdline.py | 3 ++ 6 files changed, 83 insertions(+), 12 deletions(-) diff --git a/src/briefcase/platforms/macOS/__init__.py b/src/briefcase/platforms/macOS/__init__.py index a66e46f0d..a259ebabe 100644 --- a/src/briefcase/platforms/macOS/__init__.py +++ b/src/briefcase/platforms/macOS/__init__.py @@ -389,15 +389,30 @@ def permissions_context(self, app: AppConfig, cross_platform: dict[str, str]): class macOSRunMixin: + def _debugger_app_packages_path_mapping(self, app: AppConfig) -> None: + """ + Get the path mappings for the app packages. + + :param app: The config object for the app + :returns: The path mappings for the app packages + """ + # No path mapping is required. The paths are automatically found, because + # developing an macOS app also requires a macOS host. + return None + def run_app( self, app: AppConfig, + debugger_host: str | None, + debugger_port: int | None, passthrough: list[str], **kwargs, ): """Start the application. :param app: The config object for the app + :param debugger_host: The host to use for the debugger + :param debugger_port: The port to use for the debugger :param passthrough: The list of arguments to pass to the app """ # Console apps must operate in non-streaming mode so that console input can @@ -406,12 +421,16 @@ def run_app( if app.console_app: self.run_console_app( app, + debugger_host=debugger_host, + debugger_port=debugger_port, passthrough=passthrough, **kwargs, ) else: self.run_gui_app( app, + debugger_host=debugger_host, + debugger_port=debugger_port, passthrough=passthrough, **kwargs, ) @@ -419,6 +438,8 @@ def run_app( def run_console_app( self, app: AppConfig, + debugger_host: str | None, + debugger_port: int | None, passthrough: list[str], **kwargs, ): @@ -427,7 +448,11 @@ def run_console_app( :param app: The config object for the app :param passthrough: The list of arguments to pass to the app """ - sub_kwargs = self._prepare_app_kwargs(app=app) + sub_kwargs = self._prepare_app_kwargs( + app=app, + debugger_host=debugger_host, + debugger_port=debugger_port, + ) cmdline = [self.binary_path(app) / f"Contents/MacOS/{app.formal_name}"] cmdline.extend(passthrough) @@ -466,6 +491,8 @@ def run_console_app( def run_gui_app( self, app: AppConfig, + debugger_host: str | None, + debugger_port: int | None, passthrough: list[str], **kwargs, ): @@ -513,7 +540,11 @@ def run_gui_app( app_pid = None try: # Set up the log stream - sub_kwargs = self._prepare_app_kwargs(app=app) + sub_kwargs = self._prepare_app_kwargs( + app=app, + debugger_host=debugger_host, + debugger_port=debugger_port, + ) # Start the app in a way that lets us stream the logs self.tools.subprocess.run( diff --git a/src/briefcase/platforms/macOS/app.py b/src/briefcase/platforms/macOS/app.py index d650ee307..da7bcd10a 100644 --- a/src/briefcase/platforms/macOS/app.py +++ b/src/briefcase/platforms/macOS/app.py @@ -89,6 +89,7 @@ def install_app_resources(self, app: AppConfig): class macOSAppUpdateCommand(macOSAppCreateCommand, UpdateCommand): description = "Update an existing macOS app." + supports_debugger = True class macOSAppOpenCommand(macOSAppMixin, OpenCommand): @@ -102,6 +103,7 @@ class macOSAppBuildCommand( BuildCommand, ): description = "Build a macOS app." + supports_debugger = True def build_app(self, app: AppConfig, **kwargs): """Build the macOS app. @@ -143,6 +145,7 @@ def build_app(self, app: AppConfig, **kwargs): class macOSAppRunCommand(macOSRunMixin, macOSAppMixin, RunCommand): description = "Run a macOS app." + supports_debugger = True class macOSAppPackageCommand(macOSPackageMixin, macOSAppMixin, PackageCommand): diff --git a/src/briefcase/platforms/macOS/xcode.py b/src/briefcase/platforms/macOS/xcode.py index 6497b4541..b59e6ca03 100644 --- a/src/briefcase/platforms/macOS/xcode.py +++ b/src/briefcase/platforms/macOS/xcode.py @@ -54,10 +54,12 @@ class macOSXcodeOpenCommand(macOSXcodeMixin, OpenCommand): class macOSXcodeUpdateCommand(macOSXcodeCreateCommand, UpdateCommand): description = "Update an existing macOS Xcode project." + supports_debugger = True class macOSXcodeBuildCommand(macOSXcodeMixin, BuildCommand): description = "Build a macOS Xcode project." + supports_debugger = True def build_app(self, app: BaseConfig, **kwargs): """Build the Xcode project for the application. @@ -94,6 +96,7 @@ def build_app(self, app: BaseConfig, **kwargs): class macOSXcodeRunCommand(macOSRunMixin, macOSXcodeMixin, RunCommand): description = "Run a macOS app." + supports_debugger = True class macOSXcodePackageCommand(macOSPackageMixin, macOSXcodeMixin, PackageCommand): diff --git a/tests/platforms/macOS/app/test_run.py b/tests/platforms/macOS/app/test_run.py index 7ae0413ec..5061ecedb 100644 --- a/tests/platforms/macOS/app/test_run.py +++ b/tests/platforms/macOS/app/test_run.py @@ -44,7 +44,9 @@ def test_run_gui_app(run_command, first_app_config, sleep_zero, tmp_path, monkey "briefcase.platforms.macOS.get_process_id_by_command", lambda *a, **kw: 100 ) - run_command.run_app(first_app_config, passthrough=[]) + run_command.run_app( + first_app_config, debugger_host=None, debugger_port=None, passthrough=[] + ) # Calls were made to start the app and to start a log stream. bin_path = run_command.binary_path(first_app_config) @@ -106,6 +108,8 @@ def test_run_gui_app_with_passthrough( # Run the app with args run_command.run_app( first_app_config, + debugger_host=None, + debugger_port=None, passthrough=["foo", "--bar"], ) @@ -157,7 +161,9 @@ def test_run_gui_app_failed(run_command, first_app_config, sleep_zero, tmp_path) ) with pytest.raises(BriefcaseCommandError): - run_command.run_app(first_app_config, passthrough=[]) + run_command.run_app( + first_app_config, debugger_host=None, debugger_port=None, passthrough=[] + ) # Calls were made to start the app and to start a log stream. bin_path = run_command.binary_path(first_app_config) @@ -204,7 +210,9 @@ def test_run_gui_app_find_pid_failed( ) with pytest.raises(BriefcaseCommandError) as exc_info: - run_command.run_app(first_app_config, passthrough=[]) + run_command.run_app( + first_app_config, debugger_host=None, debugger_port=None, passthrough=[] + ) # Calls were made to start the app and to start a log stream. bin_path = run_command.binary_path(first_app_config) @@ -257,7 +265,9 @@ def test_run_gui_app_test_mode( "briefcase.platforms.macOS.get_process_id_by_command", lambda *a, **kw: 100 ) - run_command.run_app(first_app_config, passthrough=[]) + run_command.run_app( + first_app_config, debugger_host=None, debugger_port=None, passthrough=[] + ) # Calls were made to start the app and to start a log stream. bin_path = run_command.binary_path(first_app_config) @@ -303,7 +313,9 @@ def test_run_console_app(run_command, first_app_config, tmp_path): # Set the app to be a console app first_app_config.console_app = True - run_command.run_app(first_app_config, passthrough=[]) + run_command.run_app( + first_app_config, debugger_host=None, debugger_port=None, passthrough=[] + ) # Calls were made to start the app and to start a log stream. bin_path = run_command.binary_path(first_app_config) @@ -332,6 +344,8 @@ def test_run_console_app_with_passthrough( # Run the app with args run_command.run_app( first_app_config, + debugger_host=None, + debugger_port=None, passthrough=["foo", "--bar"], ) @@ -358,7 +372,9 @@ def test_run_console_app_test_mode(run_command, first_app_config, sleep_zero, tm app_process = mock.MagicMock(spec_set=subprocess.Popen) run_command.tools.subprocess.Popen.return_value = app_process - run_command.run_app(first_app_config, passthrough=[]) + run_command.run_app( + first_app_config, debugger_host=None, debugger_port=None, passthrough=[] + ) # Calls were made to start the app and to start a log stream. bin_path = run_command.binary_path(first_app_config) @@ -395,7 +411,12 @@ def test_run_console_app_test_mode_with_passthrough( app_process = mock.MagicMock(spec_set=subprocess.Popen) run_command.tools.subprocess.Popen.return_value = app_process - run_command.run_app(first_app_config, passthrough=["foo", "--bar"]) + run_command.run_app( + first_app_config, + debugger_host=None, + debugger_port=None, + passthrough=["foo", "--bar"], + ) # Calls were made to start the app and to start a log stream. bin_path = run_command.binary_path(first_app_config) @@ -428,7 +449,9 @@ def test_run_console_app_failed(run_command, first_app_config, sleep_zero, tmp_p # Although the command raises an error, this could be because the script itself # raised an error. - run_command.run_app(first_app_config, passthrough=[]) + run_command.run_app( + first_app_config, debugger_host=None, debugger_port=None, passthrough=[] + ) # Calls were made to start the app and to start a log stream. bin_path = run_command.binary_path(first_app_config) diff --git a/tests/platforms/macOS/xcode/test_run.py b/tests/platforms/macOS/xcode/test_run.py index fc5e10b94..34d71e105 100644 --- a/tests/platforms/macOS/xcode/test_run.py +++ b/tests/platforms/macOS/xcode/test_run.py @@ -46,7 +46,9 @@ def test_run_app(run_command, first_app_config, sleep_zero, tmp_path, monkeypatc "briefcase.platforms.macOS.get_process_id_by_command", lambda *a, **kw: 100 ) - run_command.run_app(first_app_config, passthrough=[]) + run_command.run_app( + first_app_config, debugger_host=None, debugger_port=None, passthrough=[] + ) # Calls were made to start the app and to start a log stream. bin_path = run_command.binary_path(first_app_config) @@ -106,6 +108,8 @@ def test_run_app_with_passthrough( # Run the app with args run_command.run_app( first_app_config, + debugger_host=None, + debugger_port=None, passthrough=["foo", "--bar"], ) @@ -166,7 +170,9 @@ def test_run_app_test_mode( "briefcase.platforms.macOS.get_process_id_by_command", lambda *a, **kw: 100 ) - run_command.run_app(first_app_config, passthrough=[]) + run_command.run_app( + first_app_config, debugger_host=None, debugger_port=None, passthrough=[] + ) # Calls were made to start the app and to start a log stream. bin_path = run_command.binary_path(first_app_config) @@ -229,6 +235,8 @@ def test_run_app_test_mode_with_passthrough( # Run app in test mode with args run_command.run_app( first_app_config, + debugger_host=None, + debugger_port=None, passthrough=["foo", "--bar"], ) diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index 71e95f4a6..b177ca1a5 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -280,6 +280,9 @@ def test_run_command( "update_stub": False, "no_update": False, "test_mode": False, + "debugger": None, + "debugger_host": "localhost", + "debugger_port": 5678, "passthrough": [], **expected_options, } From 57d1d762e8d81310e73b4746634de765404452bd Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Thu, 26 Jun 2025 21:15:31 +0200 Subject: [PATCH 059/131] Update docs --- docs/reference/commands/build.rst | 1 + docs/reference/commands/run.rst | 2 ++ 2 files changed, 3 insertions(+) diff --git a/docs/reference/commands/build.rst b/docs/reference/commands/build.rst index 0d956d47e..7026b890f 100644 --- a/docs/reference/commands/build.rst +++ b/docs/reference/commands/build.rst @@ -125,6 +125,7 @@ Currently the following debuggers are supported (default is ``pdb``): - ``pdb``: This is used for debugging via console. - ``debugpy``: This is used for debugging via VSCode. +This is an experimental new feature, that is currently only support on Windows and macOS. ``--no-update`` --------------- diff --git a/docs/reference/commands/run.rst b/docs/reference/commands/run.rst index 02963c115..78e85a290 100644 --- a/docs/reference/commands/run.rst +++ b/docs/reference/commands/run.rst @@ -148,6 +148,8 @@ Currently the following debuggers are supported (default is ``pdb``): - ``pdb``: This is used for debugging via console. - ``debugpy``: This is used for debugging via VSCode. +This is an experimental new feature, that is currently only support on Windows and macOS. + ``--debugger-host `` -------------------------- From 6eaacd13240da7a94ed3fe5ed167b866feac08de Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Wed, 9 Jul 2025 22:13:38 +0200 Subject: [PATCH 060/131] Added detection if briefcase is installed as editable --- src/briefcase/debuggers/debugpy.py | 8 ++++++++ src/briefcase/debuggers/pdb.py | 6 ++++++ src/briefcase/utils.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 src/briefcase/utils.py diff --git a/src/briefcase/debuggers/debugpy.py b/src/briefcase/debuggers/debugpy.py index 7aa5b5d21..2adf64860 100644 --- a/src/briefcase/debuggers/debugpy.py +++ b/src/briefcase/debuggers/debugpy.py @@ -1,5 +1,6 @@ import briefcase from briefcase.debuggers.base import BaseDebugger, DebuggerConnectionMode +from briefcase.utils import IS_EDITABLE, REPO_ROOT class DebugpyDebugger(BaseDebugger): @@ -13,4 +14,11 @@ def connection_mode(self) -> DebuggerConnectionMode: @property def debugger_support_pkg(self) -> str: """Get the name of the debugger support package""" + if IS_EDITABLE and REPO_ROOT is not None: + local_path = ( + REPO_ROOT / "debugger-support/briefcase-debugpy-debugger-support" + ) + if local_path.exists() and local_path.is_dir(): + return str(local_path) + return f"briefcase-debugpy-debugger-support=={briefcase.__version__}" diff --git a/src/briefcase/debuggers/pdb.py b/src/briefcase/debuggers/pdb.py index 4fb74f67b..8700e7a53 100644 --- a/src/briefcase/debuggers/pdb.py +++ b/src/briefcase/debuggers/pdb.py @@ -1,5 +1,6 @@ import briefcase from briefcase.debuggers.base import BaseDebugger, DebuggerConnectionMode +from briefcase.utils import IS_EDITABLE, REPO_ROOT class PdbDebugger(BaseDebugger): @@ -13,4 +14,9 @@ def connection_mode(self) -> DebuggerConnectionMode: @property def debugger_support_pkg(self) -> str: """Get the name of the debugger support package""" + if IS_EDITABLE and REPO_ROOT is not None: + local_path = REPO_ROOT / "debugger-support/briefcase-pdb-debugger-support" + if local_path.exists() and local_path.is_dir(): + return str(local_path) + return f"briefcase-pdb-debugger-support=={briefcase.__version__}" diff --git a/src/briefcase/utils.py b/src/briefcase/utils.py new file mode 100644 index 000000000..3bc508bb8 --- /dev/null +++ b/src/briefcase/utils.py @@ -0,0 +1,29 @@ +import json +from importlib import metadata +from pathlib import Path + + +def _is_editable_pep610(dist_name: str) -> bool: + """Check if briefcase is installed as editable build. + + The check requires, that the tool that installs briefcase support PEP610 + (eg. pip since v20.1). + """ + try: + dist = metadata.distribution(dist_name) + except metadata.PackageNotFoundError: + raise + + direct_url = dist.read_text("direct_url.json") + if direct_url is None: + return False + + try: + data = json.loads(direct_url) + return data.get("dir_info", {}).get("editable", False) + except Exception: + return False + + +IS_EDITABLE = _is_editable_pep610("briefcase") +REPO_ROOT = Path(__file__).parent.parent.parent if IS_EDITABLE else None From 2cddf89d03d0c9ad26bd945dd612da3e0182fad8 Mon Sep 17 00:00:00 2001 From: timrid <6593626+timrid@users.noreply.github.com> Date: Thu, 10 Jul 2025 21:31:24 +0200 Subject: [PATCH 061/131] Added restriction hint for macOS. --- docs/reference/commands/run.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/reference/commands/run.rst b/docs/reference/commands/run.rst index 78e85a290..e249e29d9 100644 --- a/docs/reference/commands/run.rst +++ b/docs/reference/commands/run.rst @@ -150,6 +150,8 @@ Currently the following debuggers are supported (default is ``pdb``): This is an experimental new feature, that is currently only support on Windows and macOS. +On macOS you also have to specify ``min_os_version = "14.0"``to use ``debugpy``. + ``--debugger-host `` -------------------------- From 3aa88bf0ed5844e37695a56f9fe5b7e3d3eb91d0 Mon Sep 17 00:00:00 2001 From: timrid <6593626+timrid@users.noreply.github.com> Date: Thu, 10 Jul 2025 21:40:51 +0200 Subject: [PATCH 062/131] typo --- docs/reference/commands/run.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/commands/run.rst b/docs/reference/commands/run.rst index e249e29d9..417062124 100644 --- a/docs/reference/commands/run.rst +++ b/docs/reference/commands/run.rst @@ -150,7 +150,7 @@ Currently the following debuggers are supported (default is ``pdb``): This is an experimental new feature, that is currently only support on Windows and macOS. -On macOS you also have to specify ``min_os_version = "14.0"``to use ``debugpy``. +On macOS you also have to specify ``min_os_version = "14.0"`` to use ``debugpy``. ``--debugger-host `` -------------------------- From 456c373b05e9a161b096a82e1966df38077a9dba Mon Sep 17 00:00:00 2001 From: timrid <6593626+timrid@users.noreply.github.com> Date: Tue, 5 Aug 2025 19:53:02 +0200 Subject: [PATCH 063/131] more docstring optimizations --- .../briefcase-debugpy-debugger-support/setup.py | 7 ++----- debugger-support/briefcase-pdb-debugger-support/setup.py | 7 ++----- src/briefcase/debuggers/base.py | 2 +- src/briefcase/debuggers/debugpy.py | 2 +- src/briefcase/debuggers/pdb.py | 2 +- src/briefcase/utils.py | 4 ++-- 6 files changed, 9 insertions(+), 15 deletions(-) diff --git a/debugger-support/briefcase-debugpy-debugger-support/setup.py b/debugger-support/briefcase-debugpy-debugger-support/setup.py index 6b726c528..596675e11 100644 --- a/debugger-support/briefcase-debugpy-debugger-support/setup.py +++ b/debugger-support/briefcase-debugpy-debugger-support/setup.py @@ -7,8 +7,7 @@ # Copied from setuptools: # (https://github.com/pypa/setuptools/blob/7c859e017368360ba66c8cc591279d8964c031bc/setup.py#L40C6-L82) class install_with_pth(install): - """ - Custom install command to install a .pth file for distutils patching. + """Custom install command to install a .pth file for distutils patching. This hack is necessary because there's no standard way to install behavior on startup (and it's debatable if there should be one). This hack (ab)uses @@ -31,9 +30,7 @@ def finalize_options(self): self._restore_install_lib() def _restore_install_lib(self): - """ - Undo secondary effect of `extra_path` adding to `install_lib` - """ + """Undo secondary effect of `extra_path` adding to `install_lib`""" suffix = os.path.relpath(self.install_lib, self.install_libbase) if suffix.strip() == self._pth_contents.strip(): diff --git a/debugger-support/briefcase-pdb-debugger-support/setup.py b/debugger-support/briefcase-pdb-debugger-support/setup.py index 9130f3249..7c1a794d7 100644 --- a/debugger-support/briefcase-pdb-debugger-support/setup.py +++ b/debugger-support/briefcase-pdb-debugger-support/setup.py @@ -7,8 +7,7 @@ # Copied from setuptools: # (https://github.com/pypa/setuptools/blob/7c859e017368360ba66c8cc591279d8964c031bc/setup.py#L40C6-L82) class install_with_pth(install): - """ - Custom install command to install a .pth file for distutils patching. + """Custom install command to install a .pth file for distutils patching. This hack is necessary because there's no standard way to install behavior on startup (and it's debatable if there should be one). This hack (ab)uses @@ -31,9 +30,7 @@ def finalize_options(self): self._restore_install_lib() def _restore_install_lib(self): - """ - Undo secondary effect of `extra_path` adding to `install_lib` - """ + """Undo secondary effect of `extra_path` adding to `install_lib`""" suffix = os.path.relpath(self.install_lib, self.install_libbase) if suffix.strip() == self._pth_contents.strip(): diff --git a/src/briefcase/debuggers/base.py b/src/briefcase/debuggers/base.py index c53479458..70c437168 100644 --- a/src/briefcase/debuggers/base.py +++ b/src/briefcase/debuggers/base.py @@ -39,4 +39,4 @@ def connection_mode(self) -> DebuggerConnectionMode: @property @abstractmethod def debugger_support_pkg(self) -> str: - """Get the name of the debugger support package""" + """Get the name of the debugger support package.""" diff --git a/src/briefcase/debuggers/debugpy.py b/src/briefcase/debuggers/debugpy.py index 2adf64860..bf690cd84 100644 --- a/src/briefcase/debuggers/debugpy.py +++ b/src/briefcase/debuggers/debugpy.py @@ -13,7 +13,7 @@ def connection_mode(self) -> DebuggerConnectionMode: @property def debugger_support_pkg(self) -> str: - """Get the name of the debugger support package""" + """Get the name of the debugger support package.""" if IS_EDITABLE and REPO_ROOT is not None: local_path = ( REPO_ROOT / "debugger-support/briefcase-debugpy-debugger-support" diff --git a/src/briefcase/debuggers/pdb.py b/src/briefcase/debuggers/pdb.py index 8700e7a53..72c3a4c34 100644 --- a/src/briefcase/debuggers/pdb.py +++ b/src/briefcase/debuggers/pdb.py @@ -13,7 +13,7 @@ def connection_mode(self) -> DebuggerConnectionMode: @property def debugger_support_pkg(self) -> str: - """Get the name of the debugger support package""" + """Get the name of the debugger support package.""" if IS_EDITABLE and REPO_ROOT is not None: local_path = REPO_ROOT / "debugger-support/briefcase-pdb-debugger-support" if local_path.exists() and local_path.is_dir(): diff --git a/src/briefcase/utils.py b/src/briefcase/utils.py index 3bc508bb8..74e233113 100644 --- a/src/briefcase/utils.py +++ b/src/briefcase/utils.py @@ -6,8 +6,8 @@ def _is_editable_pep610(dist_name: str) -> bool: """Check if briefcase is installed as editable build. - The check requires, that the tool that installs briefcase support PEP610 - (eg. pip since v20.1). + The check requires, that the tool that installs briefcase support PEP610 (eg. pip + since v20.1). """ try: dist = metadata.distribution(dist_name) From 46a88cf54131a2f8b6bdddb04fd7b64018ac749f Mon Sep 17 00:00:00 2001 From: timrid <6593626+timrid@users.noreply.github.com> Date: Tue, 5 Aug 2025 19:56:09 +0200 Subject: [PATCH 064/131] remove restriction to min. macOS 14 --- docs/reference/commands/run.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/reference/commands/run.rst b/docs/reference/commands/run.rst index 417062124..78e85a290 100644 --- a/docs/reference/commands/run.rst +++ b/docs/reference/commands/run.rst @@ -150,8 +150,6 @@ Currently the following debuggers are supported (default is ``pdb``): This is an experimental new feature, that is currently only support on Windows and macOS. -On macOS you also have to specify ``min_os_version = "14.0"`` to use ``debugpy``. - ``--debugger-host `` -------------------------- From 202d15e60385ce225be019f72b3402b1e1f96d84 Mon Sep 17 00:00:00 2001 From: timrid <6593626+timrid@users.noreply.github.com> Date: Tue, 5 Aug 2025 19:56:19 +0200 Subject: [PATCH 065/131] correct changelog --- changes/2147.feature.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changes/2147.feature.rst b/changes/2147.feature.rst index b1f0974b9..40fcc169e 100644 --- a/changes/2147.feature.rst +++ b/changes/2147.feature.rst @@ -1 +1 @@ -Added basic functions for an ``--debug `` option. +Added basic functions for an ``--debug `` option for Windows and macOS. From 183c311b6e4dbb71782f136994bf2f2158836e71 Mon Sep 17 00:00:00 2001 From: timrid <6593626+timrid@users.noreply.github.com> Date: Tue, 5 Aug 2025 21:25:06 +0200 Subject: [PATCH 066/131] added missing unittests --- src/briefcase/commands/run.py | 4 +- src/briefcase/debuggers/debugpy.py | 7 +- src/briefcase/debuggers/pdb.py | 9 ++- src/briefcase/platforms/macOS/__init__.py | 10 --- src/briefcase/platforms/windows/__init__.py | 10 --- tests/debuggers/test_base.py | 25 +++++++ tests/platforms/macOS/app/test_run.py | 75 +++++++++++++++++++++ tests/test_utils.py | 38 +++++++++++ 8 files changed, 151 insertions(+), 27 deletions(-) create mode 100644 tests/test_utils.py diff --git a/src/briefcase/commands/run.py b/src/briefcase/commands/run.py index 626825088..4bbcc5287 100644 --- a/src/briefcase/commands/run.py +++ b/src/briefcase/commands/run.py @@ -268,7 +268,9 @@ def _debugger_app_packages_path_mapping( :param app: The config object for the app :returns: The path mappings for the app packages """ - raise NotImplementedError + # When developing an app on your host system for your host system, no path + # mapping is required. The paths are automatically found. + return None def debugger_config( self, diff --git a/src/briefcase/debuggers/debugpy.py b/src/briefcase/debuggers/debugpy.py index bf690cd84..1c430834b 100644 --- a/src/briefcase/debuggers/debugpy.py +++ b/src/briefcase/debuggers/debugpy.py @@ -1,6 +1,6 @@ import briefcase +import briefcase.utils from briefcase.debuggers.base import BaseDebugger, DebuggerConnectionMode -from briefcase.utils import IS_EDITABLE, REPO_ROOT class DebugpyDebugger(BaseDebugger): @@ -14,9 +14,10 @@ def connection_mode(self) -> DebuggerConnectionMode: @property def debugger_support_pkg(self) -> str: """Get the name of the debugger support package.""" - if IS_EDITABLE and REPO_ROOT is not None: + if briefcase.utils.IS_EDITABLE and briefcase.utils.REPO_ROOT is not None: local_path = ( - REPO_ROOT / "debugger-support/briefcase-debugpy-debugger-support" + briefcase.utils.REPO_ROOT + / "debugger-support/briefcase-debugpy-debugger-support" ) if local_path.exists() and local_path.is_dir(): return str(local_path) diff --git a/src/briefcase/debuggers/pdb.py b/src/briefcase/debuggers/pdb.py index 72c3a4c34..39f9b1ba7 100644 --- a/src/briefcase/debuggers/pdb.py +++ b/src/briefcase/debuggers/pdb.py @@ -1,6 +1,6 @@ import briefcase +import briefcase.utils from briefcase.debuggers.base import BaseDebugger, DebuggerConnectionMode -from briefcase.utils import IS_EDITABLE, REPO_ROOT class PdbDebugger(BaseDebugger): @@ -14,8 +14,11 @@ def connection_mode(self) -> DebuggerConnectionMode: @property def debugger_support_pkg(self) -> str: """Get the name of the debugger support package.""" - if IS_EDITABLE and REPO_ROOT is not None: - local_path = REPO_ROOT / "debugger-support/briefcase-pdb-debugger-support" + if briefcase.utils.IS_EDITABLE and briefcase.utils.REPO_ROOT is not None: + local_path = ( + briefcase.utils.REPO_ROOT + / "debugger-support/briefcase-pdb-debugger-support" + ) if local_path.exists() and local_path.is_dir(): return str(local_path) diff --git a/src/briefcase/platforms/macOS/__init__.py b/src/briefcase/platforms/macOS/__init__.py index 1e3ae7502..d545a8d46 100644 --- a/src/briefcase/platforms/macOS/__init__.py +++ b/src/briefcase/platforms/macOS/__init__.py @@ -390,16 +390,6 @@ def permissions_context(self, app: AppConfig, cross_platform: dict[str, str]): class macOSRunMixin: - def _debugger_app_packages_path_mapping(self, app: AppConfig) -> None: - """Get the path mappings for the app packages. - - :param app: The config object for the app - :returns: The path mappings for the app packages - """ - # No path mapping is required. The paths are automatically found, because - # developing an macOS app also requires a macOS host. - return None - def run_app( self, app: AppConfig, diff --git a/src/briefcase/platforms/windows/__init__.py b/src/briefcase/platforms/windows/__init__.py index cebad1bca..7e759ba4a 100644 --- a/src/briefcase/platforms/windows/__init__.py +++ b/src/briefcase/platforms/windows/__init__.py @@ -137,16 +137,6 @@ def _cleanup_app_support_package(self, support_path): class WindowsRunCommand(RunCommand): supports_debugger = True - def _debugger_app_packages_path_mapping(self, app: AppConfig) -> None: - """Get the path mappings for the app packages. - - :param app: The config object for the app - :returns: The path mappings for the app packages - """ - # No path mapping is required. The paths are automatically found, because - # developing an windows app also requires a windows host. - return None - def run_app( self, app: AppConfig, diff --git a/tests/debuggers/test_base.py b/tests/debuggers/test_base.py index 2b65859ae..ba3a7aa27 100644 --- a/tests/debuggers/test_base.py +++ b/tests/debuggers/test_base.py @@ -1,3 +1,6 @@ +from pathlib import Path +from tempfile import TemporaryDirectory + import pytest from briefcase.debuggers import ( @@ -50,3 +53,25 @@ def test_debugger(debugger_name, expected_class, connection_mode): assert ( f"briefcase-{debugger_name}-debugger-support" in debugger.debugger_support_pkg ) + + +@pytest.mark.parametrize( + "debugger_name", + ["pdb", "debugpy"], +) +def test_debugger_editable(debugger_name, monkeypatch): + with TemporaryDirectory() as tmp_path: + tmp_path = Path(tmp_path) + ( + tmp_path + / "debugger-support" + / f"briefcase-{debugger_name}-debugger-support" + ).mkdir(parents=True, exist_ok=True) + monkeypatch.setattr("briefcase.utils.IS_EDITABLE", True) + monkeypatch.setattr("briefcase.utils.REPO_ROOT", tmp_path) + + debugger = get_debugger(debugger_name) + assert ( + f"{tmp_path}/debugger-support/briefcase-{debugger_name}-debugger-support" + == debugger.debugger_support_pkg + ) diff --git a/tests/platforms/macOS/app/test_run.py b/tests/platforms/macOS/app/test_run.py index 5061ecedb..7dce458f8 100644 --- a/tests/platforms/macOS/app/test_run.py +++ b/tests/platforms/macOS/app/test_run.py @@ -1,3 +1,4 @@ +import json import subprocess from signal import SIGTERM from unittest import mock @@ -308,6 +309,80 @@ def test_run_gui_app_test_mode( run_command.tools.os.kill.assert_called_with(100, SIGTERM) +def test_run_gui_app_debugger( + run_command, first_app_config, sleep_zero, tmp_path, monkeypatch, dummy_debugger +): + """A macOS GUI app can be started in debug mode.""" + # Mock a popen object that represents the log stream + log_stream_process = mock.MagicMock(spec_set=subprocess.Popen) + run_command.tools.subprocess.Popen.return_value = log_stream_process + + first_app_config.debugger = dummy_debugger + + # Monkeypatch the tools get the process ID + monkeypatch.setattr( + "briefcase.platforms.macOS.get_process_id_by_command", lambda *a, **kw: 100 + ) + + run_command.run_app( + first_app_config, + debugger_host="somehost", + debugger_port=9999, + passthrough=[], + ) + + # Calls were made to start the app and to start a log stream. + bin_path = run_command.binary_path(first_app_config) + sender = bin_path / "Contents/MacOS/First App" + run_command.tools.subprocess.Popen.assert_called_with( + [ + "log", + "stream", + "--style", + "compact", + "--predicate", + f'senderImagePath=="{sender}"' + f' OR (processImagePath=="{sender}"' + ' AND senderImagePath=="/usr/lib/libffi.dylib")', + ], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + bufsize=1, + ) + run_command.tools.subprocess.run.assert_called_with( + ["open", "-n", bin_path], + cwd=tmp_path / "home", + check=True, + env={ + "BRIEFCASE_DEBUGGER": json.dumps( + { + "host": "somehost", + "port": 9999, + "app_path_mappings": { + "device_sys_path_regex": "app$", + "device_subfolders": ["first_app"], + "host_folders": [str(tmp_path / "base_path/src/first_app")], + }, + "app_packages_path_mappings": None, + } + ) + }, + ) + + # The log stream was started + run_command._stream_app_logs.assert_called_with( + first_app_config, + popen=log_stream_process, + clean_filter=macOS_log_clean_filter, + clean_output=True, + stop_func=mock.ANY, + log_stream=True, + ) + + # The app process was killed on exit. + run_command.tools.os.kill.assert_called_with(100, SIGTERM) + + def test_run_console_app(run_command, first_app_config, tmp_path): """A macOS console app can be started.""" # Set the app to be a console app diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 000000000..7b2cdebf2 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,38 @@ +import json +from importlib import metadata + +import pytest + +from briefcase.utils import _is_editable_pep610 + + +class DummyDist: + def __init__(self, direct_url): + self._direct_url = direct_url + + def read_text(self, name): + return self._direct_url if name == "direct_url.json" else None + + +@pytest.mark.parametrize( + "direct_url,is_editable", + [ + (json.dumps({"dir_info": {"editable": True}}), True), # editable + (json.dumps({"dir_info": {"editable": False}}), False), # not editable + (json.dumps({}), False), # missing dir_info + (None, False), # missing direct_url.json + ("not-json", False), # invalid JSON + ], +) +def test_is_editable_pep610(monkeypatch, direct_url, is_editable): + monkeypatch.setattr(metadata, "distribution", lambda name: DummyDist(direct_url)) + assert _is_editable_pep610("briefcase") is is_editable + + +def test_is_editable_pep610_package_not_found(monkeypatch): + def raise_not_found(name): + raise metadata.PackageNotFoundError + + monkeypatch.setattr(metadata, "distribution", raise_not_found) + with pytest.raises(metadata.PackageNotFoundError): + _is_editable_pep610("briefcase") From 92da28926a76d1372f860e2a5559c67a6b05554d Mon Sep 17 00:00:00 2001 From: timrid <6593626+timrid@users.noreply.github.com> Date: Tue, 5 Aug 2025 21:31:38 +0200 Subject: [PATCH 067/131] added missing test --- tests/debuggers/test_base.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/debuggers/test_base.py b/tests/debuggers/test_base.py index ba3a7aa27..bedb23f03 100644 --- a/tests/debuggers/test_base.py +++ b/tests/debuggers/test_base.py @@ -75,3 +75,20 @@ def test_debugger_editable(debugger_name, monkeypatch): f"{tmp_path}/debugger-support/briefcase-{debugger_name}-debugger-support" == debugger.debugger_support_pkg ) + + +@pytest.mark.parametrize( + "debugger_name", + ["pdb", "debugpy"], +) +def test_debugger_editable_path_not_found(debugger_name, monkeypatch): + with TemporaryDirectory() as tmp_path: + tmp_path = Path(tmp_path) + monkeypatch.setattr("briefcase.utils.IS_EDITABLE", True) + monkeypatch.setattr("briefcase.utils.REPO_ROOT", tmp_path) + + debugger = get_debugger(debugger_name) + assert ( + f"briefcase-{debugger_name}-debugger-support==" + in debugger.debugger_support_pkg + ) From 5a70721c37f1292e79c3bd535c6779bc2c6ba35c Mon Sep 17 00:00:00 2001 From: timrid <6593626+timrid@users.noreply.github.com> Date: Tue, 5 Aug 2025 21:40:51 +0200 Subject: [PATCH 068/131] fixed tests for windows --- src/briefcase/debuggers/debugpy.py | 3 ++- src/briefcase/debuggers/pdb.py | 3 ++- tests/debuggers/test_base.py | 6 +++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/briefcase/debuggers/debugpy.py b/src/briefcase/debuggers/debugpy.py index 1c430834b..9df5c48d4 100644 --- a/src/briefcase/debuggers/debugpy.py +++ b/src/briefcase/debuggers/debugpy.py @@ -17,7 +17,8 @@ def debugger_support_pkg(self) -> str: if briefcase.utils.IS_EDITABLE and briefcase.utils.REPO_ROOT is not None: local_path = ( briefcase.utils.REPO_ROOT - / "debugger-support/briefcase-debugpy-debugger-support" + / "debugger-support" + / "briefcase-debugpy-debugger-support" ) if local_path.exists() and local_path.is_dir(): return str(local_path) diff --git a/src/briefcase/debuggers/pdb.py b/src/briefcase/debuggers/pdb.py index 39f9b1ba7..8288fdd78 100644 --- a/src/briefcase/debuggers/pdb.py +++ b/src/briefcase/debuggers/pdb.py @@ -17,7 +17,8 @@ def debugger_support_pkg(self) -> str: if briefcase.utils.IS_EDITABLE and briefcase.utils.REPO_ROOT is not None: local_path = ( briefcase.utils.REPO_ROOT - / "debugger-support/briefcase-pdb-debugger-support" + / "debugger-support" + / "briefcase-pdb-debugger-support" ) if local_path.exists() and local_path.is_dir(): return str(local_path) diff --git a/tests/debuggers/test_base.py b/tests/debuggers/test_base.py index bedb23f03..bb755a962 100644 --- a/tests/debuggers/test_base.py +++ b/tests/debuggers/test_base.py @@ -72,7 +72,11 @@ def test_debugger_editable(debugger_name, monkeypatch): debugger = get_debugger(debugger_name) assert ( - f"{tmp_path}/debugger-support/briefcase-{debugger_name}-debugger-support" + str( + tmp_path + / "debugger-support" + / f"briefcase-{debugger_name}-debugger-support" + ) == debugger.debugger_support_pkg ) From c7f14c54d508a6b751a81f21fdc3a4238292942f Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Fri, 8 Aug 2025 23:21:43 +0200 Subject: [PATCH 069/131] moved editable check logic to debuggers folder --- src/briefcase/debuggers/base.py | 46 ++++++++++++++++++++++++++++++ src/briefcase/debuggers/debugpy.py | 19 ++++-------- src/briefcase/debuggers/pdb.py | 19 ++++-------- src/briefcase/utils.py | 29 ------------------- tests/debuggers/test_base.py | 44 ++++++++++++++++++++++++---- tests/test_utils.py | 38 ------------------------ 6 files changed, 97 insertions(+), 98 deletions(-) delete mode 100644 src/briefcase/utils.py delete mode 100644 tests/test_utils.py diff --git a/src/briefcase/debuggers/base.py b/src/briefcase/debuggers/base.py index 70c437168..a85f3b5fd 100644 --- a/src/briefcase/debuggers/base.py +++ b/src/briefcase/debuggers/base.py @@ -1,9 +1,55 @@ from __future__ import annotations import enum +import json from abc import ABC, abstractmethod +from importlib import metadata +from pathlib import Path from typing import TypedDict +import briefcase + + +def _is_editable_pep610(dist_name: str) -> bool: + """Check if briefcase is installed as editable build. + + The check requires, that the tool that installs briefcase support PEP610 (eg. pip + since v20.1). + """ + try: + dist = metadata.distribution(dist_name) + except metadata.PackageNotFoundError: + raise + + direct_url = dist.read_text("direct_url.json") + if direct_url is None: + return False + + try: + data = json.loads(direct_url) + return data.get("dir_info", {}).get("editable", False) + except Exception: + return False + + +IS_EDITABLE = _is_editable_pep610("briefcase") +REPO_ROOT = Path(__file__).parent.parent.parent.parent if IS_EDITABLE else None + + +def get_debugger_requirement(package_name: str): + """Get the requirement of a debugger support package. + + On editable installs of briefcase the path to the local package is used, to simplify + the development of the debugger support packages. On normal installs the local + version is not available, so the package from pypi is used, that corresponds to the + version of briefcase. + """ + if IS_EDITABLE and REPO_ROOT is not None: + local_path = REPO_ROOT / "debugger-support" / package_name + if local_path.exists() and local_path.is_dir(): + return str(local_path) + return f"{package_name}=={briefcase.__version__}" + class AppPathMappings(TypedDict): device_sys_path_regex: str diff --git a/src/briefcase/debuggers/debugpy.py b/src/briefcase/debuggers/debugpy.py index 9df5c48d4..2e1fd2d11 100644 --- a/src/briefcase/debuggers/debugpy.py +++ b/src/briefcase/debuggers/debugpy.py @@ -1,6 +1,8 @@ -import briefcase -import briefcase.utils -from briefcase.debuggers.base import BaseDebugger, DebuggerConnectionMode +from briefcase.debuggers.base import ( + BaseDebugger, + DebuggerConnectionMode, + get_debugger_requirement, +) class DebugpyDebugger(BaseDebugger): @@ -14,13 +16,4 @@ def connection_mode(self) -> DebuggerConnectionMode: @property def debugger_support_pkg(self) -> str: """Get the name of the debugger support package.""" - if briefcase.utils.IS_EDITABLE and briefcase.utils.REPO_ROOT is not None: - local_path = ( - briefcase.utils.REPO_ROOT - / "debugger-support" - / "briefcase-debugpy-debugger-support" - ) - if local_path.exists() and local_path.is_dir(): - return str(local_path) - - return f"briefcase-debugpy-debugger-support=={briefcase.__version__}" + return get_debugger_requirement("briefcase-debugpy-debugger-support") diff --git a/src/briefcase/debuggers/pdb.py b/src/briefcase/debuggers/pdb.py index 8288fdd78..c10553076 100644 --- a/src/briefcase/debuggers/pdb.py +++ b/src/briefcase/debuggers/pdb.py @@ -1,6 +1,8 @@ -import briefcase -import briefcase.utils -from briefcase.debuggers.base import BaseDebugger, DebuggerConnectionMode +from briefcase.debuggers.base import ( + BaseDebugger, + DebuggerConnectionMode, + get_debugger_requirement, +) class PdbDebugger(BaseDebugger): @@ -14,13 +16,4 @@ def connection_mode(self) -> DebuggerConnectionMode: @property def debugger_support_pkg(self) -> str: """Get the name of the debugger support package.""" - if briefcase.utils.IS_EDITABLE and briefcase.utils.REPO_ROOT is not None: - local_path = ( - briefcase.utils.REPO_ROOT - / "debugger-support" - / "briefcase-pdb-debugger-support" - ) - if local_path.exists() and local_path.is_dir(): - return str(local_path) - - return f"briefcase-pdb-debugger-support=={briefcase.__version__}" + return get_debugger_requirement("briefcase-pdb-debugger-support") diff --git a/src/briefcase/utils.py b/src/briefcase/utils.py deleted file mode 100644 index 74e233113..000000000 --- a/src/briefcase/utils.py +++ /dev/null @@ -1,29 +0,0 @@ -import json -from importlib import metadata -from pathlib import Path - - -def _is_editable_pep610(dist_name: str) -> bool: - """Check if briefcase is installed as editable build. - - The check requires, that the tool that installs briefcase support PEP610 (eg. pip - since v20.1). - """ - try: - dist = metadata.distribution(dist_name) - except metadata.PackageNotFoundError: - raise - - direct_url = dist.read_text("direct_url.json") - if direct_url is None: - return False - - try: - data = json.loads(direct_url) - return data.get("dir_info", {}).get("editable", False) - except Exception: - return False - - -IS_EDITABLE = _is_editable_pep610("briefcase") -REPO_ROOT = Path(__file__).parent.parent.parent if IS_EDITABLE else None diff --git a/tests/debuggers/test_base.py b/tests/debuggers/test_base.py index bb755a962..e6d837047 100644 --- a/tests/debuggers/test_base.py +++ b/tests/debuggers/test_base.py @@ -1,3 +1,5 @@ +import json +from importlib import metadata from pathlib import Path from tempfile import TemporaryDirectory @@ -9,10 +11,42 @@ get_debugger, get_debuggers, ) -from briefcase.debuggers.base import DebuggerConnectionMode +from briefcase.debuggers.base import DebuggerConnectionMode, _is_editable_pep610 from briefcase.exceptions import BriefcaseCommandError +class DummyDist: + def __init__(self, direct_url): + self._direct_url = direct_url + + def read_text(self, name): + return self._direct_url if name == "direct_url.json" else None + + +@pytest.mark.parametrize( + "direct_url,is_editable", + [ + (json.dumps({"dir_info": {"editable": True}}), True), # editable + (json.dumps({"dir_info": {"editable": False}}), False), # not editable + (json.dumps({}), False), # missing dir_info + (None, False), # missing direct_url.json + ("not-json", False), # invalid JSON + ], +) +def test_is_editable_pep610(monkeypatch, direct_url, is_editable): + monkeypatch.setattr(metadata, "distribution", lambda name: DummyDist(direct_url)) + assert _is_editable_pep610("briefcase") is is_editable + + +def test_is_editable_pep610_package_not_found(monkeypatch): + def raise_not_found(name): + raise metadata.PackageNotFoundError + + monkeypatch.setattr(metadata, "distribution", raise_not_found) + with pytest.raises(metadata.PackageNotFoundError): + _is_editable_pep610("briefcase") + + def test_get_debuggers(): debuggers = get_debuggers() assert isinstance(debuggers, dict) @@ -67,8 +101,8 @@ def test_debugger_editable(debugger_name, monkeypatch): / "debugger-support" / f"briefcase-{debugger_name}-debugger-support" ).mkdir(parents=True, exist_ok=True) - monkeypatch.setattr("briefcase.utils.IS_EDITABLE", True) - monkeypatch.setattr("briefcase.utils.REPO_ROOT", tmp_path) + monkeypatch.setattr("briefcase.debuggers.base.IS_EDITABLE", True) + monkeypatch.setattr("briefcase.debuggers.base.REPO_ROOT", tmp_path) debugger = get_debugger(debugger_name) assert ( @@ -88,8 +122,8 @@ def test_debugger_editable(debugger_name, monkeypatch): def test_debugger_editable_path_not_found(debugger_name, monkeypatch): with TemporaryDirectory() as tmp_path: tmp_path = Path(tmp_path) - monkeypatch.setattr("briefcase.utils.IS_EDITABLE", True) - monkeypatch.setattr("briefcase.utils.REPO_ROOT", tmp_path) + monkeypatch.setattr("briefcase.debuggers.base.IS_EDITABLE", True) + monkeypatch.setattr("briefcase.debuggers.base.REPO_ROOT", tmp_path) debugger = get_debugger(debugger_name) assert ( diff --git a/tests/test_utils.py b/tests/test_utils.py deleted file mode 100644 index 7b2cdebf2..000000000 --- a/tests/test_utils.py +++ /dev/null @@ -1,38 +0,0 @@ -import json -from importlib import metadata - -import pytest - -from briefcase.utils import _is_editable_pep610 - - -class DummyDist: - def __init__(self, direct_url): - self._direct_url = direct_url - - def read_text(self, name): - return self._direct_url if name == "direct_url.json" else None - - -@pytest.mark.parametrize( - "direct_url,is_editable", - [ - (json.dumps({"dir_info": {"editable": True}}), True), # editable - (json.dumps({"dir_info": {"editable": False}}), False), # not editable - (json.dumps({}), False), # missing dir_info - (None, False), # missing direct_url.json - ("not-json", False), # invalid JSON - ], -) -def test_is_editable_pep610(monkeypatch, direct_url, is_editable): - monkeypatch.setattr(metadata, "distribution", lambda name: DummyDist(direct_url)) - assert _is_editable_pep610("briefcase") is is_editable - - -def test_is_editable_pep610_package_not_found(monkeypatch): - def raise_not_found(name): - raise metadata.PackageNotFoundError - - monkeypatch.setattr(metadata, "distribution", raise_not_found) - with pytest.raises(metadata.PackageNotFoundError): - _is_editable_pep610("briefcase") From 2a1dc1b6baba634c9416acd3572ed0b9bdaece06 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sat, 9 Aug 2025 00:02:59 +0200 Subject: [PATCH 070/131] added hints and cosmetic changes --- .../briefcase_debugpy_debugger_support/_remote_debugger.py | 7 ++++--- .../src/briefcase_pdb_debugger_support/_remote_debugger.py | 1 + tests/commands/build/test_call.py | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/debugger-support/briefcase-debugpy-debugger-support/src/briefcase_debugpy_debugger_support/_remote_debugger.py b/debugger-support/briefcase-debugpy-debugger-support/src/briefcase_debugpy_debugger_support/_remote_debugger.py index f6b20b055..42090e16e 100644 --- a/debugger-support/briefcase-debugpy-debugger-support/src/briefcase_debugpy_debugger_support/_remote_debugger.py +++ b/debugger-support/briefcase-debugpy-debugger-support/src/briefcase_debugpy_debugger_support/_remote_debugger.py @@ -85,9 +85,8 @@ def start_debugpy(config_str: str, verbose: bool): port = debugger_config["port"] path_mappings = _load_path_mappings(debugger_config, verbose) - # When an app is bundled with briefcase "os.__file__" is not set at runtime - # on some platforms (eg. windows). But debugpy accesses it internally, so it - # has to be set or an Exception is raised from debugpy. + # There is a bug in debugpy that has to be handled until there is a new + # debugpy release, see https://github.com/microsoft/debugpy/issues/1943 if not hasattr(os, "__file__"): if verbose: print("'os.__file__' not available. Patching it...") @@ -101,6 +100,7 @@ def start_debugpy(config_str: str, verbose: bool): if verbose: print("Adding path mappings...") + # pydevd is dynamically loaded and only available after a debugger has connected import pydevd_file_utils pydevd_file_utils.setup_client_server_paths(path_mappings) @@ -128,3 +128,4 @@ def start_debugpy(config_str: str, verbose: bool): debugpy.wait_for_client() print("Debugger attached.") + print("-" * 75) diff --git a/debugger-support/briefcase-pdb-debugger-support/src/briefcase_pdb_debugger_support/_remote_debugger.py b/debugger-support/briefcase-pdb-debugger-support/src/briefcase_pdb_debugger_support/_remote_debugger.py index 7d7e3f424..b66e264bf 100644 --- a/debugger-support/briefcase-pdb-debugger-support/src/briefcase_pdb_debugger_support/_remote_debugger.py +++ b/debugger-support/briefcase-pdb-debugger-support/src/briefcase_pdb_debugger_support/_remote_debugger.py @@ -28,3 +28,4 @@ def start_pdb(config_str: str, verbose: bool): sys.breakpointhook = remote_pdb.set_trace print("Debugger client attached.") + print("-" * 75) diff --git a/tests/commands/build/test_call.py b/tests/commands/build/test_call.py index c998c650e..a85d7c263 100644 --- a/tests/commands/build/test_call.py +++ b/tests/commands/build/test_call.py @@ -1121,7 +1121,7 @@ def test_build_debug(build_command, first_app, second_app): def test_build_debug_unsupported(build_command, first_app, second_app): - """If the user requests a build with update and no-update, an error is raised.""" + """If the user requests a debugger but it is not supported by the build command.""" # Add two apps build_command.apps = { "first": first_app, From 0b879d55167cf1140d647e20ad767650c410e941 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Wed, 13 Aug 2025 20:49:14 +0200 Subject: [PATCH 071/131] removed "-debuger-support" from name --- .gitignore | 4 ++-- .../README.md | 0 .../pyproject.toml | 2 +- .../setup.py | 0 .../src/briefcase_debugpy_debugger_support/__init__.py | 0 .../briefcase_debugpy_debugger_support/_remote_debugger.py | 0 .../README.md | 0 .../pyproject.toml | 2 +- .../setup.py | 0 .../src/briefcase_pdb_debugger_support/__init__.py | 0 .../src/briefcase_pdb_debugger_support/_remote_debugger.py | 0 src/briefcase/debuggers/debugpy.py | 2 +- src/briefcase/debuggers/pdb.py | 2 +- 13 files changed, 6 insertions(+), 6 deletions(-) rename debugger-support/{briefcase-debugpy-debugger-support => briefcase-debugpy}/README.md (100%) rename debugger-support/{briefcase-debugpy-debugger-support => briefcase-debugpy}/pyproject.toml (90%) rename debugger-support/{briefcase-debugpy-debugger-support => briefcase-debugpy}/setup.py (100%) rename debugger-support/{briefcase-debugpy-debugger-support => briefcase-debugpy}/src/briefcase_debugpy_debugger_support/__init__.py (100%) rename debugger-support/{briefcase-debugpy-debugger-support => briefcase-debugpy}/src/briefcase_debugpy_debugger_support/_remote_debugger.py (100%) rename debugger-support/{briefcase-pdb-debugger-support => briefcase-pdb}/README.md (100%) rename debugger-support/{briefcase-pdb-debugger-support => briefcase-pdb}/pyproject.toml (91%) rename debugger-support/{briefcase-pdb-debugger-support => briefcase-pdb}/setup.py (100%) rename debugger-support/{briefcase-pdb-debugger-support => briefcase-pdb}/src/briefcase_pdb_debugger_support/__init__.py (100%) rename debugger-support/{briefcase-pdb-debugger-support => briefcase-pdb}/src/briefcase_pdb_debugger_support/_remote_debugger.py (100%) diff --git a/.gitignore b/.gitignore index 4a241b6c2..ddd4184af 100644 --- a/.gitignore +++ b/.gitignore @@ -5,8 +5,8 @@ /dist /build automation/build -debugger-support/briefcase-debugpy-debugger-support/build -debugger-support/briefcase-pdb-debugger-support/build +debugger-support/briefcase-debugpy/build +debugger-support/briefcase-pdb/build docs/_build/ distribute-* .DS_Store diff --git a/debugger-support/briefcase-debugpy-debugger-support/README.md b/debugger-support/briefcase-debugpy/README.md similarity index 100% rename from debugger-support/briefcase-debugpy-debugger-support/README.md rename to debugger-support/briefcase-debugpy/README.md diff --git a/debugger-support/briefcase-debugpy-debugger-support/pyproject.toml b/debugger-support/briefcase-debugpy/pyproject.toml similarity index 90% rename from debugger-support/briefcase-debugpy-debugger-support/pyproject.toml rename to debugger-support/briefcase-debugpy/pyproject.toml index 5bdbdc236..7408706c1 100644 --- a/debugger-support/briefcase-debugpy-debugger-support/pyproject.toml +++ b/debugger-support/briefcase-debugpy/pyproject.toml @@ -7,7 +7,7 @@ requires = [ build-backend = "setuptools.build_meta" [project] -name = "briefcase-debugpy-debugger-support" +name = "briefcase-debugpy" description = "Add-on for briefcase to add remote debugging via debugpy." license = "BSD-3-Clause" dependencies = [ diff --git a/debugger-support/briefcase-debugpy-debugger-support/setup.py b/debugger-support/briefcase-debugpy/setup.py similarity index 100% rename from debugger-support/briefcase-debugpy-debugger-support/setup.py rename to debugger-support/briefcase-debugpy/setup.py diff --git a/debugger-support/briefcase-debugpy-debugger-support/src/briefcase_debugpy_debugger_support/__init__.py b/debugger-support/briefcase-debugpy/src/briefcase_debugpy_debugger_support/__init__.py similarity index 100% rename from debugger-support/briefcase-debugpy-debugger-support/src/briefcase_debugpy_debugger_support/__init__.py rename to debugger-support/briefcase-debugpy/src/briefcase_debugpy_debugger_support/__init__.py diff --git a/debugger-support/briefcase-debugpy-debugger-support/src/briefcase_debugpy_debugger_support/_remote_debugger.py b/debugger-support/briefcase-debugpy/src/briefcase_debugpy_debugger_support/_remote_debugger.py similarity index 100% rename from debugger-support/briefcase-debugpy-debugger-support/src/briefcase_debugpy_debugger_support/_remote_debugger.py rename to debugger-support/briefcase-debugpy/src/briefcase_debugpy_debugger_support/_remote_debugger.py diff --git a/debugger-support/briefcase-pdb-debugger-support/README.md b/debugger-support/briefcase-pdb/README.md similarity index 100% rename from debugger-support/briefcase-pdb-debugger-support/README.md rename to debugger-support/briefcase-pdb/README.md diff --git a/debugger-support/briefcase-pdb-debugger-support/pyproject.toml b/debugger-support/briefcase-pdb/pyproject.toml similarity index 91% rename from debugger-support/briefcase-pdb-debugger-support/pyproject.toml rename to debugger-support/briefcase-pdb/pyproject.toml index f1721eaa9..128bf1e53 100644 --- a/debugger-support/briefcase-pdb-debugger-support/pyproject.toml +++ b/debugger-support/briefcase-pdb/pyproject.toml @@ -7,7 +7,7 @@ requires = [ build-backend = "setuptools.build_meta" [project] -name = "briefcase-pdb-debugger-support" +name = "briefcase-pdb" description = "Add-on for briefcase to add remote debugging via pdb." license = "BSD-3-Clause" dependencies = [ diff --git a/debugger-support/briefcase-pdb-debugger-support/setup.py b/debugger-support/briefcase-pdb/setup.py similarity index 100% rename from debugger-support/briefcase-pdb-debugger-support/setup.py rename to debugger-support/briefcase-pdb/setup.py diff --git a/debugger-support/briefcase-pdb-debugger-support/src/briefcase_pdb_debugger_support/__init__.py b/debugger-support/briefcase-pdb/src/briefcase_pdb_debugger_support/__init__.py similarity index 100% rename from debugger-support/briefcase-pdb-debugger-support/src/briefcase_pdb_debugger_support/__init__.py rename to debugger-support/briefcase-pdb/src/briefcase_pdb_debugger_support/__init__.py diff --git a/debugger-support/briefcase-pdb-debugger-support/src/briefcase_pdb_debugger_support/_remote_debugger.py b/debugger-support/briefcase-pdb/src/briefcase_pdb_debugger_support/_remote_debugger.py similarity index 100% rename from debugger-support/briefcase-pdb-debugger-support/src/briefcase_pdb_debugger_support/_remote_debugger.py rename to debugger-support/briefcase-pdb/src/briefcase_pdb_debugger_support/_remote_debugger.py diff --git a/src/briefcase/debuggers/debugpy.py b/src/briefcase/debuggers/debugpy.py index 2e1fd2d11..1cca9f731 100644 --- a/src/briefcase/debuggers/debugpy.py +++ b/src/briefcase/debuggers/debugpy.py @@ -16,4 +16,4 @@ def connection_mode(self) -> DebuggerConnectionMode: @property def debugger_support_pkg(self) -> str: """Get the name of the debugger support package.""" - return get_debugger_requirement("briefcase-debugpy-debugger-support") + return get_debugger_requirement("briefcase-debugpy") diff --git a/src/briefcase/debuggers/pdb.py b/src/briefcase/debuggers/pdb.py index c10553076..3d051b3e3 100644 --- a/src/briefcase/debuggers/pdb.py +++ b/src/briefcase/debuggers/pdb.py @@ -16,4 +16,4 @@ def connection_mode(self) -> DebuggerConnectionMode: @property def debugger_support_pkg(self) -> str: """Get the name of the debugger support package.""" - return get_debugger_requirement("briefcase-pdb-debugger-support") + return get_debugger_requirement("briefcase-pdb") From dacf7ea6223e06a718a15fa4a7a39b3be22e41cd Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Wed, 13 Aug 2025 21:34:54 +0200 Subject: [PATCH 072/131] fixed unit tests --- tests/debuggers/test_base.py | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/tests/debuggers/test_base.py b/tests/debuggers/test_base.py index e6d837047..9d50e0a43 100644 --- a/tests/debuggers/test_base.py +++ b/tests/debuggers/test_base.py @@ -84,9 +84,7 @@ def test_debugger(debugger_name, expected_class, connection_mode): debugger = get_debugger(debugger_name) assert isinstance(debugger, expected_class) assert debugger.connection_mode == connection_mode - assert ( - f"briefcase-{debugger_name}-debugger-support" in debugger.debugger_support_pkg - ) + assert f"briefcase-{debugger_name}" in debugger.debugger_support_pkg @pytest.mark.parametrize( @@ -96,21 +94,15 @@ def test_debugger(debugger_name, expected_class, connection_mode): def test_debugger_editable(debugger_name, monkeypatch): with TemporaryDirectory() as tmp_path: tmp_path = Path(tmp_path) - ( - tmp_path - / "debugger-support" - / f"briefcase-{debugger_name}-debugger-support" - ).mkdir(parents=True, exist_ok=True) + (tmp_path / "debugger-support" / f"briefcase-{debugger_name}").mkdir( + parents=True, exist_ok=True + ) monkeypatch.setattr("briefcase.debuggers.base.IS_EDITABLE", True) monkeypatch.setattr("briefcase.debuggers.base.REPO_ROOT", tmp_path) debugger = get_debugger(debugger_name) assert ( - str( - tmp_path - / "debugger-support" - / f"briefcase-{debugger_name}-debugger-support" - ) + str(tmp_path / "debugger-support" / f"briefcase-{debugger_name}") == debugger.debugger_support_pkg ) @@ -126,7 +118,4 @@ def test_debugger_editable_path_not_found(debugger_name, monkeypatch): monkeypatch.setattr("briefcase.debuggers.base.REPO_ROOT", tmp_path) debugger = get_debugger(debugger_name) - assert ( - f"briefcase-{debugger_name}-debugger-support==" - in debugger.debugger_support_pkg - ) + assert f"briefcase-{debugger_name}==" in debugger.debugger_support_pkg From 84ee1663a628d3a534d7afe5ffbae84889bdfbe9 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Wed, 13 Aug 2025 22:17:09 +0200 Subject: [PATCH 073/131] renamed packages --- debugger-support/briefcase-debugpy/setup.py | 4 ++-- .../__init__.py | 0 .../_remote_debugger.py | 0 debugger-support/briefcase-pdb/setup.py | 4 ++-- .../__init__.py | 0 .../_remote_debugger.py | 0 6 files changed, 4 insertions(+), 4 deletions(-) rename debugger-support/briefcase-debugpy/src/{briefcase_debugpy_debugger_support => briefcase_debugpy}/__init__.py (100%) rename debugger-support/briefcase-debugpy/src/{briefcase_debugpy_debugger_support => briefcase_debugpy}/_remote_debugger.py (100%) rename debugger-support/briefcase-pdb/src/{briefcase_pdb_debugger_support => briefcase_pdb}/__init__.py (100%) rename debugger-support/briefcase-pdb/src/{briefcase_pdb_debugger_support => briefcase_pdb}/_remote_debugger.py (100%) diff --git a/debugger-support/briefcase-debugpy/setup.py b/debugger-support/briefcase-debugpy/setup.py index 596675e11..89941f3bb 100644 --- a/debugger-support/briefcase-debugpy/setup.py +++ b/debugger-support/briefcase-debugpy/setup.py @@ -18,8 +18,8 @@ class install_with_pth(install): Please do not replicate this behavior. """ - _pth_name = "briefcase_debugpy_debugger_support" - _pth_contents = "import briefcase_debugpy_debugger_support" + _pth_name = "briefcase_debugpy" + _pth_contents = "import briefcase_debugpy" def initialize_options(self): install.initialize_options(self) diff --git a/debugger-support/briefcase-debugpy/src/briefcase_debugpy_debugger_support/__init__.py b/debugger-support/briefcase-debugpy/src/briefcase_debugpy/__init__.py similarity index 100% rename from debugger-support/briefcase-debugpy/src/briefcase_debugpy_debugger_support/__init__.py rename to debugger-support/briefcase-debugpy/src/briefcase_debugpy/__init__.py diff --git a/debugger-support/briefcase-debugpy/src/briefcase_debugpy_debugger_support/_remote_debugger.py b/debugger-support/briefcase-debugpy/src/briefcase_debugpy/_remote_debugger.py similarity index 100% rename from debugger-support/briefcase-debugpy/src/briefcase_debugpy_debugger_support/_remote_debugger.py rename to debugger-support/briefcase-debugpy/src/briefcase_debugpy/_remote_debugger.py diff --git a/debugger-support/briefcase-pdb/setup.py b/debugger-support/briefcase-pdb/setup.py index 7c1a794d7..e49e88e89 100644 --- a/debugger-support/briefcase-pdb/setup.py +++ b/debugger-support/briefcase-pdb/setup.py @@ -18,8 +18,8 @@ class install_with_pth(install): Please do not replicate this behavior. """ - _pth_name = "briefcase_pdb_debugger_support" - _pth_contents = "import briefcase_pdb_debugger_support" + _pth_name = "briefcase_pdb" + _pth_contents = "import briefcase_pdb" def initialize_options(self): install.initialize_options(self) diff --git a/debugger-support/briefcase-pdb/src/briefcase_pdb_debugger_support/__init__.py b/debugger-support/briefcase-pdb/src/briefcase_pdb/__init__.py similarity index 100% rename from debugger-support/briefcase-pdb/src/briefcase_pdb_debugger_support/__init__.py rename to debugger-support/briefcase-pdb/src/briefcase_pdb/__init__.py diff --git a/debugger-support/briefcase-pdb/src/briefcase_pdb_debugger_support/_remote_debugger.py b/debugger-support/briefcase-pdb/src/briefcase_pdb/_remote_debugger.py similarity index 100% rename from debugger-support/briefcase-pdb/src/briefcase_pdb_debugger_support/_remote_debugger.py rename to debugger-support/briefcase-pdb/src/briefcase_pdb/_remote_debugger.py From 3c5ab102cc7f27d0ad345bd14502a659d92915fa Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Wed, 13 Aug 2025 20:54:00 +0200 Subject: [PATCH 074/131] added unittests, tox-config and ci for briefcase-debugpy and briefcase-pdb --- .github/workflows/ci.yml | 151 +++++++++++++- .../briefcase-debugpy/pyproject.toml | 6 + .../src/briefcase_debugpy/_remote_debugger.py | 36 ++-- .../tests/test_path_mappings.py | 187 ++++++++++++++++++ .../tests/test_start_remote_debugger.py | 103 ++++++++++ debugger-support/briefcase-pdb/pyproject.toml | 6 + .../src/briefcase_pdb/_remote_debugger.py | 3 +- .../tests/test_start_remote_debugger.py | 77 ++++++++ tox.ini | 18 ++ 9 files changed, 566 insertions(+), 21 deletions(-) create mode 100644 debugger-support/briefcase-debugpy/tests/test_path_mappings.py create mode 100644 debugger-support/briefcase-debugpy/tests/test_start_remote_debugger.py create mode 100644 debugger-support/briefcase-pdb/tests/test_start_remote_debugger.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b6477b628..094bd6588 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,7 @@ on: push: branches: - main + - ci-test # TODO: Remove this workflow_call: inputs: attest-package: @@ -46,6 +47,56 @@ jobs: with: attest: ${{ inputs.attest-package }} + package-debugpy: + name: Package Briefcase Debugpy + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + attestations: write + # This does not work. It gerates a CI Error: Error: The artifact name is not valid: Packages-briefcase-debugger-support/briefcase-debugpy. Contains the following character: Forward slash / + # uses: beeware/.github/.github/workflows/python-package-create.yml@main + # with: + # build-subdirectory: "debugger-support/briefcase-debugpy" + steps: + - uses: actions/checkout@v4 + - uses: hynek/build-and-inspect-python-package@v2 + id: package + with: + path: "debugger-support/briefcase-debugpy" + upload-name-suffix: "-briefcase-debugpy" + attest-build-provenance-github: ${{ inputs.attest-package }} + - name: Set artifact-name output + run: echo "artifact-name=${{ steps.package.outputs.artifact-name }}" >> $GITHUB_OUTPUT + id: set-artifact-name + outputs: + artifact-name: ${{ steps.set-artifact-name.outputs.artifact-name }} + + package-pdb: + name: Package Briefcase PDB + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + attestations: write + # This does not work. It gerates a CI Error: Error: The artifact name is not valid: Packages-briefcase-debugger-support/briefcase-pdb. Contains the following character: Forward slash / + # uses: beeware/.github/.github/workflows/python-package-create.yml@main + # with: + # build-subdirectory: "debugger-support/briefcase-pdb" + steps: + - uses: actions/checkout@v4 + - uses: hynek/build-and-inspect-python-package@v2 + id: package + with: + path: "debugger-support/briefcase-pdb" + upload-name-suffix: "-briefcase-pdb" + attest-build-provenance-github: ${{ inputs.attest-package }} + - name: Set artifact-name output + run: echo "artifact-name=${{ steps.package.outputs.artifact-name }}" >> $GITHUB_OUTPUT + id: set-artifact-name + outputs: + artifact-name: ${{ steps.set-artifact-name.outputs.artifact-name }} + package-automation: name: Package Automation permissions: @@ -101,7 +152,7 @@ jobs: requirements: tox extra: dev - - name: Test + - name: Test Briefcase id: test run: | RUNNER_OS=$(cut -d- -f1 <<< ${{ matrix.platform }}) @@ -124,6 +175,104 @@ jobs: # coverage reporting must use the same Python version used to produce coverage run: tox -qe coverage$(tr -dc "0-9" <<< "${{ matrix.python-version }}") + unit-tests-debugpy: + name: Unit tests (Debugpy) + needs: [ pre-commit, towncrier, package-debugpy ] + runs-on: ${{ matrix.platform }} + continue-on-error: ${{ matrix.experimental || false }} + strategy: + fail-fast: false + matrix: + platform: [ "macos-13", "macos-latest", "windows-latest", "ubuntu-24.04" ] + python-version: [ "3.9", "3.13" ] + include: + # Ensure the Python versions between min and max are tested + - platform: "ubuntu-24.04" + python-version: "3.10" + - platform: "ubuntu-24.04" + python-version: "3.11" + - platform: "ubuntu-24.04" + python-version: "3.12" + # # Allow dev Python to fail without failing entire job + # - python-version: "3.14" + # experimental: true + steps: + - name: Checkout + uses: actions/checkout@v4.2.2 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5.6.0 + with: + python-version: ${{ matrix.python-version }} + allow-prereleases: true + + - name: Get Packages + uses: actions/download-artifact@v4.3.0 + with: + name: ${{ needs.package-debugpy.outputs.artifact-name }} + path: dist + + - name: Install Tox + uses: beeware/.github/.github/actions/install-requirement@main + with: + requirements: tox + extra: dev + + - name: Test Briefcase Debugpy + run: | + tox -e py-debugpy --installpkg dist/briefcase_debugpy-*.whl + + unit-tests-pdb: + name: Unit tests (Pdb) + needs: [ pre-commit, towncrier, package-pdb ] + runs-on: ${{ matrix.platform }} + continue-on-error: ${{ matrix.experimental || false }} + strategy: + fail-fast: false + matrix: + platform: [ "macos-13", "macos-latest", "windows-latest", "ubuntu-24.04" ] + python-version: [ "3.9", "3.13" ] + include: + # Ensure the Python versions between min and max are tested + - platform: "ubuntu-24.04" + python-version: "3.10" + - platform: "ubuntu-24.04" + python-version: "3.11" + - platform: "ubuntu-24.04" + python-version: "3.12" + # # Allow dev Python to fail without failing entire job + # - python-version: "3.14" + # experimental: true + steps: + - name: Checkout + uses: actions/checkout@v4.2.2 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5.6.0 + with: + python-version: ${{ matrix.python-version }} + allow-prereleases: true + + - name: Get Packages + uses: actions/download-artifact@v4.3.0 + with: + name: ${{ needs.package-pdb.outputs.artifact-name }} + path: dist + + - name: Install Tox + uses: beeware/.github/.github/actions/install-requirement@main + with: + requirements: tox + extra: dev + + - name: Test Briefcase Pdb + run: | + tox -e py-pdb --installpkg dist/briefcase_pdb-*.whl + coverage: name: Project coverage runs-on: ubuntu-24.04 diff --git a/debugger-support/briefcase-debugpy/pyproject.toml b/debugger-support/briefcase-debugpy/pyproject.toml index 7408706c1..e0a43d6ef 100644 --- a/debugger-support/briefcase-debugpy/pyproject.toml +++ b/debugger-support/briefcase-debugpy/pyproject.toml @@ -9,6 +9,7 @@ build-backend = "setuptools.build_meta" [project] name = "briefcase-debugpy" description = "Add-on for briefcase to add remote debugging via debugpy." +readme = "README.md" license = "BSD-3-Clause" dependencies = [ "debugpy>=1.8.14,<2.0.0" @@ -17,3 +18,8 @@ dynamic = ["version"] [tool.setuptools_scm] root = "../../" + +[project.optional-dependencies] +dev = [ + "pytest == 8.4.1" +] diff --git a/debugger-support/briefcase-debugpy/src/briefcase_debugpy/_remote_debugger.py b/debugger-support/briefcase-debugpy/src/briefcase_debugpy/_remote_debugger.py index 42090e16e..e56821faa 100644 --- a/debugger-support/briefcase-debugpy/src/briefcase_debugpy/_remote_debugger.py +++ b/debugger-support/briefcase-debugpy/src/briefcase_debugpy/_remote_debugger.py @@ -3,7 +3,7 @@ import re import sys from pathlib import Path -from typing import TypedDict +from typing import Optional, TypedDict import debugpy @@ -22,23 +22,26 @@ class AppPackagesPathMappings(TypedDict): class DebuggerConfig(TypedDict): host: str port: int - app_path_mappings: AppPathMappings | None - app_packages_path_mappings: AppPackagesPathMappings | None + app_path_mappings: Optional[AppPathMappings] + app_packages_path_mappings: Optional[AppPackagesPathMappings] -def _load_path_mappings(config: DebuggerConfig, verbose: bool) -> list[tuple[str, str]]: +def find_first_matching_path(regex: str) -> Optional[str]: + """Gibt das erste Element aus paths zurück, das auf regex matcht, sonst None.""" + for p in sys.path: + if re.search(regex, p): + return p + return None + + +def load_path_mappings(config: DebuggerConfig, verbose: bool) -> list[tuple[str, str]]: app_path_mappings = config.get("app_path_mappings", None) app_packages_path_mappings = config.get("app_packages_path_mappings", None) mappings_list = [] if app_path_mappings: - device_app_folder = next( - ( - p - for p in sys.path - if re.search(app_path_mappings["device_sys_path_regex"], p) - ), - None, + device_app_folder = find_first_matching_path( + app_path_mappings["device_sys_path_regex"] ) if device_app_folder: for app_subfolder_device, app_subfolder_host in zip( @@ -52,13 +55,8 @@ def _load_path_mappings(config: DebuggerConfig, verbose: bool) -> list[tuple[str ) ) if app_packages_path_mappings: - device_app_packages_folder = next( - ( - p - for p in sys.path - if re.search(app_packages_path_mappings["sys_path_regex"], p) - ), - None, + device_app_packages_folder = find_first_matching_path( + app_packages_path_mappings["sys_path_regex"] ) if device_app_packages_folder: mappings_list.append( @@ -83,7 +81,7 @@ def start_debugpy(config_str: str, verbose: bool): host = debugger_config["host"] port = debugger_config["port"] - path_mappings = _load_path_mappings(debugger_config, verbose) + path_mappings = load_path_mappings(debugger_config, verbose) # There is a bug in debugpy that has to be handled until there is a new # debugpy release, see https://github.com/microsoft/debugpy/issues/1943 diff --git a/debugger-support/briefcase-debugpy/tests/test_path_mappings.py b/debugger-support/briefcase-debugpy/tests/test_path_mappings.py new file mode 100644 index 000000000..219e0626d --- /dev/null +++ b/debugger-support/briefcase-debugpy/tests/test_path_mappings.py @@ -0,0 +1,187 @@ +import sys +from pathlib import Path, PosixPath, PurePosixPath, PureWindowsPath, WindowsPath + +import briefcase_debugpy._remote_debugger +from briefcase_debugpy._remote_debugger import ( + AppPackagesPathMappings, + AppPathMappings, + DebuggerConfig, + load_path_mappings, +) + + +def test_mappings_not_existing(): + """Test an complete empty config.""" + path_mappings = load_path_mappings({}, False) + assert path_mappings == [] + + +def test_mappings_none(monkeypatch): + """Test an config with no mappings set.""" + config = DebuggerConfig( + host="", + port=0, + app_path_mappings=None, + app_packages_path_mappings=None, + ) + path_mappings = load_path_mappings(config, False) + assert path_mappings == [] + + +def test_mappings_windows(monkeypatch): + """Test path mappings on an Windows system.""" + # When running tests on Linux/macOS, we have to switch to WindowsPath. + if isinstance(Path(), PosixPath): + monkeypatch.setattr(briefcase_debugpy._remote_debugger, "Path", PureWindowsPath) + + config = DebuggerConfig( + host="", + port=0, + app_path_mappings=AppPathMappings( + device_sys_path_regex="app$", + device_subfolders=["helloworld"], + host_folders=["src/helloworld"], + ), + app_packages_path_mappings=None, + ) + + sys_path = [ + "build\\helloworld\\windows\\app\\src\\python313.zip", + "build\\helloworld\\windows\\app\\src", + "build\\helloworld\\windows\\app\\src\\app", + "build\\helloworld\\windows\\app\\src\\app_packages", + ] + monkeypatch.setattr(sys, "path", sys_path) + + path_mappings = load_path_mappings(config, False) + + assert path_mappings == [ + # (host_path, device_path) + ("src/helloworld", "build\\helloworld\\windows\\app\\src\\app\\helloworld"), + ] + + +def test_mappings_macos(monkeypatch): + """Test path mappings on an macOS system.""" + # When running tests on windows, we have to switch to PosixPath. + if isinstance(Path(), WindowsPath): + monkeypatch.setattr(briefcase_debugpy._remote_debugger, "Path", PurePosixPath) + + config = DebuggerConfig( + host="", + port=0, + app_path_mappings=AppPathMappings( + device_sys_path_regex="app$", + device_subfolders=["helloworld"], + host_folders=["src/helloworld"], + ), + app_packages_path_mappings=None, + ) + + sys_path = [ + "build/helloworld/macos/app/Hello World.app/Contents/Frameworks/Python.framework/Versions/3.13/lib/python3.13", + "build/helloworld/macos/app/Hello World.app/Contents/Frameworks/Python.framework/Versions/3.13/lib/python3.13/lib-dynload", + "build/helloworld/macos/app/Hello World.app/Contents/Resources/app", + "build/helloworld/macos/app/Hello World.app/Contents/Frameworks/Python.framework/Versions/3.13/lib/python3.13/site-packages", + "build/helloworld/macos/app/Hello World.app/Contents/Resources/app_packages", + ] + monkeypatch.setattr(sys, "path", sys_path) + + path_mappings = load_path_mappings(config, False) + + assert path_mappings == [ + # (host_path, device_path) + ( + "src/helloworld", + "build/helloworld/macos/app/Hello World.app/Contents/Resources/app/helloworld", + ), + ] + + +def test_mappings_ios(monkeypatch): + """Test path mappings on an iOS system.""" + # When running tests on windows, we have to switch to PosixPath. + if isinstance(Path(), WindowsPath): + monkeypatch.setattr(briefcase_debugpy._remote_debugger, "Path", PurePosixPath) + + config = DebuggerConfig( + host="", + port=0, + app_path_mappings=AppPathMappings( + device_sys_path_regex="app$", + device_subfolders=["helloworld"], + host_folders=["src/helloworld"], + ), + app_packages_path_mappings=AppPackagesPathMappings( + sys_path_regex="app_packages$", + host_folder="APP_PACKAGES_PATH/app_packages.iphonesimulator", + ), + ) + + sys_path = [ + "CoreSimulator/Devices/RANDOM_NUMBER/data/Containers/Bundle/Application/RANDOM_NUMBER/Hello World.app/python/lib/python3.13", + "CoreSimulator/Devices/RANDOM_NUMBER/data/Containers/Bundle/Application/RANDOM_NUMBER/Hello World.app/python/lib/python3.13/lib-dynload", + "CoreSimulator/Devices/RANDOM_NUMBER/data/Containers/Bundle/Application/RANDOM_NUMBER/Hello World.app/app", + "CoreSimulator/Devices/RANDOM_NUMBER/data/Containers/Bundle/Application/RANDOM_NUMBER/Hello World.app/python/lib/python3.13/site-packages", + "CoreSimulator/Devices/RANDOM_NUMBER/data/Containers/Bundle/Application/RANDOM_NUMBER/Hello World.app/app_packages", + ] + monkeypatch.setattr(sys, "path", sys_path) + + path_mappings = load_path_mappings(config, False) + + assert path_mappings == [ + # (host_path, device_path) + ( + "src/helloworld", + "CoreSimulator/Devices/RANDOM_NUMBER/data/Containers/Bundle/Application/RANDOM_NUMBER/Hello World.app/app/helloworld", + ), + ( + "APP_PACKAGES_PATH/app_packages.iphonesimulator", + "CoreSimulator/Devices/RANDOM_NUMBER/data/Containers/Bundle/Application/RANDOM_NUMBER/Hello World.app/app_packages", + ), + ] + + +def test_mappings_android(monkeypatch): + """Test path mappings on an Android system.""" + # When running tests on windows, we have to switch to PosixPath. + if isinstance(Path(), WindowsPath): + monkeypatch.setattr(briefcase_debugpy._remote_debugger, "Path", PurePosixPath) + + config = DebuggerConfig( + host="", + port=0, + app_path_mappings=AppPathMappings( + device_sys_path_regex="app$", + device_subfolders=["helloworld"], + host_folders=["src/helloworld"], + ), + app_packages_path_mappings=AppPackagesPathMappings( + sys_path_regex="requirements$", + host_folder="/BUNDLE_PATH/app/build/python/pip/debug/common", + ), + ) + + sys_path = [ + "/data/data/com.example.helloworld/files/chaquopy/AssetFinder/app", + "/data/data/com.example.helloworld/files/chaquopy/AssetFinder/requirements", + "/data/data/com.example.helloworld/files/chaquopy/AssetFinder/stdlib-x86_64", + "/data/user/0/com.example.helloworld/files/chaquopy/stdlib-common.imy", + "/data/user/0/com.example.helloworld/files/chaquopy/bootstrap.imy", + "/data/user/0/com.example.helloworld/files/chaquopy/bootstrap-native/x86_64", + ] + monkeypatch.setattr(sys, "path", sys_path) + + path_mappings = load_path_mappings(config, False) + + assert path_mappings == [ + # (host_path, device_path) + ( + "src/helloworld", + "/data/data/com.example.helloworld/files/chaquopy/AssetFinder/app/helloworld", + ), + ( + "/BUNDLE_PATH/app/build/python/pip/debug/common", + "/data/data/com.example.helloworld/files/chaquopy/AssetFinder/requirements", + ), + ] diff --git a/debugger-support/briefcase-debugpy/tests/test_start_remote_debugger.py b/debugger-support/briefcase-debugpy/tests/test_start_remote_debugger.py new file mode 100644 index 000000000..cf057ebaa --- /dev/null +++ b/debugger-support/briefcase-debugpy/tests/test_start_remote_debugger.py @@ -0,0 +1,103 @@ +import json +import os +import sys +from pathlib import Path, PosixPath, PureWindowsPath +from unittest.mock import MagicMock + +import briefcase_debugpy +import debugpy +import pytest + + +def test_no_env_vars(monkeypatch, capsys): + """Test that nothing happens, when no env vars are set.""" + os_environ = {} + monkeypatch.setattr(os, "environ", os_environ) + + # start test function + briefcase_debugpy.start_remote_debugger() + + captured = capsys.readouterr() + assert captured.out == "" + assert captured.err == "" + + +def test_no_debugger_verbose(monkeypatch, capsys): + """Test that nothing happens except a short message, when only verbose is + requested.""" + os_environ = {} + os_environ["BRIEFCASE_DEBUG"] = "1" + monkeypatch.setattr(os, "environ", os_environ) + + # start test function + briefcase_debugpy.start_remote_debugger() + + captured = capsys.readouterr() + assert ( + captured.out + == "No 'BRIEFCASE_DEBUGGER' environment variable found. Debugger not starting.\n" + ) + assert captured.err == "" + + +@pytest.mark.parametrize("verbose", [True, False]) +def test_with_debugger(monkeypatch, capsys, verbose): + """Test a normal debug session.""" + # When running tests on Linux/macOS, we have to switch to WindowsPath. + if isinstance(Path(), PosixPath): + monkeypatch.setattr(briefcase_debugpy._remote_debugger, "Path", PureWindowsPath) + + os_environ = {} + os_environ["BRIEFCASE_DEBUG"] = "1" if verbose else "0" + os_environ["BRIEFCASE_DEBUGGER"] = json.dumps( + { + "host": "somehost", + "port": 9999, + "app_path_mappings": { + "device_sys_path_regex": "app$", + "device_subfolders": ["helloworld"], + "host_folders": ["src/helloworld"], + }, + "app_packages_path_mappings": None, + } + ) + monkeypatch.setattr(os, "environ", os_environ) + + sys_path = [ + "build\\helloworld\\windows\\app\\src\\app", + ] + monkeypatch.setattr(sys, "path", sys_path) + + fake_debugpy_listen = MagicMock() + monkeypatch.setattr(debugpy, "listen", fake_debugpy_listen) + + fake_debugpy_wait_for_client = MagicMock() + monkeypatch.setattr(debugpy, "wait_for_client", fake_debugpy_wait_for_client) + + # pydevd is dynamically loaded and only available when a real debugger is attached. So + # we fake the whole module, as otherwise the import in start_remote_debugger would fail + fake_pydevd_file_utils = MagicMock() + fake_pydevd_file_utils.setup_client_server_paths.return_value = None + monkeypatch.setitem(sys.modules, "pydevd_file_utils", fake_pydevd_file_utils) + + # start test function + briefcase_debugpy.start_remote_debugger() + + fake_debugpy_listen.assert_called_once_with( + ("somehost", 9999), + in_process_debug_adapter=True, + ) + + fake_debugpy_wait_for_client.assert_called_once() + fake_pydevd_file_utils.setup_client_server_paths.assert_called_once_with( + [ + ("src/helloworld", "build\\helloworld\\windows\\app\\src\\app\\helloworld"), + ] + ) + + captured = capsys.readouterr() + assert "Waiting for debugger to attach..." in captured.out + assert captured.err == "" + + if verbose: + assert "Extracted path mappings:\n[0] host = src/helloworld" in captured.out diff --git a/debugger-support/briefcase-pdb/pyproject.toml b/debugger-support/briefcase-pdb/pyproject.toml index 128bf1e53..9de3fa18c 100644 --- a/debugger-support/briefcase-pdb/pyproject.toml +++ b/debugger-support/briefcase-pdb/pyproject.toml @@ -9,6 +9,7 @@ build-backend = "setuptools.build_meta" [project] name = "briefcase-pdb" description = "Add-on for briefcase to add remote debugging via pdb." +readme = "README.md" license = "BSD-3-Clause" dependencies = [ "remote-pdb>=2.1.0,<3.0.0" @@ -17,3 +18,8 @@ dynamic = ["version"] [tool.setuptools_scm] root = "../../" + +[project.optional-dependencies] +dev = [ + "pytest == 8.4.1" +] diff --git a/debugger-support/briefcase-pdb/src/briefcase_pdb/_remote_debugger.py b/debugger-support/briefcase-pdb/src/briefcase_pdb/_remote_debugger.py index b66e264bf..f723d1b7b 100644 --- a/debugger-support/briefcase-pdb/src/briefcase_pdb/_remote_debugger.py +++ b/debugger-support/briefcase-pdb/src/briefcase_pdb/_remote_debugger.py @@ -14,7 +14,8 @@ def start_pdb(config_str: str, verbose: bool): print( f""" -Remote PDB server opened at {host}:{port}, waiting for connection... +Remote PDB server opened at {host}:{port}. +Waiting for debugger to attach... To connect to remote PDB use eg.: - telnet {host} {port} (Windows, Linux) - rlwrap socat - tcp:{host}:{port} (Linux, macOS) diff --git a/debugger-support/briefcase-pdb/tests/test_start_remote_debugger.py b/debugger-support/briefcase-pdb/tests/test_start_remote_debugger.py new file mode 100644 index 000000000..61c6f0dad --- /dev/null +++ b/debugger-support/briefcase-pdb/tests/test_start_remote_debugger.py @@ -0,0 +1,77 @@ +import json +import os +import sys +from unittest.mock import MagicMock + +import briefcase_pdb +import briefcase_pdb._remote_debugger + +# import remote_pdb +import pytest + + +def test_no_env_vars(monkeypatch, capsys): + """Test that nothing happens, when no env vars are set.""" + os_environ = {} + monkeypatch.setattr(os, "environ", os_environ) + + # start test function + briefcase_pdb.start_remote_debugger() + + captured = capsys.readouterr() + assert captured.out == "" + assert captured.err == "" + + +def test_no_debugger_verbose(monkeypatch, capsys): + """Test that nothing happens except a short message, when only verbose is + requested.""" + os_environ = {} + os_environ["BRIEFCASE_DEBUG"] = "1" + monkeypatch.setattr(os, "environ", os_environ) + + # start test function + briefcase_pdb.start_remote_debugger() + + captured = capsys.readouterr() + assert ( + captured.out + == "No 'BRIEFCASE_DEBUGGER' environment variable found. Debugger not starting.\n" + ) + assert captured.err == "" + + +@pytest.mark.parametrize("verbose", [True, False]) +def test_with_debugger(monkeypatch, capsys, verbose): + """Test a normal debug session.""" + os_environ = {} + os_environ["BRIEFCASE_DEBUG"] = "1" if verbose else "0" + os_environ["BRIEFCASE_DEBUGGER"] = json.dumps( + { + "host": "somehost", + "port": 9999, + } + ) + monkeypatch.setattr(os, "environ", os_environ) + + fake_remote_pdb = MagicMock() + monkeypatch.setattr(briefcase_pdb._remote_debugger, "RemotePdb", fake_remote_pdb) + + # pydevd is dynamically loaded and only available when a real debugger is attached. So + # we fake the whole module, as otherwise the import in start_remote_debugger would fail + fake_pydevd_file_utils = MagicMock() + fake_pydevd_file_utils.setup_client_server_paths.return_value = None + monkeypatch.setitem(sys.modules, "pydevd_file_utils", fake_pydevd_file_utils) + + # start test function + briefcase_pdb.start_remote_debugger() + + fake_remote_pdb.assert_called_once_with( + "somehost", + 9999, + quiet=True, + ) + + captured = capsys.readouterr() + assert "Waiting for debugger to attach..." in captured.out + assert captured.err == "" diff --git a/tox.ini b/tox.ini index b7c974de5..fab56c413 100644 --- a/tox.ini +++ b/tox.ini @@ -105,3 +105,21 @@ commands = lint : python -m sphinx {[docs]sphinx_args} {posargs} --builder linkcheck {[docs]docs_dir} {[docs]build_dir}{/}links all : python -m sphinx {[docs]sphinx_args} {posargs} --verbose --write-all --fresh-env --builder html {[docs]docs_dir} {[docs]build_dir}{/}html live : sphinx-autobuild {[docs]sphinx_args} {posargs} --builder html {[docs]docs_dir} {[docs]build_dir}{/}live + +[testenv:py{,39,310,311,312,313}-debugpy] +changedir = debugger-support/briefcase-debugpy +skip_install = True +deps = + build +commands = + python -m pip install ".[dev]" + python -m pytest {posargs:-vv --color yes} + +[testenv:py{,39,310,311,312,313}-pdb] +changedir = debugger-support/briefcase-pdb +skip_install = True +deps = + build +commands = + python -m pip install ".[dev]" + python -m pytest {posargs:-vv --color yes} From 9828dba001596a90155115a21e34659387092abe Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Fri, 15 Aug 2025 23:47:38 +0200 Subject: [PATCH 075/131] clarified default parameter of --debug --- docs/reference/commands/build.rst | 4 +++- docs/reference/commands/run.rst | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/reference/commands/build.rst b/docs/reference/commands/build.rst index 7026b890f..d13c8bb3d 100644 --- a/docs/reference/commands/build.rst +++ b/docs/reference/commands/build.rst @@ -120,10 +120,12 @@ Build the app in debug mode in the bundled app environment and establish an debugger connection via a socket. This installs the selected debugger in the bundled app. -Currently the following debuggers are supported (default is ``pdb``): +Currently the following debuggers are supported: - ``pdb``: This is used for debugging via console. - ``debugpy``: This is used for debugging via VSCode. +- +If calling only ``--debug`` without selecting a debugger explicitly, ``pdb`` is used as default. This is an experimental new feature, that is currently only support on Windows and macOS. diff --git a/docs/reference/commands/run.rst b/docs/reference/commands/run.rst index 78e85a290..11b75d4e6 100644 --- a/docs/reference/commands/run.rst +++ b/docs/reference/commands/run.rst @@ -143,11 +143,13 @@ specifying by the ``--test`` option. Run the app in debug mode in the bundled app environment and establish an debugger connection via a socket. -Currently the following debuggers are supported (default is ``pdb``): +Currently the following debuggers are supported: - ``pdb``: This is used for debugging via console. - ``debugpy``: This is used for debugging via VSCode. +If calling only ``--debug`` without selecting a debugger explicitly, ``pdb`` is used as default. + This is an experimental new feature, that is currently only support on Windows and macOS. ``--debugger-host `` From 1a812bb070016f7179f8a85d4b9c4485eab2cf60 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Fri, 15 Aug 2025 23:54:47 +0200 Subject: [PATCH 076/131] added testcase for os.__file__ bugfix --- .../tests/test_start_remote_debugger.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/debugger-support/briefcase-debugpy/tests/test_start_remote_debugger.py b/debugger-support/briefcase-debugpy/tests/test_start_remote_debugger.py index cf057ebaa..fc757eaff 100644 --- a/debugger-support/briefcase-debugpy/tests/test_start_remote_debugger.py +++ b/debugger-support/briefcase-debugpy/tests/test_start_remote_debugger.py @@ -101,3 +101,44 @@ def test_with_debugger(monkeypatch, capsys, verbose): if verbose: assert "Extracted path mappings:\n[0] host = src/helloworld" in captured.out + + +@pytest.mark.parametrize("verbose", [True, False]) +def test_os_file_bugfix(monkeypatch, capsys, verbose): + """Test if the os.__file__ bugfix is applied (see https://github.com/microsoft/debugpy/issues/1943).""" + os_environ = {} + os_environ["BRIEFCASE_DEBUG"] = "1" if verbose else "0" + os_environ["BRIEFCASE_DEBUGGER"] = json.dumps( + { + "host": "somehost", + "port": 9999, + } + ) + monkeypatch.setattr(os, "environ", os_environ) + + # Fake an environment in that "os.__file__" is not available + monkeypatch.delattr(os, "__file__", raising=False) + + fake_debugpy_listen = MagicMock() + monkeypatch.setattr(debugpy, "listen", fake_debugpy_listen) + + fake_debugpy_wait_for_client = MagicMock() + monkeypatch.setattr(debugpy, "wait_for_client", fake_debugpy_wait_for_client) + + # start test function + briefcase_debugpy.start_remote_debugger() + + fake_debugpy_listen.assert_called_once_with( + ("somehost", 9999), + in_process_debug_adapter=True, + ) + + assert hasattr(os, "__file__") + assert os.__file__ == "" + + captured = capsys.readouterr() + assert "Waiting for debugger to attach..." in captured.out + assert captured.err == "" + + if verbose: + assert "'os.__file__' not available. Patching it..." in captured.out From 68f22c6f4a2ac498dcc7b2a44dda052245c88c07 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Tue, 24 Jun 2025 22:21:09 +0200 Subject: [PATCH 077/131] Add how-to for debugging support --- .../src/briefcase_debugpy/_remote_debugger.py | 4 +- .../src/briefcase_pdb/_remote_debugger.py | 4 +- docs/how-to/debugging/console.rst | 91 +++++++++++++++++++ docs/how-to/debugging/index.rst | 12 +++ docs/how-to/debugging/vscode.rst | 89 ++++++++++++++++++ docs/how-to/index.rst | 1 + docs/reference/commands/build.rst | 7 +- docs/reference/commands/run.rst | 12 ++- 8 files changed, 214 insertions(+), 6 deletions(-) create mode 100644 docs/how-to/debugging/console.rst create mode 100644 docs/how-to/debugging/index.rst create mode 100644 docs/how-to/debugging/vscode.rst diff --git a/debugger-support/briefcase-debugpy/src/briefcase_debugpy/_remote_debugger.py b/debugger-support/briefcase-debugpy/src/briefcase_debugpy/_remote_debugger.py index e56821faa..0323dd6c6 100644 --- a/debugger-support/briefcase-debugpy/src/briefcase_debugpy/_remote_debugger.py +++ b/debugger-support/briefcase-debugpy/src/briefcase_debugpy/_remote_debugger.py @@ -106,7 +106,7 @@ def start_debugpy(config_str: str, verbose: bool): print("The debugpy server started. Waiting for debugger to attach...") print( f""" -To connect to debugpy using VSCode add the following configuration to launch.json: +To connect to debugpy using VSCode add the following configuration to '.vscode/launch.json': {{ "version": "0.2.0", "configurations": [ @@ -121,6 +121,8 @@ def start_debugpy(config_str: str, verbose: bool): }} ] }} + +For more information see: https://briefcase.readthedocs.io/en/stable/how-to/debugging/vscode.html#bundled-app """ ) debugpy.wait_for_client() diff --git a/debugger-support/briefcase-pdb/src/briefcase_pdb/_remote_debugger.py b/debugger-support/briefcase-pdb/src/briefcase_pdb/_remote_debugger.py index f723d1b7b..13d97bf92 100644 --- a/debugger-support/briefcase-pdb/src/briefcase_pdb/_remote_debugger.py +++ b/debugger-support/briefcase-pdb/src/briefcase_pdb/_remote_debugger.py @@ -17,8 +17,10 @@ def start_pdb(config_str: str, verbose: bool): Remote PDB server opened at {host}:{port}. Waiting for debugger to attach... To connect to remote PDB use eg.: - - telnet {host} {port} (Windows, Linux) + - telnet {host} {port} (Windows) - rlwrap socat - tcp:{host}:{port} (Linux, macOS) + +For more information see: https://briefcase.readthedocs.io/en/stable/how-to/debugging/console.html#bundled-app """ ) diff --git a/docs/how-to/debugging/console.rst b/docs/how-to/debugging/console.rst new file mode 100644 index 000000000..969fc88ad --- /dev/null +++ b/docs/how-to/debugging/console.rst @@ -0,0 +1,91 @@ +================= +Debug via Console +================= + +Debugging an app on the console is normally done via `PDB `_. +It is possible to debug a briefcase app at different stages in your development +process. You can debug a development app via ``briefcase dev``, but also an bundled +app that is build via ``briefcase build`` and run via ``briefcase run``. + + +Development +----------- +Debugging an development app is quiet easy. Just add ``breakpoint()`` inside +your code and start the app via ``briefcase dev``. When the breakpoint got hit +the pdb console opens on your console and you can debug your app. + + +Bundled App +----------- +It is also possible to debug a bundled app. This is currently still an **experimental feature** that is only +supported on Windows and macOS. The full power of this feature will become available when iOS and +Android are supported, because that is the only way to debug an iOS or Android app. + +To debug a bundled app a piece of the debugger has to be embedded into your app. This is done via: + +.. code-block:: console + + $ briefcase build --debug pdb + +This will build your app in debug mode and add `remote-pdb `_ +together with a package that automatically starts ``remote-pdb`` at the +startup of your bundled app. + +Then it is time to run your app. You can do this via: + +.. code-block:: console + + $ briefcase run --debug pdb + +Running the app in debug mode will automatically start the ``remote-pdb`` debugger +and wait for incoming connections. By default it will listen on ``localhost`` +and port ``5678``. + +Then it is time to create a new console window on your host system and connect +to your bundled app. + +.. tabs:: + + .. group-tab:: Windows + To connect to your application, you need access to ``telnet``. That is not activated by default, but can be + activated by running the following command with admin rights + + .. code-block:: console + + $ dism /online /Enable-Feature /FeatureName:TelnetClient + + Then you can start the connection via + + .. code-block:: console + + $ telnet localhost 5678 + + .. group-tab:: Linux + If not already done install ``rlwrap`` and ``socat`` + + .. code-block:: console + + $ sudo apt install rlwrap socat + + Then you can start the connection via + + .. code-block:: console + + $ rlwrap socat - tcp:localhost:5678 + + .. group-tab:: macOS + + If not already done install ``rlwrap`` and ``socat`` using `Homebrew `__ + + .. code-block:: console + + $ brew install rlwrap socat + + Then you can start the connection via + + .. code-block:: console + + $ rlwrap socat - tcp:localhost:5678 + + +The app will start after the connection is established. diff --git a/docs/how-to/debugging/index.rst b/docs/how-to/debugging/index.rst new file mode 100644 index 000000000..8cb7ca207 --- /dev/null +++ b/docs/how-to/debugging/index.rst @@ -0,0 +1,12 @@ +============== +Debug your app +============== + +If you get stuck when programming your app, it is time to debug your app. The +following sections describe how you can debug your app with or without an IDE. + +.. toctree:: + :maxdepth: 1 + + console + vscode diff --git a/docs/how-to/debugging/vscode.rst b/docs/how-to/debugging/vscode.rst new file mode 100644 index 000000000..06adfb060 --- /dev/null +++ b/docs/how-to/debugging/vscode.rst @@ -0,0 +1,89 @@ +================ +Debug via VSCode +================ + +Debugging is possible at different stages in your development process. It is +different to debug a development app via ``briefcase dev`` than an bundled app +that is build via ``briefcase build`` and run via ``briefcase run``. + +Development +----------- +During development on your host system you should use ``briefcase dev``. To +attach VSCode debugger you can simply create a configuration like this, +that runs ``briefcase dev`` for you and attaches a debugger. + +.. code-block:: JSON + + { + "version": "0.2.0", + "configurations": [ + { + "name": "Briefcase: Dev", + "type": "debugpy", + "request": "launch", + "module": "briefcase", + "args": [ + "dev", + ], + "justMyCode": false + }, + ] + } + + +Bundled App +----------- +It is also possible to debug a bundled app. This is currently still an **experimental feature** that is only +supported on Windows and macOS. The full power of this feature will become available when iOS and +Android are supported, because that is the only way to debug an iOS or Android app. + +To debug a bundled app a piece of the debugger has to be embedded into your app. This is done via: + +.. code-block:: console + + $ briefcase build --debug debugpy + +This will build your app in debug mode and add `debugpy `_ together with a +package that automatically starts ``debugpy`` at the startup of your bundled app. + +Then it is time to run your app. You can do this via: + +.. code-block:: console + + $ briefcase run --debug debugpy + +Running the app in debug mode will automatically start the ``debugpy`` debugger +and listen for incoming connections. By default it will listen on ``localhost`` +and port ``5678``. You can then connect your VSCode debugger to the app by +creating a configuration like this in the ``.vscode/launch.json`` file: + +.. code-block:: JSON + + { + "version": "0.2.0", + "configurations": [ + { + "name": "Briefcase: Attach", + "type": "debugpy", + "request": "attach", + "connect": { + "host": "localhost", + "port": 5678 + } + } + ] + } + +The app will not start until you attach the debugger. Once you attached the +VSCode debugger you are ready to debug your app. You can set `breakpoints `_ +, use the `data inspection `_ +, use the `debug console REPL `_ +and all other debugging features of VSCode :) + +But there are also some restrictions, that do not work: + - Restart the debugger via the green circle is not working correctly. + +Some more notes to the ``.vscodelaunch.json`` options: + - It is not required to specify ``pathMappings``. This will be done by briefcase dynamically. + - If you want to debug not only your own code but also external libraries that you have defined using :attr:`requires`, you can additionally +set `justMyCode `_ to ``false``. diff --git a/docs/how-to/index.rst b/docs/how-to/index.rst index 1e60be3e9..d206f3c03 100644 --- a/docs/how-to/index.rst +++ b/docs/how-to/index.rst @@ -17,6 +17,7 @@ stand alone. ci cli-apps x11passthrough + debugging/index external-apps publishing/index contribute/index diff --git a/docs/reference/commands/build.rst b/docs/reference/commands/build.rst index d13c8bb3d..885d8e8c8 100644 --- a/docs/reference/commands/build.rst +++ b/docs/reference/commands/build.rst @@ -122,13 +122,16 @@ bundled app. Currently the following debuggers are supported: -- ``pdb``: This is used for debugging via console. -- ``debugpy``: This is used for debugging via VSCode. +- ``pdb``: This is used for debugging via console (see :doc:`Debug via Console `) +- ``debugpy``: This is used for debugging via VSCode (see :doc:`Debug via VSCode `) - If calling only ``--debug`` without selecting a debugger explicitly, ``pdb`` is used as default. This is an experimental new feature, that is currently only support on Windows and macOS. +This option may slow down the app a little bit. + + ``--no-update`` --------------- diff --git a/docs/reference/commands/run.rst b/docs/reference/commands/run.rst index 11b75d4e6..a648672aa 100644 --- a/docs/reference/commands/run.rst +++ b/docs/reference/commands/run.rst @@ -145,8 +145,13 @@ debugger connection via a socket. Currently the following debuggers are supported: -- ``pdb``: This is used for debugging via console. -- ``debugpy``: This is used for debugging via VSCode. +- ``pdb``: This is used for debugging via console (see :doc:`Debug via Console `) +- ``debugpy``: This is used for debugging via VSCode (see :doc:`Debug via VSCode `) + +For ``debugpy`` there is also a mapping of the source code from your bundled +app to your local copy of the apps source code in the ``build`` folder. This +is useful for devices like iOS and Android, where the running source code is +not available on the host system. If calling only ``--debug`` without selecting a debugger explicitly, ``pdb`` is used as default. @@ -166,6 +171,9 @@ Specifies the port of the socket connection for the debugger. This option is only used when the ``--debug `` option is specified. The default value is ``5678``. +On Android this also forwards the port from the Android device to the host pc +via ADB if the port is ``localhost``. + Passthrough arguments --------------------- From 17f2a9320c7b13ce8ebf3d5a78d8e9eb5abe3dad Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sun, 17 Aug 2025 21:18:50 +0200 Subject: [PATCH 078/131] "justMyCode" has to be set to false --- .../src/briefcase_debugpy/_remote_debugger.py | 3 ++- docs/how-to/debugging/vscode.rst | 13 ++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/debugger-support/briefcase-debugpy/src/briefcase_debugpy/_remote_debugger.py b/debugger-support/briefcase-debugpy/src/briefcase_debugpy/_remote_debugger.py index 0323dd6c6..a75b79089 100644 --- a/debugger-support/briefcase-debugpy/src/briefcase_debugpy/_remote_debugger.py +++ b/debugger-support/briefcase-debugpy/src/briefcase_debugpy/_remote_debugger.py @@ -117,7 +117,8 @@ def start_debugpy(config_str: str, verbose: bool): "connect": {{ "host": "{host}", "port": {port} - }} + }}, + "justMyCode": false }} ] }} diff --git a/docs/how-to/debugging/vscode.rst b/docs/how-to/debugging/vscode.rst index 06adfb060..be9197e96 100644 --- a/docs/how-to/debugging/vscode.rst +++ b/docs/how-to/debugging/vscode.rst @@ -70,6 +70,7 @@ creating a configuration like this in the ``.vscode/launch.json`` file: "host": "localhost", "port": 5678 } + "justMyCode": false } ] } @@ -78,12 +79,10 @@ The app will not start until you attach the debugger. Once you attached the VSCode debugger you are ready to debug your app. You can set `breakpoints `_ , use the `data inspection `_ , use the `debug console REPL `_ -and all other debugging features of VSCode :) +and all other debugging features of VSCode 🙂 -But there are also some restrictions, that do not work: - - Restart the debugger via the green circle is not working correctly. +But there are some restrictions, that must be taken into account: -Some more notes to the ``.vscodelaunch.json`` options: - - It is not required to specify ``pathMappings``. This will be done by briefcase dynamically. - - If you want to debug not only your own code but also external libraries that you have defined using :attr:`requires`, you can additionally -set `justMyCode `_ to ``false``. +- Restart the debugger via the green circle is not working correctly. +- ``justMyCode`` has to be set to ``false``. An incorrect configuration can disrupt debugging support. +- ``pathMappings`` should not be set manually. This will be set by briefcase dynamically. An incorrect configuration can disrupt debugging support. From c4e0ed0531e2c70254767b3126f43329fbf48d8f Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sun, 17 Aug 2025 21:56:57 +0200 Subject: [PATCH 079/131] added hint to build and run, that the selected debuggers has to match. --- docs/reference/commands/build.rst | 10 ++++++++-- docs/reference/commands/run.rst | 5 ++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/reference/commands/build.rst b/docs/reference/commands/build.rst index 885d8e8c8..a7746ccad 100644 --- a/docs/reference/commands/build.rst +++ b/docs/reference/commands/build.rst @@ -124,13 +124,19 @@ Currently the following debuggers are supported: - ``pdb``: This is used for debugging via console (see :doc:`Debug via Console `) - ``debugpy``: This is used for debugging via VSCode (see :doc:`Debug via VSCode `) -- + If calling only ``--debug`` without selecting a debugger explicitly, ``pdb`` is used as default. -This is an experimental new feature, that is currently only support on Windows and macOS. +This is an **experimental** new feature, that is currently only support on Windows and macOS. This option may slow down the app a little bit. +If you have previously run the app in "normal" mode, you may need to pass ``-r`` +/ ``--update-requirements`` the first time you build in debug mode to ensure that +the debugger is embedded in your bundled app. + +The selected debugger in ``build --debug `` has to match the selected +debugger in ``run --debug ``. ``--no-update`` --------------- diff --git a/docs/reference/commands/run.rst b/docs/reference/commands/run.rst index a648672aa..f190a2e07 100644 --- a/docs/reference/commands/run.rst +++ b/docs/reference/commands/run.rst @@ -155,7 +155,10 @@ not available on the host system. If calling only ``--debug`` without selecting a debugger explicitly, ``pdb`` is used as default. -This is an experimental new feature, that is currently only support on Windows and macOS. +This is an **experimental** new feature, that is currently only support on Windows and macOS. + +The selected debugger in ``run --debug `` has to match the selected +debugger in ``build --debug ``. ``--debugger-host `` -------------------------- From 27af64ee3bcee13bd644756239f83c4fed1db40e Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sun, 17 Aug 2025 22:25:26 +0200 Subject: [PATCH 080/131] added warning to packaging --- docs/reference/commands/package.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/reference/commands/package.rst b/docs/reference/commands/package.rst index 23d012c83..00410b042 100644 --- a/docs/reference/commands/package.rst +++ b/docs/reference/commands/package.rst @@ -7,6 +7,8 @@ platform's default output format. This will produce an installable artefact. +You should not package an application that is build using ``build --test`` or ``build --debug ``. + Usage ===== From 4c8a4f05a269a3750dca36b9e8e5648163de1896 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sun, 17 Aug 2025 22:30:27 +0200 Subject: [PATCH 081/131] removed reference to android --- docs/reference/commands/run.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/reference/commands/run.rst b/docs/reference/commands/run.rst index f190a2e07..430a5ceb9 100644 --- a/docs/reference/commands/run.rst +++ b/docs/reference/commands/run.rst @@ -174,9 +174,6 @@ Specifies the port of the socket connection for the debugger. This option is only used when the ``--debug `` option is specified. The default value is ``5678``. -On Android this also forwards the port from the Android device to the host pc -via ADB if the port is ``localhost``. - Passthrough arguments --------------------- From 52241774f9e3892c6ea41676930e529afbaf5775 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Sun, 17 Aug 2025 22:39:35 +0200 Subject: [PATCH 082/131] removed test ci branch used for testing in my fork --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3bd59bd88..c32649e7d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,6 @@ on: push: branches: - main - - ci-test # TODO: Remove this workflow_call: inputs: attest-package: From 42554e9317ef431f17d3299b52e44f6bd3db6890 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Mon, 1 Sep 2025 22:52:09 +0200 Subject: [PATCH 083/131] add verbose debugpy output --- .../src/briefcase_debugpy/_remote_debugger.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/debugger-support/briefcase-debugpy/src/briefcase_debugpy/_remote_debugger.py b/debugger-support/briefcase-debugpy/src/briefcase_debugpy/_remote_debugger.py index a75b79089..df78f1e89 100644 --- a/debugger-support/briefcase-debugpy/src/briefcase_debugpy/_remote_debugger.py +++ b/debugger-support/briefcase-debugpy/src/briefcase_debugpy/_remote_debugger.py @@ -94,6 +94,11 @@ def start_debugpy(config_str: str, verbose: bool): print(f"Starting debugpy in server mode at {host}:{port}...") debugpy.listen((host, port), in_process_debug_adapter=True) + if verbose: + import pydevd + + pydevd.DebugInfoHolder.DEBUG_TRACE_LEVEL = 3 + if len(path_mappings) > 0: if verbose: print("Adding path mappings...") From d06f45c4631e48cf5114d4f4e6e352236e18f42b Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Mon, 1 Sep 2025 22:55:52 +0200 Subject: [PATCH 084/131] correct comment --- .../src/briefcase_debugpy/_remote_debugger.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/debugger-support/briefcase-debugpy/src/briefcase_debugpy/_remote_debugger.py b/debugger-support/briefcase-debugpy/src/briefcase_debugpy/_remote_debugger.py index df78f1e89..9f2f19c34 100644 --- a/debugger-support/briefcase-debugpy/src/briefcase_debugpy/_remote_debugger.py +++ b/debugger-support/briefcase-debugpy/src/briefcase_debugpy/_remote_debugger.py @@ -95,6 +95,7 @@ def start_debugpy(config_str: str, verbose: bool): debugpy.listen((host, port), in_process_debug_adapter=True) if verbose: + # pydevd is dynamically loaded and only available after debugpy is started import pydevd pydevd.DebugInfoHolder.DEBUG_TRACE_LEVEL = 3 @@ -103,7 +104,7 @@ def start_debugpy(config_str: str, verbose: bool): if verbose: print("Adding path mappings...") - # pydevd is dynamically loaded and only available after a debugger has connected + # pydevd is dynamically loaded and only available after debugpy is started import pydevd_file_utils pydevd_file_utils.setup_client_server_paths(path_mappings) From 17df33470b9bdb7c5652f51500923f0f1aa0f2c7 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Tue, 2 Sep 2025 00:10:24 +0200 Subject: [PATCH 085/131] merged "briefcase-debugpy" and "briefcase-pdb" to "briefcase-debugger" --- .../{briefcase-pdb => }/README.md | 6 +- debugger-support/briefcase-debugpy/README.md | 7 -- .../briefcase-debugpy/pyproject.toml | 25 ---- debugger-support/briefcase-debugpy/setup.py | 42 ------- .../lib/briefcase_debugger}/__init__.py | 15 ++- .../build/lib/briefcase_debugger/config.py | 20 +++ .../lib/briefcase_debugger/debugpy.py} | 32 +---- .../lib/briefcase_debugger/pdb.py} | 11 +- .../{briefcase-pdb => }/pyproject.toml | 14 ++- debugger-support/{briefcase-pdb => }/setup.py | 4 +- .../briefcase_debugger}/__init__.py | 15 ++- .../src/briefcase_debugger/config.py | 20 +++ .../src/briefcase_debugger/debugpy.py | 118 ++++++++++++++++++ .../src/briefcase_debugger/pdb.py | 33 +++++ .../test_debugpy.py} | 14 ++- .../tests/test_path_mappings.py | 0 .../test_pdb.py} | 12 +- src/briefcase/commands/run.py | 1 + src/briefcase/debuggers/base.py | 14 ++- src/briefcase/debuggers/debugpy.py | 7 +- src/briefcase/debuggers/pdb.py | 7 +- 21 files changed, 280 insertions(+), 137 deletions(-) rename debugger-support/{briefcase-pdb => }/README.md (65%) delete mode 100644 debugger-support/briefcase-debugpy/README.md delete mode 100644 debugger-support/briefcase-debugpy/pyproject.toml delete mode 100644 debugger-support/briefcase-debugpy/setup.py rename debugger-support/{briefcase-pdb/src/briefcase_pdb => build/lib/briefcase_debugger}/__init__.py (71%) create mode 100644 debugger-support/build/lib/briefcase_debugger/config.py rename debugger-support/{briefcase-debugpy/src/briefcase_debugpy/_remote_debugger.py => build/lib/briefcase_debugger/debugpy.py} (82%) rename debugger-support/{briefcase-pdb/src/briefcase_pdb/_remote_debugger.py => build/lib/briefcase_debugger/pdb.py} (79%) rename debugger-support/{briefcase-pdb => }/pyproject.toml (66%) rename debugger-support/{briefcase-pdb => }/setup.py (94%) rename debugger-support/{briefcase-debugpy/src/briefcase_debugpy => src/briefcase_debugger}/__init__.py (71%) create mode 100644 debugger-support/src/briefcase_debugger/config.py create mode 100644 debugger-support/src/briefcase_debugger/debugpy.py create mode 100644 debugger-support/src/briefcase_debugger/pdb.py rename debugger-support/{briefcase-debugpy/tests/test_start_remote_debugger.py => tests/test_debugpy.py} (93%) rename debugger-support/{briefcase-debugpy => }/tests/test_path_mappings.py (100%) rename debugger-support/{briefcase-pdb/tests/test_start_remote_debugger.py => tests/test_pdb.py} (87%) diff --git a/debugger-support/briefcase-pdb/README.md b/debugger-support/README.md similarity index 65% rename from debugger-support/briefcase-pdb/README.md rename to debugger-support/README.md index fbc945b44..6665004e2 100644 --- a/debugger-support/briefcase-pdb/README.md +++ b/debugger-support/README.md @@ -1,7 +1,7 @@ -# Briefcase Pdb Debugger Support -This package contains the debugger support package for the pdb debugger. +# Briefcase Debugger Support +This package contains the debugger support package for the `pdb` and `debugpy` debuggers. It starts the remote debugger automatically at startup through an .pth file, if a `BRIEFCASE_DEBUGGER` environment variable is set. ## Installation -Normally you do not need to install this package manually, because it is done automatically by briefcase using the `--debug=pdb` option. +Normally you do not need to install this package manually, because it is done automatically by briefcase using the `--debug=pdb` or `--debug=debugpy` option. diff --git a/debugger-support/briefcase-debugpy/README.md b/debugger-support/briefcase-debugpy/README.md deleted file mode 100644 index 0cf8ffbec..000000000 --- a/debugger-support/briefcase-debugpy/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Briefcase Debugpy Debugger Support -This package contains the debugger support package for the debugpy debugger. - -It starts the remote debugger automatically at startup through an .pth file, if a `BRIEFCASE_DEBUGGER` environment variable is set. - -## Installation -Normally you do not need to install this package manually, because it is done automatically by briefcase using the `--debug=debugpy` option. diff --git a/debugger-support/briefcase-debugpy/pyproject.toml b/debugger-support/briefcase-debugpy/pyproject.toml deleted file mode 100644 index e0a43d6ef..000000000 --- a/debugger-support/briefcase-debugpy/pyproject.toml +++ /dev/null @@ -1,25 +0,0 @@ -[build-system] -requires = [ - # keep versions in sync with automation/pyproject.toml and ../pyproject.toml - "setuptools==80.9.0", - "setuptools_scm==8.3.1", -] -build-backend = "setuptools.build_meta" - -[project] -name = "briefcase-debugpy" -description = "Add-on for briefcase to add remote debugging via debugpy." -readme = "README.md" -license = "BSD-3-Clause" -dependencies = [ - "debugpy>=1.8.14,<2.0.0" -] -dynamic = ["version"] - -[tool.setuptools_scm] -root = "../../" - -[project.optional-dependencies] -dev = [ - "pytest == 8.4.1" -] diff --git a/debugger-support/briefcase-debugpy/setup.py b/debugger-support/briefcase-debugpy/setup.py deleted file mode 100644 index 89941f3bb..000000000 --- a/debugger-support/briefcase-debugpy/setup.py +++ /dev/null @@ -1,42 +0,0 @@ -import os - -import setuptools -from setuptools.command.install import install - - -# Copied from setuptools: -# (https://github.com/pypa/setuptools/blob/7c859e017368360ba66c8cc591279d8964c031bc/setup.py#L40C6-L82) -class install_with_pth(install): - """Custom install command to install a .pth file for distutils patching. - - This hack is necessary because there's no standard way to install behavior - on startup (and it's debatable if there should be one). This hack (ab)uses - the `extra_path` behavior in Setuptools to install a `.pth` file with - implicit behavior on startup to give higher precedence to the local version - of `distutils` over the version from the standard library. - - Please do not replicate this behavior. - """ - - _pth_name = "briefcase_debugpy" - _pth_contents = "import briefcase_debugpy" - - def initialize_options(self): - install.initialize_options(self) - self.extra_path = self._pth_name, self._pth_contents - - def finalize_options(self): - install.finalize_options(self) - self._restore_install_lib() - - def _restore_install_lib(self): - """Undo secondary effect of `extra_path` adding to `install_lib`""" - suffix = os.path.relpath(self.install_lib, self.install_libbase) - - if suffix.strip() == self._pth_contents.strip(): - self.install_lib = self.install_libbase - - -setuptools.setup( - cmdclass={"install": install_with_pth}, -) diff --git a/debugger-support/briefcase-pdb/src/briefcase_pdb/__init__.py b/debugger-support/build/lib/briefcase_debugger/__init__.py similarity index 71% rename from debugger-support/briefcase-pdb/src/briefcase_pdb/__init__.py rename to debugger-support/build/lib/briefcase_debugger/__init__.py index 2faaa462a..8652a30f3 100644 --- a/debugger-support/briefcase-pdb/src/briefcase_pdb/__init__.py +++ b/debugger-support/build/lib/briefcase_debugger/__init__.py @@ -1,8 +1,9 @@ +import json import os import sys import traceback -from ._remote_debugger import start_pdb +from briefcase_debugger.config import DebuggerConfig REMOTE_DEBUGGER_STARTED = False @@ -28,9 +29,19 @@ def start_remote_debugger(): if verbose: print(f"'BRIEFCASE_DEBUGGER'={config_str}") + # Parsing config json + config: DebuggerConfig = json.loads(config_str) + # start debugger print("Starting remote debugger...") - start_pdb(config_str, verbose) + if config["debugger"] == "debugpy": + from briefcase_debugger.debugpy import start_debugpy + + start_debugpy(config, verbose) + elif config["debugger"] == "pdb": + from briefcase_debugger.pdb import start_pdb + + start_pdb(config, verbose) # only start remote debugger on the first import diff --git a/debugger-support/build/lib/briefcase_debugger/config.py b/debugger-support/build/lib/briefcase_debugger/config.py new file mode 100644 index 000000000..3c62ac87f --- /dev/null +++ b/debugger-support/build/lib/briefcase_debugger/config.py @@ -0,0 +1,20 @@ +from typing import Optional, TypedDict + + +class AppPathMappings(TypedDict): + device_sys_path_regex: str + device_subfolders: list[str] + host_folders: list[str] + + +class AppPackagesPathMappings(TypedDict): + sys_path_regex: str + host_folder: str + + +class DebuggerConfig(TypedDict): + debugger: str + host: str + port: int + app_path_mappings: Optional[AppPathMappings] + app_packages_path_mappings: Optional[AppPackagesPathMappings] diff --git a/debugger-support/briefcase-debugpy/src/briefcase_debugpy/_remote_debugger.py b/debugger-support/build/lib/briefcase_debugger/debugpy.py similarity index 82% rename from debugger-support/briefcase-debugpy/src/briefcase_debugpy/_remote_debugger.py rename to debugger-support/build/lib/briefcase_debugger/debugpy.py index 9f2f19c34..919106cac 100644 --- a/debugger-support/briefcase-debugpy/src/briefcase_debugpy/_remote_debugger.py +++ b/debugger-support/build/lib/briefcase_debugger/debugpy.py @@ -1,29 +1,12 @@ -import json import os import re import sys from pathlib import Path -from typing import Optional, TypedDict +from typing import Optional import debugpy - -class AppPathMappings(TypedDict): - device_sys_path_regex: str - device_subfolders: list[str] - host_folders: list[str] - - -class AppPackagesPathMappings(TypedDict): - sys_path_regex: str - host_folder: str - - -class DebuggerConfig(TypedDict): - host: str - port: int - app_path_mappings: Optional[AppPathMappings] - app_packages_path_mappings: Optional[AppPackagesPathMappings] +from briefcase_debugger.config import DebuggerConfig def find_first_matching_path(regex: str) -> Optional[str]: @@ -75,13 +58,10 @@ def load_path_mappings(config: DebuggerConfig, verbose: bool) -> list[tuple[str, return mappings_list -def start_debugpy(config_str: str, verbose: bool): - # Parsing config json - debugger_config: dict = json.loads(config_str) - - host = debugger_config["host"] - port = debugger_config["port"] - path_mappings = load_path_mappings(debugger_config, verbose) +def start_debugpy(config: DebuggerConfig, verbose: bool): + host = config["host"] + port = config["port"] + path_mappings = load_path_mappings(config, verbose) # There is a bug in debugpy that has to be handled until there is a new # debugpy release, see https://github.com/microsoft/debugpy/issues/1943 diff --git a/debugger-support/briefcase-pdb/src/briefcase_pdb/_remote_debugger.py b/debugger-support/build/lib/briefcase_debugger/pdb.py similarity index 79% rename from debugger-support/briefcase-pdb/src/briefcase_pdb/_remote_debugger.py rename to debugger-support/build/lib/briefcase_debugger/pdb.py index 13d97bf92..86b8747f8 100644 --- a/debugger-support/briefcase-pdb/src/briefcase_pdb/_remote_debugger.py +++ b/debugger-support/build/lib/briefcase_debugger/pdb.py @@ -1,16 +1,15 @@ -import json import sys from remote_pdb import RemotePdb +from briefcase_debugger.config import DebuggerConfig -def start_pdb(config_str: str, verbose: bool): - """Start remote PDB server.""" - debugger_config: dict = json.loads(config_str) +def start_pdb(config: DebuggerConfig, verbose: bool): + """Start remote PDB server.""" # Parsing host/port - host = debugger_config["host"] - port = debugger_config["port"] + host = config["host"] + port = config["port"] print( f""" diff --git a/debugger-support/briefcase-pdb/pyproject.toml b/debugger-support/pyproject.toml similarity index 66% rename from debugger-support/briefcase-pdb/pyproject.toml rename to debugger-support/pyproject.toml index 9de3fa18c..83c371f87 100644 --- a/debugger-support/briefcase-pdb/pyproject.toml +++ b/debugger-support/pyproject.toml @@ -7,19 +7,25 @@ requires = [ build-backend = "setuptools.build_meta" [project] -name = "briefcase-pdb" -description = "Add-on for briefcase to add remote debugging via pdb." +name = "briefcase-debugger" +description = "Add-on for briefcase to add remote debugging." readme = "README.md" license = "BSD-3-Clause" dependencies = [ - "remote-pdb>=2.1.0,<3.0.0" + # see "pdb" or "debugpy" optional dependencies below ] dynamic = ["version"] [tool.setuptools_scm] -root = "../../" +root = "../" [project.optional-dependencies] dev = [ "pytest == 8.4.1" ] +pdb = [ + "remote-pdb>=2.1.0,<3.0.0" +] +debugpy = [ + "debugpy>=1.8.14,<2.0.0" +] diff --git a/debugger-support/briefcase-pdb/setup.py b/debugger-support/setup.py similarity index 94% rename from debugger-support/briefcase-pdb/setup.py rename to debugger-support/setup.py index e49e88e89..70312716f 100644 --- a/debugger-support/briefcase-pdb/setup.py +++ b/debugger-support/setup.py @@ -18,8 +18,8 @@ class install_with_pth(install): Please do not replicate this behavior. """ - _pth_name = "briefcase_pdb" - _pth_contents = "import briefcase_pdb" + _pth_name = "briefcase_debugger" + _pth_contents = "import briefcase_debugger" def initialize_options(self): install.initialize_options(self) diff --git a/debugger-support/briefcase-debugpy/src/briefcase_debugpy/__init__.py b/debugger-support/src/briefcase_debugger/__init__.py similarity index 71% rename from debugger-support/briefcase-debugpy/src/briefcase_debugpy/__init__.py rename to debugger-support/src/briefcase_debugger/__init__.py index 3af2699b4..46b4f4b68 100644 --- a/debugger-support/briefcase-debugpy/src/briefcase_debugpy/__init__.py +++ b/debugger-support/src/briefcase_debugger/__init__.py @@ -1,8 +1,9 @@ +import json import os import sys import traceback -from ._remote_debugger import start_debugpy +from briefcase_debugger.config import DebuggerConfig REMOTE_DEBUGGER_STARTED = False @@ -28,9 +29,19 @@ def start_remote_debugger(): if verbose: print(f"'BRIEFCASE_DEBUGGER'={config_str}") + # Parsing config json + config: DebuggerConfig = json.loads(config_str) + # start debugger print("Starting remote debugger...") - start_debugpy(config_str, verbose) + if config["debugger"] == "debugpy": + from briefcase_debugger.debugpy import start_debugpy + + start_debugpy(config, verbose) + elif config["debugger"] == "pdb": + from briefcase_debugger.pdb import start_pdb + + start_pdb(config_str, verbose) # only start remote debugger on the first import diff --git a/debugger-support/src/briefcase_debugger/config.py b/debugger-support/src/briefcase_debugger/config.py new file mode 100644 index 000000000..3c62ac87f --- /dev/null +++ b/debugger-support/src/briefcase_debugger/config.py @@ -0,0 +1,20 @@ +from typing import Optional, TypedDict + + +class AppPathMappings(TypedDict): + device_sys_path_regex: str + device_subfolders: list[str] + host_folders: list[str] + + +class AppPackagesPathMappings(TypedDict): + sys_path_regex: str + host_folder: str + + +class DebuggerConfig(TypedDict): + debugger: str + host: str + port: int + app_path_mappings: Optional[AppPathMappings] + app_packages_path_mappings: Optional[AppPackagesPathMappings] diff --git a/debugger-support/src/briefcase_debugger/debugpy.py b/debugger-support/src/briefcase_debugger/debugpy.py new file mode 100644 index 000000000..785f316b6 --- /dev/null +++ b/debugger-support/src/briefcase_debugger/debugpy.py @@ -0,0 +1,118 @@ +import os +import re +import sys +from pathlib import Path +from typing import Optional + +import debugpy + +from briefcase_debugger.config import DebuggerConfig + + +def find_first_matching_path(regex: str) -> Optional[str]: + """Gibt das erste Element aus paths zurück, das auf regex matcht, sonst None.""" + for p in sys.path: + if re.search(regex, p): + return p + return None + + +def load_path_mappings(config: DebuggerConfig, verbose: bool) -> list[tuple[str, str]]: + app_path_mappings = config.get("app_path_mappings", None) + app_packages_path_mappings = config.get("app_packages_path_mappings", None) + + mappings_list = [] + if app_path_mappings: + device_app_folder = find_first_matching_path( + app_path_mappings["device_sys_path_regex"] + ) + if device_app_folder: + for app_subfolder_device, app_subfolder_host in zip( + app_path_mappings["device_subfolders"], + app_path_mappings["host_folders"], + ): + mappings_list.append( + ( + app_subfolder_host, + str(Path(device_app_folder) / app_subfolder_device), + ) + ) + if app_packages_path_mappings: + device_app_packages_folder = find_first_matching_path( + app_packages_path_mappings["sys_path_regex"] + ) + if device_app_packages_folder: + mappings_list.append( + ( + app_packages_path_mappings["host_folder"], + str(Path(device_app_packages_folder)), + ) + ) + + if verbose: + print("Extracted path mappings:") + for idx, p in enumerate(mappings_list): + print(f"[{idx}] host = {p[0]}") + print(f"[{idx}] device = {p[1]}") + + return mappings_list + + +def start_debugpy(config: DebuggerConfig, verbose: bool): + host = config["host"] + port = config["port"] + path_mappings = load_path_mappings(config, verbose) + + # There is a bug in debugpy that has to be handled until there is a new + # debugpy release, see https://github.com/microsoft/debugpy/issues/1943 + if not hasattr(os, "__file__"): + if verbose: + print("'os.__file__' not available. Patching it...") + os.__file__ = "" + + # Starting remote debugger... + print(f"Starting debugpy in server mode at {host}:{port}...") + debugpy.listen((host, port), in_process_debug_adapter=True) + + if verbose: + # pydevd is dynamically loaded and only available after debugpy is started + import pydevd + + pydevd.DebugInfoHolder.DEBUG_TRACE_LEVEL = 3 + + if len(path_mappings) > 0: + if verbose: + print("Adding path mappings...") + + # pydevd is dynamically loaded and only available after a debugger has connected + import pydevd_file_utils + + pydevd_file_utils.setup_client_server_paths(path_mappings) + + print("The debugpy server started. Waiting for debugger to attach...") + print( + f""" +To connect to debugpy using VSCode add the following configuration to '.vscode/launch.json': +{{ + "version": "0.2.0", + "configurations": [ + {{ + "name": "Briefcase: Attach (Connect)", + "type": "debugpy", + "request": "attach", + "connect": {{ + "host": "{host}", + "port": {port} + }}, + "justMyCode": false + }} + ] +}} + +For more information see: https://briefcase.readthedocs.io/en/stable/how-to/debugging/vscode.html#bundled-app +""" + ) + debugpy.wait_for_client() + + print("Debugger attached.") + print("-" * 75) diff --git a/debugger-support/src/briefcase_debugger/pdb.py b/debugger-support/src/briefcase_debugger/pdb.py new file mode 100644 index 000000000..86b8747f8 --- /dev/null +++ b/debugger-support/src/briefcase_debugger/pdb.py @@ -0,0 +1,33 @@ +import sys + +from remote_pdb import RemotePdb + +from briefcase_debugger.config import DebuggerConfig + + +def start_pdb(config: DebuggerConfig, verbose: bool): + """Start remote PDB server.""" + # Parsing host/port + host = config["host"] + port = config["port"] + + print( + f""" +Remote PDB server opened at {host}:{port}. +Waiting for debugger to attach... +To connect to remote PDB use eg.: + - telnet {host} {port} (Windows) + - rlwrap socat - tcp:{host}:{port} (Linux, macOS) + +For more information see: https://briefcase.readthedocs.io/en/stable/how-to/debugging/console.html#bundled-app +""" + ) + + # Create a RemotePdb instance + remote_pdb = RemotePdb(host, port, quiet=True) + + # Connect the remote PDB with the "breakpoint()" function + sys.breakpointhook = remote_pdb.set_trace + + print("Debugger client attached.") + print("-" * 75) diff --git a/debugger-support/briefcase-debugpy/tests/test_start_remote_debugger.py b/debugger-support/tests/test_debugpy.py similarity index 93% rename from debugger-support/briefcase-debugpy/tests/test_start_remote_debugger.py rename to debugger-support/tests/test_debugpy.py index fc757eaff..228c23ab6 100644 --- a/debugger-support/briefcase-debugpy/tests/test_start_remote_debugger.py +++ b/debugger-support/tests/test_debugpy.py @@ -4,7 +4,7 @@ from pathlib import Path, PosixPath, PureWindowsPath from unittest.mock import MagicMock -import briefcase_debugpy +import briefcase_debugger import debugpy import pytest @@ -15,7 +15,7 @@ def test_no_env_vars(monkeypatch, capsys): monkeypatch.setattr(os, "environ", os_environ) # start test function - briefcase_debugpy.start_remote_debugger() + briefcase_debugger.start_remote_debugger() captured = capsys.readouterr() assert captured.out == "" @@ -30,7 +30,7 @@ def test_no_debugger_verbose(monkeypatch, capsys): monkeypatch.setattr(os, "environ", os_environ) # start test function - briefcase_debugpy.start_remote_debugger() + briefcase_debugger.start_remote_debugger() captured = capsys.readouterr() assert ( @@ -45,7 +45,9 @@ def test_with_debugger(monkeypatch, capsys, verbose): """Test a normal debug session.""" # When running tests on Linux/macOS, we have to switch to WindowsPath. if isinstance(Path(), PosixPath): - monkeypatch.setattr(briefcase_debugpy._remote_debugger, "Path", PureWindowsPath) + monkeypatch.setattr( + briefcase_debugger._remote_debugger, "Path", PureWindowsPath + ) os_environ = {} os_environ["BRIEFCASE_DEBUG"] = "1" if verbose else "0" @@ -81,7 +83,7 @@ def test_with_debugger(monkeypatch, capsys, verbose): monkeypatch.setitem(sys.modules, "pydevd_file_utils", fake_pydevd_file_utils) # start test function - briefcase_debugpy.start_remote_debugger() + briefcase_debugger.start_remote_debugger() fake_debugpy_listen.assert_called_once_with( ("somehost", 9999), @@ -126,7 +128,7 @@ def test_os_file_bugfix(monkeypatch, capsys, verbose): monkeypatch.setattr(debugpy, "wait_for_client", fake_debugpy_wait_for_client) # start test function - briefcase_debugpy.start_remote_debugger() + briefcase_debugger.start_remote_debugger() fake_debugpy_listen.assert_called_once_with( ("somehost", 9999), diff --git a/debugger-support/briefcase-debugpy/tests/test_path_mappings.py b/debugger-support/tests/test_path_mappings.py similarity index 100% rename from debugger-support/briefcase-debugpy/tests/test_path_mappings.py rename to debugger-support/tests/test_path_mappings.py diff --git a/debugger-support/briefcase-pdb/tests/test_start_remote_debugger.py b/debugger-support/tests/test_pdb.py similarity index 87% rename from debugger-support/briefcase-pdb/tests/test_start_remote_debugger.py rename to debugger-support/tests/test_pdb.py index 61c6f0dad..0bdd42394 100644 --- a/debugger-support/briefcase-pdb/tests/test_start_remote_debugger.py +++ b/debugger-support/tests/test_pdb.py @@ -3,8 +3,8 @@ import sys from unittest.mock import MagicMock -import briefcase_pdb -import briefcase_pdb._remote_debugger +import briefcase_debugger +import briefcase_debugger.pdb # import remote_pdb import pytest @@ -16,7 +16,7 @@ def test_no_env_vars(monkeypatch, capsys): monkeypatch.setattr(os, "environ", os_environ) # start test function - briefcase_pdb.start_remote_debugger() + briefcase_debugger.start_remote_debugger() captured = capsys.readouterr() assert captured.out == "" @@ -31,7 +31,7 @@ def test_no_debugger_verbose(monkeypatch, capsys): monkeypatch.setattr(os, "environ", os_environ) # start test function - briefcase_pdb.start_remote_debugger() + briefcase_debugger.start_remote_debugger() captured = capsys.readouterr() assert ( @@ -55,7 +55,7 @@ def test_with_debugger(monkeypatch, capsys, verbose): monkeypatch.setattr(os, "environ", os_environ) fake_remote_pdb = MagicMock() - monkeypatch.setattr(briefcase_pdb._remote_debugger, "RemotePdb", fake_remote_pdb) + monkeypatch.setattr(briefcase_debugger.pdb, "RemotePdb", fake_remote_pdb) # pydevd is dynamically loaded and only available when a real debugger is attached. So # we fake the whole module, as otherwise the import in start_remote_debugger would fail @@ -64,7 +64,7 @@ def test_with_debugger(monkeypatch, capsys, verbose): monkeypatch.setitem(sys.modules, "pydevd_file_utils", fake_pydevd_file_utils) # start test function - briefcase_pdb.start_remote_debugger() + briefcase_debugger.start_remote_debugger() fake_remote_pdb.assert_called_once_with( "somehost", diff --git a/src/briefcase/commands/run.py b/src/briefcase/commands/run.py index 4bbcc5287..9f2f7e117 100644 --- a/src/briefcase/commands/run.py +++ b/src/briefcase/commands/run.py @@ -287,6 +287,7 @@ def debugger_config( app_path_mappings = self._debugger_app_path_mappings(app) app_packages_path_mappings = self._debugger_app_packages_path_mapping(app) config = DebuggerConfig( + debugger=app.debugger.name, host=debugger_host, port=debugger_port, app_path_mappings=app_path_mappings, diff --git a/src/briefcase/debuggers/base.py b/src/briefcase/debuggers/base.py index a85f3b5fd..de9b9d832 100644 --- a/src/briefcase/debuggers/base.py +++ b/src/briefcase/debuggers/base.py @@ -36,7 +36,7 @@ def _is_editable_pep610(dist_name: str) -> bool: REPO_ROOT = Path(__file__).parent.parent.parent.parent if IS_EDITABLE else None -def get_debugger_requirement(package_name: str): +def get_debugger_requirement(package_name: str, extras: str = ""): """Get the requirement of a debugger support package. On editable installs of briefcase the path to the local package is used, to simplify @@ -45,10 +45,10 @@ def get_debugger_requirement(package_name: str): version of briefcase. """ if IS_EDITABLE and REPO_ROOT is not None: - local_path = REPO_ROOT / "debugger-support" / package_name + local_path = REPO_ROOT / "debugger-support" if local_path.exists() and local_path.is_dir(): - return str(local_path) - return f"{package_name}=={briefcase.__version__}" + return f"{local_path}{extras}" + return f"{package_name}{extras}=={briefcase.__version__}" class AppPathMappings(TypedDict): @@ -63,6 +63,7 @@ class AppPackagesPathMappings(TypedDict): class DebuggerConfig(TypedDict): + debugger: str host: str port: int app_path_mappings: AppPathMappings | None @@ -77,6 +78,11 @@ class DebuggerConnectionMode(str, enum.Enum): class BaseDebugger(ABC): """Definition for a plugin that defines a new Briefcase debugger.""" + @property + @abstractmethod + def name(self) -> str: + """Return the name debugger.""" + @property @abstractmethod def connection_mode(self) -> DebuggerConnectionMode: diff --git a/src/briefcase/debuggers/debugpy.py b/src/briefcase/debuggers/debugpy.py index 1cca9f731..5e042a645 100644 --- a/src/briefcase/debuggers/debugpy.py +++ b/src/briefcase/debuggers/debugpy.py @@ -8,6 +8,11 @@ class DebugpyDebugger(BaseDebugger): """Definition for a plugin that defines a new Briefcase debugger.""" + @property + def name(self) -> str: + """Return the name debugger.""" + return "debugpy" + @property def connection_mode(self) -> DebuggerConnectionMode: """Return the connection mode of the debugger.""" @@ -16,4 +21,4 @@ def connection_mode(self) -> DebuggerConnectionMode: @property def debugger_support_pkg(self) -> str: """Get the name of the debugger support package.""" - return get_debugger_requirement("briefcase-debugpy") + return get_debugger_requirement("briefcase-debugger", "[debugpy]") diff --git a/src/briefcase/debuggers/pdb.py b/src/briefcase/debuggers/pdb.py index 3d051b3e3..ad66e6d74 100644 --- a/src/briefcase/debuggers/pdb.py +++ b/src/briefcase/debuggers/pdb.py @@ -8,6 +8,11 @@ class PdbDebugger(BaseDebugger): """Definition for a plugin that defines a new Briefcase debugger.""" + @property + def name(self) -> str: + """Return the name debugger.""" + return "pdb" + @property def connection_mode(self) -> DebuggerConnectionMode: """Return the connection mode of the debugger.""" @@ -16,4 +21,4 @@ def connection_mode(self) -> DebuggerConnectionMode: @property def debugger_support_pkg(self) -> str: """Get the name of the debugger support package.""" - return get_debugger_requirement("briefcase-pdb") + return get_debugger_requirement("briefcase-debugger", "[pdb]") From b9ebbee19a840e9c51cb4c035c4be6af1d68bb6a Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Tue, 2 Sep 2025 11:44:15 +0200 Subject: [PATCH 086/131] fixed unit tests --- .../src/briefcase_debugger/__init__.py | 2 +- debugger-support/tests/test_debugpy.py | 11 +++++++ ...pings.py => test_debugpy_path_mappings.py} | 30 +++++++++++-------- debugger-support/tests/test_pdb.py | 8 +---- .../create/test_install_app_requirements.py | 4 +++ tests/debuggers/test_base.py | 10 ++++--- tests/platforms/conftest.py | 4 +++ tests/platforms/macOS/app/test_run.py | 1 + tests/platforms/windows/app/test_run.py | 1 + .../windows/visualstudio/test_run.py | 1 + 10 files changed, 47 insertions(+), 25 deletions(-) rename debugger-support/tests/{test_path_mappings.py => test_debugpy_path_mappings.py} (86%) diff --git a/debugger-support/src/briefcase_debugger/__init__.py b/debugger-support/src/briefcase_debugger/__init__.py index 46b4f4b68..8652a30f3 100644 --- a/debugger-support/src/briefcase_debugger/__init__.py +++ b/debugger-support/src/briefcase_debugger/__init__.py @@ -41,7 +41,7 @@ def start_remote_debugger(): elif config["debugger"] == "pdb": from briefcase_debugger.pdb import start_pdb - start_pdb(config_str, verbose) + start_pdb(config, verbose) # only start remote debugger on the first import diff --git a/debugger-support/tests/test_debugpy.py b/debugger-support/tests/test_debugpy.py index 228c23ab6..233314aa8 100644 --- a/debugger-support/tests/test_debugpy.py +++ b/debugger-support/tests/test_debugpy.py @@ -53,6 +53,7 @@ def test_with_debugger(monkeypatch, capsys, verbose): os_environ["BRIEFCASE_DEBUG"] = "1" if verbose else "0" os_environ["BRIEFCASE_DEBUGGER"] = json.dumps( { + "debugger": "debugpy", "host": "somehost", "port": 9999, "app_path_mappings": { @@ -78,6 +79,8 @@ def test_with_debugger(monkeypatch, capsys, verbose): # pydevd is dynamically loaded and only available when a real debugger is attached. So # we fake the whole module, as otherwise the import in start_remote_debugger would fail + fake_pydevd = MagicMock() + monkeypatch.setitem(sys.modules, "pydevd", fake_pydevd) fake_pydevd_file_utils = MagicMock() fake_pydevd_file_utils.setup_client_server_paths.return_value = None monkeypatch.setitem(sys.modules, "pydevd_file_utils", fake_pydevd_file_utils) @@ -103,6 +106,7 @@ def test_with_debugger(monkeypatch, capsys, verbose): if verbose: assert "Extracted path mappings:\n[0] host = src/helloworld" in captured.out + assert fake_pydevd.DebugInfoHolder.DEBUG_TRACE_LEVEL == 3 @pytest.mark.parametrize("verbose", [True, False]) @@ -112,6 +116,7 @@ def test_os_file_bugfix(monkeypatch, capsys, verbose): os_environ["BRIEFCASE_DEBUG"] = "1" if verbose else "0" os_environ["BRIEFCASE_DEBUGGER"] = json.dumps( { + "debugger": "debugpy", "host": "somehost", "port": 9999, } @@ -127,6 +132,11 @@ def test_os_file_bugfix(monkeypatch, capsys, verbose): fake_debugpy_wait_for_client = MagicMock() monkeypatch.setattr(debugpy, "wait_for_client", fake_debugpy_wait_for_client) + # pydevd is dynamically loaded and only available when a real debugger is attached. So + # we fake the whole module, as otherwise the import in start_remote_debugger would fail + fake_pydevd = MagicMock() + monkeypatch.setitem(sys.modules, "pydevd", fake_pydevd) + # start test function briefcase_debugger.start_remote_debugger() @@ -144,3 +154,4 @@ def test_os_file_bugfix(monkeypatch, capsys, verbose): if verbose: assert "'os.__file__' not available. Patching it..." in captured.out + assert fake_pydevd.DebugInfoHolder.DEBUG_TRACE_LEVEL == 3 diff --git a/debugger-support/tests/test_path_mappings.py b/debugger-support/tests/test_debugpy_path_mappings.py similarity index 86% rename from debugger-support/tests/test_path_mappings.py rename to debugger-support/tests/test_debugpy_path_mappings.py index 219e0626d..1ea9f82bd 100644 --- a/debugger-support/tests/test_path_mappings.py +++ b/debugger-support/tests/test_debugpy_path_mappings.py @@ -1,30 +1,30 @@ import sys from pathlib import Path, PosixPath, PurePosixPath, PureWindowsPath, WindowsPath -import briefcase_debugpy._remote_debugger -from briefcase_debugpy._remote_debugger import ( +import briefcase_debugger.debugpy +from briefcase_debugger.config import ( AppPackagesPathMappings, AppPathMappings, DebuggerConfig, - load_path_mappings, ) def test_mappings_not_existing(): """Test an complete empty config.""" - path_mappings = load_path_mappings({}, False) + path_mappings = briefcase_debugger.debugpy.load_path_mappings({}, False) assert path_mappings == [] def test_mappings_none(monkeypatch): """Test an config with no mappings set.""" config = DebuggerConfig( + debugger="debugpy", host="", port=0, app_path_mappings=None, app_packages_path_mappings=None, ) - path_mappings = load_path_mappings(config, False) + path_mappings = briefcase_debugger.debugpy.load_path_mappings(config, False) assert path_mappings == [] @@ -32,9 +32,10 @@ def test_mappings_windows(monkeypatch): """Test path mappings on an Windows system.""" # When running tests on Linux/macOS, we have to switch to WindowsPath. if isinstance(Path(), PosixPath): - monkeypatch.setattr(briefcase_debugpy._remote_debugger, "Path", PureWindowsPath) + monkeypatch.setattr(briefcase_debugger.debugpy, "Path", PureWindowsPath) config = DebuggerConfig( + debugger="debugpy", host="", port=0, app_path_mappings=AppPathMappings( @@ -53,7 +54,7 @@ def test_mappings_windows(monkeypatch): ] monkeypatch.setattr(sys, "path", sys_path) - path_mappings = load_path_mappings(config, False) + path_mappings = briefcase_debugger.debugpy.load_path_mappings(config, False) assert path_mappings == [ # (host_path, device_path) @@ -65,9 +66,10 @@ def test_mappings_macos(monkeypatch): """Test path mappings on an macOS system.""" # When running tests on windows, we have to switch to PosixPath. if isinstance(Path(), WindowsPath): - monkeypatch.setattr(briefcase_debugpy._remote_debugger, "Path", PurePosixPath) + monkeypatch.setattr(briefcase_debugger.debugpy, "Path", PurePosixPath) config = DebuggerConfig( + debugger="debugpy", host="", port=0, app_path_mappings=AppPathMappings( @@ -87,7 +89,7 @@ def test_mappings_macos(monkeypatch): ] monkeypatch.setattr(sys, "path", sys_path) - path_mappings = load_path_mappings(config, False) + path_mappings = briefcase_debugger.debugpy.load_path_mappings(config, False) assert path_mappings == [ # (host_path, device_path) @@ -102,9 +104,10 @@ def test_mappings_ios(monkeypatch): """Test path mappings on an iOS system.""" # When running tests on windows, we have to switch to PosixPath. if isinstance(Path(), WindowsPath): - monkeypatch.setattr(briefcase_debugpy._remote_debugger, "Path", PurePosixPath) + monkeypatch.setattr(briefcase_debugger.debugpy, "Path", PurePosixPath) config = DebuggerConfig( + debugger="debugpy", host="", port=0, app_path_mappings=AppPathMappings( @@ -127,7 +130,7 @@ def test_mappings_ios(monkeypatch): ] monkeypatch.setattr(sys, "path", sys_path) - path_mappings = load_path_mappings(config, False) + path_mappings = briefcase_debugger.debugpy.load_path_mappings(config, False) assert path_mappings == [ # (host_path, device_path) @@ -146,9 +149,10 @@ def test_mappings_android(monkeypatch): """Test path mappings on an Android system.""" # When running tests on windows, we have to switch to PosixPath. if isinstance(Path(), WindowsPath): - monkeypatch.setattr(briefcase_debugpy._remote_debugger, "Path", PurePosixPath) + monkeypatch.setattr(briefcase_debugger.debugpy, "Path", PurePosixPath) config = DebuggerConfig( + debugger="debugpy", host="", port=0, app_path_mappings=AppPathMappings( @@ -172,7 +176,7 @@ def test_mappings_android(monkeypatch): ] monkeypatch.setattr(sys, "path", sys_path) - path_mappings = load_path_mappings(config, False) + path_mappings = briefcase_debugger.debugpy.load_path_mappings(config, False) assert path_mappings == [ # (host_path, device_path) diff --git a/debugger-support/tests/test_pdb.py b/debugger-support/tests/test_pdb.py index 0bdd42394..c6e0e5f61 100644 --- a/debugger-support/tests/test_pdb.py +++ b/debugger-support/tests/test_pdb.py @@ -1,6 +1,5 @@ import json import os -import sys from unittest.mock import MagicMock import briefcase_debugger @@ -48,6 +47,7 @@ def test_with_debugger(monkeypatch, capsys, verbose): os_environ["BRIEFCASE_DEBUG"] = "1" if verbose else "0" os_environ["BRIEFCASE_DEBUGGER"] = json.dumps( { + "debugger": "pdb", "host": "somehost", "port": 9999, } @@ -57,12 +57,6 @@ def test_with_debugger(monkeypatch, capsys, verbose): fake_remote_pdb = MagicMock() monkeypatch.setattr(briefcase_debugger.pdb, "RemotePdb", fake_remote_pdb) - # pydevd is dynamically loaded and only available when a real debugger is attached. So - # we fake the whole module, as otherwise the import in start_remote_debugger would fail - fake_pydevd_file_utils = MagicMock() - fake_pydevd_file_utils.setup_client_server_paths.return_value = None - monkeypatch.setitem(sys.modules, "pydevd_file_utils", fake_pydevd_file_utils) - # start test function briefcase_debugger.start_remote_debugger() diff --git a/tests/commands/create/test_install_app_requirements.py b/tests/commands/create/test_install_app_requirements.py index 59fef35ef..453d479b5 100644 --- a/tests/commands/create/test_install_app_requirements.py +++ b/tests/commands/create/test_install_app_requirements.py @@ -1089,6 +1089,10 @@ def test_app_packages_only_test_requires_test_mode( class DummyDebugger(BaseDebugger): + @property + def name(self) -> str: + return "dummy" + @property def connection_mode(self) -> DebuggerConnectionMode: raise NotImplementedError diff --git a/tests/debuggers/test_base.py b/tests/debuggers/test_base.py index 9d50e0a43..3a3da531b 100644 --- a/tests/debuggers/test_base.py +++ b/tests/debuggers/test_base.py @@ -80,11 +80,13 @@ def test_get_debugger(): ), ], ) -def test_debugger(debugger_name, expected_class, connection_mode): +def test_debugger(debugger_name, expected_class, connection_mode, monkeypatch): + monkeypatch.setattr("briefcase.debuggers.base.IS_EDITABLE", False) + debugger = get_debugger(debugger_name) assert isinstance(debugger, expected_class) assert debugger.connection_mode == connection_mode - assert f"briefcase-{debugger_name}" in debugger.debugger_support_pkg + assert f"briefcase-debugger[{debugger_name}]" in debugger.debugger_support_pkg @pytest.mark.parametrize( @@ -102,7 +104,7 @@ def test_debugger_editable(debugger_name, monkeypatch): debugger = get_debugger(debugger_name) assert ( - str(tmp_path / "debugger-support" / f"briefcase-{debugger_name}") + str(tmp_path / f"debugger-support[{debugger_name}]") == debugger.debugger_support_pkg ) @@ -118,4 +120,4 @@ def test_debugger_editable_path_not_found(debugger_name, monkeypatch): monkeypatch.setattr("briefcase.debuggers.base.REPO_ROOT", tmp_path) debugger = get_debugger(debugger_name) - assert f"briefcase-{debugger_name}==" in debugger.debugger_support_pkg + assert f"briefcase-debugger[{debugger_name}]==" in debugger.debugger_support_pkg diff --git a/tests/platforms/conftest.py b/tests/platforms/conftest.py index b33ec1a70..5fc7c1893 100644 --- a/tests/platforms/conftest.py +++ b/tests/platforms/conftest.py @@ -58,6 +58,10 @@ def underscore_app_config(first_app_config): class DummyDebugger(BaseDebugger): + @property + def name(self) -> str: + return "dummy" + @property def connection_mode(self) -> DebuggerConnectionMode: raise NotImplementedError diff --git a/tests/platforms/macOS/app/test_run.py b/tests/platforms/macOS/app/test_run.py index 7dce458f8..cc300baed 100644 --- a/tests/platforms/macOS/app/test_run.py +++ b/tests/platforms/macOS/app/test_run.py @@ -356,6 +356,7 @@ def test_run_gui_app_debugger( env={ "BRIEFCASE_DEBUGGER": json.dumps( { + "debugger": "dummy", "host": "somehost", "port": 9999, "app_path_mappings": { diff --git a/tests/platforms/windows/app/test_run.py b/tests/platforms/windows/app/test_run.py index 202e30ef7..113e4607b 100644 --- a/tests/platforms/windows/app/test_run.py +++ b/tests/platforms/windows/app/test_run.py @@ -309,6 +309,7 @@ def test_run_gui_app_debugger(run_command, first_app_config, tmp_path, dummy_deb env={ "BRIEFCASE_DEBUGGER": json.dumps( { + "debugger": "dummy", "host": "somehost", "port": 9999, "app_path_mappings": { diff --git a/tests/platforms/windows/visualstudio/test_run.py b/tests/platforms/windows/visualstudio/test_run.py index 816bff0dc..12f9cae60 100644 --- a/tests/platforms/windows/visualstudio/test_run.py +++ b/tests/platforms/windows/visualstudio/test_run.py @@ -203,6 +203,7 @@ def test_run_app_debugger(run_command, first_app_config, tmp_path, dummy_debugge env={ "BRIEFCASE_DEBUGGER": json.dumps( { + "debugger": "dummy", "host": "somehost", "port": 9999, "app_path_mappings": { From db5742216a1d2bf5491d1dce0b56a9dc11cf5e2b Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Wed, 3 Sep 2025 22:22:04 +0200 Subject: [PATCH 087/131] remove build folder --- .gitignore | 3 +- .../build/lib/briefcase_debugger/__init__.py | 54 -------- .../build/lib/briefcase_debugger/config.py | 20 --- .../build/lib/briefcase_debugger/debugpy.py | 118 ------------------ .../build/lib/briefcase_debugger/pdb.py | 33 ----- 5 files changed, 1 insertion(+), 227 deletions(-) delete mode 100644 debugger-support/build/lib/briefcase_debugger/__init__.py delete mode 100644 debugger-support/build/lib/briefcase_debugger/config.py delete mode 100644 debugger-support/build/lib/briefcase_debugger/debugpy.py delete mode 100644 debugger-support/build/lib/briefcase_debugger/pdb.py diff --git a/.gitignore b/.gitignore index ddd4184af..bcdec03c7 100644 --- a/.gitignore +++ b/.gitignore @@ -5,8 +5,7 @@ /dist /build automation/build -debugger-support/briefcase-debugpy/build -debugger-support/briefcase-pdb/build +debugger-support/build docs/_build/ distribute-* .DS_Store diff --git a/debugger-support/build/lib/briefcase_debugger/__init__.py b/debugger-support/build/lib/briefcase_debugger/__init__.py deleted file mode 100644 index 8652a30f3..000000000 --- a/debugger-support/build/lib/briefcase_debugger/__init__.py +++ /dev/null @@ -1,54 +0,0 @@ -import json -import os -import sys -import traceback - -from briefcase_debugger.config import DebuggerConfig - -REMOTE_DEBUGGER_STARTED = False - - -def start_remote_debugger(): - global REMOTE_DEBUGGER_STARTED - REMOTE_DEBUGGER_STARTED = True - - # check verbose output - verbose = os.environ.get("BRIEFCASE_DEBUG", "0") == "1" - - # reading config - config_str = os.environ.get("BRIEFCASE_DEBUGGER", None) - - # skip debugger if no config is set - if config_str is None: - if verbose: - print( - "No 'BRIEFCASE_DEBUGGER' environment variable found. Debugger not starting." - ) - return # If BRIEFCASE_DEBUGGER is not set, this packages does nothing... - - if verbose: - print(f"'BRIEFCASE_DEBUGGER'={config_str}") - - # Parsing config json - config: DebuggerConfig = json.loads(config_str) - - # start debugger - print("Starting remote debugger...") - if config["debugger"] == "debugpy": - from briefcase_debugger.debugpy import start_debugpy - - start_debugpy(config, verbose) - elif config["debugger"] == "pdb": - from briefcase_debugger.pdb import start_pdb - - start_pdb(config, verbose) - - -# only start remote debugger on the first import -if not REMOTE_DEBUGGER_STARTED: - try: - start_remote_debugger() - except Exception: - # Show exception and stop the whole application when an error occurs - print(traceback.format_exc()) - sys.exit(-1) diff --git a/debugger-support/build/lib/briefcase_debugger/config.py b/debugger-support/build/lib/briefcase_debugger/config.py deleted file mode 100644 index 3c62ac87f..000000000 --- a/debugger-support/build/lib/briefcase_debugger/config.py +++ /dev/null @@ -1,20 +0,0 @@ -from typing import Optional, TypedDict - - -class AppPathMappings(TypedDict): - device_sys_path_regex: str - device_subfolders: list[str] - host_folders: list[str] - - -class AppPackagesPathMappings(TypedDict): - sys_path_regex: str - host_folder: str - - -class DebuggerConfig(TypedDict): - debugger: str - host: str - port: int - app_path_mappings: Optional[AppPathMappings] - app_packages_path_mappings: Optional[AppPackagesPathMappings] diff --git a/debugger-support/build/lib/briefcase_debugger/debugpy.py b/debugger-support/build/lib/briefcase_debugger/debugpy.py deleted file mode 100644 index 919106cac..000000000 --- a/debugger-support/build/lib/briefcase_debugger/debugpy.py +++ /dev/null @@ -1,118 +0,0 @@ -import os -import re -import sys -from pathlib import Path -from typing import Optional - -import debugpy - -from briefcase_debugger.config import DebuggerConfig - - -def find_first_matching_path(regex: str) -> Optional[str]: - """Gibt das erste Element aus paths zurück, das auf regex matcht, sonst None.""" - for p in sys.path: - if re.search(regex, p): - return p - return None - - -def load_path_mappings(config: DebuggerConfig, verbose: bool) -> list[tuple[str, str]]: - app_path_mappings = config.get("app_path_mappings", None) - app_packages_path_mappings = config.get("app_packages_path_mappings", None) - - mappings_list = [] - if app_path_mappings: - device_app_folder = find_first_matching_path( - app_path_mappings["device_sys_path_regex"] - ) - if device_app_folder: - for app_subfolder_device, app_subfolder_host in zip( - app_path_mappings["device_subfolders"], - app_path_mappings["host_folders"], - ): - mappings_list.append( - ( - app_subfolder_host, - str(Path(device_app_folder) / app_subfolder_device), - ) - ) - if app_packages_path_mappings: - device_app_packages_folder = find_first_matching_path( - app_packages_path_mappings["sys_path_regex"] - ) - if device_app_packages_folder: - mappings_list.append( - ( - app_packages_path_mappings["host_folder"], - str(Path(device_app_packages_folder)), - ) - ) - - if verbose: - print("Extracted path mappings:") - for idx, p in enumerate(mappings_list): - print(f"[{idx}] host = {p[0]}") - print(f"[{idx}] device = {p[1]}") - - return mappings_list - - -def start_debugpy(config: DebuggerConfig, verbose: bool): - host = config["host"] - port = config["port"] - path_mappings = load_path_mappings(config, verbose) - - # There is a bug in debugpy that has to be handled until there is a new - # debugpy release, see https://github.com/microsoft/debugpy/issues/1943 - if not hasattr(os, "__file__"): - if verbose: - print("'os.__file__' not available. Patching it...") - os.__file__ = "" - - # Starting remote debugger... - print(f"Starting debugpy in server mode at {host}:{port}...") - debugpy.listen((host, port), in_process_debug_adapter=True) - - if verbose: - # pydevd is dynamically loaded and only available after debugpy is started - import pydevd - - pydevd.DebugInfoHolder.DEBUG_TRACE_LEVEL = 3 - - if len(path_mappings) > 0: - if verbose: - print("Adding path mappings...") - - # pydevd is dynamically loaded and only available after debugpy is started - import pydevd_file_utils - - pydevd_file_utils.setup_client_server_paths(path_mappings) - - print("The debugpy server started. Waiting for debugger to attach...") - print( - f""" -To connect to debugpy using VSCode add the following configuration to '.vscode/launch.json': -{{ - "version": "0.2.0", - "configurations": [ - {{ - "name": "Briefcase: Attach (Connect)", - "type": "debugpy", - "request": "attach", - "connect": {{ - "host": "{host}", - "port": {port} - }}, - "justMyCode": false - }} - ] -}} - -For more information see: https://briefcase.readthedocs.io/en/stable/how-to/debugging/vscode.html#bundled-app -""" - ) - debugpy.wait_for_client() - - print("Debugger attached.") - print("-" * 75) diff --git a/debugger-support/build/lib/briefcase_debugger/pdb.py b/debugger-support/build/lib/briefcase_debugger/pdb.py deleted file mode 100644 index 86b8747f8..000000000 --- a/debugger-support/build/lib/briefcase_debugger/pdb.py +++ /dev/null @@ -1,33 +0,0 @@ -import sys - -from remote_pdb import RemotePdb - -from briefcase_debugger.config import DebuggerConfig - - -def start_pdb(config: DebuggerConfig, verbose: bool): - """Start remote PDB server.""" - # Parsing host/port - host = config["host"] - port = config["port"] - - print( - f""" -Remote PDB server opened at {host}:{port}. -Waiting for debugger to attach... -To connect to remote PDB use eg.: - - telnet {host} {port} (Windows) - - rlwrap socat - tcp:{host}:{port} (Linux, macOS) - -For more information see: https://briefcase.readthedocs.io/en/stable/how-to/debugging/console.html#bundled-app -""" - ) - - # Create a RemotePdb instance - remote_pdb = RemotePdb(host, port, quiet=True) - - # Connect the remote PDB with the "breakpoint()" function - sys.breakpointhook = remote_pdb.set_trace - - print("Debugger client attached.") - print("-" * 75) From 58cb41b2f45cd8573dc43b82823d862c20ba844b Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Wed, 3 Sep 2025 23:01:20 +0200 Subject: [PATCH 088/131] add missing test in briefcase --- tests/debuggers/test_base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/debuggers/test_base.py b/tests/debuggers/test_base.py index 3a3da531b..a5a1c10f6 100644 --- a/tests/debuggers/test_base.py +++ b/tests/debuggers/test_base.py @@ -51,7 +51,9 @@ def test_get_debuggers(): debuggers = get_debuggers() assert isinstance(debuggers, dict) assert debuggers["pdb"] is PdbDebugger + assert debuggers["pdb"]().name == "pdb" assert debuggers["debugpy"] is DebugpyDebugger + assert debuggers["debugpy"]().name == "debugpy" def test_get_debugger(): From 1d2f50f6e61ea30923e33c282e776d9f77eecae6 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Wed, 3 Sep 2025 23:06:32 +0200 Subject: [PATCH 089/131] whe a regex is not found in path this should be an error instead of silently ignoring it. --- .../src/briefcase_debugger/debugpy.py | 35 +++++++++---------- debugger-support/tests/test_debugpy.py | 4 +-- .../tests/test_debugpy_path_mappings.py | 26 ++++++++++++++ debugger-support/tests/test_pdb.py | 2 -- 4 files changed, 43 insertions(+), 24 deletions(-) diff --git a/debugger-support/src/briefcase_debugger/debugpy.py b/debugger-support/src/briefcase_debugger/debugpy.py index 785f316b6..478275123 100644 --- a/debugger-support/src/briefcase_debugger/debugpy.py +++ b/debugger-support/src/briefcase_debugger/debugpy.py @@ -2,19 +2,18 @@ import re import sys from pathlib import Path -from typing import Optional import debugpy from briefcase_debugger.config import DebuggerConfig -def find_first_matching_path(regex: str) -> Optional[str]: +def find_first_matching_path(regex: str) -> str: """Gibt das erste Element aus paths zurück, das auf regex matcht, sonst None.""" for p in sys.path: if re.search(regex, p): return p - return None + raise ValueError(f"No sys.path entry matches regex '{regex}'") def load_path_mappings(config: DebuggerConfig, verbose: bool) -> list[tuple[str, str]]: @@ -26,28 +25,26 @@ def load_path_mappings(config: DebuggerConfig, verbose: bool) -> list[tuple[str, device_app_folder = find_first_matching_path( app_path_mappings["device_sys_path_regex"] ) - if device_app_folder: - for app_subfolder_device, app_subfolder_host in zip( - app_path_mappings["device_subfolders"], - app_path_mappings["host_folders"], - ): - mappings_list.append( - ( - app_subfolder_host, - str(Path(device_app_folder) / app_subfolder_device), - ) + for app_subfolder_device, app_subfolder_host in zip( + app_path_mappings["device_subfolders"], + app_path_mappings["host_folders"], + ): + mappings_list.append( + ( + app_subfolder_host, + str(Path(device_app_folder) / app_subfolder_device), ) + ) if app_packages_path_mappings: device_app_packages_folder = find_first_matching_path( app_packages_path_mappings["sys_path_regex"] ) - if device_app_packages_folder: - mappings_list.append( - ( - app_packages_path_mappings["host_folder"], - str(Path(device_app_packages_folder)), - ) + mappings_list.append( + ( + app_packages_path_mappings["host_folder"], + str(Path(device_app_packages_folder)), ) + ) if verbose: print("Extracted path mappings:") diff --git a/debugger-support/tests/test_debugpy.py b/debugger-support/tests/test_debugpy.py index 233314aa8..f946db3f4 100644 --- a/debugger-support/tests/test_debugpy.py +++ b/debugger-support/tests/test_debugpy.py @@ -45,9 +45,7 @@ def test_with_debugger(monkeypatch, capsys, verbose): """Test a normal debug session.""" # When running tests on Linux/macOS, we have to switch to WindowsPath. if isinstance(Path(), PosixPath): - monkeypatch.setattr( - briefcase_debugger._remote_debugger, "Path", PureWindowsPath - ) + monkeypatch.setattr(briefcase_debugger.debugpy, "Path", PureWindowsPath) os_environ = {} os_environ["BRIEFCASE_DEBUG"] = "1" if verbose else "0" diff --git a/debugger-support/tests/test_debugpy_path_mappings.py b/debugger-support/tests/test_debugpy_path_mappings.py index 1ea9f82bd..500364b01 100644 --- a/debugger-support/tests/test_debugpy_path_mappings.py +++ b/debugger-support/tests/test_debugpy_path_mappings.py @@ -2,6 +2,7 @@ from pathlib import Path, PosixPath, PurePosixPath, PureWindowsPath, WindowsPath import briefcase_debugger.debugpy +import pytest from briefcase_debugger.config import ( AppPackagesPathMappings, AppPathMappings, @@ -189,3 +190,28 @@ def test_mappings_android(monkeypatch): "/data/data/com.example.helloworld/files/chaquopy/AssetFinder/requirements", ), ] + + +def test_mappings_windows_wrong_sys_path(monkeypatch): + """Test path mappings on an Windows system with a wrong sys path set.""" + # When running tests on Linux/macOS, we have to switch to WindowsPath. + if isinstance(Path(), PosixPath): + monkeypatch.setattr(briefcase_debugger.debugpy, "Path", PureWindowsPath) + + config = DebuggerConfig( + debugger="debugpy", + host="", + port=0, + app_path_mappings=AppPathMappings( + device_sys_path_regex="app$", + device_subfolders=["helloworld"], + host_folders=["src/helloworld"], + ), + app_packages_path_mappings=None, + ) + + sys_path = [] + monkeypatch.setattr(sys, "path", sys_path) + + with pytest.raises(ValueError): + briefcase_debugger.debugpy.load_path_mappings(config, False) diff --git a/debugger-support/tests/test_pdb.py b/debugger-support/tests/test_pdb.py index c6e0e5f61..3143fa85e 100644 --- a/debugger-support/tests/test_pdb.py +++ b/debugger-support/tests/test_pdb.py @@ -4,8 +4,6 @@ import briefcase_debugger import briefcase_debugger.pdb - -# import remote_pdb import pytest From 550a3d134d52fbfa41226048db9d9e78c5dea58d Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Wed, 3 Sep 2025 23:34:07 +0200 Subject: [PATCH 090/131] REMOTE_DEBUGGER_STARTED is unnecessary --- .../src/briefcase_debugger/__init__.py | 63 +++++++++---------- debugger-support/tests/test_base.py | 47 ++++++++++++++ 2 files changed, 75 insertions(+), 35 deletions(-) create mode 100644 debugger-support/tests/test_base.py diff --git a/debugger-support/src/briefcase_debugger/__init__.py b/debugger-support/src/briefcase_debugger/__init__.py index 8652a30f3..0da61d07e 100644 --- a/debugger-support/src/briefcase_debugger/__init__.py +++ b/debugger-support/src/briefcase_debugger/__init__.py @@ -3,52 +3,45 @@ import sys import traceback -from briefcase_debugger.config import DebuggerConfig - -REMOTE_DEBUGGER_STARTED = False - def start_remote_debugger(): - global REMOTE_DEBUGGER_STARTED - REMOTE_DEBUGGER_STARTED = True + try: + # check verbose output + verbose = os.environ.get("BRIEFCASE_DEBUG", "0") == "1" - # check verbose output - verbose = os.environ.get("BRIEFCASE_DEBUG", "0") == "1" + # reading config + config_str = os.environ.get("BRIEFCASE_DEBUGGER", None) - # reading config - config_str = os.environ.get("BRIEFCASE_DEBUGGER", None) + # skip debugger if no config is set + if config_str is None: + if verbose: + print( + "No 'BRIEFCASE_DEBUGGER' environment variable found. Debugger not starting." + ) + return # If BRIEFCASE_DEBUGGER is not set, this packages does nothing... - # skip debugger if no config is set - if config_str is None: if verbose: - print( - "No 'BRIEFCASE_DEBUGGER' environment variable found. Debugger not starting." - ) - return # If BRIEFCASE_DEBUGGER is not set, this packages does nothing... - - if verbose: - print(f"'BRIEFCASE_DEBUGGER'={config_str}") - - # Parsing config json - config: DebuggerConfig = json.loads(config_str) + print(f"'BRIEFCASE_DEBUGGER'={config_str}") - # start debugger - print("Starting remote debugger...") - if config["debugger"] == "debugpy": - from briefcase_debugger.debugpy import start_debugpy + # Parsing config json + config = json.loads(config_str) - start_debugpy(config, verbose) - elif config["debugger"] == "pdb": - from briefcase_debugger.pdb import start_pdb + # start debugger + print("Starting remote debugger...") + if config["debugger"] == "debugpy": + from briefcase_debugger.debugpy import start_debugpy - start_pdb(config, verbose) + start_debugpy(config, verbose) + elif config["debugger"] == "pdb": + from briefcase_debugger.pdb import start_pdb - -# only start remote debugger on the first import -if not REMOTE_DEBUGGER_STARTED: - try: - start_remote_debugger() + start_pdb(config, verbose) + else: + raise ValueError(f"Unknown debugger '{config['debugger']}'") except Exception: # Show exception and stop the whole application when an error occurs print(traceback.format_exc()) sys.exit(-1) + + +start_remote_debugger() diff --git a/debugger-support/tests/test_base.py b/debugger-support/tests/test_base.py new file mode 100644 index 000000000..e987d411e --- /dev/null +++ b/debugger-support/tests/test_base.py @@ -0,0 +1,47 @@ +import importlib +import json +import os +import sys +from unittest.mock import MagicMock, call + +import briefcase_debugger + + +def test_auto_startup(monkeypatch, capsys): + """Test that the debugger startup code is executed on import.""" + fake_environ = MagicMock() + monkeypatch.setattr(os, "environ", fake_environ) + + fake_environ.get.return_value = None + + # reload the module to trigger the auto startup code + importlib.reload(importlib.import_module("briefcase_debugger")) + + # check if the autostart was executed + assert fake_environ.get.mock_calls == [ + call("BRIEFCASE_DEBUG", "0"), + call("BRIEFCASE_DEBUGGER", None), + ] + + +def test_unknown_debugger(monkeypatch, capsys): + """Test that an unknown debugger raises an error and stops the application.""" + os_environ = {} + os_environ["BRIEFCASE_DEBUGGER"] = json.dumps( + { + "debugger": "unknown", + "host": "somehost", + "port": 9999, + } + ) + monkeypatch.setattr(os, "environ", os_environ) + + fake_sys_exit = MagicMock() + monkeypatch.setattr(sys, "exit", fake_sys_exit) + + briefcase_debugger.start_remote_debugger() + + fake_sys_exit.assert_called_once_with(-1) + + captured = capsys.readouterr() + assert "Unknown debugger" in captured.out From 304b53130598826f6a67ffcf72c39bfc6f7dcf15 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Tue, 2 Sep 2025 11:49:46 +0200 Subject: [PATCH 091/131] add code coverage --- .github/workflows/ci.yml | 149 +++----------------------------- debugger-support/pyproject.toml | 21 ++++- tox.ini | 21 ++--- 3 files changed, 38 insertions(+), 153 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c32649e7d..f1cb99c73 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,7 @@ on: push: branches: - main + - ci-test workflow_call: inputs: attest-package: @@ -46,55 +47,15 @@ jobs: with: attest: ${{ inputs.attest-package }} - package-debugpy: - name: Package Briefcase Debugpy - runs-on: ubuntu-latest + package-debugger: + name: Package Debugger permissions: id-token: write contents: read attestations: write - # This does not work. It gerates a CI Error: Error: The artifact name is not valid: Packages-briefcase-debugger-support/briefcase-debugpy. Contains the following character: Forward slash / - # uses: beeware/.github/.github/workflows/python-package-create.yml@main - # with: - # build-subdirectory: "debugger-support/briefcase-debugpy" - steps: - - uses: actions/checkout@v4 - - uses: hynek/build-and-inspect-python-package@v2 - id: package - with: - path: "debugger-support/briefcase-debugpy" - upload-name-suffix: "-briefcase-debugpy" - attest-build-provenance-github: ${{ inputs.attest-package }} - - name: Set artifact-name output - run: echo "artifact-name=${{ steps.package.outputs.artifact-name }}" >> $GITHUB_OUTPUT - id: set-artifact-name - outputs: - artifact-name: ${{ steps.set-artifact-name.outputs.artifact-name }} - - package-pdb: - name: Package Briefcase PDB - runs-on: ubuntu-latest - permissions: - id-token: write - contents: read - attestations: write - # This does not work. It gerates a CI Error: Error: The artifact name is not valid: Packages-briefcase-debugger-support/briefcase-pdb. Contains the following character: Forward slash / - # uses: beeware/.github/.github/workflows/python-package-create.yml@main - # with: - # build-subdirectory: "debugger-support/briefcase-pdb" - steps: - - uses: actions/checkout@v4 - - uses: hynek/build-and-inspect-python-package@v2 - id: package - with: - path: "debugger-support/briefcase-pdb" - upload-name-suffix: "-briefcase-pdb" - attest-build-provenance-github: ${{ inputs.attest-package }} - - name: Set artifact-name output - run: echo "artifact-name=${{ steps.package.outputs.artifact-name }}" >> $GITHUB_OUTPUT - id: set-artifact-name - outputs: - artifact-name: ${{ steps.set-artifact-name.outputs.artifact-name }} + uses: beeware/.github/.github/workflows/python-package-create.yml@main + with: + build-subdirectory: "debugger-support" package-automation: name: Package Automation @@ -108,7 +69,7 @@ jobs: unit-tests: name: Unit tests - needs: [ pre-commit, towncrier, package ] + needs: [ pre-commit, towncrier, package, package-debugger ] runs-on: ${{ matrix.platform }} continue-on-error: ${{ matrix.experimental || false }} strategy: @@ -174,103 +135,15 @@ jobs: # coverage reporting must use the same Python version used to produce coverage run: tox -qe coverage$(tr -dc "0-9" <<< "${{ matrix.python-version }}") - unit-tests-debugpy: - name: Unit tests (Debugpy) - needs: [ pre-commit, towncrier, package-debugpy ] - runs-on: ${{ matrix.platform }} - continue-on-error: ${{ matrix.experimental || false }} - strategy: - fail-fast: false - matrix: - platform: [ "macos-13", "macos-latest", "windows-latest", "ubuntu-24.04" ] - python-version: [ "3.9", "3.13" ] - include: - # Ensure the Python versions between min and max are tested - - platform: "ubuntu-24.04" - python-version: "3.10" - - platform: "ubuntu-24.04" - python-version: "3.11" - - platform: "ubuntu-24.04" - python-version: "3.12" - # # Allow dev Python to fail without failing entire job - # - python-version: "3.14" - # experimental: true - steps: - - name: Checkout - uses: actions/checkout@v4.2.2 - with: - fetch-depth: 0 - - - name: Set up Python - uses: actions/setup-python@v5.6.0 - with: - python-version: ${{ matrix.python-version }} - allow-prereleases: true - - - name: Get Packages + - name: Get Debugger Package uses: actions/download-artifact@v4.3.0 with: - name: ${{ needs.package-debugpy.outputs.artifact-name }} + name: ${{ needs.package-debugger.outputs.artifact-name }} path: dist - - name: Install Tox - uses: beeware/.github/.github/actions/install-requirement@main - with: - requirements: tox - extra: dev - - - name: Test Briefcase Debugpy - run: | - tox -e py-debugpy --installpkg dist/briefcase_debugpy-*.whl - - unit-tests-pdb: - name: Unit tests (Pdb) - needs: [ pre-commit, towncrier, package-pdb ] - runs-on: ${{ matrix.platform }} - continue-on-error: ${{ matrix.experimental || false }} - strategy: - fail-fast: false - matrix: - platform: [ "macos-13", "macos-latest", "windows-latest", "ubuntu-24.04" ] - python-version: [ "3.9", "3.13" ] - include: - # Ensure the Python versions between min and max are tested - - platform: "ubuntu-24.04" - python-version: "3.10" - - platform: "ubuntu-24.04" - python-version: "3.11" - - platform: "ubuntu-24.04" - python-version: "3.12" - # # Allow dev Python to fail without failing entire job - # - python-version: "3.14" - # experimental: true - steps: - - name: Checkout - uses: actions/checkout@v4.2.2 - with: - fetch-depth: 0 - - - name: Set up Python - uses: actions/setup-python@v5.6.0 - with: - python-version: ${{ matrix.python-version }} - allow-prereleases: true - - - name: Get Packages - uses: actions/download-artifact@v4.3.0 - with: - name: ${{ needs.package-pdb.outputs.artifact-name }} - path: dist - - - name: Install Tox - uses: beeware/.github/.github/actions/install-requirement@main - with: - requirements: tox - extra: dev - - - name: Test Briefcase Pdb + - name: Test Debugger run: | - tox -e py-pdb --installpkg dist/briefcase_pdb-*.whl + tox -e py-debugger --installpkg dist/briefcase_debugger-*.whl coverage: name: Project coverage diff --git a/debugger-support/pyproject.toml b/debugger-support/pyproject.toml index 83c371f87..97893aa5f 100644 --- a/debugger-support/pyproject.toml +++ b/debugger-support/pyproject.toml @@ -21,7 +21,8 @@ root = "../" [project.optional-dependencies] dev = [ - "pytest == 8.4.1" + "pytest == 8.4.1", + "coverage[toml] == 7.10.2", ] pdb = [ "remote-pdb>=2.1.0,<3.0.0" @@ -29,3 +30,21 @@ pdb = [ debugpy = [ "debugpy>=1.8.14,<2.0.0" ] + +[tool.coverage.run] +parallel = true +branch = true +relative_files = true +source_pkgs = ["briefcase_debugger"] + +[tool.coverage.paths] +source = [ + "src", + "**/site-packages", +] + +[tool.coverage.report] +show_missing = true +skip_covered = true +skip_empty = true +precision = 1 diff --git a/tox.ini b/tox.ini index fab56c413..bf5319fff 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = towncrier-check,docs-lint,pre-commit,py{39,310,311,312,313}-cov,coverage-platform +envlist = towncrier-check,docs-lint,pre-commit,py{39,310,311,312,313}-cov,coverage-platform,py{39,310,311,312,313}-debugger labels = test = py-cov,coverage test39 = py39-cov,coverage39 @@ -106,20 +106,13 @@ commands = all : python -m sphinx {[docs]sphinx_args} {posargs} --verbose --write-all --fresh-env --builder html {[docs]docs_dir} {[docs]build_dir}{/}html live : sphinx-autobuild {[docs]sphinx_args} {posargs} --builder html {[docs]docs_dir} {[docs]build_dir}{/}live -[testenv:py{,39,310,311,312,313}-debugpy] -changedir = debugger-support/briefcase-debugpy +[testenv:py{,39,310,311,312,313}-debugger] +changedir = debugger-support skip_install = True deps = build commands = - python -m pip install ".[dev]" - python -m pytest {posargs:-vv --color yes} - -[testenv:py{,39,310,311,312,313}-pdb] -changedir = debugger-support/briefcase-pdb -skip_install = True -deps = - build -commands = - python -m pip install ".[dev]" - python -m pytest {posargs:-vv --color yes} + python -m pip install ".[dev,pdb,debugpy]" + python -X warn_default_encoding -m coverage run -m pytest {posargs:-vv --color yes} + python -m coverage combine + python -m coverage report --fail-under=100 From 462d1b6851e599fdb6ea27d74513658f3e185726 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Thu, 4 Sep 2025 00:01:33 +0200 Subject: [PATCH 092/131] no german --- debugger-support/src/briefcase_debugger/debugpy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debugger-support/src/briefcase_debugger/debugpy.py b/debugger-support/src/briefcase_debugger/debugpy.py index 478275123..7149ed144 100644 --- a/debugger-support/src/briefcase_debugger/debugpy.py +++ b/debugger-support/src/briefcase_debugger/debugpy.py @@ -9,7 +9,7 @@ def find_first_matching_path(regex: str) -> str: - """Gibt das erste Element aus paths zurück, das auf regex matcht, sonst None.""" + """Returns the first element of sys.paths that matches regex, otherwise None.""" for p in sys.path: if re.search(regex, p): return p From e260e9828c74b946789a3ac2d4cf396ab8c3e3fa Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Thu, 4 Sep 2025 00:29:25 +0200 Subject: [PATCH 093/131] modifications because 3.9 is not supported any longer --- debugger-support/src/briefcase_debugger/config.py | 6 +++--- debugger-support/src/briefcase_debugger/debugpy.py | 1 + src/briefcase/debuggers/__init__.py | 12 ++---------- 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/debugger-support/src/briefcase_debugger/config.py b/debugger-support/src/briefcase_debugger/config.py index 3c62ac87f..3d250b383 100644 --- a/debugger-support/src/briefcase_debugger/config.py +++ b/debugger-support/src/briefcase_debugger/config.py @@ -1,4 +1,4 @@ -from typing import Optional, TypedDict +from typing import TypedDict class AppPathMappings(TypedDict): @@ -16,5 +16,5 @@ class DebuggerConfig(TypedDict): debugger: str host: str port: int - app_path_mappings: Optional[AppPathMappings] - app_packages_path_mappings: Optional[AppPackagesPathMappings] + app_path_mappings: AppPathMappings | None + app_packages_path_mappings: AppPackagesPathMappings | None diff --git a/debugger-support/src/briefcase_debugger/debugpy.py b/debugger-support/src/briefcase_debugger/debugpy.py index 7149ed144..7ff59a532 100644 --- a/debugger-support/src/briefcase_debugger/debugpy.py +++ b/debugger-support/src/briefcase_debugger/debugpy.py @@ -28,6 +28,7 @@ def load_path_mappings(config: DebuggerConfig, verbose: bool) -> list[tuple[str, for app_subfolder_device, app_subfolder_host in zip( app_path_mappings["device_subfolders"], app_path_mappings["host_folders"], + strict=False, ): mappings_list.append( ( diff --git a/src/briefcase/debuggers/__init__.py b/src/briefcase/debuggers/__init__.py index 0574b988a..7dec9c4ac 100644 --- a/src/briefcase/debuggers/__init__.py +++ b/src/briefcase/debuggers/__init__.py @@ -1,17 +1,9 @@ -import sys - -from briefcase.exceptions import BriefcaseCommandError - -if sys.version_info >= (3, 10): # pragma: no-cover-if-lt-py310 - from importlib.metadata import entry_points -else: # pragma: no-cover-if-gte-py310 - # Before Python 3.10, entry_points did not support the group argument; - # so, the backport package must be used on older versions. - from importlib_metadata import entry_points +from importlib.metadata import entry_points from briefcase.debuggers.base import BaseDebugger from briefcase.debuggers.debugpy import DebugpyDebugger # noqa: F401 from briefcase.debuggers.pdb import PdbDebugger # noqa: F401 +from briefcase.exceptions import BriefcaseCommandError def get_debuggers() -> dict[str, type[BaseDebugger]]: From 092f356227c02a2209af667b1d5f92f467fb1cb0 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Thu, 4 Sep 2025 00:31:55 +0200 Subject: [PATCH 094/131] remove ci-test branch --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33ddb161a..5e13bf55c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,6 @@ on: push: branches: - main - - ci-test workflow_call: inputs: attest-package: From abd042d53698c23f8907a0420ada8152d35a0531 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Thu, 4 Sep 2025 22:32:22 +0200 Subject: [PATCH 095/131] update documentation, add comments, correct formating --- debugger-support/LICENSE | 27 ++++++++++ debugger-support/README.md | 51 ++++++++++++++++++- debugger-support/pyproject.toml | 3 +- .../src/briefcase_debugger/debugpy.py | 12 ++--- debugger-support/tests/test_base.py | 4 +- debugger-support/tests/test_debugpy.py | 9 ++-- .../tests/test_debugpy_path_mappings.py | 14 ++--- debugger-support/tests/test_pdb.py | 7 ++- src/briefcase/commands/run.py | 3 +- tests/debuggers/test_base.py | 8 +++ tests/platforms/macOS/app/test_run.py | 42 ++++++++++++--- tests/platforms/macOS/xcode/test_run.py | 10 +++- tests/platforms/windows/app/test_run.py | 25 +++++++-- .../windows/visualstudio/test_run.py | 10 +++- 14 files changed, 181 insertions(+), 44 deletions(-) create mode 100644 debugger-support/LICENSE diff --git a/debugger-support/LICENSE b/debugger-support/LICENSE new file mode 100644 index 000000000..bac8642ff --- /dev/null +++ b/debugger-support/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2015 Russell Keith-Magee. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. Neither the name of Briefcase-Debugger nor the names of its contributors may + be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/debugger-support/README.md b/debugger-support/README.md index 6665004e2..d5fd42dca 100644 --- a/debugger-support/README.md +++ b/debugger-support/README.md @@ -1,7 +1,56 @@ # Briefcase Debugger Support +[![Python Versions](https://img.shields.io/pypi/pyversions/briefcase-debugger.svg)](https://pypi.python.org/pypi/briefcase-debugger) +[![PyPI Version](https://img.shields.io/pypi/v/briefcase-debugger.svg)](https://pypi.python.org/pypi/briefcase-debugger) +[![Maturity](https://img.shields.io/pypi/status/briefcase-debugger.svg)](https://pypi.python.org/pypi/briefcase-debugger) +[![BSD License](https://img.shields.io/pypi/l/briefcase-debugger.svg)](https://github.com/beeware/briefcase/blob/main/LICENSE) +[![Build Status](https://github.com/beeware/briefcase/workflows/CI/badge.svg?branch=main)](https://github.com/beeware/briefcase/actions) +[![Discord server](https://img.shields.io/discord/836455665257021440?label=Discord%20Chat&logo=discord&style=plastic)](https://beeware.org/bee/chat/) + This package contains the debugger support package for the `pdb` and `debugpy` debuggers. It starts the remote debugger automatically at startup through an .pth file, if a `BRIEFCASE_DEBUGGER` environment variable is set. ## Installation -Normally you do not need to install this package manually, because it is done automatically by briefcase using the `--debug=pdb` or `--debug=debugpy` option. +As an end-user, you won't normally need to install this package. It will be installed automatically by Briefcase if you specify the `--debug=pdb` or `--debug=debugpy` option when running your application. + +## Financial support + +The BeeWare project would not be possible without the generous support +of our financial members: + +[![Anaconda logo](https://beeware.org/community/members/anaconda/anaconda-large.png)](https://anaconda.com/) + +Anaconda Inc. - Advancing AI through open source. + +Plus individual contributions from [users like +you](https://beeware.org/community/members/). If you find Briefcase, or +other BeeWare tools useful, please consider becoming a financial member. + +## Documentation + +Documentation for Briefcase can be found on [Read The +Docs](https://briefcase.readthedocs.io). + +## Community + +Briefcase is part of the [BeeWare suite](https://beeware.org). You can +talk to the community through: + +- [@beeware@fosstodon.org on Mastodon](https://fosstodon.org/@beeware) +- [Discord](https://beeware.org/bee/chat/) +- The Briefcase [GitHub Discussions + forum](https://github.com/beeware/briefcase/discussions) + +We foster a welcoming and respectful community as described in our +[BeeWare Community Code of +Conduct](https://beeware.org/community/behavior/). + +## Contributing + +If you experience problems with Briefcase, [log them on +GitHub](https://github.com/beeware/briefcase/issues). + +If you'd like to contribute to Briefcase development, our [contribution +guide](https://briefcase.readthedocs.io/en/latest/how-to/contribute/index.html) +details how to set up a development environment, and other requirements +we have as part of our contribution process. diff --git a/debugger-support/pyproject.toml b/debugger-support/pyproject.toml index 97893aa5f..74c84a13d 100644 --- a/debugger-support/pyproject.toml +++ b/debugger-support/pyproject.toml @@ -8,9 +8,10 @@ build-backend = "setuptools.build_meta" [project] name = "briefcase-debugger" -description = "Add-on for briefcase to add remote debugging." +description = "A Briefcase plugin adding remote debugging support for PDB and Visual Studio Code." readme = "README.md" license = "BSD-3-Clause" +license-files = ["LICENSE"] dependencies = [ # see "pdb" or "debugpy" optional dependencies below ] diff --git a/debugger-support/src/briefcase_debugger/debugpy.py b/debugger-support/src/briefcase_debugger/debugpy.py index 7ff59a532..4570e2984 100644 --- a/debugger-support/src/briefcase_debugger/debugpy.py +++ b/debugger-support/src/briefcase_debugger/debugpy.py @@ -10,9 +10,9 @@ def find_first_matching_path(regex: str) -> str: """Returns the first element of sys.paths that matches regex, otherwise None.""" - for p in sys.path: - if re.search(regex, p): - return p + for path in sys.path: + if re.search(regex, path): + return path raise ValueError(f"No sys.path entry matches regex '{regex}'") @@ -49,9 +49,9 @@ def load_path_mappings(config: DebuggerConfig, verbose: bool) -> list[tuple[str, if verbose: print("Extracted path mappings:") - for idx, p in enumerate(mappings_list): - print(f"[{idx}] host = {p[0]}") - print(f"[{idx}] device = {p[1]}") + for idx, mapping in enumerate(mappings_list): + print(f"[{idx}] host = {mapping[0]}") + print(f"[{idx}] device = {mapping[1]}") return mappings_list diff --git a/debugger-support/tests/test_base.py b/debugger-support/tests/test_base.py index e987d411e..9b5c1950b 100644 --- a/debugger-support/tests/test_base.py +++ b/debugger-support/tests/test_base.py @@ -8,7 +8,7 @@ def test_auto_startup(monkeypatch, capsys): - """Test that the debugger startup code is executed on import.""" + """Debugger startup code is executed on import.""" fake_environ = MagicMock() monkeypatch.setattr(os, "environ", fake_environ) @@ -25,7 +25,7 @@ def test_auto_startup(monkeypatch, capsys): def test_unknown_debugger(monkeypatch, capsys): - """Test that an unknown debugger raises an error and stops the application.""" + """An unknown debugger raises an error and stops the application.""" os_environ = {} os_environ["BRIEFCASE_DEBUGGER"] = json.dumps( { diff --git a/debugger-support/tests/test_debugpy.py b/debugger-support/tests/test_debugpy.py index f946db3f4..8585c9a16 100644 --- a/debugger-support/tests/test_debugpy.py +++ b/debugger-support/tests/test_debugpy.py @@ -10,7 +10,7 @@ def test_no_env_vars(monkeypatch, capsys): - """Test that nothing happens, when no env vars are set.""" + """Nothing happens, when no env vars are set.""" os_environ = {} monkeypatch.setattr(os, "environ", os_environ) @@ -23,8 +23,7 @@ def test_no_env_vars(monkeypatch, capsys): def test_no_debugger_verbose(monkeypatch, capsys): - """Test that nothing happens except a short message, when only verbose is - requested.""" + """Nothing happens except a short message, when only verbose is requested.""" os_environ = {} os_environ["BRIEFCASE_DEBUG"] = "1" monkeypatch.setattr(os, "environ", os_environ) @@ -42,7 +41,7 @@ def test_no_debugger_verbose(monkeypatch, capsys): @pytest.mark.parametrize("verbose", [True, False]) def test_with_debugger(monkeypatch, capsys, verbose): - """Test a normal debug session.""" + """Normal debug session.""" # When running tests on Linux/macOS, we have to switch to WindowsPath. if isinstance(Path(), PosixPath): monkeypatch.setattr(briefcase_debugger.debugpy, "Path", PureWindowsPath) @@ -109,7 +108,7 @@ def test_with_debugger(monkeypatch, capsys, verbose): @pytest.mark.parametrize("verbose", [True, False]) def test_os_file_bugfix(monkeypatch, capsys, verbose): - """Test if the os.__file__ bugfix is applied (see https://github.com/microsoft/debugpy/issues/1943).""" + """The os.__file__ bugfix has to be applied (see https://github.com/microsoft/debugpy/issues/1943).""" os_environ = {} os_environ["BRIEFCASE_DEBUG"] = "1" if verbose else "0" os_environ["BRIEFCASE_DEBUGGER"] = json.dumps( diff --git a/debugger-support/tests/test_debugpy_path_mappings.py b/debugger-support/tests/test_debugpy_path_mappings.py index 500364b01..871461821 100644 --- a/debugger-support/tests/test_debugpy_path_mappings.py +++ b/debugger-support/tests/test_debugpy_path_mappings.py @@ -11,13 +11,13 @@ def test_mappings_not_existing(): - """Test an complete empty config.""" + """Complete empty config.""" path_mappings = briefcase_debugger.debugpy.load_path_mappings({}, False) assert path_mappings == [] def test_mappings_none(monkeypatch): - """Test an config with no mappings set.""" + """Config with no mappings set.""" config = DebuggerConfig( debugger="debugpy", host="", @@ -30,7 +30,7 @@ def test_mappings_none(monkeypatch): def test_mappings_windows(monkeypatch): - """Test path mappings on an Windows system.""" + """Path mappings on an Windows system.""" # When running tests on Linux/macOS, we have to switch to WindowsPath. if isinstance(Path(), PosixPath): monkeypatch.setattr(briefcase_debugger.debugpy, "Path", PureWindowsPath) @@ -64,7 +64,7 @@ def test_mappings_windows(monkeypatch): def test_mappings_macos(monkeypatch): - """Test path mappings on an macOS system.""" + """Path mappings on an macOS system.""" # When running tests on windows, we have to switch to PosixPath. if isinstance(Path(), WindowsPath): monkeypatch.setattr(briefcase_debugger.debugpy, "Path", PurePosixPath) @@ -102,7 +102,7 @@ def test_mappings_macos(monkeypatch): def test_mappings_ios(monkeypatch): - """Test path mappings on an iOS system.""" + """Path mappings on an iOS system.""" # When running tests on windows, we have to switch to PosixPath. if isinstance(Path(), WindowsPath): monkeypatch.setattr(briefcase_debugger.debugpy, "Path", PurePosixPath) @@ -147,7 +147,7 @@ def test_mappings_ios(monkeypatch): def test_mappings_android(monkeypatch): - """Test path mappings on an Android system.""" + """Path mappings on an Android system.""" # When running tests on windows, we have to switch to PosixPath. if isinstance(Path(), WindowsPath): monkeypatch.setattr(briefcase_debugger.debugpy, "Path", PurePosixPath) @@ -193,7 +193,7 @@ def test_mappings_android(monkeypatch): def test_mappings_windows_wrong_sys_path(monkeypatch): - """Test path mappings on an Windows system with a wrong sys path set.""" + """Path mappings on an Windows system with a wrong sys path set.""" # When running tests on Linux/macOS, we have to switch to WindowsPath. if isinstance(Path(), PosixPath): monkeypatch.setattr(briefcase_debugger.debugpy, "Path", PureWindowsPath) diff --git a/debugger-support/tests/test_pdb.py b/debugger-support/tests/test_pdb.py index 3143fa85e..7b94d6033 100644 --- a/debugger-support/tests/test_pdb.py +++ b/debugger-support/tests/test_pdb.py @@ -8,7 +8,7 @@ def test_no_env_vars(monkeypatch, capsys): - """Test that nothing happens, when no env vars are set.""" + """Nothing happens, when no env vars are set.""" os_environ = {} monkeypatch.setattr(os, "environ", os_environ) @@ -21,8 +21,7 @@ def test_no_env_vars(monkeypatch, capsys): def test_no_debugger_verbose(monkeypatch, capsys): - """Test that nothing happens except a short message, when only verbose is - requested.""" + """Nothing happens except a short message, when only verbose is requested.""" os_environ = {} os_environ["BRIEFCASE_DEBUG"] = "1" monkeypatch.setattr(os, "environ", os_environ) @@ -40,7 +39,7 @@ def test_no_debugger_verbose(monkeypatch, capsys): @pytest.mark.parametrize("verbose", [True, False]) def test_with_debugger(monkeypatch, capsys, verbose): - """Test a normal debug session.""" + """Normal debug session.""" os_environ = {} os_environ["BRIEFCASE_DEBUG"] = "1" if verbose else "0" os_environ["BRIEFCASE_DEBUGGER"] = json.dumps( diff --git a/src/briefcase/commands/run.py b/src/briefcase/commands/run.py index 9f2f7e117..bc9c740c7 100644 --- a/src/briefcase/commands/run.py +++ b/src/briefcase/commands/run.py @@ -261,7 +261,8 @@ def _debugger_app_path_mappings(self, app: AppConfig) -> AppPathMappings: ) def _debugger_app_packages_path_mapping( - self, app: AppConfig + self, + app: AppConfig, ) -> AppPackagesPathMappings: """Get the path mappings for the app packages. diff --git a/tests/debuggers/test_base.py b/tests/debuggers/test_base.py index a5a1c10f6..3cc02719c 100644 --- a/tests/debuggers/test_base.py +++ b/tests/debuggers/test_base.py @@ -34,11 +34,14 @@ def read_text(self, name): ], ) def test_is_editable_pep610(monkeypatch, direct_url, is_editable): + """Detection of editable installs via PEP 610 direct_url.json works.""" monkeypatch.setattr(metadata, "distribution", lambda name: DummyDist(direct_url)) assert _is_editable_pep610("briefcase") is is_editable def test_is_editable_pep610_package_not_found(monkeypatch): + """Detection of editable install throws an Error if package is not found.""" + def raise_not_found(name): raise metadata.PackageNotFoundError @@ -48,6 +51,7 @@ def raise_not_found(name): def test_get_debuggers(): + """Builtin debuggers are available.""" debuggers = get_debuggers() assert isinstance(debuggers, dict) assert debuggers["pdb"] is PdbDebugger @@ -57,6 +61,7 @@ def test_get_debuggers(): def test_get_debugger(): + """Debugger can be retrieved by name.""" assert isinstance(get_debugger("pdb"), PdbDebugger) assert isinstance(get_debugger("debugpy"), DebugpyDebugger) @@ -83,6 +88,7 @@ def test_get_debugger(): ], ) def test_debugger(debugger_name, expected_class, connection_mode, monkeypatch): + """Debugger uses correct connection mode and support package.""" monkeypatch.setattr("briefcase.debuggers.base.IS_EDITABLE", False) debugger = get_debugger(debugger_name) @@ -96,6 +102,7 @@ def test_debugger(debugger_name, expected_class, connection_mode, monkeypatch): ["pdb", "debugpy"], ) def test_debugger_editable(debugger_name, monkeypatch): + """Debugger support package is local path in editable briefcase install.""" with TemporaryDirectory() as tmp_path: tmp_path = Path(tmp_path) (tmp_path / "debugger-support" / f"briefcase-{debugger_name}").mkdir( @@ -116,6 +123,7 @@ def test_debugger_editable(debugger_name, monkeypatch): ["pdb", "debugpy"], ) def test_debugger_editable_path_not_found(debugger_name, monkeypatch): + """Debugger support package is not the local path when path is not available.""" with TemporaryDirectory() as tmp_path: tmp_path = Path(tmp_path) monkeypatch.setattr("briefcase.debuggers.base.IS_EDITABLE", True) diff --git a/tests/platforms/macOS/app/test_run.py b/tests/platforms/macOS/app/test_run.py index e1f2c11f3..f3886d47e 100644 --- a/tests/platforms/macOS/app/test_run.py +++ b/tests/platforms/macOS/app/test_run.py @@ -46,7 +46,10 @@ def test_run_gui_app(run_command, first_app_config, sleep_zero, tmp_path, monkey ) run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] + first_app_config, + debugger_host=None, + debugger_port=None, + passthrough=[], ) # Calls were made to start the app and to start a log stream. @@ -163,7 +166,10 @@ def test_run_gui_app_failed(run_command, first_app_config, sleep_zero, tmp_path) with pytest.raises(BriefcaseCommandError): run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] + first_app_config, + debugger_host=None, + debugger_port=None, + passthrough=[], ) # Calls were made to start the app and to start a log stream. @@ -212,7 +218,10 @@ def test_run_gui_app_find_pid_failed( with pytest.raises(BriefcaseCommandError) as exc_info: run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] + first_app_config, + debugger_host=None, + debugger_port=None, + passthrough=[], ) # Calls were made to start the app and to start a log stream. @@ -267,7 +276,10 @@ def test_run_gui_app_test_mode( ) run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] + first_app_config, + debugger_host=None, + debugger_port=None, + passthrough=[], ) # Calls were made to start the app and to start a log stream. @@ -310,7 +322,12 @@ def test_run_gui_app_test_mode( def test_run_gui_app_debugger( - run_command, first_app_config, sleep_zero, tmp_path, monkeypatch, dummy_debugger + run_command, + first_app_config, + sleep_zero, + tmp_path, + monkeypatch, + dummy_debugger, ): """A macOS GUI app can be started in debug mode.""" # Mock a popen object that represents the log stream @@ -390,7 +407,10 @@ def test_run_console_app(run_command, first_app_config, tmp_path): first_app_config.console_app = True run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] + first_app_config, + debugger_host=None, + debugger_port=None, + passthrough=[], ) # Calls were made to start the app and to start a log stream. @@ -449,7 +469,10 @@ def test_run_console_app_test_mode(run_command, first_app_config, sleep_zero, tm run_command.tools.subprocess.Popen.return_value = app_process run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] + first_app_config, + debugger_host=None, + debugger_port=None, + passthrough=[], ) # Calls were made to start the app and to start a log stream. @@ -526,7 +549,10 @@ def test_run_console_app_failed(run_command, first_app_config, sleep_zero, tmp_p # Although the command raises an error, this could be because the script itself # raised an error. run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] + first_app_config, + debugger_host=None, + debugger_port=None, + passthrough=[], ) # Calls were made to start the app and to start a log stream. diff --git a/tests/platforms/macOS/xcode/test_run.py b/tests/platforms/macOS/xcode/test_run.py index e051842f6..e732601f1 100644 --- a/tests/platforms/macOS/xcode/test_run.py +++ b/tests/platforms/macOS/xcode/test_run.py @@ -46,7 +46,10 @@ def test_run_app(run_command, first_app_config, sleep_zero, tmp_path, monkeypatc ) run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] + first_app_config, + debugger_host=None, + debugger_port=None, + passthrough=[], ) # Calls were made to start the app and to start a log stream. @@ -170,7 +173,10 @@ def test_run_app_test_mode( ) run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] + first_app_config, + debugger_host=None, + debugger_port=None, + passthrough=[], ) # Calls were made to start the app and to start a log stream. diff --git a/tests/platforms/windows/app/test_run.py b/tests/platforms/windows/app/test_run.py index b0ebee12d..9983dde12 100644 --- a/tests/platforms/windows/app/test_run.py +++ b/tests/platforms/windows/app/test_run.py @@ -32,7 +32,10 @@ def test_run_gui_app(run_command, first_app_config, tmp_path): # Run the app run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] + first_app_config, + debugger_host=None, + debugger_port=None, + passthrough=[], ) # The process was started @@ -99,7 +102,10 @@ def test_run_gui_app_failed(run_command, first_app_config, tmp_path): with pytest.raises(OSError): run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] + first_app_config, + debugger_host=None, + debugger_port=None, + passthrough=[], ) # Popen was still invoked, though @@ -126,7 +132,10 @@ def test_run_console_app(run_command, first_app_config, tmp_path): # Run the app run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] + first_app_config, + debugger_host=None, + debugger_port=None, + passthrough=[], ) # The process was started @@ -182,7 +191,10 @@ def test_run_console_app_failed(run_command, first_app_config, tmp_path): with pytest.raises(OSError): run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] + first_app_config, + debugger_host=None, + debugger_port=None, + passthrough=[], ) # Popen was still invoked, though @@ -211,7 +223,10 @@ def test_run_app_test_mode(run_command, first_app_config, is_console_app, tmp_pa # Run the app run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] + first_app_config, + debugger_host=None, + debugger_port=None, + passthrough=[], ) # The process was started diff --git a/tests/platforms/windows/visualstudio/test_run.py b/tests/platforms/windows/visualstudio/test_run.py index 73f1822b9..004d384f5 100644 --- a/tests/platforms/windows/visualstudio/test_run.py +++ b/tests/platforms/windows/visualstudio/test_run.py @@ -35,7 +35,10 @@ def test_run_app(run_command, first_app_config, tmp_path): # Run the app run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] + first_app_config, + debugger_host=None, + debugger_port=None, + passthrough=[], ) # Popen was called @@ -107,7 +110,10 @@ def test_run_app_test_mode(run_command, first_app_config, tmp_path): # Run the app in test mode run_command.run_app( - first_app_config, debugger_host=None, debugger_port=None, passthrough=[] + first_app_config, + debugger_host=None, + debugger_port=None, + passthrough=[], ) # Popen was called From deae895d6ea5439dabdd2689a80aeb3cc7de4c82 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Thu, 4 Sep 2025 22:34:44 +0200 Subject: [PATCH 096/131] fixed licens badge --- debugger-support/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debugger-support/README.md b/debugger-support/README.md index d5fd42dca..ded17bd30 100644 --- a/debugger-support/README.md +++ b/debugger-support/README.md @@ -2,7 +2,7 @@ [![Python Versions](https://img.shields.io/pypi/pyversions/briefcase-debugger.svg)](https://pypi.python.org/pypi/briefcase-debugger) [![PyPI Version](https://img.shields.io/pypi/v/briefcase-debugger.svg)](https://pypi.python.org/pypi/briefcase-debugger) [![Maturity](https://img.shields.io/pypi/status/briefcase-debugger.svg)](https://pypi.python.org/pypi/briefcase-debugger) -[![BSD License](https://img.shields.io/pypi/l/briefcase-debugger.svg)](https://github.com/beeware/briefcase/blob/main/LICENSE) +[![BSD License](https://img.shields.io/pypi/l/briefcase-debugger.svg)](https://github.com/beeware/briefcase/blob/main/debugger-support/LICENSE) [![Build Status](https://github.com/beeware/briefcase/workflows/CI/badge.svg?branch=main)](https://github.com/beeware/briefcase/actions) [![Discord server](https://img.shields.io/discord/836455665257021440?label=Discord%20Chat&logo=discord&style=plastic)](https://beeware.org/bee/chat/) From 16ae953c9a1393878f140dc50e5e54d6ad61292c Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Thu, 4 Sep 2025 22:41:51 +0200 Subject: [PATCH 097/131] added more docs --- src/briefcase/debuggers/base.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/briefcase/debuggers/base.py b/src/briefcase/debuggers/base.py index de9b9d832..83dba5a22 100644 --- a/src/briefcase/debuggers/base.py +++ b/src/briefcase/debuggers/base.py @@ -43,6 +43,11 @@ def get_debugger_requirement(package_name: str, extras: str = ""): the development of the debugger support packages. On normal installs the local version is not available, so the package from pypi is used, that corresponds to the version of briefcase. + + :param package_name: The name of the debugger support package. + :param extras: Optional extras to add to the package requirement. Including square + brackets. E.g. "[debugpy]". + :return: The package requirement. """ if IS_EDITABLE and REPO_ROOT is not None: local_path = REPO_ROOT / "debugger-support" From 296f7d2761066a7a0b2d80a4da221b4e74378219 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Thu, 4 Sep 2025 22:57:55 +0200 Subject: [PATCH 098/131] moved tox debugger config --- tox.ini | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tox.ini b/tox.ini index 019026918..4791532c2 100644 --- a/tox.ini +++ b/tox.ini @@ -67,6 +67,17 @@ commands = html: python -m coverage html --skip-covered --skip-empty python -m coverage report --fail-under=100 +[testenv:py{,310,311,312,313,314}-debugger] +changedir = debugger-support +skip_install = True +deps = + build +commands = + python -m pip install ".[dev,pdb,debugpy]" + python -X warn_default_encoding -m coverage run -m pytest {posargs:-vv --color yes} + python -m coverage combine + python -m coverage report --fail-under=100 + [testenv:towncrier{,-check}] skip_install = True deps = @@ -105,14 +116,3 @@ commands = lint : python -m sphinx {[docs]sphinx_args} {posargs} --builder linkcheck {[docs]docs_dir} {[docs]build_dir}{/}links all : python -m sphinx {[docs]sphinx_args} {posargs} --verbose --write-all --fresh-env --builder html {[docs]docs_dir} {[docs]build_dir}{/}html live : sphinx-autobuild {[docs]sphinx_args} {posargs} --builder html {[docs]docs_dir} {[docs]build_dir}{/}live - -[testenv:py{,310,311,312,313,314}-debugger] -changedir = debugger-support -skip_install = True -deps = - build -commands = - python -m pip install ".[dev,pdb,debugpy]" - python -X warn_default_encoding -m coverage run -m pytest {posargs:-vv --color yes} - python -m coverage combine - python -m coverage report --fail-under=100 From caf9f5e3959b1aed5b1b08ac12166f6e6621fd4c Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Thu, 4 Sep 2025 23:00:37 +0200 Subject: [PATCH 099/131] use dependency groups --- debugger-support/pyproject.toml | 4 +++- tox.ini | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/debugger-support/pyproject.toml b/debugger-support/pyproject.toml index 74c84a13d..8203822ed 100644 --- a/debugger-support/pyproject.toml +++ b/debugger-support/pyproject.toml @@ -20,11 +20,13 @@ dynamic = ["version"] [tool.setuptools_scm] root = "../" -[project.optional-dependencies] +[dependency-groups] dev = [ "pytest == 8.4.1", "coverage[toml] == 7.10.2", ] + +[project.optional-dependencies] pdb = [ "remote-pdb>=2.1.0,<3.0.0" ] diff --git a/tox.ini b/tox.ini index 4791532c2..1f4f0683e 100644 --- a/tox.ini +++ b/tox.ini @@ -73,7 +73,7 @@ skip_install = True deps = build commands = - python -m pip install ".[dev,pdb,debugpy]" + python -m pip install ".[pdb,debugpy]" --group dev python -X warn_default_encoding -m coverage run -m pytest {posargs:-vv --color yes} python -m coverage combine python -m coverage report --fail-under=100 From 535d49ee6a55e410b92687c8477b0f79b47a6518 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Thu, 4 Sep 2025 23:10:26 +0200 Subject: [PATCH 100/131] start debugger in the .pth file and not on import --- debugger-support/setup.py | 4 +++- .../src/briefcase_debugger/__init__.py | 3 --- debugger-support/tests/test_base.py | 21 ++++++------------- 3 files changed, 9 insertions(+), 19 deletions(-) diff --git a/debugger-support/setup.py b/debugger-support/setup.py index 70312716f..fce94a2fa 100644 --- a/debugger-support/setup.py +++ b/debugger-support/setup.py @@ -19,7 +19,9 @@ class install_with_pth(install): """ _pth_name = "briefcase_debugger" - _pth_contents = "import briefcase_debugger" + _pth_contents = ( + "import briefcase_debugger; briefcase_debugger.start_remote_debugger()" + ) def initialize_options(self): install.initialize_options(self) diff --git a/debugger-support/src/briefcase_debugger/__init__.py b/debugger-support/src/briefcase_debugger/__init__.py index 0da61d07e..d0117a29b 100644 --- a/debugger-support/src/briefcase_debugger/__init__.py +++ b/debugger-support/src/briefcase_debugger/__init__.py @@ -42,6 +42,3 @@ def start_remote_debugger(): # Show exception and stop the whole application when an error occurs print(traceback.format_exc()) sys.exit(-1) - - -start_remote_debugger() diff --git a/debugger-support/tests/test_base.py b/debugger-support/tests/test_base.py index 9b5c1950b..a0fa8606a 100644 --- a/debugger-support/tests/test_base.py +++ b/debugger-support/tests/test_base.py @@ -2,27 +2,18 @@ import json import os import sys -from unittest.mock import MagicMock, call +from unittest.mock import MagicMock import briefcase_debugger -def test_auto_startup(monkeypatch, capsys): - """Debugger startup code is executed on import.""" - fake_environ = MagicMock() - monkeypatch.setattr(os, "environ", fake_environ) - - fake_environ.get.return_value = None - - # reload the module to trigger the auto startup code +def test_import_for_code_coverage(monkeypatch, capsys): + """Get 100% code coverage.""" + # The module `briefcase_debugger` is already imported through the .pth + # file. Code executed during .pth files are not covered by coverage.py. + # So we need to reload the module to get a 100% code coverage. importlib.reload(importlib.import_module("briefcase_debugger")) - # check if the autostart was executed - assert fake_environ.get.mock_calls == [ - call("BRIEFCASE_DEBUG", "0"), - call("BRIEFCASE_DEBUGGER", None), - ] - def test_unknown_debugger(monkeypatch, capsys): """An unknown debugger raises an error and stops the application.""" From 673dbf02a4501e994e12dce49e66fa66136971f2 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Thu, 4 Sep 2025 23:14:05 +0200 Subject: [PATCH 101/131] reorderd sections in pyproject.toml --- debugger-support/pyproject.toml | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/debugger-support/pyproject.toml b/debugger-support/pyproject.toml index 8203822ed..924cb700a 100644 --- a/debugger-support/pyproject.toml +++ b/debugger-support/pyproject.toml @@ -17,8 +17,9 @@ dependencies = [ ] dynamic = ["version"] -[tool.setuptools_scm] -root = "../" +[project.optional-dependencies] +pdb = ["remote-pdb>=2.1.0,<3.0.0"] +debugpy = ["debugpy>=1.8.14,<2.0.0"] [dependency-groups] dev = [ @@ -26,14 +27,6 @@ dev = [ "coverage[toml] == 7.10.2", ] -[project.optional-dependencies] -pdb = [ - "remote-pdb>=2.1.0,<3.0.0" -] -debugpy = [ - "debugpy>=1.8.14,<2.0.0" -] - [tool.coverage.run] parallel = true branch = true @@ -51,3 +44,6 @@ show_missing = true skip_covered = true skip_empty = true precision = 1 + +[tool.setuptools_scm] +root = "../" From 88ba809198fb6f44b5304867235050cf6448b2e0 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Tue, 9 Sep 2025 00:03:43 +0200 Subject: [PATCH 102/131] parameterized path mapping tests with additional cases for andorid on posix / on windows. --- .../tests/test_debugpy_path_mappings.py | 375 ++++++++++-------- 1 file changed, 220 insertions(+), 155 deletions(-) diff --git a/debugger-support/tests/test_debugpy_path_mappings.py b/debugger-support/tests/test_debugpy_path_mappings.py index 871461821..1664f5d85 100644 --- a/debugger-support/tests/test_debugpy_path_mappings.py +++ b/debugger-support/tests/test_debugpy_path_mappings.py @@ -1,5 +1,5 @@ +import os import sys -from pathlib import Path, PosixPath, PurePosixPath, PureWindowsPath, WindowsPath import briefcase_debugger.debugpy import pytest @@ -29,184 +29,249 @@ def test_mappings_none(monkeypatch): assert path_mappings == [] -def test_mappings_windows(monkeypatch): - """Path mappings on an Windows system.""" - # When running tests on Linux/macOS, we have to switch to WindowsPath. - if isinstance(Path(), PosixPath): - monkeypatch.setattr(briefcase_debugger.debugpy, "Path", PureWindowsPath) - - config = DebuggerConfig( - debugger="debugpy", - host="", - port=0, - app_path_mappings=AppPathMappings( - device_sys_path_regex="app$", - device_subfolders=["helloworld"], - host_folders=["src/helloworld"], +@pytest.mark.parametrize( + "os_name,app_path_mappings,app_packages_path_mappings,sys_path,expected_path_mappings", + [ + # Windows + pytest.param( + "nt", + AppPathMappings( + device_sys_path_regex="app$", + device_subfolders=["helloworld"], + host_folders=["C:\\PROJECT_ROOT\\src\\helloworld"], + ), + None, + [ + "C:\\PROJECT_ROOT\\build\\helloworld\\windows\\app\\src\\python313.zip", + "C:\\PROJECT_ROOT\\build\\helloworld\\windows\\app\\src", + "C:\\PROJECT_ROOT\\build\\helloworld\\windows\\app\\src\\app", + "C:\\PROJECT_ROOT\\build\\helloworld\\windows\\app\\src\\app_packages", + ], + [ + ( + "C:\\PROJECT_ROOT\\src\\helloworld", + "C:\\PROJECT_ROOT\\build\\helloworld\\windows\\app\\src\\app\\helloworld", + ), + ], + id="windows", ), - app_packages_path_mappings=None, - ) - - sys_path = [ - "build\\helloworld\\windows\\app\\src\\python313.zip", - "build\\helloworld\\windows\\app\\src", - "build\\helloworld\\windows\\app\\src\\app", - "build\\helloworld\\windows\\app\\src\\app_packages", - ] - monkeypatch.setattr(sys, "path", sys_path) - - path_mappings = briefcase_debugger.debugpy.load_path_mappings(config, False) - - assert path_mappings == [ - # (host_path, device_path) - ("src/helloworld", "build\\helloworld\\windows\\app\\src\\app\\helloworld"), - ] - - -def test_mappings_macos(monkeypatch): - """Path mappings on an macOS system.""" - # When running tests on windows, we have to switch to PosixPath. - if isinstance(Path(), WindowsPath): - monkeypatch.setattr(briefcase_debugger.debugpy, "Path", PurePosixPath) - - config = DebuggerConfig( - debugger="debugpy", - host="", - port=0, - app_path_mappings=AppPathMappings( - device_sys_path_regex="app$", - device_subfolders=["helloworld"], - host_folders=["src/helloworld"], + # Windows with `app_packages_path_mappings` (currently not used by briefcase, but principally possible) + pytest.param( + "nt", + AppPathMappings( + device_sys_path_regex="app$", + device_subfolders=["helloworld"], + host_folders=["C:\\PROJECT_ROOT\\src\\helloworld"], + ), + AppPackagesPathMappings( + sys_path_regex="app_packages$", + host_folder="C:\\PROJECT_ROOT\\build\\helloworld\\windows\\app\\src\\app_packages", + ), + [ + "C:\\PROJECT_ROOT\\build\\helloworld\\windows\\app\\src\\python313.zip", + "C:\\PROJECT_ROOT\\build\\helloworld\\windows\\app\\src", + "C:\\PROJECT_ROOT\\build\\helloworld\\windows\\app\\src\\app", + "C:\\PROJECT_ROOT\\build\\helloworld\\windows\\app\\src\\app_packages", + ], + [ + ( + "C:\\PROJECT_ROOT\\src\\helloworld", + "C:\\PROJECT_ROOT\\build\\helloworld\\windows\\app\\src\\app\\helloworld", + ), + ( + "C:\\PROJECT_ROOT\\build\\helloworld\\windows\\app\\src\\app_packages", + "C:\\PROJECT_ROOT\\build\\helloworld\\windows\\app\\src\\app_packages", + ), + ], + id="windows-with-app-packages", ), - app_packages_path_mappings=None, - ) - - sys_path = [ - "build/helloworld/macos/app/Hello World.app/Contents/Frameworks/Python.framework/Versions/3.13/lib/python3.13", - "build/helloworld/macos/app/Hello World.app/Contents/Frameworks/Python.framework/Versions/3.13/lib/python3.13/lib-dynload", - "build/helloworld/macos/app/Hello World.app/Contents/Resources/app", - "build/helloworld/macos/app/Hello World.app/Contents/Frameworks/Python.framework/Versions/3.13/lib/python3.13/site-packages", - "build/helloworld/macos/app/Hello World.app/Contents/Resources/app_packages", - ] - monkeypatch.setattr(sys, "path", sys_path) - - path_mappings = briefcase_debugger.debugpy.load_path_mappings(config, False) - - assert path_mappings == [ - # (host_path, device_path) - ( - "src/helloworld", - "build/helloworld/macos/app/Hello World.app/Contents/Resources/app/helloworld", - ), - ] - - -def test_mappings_ios(monkeypatch): - """Path mappings on an iOS system.""" - # When running tests on windows, we have to switch to PosixPath. - if isinstance(Path(), WindowsPath): - monkeypatch.setattr(briefcase_debugger.debugpy, "Path", PurePosixPath) - - config = DebuggerConfig( - debugger="debugpy", - host="", - port=0, - app_path_mappings=AppPathMappings( - device_sys_path_regex="app$", - device_subfolders=["helloworld"], - host_folders=["src/helloworld"], + # macOS + pytest.param( + "posix", + AppPathMappings( + device_sys_path_regex="app$", + device_subfolders=["helloworld"], + host_folders=["/PROJECT_ROOT/src/helloworld"], + ), + None, + [ + "/PROJECT_ROOT/build/helloworld/macos/app/Hello World.app/Contents/Frameworks/Python.framework/Versions/3.13/lib/python3.13", + "/PROJECT_ROOT/build/helloworld/macos/app/Hello World.app/Contents/Frameworks/Python.framework/Versions/3.13/lib/python3.13/lib-dynload", + "/PROJECT_ROOT/build/helloworld/macos/app/Hello World.app/Contents/Resources/app", + "/PROJECT_ROOT/build/helloworld/macos/app/Hello World.app/Contents/Frameworks/Python.framework/Versions/3.13/lib/python3.13/site-packages", + "/PROJECT_ROOT/build/helloworld/macos/app/Hello World.app/Contents/Resources/app_packages", + ], + [ + ( + "/PROJECT_ROOT/src/helloworld", + "/PROJECT_ROOT/build/helloworld/macos/app/Hello World.app/Contents/Resources/app/helloworld", + ) + ], + id="macos", ), - app_packages_path_mappings=AppPackagesPathMappings( - sys_path_regex="app_packages$", - host_folder="APP_PACKAGES_PATH/app_packages.iphonesimulator", + # iOS + pytest.param( + "posix", + AppPathMappings( + device_sys_path_regex="app$", + device_subfolders=["helloworld"], + host_folders=["/PROJECT_ROOT/src/helloworld"], + ), + AppPackagesPathMappings( + sys_path_regex="app_packages$", + host_folder="/APP_PACKAGES_PATH/app_packages.iphonesimulator", + ), + [ + "CoreSimulator/Devices/RANDOM_NUMBER/data/Containers/Bundle/Application/RANDOM_NUMBER/Hello World.app/python/lib/python3.13", + "CoreSimulator/Devices/RANDOM_NUMBER/data/Containers/Bundle/Application/RANDOM_NUMBER/Hello World.app/python/lib/python3.13/lib-dynload", + "CoreSimulator/Devices/RANDOM_NUMBER/data/Containers/Bundle/Application/RANDOM_NUMBER/Hello World.app/app", + "CoreSimulator/Devices/RANDOM_NUMBER/data/Containers/Bundle/Application/RANDOM_NUMBER/Hello World.app/python/lib/python3.13/site-packages", + "CoreSimulator/Devices/RANDOM_NUMBER/data/Containers/Bundle/Application/RANDOM_NUMBER/Hello World.app/app_packages", + ], + [ + ( + "/PROJECT_ROOT/src/helloworld", + "CoreSimulator/Devices/RANDOM_NUMBER/data/Containers/Bundle/Application/RANDOM_NUMBER/Hello World.app/app/helloworld", + ), + ( + "/APP_PACKAGES_PATH/app_packages.iphonesimulator", + "CoreSimulator/Devices/RANDOM_NUMBER/data/Containers/Bundle/Application/RANDOM_NUMBER/Hello World.app/app_packages", + ), + ], + id="ios", ), - ) - - sys_path = [ - "CoreSimulator/Devices/RANDOM_NUMBER/data/Containers/Bundle/Application/RANDOM_NUMBER/Hello World.app/python/lib/python3.13", - "CoreSimulator/Devices/RANDOM_NUMBER/data/Containers/Bundle/Application/RANDOM_NUMBER/Hello World.app/python/lib/python3.13/lib-dynload", - "CoreSimulator/Devices/RANDOM_NUMBER/data/Containers/Bundle/Application/RANDOM_NUMBER/Hello World.app/app", - "CoreSimulator/Devices/RANDOM_NUMBER/data/Containers/Bundle/Application/RANDOM_NUMBER/Hello World.app/python/lib/python3.13/site-packages", - "CoreSimulator/Devices/RANDOM_NUMBER/data/Containers/Bundle/Application/RANDOM_NUMBER/Hello World.app/app_packages", - ] - monkeypatch.setattr(sys, "path", sys_path) - - path_mappings = briefcase_debugger.debugpy.load_path_mappings(config, False) - - assert path_mappings == [ - # (host_path, device_path) - ( - "src/helloworld", - "CoreSimulator/Devices/RANDOM_NUMBER/data/Containers/Bundle/Application/RANDOM_NUMBER/Hello World.app/app/helloworld", + # Android (with VSCode running on Windows) + pytest.param( + "posix", + AppPathMappings( + device_sys_path_regex="app$", + device_subfolders=["helloworld"], + host_folders=["C:\\PROJECT_ROOT\\src\\helloworld"], + ), + AppPackagesPathMappings( + sys_path_regex="requirements$", + host_folder="C:\\BUNDLE_PATH\\app\\build\\python\\pip\\debug\\common", + ), + [ + "/data/data/com.example.helloworld/files/chaquopy/AssetFinder/app", + "/data/data/com.example.helloworld/files/chaquopy/AssetFinder/requirements", + "/data/data/com.example.helloworld/files/chaquopy/AssetFinder/stdlib-x86_64", + "/data/user/0/com.example.helloworld/files/chaquopy/stdlib-common.imy", + "/data/user/0/com.example.helloworld/files/chaquopy/bootstrap.imy", + "/data/user/0/com.example.helloworld/files/chaquopy/bootstrap-native/x86_64", + ], + [ + ( + "C:\\PROJECT_ROOT\\src\\helloworld", + "/data/data/com.example.helloworld/files/chaquopy/AssetFinder/app/helloworld", + ), + ( + "C:\\BUNDLE_PATH\\app\\build\\python\\pip\\debug\\common", + "/data/data/com.example.helloworld/files/chaquopy/AssetFinder/requirements", + ), + ], + id="android-on-windows-host", ), - ( - "APP_PACKAGES_PATH/app_packages.iphonesimulator", - "CoreSimulator/Devices/RANDOM_NUMBER/data/Containers/Bundle/Application/RANDOM_NUMBER/Hello World.app/app_packages", + # Android (with VSCode running on POSIX system) + pytest.param( + "posix", + AppPathMappings( + device_sys_path_regex="app$", + device_subfolders=["helloworld"], + host_folders=["/PROJECT_ROOT/src/helloworld"], + ), + AppPackagesPathMappings( + sys_path_regex="requirements$", + host_folder="/BUNDLE_PATH/app/build/python/pip/debug/common", + ), + [ + "/data/data/com.example.helloworld/files/chaquopy/AssetFinder/app", + "/data/data/com.example.helloworld/files/chaquopy/AssetFinder/requirements", + "/data/data/com.example.helloworld/files/chaquopy/AssetFinder/stdlib-x86_64", + "/data/user/0/com.example.helloworld/files/chaquopy/stdlib-common.imy", + "/data/user/0/com.example.helloworld/files/chaquopy/bootstrap.imy", + "/data/user/0/com.example.helloworld/files/chaquopy/bootstrap-native/x86_64", + ], + [ + ( + "/PROJECT_ROOT/src/helloworld", + "/data/data/com.example.helloworld/files/chaquopy/AssetFinder/app/helloworld", + ), + ( + "/BUNDLE_PATH/app/build/python/pip/debug/common", + "/data/data/com.example.helloworld/files/chaquopy/AssetFinder/requirements", + ), + ], + id="android-on-posix-host", ), - ] - - -def test_mappings_android(monkeypatch): - """Path mappings on an Android system.""" - # When running tests on windows, we have to switch to PosixPath. - if isinstance(Path(), WindowsPath): - monkeypatch.setattr(briefcase_debugger.debugpy, "Path", PurePosixPath) + ], +) +def test_mappings( + os_name: str, + app_path_mappings: AppPathMappings, + app_packages_path_mappings: AppPackagesPathMappings | None, + sys_path: list[str], + expected_path_mappings: tuple[str, str], + monkeypatch, +): + if os.name != os_name: + pytest.skip(f"Test only runs on {os_name} systems") config = DebuggerConfig( debugger="debugpy", host="", port=0, - app_path_mappings=AppPathMappings( - device_sys_path_regex="app$", - device_subfolders=["helloworld"], - host_folders=["src/helloworld"], - ), - app_packages_path_mappings=AppPackagesPathMappings( - sys_path_regex="requirements$", - host_folder="/BUNDLE_PATH/app/build/python/pip/debug/common", - ), + app_path_mappings=app_path_mappings, + app_packages_path_mappings=app_packages_path_mappings, ) - sys_path = [ - "/data/data/com.example.helloworld/files/chaquopy/AssetFinder/app", - "/data/data/com.example.helloworld/files/chaquopy/AssetFinder/requirements", - "/data/data/com.example.helloworld/files/chaquopy/AssetFinder/stdlib-x86_64", - "/data/user/0/com.example.helloworld/files/chaquopy/stdlib-common.imy", - "/data/user/0/com.example.helloworld/files/chaquopy/bootstrap.imy", - "/data/user/0/com.example.helloworld/files/chaquopy/bootstrap-native/x86_64", - ] monkeypatch.setattr(sys, "path", sys_path) + monkeypatch.setattr(os, "name", "nt") path_mappings = briefcase_debugger.debugpy.load_path_mappings(config, False) - assert path_mappings == [ - # (host_path, device_path) - ( - "src/helloworld", - "/data/data/com.example.helloworld/files/chaquopy/AssetFinder/app/helloworld", + assert path_mappings == expected_path_mappings + + +@pytest.mark.parametrize( + "os_name,app_path_mappings", + [ + # Windows + pytest.param( + "nt", + AppPathMappings( + device_sys_path_regex="app$", + device_subfolders=["helloworld"], + host_folders=["C:\\PROJECT_ROOT\\src\\helloworld"], + ), + id="windows", ), - ( - "/BUNDLE_PATH/app/build/python/pip/debug/common", - "/data/data/com.example.helloworld/files/chaquopy/AssetFinder/requirements", + # POSIX (macOS/iOS/Android) + pytest.param( + "posix", + AppPathMappings( + device_sys_path_regex="app$", + device_subfolders=["helloworld"], + host_folders=["/PROJECT_ROOT/src/helloworld"], + ), + id="posix", ), - ] - - -def test_mappings_windows_wrong_sys_path(monkeypatch): - """Path mappings on an Windows system with a wrong sys path set.""" - # When running tests on Linux/macOS, we have to switch to WindowsPath. - if isinstance(Path(), PosixPath): - monkeypatch.setattr(briefcase_debugger.debugpy, "Path", PureWindowsPath) + ], +) +def test_mappings_wrong_sys_path( + os_name: str, + app_path_mappings: AppPathMappings, + monkeypatch, +): + """Path mappings with a wrong sys path set.""" + if os.name != os_name: + pytest.skip(f"Test only runs on {os_name} systems") config = DebuggerConfig( debugger="debugpy", host="", port=0, - app_path_mappings=AppPathMappings( - device_sys_path_regex="app$", - device_subfolders=["helloworld"], - host_folders=["src/helloworld"], - ), + app_path_mappings=app_path_mappings, app_packages_path_mappings=None, ) From aa9b600efa8d6a241a0ccc39ec022fbc8483b1d6 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Tue, 9 Sep 2025 00:09:19 +0200 Subject: [PATCH 103/131] fixed path mapping test on posix --- debugger-support/tests/test_debugpy_path_mappings.py | 1 - 1 file changed, 1 deletion(-) diff --git a/debugger-support/tests/test_debugpy_path_mappings.py b/debugger-support/tests/test_debugpy_path_mappings.py index 1664f5d85..b4b4e27eb 100644 --- a/debugger-support/tests/test_debugpy_path_mappings.py +++ b/debugger-support/tests/test_debugpy_path_mappings.py @@ -226,7 +226,6 @@ def test_mappings( ) monkeypatch.setattr(sys, "path", sys_path) - monkeypatch.setattr(os, "name", "nt") path_mappings = briefcase_debugger.debugpy.load_path_mappings(config, False) From f57abfdb6c7bbd0bb91af9cd3ae3dbcb88bef175 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Tue, 9 Sep 2025 00:36:30 +0200 Subject: [PATCH 104/131] remove "if verbose" inside test cases --- debugger-support/tests/test_debugpy.py | 36 +++++++++++++++++++------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/debugger-support/tests/test_debugpy.py b/debugger-support/tests/test_debugpy.py index 8585c9a16..6226fedcc 100644 --- a/debugger-support/tests/test_debugpy.py +++ b/debugger-support/tests/test_debugpy.py @@ -39,8 +39,16 @@ def test_no_debugger_verbose(monkeypatch, capsys): assert captured.err == "" -@pytest.mark.parametrize("verbose", [True, False]) -def test_with_debugger(monkeypatch, capsys, verbose): +@pytest.mark.parametrize( + "verbose,some_verbose_output,pydevd_trace_level", + [ + (True, "Extracted path mappings:\n[0] host = src/helloworld", 3), + (False, "", 0), + ], +) +def test_with_debugger( + verbose, some_verbose_output, pydevd_trace_level, monkeypatch, capsys +): """Normal debug session.""" # When running tests on Linux/macOS, we have to switch to WindowsPath. if isinstance(Path(), PosixPath): @@ -78,6 +86,7 @@ def test_with_debugger(monkeypatch, capsys, verbose): # we fake the whole module, as otherwise the import in start_remote_debugger would fail fake_pydevd = MagicMock() monkeypatch.setitem(sys.modules, "pydevd", fake_pydevd) + fake_pydevd.DebugInfoHolder.DEBUG_TRACE_LEVEL = 0 fake_pydevd_file_utils = MagicMock() fake_pydevd_file_utils.setup_client_server_paths.return_value = None monkeypatch.setitem(sys.modules, "pydevd_file_utils", fake_pydevd_file_utils) @@ -101,13 +110,20 @@ def test_with_debugger(monkeypatch, capsys, verbose): assert "Waiting for debugger to attach..." in captured.out assert captured.err == "" - if verbose: - assert "Extracted path mappings:\n[0] host = src/helloworld" in captured.out - assert fake_pydevd.DebugInfoHolder.DEBUG_TRACE_LEVEL == 3 + assert some_verbose_output in captured.out + assert fake_pydevd.DebugInfoHolder.DEBUG_TRACE_LEVEL == pydevd_trace_level -@pytest.mark.parametrize("verbose", [True, False]) -def test_os_file_bugfix(monkeypatch, capsys, verbose): +@pytest.mark.parametrize( + "verbose,some_verbose_output,pydevd_trace_level", + [ + (True, "'os.__file__' not available. Patching it...", 3), + (False, "", 0), + ], +) +def test_os_file_bugfix( + verbose, some_verbose_output, pydevd_trace_level, monkeypatch, capsys +): """The os.__file__ bugfix has to be applied (see https://github.com/microsoft/debugpy/issues/1943).""" os_environ = {} os_environ["BRIEFCASE_DEBUG"] = "1" if verbose else "0" @@ -133,6 +149,7 @@ def test_os_file_bugfix(monkeypatch, capsys, verbose): # we fake the whole module, as otherwise the import in start_remote_debugger would fail fake_pydevd = MagicMock() monkeypatch.setitem(sys.modules, "pydevd", fake_pydevd) + fake_pydevd.DebugInfoHolder.DEBUG_TRACE_LEVEL = 0 # start test function briefcase_debugger.start_remote_debugger() @@ -149,6 +166,5 @@ def test_os_file_bugfix(monkeypatch, capsys, verbose): assert "Waiting for debugger to attach..." in captured.out assert captured.err == "" - if verbose: - assert "'os.__file__' not available. Patching it..." in captured.out - assert fake_pydevd.DebugInfoHolder.DEBUG_TRACE_LEVEL == 3 + assert some_verbose_output in captured.out + assert fake_pydevd.DebugInfoHolder.DEBUG_TRACE_LEVEL == pydevd_trace_level From b1ef9a0efe6957430e7ac1176b586e483200b1a0 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Tue, 9 Sep 2025 00:44:57 +0200 Subject: [PATCH 105/131] rename "debugger-support" folder to just "debugger" --- .github/workflows/ci.yml | 2 +- .gitignore | 2 +- debugger-support/README.md | 2 +- .../build/lib/briefcase_debugger/__init__.py | 44 +++++++ .../build/lib/briefcase_debugger/common.py | 0 .../build/lib/briefcase_debugger/config.py | 20 +++ .../build/lib/briefcase_debugger/debugpy.py | 116 ++++++++++++++++++ .../build/lib/briefcase_debugger/pdb.py | 33 +++++ src/briefcase/debuggers/base.py | 2 +- tests/debuggers/test_base.py | 6 +- tox.ini | 2 +- 11 files changed, 220 insertions(+), 9 deletions(-) create mode 100644 debugger-support/build/lib/briefcase_debugger/__init__.py create mode 100644 debugger-support/build/lib/briefcase_debugger/common.py create mode 100644 debugger-support/build/lib/briefcase_debugger/config.py create mode 100644 debugger-support/build/lib/briefcase_debugger/debugpy.py create mode 100644 debugger-support/build/lib/briefcase_debugger/pdb.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5e13bf55c..47b8ed652 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,7 +54,7 @@ jobs: attestations: write uses: beeware/.github/.github/workflows/python-package-create.yml@main with: - build-subdirectory: "debugger-support" + build-subdirectory: "debugger" package-automation: name: Package Automation diff --git a/.gitignore b/.gitignore index bcdec03c7..e5b45ea80 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ /dist /build automation/build -debugger-support/build +debugger/build docs/_build/ distribute-* .DS_Store diff --git a/debugger-support/README.md b/debugger-support/README.md index ded17bd30..026ddf364 100644 --- a/debugger-support/README.md +++ b/debugger-support/README.md @@ -2,7 +2,7 @@ [![Python Versions](https://img.shields.io/pypi/pyversions/briefcase-debugger.svg)](https://pypi.python.org/pypi/briefcase-debugger) [![PyPI Version](https://img.shields.io/pypi/v/briefcase-debugger.svg)](https://pypi.python.org/pypi/briefcase-debugger) [![Maturity](https://img.shields.io/pypi/status/briefcase-debugger.svg)](https://pypi.python.org/pypi/briefcase-debugger) -[![BSD License](https://img.shields.io/pypi/l/briefcase-debugger.svg)](https://github.com/beeware/briefcase/blob/main/debugger-support/LICENSE) +[![BSD License](https://img.shields.io/pypi/l/briefcase-debugger.svg)](https://github.com/beeware/briefcase/blob/main/debugger/LICENSE) [![Build Status](https://github.com/beeware/briefcase/workflows/CI/badge.svg?branch=main)](https://github.com/beeware/briefcase/actions) [![Discord server](https://img.shields.io/discord/836455665257021440?label=Discord%20Chat&logo=discord&style=plastic)](https://beeware.org/bee/chat/) diff --git a/debugger-support/build/lib/briefcase_debugger/__init__.py b/debugger-support/build/lib/briefcase_debugger/__init__.py new file mode 100644 index 000000000..d0117a29b --- /dev/null +++ b/debugger-support/build/lib/briefcase_debugger/__init__.py @@ -0,0 +1,44 @@ +import json +import os +import sys +import traceback + + +def start_remote_debugger(): + try: + # check verbose output + verbose = os.environ.get("BRIEFCASE_DEBUG", "0") == "1" + + # reading config + config_str = os.environ.get("BRIEFCASE_DEBUGGER", None) + + # skip debugger if no config is set + if config_str is None: + if verbose: + print( + "No 'BRIEFCASE_DEBUGGER' environment variable found. Debugger not starting." + ) + return # If BRIEFCASE_DEBUGGER is not set, this packages does nothing... + + if verbose: + print(f"'BRIEFCASE_DEBUGGER'={config_str}") + + # Parsing config json + config = json.loads(config_str) + + # start debugger + print("Starting remote debugger...") + if config["debugger"] == "debugpy": + from briefcase_debugger.debugpy import start_debugpy + + start_debugpy(config, verbose) + elif config["debugger"] == "pdb": + from briefcase_debugger.pdb import start_pdb + + start_pdb(config, verbose) + else: + raise ValueError(f"Unknown debugger '{config['debugger']}'") + except Exception: + # Show exception and stop the whole application when an error occurs + print(traceback.format_exc()) + sys.exit(-1) diff --git a/debugger-support/build/lib/briefcase_debugger/common.py b/debugger-support/build/lib/briefcase_debugger/common.py new file mode 100644 index 000000000..e69de29bb diff --git a/debugger-support/build/lib/briefcase_debugger/config.py b/debugger-support/build/lib/briefcase_debugger/config.py new file mode 100644 index 000000000..3d250b383 --- /dev/null +++ b/debugger-support/build/lib/briefcase_debugger/config.py @@ -0,0 +1,20 @@ +from typing import TypedDict + + +class AppPathMappings(TypedDict): + device_sys_path_regex: str + device_subfolders: list[str] + host_folders: list[str] + + +class AppPackagesPathMappings(TypedDict): + sys_path_regex: str + host_folder: str + + +class DebuggerConfig(TypedDict): + debugger: str + host: str + port: int + app_path_mappings: AppPathMappings | None + app_packages_path_mappings: AppPackagesPathMappings | None diff --git a/debugger-support/build/lib/briefcase_debugger/debugpy.py b/debugger-support/build/lib/briefcase_debugger/debugpy.py new file mode 100644 index 000000000..4570e2984 --- /dev/null +++ b/debugger-support/build/lib/briefcase_debugger/debugpy.py @@ -0,0 +1,116 @@ +import os +import re +import sys +from pathlib import Path + +import debugpy + +from briefcase_debugger.config import DebuggerConfig + + +def find_first_matching_path(regex: str) -> str: + """Returns the first element of sys.paths that matches regex, otherwise None.""" + for path in sys.path: + if re.search(regex, path): + return path + raise ValueError(f"No sys.path entry matches regex '{regex}'") + + +def load_path_mappings(config: DebuggerConfig, verbose: bool) -> list[tuple[str, str]]: + app_path_mappings = config.get("app_path_mappings", None) + app_packages_path_mappings = config.get("app_packages_path_mappings", None) + + mappings_list = [] + if app_path_mappings: + device_app_folder = find_first_matching_path( + app_path_mappings["device_sys_path_regex"] + ) + for app_subfolder_device, app_subfolder_host in zip( + app_path_mappings["device_subfolders"], + app_path_mappings["host_folders"], + strict=False, + ): + mappings_list.append( + ( + app_subfolder_host, + str(Path(device_app_folder) / app_subfolder_device), + ) + ) + if app_packages_path_mappings: + device_app_packages_folder = find_first_matching_path( + app_packages_path_mappings["sys_path_regex"] + ) + mappings_list.append( + ( + app_packages_path_mappings["host_folder"], + str(Path(device_app_packages_folder)), + ) + ) + + if verbose: + print("Extracted path mappings:") + for idx, mapping in enumerate(mappings_list): + print(f"[{idx}] host = {mapping[0]}") + print(f"[{idx}] device = {mapping[1]}") + + return mappings_list + + +def start_debugpy(config: DebuggerConfig, verbose: bool): + host = config["host"] + port = config["port"] + path_mappings = load_path_mappings(config, verbose) + + # There is a bug in debugpy that has to be handled until there is a new + # debugpy release, see https://github.com/microsoft/debugpy/issues/1943 + if not hasattr(os, "__file__"): + if verbose: + print("'os.__file__' not available. Patching it...") + os.__file__ = "" + + # Starting remote debugger... + print(f"Starting debugpy in server mode at {host}:{port}...") + debugpy.listen((host, port), in_process_debug_adapter=True) + + if verbose: + # pydevd is dynamically loaded and only available after debugpy is started + import pydevd + + pydevd.DebugInfoHolder.DEBUG_TRACE_LEVEL = 3 + + if len(path_mappings) > 0: + if verbose: + print("Adding path mappings...") + + # pydevd is dynamically loaded and only available after a debugger has connected + import pydevd_file_utils + + pydevd_file_utils.setup_client_server_paths(path_mappings) + + print("The debugpy server started. Waiting for debugger to attach...") + print( + f""" +To connect to debugpy using VSCode add the following configuration to '.vscode/launch.json': +{{ + "version": "0.2.0", + "configurations": [ + {{ + "name": "Briefcase: Attach (Connect)", + "type": "debugpy", + "request": "attach", + "connect": {{ + "host": "{host}", + "port": {port} + }}, + "justMyCode": false + }} + ] +}} + +For more information see: https://briefcase.readthedocs.io/en/stable/how-to/debugging/vscode.html#bundled-app +""" + ) + debugpy.wait_for_client() + + print("Debugger attached.") + print("-" * 75) diff --git a/debugger-support/build/lib/briefcase_debugger/pdb.py b/debugger-support/build/lib/briefcase_debugger/pdb.py new file mode 100644 index 000000000..86b8747f8 --- /dev/null +++ b/debugger-support/build/lib/briefcase_debugger/pdb.py @@ -0,0 +1,33 @@ +import sys + +from remote_pdb import RemotePdb + +from briefcase_debugger.config import DebuggerConfig + + +def start_pdb(config: DebuggerConfig, verbose: bool): + """Start remote PDB server.""" + # Parsing host/port + host = config["host"] + port = config["port"] + + print( + f""" +Remote PDB server opened at {host}:{port}. +Waiting for debugger to attach... +To connect to remote PDB use eg.: + - telnet {host} {port} (Windows) + - rlwrap socat - tcp:{host}:{port} (Linux, macOS) + +For more information see: https://briefcase.readthedocs.io/en/stable/how-to/debugging/console.html#bundled-app +""" + ) + + # Create a RemotePdb instance + remote_pdb = RemotePdb(host, port, quiet=True) + + # Connect the remote PDB with the "breakpoint()" function + sys.breakpointhook = remote_pdb.set_trace + + print("Debugger client attached.") + print("-" * 75) diff --git a/src/briefcase/debuggers/base.py b/src/briefcase/debuggers/base.py index 83dba5a22..0a3da7d87 100644 --- a/src/briefcase/debuggers/base.py +++ b/src/briefcase/debuggers/base.py @@ -50,7 +50,7 @@ def get_debugger_requirement(package_name: str, extras: str = ""): :return: The package requirement. """ if IS_EDITABLE and REPO_ROOT is not None: - local_path = REPO_ROOT / "debugger-support" + local_path = REPO_ROOT / "debugger" if local_path.exists() and local_path.is_dir(): return f"{local_path}{extras}" return f"{package_name}{extras}=={briefcase.__version__}" diff --git a/tests/debuggers/test_base.py b/tests/debuggers/test_base.py index 3cc02719c..19ab8385d 100644 --- a/tests/debuggers/test_base.py +++ b/tests/debuggers/test_base.py @@ -105,15 +105,13 @@ def test_debugger_editable(debugger_name, monkeypatch): """Debugger support package is local path in editable briefcase install.""" with TemporaryDirectory() as tmp_path: tmp_path = Path(tmp_path) - (tmp_path / "debugger-support" / f"briefcase-{debugger_name}").mkdir( - parents=True, exist_ok=True - ) + (tmp_path / "debugger").mkdir(parents=True, exist_ok=True) monkeypatch.setattr("briefcase.debuggers.base.IS_EDITABLE", True) monkeypatch.setattr("briefcase.debuggers.base.REPO_ROOT", tmp_path) debugger = get_debugger(debugger_name) assert ( - str(tmp_path / f"debugger-support[{debugger_name}]") + str(tmp_path / f"debugger[{debugger_name}]") == debugger.debugger_support_pkg ) diff --git a/tox.ini b/tox.ini index 1f4f0683e..9ca6c541e 100644 --- a/tox.ini +++ b/tox.ini @@ -68,7 +68,7 @@ commands = python -m coverage report --fail-under=100 [testenv:py{,310,311,312,313,314}-debugger] -changedir = debugger-support +changedir = debugger skip_install = True deps = build From ba8c99065edc1240ddf9db6705250c77b9d1875b Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Tue, 9 Sep 2025 00:53:06 +0200 Subject: [PATCH 106/131] actually rename the folder "debugger-support" to "debugger" --- .../build/lib/briefcase_debugger/common.py | 0 .../src/briefcase_debugger/__init__.py | 44 ------- .../src/briefcase_debugger/config.py | 20 --- .../src/briefcase_debugger/debugpy.py | 116 ------------------ .../src/briefcase_debugger/pdb.py | 33 ----- {debugger-support => debugger}/LICENSE | 0 {debugger-support => debugger}/README.md | 0 {debugger-support => debugger}/pyproject.toml | 0 {debugger-support => debugger}/setup.py | 0 .../src}/briefcase_debugger/__init__.py | 0 .../src}/briefcase_debugger/config.py | 0 .../src}/briefcase_debugger/debugpy.py | 0 .../src}/briefcase_debugger/pdb.py | 0 .../tests/test_base.py | 0 .../tests/test_debugpy.py | 0 .../tests/test_debugpy_path_mappings.py | 0 .../tests/test_pdb.py | 0 17 files changed, 213 deletions(-) delete mode 100644 debugger-support/build/lib/briefcase_debugger/common.py delete mode 100644 debugger-support/src/briefcase_debugger/__init__.py delete mode 100644 debugger-support/src/briefcase_debugger/config.py delete mode 100644 debugger-support/src/briefcase_debugger/debugpy.py delete mode 100644 debugger-support/src/briefcase_debugger/pdb.py rename {debugger-support => debugger}/LICENSE (100%) rename {debugger-support => debugger}/README.md (100%) rename {debugger-support => debugger}/pyproject.toml (100%) rename {debugger-support => debugger}/setup.py (100%) rename {debugger-support/build/lib => debugger/src}/briefcase_debugger/__init__.py (100%) rename {debugger-support/build/lib => debugger/src}/briefcase_debugger/config.py (100%) rename {debugger-support/build/lib => debugger/src}/briefcase_debugger/debugpy.py (100%) rename {debugger-support/build/lib => debugger/src}/briefcase_debugger/pdb.py (100%) rename {debugger-support => debugger}/tests/test_base.py (100%) rename {debugger-support => debugger}/tests/test_debugpy.py (100%) rename {debugger-support => debugger}/tests/test_debugpy_path_mappings.py (100%) rename {debugger-support => debugger}/tests/test_pdb.py (100%) diff --git a/debugger-support/build/lib/briefcase_debugger/common.py b/debugger-support/build/lib/briefcase_debugger/common.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/debugger-support/src/briefcase_debugger/__init__.py b/debugger-support/src/briefcase_debugger/__init__.py deleted file mode 100644 index d0117a29b..000000000 --- a/debugger-support/src/briefcase_debugger/__init__.py +++ /dev/null @@ -1,44 +0,0 @@ -import json -import os -import sys -import traceback - - -def start_remote_debugger(): - try: - # check verbose output - verbose = os.environ.get("BRIEFCASE_DEBUG", "0") == "1" - - # reading config - config_str = os.environ.get("BRIEFCASE_DEBUGGER", None) - - # skip debugger if no config is set - if config_str is None: - if verbose: - print( - "No 'BRIEFCASE_DEBUGGER' environment variable found. Debugger not starting." - ) - return # If BRIEFCASE_DEBUGGER is not set, this packages does nothing... - - if verbose: - print(f"'BRIEFCASE_DEBUGGER'={config_str}") - - # Parsing config json - config = json.loads(config_str) - - # start debugger - print("Starting remote debugger...") - if config["debugger"] == "debugpy": - from briefcase_debugger.debugpy import start_debugpy - - start_debugpy(config, verbose) - elif config["debugger"] == "pdb": - from briefcase_debugger.pdb import start_pdb - - start_pdb(config, verbose) - else: - raise ValueError(f"Unknown debugger '{config['debugger']}'") - except Exception: - # Show exception and stop the whole application when an error occurs - print(traceback.format_exc()) - sys.exit(-1) diff --git a/debugger-support/src/briefcase_debugger/config.py b/debugger-support/src/briefcase_debugger/config.py deleted file mode 100644 index 3d250b383..000000000 --- a/debugger-support/src/briefcase_debugger/config.py +++ /dev/null @@ -1,20 +0,0 @@ -from typing import TypedDict - - -class AppPathMappings(TypedDict): - device_sys_path_regex: str - device_subfolders: list[str] - host_folders: list[str] - - -class AppPackagesPathMappings(TypedDict): - sys_path_regex: str - host_folder: str - - -class DebuggerConfig(TypedDict): - debugger: str - host: str - port: int - app_path_mappings: AppPathMappings | None - app_packages_path_mappings: AppPackagesPathMappings | None diff --git a/debugger-support/src/briefcase_debugger/debugpy.py b/debugger-support/src/briefcase_debugger/debugpy.py deleted file mode 100644 index 4570e2984..000000000 --- a/debugger-support/src/briefcase_debugger/debugpy.py +++ /dev/null @@ -1,116 +0,0 @@ -import os -import re -import sys -from pathlib import Path - -import debugpy - -from briefcase_debugger.config import DebuggerConfig - - -def find_first_matching_path(regex: str) -> str: - """Returns the first element of sys.paths that matches regex, otherwise None.""" - for path in sys.path: - if re.search(regex, path): - return path - raise ValueError(f"No sys.path entry matches regex '{regex}'") - - -def load_path_mappings(config: DebuggerConfig, verbose: bool) -> list[tuple[str, str]]: - app_path_mappings = config.get("app_path_mappings", None) - app_packages_path_mappings = config.get("app_packages_path_mappings", None) - - mappings_list = [] - if app_path_mappings: - device_app_folder = find_first_matching_path( - app_path_mappings["device_sys_path_regex"] - ) - for app_subfolder_device, app_subfolder_host in zip( - app_path_mappings["device_subfolders"], - app_path_mappings["host_folders"], - strict=False, - ): - mappings_list.append( - ( - app_subfolder_host, - str(Path(device_app_folder) / app_subfolder_device), - ) - ) - if app_packages_path_mappings: - device_app_packages_folder = find_first_matching_path( - app_packages_path_mappings["sys_path_regex"] - ) - mappings_list.append( - ( - app_packages_path_mappings["host_folder"], - str(Path(device_app_packages_folder)), - ) - ) - - if verbose: - print("Extracted path mappings:") - for idx, mapping in enumerate(mappings_list): - print(f"[{idx}] host = {mapping[0]}") - print(f"[{idx}] device = {mapping[1]}") - - return mappings_list - - -def start_debugpy(config: DebuggerConfig, verbose: bool): - host = config["host"] - port = config["port"] - path_mappings = load_path_mappings(config, verbose) - - # There is a bug in debugpy that has to be handled until there is a new - # debugpy release, see https://github.com/microsoft/debugpy/issues/1943 - if not hasattr(os, "__file__"): - if verbose: - print("'os.__file__' not available. Patching it...") - os.__file__ = "" - - # Starting remote debugger... - print(f"Starting debugpy in server mode at {host}:{port}...") - debugpy.listen((host, port), in_process_debug_adapter=True) - - if verbose: - # pydevd is dynamically loaded and only available after debugpy is started - import pydevd - - pydevd.DebugInfoHolder.DEBUG_TRACE_LEVEL = 3 - - if len(path_mappings) > 0: - if verbose: - print("Adding path mappings...") - - # pydevd is dynamically loaded and only available after a debugger has connected - import pydevd_file_utils - - pydevd_file_utils.setup_client_server_paths(path_mappings) - - print("The debugpy server started. Waiting for debugger to attach...") - print( - f""" -To connect to debugpy using VSCode add the following configuration to '.vscode/launch.json': -{{ - "version": "0.2.0", - "configurations": [ - {{ - "name": "Briefcase: Attach (Connect)", - "type": "debugpy", - "request": "attach", - "connect": {{ - "host": "{host}", - "port": {port} - }}, - "justMyCode": false - }} - ] -}} - -For more information see: https://briefcase.readthedocs.io/en/stable/how-to/debugging/vscode.html#bundled-app -""" - ) - debugpy.wait_for_client() - - print("Debugger attached.") - print("-" * 75) diff --git a/debugger-support/src/briefcase_debugger/pdb.py b/debugger-support/src/briefcase_debugger/pdb.py deleted file mode 100644 index 86b8747f8..000000000 --- a/debugger-support/src/briefcase_debugger/pdb.py +++ /dev/null @@ -1,33 +0,0 @@ -import sys - -from remote_pdb import RemotePdb - -from briefcase_debugger.config import DebuggerConfig - - -def start_pdb(config: DebuggerConfig, verbose: bool): - """Start remote PDB server.""" - # Parsing host/port - host = config["host"] - port = config["port"] - - print( - f""" -Remote PDB server opened at {host}:{port}. -Waiting for debugger to attach... -To connect to remote PDB use eg.: - - telnet {host} {port} (Windows) - - rlwrap socat - tcp:{host}:{port} (Linux, macOS) - -For more information see: https://briefcase.readthedocs.io/en/stable/how-to/debugging/console.html#bundled-app -""" - ) - - # Create a RemotePdb instance - remote_pdb = RemotePdb(host, port, quiet=True) - - # Connect the remote PDB with the "breakpoint()" function - sys.breakpointhook = remote_pdb.set_trace - - print("Debugger client attached.") - print("-" * 75) diff --git a/debugger-support/LICENSE b/debugger/LICENSE similarity index 100% rename from debugger-support/LICENSE rename to debugger/LICENSE diff --git a/debugger-support/README.md b/debugger/README.md similarity index 100% rename from debugger-support/README.md rename to debugger/README.md diff --git a/debugger-support/pyproject.toml b/debugger/pyproject.toml similarity index 100% rename from debugger-support/pyproject.toml rename to debugger/pyproject.toml diff --git a/debugger-support/setup.py b/debugger/setup.py similarity index 100% rename from debugger-support/setup.py rename to debugger/setup.py diff --git a/debugger-support/build/lib/briefcase_debugger/__init__.py b/debugger/src/briefcase_debugger/__init__.py similarity index 100% rename from debugger-support/build/lib/briefcase_debugger/__init__.py rename to debugger/src/briefcase_debugger/__init__.py diff --git a/debugger-support/build/lib/briefcase_debugger/config.py b/debugger/src/briefcase_debugger/config.py similarity index 100% rename from debugger-support/build/lib/briefcase_debugger/config.py rename to debugger/src/briefcase_debugger/config.py diff --git a/debugger-support/build/lib/briefcase_debugger/debugpy.py b/debugger/src/briefcase_debugger/debugpy.py similarity index 100% rename from debugger-support/build/lib/briefcase_debugger/debugpy.py rename to debugger/src/briefcase_debugger/debugpy.py diff --git a/debugger-support/build/lib/briefcase_debugger/pdb.py b/debugger/src/briefcase_debugger/pdb.py similarity index 100% rename from debugger-support/build/lib/briefcase_debugger/pdb.py rename to debugger/src/briefcase_debugger/pdb.py diff --git a/debugger-support/tests/test_base.py b/debugger/tests/test_base.py similarity index 100% rename from debugger-support/tests/test_base.py rename to debugger/tests/test_base.py diff --git a/debugger-support/tests/test_debugpy.py b/debugger/tests/test_debugpy.py similarity index 100% rename from debugger-support/tests/test_debugpy.py rename to debugger/tests/test_debugpy.py diff --git a/debugger-support/tests/test_debugpy_path_mappings.py b/debugger/tests/test_debugpy_path_mappings.py similarity index 100% rename from debugger-support/tests/test_debugpy_path_mappings.py rename to debugger/tests/test_debugpy_path_mappings.py diff --git a/debugger-support/tests/test_pdb.py b/debugger/tests/test_pdb.py similarity index 100% rename from debugger-support/tests/test_pdb.py rename to debugger/tests/test_pdb.py From 5a560c929e287cb516d75a8d31c5fa1e77ad3383 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Tue, 16 Sep 2025 22:13:19 +0200 Subject: [PATCH 107/131] moved "debugger-host" and "debugger-port" arguments to "_add_debug_options" --- src/briefcase/commands/base.py | 18 +++++++++++++++++- src/briefcase/commands/run.py | 16 +--------------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/briefcase/commands/base.py b/src/briefcase/commands/base.py index 70738fc3c..cc4c87b0f 100644 --- a/src/briefcase/commands/base.py +++ b/src/briefcase/commands/base.py @@ -1009,7 +1009,7 @@ def _add_test_options(self, parser, context_label): help=f"{context_label} the app in test mode", ) - def _add_debug_options(self, parser, context_label): + def _add_debug_options(self, parser, context_label, run_cmd: bool = False): """Internal utility method for adding common debug-related options. :param parser: The parser to which options should be added. @@ -1031,6 +1031,22 @@ def _add_debug_options(self, parser, context_label): help=f"{context_label} the app with the specified debugger. One of {', '.join(choices_help)} (default: pdb)", ) + if run_cmd: + parser.add_argument( + "--debugger-host", + default="localhost", + help="The host on which to run the debug server (default: localhost)", + required=False, + ) + parser.add_argument( + "-dp", + "--debugger-port", + default=5678, + type=int, + help="The port on which to run the debug server (default: 5678)", + required=False, + ) + def add_options(self, parser): """Add any options that this command needs to parse from the command line. diff --git a/src/briefcase/commands/run.py b/src/briefcase/commands/run.py index bc9c740c7..8eeb3334d 100644 --- a/src/briefcase/commands/run.py +++ b/src/briefcase/commands/run.py @@ -226,21 +226,7 @@ def add_options(self, parser): self._add_test_options(parser, context_label="Run") if self.supports_debugger: - self._add_debug_options(parser, context_label="Run") - parser.add_argument( - "--debugger-host", - default="localhost", - help="The host on which to run the debug server (default: localhost)", - required=False, - ) - parser.add_argument( - "-dp", - "--debugger-port", - default=5678, - type=int, - help="The port on which to run the debug server (default: 5678)", - required=False, - ) + self._add_debug_options(parser, context_label="Run", run_cmd=True) def _debugger_app_path_mappings(self, app: AppConfig) -> AppPathMappings: """Get the path mappings for the app code. From 07b56e27c2e1f825e88440d55540e0ed0bbf7540 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Thu, 18 Sep 2025 23:27:34 +0200 Subject: [PATCH 108/131] moved "debugger_host" and "debugger_port" to AppConfig --- src/briefcase/commands/base.py | 25 ++++--- src/briefcase/commands/run.py | 29 ++------ src/briefcase/config.py | 2 + src/briefcase/platforms/macOS/__init__.py | 24 +------ src/briefcase/platforms/windows/__init__.py | 8 +-- .../create/test_generate_app_template.py | 2 + tests/commands/run/conftest.py | 1 + tests/commands/run/test_call.py | 67 +++++++------------ tests/platforms/macOS/app/test_run.py | 64 +++--------------- tests/platforms/macOS/xcode/test_run.py | 18 +---- tests/platforms/windows/app/test_run.py | 50 +++----------- .../windows/visualstudio/test_run.py | 27 ++------ 12 files changed, 77 insertions(+), 240 deletions(-) diff --git a/src/briefcase/commands/base.py b/src/briefcase/commands/base.py index cc4c87b0f..831001d92 100644 --- a/src/briefcase/commands/base.py +++ b/src/briefcase/commands/base.py @@ -692,6 +692,8 @@ def finalize( app: AppConfig | None = None, test_mode: bool = False, debugger: str | None = None, + debugger_host: str | None = None, + debugger_port: str | None = None, ): """Finalize Briefcase configuration. @@ -700,11 +702,16 @@ def finalize( 1. Ensure that the host has been verified 2. Ensure that the platform tools have been verified 3. Ensure that app configurations have been finalized. + 4. Ensure that the debugger is configured. App finalization will only occur once per invocation. :param app: If provided, the specific app configuration to finalize. By default, all apps will be finalized. + :param test_mode: Specify if the app is running in test mode + :param debugger: The debugger that should be used + :param debugger_host: The host to use for the debugger + :param debugger_port: The port to use for the debugger """ self.verify_host() self.verify_tools() @@ -712,7 +719,11 @@ def finalize( apps = self.apps.values() if app is None else [app] for app in apps: if hasattr(app, "__draft__"): - self.finalize_debugger(app, debugger) + if debugger and debugger != "": + app.debugger = get_debugger(debugger) + app.debugger_host = debugger_host + app.debugger_port = debugger_port + app.test_mode = test_mode self.finalize_app_config(app) delattr(app, "__draft__") @@ -738,18 +749,6 @@ def finalize( f"{app.app_name!r} defines 'external_package_executable_path', but not 'external_package_path'." ) - def finalize_debugger(self, app: AppConfig, debugger_name: str | None = None): - """Finalize the debugger configuration. - - This will ensure that the debugger is available and that the app configuration - is valid. - - :param app: The app configuration to finalize. - """ - if debugger_name and debugger_name != "": - debugger = get_debugger(debugger_name) - app.debugger = debugger - def verify_app(self, app: AppConfig): """Verify the app is compatible and the app tools are available. diff --git a/src/briefcase/commands/run.py b/src/briefcase/commands/run.py index 8eeb3334d..22e01e1f1 100644 --- a/src/briefcase/commands/run.py +++ b/src/briefcase/commands/run.py @@ -259,12 +259,7 @@ def _debugger_app_packages_path_mapping( # mapping is required. The paths are automatically found. return None - def debugger_config( - self, - app: AppConfig, - debugger_host: str, - debugger_port: int, - ) -> str: + def debugger_config(self, app: AppConfig) -> str: """Create the remote debugger configuration that should be saved as environment variable for this run. @@ -275,8 +270,8 @@ def debugger_config( app_packages_path_mappings = self._debugger_app_packages_path_mapping(app) config = DebuggerConfig( debugger=app.debugger.name, - host=debugger_host, - port=debugger_port, + host=app.debugger_host, + port=app.debugger_port, app_path_mappings=app_path_mappings, app_packages_path_mappings=app_packages_path_mappings, ) @@ -285,8 +280,6 @@ def debugger_config( def _prepare_app_kwargs( self, app: AppConfig, - debugger_host: str | None = None, - debugger_port: int | None = None, ): """Prepare the kwargs for running an app as a log stream. @@ -294,8 +287,6 @@ def _prepare_app_kwargs( it's been factored out. :param app: The app to be launched - :param debugger_host: The host on which to run the debug server - :param debugger_port: The port on which to run the debug server :returns: A dictionary of additional arguments to pass to the Popen """ args = {} @@ -306,10 +297,8 @@ def _prepare_app_kwargs( env["BRIEFCASE_DEBUG"] = "1" # If we're in remote debug mode, save the remote debugger config - if app.debugger and debugger_host and debugger_port: - env["BRIEFCASE_DEBUGGER"] = self.debugger_config( - app, debugger_host, debugger_port - ) + if app.debugger: + env["BRIEFCASE_DEBUGGER"] = self.debugger_config(app) if app.test_mode: # In test mode, set a BRIEFCASE_MAIN_MODULE environment variable @@ -330,16 +319,12 @@ def run_app( self, app: AppConfig, *, - debugger_host: str | None, - debugger_port: int | None, passthrough: list[str], **options, ) -> dict | None: """Start an application. :param app: The application to start - :param debugger_host: The host on which to run the debug server - :param debugger_port: The port on which to run the debug server :param passthrough: Any passthrough arguments """ @@ -378,7 +363,7 @@ def __call__( # Confirm host compatibility, that all required tools are available, # and that the app configuration is finalized. - self.finalize(app, test_mode, debugger) + self.finalize(app, test_mode, debugger, debugger_host, debugger_port) template_file = self.bundle_path(app) exec_file = self.binary_executable_path(app) @@ -417,8 +402,6 @@ def __call__( state = self.run_app( app, - debugger_host=debugger_host, - debugger_port=debugger_port, passthrough=[] if passthrough is None else passthrough, **full_options(state, options), ) diff --git a/src/briefcase/config.py b/src/briefcase/config.py index 98f72d91c..eb3cfdb52 100644 --- a/src/briefcase/config.py +++ b/src/briefcase/config.py @@ -399,6 +399,8 @@ def __init__( self.test_mode: bool = False self.debugger: BaseDebugger | None = None + self.debugger_host: str | None = None # only for run command + self.debugger_port: str | None = None # only for run command if not is_valid_app_name(self.app_name): raise BriefcaseConfigError( diff --git a/src/briefcase/platforms/macOS/__init__.py b/src/briefcase/platforms/macOS/__init__.py index 6067029b4..27a6e23cb 100644 --- a/src/briefcase/platforms/macOS/__init__.py +++ b/src/briefcase/platforms/macOS/__init__.py @@ -410,16 +410,12 @@ class macOSRunMixin: def run_app( self, app: AppConfig, - debugger_host: str | None, - debugger_port: int | None, passthrough: list[str], **kwargs, ): """Start the application. :param app: The config object for the app - :param debugger_host: The host to use for the debugger - :param debugger_port: The port to use for the debugger :param passthrough: The list of arguments to pass to the app """ # Console apps must operate in non-streaming mode so that console input can @@ -428,16 +424,12 @@ def run_app( if app.console_app: self.run_console_app( app, - debugger_host=debugger_host, - debugger_port=debugger_port, passthrough=passthrough, **kwargs, ) else: self.run_gui_app( app, - debugger_host=debugger_host, - debugger_port=debugger_port, passthrough=passthrough, **kwargs, ) @@ -445,8 +437,6 @@ def run_app( def run_console_app( self, app: AppConfig, - debugger_host: str | None, - debugger_port: int | None, passthrough: list[str], **kwargs, ): @@ -455,11 +445,7 @@ def run_console_app( :param app: The config object for the app :param passthrough: The list of arguments to pass to the app """ - sub_kwargs = self._prepare_app_kwargs( - app=app, - debugger_host=debugger_host, - debugger_port=debugger_port, - ) + sub_kwargs = self._prepare_app_kwargs(app=app) cmdline = [self.binary_path(app) / f"Contents/MacOS/{app.formal_name}"] cmdline.extend(passthrough) @@ -498,8 +484,6 @@ def run_console_app( def run_gui_app( self, app: AppConfig, - debugger_host: str | None, - debugger_port: int | None, passthrough: list[str], **kwargs, ): @@ -547,11 +531,7 @@ def run_gui_app( app_pid = None try: # Set up the log stream - sub_kwargs = self._prepare_app_kwargs( - app=app, - debugger_host=debugger_host, - debugger_port=debugger_port, - ) + sub_kwargs = self._prepare_app_kwargs(app=app) # Start the app in a way that lets us stream the logs self.tools.subprocess.run( diff --git a/src/briefcase/platforms/windows/__init__.py b/src/briefcase/platforms/windows/__init__.py index 7e53069f1..50cbb9551 100644 --- a/src/briefcase/platforms/windows/__init__.py +++ b/src/briefcase/platforms/windows/__init__.py @@ -141,8 +141,6 @@ class WindowsRunCommand(RunCommand): def run_app( self, app: AppConfig, - debugger_host: str | None, - debugger_port: int | None, passthrough: list[str], **kwargs, ): @@ -152,11 +150,7 @@ def run_app( :param passthrough: The list of arguments to pass to the app """ # Set up the log stream - kwargs = self._prepare_app_kwargs( - app=app, - debugger_host=debugger_host, - debugger_port=debugger_port, - ) + kwargs = self._prepare_app_kwargs(app=app) # Console apps must operate in non-streaming mode so that console input can # be handled correctly. However, if we're in test mode, we *must* stream so diff --git a/tests/commands/create/test_generate_app_template.py b/tests/commands/create/test_generate_app_template.py index 49292b65b..563acbb0c 100644 --- a/tests/commands/create/test_generate_app_template.py +++ b/tests/commands/create/test_generate_app_template.py @@ -32,6 +32,8 @@ def full_context(): "test_sources": None, "test_requires": None, "debugger": None, + "debugger_host": None, + "debugger_port": None, "url": "https://example.com", "author": "First Last", "author_email": "first@example.com", diff --git a/tests/commands/run/conftest.py b/tests/commands/run/conftest.py index 371136e9c..423e0b756 100644 --- a/tests/commands/run/conftest.py +++ b/tests/commands/run/conftest.py @@ -57,6 +57,7 @@ def run_app(self, app, **kwargs): app.app_name, app.test_mode, app.debugger is not None, + (app.debugger_host, app.debugger_port), kwargs.copy(), ) ) diff --git a/tests/commands/run/test_call.py b/tests/commands/run/test_call.py index 7f8ccfd8f..6c160f97b 100644 --- a/tests/commands/run/test_call.py +++ b/tests/commands/run/test_call.py @@ -34,7 +34,8 @@ def test_no_args_one_app(run_command, first_app): "first", False, False, - {"debugger_host": None, "debugger_port": None, "passthrough": []}, + (None, None), + {"passthrough": []}, ), ] @@ -71,11 +72,8 @@ def test_no_args_one_app_with_passthrough(run_command, first_app): "first", False, False, - { - "debugger_host": None, - "debugger_port": None, - "passthrough": ["foo", "--bar"], - }, + (None, None), + {"passthrough": ["foo", "--bar"]}, ), ] @@ -130,7 +128,8 @@ def test_with_arg_one_app(run_command, first_app): "first", False, False, - {"debugger_host": None, "debugger_port": None, "passthrough": []}, + (None, None), + {"passthrough": []}, ), ] @@ -167,7 +166,8 @@ def test_with_arg_two_apps(run_command, first_app, second_app): "second", False, False, - {"debugger_host": None, "debugger_port": None, "passthrough": []}, + (None, None), + {"passthrough": []}, ), ] @@ -239,10 +239,9 @@ def test_create_app_before_start(run_command, first_app_config): "first", False, False, + (None, None), { "build_state": "first", - "debugger_host": None, - "debugger_port": None, "passthrough": [], }, ), @@ -295,10 +294,9 @@ def test_build_app_before_start(run_command, first_app_unbuilt): "first", False, False, + (None, None), { "build_state": "first", - "debugger_host": None, - "debugger_port": None, "passthrough": [], }, ), @@ -351,10 +349,9 @@ def test_update_app(run_command, first_app): "first", False, False, + (None, None), { "build_state": "first", - "debugger_host": None, - "debugger_port": None, "passthrough": [], }, ), @@ -407,10 +404,9 @@ def test_update_app_requirements(run_command, first_app): "first", False, False, + (None, None), { "build_state": "first", - "debugger_host": None, - "debugger_port": None, "passthrough": [], }, ), @@ -463,10 +459,9 @@ def test_update_app_resources(run_command, first_app): "first", False, False, + (None, None), { "build_state": "first", - "debugger_host": None, - "debugger_port": None, "passthrough": [], }, ), @@ -519,10 +514,9 @@ def test_update_app_support(run_command, first_app): "first", False, False, + (None, None), { "build_state": "first", - "debugger_host": None, - "debugger_port": None, "passthrough": [], }, ), @@ -575,10 +569,9 @@ def test_update_app_stub(run_command, first_app): "first", False, False, + (None, None), { "build_state": "first", - "debugger_host": None, - "debugger_port": None, "passthrough": [], }, ), @@ -632,10 +625,9 @@ def test_update_unbuilt_app(run_command, first_app_unbuilt): "first", False, False, + (None, None), { "build_state": "first", - "debugger_host": None, - "debugger_port": None, "passthrough": [], }, ), @@ -689,10 +681,9 @@ def test_update_non_existent(run_command, first_app_config): "first", False, False, + (None, None), { "build_state": "first", - "debugger_host": None, - "debugger_port": None, "passthrough": [], }, ), @@ -745,10 +736,9 @@ def test_test_mode_existing_app(run_command, first_app): "first", True, False, + (None, None), { "build_state": "first", - "debugger_host": None, - "debugger_port": None, "passthrough": [], }, ), @@ -801,10 +791,9 @@ def test_test_mode_existing_app_with_passthrough(run_command, first_app): "first", True, False, + (None, None), { "build_state": "first", - "debugger_host": None, - "debugger_port": None, "passthrough": ["foo", "--bar"], }, ), @@ -843,7 +832,8 @@ def test_test_mode_existing_app_no_update(run_command, first_app): "first", True, False, - {"debugger_host": None, "debugger_port": None, "passthrough": []}, + (None, None), + {"passthrough": []}, ), ] @@ -894,10 +884,9 @@ def test_test_mode_existing_app_update_requirements(run_command, first_app): "first", True, False, + (None, None), { "build_state": "first", - "debugger_host": None, - "debugger_port": None, "passthrough": [], }, ), @@ -950,10 +939,9 @@ def test_test_mode_existing_app_update_resources(run_command, first_app): "first", True, False, + (None, None), { "build_state": "first", - "debugger_host": None, - "debugger_port": None, "passthrough": [], }, ), @@ -1006,10 +994,9 @@ def test_test_mode_update_existing_app(run_command, first_app): "first", True, False, + (None, None), { "build_state": "first", - "debugger_host": None, - "debugger_port": None, "passthrough": [], }, ), @@ -1062,10 +1049,9 @@ def test_test_mode_non_existent(run_command, first_app_config): "first", True, False, + (None, None), { "build_state": "first", - "debugger_host": None, - "debugger_port": None, "passthrough": [], }, ), @@ -1120,10 +1106,9 @@ def test_debug(run_command, first_app_config): "first", False, True, + ("localhost", 5678), { "build_state": "first", - "debugger_host": "localhost", - "debugger_port": 5678, "passthrough": [], }, ), diff --git a/tests/platforms/macOS/app/test_run.py b/tests/platforms/macOS/app/test_run.py index f3886d47e..bdd9d407d 100644 --- a/tests/platforms/macOS/app/test_run.py +++ b/tests/platforms/macOS/app/test_run.py @@ -45,12 +45,7 @@ def test_run_gui_app(run_command, first_app_config, sleep_zero, tmp_path, monkey "briefcase.platforms.macOS.get_process_id_by_command", lambda *a, **kw: 100 ) - run_command.run_app( - first_app_config, - debugger_host=None, - debugger_port=None, - passthrough=[], - ) + run_command.run_app(first_app_config, passthrough=[]) # Calls were made to start the app and to start a log stream. bin_path = run_command.binary_path(first_app_config) @@ -112,8 +107,6 @@ def test_run_gui_app_with_passthrough( # Run the app with args run_command.run_app( first_app_config, - debugger_host=None, - debugger_port=None, passthrough=["foo", "--bar"], ) @@ -165,12 +158,7 @@ def test_run_gui_app_failed(run_command, first_app_config, sleep_zero, tmp_path) ) with pytest.raises(BriefcaseCommandError): - run_command.run_app( - first_app_config, - debugger_host=None, - debugger_port=None, - passthrough=[], - ) + run_command.run_app(first_app_config, passthrough=[]) # Calls were made to start the app and to start a log stream. bin_path = run_command.binary_path(first_app_config) @@ -217,12 +205,7 @@ def test_run_gui_app_find_pid_failed( ) with pytest.raises(BriefcaseCommandError) as exc_info: - run_command.run_app( - first_app_config, - debugger_host=None, - debugger_port=None, - passthrough=[], - ) + run_command.run_app(first_app_config, passthrough=[]) # Calls were made to start the app and to start a log stream. bin_path = run_command.binary_path(first_app_config) @@ -275,12 +258,7 @@ def test_run_gui_app_test_mode( "briefcase.platforms.macOS.get_process_id_by_command", lambda *a, **kw: 100 ) - run_command.run_app( - first_app_config, - debugger_host=None, - debugger_port=None, - passthrough=[], - ) + run_command.run_app(first_app_config, passthrough=[]) # Calls were made to start the app and to start a log stream. bin_path = run_command.binary_path(first_app_config) @@ -335,18 +313,15 @@ def test_run_gui_app_debugger( run_command.tools.subprocess.Popen.return_value = log_stream_process first_app_config.debugger = dummy_debugger + first_app_config.debugger_host = "somehost" + first_app_config.debugger_port = 9999 # Monkeypatch the tools get the process ID monkeypatch.setattr( "briefcase.platforms.macOS.get_process_id_by_command", lambda *a, **kw: 100 ) - run_command.run_app( - first_app_config, - debugger_host="somehost", - debugger_port=9999, - passthrough=[], - ) + run_command.run_app(first_app_config, passthrough=[]) # Calls were made to start the app and to start a log stream. bin_path = run_command.binary_path(first_app_config) @@ -406,12 +381,7 @@ def test_run_console_app(run_command, first_app_config, tmp_path): # Set the app to be a console app first_app_config.console_app = True - run_command.run_app( - first_app_config, - debugger_host=None, - debugger_port=None, - passthrough=[], - ) + run_command.run_app(first_app_config, passthrough=[]) # Calls were made to start the app and to start a log stream. bin_path = run_command.binary_path(first_app_config) @@ -440,8 +410,6 @@ def test_run_console_app_with_passthrough( # Run the app with args run_command.run_app( first_app_config, - debugger_host=None, - debugger_port=None, passthrough=["foo", "--bar"], ) @@ -468,12 +436,7 @@ def test_run_console_app_test_mode(run_command, first_app_config, sleep_zero, tm app_process = mock.MagicMock(spec_set=subprocess.Popen) run_command.tools.subprocess.Popen.return_value = app_process - run_command.run_app( - first_app_config, - debugger_host=None, - debugger_port=None, - passthrough=[], - ) + run_command.run_app(first_app_config, passthrough=[]) # Calls were made to start the app and to start a log stream. bin_path = run_command.binary_path(first_app_config) @@ -512,8 +475,6 @@ def test_run_console_app_test_mode_with_passthrough( run_command.run_app( first_app_config, - debugger_host=None, - debugger_port=None, passthrough=["foo", "--bar"], ) @@ -548,12 +509,7 @@ def test_run_console_app_failed(run_command, first_app_config, sleep_zero, tmp_p # Although the command raises an error, this could be because the script itself # raised an error. - run_command.run_app( - first_app_config, - debugger_host=None, - debugger_port=None, - passthrough=[], - ) + run_command.run_app(first_app_config, passthrough=[]) # Calls were made to start the app and to start a log stream. bin_path = run_command.binary_path(first_app_config) diff --git a/tests/platforms/macOS/xcode/test_run.py b/tests/platforms/macOS/xcode/test_run.py index e732601f1..58084a878 100644 --- a/tests/platforms/macOS/xcode/test_run.py +++ b/tests/platforms/macOS/xcode/test_run.py @@ -45,12 +45,7 @@ def test_run_app(run_command, first_app_config, sleep_zero, tmp_path, monkeypatc "briefcase.platforms.macOS.get_process_id_by_command", lambda *a, **kw: 100 ) - run_command.run_app( - first_app_config, - debugger_host=None, - debugger_port=None, - passthrough=[], - ) + run_command.run_app(first_app_config, passthrough=[]) # Calls were made to start the app and to start a log stream. bin_path = run_command.binary_path(first_app_config) @@ -110,8 +105,6 @@ def test_run_app_with_passthrough( # Run the app with args run_command.run_app( first_app_config, - debugger_host=None, - debugger_port=None, passthrough=["foo", "--bar"], ) @@ -172,12 +165,7 @@ def test_run_app_test_mode( "briefcase.platforms.macOS.get_process_id_by_command", lambda *a, **kw: 100 ) - run_command.run_app( - first_app_config, - debugger_host=None, - debugger_port=None, - passthrough=[], - ) + run_command.run_app(first_app_config, passthrough=[]) # Calls were made to start the app and to start a log stream. bin_path = run_command.binary_path(first_app_config) @@ -240,8 +228,6 @@ def test_run_app_test_mode_with_passthrough( # Run app in test mode with args run_command.run_app( first_app_config, - debugger_host=None, - debugger_port=None, passthrough=["foo", "--bar"], ) diff --git a/tests/platforms/windows/app/test_run.py b/tests/platforms/windows/app/test_run.py index 9983dde12..aecb163dd 100644 --- a/tests/platforms/windows/app/test_run.py +++ b/tests/platforms/windows/app/test_run.py @@ -31,12 +31,7 @@ def test_run_gui_app(run_command, first_app_config, tmp_path): run_command.tools.subprocess.Popen.return_value = log_popen # Run the app - run_command.run_app( - first_app_config, - debugger_host=None, - debugger_port=None, - passthrough=[], - ) + run_command.run_app(first_app_config, passthrough=[]) # The process was started run_command.tools.subprocess.Popen.assert_called_with( @@ -67,8 +62,6 @@ def test_run_gui_app_with_passthrough(run_command, first_app_config, tmp_path): # Run the app with args run_command.run_app( first_app_config, - debugger_host=None, - debugger_port=None, passthrough=["foo", "--bar"], ) @@ -101,12 +94,7 @@ def test_run_gui_app_failed(run_command, first_app_config, tmp_path): run_command.tools.subprocess.Popen.side_effect = OSError with pytest.raises(OSError): - run_command.run_app( - first_app_config, - debugger_host=None, - debugger_port=None, - passthrough=[], - ) + run_command.run_app(first_app_config, passthrough=[]) # Popen was still invoked, though run_command.tools.subprocess.Popen.assert_called_with( @@ -131,12 +119,7 @@ def test_run_console_app(run_command, first_app_config, tmp_path): run_command.tools.subprocess.Popen.return_value = log_popen # Run the app - run_command.run_app( - first_app_config, - debugger_host=None, - debugger_port=None, - passthrough=[], - ) + run_command.run_app(first_app_config, passthrough=[]) # The process was started run_command.tools.subprocess.run.assert_called_with( @@ -160,8 +143,6 @@ def test_run_console_app_with_passthrough(run_command, first_app_config, tmp_pat # Run the app with args run_command.run_app( first_app_config, - debugger_host=None, - debugger_port=None, passthrough=["foo", "--bar"], ) @@ -190,12 +171,7 @@ def test_run_console_app_failed(run_command, first_app_config, tmp_path): run_command.tools.subprocess.run.side_effect = OSError with pytest.raises(OSError): - run_command.run_app( - first_app_config, - debugger_host=None, - debugger_port=None, - passthrough=[], - ) + run_command.run_app(first_app_config, passthrough=[]) # Popen was still invoked, though run_command.tools.subprocess.run.assert_called_with( @@ -222,12 +198,7 @@ def test_run_app_test_mode(run_command, first_app_config, is_console_app, tmp_pa run_command.tools.subprocess.Popen.return_value = log_popen # Run the app - run_command.run_app( - first_app_config, - debugger_host=None, - debugger_port=None, - passthrough=[], - ) + run_command.run_app(first_app_config, passthrough=[]) # The process was started exe_name = "first-app" if is_console_app else "First App" @@ -268,8 +239,6 @@ def test_run_app_test_mode_with_passthrough( # Run the app with args run_command.run_app( first_app_config, - debugger_host=None, - debugger_port=None, passthrough=["foo", "--bar"], ) @@ -304,14 +273,11 @@ def test_run_gui_app_debugger(run_command, first_app_config, tmp_path, dummy_deb run_command.tools.subprocess.Popen.return_value = log_popen first_app_config.debugger = dummy_debugger + first_app_config.debugger_host = "somehost" + first_app_config.debugger_port = 9999 # Run the app - run_command.run_app( - first_app_config, - debugger_host="somehost", - debugger_port=9999, - passthrough=[], - ) + run_command.run_app(first_app_config, passthrough=[]) # The process was started run_command.tools.subprocess.Popen.assert_called_with( diff --git a/tests/platforms/windows/visualstudio/test_run.py b/tests/platforms/windows/visualstudio/test_run.py index 004d384f5..02c888c16 100644 --- a/tests/platforms/windows/visualstudio/test_run.py +++ b/tests/platforms/windows/visualstudio/test_run.py @@ -34,12 +34,7 @@ def test_run_app(run_command, first_app_config, tmp_path): run_command.tools.subprocess.Popen.return_value = log_popen # Run the app - run_command.run_app( - first_app_config, - debugger_host=None, - debugger_port=None, - passthrough=[], - ) + run_command.run_app(first_app_config, passthrough=[]) # Popen was called run_command.tools.subprocess.Popen.assert_called_with( @@ -72,8 +67,6 @@ def test_run_app_with_args(run_command, first_app_config, tmp_path): # Run the app with args run_command.run_app( first_app_config, - debugger_host=None, - debugger_port=None, passthrough=["foo", "--bar"], ) @@ -109,12 +102,7 @@ def test_run_app_test_mode(run_command, first_app_config, tmp_path): run_command.tools.subprocess.Popen.return_value = log_popen # Run the app in test mode - run_command.run_app( - first_app_config, - debugger_host=None, - debugger_port=None, - passthrough=[], - ) + run_command.run_app(first_app_config, passthrough=[]) # Popen was called run_command.tools.subprocess.Popen.assert_called_with( @@ -149,8 +137,6 @@ def test_run_app_test_mode_with_args(run_command, first_app_config, tmp_path): # Run the app with args run_command.run_app( first_app_config, - debugger_host=None, - debugger_port=None, passthrough=["foo", "--bar"], ) @@ -181,18 +167,15 @@ def test_run_app_test_mode_with_args(run_command, first_app_config, tmp_path): def test_run_app_debugger(run_command, first_app_config, tmp_path, dummy_debugger): """A windows Visual Studio project app can be started in debug mode.""" first_app_config.debugger = dummy_debugger + first_app_config.debugger_host = "somehost" + first_app_config.debugger_port = 9999 # Set up the log streamer to return a known stream with a good returncode log_popen = mock.MagicMock() run_command.tools.subprocess.Popen.return_value = log_popen # Run the app in test mode - run_command.run_app( - first_app_config, - debugger_host="somehost", - debugger_port=9999, - passthrough=[], - ) + run_command.run_app(first_app_config, passthrough=[]) # Popen was called run_command.tools.subprocess.Popen.assert_called_with( From 886b3c691cbee83f7a6a4bf1869f66694af8e0ff Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Thu, 18 Sep 2025 23:29:11 +0200 Subject: [PATCH 109/131] fixed "debugger_port" type to int --- src/briefcase/commands/base.py | 2 +- src/briefcase/config.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/briefcase/commands/base.py b/src/briefcase/commands/base.py index 831001d92..de9fdf064 100644 --- a/src/briefcase/commands/base.py +++ b/src/briefcase/commands/base.py @@ -693,7 +693,7 @@ def finalize( test_mode: bool = False, debugger: str | None = None, debugger_host: str | None = None, - debugger_port: str | None = None, + debugger_port: int | None = None, ): """Finalize Briefcase configuration. diff --git a/src/briefcase/config.py b/src/briefcase/config.py index eb3cfdb52..403ccaa46 100644 --- a/src/briefcase/config.py +++ b/src/briefcase/config.py @@ -400,7 +400,7 @@ def __init__( self.debugger: BaseDebugger | None = None self.debugger_host: str | None = None # only for run command - self.debugger_port: str | None = None # only for run command + self.debugger_port: int | None = None # only for run command if not is_valid_app_name(self.app_name): raise BriefcaseConfigError( From c039165d63ae39cc81c7e456a2434f6dbcaf0098 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Fri, 19 Sep 2025 00:26:37 +0200 Subject: [PATCH 110/131] moved "debugger_config" to "BaseDebugger" --- src/briefcase/commands/run.py | 26 +++----------------------- src/briefcase/debuggers/base.py | 27 ++++++++++++++++++++++++++- tests/platforms/conftest.py | 5 ++++- 3 files changed, 33 insertions(+), 25 deletions(-) diff --git a/src/briefcase/commands/run.py b/src/briefcase/commands/run.py index 22e01e1f1..3a03f5eef 100644 --- a/src/briefcase/commands/run.py +++ b/src/briefcase/commands/run.py @@ -1,6 +1,5 @@ from __future__ import annotations -import json import re import subprocess from abc import abstractmethod @@ -11,7 +10,6 @@ from briefcase.debuggers.base import ( AppPackagesPathMappings, AppPathMappings, - DebuggerConfig, ) from briefcase.exceptions import BriefcaseCommandError, BriefcaseTestSuiteFailure from briefcase.integrations.subprocess import StopStreaming @@ -228,7 +226,7 @@ def add_options(self, parser): if self.supports_debugger: self._add_debug_options(parser, context_label="Run", run_cmd=True) - def _debugger_app_path_mappings(self, app: AppConfig) -> AppPathMappings: + def debugger_app_path_mappings(self, app: AppConfig) -> AppPathMappings: """Get the path mappings for the app code. :param app: The config object for the app @@ -246,7 +244,7 @@ def _debugger_app_path_mappings(self, app: AppConfig) -> AppPathMappings: host_folders=host_folders, ) - def _debugger_app_packages_path_mapping( + def debugger_app_packages_path_mapping( self, app: AppConfig, ) -> AppPackagesPathMappings: @@ -259,24 +257,6 @@ def _debugger_app_packages_path_mapping( # mapping is required. The paths are automatically found. return None - def debugger_config(self, app: AppConfig) -> str: - """Create the remote debugger configuration that should be saved as environment - variable for this run. - - :param app: The app to be debugged - :returns: The remote debugger configuration - """ - app_path_mappings = self._debugger_app_path_mappings(app) - app_packages_path_mappings = self._debugger_app_packages_path_mapping(app) - config = DebuggerConfig( - debugger=app.debugger.name, - host=app.debugger_host, - port=app.debugger_port, - app_path_mappings=app_path_mappings, - app_packages_path_mappings=app_packages_path_mappings, - ) - return json.dumps(config) - def _prepare_app_kwargs( self, app: AppConfig, @@ -298,7 +278,7 @@ def _prepare_app_kwargs( # If we're in remote debug mode, save the remote debugger config if app.debugger: - env["BRIEFCASE_DEBUGGER"] = self.debugger_config(app) + env["BRIEFCASE_DEBUGGER"] = app.debugger.get_env_config(self, app) if app.test_mode: # In test mode, set a BRIEFCASE_MAIN_MODULE environment variable diff --git a/src/briefcase/debuggers/base.py b/src/briefcase/debuggers/base.py index 0a3da7d87..073ab2035 100644 --- a/src/briefcase/debuggers/base.py +++ b/src/briefcase/debuggers/base.py @@ -5,10 +5,15 @@ from abc import ABC, abstractmethod from importlib import metadata from pathlib import Path -from typing import TypedDict +from typing import TYPE_CHECKING, TypedDict import briefcase +if TYPE_CHECKING: + # avoid circular imports + from briefcase.commands.run import RunCommand + from briefcase.config import AppConfig + def _is_editable_pep610(dist_name: str) -> bool: """Check if briefcase is installed as editable build. @@ -97,3 +102,23 @@ def connection_mode(self) -> DebuggerConnectionMode: @abstractmethod def debugger_support_pkg(self) -> str: """Get the name of the debugger support package.""" + + def get_env_config( + self, + cmd: RunCommand, + app: AppConfig, + ) -> str: + """Get the environment config to start the debugger. + + :param cmd: The command that starts the debugger + :param app: The app to be debugged + :returns: The remote debugger configuration + """ + config = DebuggerConfig( + debugger=app.debugger.name, + host=app.debugger_host, + port=app.debugger_port, + app_path_mappings=cmd.debugger_app_path_mappings(app), + app_packages_path_mappings=cmd.debugger_app_packages_path_mapping(app), + ) + return json.dumps(config) diff --git a/tests/platforms/conftest.py b/tests/platforms/conftest.py index 5fc7c1893..f5009ba8d 100644 --- a/tests/platforms/conftest.py +++ b/tests/platforms/conftest.py @@ -1,7 +1,10 @@ import pytest from briefcase.config import AppConfig -from briefcase.debuggers.base import BaseDebugger, DebuggerConnectionMode +from briefcase.debuggers.base import ( + BaseDebugger, + DebuggerConnectionMode, +) @pytest.fixture From 6c60d3c3237ed09d03c2e33f2db9d807d055aec3 Mon Sep 17 00:00:00 2001 From: timrid <6593626+timrid@users.noreply.github.com> Date: Fri, 19 Sep 2025 21:43:21 +0200 Subject: [PATCH 111/131] use `nc` instead of `rlwrap socat` and make hints platform aware. --- debugger/src/briefcase_debugger/config.py | 1 + debugger/src/briefcase_debugger/pdb.py | 23 ++++++++++++++++------- docs/how-to/debugging/console.rst | 19 ++----------------- src/briefcase/debuggers/base.py | 3 +++ 4 files changed, 22 insertions(+), 24 deletions(-) diff --git a/debugger/src/briefcase_debugger/config.py b/debugger/src/briefcase_debugger/config.py index 3d250b383..0518d102f 100644 --- a/debugger/src/briefcase_debugger/config.py +++ b/debugger/src/briefcase_debugger/config.py @@ -16,5 +16,6 @@ class DebuggerConfig(TypedDict): debugger: str host: str port: int + host_os: str app_path_mappings: AppPathMappings | None app_packages_path_mappings: AppPackagesPathMappings | None diff --git a/debugger/src/briefcase_debugger/pdb.py b/debugger/src/briefcase_debugger/pdb.py index 86b8747f8..7c27b33a8 100644 --- a/debugger/src/briefcase_debugger/pdb.py +++ b/debugger/src/briefcase_debugger/pdb.py @@ -11,17 +11,26 @@ def start_pdb(config: DebuggerConfig, verbose: bool): host = config["host"] port = config["port"] - print( - f""" + # Print help message + host_os = config["host_os"] + if host_os == "Windows": + cmds_hint = f" telnet {host} {port}" + elif host_os in ("Linux", "Darwin"): + cmds_hint = f" nc {host} {port}" + else: + cmds_hint = f"""\ + - telnet {host} {port} + - nc {host} {port} +""" + print(f""" Remote PDB server opened at {host}:{port}. Waiting for debugger to attach... -To connect to remote PDB use eg.: - - telnet {host} {port} (Windows) - - rlwrap socat - tcp:{host}:{port} (Linux, macOS) +You are using '{host_os}' as host OS. To connect to remote PDB use for example: + +{cmds_hint} For more information see: https://briefcase.readthedocs.io/en/stable/how-to/debugging/console.html#bundled-app -""" - ) +""") # Create a RemotePdb instance remote_pdb = RemotePdb(host, port, quiet=True) diff --git a/docs/how-to/debugging/console.rst b/docs/how-to/debugging/console.rst index 969fc88ad..2c5b96e62 100644 --- a/docs/how-to/debugging/console.rst +++ b/docs/how-to/debugging/console.rst @@ -61,31 +61,16 @@ to your bundled app. $ telnet localhost 5678 .. group-tab:: Linux - If not already done install ``rlwrap`` and ``socat`` .. code-block:: console - $ sudo apt install rlwrap socat - - Then you can start the connection via - - .. code-block:: console - - $ rlwrap socat - tcp:localhost:5678 + $ nc localhost 5678 .. group-tab:: macOS - If not already done install ``rlwrap`` and ``socat`` using `Homebrew `__ - - .. code-block:: console - - $ brew install rlwrap socat - - Then you can start the connection via - .. code-block:: console - $ rlwrap socat - tcp:localhost:5678 + $ nc localhost 5678 The app will start after the connection is established. diff --git a/src/briefcase/debuggers/base.py b/src/briefcase/debuggers/base.py index 073ab2035..b37f93294 100644 --- a/src/briefcase/debuggers/base.py +++ b/src/briefcase/debuggers/base.py @@ -2,6 +2,7 @@ import enum import json +import platform from abc import ABC, abstractmethod from importlib import metadata from pathlib import Path @@ -76,6 +77,7 @@ class DebuggerConfig(TypedDict): debugger: str host: str port: int + host_os: str app_path_mappings: AppPathMappings | None app_packages_path_mappings: AppPackagesPathMappings | None @@ -118,6 +120,7 @@ def get_env_config( debugger=app.debugger.name, host=app.debugger_host, port=app.debugger_port, + host_os=platform.system(), app_path_mappings=cmd.debugger_app_path_mappings(app), app_packages_path_mappings=cmd.debugger_app_packages_path_mapping(app), ) From e658d8560843a97e5301fc67c2ebd4b9e7726829 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Fri, 19 Sep 2025 22:16:23 +0200 Subject: [PATCH 112/131] fixed unit tests --- debugger/tests/test_base.py | 1 + debugger/tests/test_debugpy.py | 2 ++ debugger/tests/test_pdb.py | 14 +++++++++++++- tests/platforms/macOS/app/test_run.py | 2 ++ tests/platforms/windows/app/test_run.py | 2 ++ tests/platforms/windows/visualstudio/test_run.py | 2 ++ 6 files changed, 22 insertions(+), 1 deletion(-) diff --git a/debugger/tests/test_base.py b/debugger/tests/test_base.py index a0fa8606a..cf9f63adc 100644 --- a/debugger/tests/test_base.py +++ b/debugger/tests/test_base.py @@ -23,6 +23,7 @@ def test_unknown_debugger(monkeypatch, capsys): "debugger": "unknown", "host": "somehost", "port": 9999, + "host_os": "Windows", } ) monkeypatch.setattr(os, "environ", os_environ) diff --git a/debugger/tests/test_debugpy.py b/debugger/tests/test_debugpy.py index 6226fedcc..e675e3d32 100644 --- a/debugger/tests/test_debugpy.py +++ b/debugger/tests/test_debugpy.py @@ -61,6 +61,7 @@ def test_with_debugger( "debugger": "debugpy", "host": "somehost", "port": 9999, + "host_os": "SomeOS", "app_path_mappings": { "device_sys_path_regex": "app$", "device_subfolders": ["helloworld"], @@ -132,6 +133,7 @@ def test_os_file_bugfix( "debugger": "debugpy", "host": "somehost", "port": 9999, + "host_os": "SomeOS", } ) monkeypatch.setattr(os, "environ", os_environ) diff --git a/debugger/tests/test_pdb.py b/debugger/tests/test_pdb.py index 7b94d6033..68ffa8a89 100644 --- a/debugger/tests/test_pdb.py +++ b/debugger/tests/test_pdb.py @@ -38,7 +38,16 @@ def test_no_debugger_verbose(monkeypatch, capsys): @pytest.mark.parametrize("verbose", [True, False]) -def test_with_debugger(monkeypatch, capsys, verbose): +@pytest.mark.parametrize( + "host_os,expected_host_cmds", + [ + ("Windows", ["telnet somehost 9999"]), + ("Darwin", ["nc somehost 9999"]), + ("Linux", ["nc somehost 9999"]), + ("UnknownOS", ["nc somehost 9999", "telnet somehost 9999"]), + ], +) +def test_with_debugger(monkeypatch, host_os, expected_host_cmds, capsys, verbose): """Normal debug session.""" os_environ = {} os_environ["BRIEFCASE_DEBUG"] = "1" if verbose else "0" @@ -47,6 +56,7 @@ def test_with_debugger(monkeypatch, capsys, verbose): "debugger": "pdb", "host": "somehost", "port": 9999, + "host_os": host_os, } ) monkeypatch.setattr(os, "environ", os_environ) @@ -65,4 +75,6 @@ def test_with_debugger(monkeypatch, capsys, verbose): captured = capsys.readouterr() assert "Waiting for debugger to attach..." in captured.out + for cmd in expected_host_cmds: + assert cmd in captured.out assert captured.err == "" diff --git a/tests/platforms/macOS/app/test_run.py b/tests/platforms/macOS/app/test_run.py index bdd9d407d..872bd2406 100644 --- a/tests/platforms/macOS/app/test_run.py +++ b/tests/platforms/macOS/app/test_run.py @@ -1,4 +1,5 @@ import json +import platform import subprocess from signal import SIGTERM from unittest import mock @@ -351,6 +352,7 @@ def test_run_gui_app_debugger( "debugger": "dummy", "host": "somehost", "port": 9999, + "host_os": platform.system(), "app_path_mappings": { "device_sys_path_regex": "app$", "device_subfolders": ["first_app"], diff --git a/tests/platforms/windows/app/test_run.py b/tests/platforms/windows/app/test_run.py index aecb163dd..630279d72 100644 --- a/tests/platforms/windows/app/test_run.py +++ b/tests/platforms/windows/app/test_run.py @@ -1,4 +1,5 @@ import json +import platform import subprocess from unittest import mock @@ -293,6 +294,7 @@ def test_run_gui_app_debugger(run_command, first_app_config, tmp_path, dummy_deb "debugger": "dummy", "host": "somehost", "port": 9999, + "host_os": platform.system(), "app_path_mappings": { "device_sys_path_regex": "app$", "device_subfolders": ["first_app"], diff --git a/tests/platforms/windows/visualstudio/test_run.py b/tests/platforms/windows/visualstudio/test_run.py index 02c888c16..93d9a0165 100644 --- a/tests/platforms/windows/visualstudio/test_run.py +++ b/tests/platforms/windows/visualstudio/test_run.py @@ -2,6 +2,7 @@ # implementation. Do a surface-level verification here, but the app # tests provide the actual test coverage. import json +import platform import subprocess from unittest import mock @@ -194,6 +195,7 @@ def test_run_app_debugger(run_command, first_app_config, tmp_path, dummy_debugge "debugger": "dummy", "host": "somehost", "port": 9999, + "host_os": platform.system(), "app_path_mappings": { "device_sys_path_regex": "app$", "device_subfolders": ["first_app"], From 4c9c81b5d63b5d2b5365d3a74a0065b1daa8bc5b Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Fri, 19 Sep 2025 22:55:12 +0200 Subject: [PATCH 113/131] debugpy v1.8.17 fixed the os.__file__ handling. So this workaround is not necessary anymore. --- debugger/pyproject.toml | 2 +- debugger/src/briefcase_debugger/debugpy.py | 8 ------ debugger/tests/test_debugpy.py | 30 ++++++++-------------- 3 files changed, 11 insertions(+), 29 deletions(-) diff --git a/debugger/pyproject.toml b/debugger/pyproject.toml index 924cb700a..f5f8305b4 100644 --- a/debugger/pyproject.toml +++ b/debugger/pyproject.toml @@ -19,7 +19,7 @@ dynamic = ["version"] [project.optional-dependencies] pdb = ["remote-pdb>=2.1.0,<3.0.0"] -debugpy = ["debugpy>=1.8.14,<2.0.0"] +debugpy = ["debugpy>=1.8.17,<2.0.0"] [dependency-groups] dev = [ diff --git a/debugger/src/briefcase_debugger/debugpy.py b/debugger/src/briefcase_debugger/debugpy.py index 4570e2984..bea1f0547 100644 --- a/debugger/src/briefcase_debugger/debugpy.py +++ b/debugger/src/briefcase_debugger/debugpy.py @@ -1,4 +1,3 @@ -import os import re import sys from pathlib import Path @@ -61,13 +60,6 @@ def start_debugpy(config: DebuggerConfig, verbose: bool): port = config["port"] path_mappings = load_path_mappings(config, verbose) - # There is a bug in debugpy that has to be handled until there is a new - # debugpy release, see https://github.com/microsoft/debugpy/issues/1943 - if not hasattr(os, "__file__"): - if verbose: - print("'os.__file__' not available. Patching it...") - os.__file__ = "" - # Starting remote debugger... print(f"Starting debugpy in server mode at {host}:{port}...") debugpy.listen((host, port), in_process_debug_adapter=True) diff --git a/debugger/tests/test_debugpy.py b/debugger/tests/test_debugpy.py index e675e3d32..c9461d001 100644 --- a/debugger/tests/test_debugpy.py +++ b/debugger/tests/test_debugpy.py @@ -115,32 +115,22 @@ def test_with_debugger( assert fake_pydevd.DebugInfoHolder.DEBUG_TRACE_LEVEL == pydevd_trace_level -@pytest.mark.parametrize( - "verbose,some_verbose_output,pydevd_trace_level", - [ - (True, "'os.__file__' not available. Patching it...", 3), - (False, "", 0), - ], -) -def test_os_file_bugfix( - verbose, some_verbose_output, pydevd_trace_level, monkeypatch, capsys -): - """The os.__file__ bugfix has to be applied (see https://github.com/microsoft/debugpy/issues/1943).""" +def test_with_debugger_without_path_mappings(monkeypatch, capsys): + """Debug session without path mappings.""" os_environ = {} - os_environ["BRIEFCASE_DEBUG"] = "1" if verbose else "0" + os_environ["BRIEFCASE_DEBUG"] = "0" os_environ["BRIEFCASE_DEBUGGER"] = json.dumps( { "debugger": "debugpy", "host": "somehost", "port": 9999, "host_os": "SomeOS", + "app_path_mappings": None, + "app_packages_path_mappings": None, } ) monkeypatch.setattr(os, "environ", os_environ) - # Fake an environment in that "os.__file__" is not available - monkeypatch.delattr(os, "__file__", raising=False) - fake_debugpy_listen = MagicMock() monkeypatch.setattr(debugpy, "listen", fake_debugpy_listen) @@ -152,6 +142,9 @@ def test_os_file_bugfix( fake_pydevd = MagicMock() monkeypatch.setitem(sys.modules, "pydevd", fake_pydevd) fake_pydevd.DebugInfoHolder.DEBUG_TRACE_LEVEL = 0 + fake_pydevd_file_utils = MagicMock() + fake_pydevd_file_utils.setup_client_server_paths.return_value = None + monkeypatch.setitem(sys.modules, "pydevd_file_utils", fake_pydevd_file_utils) # start test function briefcase_debugger.start_remote_debugger() @@ -161,12 +154,9 @@ def test_os_file_bugfix( in_process_debug_adapter=True, ) - assert hasattr(os, "__file__") - assert os.__file__ == "" + fake_debugpy_wait_for_client.assert_called_once() + fake_pydevd_file_utils.setup_client_server_paths.assert_not_called() captured = capsys.readouterr() assert "Waiting for debugger to attach..." in captured.out assert captured.err == "" - - assert some_verbose_output in captured.out - assert fake_pydevd.DebugInfoHolder.DEBUG_TRACE_LEVEL == pydevd_trace_level From b60c0c51ba30203f400767e3ce1cfcb97b37cd23 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Fri, 19 Sep 2025 23:35:44 +0200 Subject: [PATCH 114/131] remove "PureWindowsPath" from tests --- debugger/tests/test_debugpy.py | 67 +++++++++++++++----- debugger/tests/test_debugpy_path_mappings.py | 2 +- 2 files changed, 51 insertions(+), 18 deletions(-) diff --git a/debugger/tests/test_debugpy.py b/debugger/tests/test_debugpy.py index c9461d001..4a36730ad 100644 --- a/debugger/tests/test_debugpy.py +++ b/debugger/tests/test_debugpy.py @@ -1,12 +1,12 @@ import json import os import sys -from pathlib import Path, PosixPath, PureWindowsPath from unittest.mock import MagicMock import briefcase_debugger import debugpy import pytest +from briefcase_debugger.config import AppPathMappings def test_no_env_vars(monkeypatch, capsys): @@ -39,20 +39,62 @@ def test_no_debugger_verbose(monkeypatch, capsys): assert captured.err == "" +@pytest.mark.parametrize( + "os_name,app_path_mappings,sys_path,expected_path_mappings", + [ + ( + "nt", + AppPathMappings( + device_sys_path_regex="app$", + device_subfolders=["helloworld"], + host_folders=["C:\\PROJECT_ROOT\\src\\helloworld"], + ), + ["C:\\PROJECT_ROOT\\build\\helloworld\\windows\\app\\src\\app"], + [ + ( + "C:\\PROJECT_ROOT\\src\\helloworld", + "C:\\PROJECT_ROOT\\build\\helloworld\\windows\\app\\src\\app\\helloworld", + ) + ], + ), + ( + "posix", + AppPathMappings( + device_sys_path_regex="app$", + device_subfolders=["helloworld"], + host_folders=["/PROJECT_ROOT/src/helloworld"], + ), + [ + "/PROJECT_ROOT/build/helloworld/macos/app/Hello World.app/Contents/Resources/app" + ], + ( + "/PROJECT_ROOT/src/helloworld", + "/PROJECT_ROOT/build/helloworld/macos/app/Hello World.app/Contents/Resources/app/helloworld", + ), + ), + ], +) @pytest.mark.parametrize( "verbose,some_verbose_output,pydevd_trace_level", [ - (True, "Extracted path mappings:\n[0] host = src/helloworld", 3), + (True, "Extracted path mappings:\n[0] host = ", 3), (False, "", 0), ], ) def test_with_debugger( - verbose, some_verbose_output, pydevd_trace_level, monkeypatch, capsys + os_name: str, + app_path_mappings: AppPathMappings, + sys_path: list[str], + expected_path_mappings: list[tuple[str, str]], + verbose: bool, + some_verbose_output: str, + pydevd_trace_level: int, + monkeypatch, + capsys, ): """Normal debug session.""" - # When running tests on Linux/macOS, we have to switch to WindowsPath. - if isinstance(Path(), PosixPath): - monkeypatch.setattr(briefcase_debugger.debugpy, "Path", PureWindowsPath) + if os.name != os_name: + pytest.skip(f"Test only runs on {os_name} systems") os_environ = {} os_environ["BRIEFCASE_DEBUG"] = "1" if verbose else "0" @@ -62,19 +104,12 @@ def test_with_debugger( "host": "somehost", "port": 9999, "host_os": "SomeOS", - "app_path_mappings": { - "device_sys_path_regex": "app$", - "device_subfolders": ["helloworld"], - "host_folders": ["src/helloworld"], - }, + "app_path_mappings": app_path_mappings, "app_packages_path_mappings": None, } ) monkeypatch.setattr(os, "environ", os_environ) - sys_path = [ - "build\\helloworld\\windows\\app\\src\\app", - ] monkeypatch.setattr(sys, "path", sys_path) fake_debugpy_listen = MagicMock() @@ -102,9 +137,7 @@ def test_with_debugger( fake_debugpy_wait_for_client.assert_called_once() fake_pydevd_file_utils.setup_client_server_paths.assert_called_once_with( - [ - ("src/helloworld", "build\\helloworld\\windows\\app\\src\\app\\helloworld"), - ] + expected_path_mappings ) captured = capsys.readouterr() diff --git a/debugger/tests/test_debugpy_path_mappings.py b/debugger/tests/test_debugpy_path_mappings.py index b4b4e27eb..8cbaa535c 100644 --- a/debugger/tests/test_debugpy_path_mappings.py +++ b/debugger/tests/test_debugpy_path_mappings.py @@ -211,7 +211,7 @@ def test_mappings( app_path_mappings: AppPathMappings, app_packages_path_mappings: AppPackagesPathMappings | None, sys_path: list[str], - expected_path_mappings: tuple[str, str], + expected_path_mappings: list[tuple[str, str]], monkeypatch, ): if os.name != os_name: From 1218a6a21172d7bb4a189b920deacdda638c55b5 Mon Sep 17 00:00:00 2001 From: Tim Rid <6593626+timrid@users.noreply.github.com> Date: Fri, 19 Sep 2025 23:41:36 +0200 Subject: [PATCH 115/131] fixed unit tests --- debugger/tests/test_debugpy.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/debugger/tests/test_debugpy.py b/debugger/tests/test_debugpy.py index 4a36730ad..c3cc545b4 100644 --- a/debugger/tests/test_debugpy.py +++ b/debugger/tests/test_debugpy.py @@ -67,10 +67,12 @@ def test_no_debugger_verbose(monkeypatch, capsys): [ "/PROJECT_ROOT/build/helloworld/macos/app/Hello World.app/Contents/Resources/app" ], - ( - "/PROJECT_ROOT/src/helloworld", - "/PROJECT_ROOT/build/helloworld/macos/app/Hello World.app/Contents/Resources/app/helloworld", - ), + [ + ( + "/PROJECT_ROOT/src/helloworld", + "/PROJECT_ROOT/build/helloworld/macos/app/Hello World.app/Contents/Resources/app/helloworld", + ) + ], ), ], ) From 9657c95b60a7055df8e7401a32b75eedbd745c09 Mon Sep 17 00:00:00 2001 From: timrid <6593626+timrid@users.noreply.github.com> Date: Thu, 23 Oct 2025 21:08:22 +0200 Subject: [PATCH 116/131] updated docs --- debugger/src/briefcase_debugger/debugpy.py | 2 +- debugger/tests/test_debugpy_path_mappings.py | 4 +- docs/how-to/debugging/console.rst | 76 ------------------- docs/how-to/debugging/index.rst | 2 +- docs/how-to/debugging/pdb.rst | 77 ++++++++++++++++++++ docs/how-to/debugging/vscode.rst | 60 +++++++++------ docs/reference/commands/build.rst | 15 ++-- docs/reference/commands/package.rst | 3 +- docs/reference/commands/run.rst | 24 +++--- 9 files changed, 143 insertions(+), 120 deletions(-) delete mode 100644 docs/how-to/debugging/console.rst create mode 100644 docs/how-to/debugging/pdb.rst diff --git a/debugger/src/briefcase_debugger/debugpy.py b/debugger/src/briefcase_debugger/debugpy.py index bea1f0547..2f6b8b6ab 100644 --- a/debugger/src/briefcase_debugger/debugpy.py +++ b/debugger/src/briefcase_debugger/debugpy.py @@ -82,7 +82,7 @@ def start_debugpy(config: DebuggerConfig, verbose: bool): print("The debugpy server started. Waiting for debugger to attach...") print( f""" -To connect to debugpy using VSCode add the following configuration to '.vscode/launch.json': +To connect to debugpy using VS Code add the following configuration to '.vscode/launch.json': {{ "version": "0.2.0", "configurations": [ diff --git a/debugger/tests/test_debugpy_path_mappings.py b/debugger/tests/test_debugpy_path_mappings.py index 8cbaa535c..57035a544 100644 --- a/debugger/tests/test_debugpy_path_mappings.py +++ b/debugger/tests/test_debugpy_path_mappings.py @@ -140,7 +140,7 @@ def test_mappings_none(monkeypatch): ], id="ios", ), - # Android (with VSCode running on Windows) + # Android (with VS Code running on Windows) pytest.param( "posix", AppPathMappings( @@ -172,7 +172,7 @@ def test_mappings_none(monkeypatch): ], id="android-on-windows-host", ), - # Android (with VSCode running on POSIX system) + # Android (with VS Code running on POSIX system) pytest.param( "posix", AppPathMappings( diff --git a/docs/how-to/debugging/console.rst b/docs/how-to/debugging/console.rst deleted file mode 100644 index 2c5b96e62..000000000 --- a/docs/how-to/debugging/console.rst +++ /dev/null @@ -1,76 +0,0 @@ -================= -Debug via Console -================= - -Debugging an app on the console is normally done via `PDB `_. -It is possible to debug a briefcase app at different stages in your development -process. You can debug a development app via ``briefcase dev``, but also an bundled -app that is build via ``briefcase build`` and run via ``briefcase run``. - - -Development ------------ -Debugging an development app is quiet easy. Just add ``breakpoint()`` inside -your code and start the app via ``briefcase dev``. When the breakpoint got hit -the pdb console opens on your console and you can debug your app. - - -Bundled App ------------ -It is also possible to debug a bundled app. This is currently still an **experimental feature** that is only -supported on Windows and macOS. The full power of this feature will become available when iOS and -Android are supported, because that is the only way to debug an iOS or Android app. - -To debug a bundled app a piece of the debugger has to be embedded into your app. This is done via: - -.. code-block:: console - - $ briefcase build --debug pdb - -This will build your app in debug mode and add `remote-pdb `_ -together with a package that automatically starts ``remote-pdb`` at the -startup of your bundled app. - -Then it is time to run your app. You can do this via: - -.. code-block:: console - - $ briefcase run --debug pdb - -Running the app in debug mode will automatically start the ``remote-pdb`` debugger -and wait for incoming connections. By default it will listen on ``localhost`` -and port ``5678``. - -Then it is time to create a new console window on your host system and connect -to your bundled app. - -.. tabs:: - - .. group-tab:: Windows - To connect to your application, you need access to ``telnet``. That is not activated by default, but can be - activated by running the following command with admin rights - - .. code-block:: console - - $ dism /online /Enable-Feature /FeatureName:TelnetClient - - Then you can start the connection via - - .. code-block:: console - - $ telnet localhost 5678 - - .. group-tab:: Linux - - .. code-block:: console - - $ nc localhost 5678 - - .. group-tab:: macOS - - .. code-block:: console - - $ nc localhost 5678 - - -The app will start after the connection is established. diff --git a/docs/how-to/debugging/index.rst b/docs/how-to/debugging/index.rst index 8cb7ca207..16a64917b 100644 --- a/docs/how-to/debugging/index.rst +++ b/docs/how-to/debugging/index.rst @@ -8,5 +8,5 @@ following sections describe how you can debug your app with or without an IDE. .. toctree:: :maxdepth: 1 - console + pdb vscode diff --git a/docs/how-to/debugging/pdb.rst b/docs/how-to/debugging/pdb.rst new file mode 100644 index 000000000..ebc06490d --- /dev/null +++ b/docs/how-to/debugging/pdb.rst @@ -0,0 +1,77 @@ +============= +Debug via PDB +============= + +It is possible to debug a Briefcase app via `PDB `_ +at different stages in your development process. You can debug a development app +via ``briefcase dev``, but also a bundled app that is built via ``briefcase build`` +and run via ``briefcase run``. + + +Development +----------- +Debugging a development app is quite easy. Just add ``breakpoint()`` inside your +code and start the app via ``briefcase dev``. When the breakpoint is hit, the pdb +console opens in your terminal and you can debug your app. + + +Bundled App +----------- +It is also possible to debug a bundled app. This is currently still an +**experimental feature** that is only supported on Windows and macOS. + +To debug a bundled app, a piece of the debugger has to be embedded into your app. +This is done via: + +.. code-block:: console + + $ briefcase build --debug pdb + +This will build your app in debug mode and add `remote-pdb `_ +together with a package that automatically starts ``remote-pdb`` on startup of +your bundled app. + +Then it is time to run your app. You can do this via: + +.. code-block:: console + + $ briefcase run --debug pdb + +Running the app in debug mode will automatically start the ``remote-pdb`` debugger +and wait for incoming connections. By default, it will listen on ``localhost`` and +port ``5678``. + +In a separate terminal on your host system, connect to your bundled app: + +.. tabs:: + + .. group-tab:: Windows + To connect to your application, you need access to ``telnet``. That is not activated by default, but can be + activated by running the following command with admin rights + + .. code-block:: console + + $ dism /online /Enable-Feature /FeatureName:TelnetClient + + Then you can start the connection via + + .. code-block:: console + + $ telnet localhost 5678 + + .. group-tab:: Linux + + .. code-block:: console + + $ nc localhost 5678 + + .. group-tab:: macOS + + .. code-block:: console + + $ nc localhost 5678 + + +The app will start after the connection is established. + +For more information, see :ref:`here `. diff --git a/docs/how-to/debugging/vscode.rst b/docs/how-to/debugging/vscode.rst index be9197e96..f2154c35e 100644 --- a/docs/how-to/debugging/vscode.rst +++ b/docs/how-to/debugging/vscode.rst @@ -1,16 +1,16 @@ -================ -Debug via VSCode -================ +================= +Debug via VS Code +================= Debugging is possible at different stages in your development process. It is -different to debug a development app via ``briefcase dev`` than an bundled app -that is build via ``briefcase build`` and run via ``briefcase run``. +different to debug a development app via ``briefcase dev`` than a bundled app +that is built via ``briefcase build`` and run via ``briefcase run``. Development ----------- During development on your host system you should use ``briefcase dev``. To -attach VSCode debugger you can simply create a configuration like this, -that runs ``briefcase dev`` for you and attaches a debugger. +attach the VS Code debugger you have to create a configuration in your +``.vscode/launch.json`` file like this: .. code-block:: JSON @@ -30,21 +30,28 @@ that runs ``briefcase dev`` for you and attaches a debugger. ] } +To start a debug session, open the debug view in VS Code using the sidebar, select the +"Briefcase: Dev" configuration and press ``Start Debugging (F5)``. That will run +``briefcase dev`` in a debug session. + +For more details about the VS Code configurations in the ``.vscode/launch.json`` file +see the `VS Code documentation `_. Bundled App ----------- -It is also possible to debug a bundled app. This is currently still an **experimental feature** that is only -supported on Windows and macOS. The full power of this feature will become available when iOS and -Android are supported, because that is the only way to debug an iOS or Android app. +It is also possible to debug a bundled app. This is currently still an +**experimental feature** that is only supported on Windows and macOS. -To debug a bundled app a piece of the debugger has to be embedded into your app. This is done via: +To debug a bundled app a piece of the debugger has to be embedded into your app. +This is done via: .. code-block:: console $ briefcase build --debug debugpy -This will build your app in debug mode and add `debugpy `_ together with a -package that automatically starts ``debugpy`` at the startup of your bundled app. +This will build your app in debug mode and add `debugpy `_ +together with a package that automatically starts ``debugpy`` at the startup of +your bundled app. Then it is time to run your app. You can do this via: @@ -54,7 +61,7 @@ Then it is time to run your app. You can do this via: Running the app in debug mode will automatically start the ``debugpy`` debugger and listen for incoming connections. By default it will listen on ``localhost`` -and port ``5678``. You can then connect your VSCode debugger to the app by +and port ``5678``. You can then connect your VS Code debugger to the app by creating a configuration like this in the ``.vscode/launch.json`` file: .. code-block:: JSON @@ -75,14 +82,21 @@ creating a configuration like this in the ``.vscode/launch.json`` file: ] } -The app will not start until you attach the debugger. Once you attached the -VSCode debugger you are ready to debug your app. You can set `breakpoints `_ -, use the `data inspection `_ -, use the `debug console REPL `_ -and all other debugging features of VSCode 🙂 +The app will not start until you attach the debugger. Once you attach the +VS Code debugger, you can set `breakpoints `_, +use the `data inspection `_, +use the `debug console REPL `_ +and all other debugging features of VS Code. + +But there are some restrictions that must be taken into account: -But there are some restrictions, that must be taken into account: +- Restart your application via the green circle is not working. You have to stop + the app manually and start it again via ``briefcase run --debug debugpy``. +- ``justMyCode`` has to be set to ``false``. When setting it to ``true``, or not + defining it at all, breakpoints are missed on some platforms (e.g., Windows). + The reason for this is currently unknown. +- ``pathMappings`` should not be set manually in the ``launch.json``. The path + mappings will be set by Briefcase programmatically and if setting it manually + too the manual setting will overwrite settings by Briefcase. -- Restart the debugger via the green circle is not working correctly. -- ``justMyCode`` has to be set to ``false``. An incorrect configuration can disrupt debugging support. -- ``pathMappings`` should not be set manually. This will be set by briefcase dynamically. An incorrect configuration can disrupt debugging support. +For more information see :ref:`here `. diff --git a/docs/reference/commands/build.rst b/docs/reference/commands/build.rst index a7746ccad..3caac4d86 100644 --- a/docs/reference/commands/build.rst +++ b/docs/reference/commands/build.rst @@ -116,18 +116,19 @@ your testing requirements are present in the test app. ``--debug `` ---------------------- -Build the app in debug mode in the bundled app environment and establish an -debugger connection via a socket. This installs the selected debugger in the -bundled app. +Build the app in debug mode in the bundled app environment and installs the +selected debugger in the bundled app. Currently the following debuggers are supported: -- ``pdb``: This is used for debugging via console (see :doc:`Debug via Console `) -- ``debugpy``: This is used for debugging via VSCode (see :doc:`Debug via VSCode `) +- ``pdb``: This is used for debugging via console (see :doc:`Debug via PDB `) +- ``debugpy``: This is used for debugging via VS Code (see :doc:`Debug via VS Code `) -If calling only ``--debug`` without selecting a debugger explicitly, ``pdb`` is used as default. +If calling only ``--debug`` without selecting a debugger explicitly, ``pdb`` is +used as default. -This is an **experimental** new feature, that is currently only support on Windows and macOS. +This is an **experimental** new feature, that is currently only supported on +Windows and macOS. This option may slow down the app a little bit. diff --git a/docs/reference/commands/package.rst b/docs/reference/commands/package.rst index 00410b042..d28032c4d 100644 --- a/docs/reference/commands/package.rst +++ b/docs/reference/commands/package.rst @@ -7,7 +7,8 @@ platform's default output format. This will produce an installable artefact. -You should not package an application that is build using ``build --test`` or ``build --debug ``. +You should not package an application that was built using ``build --test`` or +``build --debug ``. Usage ===== diff --git a/docs/reference/commands/run.rst b/docs/reference/commands/run.rst index 430a5ceb9..91122a08b 100644 --- a/docs/reference/commands/run.rst +++ b/docs/reference/commands/run.rst @@ -137,25 +137,31 @@ contains the most recent test code. To prevent this update and build, use the Prevent the automated update and build of app code that is performed when specifying by the ``--test`` option. +.. _run-debug: + ``--debug `` ---------------------- -Run the app in debug mode in the bundled app environment and establish an +Run the app in debug mode in the bundled app environment and establish a debugger connection via a socket. Currently the following debuggers are supported: -- ``pdb``: This is used for debugging via console (see :doc:`Debug via Console `) -- ``debugpy``: This is used for debugging via VSCode (see :doc:`Debug via VSCode `) +- ``pdb``: This is used for debugging via console (see :doc:`Debug via PDB `) +- ``debugpy``: This is used for debugging via VS Code (see :doc:`Debug via VS Code `) -For ``debugpy`` there is also a mapping of the source code from your bundled -app to your local copy of the apps source code in the ``build`` folder. This -is useful for devices like iOS and Android, where the running source code is -not available on the host system. +For ``debugpy`` Briefcase will automatically apply path mapping of the source code +from your bundled app in the ``build`` folder to your local source code defined +under ``sources`` in your ``pyproject.toml``. This would collide with an existing +``pathMappings`` setting in your VS Code ``launch.json`` file. Therefore, if you +are using ``debugpy``, do not set ``pathMappings`` manually in your VS Code +``launch.json``. -If calling only ``--debug`` without selecting a debugger explicitly, ``pdb`` is used as default. +If calling only ``--debug`` without selecting a debugger explicitly, ``pdb`` +is used as default. -This is an **experimental** new feature, that is currently only support on Windows and macOS. +This is an **experimental** new feature, that is currently only supported on +Windows and macOS. The selected debugger in ``run --debug `` has to match the selected debugger in ``build --debug ``. From 56623bd3c3263da65e2b9eef008414c467cbda87 Mon Sep 17 00:00:00 2001 From: timrid <6593626+timrid@users.noreply.github.com> Date: Thu, 23 Oct 2025 21:50:21 +0200 Subject: [PATCH 117/131] simplify help msg creation --- src/briefcase/commands/base.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/briefcase/commands/base.py b/src/briefcase/commands/base.py index de9fdf064..c39525fe4 100644 --- a/src/briefcase/commands/base.py +++ b/src/briefcase/commands/base.py @@ -1017,7 +1017,6 @@ def _add_debug_options(self, parser, context_label, run_cmd: bool = False): """ debuggers = get_debuggers() debugger_names = list(reversed(debuggers.keys())) - choices_help = [f"'{choice}'" for choice in debugger_names] parser.add_argument( "--debug", @@ -1027,7 +1026,7 @@ def _add_debug_options(self, parser, context_label, run_cmd: bool = False): const="pdb", choices=debugger_names, metavar="DEBUGGER", - help=f"{context_label} the app with the specified debugger. One of {', '.join(choices_help)} (default: pdb)", + help=f"{context_label} the app with the specified debugger. One of %(choices)s (default: %(const)s)", ) if run_cmd: From 1800212bc60ccf3f57298ff498c86e96ffcf50bf Mon Sep 17 00:00:00 2001 From: timrid <6593626+timrid@users.noreply.github.com> Date: Thu, 23 Oct 2025 21:53:57 +0200 Subject: [PATCH 118/131] refactored commands to variable and removed host os hint. --- debugger/src/briefcase_debugger/pdb.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/debugger/src/briefcase_debugger/pdb.py b/debugger/src/briefcase_debugger/pdb.py index 7c27b33a8..76fa11457 100644 --- a/debugger/src/briefcase_debugger/pdb.py +++ b/debugger/src/briefcase_debugger/pdb.py @@ -13,19 +13,21 @@ def start_pdb(config: DebuggerConfig, verbose: bool): # Print help message host_os = config["host_os"] + telnet_cmd = f"telnet {host} {port}" + nc_cmd = f"nc {host} {port}" if host_os == "Windows": - cmds_hint = f" telnet {host} {port}" + cmds_hint = f" {telnet_cmd}" elif host_os in ("Linux", "Darwin"): - cmds_hint = f" nc {host} {port}" + cmds_hint = f" {nc_cmd}" else: cmds_hint = f"""\ - - telnet {host} {port} - - nc {host} {port} + - {telnet_cmd} + - {nc_cmd} """ print(f""" Remote PDB server opened at {host}:{port}. Waiting for debugger to attach... -You are using '{host_os}' as host OS. To connect to remote PDB use for example: +To connect to remote PDB use for example: {cmds_hint} From ea66f50d3cf457e10c683dbf1f2846154e478c5f Mon Sep 17 00:00:00 2001 From: timrid <6593626+timrid@users.noreply.github.com> Date: Sat, 25 Oct 2025 15:38:34 +0200 Subject: [PATCH 119/131] use "artifact-basename" in ci.yml --- .github/workflows/ci.yml | 51 ++++++++++++++-------------------------- 1 file changed, 17 insertions(+), 34 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 47b8ed652..e3932d2d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,7 @@ on: push: branches: - main + - ci workflow_call: inputs: attest-package: @@ -11,9 +12,9 @@ on: default: "false" type: string outputs: - artifact-name: - description: "Name of the uploaded artifact; use for artifact retrieval." - value: ${{ jobs.package.outputs.artifact-name }} + artifact-basename: + description: "Base name of the uploaded artifacts; use for artifact retrieval." + value: ${{ jobs.package.outputs.artifact-basename }} # Cancel active CI runs for a PR before starting another run concurrency: @@ -42,33 +43,20 @@ jobs: id-token: write contents: read attestations: write - uses: beeware/.github/.github/workflows/python-package-create.yml@main + strategy: + matrix: + subdir: + - "." + - "debugger" + - "automation" + uses: timrid/beeware-.github/.github/workflows/python-package-create.yml@no-differentiator-when-subdirectory-is-root with: + build-subdirectory: ${{ matrix.subdir }} attest: ${{ inputs.attest-package }} - package-debugger: - name: Package Debugger - permissions: - id-token: write - contents: read - attestations: write - uses: beeware/.github/.github/workflows/python-package-create.yml@main - with: - build-subdirectory: "debugger" - - package-automation: - name: Package Automation - permissions: - id-token: write - contents: read - attestations: write - uses: beeware/.github/.github/workflows/python-package-create.yml@main - with: - build-subdirectory: "automation" - unit-tests: name: Unit tests - needs: [ pre-commit, towncrier, package, package-debugger ] + needs: [ pre-commit, towncrier, package ] runs-on: ${{ matrix.platform }} continue-on-error: ${{ matrix.experimental || false }} strategy: @@ -102,7 +90,8 @@ jobs: - name: Get Packages uses: actions/download-artifact@v5.0.0 with: - name: ${{ needs.package.outputs.artifact-name }} + pattern: ${{ format('{0}*', needs.package.outputs.artifact-basename) }} + merge-multiple: true path: dist - name: Install Tox @@ -134,12 +123,6 @@ jobs: # coverage reporting must use the same Python version used to produce coverage run: tox -qe coverage$(tr -dc "0-9" <<< "${{ matrix.python-version }}") - - name: Get Debugger Package - uses: actions/download-artifact@v4.3.0 - with: - name: ${{ needs.package-debugger.outputs.artifact-name }} - path: dist - - name: Test Debugger run: | tox -e py-debugger --installpkg dist/briefcase_debugger-*.whl @@ -194,7 +177,7 @@ jobs: verify-projects: name: Verify project - needs: [ package, package-automation, unit-tests ] + needs: [ package, unit-tests ] uses: beeware/.github/.github/workflows/app-create-verify.yml@main with: runner-os: ${{ matrix.runner-os }} @@ -207,7 +190,7 @@ jobs: verify-apps: name: Build app - needs: [ package, package-automation, unit-tests ] + needs: [ package, unit-tests ] uses: beeware/.github/.github/workflows/app-build-verify.yml@main with: # Builds on Linux must use System Python; otherwise, fall back to version all GUI toolkits support From cfe33c91c2c9e7fd8a4077f37f3ee060b481dd29 Mon Sep 17 00:00:00 2001 From: timrid <6593626+timrid@users.noreply.github.com> Date: Sat, 25 Oct 2025 17:30:26 +0200 Subject: [PATCH 120/131] updated release.yml --- .github/workflows/release.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 87f132aee..5814ff516 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,10 +38,11 @@ jobs: with: python-version: "3.x" - - name: Get packages + - name: Get Packages uses: actions/download-artifact@v5.0.0 with: - name: ${{ needs.ci.outputs.artifact-name }} + pattern: ${{ format('{0}*', needs.package.outputs.artifact-basename) }} + merge-multiple: true path: dist - name: Purge non-release packages From 90f3f4c2ca6808d929de97035bb1c4c061ae7c66 Mon Sep 17 00:00:00 2001 From: timrid <6593626+timrid@users.noreply.github.com> Date: Sat, 25 Oct 2025 21:37:20 +0200 Subject: [PATCH 121/131] converted docs from .rst to .md --- docs/en/SUMMARY.md | 3 + docs/en/how-to/debugging/pdb.md | 65 ++++++++++++++++ docs/en/how-to/debugging/vscode.md | 77 +++++++++++++++++++ docs/en/reference/commands/build.md | 19 +++++ docs/en/reference/commands/package.md | 2 + docs/en/reference/commands/run.md | 25 +++++++ docs/how-to/debugging/index.rst | 12 --- docs/how-to/debugging/pdb.rst | 77 ------------------- docs/how-to/debugging/vscode.rst | 102 -------------------------- 9 files changed, 191 insertions(+), 191 deletions(-) create mode 100644 docs/en/how-to/debugging/pdb.md create mode 100644 docs/en/how-to/debugging/vscode.md delete mode 100644 docs/how-to/debugging/index.rst delete mode 100644 docs/how-to/debugging/pdb.rst delete mode 100644 docs/how-to/debugging/vscode.rst diff --git a/docs/en/SUMMARY.md b/docs/en/SUMMARY.md index be41dc29f..3f5136950 100644 --- a/docs/en/SUMMARY.md +++ b/docs/en/SUMMARY.md @@ -14,6 +14,9 @@ - [Packaging external apps](how-to/building/external-apps.md) - Testing apps - [Testing Linux Apps with Docker](how-to/testing/x11passthrough.md) + - Debugging apps + - [Debug via PDB](how-to/debugging/pdb.md) + - [Debug via VS Code](how-to/debugging/vscode.md) - Publishing your app - ./how-to/publishing/* - [Contributing to Briefcase](how-to/contribute/index.md) diff --git a/docs/en/how-to/debugging/pdb.md b/docs/en/how-to/debugging/pdb.md new file mode 100644 index 000000000..d347ef6f3 --- /dev/null +++ b/docs/en/how-to/debugging/pdb.md @@ -0,0 +1,65 @@ +# Debug via PDB { #debug-pdb } + +It is possible to debug a Briefcase app via [PDB](https://docs.python.org/3/library/pdb.html) at different stages in your development process. You can debug a development app via `briefcase dev`, but also a bundled app that is built via `briefcase build` and run via `briefcase run`. + +## Development + +Debugging a development app is quite easy. Just add `breakpoint()` inside your code and start the app via `briefcase dev`. When the breakpoint is hit, the pdb console opens in your terminal and you can debug your app. + +## Bundled App + +It is also possible to debug a bundled app. This is currently still an **experimental feature** that is only supported on Windows and macOS. + +To debug a bundled app, a piece of the debugger has to be embedded into your app. This is done via: + +```console +$ briefcase build --debug pdb +``` + +This will build your app in debug mode and add [`remote-pdb`](https://pypi.org/project/remote-pdb/) together with a package that automatically starts `remote-pdb` on startup of your bundled app. + +Then it is time to run your app. You can do this via: + +```console +$ briefcase run --debug pdb +``` + +Running the app in debug mode will automatically start the `remote-pdb` debugger and wait for incoming connections. By default, it will listen on `localhost` and port `5678`. + +In a separate terminal on your host system, connect to your bundled app: + +/// tab | macOS + +```console +$ nc localhost 5678 +``` + +/// + +/// tab | Linux + +```console +$ nc localhost 5678 +``` + +/// + +/// tab | Windows + +To connect to your application, you need access to `telnet`. That is not activated by default, but can be activated by running the following command with admin rights + +```console +$ dism /online /Enable-Feature /FeatureName:TelnetClient +``` + +Then you can start the connection via + +```console +$ telnet localhost 5678 +``` + +/// + +The app will start after the connection is established. + +For more information, see [here][run-debug]. diff --git a/docs/en/how-to/debugging/vscode.md b/docs/en/how-to/debugging/vscode.md new file mode 100644 index 000000000..2e3ab6019 --- /dev/null +++ b/docs/en/how-to/debugging/vscode.md @@ -0,0 +1,77 @@ +# Debug via VS Code { #debug-vscode } + +Debugging is possible at different stages in your development process. It is different to debug a development app via `briefcase dev` than a bundled app that is built via `briefcase build` and run via `briefcase run`. + +## Development + +During development on your host system you should use `briefcase dev`. To attach the VS Code debugger you have to create a configuration in your `.vscode/launch.json` file like this: + +```json +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Briefcase: Dev", + "type": "debugpy", + "request": "launch", + "module": "briefcase", + "args": [ + "dev", + ], + "justMyCode": false + }, + ] +} +``` + +To start a debug session, open the debug view in VS Code using the sidebar, select the "Briefcase: Dev" configuration and press `Start Debugging (F5)`. That will run `briefcase dev` in a debug session. + +For more details about the VS Code configurations in the `.vscode/launch.json` file see the [VS Code documentation](https://code.visualstudio.com/docs/python/debugging). + +## Bundled App + +It is also possible to debug a bundled app. This is currently still an **experimental feature** that is only supported on Windows and macOS. + +To debug a bundled app a piece of the debugger has to be embedded into your app. This is done via: + +```console +$ briefcase build --debug debugpy +``` + +This will build your app in debug mode and add [debugpy](https://code.visualstudio.com/docs/debugtest/debugging#_debug-console-repl) together with a package that automatically starts `debugpy` at the startup of your bundled app. + +Then it is time to run your app. You can do this via: + +```console +$ briefcase run --debug debugpy +``` + +Running the app in debug mode will automatically start the `debugpy` debugger and listen for incoming connections. By default it will listen on `localhost` and port `5678`. You can then connect your VS Code debugger to the app by creating a configuration like this in the `.vscode/launch.json` file: + +```json +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Briefcase: Attach", + "type": "debugpy", + "request": "attach", + "connect": { + "host": "localhost", + "port": 5678 + } + "justMyCode": false + } + ] +} +``` + +The app will not start until you attach the debugger. Once you attach the VS Code debugger, you can set [breakpoints](https://code.visualstudio.com/docs/debugtest/debugging#_breakpoints), use the [data inspection](https://code.visualstudio.com/docs/debugtest/debugging#_data-inspection), use the [debug console REPL](https://code.visualstudio.com/docs/debugtest/debugging#_debug-console-repl) and all other debugging features of VS Code. + +But there are some restrictions that must be taken into account: + +- Restart your application via the green circle is not working. You have to stop the app manually and start it again via `briefcase run --debug debugpy`. +- `justMyCode` has to be set to `false`. When setting it to `true`, or not defining it at all, breakpoints are missed on some platforms (e.g., Windows). The reason for this is currently unknown. +- `pathMappings` should not be set manually in the `launch.json`. The path mappings will be set by Briefcase programmatically and if setting it manually too the manual setting will overwrite settings by Briefcase. + +For more information see [here][run-debug]. diff --git a/docs/en/reference/commands/build.md b/docs/en/reference/commands/build.md index 95cdda4b0..b367e0535 100644 --- a/docs/en/reference/commands/build.md +++ b/docs/en/reference/commands/build.md @@ -93,3 +93,22 @@ If you have previously run the app in "normal" mode, you may need to pass `-r` / ### `--no-update` Prevent the automated update of app code that is performed when specifying by the `--test` option. + +### `--debug ` + +Build the app in debug mode in the bundled app environment and installs the selected debugger in the bundled app. + +Currently the following debuggers are supported: + +- `pdb`: This is used for debugging via console (see [Debug via PDB][debug-pdb]) +- `debugpy`: This is used for debugging via VS Code (see [Debug via VS Code][debug-vscode]) + +If calling only `--debug` without selecting a debugger explicitly, `pdb` is used as default. + +This is an **experimental** new feature, that is currently only supported on Windows and macOS. + +This option may slow down the app a little bit. + +If you have previously run the app in "normal" mode, you may need to pass `-r` / `--update-requirements` the first time you build in debug mode to ensure that the debugger is embedded in your bundled app. + +The selected debugger in `build --debug ` has to match the selected debugger in `run --debug `. diff --git a/docs/en/reference/commands/package.md b/docs/en/reference/commands/package.md index d51fd493d..5d5409aa2 100644 --- a/docs/en/reference/commands/package.md +++ b/docs/en/reference/commands/package.md @@ -4,6 +4,8 @@ Compile/build an application installer. By default, targets the current platform This will produce an installable artefact. +You should not package an application that was built using `build --test` or `build --debug `. + ## Usage To build an installer of the default output format for the current platform: diff --git a/docs/en/reference/commands/run.md b/docs/en/reference/commands/run.md index ae4569bb1..0e44575bb 100644 --- a/docs/en/reference/commands/run.md +++ b/docs/en/reference/commands/run.md @@ -98,6 +98,31 @@ Run the app in test mode in the bundled app environment. Running `run --test` wi Prevent the automated update and build of app code that is performed when specifying by the `--test` option. +## `--debug ` { #run-debug } + +Run the app in debug mode in the bundled app environment and establish a debugger connection via a socket. + +Currently the following debuggers are supported: + +- `pdb`: This is used for debugging via console (see [Debug via PDB][debug-pdb]) +- `debugpy`: This is used for debugging via VS Code (see [Debug via VS Code][debug-vscode]) + +For `debugpy` Briefcase will automatically apply path mapping of the source code from your bundled app in the `build` folder to your local source code defined under `sources` in your `pyproject.toml`. This would collide with an existing `pathMappings` setting in your VS Code `launch.json` file. Therefore, if you are using `debugpy`, do not set `pathMappings` manually in your VS Code `launch.json`. + +If calling only `--debug` without selecting a debugger explicitly, `pdb` is used as default. + +This is an **experimental** new feature, that is currently only supported on Windows and macOS. + +The selected debugger in `run --debug ` has to match the selected debugger in `build --debug `. + +## `--debugger-host ` + +Specifies the host of the socket connection for the debugger. This option is only used when the `--debug ` option is specified. The default value is `localhost`. + +## `--debugger-port ` + +Specifies the port of the socket connection for the debugger. This option is only used when the `--debug ` option is specified. The default value is `5678`. + ## Passthrough arguments If you want to pass any arguments to your app's command line, you can specify them using the `--` marker to separate Briefcase's arguments from your app's arguments. For example: diff --git a/docs/how-to/debugging/index.rst b/docs/how-to/debugging/index.rst deleted file mode 100644 index 16a64917b..000000000 --- a/docs/how-to/debugging/index.rst +++ /dev/null @@ -1,12 +0,0 @@ -============== -Debug your app -============== - -If you get stuck when programming your app, it is time to debug your app. The -following sections describe how you can debug your app with or without an IDE. - -.. toctree:: - :maxdepth: 1 - - pdb - vscode diff --git a/docs/how-to/debugging/pdb.rst b/docs/how-to/debugging/pdb.rst deleted file mode 100644 index ebc06490d..000000000 --- a/docs/how-to/debugging/pdb.rst +++ /dev/null @@ -1,77 +0,0 @@ -============= -Debug via PDB -============= - -It is possible to debug a Briefcase app via `PDB `_ -at different stages in your development process. You can debug a development app -via ``briefcase dev``, but also a bundled app that is built via ``briefcase build`` -and run via ``briefcase run``. - - -Development ------------ -Debugging a development app is quite easy. Just add ``breakpoint()`` inside your -code and start the app via ``briefcase dev``. When the breakpoint is hit, the pdb -console opens in your terminal and you can debug your app. - - -Bundled App ------------ -It is also possible to debug a bundled app. This is currently still an -**experimental feature** that is only supported on Windows and macOS. - -To debug a bundled app, a piece of the debugger has to be embedded into your app. -This is done via: - -.. code-block:: console - - $ briefcase build --debug pdb - -This will build your app in debug mode and add `remote-pdb `_ -together with a package that automatically starts ``remote-pdb`` on startup of -your bundled app. - -Then it is time to run your app. You can do this via: - -.. code-block:: console - - $ briefcase run --debug pdb - -Running the app in debug mode will automatically start the ``remote-pdb`` debugger -and wait for incoming connections. By default, it will listen on ``localhost`` and -port ``5678``. - -In a separate terminal on your host system, connect to your bundled app: - -.. tabs:: - - .. group-tab:: Windows - To connect to your application, you need access to ``telnet``. That is not activated by default, but can be - activated by running the following command with admin rights - - .. code-block:: console - - $ dism /online /Enable-Feature /FeatureName:TelnetClient - - Then you can start the connection via - - .. code-block:: console - - $ telnet localhost 5678 - - .. group-tab:: Linux - - .. code-block:: console - - $ nc localhost 5678 - - .. group-tab:: macOS - - .. code-block:: console - - $ nc localhost 5678 - - -The app will start after the connection is established. - -For more information, see :ref:`here `. diff --git a/docs/how-to/debugging/vscode.rst b/docs/how-to/debugging/vscode.rst deleted file mode 100644 index f2154c35e..000000000 --- a/docs/how-to/debugging/vscode.rst +++ /dev/null @@ -1,102 +0,0 @@ -================= -Debug via VS Code -================= - -Debugging is possible at different stages in your development process. It is -different to debug a development app via ``briefcase dev`` than a bundled app -that is built via ``briefcase build`` and run via ``briefcase run``. - -Development ------------ -During development on your host system you should use ``briefcase dev``. To -attach the VS Code debugger you have to create a configuration in your -``.vscode/launch.json`` file like this: - -.. code-block:: JSON - - { - "version": "0.2.0", - "configurations": [ - { - "name": "Briefcase: Dev", - "type": "debugpy", - "request": "launch", - "module": "briefcase", - "args": [ - "dev", - ], - "justMyCode": false - }, - ] - } - -To start a debug session, open the debug view in VS Code using the sidebar, select the -"Briefcase: Dev" configuration and press ``Start Debugging (F5)``. That will run -``briefcase dev`` in a debug session. - -For more details about the VS Code configurations in the ``.vscode/launch.json`` file -see the `VS Code documentation `_. - -Bundled App ------------ -It is also possible to debug a bundled app. This is currently still an -**experimental feature** that is only supported on Windows and macOS. - -To debug a bundled app a piece of the debugger has to be embedded into your app. -This is done via: - -.. code-block:: console - - $ briefcase build --debug debugpy - -This will build your app in debug mode and add `debugpy `_ -together with a package that automatically starts ``debugpy`` at the startup of -your bundled app. - -Then it is time to run your app. You can do this via: - -.. code-block:: console - - $ briefcase run --debug debugpy - -Running the app in debug mode will automatically start the ``debugpy`` debugger -and listen for incoming connections. By default it will listen on ``localhost`` -and port ``5678``. You can then connect your VS Code debugger to the app by -creating a configuration like this in the ``.vscode/launch.json`` file: - -.. code-block:: JSON - - { - "version": "0.2.0", - "configurations": [ - { - "name": "Briefcase: Attach", - "type": "debugpy", - "request": "attach", - "connect": { - "host": "localhost", - "port": 5678 - } - "justMyCode": false - } - ] - } - -The app will not start until you attach the debugger. Once you attach the -VS Code debugger, you can set `breakpoints `_, -use the `data inspection `_, -use the `debug console REPL `_ -and all other debugging features of VS Code. - -But there are some restrictions that must be taken into account: - -- Restart your application via the green circle is not working. You have to stop - the app manually and start it again via ``briefcase run --debug debugpy``. -- ``justMyCode`` has to be set to ``false``. When setting it to ``true``, or not - defining it at all, breakpoints are missed on some platforms (e.g., Windows). - The reason for this is currently unknown. -- ``pathMappings`` should not be set manually in the ``launch.json``. The path - mappings will be set by Briefcase programmatically and if setting it manually - too the manual setting will overwrite settings by Briefcase. - -For more information see :ref:`here `. From f6485089ce484813eafa94ba45f21c711d7e97ff Mon Sep 17 00:00:00 2001 From: timrid <6593626+timrid@users.noreply.github.com> Date: Sat, 25 Oct 2025 21:45:00 +0200 Subject: [PATCH 122/131] environments created with tox_uv dont have pip preinstalled --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index faf36e502..615777462 100644 --- a/tox.ini +++ b/tox.ini @@ -73,7 +73,7 @@ skip_install = True deps = build commands = - python -m pip install ".[pdb,debugpy]" --group dev + uv pip install ".[pdb,debugpy]" --group dev python -X warn_default_encoding -m coverage run -m pytest {posargs:-vv --color yes} python -m coverage combine python -m coverage report --fail-under=100 From 8b13a6ced692ea0b816db6c31c2e0fc471286114 Mon Sep 17 00:00:00 2001 From: timrid <6593626+timrid@users.noreply.github.com> Date: Sat, 25 Oct 2025 22:03:07 +0200 Subject: [PATCH 123/131] fixed ruff errors --- debugger/pyproject.toml | 5 +++++ debugger/tests/test_debugpy.py | 6 +++--- debugger/tests/test_debugpy_path_mappings.py | 12 +++++++++--- debugger/tests/test_pdb.py | 2 +- tests/commands/build/test_call.py | 2 +- tests/debuggers/test_base.py | 8 +++----- 6 files changed, 22 insertions(+), 13 deletions(-) diff --git a/debugger/pyproject.toml b/debugger/pyproject.toml index f5f8305b4..ed36a85a4 100644 --- a/debugger/pyproject.toml +++ b/debugger/pyproject.toml @@ -45,5 +45,10 @@ skip_covered = true skip_empty = true precision = 1 +[tool.ruff.lint] +ignore = [ + "T20", # flake8-print (useful for briefcase, but not in the debugger) +] + [tool.setuptools_scm] root = "../" diff --git a/debugger/tests/test_debugpy.py b/debugger/tests/test_debugpy.py index c3cc545b4..7c1f092c3 100644 --- a/debugger/tests/test_debugpy.py +++ b/debugger/tests/test_debugpy.py @@ -40,7 +40,7 @@ def test_no_debugger_verbose(monkeypatch, capsys): @pytest.mark.parametrize( - "os_name,app_path_mappings,sys_path,expected_path_mappings", + ("os_name", "app_path_mappings", "sys_path", "expected_path_mappings"), [ ( "nt", @@ -77,7 +77,7 @@ def test_no_debugger_verbose(monkeypatch, capsys): ], ) @pytest.mark.parametrize( - "verbose,some_verbose_output,pydevd_trace_level", + ("verbose", "some_verbose_output", "pydevd_trace_level"), [ (True, "Extracted path mappings:\n[0] host = ", 3), (False, "", 0), @@ -147,7 +147,7 @@ def test_with_debugger( assert captured.err == "" assert some_verbose_output in captured.out - assert fake_pydevd.DebugInfoHolder.DEBUG_TRACE_LEVEL == pydevd_trace_level + assert pydevd_trace_level == fake_pydevd.DebugInfoHolder.DEBUG_TRACE_LEVEL def test_with_debugger_without_path_mappings(monkeypatch, capsys): diff --git a/debugger/tests/test_debugpy_path_mappings.py b/debugger/tests/test_debugpy_path_mappings.py index 57035a544..e40642826 100644 --- a/debugger/tests/test_debugpy_path_mappings.py +++ b/debugger/tests/test_debugpy_path_mappings.py @@ -30,7 +30,13 @@ def test_mappings_none(monkeypatch): @pytest.mark.parametrize( - "os_name,app_path_mappings,app_packages_path_mappings,sys_path,expected_path_mappings", + ( + "os_name", + "app_path_mappings", + "app_packages_path_mappings", + "sys_path", + "expected_path_mappings", + ), [ # Windows pytest.param( @@ -233,7 +239,7 @@ def test_mappings( @pytest.mark.parametrize( - "os_name,app_path_mappings", + ("os_name", "app_path_mappings"), [ # Windows pytest.param( @@ -277,5 +283,5 @@ def test_mappings_wrong_sys_path( sys_path = [] monkeypatch.setattr(sys, "path", sys_path) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=r"No sys.path entry matches regex"): briefcase_debugger.debugpy.load_path_mappings(config, False) diff --git a/debugger/tests/test_pdb.py b/debugger/tests/test_pdb.py index 68ffa8a89..6c3a362ab 100644 --- a/debugger/tests/test_pdb.py +++ b/debugger/tests/test_pdb.py @@ -39,7 +39,7 @@ def test_no_debugger_verbose(monkeypatch, capsys): @pytest.mark.parametrize("verbose", [True, False]) @pytest.mark.parametrize( - "host_os,expected_host_cmds", + ("host_os", "expected_host_cmds"), [ ("Windows", ["telnet somehost 9999"]), ("Darwin", ["nc somehost 9999"]), diff --git a/tests/commands/build/test_call.py b/tests/commands/build/test_call.py index a85d7c263..0ceeaa019 100644 --- a/tests/commands/build/test_call.py +++ b/tests/commands/build/test_call.py @@ -1130,7 +1130,7 @@ def test_build_debug_unsupported(build_command, first_app, second_app): # Configure command line options with pytest.raises(SystemExit): - options, _ = build_command.parse_options(["--debug=pdb"]) + _, _ = build_command.parse_options(["--debug=pdb"]) def test_build_invalid_update(build_command, first_app, second_app): diff --git a/tests/debuggers/test_base.py b/tests/debuggers/test_base.py index 19ab8385d..0b130b0d8 100644 --- a/tests/debuggers/test_base.py +++ b/tests/debuggers/test_base.py @@ -24,7 +24,7 @@ def read_text(self, name): @pytest.mark.parametrize( - "direct_url,is_editable", + ("direct_url", "is_editable"), [ (json.dumps({"dir_info": {"editable": True}}), True), # editable (json.dumps({"dir_info": {"editable": False}}), False), # not editable @@ -66,14 +66,12 @@ def test_get_debugger(): assert isinstance(get_debugger("debugpy"), DebugpyDebugger) # Test with an unknown debugger name - try: + with pytest.raises(BriefcaseCommandError, match="Unknown debugger: unknown"): get_debugger("unknown") - except BriefcaseCommandError as e: - assert str(e) == "Unknown debugger: unknown" @pytest.mark.parametrize( - "debugger_name, expected_class, connection_mode", + ("debugger_name", "expected_class", "connection_mode"), [ ( "pdb", From 35907f237988ad91646e1d7247b64aff4e349d35 Mon Sep 17 00:00:00 2001 From: timrid <6593626+timrid@users.noreply.github.com> Date: Sat, 25 Oct 2025 22:26:30 +0200 Subject: [PATCH 124/131] fixed spelling issues --- docs/en/how-to/debugging/pdb.md | 2 +- docs/spelling_wordlist | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/en/how-to/debugging/pdb.md b/docs/en/how-to/debugging/pdb.md index d347ef6f3..d35a779dd 100644 --- a/docs/en/how-to/debugging/pdb.md +++ b/docs/en/how-to/debugging/pdb.md @@ -4,7 +4,7 @@ It is possible to debug a Briefcase app via [PDB](https://docs.python.org/3/libr ## Development -Debugging a development app is quite easy. Just add `breakpoint()` inside your code and start the app via `briefcase dev`. When the breakpoint is hit, the pdb console opens in your terminal and you can debug your app. +Debugging a development app is quite easy. Just add `breakpoint()` inside your code and start the app via `briefcase dev`. When the breakpoint is hit, the PDB console opens in your terminal and you can debug your app. ## Bundled App diff --git a/docs/spelling_wordlist b/docs/spelling_wordlist index 1fbfb959a..9e18b5237 100644 --- a/docs/spelling_wordlist +++ b/docs/spelling_wordlist @@ -24,6 +24,7 @@ beeware BeeWare BeeWare's blobless +breakpoint Bugfix Bugfixes ce @@ -44,6 +45,7 @@ CTRL customizations datetime DBus +debugpy dev dialogs Diataxis @@ -121,6 +123,7 @@ OSX passthrough Passthrough pbb +PDB PFX phablet PID @@ -133,6 +136,7 @@ precompiled proxied Proxied proxying +programmatically PRs PursuedPyBear px @@ -153,6 +157,7 @@ pytest RCEdit README RedHat +REPL ReST reStructuredText RGB From 0342efc60475cd61bd4a8e24af37b3dff1bcccfe Mon Sep 17 00:00:00 2001 From: timrid <6593626+timrid@users.noreply.github.com> Date: Tue, 28 Oct 2025 19:19:43 +0100 Subject: [PATCH 125/131] fix ci --- .github/workflows/ci.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4dcdb5e3d..d21679a66 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,6 @@ on: push: branches: - main - - ci workflow_call: inputs: attest-package: @@ -50,10 +49,10 @@ jobs: strategy: matrix: subdir: - - "." + - "" # root briefcase package - "debugger" - "automation" - uses: timrid/beeware-.github/.github/workflows/python-package-create.yml@no-differentiator-when-subdirectory-is-root + uses: beeware/.github/.github/workflows/python-package-create.yml@main with: build-subdirectory: ${{ matrix.subdir }} attest: ${{ inputs.attest-package }} From e6c4138286feba1b5baca5d340df360ebee639e5 Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Mon, 3 Nov 2025 20:11:48 +0000 Subject: [PATCH 126/131] Documentation cleanups --- docs/en/how-to/debugging/vscode.md | 4 ++-- docs/en/reference/commands/build.md | 2 +- docs/en/reference/commands/run.md | 4 +--- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/en/how-to/debugging/vscode.md b/docs/en/how-to/debugging/vscode.md index 2e3ab6019..29e25dd54 100644 --- a/docs/en/how-to/debugging/vscode.md +++ b/docs/en/how-to/debugging/vscode.md @@ -1,6 +1,6 @@ # Debug via VS Code { #debug-vscode } -Debugging is possible at different stages in your development process. It is different to debug a development app via `briefcase dev` than a bundled app that is built via `briefcase build` and run via `briefcase run`. +Debugging is possible at different stages in your development process. You can debug a development app via `briefcase dev`, or a bundled app that is built via `briefcase build` and run via `briefcase run`. ## Development @@ -70,7 +70,7 @@ The app will not start until you attach the debugger. Once you attach the VS Cod But there are some restrictions that must be taken into account: -- Restart your application via the green circle is not working. You have to stop the app manually and start it again via `briefcase run --debug debugpy`. +- Restarting your application via the green circle is not working. You have to stop the app manually and start it again via `briefcase run --debug debugpy`. - `justMyCode` has to be set to `false`. When setting it to `true`, or not defining it at all, breakpoints are missed on some platforms (e.g., Windows). The reason for this is currently unknown. - `pathMappings` should not be set manually in the `launch.json`. The path mappings will be set by Briefcase programmatically and if setting it manually too the manual setting will overwrite settings by Briefcase. diff --git a/docs/en/reference/commands/build.md b/docs/en/reference/commands/build.md index b367e0535..a06a46d60 100644 --- a/docs/en/reference/commands/build.md +++ b/docs/en/reference/commands/build.md @@ -96,7 +96,7 @@ Prevent the automated update of app code that is performed when specifying by th ### `--debug ` -Build the app in debug mode in the bundled app environment and installs the selected debugger in the bundled app. +Install the selected debugger into the bundled app. Currently the following debuggers are supported: diff --git a/docs/en/reference/commands/run.md b/docs/en/reference/commands/run.md index 0e44575bb..e6042a239 100644 --- a/docs/en/reference/commands/run.md +++ b/docs/en/reference/commands/run.md @@ -100,15 +100,13 @@ Prevent the automated update and build of app code that is performed when specif ## `--debug ` { #run-debug } -Run the app in debug mode in the bundled app environment and establish a debugger connection via a socket. +Run the app in debug mode. Currently the following debuggers are supported: - `pdb`: This is used for debugging via console (see [Debug via PDB][debug-pdb]) - `debugpy`: This is used for debugging via VS Code (see [Debug via VS Code][debug-vscode]) -For `debugpy` Briefcase will automatically apply path mapping of the source code from your bundled app in the `build` folder to your local source code defined under `sources` in your `pyproject.toml`. This would collide with an existing `pathMappings` setting in your VS Code `launch.json` file. Therefore, if you are using `debugpy`, do not set `pathMappings` manually in your VS Code `launch.json`. - If calling only `--debug` without selecting a debugger explicitly, `pdb` is used as default. This is an **experimental** new feature, that is currently only supported on Windows and macOS. From f1f11b6fc64e417ee8b680de81e2236d1bfd1dca Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Mon, 3 Nov 2025 20:24:00 +0000 Subject: [PATCH 127/131] Rename 2147.feature.rst to 2147.feature.md and reword --- changes/2147.feature.md | 1 + changes/2147.feature.rst | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 changes/2147.feature.md delete mode 100644 changes/2147.feature.rst diff --git a/changes/2147.feature.md b/changes/2147.feature.md new file mode 100644 index 000000000..9c18cfbf7 --- /dev/null +++ b/changes/2147.feature.md @@ -0,0 +1 @@ +Added a `--debug` option to the `build` and `run` commands for Windows and macOS. diff --git a/changes/2147.feature.rst b/changes/2147.feature.rst deleted file mode 100644 index 40fcc169e..000000000 --- a/changes/2147.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Added basic functions for an ``--debug `` option for Windows and macOS. From 5e74fef217b26e1da95c9de35f5f625a69203bc5 Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Mon, 3 Nov 2025 21:04:48 +0000 Subject: [PATCH 128/131] Update docs URLs; use `%(default)s` in argparse help --- debugger/src/briefcase_debugger/debugpy.py | 2 +- debugger/src/briefcase_debugger/pdb.py | 2 +- src/briefcase/commands/base.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/debugger/src/briefcase_debugger/debugpy.py b/debugger/src/briefcase_debugger/debugpy.py index 2f6b8b6ab..b216860fe 100644 --- a/debugger/src/briefcase_debugger/debugpy.py +++ b/debugger/src/briefcase_debugger/debugpy.py @@ -99,7 +99,7 @@ def start_debugpy(config: DebuggerConfig, verbose: bool): ] }} -For more information see: https://briefcase.readthedocs.io/en/stable/how-to/debugging/vscode.html#bundled-app +For more information see: https://briefcase.beeware.org/en/stable/how-to/debugging/vscode/#bundled-app """ ) debugpy.wait_for_client() diff --git a/debugger/src/briefcase_debugger/pdb.py b/debugger/src/briefcase_debugger/pdb.py index 76fa11457..d889fb2b9 100644 --- a/debugger/src/briefcase_debugger/pdb.py +++ b/debugger/src/briefcase_debugger/pdb.py @@ -31,7 +31,7 @@ def start_pdb(config: DebuggerConfig, verbose: bool): {cmds_hint} -For more information see: https://briefcase.readthedocs.io/en/stable/how-to/debugging/console.html#bundled-app +For more information see: https://briefcase.beeware.org/en/stable/how-to/debugging/pdb/#bundled-app """) # Create a RemotePdb instance diff --git a/src/briefcase/commands/base.py b/src/briefcase/commands/base.py index c7636437e..c41742167 100644 --- a/src/briefcase/commands/base.py +++ b/src/briefcase/commands/base.py @@ -1045,7 +1045,7 @@ def _add_debug_options(self, parser, context_label, run_cmd: bool = False): parser.add_argument( "--debugger-host", default="localhost", - help="The host on which to run the debug server (default: localhost)", + help="The host on which to run the debug server (default: %(default)s)", required=False, ) parser.add_argument( @@ -1053,7 +1053,7 @@ def _add_debug_options(self, parser, context_label, run_cmd: bool = False): "--debugger-port", default=5678, type=int, - help="The port on which to run the debug server (default: 5678)", + help="The port on which to run the debug server (default: %(default)s)", required=False, ) From 234195ee2cc236fd1ab4ba687e443a3358b8c4aa Mon Sep 17 00:00:00 2001 From: timrid <6593626+timrid@users.noreply.github.com> Date: Fri, 7 Nov 2025 15:06:18 +0100 Subject: [PATCH 129/131] add hint that you have to define `breakpoint()` somewhere when using PDB. --- docs/en/how-to/debugging/pdb.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/how-to/debugging/pdb.md b/docs/en/how-to/debugging/pdb.md index d35a779dd..adfc2cf25 100644 --- a/docs/en/how-to/debugging/pdb.md +++ b/docs/en/how-to/debugging/pdb.md @@ -10,7 +10,9 @@ Debugging a development app is quite easy. Just add `breakpoint()` inside your c It is also possible to debug a bundled app. This is currently still an **experimental feature** that is only supported on Windows and macOS. -To debug a bundled app, a piece of the debugger has to be embedded into your app. This is done via: +To debug a bundled app, at first you have to add `breakpoint()` somewhere in your code, where the debugger should halt. + +Then you have to built your app with the debugger embedded into your app. This is done via: ```console $ briefcase build --debug pdb From 4b123ba1f9aaba26c3e8da6f034a556b1c13f179 Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Tue, 11 Nov 2025 19:55:30 +0000 Subject: [PATCH 130/131] Add missing comma --- docs/en/how-to/debugging/vscode.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/how-to/debugging/vscode.md b/docs/en/how-to/debugging/vscode.md index 29e25dd54..beec5a78d 100644 --- a/docs/en/how-to/debugging/vscode.md +++ b/docs/en/how-to/debugging/vscode.md @@ -59,7 +59,7 @@ Running the app in debug mode will automatically start the `debugpy` debugger an "connect": { "host": "localhost", "port": 5678 - } + }, "justMyCode": false } ] From c9172696515b5f4a06ef565b3877c6db07dfa7ba Mon Sep 17 00:00:00 2001 From: Malcolm Smith Date: Tue, 11 Nov 2025 19:59:18 +0000 Subject: [PATCH 131/131] Fix long line --- src/briefcase/commands/base.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/briefcase/commands/base.py b/src/briefcase/commands/base.py index c41742167..87b463340 100644 --- a/src/briefcase/commands/base.py +++ b/src/briefcase/commands/base.py @@ -1038,7 +1038,10 @@ def _add_debug_options(self, parser, context_label, run_cmd: bool = False): const="pdb", choices=debugger_names, metavar="DEBUGGER", - help=f"{context_label} the app with the specified debugger. One of %(choices)s (default: %(const)s)", + help=( + f"{context_label} the app with the specified debugger. " + f"One of %(choices)s (default: %(const)s)" + ), ) if run_cmd: