Ser*_* jr 21 python makefile python-install python-packaging
序言:
Python setuptools用于包分发.我有一个Python包(让我们称之为my_package),它有几个extra_require包.一切工作只是找到(安装和构建包,以及额外的,如果被要求),因为所有extra_require都是python包本身和pip正确解决了一切.一个简单的pip install my_package工作就像一个魅力.
设置:
现在,对于其中一个附加内容(让我们称之为extra1),我需要调用非python库的二进制文件X.
模块X本身(源代码)已添加到my_package代码库中,并包含在发行版中my_package.遗憾的是,要使用,X需要首先在目标机器上编译成二进制文件(C++实现;我假设这样的编译将在my_package安装的构建阶段进行).有一个Makefile在X不同的平台的编译优化库,使所有需要的,是运行make在各自的目录X库的my_package构建过程中运行时.
问题1:如何make使用setuptools/distutils在包的构建过程中运行终端命令(即在我的情况下)?
问题2:如何确保只有extra1在安装过程中指定了相应的终端命令才能执行?
例:
pip install my_package,则不会发生这种额外的库编译X.pip install my_package [extra1],则X需要编译模块,因此将在目标计算机上创建并提供相应的二进制文件.两年前我对此发表评论很久以后,这个问题又回来困扰着我!我自己最近遇到了几乎同样的问题,我发现文档非常稀缺,因为我认为你们大多数人都必须经历过.所以我试着研究一下setuptools和distutils的一些源代码,看看我是否能找到一个或多或少的标准方法来解决你提出的问题.
你提出的第一个问题
问题1:如何
make使用setuptools/distutils在包的构建过程中运行终端命令(即在我的情况下)?
有许多方法,所有这些方法都涉及设置cmdclass呼叫时setup.该参数cmdclass的setup必须是从继承的命令名,将取决于构建执行或安装分发的需要,和类之间的映射distutils.cmd.Command基类(作为侧面说明,setuptools.command.Command类衍生自distutils" Command类,以便可以从直接导出setuptools实现).
将cmdclass允许您定义的任何命令的名字,像什么ayoon打电话时也然后执行它专门python setup.py --install-option="customcommand"在命令行.这个问题是,它不是在尝试通过pip或通过调用安装包时将执行的标准命令python setup.py install.解决这个问题的标准方法是检查setup在正常安装中尝试执行哪些命令,然后重载该特定命令cmdclass.
从查看setuptools.setup和distutils.setup,setup将运行它在命令行中找到的命令,这让我们假设只是一个简单的install.在这种情况下setuptools.setup,这将触发一系列测试,这些测试将查看是否要求对distutils.install命令类进行简单调用,如果没有这样,它将尝试运行bdist_egg.反过来,这个命令做了很多事情,但重要的决定是否调用build_clib,build_py和/或build_ext命令.在distutils.install简单的运行build,如果必要的,这也运行build_clib,build_py和/或build_ext.这意味着,无论你是否使用setuptools或者distutils,如果有必要从源代码,命令建立build_clib,build_py和/或build_ext会拼命地跑,所以这些都是我们将要与超载的人cmdclass的setup,问题就变成它的他们三个.
build_py 用于"构建"纯python包,所以我们可以安全地忽略它.build_ext用于构建声明的Extension模块,这些模块通过函数ext_modules调用的参数传递setup.如果我们希望重载此类,则构建每个扩展的主要方法是build_extension(或此处为distutils)build_clib用于构建声明的库,这些库通过函数libraries调用的参数传递setup.在这种情况下,我们应该使用派生类重载的主要方法是build_libraries方法(此处为distutils).我将共享一个示例包,通过使用setuptools build_ext命令通过Makefile构建一个玩具c静态库.该方法可以适用于使用该build_clib命令,但您必须签出的源代码build_clib.build_libraries.
setup.py
import os, subprocess
import setuptools
from setuptools.command.build_ext import build_ext
from distutils.errors import DistutilsSetupError
from distutils import log as distutils_logger
extension1 = setuptools.extension.Extension('test_pack_opt.test_ext',
sources = ['test_pack_opt/src/test.c'],
libraries = [':libtestlib.a'],
library_dirs = ['test_pack_opt/lib/'],
)
class specialized_build_ext(build_ext, object):
"""
Specialized builder for testlib library
"""
special_extension = extension1.name
def build_extension(self, ext):
if ext.name!=self.special_extension:
# Handle unspecial extensions with the parent class' method
super(specialized_build_ext, self).build_extension(ext)
else:
# Handle special extension
sources = ext.sources
if sources is None or not isinstance(sources, (list, tuple)):
raise DistutilsSetupError(
"in 'ext_modules' option (extension '%s'), "
"'sources' must be present and must be "
"a list of source filenames" % ext.name)
sources = list(sources)
if len(sources)>1:
sources_path = os.path.commonpath(sources)
else:
sources_path = os.path.dirname(sources[0])
sources_path = os.path.realpath(sources_path)
if not sources_path.endswith(os.path.sep):
sources_path+= os.path.sep
if not os.path.exists(sources_path) or not os.path.isdir(sources_path):
raise DistutilsSetupError(
"in 'extensions' option (extension '%s'), "
"the supplied 'sources' base dir "
"must exist" % ext.name)
output_dir = os.path.realpath(os.path.join(sources_path,'..','lib'))
if not os.path.exists(output_dir):
os.makedirs(output_dir)
output_lib = 'libtestlib.a'
distutils_logger.info('Will execute the following command in with subprocess.Popen: \n{0}'.format(
'make static && mv {0} {1}'.format(output_lib, os.path.join(output_dir, output_lib))))
make_process = subprocess.Popen('make static && mv {0} {1}'.format(output_lib, os.path.join(output_dir, output_lib)),
cwd=sources_path,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
shell=True)
stdout, stderr = make_process.communicate()
distutils_logger.debug(stdout)
if stderr:
raise DistutilsSetupError('An ERROR occured while running the '
'Makefile for the {0} library. '
'Error status: {1}'.format(output_lib, stderr))
# After making the library build the c library's python interface with the parent build_extension method
super(specialized_build_ext, self).build_extension(ext)
setuptools.setup(name = 'tester',
version = '1.0',
ext_modules = [extension1],
packages = ['test_pack', 'test_pack_opt'],
cmdclass = {'build_ext': specialized_build_ext},
)
Run Code Online (Sandbox Code Playgroud)
test_pack/__ init__.py
from __future__ import absolute_import, print_function
def py_test_fun():
print('Hello from python test_fun')
try:
from test_pack_opt.test_ext import test_fun as c_test_fun
test_fun = c_test_fun
except ImportError:
test_fun = py_test_fun
Run Code Online (Sandbox Code Playgroud)
test_pack_opt/__ init__.py
from __future__ import absolute_import, print_function
import test_pack_opt.test_ext
Run Code Online (Sandbox Code Playgroud)
test_pack_opt/SRC /生成文件
LIBS = testlib.so testlib.a
SRCS = testlib.c
OBJS = testlib.o
CFLAGS = -O3 -fPIC
CC = gcc
LD = gcc
LDFLAGS =
all: shared static
shared: libtestlib.so
static: libtestlib.a
libtestlib.so: $(OBJS)
$(LD) -pthread -shared $(OBJS) $(LDFLAGS) -o $@
libtestlib.a: $(OBJS)
ar crs $@ $(OBJS) $(LDFLAGS)
clean: cleantemp
rm -f $(LIBS)
cleantemp:
rm -f $(OBJS) *.mod
.SUFFIXES: $(SUFFIXES) .c
%.o:%.c
$(CC) $(CFLAGS) -c $<
Run Code Online (Sandbox Code Playgroud)
test_pack_opt/SRC/test.c的
#include <Python.h>
#include "testlib.h"
static PyObject*
test_ext_mod_test_fun(PyObject* self, PyObject* args, PyObject* keywds){
testlib_fun();
return Py_None;
}
static PyMethodDef TestExtMethods[] = {
{"test_fun", (PyCFunction) test_ext_mod_test_fun, METH_VARARGS | METH_KEYWORDS, "Calls function in shared library"},
{NULL, NULL, 0, NULL}
};
#if PY_VERSION_HEX >= 0x03000000
static struct PyModuleDef moduledef = {
PyModuleDef_HEAD_INIT,
"test_ext",
NULL,
-1,
TestExtMethods,
NULL,
NULL,
NULL,
NULL
};
PyMODINIT_FUNC
PyInit_test_ext(void)
{
PyObject *m = PyModule_Create(&moduledef);
if (!m) {
return NULL;
}
return m;
}
#else
PyMODINIT_FUNC
inittest_ext(void)
{
PyObject *m = Py_InitModule("test_ext", TestExtMethods);
if (m == NULL)
{
return;
}
}
#endif
Run Code Online (Sandbox Code Playgroud)
test_pack_opt/SRC/testlib.c
#include "testlib.h"
void testlib_fun(void){
printf("Hello from testlib_fun!\n");
}
Run Code Online (Sandbox Code Playgroud)
test_pack_opt/SRC/testlib.h
#ifndef TESTLIB_H
#define TESTLIB_H
#include <stdio.h>
void testlib_fun(void);
#endif
Run Code Online (Sandbox Code Playgroud)
在这个例子中,我想用自定义Makefile构建的c库只有一个打印"Hello from testlib_fun!\n"到stdout的函数.该test.c脚本是python和这个库的单个函数之间的简单接口.我的想法是,我告诉setup我要构建test_pack_opt.test_ext一个名为ac的扩展名,它只有一个源文件:test.c接口脚本,我还告诉扩展名它必须链接到静态库libtestlib.a.主要的是我build_ext使用了cmdclass 重载specialized_build_ext(build_ext, object).object只有当您希望能够调用super分派给父类方法时,才需要继承.该build_extension方法接受一个Extension实例作为其第二个参数,为了工作很好的与其它Extension需要的默认行为情况下build_extension,我检查,如果这个扩展有一个特殊的名字,如果没有我调用super的build_extension方法.
对于特殊库,我只需用Make调用Makefile subprocess.Popen('make static ...').传递给shell的其余命令只是将静态库移动到某个默认位置,在该位置应该找到库,以便能够将其链接到已编译扩展的其余部分(这也是使用super'小号build_extension法).
正如您可以想象的那样,您可以通过多种方式以不同的方式组织此代码,将它们全部列出是没有意义的.我希望这个例子用于说明如何调用Makefile,以及在标准安装中应该重载哪些cmdclass和Command派生类make.
现在,问题2.
问题2:如何确保只有在安装过程中指定了相应的extra1时才执行这样的终端命令?
使用弃用的features参数可以实现这一点setuptools.setup.标准方法是尝试根据满足的要求安装软件包.install_requires列出强制要求,extras_requires列出可选要求.例如,从setuptools文档中
setup(
name="Project-A",
...
extras_require={
'PDF': ["ReportLab>=1.2", "RXP"],
'reST': ["docutils>=0.3"],
}
)
Run Code Online (Sandbox Code Playgroud)
您可以通过调用强制安装可选的必需软件包pip install Project-A[PDF],但如果出于某种原因,'PDF'事先满足了对指定额外软件的要求,则pip install Project-A最终会使用相同的"Project-A"功能.这意味着"Project-A"的安装方式不是针对命令行中指定的每个额外项目自定义的,"Project-A"将始终尝试以相同的方式安装,并且由于不可用而最终可能会减少功能可选要求.
根据我的理解,这意味着只有在指定[extra1]时才能编译和安装模块X,您应该将模块X作为单独的包发送并依赖于它extras_require.让我们假设模块X将被运送my_package_opt,你的设置my_package应该是这样的
setup(
name="my_package",
...
extras_require={
'extra1': ["my_package_opt"],
}
)
Run Code Online (Sandbox Code Playgroud)
好吧,对不起,我的回答结束了这么长时间,但我希望它有所帮助.不要犹豫,指出任何概念或命名错误,因为我大多试图从setuptools源代码中推断出这一点.
不幸的是,有关 setup.py 和 pip 之间交互的文档非常缺乏,但您应该能够执行以下操作:
import subprocess
from setuptools import Command
from setuptools import setup
class CustomInstall(Command):
user_options = []
def initialize_options(self):
pass
def finalize_options(self):
pass
def run(self):
subprocess.call(
['touch',
'/home/{{YOUR_USERNAME}}/'
'and_thats_why_you_should_never_run_pip_as_sudo']
)
setup(
name='hack',
version='0.1',
cmdclass={'customcommand': CustomInstall}
)
Run Code Online (Sandbox Code Playgroud)
这使您可以使用命令运行任意代码,并且还支持各种自定义选项解析(此处未演示)。
将其放入setup.py文件中并尝试以下操作:
pip install --install-option="customcommand" .
请注意,此命令是在主安装序列之后执行的,因此根据您要执行的操作,它可能不起作用。查看详细的 pip install 输出:
(.venv) ayoon:tmp$ pip install -vvv --install-option="customcommand" .
/home/ayoon/tmp/.venv/lib/python3.6/site-packages/pip/commands/install.py:194: UserWarning: Disabling all use of wheels due to the use of --build-options / -
-global-options / --install-options.
cmdoptions.check_install_build_global(options)
Processing /home/ayoon/tmp
Running setup.py (path:/tmp/pip-j57ovc7i-build/setup.py) egg_info for package from file:///home/ayoon/tmp
Running command python setup.py egg_info
running egg_info
creating pip-egg-info/hack.egg-info
writing pip-egg-info/hack.egg-info/PKG-INFO
writing dependency_links to pip-egg-info/hack.egg-info/dependency_links.txt
writing top-level names to pip-egg-info/hack.egg-info/top_level.txt
writing manifest file 'pip-egg-info/hack.egg-info/SOURCES.txt'
reading manifest file 'pip-egg-info/hack.egg-info/SOURCES.txt'
writing manifest file 'pip-egg-info/hack.egg-info/SOURCES.txt'
Source in /tmp/pip-j57ovc7i-build has version 0.1, which satisfies requirement hack==0.1 from file:///home/ayoon/tmp
Could not parse version from link: file:///home/ayoon/tmp
Installing collected packages: hack
Running setup.py install for hack ... Running command /home/ayoon/tmp/.venv/bin/python3.6 -u -c "import setuptools, tokenize;__file__='/tmp/pip-j57ovc7
i-build/setup.py';f=getattr(tokenize, 'open', open)(__file__);code=f.read().replace('\r\n', '\n');f.close();exec(compile(code, __file__, 'exec'))" install --
record /tmp/pip-_8hbltc6-record/install-record.txt --single-version-externally-managed --compile --install-headers /home/ayoon/tmp/.venv/include/site/python3
.6/hack customcommand
running install
running build
running install_egg_info
running egg_info
writing hack.egg-info/PKG-INFO
writing dependency_links to hack.egg-info/dependency_links.txt
writing top-level names to hack.egg-info/top_level.txt
reading manifest file 'hack.egg-info/SOURCES.txt'
writing manifest file 'hack.egg-info/SOURCES.txt'
Copying hack.egg-info to /home/ayoon/tmp/.venv/lib/python3.6/site-packages/hack-0.1-py3.6.egg-info
running install_scripts
writing list of installed files to '/tmp/pip-_8hbltc6-record/install-record.txt'
running customcommand
done
Removing source in /tmp/pip-j57ovc7i-build
Successfully installed hack-0.1
Run Code Online (Sandbox Code Playgroud)