Skip to content

Commit 3f2844f

Browse files
committed
pip: Support requirements file with hashes
Fixes: #526
1 parent 00db960 commit 3f2844f

1 file changed

Lines changed: 81 additions & 20 deletions

File tree

pip/flatpak-pip-generator.py

Lines changed: 81 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -299,21 +299,59 @@ def make_source(
299299
return source
300300

301301

302+
def parse_req_hashes(raw_lines: list[str]) -> dict[str, set[str]]:
303+
result: dict[str, set[str]] = {}
304+
for line in raw_lines:
305+
hashes = set(re.findall(r"--hash=sha256:([a-f0-9]+)", line))
306+
if not hashes:
307+
continue
308+
name_part = re.split(r"[=\s;]", line.split("--hash")[0].strip())[0]
309+
if not name_part:
310+
continue
311+
normalized = normalize_name(name_part)
312+
result.setdefault(normalized, set()).update(hashes)
313+
return result
314+
315+
302316
def resolve_package_sources(
303317
name: str,
304318
version: str,
305319
candidates: list[str],
306320
is_preferred: bool,
321+
known_hashes: set[str] | None = None,
307322
) -> tuple[list[dict], list[str]]:
323+
sources_out: list[dict] = []
308324
pypi_files: list[dict] | None = None
309325

326+
def fetch_pypi_files(name: str, version: str) -> list[dict]:
327+
url = f"https://pypi.org/pypi/{name}/{version}/json"
328+
print(f"Fetching PyPI metadata for {name}=={version}")
329+
with urllib.request.urlopen(url) as response: # noqa: S310
330+
return json.loads(response.read().decode("utf-8"))["urls"]
331+
310332
def get_pypi_files() -> list[dict]:
311333
nonlocal pypi_files
312334
if pypi_files is None:
313-
url = f"https://pypi.org/pypi/{name}/{version}/json"
314-
print(f"Fetching PyPI metadata for {name}=={version}")
315-
with urllib.request.urlopen(url) as response: # noqa: S310
316-
pypi_files = json.loads(response.read().decode("utf-8"))["urls"]
335+
all_files = fetch_pypi_files(name, version)
336+
if known_hashes:
337+
pypi_hashes = {f["digests"]["sha256"] for f in all_files}
338+
if known_hashes != pypi_hashes:
339+
print(
340+
f"\nWARNING: Requirements file does not include hashes for all "
341+
f"artifacts for {name}=={version}. Resolution may be"
342+
"restricted.\n"
343+
)
344+
missing = known_hashes - {f["digests"]["sha256"] for f in all_files}
345+
if missing:
346+
sys.exit(
347+
f"ERROR: Hash(es) {missing} for {name}=={version} "
348+
"not found on PyPI. Aborting."
349+
)
350+
pypi_files = [
351+
f for f in all_files if f["digests"]["sha256"] in known_hashes
352+
]
353+
else:
354+
pypi_files = all_files
317355
return pypi_files
318356

319357
def is_universal(filename: str) -> bool:
@@ -440,12 +478,17 @@ def collect(compat_fn) -> list[dict]:
440478
if any(is_platform_wheel(f["filename"]) for f in get_pypi_files()):
441479
return [], [f"__PLATFORM_ONLY__:{name}"]
442480

443-
return [], [f"{name}: No suitable source found on PyPI"]
481+
if known_hashes:
482+
return [], [
483+
f"{name}: No suitable source found among artifacts matching the "
484+
"hashes in the requirements file."
485+
]
486+
else:
487+
return [], [f"{name}: No suitable source found on PyPI"]
444488

445489
assert SUPPORTED_TAG_SET is not None
446490
native_py_ver = runtime_python_ver(SUPPORTED_TAG_SET)
447491

448-
sources_out: list[dict] = []
449492
errors: list[str] = []
450493

451494
for arch in DEFAULT_WHEEL_ARCHES:
@@ -461,7 +504,15 @@ def collect(compat_fn) -> list[dict]:
461504
arch_candidates = arch_platform_candidates(arch, cast(int, py_ver))
462505

463506
if not arch_candidates:
464-
errors.append(f"{name}: No platform wheel found for arch '{arch}' on PyPI")
507+
if known_hashes:
508+
errors.append(
509+
f"{name}: No compatible platform wheel for arch '{arch}' among artifacts matching "
510+
"the hashes in the requirements file."
511+
)
512+
else:
513+
errors.append(
514+
f"{name}: No platform wheel found for arch '{arch}' on PyPI"
515+
)
465516
continue
466517

467518
wheel = max(
@@ -720,28 +771,37 @@ def to_ver(v: str) -> Version | None:
720771

721772

722773
packages = []
774+
req_hashes_by_pkg: dict[str, set[str]] = {}
775+
723776
if opts.requirements_file:
724777
requirements_file_input = os.path.expanduser(opts.requirements_file)
725778
try:
726779
with open(requirements_file_input) as in_req_file:
727-
reqs = parse_continuation_lines(in_req_file)
780+
raw_lines = list(parse_continuation_lines(in_req_file))
781+
req_hashes_by_pkg = parse_req_hashes(raw_lines)
728782
reqs_as_str = handle_req_env_markers(
729-
"\n".join([r.split("--hash")[0] for r in reqs])
783+
"\n".join(r.split("--hash")[0] for r in raw_lines)
730784
)
731-
reqs_list_raw = reqs_as_str.splitlines()
732-
py_version_regex = re.compile(
733-
r";.*python_version .+$"
734-
) # Remove when pip-generator can handle python_version
735-
reqs_list = [py_version_regex.sub("", p) for p in reqs_list_raw]
785+
py_version_regex = re.compile(r";.*python_version .+$")
786+
reqs_list = [
787+
py_version_regex.sub("", line) for line in reqs_as_str.splitlines()
788+
]
736789
if opts.ignore_pkg:
737-
reqs_new = "\n".join(i for i in reqs_list if i not in opts.ignore_pkg)
738-
else:
739-
reqs_new = reqs_as_str
740-
packages = list(requirements.parse(reqs_new))
790+
reqs_list = [
791+
line
792+
for line in reqs_list
793+
if line.strip().split("==")[0].strip() not in opts.ignore_pkg
794+
]
795+
raw_lines = [
796+
line
797+
for line in raw_lines
798+
if line.strip().split("==")[0].strip() not in opts.ignore_pkg
799+
]
800+
packages = list(requirements.parse("\n".join(reqs_list)))
741801
with tempfile.NamedTemporaryFile(
742802
"w", delete=False, prefix="requirements."
743803
) as temp_req_file:
744-
temp_req_file.write(reqs_new)
804+
temp_req_file.write("\n".join(raw_lines))
745805
requirements_file_output = temp_req_file.name
746806
except FileNotFoundError as err:
747807
print(err)
@@ -954,8 +1014,9 @@ def to_ver(v: str) -> Version | None:
9541014
version = get_file_version(candidates[0])
9551015
is_preferred = name_cf in prefer_set
9561016

1017+
known_hashes = req_hashes_by_pkg.get(name_cf) if use_hash else None
9571018
resolved, errors = resolve_package_sources(
958-
name_cf, version, candidates, is_preferred
1019+
name_cf, version, candidates, is_preferred, known_hashes
9591020
)
9601021

9611022
for error in errors:

0 commit comments

Comments
 (0)