Skip to content

Commit 7547613

Browse files
WIP: Add requirement installer argument configuration
1 parent 33b7fa3 commit 7547613

File tree

12 files changed

+689
-21
lines changed

12 files changed

+689
-21
lines changed

changes/1270.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Briefcase now supports per-app configuration of ``pip install`` command line arguments using ``requirement_installer_args``.

docs/reference/configuration.rst

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,53 @@ multiple paragraphs, if necessary. The long description *must not* be a copy of
309309
the ``description``, or include the ``description`` as the first line of the
310310
``long_description``.
311311

312+
``requirement_installer_args``
313+
~~~~~~~~~~~~~~~~~~~~~~~~~
314+
315+
A list of strings of arguments to pass to the requirement installer when building the app.
316+
317+
Strings will be automatically transformed to absolute paths if they appear to be relative paths
318+
(i.e., starting with ``./`` or ``../``) and resolve to an existing path relative to the app's
319+
configuration file. This is done to support build targets where the requirement installer
320+
command does not run with the same working directory as the configuration file.
321+
322+
If you encounter a false-positive and need to prevent this transformation,
323+
you may do so by using a single string for the argument name and the value.
324+
Arguments starting with ``-`` will never be transformed, even if they happen to resolve to
325+
an existing path relative to the configuration file.
326+
327+
The following examples will have the relative path transformed to an absolute one when Briefcase
328+
runs the requirement installation command if the path ``wheels`` exists relative to the configuration file:
329+
330+
.. code-block:: TOML
331+
332+
requirement_installer_args = ["--find-links", "./wheels"]
333+
334+
requirement_installer_args = ["-f", "../wheels"]
335+
336+
On the other hand, the next two examples avoid it because the string starts with ``-``, does not start with
337+
a relative path indication (``./`` or ``../``), or do not resolve to an existing path:
338+
339+
.. code-block:: TOML
340+
341+
requirement_installer_args = ["-f./wheels"]
342+
343+
requirement_installer_args = ["--find-links=./wheels"]
344+
345+
requirement_installer_args = ["-f", "wheels"]
346+
347+
requirement_installer_args = ["-f", "./this/path/does/not/exist"]
348+
349+
.. admonition:: Supported arguments
350+
351+
The arguments supported in ``requirement_installer_args`` depend on the requirement installer backend.
352+
353+
The only currently supported requirement installer is ``pip``. As such, the list should only contain valid
354+
arguments to the ``pip install`` command.
355+
356+
Briefcase does not validate the inputs to this configuration, and will only report errors directly indicated
357+
by the requirement installer backend.
358+
312359
``primary_color``
313360
~~~~~~~~~~~~~~~~~
314361

