Files

123 lines
5.6 KiB
Python

"""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)