J'écris une extension Python qui relie une bibliothèque C++ et j'utilise cmake pour aider avec le processus de construction. Cela signifie qu'en ce moment, la seule façon dont je sais comment la regrouper, Je dois d'abord les compiler avec cmake avant de pouvoir exécuter setup.py bdist_wheel. Il doit y avoir un meilleur moyen.
Je me demandais s'il était possible (ou quelqu'un a essayé) d'invoquer CMake dans le cadre du processus de construction setup.py ext_modules? Je suppose qu'il existe un moyen de créer une sous-classe de quelque chose, mais je ne sais pas où chercher.
J'utilise CMake car cela me donne beaucoup plus de contrôle pour la construction d'extensions de bibliothèques c et c ++ avec des étapes de construction complexes exactement comme je le veux. De plus, je peux facilement créer des extensions Python directement avec cmake avec la commande PYTHON_ADD_MODULE () dans la commande findPythonLibs.cmake. Je souhaite juste que ce ne soit qu'une étape.
Ce que vous devez essentiellement faire est de remplacer le build_ext
classe de commande dans votre setup.py
et l'enregistrer dans les classes de commandes. Dans votre implément personnalisé de build_ext
, configurez et appelez cmake
pour configurer puis construire les modules d'extension. Malheureusement, les documents officiels sont plutôt laconiques sur la façon d'implémenter des commandes distutils
personnalisées (voir Extending Distutils ); Je trouve qu'il est beaucoup plus utile d'étudier directement le code des commandes. Par exemple, voici le code source de build_ext
commande .
J'ai préparé un projet simple composé d'une seule extension C foo
et d'un module python module spam.eggs
:
so-42585210/
├── spam
│ ├── __init__.py # empty
│ ├── eggs.py
│ ├── foo.c
│ └── foo.h
├── CMakeLists.txt
└── setup.py
Ce ne sont que quelques bouts simples que j'ai écrits pour tester le script de configuration.
spam/eggs.py
(uniquement pour tester les appels de bibliothèque):
from ctypes import cdll
import pathlib
def wrap_bar():
foo = cdll.LoadLibrary(str(pathlib.Path(__file__).with_name('libfoo.dylib')))
return foo.bar()
spam/foo.c
:
#include "foo.h"
int bar() {
return 42;
}
spam/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})
C'est là que la magie opère. Bien sûr, il y a beaucoup de place pour des améliorations - vous pouvez passer des options supplémentaires à la classe CMakeExtension
si vous en avez besoin (pour plus d'informations sur les extensions, voir Construction d'extensions C et C++ ), rendez les options CMake configurables via setup.cfg
en remplaçant les méthodes initialize_options
et finalize_options
etc.
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,
}
)
Construisez la roue du projet, installez-la. Testez la bibliothèque est installée:
$ 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
Exécutez la fonction wrapper à partir de spam.eggs
module:
$ python -c "from spam import eggs; print(eggs.wrap_bar())"
42
Je voudrais ajouter ma propre réponse à cela, comme une sorte d'addendum à ce que décrit le hoefling.
Merci, hoefling, car votre réponse m'a aidé à me mettre sur la bonne voie pour écrire un script d'installation de la même manière pour mon propre référentiel.
La principale motivation pour écrire cette réponse est d'essayer de "coller ensemble" les pièces manquantes. L'OP ne précise pas la nature du C/C++ Python en cours de développement; je tiens à préciser dès le départ que les étapes ci-dessous concernent une chaîne de construction cmake C/C++ crée plusieurs fichiers .dll
/.so
ainsi qu'un fichier *.pyd
/so
précompilé en plus de certains fichiers génériques .py
qui doivent être placé dans le répertoire des scripts.
Tous ces fichiers se concrétisent directement après l'exécution de la commande cmake build ... fun. Il n'y a aucune recommandation pour construire un setup.py de cette façon.
Parce que setup.py implique que vos scripts feront partie de votre package/bibliothèque et que les fichiers .dll
Qui doivent être construits doivent être déclarés via la partie bibliothèques, avec les sources et les répertoires inclus, il y a aucun moyen intuitif de dire à setuptools que les bibliothèques, les scripts et les fichiers de données qui résultent d'un appel à cmake -b
survenu dans build_ext
devraient tous aller à leur place respective. Pire encore si vous souhaitez que ce module soit suivi par setuptools et entièrement désinstallable, ce qui signifie que les utilisateurs peuvent le désinstaller et que chaque trace soit effacée de leur système, si vous le souhaitez.
Le module pour lequel j'écrivais un fichier setup.py est bpy, l'équivalent .pyd
/.so
De la construction de blender en tant que module python comme décrit ici:
https://wiki.blender.org/wiki//User:Ideasman42/BlenderAsPyModule (meilleures instructions mais lien maintenant mort) http://www.gizmoplex.com/wordpress/compile -blender-as-python-module / (peut-être des instructions pires mais semble toujours en ligne)
Vous pouvez consulter mon référentiel sur github ici:
https://github.com/TylerGubala/blenderpy
C'est ma motivation derrière la rédaction de cette réponse, et j'espère que cela aidera toute autre personne essayant d'accomplir quelque chose de similaire plutôt que de jeter sa chaîne de construction cmake ou, pire encore, d'avoir à maintenir deux environnements de construction distincts. Je m'excuse si c'est hors sujet.
Étendez la classe setuptools.Extension
Avec une classe à moi, qui ne contient pas d'entrées pour les propriétés sources ou libs
Étendez la classe setuptools.commands.build_ext.build_ext
Avec une classe à moi, qui a une méthode personnalisée qui exécute mes étapes de construction nécessaires (git, svn, cmake, cmake --build)
Étendez la classe distutils.command.install_data.install_data
(Beurk, distutils
... mais il ne semble pas y avoir d'équivalent setuputils) avec une classe à moi, pour marquer les bibliothèques binaires construites pendant l'enregistrement de setuptools création (installed-files.txt) telle que
Les bibliothèques seront enregistrées et seront désinstallées avec pip uninstall package_name
La commande py setup.py bdist_wheel
Fonctionnera également en mode natif et peut être utilisée pour fournir des versions précompilées de votre code source
Étendez la classe setuptools.command.install_lib.install_lib
Avec une classe à moi, ce qui garantira que les bibliothèques construites seront déplacées de leur dossier de construction résultant vers le dossier dans lequel setuptools les attend (sous Windows, il mettra le .dll
fichiers dans un dossier bin/Release et pas là où setuptools l'attend)
Étendez la classe setuptools.command.install_scripts.install_scripts
Avec une classe à moi de sorte que les fichiers de scripts soient copiés dans le bon répertoire (Blender s'attend à ce que le 2.79 ou tout autre répertoire soit à l'emplacement des scripts)
Une fois les étapes de génération effectuées, copiez ces fichiers dans un répertoire connu que setuptools copiera dans le répertoire site-packages de mon environnement. À ce stade, les classes setuptools et distutils restantes peuvent reprendre l'écriture de l'enregistrement installed-files.txt et seront entièrement amovibles!
Voici un échantillon, plus ou moins de mon référentiel, mais taillé pour plus de clarté sur les choses plus spécifiques (vous pouvez toujours vous diriger vers le repo et le regarder par vous-même)
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
PACKAGE_NAME = "example"
class CMakeExtension(Extension):
"""
An extension to run the cmake build
This simply overrides the base extension class so that setuptools
doesn't try to build your sources for you
"""
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
Listing the installed files in the Egg-info guarantees that
all of the package files will be uninstalled when the user
uninstalls your package through pip
"""
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
# Depending on the files that are generated from your cmake
# build chain, you may need to change the below code, such that
# your files are moved to the appropriate location when the installation
# is run
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?
# These are the additional installation files that should be
# included in the package, but are resultant of the cmake build
# step; depending on the files that are generated from your cmake
# build chain, you may need to modify the below code
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 == 'example_extension':
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)
# Change your cmake arguments below as necessary
# Below is just an example set of arguments for building Blender as a Python module
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, _pyd) for _pyd in
os.listdir(bin_dir) if
os.path.isfile(os.path.join(bin_dir, _pyd)) and
os.path.splitext(_pyd)[0].startswith(PACKAGE_NAME) and
os.path.splitext(_pyd)[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="example_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
}
)
Une fois le setup.py
Créé de cette façon, la construction du module python est aussi simple que d'exécuter py setup.py
, Qui exécutera la construction et produira les fichiers de sortie.
Il est recommandé de produire une roue pour les utilisateurs sur Internet lent ou qui ne veulent pas construire à partir de sources. Pour ce faire, vous souhaiterez installer le package wheel
(py -m pip install wheel
) Et produire une distribution de roue en effectuant py setup.py bdist_wheel
, Puis le télécharger à l'aide de twine
comme tout autre package.