python-enviroment-install.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. Python 依赖安装和同步脚本
  5. 功能:检查、安装 Python 依赖到虚拟环境,然后同步所有已安装的包到 environment.txt
  6. 约定:由嵌入式 Python(python/x64/py 或 python/arm64/py)运行;虚拟环境在 env,可用 virtualenv 创建。
  7. """
  8. import os
  9. import sys
  10. import subprocess
  11. import platform
  12. import tempfile
  13. import urllib.request
  14. import urllib.error
  15. import ssl
  16. from pathlib import Path
  17. # pip 损坏时用 get-pip.py 重装;优先国内源避免 SSL/外网问题(见 https://pip.pypa.io/en/stable/installation/)
  18. GET_PIP_URLS = [
  19. "http://mirrors.aliyun.com/pypi/get-pip.py",
  20. "https://mirrors.aliyun.com/pypi/get-pip.py",
  21. "https://bootstrap.pypa.io/get-pip.py",
  22. ]
  23. # 国内 pip 镜像源,避免外网慢或超时(可改为阿里/豆瓣等)
  24. PIP_INDEX_URL = os.environ.get("PIP_INDEX_URL", "https://pypi.tuna.tsinghua.edu.cn/simple")
  25. PIP_TRUSTED_HOST = "pypi.tuna.tsinghua.edu.cn"
  26. PIP_INDEX_ARGS = f'-i {PIP_INDEX_URL} --trusted-host {PIP_TRUSTED_HOST}'
  27. # 脚本所在目录即本环境目录(python/arm64 或 python/x64),venv 固定为 env
  28. SCRIPT_DIR = Path(__file__).parent.absolute()
  29. PROJECT_ROOT = SCRIPT_DIR.parent.parent.absolute()
  30. _env_path = os.environ.get("PYTHON_VENV_PATH", "").strip()
  31. VENV_PATH = Path(_env_path) if _env_path else (SCRIPT_DIR / "env")
  32. ENVIRONMENT_FILE = SCRIPT_DIR / "environment.txt"
  33. REQUIREMENTS_FILE = PROJECT_ROOT / "requirements.txt"
  34. # 根据操作系统确定虚拟环境的 Python 和 pip 路径
  35. # Windows 优先使用 .bat 脚本(可读、可编辑),否则用 .exe
  36. if platform.system() == "Windows":
  37. VENV_PYTHON = VENV_PATH / "Scripts" / "python.exe"
  38. VENV_PIP = VENV_PATH / "Scripts" / "pip.exe"
  39. PY_SCRIPTS = SCRIPT_DIR / "py" / "Scripts"
  40. PY_PIP = PY_SCRIPTS / "pip.bat" if (PY_SCRIPTS / "pip.bat").exists() else PY_SCRIPTS / "pip.exe"
  41. PY_VIRTUALENV = PY_SCRIPTS / "virtualenv.bat" if (PY_SCRIPTS / "virtualenv.bat").exists() else PY_SCRIPTS / "virtualenv.exe"
  42. else:
  43. VENV_PYTHON = VENV_PATH / "bin" / "python"
  44. VENV_PIP = VENV_PATH / "bin" / "pip"
  45. PY_SCRIPTS = SCRIPT_DIR / "py" / "bin"
  46. PY_PIP = PY_SCRIPTS / "pip"
  47. PY_VIRTUALENV = PY_SCRIPTS / "virtualenv"
  48. def run_command(cmd, check=True, capture_output=True):
  49. """运行命令并返回结果"""
  50. try:
  51. result = subprocess.run(
  52. cmd,
  53. shell=True,
  54. check=check,
  55. capture_output=capture_output,
  56. text=True,
  57. encoding='utf-8'
  58. )
  59. return result.returncode == 0, result.stdout, result.stderr
  60. except subprocess.CalledProcessError as e:
  61. return False, e.stdout if hasattr(e, 'stdout') else "", str(e)
  62. def _find_or_download_get_pip(get_pip_path):
  63. """优先使用项目内已有 get-pip.py,否则从国内源/官方源下载"""
  64. for local_dir in (SCRIPT_DIR, PROJECT_ROOT):
  65. candidate = Path(local_dir) / "get-pip.py"
  66. if candidate.exists():
  67. with open(candidate, "rb") as f:
  68. Path(get_pip_path).write_bytes(f.read())
  69. return True
  70. return _download_get_pip(get_pip_path)
  71. def _download_get_pip(get_pip_path):
  72. """从国内源或官方源下载 get-pip.py,HTTPS 遇 SSL 时用不验证上下文重试"""
  73. last_err = None
  74. for url in GET_PIP_URLS:
  75. try:
  76. ctx = None
  77. if url.startswith('https://'):
  78. ctx = ssl.create_default_context()
  79. req = urllib.request.urlopen(url, timeout=30, context=ctx)
  80. with open(get_pip_path, 'wb') as f:
  81. f.write(req.read())
  82. return True
  83. except urllib.error.URLError as e:
  84. last_err = e
  85. if 'SSL' in str(e) or 'CERTIFICATE' in str(e).upper():
  86. try:
  87. ctx = ssl._create_unverified_context()
  88. req = urllib.request.urlopen(url, timeout=30, context=ctx)
  89. with open(get_pip_path, 'wb') as f:
  90. f.write(req.read())
  91. return True
  92. except Exception as retry_e:
  93. last_err = retry_e
  94. except Exception as e:
  95. last_err = e
  96. if last_err:
  97. print(f"[WARN] Failed to download get-pip.py: {last_err}")
  98. return False
  99. def repair_pip_with_get_pip():
  100. """当 pip 损坏(如缺少 pip._internal.operations.build)时,用 get-pip.py 强制重装 pip(优先国内源)"""
  101. try:
  102. fd, get_pip_path = tempfile.mkstemp(suffix='.py')
  103. os.close(fd)
  104. if not _find_or_download_get_pip(get_pip_path):
  105. return False
  106. ok, out, err = run_command(
  107. f'"{sys.executable}" "{get_pip_path}" --force-reinstall -i {PIP_INDEX_URL} --trusted-host {PIP_TRUSTED_HOST} --no-warn-script-location',
  108. check=False
  109. )
  110. try:
  111. os.unlink(get_pip_path)
  112. except Exception:
  113. pass
  114. return ok
  115. except Exception as e:
  116. print(f"[WARN] Failed to download or run get-pip.py: {e}")
  117. return False
  118. def check_pip():
  119. """先检测 pip 是否已安装,未安装则退出"""
  120. if not PY_PIP.exists():
  121. print(f"[X] pip is missing. Put pip.exe in: {PY_SCRIPTS}")
  122. sys.exit(1)
  123. print("[OK] pip is ready")
  124. def _venv_home():
  125. """读取 venv 的 pyvenv.cfg 中的 home(创建该 venv 的 Python 路径)"""
  126. cfg = VENV_PATH / "pyvenv.cfg"
  127. if not cfg.exists():
  128. return None
  129. try:
  130. for line in cfg.read_text(encoding="utf-8").splitlines():
  131. line = line.strip()
  132. if line.startswith("home ") or line.startswith("home="):
  133. return line.split("=", 1)[-1].strip()
  134. except Exception:
  135. pass
  136. return None
  137. def ensure_venv():
  138. """确保虚拟环境存在,且由当前 Python (sys.executable) 创建"""
  139. current_python_home = str(Path(sys.executable).resolve().parent)
  140. if VENV_PATH.exists():
  141. existing_home = _venv_home()
  142. if existing_home:
  143. existing_home = str(Path(existing_home).resolve())
  144. if existing_home and existing_home != current_python_home:
  145. print("[WARN] venv was created by another Python, recreating with current Python...")
  146. import shutil
  147. try:
  148. shutil.rmtree(VENV_PATH)
  149. except Exception as e:
  150. print(f"[X] Failed to remove old venv: {e}")
  151. sys.exit(1)
  152. elif existing_home == current_python_home:
  153. return True
  154. if not VENV_PATH.exists():
  155. print("[WARN] Virtual environment not found, creating...")
  156. success, out, err = run_command(f'"{sys.executable}" -m venv "{VENV_PATH}"', check=False)
  157. merged_err = (err or "") + (out or "")
  158. if not success and "No module named venv" in merged_err:
  159. # 嵌入式 Python 无 venv,优先使用已有的 virtualenv.exe,否则用 pip 安装
  160. if PY_VIRTUALENV.exists():
  161. venv_cmd = f'"{PY_VIRTUALENV}" "{VENV_PATH}"'
  162. else:
  163. if not PY_PIP.exists():
  164. print(f"[X] Missing: {PY_PIP} and {PY_VIRTUALENV}. Put pip and virtualenv in py/Scripts/")
  165. sys.exit(1)
  166. print("[WARN] venv module is missing. Installing virtualenv with pip...")
  167. pip_ok, pip_out, pip_err = run_command(f'"{PY_PIP}" install {PIP_INDEX_ARGS} --no-warn-script-location virtualenv', check=False)
  168. if not pip_ok:
  169. print(f"[X] Failed to install virtualenv. Error: {(pip_err or '') + (pip_out or '')}")
  170. sys.exit(1)
  171. print("[OK] virtualenv installed successfully via pip")
  172. venv_cmd = f'"{PY_VIRTUALENV}" "{VENV_PATH}"' if PY_VIRTUALENV.exists() else f'"{sys.executable}" -m virtualenv "{VENV_PATH}"'
  173. success, out, err = run_command(venv_cmd, check=False)
  174. merged_err = (err or "") + (out or "")
  175. # virtualenv 损坏(如 No discovery plugin found)时:先删原虚拟环境,再重装 virtualenv 并重建
  176. if not success:
  177. err_msg = merged_err.strip() or '(no error output)'
  178. if "No discovery plugin found" in err_msg or "discovery plugin" in err_msg.lower():
  179. import shutil
  180. # 先删除原来的虚拟环境(含残缺的 env 目录)
  181. if VENV_PATH.exists():
  182. try:
  183. shutil.rmtree(VENV_PATH)
  184. print("[OK] Old virtual environment removed, recreating...")
  185. except Exception as e:
  186. print(f"[WARN] Failed to remove env: {e}")
  187. print("[WARN] virtualenv is broken, reinstalling virtualenv...")
  188. reinstall_ok, _, reinstall_err = run_command(
  189. f'"{sys.executable}" -m pip install {PIP_INDEX_ARGS} --no-warn-script-location --force-reinstall virtualenv',
  190. check=False
  191. )
  192. # pip 自身损坏(如缺少 pip._internal.operations.build)时用 get-pip.py 重装(不依赖残缺 pip,优先国内源)
  193. if not reinstall_ok and ("pip._internal" in (reinstall_err or "") or "ModuleNotFoundError" in (reinstall_err or "")):
  194. print("[WARN] pip is broken (embedded pip may be incomplete), reinstalling pip with get-pip.py...")
  195. if repair_pip_with_get_pip():
  196. reinstall_ok, _, reinstall_err = run_command(
  197. f'"{sys.executable}" -m pip install {PIP_INDEX_ARGS} --no-warn-script-location --force-reinstall virtualenv',
  198. check=False
  199. )
  200. if not reinstall_ok:
  201. ensure_ok, _, _ = run_command(f'"{sys.executable}" -m ensurepip --upgrade', check=False)
  202. if ensure_ok:
  203. reinstall_ok, _, reinstall_err = run_command(
  204. f'"{sys.executable}" -m pip install {PIP_INDEX_ARGS} --no-warn-script-location --force-reinstall virtualenv',
  205. check=False
  206. )
  207. if not reinstall_ok:
  208. print(f"[X] Failed to reinstall virtualenv: {reinstall_err or '(no output)'}")
  209. print("[HINT] Embedded pip in python/x64/py may be incomplete (missing pip._internal.operations.build).")
  210. print(" Option 1: put get-pip.py in python/x64/ or project root, then rerun this script (local file is preferred).")
  211. print(" Option 2: run manually (download get-pip.py from: http://mirrors.aliyun.com/pypi/get-pip.py):")
  212. print(" python get-pip.py --force-reinstall -i https://pypi.tuna.tsinghua.edu.cn/simple --trusted-host pypi.tuna.tsinghua.edu.cn")
  213. sys.exit(1)
  214. if not reinstall_ok:
  215. print(f"[X] Failed to reinstall virtualenv: {reinstall_err or '(no output)'}")
  216. sys.exit(1)
  217. print("[OK] virtualenv reinstalled")
  218. # 重新创建虚拟环境:优先 -m venv,再 fallback 到 virtualenv
  219. success, out, err = run_command(f'"{sys.executable}" -m venv "{VENV_PATH}"', check=False)
  220. merged_err = (err or "") + (out or "")
  221. if not success and "No module named venv" in merged_err:
  222. venv_cmd = f'"{PY_VIRTUALENV}" "{VENV_PATH}"' if PY_VIRTUALENV.exists() else f'"{sys.executable}" -m virtualenv "{VENV_PATH}"'
  223. success, out, err = run_command(venv_cmd, check=False)
  224. merged_err = (err or "") + (out or "")
  225. if not success:
  226. err_msg = merged_err.strip() or '(no error output)'
  227. print(f"[X] Failed to create virtual environment: {err_msg}")
  228. if "No discovery plugin found" in err_msg:
  229. print("[HINT] virtualenv dist-info is missing entry_points.txt. Check virtualenv-*.dist-info/")
  230. if "pythonw.exe" in err_msg or "FileNotFoundError" in err_msg:
  231. print("[HINT] py/pythonw.exe is required. You can copy it from python.exe.")
  232. sys.exit(1)
  233. print("[OK] Virtual environment created successfully")
  234. return True
  235. def get_venv_pip():
  236. """获取虚拟环境的 pip 命令"""
  237. if platform.system() == "Windows":
  238. return str(VENV_PIP)
  239. else:
  240. return str(VENV_PIP)
  241. def read_dependencies(source_file):
  242. """读取依赖列表"""
  243. if not source_file.exists():
  244. return []
  245. dependencies = []
  246. with open(source_file, 'r', encoding='utf-8') as f:
  247. for line in f:
  248. line = line.strip()
  249. # 跳过注释和空行
  250. if line and not line.startswith('#'):
  251. dependencies.append(line)
  252. return dependencies
  253. def get_installed_packages_from_filesystem():
  254. """直接从文件系统获取已安装的包列表(快速方法)"""
  255. if platform.system() == "Windows":
  256. site_packages = VENV_PATH / "Lib" / "site-packages"
  257. else:
  258. # Linux/Mac: 需要找到 site-packages 路径
  259. import sysconfig
  260. site_packages = Path(sysconfig.get_path('purelib', vars={'base': str(VENV_PATH)}))
  261. installed_packages = set()
  262. if site_packages.exists():
  263. for item in site_packages.iterdir():
  264. if item.is_dir():
  265. pkg_name = item.name
  266. # 处理 .dist-info 和 .egg-info 文件夹(最准确的包名来源)
  267. if pkg_name.endswith('.dist-info'):
  268. # 从 dist-info 文件夹名提取包名(格式:package-name-version.dist-info)
  269. parts = pkg_name.replace('.dist-info', '').rsplit('-', 1)
  270. if len(parts) >= 1:
  271. installed_packages.add(parts[0].lower().replace('_', '-'))
  272. elif pkg_name.endswith('.egg-info'):
  273. # 从 egg-info 文件夹名提取包名
  274. parts = pkg_name.replace('.egg-info', '').rsplit('-', 1)
  275. if len(parts) >= 1:
  276. installed_packages.add(parts[0].lower().replace('_', '-'))
  277. elif pkg_name not in ['__pycache__', 'dist-info', 'egg-info']:
  278. # 检查是否是 Python 包(有 __init__.py 或 .py 文件)
  279. if (item / "__init__.py").exists() or any(item.glob("*.py")):
  280. pkg_lower = pkg_name.lower()
  281. installed_packages.add(pkg_lower)
  282. # 添加下划线和连字符的变体
  283. installed_packages.add(pkg_lower.replace('_', '-'))
  284. installed_packages.add(pkg_lower.replace('-', '_'))
  285. # 特殊映射:opencv-python 安装后显示为 cv2
  286. if 'cv2' in installed_packages:
  287. installed_packages.add('opencv-python')
  288. installed_packages.add('opencv-contrib-python')
  289. installed_packages.add('opencv-python-headless')
  290. return installed_packages
  291. def check_package_installed(package_name, installed_packages_set=None):
  292. """检查包是否已安装(使用文件系统快速检查)"""
  293. # 提取包名(支持 ==, >=, <=, >, <, ~= 等版本操作符)
  294. pkg_name = package_name.split('==')[0].split('>=')[0].split('<=')[0].split('>')[0].split('<')[0].split('~=')[0].strip()
  295. pkg_name_lower = pkg_name.lower()
  296. # 如果没有提供已安装包集合,则获取一次(避免重复调用)
  297. if installed_packages_set is None:
  298. installed_packages_set = get_installed_packages_from_filesystem()
  299. # 快速检查(使用已获取的集合)
  300. return (
  301. pkg_name_lower in installed_packages_set or
  302. pkg_name_lower.replace('-', '_') in installed_packages_set or
  303. pkg_name_lower.replace('_', '-') in installed_packages_set
  304. )
  305. def install_packages(packages, source_file, venv_pip):
  306. """安装包到虚拟环境,每装好一个打印进度"""
  307. failed_packages = []
  308. total = len(packages)
  309. for i, package in enumerate(packages, 1):
  310. pkg_display = package.split("==")[0].strip() if "==" in package else package.strip()
  311. print(f"[{i}/{total}] Installing {pkg_display} ...", end=" ", flush=True)
  312. cmd = f'"{venv_pip}" install {PIP_INDEX_ARGS} --no-warn-script-location {package}'
  313. success, _, error = run_command(cmd, check=False)
  314. if success:
  315. print("OK")
  316. else:
  317. print("FAILED")
  318. failed_packages.append(package)
  319. if failed_packages:
  320. return False, failed_packages
  321. return True, []
  322. def sync_environment_file(venv_pip, silent=False):
  323. """同步所有已安装的包到 environment.txt"""
  324. cmd = f'"{venv_pip}" freeze'
  325. success, output, error = run_command(cmd, check=False)
  326. if not success:
  327. if not silent:
  328. print(f"[X] Failed to get installed packages list: {error}")
  329. return False
  330. # 使用 UTF-8 无 BOM 编码写入文件
  331. with open(ENVIRONMENT_FILE, 'w', encoding='utf-8', newline='\n') as f:
  332. f.write(output)
  333. if not silent:
  334. package_count = len([line for line in output.strip().split('\n') if line.strip()])
  335. print(f"[OK] All installed packages synced to {ENVIRONMENT_FILE}")
  336. print(f" Total packages: {package_count}")
  337. return True
  338. def main():
  339. """主函数"""
  340. # 1. 先检测 pip 是否安装
  341. check_pip()
  342. # 2. 再检测并创建虚拟环境
  343. if not ensure_venv():
  344. sys.exit(1)
  345. venv_pip = get_venv_pip()
  346. # 确定依赖源文件(优先使用 requirements.txt,如果没有则使用 environment.txt)
  347. if REQUIREMENTS_FILE.exists():
  348. source_file = REQUIREMENTS_FILE
  349. elif ENVIRONMENT_FILE.exists():
  350. source_file = ENVIRONMENT_FILE
  351. else:
  352. sync_environment_file(venv_pip)
  353. sys.exit(0)
  354. # 读取依赖列表
  355. required_packages = read_dependencies(source_file)
  356. if not required_packages:
  357. print("[OK] No dependencies specified")
  358. sys.exit(0)
  359. # 快速检查缺失的依赖(使用文件系统)
  360. missing_packages = []
  361. installed_count = 0
  362. missing_count = 0
  363. # 一次性获取所有已安装的包(只检查一次文件系统)
  364. installed_packages_set = get_installed_packages_from_filesystem()
  365. for package in required_packages:
  366. package_line = package.strip()
  367. if not package_line:
  368. continue
  369. # 提取包名
  370. package_name = package_line.split('==')[0].split('>=')[0].split('<=')[0].split('>')[0].split('<')[0].split('~=')[0].strip()
  371. pkg_name_lower = package_name.lower()
  372. # 快速检查(使用已获取的集合)
  373. is_installed = (
  374. pkg_name_lower in installed_packages_set or
  375. pkg_name_lower.replace('-', '_') in installed_packages_set or
  376. pkg_name_lower.replace('_', '-') in installed_packages_set
  377. )
  378. if is_installed:
  379. installed_count += 1
  380. else:
  381. missing_packages.append(package_line)
  382. missing_count += 1
  383. # 如果有缺失的依赖,显示必要信息并安装
  384. if missing_count > 0:
  385. print(f"[X] Missing {missing_count} package(s) out of {len(required_packages)}")
  386. print("Missing packages:")
  387. for missing in missing_packages:
  388. print(f" - {missing}")
  389. print(f"\nInstalling {missing_count} missing package(s)...\n")
  390. success, failed = install_packages(missing_packages, source_file, venv_pip)
  391. if success:
  392. print("[OK] All packages installed successfully")
  393. else:
  394. if failed:
  395. print(f"[X] Failed to install {len(failed)} package(s):")
  396. for pkg in failed:
  397. print(f" - {pkg}")
  398. else:
  399. print("[X] Some packages installation failed")
  400. # 即使有失败,也继续同步已安装的包
  401. print("[WARN] Continuing to sync installed packages...")
  402. # 同步所有已安装的包到 environment.txt
  403. sync_environment_file(venv_pip)
  404. else:
  405. # 所有依赖都齐全时,只显示一行信息(包含同步结果)
  406. sync_environment_file(venv_pip, silent=True)
  407. print(f"[OK] All dependencies are installed ({len(required_packages)} packages)")
  408. sys.exit(0)
  409. if __name__ == "__main__":
  410. main()