From 7b5bf7f129f86e975fadd9b4c783c55d2847cd06 Mon Sep 17 00:00:00 2001 From: seelabs Date: Fri, 29 May 2015 06:54:39 -0700 Subject: [PATCH] Add support for scons ninja backend --- SConstruct | 44 +++++++++++++ src/ripple/site_scons/scons_to_ninja.py | 85 +++++++++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 src/ripple/site_scons/scons_to_ninja.py diff --git a/SConstruct b/SConstruct index 11b7d1a6d4..4361918b4b 100644 --- a/SConstruct +++ b/SConstruct @@ -60,6 +60,11 @@ The following environment variables modify the build environment: OPENSSL_ROOT Path to the openssl directory. +The following extra options may be used: + --ninja Generate a `build.ninja` build file for the specified target + (see: https://martine.github.io/ninja/). Only gcc and clang targets + are supported. + ''' # ''' @@ -83,11 +88,16 @@ import time import SCons.Action sys.path.append(os.path.join('src', 'beast', 'site_scons')) +sys.path.append(os.path.join('src', 'ripple', 'site_scons')) import Beast +import scons_to_ninja #------------------------------------------------------------------------------ +AddOption('--ninja', dest='ninja', action='store_true', + help='generate ninja build file build.ninja') + def parse_time(t): return time.strptime(t, '%a %b %d %H:%M:%S %Z %Y') @@ -591,6 +601,7 @@ class ObjectBuilder(object): self.env = env self.variant_dirs = variant_dirs self.objects = [] + self.child_envs = [] def add_source_files(self, *filenames, **kwds): for filename in filenames: @@ -598,6 +609,7 @@ class ObjectBuilder(object): if kwds: env = env.Clone() env.Prepend(**kwds) + self.child_envs.append(env) o = env.Object(Beast.variantFile(filename, self.variant_dirs)) self.objects.append(o) @@ -733,6 +745,31 @@ def should_prepare_targets(style, toolchain, variant): if should_prepare_target(t, style, toolchain, variant): return True +def should_build_ninja(style, toolchain, variant): + """ + Return True if a ninja build file should be generated. + + Typically, scons will be called as follows to generate a ninja build file: + `scons ninja=1 gcc.debug` where `gcc.debug` may be replaced with any of our + non-visual studio targets. Raise an exception if we cannot generate the + requested ninja build file (for example, if multiple targets are requested). + """ + if not GetOption('ninja'): + return False + if len(COMMAND_LINE_TARGETS) != 1: + raise Exception('Can only generate a ninja file for a single target') + cl_target = COMMAND_LINE_TARGETS[0] + if 'vcxproj' in cl_target: + raise Exception('Cannot generate a ninja file for a vcxproj') + s = cl_target.split('.') + if ( style == 'unity' and 'nounity' in s or + style == 'classic' and 'nounity' not in s or + len(s) == 1 ): + return False + if len(s) == 2 or len(s) == 3: + return s[0] == toolchain and s[1] == variant + return False + for tu_style in ['classic', 'unity']: if tu_style == 'classic': sources = get_classic_sources() @@ -861,6 +898,13 @@ for tu_style in ['classic', 'unity']: aliases[variant].extend(target) env.Alias(variant_name, target) + # ninja support + if should_build_ninja(tu_style, toolchain, variant): + print('Generating ninja: {}:{}:{}'.format(tu_style, toolchain, variant)) + scons_to_ninja.GenerateNinjaFile( + [object_builder.env] + object_builder.child_envs, + dest_file='build.ninja') + for key, value in aliases.iteritems(): env.Alias(key, value) diff --git a/src/ripple/site_scons/scons_to_ninja.py b/src/ripple/site_scons/scons_to_ninja.py new file mode 100644 index 0000000000..17eeb13755 --- /dev/null +++ b/src/ripple/site_scons/scons_to_ninja.py @@ -0,0 +1,85 @@ +# 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(envs, dest_file): + # Tell SCons not to run any commands, just report what would be run. + for e in envs: + e.SetOption('no_exec', True) + # Tell SCons that everything needs rebuilding. + e.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) + for e in envs: + e.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)