利用setuptools制作自己的安装包

编写好程序后,如果想要移植到另外的一台机器上,比较方便的方式之一就是将程序代码、配置文件和依赖打包成一个标准的库,然后分发给各个客户机,使用python setup install命令安装即可,也可以上传到pypi将自己的轮子贡献出来给大家使用。

制作一个安装包

其实制作安装包非常简单,在项目文件夹下创建一个setup.py文件,然后填入一下内容:

1
2
3
4
5
6
7
8
9
10
from distutils.core import setup
setup(name='Distutils',
version='1.0',
description='Python Distribution Utilities',
author='Greg Ward',
author_email='gward@python.net',
url='https://www.python.org/sigs/distutils-sig/',
packages=['distutils', 'distutils.command'],
)

这里使用的是distutils,它是python标准的打包工具,包含了标准的库文件。还有一种是 setuptools,支持easy_install安装,相对于distutils来说,更为全面。

这是一个非常简单的安装脚本,setup()里填写的是一个些跟包相关的信息,具体包含的字段信息,可以查阅官方文档。

有了安装脚本,接下来执行打包命令:

1
python setup.py sdist

  1. 该命令会创建一个dist文件夹,里边包含一个后缀为tar.gz的包,这就是我们可以解压,进行安装的包。
  2. 使用者拿到这个包后,解压,到目录下执行:python setup.py install,那么脚本文件就会被拷贝到python类路径下,可以被导入使用(如果安装是egg文件,会把egg文件拷贝到dist-packages目录下)。
  3. 对于windows,可以执行ython setup.py bdist_wininst 但系统必须有rpm命令的支持。可以运行下面的命令查看所有格式的支持:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    root@network:/kong/setup# python setup.py bdist --help-formats
    List of available distribution formats:
    --formats=rpm RPM distribution
    --formats=gztar gzip'ed tar file
    --formats=bztar bzip2'ed tar file
    --formats=ztar compressed tar file
    --formats=tar tar file
    --formats=wininst Windows executable installer
    --formats=zip ZIP file
    --formats=msi Microsoft Installer

setup的一些参数

1. packages

告诉Distutils需要处理那些包(包含init.py的文件夹)

2. package_dir

告诉Distutils哪些目录下的文件被映射到哪个源码包,感觉好像是一个相对路径的定义。一个例子:package_dir = {‘’: ‘lib’},表示以lib为主目录。

3. ext_modules

是一个包含Extension实例的列表,Extension的定义也有一些参数。

4. ext_package

定义extension的相对路径

5. requires

定义依赖哪些模块

6. provides

定义可以为哪些模块提供依赖

7. scripts

指定python源码文件,可以从命令行执行。在安装时指定–install-script

8. package_data

通常包含与包实现相关的一些数据文件或类似于readme的文件。

1
package_data = {'': ['*.txt'], 'mypkg': ['data/*.dat'],}

表示包含所有目录下的txt文件和mypkg/data目录下的所有dat文件.

9. data_files

指定其他的一些文件(如配置文件)

1
2
3
4
5
setup(...,
data_files=[('bitmaps', ['bm/b1.gif', 'bm/b2.gif']),
('config', ['cfg/data.cfg']),
('/etc/init.d', ['init-script'])]
)

规定了哪些文件被安装到哪些目录中。如果目录名是相对路径,则是相对于sys.prefix或sys.exec_prefix的路径。如果没有提供模板,会被添加到MANIFEST文件中。
执行sdist命令时,默认会打包哪些东西呢?

  • 所有由py_modules或packages指定的源码文件
  • 所有由ext_modules或libraries指定的C源码文件
  • 由scripts指定的脚本文件
  • 类似于test/test*.py的文件
  • README.txt或README,setup.py,setup.cfg
  • 所有package_data或data_files指定的文件

还有一种方式是写一个manifest template,名为MANIFEST.in,定义如何生成MANIFEST文件,内容就是需要包含在分发包中的文件。一个MANIFEST.in文件如下:

1
2
3
include *.txt
recursive-include examples *.txt *.py
prune examples/sample?/buil

管理依赖

