Skip to content

Commit 9938540

Browse files
Add per-app pip install argument configuration
1 parent 33b7fa3 commit 9938540

File tree

6 files changed

+141
-8
lines changed

6 files changed

+141
-8
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 ``pip_install_arguments``.

docs/reference/configuration.rst

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,56 @@ 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+
``pip_install_arguments``
313+
~~~~~~~~~~~~~~~~~~~~~~~~~
314+
315+
A list of strings of arguments to pass to ``pip install`` when building the app.
316+
317+
To facilitate references to items included in ``sources``, entries are treated as
318+
format strings. The variable ``app_path`` represents the location ``sources``
319+
are copied to.
320+
321+
For compatibility with all build targets, arguments and values should be defined
322+
together in a single string per argument. The following is a correct example:
323+
324+
.. code-block:: TOML
325+
326+
pip_install_arguments = [
327+
"--extra-index-url=https://example.com",
328+
"-f{app_path}/wheels",
329+
]
330+
331+
The following examples are **incorrect** and may unexpectedly fail for some build targets:
332+
333+
.. code-block:: TOML
334+
335+
pip_install_arguments = [
336+
"--extra-index-url", "https://example.com",
337+
"-f", "{app_path}/wheels",
338+
]
339+
340+
pip_install_arguments = [
341+
"--extra-index-url https://example.com",
342+
"-f {app_path}/wheels",
343+
]
344+
345+
.. admonition:: Referencing local paths
346+
347+
Use the ``app_path`` template variable to build paths based on directories copied from ``sources``.
348+
349+
This facilitates a local packages directory as used for the ``--find-links`` option. Given a directory
350+
in the repository root named ``wheels``, the following will make it available to ``pip install``
351+
regardless of the build context:
352+
353+
.. code-block:: TOML
354+
355+
sources = [
356+
"src/<my-app>",
357+
"wheels",
358+
]
359+
360+
pip_install_arguments = ["--find-links={app_path}/wheels"]
361+
312362
``primary_color``
313363
~~~~~~~~~~~~~~~~~
314364

