summaryrefslogtreecommitdiff
path: root/site_scons
diff options
context:
space:
mode:
authorMathew Robinson <mathew@chasinglogic.io>2020-02-20 15:12:21 -0500
committerEvergreen Agent <no-reply@evergreen.mongodb.com>2020-02-21 19:08:36 +0000
commitdbba9fcb5caec13f8032251d783e670b5af5bc0d (patch)
tree33ecc997c99d21a8130aaf7adb1a66f6965d1c38 /site_scons
parent26fd0045191ca641148c6e4d407c404cf9d7876a (diff)
downloadmongo-dbba9fcb5caec13f8032251d783e670b5af5bc0d.tar.gz
SERVER-46282 Do A/B releases of Ninja
Diffstat (limited to 'site_scons')
-rw-r--r--site_scons/site_tools/ninja_next.py1407
1 files changed, 1407 insertions, 0 deletions
diff --git a/site_scons/site_tools/ninja_next.py b/site_scons/site_tools/ninja_next.py
new file mode 100644
index 00000000000..a865cc6dee3
--- /dev/null
+++ b/site_scons/site_tools/ninja_next.py
@@ -0,0 +1,1407 @@
+# Copyright 2019 MongoDB Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Generate build.ninja files from SCons aliases."""
+
+import sys
+import os
+import importlib
+import io
+import shutil
+import shlex
+
+from glob import glob
+from os.path import join as joinpath
+from os.path import splitext
+
+import SCons
+from SCons.Action import _string_from_cmd_list, get_default_ENV
+from SCons.Util import is_List, flatten_sequence
+from SCons.Script import COMMAND_LINE_TARGETS
+
+NINJA_SYNTAX = "NINJA_SYNTAX"
+NINJA_RULES = "__NINJA_CUSTOM_RULES"
+NINJA_POOLS = "__NINJA_CUSTOM_POOLS"
+NINJA_CUSTOM_HANDLERS = "__NINJA_CUSTOM_HANDLERS"
+NINJA_BUILD = "NINJA_BUILD"
+NINJA_WHEREIS_MEMO = {}
+NINJA_STAT_MEMO = {}
+
+__NINJA_RULE_MAPPING = {}
+
+# These are the types that get_command can do something with
+COMMAND_TYPES = (
+ SCons.Action.CommandAction,
+ SCons.Action.CommandGeneratorAction,
+)
+
+
+def _install_action_function(_env, node):
+ """Install files using the install or copy commands"""
+ return {
+ "outputs": get_outputs(node),
+ "rule": "INSTALL",
+ "inputs": [get_path(src_file(s)) for s in node.sources],
+ "implicit": get_dependencies(node),
+ }
+
+
+def _lib_symlink_action_function(_env, node):
+ """Create shared object symlinks if any need to be created"""
+ symlinks = getattr(getattr(node, "attributes", None), "shliblinks", None)
+
+ if not symlinks or symlinks is None:
+ return None
+
+ outputs = [link.get_dir().rel_path(linktgt) for link, linktgt in symlinks]
+ inputs = [link.get_path() for link, _ in symlinks]
+
+ return {
+ "outputs": outputs,
+ "inputs": inputs,
+ "rule": "SYMLINK",
+ "implicit": get_dependencies(node),
+ }
+
+
+def is_valid_dependent_node(node):
+ """
+ Return True if node is not an alias or is an alias that has children
+
+ This prevents us from making phony targets that depend on other
+ phony targets that will never have an associated ninja build
+ target.
+
+ We also have to specify that it's an alias when doing the builder
+ check because some nodes (like src files) won't have builders but
+ are valid implicit dependencies.
+ """
+ return not isinstance(node, SCons.Node.Alias.Alias) or node.children()
+
+
+def alias_to_ninja_build(node):
+ """Convert an Alias node into a Ninja phony target"""
+ return {
+ "outputs": get_outputs(node),
+ "rule": "phony",
+ "implicit": [
+ get_path(n) for n in node.children() if is_valid_dependent_node(n)
+ ],
+ }
+
+
+def get_dependencies(node):
+ """Return a list of dependencies for node."""
+ return [get_path(src_file(child)) for child in node.children()]
+
+
+def get_inputs(node):
+ """Collect the Ninja inputs for node."""
+ executor = node.get_executor()
+ if executor is not None:
+ inputs = executor.get_all_sources()
+ else:
+ inputs = node.sources
+
+ inputs = [get_path(src_file(o)) for o in inputs]
+ return inputs
+
+
+def get_outputs(node):
+ """Collect the Ninja outputs for node."""
+ executor = node.get_executor()
+ if executor is not None:
+ outputs = executor.get_all_targets()
+ else:
+ if hasattr(node, "target_peers"):
+ outputs = node.target_peers
+ else:
+ outputs = [node]
+
+ outputs = [get_path(o) for o in outputs]
+ return outputs
+
+
+class SConsToNinjaTranslator:
+ """Translates SCons Actions into Ninja build objects."""
+
+ def __init__(self, env):
+ self.env = env
+ self.func_handlers = {
+ # Skip conftest builders
+ "_createSource": ninja_noop,
+ # SCons has a custom FunctionAction that just makes sure the
+ # target isn't static. We let the commands that ninja runs do
+ # this check for us.
+ "SharedFlagChecker": ninja_noop,
+ # The install builder is implemented as a function action.
+ "installFunc": _install_action_function,
+ "LibSymlinksActionFunction": _lib_symlink_action_function,
+ }
+
+ self.func_handlers.update(self.env[NINJA_CUSTOM_HANDLERS])
+
+ # pylint: disable=too-many-return-statements
+ def action_to_ninja_build(self, node, action=None):
+ """Generate build arguments dictionary for node."""
+ # Use False since None is a valid value for this Attribute
+ build = getattr(node.attributes, NINJA_BUILD, False)
+ if build is not False:
+ return build
+
+ if node.builder is None:
+ return None
+
+ if action is None:
+ action = node.builder.action
+
+ # Ideally this should never happen, and we do try to filter
+ # Ninja builders out of being sources of ninja builders but I
+ # can't fix every DAG problem so we just skip ninja_builders
+ # if we find one
+ if node.builder == self.env["BUILDERS"]["Ninja"]:
+ return None
+
+ if isinstance(action, SCons.Action.FunctionAction):
+ return self.handle_func_action(node, action)
+
+ if isinstance(action, SCons.Action.LazyAction):
+ # pylint: disable=protected-access
+ action = action._generate_cache(node.env if node.env else self.env)
+ return self.action_to_ninja_build(node, action=action)
+
+ if isinstance(action, SCons.Action.ListAction):
+ return self.handle_list_action(node, action)
+
+ if isinstance(action, COMMAND_TYPES):
+ return get_command(node.env if node.env else self.env, node, action)
+
+ raise Exception("Got an unbuildable ListAction for: {}".format(str(node)))
+
+ def handle_func_action(self, node, action):
+ """Determine how to handle the function action."""
+ name = action.function_name()
+ # This is the name given by the Subst/Textfile builders. So return the
+ # node to indicate that SCons is required
+ if name == "_action":
+ return {
+ "rule": "TEMPLATE",
+ "outputs": get_outputs(node),
+ "implicit": get_dependencies(node),
+ }
+
+ handler = self.func_handlers.get(name, None)
+ if handler is not None:
+ return handler(node.env if node.env else self.env, node)
+
+ raise Exception(
+ "Found unhandled function action {}, "
+ " generating scons command to build\n"
+ "Note: this is less efficient than Ninja,"
+ " you can write your own ninja build generator for"
+ " this function using NinjaRegisterFunctionHandler".format(name)
+ )
+
+ # pylint: disable=too-many-branches
+ def handle_list_action(self, node, action):
+ """TODO write this comment"""
+ results = [
+ self.action_to_ninja_build(node, action=act)
+ for act in action.list
+ if act is not None
+ ]
+ results = [
+ result for result in results if result is not None and result["outputs"]
+ ]
+ if not results:
+ return None
+
+ # No need to process the results if we only got a single result
+ if len(results) == 1:
+ return results[0]
+
+ all_outputs = list({output for build in results for output in build["outputs"]})
+ dependencies = list({dep for build in results for dep in build["implicit"]})
+
+ if results[0]["rule"] == "CMD":
+ cmdline = ""
+ for cmd in results:
+
+ # Occasionally a command line will expand to a
+ # whitespace only string (i.e. ' '). Which is not a
+ # valid command but does not trigger the empty command
+ # condition if not cmdstr. So here we strip preceding
+ # and proceeding whitespace to make strings like the
+ # above become empty strings and so will be skipped.
+ cmdstr = cmd["variables"]["cmd"].strip()
+ if not cmdstr:
+ continue
+
+ # Skip duplicate commands
+ if cmdstr in cmdline:
+ continue
+
+ if cmdline:
+ cmdline += " && "
+
+ cmdline += cmdstr
+
+ # Remove all preceding and proceeding whitespace
+ cmdline = cmdline.strip()
+
+ # Make sure we didn't generate an empty cmdline
+ if cmdline:
+ ninja_build = {
+ "outputs": all_outputs,
+ "rule": "CMD",
+ "variables": {
+ "cmd": cmdline,
+ "env": get_command_env(node.env if node.env else self.env),
+ },
+ "implicit": dependencies,
+ }
+
+ if node.env and node.env.get("NINJA_POOL", None) is not None:
+ ninja_build["pool"] = node.env["pool"]
+
+ return ninja_build
+
+ elif results[0]["rule"] == "phony":
+ return {
+ "outputs": all_outputs,
+ "rule": "phony",
+ "implicit": dependencies,
+ }
+
+ elif results[0]["rule"] == "INSTALL":
+ return {
+ "outputs": all_outputs,
+ "rule": "INSTALL",
+ "inputs": [get_path(src_file(s)) for s in node.sources],
+ "implicit": dependencies,
+ }
+
+ raise Exception("Unhandled list action with rule: " + results[0]["rule"])
+
+
+# pylint: disable=too-many-instance-attributes
+class NinjaState:
+ """Maintains state of Ninja build system as it's translated from SCons."""
+
+ def __init__(self, env, writer_class):
+ self.env = env
+ self.writer_class = writer_class
+ self.__generated = False
+ self.translator = SConsToNinjaTranslator(env)
+ self.generated_suffixes = env.get("NINJA_GENERATED_SOURCE_SUFFIXES", [])
+
+ # List of generated builds that will be written at a later stage
+ self.builds = list()
+
+ # List of targets for which we have generated a build. This
+ # allows us to take multiple Alias nodes as sources and to not
+ # fail to build if they have overlapping targets.
+ self.built = set()
+
+ # SCons sets this variable to a function which knows how to do
+ # shell quoting on whatever platform it's run on. Here we use it
+ # to make the SCONS_INVOCATION variable properly quoted for things
+ # like CCFLAGS
+ escape = env.get("ESCAPE", lambda x: x)
+
+ self.variables = {
+ "COPY": "cmd.exe /c 1>NUL copy" if sys.platform == "win32" else "cp",
+ "SCONS_INVOCATION": "{} {} __NINJA_NO=1 $out".format(
+ sys.executable,
+ " ".join(
+ [escape(arg) for arg in sys.argv if arg not in COMMAND_LINE_TARGETS]
+ ),
+ ),
+ "SCONS_INVOCATION_W_TARGETS": "{} {}".format(
+ sys.executable, " ".join([escape(arg) for arg in sys.argv])
+ ),
+ # This must be set to a global default per:
+ # https://ninja-build.org/manual.html
+ #
+ # (The deps section)
+ "msvc_deps_prefix": "Note: including file:",
+ }
+
+ self.rules = {
+ "CMD": {
+ "command": "cmd /c $env$cmd" if sys.platform == "win32" else "$env$cmd",
+ "description": "Building $out",
+ "pool": "local_pool",
+ },
+ # We add the deps processing variables to this below. We
+ # don't pipe these through cmd.exe on Windows because we
+ # use this to generate a compile_commands.json database
+ # which can't use the shell command as it's compile
+ # command.
+ "CC": {
+ "command": "$env$CC @$out.rsp",
+ "description": "Compiling $out",
+ "rspfile": "$out.rsp",
+ "rspfile_content": "$rspc",
+ },
+ "CXX": {
+ "command": "$env$CXX @$out.rsp",
+ "description": "Compiling $out",
+ "rspfile": "$out.rsp",
+ "rspfile_content": "$rspc",
+ },
+ "LINK": {
+ "command": "$env$LINK @$out.rsp",
+ "description": "Linking $out",
+ "rspfile": "$out.rsp",
+ "rspfile_content": "$rspc",
+ "pool": "local_pool",
+ },
+ "AR": {
+ "command": "$env$AR @$out.rsp",
+ "description": "Archiving $out",
+ "rspfile": "$out.rsp",
+ "rspfile_content": "$rspc",
+ "pool": "local_pool",
+ },
+ "SYMLINK": {
+ "command": (
+ "cmd /c mklink $out $in"
+ if sys.platform == "win32"
+ else "ln -s $in $out"
+ ),
+ "description": "Symlink $in -> $out",
+ },
+ "INSTALL": {
+ "command": "$COPY $in $out",
+ "description": "Install $out",
+ "pool": "install_pool",
+ # On Windows cmd.exe /c copy does not always correctly
+ # update the timestamp on the output file. This leads
+ # to a stuck constant timestamp in the Ninja database
+ # and needless rebuilds.
+ #
+ # Adding restat here ensures that Ninja always checks
+ # the copy updated the timestamp and that Ninja has
+ # the correct information.
+ "restat": 1,
+ },
+ "TEMPLATE": {
+ "command": "$SCONS_INVOCATION $out",
+ "description": "Rendering $out",
+ "pool": "scons_pool",
+ "restat": 1,
+ },
+ "SCONS": {
+ "command": "$SCONS_INVOCATION $out",
+ "description": "SCons $out",
+ "pool": "scons_pool",
+ # restat
+ # if present, causes Ninja to re-stat the command's outputs
+ # after execution of the command. Each output whose
+ # modification time the command did not change will be
+ # treated as though it had never needed to be built. This
+ # may cause the output's reverse dependencies to be removed
+ # from the list of pending build actions.
+ #
+ # We use restat any time we execute SCons because
+ # SCons calls in Ninja typically create multiple
+ # targets. But since SCons is doing it's own up to
+ # date-ness checks it may only update say one of
+ # them. Restat will find out which of the multiple
+ # build targets did actually change then only rebuild
+ # those targets which depend specifically on that
+ # output.
+ "restat": 1,
+ },
+ "REGENERATE": {
+ "command": "$SCONS_INVOCATION_W_TARGETS",
+ "description": "Regenerating $out",
+ "generator": 1,
+ # Console pool restricts to 1 job running at a time,
+ # it additionally has some special handling about
+ # passing stdin, stdout, etc to process in this pool
+ # that we need for SCons to behave correctly when
+ # regenerating Ninja
+ "pool": "console",
+ # Again we restat in case Ninja thought the
+ # build.ninja should be regenerated but SCons knew
+ # better.
+ "restat": 1,
+ },
+ }
+
+ self.pools = {
+ "local_pool": self.env.GetOption("num_jobs"),
+ "install_pool": self.env.GetOption("num_jobs") / 2,
+ "scons_pool": 1,
+ }
+
+ for rule in ["CC", "CXX"]:
+ if env["PLATFORM"] == "win32":
+ self.rules[rule]["deps"] = "msvc"
+ else:
+ self.rules[rule]["deps"] = "gcc"
+ self.rules[rule]["depfile"] = "$out.d"
+
+ self.rules.update(env.get(NINJA_RULES, {}))
+ self.pools.update(env.get(NINJA_POOLS, {}))
+
+ def generate_builds(self, node):
+ """Generate a ninja build rule for node and it's children."""
+ # Filter out nodes with no builder. They are likely source files
+ # and so no work needs to be done, it will be used in the
+ # generation for some real target.
+ #
+ # Note that all nodes have a builder attribute but it is sometimes
+ # set to None. So we cannot use a simpler hasattr check here.
+ if getattr(node, "builder", None) is None:
+ return
+
+ stack = [[node]]
+ while stack:
+ frame = stack.pop()
+ for child in frame:
+ outputs = set(get_outputs(child))
+ # Check if all the outputs are in self.built, if they
+ # are we've already seen this node and it's children.
+ if not outputs.isdisjoint(self.built):
+ continue
+
+ self.built = self.built.union(outputs)
+ stack.append(child.children())
+
+ if isinstance(child, SCons.Node.Alias.Alias):
+ build = alias_to_ninja_build(child)
+ elif node.builder is not None:
+ # Use False since None is a valid value for this attribute
+ build = getattr(child.attributes, NINJA_BUILD, False)
+ if build is False:
+ build = self.translator.action_to_ninja_build(child)
+ else:
+ build = None
+
+ # Some things are unbuild-able or need not be built in Ninja
+ if build is None or build == 0:
+ continue
+
+ self.builds.append(build)
+
+ def is_generated_source(self, output):
+ """Check if output ends with a known generated suffix."""
+ _, suffix = splitext(output)
+ return suffix in self.generated_suffixes
+
+ def has_generated_sources(self, output):
+ """
+ Determine if output indicates this is a generated header file.
+ """
+ for generated in output:
+ if self.is_generated_source(generated):
+ return True
+ return False
+
+ # pylint: disable=too-many-branches,too-many-locals
+ def generate(self, ninja_file, fallback_default_target=None):
+ """
+ Generate the build.ninja.
+
+ This should only be called once for the lifetime of this object.
+ """
+ if self.__generated:
+ return
+
+ content = io.StringIO()
+ ninja = self.writer_class(content, width=100)
+
+ ninja.comment("Generated by scons. DO NOT EDIT.")
+
+ for pool_name, size in self.pools.items():
+ ninja.pool(pool_name, size)
+
+ for var, val in self.variables.items():
+ ninja.variable(var, val)
+
+ for rule, kwargs in self.rules.items():
+ ninja.rule(rule, **kwargs)
+
+ generated_source_files = {
+ output
+ # First find builds which have header files in their outputs.
+ for build in self.builds
+ if self.has_generated_sources(build["outputs"])
+ for output in build["outputs"]
+ # Collect only the header files from the builds with them
+ # in their output. We do this because is_generated_source
+ # returns True if it finds a header in any of the outputs,
+ # here we need to filter so we only have the headers and
+ # not the other outputs.
+ if self.is_generated_source(output)
+ }
+
+ if generated_source_files:
+ ninja.build(
+ outputs="_generated_sources",
+ rule="phony",
+ implicit=list(generated_source_files),
+ )
+
+ template_builders = []
+
+ for build in self.builds:
+ if build["rule"] == "TEMPLATE":
+ template_builders.append(build)
+ continue
+
+ implicit = build.get("implicit", [])
+ implicit.append(ninja_file)
+ build["implicit"] = implicit
+
+ # Don't make generated sources depend on each other. We
+ # have to check that none of the outputs are generated
+ # sources and none of the direct implicit dependencies are
+ # generated sources or else we will create a dependency
+ # cycle.
+ if (
+ generated_source_files
+ and not build["rule"] == "INSTALL"
+ and set(build["outputs"]).isdisjoint(generated_source_files)
+ and set(implicit).isdisjoint(generated_source_files)
+ ):
+
+ # Make all non-generated source targets depend on
+ # _generated_sources. We use order_only for generated
+ # sources so that we don't rebuild the world if one
+ # generated source was rebuilt. We just need to make
+ # sure that all of these sources are generated before
+ # other builds.
+ build["order_only"] = "_generated_sources"
+
+ # When using a depfile Ninja can only have a single output
+ # but SCons will usually have emitted an output for every
+ # thing a command will create because it's caching is much
+ # more complex than Ninja's. This includes things like DWO
+ # files. Here we make sure that Ninja only ever sees one
+ # target when using a depfile. It will still have a command
+ # that will create all of the outputs but most targets don't
+ # depend direclty on DWO files and so this assumption is safe
+ # to make.
+ rule = self.rules.get(build["rule"])
+
+ # Some rules like 'phony' and other builtins we don't have
+ # listed in self.rules so verify that we got a result
+ # before trying to check if it has a deps key.
+ #
+ # Anything using deps or rspfile in Ninja can only have a single
+ # output, but we may have a build which actually produces
+ # multiple outputs which other targets can depend on. Here we
+ # slice up the outputs so we have a single output which we will
+ # use for the "real" builder and multiple phony targets that
+ # match the file names of the remaining outputs. This way any
+ # build can depend on any output from any build.
+ if rule is not None and (rule.get("deps") or rule.get("rspfile")):
+ first_output, remaining_outputs = (
+ build["outputs"][0],
+ build["outputs"][1:],
+ )
+
+ if remaining_outputs:
+ ninja.build(
+ outputs=remaining_outputs, rule="phony", implicit=first_output,
+ )
+
+ build["outputs"] = first_output
+
+ ninja.build(**build)
+
+ template_builds = dict()
+ for template_builder in template_builders:
+
+ # Special handling for outputs and implicit since we need to
+ # aggregate not replace for each builder.
+ for agg_key in ["outputs", "implicit"]:
+ new_val = template_builds.get(agg_key, [])
+
+ # Use pop so the key is removed and so the update
+ # below will not overwrite our aggregated values.
+ cur_val = template_builder.pop(agg_key, [])
+ if isinstance(cur_val, list):
+ new_val += cur_val
+ else:
+ new_val.append(cur_val)
+ template_builds[agg_key] = new_val
+
+ # Collect all other keys
+ template_builds.update(template_builder)
+
+ if template_builds.get("outputs", []):
+ ninja.build(**template_builds)
+
+ # We have to glob the SCons files here to teach the ninja file
+ # how to regenerate itself. We'll never see ourselves in the
+ # DAG walk so we can't rely on action_to_ninja_build to
+ # generate this rule even though SCons should know we're
+ # dependent on SCons files.
+ #
+ # TODO: We're working on getting an API into SCons that will
+ # allow us to query the actual SConscripts used. Right now
+ # this glob method has deficiencies like skipping
+ # jstests/SConscript and being specific to the MongoDB
+ # repository layout.
+ ninja.build(
+ ninja_file,
+ rule="REGENERATE",
+ implicit=[
+ self.env.File("#SConstruct").get_abspath(),
+ os.path.abspath(__file__),
+ ]
+ + glob("src/**/SConscript", recursive=True),
+ )
+
+ ninja.build(
+ "scons-invocation",
+ rule="CMD",
+ pool="console",
+ variables={"cmd": "echo $SCONS_INVOCATION_W_TARGETS"},
+ )
+
+ # If we ever change the name/s of the rules that include
+ # compile commands (i.e. something like CC) we will need to
+ # update this build to reflect that complete list.
+ ninja.build(
+ "compile_commands.json",
+ rule="CMD",
+ pool="console",
+ variables={
+ "cmd": "ninja -f {} -t compdb CC CXX > compile_commands.json".format(
+ ninja_file
+ )
+ },
+ )
+
+ ninja.build(
+ "compiledb", rule="phony", implicit=["compile_commands.json"],
+ )
+
+ # Look in SCons's list of DEFAULT_TARGETS, find the ones that
+ # we generated a ninja build rule for.
+ scons_default_targets = [
+ get_path(tgt)
+ for tgt in SCons.Script.DEFAULT_TARGETS
+ if get_path(tgt) in self.built
+ ]
+
+ # If we found an overlap between SCons's list of default
+ # targets and the targets we created ninja builds for then use
+ # those as ninja's default as well.
+ if scons_default_targets:
+ ninja.default(" ".join(scons_default_targets))
+
+ # If not then set the default to the fallback_default_target we were given.
+ # Otherwise we won't create a default ninja target.
+ elif fallback_default_target is not None:
+ ninja.default(fallback_default_target)
+
+ with open(ninja_file, "w") as build_ninja:
+ build_ninja.write(content.getvalue())
+
+ self.__generated = True
+
+
+def get_path(node):
+ """
+ Return a fake path if necessary.
+
+ As an example Aliases use this as their target name in Ninja.
+ """
+ if hasattr(node, "get_path"):
+ return node.get_path()
+ return str(node)
+
+
+def rfile(node):
+ """
+ Return the repository file for node if it has one. Otherwise return node
+ """
+ if hasattr(node, "rfile"):
+ return node.rfile()
+ return node
+
+
+def src_file(node):
+ """Returns the src code file if it exists."""
+ if hasattr(node, "srcnode"):
+ src = node.srcnode()
+ if src.stat() is not None:
+ return src
+ return get_path(node)
+
+
+def get_comstr(env, action, targets, sources):
+ """Get the un-substituted string for action."""
+ # Despite being having "list" in it's name this member is not
+ # actually a list. It's the pre-subst'd string of the command. We
+ # use it to determine if the command we're about to generate needs
+ # to use a custom Ninja rule. By default this redirects CC, CXX,
+ # AR, SHLINK, and LINK commands to their respective rules but the
+ # user can inject custom Ninja rules and tie them to commands by
+ # using their pre-subst'd string.
+ if hasattr(action, "process"):
+ return action.cmd_list
+
+ return action.genstring(targets, sources, env)
+
+
+def get_command_env(env):
+ """
+ Return a string that sets the enrivonment for any environment variables that
+ differ between the OS environment and the SCons command ENV.
+
+ It will be compatible with the default shell of the operating system.
+ """
+ try:
+ return env["NINJA_ENV_VAR_CACHE"]
+ except KeyError:
+ pass
+
+ # Scan the ENV looking for any keys which do not exist in
+ # os.environ or differ from it. We assume if it's a new or
+ # differing key from the process environment then it's
+ # important to pass down to commands in the Ninja file.
+ ENV = get_default_ENV(env)
+ scons_specified_env = {
+ key: value
+ for key, value in ENV.items()
+ if key not in os.environ or os.environ.get(key, None) != value
+ }
+
+ windows = env["PLATFORM"] == "win32"
+ command_env = ""
+ for key, value in scons_specified_env.items():
+ # Ensure that the ENV values are all strings:
+ if is_List(value):
+ # If the value is a list, then we assume it is a
+ # path list, because that's a pretty common list-like
+ # value to stick in an environment variable:
+ value = flatten_sequence(value)
+ value = joinpath(map(str, value))
+ else:
+ # If it isn't a string or a list, then we just coerce
+ # it to a string, which is the proper way to handle
+ # Dir and File instances and will produce something
+ # reasonable for just about everything else:
+ value = str(value)
+
+ if windows:
+ command_env += "set '{}={}' && ".format(key, value)
+ else:
+ command_env += "{}={} ".format(key, value)
+
+ env["NINJA_ENV_VAR_CACHE"] = command_env
+ return command_env
+
+
+def gen_get_response_file_command(env, rule, tool, tool_is_dynamic=False):
+ """Generate a response file command provider for rule name."""
+
+ # If win32 using the environment with a response file command will cause
+ # ninja to fail to create the response file. Additionally since these rules
+ # generally are not piping through cmd.exe /c any environment variables will
+ # make CreateProcess fail to start.
+ #
+ # On POSIX we can still set environment variables even for compile
+ # commands so we do so.
+ use_command_env = not env["PLATFORM"] == "win32"
+ if "$" in tool:
+ tool_is_dynamic = True
+
+ def get_response_file_command(env, node, action, targets, sources, executor=None):
+ if hasattr(action, "process"):
+ cmd_list, _, _ = action.process(targets, sources, env, executor=executor)
+ cmd_list = [str(c) for c in cmd_list[0]]
+ else:
+ command = generate_command(
+ env, node, action, targets, sources, executor=executor
+ )
+ cmd_list = shlex.split(command)
+
+ if tool_is_dynamic:
+ tool_command = env.subst(
+ tool, target=targets, source=sources, executor=executor
+ )
+ else:
+ tool_command = tool
+
+ try:
+ # Add 1 so we always keep the actual tool inside of cmd
+ tool_idx = cmd_list.index(tool_command) + 1
+ except ValueError:
+ raise Exception(
+ "Could not find tool {} in {} generated from {}".format(
+ tool, cmd_list, get_comstr(env, action, targets, sources)
+ )
+ )
+
+ cmd, rsp_content = cmd_list[:tool_idx], cmd_list[tool_idx:]
+
+ # Some commands may have a need to pass a literal $ to a tool
+ # like ld (think $ORIGIN). We need to protect that $ from
+ # Ninja by using $$.
+ rsp_content = " ".join(rsp_content).replace('$', '$$')
+
+ variables = {"rspc": rsp_content}
+ variables[rule] = cmd
+ if use_command_env:
+ variables["env"] = get_command_env(env)
+ return rule, variables
+
+ return get_response_file_command
+
+
+def generate_command(env, node, action, targets, sources, executor=None):
+ # Actions like CommandAction have a method called process that is
+ # used by SCons to generate the cmd_line they need to run. So
+ # check if it's a thing like CommandAction and call it if we can.
+ if hasattr(action, "process"):
+ cmd_list, _, _ = action.process(targets, sources, env, executor=executor)
+ cmd = _string_from_cmd_list(cmd_list[0])
+ else:
+ # Anything else works with genstring, this is most commonly hit by
+ # ListActions which essentially call process on all of their
+ # commands and concatenate it for us.
+ genstring = action.genstring(targets, sources, env)
+ if executor is not None:
+ cmd = env.subst(genstring, executor=executor)
+ else:
+ cmd = env.subst(genstring, targets, sources)
+
+ cmd = cmd.replace("\n", " && ").strip()
+ if cmd.endswith("&&"):
+ cmd = cmd[0:-2].strip()
+
+ return cmd
+
+
+def get_shell_command(env, node, action, targets, sources, executor=None):
+ return (
+ "CMD",
+ {
+ "cmd": generate_command(env, node, action, targets, sources, executor=None),
+ "env": get_command_env(env),
+ },
+ )
+
+
+def get_command(env, node, action): # pylint: disable=too-many-branches
+ """Get the command to execute for node."""
+ if node.env:
+ sub_env = node.env
+ else:
+ sub_env = env
+
+ executor = node.get_executor()
+ if executor is not None:
+ tlist = executor.get_all_targets()
+ slist = executor.get_all_sources()
+ else:
+ if hasattr(node, "target_peers"):
+ tlist = node.target_peers
+ else:
+ tlist = [node]
+ slist = node.sources
+
+ # Retrieve the repository file for all sources
+ slist = [rfile(s) for s in slist]
+
+ # Generate a real CommandAction
+ if isinstance(action, SCons.Action.CommandGeneratorAction):
+ # pylint: disable=protected-access
+ action = action._generate(tlist, slist, sub_env, 1, executor=executor)
+
+ variables = {}
+
+ comstr = get_comstr(sub_env, action, tlist, slist)
+ if not comstr:
+ return None
+
+ provider = __NINJA_RULE_MAPPING.get(comstr, get_shell_command)
+ rule, variables = provider(sub_env, node, action, tlist, slist, executor=executor)
+
+ # Get the dependencies for all targets
+ implicit = list({dep for tgt in tlist for dep in get_dependencies(tgt)})
+
+ ninja_build = {
+ "outputs": get_outputs(node),
+ "inputs": get_inputs(node),
+ "implicit": implicit,
+ "rule": rule,
+ "variables": variables,
+ }
+
+ # Don't use sub_env here because we require that NINJA_POOL be set
+ # on a per-builder call basis to prevent accidental strange
+ # behavior like env['NINJA_POOL'] = 'console' and sub_env can be
+ # the global Environment object if node.env is None.
+ # Example:
+ #
+ # Allowed:
+ #
+ # env.Command("ls", NINJA_POOL="ls_pool")
+ #
+ # Not allowed and ignored:
+ #
+ # env["NINJA_POOL"] = "ls_pool"
+ # env.Command("ls")
+ #
+ if node.env and node.env.get("NINJA_POOL", None) is not None:
+ ninja_build["pool"] = node.env["NINJA_POOL"]
+
+ return ninja_build
+
+
+def ninja_builder(env, target, source):
+ """Generate a build.ninja for source."""
+ if not isinstance(source, list):
+ source = [source]
+ if not isinstance(target, list):
+ target = [target]
+
+ # We have no COMSTR equivalent so print that we're generating
+ # here.
+ print("Generating:", str(target[0]))
+
+ # The environment variable NINJA_SYNTAX points to the
+ # ninja_syntax.py module from the ninja sources found here:
+ # https://github.com/ninja-build/ninja/blob/master/misc/ninja_syntax.py
+ #
+ # This should be vendored into the build sources and it's location
+ # set in NINJA_SYNTAX. This code block loads the location from
+ # that variable, gets the absolute path to the vendored file, gets
+ # it's parent directory then uses importlib to import the module
+ # dynamically.
+ ninja_syntax_file = env[NINJA_SYNTAX]
+ if isinstance(ninja_syntax_file, str):
+ ninja_syntax_file = env.File(ninja_syntax_file).get_abspath()
+ ninja_syntax_mod_dir = os.path.dirname(ninja_syntax_file)
+ sys.path.append(ninja_syntax_mod_dir)
+ ninja_syntax_mod_name = os.path.basename(ninja_syntax_file)
+ ninja_syntax = importlib.import_module(ninja_syntax_mod_name.replace(".py", ""))
+
+ suffix = env.get("NINJA_SUFFIX", "")
+ if suffix and not suffix[0] == ".":
+ suffix = "." + suffix
+
+ generated_build_ninja = target[0].get_abspath() + suffix
+ ninja_state = NinjaState(env, ninja_syntax.Writer)
+
+ for src in source:
+ ninja_state.generate_builds(src)
+
+ ninja_state.generate(generated_build_ninja, str(source[0]))
+
+ return 0
+
+
+# pylint: disable=too-few-public-methods
+class AlwaysExecAction(SCons.Action.FunctionAction):
+ """Override FunctionAction.__call__ to always execute."""
+
+ def __call__(self, *args, **kwargs):
+ kwargs["execute"] = 1
+ return super().__call__(*args, **kwargs)
+
+
+def ninja_print(_cmd, target, _source, env):
+ """Tag targets with the commands to build them."""
+ if target:
+ for tgt in target:
+ if (
+ tgt.has_builder()
+ # Use 'is False' because not would still trigger on
+ # None's which we don't want to regenerate
+ and getattr(tgt.attributes, NINJA_BUILD, False) is False
+ and isinstance(tgt.builder.action, COMMAND_TYPES)
+ ):
+ ninja_action = get_command(env, tgt, tgt.builder.action)
+ setattr(tgt.attributes, NINJA_BUILD, ninja_action)
+ # Preload the attributes dependencies while we're still running
+ # multithreaded
+ get_dependencies(tgt)
+ return 0
+
+
+def register_custom_handler(env, name, handler):
+ """Register a custom handler for SCons function actions."""
+ env[NINJA_CUSTOM_HANDLERS][name] = handler
+
+
+def register_custom_rule_mapping(env, pre_subst_string, rule):
+ """Register a custom handler for SCons function actions."""
+ global __NINJA_RULE_MAPPING
+ __NINJA_RULE_MAPPING[pre_subst_string] = rule
+
+
+def register_custom_rule(env, rule, command, description="", deps=None, pool=None):
+ """Allows specification of Ninja rules from inside SCons files."""
+ rule_obj = {
+ "command": command,
+ "description": description if description else "{} $out".format(rule),
+ }
+
+ if deps is not None:
+ rule_obj["deps"] = deps
+
+ if pool is not None:
+ rule_obj["pool"] = pool
+
+ env[NINJA_RULES][rule] = rule_obj
+
+
+def register_custom_pool(env, pool, size):
+ """Allows the creation of custom Ninja pools"""
+ env[NINJA_POOLS][pool] = size
+
+
+def ninja_csig(original):
+ """Return a dummy csig"""
+
+ def wrapper(self):
+ name = str(self)
+ if "SConscript" in name or "SConstruct" in name:
+ return original(self)
+ return "dummy_ninja_csig"
+
+ return wrapper
+
+
+def ninja_contents(original):
+ """Return a dummy content without doing IO"""
+
+ def wrapper(self):
+ name = str(self)
+ if "SConscript" in name or "SConstruct" in name:
+ return original(self)
+ return bytes("dummy_ninja_contents", encoding="utf-8")
+
+ return wrapper
+
+
+def ninja_stat(_self, path):
+ """
+ Eternally memoized stat call.
+
+ SCons is very aggressive about clearing out cached values. For our
+ purposes everything should only ever call stat once since we're
+ running in a no_exec build the file system state should not
+ change. For these reasons we patch SCons.Node.FS.LocalFS.stat to
+ use our eternal memoized dictionary.
+ """
+ global NINJA_STAT_MEMO
+
+ try:
+ return NINJA_STAT_MEMO[path]
+ except KeyError:
+ try:
+ result = os.stat(path)
+ except os.error:
+ result = None
+
+ NINJA_STAT_MEMO[path] = result
+ return result
+
+
+def ninja_noop(*_args, **_kwargs):
+ """
+ A general purpose no-op function.
+
+ There are many things that happen in SCons that we don't need and
+ also don't return anything. We use this to disable those functions
+ instead of creating multiple definitions of the same thing.
+ """
+ return None
+
+
+def ninja_whereis(thing, *_args, **_kwargs):
+ """Replace env.WhereIs with a much faster version"""
+ global NINJA_WHEREIS_MEMO
+
+ # Optimize for success, this gets called significantly more often
+ # when the value is already memoized than when it's not.
+ try:
+ return NINJA_WHEREIS_MEMO[thing]
+ except KeyError:
+ # We do not honor any env['ENV'] or env[*] variables in the
+ # generated ninja ile. Ninja passes your raw shell environment
+ # down to it's subprocess so the only sane option is to do the
+ # same during generation. At some point, if and when we try to
+ # upstream this, I'm sure a sticking point will be respecting
+ # env['ENV'] variables and such but it's actually quite
+ # complicated. I have a naive version but making it always work
+ # with shell quoting is nigh impossible. So I've decided to
+ # cross that bridge when it's absolutely required.
+ path = shutil.which(thing)
+ NINJA_WHEREIS_MEMO[thing] = path
+ return path
+
+
+def ninja_always_serial(self, num, taskmaster):
+ """Replacement for SCons.Job.Jobs constructor which always uses the Serial Job class."""
+ # We still set self.num_jobs to num even though it's a lie. The
+ # only consumer of this attribute is the Parallel Job class AND
+ # the Main.py function which instantiates a Jobs class. It checks
+ # if Jobs.num_jobs is equal to options.num_jobs, so if the user
+ # provides -j12 but we set self.num_jobs = 1 they get an incorrect
+ # warning about this version of Python not supporting parallel
+ # builds. So here we lie so the Main.py will not give a false
+ # warning to users.
+ self.num_jobs = num
+ self.job = SCons.Job.Serial(taskmaster)
+
+
+class NinjaNoResponseFiles(SCons.Platform.TempFileMunge):
+ """Overwrite the __call__ method of SCons' TempFileMunge to not delete."""
+
+ def __call__(self, target, source, env, for_signature):
+ return self.cmd
+
+ def _print_cmd_str(*_args, **_kwargs):
+ """Disable this method"""
+ pass
+
+
+def exists(env):
+ """Enable if called."""
+
+ # This variable disables the tool when storing the SCons command in the
+ # generated ninja file to ensure that the ninja tool is not loaded when
+ # SCons should do actual work as a subprocess of a ninja build. The ninja
+ # tool is very invasive into the internals of SCons and so should never be
+ # enabled when SCons needs to build a target.
+ if env.get("__NINJA_NO", "0") == "1":
+ return False
+
+ return True
+
+
+def generate(env):
+ """Generate the NINJA builders."""
+ env[NINJA_SYNTAX] = env.get(NINJA_SYNTAX, "ninja_syntax.py")
+
+ # Add the Ninja builder.
+ always_exec_ninja_action = AlwaysExecAction(ninja_builder, {})
+ ninja_builder_obj = SCons.Builder.Builder(action=always_exec_ninja_action)
+ env.Append(BUILDERS={"Ninja": ninja_builder_obj})
+
+ # This adds the required flags such that the generated compile
+ # commands will create depfiles as appropriate in the Ninja file.
+ if env["PLATFORM"] == "win32":
+ env.Append(CCFLAGS=["/showIncludes"])
+ else:
+ env.Append(CCFLAGS=["-MMD", "-MF", "${TARGET}.d"])
+
+ # Provide a way for custom rule authors to easily access command
+ # generation.
+ env.AddMethod(get_shell_command, "NinjaGetShellCommand")
+ env.AddMethod(gen_get_response_file_command, "NinjaGenResponseFileProvider")
+
+ # Provides a way for users to handle custom FunctionActions they
+ # want to translate to Ninja.
+ env[NINJA_CUSTOM_HANDLERS] = {}
+ env.AddMethod(register_custom_handler, "NinjaRegisterFunctionHandler")
+
+ # Provides a mechanism for inject custom Ninja rules which can
+ # then be mapped using NinjaRuleMapping.
+ env[NINJA_RULES] = {}
+ env.AddMethod(register_custom_rule, "NinjaRule")
+
+ # Provides a mechanism for inject custom Ninja pools which can
+ # be used by providing the NINJA_POOL="name" as an
+ # OverrideEnvironment variable in a builder call.
+ env[NINJA_POOLS] = {}
+ env.AddMethod(register_custom_pool, "NinjaPool")
+
+ # Add the ability to register custom NinjaRuleMappings for Command
+ # builders. We don't store this dictionary in the env to prevent
+ # accidental deletion of the CC/XXCOM mappings. You can still
+ # overwrite them if you really want to but you have to explicit
+ # about it this way. The reason is that if they were accidentally
+ # deleted you would get a very subtly incorrect Ninja file and
+ # might not catch it.
+ env.AddMethod(register_custom_rule_mapping, "NinjaRuleMapping")
+
+ # TODO: change LINKCOM and SHLINKCOM to handle embedding manifest exe checks
+ # without relying on the SCons hacks that SCons uses by default.
+ if env["PLATFORM"] == "win32":
+ from SCons.Tool.mslink import compositeLinkAction
+
+ if env["LINKCOM"] == compositeLinkAction:
+ env["LINKCOM"] = '${TEMPFILE("$LINK $LINKFLAGS /OUT:$TARGET.windows $_LIBDIRFLAGS $_LIBFLAGS $_PDB $SOURCES.windows", "$LINKCOMSTR")}'
+ env["SHLINKCOM"] = '${TEMPFILE("$SHLINK $SHLINKFLAGS $_SHLINK_TARGETS $_LIBDIRFLAGS $_LIBFLAGS $_PDB $_SHLINK_SOURCES", "$SHLINKCOMSTR")}'
+
+ # Normally in SCons actions for the Program and *Library builders
+ # will return "${*COM}" as their pre-subst'd command line. However
+ # if a user in a SConscript overwrites those values via key access
+ # like env["LINKCOM"] = "$( $ICERUN $)" + env["LINKCOM"] then
+ # those actions no longer return the "bracketted" string and
+ # instead return something that looks more expanded. So to
+ # continue working even if a user has done this we map both the
+ # "bracketted" and semi-expanded versions.
+ def robust_rule_mapping(var, rule, tool):
+ provider = gen_get_response_file_command(env, rule, tool)
+ env.NinjaRuleMapping("${" + var + "}", provider)
+ env.NinjaRuleMapping(env[var], provider)
+
+ robust_rule_mapping("CCCOM", "CC", env["CC"])
+ robust_rule_mapping("SHCCCOM", "CC", env["CC"])
+ robust_rule_mapping("CXXCOM", "CXX", env["CXX"])
+ robust_rule_mapping("SHCXXCOM", "CXX", env["CXX"])
+ robust_rule_mapping("LINKCOM", "LINK", "$LINK")
+ robust_rule_mapping("SHLINKCOM", "LINK", "$SHLINK")
+ robust_rule_mapping("ARCOM", "AR", env["AR"])
+
+ # Make SCons node walk faster by preventing unnecessary work
+ env.Decider("timestamp-match")
+
+ # Used to determine if a build generates a source file. Ninja
+ # requires that all generated sources are added as order_only
+ # dependencies to any builds that *might* use them.
+ env["NINJA_GENERATED_SOURCE_SUFFIXES"] = [".h", ".hpp"]
+
+ if env["PLATFORM"] != "win32" and env.get("RANLIBCOM"):
+ # There is no way to translate the ranlib list action into
+ # Ninja so add the s flag and disable ranlib.
+ #
+ # This is equivalent to Meson.
+ # https://github.com/mesonbuild/meson/blob/master/mesonbuild/linkers.py#L143
+ old_arflags = str(env["ARFLAGS"])
+ if "s" not in old_arflags:
+ old_arflags += "s"
+
+ env["ARFLAGS"] = SCons.Util.CLVar([old_arflags])
+
+ # Disable running ranlib, since we added 's' above
+ env["RANLIBCOM"] = ""
+
+ # This is the point of no return, anything after this comment
+ # makes changes to SCons that are irreversible and incompatible
+ # with a normal SCons build. We return early if __NINJA_NO=1 has
+ # been given on the command line (i.e. by us in the generated
+ # ninja file) here to prevent these modifications from happening
+ # when we want SCons to do work. Everything before this was
+ # necessary to setup the builder and other functions so that the
+ # tool can be unconditionally used in the users's SCons files.
+
+ if not exists(env):
+ return
+
+ # Set a known variable that other tools can query so they can
+ # behave correctly during ninja generation.
+ env["GENERATING_NINJA"] = True
+
+ # These methods are no-op'd because they do not work during ninja
+ # generation, expected to do no work, or simply fail. All of which
+ # are slow in SCons. So we overwrite them with no logic.
+ SCons.Node.FS.File.make_ready = ninja_noop
+ SCons.Node.FS.File.prepare = ninja_noop
+ SCons.Node.FS.File.push_to_cache = ninja_noop
+ SCons.Executor.Executor.prepare = ninja_noop
+ SCons.Node.FS.File.built = ninja_noop
+
+ # We make lstat a no-op because it is only used for SONAME
+ # symlinks which we're not producing.
+ SCons.Node.FS.LocalFS.lstat = ninja_noop
+
+ # This is a slow method that isn't memoized. We make it a noop
+ # since during our generation we will never use the results of
+ # this or change the results.
+ SCons.Node.FS.is_up_to_date = ninja_noop
+
+ # We overwrite stat and WhereIs with eternally memoized
+ # implementations. See the docstring of ninja_stat and
+ # ninja_whereis for detailed explanations.
+ SCons.Node.FS.LocalFS.stat = ninja_stat
+ SCons.Util.WhereIs = ninja_whereis
+
+ # Monkey patch get_csig and get_contents for some classes. It
+ # slows down the build significantly and we don't need contents or
+ # content signatures calculated when generating a ninja file since
+ # we're not doing any SCons caching or building.
+ SCons.Executor.Executor.get_contents = ninja_contents(
+ SCons.Executor.Executor.get_contents
+ )
+ SCons.Node.Alias.Alias.get_contents = ninja_contents(
+ SCons.Node.Alias.Alias.get_contents
+ )
+ SCons.Node.FS.File.get_contents = ninja_contents(SCons.Node.FS.File.get_contents)
+ SCons.Node.FS.File.get_csig = ninja_csig(SCons.Node.FS.File.get_csig)
+ SCons.Node.FS.Dir.get_csig = ninja_csig(SCons.Node.FS.Dir.get_csig)
+ SCons.Node.Alias.Alias.get_csig = ninja_csig(SCons.Node.Alias.Alias.get_csig)
+
+ # Replace false Compiling* messages with a more accurate output
+ #
+ # We also use this to tag all Nodes with Builders using
+ # CommandActions with the final command that was used to compile
+ # it for passing to Ninja. If we don't inject this behavior at
+ # this stage in the build too much state is lost to generate the
+ # command at the actual ninja_builder execution time for most
+ # commands.
+ #
+ # We do attempt command generation again in ninja_builder if it
+ # hasn't been tagged and it seems to work for anything that
+ # doesn't represent as a non-FunctionAction during the print_func
+ # call.
+ env["PRINT_CMD_LINE_FUNC"] = ninja_print
+
+ # This reduces unnecessary subst_list calls to add the compiler to
+ # the implicit dependencies of targets. Since we encode full paths
+ # in our generated commands we do not need these slow subst calls
+ # as executing the command will fail if the file is not found
+ # where we expect it.
+ env["IMPLICIT_COMMAND_DEPENDENCIES"] = False
+
+ # Set build to no_exec, our sublcass of FunctionAction will force
+ # an execution for ninja_builder so this simply effects all other
+ # Builders.
+ env.SetOption("no_exec", True)
+
+ # This makes SCons more aggressively cache MD5 signatures in the
+ # SConsign file.
+ env.SetOption("max_drift", 1)
+
+ # The Serial job class is SIGNIFICANTLY (almost twice as) faster
+ # than the Parallel job class for generating Ninja files. So we
+ # monkey the Jobs constructor to only use the Serial Job class.
+ SCons.Job.Jobs.__init__ = ninja_always_serial
+
+ # We will eventually need to overwrite TempFileMunge to make it
+ # handle persistent tempfiles or get an upstreamed change to add
+ # some configurability to it's behavior in regards to tempfiles.
+ #
+ # Set all three environment variables that Python's
+ # tempfile.mkstemp looks at as it behaves differently on different
+ # platforms and versions of Python.
+ os.environ["TMPDIR"] = env.Dir("$BUILD_DIR/response_files").get_abspath()
+ os.environ["TEMP"] = os.environ["TMPDIR"]
+ os.environ["TMP"] = os.environ["TMPDIR"]
+ if not os.path.isdir(os.environ["TMPDIR"]):
+ env.Execute(SCons.Defaults.Mkdir(os.environ["TMPDIR"]))
+
+ env["TEMPFILE"] = NinjaNoResponseFiles
+
+ # Force the SConsign to be written, we benefit from SCons caching of
+ # implicit dependencies and conftests. Unfortunately, we have to do this
+ # using an atexit handler because SCons will not write the file when in a
+ # no_exec build.
+ import atexit
+
+ atexit.register(SCons.SConsign.write)