| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474 |
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
- """
- Python 依赖安装和同步脚本
- 功能:检查、安装 Python 依赖到虚拟环境,然后同步所有已安装的包到 environment.txt
- 约定:由嵌入式 Python(python/x64/py 或 python/arm64/py)运行;虚拟环境在 env,可用 virtualenv 创建。
- """
- import os
- import sys
- import subprocess
- import platform
- import tempfile
- import urllib.request
- import urllib.error
- import ssl
- from pathlib import Path
- # pip 损坏时用 get-pip.py 重装;优先国内源避免 SSL/外网问题(见 https://pip.pypa.io/en/stable/installation/)
- GET_PIP_URLS = [
- "http://mirrors.aliyun.com/pypi/get-pip.py",
- "https://mirrors.aliyun.com/pypi/get-pip.py",
- "https://bootstrap.pypa.io/get-pip.py",
- ]
- # 国内 pip 镜像源,避免外网慢或超时(可改为阿里/豆瓣等)
- PIP_INDEX_URL = os.environ.get("PIP_INDEX_URL", "https://pypi.tuna.tsinghua.edu.cn/simple")
- PIP_TRUSTED_HOST = "pypi.tuna.tsinghua.edu.cn"
- PIP_INDEX_ARGS = f'-i {PIP_INDEX_URL} --trusted-host {PIP_TRUSTED_HOST}'
- # 脚本所在目录即本环境目录(python/arm64 或 python/x64),venv 固定为 env
- SCRIPT_DIR = Path(__file__).parent.absolute()
- PROJECT_ROOT = SCRIPT_DIR.parent.parent.absolute()
- _env_path = os.environ.get("PYTHON_VENV_PATH", "").strip()
- VENV_PATH = Path(_env_path) if _env_path else (SCRIPT_DIR / "env")
- ENVIRONMENT_FILE = SCRIPT_DIR / "environment.txt"
- REQUIREMENTS_FILE = PROJECT_ROOT / "requirements.txt"
- # 根据操作系统确定虚拟环境的 Python 和 pip 路径
- # Windows 优先使用 .bat 脚本(可读、可编辑),否则用 .exe
- if platform.system() == "Windows":
- VENV_PYTHON = VENV_PATH / "Scripts" / "python.exe"
- VENV_PIP = VENV_PATH / "Scripts" / "pip.exe"
- PY_SCRIPTS = SCRIPT_DIR / "py" / "Scripts"
- PY_PIP = PY_SCRIPTS / "pip.bat" if (PY_SCRIPTS / "pip.bat").exists() else PY_SCRIPTS / "pip.exe"
- PY_VIRTUALENV = PY_SCRIPTS / "virtualenv.bat" if (PY_SCRIPTS / "virtualenv.bat").exists() else PY_SCRIPTS / "virtualenv.exe"
- else:
- VENV_PYTHON = VENV_PATH / "bin" / "python"
- VENV_PIP = VENV_PATH / "bin" / "pip"
- PY_SCRIPTS = SCRIPT_DIR / "py" / "bin"
- PY_PIP = PY_SCRIPTS / "pip"
- PY_VIRTUALENV = PY_SCRIPTS / "virtualenv"
- def run_command(cmd, check=True, capture_output=True):
- """运行命令并返回结果"""
- try:
- result = subprocess.run(
- cmd,
- shell=True,
- check=check,
- capture_output=capture_output,
- text=True,
- encoding='utf-8'
- )
- return result.returncode == 0, result.stdout, result.stderr
- except subprocess.CalledProcessError as e:
- return False, e.stdout if hasattr(e, 'stdout') else "", str(e)
- def _find_or_download_get_pip(get_pip_path):
- """优先使用项目内已有 get-pip.py,否则从国内源/官方源下载"""
- for local_dir in (SCRIPT_DIR, PROJECT_ROOT):
- candidate = Path(local_dir) / "get-pip.py"
- if candidate.exists():
- with open(candidate, "rb") as f:
- Path(get_pip_path).write_bytes(f.read())
- return True
- return _download_get_pip(get_pip_path)
- def _download_get_pip(get_pip_path):
- """从国内源或官方源下载 get-pip.py,HTTPS 遇 SSL 时用不验证上下文重试"""
- last_err = None
- for url in GET_PIP_URLS:
- try:
- ctx = None
- if url.startswith('https://'):
- ctx = ssl.create_default_context()
- req = urllib.request.urlopen(url, timeout=30, context=ctx)
- with open(get_pip_path, 'wb') as f:
- f.write(req.read())
- return True
- except urllib.error.URLError as e:
- last_err = e
- if 'SSL' in str(e) or 'CERTIFICATE' in str(e).upper():
- try:
- ctx = ssl._create_unverified_context()
- req = urllib.request.urlopen(url, timeout=30, context=ctx)
- with open(get_pip_path, 'wb') as f:
- f.write(req.read())
- return True
- except Exception as retry_e:
- last_err = retry_e
- except Exception as e:
- last_err = e
- if last_err:
- print(f"[WARN] Failed to download get-pip.py: {last_err}")
- return False
- def repair_pip_with_get_pip():
- """当 pip 损坏(如缺少 pip._internal.operations.build)时,用 get-pip.py 强制重装 pip(优先国内源)"""
- try:
- fd, get_pip_path = tempfile.mkstemp(suffix='.py')
- os.close(fd)
- if not _find_or_download_get_pip(get_pip_path):
- return False
- ok, out, err = run_command(
- f'"{sys.executable}" "{get_pip_path}" --force-reinstall -i {PIP_INDEX_URL} --trusted-host {PIP_TRUSTED_HOST} --no-warn-script-location',
- check=False
- )
- try:
- os.unlink(get_pip_path)
- except Exception:
- pass
- return ok
- except Exception as e:
- print(f"[WARN] Failed to download or run get-pip.py: {e}")
- return False
- def check_pip():
- """先检测 pip 是否已安装,未安装则退出"""
- if not PY_PIP.exists():
- print(f"[X] pip is missing. Put pip.exe in: {PY_SCRIPTS}")
- sys.exit(1)
- print("[OK] pip is ready")
- def _venv_home():
- """读取 venv 的 pyvenv.cfg 中的 home(创建该 venv 的 Python 路径)"""
- cfg = VENV_PATH / "pyvenv.cfg"
- if not cfg.exists():
- return None
- try:
- for line in cfg.read_text(encoding="utf-8").splitlines():
- line = line.strip()
- if line.startswith("home ") or line.startswith("home="):
- return line.split("=", 1)[-1].strip()
- except Exception:
- pass
- return None
- def ensure_venv():
- """确保虚拟环境存在,且由当前 Python (sys.executable) 创建"""
- current_python_home = str(Path(sys.executable).resolve().parent)
- if VENV_PATH.exists():
- existing_home = _venv_home()
- if existing_home:
- existing_home = str(Path(existing_home).resolve())
- if existing_home and existing_home != current_python_home:
- print("[WARN] venv was created by another Python, recreating with current Python...")
- import shutil
- try:
- shutil.rmtree(VENV_PATH)
- except Exception as e:
- print(f"[X] Failed to remove old venv: {e}")
- sys.exit(1)
- elif existing_home == current_python_home:
- return True
- if not VENV_PATH.exists():
- print("[WARN] Virtual environment not found, creating...")
- success, out, err = run_command(f'"{sys.executable}" -m venv "{VENV_PATH}"', check=False)
- merged_err = (err or "") + (out or "")
- if not success and "No module named venv" in merged_err:
- # 嵌入式 Python 无 venv,优先使用已有的 virtualenv.exe,否则用 pip 安装
- if PY_VIRTUALENV.exists():
- venv_cmd = f'"{PY_VIRTUALENV}" "{VENV_PATH}"'
- else:
- if not PY_PIP.exists():
- print(f"[X] Missing: {PY_PIP} and {PY_VIRTUALENV}. Put pip and virtualenv in py/Scripts/")
- sys.exit(1)
- print("[WARN] venv module is missing. Installing virtualenv with pip...")
- pip_ok, pip_out, pip_err = run_command(f'"{PY_PIP}" install {PIP_INDEX_ARGS} --no-warn-script-location virtualenv', check=False)
- if not pip_ok:
- print(f"[X] Failed to install virtualenv. Error: {(pip_err or '') + (pip_out or '')}")
- sys.exit(1)
- print("[OK] virtualenv installed successfully via pip")
- venv_cmd = f'"{PY_VIRTUALENV}" "{VENV_PATH}"' if PY_VIRTUALENV.exists() else f'"{sys.executable}" -m virtualenv "{VENV_PATH}"'
- success, out, err = run_command(venv_cmd, check=False)
- merged_err = (err or "") + (out or "")
- # virtualenv 损坏(如 No discovery plugin found)时:先删原虚拟环境,再重装 virtualenv 并重建
- if not success:
- err_msg = merged_err.strip() or '(no error output)'
- if "No discovery plugin found" in err_msg or "discovery plugin" in err_msg.lower():
- import shutil
- # 先删除原来的虚拟环境(含残缺的 env 目录)
- if VENV_PATH.exists():
- try:
- shutil.rmtree(VENV_PATH)
- print("[OK] Old virtual environment removed, recreating...")
- except Exception as e:
- print(f"[WARN] Failed to remove env: {e}")
- print("[WARN] virtualenv is broken, reinstalling virtualenv...")
- reinstall_ok, _, reinstall_err = run_command(
- f'"{sys.executable}" -m pip install {PIP_INDEX_ARGS} --no-warn-script-location --force-reinstall virtualenv',
- check=False
- )
- # pip 自身损坏(如缺少 pip._internal.operations.build)时用 get-pip.py 重装(不依赖残缺 pip,优先国内源)
- if not reinstall_ok and ("pip._internal" in (reinstall_err or "") or "ModuleNotFoundError" in (reinstall_err or "")):
- print("[WARN] pip is broken (embedded pip may be incomplete), reinstalling pip with get-pip.py...")
- if repair_pip_with_get_pip():
- reinstall_ok, _, reinstall_err = run_command(
- f'"{sys.executable}" -m pip install {PIP_INDEX_ARGS} --no-warn-script-location --force-reinstall virtualenv',
- check=False
- )
- if not reinstall_ok:
- ensure_ok, _, _ = run_command(f'"{sys.executable}" -m ensurepip --upgrade', check=False)
- if ensure_ok:
- reinstall_ok, _, reinstall_err = run_command(
- f'"{sys.executable}" -m pip install {PIP_INDEX_ARGS} --no-warn-script-location --force-reinstall virtualenv',
- check=False
- )
- if not reinstall_ok:
- print(f"[X] Failed to reinstall virtualenv: {reinstall_err or '(no output)'}")
- print("[HINT] Embedded pip in python/x64/py may be incomplete (missing pip._internal.operations.build).")
- print(" Option 1: put get-pip.py in python/x64/ or project root, then rerun this script (local file is preferred).")
- print(" Option 2: run manually (download get-pip.py from: http://mirrors.aliyun.com/pypi/get-pip.py):")
- print(" python get-pip.py --force-reinstall -i https://pypi.tuna.tsinghua.edu.cn/simple --trusted-host pypi.tuna.tsinghua.edu.cn")
- sys.exit(1)
- if not reinstall_ok:
- print(f"[X] Failed to reinstall virtualenv: {reinstall_err or '(no output)'}")
- sys.exit(1)
- print("[OK] virtualenv reinstalled")
- # 重新创建虚拟环境:优先 -m venv,再 fallback 到 virtualenv
- success, out, err = run_command(f'"{sys.executable}" -m venv "{VENV_PATH}"', check=False)
- merged_err = (err or "") + (out or "")
- if not success and "No module named venv" in merged_err:
- venv_cmd = f'"{PY_VIRTUALENV}" "{VENV_PATH}"' if PY_VIRTUALENV.exists() else f'"{sys.executable}" -m virtualenv "{VENV_PATH}"'
- success, out, err = run_command(venv_cmd, check=False)
- merged_err = (err or "") + (out or "")
- if not success:
- err_msg = merged_err.strip() or '(no error output)'
- print(f"[X] Failed to create virtual environment: {err_msg}")
- if "No discovery plugin found" in err_msg:
- print("[HINT] virtualenv dist-info is missing entry_points.txt. Check virtualenv-*.dist-info/")
- if "pythonw.exe" in err_msg or "FileNotFoundError" in err_msg:
- print("[HINT] py/pythonw.exe is required. You can copy it from python.exe.")
- sys.exit(1)
- print("[OK] Virtual environment created successfully")
- return True
- def get_venv_pip():
- """获取虚拟环境的 pip 命令"""
- if platform.system() == "Windows":
- return str(VENV_PIP)
- else:
- return str(VENV_PIP)
- def read_dependencies(source_file):
- """读取依赖列表"""
- if not source_file.exists():
- return []
-
- dependencies = []
- with open(source_file, 'r', encoding='utf-8') as f:
- for line in f:
- line = line.strip()
- # 跳过注释和空行
- if line and not line.startswith('#'):
- dependencies.append(line)
- return dependencies
- def get_installed_packages_from_filesystem():
- """直接从文件系统获取已安装的包列表(快速方法)"""
- if platform.system() == "Windows":
- site_packages = VENV_PATH / "Lib" / "site-packages"
- else:
- # Linux/Mac: 需要找到 site-packages 路径
- import sysconfig
- site_packages = Path(sysconfig.get_path('purelib', vars={'base': str(VENV_PATH)}))
-
- installed_packages = set()
-
- if site_packages.exists():
- for item in site_packages.iterdir():
- if item.is_dir():
- pkg_name = item.name
-
- # 处理 .dist-info 和 .egg-info 文件夹(最准确的包名来源)
- if pkg_name.endswith('.dist-info'):
- # 从 dist-info 文件夹名提取包名(格式:package-name-version.dist-info)
- parts = pkg_name.replace('.dist-info', '').rsplit('-', 1)
- if len(parts) >= 1:
- installed_packages.add(parts[0].lower().replace('_', '-'))
- elif pkg_name.endswith('.egg-info'):
- # 从 egg-info 文件夹名提取包名
- parts = pkg_name.replace('.egg-info', '').rsplit('-', 1)
- if len(parts) >= 1:
- installed_packages.add(parts[0].lower().replace('_', '-'))
- elif pkg_name not in ['__pycache__', 'dist-info', 'egg-info']:
- # 检查是否是 Python 包(有 __init__.py 或 .py 文件)
- if (item / "__init__.py").exists() or any(item.glob("*.py")):
- pkg_lower = pkg_name.lower()
- installed_packages.add(pkg_lower)
- # 添加下划线和连字符的变体
- installed_packages.add(pkg_lower.replace('_', '-'))
- installed_packages.add(pkg_lower.replace('-', '_'))
-
- # 特殊映射:opencv-python 安装后显示为 cv2
- if 'cv2' in installed_packages:
- installed_packages.add('opencv-python')
- installed_packages.add('opencv-contrib-python')
- installed_packages.add('opencv-python-headless')
-
- return installed_packages
- def check_package_installed(package_name, installed_packages_set=None):
- """检查包是否已安装(使用文件系统快速检查)"""
- # 提取包名(支持 ==, >=, <=, >, <, ~= 等版本操作符)
- pkg_name = package_name.split('==')[0].split('>=')[0].split('<=')[0].split('>')[0].split('<')[0].split('~=')[0].strip()
- pkg_name_lower = pkg_name.lower()
-
- # 如果没有提供已安装包集合,则获取一次(避免重复调用)
- if installed_packages_set is None:
- installed_packages_set = get_installed_packages_from_filesystem()
-
- # 快速检查(使用已获取的集合)
- return (
- pkg_name_lower in installed_packages_set or
- pkg_name_lower.replace('-', '_') in installed_packages_set or
- pkg_name_lower.replace('_', '-') in installed_packages_set
- )
- def install_packages(packages, source_file, venv_pip):
- """安装包到虚拟环境,每装好一个打印进度"""
- failed_packages = []
- total = len(packages)
- for i, package in enumerate(packages, 1):
- pkg_display = package.split("==")[0].strip() if "==" in package else package.strip()
- print(f"[{i}/{total}] Installing {pkg_display} ...", end=" ", flush=True)
- cmd = f'"{venv_pip}" install {PIP_INDEX_ARGS} --no-warn-script-location {package}'
- success, _, error = run_command(cmd, check=False)
- if success:
- print("OK")
- else:
- print("FAILED")
- failed_packages.append(package)
- if failed_packages:
- return False, failed_packages
- return True, []
- def sync_environment_file(venv_pip, silent=False):
- """同步所有已安装的包到 environment.txt"""
- cmd = f'"{venv_pip}" freeze'
- success, output, error = run_command(cmd, check=False)
-
- if not success:
- if not silent:
- print(f"[X] Failed to get installed packages list: {error}")
- return False
-
- # 使用 UTF-8 无 BOM 编码写入文件
- with open(ENVIRONMENT_FILE, 'w', encoding='utf-8', newline='\n') as f:
- f.write(output)
-
- if not silent:
- package_count = len([line for line in output.strip().split('\n') if line.strip()])
- print(f"[OK] All installed packages synced to {ENVIRONMENT_FILE}")
- print(f" Total packages: {package_count}")
- return True
- def main():
- """主函数"""
- # 1. 先检测 pip 是否安装
- check_pip()
- # 2. 再检测并创建虚拟环境
- if not ensure_venv():
- sys.exit(1)
-
- venv_pip = get_venv_pip()
-
- # 确定依赖源文件(优先使用 requirements.txt,如果没有则使用 environment.txt)
- if REQUIREMENTS_FILE.exists():
- source_file = REQUIREMENTS_FILE
- elif ENVIRONMENT_FILE.exists():
- source_file = ENVIRONMENT_FILE
- else:
- sync_environment_file(venv_pip)
- sys.exit(0)
-
- # 读取依赖列表
- required_packages = read_dependencies(source_file)
-
- if not required_packages:
- print("[OK] No dependencies specified")
- sys.exit(0)
-
- # 快速检查缺失的依赖(使用文件系统)
- missing_packages = []
- installed_count = 0
- missing_count = 0
-
- # 一次性获取所有已安装的包(只检查一次文件系统)
- installed_packages_set = get_installed_packages_from_filesystem()
-
- for package in required_packages:
- package_line = package.strip()
- if not package_line:
- continue
-
- # 提取包名
- package_name = package_line.split('==')[0].split('>=')[0].split('<=')[0].split('>')[0].split('<')[0].split('~=')[0].strip()
- pkg_name_lower = package_name.lower()
-
- # 快速检查(使用已获取的集合)
- is_installed = (
- pkg_name_lower in installed_packages_set or
- pkg_name_lower.replace('-', '_') in installed_packages_set or
- pkg_name_lower.replace('_', '-') in installed_packages_set
- )
-
- if is_installed:
- installed_count += 1
- else:
- missing_packages.append(package_line)
- missing_count += 1
-
- # 如果有缺失的依赖,显示必要信息并安装
- if missing_count > 0:
- print(f"[X] Missing {missing_count} package(s) out of {len(required_packages)}")
-
- print("Missing packages:")
- for missing in missing_packages:
- print(f" - {missing}")
-
- print(f"\nInstalling {missing_count} missing package(s)...\n")
-
- success, failed = install_packages(missing_packages, source_file, venv_pip)
- if success:
- print("[OK] All packages installed successfully")
- else:
- if failed:
- print(f"[X] Failed to install {len(failed)} package(s):")
- for pkg in failed:
- print(f" - {pkg}")
- else:
- print("[X] Some packages installation failed")
- # 即使有失败,也继续同步已安装的包
- print("[WARN] Continuing to sync installed packages...")
-
- # 同步所有已安装的包到 environment.txt
- sync_environment_file(venv_pip)
- else:
- # 所有依赖都齐全时,只显示一行信息(包含同步结果)
- sync_environment_file(venv_pip, silent=True)
- print(f"[OK] All dependencies are installed ({len(required_packages)} packages)")
-
- sys.exit(0)
- if __name__ == "__main__":
- main()
|