| # Copyright (c) 2014 The Native Client Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import atexit |
| import os |
| import sys |
| |
| import SCons.Action |
| |
| # This implements a Ninja backend for SCons. This allows SCons to be used |
| # as a Ninja file generator, similar to how Gyp will generate Ninja files. |
| # |
| # This is a way to bypass SCons's slow startup time. After running SCons |
| # to generate a Ninja file (which is fairly slow), you can rebuild targets |
| # quickly using Ninja, as long as the .scons files haven't changed. |
| # |
| # The implementation is fairly hacky: It hooks PRINT_CMD_LINE_FUNC to |
| # discover the build commands that SCons would normally run. |
| # |
| # A cleaner implementation would traverse the node graph instead. |
| # Traversing the node graph is itself straightforward, but finding the |
| # command associated with a node is not -- I couldn't figure out how to do |
| # that. |
| |
| |
| # This is necessary to handle SCons's "variant dir" feature. The filename |
| # associated with a Scons node can be ambiguous: it might come from the |
| # build dir or the source dir. |
| def GetRealNode(node): |
| src = node.srcnode() |
| if src.stat() is not None: |
| return src |
| return node |
| |
| |
| def GenerateNinjaFile(env, dest_file): |
| # Tell SCons not to run any commands, just report what would be run. |
| env.SetOption('no_exec', True) |
| # Tell SCons that everything needs rebuilding. |
| env.Decider(lambda dependency, target, prev_ni: True) |
| |
| # Use a list to ensure that the output is ordered deterministically. |
| node_list = [] |
| node_map = {} |
| |
| def CustomCommandPrinter(cmd, targets, source, env): |
| assert len(targets) == 1, len(targets) |
| node = targets[0] |
| # There can sometimes be multiple commands per target (e.g. ar+ranlib). |
| # We must collect these together to output a single Ninja rule. |
| if node not in node_map: |
| node_list.append(node) |
| node_map.setdefault(node, []).append(cmd) |
| |
| env.Append(PRINT_CMD_LINE_FUNC=CustomCommandPrinter) |
| |
| def WriteFile(): |
| dest_temp = '%s.tmp' % dest_file |
| ninja_fh = open(dest_temp, 'w') |
| ninja_fh.write("""\ |
| # Generated by scons_to_ninja.py |
| |
| # Generic rule for handling any command. |
| rule cmd |
| command = $cmd |
| |
| # NaCl overrides SCons's Install() step to create hard links, for speed. |
| # To coexist with that, we must remove the file before copying, otherwise |
| # cp complains the source and dest "are the same file". We also create |
| # hard links here (with -l) for speed. |
| rule install |
| command = rm -f $out && cp -l $in $out |
| """) |
| |
| for node in node_list: |
| dest_path = node.get_path() |
| cmds = node_map[node] |
| deps = [GetRealNode(dep).get_path() for dep in node.all_children()] |
| |
| action = node.builder.action |
| if type(action) == SCons.Action.FunctionAction: |
| funcname = action.function_name() |
| if funcname == 'installFunc': |
| assert len(deps) == 1, len(deps) |
| ninja_fh.write('\nbuild %s: install %s\n' |
| % (dest_path, ' '.join(deps))) |
| continue |
| else: |
| sys.stderr.write('Unknown FunctionAction, %r: skipping target %r\n' |
| % (funcname, dest_path)) |
| continue |
| |
| ninja_fh.write('\nbuild %s: cmd %s\n' |
| % (dest_path, ' '.join(deps))) |
| ninja_fh.write(' cmd = %s\n' % ' && '.join(cmds)) |
| |
| # Make the result file visible atomically. |
| os.rename(dest_temp, dest_file) |
| |
| atexit.register(WriteFile) |