python-enviroment-install.py 18 KB

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