For additional context about technologies to be used, project structure, shell commands, and other important information, read the current plan
Briefcase converts Python projects into standalone native applications for macOS, Windows, Linux, iOS, Android, and Web. This guide provides the context AI coding agents need to contribute effectively.
- Language: Python >= 3.10 (3.10–3.14 supported)
- Dev environment: Python 3.13 virtualenv with
devdependency group - License: BSD-3-Clause
- Entry point:
briefcaseviasrc/briefcase/__main__.py:main() - Test framework: pytest (100% coverage required, no exceptions)
- Linting: ruff (format + check), codespell, docformatter
- Docs: MkDocs
All development must use a Python 3.13 virtual environment with the dev dependency group installed:
python3.13 -m venv venv
source venv/bin/activate # or venv\Scripts\activate on Windows
pip install -e . --group devThis installs tox, pre-commit, and other development tooling. All test execution and CI-equivalent checks run through tox.
src/briefcase/
├── __main__.py # CLI entry point
├── cmdline.py # Command-line parsing and dispatch
├── config.py # AppConfig / GlobalConfig from pyproject.toml
├── console.py # Rich-based Console (ALL user I/O)
├── exceptions.py # BriefcaseError hierarchy
├── constants.py # Reserved words and constants
├── commands/ # Command implementations
│ ├── base.py # BaseCommand ABC
│ ├── create.py, build.py, run.py, update.py, package.py,
│ │ publish.py, dev.py, new.py, open.py, convert.py, upgrade.py
├── platforms/ # Platform plugins (one subdir per OS)
│ ├── __init__.py # get_platforms(), get_output_formats()
│ ├── macOS/, linux/, windows/, android/, iOS/, web/
├── integrations/ # External tool wrappers
│ ├── base.py # Tool / ManagedTool ABCs, ToolCache
│ └── subprocess.py, android_sdk.py, docker.py, xcode.py, ...
├── bootstraps/ # App template bootstraps
├── channels/ # Publication channels (App Store, Play Store)
└── debuggers/ # Debugger plugins (pdb, debugpy)
tests/ # Mirrors src/ structure exactly
├── conftest.py # Root fixtures (no_print, dummy_console, configs)
├── utils.py # DummyConsole, PartialMatchString, file helpers
├── commands/
├── platforms/
├── integrations/
└── ...
docs/en/ # MkDocs documentation (English)
changes/ # Towncrier changelog fragments
automation/ # Separate automation subpackage
debugger/ # Separate debugger subpackage (own pyproject.toml)
All user-facing output MUST go through the Console object, never raw print(). The T20 ruff rule bans print statements in source code, and the no_print autouse test fixture will fail any test where briefcase code calls print().
Use self.tools.console.info(), .verbose(), .debug(), .warning(), or .error() instead.
Coverage is enforced by coverage report --fail-under=100 in CI. Every line of new code must be covered. Platform-specific code that is unreachable on certain OSes must use conditional coverage pragmas:
# pragma: no-cover-if-not-macos
# pragma: no-cover-if-is-windows
# pragma: no-cover-if-lt-py312Each pragma must have a corresponding rule in pyproject.toml under [tool.coverage.coverage_conditional_plugin.rules].
Red/Green TDD should be used when adding features or fixing bugs.
pytest is configured with filterwarnings = ["error"]. Do not suppress warnings — fix the cause.
Do not make changes to AGENTS.md unless specifically directed to do so.
cmdline.parse_cmdline()resolves platform + format from CLI args- Loads the format module via entry points
- Gets the command class via
getattr(format_module, command_name) - Command classes are composed via multiple inheritance:
class macOSAppBuildCommand(
macOSAppMixin, # format-specific paths/behavior
macOSSigningMixin, # platform signing logic
AppPackagesMergeMixin, # utility mixin
BuildCommand, # base command from commands/
):
...Each format file (e.g., platforms/macOS/app.py) must export module-level aliases matching command names:
create = macOSAppCreateCommand
update = macOSAppUpdateCommand
build = macOSAppBuildCommand
run = macOSAppRunCommand
package = macOSAppPackageCommand
publish = macOSAppPublishCommand
open = macOSAppOpenCommandNew platforms/formats MUST register via entry points in pyproject.toml, never by hard-coding in core logic.
ToolABC withverify()classmethod (callsverify_host()thenverify_install())ManagedTool(Tool)addsexists(),install(),uninstall(),upgrade()- Tools register via
__init_subclass__intotool_registry - Accessed through
ToolCacheoncommand.tools(e.g.,self.tools.subprocess,self.tools.java) ToolCachewraps stdlib modules (os,platform,shutil,sys) to enable test mocking
All errors derive from BriefcaseError(Exception) with an error_code integer. Key subtypes:
BriefcaseCommandError(200) — general operational errorsBriefcaseConfigError(100) — configuration problemsNetworkFailure,MissingToolError,InvalidDeviceError— specific failure modesHelpText— displays help, not an errorBriefcaseWarning— non-fatal (exit code 0)
All commands below assume the dev dependency group is installed in a Python 3.13 virtual environment (see setup above).
Tests mirror the source tree. Within each area, tests are organized by method/behavior:
tests/commands/create/test_install_app_requirements.py
tests/commands/build/test_call.py
tests/platforms/macOS/app/test_build.py
Tests create concrete subclasses of abstract commands that track method calls in an actions list:
class DummyBuildCommand(BuildCommand):
def build(self, app, **kwargs):
self.actions.append(("build", app.app_name))
return {}
# Then assert exact action sequence:
assert build_command.actions == [
("verify-host",),
("verify-tools",),
("build", "first"),
]- Use
MagicMock(spec_set=...)(notspec=) for strict mocking - External tools: mock via
mock.MagicMock(spec_set=Subprocess)oncommand.tools.subprocess - Filesystem layout: use
create_file()fromtests/utils.py - Downloads: use
mock_file_download(),mock_zip_download(),mock_tgz_download()side-effect factories fromtests/utils.py ToolCachemocks wrap stdlib modules for test isolation
no_print(autouse) — fails tests that callprint()from briefcase codedummy_console—DummyConsolethat records prompts and returns programmed valuessleep_zero— replacestime.sleepwith instant returnsfirst_app_config/first_app_unbuilt/first_app— graduated app fixtures (config only / bundle exists / binary exists)
DummyConsole— captures user interactionPartialMatchString/NoMatchString— flexible assertion matchingcreate_file(),create_plist_file(),create_zip_file(),create_tgz_file()— filesystem helperscreate_wheel(),create_installed_package()— fake Python package creation
All commands below assume the dev dependency group is installed in a Python 3.13 virtual environment (see Development Environment Setup above).
# Run full test suite with coverage
tox -e py-cov
# Run tests fast (parallel, no coverage)
tox -e py-fast
# Run pre-commit hooks
tox -e pre-commit
# Run just ruff
ruff check src/ tests/
ruff format --check src/ tests/
# Coverage report (must be 100%)
tox -e coverage
# Lint docs
tox -e docs-lint
# Build docs
tox -e docs-all
# Check changelog fragments
tox -e towncrier-checkEvery change requires a towncrier fragment in changes/:
changes/{issue_number}.{type}.md
Types: feature, bugfix, removal, doc, misc.
Fragment text must describe user-facing impact, not implementation details. Use misc for housekeeping, minor changes, or changes to features not yet in a formal release.
Markdown files in this project (including AGENTS.md, docs/en/, and changes/ fragments) must not use hard line breaks to enforce an 80-character column limit. Each paragraph or list item must be written as a single unbroken line, regardless of its length. Let the reader's editor or renderer handle wrapping.
This rule applies to all prose. Code blocks, directory trees, and other pre-formatted blocks are exempt — keep those readable within their own constraints.
When writing or editing any .md file, do not insert newlines mid-sentence or mid-paragraph to stay within 80 columns.
- Class naming:
{Platform}{Format}{Action}Command(e.g.,macOSAppBuildCommand) - Mixin naming:
{Platform}{Feature}Mixin(e.g.,macOSSigningMixin) - Platform casing: Preserve original (macOS, iOS, not macos)
- Docstrings: Google/Sphinx style with
:param:/:returns:/:raises:— formatted by docformatter, using Markdown formatting - Imports:
from __future__ import annotationsused widely for PEP 604 unions - Paths: Always use
pathlib.Pathobjects, never raw strings - Function call formatting: When a function call with more than one argument cannot fit on a single line, place each argument on its own line with a trailing comma on the last argument — do not use the "multiple arguments on one wrapped line" style that ruff also permits. Prefer:
over:
my_function( arg1, arg2, arg3, )
my_function( arg1, arg2, arg3 )
- Long string arguments: When a string argument must be split across lines to satisfy line length requirements, wrap the concatenated string literals in parentheses so it is clear the string is a single argument. Prefer:
over:
my_function( ( "this is a very long string " "that is wrapped over two lines" ), second_argument, )
my_function( "this is a very long string " "that is wrapped over two lines", second_argument, )