#!/usr/bin/env python3 """LocalNav 一键部署:环境检测、生成 .env、安装依赖。""" from __future__ import annotations import os import platform import secrets import shutil import subprocess import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent ENV_EXAMPLE = ROOT / ".env.example" ENV_FILE = ROOT / ".env" VENV_DIR = ROOT / ".venv" REQUIREMENTS = ROOT / "requirements.txt" MIN_PYTHON = (3, 10) DEFAULTS = { "NAV_ADMIN_USERNAME": "admin", "NAV_ADMIN_PASSWORD": "admin123", "NAV_HUB_AUTO_LOGIN": "1", "NAV_COOKIES_INSECURE_HTTP": "1", } def _ok(msg: str) -> None: print(f"[OK] {msg}") def _warn(msg: str) -> None: print(f"[WARN] {msg}") def _fail(msg: str) -> None: print(f"[FAIL] {msg}", file=sys.stderr) sys.exit(1) def check_python() -> str: ver = sys.version_info if ver < MIN_PYTHON: _fail(f"需要 Python {MIN_PYTHON[0]}.{MIN_PYTHON[1]}+,当前 {ver.major}.{ver.minor}") _ok(f"Python {ver.major}.{ver.minor}.{ver.micro}") return sys.executable def check_pip(py_exe: str) -> None: try: subprocess.run( [py_exe, "-m", "pip", "--version"], check=True, capture_output=True, text=True, ) _ok("pip 可用") except (subprocess.CalledProcessError, FileNotFoundError): _fail("pip 不可用,请先安装 pip") def optional_tools() -> None: for name in ("git", "pm2"): if shutil.which(name): _ok(f"可选工具 {name} 已安装") else: _warn(f"未检测到 {name}(可选)") def read_env_lines(path: Path) -> list[str]: if not path.is_file(): return [] return path.read_text(encoding="utf-8-sig").splitlines() def parse_env_keys(lines: list[str]) -> dict[str, str]: out: dict[str, str] = {} for line in lines: s = line.strip() if not s or s.startswith("#") or "=" not in s: continue key, _, val = s.partition("=") out[key.strip()] = val.strip() return out def write_env(lines: list[str], updates: dict[str, str]) -> None: existing = parse_env_keys(lines) merged = {**existing, **updates} out_lines: list[str] = [] written: set[str] = set() for line in lines: s = line.strip() if not s or s.startswith("#") or "=" not in s: out_lines.append(line) continue key, _, _ = s.partition("=") key = key.strip() if key in merged and key not in written: out_lines.append(f"{key}={merged[key]}") written.add(key) else: out_lines.append(line) for key, val in merged.items(): if key not in written: out_lines.append(f"{key}={val}") ENV_FILE.write_text("\n".join(out_lines) + "\n", encoding="utf-8") def ensure_env() -> None: if not ENV_FILE.is_file(): if ENV_EXAMPLE.is_file(): shutil.copy2(ENV_EXAMPLE, ENV_FILE) _ok("已从 .env.example 创建 .env") else: ENV_FILE.write_text("", encoding="utf-8") _ok("已创建空 .env") lines = read_env_lines(ENV_FILE) parsed = parse_env_keys(lines) updates: dict[str, str] = {} secret = parsed.get("NAV_SECRET_KEY", "").strip() if not secret: updates["NAV_SECRET_KEY"] = secrets.token_hex(32) _ok("已自动生成 NAV_SECRET_KEY") for key, val in DEFAULTS.items(): if not parsed.get(key, "").strip(): updates[key] = val if updates: write_env(lines, updates) _ok(f"已写入 .env 配置项:{', '.join(updates.keys())}") else: _ok(".env 已完整,未覆盖现有配置") def venv_python() -> str: if platform.system() == "Windows": py = VENV_DIR / "Scripts" / "python.exe" else: py = VENV_DIR / "bin" / "python" return str(py) def ensure_venv(base_py: str) -> str: if not VENV_DIR.is_dir(): subprocess.run([base_py, "-m", "venv", str(VENV_DIR)], check=True) _ok(f"已创建虚拟环境 {VENV_DIR}") else: _ok("虚拟环境已存在") return venv_python() def install_requirements(py_exe: str) -> None: cmd = [py_exe, "-m", "pip", "install", "-r", str(REQUIREMENTS)] try: subprocess.run(cmd, check=True, cwd=ROOT) except subprocess.CalledProcessError: _warn("默认 pip 源失败,尝试 https://pypi.org/simple") subprocess.run([*cmd, "-i", "https://pypi.org/simple"], check=True, cwd=ROOT) _ok("依赖安装完成") def smoke_test(py_exe: str) -> None: subprocess.run( [ py_exe, "-c", "from app import create_app; create_app(); print('app import ok')", ], check=True, cwd=ROOT, ) _ok("应用启动检查通过") def print_summary() -> None: port = os.environ.get("NAV_PORT", "5070") print() print("=" * 50) print("部署完成") print(f" 访问地址: http://0.0.0.0:{port}") print(" 默认账号: admin / admin123") print(" 生产环境请尽快在「系统设置」中修改密码") print() print("启动方式(任选其一):") if platform.system() == "Windows": print(f" {venv_python()} app.py") else: print(f" {venv_python()} app.py") print(" pm2 start ecosystem.config.cjs") print("=" * 50) def main() -> None: print(f"LocalNav 一键部署 · {platform.system()} {platform.release()}") print(f"项目目录: {ROOT}") print() base_py = check_python() check_pip(base_py) optional_tools() ensure_env() py_exe = ensure_venv(base_py) install_requirements(py_exe) smoke_test(py_exe) print_summary() if __name__ == "__main__": main()