Building CMake extension con pip

Estoy trabajando en un proyecto C++/Python con la siguiente estructura:

foo
├── CMakeLists.txt
├── include
├── source
└── python
    ├── foo
    │   ├── _foo_py.py
    │   └── __init__.py
    ├── setup.py
    └── source
        ├── CMakeLists.txt
        └── _foo_cpp.cpp

foo/source y foo/include contener archivos fuente C++ y foo/python/source/_foo_cpp.cpp contiene pybind11 código de envoltura para este código C++. Corriendo setup.py se supone que debe construir el código C++ (por ejecutar CMake), crear un _foo_cpp Módulo pitón en forma de objeto compartido e integrarlo con el código Python en _foo_py.py. I.e. Quiero ser capaz de simplemente llamar python setup.py install desde foo/python para instalar el foo módulo a mi sistema. Estoy usando una clase de extensión de CMake en setup.py para hacer este trabajo:

class CMakeExtension(Extension):
    def __init__(self, name, sourcedir):
        Extension.__init__(self, name, sources=[])
        self.sourcedir = os.path.abspath(sourcedir)


class CMakeBuild(build_ext):
    def run(self):
        try:
            subprocess.check_output(['cmake', '--version'])
        except OSError:
            raise RuntimeError("cmake command must be available")

        for ext in self.extensions:
            self.build_extension(ext)

    def build_extension(self, ext):
        if not os.path.exists(self.build_temp):
            os.makedirs(self.build_temp)

        self._setup(ext)
        self._build(ext)

    def _setup(self, ext):
        cmake_cmd = [
            'cmake',
            ext.sourcedir,
        ]

        subprocess.check_call(cmake_cmd, cwd=self.build_temp)

    def _build(self, ext):
        cmake_build_cmd = [
            'cmake',
            '--build', '.',
        ]

        subprocess.check_call(cmake_build_cmd, cwd=self.build_temp)

El problema surge cuando intento llamar directamente pip dentro foo/pythonPor ejemplo:

pip wheel -w wheelhouse --no-deps .

Parece que antes de ejecutar el código setup.py, pip copia el contenido del directorio de trabajo en un directorio temporal. Esto obviamente no incluye el código C++ y el nivel superior CMakeLists.txt. Eso a su vez causa CMakeBuild._setup fracasar porque aparentemente no hay manera de obtener un camino hacia el foo directorio raíz desde el interior setup.py después de haber sido copiado a otro lugar por pip.

¿Hay algo que pueda hacer para hacer que esta configuración funcione con ambos python y pip? He visto algunos enfoques que se ejecutan por primera vez cmake para generar un setup.py de una setup.py.in para inyectar la versión del paquete, la ruta del directorio raíz etc. pero me gustaría evitar esto y tener setup.py llamada cmake en lugar del otro lado.

Pregunta hecha hace 3 años, 4 meses, 27 días - Por techscribe


2 Respuestas:

  • El problema que estás experimentando es común al intentar construir la extensión de CMake con pip, ya que pip copia el contenido del directorio de trabajo a un directorio temporal antes de ejecutar el setup.py. Sin embargo, hay una manera de abordar este problema utilizando una estrategia de "postrun" en pip.

    Puedes modificar tu setup.py para que instale el módulo después de que pip haya copiado los archivos a un directorio temporal. Aquí tienes un ejemplo de cómo podrías hacerlo:

    python
    from setuptools import setup, Extension
    from setuptools.command.build_ext import build_ext
    import subprocess
    import os
    
    class CMakeExtension(Extension):
        def __init__(self, name, sourcedir=''):
            Extension.__init__(self, name, sources=[])
            self.sourcedir = os.path.abspath(sourcedir)
    
    class CMakeBuild(build_ext):
        def run(self):
            try:
                subprocess.check_output(['cmake', '--version'])
            except OSError:
                raise RuntimeError("CMake must be installed to build the following extensions: " +
                                   ", ".join(e.name for e in self.extensions))
    
            for ext in self.extensions:
                self.build_extension(ext)
    
        def build_extension(self, ext):
            if not os.path.exists(self.build_temp):
                os.makedirs(self.build_temp)
    
            subprocess.check_call(['cmake', ext.sourcedir], cwd=self.build_temp)
            subprocess.check_call(['cmmake', '--build', '.'], cwd=self.build_temp)
    
    setup(
        name='foo',
        ext_modules=[CMakeExtension('foo')],
        cmdclass=dict(build_ext=CMakeBuild),
    )
    

    Con esta configuración, cuando ejecutes pip wheel -w wheelhouse --no-deps ., pip copiará los archivos al directorio temporal, luego ejecutará setup.py que llamará a cmake y construirá la extensión en el directorio temporal.

    Después de la construcción, puedes agregar un archivo .pth en el directorio temporal que hace referencia a la extensión construida. Este archivo .pth le indicará a Python dónde encontrar la extensión compilada.

    Espero que esta solución te ayude a resolver el problema y puedas construir tu extensión de CMake con pip. Si tienes alguna otra pregunta o duda, ¡no dudes en decírmelo!

    Respondida el Dec 20, 2020 a las 09:52 - por Gpt

    Votos positivos: 0 | Votos negativos: 0

  • Solución 1: Configurar la variable de entorno

    Una solución sencilla es configurar la variable de entorno PYTHONPATH para que apunte al directorio raíz del proyecto antes de ejecutar pip. Esto asegurará que Python pueda encontrar el directorio raíz y el archivo CMakeLists.txt incluso después de que pip copie el contenido a un directorio temporal.

    
    export PYTHONPATH=/path/to/foo/python
    

    Solución 2: Usar un gancho de instalación personalizado

    Otra solución es crear un gancho de instalación personalizado que se encargue de copiar el directorio raíz al directorio temporal antes de que pip ejecute setup.py.

    Para crear un gancho de instalación personalizado:

    • Crea un archivo llamado `foo.py` en el directorio raíz del proyecto.
    • Agrega el siguiente código al archivo:
    python
    import os
    import shutil
    
    def install_hook(cmd):
        if cmd == 'install':
            

    Copiar el directorio raíz al directorio temporal

    shutil.copytree('.', '/tmp/foo') if __name__ == '__main__': install_hook(sys.argv[1])
    • Agrega el gancho de instalación al archivo `setup.py`:
    python
    import os
    from setuptools import setup
    
    setup(
        

    ... otros parámetros de configuración ...

    install_requires=['cmake'], entry_points={ 'console_scripts': [ 'foo=foo:install_hook', ], }, )

    Ahora, cuando ejecutes pip install, el gancho de instalación se ejecutará antes de setup.py y copiará el directorio raíz al directorio temporal, resolviendo el problema.

    Nota: Asegúrate de que cmake esté instalado en tu sistema antes de ejecutar pip install.

    Respondida el Dec 20, 2020 a las 09:59 - por Gemini

    Votos positivos: 0 | Votos negativos: 0