Skip to content

Commit

Permalink
Merge pull request #2059 from sarayourfriend/add/requirement-installe…
Browse files Browse the repository at this point in the history
…r-config

Add per-app pip install argument configuration
  • Loading branch information
freakboy3742 authored Dec 11, 2024
2 parents 91e6caa + 8234fd6 commit 4005202
Show file tree
Hide file tree
Showing 13 changed files with 517 additions and 22 deletions.
1 change: 1 addition & 0 deletions changes/1270.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Briefcase now supports per-app configuration of ``pip install`` command line arguments using ``requirement_installer_args``.
53 changes: 53 additions & 0 deletions docs/reference/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,59 @@ multiple paragraphs, if necessary. The long description *must not* be a copy of
the ``description``, or include the ``description`` as the first line of the
``long_description``.

``requirement_installer_args``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

A list of strings of arguments to pass to the requirement installer when building the
app.

Strings will be automatically transformed to absolute paths if they appear to be
relative paths (i.e., starting with ``./`` or ``../``) and resolve to an existing path
relative to the app's configuration file. This is done to support build targets where
the requirement installer command does not run with the same working directory as the
configuration file.

If you encounter a false-positive and need to prevent this transformation,
you may do so by using a single string for the argument name and the value.
Arguments starting with ``-`` will never be transformed, even if they happen to resolve
to an existing path relative to the configuration file.

The following examples will have the relative path transformed to an absolute one when
Briefcase runs the requirement installation command if the path ``wheels`` exists
relative to the configuration file:

.. code-block:: TOML
requirement_installer_args = ["--find-links", "./wheels"]
requirement_installer_args = ["-f", "../wheels"]
On the other hand, the next two examples avoid it because the string starts with ``-``,
does not start with a relative path indication (``./`` or ``../``), or do not resolve
to an existing path:

.. code-block:: TOML
requirement_installer_args = ["-f./wheels"]
requirement_installer_args = ["--find-links=./wheels"]
requirement_installer_args = ["-f", "wheels"]
requirement_installer_args = ["-f", "./this/path/does/not/exist"]
.. admonition:: Supported arguments

The arguments supported in ``requirement_installer_args`` depend on the requirement
installer backend.

The only currently supported requirement installer is ``pip``. As such, the list
should only contain valid
arguments to the ``pip install`` command.

Briefcase does not validate the inputs to this configuration, and will only report
errors directly indicated by the requirement installer backend.

``primary_color``
~~~~~~~~~~~~~~~~~

Expand Down
12 changes: 12 additions & 0 deletions docs/reference/platforms/linux/system.rst
Original file line number Diff line number Diff line change
Expand Up @@ -238,3 +238,15 @@ perform container setup operations as ``root``, switch the container's user to

USER brutus
"""

Platform quirks
===============

Local path references and Docker builds
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Docker builds are not able to reference local paths in the ``requires`` and
``requirement_installer_args`` configurations. This is because the Docker container only
has access to specific file paths on the host system. See `issue #2018
<https://github.com/beeware/briefcase/issues/2081>`__ for more discussion of the
problem, and some possible workarounds.
11 changes: 11 additions & 0 deletions src/briefcase/commands/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,17 @@ def app_requirements_path(self, app: AppConfig) -> Path:
"""
return self.bundle_path(app) / self.path_index(app, "app_requirements_path")

def app_requirement_installer_args_path(self, app: AppConfig) -> Path:
"""Obtain the path into which a newline delimited requirement installer args
file should be written.
:param app: The config object for the app
:return: The full path where the file should be written
"""
return self.bundle_path(app) / self.path_index(
app, "app_requirement_installer_args_path"
)

def app_packages_path(self, app: AppConfig) -> Path:
"""Obtain the path into which requirements should be installed.
Expand Down
62 changes: 50 additions & 12 deletions src/briefcase/commands/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import hashlib
import os
import platform
import re
import shutil
import subprocess
import sys
Expand Down Expand Up @@ -504,31 +505,42 @@ def _write_requirements_file(
app: AppConfig,
requires: list[str],
requirements_path: Path,
requirement_installer_args_path: Path | None,
):
"""Configure application requirements by writing a requirements.txt file.
:param app: The app configuration
:param requires: The full list of requirements
:param requirements_path: The full path to a requirements.txt file that will be
written.
:param requirement_installer_args_path: The full path to where newline
delimited additional requirement installer argumentss should be written if
the template supports it.
"""

with self.input.wait_bar("Writing requirements file..."):
with requirements_path.open("w", encoding="utf-8") as f:
# Add timestamp so build systems (such as Gradle) detect a change
# in the file and perform a re-installation of all requirements.
f.write(f"# Generated {datetime.now()}\n")

if requires:
# Add timestamp so build systems (such as Gradle) detect a change
# in the file and perform a re-installation of all requirements.
f.write(f"# Generated {datetime.now()}\n")
for requirement in requires:
# If the requirement is a local path, convert it to
# absolute, because Flatpak moves the requirements file
# to a different place before using it.
if _is_local_requirement(requirement):
if _is_local_path(requirement):
# We use os.path.abspath() rather than Path.resolve()
# because we *don't* want Path's symlink resolving behavior.
requirement = os.path.abspath(self.base_path / requirement)
f.write(f"{requirement}\n")

if requirement_installer_args_path:
pip_args = "\n".join(self._extra_pip_args(app))
requirement_installer_args_path.write_text(
f"{pip_args}\n", encoding="utf-8"
)

def _pip_requires(self, app: AppConfig, requires: list[str]):
"""Convert the list of requirements to be passed to pip into its final form.
Expand All @@ -544,7 +556,17 @@ def _extra_pip_args(self, app: AppConfig):
:param app: The app configuration
:returns: A list of additional arguments
"""
return []
args: list[str] = []
for argument in app.requirement_installer_args:
to_append = argument
if relative_path_matcher.match(argument) and _is_local_path(argument):
abs_path = os.path.abspath(self.base_path / argument)
if Path(abs_path).exists():
to_append = abs_path

args.append(to_append)

return args

def _pip_install(
self,
Expand Down Expand Up @@ -652,8 +674,21 @@ def install_app_requirements(self, app: AppConfig, test_mode: bool):

try:
requirements_path = self.app_requirements_path(app)
self._write_requirements_file(app, requires, requirements_path)
except KeyError:
requirements_path = None

try:
requirement_installer_args_path = self.app_requirement_installer_args_path(
app
)
except KeyError:
requirement_installer_args_path = None

if requirements_path:
self._write_requirements_file(
app, requires, requirements_path, requirement_installer_args_path
)
else:
try:
app_packages_path = self.app_packages_path(app)
self._install_app_requirements(app, requires, app_packages_path)
Expand Down Expand Up @@ -972,15 +1007,18 @@ def _has_url(requirement):
)


def _is_local_requirement(requirement):
"""Determine if the requirement is a local file path.
def _is_local_path(reference):
"""Determine if the reference is a local file path.
:param requirement: The requirement to check
:returns: True if the requirement is a local file path
:param reference: The reference to check
:returns: True if the reference is a local file path
"""
# Windows allows both / and \ as a path separator in requirements.
# Windows allows both / and \ as a path separator in references.
separators = [os.sep]
if os.altsep:
separators.append(os.altsep)

