offline_env_bundle.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. """Pack and verify the locked Kylin ARM64 CPU environment archive."""
  2. import argparse
  3. from hashlib import sha256
  4. from pathlib import Path
  5. import tarfile
  6. import zipfile
  7. from scripts.verify_offline_wheels import verify as verify_wheels
  8. ROOT = Path(__file__).resolve().parents[1]
  9. UV_ARCHIVE_SHA256 = 'bb66cb52e7b1823aed1183630d8d8e5c958840d584a4c55ec10a4cfc168dcca2'
  10. UV_MEMBER = 'uv-aarch64-unknown-linux-gnu/uv'
  11. def digest(path):
  12. result = sha256()
  13. with path.open('rb') as stream:
  14. for block in iter(lambda: stream.read(1024 * 1024), b''):
  15. result.update(block)
  16. return result.hexdigest()
  17. def pack(source, output):
  18. source, output = Path(source), Path(output)
  19. if output.exists():
  20. raise ValueError('environment_archive_exists')
  21. requirements = source / 'requirements-arm64-cpu.txt'
  22. wheelhouse = source / 'wheelhouse'
  23. verify_wheels(requirements, wheelhouse, ROOT / 'uv.lock')
  24. uv_archive = source / 'uv-aarch64-unknown-linux-gnu.tar.gz'
  25. if digest(uv_archive) != UV_ARCHIVE_SHA256:
  26. raise ValueError('uv_archive_hash_mismatch')
  27. with tarfile.open(uv_archive, 'r:gz') as archive:
  28. member = archive.getmember(UV_MEMBER)
  29. if not member.isfile() or member.size > 100_000_000:
  30. raise ValueError('invalid_uv_binary')
  31. uv_bytes = archive.extractfile(member).read()
  32. if uv_bytes[:4] != b'\x7fELF' or int.from_bytes(uv_bytes[18:20], 'little') != 183:
  33. raise ValueError('uv_not_aarch64_elf')
  34. files = {'uv': uv_bytes}
  35. paths = {
  36. 'requirements-arm64-cpu.txt': requirements,
  37. 'install_offline_env.sh': ROOT / 'scripts/install_offline_env.sh',
  38. }
  39. paths.update({f'wheelhouse/{path.name}': path for path in sorted(wheelhouse.glob('*.whl'))})
  40. sums = {name: sha256(content).hexdigest() for name, content in files.items()}
  41. sums.update({name: digest(path) for name, path in paths.items()})
  42. checksums = ''.join(f'{value} {name}\n' for name, value in sorted(sums.items()))
  43. output.parent.mkdir(parents=True, exist_ok=True)
  44. partial = output.with_name(output.name + '.partial')
  45. if partial.exists():
  46. raise ValueError('partial_environment_archive_exists')
  47. try:
  48. with zipfile.ZipFile(partial, 'w', allowZip64=True) as archive:
  49. archive.writestr('uv', uv_bytes, compress_type=zipfile.ZIP_STORED)
  50. for name, path in sorted(paths.items()):
  51. compression = zipfile.ZIP_STORED if path.suffix == '.whl' else zipfile.ZIP_DEFLATED
  52. archive.write(path, name, compress_type=compression)
  53. archive.writestr('SHA256SUMS', checksums)
  54. verify(partial)
  55. partial.replace(output)
  56. finally:
  57. partial.unlink(missing_ok=True)
  58. return len(sums)
  59. def verify(path):
  60. with zipfile.ZipFile(path) as archive:
  61. lines = archive.read('SHA256SUMS').decode('ascii').splitlines()
  62. expected = {}
  63. for line in lines:
  64. checksum, name = line.split(' ', 1)
  65. if (len(checksum) != 64 or name in expected or name.startswith('/')
  66. or '..' in Path(name).parts or '\\' in name):
  67. raise ValueError('invalid_environment_manifest')
  68. expected[name] = checksum
  69. if set(archive.namelist()) != set(expected) | {'SHA256SUMS'}:
  70. raise ValueError('unexpected_environment_members')
  71. for name, checksum in expected.items():
  72. with archive.open(name) as stream:
  73. actual = sha256()
  74. for block in iter(lambda: stream.read(1024 * 1024), b''):
  75. actual.update(block)
  76. if actual.hexdigest() != checksum:
  77. raise ValueError(f'environment_member_hash_mismatch:{name}')
  78. return len(expected)
  79. def main():
  80. parser = argparse.ArgumentParser(description=__doc__)
  81. commands = parser.add_subparsers(dest='action', required=True)
  82. pack_parser = commands.add_parser('pack')
  83. pack_parser.add_argument('source', type=Path)
  84. pack_parser.add_argument('output', type=Path)
  85. verify_parser = commands.add_parser('verify')
  86. verify_parser.add_argument('archive', type=Path)
  87. args = parser.parse_args()
  88. count = pack(args.source, args.output) if args.action == 'pack' else verify(args.archive)
  89. print(f'Offline ARM64 environment archive verified: {count} files.')
  90. if __name__ == '__main__':
  91. main()