Files
school-meal/build.py
T
Chunyu d7553520ce feat: 现代化翻页组件 + 完整项目更新
- 新增 Pagination 通用组件:首页/上一页/页码/下一页/末页导航
- PickModal 和 SwapModal 集成分页功能,每页20项
- 搜索/筛选切换自动回到第1页
- 现代化CSS样式:hover浮起动效、主色高亮、移动端适配
- 新增 dashboard、admin 管理模板
- 菜系(cuisine)模型和迁移
- 构建脚本 build.py 支持 PyInstaller 打包
- 前端资源重新构建
2026-08-28 13:44:21 +08:00

286 lines
9.4 KiB
Python

#!/usr/bin/env python
"""
Build script: compile frontend, then package everything into a distributable
folder via PyInstaller. Optionally build a Windows installer via Inno Setup.
Usage:
python build.py # full build (windowed desktop EXE)
python build.py --clean # clean previous build artifacts first
python build.py --installer # build EXE then compile installer
"""
import argparse
import glob
import os
import shutil
import subprocess
import sys
from PyInstaller.utils.hooks import collect_submodules
ROOT = os.path.dirname(os.path.abspath(__file__))
BACKEND = os.path.join(ROOT, "backend")
FRONTEND = os.path.join(ROOT, "frontend")
DIST = os.path.join(ROOT, "dist")
BUILD = os.path.join(ROOT, "build")
INSTALLER_DIR = os.path.join(ROOT, "installer")
ISS_SCRIPT = os.path.join(ROOT, "school-meal-setup.iss")
FRONTEND_BUILD = os.path.join(FRONTEND, "dist")
STATIC_FE = os.path.join(BACKEND, "static", "frontend")
SPEC_FILE = os.path.join(ROOT, "school-meal.spec")
def run(cmd, cwd=None, shell=False):
print(f" > {cmd if isinstance(cmd, str) else ' '.join(cmd)}")
result = subprocess.run(cmd, cwd=cwd, shell=shell)
if result.returncode != 0:
print(f"ERROR: command failed with exit code {result.returncode}")
sys.exit(1)
def clean():
print("[clean] Removing previous build artifacts...")
for d in [DIST, BUILD, STATIC_FE]:
if os.path.isdir(d):
shutil.rmtree(d)
for f in glob.glob(os.path.join(ROOT, "*.spec")):
os.remove(f)
def build_frontend():
print("\n[1/3] Building React frontend...")
run("npm run build", cwd=FRONTEND, shell=True)
if not os.path.isdir(FRONTEND_BUILD):
print("ERROR: frontend/dist not found after build")
sys.exit(1)
print(" Copying to backend/static/frontend/")
if os.path.isdir(STATIC_FE):
shutil.rmtree(STATIC_FE)
shutil.copytree(FRONTEND_BUILD, STATIC_FE)
venv_site = (
os.path.join(ROOT, ".venv", "Lib", "site-packages")
if os.path.isdir(os.path.join(ROOT, ".venv"))
else ""
)
def create_spec():
"""Generate the PyInstaller .spec file dynamically."""
print("\n[2/3] Creating PyInstaller spec...")
# Collect Jazzmin admin theme static files
jazzmin_static = os.path.join(venv_site, "jazzmin", "static")
datas_extra = []
if os.path.isdir(jazzmin_static):
for root, dirs, files in os.walk(jazzmin_static):
rel = os.path.relpath(root, jazzmin_static)
for f in files:
src = os.path.join(root, f)
dst = os.path.join("static", rel) if rel != "." else "static"
datas_extra.append((src, dst))
datas = [
(os.path.join(BACKEND, "static"), "backend/static"),
(os.path.join(BACKEND, "templates"), "backend/templates"),
(os.path.join(BACKEND, "meals", "migrations"), "backend/meals/migrations"),
(os.path.join(BACKEND, "config"), "backend/config"),
(os.path.join(BACKEND, "meals", "management"), "backend/meals/management"),
] + datas_extra
# Hidden imports
hidden = []
for pkg in ["rest_framework", "corsheaders", "jazzmin", "rich"]:
try:
hidden.extend(collect_submodules(pkg))
except Exception:
pass
hidden += [
"meals.views_dashboard", "meals", "meals.apps", "meals.models",
"meals.admin", "meals.urls", "meals.views", "meals.views_spa",
"meals.serializers", "meals.management", "meals.management.commands",
"meals.management.commands.seed_dishes",
"meals.management.commands.import_dishes",
"config", "config.settings", "config.wsgi", "config.asgi", "config.urls",
"django.contrib.admin", "django.contrib.auth",
"django.contrib.contenttypes", "django.contrib.sessions",
"django.contrib.messages", "django.contrib.staticfiles",
"pypinyin", "openpyxl",
"tkinter", "tkinter.messagebox", "tkinter.constants",
"webview", "webview.window", "webview.platforms.edgechromium",
"pythonnet", "clr_loader", "cffi", "bottle",
]
seen = set()
unique_hidden = [h for h in hidden if not (h in seen or seen.add(h))]
# Build spec content using list of lines to avoid f-string quoting issues
desktop_py = os.path.join(ROOT, "desktop.py").replace("\\", "/")
root_path = ROOT.replace("\\", "/")
backend_path = BACKEND.replace("\\", "/")
icon_path = os.path.join(ROOT, "app.ico").replace("\\", "/")
lines = [
"# -*- mode: python ; coding: utf-8 -*-",
"# Auto-generated by build.py",
"block_cipher = None",
"",
"a = Analysis(",
f" [r'{desktop_py}'],",
f" pathex=[r'{root_path}', r'{backend_path}'],",
" binaries=[],",
" datas=[",
]
for src, dst in datas:
lines.append(f" (r'{src}', r'{dst}'),")
lines += [
" ],",
" hiddenimports=[",
]
for h in unique_hidden:
lines.append(f" '{h}',")
lines += [
" ],",
" hookspath=[],",
" hooksconfig={},",
" runtime_hooks=[],",
" excludes=[],",
" win_no_prefer_redirects=False,",
" win_private_assemblies=False,",
" cipher=block_cipher,",
" noarchive=False,",
")",
"",
"pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)",
"",
"exe = EXE(",
" pyz,",
" a.scripts,",
" [],",
" exclude_binaries=True,",
" name='school-meal',",
" debug=False,",
" bootloader_ignore_signals=False,",
" strip=False,",
" upx=True,",
" console=False,",
" disable_windowed_traceback=False,",
" target_arch=None,",
" codesign_identity=None,",
" entitlements_file=None,",
f" icon=r'{icon_path}',",
")",
"",
"coll = COLLECT(",
" exe,",
" a.binaries,",
" a.zipfiles,",
" a.datas,",
" strip=False,",
" upx=True,",
" upx_exclude=[],",
" name='school-meal',",
")",
"",
]
with open(SPEC_FILE, "w", encoding="utf-8") as f:
f.write("\n".join(lines))
print(f" Spec written to {SPEC_FILE}")
def run_pyinstaller():
print("\n[3/3] Running PyInstaller...")
run([sys.executable, "-m", "PyInstaller", "--clean", "--noconfirm", SPEC_FILE])
out_dir = os.path.join(DIST, "school-meal")
bat_path = os.path.join(out_dir, "school-meal.bat")
with open(bat_path, "w", encoding="utf-8") as f:
f.write("@echo off\n")
f.write("chcp 65001 >nul\n")
f.write("cd /d %~dp0\n")
f.write('echo Starting school meal planner...\n')
f.write('start "" school-meal.exe\n')
size_mb = sum(
os.path.getsize(os.path.join(dp, fn))
for dp, _, fnames in os.walk(out_dir)
for fn in fnames
) / (1024 * 1024)
print(f"\nBuild complete! Output: {out_dir}")
print(f"Total size: {size_mb:.1f} MB")
def find_iscc():
"""Locate the Inno Setup command-line compiler."""
candidates = [
os.path.join(os.environ.get("ProgramFiles(x86)", ""), "Inno Setup 6", "ISCC.exe"),
os.path.join(os.environ.get("ProgramFiles", ""), "Inno Setup 6", "ISCC.exe"),
os.path.join(os.environ.get("ProgramFiles(x86)", ""), "Inno Setup 5", "ISCC.exe"),
os.path.join(os.environ.get("ProgramFiles", ""), "Inno Setup 5", "ISCC.exe"),
]
for path in candidates:
if os.path.isfile(path):
return path
from shutil import which
path = which("iscc")
if path:
return path
return None
def build_installer():
"""Compile the Inno Setup .iss script to produce an installer .exe."""
print("\n[installer] Building Windows installer...")
if not os.path.isfile(ISS_SCRIPT):
print(f"ERROR: {ISS_SCRIPT} not found. Cannot build installer.")
sys.exit(1)
iscc = find_iscc()
if not iscc:
print("ERROR: Inno Setup compiler (ISCC.exe) not found.")
print("Please install Inno Setup 6 from https://jrsoftware.org/isdl.php")
sys.exit(1)
print(f" Using: {iscc}")
run([iscc, ISS_SCRIPT])
if os.path.isdir(INSTALLER_DIR):
size_mb = sum(
os.path.getsize(os.path.join(dp, fn))
for dp, _, fnames in os.walk(INSTALLER_DIR)
for fn in fnames
) / (1024 * 1024)
print(f"\nInstaller built! Output: {INSTALLER_DIR}")
print(f"Installer size: {size_mb:.1f} MB")
else:
print("ERROR: Installer output directory not created.")
sys.exit(1)
def main():
parser = argparse.ArgumentParser(description="Build school-meal package")
parser.add_argument("--clean", action="store_true", help="Clean previous build artifacts")
parser.add_argument("--installer", action="store_true", help="Also build Windows installer after EXE build")
args = parser.parse_args()
if args.clean:
clean()
print("Clean done. Exiting.\n")
return
os.chdir(ROOT)
build_frontend()
create_spec()
run_pyinstaller()
if args.installer:
build_installer()
print("\nDone! Installer at installer/学校订餐菜单生成器-安装包.exe")
else:
print("\nDone! Run dist/school-meal/school-meal.exe to launch the desktop app.")
if __name__ == "__main__":
main()