我们在编写程序的时候,肯定会用到各种各样的库文件,如果不对依赖进行安装打包,那么使用者就要一个一个地对依赖进行安装,显然是非常不方便的。而我们可以在setup.py文件中通过使用install_requires显示说明需要安装的依赖,setup.py在执行安装的时候,会自动在pypi搜索并安装该依赖到客户机中。

对于非pypi中的第三方库,setup提供了另外的一个dependency_来供开发者填入需要安装的库url。

安装过程中的交互

一般情况下,我们的配置文件都是写在data_files参数中,从而跟随安装过程到目标文件夹中。但是有些时候,我们希望可以在安装过程中跟用户进行一些交互,让用户输入一些特定的环境变量。 这个时候,我们就可以在setup.py文件中编写一些input 用来记录用户的输入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def initialize_options(self):
print('欢迎使用润联GPIO按钮监控安装程序\n')
print(self.binaries_directory())
install.initialize_options(self)
# 打开配置文件
input('我们需要提供一些配置信息,准备好了请按任意键开始...\n')
f = open('gpio_config.py', 'w')
PID = input('请给本机指定一个唯一指定标识,不填则取本机MAC地址:')
if not PID:
PID = ':'.join(("%012X" % get_mac())[i:i+2]
for i in range(0, 12, 2))
SWICH = input('指定开关编号(BCM编码):')
if not SWICH:
raise ("错误的开关编号")
HOST = input('请输入odoo的URL:')
PORT = input('请输入Odoo的端口号,默认8069:')
if not PORT:
PORT = 8069
DB = input('请输入数据库名:')
USER = input('请输入用户名:')
PASSWORD = input('请输入密码:')
f.write('SWITCH=%s\n' % SWICH)
f.write('PID="%s"\n' % PID)
f.write('HOST="%s"\n' % HOST)
f.write('DB="%s"\n' % DB)
f.write('USER="%s"\n' % USER)
f.write('PASSWORD="%s"\n' % PASSWORD)
f.write('PORT=%s\n' % PORT)
f.close()

这段程序就是把用户的输入,通过写文件的方式写到配置文件中,再跟随安装脚本移动到目标文件中。

自启动服务

如果希望将安装的脚本做成一个服务,就需要将写好的服务脚本,移动到/lib/systemd/system目录中。这里有个问题,服务脚本中的程序路径都是要求绝对路径,那么我们如何在安装过程中确定程序会被安装到哪个目录下呢?

这里提供一个函数,用来确定目标机器的安装路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def binaries_directory(self):
'''
获取安装路径
'''
if '--user' in sys.argv:
paths = (site.getusersitepackages(),)
else:
py_version = '%s.%s' % (sys.version_info[0], sys.version_info[1])
paths = (s % (py_version) for s in (
sys.prefix + '/lib/python%s/dist-packages/',
sys.prefix + '/lib/python%s/site-packages/',
sys.prefix + '/local/lib/python%s/dist-packages/',
sys.prefix + '/local/lib/python%s/site-packages/',
'/Library/Python/%s/site-packages/',
))
for path in paths:
if os.path.exists(path):
return path
return None

当然只针对*unix系统啦。

知道路径,我们可以跟配置文件类似的动态写入我们的服务脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
cf = open('/lib/systemd/system/rlgpio.service', 'w')
fcontent = [
"[Unit]\n",
"Description = Runlian GPIO Service\n\n",
"[Service]\n",
"Restart=on-failure\n",
"ExecStart={0} {1}\n".format(
sys.executable, pth+"gpio_service.py"),
"RemainAfterExit=yes\n\n",
"[Install]\n",
"WantedBy=multi-user.target"
]
cf.writelines(fcontent)

安装完成后的动作

如果需要在安装完成后执行一些动作,就需要在run方法里写一些脚本,比如,我这里就是在安装过程中,对安装的服务执行了enable和restart操作:

1
2
3
4
5
6
7
8
9
10
def run(self):
install.run(self)
print('启动服务中...')
call(["systemctl", "enable", "rlgpio.service"])
call(["systemctl", "daemon-reload"])
r = call(["systemctl", "restart", "rlgpio.service"])
if r == 0:
print('服务启动成功')
else:
print("服务启动失败")

这样就是一个安装包的制作过程。