一次曲折的Python绿色化经历

1. 背景

这段时间和老师在做一个模拟器相关的项目,我这边是做一个本地运行算法再通过 TCP 发信号控制模拟器的东西。前两天算是收尾了。于是老师让我把我做的Python部分的程序打包一下。

打包就打包呗,我兴冲冲地打算用 pyinstaller 做成可执行软件就行了,不过老师拦住我,对我提出了以下的要求:

  1. 不许打包成 *.exe,要保持源代码,方便以后的人阅读修改
  2. 把打包好的东西放到我们的 SVN 服务器上,这样以后别人随时可以 down 下来
  3. 要求 down 下来的程序可以直接给一个不懂行的人随意运行,且不许在宿主机上装任何环境

em.....为啥要求这么多?

不过还好,我仔细想了想,感觉貌似还好啊,只要把 Python 的环境整个放进去,然后写个 bat 脚本运行不久行了嘛!

就像这样
就像这样

这种邪恶的手段,江湖人称绿化,我今天就要把 Python 给绿了!

2. 操作猛如虎!!然后....就跪了

说干就干,我首先建了一份 python 的虚拟环境,把程序要用到的库进行最小化安装,包括:

  • PyQT5
  • numpy
  • matplotlib

为了保证环境相同,所以需要先查看一下各个库的版本,这个简单,运行:

1
pip freeze > requirements.txt

查看一下相应的版本:

cycler==0.10.0
kiwisolver==1.1.0
matplotlib==3.1.1
numpy==1.17.2
pyparsing==2.4.2
PyQt5==5.9.2
python-dateutil==2.8.0
sip==4.19.8
six==1.12.0

根据版本安装就行了。

然后把源代码和 python 环境的整个目录移到同一个目录下。

这里插一句话,我现在特别后悔用了 PyQt,因为我做的 GUI 界面极其简单,但整个 Python36 环境非常大(300多M),虽然 numpy 占了最主要的空间,但是 PyQt5 也贡献了不少,所以我得到了一个教训:以后做简单的 GUI 就直接用 tkinter 吧,PyQt 实在太大了

言归正传,然后我再编写 Run_WithoutEnvBuilt.bat,表示使用绿化 py 来运行程序,脚本内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@echo off
:: %~dp0 是一个变量,表示当前的目录
set DIR_PATH=%~dp0
:: 计算 Python36 的目录的位置
set PYTHON_REL_PATH=Python36
set PYTHON_PATH=%DIR_PATH%%PYTHON_REL_PATH%

:: 设置环境变量,把其他变量全部清了,然后只保留我们绿化后的 python 目录
set PATH=%PYTHON_PATH%;%PYTHON_PATH%\Scripts;%PYTHON_PATH%\DLLS;%PYTHON_PATH%\libs;

cd /d %~dp0
cd SrcCode
%PYTHON_PATH%\python.exe main.py
cd ..
pause

注意一点,在命令行下运行 set PATH=xxx 只会在本次运行环境内临时更改环境变量,所以完全不用担心会不会给你带来麻烦。

在本机上点击运行一下,成功运行!!

开开心心地在别的机器下试着运行,然后:

