Skip to content

Commit 1b618b6

Browse files
committed
Dedupe packages in requirements-build.txt
1 parent 6afbceb commit 1b618b6

File tree

3 files changed

+132
-9
lines changed

3 files changed

+132
-9
lines changed

requirements-build.txt

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,21 +42,18 @@ hatch-vcs==0.5.0
4242
# platformdirs
4343
# termcolor
4444
# urllib3
45-
hatchling==1.26.3
45+
hatchling==1.29.0
4646
# via
4747
# hatch-fancy-pypi-readme
4848
# llama-stack-client
4949
# openai
50-
hatchling==1.29.0
51-
# via
5250
# attrs
5351
# banks
5452
# chardet
5553
# einops
5654
# expandvars
5755
# filelock
5856
# fsspec
59-
# hatch-fancy-pypi-readme
6057
# hatch-vcs
6158
# jsonschema
6259
# latex2mathml
@@ -130,9 +127,6 @@ setuptools-scm==10.0.5
130127
# tabulate
131128
# tenacity
132129
# tqdm
133-
setuptools-scm==9.2.2
134-
# via
135-
# hatch-vcs
136130
# urllib3
137131
tomlkit==0.14.0
138132
# via uv-dynamic-versioning
@@ -157,9 +151,8 @@ wheel==0.46.3
157151
# wrapt
158152

159153
# The following packages are considered to be unsafe in a requirements file:
160-
setuptools==82.0.0
161-
# via charset-normalizer
162154
setuptools==82.0.1
155+
# via charset-normalizer
163156
# via
164157
# calver
165158
# certifi
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
#!/usr/bin/env python3
2+
"""Collapse duplicate top-level pins in pip-compile / pybuild-deps output.
3+
4+
``pybuild-deps compile`` can emit multiple ``name==version`` blocks for the same
5+
distribution when different build paths resolve different versions. pip then
6+
sees conflicting requirements; we keep the highest version and merge comments.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
import sys
12+
from collections import defaultdict
13+
from pathlib import Path
14+
15+
from packaging.utils import canonicalize_name
16+
from packaging.version import Version
17+
18+
19+
def parse_pin_line(line: str) -> tuple[str, str] | None:
20+
"""Parse a single ``name==version`` line."""
21+
s = line.strip()
22+
if not s or s.startswith("#"):
23+
return None
24+
if "==" not in s:
25+
return None
26+
name, _, ver = s.partition("==")
27+
name, ver = name.strip(), ver.strip()
28+
if not name or not ver:
29+
return None
30+
return name, ver
31+
32+
33+
def is_pin_line(line: str) -> bool:
34+
"""Return True if ``line`` is a top-level pin (not a comment continuation)."""
35+
if not line.strip():
36+
return False
37+
if line[0].isspace():
38+
return False
39+
return parse_pin_line(line) is not None
40+
41+
42+
def split_segments(lines: list[str]) -> list[tuple[str, list[str]]]:
43+
"""Split into ``('raw', ...)`` or ``('pin', block)`` segments."""
44+
out: list[tuple[str, list[str]]] = []
45+
i = 0
46+
while i < len(lines):
47+
line = lines[i]
48+
if is_pin_line(line):
49+
block = [line]
50+
i += 1
51+
while i < len(lines) and lines[i].startswith((" ", "\t")):
52+
block.append(lines[i])
53+
i += 1
54+
out.append(("pin", block))
55+
else:
56+
out.append(("raw", [line]))
57+
i += 1
58+
return out
59+
60+
61+
def dedupe_pin_blocks(blocks: list[list[str]]) -> list[str]:
62+
"""Pick the highest version and merge ``# via`` comment lines from all blocks."""
63+
64+
def version_key(b: list[str]) -> Version:
65+
parsed = parse_pin_line(b[0])
66+
assert parsed is not None
67+
return Version(parsed[1])
68+
69+
winner = max(blocks, key=version_key)
70+
merged_tail: list[str] = []
71+
seen: set[str] = set()
72+
for b in blocks:
73+
for line in b[1:]:
74+
key = line.strip()
75+
if key and key not in seen:
76+
seen.add(key)
77+
merged_tail.append(line)
78+
return [winner[0], *merged_tail]
79+
80+
81+
def dedupe_file(path: Path) -> None:
82+
"""Rewrite *path* in place with duplicate pins collapsed."""
83+
lines = path.read_text().splitlines(keepends=True)
84+
segments = split_segments(lines)
85+
by_name: dict[str, list[list[str]]] = defaultdict(list)
86+
for kind, payload in segments:
87+
if kind != "pin":
88+
continue
89+
parsed = parse_pin_line(payload[0])
90+
assert parsed is not None
91+
name, _ = parsed
92+
by_name[canonicalize_name(name)].append(payload)
93+
94+
deduped: dict[str, list[str]] = {
95+
k: dedupe_pin_blocks(v) for k, v in by_name.items()
96+
}
97+
98+
out: list[str] = []
99+
seen_pin: set[str] = set()
100+
for kind, payload in segments:
101+
if kind == "raw":
102+
out.extend(payload)
103+
continue
104+
parsed = parse_pin_line(payload[0])
105+
assert parsed is not None
106+
name, _ = parsed
107+
canon = canonicalize_name(name)
108+
if canon in seen_pin:
109+
continue
110+
seen_pin.add(canon)
111+
out.extend(deduped[canon])
112+
113+
path.write_text("".join(out))
114+
115+
116+
def main() -> None:
117+
"""CLI: single argument, path to ``requirements-build.txt``."""
118+
if len(sys.argv) != 2:
119+
print(
120+
"usage: dedupe_requirements_build.py <requirements-build.txt>",
121+
file=sys.stderr,
122+
)
123+
sys.exit(2)
124+
dedupe_file(Path(sys.argv[1]))
125+
126+
127+
if __name__ == "__main__":
128+
main()

scripts/konflux_requirements.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ uv run pybuild-deps compile --output-file="$BUILD_FILE" "$SOURCE_FILE"
9191

9292
# pin maturin to the version available in the Red Hat registry
9393
sed -i 's/maturin==[0-9.]*/maturin==1.10.2/' "$BUILD_FILE"
94+
# pybuild-deps can emit duplicate pins for the same package at different versions
95+
uv run python scripts/dedupe_requirements_build.py "$BUILD_FILE"
9496

9597
# remove intermediate files
9698
rm "$RAW_REQ_FILE" "$WHEEL_FILE" "$WHEEL_FILE_PYPI" "$SOURCE_FILE"

0 commit comments

Comments
 (0)