summaryrefslogtreecommitdiffstats
path: root/python/mach/mach/requirements.py
diff options
context:
space:
mode:
Diffstat (limited to 'python/mach/mach/requirements.py')
-rw-r--r--python/mach/mach/requirements.py183
1 files changed, 183 insertions, 0 deletions
diff --git a/python/mach/mach/requirements.py b/python/mach/mach/requirements.py
new file mode 100644
index 0000000000..d5141e23f6
--- /dev/null
+++ b/python/mach/mach/requirements.py
@@ -0,0 +1,183 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+import os
+from pathlib import Path
+
+from packaging.requirements import Requirement
+
+THUNDERBIRD_PYPI_ERROR = """
+Thunderbird requirements definitions cannot include PyPI packages.
+""".strip()
+
+
+class PthSpecifier:
+ def __init__(self, path: str):
+ self.path = path
+
+
+class PypiSpecifier:
+ def __init__(self, requirement):
+ self.requirement = requirement
+
+
+class PypiOptionalSpecifier(PypiSpecifier):
+ def __init__(self, repercussion, requirement):
+ super().__init__(requirement)
+ self.repercussion = repercussion
+
+
+class MachEnvRequirements:
+ """Requirements associated with a "site dependency manifest", as
+ defined in "python/sites/".
+
+ Represents the dependencies of a site. The source files consist
+ of colon-delimited fields. The first field
+ specifies the action. The remaining fields are arguments to that
+ action. The following actions are supported:
+
+ pth -- Adds the path given as argument to "mach.pth" under
+ the virtualenv's site packages directory.
+
+ pypi -- Fetch the package, plus dependencies, from PyPI.
+
+ pypi-optional -- Attempt to install the package and dependencies from PyPI.
+ Continue using the site, even if the package could not be installed.
+
+ packages.txt -- Denotes that the specified path is a child manifest. It
+ will be read and processed as if its contents were concatenated
+ into the manifest being read.
+
+ thunderbird-packages.txt -- Denotes a Thunderbird child manifest.
+ Thunderbird child manifests are only activated when working on Thunderbird,
+ and they can cannot have "pypi" or "pypi-optional" entries.
+ """
+
+ def __init__(self):
+ self.requirements_paths = []
+ self.pth_requirements = []
+ self.pypi_requirements = []
+ self.pypi_optional_requirements = []
+ self.vendored_requirements = []
+
+ def pths_as_absolute(self, topsrcdir: str):
+ return [
+ os.path.normcase(Path(topsrcdir) / pth.path)
+ for pth in (self.pth_requirements + self.vendored_requirements)
+ ]
+
+ @classmethod
+ def from_requirements_definition(
+ cls,
+ topsrcdir: str,
+ is_thunderbird,
+ only_strict_requirements,
+ requirements_definition,
+ ):
+ requirements = cls()
+ _parse_mach_env_requirements(
+ requirements,
+ Path(requirements_definition),
+ Path(topsrcdir),
+ is_thunderbird,
+ only_strict_requirements,
+ )
+ return requirements
+
+
+def _parse_mach_env_requirements(
+ requirements_output,
+ root_requirements_path: Path,
+ topsrcdir: Path,
+ is_thunderbird,
+ only_strict_requirements,
+):
+ def _parse_requirements_line(
+ current_requirements_path: Path, line, line_number, is_thunderbird_packages_txt
+ ):
+ line = line.strip()
+ if not line or line.startswith("#"):
+ return
+
+ action, params = line.rstrip().split(":", maxsplit=1)
+ if action == "pth":
+ path = topsrcdir / params
+ if not path.exists():
+ # In sparse checkouts, not all paths will be populated.
+ return
+
+ requirements_output.pth_requirements.append(PthSpecifier(params))
+ elif action == "vendored":
+ requirements_output.vendored_requirements.append(PthSpecifier(params))
+ elif action == "packages.txt":
+ _parse_requirements_definition_file(
+ topsrcdir / params,
+ is_thunderbird_packages_txt,
+ )
+ elif action == "pypi":
+ if is_thunderbird_packages_txt:
+ raise Exception(THUNDERBIRD_PYPI_ERROR)
+
+ requirements_output.pypi_requirements.append(
+ PypiSpecifier(
+ _parse_package_specifier(params, only_strict_requirements)
+ )
+ )
+ elif action == "pypi-optional":
+ if is_thunderbird_packages_txt:
+ raise Exception(THUNDERBIRD_PYPI_ERROR)
+
+ if len(params.split(":", maxsplit=1)) != 2:
+ raise Exception(
+ "Expected pypi-optional package to have a repercussion "
+ 'description in the format "package:fallback explanation", '
+ 'found "{}"'.format(params)
+ )
+ raw_requirement, repercussion = params.split(":")
+ requirements_output.pypi_optional_requirements.append(
+ PypiOptionalSpecifier(
+ repercussion,
+ _parse_package_specifier(raw_requirement, only_strict_requirements),
+ )
+ )
+ elif action == "thunderbird-packages.txt":
+ if is_thunderbird:
+ _parse_requirements_definition_file(
+ topsrcdir / params, is_thunderbird_packages_txt=True
+ )
+ else:
+ raise Exception("Unknown requirements definition action: %s" % action)
+
+ def _parse_requirements_definition_file(
+ requirements_path: Path, is_thunderbird_packages_txt
+ ):
+ """Parse requirements file into list of requirements"""
+ if not requirements_path.is_file():
+ raise Exception(f'Missing requirements file at "{requirements_path}"')
+
+ requirements_output.requirements_paths.append(str(requirements_path))
+
+ with open(requirements_path, "r") as requirements_file:
+ lines = [line for line in requirements_file]
+
+ for number, line in enumerate(lines, start=1):
+ _parse_requirements_line(
+ requirements_path, line, number, is_thunderbird_packages_txt
+ )
+
+ _parse_requirements_definition_file(root_requirements_path, False)
+
+
+class UnexpectedFlexibleRequirementException(Exception):
+ def __init__(self, raw_requirement):
+ self.raw_requirement = raw_requirement
+
+
+def _parse_package_specifier(raw_requirement, only_strict_requirements):
+ requirement = Requirement(raw_requirement)
+
+ if only_strict_requirements and [
+ s for s in requirement.specifier if s.operator != "=="
+ ]:
+ raise UnexpectedFlexibleRequirementException(raw_requirement)
+ return requirement