mapnik/scons/scons-local-4.8.1/SCons/Tool/ninja/NinjaState.py
2024-09-09 10:56:17 +01:00

923 lines
39 KiB
Python
Vendored

# MIT License
#
# Copyright The SCons Foundation
#
# 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
# "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.
import io
import os
import pathlib
import signal
import tempfile
import shutil
import sys
import random
import filecmp
from os.path import splitext
from tempfile import NamedTemporaryFile
import ninja
import hashlib
import SCons
from SCons.Script import COMMAND_LINE_TARGETS
from SCons.Util import wait_for_process_to_die
from SCons.Errors import InternalError
from .Globals import COMMAND_TYPES, NINJA_RULES, NINJA_POOLS, \
NINJA_CUSTOM_HANDLERS, NINJA_DEFAULT_TARGETS
from .Rules import _install_action_function, _mkdir_action_function, _lib_symlink_action_function, _copy_action_function
from .Utils import get_path, alias_to_ninja_build, generate_depfile, ninja_noop, get_order_only, \
get_outputs, get_inputs, get_dependencies, get_rule, get_command_env, to_escaped_list, ninja_sorted_build
from .Methods import get_command
# pylint: disable=too-many-instance-attributes
class NinjaState:
"""Maintains state of Ninja build system as it's translated from SCons."""
def __init__(self, env, ninja_file, ninja_syntax) -> None:
self.env = env
self.ninja_file = ninja_file
self.ninja_bin_path = env.get('NINJA')
if not self.ninja_bin_path:
# default to using ninja installed with python module
ninja_bin = 'ninja.exe' if env["PLATFORM"] == "win32" else 'ninja'
self.ninja_bin_path = os.path.abspath(os.path.join(
ninja.__file__,
os.pardir,
'data',
'bin',
ninja_bin))
if not os.path.exists(self.ninja_bin_path):
# couldn't find it, just give the bin name and hope
# its in the path later
self.ninja_bin_path = ninja_bin
self.ninja_syntax = ninja_syntax
self.writer_class = ninja_syntax.Writer
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 = dict()
# 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
scons_escape = env.get("ESCAPE", lambda x: x)
# The daemon port should be the same across runs, unless explicitly set
# or if the portfile is deleted. This ensures the ninja file is deterministic
# across regen's if nothings changed. The construction var should take preference,
# then portfile is next, and then otherwise create a new random port to persist in
# use.
scons_daemon_port = None
os.makedirs(get_path(self.env.get("NINJA_DIR")), exist_ok=True)
scons_daemon_port_file = str(pathlib.Path(get_path(self.env.get("NINJA_DIR"))) / "scons_daemon_portfile")
if env.get('NINJA_SCONS_DAEMON_PORT') is not None:
scons_daemon_port = int(env.get('NINJA_SCONS_DAEMON_PORT'))
elif os.path.exists(scons_daemon_port_file):
with open(scons_daemon_port_file) as f:
scons_daemon_port = int(f.read())
else:
scons_daemon_port = random.randint(10000, 60000)
with open(scons_daemon_port_file, 'w') as f:
f.write(str(scons_daemon_port))
# if SCons was invoked from python, we expect the first arg to be the scons.py
# script, otherwise scons was invoked from the scons script
python_bin = ''
if os.path.basename(sys.argv[0]) == 'scons.py':
python_bin = ninja_syntax.escape(scons_escape(sys.executable))
self.variables = {
"COPY": "cmd.exe /c 1>NUL copy" if sys.platform == "win32" else "cp",
'PORT': scons_daemon_port,
'NINJA_DIR_PATH': env.get('NINJA_DIR').abspath,
'PYTHON_BIN': sys.executable,
'NINJA_TOOL_DIR': pathlib.Path(__file__).parent,
'NINJA_SCONS_DAEMON_KEEP_ALIVE': str(env.get('NINJA_SCONS_DAEMON_KEEP_ALIVE')),
"SCONS_INVOCATION": '{} {} --disable-ninja __NINJA_NO=1 $out'.format(
python_bin,
" ".join(
[ninja_syntax.escape(scons_escape(arg)) for arg in sys.argv if arg not in COMMAND_LINE_TARGETS]
),
),
"SCONS_INVOCATION_W_TARGETS": "{} {} NINJA_DISABLE_AUTO_RUN=1".format(
python_bin, " ".join([
ninja_syntax.escape(scons_escape(arg))
for arg in sys.argv
if arg != 'NINJA_DISABLE_AUTO_RUN=1'])
),
# This must be set to a global default per:
# https://ninja-build.org/manual.html#_deps
# English Visual Studio will have the default below,
# otherwise the user can define the variable in the first environment
# that initialized ninja tool
"msvc_deps_prefix": env.get("NINJA_MSVC_DEPS_PREFIX", "Note: including file:")
}
self.rules = {
"CMD": {
"command": "cmd /c $env$cmd $in $out" if sys.platform == "win32" else "$env$cmd $in $out",
"description": "Building $out",
"pool": "local_pool",
},
"GENERATED_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_RSP": {
"command": "$env$CC @$out.rsp",
"description": "Compiling $out",
"rspfile": "$out.rsp",
"rspfile_content": "$rspc",
},
"CXX_RSP": {
"command": "$env$CXX @$out.rsp",
"description": "Compiling $out",
"rspfile": "$out.rsp",
"rspfile_content": "$rspc",
},
"LINK_RSP": {
"command": "$env$LINK @$out.rsp",
"description": "Linking $out",
"rspfile": "$out.rsp",
"rspfile_content": "$rspc",
"pool": "local_pool",
},
# Ninja does not automatically delete the archive before
# invoking ar. The ar utility will append to an existing archive, which
# can cause duplicate symbols if the symbols moved between object files.
# Native SCons will perform this operation so we need to force ninja
# to do the same. See related for more info:
# https://jira.mongodb.org/browse/SERVER-49457
"AR_RSP": {
"command": "{}$env$AR @$out.rsp".format(
'' if sys.platform == "win32" else "rm -f $out && "
),
"description": "Archiving $out",
"rspfile": "$out.rsp",
"rspfile_content": "$rspc",
"pool": "local_pool",
},
"CC": {
"command": "$env$CC $rspc",
"description": "Compiling $out",
},
"CXX": {
"command": "$env$CXX $rspc",
"description": "Compiling $out",
},
"LINK": {
"command": "$env$LINK $rspc",
"description": "Linking $out",
"pool": "local_pool",
},
"AR": {
"command": "{}$env$AR $rspc".format(
'' if sys.platform == "win32" else "rm -f $out && "
),
"description": "Archiving $out",
"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": "$PYTHON_BIN $NINJA_TOOL_DIR/ninja_daemon_build.py $PORT $NINJA_DIR_PATH $out",
"description": "Defer to SCons to build $out",
"pool": "local_pool",
"restat": 1
},
"EXIT_SCONS_DAEMON": {
"command": "$PYTHON_BIN $NINJA_TOOL_DIR/ninja_daemon_build.py $PORT $NINJA_DIR_PATH --exit",
"description": "Shutting down ninja scons daemon server",
"pool": "local_pool",
"restat": 1
},
"SCONS": {
"command": "$SCONS_INVOCATION $out",
"description": "$SCONS_INVOCATION $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,
},
"SCONS_DAEMON": {
"command": "$PYTHON_BIN $NINJA_TOOL_DIR/ninja_run_daemon.py $PORT $NINJA_DIR_PATH $NINJA_SCONS_DAEMON_KEEP_ALIVE $SCONS_INVOCATION",
"description": "Starting scons daemon...",
"pool": "local_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 $self",
"generator": 1,
"pool": "console",
"restat": 1,
},
}
if env['PLATFORM'] == 'darwin' and env.get('AR', "") == 'ar':
self.rules["AR"] = {
"command": "rm -f $out && $env$AR $rspc",
"description": "Archiving $out",
"pool": "local_pool",
}
self.pools = {"scons_pool": 1}
def add_build(self, node) -> bool:
if not node.has_builder():
return False
if isinstance(node, SCons.Node.Python.Value):
return False
if isinstance(node, SCons.Node.Alias.Alias):
build = alias_to_ninja_build(node)
else:
build = self.translator.action_to_ninja_build(node)
# Some things are unbuild-able or need not be built in Ninja
if build is None:
return False
node_string = str(node)
if node_string in self.builds:
# TODO: If we work out a way to handle Alias() with same name as file this logic can be removed
# This works around adding Alias with the same name as a Node.
# It's not great way to workaround because it force renames the alias,
# but the alternative is broken ninja support.
warn_msg = f"Alias {node_string} name the same as File node, ninja does not support this. Renaming Alias {node_string} to {node_string}_alias."
if isinstance(node, SCons.Node.Alias.Alias):
for i, output in enumerate(build["outputs"]):
if output == node_string:
build["outputs"][i] += "_alias"
node_string += "_alias"
print(warn_msg)
elif self.builds[node_string]["rule"] == "phony":
for i, output in enumerate(self.builds[node_string]["outputs"]):
if output == node_string:
self.builds[node_string]["outputs"][i] += "_alias"
tmp_build = self.builds[node_string].copy()
del self.builds[node_string]
node_string += "_alias"
self.builds[node_string] = tmp_build
print(warn_msg)
else:
raise InternalError(f"Node {node_string} added to ninja build state more than once")
self.builds[node_string] = build
return True
# TODO: rely on SCons to tell us what is generated source
# or some form of user scanner maybe (Github Issue #3624)
def is_generated_source(self, output) -> bool:
"""Check if output ends with a known generated suffix."""
_, suffix = splitext(output)
return suffix in self.generated_suffixes
def has_generated_sources(self, output) -> bool:
"""
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):
"""
Generate the build.ninja.
This should only be called once for the lifetime of this object.
"""
if self.__generated:
return
num_jobs = self.env.get('NINJA_MAX_JOBS', self.env.GetOption("num_jobs"))
self.pools.update({
"local_pool": num_jobs,
"install_pool": num_jobs / 2,
})
deps_format = self.env.get("NINJA_DEPFILE_PARSE_FORMAT", 'msvc' if self.env['PLATFORM'] == 'win32' else 'gcc')
for rule in ["CC", "CXX"]:
if deps_format == "msvc":
self.rules[rule]["deps"] = "msvc"
elif deps_format == "gcc" or deps_format == "clang":
self.rules[rule]["deps"] = "gcc"
self.rules[rule]["depfile"] = "$out.d"
else:
raise Exception(f"Unknown 'NINJA_DEPFILE_PARSE_FORMAT'={self.env['NINJA_DEPFILE_PARSE_FORMAT']}, use 'mvsc', 'gcc', or 'clang'.")
for key, rule in self.env.get(NINJA_RULES, {}).items():
# make a non response file rule for users custom response file rules.
if rule.get('rspfile') is not None:
self.rules.update({key + '_RSP': rule})
non_rsp_rule = rule.copy()
del non_rsp_rule['rspfile']
del non_rsp_rule['rspfile_content']
self.rules.update({key: non_rsp_rule})
else:
self.rules.update({key: rule})
self.pools.update(self.env.get(NINJA_POOLS, {}))
content = io.StringIO()
ninja = self.writer_class(content, width=100)
ninja.comment("Generated by scons. DO NOT EDIT.")
ninja.variable("builddir", get_path(self.env.Dir(self.env['NINJA_DIR']).path))
for pool_name, size in sorted(self.pools.items()):
ninja.pool(pool_name, min(self.env.get('NINJA_MAX_JOBS', size), size))
for var, val in sorted(self.variables.items()):
ninja.variable(var, val)
for rule, kwargs in sorted(self.rules.items()):
if self.env.get('NINJA_MAX_JOBS') is not None and 'pool' not in kwargs:
kwargs['pool'] = 'local_pool'
ninja.rule(rule, **kwargs)
# If the user supplied an alias to determine generated sources, use that, otherwise
# determine what the generated sources are dynamically.
generated_sources_alias = self.env.get('NINJA_GENERATED_SOURCE_ALIAS_NAME')
generated_sources_build = None
if generated_sources_alias:
generated_sources_build = self.builds.get(generated_sources_alias)
if generated_sources_build is None or generated_sources_build["rule"] != 'phony':
raise Exception(
"ERROR: 'NINJA_GENERATED_SOURCE_ALIAS_NAME' set, but no matching Alias object found."
)
if generated_sources_alias and generated_sources_build:
generated_source_files = sorted(
[] if not generated_sources_build else generated_sources_build['implicit']
)
def check_generated_source_deps(build):
return (
build != generated_sources_build
and set(build["outputs"]).isdisjoint(generated_source_files)
)
else:
generated_sources_build = None
generated_source_files = sorted({
output
# First find builds which have header files in their outputs.
for build in self.builds.values()
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:
generated_sources_alias = "_ninja_generated_sources"
ninja.build(
outputs=generated_sources_alias,
rule="phony",
implicit=generated_source_files
)
def check_generated_source_deps(build):
return (
not build["rule"] == "INSTALL"
and set(build["outputs"]).isdisjoint(generated_source_files)
and set(build.get("implicit", [])).isdisjoint(generated_source_files)
)
template_builders = []
scons_compiledb = False
if SCons.Script._Get_Default_Targets == SCons.Script._Set_Default_Targets_Has_Not_Been_Called:
all_targets = set()
else:
all_targets = None
for build in [self.builds[key] for key in sorted(self.builds.keys())]:
if "compile_commands.json" in build["outputs"]:
scons_compiledb = True
# this is for the no command line targets, no SCons default case. We want this default
# to just be all real files in the build.
if all_targets is not None and build['rule'] != 'phony':
all_targets = all_targets | set(build["outputs"])
if build["rule"] == "TEMPLATE":
template_builders.append(build)
continue
if "implicit" in build:
build["implicit"].sort()
# 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 check_generated_source_deps(build)
):
# 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.
order_only = build.get("order_only", [])
order_only.append(generated_sources_alias)
build["order_only"] = order_only
if "order_only" in build:
build["order_only"].sort()
# 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 directly 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.
#
# We assume that the first listed output is the 'key'
# output and is stably presented to us by SCons. For
# instance if -gsplit-dwarf is in play and we are
# producing foo.o and foo.dwo, we expect that outputs[0]
# from SCons will be the foo.o file and not the dwo
# file. If instead we just sorted the whole outputs array,
# we would find that the dwo file becomes the
# first_output, and this breaks, for instance, header
# dependency scanning.
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_sorted_build(
ninja,
outputs=remaining_outputs, rule="phony", implicit=first_output,
)
build["outputs"] = first_output
# Optionally a rule can specify a depfile, and SCons can generate implicit
# dependencies into the depfile. This allows for dependencies to come and go
# without invalidating the ninja file. The depfile was created in ninja specifically
# for dealing with header files appearing and disappearing across rebuilds, but it can
# be repurposed for anything, as long as you have a way to regenerate the depfile.
# More specific info can be found here: https://ninja-build.org/manual.html#_depfile
if rule is not None and rule.get('depfile') and build.get('deps_files'):
path = build['outputs'] if SCons.Util.is_List(build['outputs']) else [build['outputs']]
generate_depfile(self.env, path[0], build.pop('deps_files', []))
if "inputs" in build:
build["inputs"].sort()
ninja_sorted_build(
ninja,
**build
)
scons_daemon_dirty = str(pathlib.Path(get_path(self.env.get("NINJA_DIR"))) / "scons_daemon_dirty")
for template_builder in template_builders:
template_builder["implicit"] += [scons_daemon_dirty]
ninja_sorted_build(
ninja,
**template_builder
)
# 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.
ninja_file_path = self.env.File(self.ninja_file).path
regenerate_deps = to_escaped_list(self.env, self.env['NINJA_REGENERATE_DEPS'])
ninja_sorted_build(
ninja,
outputs=ninja_file_path,
rule="REGENERATE",
implicit=regenerate_deps,
variables={
"self": ninja_file_path
}
)
ninja_sorted_build(
ninja,
outputs=regenerate_deps,
rule="phony",
variables={
"self": ninja_file_path,
}
)
if not scons_compiledb:
# 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_sorted_build(
ninja,
outputs="compile_commands.json",
rule="CMD",
pool="console",
implicit=[str(self.ninja_file)],
variables={
"cmd": "{} -f {} -t compdb {}CC CXX > compile_commands.json".format(
# NINJA_COMPDB_EXPAND - should only be true for ninja
# This was added to ninja's compdb tool in version 1.9.0 (merged April 2018)
# https://github.com/ninja-build/ninja/pull/1223
# TODO: add check in generate to check version and enable this by default if it's available.
self.ninja_bin_path, str(self.ninja_file),
'-x ' if self.env.get('NINJA_COMPDB_EXPAND', True) else ''
)
},
)
ninja_sorted_build(
ninja,
outputs="compiledb", rule="phony", implicit=["compile_commands.json"],
)
ninja_sorted_build(
ninja,
outputs=["run_ninja_scons_daemon_phony", scons_daemon_dirty],
rule="SCONS_DAEMON",
)
ninja.build(
"shutdown_ninja_scons_daemon_phony",
rule="EXIT_SCONS_DAEMON",
)
if all_targets is None:
# Look in SCons's list of DEFAULT_TARGETS, find the ones that
# we generated a ninja build rule for.
all_targets = [str(node) for node in NINJA_DEFAULT_TARGETS]
else:
all_targets = list(all_targets)
if len(all_targets) == 0:
all_targets = ["phony_default"]
ninja_sorted_build(
ninja,
outputs=all_targets,
rule="phony",
)
ninja.default([self.ninja_syntax.escape_path(path) for path in sorted(all_targets)])
with NamedTemporaryFile(delete=False, mode='w') as temp_ninja_file:
temp_ninja_file.write(content.getvalue())
if self.env.GetOption('skip_ninja_regen') and os.path.exists(ninja_file_path) and filecmp.cmp(temp_ninja_file.name, ninja_file_path):
os.unlink(temp_ninja_file.name)
else:
daemon_dir = pathlib.Path(tempfile.gettempdir()) / ('scons_daemon_' + str(hashlib.md5(str(get_path(self.env["NINJA_DIR"])).encode()).hexdigest()))
pidfile = None
if os.path.exists(scons_daemon_dirty):
pidfile = scons_daemon_dirty
elif os.path.exists(daemon_dir / 'pidfile'):
pidfile = daemon_dir / 'pidfile'
if pidfile:
with open(pidfile) as f:
pid = int(f.readline())
try:
os.kill(pid, signal.SIGINT)
except OSError:
pass
# wait for the server process to fully killed
# TODO: update wait_for_process_to_die() to handle timeout and then catch exception
# here and do something smart.
wait_for_process_to_die(pid)
if os.path.exists(scons_daemon_dirty):
os.unlink(scons_daemon_dirty)
shutil.move(temp_ninja_file.name, ninja_file_path)
self.__generated = True
class SConsToNinjaTranslator:
"""Translates SCons Actions into Ninja build objects."""
def __init__(self, env) -> None:
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.
# TODO: use command action #3573
"installFunc": _install_action_function,
"MkdirFunc": _mkdir_action_function,
"Mkdir": _mkdir_action_function,
"LibSymlinksActionFunction": _lib_symlink_action_function,
"Copy": _copy_action_function
}
self.loaded_custom = False
# pylint: disable=too-many-return-statements
def action_to_ninja_build(self, node, action=None):
"""Generate build arguments dictionary for node."""
if not self.loaded_custom:
self.func_handlers.update(self.env[NINJA_CUSTOM_HANDLERS])
self.loaded_custom = True
if node.builder is None:
return None
if action is None:
action = node.builder.action
if node.env and node.env.get("NINJA_SKIP"):
return None
build = {}
env = node.env if node.env else self.env
# 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 SCons.Tool.ninja.NINJA_STATE.ninja_file == str(node):
build = None
elif isinstance(action, SCons.Action.FunctionAction):
build = self.handle_func_action(node, action)
elif isinstance(action, SCons.Action.LazyAction):
# pylint: disable=protected-access
action = action._generate_cache(env)
build = self.action_to_ninja_build(node, action=action)
elif isinstance(action, SCons.Action.ListAction):
build = self.handle_list_action(node, action)
elif isinstance(action, COMMAND_TYPES):
build = get_command(env, node, action)
else:
return {
"rule": "TEMPLATE",
"order_only": get_order_only(node),
"outputs": get_outputs(node),
"inputs": get_inputs(node),
"implicit": get_dependencies(node, skip_sources=True),
}
if build is not None:
build["order_only"] = get_order_only(node)
# TODO: WPD Is this testing the filename to verify it's a configure context generated file?
if not node.is_conftest():
node_callback = node.check_attributes("ninja_build_callback")
if callable(node_callback):
node_callback(env, node, build)
return build
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. We skip sources here because
# dependencies don't really matter when we're going to shove these to
# the bottom of ninja's DAG anyway and Textfile builders can have text
# content as their source which doesn't work as an implicit dep in
# ninja.
if name == 'ninja_builder':
return None
handler = self.func_handlers.get(name, None)
if handler is not None:
return handler(node.env if node.env else self.env, node)
elif name == "ActionCaller":
action_to_call = str(action).split('(')[0].strip()
handler = self.func_handlers.get(action_to_call, None)
if handler is not None:
return handler(node.env if node.env else self.env, node)
SCons.Warnings.SConsWarning(
"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)
)
return {
"rule": "TEMPLATE",
"order_only": get_order_only(node),
"outputs": get_outputs(node),
"inputs": get_inputs(node),
"implicit": get_dependencies(node, skip_sources=True),
}
# 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.get("implicit", [])})
if results[0]["rule"] == "CMD" or results[0]["rule"] == "GENERATED_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.
if not cmd.get("variables") or not cmd["variables"].get("cmd"):
continue
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()
env = node.env if node.env else self.env
executor = node.get_executor()
if executor is not None:
targets = executor.get_all_targets()
else:
if hasattr(node, "target_peers"):
targets = node.target_peers
else:
targets = [node]
# Make sure we didn't generate an empty cmdline
if cmdline:
ninja_build = {
"outputs": all_outputs,
"rule": get_rule(node, "GENERATED_CMD"),
"variables": {
"cmd": cmdline,
"env": get_command_env(env, targets, node.sources),
},
"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": get_rule(node, "INSTALL"),
"inputs": get_inputs(node),
"implicit": dependencies,
}
return {
"rule": "TEMPLATE",
"order_only": get_order_only(node),
"outputs": get_outputs(node),
"inputs": get_inputs(node),
"implicit": get_dependencies(node, skip_sources=True),
}