"""Offline setup and EXE build, intended to run on the Windows 7 server.""" import argparse from datetime import datetime import hashlib import importlib.metadata import json import os from pathlib import Path import platform import shutil import struct import subprocess import sys ROOT = Path(__file__).resolve().parents[1] ARCH = 'x64' if struct.calcsize('P') == 8 else 'x86' def run(args, **kwargs): print('>', subprocess.list2cmdline([str(x) for x in args]), flush=True) subprocess.run([str(x) for x in args], check=True, **kwargs) def verify(): manifest = json.loads((ROOT/'SHA256SUMS.json').read_text(encoding='utf-8')) for name, digest in manifest.items(): path = (ROOT/name).resolve() if ROOT.resolve() not in path.parents: raise RuntimeError('Invalid manifest path') if hashlib.sha256(path.read_bytes()).hexdigest() != digest: raise RuntimeError('File checksum mismatch: ' + name) print('Package checksums verified.', flush=True) def check_python(): if os.name != 'nt' or sys.version_info[:3] != (3, 8, 10): raise RuntimeError('Use the bundled official Windows Python 3.8.10 installer first.') version = sys.getwindowsversion() if (version.major, version.minor) < (6, 1) or ((version.major, version.minor) == (6, 1) and version.service_pack_major < 1): raise RuntimeError('Windows 7 SP1 or later is required.') print('Python:', sys.version.split()[0], ARCH, '| OS:', platform.platform(), flush=True) if (version.major, version.minor) != (6, 1): print('NOTE: This is not Windows 7. The resulting EXE must still be built/tested on Windows 7.', flush=True) def setup(): check_python() verify() env_dir = ROOT/'.buildenv' python = env_dir/'Scripts/python.exe' if not python.exists(): run([sys.executable, '-m', 'venv', env_dir]) wheels = ROOT/'wheels'/ARCH options = ['--no-index', '--find-links', wheels, '--disable-pip-version-check', '--no-cache-dir'] run([python, '-m', 'pip', 'install'] + options + ['pip==25.0.1', 'setuptools==68.2.2', 'wheel==0.45.1']) run([python, '-m', 'pip', 'install'] + options + ['-r', ROOT/'win7/requirements-win7.txt']) run([python, '-m', 'pip', 'check']) run([python, ROOT/'win7/selftest.py', ROOT/'po_app']) print('SETUP COMPLETE. Run 02_build_exe.cmd next.', flush=True) def build(): check_python() verify() # Running from the dedicated environment keeps modern packages out of the build. expected = (ROOT/'.buildenv/Scripts/python.exe').resolve() if Path(sys.executable).resolve() != expected: raise RuntimeError('Run 02_build_exe.cmd after 01_install_env.cmd.') for line in (ROOT/'win7/requirements-win7.txt').read_text().splitlines(): if not line or line.startswith('#'): continue package, pinned = line.split('==') if importlib.metadata.version(package) != pinned: raise RuntimeError('Dependency changed; run 01_install_env.cmd again: ' + package) run([sys.executable, ROOT/'win7/selftest.py', ROOT/'po_app']) build_id = datetime.now().strftime('%Y%m%d-%H%M%S-%f') + '-' + ARCH work = ROOT/'build'/build_id output = ROOT/'dist'/build_id work.mkdir(parents=True) output.mkdir(parents=True) env = os.environ.copy() env['PYINSTALLER_CONFIG_DIR'] = str(work/'cache') command = [sys.executable, '-m', 'PyInstaller', '--noconfirm', '--onefile', '--console', '--noupx', '--name', 'PO_Server_Win7', '--distpath', output, '--workpath', work/'temp', '--specpath', work, '--paths', ROOT/'po_app', '--add-data', str(ROOT/'po_app/templates') + ';templates', '--add-data', str(ROOT/'po_app/static') + ';static', '--hidden-import', 'sqlalchemy.dialects.sqlite', '--hidden-import', 'openpyxl', '--hidden-import', 'xlrd', '--collect-all', 'reportlab', ROOT/'po_app/app.py'] log_path = work/'build.log' print('Building EXE. Build log:', log_path, flush=True) with log_path.open('wb') as log: subprocess.run([str(x) for x in command], cwd=ROOT, env=env, stdout=log, stderr=subprocess.STDOUT, check=True) exe = output/'PO_Server_Win7.exe' run([sys.executable, ROOT/'win7/probe_exe.py', exe]) info = {'built_on': platform.platform(), 'python': sys.version, 'architecture': ARCH, 'source_commit': json.loads((ROOT/'PACKAGE_INFO.json').read_text(encoding='utf-8'))['source_commit'], 'sha256': hashlib.sha256(exe.read_bytes()).hexdigest(), 'source_selftest': 'passed', 'exe_startup_probe': 'passed', 'built_on_windows7': sys.getwindowsversion()[:2] == (6, 1)} (output/'BUILD_INFO.json').write_text(json.dumps(info, ensure_ascii=False, indent=2), encoding='utf-8') shutil.copy2(ROOT/'README_WIN7.txt', output/'README_WIN7.txt') for pdf in (ROOT/'manual').glob('*.pdf'): shutil.copy2(pdf, output/pdf.name) print('BUILD COMPLETE:', exe, flush=True) print('Copy EXE to a fixed service folder. Keep the original instance/uploads/exports when upgrading.', flush=True) if __name__ == '__main__': parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('command', choices=['setup', 'build', 'verify']) args = parser.parse_args() try: {'setup': setup, 'build': build, 'verify': verify}[args.command]() except Exception: import traceback traceback.print_exc() print('FAILED. Do not replace the running server. Check the message and build log above.', flush=True) sys.exit(1)