fix_guanlan_pythonpath.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """第二处修复: 机器全局 PYTHONPATH 污染 .venv。
  4. 实测 (本机 PYTHONPATH="D:\\Program Files\\Python\\Lib\\site-packages;") :
  5. pip 在 venv 里看到系统 site-packages 的包, 判定 "Requirement already satisfied ... outside environment",
  6. 于是没有把传递依赖装进 .venv: venv 里缺 urllib3 / polars / pyyaml / jinja2 / python-dotenv 等,
  7. 并且 sys.path 里 PYTHONPATH 排在 venv site-packages **前面**, requests 被系统 2.33.0 顶掉了 pin 住的 2.34.2。
  8. 后果: 一旦换个没设 PYTHONPATH 的 shell (或别的机器用户), import requests/polars/yaml 直接 ModuleNotFoundError。
  9. 修复 (都不改变功能, 只要求 "跑的就是 .venv 里那一套"):
  10. D. install.ps1 / install.sh : 装依赖前摘掉 PYTHONPATH, 让 pip 把 requirements.txt 真正装进 .venv。
  11. E. guanlan.py : 启动时把 PYTHONPATH 从 sys.path/b环境里摘掉 (check/serve/stop 与子进程都干净)。
  12. 幂等: 重复运行报 "已修过"。
  13. """
  14. from __future__ import annotations
  15. import pathlib
  16. import shutil
  17. import sys
  18. ROOT = pathlib.Path(r"F:\temp\guanlan-rudong-v2_0.2.0")
  19. GUANLAN = ROOT / "guanlan.py"
  20. INSTALL_PS1 = ROOT / "install.ps1"
  21. INSTALL_SH = ROOT / "install.sh"
  22. BAK = ".bak-pyfix"
  23. changed: list[str] = []
  24. def fail(msg: str) -> None:
  25. print(" [X] " + msg)
  26. sys.exit(1)
  27. def backup(p: pathlib.Path) -> None:
  28. b = p.with_name(p.name + BAK)
  29. if not b.exists():
  30. shutil.copy2(p, b)
  31. print(f" 备份 {p.name} -> {b.name}")
  32. def sub1(text: str, old: str, new: str, what: str) -> str:
  33. n = text.count(old)
  34. if n != 1:
  35. fail(f"{what}: 锚点命中 {n} 次 (期望 1)")
  36. return text.replace(old, new)
  37. # ------------------------------------------------------------------ D1. install.ps1
  38. PS1_ANCHOR = '$ErrorActionPreference = "Stop"; $env:PYTHONUTF8 = "1"\n'
  39. PS1_INSERT = PS1_ANCHOR + (
  40. '# 机器上若全局设了 PYTHONPATH (例如指向 D:\\Program Files\\Python\\Lib\\site-packages), pip 会把那里的包\n'
  41. '# 当成 "已满足", 于是不装进 .venv —— venv 里就缺 urllib3 / polars / pyyaml / jinja2 等传递依赖,\n'
  42. '# 换个没设 PYTHONPATH 的 shell 立刻 import 失败; 且 PYTHONPATH 排在 venv 之前会顶掉 pin 住的版本。\n'
  43. '# 先把环境变量摘掉, 让下面 pip 老老实实按 requirements.txt 装进 .venv。\n'
  44. 'Remove-Item Env:PYTHONPATH -ErrorAction SilentlyContinue\n'
  45. )
  46. def fix_install_ps1() -> None:
  47. raw = INSTALL_PS1.read_bytes()
  48. text = raw.decode("utf-8-sig").replace("\r\n", "\n")
  49. if "Remove-Item Env:PYTHONPATH" in text:
  50. print(" [=] install.ps1 已修过, 跳过")
  51. return
  52. backup(INSTALL_PS1)
  53. text = sub1(text, PS1_ANCHOR, PS1_INSERT, "install.ps1")
  54. INSTALL_PS1.write_bytes(text.replace("\r\n", "\n").replace("\n", "\r\n").encode("utf-8-sig"))
  55. changed.append("install.ps1: 装依赖前 Remove-Item Env:PYTHONPATH")
  56. print(" [OK] install.ps1 已修 (保持 UTF-8 BOM + CRLF)")
  57. # ------------------------------------------------------------------ D2. install.sh
  58. SH_ANCHOR = 'set -e\n'
  59. SH_INSERT = SH_ANCHOR + (
  60. '# 同理: 全局 PYTHONPATH 会让 pip 误判依赖已满足而不装进 .venv (见 install.ps1 注释)\n'
  61. 'unset PYTHONPATH\n'
  62. )
  63. def fix_install_sh() -> None:
  64. raw = INSTALL_SH.read_bytes()
  65. if raw.startswith(b"\xef\xbb\xbf"):
  66. fail("install.sh 不该有 BOM")
  67. text = raw.decode("utf-8")
  68. if "unset PYTHONPATH" in text:
  69. print(" [=] install.sh 已修过, 跳过")
  70. return
  71. backup(INSTALL_SH)
  72. text = sub1(text, SH_ANCHOR, SH_INSERT, "install.sh")
  73. INSTALL_SH.write_bytes(text.encode("utf-8")) # 保持 LF / 无 BOM
  74. changed.append("install.sh: 装依赖前 unset PYTHONPATH")
  75. print(" [OK] install.sh 已修 (保持 LF)")
  76. # ------------------------------------------------------------------ E. guanlan.py
  77. G_ANCHOR = 'WIN = os.name == "nt"\n'
  78. G_INSERT = G_ANCHOR + '''
  79. def _hermetic():
  80. """摘掉机器全局的 PYTHONPATH: 本项目依赖都装在 .venv 里, 而 PYTHONPATH 会被插到 sys.path 前面,
  81. 顶掉 venv 里 pin 住的版本 (实测 requests 2.33.0 顶掉 2.34.2), 还会让 pip 少装传递依赖。
  82. 子进程从 env() 继承的是摘干净之后的环境。"""
  83. for d in [x for x in os.environ.pop("PYTHONPATH", "").split(os.pathsep) if x]:
  84. while d in sys.path: sys.path.remove(d)
  85. _hermetic()
  86. '''
  87. def fix_guanlan() -> None:
  88. raw = GUANLAN.read_bytes()
  89. text = raw.decode("utf-8")
  90. if "_hermetic()" in text:
  91. print(" [=] guanlan.py 已修过, 跳过")
  92. return
  93. backup(GUANLAN)
  94. text = sub1(text, G_ANCHOR, G_INSERT, "guanlan.py")
  95. compile(text, str(GUANLAN), "exec")
  96. GUANLAN.write_bytes(text.encode("utf-8"))
  97. changed.append("guanlan.py: 启动时摘掉 PYTHONPATH (自身与子进程都跑 .venv 的那套)")
  98. print(" [OK] guanlan.py 已修")
  99. def main() -> int:
  100. print(f"目标: {ROOT}")
  101. print("\n== D1 install.ps1"); fix_install_ps1()
  102. print("\n== D2 install.sh"); fix_install_sh()
  103. print("\n== E guanlan.py"); fix_guanlan()
  104. print("\n完成:")
  105. for c in changed or ["(无需改动, 之前已修)"]:
  106. print(" - " + c)
  107. return 0
  108. if __name__ == "__main__":
  109. sys.exit(main())