fetch_offline_deps.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """离线依赖取件 (在**有网**的机器上跑一次, 产物随包/U 盘带到内网机器).
  4. 三类:
  5. wheels 各平台 Python 依赖轮子 (win_amd64 / linux_x86_64 / linux_aarch64 / macos_arm64)
  6. python 便携 CPython 3.12 运行时 (麒麟/统信自带多为 3.7/3.9, 跑不了本项目依赖)
  7. ollama Ollama 离线安装包 (Windows .exe ~1.5G / Linux .tar.zst ~1.4G; **体积大, 不进主程序包**, 单独带)
  8. 用法: python scripts/fetch_offline_deps.py [wheels|python|ollama|all] [--arch x86_64,aarch64]
  9. 产物带 SHA256 记到 vendor/MANIFEST.json; 断点续传 (再跑一次自动接着下)。模型不在此列 —— 见 §模型."""
  10. from __future__ import annotations
  11. import argparse, hashlib, json, subprocess, sys, time, urllib.request
  12. from pathlib import Path
  13. ROOT = Path(__file__).resolve().parents[1]
  14. PY_TAG = "20260901"; PY_VER = "3.12.14"
  15. PY_BASE = f"https://github.com/astral-sh/python-build-standalone/releases/download/{PY_TAG}"
  16. PY_FILES = {"linux_x86_64": f"cpython-{PY_VER}+{PY_TAG}-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz",
  17. "linux_aarch64": f"cpython-{PY_VER}+{PY_TAG}-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz",
  18. "win_amd64": f"cpython-{PY_VER}+{PY_TAG}-x86_64-pc-windows-msvc-install_only_stripped.tar.gz"}
  19. OL_VER = "v0.33.3"; OL_BASE = f"https://github.com/ollama/ollama/releases/download/{OL_VER}"
  20. OL_FILES = {"win_amd64": "OllamaSetup.exe", "linux_x86_64": "ollama-linux-amd64.tar.zst", "linux_aarch64": "ollama-linux-arm64.tar.zst"}
  21. WHEEL_PLAT = {"win_amd64": ["win_amd64"],
  22. "linux_x86_64": ["manylinux_2_28_x86_64", "manylinux_2_27_x86_64", "manylinux_2_26_x86_64", "manylinux_2_24_x86_64", "manylinux_2_17_x86_64", "manylinux2014_x86_64"],
  23. "linux_aarch64": ["manylinux_2_28_aarch64", "manylinux_2_27_aarch64", "manylinux_2_26_aarch64", "manylinux_2_24_aarch64", "manylinux_2_17_aarch64", "manylinux2014_aarch64"]}
  24. def sha256(p: Path) -> str:
  25. h = hashlib.sha256()
  26. with p.open("rb") as f:
  27. for b in iter(lambda: f.read(1 << 20), b""): h.update(b)
  28. return h.hexdigest()
  29. def get(url: str, dst: Path):
  30. """断点续传下载 (curl); 已完整则跳过."""
  31. dst.parent.mkdir(parents=True, exist_ok=True)
  32. r = subprocess.run(["curl", "-sL", "-C", "-", "--retry", "5", "--retry-delay", "3", "-o", str(dst), url])
  33. if r.returncode: raise SystemExit(f"下载失败 {url} (rc={r.returncode}); 再跑一次会续传")
  34. return dst
  35. def wheels(targets):
  36. for t in targets:
  37. d = ROOT / "wheels" / t; d.mkdir(parents=True, exist_ok=True)
  38. cmd = [sys.executable, "-m", "pip", "download", "-r", str(ROOT / "requirements.txt"), "--only-binary=:all:",
  39. "--python-version", "3.12", "--implementation", "cp", "-d", str(d), "-q"]
  40. for p in WHEEL_PLAT[t]: cmd += ["--platform", p]
  41. cmd += ["--abi", "cp312", "--abi", "abi3", "--abi", "none"]
  42. print(f" 轮子 {t} …", flush=True); subprocess.run(cmd, check=True)
  43. print(f" {len(list(d.glob('*.whl')))} 个")
  44. def main():
  45. ap = argparse.ArgumentParser(); ap.add_argument("what", nargs="?", default="all", choices=["wheels", "python", "ollama", "all"])
  46. ap.add_argument("--targets", default="win_amd64,linux_x86_64,linux_aarch64"); a = ap.parse_args()
  47. ts = [t.strip() for t in a.targets.split(",") if t.strip()]
  48. man_p = ROOT / "vendor/MANIFEST.json"; man = json.loads(man_p.read_text(encoding="utf-8")) if man_p.exists() else {}
  49. if a.what in ("wheels", "all"): wheels(ts)
  50. if a.what in ("python", "all"):
  51. for t in ts:
  52. f = PY_FILES.get(t)
  53. if not f: continue
  54. p = get(f"{PY_BASE}/{f}", ROOT / "vendor/python" / f); man[f"python/{f}"] = dict(sha256=sha256(p), bytes=p.stat().st_size, url=f"{PY_BASE}/{f}")
  55. print(f" 便携 Python {t}: {p.stat().st_size/1e6:.0f} MB")
  56. if a.what in ("ollama", "all"):
  57. for t in ts:
  58. f = OL_FILES.get(t)
  59. if not f: continue
  60. print(f" Ollama {t} ({f}) 下载中 (1.4–1.6 GB, 可中断续传) …", flush=True)
  61. p = get(f"{OL_BASE}/{f}", ROOT / "vendor/ollama" / f); man[f"ollama/{f}"] = dict(sha256=sha256(p), bytes=p.stat().st_size, url=f"{OL_BASE}/{f}", version=OL_VER)
  62. print(f" {p.stat().st_size/1e6:.0f} MB")
  63. man_p.parent.mkdir(parents=True, exist_ok=True); man["_built"] = time.strftime("%Y-%m-%d %H:%M")
  64. man_p.write_text(json.dumps(man, ensure_ascii=False, indent=1), encoding="utf-8"); print(f"清单 → {man_p}")
  65. print("模型不在此列: 在有网机器 `ollama pull qwen3:8b deepseek-r1:14b bge-m3`, 再把 ~/.ollama/models (Windows %USERPROFILE%\\.ollama\\models) 整个目录拷到内网机同位置。")
  66. if __name__ == "__main__":
  67. main()