twine scripts practices for create best python makefile python-packaging python-install

scripts - Python setuptools/distutils compilación personalizada para el paquete `extra` con Makefile



python pypi package (2)

Preámbulo: las herramientas de configuración de Python se utilizan para la distribución del paquete. Tengo un paquete de Python (llamémoslo my_package ), que tiene varios paquetes extra_require . Todo funciona simplemente encontrar (instalación y compilación del paquete, así como los extras, si se solicitaron), ya que todos los extra_require fueron paquetes de Python y el pip lo resolvió todo correctamente. Un simple pip install my_package funcionó a la pip install my_package .

Configuración: Ahora, para uno de los extras (llamémoslo extra1 ) necesito llamar a un binario de una biblioteca que no sea de python X

El módulo X sí (código fuente) se agregó a la my_package código my_package y se incluyó en la distribución my_package . Lamentablemente para mí, para ser utilizado, X debe compilarse primero en un binario en la máquina de destino (implementación de C ++; supongo que dicha compilación se realizará en la etapa de my_package instalación de my_package ). Existe un Makefile en la biblioteca X optimizado para la compilación de diferentes plataformas, por lo que todo lo que se necesita es ejecutar make en el directorio respectivo de la biblioteca X en my_package cuando el proceso de compilación se está ejecutando.

Pregunta # 1 : ¿cómo ejecutar un comando de terminal (es decir, make en mi caso) durante el proceso de compilación del paquete, usando setuptools / distutils?

Pregunta n. ° 2 : ¿cómo garantizar que dicho comando de terminal se ejecute solo si se especifica el extra1 correspondiente durante el proceso de instalación?

Ejemplo:

  1. Si alguien ejecuta pip install my_package , no pip install my_package tal compilación adicional de la biblioteca X
  2. Si alguien ejecuta pip install my_package [extra1] , el módulo X debe compilarse, por lo que el binario correspondiente se creará y estará disponible en la máquina de destino.

¡Esta pregunta volvió a atormentarme mucho después de comentarla hace dos años! Yo mismo tuve casi el mismo problema recientemente, y encontré la documentación MUY escasa, ya que creo que la mayoría de ustedes debe haber experimentado. Así que traté de investigar un poco el código fuente de setuptools y distutils para ver si podía encontrar un enfoque más o menos estándar para ambas preguntas.

La primera pregunta que hiciste

Pregunta # 1 : ¿cómo ejecutar un comando de terminal (es decir, make en mi caso) durante el proceso de compilación del paquete, usando setuptools / distutils?

tiene muchos enfoques y todos ellos implican establecer una cmdclass al llamar a la setup . El parámetro cmdclass de la setup debe ser una asignación entre los nombres de los comandos que se ejecutarán según las necesidades de compilación o instalación de la distribución, y las clases que heredan de la clase base distutils.cmd.Command (como nota al setuptools.command.Command , setuptools.command.Command la clase se deriva de la clase Command de distutils para que pueda derivar directamente de la implementación de setuptools ).

El cmdclass permite definir cualquier nombre de comando, como lo hizo ayoon y luego ejecutarlo específicamente cuando se llama a python setup.py --install-option="customcommand" desde la línea de comando. El problema con esto, es que no es el comando estándar que se ejecutará al intentar instalar un paquete a través de pip o al llamar a python setup.py install . La forma estándar de abordar esto es verificar qué comandos de setup intentará ejecutar en una instalación normal y luego sobrecargar esa cmdclass particular.

Desde la búsqueda en setuptools.setup y distutils.setup , setup ejecutará los comandos que se encuentran en la línea de comandos , lo que permite asumir que es solo una install simple. En el caso de setuptools.setup , esto activará una serie de pruebas que verán si se debe recurrir a una simple llamada a la clase de comando distutils.install , y si esto no ocurre, intentará ejecutar bdist_egg . A su vez, este comando hace muchas cosas, pero crucialmente decide si llamar a los build_clib , build_py y / o build_ext . El distutils.install simplemente ejecuta build si es necesario, que también ejecuta build_clib , build_py y / o build_ext . Esto significa que, independientemente de si utiliza setuptools o distutils , si es necesario compilar desde la fuente, se build_clib los comandos build_clib , build_py y / o build_ext , por lo que estos serán los que querremos sobrecargar con la cmdclass de setup , la pregunta se convierte en cuál de los tres.

  • build_py se utiliza para "construir" paquetes de python puros, por lo que podemos ignorarlo de forma segura.
  • build_ext se usa para construir módulos de extensión declarados que se pasan a través del parámetro ext_modules de la llamada a la función de setup . Si deseamos sobrecargar esta clase, el método principal que construye cada extensión es build_extension (o here para distutils)
  • build_clib se usa para construir bibliotecas declaradas que se pasan a través del parámetro de libraries de la llamada a la función de setup . En este caso, el método principal que debemos sobrecargar con nuestra clase derivada es el método build_libraries ( here para distutils ).

