python-enviroment-install.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  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. from pathlib import Path
  12. # 脚本所在目录即本环境目录(python/arm64 或 python/x64),venv 固定为 env
  13. SCRIPT_DIR = Path(__file__).parent.absolute()
  14. PROJECT_ROOT = SCRIPT_DIR.parent.parent.absolute()
  15. _env_path = os.environ.get("PYTHON_VENV_PATH", "").strip()
  16. VENV_PATH = Path(_env_path) if _env_path else SCRIPT_DIR / "env"
  17. ENVIRONMENT_FILE = SCRIPT_DIR / "environment.txt"
  18. REQUIREMENTS_FILE = PROJECT_ROOT / "requirements.txt"
  19. # 根据操作系统确定虚拟环境的 Python 和 pip 路径
  20. if platform.system() == "Windows":
  21. VENV_PYTHON = VENV_PATH / "Scripts" / "python.exe"
  22. VENV_PIP = VENV_PATH / "Scripts" / "pip.exe"
  23. else:
  24. VENV_PYTHON = VENV_PATH / "bin" / "python"
  25. VENV_PIP = VENV_PATH / "bin" / "pip"
  26. def run_command(cmd, check=True, capture_output=True):
  27. """运行命令并返回结果"""
  28. try:
  29. result = subprocess.run(
  30. cmd,
  31. shell=True,
  32. check=check,
  33. capture_output=capture_output,
  34. text=True,
  35. encoding='utf-8'
  36. )
  37. return result.returncode == 0, result.stdout, result.stderr
  38. except subprocess.CalledProcessError as e:
  39. return False, e.stdout if hasattr(e, 'stdout') else "", str(e)
  40. def _venv_home():
  41. """读取 venv 的 pyvenv.cfg 中的 home(创建该 venv 的 Python 路径)"""
  42. cfg = VENV_PATH / "pyvenv.cfg"
  43. if not cfg.exists():
  44. return None
  45. try:
  46. for line in cfg.read_text(encoding="utf-8").splitlines():
  47. line = line.strip()
  48. if line.startswith("home ") or line.startswith("home="):
  49. return line.split("=", 1)[-1].strip()
  50. except Exception:
  51. pass
  52. return None
  53. def ensure_venv():
  54. """确保虚拟环境存在,且由当前 Python (sys.executable) 创建"""
  55. current_python_home = str(Path(sys.executable).resolve().parent)
  56. if VENV_PATH.exists():
  57. existing_home = _venv_home()
  58. if existing_home:
  59. existing_home = str(Path(existing_home).resolve())
  60. if existing_home and existing_home != current_python_home:
  61. print("[WARN] venv was created by another Python, recreating with current Python...")
  62. import shutil
  63. try:
  64. shutil.rmtree(VENV_PATH)
  65. except Exception as e:
  66. print(f"[X] Failed to remove old venv: {e}")
  67. sys.exit(1)
  68. elif existing_home == current_python_home:
  69. return True
  70. if not VENV_PATH.exists():
  71. print("[WARN] Virtual environment not found, creating...")
  72. success, _, error = run_command(f'"{sys.executable}" -m venv "{VENV_PATH}"', check=False)
  73. if not success and "No module named venv" in (error or ""):
  74. print("[WARN] venv module not found, using virtualenv...")
  75. run_command(f'"{sys.executable}" -m pip install virtualenv -q', check=False)
  76. success, _, error = run_command(f'"{sys.executable}" -m virtualenv "{VENV_PATH}"', check=False)
  77. if not success:
  78. print(f"[X] Failed to create virtual environment: {error}")
  79. sys.exit(1)
  80. print("[OK] Virtual environment created successfully")
  81. return True
  82. def get_venv_pip():
  83. """获取虚拟环境的 pip 命令"""
  84. if platform.system() == "Windows":
  85. return str(VENV_PIP)
  86. else:
  87. return str(VENV_PIP)
  88. def read_dependencies(source_file):
  89. """读取依赖列表"""
  90. if not source_file.exists():
  91. return []
  92. dependencies = []
  93. with open(source_file, 'r', encoding='utf-8') as f:
  94. for line in f:
  95. line = line.strip()
  96. # 跳过注释和空行
  97. if line and not line.startswith('#'):
  98. dependencies.append(line)
  99. return dependencies
  100. def get_installed_packages_from_filesystem():
  101. """直接从文件系统获取已安装的包列表(快速方法)"""
  102. if platform.system() == "Windows":
  103. site_packages = VENV_PATH / "Lib" / "site-packages"
  104. else:
  105. # Linux/Mac: 需要找到 site-packages 路径
  106. import sysconfig
  107. site_packages = Path(sysconfig.get_path('purelib', vars={'base': str(VENV_PATH)}))
  108. installed_packages = set()
  109. if site_packages.exists():
  110. for item in site_packages.iterdir():
  111. if item.is_dir():
  112. pkg_name = item.name
  113. # 处理 .dist-info 和 .egg-info 文件夹(最准确的包名来源)
  114. if pkg_name.endswith('.dist-info'):
  115. # 从 dist-info 文件夹名提取包名(格式:package-name-version.dist-info)
  116. parts = pkg_name.replace('.dist-info', '').rsplit('-', 1)
  117. if len(parts) >= 1:
  118. installed_packages.add(parts[0].lower().replace('_', '-'))
  119. elif pkg_name.endswith('.egg-info'):
  120. # 从 egg-info 文件夹名提取包名
  121. parts = pkg_name.replace('.egg-info', '').rsplit('-', 1)
  122. if len(parts) >= 1:
  123. installed_packages.add(parts[0].lower().replace('_', '-'))
  124. elif pkg_name not in ['__pycache__', 'dist-info', 'egg-info']:
  125. # 检查是否是 Python 包(有 __init__.py 或 .py 文件)
  126. if (item / "__init__.py").exists() or any(item.glob("*.py")):
  127. pkg_lower = pkg_name.lower()
  128. installed_packages.add(pkg_lower)
  129. # 添加下划线和连字符的变体
  130. installed_packages.add(pkg_lower.replace('_', '-'))
  131. installed_packages.add(pkg_lower.replace('-', '_'))
  132. # 特殊映射:opencv-python 安装后显示为 cv2
  133. if 'cv2' in installed_packages:
  134. installed_packages.add('opencv-python')
  135. installed_packages.add('opencv-contrib-python')
  136. installed_packages.add('opencv-python-headless')
  137. return installed_packages
  138. def check_package_installed(package_name, installed_packages_set=None):
  139. """检查包是否已安装(使用文件系统快速检查)"""
  140. # 提取包名(支持 ==, >=, <=, >, <, ~= 等版本操作符)
  141. pkg_name = package_name.split('==')[0].split('>=')[0].split('<=')[0].split('>')[0].split('<')[0].split('~=')[0].strip()
  142. pkg_name_lower = pkg_name.lower()
  143. # 如果没有提供已安装包集合,则获取一次(避免重复调用)
  144. if installed_packages_set is None:
  145. installed_packages_set = get_installed_packages_from_filesystem()
  146. # 快速检查(使用已获取的集合)
  147. return (
  148. pkg_name_lower in installed_packages_set or
  149. pkg_name_lower.replace('-', '_') in installed_packages_set or
  150. pkg_name_lower.replace('_', '-') in installed_packages_set
  151. )
  152. def install_packages(packages, source_file, venv_pip):
  153. """安装包到虚拟环境"""
  154. failed_packages = []
  155. if source_file == REQUIREMENTS_FILE and REQUIREMENTS_FILE.exists():
  156. # 使用 requirements.txt 批量安装
  157. cmd = f'"{venv_pip}" install -r "{source_file}"'
  158. success, _, error = run_command(cmd, check=False)
  159. if not success:
  160. print(f"[X] Installation failed: {error}")
  161. return False, failed_packages
  162. else:
  163. # 逐个安装
  164. for package in packages:
  165. cmd = f'"{venv_pip}" install {package}'
  166. success, _, error = run_command(cmd, check=False)
  167. if not success:
  168. print(f"[X] Failed to install: {package}")
  169. failed_packages.append(package)
  170. if failed_packages:
  171. return False, failed_packages
  172. return True, []
  173. def sync_environment_file(venv_pip, silent=False):
  174. """同步所有已安装的包到 environment.txt"""
  175. cmd = f'"{venv_pip}" freeze'
  176. success, output, error = run_command(cmd, check=False)
  177. if not success:
  178. if not silent:
  179. print(f"[X] Failed to get installed packages list: {error}")
  180. return False
  181. # 使用 UTF-8 无 BOM 编码写入文件
  182. with open(ENVIRONMENT_FILE, 'w', encoding='utf-8', newline='\n') as f:
  183. f.write(output)
  184. if not silent:
  185. package_count = len([line for line in output.strip().split('\n') if line.strip()])
  186. print(f"[OK] All installed packages synced to {ENVIRONMENT_FILE}")
  187. print(f" Total packages: {package_count}")
  188. return True
  189. def main():
  190. """主函数"""
  191. # 确保虚拟环境存在
  192. if not ensure_venv():
  193. sys.exit(1)
  194. venv_pip = get_venv_pip()
  195. # 确定依赖源文件(优先使用 requirements.txt,如果没有则使用 environment.txt)
  196. if REQUIREMENTS_FILE.exists():
  197. source_file = REQUIREMENTS_FILE
  198. elif ENVIRONMENT_FILE.exists():
  199. source_file = ENVIRONMENT_FILE
  200. else:
  201. sync_environment_file(venv_pip)
  202. sys.exit(0)
  203. # 读取依赖列表
  204. required_packages = read_dependencies(source_file)
  205. if not required_packages:
  206. print("[OK] No dependencies specified")
  207. sys.exit(0)
  208. # 快速检查缺失的依赖(使用文件系统)
  209. missing_packages = []
  210. installed_count = 0
  211. missing_count = 0
  212. # 一次性获取所有已安装的包(只检查一次文件系统)
  213. installed_packages_set = get_installed_packages_from_filesystem()
  214. for package in required_packages:
  215. package_line = package.strip()
  216. if not package_line:
  217. continue
  218. # 提取包名
  219. package_name = package_line.split('==')[0].split('>=')[0].split('<=')[0].split('>')[0].split('<')[0].split('~=')[0].strip()
  220. pkg_name_lower = package_name.lower()
  221. # 快速检查(使用已获取的集合)
  222. is_installed = (
  223. pkg_name_lower in installed_packages_set or
  224. pkg_name_lower.replace('-', '_') in installed_packages_set or
  225. pkg_name_lower.replace('_', '-') in installed_packages_set
  226. )
  227. if is_installed:
  228. installed_count += 1
  229. else:
  230. missing_packages.append(package_line)
  231. missing_count += 1
  232. # 如果有缺失的依赖,显示必要信息并安装
  233. if missing_count > 0:
  234. print(f"[X] Missing {missing_count} package(s) out of {len(required_packages)}")
  235. print("Missing packages:")
  236. for missing in missing_packages:
  237. print(f" - {missing}")
  238. print("\nInstalling missing packages...")
  239. success, failed = install_packages(missing_packages, source_file, venv_pip)
  240. if success:
  241. print("[OK] All packages installed successfully")
  242. else:
  243. if failed:
  244. print(f"[X] Failed to install {len(failed)} package(s):")
  245. for pkg in failed:
  246. print(f" - {pkg}")
  247. else:
  248. print("[X] Some packages installation failed")
  249. # 即使有失败,也继续同步已安装的包
  250. print("[WARN] Continuing to sync installed packages...")
  251. # 同步所有已安装的包到 environment.txt
  252. sync_environment_file(venv_pip)
  253. else:
  254. # 所有依赖都齐全时,只显示一行信息(包含同步结果)
  255. sync_environment_file(venv_pip, silent=True)
  256. print(f"[OK] All dependencies are installed ({len(required_packages)} packages)")
  257. sys.exit(0)
  258. if __name__ == "__main__":
  259. main()