如何在arm单板中压缩python

注:下文的CSU是我们单板的名称。

前言

平时开发时,采用NFS我认为是最方便的方式,但是,如果哪一天真要用python程序直接在CSU上跑,NFS明显是不现实的,因此,必须要研究如何将python挤进CSU里。

方案思路

思路1:采用专业打包工具,例如cx_Freeze

思路2:自己组建一个最小运行环境

2.1 打包思路

这种思路是将python程序变成可执行文件,并将用到的库放在一起,即不用安装python,也可以运行。

其实,我个人是最认同这种思路的,通过打包工具将python程序实际需要的库打包起来,可以做到最小的效果,而且还非常方便。

但是,打包工具面对交叉编译的环境,真不知道怎么弄。

我采用了可以跨平台的cx_Freeze,这个工具不仅支持python2python3,而且支持linuxwindowsMac OS的系统。功能是比较强大的。我在linuxwindows下都可以非常方便的打包python程序。

但是,如何打包armpython程序,这个我真不会了,查阅了官方和论坛很多资料,都没有找到解决方案。我自己也尝试了很多方法,都没有成功,最纠结的地方在于:

如果在pclinux上运行cx_Freeze打包,那么,会将pc版本的python相关的库和文件打包进来,如果将python的链接指向arm版本的pythonarm版本的python又无法解析cx_Freezesetup文件,这是个很矛盾的地方。而且,安装cx_Freeze中,需要用到gcc编译一些和平台相关的.so动态库文件,如何将gcc换成arm-none-linux-gnueabi-gcc也是个麻烦事,至少官方文档中没有说明,除非修改其安装源码。

那在CSU上安装cx_Freeze呢?安装倒是可以,选择的也是arm版本的python,但是,最关键的就是,运行中,需要用到gcc进行编译。CSU怎么在自身进行编译呢?这是个问题,如果这个关键问题解决了,估计可以迎刃而解。希望有人可以解决这个问题啊。

那看来没有办法了,只能使用思路2了。

2.2 组建最小环境思路

pc版本的python,安装后有70M,明显是放不进CSU的,因此,必须(减肥)瘦身。

经过最终尝试,全功能环境只需要6M,最小环境只需要1.2M的空间即可。

研制最小环境

python要瘦身,就必须理解python的文件结构。

安装路径下,真正在运行中用到着的只有binlib两个文件夹。

为了讲解不那么抽象,这里以python2.7为例进行陈述。

(为什么选择2.7呢,因为,我发现同样是打印1000次字符串,CSU环境下,python2.7python3.3快了3倍速度。而且很多网络的第三方库都不支持python3.3。)

3.1 排除非必需文件

bin文件夹中,只有python2.7这个文件,是真正用到的执行文件。

lib中,只有libpython2.7.so.1.0python2.7文件夹是真正用到着的。

其余的在大量的实验中,均发现是可以删除的。

(注意,如果configure中,采用静态库进行编译,那么是没有libpython2.7.so.1.0文件的,当然,这个文件的内容被放进执行程序中而已)

(采用动态库的好处是,其他程序可以使用libpython的库了,那谁会用到这个库呢,举例,大名鼎鼎的vim就会用到这个库。在虚拟机下,ldd /usr/bin/vim,可以看到vim使用的动态库中,就有libpython。如果CSU哪天需要安装vim,那么这个库是必要的)

(再啰嗦一下,ubuntudebian系列的都是)默认将python库安装在/usr/lib路径下,将site-packagesdist-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就够了。

(由于我的pclinuxcsulinux有些时间的不对应的问题,csu在加载.pyc文件时,会有bad mtime的告警。因此,我是只保留py文件后,在csu的下进行重编译,然后再删除所有py文件。

重编译的方法如下:

csu中进入python的输入命令界面(开发中,我都是通过NFSCSU使用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.zippython是可以从这个压缩文件中读取的。

3.3.2.1 python支持zip

由于python默认给pc使用,官方认为使用压缩库的概率小,因此,默认python不支持zip库的解压的。需要修改如下:

cdpython源码包的arm_build下(该文件夹默认没有,请查看《CSU移植python》一文)。

vim Modules/Setup

输入?zlib     ?表示从后往前搜索)

将如下这句取消注释,让其生效:

zlib zlibmodule.c -I$(prefix)/include -L$(exec_prefix)/lib -lz

:wq

(我的ubuntu没有fedora12 DVD版集成那么多的东西,还需要安装zlibczlib1g.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 查看当前文件夹的大小,发现有70MCSU肯定放不进去。

cd ./lib-dynload

这里有蛮多平台依赖的so文件,用上面介绍的arm-none-linux-gnueabi-stripupx把它们全部缩小。

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是可以放进去的。

(注:我很喜欢用llalias 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打包就行了。体积大约100K

(从verbose中提取所需库名,然后从pythonx.x.bak中复制到python.x.x.min中)

(这个程序第一句为#!/usr/bin/env python,其作用是让程序可以直接执行。一般执行py文件,必须python xxxx.py。加了这句,就可以./xxxx.py直接执行了,更接近脚本的使用习惯。

如果你是在windows下编辑,然后复制到linux中用,是无法直接执行的。因为win的换行为/r/nlinux的只是/n,因此,linux会认为执行程序是python/r,当然会出现没有该文件的错误。

)

3.4 保留os.pyc

这在CSU中非常关键。

正常情况下,制作了pythonxx.zip,就可以把文件夹pythonx.x给删除了。在pclinux上进行验证,也是如此。

但是可能由于交叉编译的原因,交叉编译的python可执行文件不具有os这个模块的功能,在只使用pythonxx.zip的情况下,armpython是无法启动的。

不知道是CSU的特殊性还是python最新版本的原因,该问题,查阅了中外各种资料,都没有发现。这个问题完全靠硬功夫一点一点摸索出来,发现pythonx.x文件夹里只要有os.pycarmpython才能正常启动。

这个地方是要特别关注的。

懒人时刻

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 全懒人时刻

这里做好了一个全功能的压缩库

python压缩库

使用方法:

复制该文件到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找下去,这是要注意的。)

后记

这文档花了好长时间来写,主要是写自动化脚本时,身体疲倦,硬着写,结果不但没有减少故障,反而增加了一堆莫名其妙的故障,写好的脚本居然不小心被删等,走了好多弯路。

编程是个高级智力活动,疲惫状态下开发,只会让开发时间更长,这是真理。

希望大家可以喜欢和应用python

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值