Welcome, guest | Sign In | My Account | Store | Cart
# -*- 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()))

History