Files
LocalNav/scripts/install.py
T
dekun 6897f88afd Add install script for clone-first one-click deployment.
Prompt for repo URL and install directory, auto git clone, then run project setup. Keep deploy.py for in-repo updates only.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-12 11:52:57 +08:00

164 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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/LocalNavWindows 默认 ./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()