Этот вопрос вернулся, чтобы преследовать меня, после того, как я прокомментировал это два года назад! В последнее время у меня была почти такая же проблема, и я нашел документацию ОЧЕНЬ скудной, поскольку, как я думаю, большинство из вас, должно быть, испытало. Поэтому я попытался изучить немного исходного кода 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
будет запускать команды он found in the command line, что позволяет предположить, это обычный 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
используется для создания заявленных модулей расширения, которые проходят через параметр ext_modules
вызова функции setup
.Если мы хотим перегружать этот класс, основной метод, который строит каждое расширение является build_extension
(или here для Distutils)
build_clib
используются для построения объявленных библиотек, которые передаются через параметр вызова функции setup
libraries
. В этом случае основным методом, который мы должны перегрузить с нашим производным классом, является метод build_libraries
(here для distutils
).
Я поделюсь пример пакета, который строит игрушечный гр статическую библиотеку через Makefile, используя setuptools
build_ext
команду. Этот подход может быть адаптирован для использования команды 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.commonprefix(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 [email protected]
libtestlib.a: $(OBJS)
ar crs [email protected] $(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 только имеет одну функцию, которая печатает "Hello from testlib_fun!\n"
на стандартный вывод. Сценарий test.c
- это простой интерфейс между python и единственной функцией этой библиотеки. Идея заключается в том, что я указываю setup
, что хочу построить расширение c с именем test_pack_opt.test_ext
, в котором есть только один исходный файл: сценарий интерфейса test.c
, а также указать расширение, которое оно должно связывать с статической библиотекой libtestlib.a
. Главное, что я перегружаю cmdclass build_ext
, используя specialized_build_ext(build_ext, object)
. Наследование от object
необходимо только в том случае, если вы хотите позвонить super
для отправки методам родительского класса. Метод build_extension
принимает в качестве второго аргумента экземпляр Extension
, чтобы работать с другими Extension
экземплярами, которым требуется поведение по умолчанию build_extension
. Я проверяю, имеет ли это расширение имя специального, и если это не я вызываю super
build_extension
метод.
Для специальной библиотеки я вызываю Makefile просто с subprocess.Popen('make static ...'). The rest of the command passed to the shell is just to move the static library to a certain default location in which the library should be found to be able to link it to the rest of the compiled extension (which is also just compiled using the
супер 's
метод build_extension`).
Как вы можете себе представить, есть только несколько способов, которыми вы могли бы организовать этот код по-разному, нет смысла перечислять их все. Надеюсь, что этот пример служит для иллюстрации того, как вызвать Makefile и какие производные классы cmdclass
и Command
вы должны перегрузить для вызова make
в стандартной установке.
Теперь на вопрос 2.
Вопрос № 2: как обеспечить, что такой терминал команда выполняется, только если соответствующая extra1 задается в процессе установки?
Это возможно с использованием устаревшего параметра features
setuptools.setup
. Стандартный способ - попытаться установить пакет в зависимости от требований, которые выполняются. install_requires
перечислены обязательные требования, в номерах extras_requires
перечислены дополнительные требования. Например от setuptools
documentation
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
.
Положительный дубликат [Как запустить Makefile в setup.py] (http://stackoverflow.com/questions/1754966/how-can-i-run-a-makefile-in-setup-py)? – lucianopaz
Не совсем. Это а) не имеет ответа на ситуацию, когда такая установка требуется, только когда задействован «extra1». б) Это не очень информативно/подробно. Я был бы признателен за более подробный ответ, и я считаю, что это было бы очень информативным для сообщества, если бы был предоставлен довольно подробный ответ. –
У 'X' есть' setup.py' и, следовательно, является обычным пакетом Python? – fpbhb