Preamble:
Python setuptools are used for the package distribution. I have a Python package (let us call it my_package
), that has several extra_require
packages to it. Everything works just find (installation and build of the package, as well as extras, if were requested), as all extra_require
were python packages themselves and pip correctly resolved everything. A simple pip install my_package
worked like a charm.
Setup:
Now, for one of the extras (let us call it extra1
) I need to call a binary of a non-python library X
.
Module X
itself (source code) was added to the my_package
codebase and was included in the distribution my_package
. Sadly for me, to be utilized, X
needs to be compiled first into a binary on the target machine (C++ implementation; I assume such compilation shall happen on the build stage of my_package
installation). There is a Makefile
in the X
library optimized for different platform compilation, so all that is needed, is to run make
in the respective directory of X
library in the my_package
when the build process is running.
Question #1: how to run a terminal command (i.e., make
in my case) during the build process of the package, using setuptools/distutils?
Question #2: how to ensure, that such terminal command is executed only if the corresponding extra1
is specified during the installation process?
Example:
- If someone runs
pip install my_package
, no such additional compilation of libraryX
shall happen. - If someone runs
pip install my_package [extra1]
, moduleX
needs to be compiled, so the corresponding binary would be created and available on the target machine.
This question came back to haunt me long after I commented on it two years ago! I had almost the same problem myself recently, and I found the documentation VERY scarce, as I think most of you must have experienced. So I tried to research a bit of the source code of setuptools and distutils to see if I could find a more or less standard approach to both the questions you asked.
The first question you asked
has many approaches and all of them involve setting a
cmdclass
when callingsetup
. The parametercmdclass
ofsetup
must be a mapping between command names that will execute depending on the build or install needs of the distribution, and classes that inherit fromdistutils.cmd.Command
base class (as a side note, thesetuptools.command.Command
class is derived fromdistutils
'Command
class so you can derive directly fromsetuptools
implementation).The
cmdclass
allows you to define any command name, like what ayoon did and then execute it specifically when callingpython setup.py --install-option="customcommand"
from the command line. The problem with this, is that it is not the standard command that will be executed when trying to install a package throughpip
or by callingpython setup.py install
. The standard way to approach this is to check what commands willsetup
try to execute in a normal install and then overload that particularcmdclass
.From looking into
setuptools.setup
anddistutils.setup
,setup
will run the commands it found in the command line, which lets assume is just a plaininstall
. In the case ofsetuptools.setup
, this will trigger a series of tests that will see whether to resort to a simple call to thedistutils.install
command class, and if this does not occur, it will attempt to runbdist_egg
. In turn, this command does many things but crucially decides on whether to call thebuild_clib
,build_py
and/or thebuild_ext
commands. Thedistutils.install
simply runsbuild
if necessary which also runsbuild_clib
,build_py
and/orbuild_ext
. This means that regardless of whether you usesetuptools
ordistutils
, if it is necessary to build from source, the commandsbuild_clib
,build_py
, and/orbuild_ext
will be runned, so these are the ones that we will want to overload with thecmdclass
ofsetup
, the question becomes which of the three.build_py
is used to "build" pure python packages, so we can safely ignore it.build_ext
is used to build declared Extension modules that are passed through theext_modules
parameter of the call to thesetup
function. If we wish to overload this class, the main method that builds each extension isbuild_extension
(or here for distutils)build_clib
is used to build declared libraries that are passed through thelibraries
parameter of the call to thesetup
function. In this case, the main method that we should overload with our derived class is thebuild_libraries
method (here fordistutils
).I'll share an example package that builds a toy c static library through a Makefile by using
setuptools
build_ext
command. The approach can be adapted to using thebuild_clib
command, but you'll have to checkout the source code ofbuild_clib.build_libraries
.setup.py
test_pack/__init__.py
test_pack_opt/__init__.py
test_pack_opt/src/Makefile
test_pack_opt/src/test.c
test_pack_opt/src/testlib.c
test_pack_opt/src/testlib.h
In this example, the c library that I want to build using the custom Makefile just has one function which prints
"Hello from testlib_fun!\n"
to stdout. Thetest.c
script is a simple interface between python and this library's single function. The idea is that I tellsetup
that I want to build a c extension namedtest_pack_opt.test_ext
, which only has a single source file: thetest.c
interface script, and I also tell the extension that it must link against the static librarylibtestlib.a
. The main thing is that I overload thebuild_ext
cmdclass usingspecialized_build_ext(build_ext, object)
. The inheritance fromobject
is only necessary if you want to be able to callsuper
to dispatch to parent class methods. Thebuild_extension
method takes anExtension
instance as its second argument, in order to work nice with otherExtension
instances that require the default behavior ofbuild_extension
, I check if this extension has the name of the special one and if it doesn't I call thesuper
'sbuild_extension
method.For the special library, I call the Makefile simply with
subprocess.Popen('make static ...')
. The rest of the command passed to the shell is just to move the static library to a certain default location in which the library should be found to be able to link it to the rest of the compiled extension (which is also just compiled using thesuper
'sbuild_extension
method).As you can imagine there are just sooo many ways in which you could organize this code differently, it does not make sense to list them all. I hope this example serves to illustrate how to call the Makefile, and which
cmdclass
andCommand
derived class you should overload to callmake
in a standard installation.Now, onto question 2.
This was possible with the deprecated
features
parameter ofsetuptools.setup
. The standard way is to try to install the package depending on the requirements that are met.install_requires
lists the mandatory requirements, theextras_requires
lists the optional requirements. For example from thesetuptools
documentationyou could force the installation of the optional required packages by calling
pip install Project-A[PDF]
, but if for some reason the requirements for the'PDF'
named extra were satisfied before hand,pip install Project-A
would end up with the same"Project-A"
functionality. This means that the way in which "Project-A" is installed is not customized for each extra specified at the command line, "Project-A" will always try to install in the same way and may end up with reduced functionality because of unavailable optional requirements.From what I understood, this means that in order to get your module X to be compiled and installed only if [extra1] is specified, you should ship module X as a separate package and depend on it through an
extras_require
. Lets imagine module X will be shipped inmy_package_opt
, your setup formy_package
should look likeWell, I'm sorry that my answer ended up being so long but I hope it helps. Don't hesitate in pointing out any conceptual or naming error, as I mostly tried to deduce this from the
setuptools
source code.Unfortunately, the docs are extremely scarce around the interaction between setup.py and pip, but you should be able to do something like this:
This gives you a hook into running arbitrary code with commands, and also supports a variety of custom option parsing (not demonstrated here).
Put this in a
setup.py
file and try this:pip install --install-option="customcommand" .
Note that this command is executed after the main install sequence, so depending on exactly what you're trying to do, it may not work. See the verbose pip install output: