commit 8fa480f29a0843345e6f117c1a2be6ff4d6b1795
parent eade387da45eb7b220989935c9b587b2cf4d2141
Author: Alex Hochheiden <ahochheiden@mozilla.com>
Date: Fri, 14 Nov 2025 14:23:32 +0000
Bug 1999798 - Add 'requirements-txt' specifier to support `requirement.txt` files in `<site>.txt` files r=ahal
Differential Revision: https://phabricator.services.mozilla.com/D272408
Diffstat:
3 files changed, 56 insertions(+), 1 deletion(-)
diff --git a/python/mach/mach/requirements.py b/python/mach/mach/requirements.py
@@ -28,6 +28,11 @@ class PypiOptionalSpecifier(PypiSpecifier):
self.repercussion = repercussion
+class RequirementsTxtSpecifier:
+ def __init__(self, path: str):
+ self.path = path
+
+
class MachEnvRequirements:
"""Requirements associated with a "site dependency manifest", as
defined in "python/sites/".
@@ -48,6 +53,10 @@ class MachEnvRequirements:
pypi-optional -- Attempt to install the package and dependencies from PyPI.
Continue using the site, even if the package could not be installed.
+ requirements-txt -- Specifies a path to a 'requirements.txt' lockfile.
+ All requirements from the lockfile will be installed with hash
+ validation. All entries must include hashes.
+
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.
@@ -65,6 +74,7 @@ class MachEnvRequirements:
self.pypi_optional_requirements = []
self.vendored_requirements = []
self.vendored_fallback_requirements = []
+ self.requirements_txt_files = []
def pths_as_absolute(self, topsrcdir: str):
return [
@@ -189,13 +199,25 @@ def _parse_mach_env_requirements(
_parse_package_specifier(raw_requirement, only_strict_requirements),
)
)
+ elif action == "requirements-txt":
+ if is_thunderbird_packages_txt:
+ raise Exception(THUNDERBIRD_PYPI_ERROR)
+
+ requirements_txt_path = topsrcdir / params
+ if not requirements_txt_path.exists():
+ raise Exception(
+ f"requirements.txt file not found: {requirements_txt_path}"
+ )
+
+ req_txt_specifier = RequirementsTxtSpecifier(str(requirements_txt_path))
+ requirements_output.requirements_txt_files.append(req_txt_specifier)
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)
+ raise Exception(f"Unknown requirements definition action: {action}")
def _parse_requirements_definition_file(
requirements_path: Path, is_thunderbird_packages_txt
diff --git a/python/mach/mach/site.py b/python/mach/mach/site.py
@@ -1534,6 +1534,10 @@ def _create_venv_with_pthfile(
os.environ["VIRTUAL_ENV"] = virtualenv_root
if populate_with_pip:
+ for requirements_txt_file in requirements.requirements_txt_files:
+ target_venv.pip_install(
+ ["--requirement", requirements_txt_file.path, "--require-hashes"]
+ )
if requirements.pypi_requirements:
requirements_list = [
str(req.requirement) for req in requirements.pypi_requirements
@@ -1568,6 +1572,16 @@ def _is_venv_up_to_date(
False, f'"{dep_file}" has changed since the virtualenv was created'
)
+ for requirements_txt_file in requirements.requirements_txt_files:
+ req_txt_path = requirements_txt_file.path
+ if (
+ os.path.exists(req_txt_path)
+ and os.path.getmtime(req_txt_path) > metadata_mtime
+ ):
+ return SiteUpToDateResult(
+ False, f'"{req_txt_path}" has changed since the virtualenv was created'
+ )
+
try:
existing_metadata = MozSiteMetadata.from_path(target_venv.prefix)
except MozSiteMetadataOutOfDateError as e:
diff --git a/python/mozbuild/mozbuild/lockfiles/site_dependency_extractor.py b/python/mozbuild/mozbuild/lockfiles/site_dependency_extractor.py
@@ -49,6 +49,7 @@ class SiteDependencyExtractor:
"pypi-optional": self._handle_pypi,
"vendored": self._handle_vendored,
"vendored-fallback": self._handle_vendored_fallback,
+ "requirements-txt": self._handle_requirements_txt,
}
for raw in self.site_file.read_text().splitlines():
@@ -121,6 +122,24 @@ class SiteDependencyExtractor:
raise DependencyParseError(f"Missing version in pypi spec: '{pkg_spec}'")
self.dependencies.append(Dependency(name=name, version=version, path=None))
+ def _handle_requirements_txt(self, rest: str) -> None:
+ requirements_txt_path = Path(self.topsrcdir) / rest
+ if not requirements_txt_path.exists():
+ raise DependencyParseError(
+ f"requirements.txt file not found: {requirements_txt_path}"
+ )
+
+ with requirements_txt_path.open(encoding="utf-8") as f:
+ for raw_line in f:
+ line = raw_line.strip()
+
+ if not line or line.startswith("#") or line.startswith("-"):
+ continue
+
+ pypi_requirement_spec = line.split("\\")[0].strip().rstrip()
+ if pypi_requirement_spec:
+ self._handle_pypi(pypi_requirement_spec)
+
def _version_from_metadata_files(self, path: Path) -> Optional[str]:
def _extract_version(file_path: Path) -> Optional[str]:
try: