python-enviroment-install.py 16 KB

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