from numpy.core._multiarray_umath import (

ImportError: DLL load failed: 找不到指定模块

3. 震惊!99%的人都不知道 pip 还有这个坑!

此时,天真的我还没有意识到事情的严重性,直接把报错的文字放到网上一搜,一眼看过去,大家的建议格外热情:

重装一遍 numpy

重装就重装呗,我也没过脑子,反正刚才运行脚本的时候就设过一遍,这回刚好直接用,于是我:

1
pip install numpy

结果让我意想不到的事情发生了:

3_PIP_Failed
3_PIP_Failed

我擦,这是怎么回事,为啥我的 pip 没法用了。我在这个坑上我停了很久,后来才通过一篇博客了解到是怎么回事。

在解释这个问题前,首先请大家思考一个问题,当我们使用 pip 的时候,它是如何定位到对应的 python 解释器的路径的呢?也许大家会不假思索地回答:当然是通过环境变量喽!

没错,一开始我也这么想,但当我用 winhex 直接去查看 pip 二进制内容时,却惊讶地发现:

没错,pip 居然直接把 python.exe 的绝对路径写在了二进制文件里面了!其实仔细一想,他这样的做法也是有他的道理的,毕竟他需要考虑到同一台机器上安装了不同个 Python 的情况。但是对于我们要做绿化的 python 就很要命了。

乍办呢?很简单,我们直接手动把它的二进制文件修改一下,只保留一个 Python.exe,然后让他通过环境变量来寻找就行了。

这是我参考的博文链接

然后在机器上重装 numpy ,OK 运行成功。再换一台机器试一试:

em... 这就很尴尬了。

4. 神器 Dependency Walker

我重新思考了一下,决定认真看一看问题到底出在哪里,我仔细看了一下出错的文件和目录,但是。。。呃,貌似没啥问题?

这一出错的行需要用到的 _multiarray_umath 是一个 pyd 文件,就在同一级目录下好好地躺着呢。而 pyd 文件是 python 版本的 DLL 文件,numpy 底层又是靠 C 实现的,莫非是 pyd 找不到依赖的 DLL 了?

在博友的推荐下,我下载了一个 Dependency Walker 来分析一下,用法很简单,选中一个 .exe 或者 .dll 文件即可。

然后我就看到结果了:

说实话当我第一次看到结果时心都快凉了,居然会缺失这么多,这还搞鬼啊!不过冷静下来,我决定先看看一个正常的库是什么样的。我再次用 Dependency Walker,这一次我打开了可以正常运行的环境里的 pyd 文件。

这下我也就放心了,看样子并非所有显示缺失的库都是真正缺失的,那剩下的就是找不同了。很快我把目光转移到最上面的 LIBOPENBLAS 上面。这是什么?使用 Everything 来查找一下。

转到无法正常执行的那个目录下,仔细观察,发现果然没有 .libs 这个文件夹,也因此缺失了 openblas.dll。把这个文件夹整体拷贝过来,OK,可以运行了!

搞定!提交 SVN。

5. 知道真相的我眼泪掉了下来

然而问题又出现了。当我换了一台虚拟机,再次运行的时候,发现又出现了 DLL 报错。这次我就摸不着头脑了,不是已经把 DLL 加入进来了吗?

结果当我进入 numpy/core 目录下时,惊讶地发现居然 openblas 又没有了。我仔细观察了一下,突然吐血地发现了问题所在,原来 SVN 根本就没把 .libs 加入监控,结果每次提交的时候这个目录都没有提交上去!!

6.最后的问题:PYQT

在把 .libs 加入监视后,再次运行,这次出现了新的报错:

This application failed to start because it could not find or load the Qt platform plugin "windows"in "".

这次的报错很容易就在网上找到了解决方案,需要把 Qt 下的 platform 路径给出特定的环境变量命名:

1
2
3
set PATH=%PYTHON_PATH%;%PYTHON_PATH%\Scripts;%PYTHON_PATH%\DLLS;%PYTHON_PATH%\libs;
:: 这里,加入新的变量
set QT_PLUGIN_PATH=%PYTHON_PATH%\Lib\site-packages\PyQt5\Qt\plugins;

问题解决。

总结

前面的讲述基本是按照时间顺序来的,现在总结一下,我到底遇到了什么问题吧。

简单来讲,其实这本来就不是个问题:我配好了环境,copy 到同一个目录,编写好了脚本,设好了环境变量,按理来讲是可以运行的。但偏偏由于我对 SVN 的疏忽,导致一些关键的 dll 目录没有被加入进来,所以在另一台机器上 down 下来时就会提示缺少了相关的 dll。

当然,除此之外还有一个问题就是 PyQT5 的环境变量没有设置好,不过这是小坑,网上一搜就搜出来了。

总而言之,这次的麻烦完完全全就是我自己惹出来的,由于一个疏忽,原本 2 个小时的工作量硬生生被延长了 10 倍。解决问题的方法很明确,就是把对应的 dll 加入 SVN track 就行了,#3 和 #4 中我做的工作完完全全是在绕弯路。。。