Python setuptools/distutils为Makefi的“extra”包自定义构建

2024-09-29 04:19:31 发布

您现在位置:Python中文网/ 问答频道 /正文

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

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

模块X本身(源代码)被添加到my_package代码库中,并包含在发行版my_package中。可悲的是,对于我来说,^ {CD6}}需要首先编译成目标机器上的二进制(C++实现;我假定这样的编译应该发生在^ {CD1}}安装的构建阶段)。在为不同平台编译而优化的X库中有一个Makefile,因此所需的就是在生成过程运行时,在my_package中的X库的相应目录中运行make。在

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

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

示例:

  1. 如果有人运行pip install my_package,则不会再编译库{}。在
  2. 如果有人运行pip install my_package [extra1],则需要编译模块X,这样就可以创建相应的二进制文件,并在目标计算机上使用。在

Tags: 模块installpip文件终端package目标make
2条回答

两年前,我对这个问题发表评论很久之后,这个问题就一直困扰着我!我自己最近也遇到了同样的问题,我发现文档非常稀少,我想你们中的大多数人一定都经历过。所以我试着研究一下setuptools和{a2}的源代码,看看能不能找到一种或多或少标准化的方法来回答你提出的这两个问题。在


你问的第一个问题

Question #1: how to run a terminal command (i.e., make in my case) during the build process of the package, using setuptools/distutils?

有很多方法,它们都涉及在调用setup时设置cmdclasssetup的参数cmdclass必须是将根据发行版的构建或安装需要执行的命令名与继承自^{}基类的类之间的映射(作为补充说明,setuptools.command.Command类是从distutils'Command类派生的,因此可以直接从setuptools实现派生)。在

cmdclass允许您定义任何命令名,如ayoon所做的,然后在从命令行调用python setup.py install-option="customcommand"时具体执行它。问题在于,当试图通过pip或调用python setup.py install安装包时,它不是标准命令。实现这一点的标准方法是检查setup将尝试在正常安装中执行哪些命令,然后重载特定的cmdclass。在

^{}^{}开始,setup将运行命令it found in the command line,这让我们假设只是一个普通的install。在setuptools.setup的情况下,这将触发一系列测试,这些测试将查看是否要求助于对distutils.install命令类的简单调用,如果没有发生这种情况,它将尝试运行^{}。反过来,这个命令可以做很多事情,但关键是决定是否调用build_clibbuild_py和/或build_ext命令。如果需要,distutils.install只运行build,它还运行^{}^{}和/或{a11}。这意味着无论您使用setuptools还是distutils,如果需要从源代码构建,命令^{}^{}和/或^{}将被运行,因此我们希望用setup的{}重载这些命令,问题就变成这三个命令中的哪一个。在

  • build_py用于“构建”纯python包,因此我们可以安全地忽略它。在
  • build_ext用于生成声明的扩展模块,这些模块通过对setup函数的调用的ext_modules参数传递。如果我们希望重载这个类,构建每个扩展的主方法是^{}(或者对于distutils,here
  • build_clib用于生成声明的库,这些库通过调用libraries参数传递给setup函数。在本例中,我们应该用派生类重载的主要方法是^{}方法(here表示distutils)。在

我将分享一个示例包,它通过Makefile使用setuptoolsbuild_ext命令,通过Makefile构建一个toyc静态库。这种方法可以适应使用build_clib命令,但是您必须签出build_clib.build_libraries的源代码。在

设置.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},
       )

测试包

^{pr2}$

测试软件包

from __future__ import absolute_import, print_function
import test_pack_opt.test_ext

测试软件包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。test.c脚本是python和这个库的单个函数之间的一个简单接口。我的想法是告诉setup我想构建一个名为test_pack_opt.test_ext的c扩展,它只有一个源文件:test.c接口脚本,我还告诉扩展必须链接到静态库libtestlib.a。最重要的是我结束了使用specialized_build_ext(build_ext, object)加载build_extcmdclass。只有当您希望能够调用super来分派给父类方法时,object的继承才是必需的。build_extension方法以一个Extension实例作为第二个参数,为了更好地处理其他需要build_extension行为的Extension实例,我检查这个扩展是否有特殊的名称,如果它没有,我调用super的{}方法。在

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

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


现在,进入问题2。在

Question #2: how to ensure, that such terminal command is executed only if the corresponding extra1 is specified during the installation process?

这可能是因为不推荐使用的features参数setuptools.setup。标准的方法是根据满足的要求尝试安装包。^{列出强制要求,extras_requires列出可选要求。例如从^{} documentation

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源代码中推断出来的。在

不幸的是,这些文档在设置.py还有皮普,但你应该能做这样的事:

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安装输出:

^{pr2}$

相关问题 更多 >