99 lines
5.4 KiB
Python
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)
|