Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
7209dba
Add UV environment manager implementation.
freakboy3742 Jun 16, 2026
8605180
Add ability to select a custom environment manager.
freakboy3742 Jun 17, 2026
e430df3
Add conda and pixi environment management.
freakboy3742 Jun 17, 2026
4f5289c
Use conda run and conda install
freakboy3742 Jun 17, 2026
71990de
Use pixi run and pixi add
freakboy3742 Jun 17, 2026
3ab6c3f
WIP: use venv interface for linux, macOS and Windows apps.
freakboy3742 Jun 30, 2026
51ebc60
Merge branch 'main' into alt-env-support
freakboy3742 Jul 3, 2026
c7359c3
Conda apps working on macOS.
freakboy3742 Jul 3, 2026
8665815
Clarify naming and typing of app environment method.
freakboy3742 Jul 3, 2026
0929644
Correct usage of environments in update command.
freakboy3742 Jul 3, 2026
9d7b804
Prevent universal apps on macOS with an environment-provided Python.
freakboy3742 Jul 3, 2026
b1cb520
Add Windows integration.
freakboy3742 Jul 7, 2026
424aeec
Add iOS support.
freakboy3742 Jul 10, 2026
4fa0300
Update test suite.
freakboy3742 Jul 16, 2026
98efc00
Add tests for platforms.
freakboy3742 Jul 22, 2026
afde980
Add full tests for environments.
freakboy3742 Jul 22, 2026
650f80d
Merge branch 'main' into alt-env-support
freakboy3742 Jul 22, 2026
e181058
Correct spelling issues.
freakboy3742 Jul 22, 2026
64e4235
Monkeypatch some verification steps.
freakboy3742 Jul 23, 2026
b8346d6
Fixes for Windows compatibility.
freakboy3742 Jul 23, 2026
8585aa6
Don't verify the app until we have a generated template.
freakboy3742 Jul 23, 2026
8a4f6d9
Correct expected command order.
freakboy3742 Jul 23, 2026
7e3890b
Merge branch 'main' into alt-env-support
freakboy3742 Jul 23, 2026
7b8911f
Merge branch 'main' into alt-env-support
freakboy3742 Jul 28, 2026
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/2231.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Experimental support has been added for using `uv` to manage Python environments.
1 change: 1 addition & 0 deletions changes/596.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Experimental support has been added for using `conda` or `pixi` to manage Python environments.
13 changes: 8 additions & 5 deletions docs/en/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ The contact email address for the person or organization responsible for the pro
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
* `uv` - The [uv](https://docs.astral.sh/uv/) environment manager
* `conda` - The [Conda](https://docs.conda.io/) environment manager

Defaults to `venv`.

Expand Down Expand Up @@ -179,7 +181,7 @@ cleanup_paths = [
"path/to/unneeded_file.txt",
"path/to/unneeded_directory",
"path/**/*.exe",
"{app.formal_name}/content/extra.doc"
"{app.formal_name}/content/extra.doc",
]
```

Expand Down Expand Up @@ -330,23 +332,24 @@ Any PEP 508 version specifier is legal. For example:
```python
requires = [
"pillow==9.1.0",
"--no-binary", "pillow",
"--no-binary",
"pillow",
]
```

- Git repository:
```python
requires=["git+https://github.com/beeware/briefcase.git"]
requires = ["git+https://github.com/beeware/briefcase.git"]
```

- Local directory:
```python
requires=["mysrc/myapp"]
requires = ["mysrc/myapp"]
```

- Local wheel file:
```python
requires=["fullpath/wheelfile.whl"]
requires = ["fullpath/wheelfile.whl"]
```

#### `revision`
Expand Down
2 changes: 2 additions & 0 deletions docs/spelling_wordlist
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ CLI
cmd
CMD
codebase
Conda
cookiecutter
Cookiecutter
cryptographic
Expand Down Expand Up @@ -228,6 +229,7 @@ UTF
UTI
UTIs
UTTypeConformsTo
uv
vendored
Ventura
verbing
Expand Down
202 changes: 91 additions & 111 deletions src/briefcase/commands/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@
import os
import platform
import shutil
import subprocess
import sys
from collections.abc import Collection
from datetime import date, datetime
from pathlib import Path
from typing import Literal

import briefcase
from briefcase.config import AppConfig, FinalizedAppConfig
from briefcase.config import AppConfig, EnvManagerT, FinalizedAppConfig
from briefcase.exceptions import (
BriefcaseCommandError,
InvalidStubBinary,
Expand All @@ -21,11 +20,11 @@
MissingNetworkResourceError,
MissingStubBinary,
MissingSupportPackage,
RequirementsInstallError,
UnsupportedPlatform,
)
from briefcase.integrations.git import Git
from briefcase.integrations.subprocess import NativeAppContext
from briefcase.integrations.virtual_environment import VirtualEnvironment

from .base import BaseCommand, full_options

Expand Down Expand Up @@ -84,6 +83,7 @@ def write_dist_info(app: FinalizedAppConfig, dist_info_path: Path):
class CreateCommand(BaseCommand):
command = "create"
description = "Create a new app for a target platform."
require_binary_installs = False

def add_options(self, parser):
super().add_options(parser)
Expand Down Expand Up @@ -286,6 +286,42 @@ def generate_app_template(self, app: FinalizedAppConfig):
extra_context=extra_context,
)

def create_app_environment(
self,
app: FinalizedAppConfig,
platform: str,
arch: str,
env_manager: EnvManagerT | None | Literal["noop"] = None,
recreate: bool = True,
**kwargs,
) -> VirtualEnvironment:
"""Create an isolated virtual environment in which the app can be built.

:param app: The config object for the app
:param platform: The platform being targeted.
:param arch: The architecture for the environment.
:param env_manager: An explicit environment manager to use. Defaults to the
app's configured environment manager.
:param recreate: If the environment already exists, should it be re-created?
Defaults to True (i.e., recreate by default).
"""
if env_manager is None:
env_manager = app.env_manager
elif env_manager == "noop":
env_manager = None

venv = self.tools.virtual_environment[env_manager](
app=app,
tools=self.tools,
base_path=self.base_path,
name=f"{platform}-{arch}",
platform=platform,
arch=arch,
**kwargs,
)
venv.prepare(recreate=recreate)
return venv

def _unpack_support_package(self, support_file_path, support_path):
"""Unpack a support package into a specific location.

Expand Down Expand Up @@ -571,119 +607,38 @@ def _write_requirements_file(
f"{pip_args}\n", encoding="utf-8"
)

def _pip_requires(self, app: FinalizedAppConfig, requires: list[str]):
"""Convert the list of requirements to be passed to pip into its final form.

:param app: The app configuration
:param requires: The user-specified list of app requirements
:returns: The final list of requirement arguments to pass to pip
"""
return requires

def _extra_pip_args(self, app: FinalizedAppConfig):
"""Any additional arguments that must be passed to pip when installing packages.

:param app: The app configuration
:returns: A list of additional arguments
"""
return self.tools.file.resolve_relative_args(
app.requirement_installer_args,
self.base_path,
)

def _pip_install(
self,
app: FinalizedAppConfig,
app_packages_path: Path,
pip_args: list[str],
install_hint: str = "",
**pip_kwargs,
):
"""Invoke pip to install a set of requirements.

:param app: The app configuration
:param app_packages_path: The full path of the app_packages folder into which
requirements should be installed.
:param pip_args: The list of arguments (including the list of requirements to
install) to pass to pip. This is in addition to the default arguments that
disable pip version checks, forces upgrades, and installs into the nominated
``app_packages`` path.
:param install_hint: Additional hint information to provide in the exception
message if the pip install call fails.
:param pip_kwargs: Any additional keyword arguments to pass to
``subprocess.run`` when invoking pip.
"""
try:
self.tools[app].app_context.run(
[
sys.executable,
"-u",
"-X",
"utf8",
"-m",
"pip",
"install",
"--disable-pip-version-check",
"--upgrade",
"--no-user",
f"--target={app_packages_path}",
]
+ (["-vv"] if self.console.is_deep_debug else [])
+ self._extra_pip_args(app)
+ pip_args,
check=True,
encoding="UTF-8",
**pip_kwargs,
)
except subprocess.CalledProcessError as e:
raise RequirementsInstallError(install_hint=install_hint) from e

def _install_app_requirements(
self,
app: FinalizedAppConfig,
venv: VirtualEnvironment,
requires: list[str],
app_packages_path: Path,
*,
progress_message: str = "Installing app requirements...",
pip_args: list[str] | None = None,
pip_kwargs: dict[str, dict[str, str | None]] | None = None,
install_hint: str = "",
):
"""Install requirements for the app with pip.
"""Install requirements for the app.

:param app: The app configuration
:param venv: The virtual environment where requirements should be installed
:param requires: The list of requirements to install
:param app_packages_path: The full path of the app_packages folder into which
requirements should be installed.
:param progress_message: The waitbar progress message to display to the user.
:param pip_args: Any additional command line arguments to use when invoking pip.
:param pip_kwargs: Any additional keyword arguments to pass to the subprocess
when invoking pip.
:param install_hint: Additional hint information to provide in the exception
message if the pip install call fails.
"""
# Clear existing dependency directory
if app_packages_path.is_dir():
self.tools.shutil.rmtree(app_packages_path)
self.tools.os.mkdir(app_packages_path)

# Install requirements
if requires:
with self.console.wait_bar(progress_message):
self._pip_install(
app,
app_packages_path=app_packages_path,
pip_args=(
([] if pip_args is None else pip_args)
+ self._pip_requires(app, requires)
),
install_hint=install_hint,
**(pip_kwargs or {}),
)
else:
self.console.info("No application requirements.")
with (
self.console.wait_bar("Installing app requirements..."),
):
venv.install_requirements(
requires,
allow_editable=False,
require_binary=self.require_binary_installs,
install_path=app_packages_path,
extra_installer_args=app.requirement_installer_args,
)

def install_app_requirements(self, app: FinalizedAppConfig):
def install_app_requirements(
self,
app: FinalizedAppConfig,
venv: VirtualEnvironment,
):
"""Handle requirements for the app.

This will result in either (in preferential order):
Expand Down Expand Up @@ -723,7 +678,17 @@ def install_app_requirements(self, app: FinalizedAppConfig):
else:
try:
app_packages_path = self.app_packages_path(app)
self._install_app_requirements(app, requires, app_packages_path)
# Clear existing dependency directory
if app_packages_path.is_dir():
self.tools.shutil.rmtree(app_packages_path)
self.tools.os.mkdir(app_packages_path)

if requires:
self._install_app_requirements(
app, venv, requires, app_packages_path
)
else:
self.console.info("No application requirements.")
except KeyError as e:
raise BriefcaseCommandError(
"Application path index file does not define "
Expand Down Expand Up @@ -953,6 +918,11 @@ def create_app(self, app: FinalizedAppConfig, **options):
self.console.info("Generating application template...", prefix=app.app_name)
self.generate_app_template(app=app)

# Verify that the app configuration is valid. This needs to happen *after*
# the template is generated, because verification may require files from
# the template (such as a Dockerfile).
self.verify_app(app)

# External apps (apps that define 'external_package_path') need the packaging
# metadata from the template, but not the app content, dependencies, support
# package etc. App *resources* are installed, because they might be required for
Expand All @@ -972,8 +942,16 @@ def create_app(self, app: FinalizedAppConfig, **options):
prefix=app.app_name,
)
else:
self.console.info("Installing support package...", prefix=app.app_name)
self.install_app_support_package(app=app)
self.console.info("Creating app environment...", prefix=app.app_name)
venv = self.create_app_environment(
app=app,
platform=self.platform,
arch=self.tools.host_arch,
)

if not venv.provides_python:
self.console.info("Installing support package...", prefix=app.app_name)
self.install_app_support_package(app=app)

try:
# If the platform uses a stub binary, the template will define a binary
Expand All @@ -986,21 +964,23 @@ def create_app(self, app: FinalizedAppConfig, **options):
self.console.info("Installing stub binary...", prefix=app.app_name)
self.install_stub_binary(app=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.info("Installing application code...", prefix=app.app_name)
self.install_app_code(app=app)

self.console.info("Installing requirements...", prefix=app.app_name)
self.install_app_requirements(app=app)
self.install_app_requirements(app=app, venv=venv)

self.console.info(
"Installing application resources...", prefix=app.app_name
)
self.install_app_resources(app=app)

if venv.provides_python:
self.console.info(
"Installing managed Python environment...", prefix=app.app_name
)
self.install_managed_python_env(app=app, venv=venv)

self.console.info("Removing unneeded app content...", prefix=app.app_name)
self.cleanup_app_content(app=app)

Expand Down
Loading