• 发生时间:2022-05-27
  • 问题描述:
    • 在 Windows 系统下的 git bash 中,使用 virtualenv 创建 Python 虚拟环境。
    • 进入虚拟环境,使用 pyinstaller 打包项目代码。
    • 打包完成后,运行生成的二进制文件,报错说找不到模块 A,但虚拟环境中已经安装了模块 A。
  • 问题类别:软件开发
  • 原因分析:
    • 首先怀疑是 pyinstaller 缓存的问题,但删掉所有临时目录后,重新打包,问题依然存在。
    • 后来想到可能打包时是在外部环境而非虚拟环境中查找所安装的模块,而外部环境确实是没安装模块 A 的。
    • 重新运行 pyinstaller 并观察输出日志,发现打包时加载模块等使用的均是外部环境的路径,并且 pyinstaller 也是使用外部环境的 pyinstaller。
    • 经过多次实验(详见下文),最终发现原因:
      • 我创建虚拟环境后,进入虚拟环境,在虚拟环境还没安装 pyinstaller 时,执行过 pyinstaller -v命令。
      • 由于虚拟环境中没有安装 pyinstaller,系统最终找到外部环境的 pyinstaller,输出其版本信息。
      • 虽然后来我在虚拟环境中安装 pyinstaller 了,但由于 Bash 对已执行命令的缓存机制,导致系统依然使用外部环境的 pyinstaller。
      • 使用外部环境的 pyinstaller 进行打包时,只会加载来自外部环境的模块,导致即使我在虚拟环境中安装了模块 A,也不会被打包进去。
  • 解决方案:这个问题有多种解决方案:
    • 方案1:使用 hash -r 清除 bash 对已执行命令的缓存,再重新打包。
    • 方案2:通过 python -m pyinstaller 的方式来调用 pyinstaller 进行打包。 (创建虚拟环境时就会生成对应的 python.exe,所以 python 路径肯定不会找错。)
    • 方案3:重启 git bash 再打包。
  • 实施结果:采取以上方案后,重新打包,可以正常运行打包结果。
  • 经验总结:使用 bash 时要注意它具有缓存命令路径这个特性,如果不需要这个特性可以将其关闭。

探索 bash 缓存命令路径特性

当 bash 缓存命令路径机制没有被误触发时

  1. 确认外部环境的 pyinstaller 版本及路径。
$ pyinstaller -v
5.1

$ type pyinstaller
pyinstaller is hashed (/c/Users/minieye/AppData/Local/Programs/Python/Python38/Scripts/pyinstaller)

# 从输出结果可以看出,pyinstaller 的路径已经被缓存起来。
  1. 创建虚拟环境。
$ virtualenv --python=C:\\Users\\minieye\\AppData\\Local\\Programs\\Python\\Python36\\python.exe venv3.6
created virtual environment CPython3.6.5.final.0-64 in 706ms
  creator CPython3Windows(dest=C:\Users\minieye\src\demo\venv3.6, clear=False, no_vcs_ignore=False, global=False)
  seeder FromAppData(download=False, pip=bundle, setuptools=bundle, wheel=bundle, via=copy, app_data_dir=C:\Users\minieye\AppData\Local\pypa\virtualenv)
    added seed packages: pip==21.3.1, setuptools==59.6.0, wheel==0.37.1
  activators BashActivator,BatchActivator,FishActivator,NushellActivator,PowerShellActivator,PythonActivator
  1. 进入虚拟环境。
$ source venv3.6/Scripts/activate
(venv3.6)
  1. 在虚拟环境中安装 Pyinstaller。
$ pip install pyinstaller==4.10

# 输出结果太长,省略掉。
  1. 查询 pyinstaller 版本及所在路径。
$ pyinstaller -v
4.10
(venv3.6)

$ type pyinstaller
pyinstaller is hashed (/c/Users/minieye/src/demo/venv3.6/Scripts/pyinstaller)
(venv3.6)

# 可以看到此时查到的 pyinstaller 是虚拟环境下安装的版本,符合我们预期。

当 bash 缓存命令路径机制被误触发时

我们再做一个实验,前面3步跟上面的完全一样,唯一的区别是 在虚拟环境下安装 pyinstaller 前先查询一下其版本。

让我们看看会发生什么。

  1. 进入新创建的虚拟环境后,首先查询一下 pyinstaller 版本及路径。
$ pyinstaller -v
5.1
(venv3.6)

$ type pyinstaller
pyinstaller is hashed (/c/Users/minieye/AppData/Local/Programs/Python/Python38/Scripts/pyinstaller)
(venv3.6)

# 由于虚拟环境下没有安装 pyinstaller,此时系统找到的是外部环境的 pyinstaller,并将其路径缓存起来了。
  1. 这时安装一下 pyinstaller。
$ pip install pyinstaller==4.10

# 输出结果太长,省略掉。
  1. 然后重新查询一下 pyinstaller 版本及路径。
$ pyinstaller -v
5.1
(venv3.6)

$ type pyinstaller
pyinstaller is hashed (/c/Users/minieye/AppData/Local/Programs/Python/Python38/Scripts/pyinstaller)
(venv3.6)

# 跟安装前的结果完全一样,说明 bash 的缓存命令路径机制导致虚拟环境中的命令被屏蔽了。
  1. 如果我们把 bash 缓存清除掉,再重新查询,就能正常找到虚拟环境下的 pyinstaller 命令了。
$ hash -r
(venv3.6)

$ pyinstaller -v
4.10
(venv3.6)

$ type pyinstaller
pyinstaller is hashed (/c/Users/minieye/src/demo/venv3.6/Scripts/pyinstaller)
(venv3.6)