123 lines
5.6 KiB
Python
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)
|