Compartiré un paquete de ejemplo que construye una biblioteca estática de toy c a través de un Makefile usando el comando setuptools build_ext . El enfoque puede adaptarse al uso del comando build_clib , pero tendrá que build_clib.build_libraries el código fuente de 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

En este ejemplo, la biblioteca c que quiero compilar utilizando el Makefile personalizado solo tiene una función que imprime "Hello from testlib_fun!/n" a stdout. El script test.c es una interfaz simple entre python y la función única de esta biblioteca. La idea es que le diga a la setup que quiero compilar una extensión de CA llamada test_pack_opt.test_ext , que solo tiene un único archivo de origen: el test.c interfaz test.c , y también le digo a la extensión que debe enlazar contra la biblioteca estática libtestlib.a Lo principal es que sobrecargo el build_ext build_ext usando el_build_ext specialized_build_ext(build_ext, object) . La herencia del object solo es necesaria si desea poder llamar a super para enviar a los métodos de clase padre. El método build_extension toma una instancia de Extension como su segundo argumento, para funcionar bien con otras instancias de Extension que requieren el comportamiento predeterminado de build_extension , build_extension si esta extensión tiene el nombre de la especial y si no la llamo El método de build_extension super .

Para la biblioteca especial, llamo a Makefile simplemente con subprocess.Popen(''make static ...'') . El resto del comando que se pasa al shell es solo para mover la biblioteca estática a una determinada ubicación predeterminada en la que debería encontrarse la biblioteca para poder vincularla con el resto de la extensión compilada (que también se compila con el super método de build_extension ).

Como puede imaginar, hay tantas maneras de organizar este código de manera diferente, no tiene sentido enumerarlos todos. Espero que este ejemplo sirva para ilustrar cómo llamar a Makefile y a qué cmdclass y clase derivada del Command debe sobrecargar para llamar a make en una instalación estándar.

Ahora, en la pregunta 2.

Pregunta n. ° 2 : ¿cómo garantizar que dicho comando de terminal se ejecute solo si se especifica el extra1 correspondiente durante el proceso de instalación?

Esto fue posible con el parámetro de features obsoletas de setuptools.setup . La forma estándar es intentar instalar el paquete según los requisitos que se cumplan. install_requires enumera los requisitos obligatorios, los extras_requires enumera los requisitos opcionales. Por ejemplo de la documentación de setuptools

setup( name="Project-A", ... extras_require={ ''PDF'': ["ReportLab>=1.2", "RXP"], ''reST'': ["docutils>=0.3"], } )

podría forzar la instalación de los paquetes opcionales requeridos llamando a pip install Project-A[PDF] , pero si por alguna razón los requisitos para el ''PDF'' llamado extra se cumplieran de pip install Project-A , pip install Project-A terminaría con el misma funcionalidad "Project-A" . Esto significa que la forma en que se instala "Project-A" no se personaliza para cada extra especificado en la línea de comandos, "Project-A" siempre intentará instalar de la misma manera y puede terminar con una funcionalidad reducida debido a que no está disponible Requisitos opcionales.

Por lo que entendí, esto significa que para que su módulo X se compile e instale solo si se especifica [extra1], debe enviar el módulo X como un paquete separado y depender de él a través de un extras_require . Imaginemos que el módulo X se enviará en my_package_opt , su configuración para my_package debería verse como

setup( name="my_package", ... extras_require={ ''extra1'': ["my_package_opt"], } )

Bueno, lamento que mi respuesta haya sido tan larga, pero espero que sirva. No dude en señalar cualquier error conceptual o de nombre, ya que la mayoría de las setuptools intenté deducirlo del código fuente de setuptools .


Desafortunadamente, los documentos son extremadamente escasos en cuanto a la interacción entre setup.py y pip, pero deberías poder hacer algo como esto:

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

Esto le da un gancho para ejecutar código arbitrario con comandos y también admite una variedad de análisis de opciones personalizadas (no se muestra aquí).

Pon esto en un archivo setup.py y prueba esto:

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

Tenga en cuenta que este comando se ejecuta después de la secuencia de instalación principal, por lo que dependiendo de lo que está intentando hacer, es posible que no funcione. Ver el resultado de la instalación de pip detallada:

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