2016-12-15 2 views
16

Präambel: Python Setuptools für die Paketverteilung verwendet werden. Ich habe ein Python-Paket (nennen wir es my_package), das mehrere extra_require Pakete dazu hat. Alles funktioniert nur find (Installation und Build des Pakets, sowie Extras, wenn angefordert wurden), da alle extra_require Python-Pakete selbst waren und Pip alles korrekt gelöst hat. Eine einfache pip install my_package arbeitete wie ein Charme.Python Setuptools/distutils bauen speziell für das `extra` Paket mit Makefile

Setup: nun für eines der Extras (nennen wir es extra1) Ich brauche X eine binäre einer nicht-Python-Bibliothek zu nennen.

Das Modul X selbst (Quellcode) wurde zur Codebasis my_package hinzugefügt und war in der Distribution my_package enthalten. Traurig für mich, X verwendet werden muss zuerst in eine Binärdatei auf dem Zielcomputer kompiliert werden (C++ - Implementierung; ich nehme an, solche Kompilierung soll in der Build-Phase von my_package Installation passieren). Es gibt einen Makefile in der X Bibliothek, die für die unterschiedliche Plattformkompilierung optimiert ist, so dass nur make in dem entsprechenden Verzeichnis der X Bibliothek im my_package ausgeführt werden muss, wenn der Buildprozess ausgeführt wird.

Frage # 1: wie ein Terminal-Befehl auszuführen (das heißt, make in meinem Fall) während der Erstellung des Paketes, Setuptools/distutils verwenden?

Frage # 2: Wie kann sichergestellt werden, dass ein solcher Terminalbefehl nur ausgeführt wird, wenn während des Installationsprozesses der entsprechende extra1 angegeben wird?

Beispiel:

  1. Wenn jemand pip install my_package läuft, keine solche zusätzliche Erstellung von Bibliotheks X soll geschehen.
  2. Wenn jemand pip install my_package [extra1] ausführt, muss das Modul X kompiliert werden, damit die entsprechende Binärdatei erstellt und auf dem Zielcomputer verfügbar ist.
+1

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

+1

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

+0

Hat 'X' ein' setup.py' und ist somit ein normales Python-Paket? – fpbhb

Antwort

1

Leider sind die docs extrem knapp um die Interaktion zwischen setup.py und Pip, aber Sie sollten in der Lage sein, so etwas zu tun:

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

Dadurch erhalten Sie einen Haken in beliebiger Code ausgeführt wird mit Befehlen, und unterstützt auch eine Vielzahl von benutzerdefinierten Option Parsing (hier nicht gezeigt).

Setzen Sie dieses in einer setup.py Datei und versuchen, diese:

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

Beachten Sie, dass dieser Befehl nach der Hauptinstallationsreihenfolge ausgeführt wird, so dass je nach genau das, was Sie versuchen, es zu tun funktioniert möglicherweise nicht.Siehe die ausführliche Pip Installation Ausgabe:

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

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 setuptoolsbuild_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 superbuild_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.