プリアンブル: 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
が指定された場合にのみ、そのようなターミナルコマンドが実行されるようにするにはどうすればよいですか?
例:
pip install my_package
を実行した場合、ライブラリX
のそのような追加のコンパイルは発生しません。pip install my_package [extra1]
を実行する場合、モジュールX
をコンパイルする必要があるため、対応するバイナリが作成され、ターゲットマシンで使用可能になります。この質問は、2年前にコメントした後、ずっと私を悩ませてきました。私自身も最近ほぼ同じ問題を抱えていましたが、ほとんどの人が経験したに違いないと思うので、ドキュメントが非常に少ないことがわかりました。そこで、 setuptools と distutils のソースコードを少し調べて、あなたが尋ねた両方の質問に対して多かれ少なかれ標準的なアプローチを見つけることができるかどうかを調べました。
あなたが最初に尋ねた質問
質問#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
_ が実行されるので、これらはcmdclass
のsetup
でオーバーロードする必要があるものであり、問題は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
ソースコードからこれを推測しようとしたので、概念エラーや名前付けエラーを指摘することを躊躇しないでください。
残念ながら、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