Although this happens quite late, here is a suggestion for a solution.
Basically, this is just a subclassification of the distutils ' sdist with the addition of user logic and its registration in the configuration function. Unfortunately, the official documentation for this topic is a bit vague and concise ; The Distutils extension provides at least a tiny example to get you started. I was much better off reading the module code in distutils.command to see how the actual commands are implemented.
To execute an arbitrary command, you can use the distutils.cmd.Command::spawn method, which executes the input string passed, raising a DistutilsExecError if the command exit code is non-zero:
from distutils.command.sdist import sdist as sdist_orig from distutils.errors import DistutilsExecError from setuptools import setup class sdist(sdist_orig): def run(self): try: self.spawn(['ls', '-l']) except DistutilsExecError: self.warn('listing directory failed') super().run() setup(name='spam', version='0.1', packages=[], cmdclass={ 'sdist': sdist } )
Running the setup script above gives:
$ python setup.py sdist running sdist ls -l total 24 -rw-r--r-- 1 hoefling staff 52 23 Dez 19:06 MANIFEST drwxr-xr-x 3 hoefling staff 96 23 Dez 19:06 dist -rw-r--r-- 1 hoefling staff 484 23 Dez 19:07 setup.py running check ... writing manifest file 'MANIFEST' creating spam-0.1 making hard links in spam-0.1... hard linking setup.py -> spam-0.1 Creating tar archive removing 'spam-0.1' (and everything under it)
Team reuse
Here is a (albeit simplified) real-life example of the team that we use in our projects, which is used around NodeJS projects and calls yarn :
import distutils import os import pathlib import setuptools _YARN_CMD_SEP = ';' _HELP_MSG_SUBCMD = ( 'yarn subcommands to execute (separated ' 'by {})'.format(_YARN_CMD_SEP) ) _HELP_MSG_PREFIX = ( 'path to directory containing package.json. ' 'If not set, current directory is assumed.' ) class yarn(setuptools.Command): description = ('runs yarn commands. Assumes yarn is ' 'already installed by the user.') user_options = [ ('subcommands=', None, _HELP_MSG_SUBCMD), ('prefix=', None, _HELP_MSG_PREFIX), ] def initialize_options(self) -> None: self.subcommands = [] self.prefix = None
Usage example:
$ python setup.py yarn --prefix=. --subcommands="add leftpad; remove leftpad" running yarn running yarn add leftpad ... yarn add leftpad yarn add v1.3.2 warning package.json: No license field warning No license field [1/4] 🔍 Resolving packages... [2/4] 🚚 Fetching packages... [3/4] 🔗 Linking dependencies... [4/4] 📃 Building fresh packages... success Saved lockfile. success Saved 1 new dependency. └─ leftpad@0.0.1 warning No license field ✨ Done in 0.33s. running yarn remove leftpad ... yarn remove leftpad yarn remove v1.3.2 warning package.json: No license field [1/2] Removing module leftpad... [2/2] Regenerating lockfile and installing missing dependencies... warning No license field success Uninstalled packages. ✨ Done in 0.13s.
You can also use yarn in your command chain like every other command: python setup.py yarn test sdist , etc.