繁体   English   中英

Python setuptools / distutils使用Makefile定制构建`extra`包

[英]Python setuptools/distutils custom build for the `extra` package with Makefile

序言: Python setuptools用于包分发。 我有一个Python包(我们称之为my_package ),它有几个extra_require包。 一切工作只是找到(安装和构建包,以及额外的,如果被要求),因为所有extra_require都是python包本身和pip正确解决了一切。 一个简单的pip install my_package就像一个魅力。

设置:现在,对于其中一个附加内容(我们称之为extra1 ),我需要调用非python库X的二进制文件。

模块X本身(源代码)已添加到my_package代码库中,并包含在分发my_package 遗憾的是,要使用, X需要首先编译成目标机器上的二进制文件(C ++实现;我假设这样的编译将在my_package安装的构建阶段进行)。 X库中的Makefile针对不同的平台编译进行了优化,因此所需要的my_package在构建过程运行时在my_packageX库的相应目录中运行make

问题1:如何运行终端命令(即, make在包装的构建过程中我的情况下),使用setuptools的/的distutils?

问题2 :如何确保只有在安装过程中指定了相应的extra1时才执行这样的终端命令?

例:

  1. 如果有人运行pip install my_package ,则不pip install my_packageX额外的编译。
  2. 如果某人运行pip install my_package [extra1] ,则需要编译模块X ,因此将在目标机器上创建并提供相应的二进制文件。

两年前我对此发表评论很久以后,这个问题又回来困扰着我! 我自己最近几乎遇到了同样的问题,我发现文档很少,因为我认为你们大多数人都必须经历过。 所以我试着研究一下setuptoolsdistutils的一些源代码,看看我是否能找到一个或多或少的标准方法来解决你提出的问题。


你提出的第一个问题

问题1:如何运行终端命令(即, make在包装的构建过程中我的情况下),使用setuptools的/的distutils?

有很多方法,所有这些方法都涉及在调用setup时设置cmdclass setup的参数cmdclass必须是将根据分发的构建或安装需求执行的命令名称之间的映射,以及从distutils.cmd.Command基类继承的类(作为旁注, setuptools.command.Command class派生自distutilsCommand类,因此您可以直接从setuptools实现派生。

cmdclass允许您定义任何命令名称,就像ayoon所做的那样,然后在命令行调用python setup.py --install-option="customcommand"时专门执行它。 这个问题是,当尝试通过pip或通过调用python setup.py install来安装软件包时,它不是标准命令。 接近这个标准的方法是检查什么命令将setup尝试在正常执行安装,然后重载特殊cmdclass

从查看setuptools.setupdistutils.setupsetup将运行它在命令行中找到的命令 ,假设它只是一个普通的install setuptools.setup的情况下,这将触发一系列测试,这些测试将查看是否诉诸于对distutils.install命令类的简单调用,如果不这样做,它将尝试运行bdist_egg 反过来,这个命令做了很多事情,但关键决定是否调用build_clibbuild_py和/或build_ext命令。 distutils.install只需运行build在必要时他也有build_clibbuild_py和/或build_ext 这意味着无论您是使用setuptools还是distutils ,如果有必要从源代码构建,那么将运行命令build_clibbuild_py和/或build_ext ,因此我们将要使用cmdclass重载这些cmdclasssetup ,问题变成了三个中的哪一个。

  • build_py用于“构建”纯python包,因此我们可以放心地忽略它。
  • build_ext用于构建声明的Extension模块,这些模块通过调用setup函数的ext_modules参数传递。 如果我们希望重载此类,那么构建每个扩展的主要方法是build_extension (或者这里是distutils)
  • build_clib用于构建声明的库,这些libraries通过对setup函数的调用的libraries参数传递。 在这种情况下,我们应该使用派生类重载的主要方法是build_libraries方法( 此处distutils )。

我将共享一个示例包,它通过使用setuptools build_ext命令通过Makefile构建一个玩具c静态库。 该方法可以适用于使用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 /生成文件

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。 test.c脚本是python和这个库的单个函数之间的简单接口。 我的想法是,我告诉setup程序我要构建名为test_pack_opt.test_ext ac扩展,它只有一个源文件: test.c接口脚本,我还告诉扩展它必须链接到静态库libtestlib.a 主要的是我使用specialized_build_ext(build_ext, object)重载build_ext cmdclass。 只有当您希望能够调用super来调度父类方法时,才需要继承object build_extension方法将Extension实例作为其第二个参数,为了与需要build_extension的默认行为的其他Extension实例很好地工作,我检查这个扩展名是否具有特殊名称,如果不是,我调用superbuild_extension方法。

对于特殊库,我只使用subprocess.Popen('make static ...')调用Makefile。 传递给shell的其余命令只是将静态库移动到某个默认位置,在该位置应该找到库以便能够将其链接到已编译扩展的其余部分(这也是使用super编译的的build_extension方法)。

正如您可以想象的那样,您可以通过多种方式以不同的方式组织此代码,将它们全部列出是没有意义的。 我希望这个例子用于说明如何调用Makefile,以及您应该重载哪个cmdclassCommand派生类以在标准安装中调用make


现在,问题2。

问题2 :如何确保只有在安装过程中指定了相应的extra1时才执行这样的终端命令?

使用setuptools.setup的deprecated features参数可以实现这setuptools.setup 标准方法是尝试根据满足的要求安装软件包。 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]来强制安装可选的必需软件包,但是如果出于某种原因需要事先满足'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" .

请注意,此命令在主安装序列之后执行,因此,根据您要执行的操作,它可能无法正常工作。 请参阅详细的pip install输出:

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM