这个问题在我两年前评论它很久之后又回来困扰着我!我自己最近也遇到了几乎同样的问题,而且我发现文档非常稀缺,我想你们中的大多数人一定都经历过。因此,我尝试研究一些setuptools和distutils的源代码,看看是否可以找到一种或多或少的标准方法来解决您提出的两个问题。
你问的第一个问题
问题 #1:如何make
在包的构建过程中使用 setuptools/distutils 运行终端命令(即,在我的情况下)?
有很多方法,所有方法都涉及cmdclass
在调用时设置 a 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
and开始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
将被运行,所以这些是我们想要用 of 重载的cmdclass
,setup
问题变成了三个中的哪一个。
build_py
用于“构建”纯 python 包,所以我们可以放心地忽略它。
build_ext
用于构建通过ext_modules
调用setup
函数的参数传递的已声明扩展模块。如果我们希望重载这个类,构建每个扩展的主要方法是build_extension
(或这里为 distutils)
build_clib
用于构建通过libraries
调用setup
函数的参数传递的声明库。在这种情况下,我们应该用派生类重载的主要方法是build_libraries
方法(此处为distutils
)。
setuptools
build_ext
我将分享一个示例包,它使用命令通过 Makefile 构建玩具 c 静态库。该方法可以适应使用该build_clib
命令,但您必须签出build_clib.build_libraries
.
安装程序.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},
)
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
test_pack_opt/__init__.py
from __future__ import absolute_import, print_function
import test_pack_opt.test_ext
test_pack_opt/src/Makefile
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 $<
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
test_pack_opt/src/testlib.c
#include "testlib.h"
void testlib_fun(void){
printf("Hello from testlib_fun!\n");
}
test_pack_opt/src/testlib.h
#ifndef TESTLIB_H
#define TESTLIB_H
#include <stdio.h>
void testlib_fun(void);
#endif
在这个例子中,我想使用自定义 Makefile 构建的 c 库只有一个打印"Hello from testlib_fun!\n"
到标准输出的函数。该test.c
脚本是 python 和这个库的单个函数之间的一个简单接口。我的想法是我告诉setup
我要构建名为 的 ac 扩展test_pack_opt.test_ext
,它只有一个源文件:test.c
接口脚本,我还告诉扩展它必须链接到静态库libtestlib.a
。最主要的是我build_ext
使用specialized_build_ext(build_ext, object)
. object
仅当您希望能够调用super
以调度到父类方法时,才需要继承 from 。该build_extension
方法将Extension
实例作为其第二个参数,以便与其他Extension
需要默认行为的实例build_extension
,我检查这个扩展是否具有特殊的名称,如果没有,我调用super
'build_extension
方法。
对于特殊库,我只用subprocess.Popen('make static ...')
. 传递给 shell 的命令的其余部分只是将静态库移动到某个默认位置,在该位置应该找到该库以便能够将其链接到编译扩展的其余部分(也只是使用super
' sbuild_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"],
}
)
您可以通过调用来强制安装可选的必需包pip install Project-A[PDF]
,但如果由于某种原因'PDF'
事先满足了命名额外的要求,pip install Project-A
最终将获得相同的"Project-A"
功能。这意味着“Project-A”的安装方式并未针对命令行中指定的每个额外内容进行自定义,“Project-A”将始终尝试以相同的方式安装,并且可能由于不可用而导致功能减少可选要求。
据我了解,这意味着为了让您的模块 X 仅在指定 [extra1] 时才被编译和安装,您应该将模块 X 作为一个单独的包提供,并通过extras_require
. 让我们想象模块 X 将被装运my_package_opt
,您的设置my_package
应该如下所示
setup(
name="my_package",
...
extras_require={
'extra1': ["my_package_opt"],
}
)
好吧,我很抱歉我的回答结束了这么长,但我希望它有所帮助。不要犹豫指出任何概念或命名错误,因为我主要试图从setuptools
源代码中推断出这一点。