web-dev-qa-db-ja.com

Python setuptools / distutilsMakefileを使用した `extra`パッケージのカスタムビルド

プリアンブル: Python setuptoolsがパッケージ配布に使用されます。Pythonパッケージ(my_packageと呼びましょう)があります。いくつかのextra_requireパッケージがあります。すべてのextra_requireはpythonパッケージ自体であり、pipはすべてを正しく解決したため、すべてが見つかります(パッケージのインストールとビルド、および要求された場合は追加)。 。単純なpip install my_packageは魅力のように機能しました。

Setup:ここで、エクストラの1つ(extra1と呼びましょう)について、Python以外のライブラリのバイナリを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をコンパイルする必要があるため、対応するバイナリが作成され、ターゲットマシンで使用可能になります。
22

この質問は、2年前にコメントした後、ずっと私を悩ませてきました。私自身も最近ほぼ同じ問題を抱えていましたが、ほとんどの人が経験したに違いないと思うので、ドキュメントが非常に少ないことがわかりました。そこで、 setuptoolsdistutils のソースコードを少し調べて、あなたが尋ねた両方の質問に対して多かれ少なかれ標準的なアプローチを見つけることができるかどうかを調べました。


あなたが最初に尋ねた質問

質問#1:setuptools/distutilsを使用して、パッケージのビルドプロセス中にターミナルコマンド(つまり、私の場合はmake)を実行する方法は?

には多くのアプローチがあり、それらはすべて、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はコマンドを実行します コマンドラインにあります 単なる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でオーバーロードする必要があるものであり、問​​題は3つのうちのどれになります。

  • _build_py_は、純粋なpythonパッケージを「ビルド」するために使用されるため、無視しても問題ありません。
  • _build_ext_は、setup関数の呼び出しの_ext_modules_パラメーターを介して渡される宣言された拡張モジュールを構築するために使用されます。このクラスをオーバーロードしたい場合、各拡張機能を構築する主なメソッドは _build_extension_ (または here distutilsの場合)です。
  • _build_clib_は、libraries関数の呼び出しのsetupパラメーターを介して渡される宣言済みライブラリーを構築するために使用されます。この場合、派生クラスでオーバーロードする必要がある主なメソッドは、 _build_libraries_ メソッド( here for distutils)です。

setuptools _build_ext_コマンドを使用して、Makefileを介しておもちゃの静的ライブラリを構築するサンプルパッケージを共有します。このアプローチは、_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.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
_

この例では、カスタムMakefileを使用して構築するcライブラリには、_"Hello from testlib_fun!\n"_をstdoutに出力する関数が1つだけあります。 _test.c_スクリプトは、pythonとこのライブラリの単一関数の間の単純なインターフェイスです。アイデアは、_test_pack_opt.test_ext_という名前のAC拡張機能を構築することをsetupに伝えることです。ソースファイルは1つだけです:_test.c_インターフェイススクリプト。また、静的ライブラリ_libtestlib.a_に対してリンクする必要があることを拡張機能に伝えます。主なことは、_build_ext_ cmdclassをオーバーロードすることです。 specialized_build_ext(build_ext, object)を使用します。objectからの継承は、superを呼び出して親クラスのメソッドにディスパッチできるようにする場合にのみ必要です。_build_extension_メソッドは、2番目の引数としてExtensionインスタンスを順番に取ります。 _build_extension_のデフォルトの動作を必要とする他のExtensionインスタンスでうまく機能するために、この拡張機能に特別な名前があるかどうかを確認し、ない場合はsuperの_build_extension_メソッドを呼び出します。

特別なライブラリの場合、subprocess.Popen('make static ...')を使用してMakefileを呼び出すだけです。シェルに渡されるコマンドの残りの部分は、静的ライブラリを特定のデフォルトの場所に移動することです。この場所で、ライブラリは、コンパイルされた拡張機能の残りの部分にリンクできるようになります(これもsuperを使用してコンパイルされます)。 _build_extension_メソッド)。

このコードを別の方法で整理する方法は非常にたくさんあると想像できるので、それらすべてをリストすることは意味がありません。この例が、Makefileの呼び出し方法と、標準インストールでcmdclassを呼び出すためにオーバーロードする必要があるCommandおよびmake派生クラスを説明するのに役立つことを願っています。


さて、質問2に移りましょう。

質問#2:インストールプロセス中に対応するextra1が指定された場合にのみ、そのようなターミナルコマンドが実行されるようにするにはどうすればよいですか?

これは、_setuptools.setup_の非推奨のfeaturesパラメーターで可能でした。標準的な方法は、満たされている要件に応じてパッケージのインストールを試みることです。 _install_requires_は必須要件をリストし、_extras_requires_はオプション要件をリストします。たとえば setuptoolsドキュメント から

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

_pip install Project-A[PDF]_を呼び出すことで、オプションの必須パッケージのインストールを強制できますが、何らかの理由で、extraという名前の_'PDF'_の要件が事前に満たされている場合、_pip install Project-A_は同じ結果になります。 _"Project-A"_機能。これは、「Project-A」のインストール方法がコマンドラインで指定された追加ごとにカスタマイズされていないことを意味します。「Project-A」は常に同じ方法でインストールを試み、使用できないために機能が低下する可能性があります。オプションの要件。

私が理解したところによると、これは、[extra1]が指定されている場合にのみモジュールXをコンパイルしてインストールするには、モジュールXを別のパッケージとして出荷し、_extras_require_を介して依存する必要があることを意味します。モジュールXが_my_package_opt_で出荷されると想像してみましょう。_my_package_のセットアップは次のようになります。

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

ええと、私の答えが長すぎて申し訳ありませんが、それがお役に立てば幸いです。私は主にsetuptoolsソースコードからこれを推測しようとしたので、概念エラーや名前付けエラーを指摘することを躊躇しないでください。

7
lucianopaz

残念ながら、setup.pyとpipの間の相互作用に関するドキュメントは非常に不足していますが、次のようなことができるはずです。

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

このコマンドはafterメインインストールシーケンスで実行されるため、実行しようとしている内容によっては、機能しない場合があることに注意してください。詳細なpipインストール出力を参照してください。

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