| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140 |
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
- """第二处修复: 机器全局 PYTHONPATH 污染 .venv。
- 实测 (本机 PYTHONPATH="D:\\Program Files\\Python\\Lib\\site-packages;") :
- pip 在 venv 里看到系统 site-packages 的包, 判定 "Requirement already satisfied ... outside environment",
- 于是没有把传递依赖装进 .venv: venv 里缺 urllib3 / polars / pyyaml / jinja2 / python-dotenv 等,
- 并且 sys.path 里 PYTHONPATH 排在 venv site-packages **前面**, requests 被系统 2.33.0 顶掉了 pin 住的 2.34.2。
- 后果: 一旦换个没设 PYTHONPATH 的 shell (或别的机器用户), import requests/polars/yaml 直接 ModuleNotFoundError。
- 修复 (都不改变功能, 只要求 "跑的就是 .venv 里那一套"):
- D. install.ps1 / install.sh : 装依赖前摘掉 PYTHONPATH, 让 pip 把 requirements.txt 真正装进 .venv。
- E. guanlan.py : 启动时把 PYTHONPATH 从 sys.path/b环境里摘掉 (check/serve/stop 与子进程都干净)。
- 幂等: 重复运行报 "已修过"。
- """
- from __future__ import annotations
- import pathlib
- import shutil
- import sys
- ROOT = pathlib.Path(r"F:\temp\guanlan-rudong-v2_0.2.0")
- GUANLAN = ROOT / "guanlan.py"
- INSTALL_PS1 = ROOT / "install.ps1"
- INSTALL_SH = ROOT / "install.sh"
- BAK = ".bak-pyfix"
- changed: list[str] = []
- def fail(msg: str) -> None:
- print(" [X] " + msg)
- sys.exit(1)
- def backup(p: pathlib.Path) -> None:
- b = p.with_name(p.name + BAK)
- if not b.exists():
- shutil.copy2(p, b)
- print(f" 备份 {p.name} -> {b.name}")
- def sub1(text: str, old: str, new: str, what: str) -> str:
- n = text.count(old)
- if n != 1:
- fail(f"{what}: 锚点命中 {n} 次 (期望 1)")
- return text.replace(old, new)
- # ------------------------------------------------------------------ D1. install.ps1
- PS1_ANCHOR = '$ErrorActionPreference = "Stop"; $env:PYTHONUTF8 = "1"\n'
- PS1_INSERT = PS1_ANCHOR + (
- '# 机器上若全局设了 PYTHONPATH (例如指向 D:\\Program Files\\Python\\Lib\\site-packages), pip 会把那里的包\n'
- '# 当成 "已满足", 于是不装进 .venv —— venv 里就缺 urllib3 / polars / pyyaml / jinja2 等传递依赖,\n'
- '# 换个没设 PYTHONPATH 的 shell 立刻 import 失败; 且 PYTHONPATH 排在 venv 之前会顶掉 pin 住的版本。\n'
- '# 先把环境变量摘掉, 让下面 pip 老老实实按 requirements.txt 装进 .venv。\n'
- 'Remove-Item Env:PYTHONPATH -ErrorAction SilentlyContinue\n'
- )
- def fix_install_ps1() -> None:
- raw = INSTALL_PS1.read_bytes()
- text = raw.decode("utf-8-sig").replace("\r\n", "\n")
- if "Remove-Item Env:PYTHONPATH" in text:
- print(" [=] install.ps1 已修过, 跳过")
- return
- backup(INSTALL_PS1)
- text = sub1(text, PS1_ANCHOR, PS1_INSERT, "install.ps1")
- INSTALL_PS1.write_bytes(text.replace("\r\n", "\n").replace("\n", "\r\n").encode("utf-8-sig"))
- changed.append("install.ps1: 装依赖前 Remove-Item Env:PYTHONPATH")
- print(" [OK] install.ps1 已修 (保持 UTF-8 BOM + CRLF)")
- # ------------------------------------------------------------------ D2. install.sh
- SH_ANCHOR = 'set -e\n'
- SH_INSERT = SH_ANCHOR + (
- '# 同理: 全局 PYTHONPATH 会让 pip 误判依赖已满足而不装进 .venv (见 install.ps1 注释)\n'
- 'unset PYTHONPATH\n'
- )
- def fix_install_sh() -> None:
- raw = INSTALL_SH.read_bytes()
- if raw.startswith(b"\xef\xbb\xbf"):
- fail("install.sh 不该有 BOM")
- text = raw.decode("utf-8")
- if "unset PYTHONPATH" in text:
- print(" [=] install.sh 已修过, 跳过")
- return
- backup(INSTALL_SH)
- text = sub1(text, SH_ANCHOR, SH_INSERT, "install.sh")
- INSTALL_SH.write_bytes(text.encode("utf-8")) # 保持 LF / 无 BOM
- changed.append("install.sh: 装依赖前 unset PYTHONPATH")
- print(" [OK] install.sh 已修 (保持 LF)")
- # ------------------------------------------------------------------ E. guanlan.py
- G_ANCHOR = 'WIN = os.name == "nt"\n'
- G_INSERT = G_ANCHOR + '''
- def _hermetic():
- """摘掉机器全局的 PYTHONPATH: 本项目依赖都装在 .venv 里, 而 PYTHONPATH 会被插到 sys.path 前面,
- 顶掉 venv 里 pin 住的版本 (实测 requests 2.33.0 顶掉 2.34.2), 还会让 pip 少装传递依赖。
- 子进程从 env() 继承的是摘干净之后的环境。"""
- for d in [x for x in os.environ.pop("PYTHONPATH", "").split(os.pathsep) if x]:
- while d in sys.path: sys.path.remove(d)
- _hermetic()
- '''
- def fix_guanlan() -> None:
- raw = GUANLAN.read_bytes()
- text = raw.decode("utf-8")
- if "_hermetic()" in text:
- print(" [=] guanlan.py 已修过, 跳过")
- return
- backup(GUANLAN)
- text = sub1(text, G_ANCHOR, G_INSERT, "guanlan.py")
- compile(text, str(GUANLAN), "exec")
- GUANLAN.write_bytes(text.encode("utf-8"))
- changed.append("guanlan.py: 启动时摘掉 PYTHONPATH (自身与子进程都跑 .venv 的那套)")
- print(" [OK] guanlan.py 已修")
- def main() -> int:
- print(f"目标: {ROOT}")
- print("\n== D1 install.ps1"); fix_install_ps1()
- print("\n== D2 install.sh"); fix_install_sh()
- print("\n== E guanlan.py"); fix_guanlan()
- print("\n完成:")
- for c in changed or ["(无需改动, 之前已修)"]:
- print(" - " + c)
- return 0
- if __name__ == "__main__":
- sys.exit(main())
|