pyproject-rpm-macros/pyproject_patch_metadata.py
Charalampos Stratakis b7f03fc84c Add %pyproject_patch_dependency macro for overriding dependency constraints
Overrides are declared in %prep and automatically apply to both
BuildRequires (via %pyproject_buildrequires) and runtime Requires
(by patching installed .dist-info/METADATA during %pyproject_install).

Supported actions: drop_upper, drop_lower, drop_constraints,
set_upper, set_lower, and ignore. For == and ~=, drop_upper/drop_lower
decompose into half-bounds following PEP 440 semantics.

The :br_only suffix restricts an override to BuildRequires only,
for multi-wheel specs where a sibling package must be kept as a
runtime dependency.

Resolves: rhbz#2386906

Assisted-by: Claude (Anthropic)
Made-with: Cursor
(cherry picked from Fedora commit 5d9798f289)
2026-08-19 17:32:22 +02:00

135 lines
3.9 KiB
Python

"""Patch .dist-info/METADATA files based on dependency override specifications.
Called from %pyproject_install to apply runtime dependency overrides
to installed wheel metadata, so that pythondistdeps.py generates
correct Requires.
"""
import argparse
import sys
def print_err(*args, **kwargs):
kwargs.setdefault('file', sys.stderr)
print(*args, **kwargs)
try:
from packaging.requirements import Requirement
from packaging.specifiers import SpecifierSet
from packaging.utils import canonicalize_name
except ImportError as e:
print_err('Import error:', e)
sys.exit(1)
# uses packaging, needs to be imported after packaging is verified to be present
from pyproject_dependency_overrides import (
parse_override_string, apply_overrides_to_specifiers,
)
def parse_overrides(override_lines):
"""Parse override lines, skipping br_only entries."""
overrides = {}
for line in override_lines:
parts = line.split(':')
if parts and parts[-1].strip() == 'br_only':
continue
package, action, value = parse_override_string(line)
overrides.setdefault(package, []).append(
{'action': action, 'value': value})
return overrides
def apply_overrides_to_requirement(req, overrides_for_pkg):
"""Apply overrides to a single Requirement object. Returns None if ignored."""
for override in overrides_for_pkg:
if override['action'] == 'ignore':
return None
specifiers = apply_overrides_to_specifiers(
list(req.specifier), overrides_for_pkg)
if specifiers:
req.specifier = SpecifierSet(','.join(str(s) for s in specifiers))
else:
req.specifier = SpecifierSet()
return req
def patch_metadata(metadata_path, overrides):
"""Read METADATA, apply overrides to Requires-Dist lines, and rewrite."""
if not overrides:
return
with open(metadata_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
new_lines = []
patched = False
for line in lines:
if line.startswith('Requires-Dist:'):
req_str = line[len('Requires-Dist:'):].strip()
try:
req = Requirement(req_str)
except Exception:
new_lines.append(line)
continue
name = canonicalize_name(req.name)
if name in overrides:
result = apply_overrides_to_requirement(req, overrides[name])
if result is None:
print_err(f'Removing dependency {name} from METADATA per override')
patched = True
continue
new_line = f'Requires-Dist: {result}\n'
if new_line != line:
print_err(f'Patching dependency {name} in METADATA: {req_str.strip()} -> {result}')
patched = True
new_lines.append(new_line)
else:
new_lines.append(line)
else:
new_lines.append(line)
if patched:
with open(metadata_path, 'w', encoding='utf-8') as f:
f.writelines(new_lines)
print_err(f'Patched {metadata_path}')
def main(argv=None):
parser = argparse.ArgumentParser(
description='Patch .dist-info/METADATA based on dependency overrides')
parser.add_argument(
'--overrides', required=True,
help='Path to the dependency overrides file')
parser.add_argument(
'--metadata', required=True,
help='Path to the .dist-info/METADATA file')
args = parser.parse_args(argv)
try:
with open(args.overrides, 'r', encoding='utf-8') as f:
override_lines = f.read().split()
except FileNotFoundError:
return
try:
overrides = parse_overrides(override_lines)
except ValueError as e:
print_err(f'ERROR: {e}')
sys.exit(1)
if overrides:
patch_metadata(args.metadata, overrides)
if __name__ == '__main__':
main()