return any(sep in requirement for sep in separators) and (not _has_url(requirement))
return any(sep in reference for sep in separators) and (not _has_url(reference))


relative_path_matcher = re.compile(r"^\.{1,2}[\\/]")
3 changes: 2 additions & 1 deletion src/briefcase/commands/dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ def install_dev_requirements(self, app: AppConfig, **options):
"--upgrade",
]
+ (["-vv"] if self.logger.is_deep_debug else [])
+ requires,
+ requires
+ app.requirement_installer_args,
check=True,
encoding="UTF-8",
)
Expand Down
4 changes: 4 additions & 0 deletions src/briefcase/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ def __init__(
supported=True,
long_description=None,
console_app=False,
requirement_installer_args: list[str] | None = None,
**kwargs,
):
super().__init__(**kwargs)
Expand Down Expand Up @@ -295,6 +296,9 @@ def __init__(
self.long_description = long_description
self.license = license
self.console_app = console_app
self.requirement_installer_args = (
[] if requirement_installer_args is None else requirement_installer_args
)

if not is_valid_app_name(self.app_name):
raise BriefcaseConfigError(
Expand Down
2 changes: 1 addition & 1 deletion src/briefcase/platforms/iOS/xcode.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ def _extra_pip_args(self, app: AppConfig):
:param app: The app configuration
:returns: A list of additional arguments
"""
return [
return super()._extra_pip_args(app) + [
"--prefer-binary",
"--extra-index-url",
"https://pypi.anaconda.org/beeware/simple",
Expand Down
6 changes: 3 additions & 3 deletions src/briefcase/platforms/linux/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import sys
from pathlib import Path

from briefcase.commands.create import _is_local_requirement
from briefcase.commands.create import _is_local_path
from briefcase.commands.open import OpenCommand
from briefcase.config import AppConfig
from briefcase.exceptions import BriefcaseCommandError, ParseError
Expand Down Expand Up @@ -156,7 +156,7 @@ def _install_app_requirements(

# Iterate over every requirement, looking for local references
for requirement in requires:
if _is_local_requirement(requirement):
if _is_local_path(requirement):
if Path(requirement).is_dir():
# Requirement is a filesystem reference
# Build an sdist for the local requirement
Expand Down Expand Up @@ -210,7 +210,7 @@ def _pip_requires(self, app: AppConfig, requires: list[str]):
final = [
requirement
for requirement in super()._pip_requires(app, requires)
if not _is_local_requirement(requirement)
if not _is_local_path(requirement)
]

# Add in any local packages.
Expand Down
20 changes: 20 additions & 0 deletions tests/commands/create/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,21 @@ def app_requirements_path_index(bundle_path):
tomli_w.dump(index, f)


@pytest.fixture
def app_requirement_installer_args_path_index(bundle_path):
with (bundle_path / "briefcase.toml").open("wb") as f:
index = {
"paths": {
"app_path": "path/to/app",
"app_requirements_path": "path/to/requirements.txt",
"app_requirement_installer_args_path": "path/to/installer-args.txt",
"support_path": "path/to/support",
"support_revision": 37,
}
}
tomli_w.dump(index, f)


@pytest.fixture
def no_support_revision_index(bundle_path):
with (bundle_path / "briefcase.toml").open("wb") as f:
Expand Down Expand Up @@ -309,6 +324,11 @@ def app_requirements_path(bundle_path):
return bundle_path / "path/to/requirements.txt"


@pytest.fixture
def app_requirement_installer_args_path(bundle_path):
return bundle_path / "path/to/installer-args.txt"


@pytest.fixture
def app_packages_path(bundle_path):
return bundle_path / "path/to/app_packages"
Expand Down
1 change: 1 addition & 0 deletions tests/commands/create/test_generate_app_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ def full_context():
"requests": {},
"document_types": {},
"license": {"file": "LICENSE"},
"requirement_installer_args": [],
# Properties of the generating environment
"python_version": platform.python_version(),
"host_arch": "gothic",
Expand Down
Loading

0 comments on commit 4005202

Please sign in to comment.