Files

99 lines
5.4 KiB
Python

"""Assemble the offline Win7 source kit from verified downloads in a local cache."""
import argparse
from datetime import datetime
import hashlib
import json
from pathlib import Path
import shutil
import subprocess
import zipfile
REPO = Path(__file__).resolve().parents[2]
HERE = Path(__file__).resolve().parent
def assemble(cache, output):
cache, output = cache.resolve(), output.resolve()
if REPO not in output.parents:
raise ValueError('Output must be within this workspace')
if output.exists() or output.with_suffix('.zip').exists():
raise ValueError('Choose a new output directory; existing packages are not overwritten')
# Only committed application files are shipped, never runtime directories.
subprocess.run(['git', 'diff', '--exit-code', 'HEAD', '--', 'po_app'], cwd=REPO, check=True,
stdout=subprocess.DEVNULL)
commit = subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=REPO, text=True).strip()
names = subprocess.check_output(['git', 'ls-files', '-z', 'po_app'], cwd=REPO).decode('utf-8').split('\0')
output.mkdir(parents=True)
def copy(source, relative):
dest = output/relative
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(source, dest)
for name in names:
if not name:
continue
if name.endswith('.py') or name.startswith(('po_app/templates/', 'po_app/static/')):
copy(REPO/name, name)
for name in ['build.py', 'selftest.py', 'probe_exe.py', 'requirements-win7.txt', 'make_waitress_wheel.py', 'install_env.ps1']:
copy(HERE/name, 'win7/' + name)
for name in ['00_check_environment.cmd', '01_install_env.cmd', '02_build_exe.cmd']:
(output/name).write_bytes((HERE/name).read_text(encoding='utf-8').replace('\r\n', '\n').replace('\n', '\r\n').encode('ascii'))
(output/'README_WIN7.txt').write_text((HERE/'README_WIN7.md').read_text(encoding='utf-8'), encoding='utf-8-sig')
copy(REPO/'output/pdf/博特维采购订单系统_v4_使用与升级手册.pdf', 'manual/博特维采购订单系统_v4_使用与升级手册.pdf')
for sample in ('采购单_签章调整样张.pdf', '委外订单_签章调整样张.pdf'):
copy(REPO/'output/pdf'/sample, 'samples/' + sample)
for name, expected_md5 in [('python-3.8.10-amd64.exe', '62cf1a12a5276b0259e8761d4cf4fe42'),
('python-3.8.10.exe', 'b355cfc84b681ace8908ae50908e8761')]:
if hashlib.md5((cache/name).read_bytes()).hexdigest() != expected_md5:
raise ValueError('Installer differs from python.org download: ' + name)
copy(cache/name, 'installers/' + name)
pins = {}
for line in (HERE/'requirements-win7.txt').read_text().splitlines():
if line and not line.startswith('#'):
name, version = line.split('==')
pins[name.lower().replace('-', '_')] = version
for arch, folder in [('x64', 'wheels64'), ('x86', 'wheels32')]:
selected = []
for wheel in (cache/folder).glob('*.whl'):
name, version = wheel.name.split('-')[:2]
if pins.get(name.lower()) == version:
copy(wheel, 'wheels/' + arch + '/' + wheel.name)
selected.append(name.lower())
if set(selected) != set(pins) or len(selected) != len(pins):
raise ValueError('Incomplete wheel set: %s, missing %s' % (arch, set(pins)-set(selected)))
copy(cache/'waitress-3.0.2-py3-none-any.whl', 'third_party/waitress-3.0.2-py3-none-any.whl')
info = {'source_commit': commit, 'created_at': datetime.now().isoformat(timespec='seconds'),
'setup_revision': '20260917-reuse-installed-python',
'purpose': 'Build v4 on the target Windows 7 SP1 server',
'architectures': ['x86', 'x64'], 'python': '3.8.10',
'windows7_field_verified': False,
'validation_note': 'See VALIDATION.txt for local tests; target Win7 acceptance is still required.'}
(output/'PACKAGE_INFO.json').write_text(json.dumps(info, ensure_ascii=False, indent=2), encoding='utf-8')
if (cache/'VALIDATION.txt').exists():
copy(cache/'VALIDATION.txt', 'VALIDATION.txt')
hashes = {p.relative_to(output).as_posix(): hashlib.sha256(p.read_bytes()).hexdigest()
for p in sorted(output.rglob('*')) if p.is_file()}
(output/'SHA256SUMS.json').write_text(json.dumps(hashes, ensure_ascii=False, indent=2), encoding='utf-8')
archive = output.with_suffix('.zip')
with zipfile.ZipFile(archive, 'x', zipfile.ZIP_DEFLATED) as zipout:
for path in sorted(output.rglob('*')):
if path.is_file():
zipout.write(path, (Path(output.name)/path.relative_to(output)).as_posix())
with zipfile.ZipFile(archive) as zipout:
assert zipout.testzip() is None
assert all(zipout.read(output.name + '/' + name) == (output/name).read_bytes() for name in hashes)
digest = hashlib.sha256(archive.read_bytes()).hexdigest()
archive.with_suffix('.zip.sha256.txt').write_text(digest + ' ' + archive.name + '\n', encoding='ascii')
print(archive)
print('Bytes:', archive.stat().st_size, 'SHA256:', digest)
return archive
if __name__ == '__main__':
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--cache', type=Path, default=REPO/'tools/out/win7-prep')
parser.add_argument('--output', type=Path, required=True)
args = parser.parse_args()
assemble(args.cache, args.output)