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:
- Si alguien ejecuta
pip install my_package
, nopip install my_package
tal compilación adicional de la bibliotecaX
- Si alguien ejecuta
pip install my_package [extra1]
, el móduloX
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ámetroext_modules
de la llamada a la función desetup
. Si deseamos sobrecargar esta clase, el método principal que construye cada extensión esbuild_extension
(o here para distutils) -
build_clib
se usa para construir bibliotecas declaradas que se pasan a través del parámetro delibraries
de la llamada a la función desetup
. En este caso, el método principal que debemos sobrecargar con nuestra clase derivada es el métodobuild_libraries
( here paradistutils
).
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