Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/2958.misc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Applications can now specify the environment manager that should be used when creating isolated environments for apps.
10 changes: 10 additions & 0 deletions docs/en/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,16 @@ The person or organization responsible for the project.

The contact email address for the person or organization responsible for the project.

#### `env_manager`

**EXPERIMENTAL** This is an experimental feature. It is currently only used by developer mode.

The environment manager to use when creating isolated Python environments and installing requirements. Must be one of:

* `venv` - The `venv` package provided by the Python standard library

Defaults to `venv`.

#### `license_files`

A [PEP 639](https://peps.python.org/pep-0639/) specification for the files in the project that define licenses that should be included with the packaged app. `license_files` must be a list of strings, each of which references a filename in the project (relative to the location of the `pyproject.toml`):
Expand Down
17 changes: 13 additions & 4 deletions src/briefcase/commands/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from briefcase.config import (
AppConfig,
DraftAppConfig,
EnvManagerT,
FinalizedAppConfig,
GlobalConfig,
parse_config,
Expand Down Expand Up @@ -136,6 +137,7 @@ class BaseCommand(ABC):
cmd_line = "briefcase {command} {platform} {output_format}"
supported_host_os: Collection[str] = {"Darwin", "Linux", "Windows"}
supported_host_os_reason = f"This command is not supported on {platform.system()}."
supported_env_managers: Collection[EnvManagerT] = {"venv"}

# defined by platform-specific subclasses
command: str
Expand All @@ -154,7 +156,7 @@ class BaseCommand(ABC):
def __init__(
self,
console: Console,
tools: ToolCache = None,
tools: ToolCache | None = None,
apps: dict[str, AppConfig] | None = None,
base_path: Path | None = None,
data_path: Path | None = None,
Expand Down Expand Up @@ -735,14 +737,21 @@ def finalize_app_config(self, app: DraftAppConfig, **kwargs) -> FinalizedAppConf
configuration, and performs any other app-specific platform configuration and
verification that is required as a result of command-line arguments.

Platform overrides should call ``super().finalize_app_config(app, **kwargs)``
to construct the ``FinalizedAppConfig``.
Platform overrides should call `super().finalize_app_config(app, **kwargs)`
to construct the `FinalizedAppConfig`.

:param app: The app configuration to finalize.
:param kwargs: Runtime attributes forwarded to the FinalizedAppConfig
constructor (``test_mode``, ``debugger``, etc.).
constructor (`test_mode`, `debugger`, etc.).
:returns: The finalized app configuration.
"""
if app.env_manager not in self.supported_env_managers:
raise BriefcaseConfigError(
f"{app.app_name!r} declares the use of a {app.env_manager!r} "
f"environment, but {self.platform} {self.output_format} "
f"projects do not support environments of that type."
)

return FinalizedAppConfig(app, **kwargs)

def resolve_apps(
Expand Down
92 changes: 47 additions & 45 deletions src/briefcase/commands/dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from briefcase.commands.run import RunAppMixin
from briefcase.config import FinalizedAppConfig
from briefcase.exceptions import BriefcaseCommandError
from briefcase.integrations.subprocess import NativeAppContext
from briefcase.integrations.virtual_environment import VirtualEnvironment

from .base import BaseCommand
Expand Down Expand Up @@ -47,10 +48,6 @@ def platform(self):
"win32": "windows",
}[sys.platform]

def bundle_path(self, app):
"""A placeholder; Dev command doesn't have a bundle path."""
raise NotImplementedError()

def binary_path(self, app):
"""A placeholder; Dev command doesn't have a binary path."""
raise NotImplementedError()
Expand Down Expand Up @@ -86,6 +83,11 @@ def add_options(self, parser):
help="Run the app in test mode",
)

def verify_app_tools(self, app: FinalizedAppConfig):
"""Verify that tools needed to run the command for this app exist."""
super().verify_app_tools(app)
NativeAppContext.verify(tools=self.tools, app=app)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This (and removing the NotImplemented for bundle_path) is needed to ensure that there is an app context; the venv commands are then run in that app context.


def install_dev_requirements(
self,
app: FinalizedAppConfig,
Expand All @@ -112,7 +114,7 @@ def install_dev_requirements(
venv.install_requirements(
requires,
allow_editable=True,
installer_args=app.requirement_installer_args,
extra_installer_args=app.requirement_installer_args,
)

def run_dev_app(
Expand Down Expand Up @@ -220,14 +222,6 @@ def venv_name(self) -> str:
ext = importlib.machinery.EXTENSION_SUFFIXES[0].split(".")[1]
return f"dev.{ext}"

def venv_path(self, appname: str) -> Path:
"""Return the path for the app's virtual environment.

:param app: The app config
:returns: Path where the venv should be located
"""
return self.base_path / ".briefcase" / appname / self.venv_name

def __call__(
self,
appname: str | None = None,
Expand Down Expand Up @@ -299,38 +293,46 @@ def __call__(

if isolated:
self.console.info("Activating dev environment...", prefix=app.app_name)
env_manager = app.env_manager
else:
env_manager = None

venv = self.tools.virtual_environment[env_manager](
name=self.venv_name,
app=app,
tools=self.tools,
platform=self.platform,
arch=self.tools.host_arch,
base_path=self.base_path,
)
created = venv.prepare(recreate=update_requirements)

with self.tools.virtual_environment(
venv_path=self.venv_path(app.app_name),
isolated=isolated,
recreate=update_requirements,
) as venv:
if venv.created:
self.console.info("Installing requirements...", prefix=app.app_name)
try:
self.install_dev_requirements(app, venv, **options)
except Exception:
# If any problem occurs during installing requirements, remove the
# venv; it will need to be re-created on the next run.
venv.clean()
raise

write_dist_info(
app,
self.app_module_path(app).parent / app.dist_info_name,
)
if created:
self.console.info("Installing requirements...", prefix=app.app_name)
try:
self.install_dev_requirements(app, venv, **options)
except Exception:
# If any problem occurs during installing requirements, remove the
# venv; it will need to be re-created on the next run.
venv.clean()
raise

write_dist_info(
app,
self.app_module_path(app).parent / app.dist_info_name,
)

if run_app:
if app.test_mode:
self.console.info(
"Running test suite in dev environment...", prefix=app.app_name
)
else:
self.console.info("Starting in dev mode...", prefix=app.app_name)
return self.run_dev_app(
app,
env=self.get_environment(app),
venv=venv,
passthrough=[] if passthrough is None else passthrough,
**options,
if run_app:
if app.test_mode:
self.console.info(
"Running test suite in dev environment...", prefix=app.app_name
)
else:
self.console.info("Starting in dev mode...", prefix=app.app_name)
return self.run_dev_app(
app,
env=self.get_environment(app),
venv=venv,
passthrough=[] if passthrough is None else passthrough,
**options,
)
14 changes: 14 additions & 0 deletions src/briefcase/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import unicodedata
from email.utils import getaddresses
from pathlib import Path
from typing import Literal
from urllib.parse import urlparse

from build import BuildBackendException
Expand All @@ -26,6 +27,8 @@
from .constants import MIME_TYPE_REGISTRIES, RESERVED_WORDS
from .exceptions import BriefcaseConfigError, InvalidVersionError

EnvManagerT = Literal["venv", "uv", "conda"]

# PEP 508 restricts the naming of modules. The PEP defines a regex that uses
# re.IGNORECASE; but in in practice, packaging uses a version that rolls out the lower
# case, which has very slightly different semantics with non-ASCII characters. This
Expand Down Expand Up @@ -452,6 +455,7 @@ class AppConfig(BaseConfig):
and identity (``__eq__``/``__hash__`` based on ``app_name``).
"""

project_name: str
app_name: str
version: Version
bundle: str
Expand Down Expand Up @@ -480,6 +484,8 @@ class AppConfig(BaseConfig):
install_launcher: bool
install_options: dict
uninstall_options: dict
requires_python: str | None
env_manager: EnvManagerT

test_mode: bool
debugger: BaseDebugger | None
Expand Down Expand Up @@ -611,6 +617,7 @@ def __init__(
external_package_path: str | None = None,
external_package_executable_path: str | None = None,
install_launcher: bool | None = None,
env_manager: EnvManagerT | None = None,
**kwargs,
):
super().__init__(**kwargs)
Expand Down Expand Up @@ -713,6 +720,13 @@ def __init__(
f"package named {self.module_name!r}."
)

if env_manager == "venv":
self.env_manager = env_manager
elif env_manager is None:
self.env_manager = "venv"
else:
raise BriefcaseConfigError(f"Unknown environment manager {env_manager!r}")


class FinalizedAppConfig(AppConfig):
"""An AppConfig that has been through platform finalization.
Expand Down
Loading