복붙노트

[PYTHON] setup.py에서 CMake를 사용하도록 setuptools 확장을 확장 하시겠습니까?

PYTHON

setup.py에서 CMake를 사용하도록 setuptools 확장을 확장 하시겠습니까?

저는 C ++ 라이브러리를 링크하는 파이썬 확장을 작성 중이며 cmake를 사용하여 빌드 프로세스를 돕고 있습니다. 이것은 현재, 묶는 방법을 알고있는 유일한 방법이라는 것을 의미합니다. setup.py bdist_wheel을 실행하기 전에 cmake를 먼저 컴파일해야합니다. 더 좋은 방법이 있어야합니다.

setup.py ext_modules 빌드 프로세스의 일부로 CMake를 호출 할 수 있는지 궁금합니다. 거기에 뭔가의 하위 클래스를 만드는 방법이 있다고 생각하지만, 어디서 볼 것인지 잘 모르겠습니다.

CMake를 사용하고 있습니다. C와 C ++ 라이브러리 확장을 원하는대로 정확하게 빌드 할 수있는 빌드를 제공합니다. 또한 findPythonLibs.cmake에서 PYTHON_ADD_MODULE () 명령을 사용하여 cmake로 직접 Python 확장을 쉽게 빌드 할 수 있습니다. 나는 이것이 모두 한 걸음 이었으면 좋겠다.

해결법

  1. ==============================

    1.기본적으로 setup.py에서 build_ext 명령 클래스를 덮어 쓰고 명령 클래스에 등록해야합니다. build_ext의 사용자 정의 impl에서 cmake를 구성하고 호출하여 구성한 다음 확장 모듈을 빌드하십시오. 불행히도 공식 문서는 커스텀 distutils 커맨드를 구현하는 방법에 대해 간결합니다 (Distutils 확장 참고). 나는 명령 코드를 직접 연구하는 것이 훨씬 더 도움이된다고 생각한다. 예를 들어, 다음은 build_ext 명령의 소스 코드입니다.

    기본적으로 setup.py에서 build_ext 명령 클래스를 덮어 쓰고 명령 클래스에 등록해야합니다. build_ext의 사용자 정의 impl에서 cmake를 구성하고 호출하여 구성한 다음 확장 모듈을 빌드하십시오. 불행히도 공식 문서는 커스텀 distutils 커맨드를 구현하는 방법에 대해 간결합니다 (Distutils 확장 참고). 나는 명령 코드를 직접 연구하는 것이 훨씬 더 도움이된다고 생각한다. 예를 들어, 다음은 build_ext 명령의 소스 코드입니다.

    단일 C 확장 foo와 python 모듈 spam.eggs로 구성된 간단한 프로젝트를 준비했습니다.

    so-42585210/
    ├── spam
    │   ├── __init__.py  # empty
    │   ├── eggs.py
    │   ├── foo.c
    │   └── foo.h
    ├── CMakeLists.txt
    └── setup.py
    

    이것들은 설치 스크립트를 테스트하기 위해 작성한 간단한 스텁 일뿐입니다.

    spam / eggs.py (라이브러리 호출 테스트 용) :

    from ctypes import cdll
    import pathlib
    
    
    def wrap_bar():
        foo = cdll.LoadLibrary(str(pathlib.Path(__file__).with_name('libfoo.dylib')))
        return foo.bar()
    

    스팸 / foo.c :

    #include "foo.h"
    
    int bar() {
        return 42;
    }
    

    스팸 / foo.h :

    #ifndef __FOO_H__
    #define __FOO_H__
    
    int bar();
    
    #endif
    

    CMakeLists.txt :

    cmake_minimum_required(VERSION 3.10.1)
    project(spam)
    set(src "spam")
    set(foo_src "spam/foo.c")
    add_library(foo SHARED ${foo_src})
    

    이것은 마술이 일어나는 곳입니다. 물론, 개선의 여지가 많이 있습니다 - 필요한 경우 CMakeExtension 클래스에 추가 옵션을 전달할 수 있습니다 (확장에 대한 자세한 내용은 C 및 C ++ 확장 빌드 참조). CMake 옵션은 setup.cfg를 통해 다음과 같이 구성 할 수 있습니다. initialize_options 및 finalize_options 등의 메소드를 오버라이드 (override)합니다.

    import os
    import pathlib
    
    from setuptools import setup, Extension
    from setuptools.command.build_ext import build_ext as build_ext_orig
    
    
    class CMakeExtension(Extension):
    
        def __init__(self, name):
            # don't invoke the original build_ext for this special extension
            super().__init__(name, sources=[])
    
    
    class build_ext(build_ext_orig):
    
        def run(self):
            for ext in self.extensions:
                self.build_cmake(ext)
            super().run()
    
        def build_cmake(self, ext):
            cwd = pathlib.Path().absolute()
    
            # these dirs will be created in build_py, so if you don't have
            # any python sources to bundle, the dirs will be missing
            build_temp = pathlib.Path(self.build_temp)
            build_temp.mkdir(parents=True, exist_ok=True)
            extdir = pathlib.Path(self.get_ext_fullpath(ext.name))
            extdir.mkdir(parents=True, exist_ok=True)
    
            # example of cmake args
            config = 'Debug' if self.debug else 'Release'
            cmake_args = [
                '-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=' + str(extdir.parent.absolute()),
                '-DCMAKE_BUILD_TYPE=' + config
            ]
    
            # example of build args
            build_args = [
                '--config', config,
                '--', '-j4'
            ]
    
            os.chdir(str(build_temp))
            self.spawn(['cmake', str(cwd)] + cmake_args)
            if not self.dry_run:
                self.spawn(['cmake', '--build', '.'] + build_args)
            os.chdir(str(cwd))
    
    
    setup(
        name='spam',
        version='0.1',
        packages=['spam'],
        ext_modules=[CMakeExtension('spam/foo')],
        cmdclass={
            'build_ext': build_ext,
        }
    )
    

    프로젝트의 바퀴를 만들고 설치하십시오. 테스트 라이브러리가 설치되어 있습니다 :

    $ pip show -f spam
    Name: spam
    Version: 0.1
    Summary: UNKNOWN
    Home-page: UNKNOWN
    Author: UNKNOWN
    Author-email: UNKNOWN
    License: UNKNOWN
    Location: /Users/hoefling/.virtualenvs/stackoverflow/lib/python3.6/site-packages
    Requires: 
    Files:
      spam-0.1.dist-info/DESCRIPTION.rst
      spam-0.1.dist-info/INSTALLER
      spam-0.1.dist-info/METADATA
      spam-0.1.dist-info/RECORD
      spam-0.1.dist-info/WHEEL
      spam-0.1.dist-info/metadata.json
      spam-0.1.dist-info/top_level.txt
      spam/__init__.py
      spam/__pycache__/__init__.cpython-36.pyc
      spam/__pycache__/eggs.cpython-36.pyc
      spam/eggs.py
      spam/libfoo.dylib
    

    spam.eggs 모듈에서 래퍼 함수를 ​​실행합니다.

    $ python -c "from spam import eggs; print(eggs.wrap_bar())"
    42
    
  2. ==============================

    2.나는 hoefling이 묘사 ​​한 것에 대한 부록의 일종으로서 이것에 대한 내 자신의 대답을 추가하고 싶다.

    나는 hoefling이 묘사 ​​한 것에 대한 부록의 일종으로서 이것에 대한 내 자신의 대답을 추가하고 싶다.

    감사합니다, hoefling, 귀하의 대답은 제 자신의 저장소에 대해 거의 동일한 방식으로 설치 스크립트를 작성하는 데 도움이되었습니다.

    이 대답을 쓰는 주된 동기는 누락 된 부분을 "서로 붙이기"위해 노력하는 것입니다. OP는 개발중인 C / C ++ Python 모듈의 성격을 기술하지 않습니다. 나는 아래의 단계는 여러 .dll / .so 파일뿐만 아니라 미리 컴파일 된 * .pyd / so 파일을 생성하는 C / C ++ cmake 빌드 체인을위한 것임을 분명히하고 싶습니다. 스크립트 디렉토리에 배치해야하는 파일

    이 모든 파일들은 cmake 빌드 명령이 실행 된 직후 결실을 맺습니다 ... 재미. 이 방법으로 setup.py를 빌드하는 것에 대한 권장 사항은 없습니다.

    setup.py는 스크립트가 패키지 / 라이브러리의 일부가 될 것이며 빌드해야하는 .dll 파일은 소스가 있고 나열된 디렉토리를 포함하여 라이브러리 부분을 통해 선언되어야 함을 의미하므로 직관적 인 방법은 없습니다. build_ext에서 발생 된 cmake -b에 대한 한 번의 호출로 인해 생성 된 라이브러리, 스크립트 및 데이터 파일이 모두 각각의 위치에 있어야 함을 setuptools에 알립니다. 이 모듈을 setuptools로 추적하고 완전히 설치 제거 가능하게하려면 사용자가이를 제거하고 원하는 경우 모든 추적을 시스템에서 지울 수 있음을 의미하므로 더욱 심합니다.

    내가 setup.py를 작성한 모듈은 여기에 설명 된 바와 같이 파이썬 모듈로서 블렌더를 빌드하는 bpy, .pyd / .so 등가물입니다 :

    https://wiki.blender.org/wiki//User:Ideasman42/BlenderAsPyModule (더 나은 지침이지만 이제 죽은 링크) http://www.gizmoplex.com/wordpress/compile-blender-as-python-module/ (아마도 악화 된 지시 사항이지만 온라인 상태 인 것 같습니다)

    github에서 내 저장소를 확인할 수 있습니다.

    https://github.com/TylerGubala/blenderpy

    이것이 바로이 답변을 작성한 동기 중 하나이며, cmake 빌드 체인을 버리거나 다른 두 가지 빌드 환경을 유지해야하는 것보다는 비슷한 것을 성취하려고하는 다른 누구에게도 도움이되기를 바랍니다. 화제면 사과드립니다.

    다음은 저의 저장소에서 샘플을 볼 수도 있지만 더 구체적인 것의 명확성을 위해 정리 한 것입니다 (항상 repo로 가서 직접 살펴볼 수 있습니다)

    from distutils.command.install_data import install_data
    from setuptools import find_packages, setup, Extension
    from setuptools.command.build_ext import build_ext
    from setuptools.command.install_lib import install_lib
    from setuptools.command.install_scripts import install_scripts
    import struct
    
    BITS = struct.calcsize("P") * 8
    
    class CMakeExtension(Extension):
        """
        An extension to run the cmake build
        """
    
        def __init__(self, name, sources=[]):
    
            super().__init__(name = name, sources = sources)
    
    class InstallCMakeLibsData(install_data):
        """
        Just a wrapper to get the install data into the egg-info
        """
    
        def run(self):
            """
            Outfiles are the libraries that were built using cmake
            """
    
            # There seems to be no other way to do this; I tried listing the
            # libraries during the execution of the InstallCMakeLibs.run() but
            # setuptools never tracked them, seems like setuptools wants to
            # track the libraries through package data more than anything...
            # help would be appriciated
    
            self.outfiles = self.distribution.data_files
    
    class InstallCMakeLibs(install_lib):
        """
        Get the libraries from the parent distribution, use those as the outfiles
    
        Skip building anything; everything is already built, forward libraries to
        the installation step
        """
    
        def run(self):
            """
            Copy libraries from the bin directory and place them as appropriate
            """
    
            self.announce("Moving library files", level=3)
    
            # We have already built the libraries in the previous build_ext step
    
            self.skip_build = True
    
            bin_dir = self.distribution.bin_dir
    
            libs = [os.path.join(bin_dir, _lib) for _lib in 
                    os.listdir(bin_dir) if 
                    os.path.isfile(os.path.join(bin_dir, _lib)) and 
                    os.path.splitext(_lib)[1] in [".dll", ".so"]
                    and not (_lib.startswith("python") or _lib.startswith(PACKAGE_NAME))]
    
            for lib in libs:
    
                shutil.move(lib, os.path.join(self.build_dir,
                                              os.path.basename(lib)))
    
            # Mark the libs for installation, adding them to 
            # distribution.data_files seems to ensure that setuptools' record 
            # writer appends them to installed-files.txt in the package's egg-info
            #
            # Also tried adding the libraries to the distribution.libraries list, 
            # but that never seemed to add them to the installed-files.txt in the 
            # egg-info, and the online recommendation seems to be adding libraries 
            # into eager_resources in the call to setup(), which I think puts them 
            # in data_files anyways. 
            # 
            # What is the best way?
    
            self.distribution.data_files = [os.path.join(self.install_dir, 
                                                         os.path.basename(lib))
                                            for lib in libs]
    
            # Must be forced to run after adding the libs to data_files
    
            self.distribution.run_command("install_data")
    
            super().run()
    
    class InstallCMakeScripts(install_scripts):
        """
        Install the scripts in the build dir
        """
    
        def run(self):
            """
            Copy the required directory to the build directory and super().run()
            """
    
            self.announce("Moving scripts files", level=3)
    
            # Scripts were already built in a previous step
    
            self.skip_build = True
    
            bin_dir = self.distribution.bin_dir
    
            scripts_dirs = [os.path.join(bin_dir, _dir) for _dir in
                            os.listdir(bin_dir) if
                            os.path.isdir(os.path.join(bin_dir, _dir))]
    
            for scripts_dir in scripts_dirs:
    
                shutil.move(scripts_dir,
                            os.path.join(self.build_dir,
                                         os.path.basename(scripts_dir)))
    
            # Mark the scripts for installation, adding them to 
            # distribution.scripts seems to ensure that the setuptools' record 
            # writer appends them to installed-files.txt in the package's egg-info
    
            self.distribution.scripts = scripts_dirs
    
            super().run()
    
    class BuildCMakeExt(build_ext):
        """
        Builds using cmake instead of the python setuptools implicit build
        """
    
        def run(self):
            """
            Perform build_cmake before doing the 'normal' stuff
            """
    
            for extension in self.extensions:
    
                if extension.name == "bpy":
    
                    self.build_cmake(extension)
    
            super().run()
    
        def build_cmake(self, extension: Extension):
            """
            The steps required to build the extension
            """
    
            self.announce("Preparing the build environment", level=3)
    
            build_dir = pathlib.Path(self.build_temp)
    
            extension_path = pathlib.Path(self.get_ext_fullpath(extension.name))
    
            os.makedirs(build_dir, exist_ok=True)
            os.makedirs(extension_path.parent.absolute(), exist_ok=True)
    
            # Now that the necessary directories are created, build
    
            self.announce("Configuring cmake project", level=3)
    
            self.spawn(['cmake', '-H'+SOURCE_DIR, '-B'+self.build_temp,
                        '-DWITH_PLAYER=OFF', '-DWITH_PYTHON_INSTALL=OFF',
                        '-DWITH_PYTHON_MODULE=ON',
                        f"-DCMAKE_GENERATOR_PLATFORM=x"
                        f"{'86' if BITS == 32 else '64'}"])
    
            self.announce("Building binaries", level=3)
    
            self.spawn(["cmake", "--build", self.build_temp, "--target", "INSTALL",
                        "--config", "Release"])
    
            # Build finished, now copy the files into the copy directory
            # The copy directory is the parent directory of the extension (.pyd)
    
            self.announce("Moving built python module", level=3)
    
            bin_dir = os.path.join(build_dir, 'bin', 'Release')
            self.distribution.bin_dir = bin_dir
    
            pyd_path = [os.path.join(bin_dir, _bpy) for _bpy in
                        os.listdir(bin_dir) if
                        os.path.isfile(os.path.join(bin_dir, _bpy)) and
                        os.path.splitext(_bpy)[0].startswith(PACKAGE_NAME) and
                        os.path.splitext(_bpy)[1] in [".pyd", ".so"]][0]
    
            shutil.move(pyd_path, extension_path)
    
            # After build_ext is run, the following commands will run:
            # 
            # install_lib
            # install_scripts
            # 
            # These commands are subclassed above to avoid pitfalls that
            # setuptools tries to impose when installing these, as it usually
            # wants to build those libs and scripts as well or move them to a
            # different place. See comments above for additional information
    
    setup(name='my_package',
          version='1.0.0a0',
          packages=find_packages(),
          ext_modules=[CMakeExtension(name="test_extension")],
          description='An example cmake extension module',
          long_description=open("./README.md", 'r').read(),
          long_description_content_type="text/markdown",
          keywords="test, cmake, extension",
          classifiers=["Intended Audience :: Developers",
                       "License :: OSI Approved :: "
                       "GNU Lesser General Public License v3 (LGPLv3)",
                       "Natural Language :: English",
                       "Programming Language :: C",
                       "Programming Language :: C++",
                       "Programming Language :: Python",
                       "Programming Language :: Python :: 3.6",
                       "Programming Language :: Python :: Implementation :: CPython"],
          license='GPL-3.0',
          cmdclass={
              'build_ext': BuildCMakeExt,
              'install_data': InstallCMakeLibsData,
              'install_lib': InstallCMakeLibs,
              'install_scripts': InstallCMakeScripts
              }
        )
    
  3. from https://stackoverflow.com/questions/42585210/extending-setuptools-extension-to-use-cmake-in-setup-py by cc-by-sa and MIT license