Diese Frage kam zurück, mich zu verfolgen, lange nachdem ich es vor zwei Jahren kommentierte! Ich hatte in letzter Zeit fast das selbe Problem, und ich fand die Dokumentation SEHR selten, wie ich denke, die meisten von euch müssen es erlebt haben. Also habe ich versucht, einen Teil des Quellcodes von setuptools und distutils zu recherchieren, um zu sehen, ob ich einen mehr oder weniger standardmäßigen Ansatz für beide Fragen finden konnte.
Die erste Frage gestellt Sie
Frage # 1: wie ein Terminal-Befehl auszuführen (dh make
in meinem Fall) während des Erstellungsprozesses des Pakets, mit Setuptool/distutils ?
hat viele Ansätze und alle von ihnen beinhalten eine cmdclass
Einstellung, wenn setup
aufrufen. Der Parameter cmdclass
von setup
muss eine Zuordnung zwischen Befehlsnamen sein, die abhängig von den Build- oder Installationsanforderungen der Verteilung ausgeführt werden, und Klassen, die von distutils.cmd.Command
Basisklasse erben (als eine Randnotiz ist die Klasse setuptools.command.Command
von distutils
'Command
Klasse abgeleitet so können Sie direkt aus setuptools
Implementierung ableiten.
die cmdclass
können Sie einen beliebigen Befehlsnamen definieren, wie das, was ayoon tat und dann speziell ausgeführt werden, wenn python setup.py --install-option="customcommand"
von der Kommandozeile aufrufen. Das Problem dabei ist, dass dies nicht der Fall der Standardbefehl, der ausgeführt wird, wenn versucht wird, ein Paket über pip
oderzu installieren. Der übliche Weg, dies zu erreichen, besteht darin, zu prüfen, welche Befehle setup
versuchen, in einer normalen Installation auszuführen und dann das spezielle cmdclass
zu überlasten.
Vom Blick in setuptools.setup
und distutils.setup
wird setup
die entsprechenden Befehle ausführen es found in the command line, die nur eine einfache install
ist vermuten lässt. Im Fall von setuptools.setup
löst dies eine Reihe von Tests aus, bei denen überprüft wird, ob auf einen einfachen Aufruf der Befehlsklasse distutils.install
zurückgegriffen werden soll. Wenn dies nicht geschieht, wird versucht, bdist_egg
auszuführen. Dieser Befehl wiederum macht viele Dinge, entscheidet aber entscheidend darüber, ob die Befehle build_clib
, build_py
und/oder build_ext
aufgerufen werden sollen. Die distutils.install
läuft bei Bedarf einfach build
, die auch build_clib
, build_py
und/oder build_ext
läuft. Dies bedeutet, dass unabhängig davon, ob Sie setuptools
oder distutils
verwenden, die Befehle build_clib
, build_py
und/oder build_ext
ausgeführt werden müssen, wenn es erforderlich ist, aus der Quelle zu erstellen. Dies sind also diejenigen, die wir mit der cmdclass
von überladen möchten setup
, wird die Frage, welche der drei.
build_py
wird verwendet, reine Python-Pakete zu „bauen“, so können wir sie ignorieren.
build_ext
wird verwendet, um deklarierte Erweiterungsmodule zu erstellen, die über den Parameter ext_modules
des Aufrufs an die setup
-Funktion übergeben werden.Wenn wir diese Klasse zu überlasten wollen, die wichtigste Methode, die jede Erweiterung baut ist build_extension
(oder here für distutils)
build_clib
verwendet wird, um erklärten Bibliotheken aufbauen, die durch die libraries
Parameter des Aufrufs an die setup
Funktion übergeben werden. In diesem Fall ist die Hauptmethode, die wir mit unserer abgeleiteten Klasse überladen sollten, die Methode build_libraries
(here für distutils
).
Ich werde ein Beispiel Paket teilen, die mithilfe setuptools
build_ext
Befehl ein Spielzeug c statische Bibliothek durch ein Makefile aufbaut. Der Ansatz kann an den Befehl build_clib
angepasst werden, aber Sie müssen den Quellcode von build_clib.build_libraries
auschecken.
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
In diesem Beispiel ist die C-Bibliothek, die ich den Brauch mit Makefile hat nur eine Funktion bauen will, die "Hello from testlib_fun!\n"
auf stdout ausgibt. Das Skript test.c
ist eine einfache Schnittstelle zwischen Python und der einzelnen Funktion dieser Bibliothek. Die Idee ist, dass ich setup
, dass ich eine c-Erweiterung namens test_pack_opt.test_ext
erstellen möchte, die nur eine einzige Quelldatei hat: das test.c
Interface-Skript, und ich sage auch die Erweiterung, dass es gegen die statische Bibliothek libtestlib.a
verknüpfen muss. Die Hauptsache ist, dass ich die build_ext
cmdclass über specialized_build_ext(build_ext, object)
überbelaste. Die Vererbung von object
ist nur erforderlich, wenn Sie in der Lage sein möchten, super
aufrufen, um zu übergeordneten Klassenmethoden zu senden. Die build_extension
Methode verwendet eine Extension
Instanz als zweites Argument, um mit anderen Extension
Instanzen, die das Standardverhalten von build_extension
erfordern, zu arbeiten, überprüfe ich, ob diese Erweiterung den Namen des speziellen hat, und wenn es nicht ich rufe super
build_extension
Methode.
Für die spezielle Bibliothek, rufe ich das Makefile einfach mit 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
super 's
build_extension` Methode).
Wie Sie sich vorstellen können gibt es einfach so viele Möglichkeiten, wie Sie diesen Code anders organisieren können, es macht keinen Sinn, sie alle aufzulisten. Ich hoffe, dieses Beispiel dient dazu, zu illustrieren, wie das Makefile aufgerufen wird und welche cmdclass
und Command
abgeleitete Klasse überladen werden sollte, um make
in einer Standardinstallation aufzurufen.
Nun, auf Frage 2.
Frage # 2: Wie, um sicherzustellen, dass solche Terminal-Befehl wird nur ausgeführt, wenn der entsprechende Extra1 während des Installationsprozesses angegeben wird?
Dies war mit dem veralteten Parameter features
von setuptools.setup
möglich. Der Standardweg besteht darin, zu versuchen, das Paket abhängig von den Anforderungen zu installieren, die erfüllt werden. listet die obligatorischen Anforderungen auf, die extras_requires
listet die optionalen Anforderungen auf. Zum Beispiel aus dem setuptools
documentation
setup(
name="Project-A",
...
extras_require={
'PDF': ["ReportLab>=1.2", "RXP"],
'reST': ["docutils>=0.3"],
}
)
Sie die Installation der optionalen erforderlichen Pakete zwingen könnte pip install Project-A[PDF]
durch den Aufruf, aber wenn aus irgendeinem Grund die Anforderungen an die 'PDF'
zusätzliche Namen, bevor die Hand zufrieden waren, würden pip install Project-A
mit dem gleichen Ende "Project-A"
Funktionalität. Das bedeutet, dass die Art und Weise, in der "Project-A" installiert wird, nicht für jedes in der Befehlszeile angegebene Extra angepasst wird. "Project-A" wird immer versuchen, auf die gleiche Weise zu installieren und könnte aufgrund der Nichtverfügbarkeit eine eingeschränkte Funktionalität aufweisen optionale Anforderungen.
Von dem, was ich verstanden, bedeutet dies, dass, um das Modul X zu erhalten, um nur dann kompiliert und installiert werden, wenn [Extra1] angegeben ist, sollten Sie Modul X als separates Paket und hängen von ihm durch eine extras_require
versenden. Lässt sich vorstellen Modul X in my_package_opt
versendet wird, das Setup für my_package
sollte so aussehen
setup(
name="my_package",
...
extras_require={
'extra1': ["my_package_opt"],
}
)
Nun, es tut mir leid, dass meine Antwort so lang ist beendet, aber ich hoffe, es hilft. Zögern Sie nicht, auf einen konzeptionellen oder Namensfehler hinzuweisen, da ich versucht habe, dies aus dem Quellcode setuptools
abzuleiten.
Posibles Duplikat von [Wie kann ich ein Makefile in setup.py ausführen] (http://stackoverflow.com/questions/1754966/how-can-i-run-a-makefile-in-setup-py)? – lucianopaz
Nicht genau. Es a) hat nicht die Antwort für eine Situation, wenn eine solche Installation erforderlich ist, nur wenn die "extra1" beteiligt ist. b) Es ist nicht wirklich informativ/detailliert. Ich würde mich über eine ausführlichere Antwort freuen, und ich glaube, dass dies für die Gemeinschaft sehr informativ wäre, wenn eine ziemlich detaillierte Antwort gegeben würde. –
Hat 'X' ein' setup.py' und ist somit ein normales Python-Paket? – fpbhb