#!/usr/bin/env python3 """LocalNav 一键安装:无需预先克隆,输入仓库地址后自动克隆并部署。""" from __future__ import annotations import argparse import os import platform import shutil import subprocess import sys from pathlib import Path DEFAULT_REPO = "https://git.bz121.com/dekun/LocalNav.git" MIN_PYTHON = (3, 10) 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 default_install_dir() -> Path: if platform.system() == "Windows": return Path.cwd() / "LocalNav" return Path("/opt/LocalNav") 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_git() -> str: git = shutil.which("git") if not git: _fail("未检测到 git,请先安装:Ubuntu 执行 apt install -y git") _ok(f"git 可用 ({git})") return git def prompt_value(label: str, default: str) -> str: try: raw = input(f"{label} [{default}]: ").strip() except (EOFError, KeyboardInterrupt): print() _fail("已取消") return raw or default def resolve_paths(repo: str, dest: Path) -> tuple[str, Path]: dest = dest.expanduser().resolve() repo = repo.strip() if not repo: _fail("仓库地址不能为空") if dest.name == "" or str(dest).endswith(("/", "\\")): _fail("安装目录无效") return repo, dest def is_localnav_project(path: Path) -> bool: return (path / "app.py").is_file() and (path / "scripts" / "deploy.py").is_file() def clone_or_update(git_exe: str, repo: str, dest: Path) -> None: if dest.exists() and not dest.is_dir(): _fail(f"安装路径已存在且不是目录: {dest}") if is_localnav_project(dest): _ok(f"目录已是 LocalNav 项目,执行 git pull: {dest}") subprocess.run([git_exe, "-C", str(dest), "pull", "--ff-only"], check=True) return if dest.exists(): if (dest / ".git").is_dir(): _ok(f"目录为 Git 仓库,执行 git pull: {dest}") subprocess.run([git_exe, "-C", str(dest), "pull", "--ff-only"], check=True) if not is_localnav_project(dest): _fail(f"目录 {dest} 不是 LocalNav 项目(缺少 app.py)") return _fail(f"目录已存在且不是 Git 仓库: {dest}") dest.parent.mkdir(parents=True, exist_ok=True) _ok(f"正在克隆 {repo} -> {dest}") subprocess.run([git_exe, "clone", repo, str(dest)], check=True) if not is_localnav_project(dest): _fail("克隆完成但未找到 app.py,请检查仓库地址是否正确") def run_project_setup(py_exe: str, project_dir: Path) -> None: deploy_script = project_dir / "scripts" / "deploy.py" if not deploy_script.is_file(): _fail(f"未找到部署脚本: {deploy_script}") _ok(f"开始项目内安装: {project_dir}") subprocess.run([py_exe, str(deploy_script)], cwd=project_dir, check=True) def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="LocalNav 一键安装(自动克隆 + 部署)") parser.add_argument( "--repo", default="", help=f"Git 仓库地址(默认 {DEFAULT_REPO})", ) parser.add_argument( "--dir", default="", help="安装目录(Linux 默认 /opt/LocalNav,Windows 默认 ./LocalNav)", ) parser.add_argument( "-y", "--yes", action="store_true", help="使用默认仓库与安装目录,不交互提问", ) return parser.parse_args() def main() -> None: args = parse_args() print(f"LocalNav 一键安装 · {platform.system()} {platform.release()}") print("无需预先克隆,将自动拉取代码并完成部署。") print() py_exe = check_python() git_exe = check_git() default_dest = default_install_dir() if args.yes: repo = args.repo.strip() or DEFAULT_REPO dest = Path(args.dir).expanduser() if args.dir.strip() else default_dest else: repo = args.repo.strip() or prompt_value("Git 仓库地址", DEFAULT_REPO) dest_str = args.dir.strip() or prompt_value("安装目录", str(default_dest)) dest = Path(dest_str) repo, dest = resolve_paths(repo, dest) print() print(f" 仓库: {repo}") print(f" 目录: {dest}") print() clone_or_update(git_exe, repo, dest) run_project_setup(py_exe, dest) print() print(f"安装目录: {dest}") print(f"进入目录: cd {dest}") if __name__ == "__main__": main()