注:下文的CSU是我们单板的名称。
1 前言
平时开发时,采用NFS我认为是最方便的方式,但是,如果哪一天真要用python程序直接在CSU上跑,NFS明显是不现实的,因此,必须要研究如何将python挤进CSU里。
2 方案思路
思路1:采用专业打包工具,例如cx_Freeze;
思路2:自己组建一个最小运行环境
2.1 打包思路
这种思路是将python程序变成可执行文件,并将用到的库放在一起,即不用安装python,也可以运行。
其实,我个人是最认同这种思路的,通过打包工具将python程序实际需要的库打包起来,可以做到最小的效果,而且还非常方便。
但是,打包工具面对交叉编译的环境,真不知道怎么弄。
我采用了可以跨平台的cx_Freeze,这个工具不仅支持python2与python3,而且支持linux、windows、Mac OS的系统。功能是比较强大的。我在linux和windows下都可以非常方便的打包python程序。
但是,如何打包arm的python程序,这个我真不会了,查阅了官方和论坛很多资料,都没有找到解决方案。我自己也尝试了很多方法,都没有成功,最纠结的地方在于:
如果在pc的linux上运行cx_Freeze打包,那么,会将pc版本的python相关的库和文件打包进来,如果将python的链接指向arm版本的python,arm版本的python又无法解析cx_Freeze的setup文件,这是个很矛盾的地方。而且,安装cx_Freeze中,需要用到gcc编译一些和平台相关的.so动态库文件,如何将gcc换成arm-none-linux-gnueabi-gcc也是个麻烦事,至少官方文档中没有说明,除非修改其安装源码。
那在CSU上安装cx_Freeze呢?安装倒是可以,选择的也是arm版本的python,但是,最关键的就是,运行中,需要用到gcc进行编译。CSU怎么在自身进行编译呢?这是个问题,如果这个关键问题解决了,估计可以迎刃而解。希望有人可以解决这个问题啊。
那看来没有办法了,只能使用思路2了。
2.2 组建最小环境思路
pc版本的python,安装后有70多M,明显是放不进CSU的,因此,必须(减肥)瘦身。
经过最终尝试,全功能环境只需要6M,最小环境只需要1.2M的空间即可。
3 研制最小环境
python要瘦身,就必须理解python的文件结构。
安装路径下,真正在运行中用到着的只有bin和lib两个文件夹。
为了讲解不那么抽象,这里以python2.7为例进行陈述。
(为什么选择2.7呢,因为,我发现同样是打印1000次字符串,CSU环境下,python2.7比python3.3快了3倍速度。而且很多网络的第三方库都不支持python3.3。)
3.1 排除非必需文件
bin文件夹中,只有python2.7这个文件,是真正用到的执行文件。
lib中,只有libpython2.7.so.1.0和python2.7文件夹是真正用到着的。
其余的在大量的实验中,均发现是可以删除的。
(注意,如果configure中,采用静态库进行编译,那么是没有libpython2.7.so.1.0文件的,当然,这个文件的内容被放进执行程序中而已)
(采用动态库的好处是,其他程序可以使用libpython的库了,那谁会用到这个库呢,举例,大名鼎鼎的vim就会用到这个库。在虚拟机下,ldd /usr/bin/vim,可以看到vim使用的动态库中,就有libpython。如果CSU哪天需要安装vim,那么这个库是必要的)
(再啰嗦一下,ubuntu(debian系列的都是)默认将python库安装在/usr/lib路径下,将site-packages和dist-packages这两个专门存放用户第三方python库的文件夹放在/usr/local/lib路径下。如果你是自己下载源码包进行安装,默认都是安装在/usr/local/lib上的。这时,/usr/lib路径下就会有原有版本的python可以删除了,除了可以节省磁盘空间,还可以减少因为搜索路径问题带来的麻烦,我就碰到过。因此,建议删除原有的/usr/lib里面的python文件夹。但注意的是,不要删除libpython动态库,因为,其他程序,例如vim要用到,而ubuntu搜索默认库的路径是/lib,/usr/lib,因此,建议不要移动这几个python动态库。如果不幸被你删除了,可以在源码包重新三部曲,关键是./configure --enable-shared --prefix=/usr)
3.2 缩小必要文件的体积
3.2.1 删掉调试信息
arm-none-linux-gnueabi-strip python2.7
arm-none-linux-gnueabi-strip libpython2.7.so.1.0
这时已经有大幅度的瘦身效果了,已经比较让人满意了。
如果需要进一步缩小,可以使用下面的upx技术。
3.2.2 采用upx技术。
upx可以将执行程序和动态库进行压缩,我平均压缩效果都大于50%,当然,代价就是加载时慢一点。
(upx大家可以关注一下,以前很多木马就是用了upx技术,通过这个壳,躲过了杀毒软件的查杀)
ubuntu中,apt-get install upx
由于动态库默认是只读权限,因此这里必须增加可写权限:
chmod +w libpython2.7.so.1.0
压缩
upx libpython2.7.so.1.0
这时,动态库已经从1.8M降为只有不到1M了。
(没有必要使用upx --best这种终极压缩,解压代价太大了)
python2.7由于只有8.7k,太小了,upx压缩没有必要。
3.3 库的最小化
其实,真正的难点在于让/lib/python2.7这个庞大的文件夹缩小。
该文件夹包含了平台依赖和平台独立的两种文件。
平台依赖的动态库.so文件放在python2.7/lib-dynload里。
平台独立的文件为其他的.py,.pyc,pyo文件,这些都可以在不同的平台动态解析。
3.3.1 只保留pyc文件
.py是最基本的文件,理论上来说,有这种文件就够了。
.pyc是.py compile后的字节流文件,不但可以加快加载文件的速度,还可以有一定的保密作用(当然,道高一尺魔高一丈,现在可以反编译这些pyc文件)。其实,python在读取.py时,也会自动生成.pyc文件的。
.pyo是.py加了优化参数后的compile后的字节流文件,官方表明优化效果很少,例如只是删除断言之类的。
因此,这里,删除所有的.py和.pyo文件,只保留.pyc就够了。
(由于我的pc的linux和csu的linux有些时间的不对应的问题,csu在加载.pyc文件时,会有bad mtime的告警。因此,我是只保留py文件后,在csu的下进行重编译,然后再删除所有py文件。
重编译的方法如下:
csu中进入python的输入命令界面(开发中,我都是通过NFS让CSU使用python的)
import compileall
compileall.compile_dir('./xxx')
则可以编译该文件夹下的所有py文件为pyc文件。
)
3.3.2 使用zip库
python是支持zip格式的库的。
python环境下输入:
import sys
sys.path
这会得到python搜索库的所有路径,可以发现第一个路径非常有趣,叫/lib/python27.zip。
也就是说,把python2.7库文件夹压缩为python27.zip,python是可以从这个压缩文件中读取的。
3.3.2.1 让python支持zip库
由于python默认给pc使用,官方认为使用压缩库的概率小,因此,默认python不支持zip库的解压的。需要修改如下:
cd到python源码包的arm_build下(该文件夹默认没有,请查看《CSU移植python》一文)。
vim Modules/Setup
输入?zlib (?表示从后往前搜索)
将如下这句取消注释,让其生效:
zlib zlibmodule.c -I$(prefix)/include -L$(exec_prefix)/lib -lz
:wq
(我的ubuntu没有fedora12 DVD版集成那么多的东西,还需要安装zlibc和zlib1g.dev。
apt-get install zlibc 这条是必要的,已经验证
apt-get install zlib1g.dev 这条不确定是否必要)
重新生成与安装arm版本的python。
make python && make -i install
这样python就会集成zlib功能了。
3.3.2.2 制作全功能的zip库
由于CSU目前是网络加载虚拟机的文件夹,因此,在虚拟机侧或CSU侧制作zip库,效果是一样的。考虑pc压缩快一些,这里在pc压缩,csu侧进行测试验证。
cd 到/opt/arm_python/lib/python2.7
建议复制一份完整的库到python2.7.bak文件夹中。
mkdir ../python2.7.bak
cp ./* ../python2.7.bak
采用du –h 查看当前文件夹的大小,发现有70多M,CSU肯定放不进去。
cd ./lib-dynload
这里有蛮多平台依赖的so文件,用上面介绍的arm-none-linux-gnueabi-strip和upx把它们全部缩小。
cd .. 回到python2.7目录。
find -name ‘*.py’ -delete
(比 find -name‘*.py’ -print0 | xargs -0 rm 要方便;注意:find -name‘*.py’ | xargs rm 的用法不推荐,当文件名有空格时,会有错误。加了-0就可以解决这个。但是还是推荐我使用的那句。)
find -name ‘*.pyo’ -delete
rm -r ./config ./test
删除掉包括子目录下的所有.py和.pyo文件,删除不是库功能的config文件夹,删除用不到的test文件夹。
zip –r ../python27.zip .
(注意,要到python2.7里面进行压缩,才不会包含python2.7这个文件夹,包含了,就用不了)
cd -
ll 查看一下全功能库的大小,只有5.2M!CSU是可以放进去的。
(注:我很喜欢用ll,alias ll=’ls -alh’)
3.3.2.3 制作最小包的zip库
所谓的最小包,指的是在shell中执行python成功(可以进入python的命令环境)所需要的最少库。
那如何知道哪些是最基本的库呢?
执行python –v。
可以打印加载python中出现的信息。
通过上面显示的所有加载库名称,就可以知道最基本的库了。
这里采用智能方法提取。
python –v 2>verbose.txt
(经测试,所有信息均是错误输出,所有只要2就可以了)
执行后,由于原来的等待用户输入的那一行也跑到文本中了,因此,无法继续了,新开一个shell窗口,ctrl+d关闭掉这个。
在/opt/arm_python/code里放入如下这个程序:
#!/usr/bin/env python
import sys
import shutil
import os
class CpBasicModules():
def get_python_info(self):
version = sys.version[:3]
if len(sys.argv) == 1:
print('default python version is {}'.format(version))
elif len(sys.argv) == 2:
version = sys.argv[1].strip().strip('python')
if version[0].isdigit() and version[1] == '.' and version[2].isdigit():
print('your python version is {}'.format(version))
else:
print('input python version like 2.7 , 3.3, or else')
exit(1)
else:
print("too many paras")
exit(1)
self.python_zip = 'python{}{}.zip'.format(version[0], version[2])
self.version = version
def get_basic_modules(self):
try:
this_file = open('../bin/verbose.txt', 'r')
except:
print('''
make sure verbose.txt is in your bin folder,
and this file is in code folder,
and code folder is parallel with bin folder
''')
exit(1)
lines = this_file.readlines()
this_file.close()
basic_module = []
for line in lines:
if '.pyc' not in line:continue
parts = line.split(self.python_zip)
pyc_name = parts[-1].lstrip('/').rstrip('\n')
basic_module.append(pyc_name)
self.basic_modules = basic_module
def init_target_folder(self):
target_folder = os.path.join(os.getcwd(), '../lib/python{}.min'.format(self.version))
if os.path.exists(target_folder):
shutil.rmtree(target_folder)
os.mkdir(target_folder)
self.target_folder = target_folder
def cp_files(self):
sh_name = 'get_python_min_lib.sh'
sh_file_path = os.path.join(os.getcwd(), sh_name)
src_folder = os.path.join(os.getcwd(), '../lib/python{}.bak'.format(self.version))
with open(sh_file_path, 'w') as sh_file:
str = 'cd {}'.format(src_folder)
str += ' && '
str += 'cp --parents '+' '.join(self.basic_modules)+' '+self.target_folder
sh_file.write(str)
os.system('chmod +x {}'.format(sh_file_path))
os.system(sh_file_path)
os.system('rm {}'.format(sh_file_path))
def main(self):
self.get_python_info()
self.get_basic_modules()
self.init_target_folder()
self.cp_files()
if __name__ == "__main__":
CpBasicModules().main()
执行它,会在lib下生成一个pythonx.x.min的文件夹,里面的内容就是python所需要加载的模块。然后自己zip打包就行了。体积大约100多K。
(从verbose中提取所需库名,然后从pythonx.x.bak中复制到python.x.x.min中)
(这个程序第一句为#!/usr/bin/env python,其作用是让程序可以直接执行。一般执行py文件,必须python xxxx.py。加了这句,就可以./xxxx.py直接执行了,更接近脚本的使用习惯。
如果你是在windows下编辑,然后复制到linux中用,是无法直接执行的。因为win的换行为/r/n,linux的只是/n,因此,linux会认为执行程序是python/r,当然会出现没有该文件的错误。
)
3.4 保留os.pyc
这在CSU中非常关键。
正常情况下,制作了pythonxx.zip,就可以把文件夹pythonx.x给删除了。在pc的linux上进行验证,也是如此。
但是可能由于交叉编译的原因,交叉编译的python可执行文件不具有os这个模块的功能,在只使用pythonxx.zip的情况下,arm的python是无法启动的。
不知道是CSU的特殊性还是python最新版本的原因,该问题,查阅了中外各种资料,都没有发现。这个问题完全靠硬功夫一点一点摸索出来,发现pythonx.x文件夹里只要有os.pyc,arm的python才能正常启动。
这个地方是要特别关注的。
4 懒人时刻
4.1 次懒人时刻
做了脚本(起初用bash编写,被误删了,伤心啊。后来用python重构了,并增加了人机交互)
#!/usr/bin/env python
#from __future__ import print_function
from os import *
from shutil import *
import sys
#interesting , you guess why i import io.
import io
class Setup():
def __init__(self):
self.manual=False
self.script_dir=getcwd()
self.arm_build = path.join(self.script_dir,'arm_build')
if not path.exists(self.arm_build):mkdir(self.arm_build)
self.arm_parser_dir = path.join(self.arm_build,'Parser')
if not path.exists(self.arm_parser_dir):mkdir(self.arm_parser_dir)
self.arm_install = '/opt/arm_python'
self.lib_shared = False
def find_para(self,sub_part1,sub_part2=None):
for each in sys.argv:
if sub_part1 in each and (sub_part2 is None or sub_part2 in each):
return True
else:
return False
def setup_pc(self):
chdir(self.script_dir)
configure_pc=True
if self.manual:
print("make pgen?(y or n)")
if 'n' in raw_input().lower():configure_pc=False
if configure_pc:
#system('./configure --silent && make Parser/pgen --silent')
copy(path.join(self.script_dir,'Parser','pgen'),self.arm_parser_dir)
system('touch -t 12312359 '+path.join(self.arm_parser_dir,'pgen'))
if self.manual:
print("update your pc python?(y or n)")
if 'y' in raw_input():
system("make --silent && make install >/dev/null")
def setup_arm(self):
chdir(self.arm_build)
#get arm install folder
arm_install = self.arm_install
if self.manual:
print("--prefix=(default is {},hit enter means default)".format(arm_install))
prefix=raw_input()
if prefix != '':arm_install = prefix
if not path.exists(arm_install):mkdir(arm_install)
print("working dir is {} now".format(getcwd()))
with io.open('config.site','w') as config_site:
config_site.write(u'ac_cv_file__dev_ptmx=yes\n')
config_site.write(u'ac_cv_file__dev_ptc=yes\n')
environ['CONFIG_SITE']=path.join(getcwd(),'config.site')
configure_arm = True
if self.manual:
print("configure arm?(y or n)")
if 'n' in raw_input():configure_arm=False
if configure_arm:
print("configure arm now")
#shared lib or static lib
enable_shared = ''
if self.manual:
print("enable shared lib:(y or n)")
if 'y' in raw_input().lower():
enable_shared=' --enable-shared'
self.lib_shared = True
print("you choose static library!")
configure_arm='''../configure --host=arm-none-linux-gnueabi --build=i686-linux-gnu \
--target=arm-none-linux-gnueabi --disable-ipv6 --prefix={} \
--silent {}'''.format(arm_install,enable_shared)
print(configure_arm)
system(configure_arm)
else:
print("skip arm's configure to save time")
print("add zlib function")
with io.open('Modules/Setup','r+') as setup_file:
new_lines = setup_file.read().replace('#zlib zlibmodule.c','zlib zlibmodule.c')
make = True
if self.manual:
print("make arm python and install?(y or n)")
if 'n' in raw_input().lower():make=False
if make:
with io.open('Modules/Setup','w') as setup_file:
setup_file.write(new_lines)
print("make arm and install now")
system("make clean && make python --silent && make -i install")
def make_zip(self):
self.make_zip = True
if self.manual:
print("make python.tgz file?(y or n)")
if 'n' in raw_input().lower():
self.make_zip = False
if not self.make_zip:return
#get version
python_num = 0
for each in listdir(path.join(self.arm_install,'bin' )):
if len(each) == 9 and 'python' == each[:6]:
self.version = each[6:]
python_num += 1
if python_num == 0:
print("we cann't find version , check your bin")
exit(1)
elif python_num == 1:
print("python version is {}".format(self.version))
else:
while True:
print("you have more than 1 python")
print("input the version you want to tar(input like 2.7 or 3.3)")
str = raw_input()
if str[0].isdigit() and str[1] == '.' and str[2].isdigit():
self.version = str
break
else:
print("your input is suck~!")
#copy files
python_bin = 'python'+self.version
if self.lib_shared:python_lib = 'libpython'+self.version+'.so.1.0'
python_lib_sub = python_bin
temp_dir = '/opt/arm_python_temp'
temp_bin_dir = path.join(temp_dir, 'bin')
temp_lib_dir = path.join(temp_dir, 'lib')
temp_lib_python_dir = path.join(temp_lib_dir, 'python{}'.format(self.version))
temp_bin = path.join(temp_bin_dir, python_bin)
if self.lib_shared:temp_lib = path.join(temp_lib_dir, python_lib)
if path.exists(temp_dir):rmtree(temp_dir)
if not path.exists(temp_bin_dir):makedirs(temp_bin_dir)
if not path.exists(temp_lib_dir):makedirs(temp_lib_dir)
copy(path.join(self.arm_install, 'bin', python_bin),temp_bin_dir )
if self.lib_shared:copy(path.join(self.arm_install, 'lib', python_lib), temp_lib_dir)
copytree(path.join(self.arm_install, 'lib', python_lib_sub), temp_lib_python_dir)
#compress files
chdir(path.join(temp_lib_python_dir, 'lib-dynload'))
if self.lib_shared:
system("chmod +w {}".format(temp_lib))
system("arm-none-linux-gnueabi-strip {} {} ./*".format(temp_bin, temp_lib))
system("upx {} ./*".format(temp_lib))
system("chmod -w {}".format(temp_lib))
else:
system("arm-none-linux-gnueabi-strip {} ./*".format(temp_bin))
system("upx {} ./*".format(temp_bin))
#delete the useless and make lib zip
chdir('..')
assert(python_lib_sub in getcwd())
system("find -name '*.py' -delete")
system("find -name '*.pyo' -delete")
#TODO
rmtree('config')
rmtree('test')
python_lib_zip = "python{}{}.zip".format(self.version[0], self.version[2])
system("zip -r ../{} .".format(python_lib_zip))
#leave the necessary file
system("rm -r `ls | grep -Ev os.pyc`")
chdir(temp_dir)
compress_python = (path.join(self.arm_install,'python.tgz'))
print("final target is {}".format(compress_python))
system("rm {}".format(compress_python))
system("tar -czf {} .".format(compress_python))
chdir('..')
#assert('python.tgz' in listdir())
rmtree(temp_dir)
def main(self):
if self.find_para('manual'):
self.manual=True
self.setup_pc()
self.setup_arm()
self.make_zip()
print("Finish")
if __name__ == "__main__":
this=Setup()
this.main()
执行上述代码即可实现自动化。
最后/opt/arm_python/python.tgz就是目标文件了。
(加入参数manual可以人工选择过程,有些过程重复过的,就没有必要重复了,有些过程很花时间的)
4.2 全懒人时刻
这里做好了一个全功能的压缩库
使用方法:
复制该文件到CSU的/usr下,tar xzf python.tgz即可。
(这是采用静态库编译的,因为,静态库体积可以做的更小。如果需要动态库的,人工模式运行arm_python_setup.py,会有提示告诉你选择的)
(如果放在/root/power下,PATH要加入/root/power/bin才行。注意:采用ln的方式到/usr/bin是不行的,因为python会到/usr/lib里找库,问题是,找不到就放弃了,而不是根据LD_LIBRARY_PATH找下去,这是要注意的。)
5 后记
这文档花了好长时间来写,主要是写自动化脚本时,身体疲倦,硬着写,结果不但没有减少故障,反而增加了一堆莫名其妙的故障,写好的脚本居然不小心被删等,走了好多弯路。
编程是个高级智力活动,疲惫状态下开发,只会让开发时间更长,这是真理。
希望大家可以喜欢和应用python。