Skip to content

Commit c7359c3

Browse files
committed
Conda apps working on macOS.
1 parent 51ebc60 commit c7359c3

12 files changed

Lines changed: 113 additions & 83 deletions

File tree

src/briefcase/commands/base.py

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ class BaseCommand(ABC):
156156
def __init__(
157157
self,
158158
console: Console,
159-
tools: ToolCache = None,
159+
tools: ToolCache | None = None,
160160
apps: dict[str, AppConfig] | None = None,
161161
base_path: Path | None = None,
162162
data_path: Path | None = None,
@@ -725,15 +725,6 @@ def verify_tools(self):
725725
"""
726726
return
727727

728-
def verify_env_manager(self, app: AppConfig):
729-
"""Verify that the requested environment manager can be used."""
730-
if app.env_manager not in self.supported_env_managers:
731-
raise BriefcaseConfigError(
732-
f"{app.app_name!r} declares the use of a {app.env_manager!r} "
733-
f"environment, but {self.platform} {self.output_format} "
734-
f"projects do not support environments of that type."
735-
)
736-
737728
def finalize_app_config(self, app: DraftAppConfig, **kwargs) -> FinalizedAppConfig:
738729
"""Finalize the application config.
739730
@@ -746,14 +737,21 @@ def finalize_app_config(self, app: DraftAppConfig, **kwargs) -> FinalizedAppConf
746737
configuration, and performs any other app-specific platform configuration and
747738
verification that is required as a result of command-line arguments.
748739
749-
Platform overrides should call ``super().finalize_app_config(app, **kwargs)``
750-
to construct the ``FinalizedAppConfig``.
740+
Platform overrides should call `super().finalize_app_config(app, **kwargs)`
741+
to construct the `FinalizedAppConfig`.
751742
752743
:param app: The app configuration to finalize.
753744
:param kwargs: Runtime attributes forwarded to the FinalizedAppConfig
754-
constructor (``test_mode``, ``debugger``, etc.).
745+
constructor (`test_mode`, `debugger`, etc.).
755746
:returns: The finalized app configuration.
756747
"""
748+
if app.env_manager not in self.supported_env_managers:
749+
raise BriefcaseConfigError(
750+
f"{app.app_name!r} declares the use of a {app.env_manager!r} "
751+
f"environment, but {self.platform} {self.output_format} "
752+
f"projects do not support environments of that type."
753+
)
754+
757755
return FinalizedAppConfig(app, **kwargs)
758756

759757
def resolve_apps(
@@ -809,8 +807,6 @@ def finalize(
809807
finalized: dict[str, FinalizedAppConfig] = {}
810808
for app in apps:
811809
if not isinstance(app, FinalizedAppConfig):
812-
self.verify_env_manager(app)
813-
814810
app = self.finalize_app_config(
815811
app,
816812
test_mode=test_mode,

src/briefcase/commands/create.py

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -120,26 +120,22 @@ def support_package_url(self, support_revision: str) -> str:
120120
f"{self.support_package_filename(support_revision)}"
121121
)
122122

123-
def stub_binary_filename(self, support_revision: str, is_console_app: bool) -> str:
123+
def stub_binary_filename(
124+
self,
125+
support_revision: str,
126+
app: FinalizedAppConfig,
127+
) -> str:
124128
"""The filename for the stub binary."""
125-
stub_type = "Console" if is_console_app else "GUI"
126-
win_suffix = (
127-
f"-{self.tools.host_arch.lower()}"
128-
if self.tools.host_os == "Windows"
129-
else ""
130-
)
131-
return (
132-
f"{stub_type}-Stub-{self.python_version_tag}{win_suffix}"
133-
f"-b{support_revision}.zip"
134-
)
129+
stub_type = "Console" if app.console_app else "GUI"
130+
return f"{stub_type}-Stub-{self.python_version_tag}-b{support_revision}.zip"
135131

136-
def stub_binary_url(self, support_revision: str, is_console_app: bool) -> str:
132+
def stub_binary_url(self, support_revision: str, app: FinalizedAppConfig) -> str:
137133
"""The URL of the stub binary to use for apps of this type."""
138134
return (
139135
"https://briefcase-support.s3.amazonaws.com/python/"
140136
f"{self.python_version_tag}/"
141137
f"{self.platform}/"
142-
f"{self.stub_binary_filename(support_revision, is_console_app)}"
138+
f"{self.stub_binary_filename(support_revision, app)}"
143139
)
144140

145141
def icon_targets(self, app: FinalizedAppConfig):
@@ -296,15 +292,14 @@ def app_environment(
296292
app: FinalizedAppConfig,
297293
host_arch: str | None = None,
298294
) -> dict[str, VirtualEnvironment]:
299-
"""Create an isolated virtual environment for the."""
295+
"""Create an isolated virtual environment in which the app can be built."""
300296
if host_arch is None:
301297
host_arch = self.tools.host_arch
302298

303-
env_name = f"{app.env_manager}-{self.tools.host_os}-{self.tools.host_arch}"
304-
venv = self.tools.virtual_environment(
305-
env_manager=app.env_manager,
299+
env_name = f"{app.env_manager}-{self.platform}-{self.tools.host_arch}"
300+
venv = self.tools.virtual_environment[app.env_manager](
301+
tools=self.tools,
306302
venv_path=self.base_path / ".briefcase/" / app.app_name / env_name,
307-
isolated=True,
308303
platform=self.platform,
309304
arch=host_arch,
310305
)
@@ -510,9 +505,7 @@ def _download_stub_binary(self, app: FinalizedAppConfig) -> Path:
510505
except AttributeError:
511506
stub_binary_revision = self.stub_binary_revision(app)
512507

513-
stub_binary_url = self.stub_binary_url(
514-
stub_binary_revision, app.console_app
515-
)
508+
stub_binary_url = self.stub_binary_url(stub_binary_revision, app=app)
516509
custom_stub_binary = False
517510
self.console.info(f"Using stub binary {stub_binary_url}")
518511

@@ -947,6 +940,7 @@ def create_app(self, app: FinalizedAppConfig, **options):
947940
prefix=app.app_name,
948941
)
949942
else:
943+
self.console.info("Creating app environment...", prefix=app.app_name)
950944
venv = self.app_environment(app=app)
951945

952946
if not venv.provides_python:
@@ -983,7 +977,7 @@ def create_app(self, app: FinalizedAppConfig, **options):
983977
self.console.info(
984978
"Installing managed Python environment...", prefix=app.app_name
985979
)
986-
self.install_managed_python_env(app=app)
980+
self.install_managed_python_env(app=app, venv=venv)
987981

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

src/briefcase/commands/dev.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -291,14 +291,16 @@ def __call__(
291291

292292
if isolated:
293293
self.console.info("Activating dev environment...", prefix=app.app_name)
294+
env_manager = app.env_manager
295+
else:
296+
env_manager = None
294297

295-
venv = self.tools.virtual_environment(
296-
env_manager=app.env_manager,
298+
venv = self.tools.virtual_environment[env_manager](
299+
tools=self.tools,
297300
venv_path=(
298301
self.base_path
299302
/ f".briefcase/{app.app_name}/{app.env_manager}-{self.venv_name}"
300303
),
301-
isolated=isolated,
302304
)
303305
created = venv.prepare(recreate=update_requirements)
304306

src/briefcase/integrations/virtual_environment/base.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
class VirtualEnvironment(ABC):
1212
"""A managed Python environment."""
1313

14+
provides_python: bool = False
15+
1416
def __init__(
1517
self,
1618
tools: ToolCache,
@@ -33,10 +35,6 @@ def __init__(
3335
self.platform = platform
3436
self.arch = arch
3537

36-
@property
37-
def provides_python(self) -> bool:
38-
return False
39-
4038
@property
4139
def bin_dir(self) -> Path:
4240
"""The venv's binary directory (`bin` on POSIX, `Scripts` on Windows)."""

src/briefcase/integrations/virtual_environment/conda.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,7 @@ class CondaVirtualEnvironment(VirtualEnvironment):
1919
(prepending the environment's binary directory to ``PATH``).
2020
"""
2121

22-
@property
23-
def provides_python(self) -> bool:
24-
return False
22+
provides_python: bool = True
2523

2624
@property
2725
def python_version(self) -> str:
@@ -71,12 +69,15 @@ def prepare(self, recreate=False) -> bool:
7169
)
7270
if self.platform:
7371
match self.platform, self.arch:
74-
case "darwin", "arm64":
72+
case "macOS", "arm64":
7573
conda_subdir = "osx-arm64"
76-
case "darwin", "x86_64":
74+
case "macOS", "x86_64":
7775
conda_subdir = "osx-64"
7876
case _, _:
79-
raise BriefcaseCommandError("Unsupported platform")
77+
raise BriefcaseCommandError(
78+
"Briefcase cannot create a Conda environment "
79+
f"for {self.platform} {self.arch}"
80+
)
8081

8182
self.tools.subprocess.run(
8283
[

src/briefcase/integrations/virtual_environment/pixi.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,7 @@ class PixiVirtualEnvironment(VirtualEnvironment):
1919
directory and Python executable are resolved relative to that location.
2020
"""
2121

22-
@property
23-
def provides_python(self) -> bool:
24-
return False
22+
provides_python: bool = True
2523

2624
@property
2725
def python_version(self) -> str:

src/briefcase/integrations/virtual_environment/tool.py

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
from pathlib import Path
2-
31
from briefcase.config import EnvManagerT
42
from briefcase.integrations.base import Tool, ToolCache
53
from briefcase.integrations.virtual_environment.base import VirtualEnvironment
@@ -24,15 +22,10 @@ def verify_install(cls, tools: ToolCache, **kwargs) -> "VirtualEnvironmentManage
2422
tools.virtual_environment = VirtualEnvironmentManager(tools=tools)
2523
return tools.virtual_environment
2624

27-
def __call__(
25+
def __getitem__(
2826
self,
29-
venv_path: Path,
30-
*,
31-
isolated: bool = True,
32-
platform: str | None = None,
33-
arch: str | None = None,
34-
env_manager: EnvManagerT = "venv",
35-
) -> VirtualEnvironment:
27+
env_manager: EnvManagerT | None = "venv",
28+
) -> type[VirtualEnvironment]:
3629
"""Construct and return a `VirtualEnvironment` for the requested mode.
3730
3831
The constructor of the returned object performs the lifecycle work
@@ -52,19 +45,10 @@ def __call__(
5245
:raises BriefcaseCommandError: if the environment cannot be created or
5346
initialised.
5447
"""
55-
if not isolated:
56-
env_manager = None
57-
58-
venv: VirtualEnvironment = {
48+
return {
5949
None: NoOpVirtualEnvironment,
6050
"uv": UvVirtualEnvironment,
51+
"venv": VenvVirtualEnvironment,
6152
"conda": CondaVirtualEnvironment,
6253
"pixi": PixiVirtualEnvironment,
63-
}.get(env_manager, VenvVirtualEnvironment)(
64-
self.tools,
65-
venv_path,
66-
platform=platform,
67-
arch=arch,
68-
)
69-
70-
return venv
54+
}[env_manager]

src/briefcase/integrations/virtual_environment/uv.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -147,17 +147,21 @@ def install_requirements(
147147
that can be displayed to the user.
148148
"""
149149
uv_deps = []
150+
has_source_deps = False
150151
for req in requires:
151152
# Any requirement that is a local path, but *not* a reference to an archive
152153
# file (zip, tgz, etc) or wheel can be installed editable. If in doubt,
153154
# install non-editable.
154155
if (
155-
allow_editable
156-
and self.tools.file.is_local_path(req)
156+
self.tools.file.is_local_path(req)
157157
and not self.tools.file.is_archive(req)
158158
and Path(req).suffix != ".whl"
159159
):
160-
uv_deps.extend(["--no-binary", req, "-e", req])
160+
has_source_deps = True
161+
if allow_editable:
162+
uv_deps.extend(["-e", req])
163+
else:
164+
uv_deps.append(req)
161165
else:
162166
uv_deps.append(req)
163167

@@ -172,7 +176,10 @@ def install_requirements(
172176
if min_os_version and self.platform == "darwin":
173177
env = {"MACOSX_DEPLOYMENT_TARGET": min_os_version}
174178

175-
if require_binary:
179+
if require_binary and not has_source_deps:
180+
# uv can't install a local directory if `--only-binary` is specified.
181+
# --only-binary is *required* for normal pip when --platform is used;
182+
# but it's not required for uv when using `--python-platform`.
176183
install_args.extend(["--only-binary", ":all:"])
177184

178185
if not include_deps:

src/briefcase/platforms/macOS/__init__.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,31 @@ def generate_app_template(self, app: FinalizedAppConfig):
206206
# picked up on the next run of any Briefcase command).
207207
self.verify_not_on_icloud(app, cleanup=True)
208208

209+
def output_format_template_context(self, app: FinalizedAppConfig):
210+
"""Additional template context required by the output format.
211+
212+
:param app: The config object for the app
213+
"""
214+
# If the environment manager provides Python, it will be in `libPython3.X`
215+
# format, rather than Python.XCframework format.
216+
venv_class = self.tools.virtual_environment[app.env_manager]
217+
return {
218+
"use_framework": not venv_class.provides_python,
219+
}
220+
221+
def stub_binary_filename(
222+
self,
223+
support_revision: str,
224+
app: FinalizedAppConfig,
225+
) -> str:
226+
"""The filename for the stub binary."""
227+
stub_type = "Console" if app.console_app else "GUI"
228+
venv_class = self.tools.virtual_environment[app.env_manager]
229+
if self.tools.host_os == "Darwin" and venv_class.provides_python:
230+
stub_type = f"L{stub_type}"
231+
232+
return f"{stub_type}-Stub-{self.python_version_tag}-b{support_revision}.zip"
233+
209234
def _install_app_requirements(
210235
self,
211236
app: FinalizedAppConfig,
@@ -331,6 +356,9 @@ def _install_app_requirements(
331356
other_suffix=f"_{other_arch}",
332357
)
333358
if binary_packages:
359+
self.console.info(
360+
f"Creating {other_arch} app environment...", prefix=app.app_name
361+
)
334362
other_venv = self.app_environment(app, host_arch=other_arch)
335363
with self.console.wait_bar(
336364
f"Installing binary app requirements for {other_arch}..."

src/briefcase/platforms/macOS/app.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
UpdateCommand,
1414
)
1515
from briefcase.config import FinalizedAppConfig
16+
from briefcase.integrations.virtual_environment import VirtualEnvironment
1617
from briefcase.platforms.macOS import (
1718
SigningIdentity,
1819
macOSCreateMixin,
@@ -87,14 +88,18 @@ def install_app_resources(self, app: FinalizedAppConfig):
8788
# cache for the app, ensuring the current icon is loaded.
8889
self.binary_path(app).touch(exist_ok=True)
8990

90-
def install_managed_python_env(self, app: FinalizedAppConfig):
91+
def install_managed_python_env(
92+
self,
93+
app: FinalizedAppConfig,
94+
venv: VirtualEnvironment,
95+
):
9196
"""Copy the managed Python environment into the app bundle."""
9297
runtime_support_path = self.support_path(app, runtime=True)
9398
if runtime_support_path.is_dir():
9499
self.tools.shutil.rmtree(runtime_support_path)
95100

96101
self.tools.shutil.copytree(
97-
self.venv_path(app),
102+
venv.venv_path,
98103
runtime_support_path,
99104
symlinks=True,
100105
)

0 commit comments

Comments
 (0)