288 lines
11 KiB
Python
Vendored
288 lines
11 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
|
|
# 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 os
|
|
import shlex
|
|
import textwrap
|
|
from typing import Optional
|
|
|
|
import SCons
|
|
from SCons.Subst import SUBST_CMD
|
|
from SCons.Tool.ninja import NINJA_CUSTOM_HANDLERS, NINJA_RULES, NINJA_POOLS
|
|
from SCons.Tool.ninja.Globals import __NINJA_RULE_MAPPING
|
|
from SCons.Tool.ninja.Utils import get_targets_sources, get_dependencies, get_order_only, get_outputs, get_inputs, \
|
|
get_rule, get_path, generate_command, get_command_env, get_comstr
|
|
from SCons.Util.sctyping import ExecutorType
|
|
|
|
|
|
def register_custom_handler(env, name, handler) -> None:
|
|
"""Register a custom handler for SCons function actions."""
|
|
env[NINJA_CUSTOM_HANDLERS][name] = handler
|
|
|
|
|
|
def register_custom_rule_mapping(env, pre_subst_string, rule) -> None:
|
|
"""Register a function to call for a given rule."""
|
|
SCons.Tool.ninja.Globals.__NINJA_RULE_MAPPING[pre_subst_string] = rule
|
|
|
|
|
|
def register_custom_rule(env, rule, command, description: str="", deps=None, pool=None, use_depfile: bool=False, use_response_file: bool=False, response_file_content: str="$rspc") -> None:
|
|
"""Allows specification of Ninja rules from inside SCons files."""
|
|
rule_obj = {
|
|
"command": command,
|
|
"description": description if description else f"{rule} $out",
|
|
}
|
|
|
|
if use_depfile:
|
|
rule_obj["depfile"] = os.path.join(get_path(env['NINJA_DIR']), '$out.depfile')
|
|
|
|
if deps is not None:
|
|
rule_obj["deps"] = deps
|
|
|
|
if pool is not None:
|
|
rule_obj["pool"] = pool
|
|
|
|
if use_response_file:
|
|
rule_obj["rspfile"] = "$out.rsp"
|
|
rule_obj["rspfile_content"] = response_file_content
|
|
|
|
env[NINJA_RULES][rule] = rule_obj
|
|
|
|
|
|
def register_custom_pool(env, pool, size) -> None:
|
|
"""Allows the creation of custom Ninja pools"""
|
|
env[NINJA_POOLS][pool] = size
|
|
|
|
|
|
def set_build_node_callback(env, node, callback) -> None:
|
|
if not node.is_conftest():
|
|
node.attributes.ninja_build_callback = callback
|
|
|
|
|
|
def get_generic_shell_command(env, node, action, targets, sources, executor: Optional[ExecutorType] = None):
|
|
return (
|
|
"GENERATED_CMD",
|
|
{
|
|
"cmd": generate_command(env, node, action, targets, sources, executor=executor),
|
|
"env": get_command_env(env, targets, sources),
|
|
},
|
|
# Since this function is a rule mapping provider, it must return a list of dependencies,
|
|
# and usually this would be the path to a tool, such as a compiler, used for this rule.
|
|
# However this function is to generic to be able to reliably extract such deps
|
|
# from the command, so we return a placeholder empty list. It should be noted that
|
|
# generally this function will not be used solely and is more like a template to generate
|
|
# the basics for a custom provider which may have more specific options for a provider
|
|
# function for a custom NinjaRuleMapping.
|
|
[]
|
|
)
|
|
|
|
|
|
def CheckNinjaCompdbExpand(env, context):
|
|
""" Configure check testing if ninja's compdb can expand response files"""
|
|
|
|
# TODO: When would this be false?
|
|
context.Message('Checking if ninja compdb can expand response files... ')
|
|
ret, output = context.TryAction(
|
|
action='ninja -f $SOURCE -t compdb -x CMD_RSP > $TARGET',
|
|
extension='.ninja',
|
|
text=textwrap.dedent("""
|
|
rule CMD_RSP
|
|
command = $cmd @$out.rsp > fake_output.txt
|
|
description = Building $out
|
|
rspfile = $out.rsp
|
|
rspfile_content = $rspc
|
|
build fake_output.txt: CMD_RSP fake_input.txt
|
|
cmd = echo
|
|
pool = console
|
|
rspc = "test"
|
|
"""))
|
|
result = '@fake_output.txt.rsp' not in output
|
|
context.Result(result)
|
|
return result
|
|
|
|
|
|
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()
|
|
tlist, slist = get_targets_sources(node)
|
|
|
|
# Generate a real CommandAction
|
|
if isinstance(action, SCons.Action.CommandGeneratorAction):
|
|
# pylint: disable=protected-access
|
|
action = action._generate(tlist, slist, sub_env, SUBST_CMD, executor=executor)
|
|
|
|
variables = {}
|
|
|
|
# since we will check the ninja rule map for this command str, we must make sure
|
|
# its string so its hashable.
|
|
comstr = str(get_comstr(sub_env, action, tlist, slist))
|
|
if not comstr:
|
|
return None
|
|
|
|
provider = __NINJA_RULE_MAPPING.get(comstr, get_generic_shell_command)
|
|
rule, variables, provider_deps = provider(sub_env, node, action, tlist, slist, executor=executor)
|
|
if node.get_env().get('NINJA_FORCE_SCONS_BUILD'):
|
|
rule = 'TEMPLATE'
|
|
|
|
# Get the dependencies for all targets
|
|
implicit = list({dep for tgt in tlist for dep in get_dependencies(tgt)})
|
|
|
|
# Now add in the other dependencies related to the command,
|
|
# e.g. the compiler binary. The ninja rule can be user provided so
|
|
# we must do some validation to resolve the dependency path for ninja.
|
|
for provider_dep in provider_deps:
|
|
|
|
provider_dep = sub_env.subst(provider_dep)
|
|
if not provider_dep:
|
|
continue
|
|
|
|
# If the tool is a node, then SCons will resolve the path later, if its not
|
|
# a node then we assume it generated from build and make sure it is existing.
|
|
if isinstance(provider_dep, SCons.Node.Node) or os.path.exists(provider_dep):
|
|
implicit.append(provider_dep)
|
|
continue
|
|
|
|
# in some case the tool could be in the local directory and be supplied without the ext
|
|
# such as in windows, so append the executable suffix and check.
|
|
prog_suffix = sub_env.get('PROGSUFFIX', '')
|
|
provider_dep_ext = provider_dep if provider_dep.endswith(prog_suffix) else provider_dep + prog_suffix
|
|
if os.path.exists(provider_dep_ext):
|
|
implicit.append(provider_dep_ext)
|
|
continue
|
|
|
|
# Many commands will assume the binary is in the path, so
|
|
# we accept this as a possible input from a given command.
|
|
|
|
provider_dep_abspath = sub_env.WhereIs(provider_dep) or sub_env.WhereIs(provider_dep, path=os.environ["PATH"])
|
|
if provider_dep_abspath:
|
|
implicit.append(provider_dep_abspath)
|
|
continue
|
|
|
|
# Possibly these could be ignore and the build would still work, however it may not always
|
|
# rebuild correctly, so we hard stop, and force the user to fix the issue with the provided
|
|
# ninja rule.
|
|
raise Exception("Could not resolve path for %s dependency on node '%s'" % (provider_dep, node))
|
|
|
|
ninja_build = {
|
|
"order_only": get_order_only(node),
|
|
"outputs": get_outputs(node),
|
|
"inputs": get_inputs(node),
|
|
"implicit": implicit,
|
|
"rule": get_rule(node, 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")
|
|
#
|
|
# TODO: Why not alloe env['NINJA_POOL'] ? (bdbaddog)
|
|
if node.env and node.env.get("NINJA_POOL", None) is not None:
|
|
ninja_build["pool"] = node.env["NINJA_POOL"]
|
|
|
|
return ninja_build
|
|
|
|
|
|
def gen_get_response_file_command(env, rule, tool, tool_is_dynamic: bool=False, custom_env={}):
|
|
"""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: Optional[ExecutorType] = None):
|
|
if hasattr(action, "process"):
|
|
cmd_list, _, _ = action.process(targets, sources, env, executor=executor)
|
|
cmd_list = [str(c).replace("$", "$$") 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:]
|
|
|
|
# Canonicalize the path to have forward (posix style) dir sep characters.
|
|
if os.altsep:
|
|
rsp_content = [rsp_content_item.replace(os.sep, os.altsep) for rsp_content_item in rsp_content]
|
|
rsp_content = ['"' + rsp_content_item + '"' for rsp_content_item in rsp_content]
|
|
rsp_content = " ".join(rsp_content)
|
|
|
|
variables = {"rspc": rsp_content, rule: cmd}
|
|
if use_command_env:
|
|
variables["env"] = get_command_env(env, targets, sources)
|
|
|
|
for key, value in custom_env.items():
|
|
variables["env"] += env.subst(
|
|
"export %s=%s;" % (key, value), target=targets, source=sources, executor=executor
|
|
) + " "
|
|
|
|
if node.get_env().get('NINJA_FORCE_SCONS_BUILD'):
|
|
ret_rule = 'TEMPLATE'
|
|
else:
|
|
if len(' '.join(cmd_list)) < env.get('MAXLINELENGTH', 2048):
|
|
ret_rule = rule
|
|
else:
|
|
ret_rule = rule + '_RSP'
|
|
|
|
return ret_rule, variables, [tool_command]
|
|
|
|
return get_response_file_command
|