src/briefcase/commands/create.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -515,10 +515,14 @@ def _write_requirements_file(
515515

516516
with self.input.wait_bar("Writing requirements file..."):
517517
with requirements_path.open("w", encoding="utf-8") as f:
518+
# Add timestamp so build systems (such as Gradle) detect a change
519+
# in the file and perform a re-installation of all requirements.
520+
f.write(f"# Generated {datetime.now()}\n")
521+
522+
for arg in self._extra_pip_args(app):
523+
f.write(f"{arg}\n")
524+
518525
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")
522526
for requirement in requires:
523527
# If the requirement is a local path, convert it to
524528
# absolute, because Flatpak moves the requirements file
@@ -544,7 +548,10 @@ def _extra_pip_args(self, app: AppConfig):
544548
:param app: The app configuration
545549
:returns: A list of additional arguments
546550
"""
547-
return []
551+
return [
552+
argument.format(app_path=self.app_path(app))
553+
for argument in app.pip_install_arguments
554+
]
548555

549556
def _pip_install(
550557
self,
@@ -618,7 +625,7 @@ def _install_app_requirements(
618625
self.tools.os.mkdir(app_packages_path)
619626

620627
# Install requirements
621-
if requires:
628+
if requires or app.pip_install_arguments:
622629
with self.input.wait_bar(progress_message):
623630
self._pip_install(
624631
app,

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+
pip_install_arguments: 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.pip_install_arguments = (
300+
[] if pip_install_arguments is None else pip_install_arguments
301+
)
298302

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

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",

tests/commands/create/test_install_app_requirements.py

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,46 @@ def test_app_packages_valid_requires(
162162
assert myapp.test_requires is None
163163

164164

165+
def test_app_packages_pip_install_arguments(
166+
create_command,
167+
myapp,
168+
app_packages_path,
169+
app_path,
170+
app_packages_path_index,
171+
):
172+
"""If an app has a valid list of requirements, pip is invoked."""
173+
myapp.pip_install_arguments = ["--extra-index-url={app_path}/packages"]
174+
myapp.requires = ["my-special-package"]
175+
176+
create_command.install_app_requirements(myapp, test_mode=False)
177+
178+
# A request was made to install requirements
179+
create_command.tools[myapp].app_context.run.assert_called_with(
180+
[
181+
sys.executable,
182+
"-u",
183+
"-X",
184+
"utf8",
185+
"-m",
186+
"pip",
187+
"install",
188+
"--disable-pip-version-check",
189+
"--no-python-version-warning",
190+
"--upgrade",
191+
"--no-user",
192+
f"--target={app_packages_path}",
193+
f"--extra-index-url={app_path}/packages",
194+
"my-special-package",
195+
],
196+
check=True,
197+
encoding="UTF-8",
198+
)
199+
200+
# Original app definitions haven't changed
201+
assert myapp.requires == ["my-special-package"]
202+
assert myapp.test_requires is None
203+
204+
165205
def test_app_packages_valid_requires_no_support_package(
166206
create_command,
167207
myapp,
@@ -427,6 +467,7 @@ def test_app_requirements_no_requires(
427467
myapp,
428468
app_requirements_path,
429469
app_requirements_path_index,
470+
mock_now,
430471
):
431472
"""If an app has no requirements, a requirements file is still written."""
432473
myapp.requires = None
@@ -437,7 +478,7 @@ def test_app_requirements_no_requires(
437478
# requirements.txt doesn't exist either
438479
assert app_requirements_path.exists()
439480
with app_requirements_path.open(encoding="utf-8") as f:
440-
assert f.read() == ""
481+
assert f.read() == f"{GENERATED_DATETIME}\n"
441482

442483
# Original app definitions haven't changed
443484
assert myapp.requires is None
@@ -449,6 +490,7 @@ def test_app_requirements_empty_requires(
449490
myapp,
450491
app_requirements_path,
451492
app_requirements_path_index,
493+
mock_now,
452494
):
453495
"""If an app has an empty requirements list, a requirements file is still
454496
written."""
@@ -460,7 +502,7 @@ def test_app_requirements_empty_requires(
460502
# requirements.txt doesn't exist either
461503
assert app_requirements_path.exists()
462504
with app_requirements_path.open(encoding="utf-8") as f:
463-
assert f.read() == ""
505+
assert f.read() == f"{GENERATED_DATETIME}\n"
464506

465507
# Original app definitions haven't changed
466508
assert myapp.requires == []
@@ -491,6 +533,35 @@ def test_app_requirements_requires(
491533
assert myapp.test_requires is None
492534

493535

536+
def test_app_requirements_pip_install_arguments(
537+
create_command,
538+
myapp,
539+
app_path,
540+
app_requirements_path,
541+
mock_now,
542+
app_requirements_path_index,
543+
):
544+
"""If an app has an empty requirements list, a requirements file is still
545+
written."""
546+
myapp.pip_install_arguments = ["--extra-index-url={app_path}/packages"]
547+
myapp.requires = ["my-favourite-package"]
548+
549+
# Install requirements into the bundle
550+
create_command.install_app_requirements(myapp, test_mode=False)
551+
552+
# requirements.txt doesn't exist either
553+
assert app_requirements_path.exists()
554+
with app_requirements_path.open(encoding="utf-8") as f:
555+
assert (
556+
f.read()
557+
== f"{GENERATED_DATETIME}\n--extra-index-url={app_path}/packages\nmy-favourite-package\n"
558+
)
559+
560+
# Original app definitions haven't changed
561+
assert myapp.requires == ["my-favourite-package"]
562+
assert myapp.test_requires is None
563+
564+
494565
@pytest.mark.parametrize(
495566
"altsep, requirement, expected",
496567
[

0 commit comments

Comments
 (0)