# -*- coding: utf-8 -*- # Copyright (c) 2015 Zachary Weinberg # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. """Extract compilation commands from Distutils for use in a makefile. Here is an example makefile that builds two modules, mod1 and mod2, using this program: CC = cc CXX = c++ PYTHON = python all: # is the default. include python-vars.mk all: mod1.$M mod2.$M # mod1 is written in C; mod2 is written in C++ mod1.$M: LIBS = -lthis -lthat mod1.$M: foo.$O bar.$O baz.$O $(CC) $(LINKER_ARGS) mod2.$M: quux.$O blurf.$O $(CXX) $(LINKER_ARGS) # Header-file dependencies foo.$O: foo.h bar.h bar.$O: bar.h baz.h baz.$O: baz.h quux.$O: quux.h blurf.h blurf.$O: blurf.h clean: -rm -f mod1.$M mod2.$M foo.$O bar.$O baz.$O quux.$O blurf.$O # Boilerplate; you shouldn't need to change anything below. python-vars.mk: $(PYTHON) get-module-compile-cmds.py $@ %.$O: %.c $(CC) $(COMPILER_ARGS) %.$O: %.cc $(CXX) $(COMPILER_ARGS) .PHONY: all clean The sample code shown above assumes GNU Make, but the output of this program should be usable with any make implementation that supports $<, $^, and $@. Installation is not currently supported. """ from distutils.dist import Distribution from distutils.command.build_ext import build_ext from distutils.log import set_verbosity # io.StringIO exists in 2.7 but doesn't play nice with distutils, so # try the old cStringIO first. try: from cStringIO import StringIO except: from io import StringIO # shlex.quote is the documented API for shell quotation, but only # exists in 3.3 and later. pipes.quote is undocumented but has existed # since 2.0. import shlex try: from shlex import quote as shellquote except: from pipes import quote as shellquote import sys if len(sys.argv) != 2: raise SystemExit("usage: $(PYTHON) {} output-file" .format(sys.argv[0])) # There is no way to get distutils to just _tell_ you what commands # are; you have to run them, in dry-run mode so it doesn't actually do # anything, and capture the echoed command lines. class CaptureStdout: def __init__(self): self.old_stdout = None self.stdout = StringIO() def __enter__(self): self.old_stdout = sys.stdout sys.stdout = self.stdout return self.stdout def __exit__(self, *dontcare): sys.stdout = self.old_stdout self.stdout.close() # What one gets back from the captured stdout needs a little # postprocessing in order to be usable in a Makefile. def munge_command(inputvar, srcext, objextname, cmd): cmd = shlex.split(cmd.strip()) munged = [] # The first thing on the line will be the compiler itself; throw # that out. Find dummy.srcext and dummy.objext, and substitute # appropriate Makefile variable names. Also, determine what objext # actually is. dummy_srcext = "dummy." + srcext objext = None for arg in cmd[1:]: if arg == dummy_srcext: munged.append(inputvar) # either $< or $^, depending elif arg.startswith("dummy."): munged.append("$@") objext = arg[len("dummy."):] else: if shellquote(arg) != arg: raise SystemExit("error: command {!r}: " "cannot put {!r} into a makefile" .format(cmd, arg)) munged.append(arg) if not objext: raise SystemExit("error: command {!r}: failed to determine {}" .format(cmd, objextname)) return " ".join(munged), objext # The easiest way to ensure that we use a properly configured compiler # is to subclass build_ext, because some of the work for that is only # done when build_ext.run() is called, grumble. class stub_build_ext_report: def __init__(self): self.compile_command = None self.link_command = None self.objext = None self.modext = None class stub_build_ext(build_ext): def __init__(self, reporter, *args, **kwargs): self.reporter = reporter build_ext.__init__(self, *args, **kwargs) def build_extensions(self): with CaptureStdout() as cap: self.compiler.compile(["dummy.c"], output_dir="") ccmd = cap.getvalue() ccmd, objext = munge_command("$<", "c", "objext", ccmd) self.reporter.compile_command = ccmd self.reporter.objext = objext with CaptureStdout() as cap: self.compiler.link_shared_object( ["dummy." + objext], self.get_ext_filename("dummy")) lcmd = cap.getvalue() lcmd, modext = munge_command("$^ $(LIBS)", objext, "modext", lcmd) self.reporter.link_command = lcmd self.reporter.modext = modext # The generated Makefile fragment should depend on the physical file for # every Distutils module that has been loaded by this program. def get_fragment_dependencies(): distutils_modules = sorted(m.__file__ for n, m in sys.modules.items() if n.startswith("distutils")) return " \\\n\t".join(distutils_modules) results = stub_build_ext_report() set_verbosity(1) fake_dist = Distribution({"ext_modules": "not empty"}) fake_build_ext = stub_build_ext(results, fake_dist) fake_build_ext.inplace = True fake_build_ext.dry_run = True fake_build_ext.finalize_options() fake_build_ext.run() # Sanity check. if (not results.objext or not results.modext or not results.compile_command or not results.link_command): raise SystemExit("failed to probe compilation environment") with open(sys.argv[1], "w") as f: f.write("""\ O = {objext} M = {modext} COMPILER_ARGS = {compile} LINKER_ARGS = {link} """.format(objext = results.objext, modext = results.modext, compile = results.compile_command, link = results.link_command)) f.write("\n{}: {} \\\n\t{}\n" .format(sys.argv[1], sys.argv[0], get_fragment_dependencies()))