blob: 10e67f37620e8c85ec1ed732af0b9b36e21d94f0 [file] [log] [blame]
# 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)