Skip to content

Commit

Permalink
Add requirement installer argument configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
sarayourfriend committed Nov 16, 2024
1 parent 33b7fa3 commit 3d26baa
Show file tree
Hide file tree
Showing 12 changed files with 696 additions and 21 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``.
47 changes: 47 additions & 0 deletions docs/reference/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,53 @@ 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
39 changes: 27 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 @@ -515,15 +516,16 @@ def _write_requirements_file(

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)
Expand All @@ -544,7 +546,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:
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():
args.append(abs_path)
continue

args.append(argument)

return args

def _pip_install(
self,
Expand Down Expand Up @@ -618,7 +630,7 @@ def _install_app_requirements(
self.tools.os.mkdir(app_packages_path)

# Install requirements
if requires:
if requires or app.requirement_installer_args:
with self.input.wait_bar(progress_message):
self._pip_install(
app,
Expand Down Expand Up @@ -972,15 +984,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}[\\/]")
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
18 changes: 18 additions & 0 deletions src/briefcase/platforms/android/gradle.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,24 @@ def permissions_context(self, app: AppConfig, x_permissions: dict[str, str]):
"features": features,
}

def _write_requirements_file(
self, app: AppConfig, requires: list[str], requirements_path: Path
):
super()._write_requirements_file(app, requires, requirements_path)

# Flatpak runs ``pip install`` using an ``install_requirements.sh`` which Briefcase uses
# to indicate user-configured arguments to the command
pip_options = "\n".join(
[f"# Generated {datetime.datetime.now()}"] + self._extra_pip_args(app)
)

# The file should exist in the same directory as the ``requirements.txt``
pip_options_path = requirements_path.parent / "pip-options.txt"

pip_options_path.unlink(missing_ok=True)

pip_options_path.write_text(pip_options + "\n", encoding="utf-8")


class GradleUpdateCommand(GradleCreateCommand, UpdateCommand):
description = "Update an existing Android Gradle project."
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
35 changes: 35 additions & 0 deletions src/briefcase/platforms/linux/flatpak.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from __future__ import annotations

from datetime import datetime
from pathlib import Path

from briefcase.commands import (
BuildCommand,
CreateCommand,
Expand Down Expand Up @@ -149,6 +152,38 @@ def permissions_context(self, app: AppConfig, x_permissions: dict[str, str]):
"finish_args": finish_args,
}

def _write_requirements_file(
self, app: AppConfig, requires: list[str], requirements_path: Path
):
super()._write_requirements_file(app, requires, requirements_path)

# Flatpak runs ``pip install`` using an ``install_requirements.sh`` which Briefcase uses
# to indicate user-configured arguments to the command

pip_install_command = " ".join(
[
"/app/bin/python3",
"-m",
"pip",
"install",
"--no-cache-dir",
"-r",
"requirements.txt",
# $INSTALL_TARGET populated by the Flatpak manifest command executing ``install_requirements.sh``
'--target="$INSTALL_TARGET"',
]
+ self._extra_pip_args(app)
)

# The file should exist in the same directory as the ``requirements.txt``
install_requirements_path = requirements_path.parent / "install_requirements.sh"

install_requirements_path.unlink(missing_ok=True)

install_requirements_path.write_text(
f"# Generated {datetime.now()}\n{pip_install_command}\n", encoding="utf-8"
)


class LinuxFlatpakUpdateCommand(LinuxFlatpakCreateCommand, UpdateCommand):
description = "Update an existing Linux Flatpak."
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 3d26baa

Please sign in to comment.