Этот вопрос не давал мне покоя еще долго после того, как я прокомментировал его два года назад! Недавно у меня была почти такая же проблема, и я нашел документацию ОЧЕНЬ скудной, как, я думаю, большинство из вас должно было испытать. Поэтому я попытался немного изучить исходный код 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
используется для построения объявленных модулей расширения, которые передаются через параметр ext_modules
вызова функции setup
. Если мы хотим перегрузить этот класс, основным методом, который создает каждое расширение, является build_extension
(или здесь для distutils)
build_clib
используется для сборки объявленных библиотек, которые передаются через параметр libraries
вызова функции setup
. В этом случае основным методом, который мы должны перегрузить с нашим производным классом, является build_libraries
(здесь для distutils
).
Я поделюсь примером пакета, который создает статическую библиотеку toy c через 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.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
В этом примере библиотека c, которую я хочу создать с помощью пользовательского 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
, я проверяю, имеет ли это расширение имя специального, и если это не так, я вызываю Метод build_extension
super
.
Для специальной библиотеки я вызываю Makefile просто с помощью subprocess.Popen('make static ...')
. Остальная часть команды, переданной в оболочку, предназначена только для перемещения статической библиотеки в определенное место по умолчанию, в котором должна быть найдена библиотека, чтобы иметь возможность связать ее с остальной частью скомпилированного расширения (которое также только что скомпилировано с использованием super
). build_extension
способ).
Как вы понимаете, существует ооочень много способов организовать этот код по-разному, нет смысла перечислять их все. Я надеюсь, что этот пример служит иллюстрацией того, как вызывать Makefile и какие производные классы cmdclass
и Command
следует перегрузить для вызова make
в стандартной установке.
Теперь по вопросу 2.
Вопрос #2: как сделать так, чтобы такая терминальная команда выполнялась только в том случае, если в процессе установки указана соответствующая дополнительная1?
Это было возможно с устаревшим параметром 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"
. Это означает, что способ установки «Проект-А» не настраивается для каждого дополнительного параметра, указанного в командной строке, «Проект-А» всегда будет пытаться установить одним и тем же способом и может иметь ограниченную функциональность из-за недоступности. необязательные требования.
Насколько я понял, это означает, что для того, чтобы ваш модуль X был скомпилирован и установлен только в том случае, если указан [extra1], вы должны поставлять модуль X как отдельный пакет и зависеть от него через файл extras_require
. Давайте представим, что модуль X будет отправлен в my_package_opt
, ваша установка для my_package
должна выглядеть так:
setup(
name="my_package",
...
extras_require={
'extra1': ["my_package_opt"],
}
)
Что ж, извините, что мой ответ получился таким длинным, но я надеюсь, что это поможет. Не стесняйтесь указывать на любую концептуальную ошибку или ошибку в именовании, поскольку я в основном пытался вывести ее из исходного кода setuptools
.
person
lucianopaz
schedule
06.02.2018
X
setup.py
и, следовательно, является ли он обычным пакетом Python? - person fpbhb   schedule 22.12.2016X
— это cpp-пакет с Makefile. Makefile сам по себе очень гибкий и поддерживает несколько платформ. Итак, все, что нужно сделать, — это сделать одну команду make в соответствующей подпапке. - person Sergey Aganezov jr   schedule 23.12.2016X
как с зависимостью, отличной от Python, которую нельзя установить с помощьюpip
. т.е. вам (и вашим пользователям) придется установитьX
с помощью диспетчера пакетов ОС или вручную. Обратите внимание, что вы даже не можете надеяться на достойныйmake
на всех платформах. - person fpbhb   schedule 23.12.2016X
отдельно. - person Sergey Aganezov jr   schedule 24.12.2016