It's normal to extend python with C/C++. I have the need to extend with C#.
It works well to call C# libraries with the pythonnet package.
What I'm struggling with now is to create a python package that contains the C# binaries. Specifically it's setuptools that I'm unsure about.
I'm used to building my packages with:
python setup.py bdist_wheel
I have created a "command" in setup.py that can publish the C# binaries. It can be invoked with
python setup.py publish_dotnet
But how do I combine them? I could do:
python setup.py publish_dotnet python setup.py bdist_wheel
I.e. running two commands and having bdist_wheel assume the publish_dotnet command has been run. But I'm hoping there is a way to boil it down to running one command that both builds the dotnet code and package it into a wheel.
For reference, here's a setup.py that adds arbitrary files to the wheel:
from setuptools import setup, find_namespace_packages
setup(
packages=find_namespace_packages(where="src"),
package_dir={"": "src"},
package_data={
"mypkg1.data": ["*.*"],
}
)Here's the setup.py that builds the dotnet code:
import os
import shutil
import distutils
from setuptools import Command, find_packages, setup
from setuptools.command.build import build
class publish_dotnet(Command):
user_options = [
("dotnet-config=", None, "dotnet build configuration"),
(
"inplace",
"i",
"ignore build-lib and put compiled extensions into the source " + "directory alongside your pure Python modules",
),
]
def initialize_options(self):
self.announce("publish_dotnet.initialize_options", level=distutils.log.INFO)
self.dotnet_config = None
self.build_lib = None
self.inplace = False
def finalize_options(self):
self.announce("publish_dotnet.finalize_options", level=distutils.log.INFO)
if self.dotnet_config is None:
self.dotnet_config = "release"
build = self.distribution.get_command_obj("build")
build.ensure_finalized()
if self.inplace:
self.build_lib = "."
else:
self.build_lib = build.build_lib
def run(self):
dotnet_publish_dir = "dotnet_published_binaries"
self.announce("deleting old artifacts...", level=distutils.log.INFO)
if os.path.isdir(dotnet_publish_dir):
shutil.rmtree(dotnet_publish_dir)
self.announce("publishing...", level=distutils.log.INFO)
cmd = ["dotnet", "publish", "-o", dotnet_publish_dir, r"src\csharp\ClassLibrary1\ClassLibrary1.csproj"]
self.spawn(cmd)
setup(
cmdclass={"publish_dotnet": publish_dotnet},
)Note: The solution below only works when installing a source distribution zip or tarball, or installing in editable mode from a source tree. It will not work when installing from a binary wheel (.whl)
This solution is more transparent:
You will make a few additions to setup.py and there is no need for an extra file.
Also you need to consider two different post-installations; one for development/editable mode and the other one for install mode.
Add these two classes that includes your post-install script to setup.py:
from setuptools import setup
from setuptools.command.develop import develop
from setuptools.command.install import install
class PostDevelopCommand(develop):
"""Post-installation for development mode."""
def run(self):
develop.run(self)
# PUT YOUR POST-INSTALL SCRIPT HERE or CALL A FUNCTION
class PostInstallCommand(install):
"""Post-installation for installation mode."""
def run(self):
install.run(self)
# PUT YOUR POST-INSTALL SCRIPT HERE or CALL A FUNCTION
and insert cmdclass argument to setup() function in setup.py:
setup(
...
cmdclass={
'develop': PostDevelopCommand,
'install': PostInstallCommand,
},
...
)
You can even call shell commands during installation, like in this example which does pre-installation preparation:
from setuptools import setup
from setuptools.command.develop import develop
from setuptools.command.install import install
from subprocess import check_call
class PreDevelopCommand(develop):
"""Pre-installation for development mode."""
def run(self):
check_call("apt-get install this-package".split())
develop.run(self)
class PreInstallCommand(install):
"""Pre-installation for installation mode."""
def run(self):
check_call("apt-get install this-package".split())
install.run(self)
setup(
...
P.S. there are no any pre-install entry points available on setuptools. Read this discussion if you are wondering why there is none.
Note: The solution below only works when installing a source distribution zip or tarball, or installing in editable mode from a source tree. It will not work when installing from a binary wheel (.whl)
This is the only strategy that has worked for me when the post-install script requires that the package dependencies have already been installed:
import atexit
from setuptools.command.install import install
def _post_install():
print('POST INSTALL')
class new_install(install):
def __init__(self, *args, **kwargs):
super(new_install, self).__init__(*args, **kwargs)
atexit.register(_post_install)
setuptools.setup(
cmdclass={'install': new_install},