6897f88afd
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>
164 lines
4.8 KiB
Python
164 lines
4.8 KiB
Python
#!/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()
|