2016-12-15 5 views
16

Преамбула: Python setuptools используются для распространения пакета. У меня есть пакет Python (назовем его my_package), у которого есть несколько пакетов extra_require. Все работает, просто найдите (установка и сборка пакета, а также дополнительные функции, если были запрошены), так как все extra_require были самими пакетами python, и pip правильно разрешил все. Простой pip install my_package работал как шарм.Python setuptools/distutils custom build для пакета `extra` с Makefile

Установка: Теперь, для одного из статистов (назовем его extra1) Мне нужно вызвать двоичный не-питона библиотеки X.

Модуль X сам (исходный код) был добавлен к кодовой базе my_package и был включен в дистрибутив my_package. К сожалению, для меня, чтобы быть использованным, X необходимо сначала скомпилировать в двоичный файл на целевой машине (реализация на C++, я предполагаю, что такая компиляция должна произойти на этапе сборки установки my_package). Существует Makefile в библиотеке X, оптимизированной для различной компиляции платформы, поэтому все, что необходимо, - это запустить make в соответствующем каталоге библиотеки X в my_package, когда процесс сборки запущен.

Вопрос № 1: как выполнить команду терминала (то есть, make в моем случае) в процессе сборки пакета, используя Setuptools/Distutils?

Вопрос № 2: как обеспечить, чтобы такая команда терминала выполнялась только в том случае, если в процессе установки указан соответствующий extra1?

Пример:

  1. Если кто-то бежит pip install my_package, не такая дополнительная компиляция библиотеки X не должно произойти.
  2. Если кто-то запустил pip install my_package [extra1], модуль X должен быть скомпилирован, поэтому соответствующий файл будет создан и доступен на целевой машине.
+1

Положительный дубликат [Как запустить Makefile в setup.py] (http://stackoverflow.com/questions/1754966/how-can-i-run-a-makefile-in-setup-py)? – lucianopaz

+1

Не совсем. Это а) не имеет ответа на ситуацию, когда такая установка требуется, только когда задействован «extra1». б) Это не очень информативно/подробно. Я был бы признателен за более подробный ответ, и я считаю, что это было бы очень информативным для сообщества, если бы был предоставлен довольно подробный ответ. –

+0

У 'X' есть' setup.py' и, следовательно, является обычным пакетом Python? – fpbhb

ответ

1

К сожалению, документы крайне редки вокруг взаимодействия между setup.py и пип, но вы должны быть в состоянии сделать что-то вроде этого:

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} 
) 

Это дает вам крюк в произвольный код работает с командами, а также поддерживает различные пользовательские парсинг параметров (не показано здесь).

Поместите это в setup.py файл и попробуйте это:

pip install --install-option="customcommand" .

Обратите внимание, что эта команда выполняется после основной установки последовательности, поэтому в зависимости от того, что вы пытаетесь сделать, это может не работать.См. Подробный вывод установки:

(.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 
0

Этот вопрос вернулся, чтобы преследовать меня, после того, как я прокомментировал это два года назад! В последнее время у меня была почти такая же проблема, и я нашел документацию ОЧЕНЬ скудной, поскольку, как я думаю, большинство из вас, должно быть, испытало. Поэтому я попытался изучить немного исходного кода 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, поэтому это те, которые мы хотим перегрузить с помощью cmdclasssetup, вопрос становится какой из трех.

  • build_py используется для «сборки» чистых пакетов python, поэтому мы можем смело игнорировать его.
  • build_ext используется для создания заявленных модулей расширения, которые проходят через параметр ext_modules вызова функции setup.Если мы хотим перегружать этот класс, основной метод, который строит каждое расширение является build_extension (или here для Distutils)
  • build_clib используются для построения объявленных библиотек, которые передаются через параметр вызова функции setuplibraries. В этом случае основным методом, который мы должны перегрузить с нашим производным классом, является метод build_libraries (here для distutils).

Я поделюсь пример пакета, который строит игрушечный гр статическую библиотеку через Makefile, используя setuptoolsbuild_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. Я проверяю, имеет ли это расширение имя специального, и если это не я вызываю superbuild_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 задается в процессе установки?

Это возможно с использованием устаревшего параметра featuressetuptools.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.

 Смежные вопросы

  • Нет связанных вопросов^_^