src/briefcase/commands/create.py

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import hashlib
44
import os
55
import platform
6+
import re
67
import shutil
78
import subprocess
89
import sys
@@ -515,15 +516,16 @@ def _write_requirements_file(
515516

516517
with self.input.wait_bar("Writing requirements file..."):
517518
with requirements_path.open("w", encoding="utf-8") as f:
519+
# Add timestamp so build systems (such as Gradle) detect a change
520+
# in the file and perform a re-installation of all requirements.
521+
f.write(f"# Generated {datetime.now()}\n")
522+
518523
if requires:
519-
# Add timestamp so build systems (such as Gradle) detect a change
520-
# in the file and perform a re-installation of all requirements.
521-
f.write(f"# Generated {datetime.now()}\n")
522524
for requirement in requires:
523525
# If the requirement is a local path, convert it to
524526
# absolute, because Flatpak moves the requirements file
525527
# to a different place before using it.
526-
if _is_local_requirement(requirement):
528+
if _is_local_path(requirement):
527529
# We use os.path.abspath() rather than Path.resolve()
528530
# because we *don't* want Path's symlink resolving behavior.
529531
requirement = os.path.abspath(self.base_path / requirement)
@@ -544,7 +546,17 @@ def _extra_pip_args(self, app: AppConfig):
544546
:param app: The app configuration
545547
:returns: A list of additional arguments
546548
"""
547-
return []
549+
args: list[str] = []
550+
for argument in app.requirement_installer_args:
551+
if relative_path_matcher.match(argument) and _is_local_path(argument):
552+
abs_path = os.path.abspath(self.base_path / argument)
553+
if Path(abs_path).exists():
554+
args.append(abs_path)
555+
continue
556+
557+
args.append(argument)
558+
559+
return args
548560

549561
def _pip_install(
550562
self,
@@ -618,7 +630,7 @@ def _install_app_requirements(
618630
self.tools.os.mkdir(app_packages_path)
619631

620632
# Install requirements
621-
if requires:
633+
if requires or app.requirement_installer_args:
622634
with self.input.wait_bar(progress_message):
623635
self._pip_install(
624636
app,
@@ -972,15 +984,18 @@ def _has_url(requirement):
972984
)
973985

974986

975-
def _is_local_requirement(requirement):
976-
"""Determine if the requirement is a local file path.
987+
def _is_local_path(reference):
988+
"""Determine if the reference is a local file path.
977989
978-
:param requirement: The requirement to check
979-
:returns: True if the requirement is a local file path
990+
:param reference: The reference to check
991+
:returns: True if the reference is a local file path
980992
"""
981-
# Windows allows both / and \ as a path separator in requirements.
993+
# Windows allows both / and \ as a path separator in references.
982994
separators = [os.sep]
983995
if os.altsep:
984996
separators.append(os.altsep)
985997

986-
return any(sep in requirement for sep in separators) and (not _has_url(requirement))
998+
return any(sep in reference for sep in separators) and (not _has_url(reference))
999+
1000+
1001+
relative_path_matcher = re.compile(r"^\.{1,2}[\\/]")

src/briefcase/config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,7 @@ def __init__(
266266
supported=True,
267267
long_description=None,
268268
console_app=False,
269+
requirement_installer_args: list[str] | None = None,
269270
**kwargs,
270271
):
271272
super().__init__(**kwargs)
@@ -295,6 +296,9 @@ def __init__(
295296
self.long_description = long_description
296297
self.license = license
297298
self.console_app = console_app
299+
self.requirement_installer_args = (
300+
[] if requirement_installer_args is None else requirement_installer_args
301+
)
298302

299303
if not is_valid_app_name(self.app_name):
300304
raise BriefcaseConfigError(

src/briefcase/platforms/android/gradle.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,24 @@ def permissions_context(self, app: AppConfig, x_permissions: dict[str, str]):
282282
"features": features,
283283
}
284284

285+
def _write_requirements_file(
286+
self, app: AppConfig, requires: list[str], requirements_path: Path
287+
):
288+
super()._write_requirements_file(app, requires, requirements_path)
289+
290+
# Flatpak runs ``pip install`` using an ``install_requirements.sh`` which Briefcase uses
291+
# to indicate user-configured arguments to the command
292+
pip_options = "\n".join(
293+
[f"# Generated {datetime.datetime.now()}"] + self._extra_pip_args(app)
294+
)
295+
296+
# The file should exist in the same directory as the ``requirements.txt``
297+
pip_options_path = requirements_path.parent / "pip-options.txt"
298+
299+
pip_options_path.unlink(missing_ok=True)
300+
301+
pip_options_path.write_text(pip_options + "\n", encoding="utf-8")
302+
285303

286304
class GradleUpdateCommand(GradleCreateCommand, UpdateCommand):
287305
description = "Update an existing Android Gradle project."

src/briefcase/platforms/iOS/xcode.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ def _extra_pip_args(self, app: AppConfig):
305305
:param app: The app configuration
306306
:returns: A list of additional arguments
307307
"""
308-
return [
308+
return super()._extra_pip_args(app) + [
309309
"--prefer-binary",
310310
"--extra-index-url",
311311
"https://pypi.anaconda.org/beeware/simple",

src/briefcase/platforms/linux/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import sys
55
from pathlib import Path
66

7-
from briefcase.commands.create import _is_local_requirement
7+
from briefcase.commands.create import _is_local_path
88
from briefcase.commands.open import OpenCommand
99
from briefcase.config import AppConfig
1010
from briefcase.exceptions import BriefcaseCommandError, ParseError
@@ -156,7 +156,7 @@ def _install_app_requirements(
156156

157157
# Iterate over every requirement, looking for local references
158158
for requirement in requires:
159-
if _is_local_requirement(requirement):
159+
if _is_local_path(requirement):
160160
if Path(requirement).is_dir():
161161
# Requirement is a filesystem reference
162162
# Build an sdist for the local requirement
@@ -210,7 +210,7 @@ def _pip_requires(self, app: AppConfig, requires: list[str]):
210210
final = [
211211
requirement
212212
for requirement in super()._pip_requires(app, requires)
213-
if not _is_local_requirement(requirement)
213+
if not _is_local_path(requirement)
214214
]
215215

216216
# Add in any local packages.

src/briefcase/platforms/linux/flatpak.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
from __future__ import annotations
22

3+
from datetime import datetime
4+
from pathlib import Path
5+
36
from briefcase.commands import (
47
BuildCommand,
58
CreateCommand,
@@ -149,6 +152,38 @@ def permissions_context(self, app: AppConfig, x_permissions: dict[str, str]):
149152
"finish_args": finish_args,
150153
}
151154

155+
def _write_requirements_file(
156+
self, app: AppConfig, requires: list[str], requirements_path: Path
157+
):
158+
super()._write_requirements_file(app, requires, requirements_path)
159+
160+
# Flatpak runs ``pip install`` using an ``install_requirements.sh`` which Briefcase uses
161+
# to indicate user-configured arguments to the command
162+
163+
pip_install_command = " ".join(
164+
[
165+
"/app/bin/python3",
166+
"-m",
167+
"pip",
168+
"install",
169+
"--no-cache-dir",
170+
"-r",
171+
"requirements.txt",
172+
# $INSTALL_TARGET populated by the Flatpak manifest command executing ``install_requirements.sh``
173+
'--target="$INSTALL_TARGET"',
174+
]
175+
+ self._extra_pip_args(app)
176+
)
177+
178+
# The file should exist in the same directory as the ``requirements.txt``
179+
install_requirements_path = requirements_path.parent / "install_requirements.sh"
180+
181+
install_requirements_path.unlink(missing_ok=True)
182+
183+
install_requirements_path.write_text(
184+
f"# Generated {datetime.now()}\n{pip_install_command}\n", encoding="utf-8"
185+
)
186+
152187

153188
class LinuxFlatpakUpdateCommand(LinuxFlatpakCreateCommand, UpdateCommand):
154189
description = "Update an existing Linux Flatpak."

tests/commands/create/test_generate_app_template.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ def full_context():
4343
"requests": {},
4444
"document_types": {},
4545
"license": {"file": "LICENSE"},
46+
"requirement_installer_args": [],
4647
# Properties of the generating environment
4748
"python_version": platform.python_version(),
4849
"host_arch": "gothic",

0 commit comments

Comments
 (0)