verify_offline_wheels.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  1. """Verify the locked ARM64 wheelhouse before offline delivery."""
  2. import argparse
  3. from email.parser import BytesParser
  4. from email.policy import default
  5. from hashlib import sha256
  6. from pathlib import Path
  7. import tomllib
  8. from zipfile import ZipFile
  9. from packaging.markers import default_environment
  10. from packaging.requirements import Requirement
  11. from packaging.tags import compatible_tags, cpython_tags
  12. from packaging.utils import canonicalize_name, parse_wheel_filename
  13. ROOT = Path(__file__).resolve().parents[1]
  14. PLATFORMS = [f'manylinux_2_{version}_aarch64' for version in range(28, 16, -1)]
  15. PLATFORMS += ['manylinux2014_aarch64', 'linux_aarch64']
  16. TAGS = set(cpython_tags(python_version=(3, 14), abis=['cp314'], platforms=PLATFORMS))
  17. TAGS.update(compatible_tags(python_version=(3, 14), interpreter='cp314', platforms=PLATFORMS))
  18. def verify(requirements_path, wheelhouse, lock_path):
  19. required = {}
  20. for line in requirements_path.read_text(encoding='utf-8').splitlines():
  21. if not line or line.startswith('#'):
  22. continue
  23. requirement = Requirement(line)
  24. name = canonicalize_name(requirement.name)
  25. if name in required or len(requirement.specifier) != 1:
  26. raise ValueError(f'invalid_target_requirement:{name}')
  27. required[name] = requirement.specifier
  28. with lock_path.open('rb') as stream:
  29. locked = tomllib.load(stream)['package']
  30. hashes = {}
  31. for package in locked:
  32. key = (canonicalize_name(package['name']), package['version'])
  33. hashes.setdefault(key, set()).update(
  34. wheel['hash'].removeprefix('sha256:') for wheel in package.get('wheels', [])
  35. )
  36. seen = set()
  37. total = 0
  38. installed = {}
  39. dependencies = []
  40. for path in sorted(wheelhouse.iterdir()):
  41. if not path.is_file() or path.suffix != '.whl':
  42. raise ValueError(f'unexpected_wheelhouse_file:{path.name}')
  43. name, version, _, tags = parse_wheel_filename(path.name)
  44. if name not in required or version not in required[name] or name in seen:
  45. raise ValueError(f'unexpected_wheel:{path.name}')
  46. if not tags & TAGS:
  47. raise ValueError(f'incompatible_wheel:{path.name}')
  48. digest = sha256()
  49. with path.open('rb') as stream:
  50. for block in iter(lambda: stream.read(1024 * 1024), b''):
  51. digest.update(block)
  52. if digest.hexdigest() not in hashes.get((name, str(version)), set()):
  53. raise ValueError(f'lock_hash_mismatch:{path.name}')
  54. with ZipFile(path) as archive:
  55. metadata_names = [entry for entry in archive.namelist()
  56. if entry.count('/') == 1 and entry.endswith('.dist-info/METADATA')]
  57. if len(metadata_names) != 1:
  58. raise ValueError(f'invalid_wheel_metadata:{path.name}')
  59. metadata = BytesParser(policy=default).parsebytes(archive.read(metadata_names[0]))
  60. dependencies.extend((name, Requirement(value)) for value in metadata.get_all('Requires-Dist', []))
  61. seen.add(name)
  62. installed[name] = version
  63. total += path.stat().st_size
  64. if seen != set(required):
  65. raise ValueError(f'missing_wheels:{sorted(set(required) - seen)}')
  66. environment = default_environment()
  67. environment.update({
  68. 'extra': '', 'implementation_name': 'cpython', 'os_name': 'posix',
  69. 'platform_machine': 'aarch64', 'platform_python_implementation': 'CPython',
  70. 'platform_system': 'Linux', 'python_full_version': '3.14.5',
  71. 'python_version': '3.14', 'sys_platform': 'linux',
  72. })
  73. for parent, requirement in dependencies:
  74. if requirement.marker and not requirement.marker.evaluate(environment):
  75. continue
  76. dependency = canonicalize_name(requirement.name)
  77. if dependency not in installed or installed[dependency] not in requirement.specifier:
  78. raise ValueError(f'missing_dependency:{parent}:{requirement}')
  79. return len(seen), total
  80. def main():
  81. parser = argparse.ArgumentParser(description=__doc__)
  82. parser.add_argument('requirements', type=Path)
  83. parser.add_argument('wheelhouse', type=Path)
  84. parser.add_argument('--lock', type=Path, default=ROOT / 'uv.lock')
  85. args = parser.parse_args()
  86. count, total = verify(args.requirements, args.wheelhouse, args.lock)
  87. print(f'Locked ARM64 wheelhouse verified: {count} wheels, {total} bytes.')
  88. if __name__ == '__main__':
  89. main()