diff --git a/CHANGES.txt b/CHANGES.txt index 33cc7cefd5..fd316d3a80 100755 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -35,6 +35,7 @@ RELEASE VERSION/DATE TO BE FILLED IN LATER - Fix versioned shared library naming for MacOS platform. (Previously was libxyz.dylib.1.2.3, has been fixed to libxyz.1.2.3.dylib. Additionally the sonamed symlink had the same issue, that is now resolved as well) + - Add experimental ninja builder. (Contributed by MongoDB, Daniel Moody and many others). - Fix #3955 - _LIBDIRFLAGS leaving $( and $) in *COMSTR output. Added affect_signature flag to _concat function. If set to False, it will prepend and append $( and $). That way the various Environment variables can use that rather than "$( _concat(...) $)". diff --git a/SCons/Node/__init__.py b/SCons/Node/__init__.py index a449082a06..ec742a686b 100644 --- a/SCons/Node/__init__.py +++ b/SCons/Node/__init__.py @@ -946,6 +946,11 @@ def is_conftest(self): return False return True + def check_attributes(self, name): + """ Simple API to check if the node.attributes for name has been set""" + return getattr(getattr(self, "attributes", None), name, None) + + def alter_targets(self): """Return a list of alternate targets for this Node. """ diff --git a/SCons/SConf.py b/SCons/SConf.py index 68e890a69b..38a2b945ac 100644 --- a/SCons/SConf.py +++ b/SCons/SConf.py @@ -606,7 +606,7 @@ def TryBuild(self, builder, text=None, extension=""): f = "_".join([f, textSig, textSigCounter]) textFile = self.confdir.File(f + extension) - self._set_conftest_node(sourcetext) + self._set_conftest_node(textFile) textFileNode = self.env.SConfSourceBuilder(target=textFile, source=sourcetext) nodesToBeBuilt.extend(textFileNode) @@ -625,6 +625,7 @@ def TryBuild(self, builder, text=None, extension=""): pref = self.env.subst( builder.builder.prefix ) suff = self.env.subst( builder.builder.suffix ) target = self.confdir.File(pref + f + suff) + self._set_conftest_node(target) try: # Slide our wrapper into the construction environment as diff --git a/SCons/Script/SConsOptions.py b/SCons/Script/SConsOptions.py index 4988a8d96e..def866303f 100644 --- a/SCons/Script/SConsOptions.py +++ b/SCons/Script/SConsOptions.py @@ -39,7 +39,7 @@ diskcheck_all = SCons.Node.FS.diskcheck_types() -experimental_features = {'warp_speed', 'transporter'} +experimental_features = {'warp_speed', 'transporter', 'ninja'} def diskcheck_convert(value): @@ -749,7 +749,7 @@ def experimental_callback(option, opt, value, parser): op.add_option('--experimental', dest='experimental', action='callback', - default={}, # empty set + default=set(), # empty set type='str', # choices=experimental_options+experimental_features, callback =experimental_callback, diff --git a/SCons/Tool/ninja/Globals.py b/SCons/Tool/ninja/Globals.py new file mode 100644 index 0000000000..0dc46ea840 --- /dev/null +++ b/SCons/Tool/ninja/Globals.py @@ -0,0 +1,40 @@ +# 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 SCons.Action + +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, +) +ninja_builder_initialized = False \ No newline at end of file diff --git a/SCons/Tool/ninja/Methods.py b/SCons/Tool/ninja/Methods.py new file mode 100644 index 0000000000..ef8e44f6a9 --- /dev/null +++ b/SCons/Tool/ninja/Methods.py @@ -0,0 +1,268 @@ +# 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 + +import SCons +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 + + +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 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="", deps=None, pool=None, use_depfile=False, use_response_file=False, response_file_content="$rspc"): + """Allows specification of Ninja rules from inside SCons files.""" + rule_obj = { + "command": command, + "description": description if description else "{} $out".format(rule), + } + + 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): + """Allows the creation of custom Ninja pools""" + env[NINJA_POOLS][pool] = size + + +def set_build_node_callback(env, node, callback): + if not node.is_conftest(): + node.attributes.ninja_build_callback = callback + + +def get_generic_shell_command(env, node, action, targets, sources, executor=None): + return ( + "GENERATED_CMD", + { + "cmd": generate_command(env, node, action, targets, sources, executor=executor), + "env": get_command_env(env), + }, + # 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, 1, executor=executor) + + variables = {} + + comstr = 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) + + # 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=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=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:] + 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) + + for key, value in custom_env.items(): + variables["env"] += env.subst( + "export %s=%s;" % (key, value), target=targets, source=sources, executor=executor + ) + " " + return rule, variables, [tool_command] + + return get_response_file_command \ No newline at end of file diff --git a/SCons/Tool/ninja/NinjaState.py b/SCons/Tool/ninja/NinjaState.py new file mode 100644 index 0000000000..e7d9882a1b --- /dev/null +++ b/SCons/Tool/ninja/NinjaState.py @@ -0,0 +1,711 @@ +# 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 sys +from os.path import splitext +import ninja + +import SCons +from SCons.Script import COMMAND_LINE_TARGETS +from SCons.Util import is_List +from SCons.Errors import InternalError +from .Globals import COMMAND_TYPES, NINJA_RULES, NINJA_POOLS, \ + NINJA_CUSTOM_HANDLERS +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 +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, writer_class): + 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.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 = dict() + + # 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) + + # 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 = escape(sys.executable) + self.variables = { + "COPY": "cmd.exe /c 1>NUL copy" if sys.platform == "win32" else "cp", + "SCONS_INVOCATION": '{} {} --disable-ninja __NINJA_NO=1 $out'.format( + python_bin, + " ".join( + [escape(arg) for arg in sys.argv if arg not in COMMAND_LINE_TARGETS] + ), + ), + "SCONS_INVOCATION_W_TARGETS": "{} {}".format( + python_bin, " ".join([escape(arg) for arg in sys.argv]) + ), + # 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": { + "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", + }, + # 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": { + "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", + }, + "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 $SCONS_INVOCATION $out", + "pool": "scons_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, + }, + "REGENERATE": { + "command": "$SCONS_INVOCATION_W_TARGETS", + "description": "Regenerating $out", + "generator": 1, + "depfile": os.path.join(get_path(env['NINJA_DIR']), '$out.depfile'), + # 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, + }, + } + + if env['PLATFORM'] == 'darwin' and env['AR'] == 'ar': + self.rules["AR"] = { + "command": "rm -f $out && $env$AR $rspc", + "description": "Archiving $out", + "pool": "local_pool", + } + + num_jobs = self.env.get('NINJA_MAX_JOBS', self.env.GetOption("num_jobs")) + self.pools = { + "local_pool": num_jobs, + "install_pool": 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" + + def add_build(self, node): + if not node.has_builder(): + 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: + raise InternalError("Node {} added to ninja build state more than once".format(node_string)) + self.builds[node_string] = build + self.built.update(build["outputs"]) + 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): + """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): + """ + Generate the build.ninja. + + This should only be called once for the lifetime of this object. + """ + if self.__generated: + return + + self.rules.update(self.env.get(NINJA_RULES, {})) + 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['NINJA_DIR'])) + + for pool_name, size in self.pools.items(): + ninja.pool(pool_name, min(self.env.get('NINJA_MAX_JOBS', size), size)) + + for var, val in self.variables.items(): + ninja.variable(var, val) + + for rule, kwargs in 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) + + 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: + ninja.build( + outputs="_generated_sources", + rule="phony", + implicit=generated_source_files + ) + + template_builders = [] + + for build in [self.builds[key] for key in sorted(self.builds.keys())]: + 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 not build["rule"] == "INSTALL" + and set(build["outputs"]).isdisjoint(generated_source_files) + and set(build.get("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. + order_only = build.get("order_only", []) + order_only.append("_generated_sources") + 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.build( + outputs=sorted(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.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", "inputs"]: + 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 is_List(cur_val): + 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. + # + # The REGENERATE rule uses depfile, so we need to generate the depfile + # in case any of the SConscripts have changed. The depfile needs to be + # path with in the build and the passed ninja file is an abspath, so + # we will use SCons to give us the path within the build. Normally + # generate_depfile should not be called like this, but instead be called + # through the use of custom rules, and filtered out in the normal + # list of build generation about. However, because the generate rule + # is hardcoded here, we need to do this generate_depfile call manually. + ninja_file_path = self.env.File(self.ninja_file).path + generate_depfile( + self.env, + ninja_file_path, + self.env['NINJA_REGENERATE_DEPS'] + ) + + ninja.build( + ninja_file_path, + rule="REGENERATE", + implicit=[__file__], + ) + + # 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", + 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.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)) + + with open(str(self.ninja_file), "w") as build_ninja: + build_ninja.write(content.getvalue()) + + self.__generated = True + + +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. + # TODO: use command action #3573 + "installFunc": _install_action_function, + "MkdirFunc": _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: + raise Exception("Got an unbuildable ListAction for: {}".format(str(node))) + + 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["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. + 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": get_rule(node, "GENERATED_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": get_rule(node, "INSTALL"), + "inputs": get_inputs(node), + "implicit": dependencies, + } + + raise Exception("Unhandled list action with rule: " + results[0]["rule"]) diff --git a/SCons/Tool/ninja/Overrides.py b/SCons/Tool/ninja/Overrides.py new file mode 100644 index 0000000000..c6f12074b7 --- /dev/null +++ b/SCons/Tool/ninja/Overrides.py @@ -0,0 +1,96 @@ +# 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. +""" +This module is to hold logic which overrides default SCons behaviors to enable +ninja file generation +""" +import SCons + + +def ninja_hack_linkcom(env): + # 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.get("LINKCOM", None) == 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")}' + + +def ninja_hack_arcom(env): + """ + Force ARCOM so use 's' flag on ar instead of separately running ranlib + """ + 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"] = "" + + +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 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) + + +# 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) \ No newline at end of file diff --git a/SCons/Tool/ninja/Rules.py b/SCons/Tool/ninja/Rules.py new file mode 100644 index 0000000000..a2f6bc5456 --- /dev/null +++ b/SCons/Tool/ninja/Rules.py @@ -0,0 +1,81 @@ +# 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. + +from .Utils import get_outputs, get_rule, get_inputs, get_dependencies + + +def _install_action_function(_env, node): + """Install files using the install or copy commands""" + return { + "outputs": get_outputs(node), + "rule": get_rule(node, "INSTALL"), + "inputs": get_inputs(node), + "implicit": get_dependencies(node), + } + + +def _mkdir_action_function(env, node): + return { + "outputs": get_outputs(node), + "rule": get_rule(node, "CMD"), + # implicit explicitly omitted, we translate these so they can be + # used by anything that depends on these but commonly this is + # hit with a node that will depend on all of the fake + # srcnode's that SCons will never give us a rule for leading + # to an invalid ninja file. + "variables": { + # On Windows mkdir "-p" is always on + "cmd": "{mkdir}".format( + mkdir="mkdir $out & exit 0" if env["PLATFORM"] == "win32" else "mkdir -p $out", + ), + }, + } + + +def _copy_action_function(env, node): + return { + "outputs": get_outputs(node), + "inputs": get_inputs(node), + "rule": get_rule(node, "CMD"), + "variables": { + "cmd": "$COPY $in $out", + }, + } + + +def _lib_symlink_action_function(_env, node): + """Create shared object symlinks if any need to be created""" + symlinks = node.check_attributes("shliblinks") + + 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": get_rule(node, "SYMLINK"), + "implicit": get_dependencies(node), + } diff --git a/SCons/Tool/ninja/Utils.py b/SCons/Tool/ninja/Utils.py new file mode 100644 index 0000000000..3bdefe8207 --- /dev/null +++ b/SCons/Tool/ninja/Utils.py @@ -0,0 +1,419 @@ +# 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 shutil +from os.path import join as joinpath + +import SCons +from SCons.Action import get_default_ENV, _string_from_cmd_list +from SCons.Script import AddOption +from SCons.Util import is_List, flatten_sequence + + +def ninja_add_command_line_options(): + """ + Add additional command line arguments to SCons specific to the ninja tool + """ + AddOption('--disable-execute-ninja', + dest='disable_execute_ninja', + metavar='BOOL', + action="store_true", + default=False, + help='Disable ninja automatically building after scons') + + AddOption('--disable-ninja', + dest='disable_ninja', + metavar='BOOL', + action="store_true", + default=False, + help='Disable ninja automatically building after scons') + + +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. + """ + if isinstance(node, SCons.Node.Alias.Alias): + return node.children() + + return not node.get_env().get("NINJA_SKIP") + + + + +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(src_file(n)) for n in node.children() if is_valid_dependent_node(n) + ], + } + + +def check_invalid_ninja_node(node): + return not isinstance(node, (SCons.Node.FS.Base, SCons.Node.Alias.Alias)) + + +def filter_ninja_nodes(node_list): + ninja_nodes = [] + for node in node_list: + if isinstance(node, (SCons.Node.FS.Base, SCons.Node.Alias.Alias)) and not node.get_env().get('NINJA_SKIP'): + ninja_nodes.append(node) + else: + continue + return ninja_nodes + + +def get_input_nodes(node): + if node.get_executor() is not None: + inputs = node.get_executor().get_all_sources() + else: + inputs = node.sources + return inputs + + +def invalid_ninja_nodes(node, targets): + result = False + for node_list in [node.prerequisites, get_input_nodes(node), node.children(), targets]: + if node_list: + result = result or any([check_invalid_ninja_node(node) for node in node_list]) + return result + + +def get_order_only(node): + """Return a list of order only dependencies for node.""" + if node.prerequisites is None: + return [] + return [get_path(src_file(prereq)) for prereq in filter_ninja_nodes(node.prerequisites)] + + +def get_dependencies(node, skip_sources=False): + """Return a list of dependencies for node.""" + if skip_sources: + return [ + get_path(src_file(child)) + for child in filter_ninja_nodes(node.children()) + if child not in node.sources + ] + return [get_path(src_file(child)) for child in filter_ninja_nodes(node.children())] + + +def get_inputs(node): + """Collect the Ninja inputs for node.""" + return [get_path(src_file(o)) for o in filter_ninja_nodes(get_input_nodes(node))] + + +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 filter_ninja_nodes(outputs)] + + return outputs + + +def get_targets_sources(node): + 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] + return tlist, slist + +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_rule(node, rule): + tlist, slist = get_targets_sources(node) + if invalid_ninja_nodes(node, tlist): + return "TEMPLATE" + else: + return rule + + +def generate_depfile(env, node, dependencies): + """ + Ninja tool function for writing a depfile. The depfile should include + the node path followed by all the dependent files in a makefile format. + + dependencies arg can be a list or a subst generator which returns a list. + """ + + depfile = os.path.join(get_path(env['NINJA_DIR']), str(node) + '.depfile') + + # subst_list will take in either a raw list or a subst callable which generates + # a list, and return a list of CmdStringHolders which can be converted into raw strings. + # If a raw list was passed in, then scons_list will make a list of lists from the original + # values and even subst items in the list if they are substitutable. Flatten will flatten + # the list in that case, to ensure for either input we have a list of CmdStringHolders. + deps_list = env.Flatten(env.subst_list(dependencies)) + + # Now that we have the deps in a list as CmdStringHolders, we can convert them into raw strings + # and make sure to escape the strings to handle spaces in paths. We also will sort the result + # keep the order of the list consistent. + escaped_depends = sorted([dep.escape(env.get("ESCAPE", lambda x: x)) for dep in deps_list]) + depfile_contents = str(node) + ": " + ' '.join(escaped_depends) + + need_rewrite = False + try: + with open(depfile, 'r') as f: + need_rewrite = (f.read() != depfile_contents) + except FileNotFoundError: + need_rewrite = True + + if need_rewrite: + os.makedirs(os.path.dirname(depfile) or '.', exist_ok=True) + with open(depfile, 'w') as f: + f.write(depfile_contents) + + +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 get_command_env(env): + """ + Return a string that sets the environment 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() + # TODO: Remove this filter, unless there's a good reason to keep. SCons's behavior shouldn't depend on shell's. + 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: + # We address here *only* the specific case that a user might have + # an environment variable which somehow gets included and has + # spaces in the value. These are escapes that Ninja handles. This + # doesn't make builds on paths with spaces (Ninja and SCons issues) + # nor expanding response file paths with spaces (Ninja issue) work. + value = value.replace(r' ', r'$ ') + command_env += "export {}='{}';".format(key, value) + + env["NINJA_ENV_VAR_CACHE"] = command_env + return command_env + + +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 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() + + # Escape dollars as necessary + return cmd.replace("$", "$$") + + +def ninja_csig(original): + """Return a dummy csig""" + + def wrapper(self): + if isinstance(self, SCons.Node.Node) and self.is_sconscript(): + return original(self) + return "dummy_ninja_csig" + + return wrapper + + +def ninja_contents(original): + """Return a dummy content without doing IO""" + + def wrapper(self): + if isinstance(self, SCons.Node.Node) and self.is_sconscript(): + 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. + """ + + try: + return SCons.Tool.ninja.Globals.NINJA_STAT_MEMO[path] + except KeyError: + try: + result = os.stat(path) + except os.error: + result = None + + SCons.Tool.ninja.Globals.NINJA_STAT_MEMO[path] = result + return result + + +def ninja_whereis(thing, *_args, **_kwargs): + """Replace env.WhereIs with a much faster version""" + + # Optimize for success, this gets called significantly more often + # when the value is already memoized than when it's not. + try: + return SCons.Tool.ninja.Globals.NINJA_WHEREIS_MEMO[thing] + except KeyError: + # TODO: Fix this to respect env['ENV']['PATH']... WPD + # We do not honor any env['ENV'] or env[*] variables in the + # generated ninja file. 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) + SCons.Tool.ninja.Globals.NINJA_WHEREIS_MEMO[thing] = path + return path + + +def ninja_print_conf_log(s, target, source, env): + """Command line print only for conftest to generate a correct conf log.""" + if target and target[0].is_conftest(): + action = SCons.Action._ActionAction() + action.print_cmd_line(s, target, source, env) \ No newline at end of file diff --git a/SCons/Tool/ninja/__init__.py b/SCons/Tool/ninja/__init__.py new file mode 100644 index 0000000000..d940bb1636 --- /dev/null +++ b/SCons/Tool/ninja/__init__.py @@ -0,0 +1,450 @@ +# MIT License +# +# Copyright 2020 MongoDB Inc. +# +# 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. +# + +"""Generate build.ninja files from SCons aliases.""" + +import importlib +import os +import subprocess +import sys + +import SCons +import SCons.Tool.ninja.Globals +from SCons.Script import GetOption + +from .Globals import NINJA_RULES, NINJA_POOLS, NINJA_CUSTOM_HANDLERS +from .Methods import register_custom_handler, register_custom_rule_mapping, register_custom_rule, register_custom_pool, \ + set_build_node_callback, get_generic_shell_command, CheckNinjaCompdbExpand, get_command, \ + gen_get_response_file_command +from .NinjaState import NinjaState +from .Overrides import ninja_hack_linkcom, ninja_hack_arcom, NinjaNoResponseFiles, ninja_always_serial, AlwaysExecAction +from .Utils import ninja_add_command_line_options, \ + ninja_noop, ninja_print_conf_log, ninja_csig, ninja_contents, ninja_stat, ninja_whereis + +NINJA_STATE = None + + +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])) + + generated_build_ninja = target[0].get_abspath() + NINJA_STATE.generate() + + if env["PLATFORM"] == "win32": + # TODO: Is this necessary as you set env variable in the ninja build file per target? + # this is not great, its doesn't consider specific + # node environments, which means on linux the build could + # behave differently, because on linux you can set the environment + # per command in the ninja file. This is only needed if + # running ninja directly from a command line that hasn't + # had the environment setup (vcvarsall.bat) + with open('run_ninja_env.bat', 'w') as f: + for key in env['ENV']: + f.write('set {}={}\n'.format(key, env['ENV'][key])) + f.write('{} -f {} %*\n'.format(NINJA_STATE.ninja_bin_path, generated_build_ninja)) + cmd = ['run_ninja_env.bat'] + + else: + cmd = [NINJA_STATE.ninja_bin_path, '-f', generated_build_ninja] + + if not env.get("NINJA_DISABLE_AUTO_RUN"): + print("Executing:", str(' '.join(cmd))) + + # execute the ninja build at the end of SCons, trying to + # reproduce the output like a ninja build would + def execute_ninja(): + + proc = subprocess.Popen(cmd, + stderr=sys.stderr, + stdout=subprocess.PIPE, + universal_newlines=True, + env=os.environ if env["PLATFORM"] == "win32" else env['ENV'] + ) + for stdout_line in iter(proc.stdout.readline, ""): + yield stdout_line + proc.stdout.close() + return_code = proc.wait() + if return_code: + raise subprocess.CalledProcessError(return_code, 'ninja') + + erase_previous = False + for output in execute_ninja(): + output = output.strip() + if erase_previous: + sys.stdout.write('\x1b[2K') # erase previous line + sys.stdout.write("\r") + else: + sys.stdout.write(os.linesep) + sys.stdout.write(output) + sys.stdout.flush() + # this will only erase ninjas [#/#] lines + # leaving warnings and other output, seems a bit + # prone to failure with such a simple check + erase_previous = output.startswith('[') + + +def exists(env): + """Enable if called.""" + + if 'ninja' not in GetOption('experimental'): + return False + + # 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 + + try: + import ninja + return ninja.__file__ + except ImportError: + SCons.Warnings.SConsWarning("Failed to import ninja, attempt normal SCons build.") + return False + + +def ninja_emitter(target, source, env): + """ fix up the source/targets """ + + ninja_file = env.File(env.subst("$NINJA_FILE_NAME")) + ninja_file.attributes.ninja_file = True + + # Someone called env.Ninja('my_targetname.ninja') + if not target and len(source) == 1: + target = source + + # Default target name is $NINJA_PREFIX.$NINJA.SUFFIX + if not target: + target = [ninja_file, ] + + # No source should have been passed. Drop it. + if source: + source = [] + + return target, source + + +def generate(env): + """Generate the NINJA builders.""" + global NINJA_STATE + + if 'ninja' not in GetOption('experimental'): + return + + if not SCons.Tool.ninja.Globals.ninja_builder_initialized: + SCons.Tool.ninja.Globals.ninja_builder_initialized = True + + ninja_add_command_line_options() + + try: + import ninja # noqa: F401 + except ImportError: + SCons.Warnings.SConsWarning("Failed to import ninja, attempt normal SCons build.") + return + + env["NINJA_DISABLE_AUTO_RUN"] = GetOption('disable_execute_ninja') + + env["NINJA_FILE_NAME"] = env.get("NINJA_FILE_NAME", "build.ninja") + + # Add the Ninja builder. + always_exec_ninja_action = AlwaysExecAction(ninja_builder, {}) + ninja_builder_obj = SCons.Builder.Builder(action=always_exec_ninja_action, + emitter=ninja_emitter) + env.Append(BUILDERS={"Ninja": ninja_builder_obj}) + + env["NINJA_ALIAS_NAME"] = env.get("NINJA_ALIAS_NAME", "generate-ninja") + env['NINJA_DIR'] = env.get("NINJA_DIR", env.Dir(".ninja").path) + + # here we allow multiple environments to construct rules and builds + # into the same ninja file + if NINJA_STATE is None: + ninja_file = env.Ninja() + env.AlwaysBuild(ninja_file) + env.Alias("$NINJA_ALIAS_NAME", ninja_file) + else: + if str(NINJA_STATE.ninja_file) != env["NINJA_FILE_NAME"]: + SCons.Warnings.SConsWarning("Generating multiple ninja files not supported, set ninja file name before tool initialization.") + ninja_file = [NINJA_STATE.ninja_file] + + def ninja_generate_deps(env): + """Return a list of SConscripts + TODO: Should we also include files loaded from site_scons/*** + or even all loaded modules? https://stackoverflow.com/questions/4858100/how-to-list-imported-modules + TODO: Do we want this to be Nodes? + """ + return sorted([str(s) for s in SCons.Node.SConscriptNodes]) + + env['_NINJA_REGENERATE_DEPS_FUNC'] = ninja_generate_deps + + env['NINJA_REGENERATE_DEPS'] = env.get('NINJA_REGENERATE_DEPS', '${_NINJA_REGENERATE_DEPS_FUNC(__env__)}') + + # 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"]) + + env.AddMethod(CheckNinjaCompdbExpand, "CheckNinjaCompdbExpand") + + # Provide a way for custom rule authors to easily access command + # generation. + env.AddMethod(get_generic_shell_command, "NinjaGetGenericShellCommand") + env.AddMethod(get_command, "NinjaGetCommand") + env.AddMethod(gen_get_response_file_command, "NinjaGenResponseFileProvider") + env.AddMethod(set_build_node_callback, "NinjaSetBuildNodeCallback") + + # 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") + + # on windows we need to change the link action + ninja_hack_linkcom(env) + + # 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.get(var, None), provider) + + robust_rule_mapping("CCCOM", "CC", "$CC") + robust_rule_mapping("SHCCCOM", "CC", "$CC") + robust_rule_mapping("CXXCOM", "CXX", "$CXX") + robust_rule_mapping("SHCXXCOM", "CXX", "$CXX") + robust_rule_mapping("LINKCOM", "LINK", "$LINK") + robust_rule_mapping("SHLINKCOM", "LINK", "$SHLINK") + robust_rule_mapping("ARCOM", "AR", "$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. + # TODO: switch to using SCons to help determine this (Github Issue #3624) + env["NINJA_GENERATED_SOURCE_SUFFIXES"] = [".h", ".hpp"] + + # Force ARCOM so use 's' flag on ar instead of separately running ranlib + ninja_hack_arcom(env) + + if GetOption('disable_ninja'): + return env + + SCons.Warnings.SConsWarning("Initializing ninja tool... this feature is experimental. SCons internals and all environments will be affected.") + + # 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.Taskmaster.Task.prepare = ninja_noop + SCons.Node.FS.File.built = ninja_noop + SCons.Node.Node.visited = 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) + + # Ignore CHANGED_SOURCES and CHANGED_TARGETS. We don't want those + # to have effect in a generation pass because the generator + # shouldn't generate differently depending on the current local + # state. Without this, when generating on Windows, if you already + # had a foo.obj, you would omit foo.cpp from the response file. Do the same for UNCHANGED. + SCons.Executor.Executor._get_changed_sources = SCons.Executor.Executor._get_sources + SCons.Executor.Executor._get_changed_targets = SCons.Executor.Executor._get_targets + SCons.Executor.Executor._get_unchanged_sources = SCons.Executor.Executor._get_sources + SCons.Executor.Executor._get_unchanged_targets = SCons.Executor.Executor._get_targets + + # Replace false action messages with nothing. + env["PRINT_CMD_LINE_FUNC"] = ninja_print_conf_log + + # 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 + + # This makes SCons more aggressively cache MD5 signatures in the + # SConsign file. + # TODO: WPD shouldn't this be set to 0? + 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 + + ninja_syntax = importlib.import_module(".ninja_syntax", package='ninja') + + if NINJA_STATE is None: + NINJA_STATE = NinjaState(env, ninja_file[0], ninja_syntax.Writer) + + + # TODO: this is hacking into scons, preferable if there were a less intrusive way + # We will subvert the normal builder execute to make sure all the ninja file is dependent + # on all targets generated from any builders + SCons_Builder_BuilderBase__execute = SCons.Builder.BuilderBase._execute + + def NinjaBuilderExecute(self, env, target, source, overwarn={}, executor_kw={}): + # this ensures all environments in which a builder executes from will + # not create list actions for linking on windows + ninja_hack_linkcom(env) + targets = SCons_Builder_BuilderBase__execute(self, env, target, source, overwarn=overwarn, executor_kw=executor_kw) + + if not SCons.Util.is_List(target): + target = [target] + + for target in targets: + if target.check_attributes('ninja_file') is None and not target.is_conftest(): + env.Depends(ninja_file, targets) + return targets + SCons.Builder.BuilderBase._execute = NinjaBuilderExecute + + # Here we monkey patch the Task.execute method to not do a bunch of + # unnecessary work. If a build is a regular builder (i.e not a conftest and + # not our own Ninja builder) then we add it to the NINJA_STATE. Otherwise we + # build it like normal. This skips all of the caching work that this method + # would normally do since we aren't pulling any of these targets from the + # cache. + # + # In the future we may be able to use this to actually cache the build.ninja + # file once we have the upstream support for referencing SConscripts as File + # nodes. + def ninja_execute(self): + + target = self.targets[0] + if target.get_env().get('NINJA_SKIP'): + return + if target.check_attributes('ninja_file') is None or not target.is_conftest: + NINJA_STATE.add_build(target) + else: + target.build() + + SCons.Taskmaster.Task.execute = ninja_execute + + # Make needs_execute always return true instead of determining out of + # date-ness. + SCons.Script.Main.BuildTask.needs_execute = lambda x: True + + # 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. + # build_dir = env.subst("$NINJA_DIR") + # if build_dir == "": + # build_dir = "." + # os.environ["TMPDIR"] = env.Dir("{}/.response_files".format(build_dir)).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['TEMPFILEDIR'] = "$NINJA_DIR/.response_files" + env["TEMPFILE"] = NinjaNoResponseFiles diff --git a/SCons/Tool/ninja/ninja.xml b/SCons/Tool/ninja/ninja.xml new file mode 100644 index 0000000000..0ec1708917 --- /dev/null +++ b/SCons/Tool/ninja/ninja.xml @@ -0,0 +1,325 @@ + + + + + %scons; + + %builders-mod; + + %functions-mod; + + %tools-mod; + + %variables-mod; + ]> + + + + + + + Sets up &b-link-Ninja; builder which generates a ninja build file, and then optionally runs &ninja;. + + + This is an experimental feature. + + This functionality is subject to change and/or removal without deprecation cycle. + + + + + NINJA_DISABLE_AUTO_RUN + NINJA_ALIAS_NAME + NINJA_DIR + NINJA_COMPDB_EXPAND + NINJA_ENV_VAR_CACHE + NINJA_FILE_NAME + NINJA_GENERATED_SOURCE_SUFFIXES + NINJA_MSVC_DEPS_PREFIX + NINJA_POOL + NINJA_REGENERATE_DEPS + NINJA_SYNTAX + _NINJA_REGENERATE_DEPS_FUNC + __NINJA_NO + IMPLICIT_COMMAND_DEPENDENCIES + + + + + + + + + + + + + AR + ARCOM + ARFLAGS + CC + CCCOM + CCFLAGS + CXX + CXXCOM + ESCAPE + LINK + LINKCOM + PLATFORM + RANLIB + RANLIBCOM + SHCCCOM + SHCXXCOM + SHLINK + SHLINKCOM + PROGSUFFIX + PRINT_CMD_LINE_FUNC + + + + + + + + + + + &b-Ninja; is a special builder which + adds a target to create a ninja build file. + The builder does not require any source files to be specified. + + + This is an experimental feature. To enable it you must use one of the following methods + + + + # On the command line + --experimental=ninja + + # Or in your SConstruct + SetOption('experimental', 'ninja') + + + This functionality is subject to change and/or removal without deprecation cycle. + + + + To use this tool you must install pypi's ninja + package. + This can be done via + pip install ninja + + + + + + If called with no arguments, + the builder will default to a target name of + ninja.build. + + + If called with a single positional argument, + &scons; will "deduce" the target name from that source + argument, giving it the same name, and then + ignore the source. + This is the usual way to call the builder if a + non-default target name is wanted. + + + If called with either the + target= + or source= keyword arguments, + the value of the argument is taken as the target name. + If called with both, the + target= + value is used and source= is ignored. + If called with multiple sources, + the source list will be ignored, + since there is no way to deduce what the intent was; + in this case the default target name will be used. + + + Available since &scons; 4.2. + + + + + + + + + The list of source file suffixes which are generated by SCons build steps. + All source files which match these suffixes will be added to the _generated_sources alias in the output + ninja.build file. + Then all other source files will be made to depend on this in the ninja.build file, forcing the + generated sources to be built first. + + + + + + + + This propagates directly into the generated ninja.build file. + From Ninja's docs + defines the string which should be stripped from msvc’s /showIncludes output + + + + + + + + This propagates directly into the generated ninja.build file. + From Ninja's docs: +
+ + builddir + A directory for some Ninja output files. ... (You can also store other build output in this + directory.) + +
+
+
+
+ + + + + A generator function used to create a ninja depsfile which includes all the files which would require + SCons to be invoked if they change. + Or a list of said files. + + + + + + + + Boolean value (True|False) to instruct ninja to expand the command line arguments normally put into + response files. + This prevents lines in the compilation database like gcc @rsp_file and instead yields + gcc -c -o myfile.o myfile.c -Ia -DXYZ + + + Ninja's compdb tool added the -x flag in Ninja V1.9.0 + + + + + + + + A string that sets the environment 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. + + If not explicitly specified, SCons will generate this dynamically from the Environment()'s 'ENV' + env['ENV'] + where those values differ from the existing shell.. + + + + + + + + Set the ninja_pool for this or all targets in scope for this env var. + + + + + + + + Boolean (True|False). Default: False + When True, SCons will not run ninja automatically after creating the ninja.build file. + + + + + + + + + Internal flag. Used to tell SCons whether or not to try to import pypi's ninja python package. + This is set to True when being called by Ninja? + + + + + + + + + The filename for the generated Ninja build file defaults to + ninja.build + + + + + + + + Name of the Alias() which is will cause SCons to create the ninja.build file, and + then (optionally) run ninja. + + + + + + + + Theres also NINJA_SYNTAX which is the path to a custom ninja_syntax.py file which is used in generation. + The tool currently assumes you have ninja installed through pip, and grabs the syntax file from that + installation if none specified. + + + + + + + + Internal value used to specify the function to call with argument env to generate the list of files + which if changed would require the ninja file to be regenerated. + + + + + +
diff --git a/SCons/__init__.py b/SCons/__init__.py index bc0f428f53..cd521d1f1c 100644 --- a/SCons/__init__.py +++ b/SCons/__init__.py @@ -1,9 +1,9 @@ __version__="4.1.1a" __copyright__="Copyright (c) 2001 - 2021 The SCons Foundation" __developer__="bdbaddog" -__date__="2021-04-18 22:22:37" +__date__="2021-05-30 21:33:41" __buildsys__="ProDog2020" -__revision__="1850bdd13c7668aac493b9f5b896578813e60ca9" -__build__="1850bdd13c7668aac493b9f5b896578813e60ca9" +__revision__="b4c7a0966c36d37d0c2ea125dd98bff0061e28f2" +__build__="b4c7a0966c36d37d0c2ea125dd98bff0061e28f2" # make sure compatibility is always in place import SCons.compat # noqa \ No newline at end of file diff --git a/bin/SConsDoc.py b/bin/SConsDoc.py index baffbdcc20..a46505785c 100644 --- a/bin/SConsDoc.py +++ b/bin/SConsDoc.py @@ -364,9 +364,18 @@ def validateXml(fpath, xmlschema_context): doc.xinclude() try: TreeFactory.xmlschema.assertValid(doc) + except etree.XMLSchemaValidateError as e: + print("ERROR: %s fails to validate:" % fpath) + print(e) + print(e.error_log.last_error.message) + print("In file: [%s]" % e.error_log.last_error.filename) + print("Line : %d" % e.error_log.last_error.line) + return False + except Exception as e: print("ERROR: %s fails to validate:" % fpath) print(e) + return False return True diff --git a/doc/generated/builders.gen b/doc/generated/builders.gen index 14508ea96f..6499ef38b4 100644 --- a/doc/generated/builders.gen +++ b/doc/generated/builders.gen @@ -1221,6 +1221,69 @@ env.MSVSSolution( + + Ninja() + env.Ninja() + + &b-Ninja; is a special builder which + adds a target to create a ninja build file. + The builder does not require any source files to be specified. + + + This is an experimental feature. To enable it you must use one of the following methods + + + + # On the command line + --experimental=ninja + + # Or in your SConstruct + SetOption('experimental', 'ninja') + + + This functionality is subject to change and/or removal without deprecation cycle. + + + + To use this tool you must install pypi's ninja + package. + This can be done via + pip install ninja + + + + + + If called with no arguments, + the builder will default to a target name of + ninja.build. + + + If called with a single positional argument, + &scons; will "deduce" the target name from that source + argument, giving it the same name, and then + ignore the source. + This is the usual way to call the builder if a + non-default target name is wanted. + + + If called with either the + target= + or source= keyword arguments, + the value of the argument is taken as the target name. + If called with both, the + target= + value is used and source= is ignored. + If called with multiple sources, + the source list will be ignored, + since there is no way to deduce what the intent was; + in this case the default target name will be used. + + + Available since &scons; 4.2. + + + Object() env.Object() diff --git a/doc/generated/builders.mod b/doc/generated/builders.mod index 4835118f69..3542b99110 100644 --- a/doc/generated/builders.mod +++ b/doc/generated/builders.mod @@ -37,6 +37,7 @@ THIS IS AN AUTOMATICALLY-GENERATED FILE. DO NOT EDIT. MOFiles"> MSVSProject"> MSVSSolution"> +Ninja"> Object"> Package"> PCH"> @@ -94,6 +95,7 @@ THIS IS AN AUTOMATICALLY-GENERATED FILE. DO NOT EDIT. env.MOFiles"> env.MSVSProject"> env.MSVSSolution"> +env.Ninja"> env.Object"> env.Package"> env.PCH"> @@ -157,6 +159,7 @@ THIS IS AN AUTOMATICALLY-GENERATED FILE. DO NOT EDIT. MOFiles"> MSVSProject"> MSVSSolution"> +Ninja"> Object"> Package"> PCH"> @@ -214,6 +217,7 @@ THIS IS AN AUTOMATICALLY-GENERATED FILE. DO NOT EDIT. env.MOFiles"> env.MSVSProject"> env.MSVSSolution"> +env.Ninja"> env.Object"> env.Package"> env.PCH"> diff --git a/doc/generated/examples/caching_ex-random_1.xml b/doc/generated/examples/caching_ex-random_1.xml index a5593d302d..2990d7477d 100644 --- a/doc/generated/examples/caching_ex-random_1.xml +++ b/doc/generated/examples/caching_ex-random_1.xml @@ -1,8 +1,8 @@ % scons -Q -cc -o f2.o -c f2.c cc -o f1.o -c f1.c cc -o f3.o -c f3.c -cc -o f4.o -c f4.c cc -o f5.o -c f5.c +cc -o f4.o -c f4.c +cc -o f2.o -c f2.c cc -o prog f1.o f2.o f3.o f4.o f5.o diff --git a/doc/generated/examples/troubleshoot_Dump_1.xml b/doc/generated/examples/troubleshoot_Dump_1.xml index 8f4e624d94..2b4178bd8d 100644 --- a/doc/generated/examples/troubleshoot_Dump_1.xml +++ b/doc/generated/examples/troubleshoot_Dump_1.xml @@ -62,8 +62,8 @@ scons: Reading SConscript files ... 'TEMPFILEARGJOIN': ' ', 'TEMPFILEPREFIX': '@', 'TOOLS': ['install', 'install'], - '_CPPDEFFLAGS': '${_defines(CPPDEFPREFIX, CPPDEFINES, CPPDEFSUFFIX, ' - '__env__)}', + '_CPPDEFFLAGS': '${_defines(CPPDEFPREFIX, CPPDEFINES, CPPDEFSUFFIX, __env__, ' + 'TARGET, SOURCE)}', '_CPPINCFLAGS': '$( ${_concat(INCPREFIX, CPPPATH, INCSUFFIX, __env__, RDirs, ' 'TARGET, SOURCE)} $)', '_LIBDIRFLAGS': '$( ${_concat(LIBDIRPREFIX, LIBPATH, LIBDIRSUFFIX, __env__, ' diff --git a/doc/generated/examples/troubleshoot_Dump_2.xml b/doc/generated/examples/troubleshoot_Dump_2.xml index f2fc5e6f6f..c2fced13c1 100644 --- a/doc/generated/examples/troubleshoot_Dump_2.xml +++ b/doc/generated/examples/troubleshoot_Dump_2.xml @@ -107,8 +107,8 @@ scons: Reading SConscript files ... 'TOOLS': ['msvc', 'install', 'install'], 'VSWHERE': None, '_CCCOMCOM': '$CPPFLAGS $_CPPDEFFLAGS $_CPPINCFLAGS $CCPCHFLAGS $CCPDBFLAGS', - '_CPPDEFFLAGS': '${_defines(CPPDEFPREFIX, CPPDEFINES, CPPDEFSUFFIX, ' - '__env__)}', + '_CPPDEFFLAGS': '${_defines(CPPDEFPREFIX, CPPDEFINES, CPPDEFSUFFIX, __env__, ' + 'TARGET, SOURCE)}', '_CPPINCFLAGS': '$( ${_concat(INCPREFIX, CPPPATH, INCSUFFIX, __env__, RDirs, ' 'TARGET, SOURCE)} $)', '_LIBDIRFLAGS': '$( ${_concat(LIBDIRPREFIX, LIBPATH, LIBDIRSUFFIX, __env__, ' diff --git a/doc/generated/examples/troubleshoot_explain1_3.xml b/doc/generated/examples/troubleshoot_explain1_3.xml index 6b90e1cabe..14a105e048 100644 --- a/doc/generated/examples/troubleshoot_explain1_3.xml +++ b/doc/generated/examples/troubleshoot_explain1_3.xml @@ -2,5 +2,5 @@ cp file.in file.oout scons: warning: Cannot find target file.out after building -File "/Users/bdbaddog/devel/scons/git/as_scons/scripts/scons.py", line 96, in <module> +File "/Users/bdbaddog/devel/scons/git/scons-bugfixes-4/scripts/scons.py", line 96, in <module> diff --git a/doc/generated/examples/troubleshoot_stacktrace_2.xml b/doc/generated/examples/troubleshoot_stacktrace_2.xml index 8e8809c461..75dd6fba03 100644 --- a/doc/generated/examples/troubleshoot_stacktrace_2.xml +++ b/doc/generated/examples/troubleshoot_stacktrace_2.xml @@ -3,10 +3,10 @@ scons: *** [prog.o] Source `prog.c' not found, needed by target `prog.o'. scons: internal stack trace: File "SCons/Job.py", line 195, in start task.prepare() - File "SCons/Script/Main.py", line 179, in prepare + File "SCons/Script/Main.py", line 178, in prepare return SCons.Taskmaster.OutOfDateTask.prepare(self) File "SCons/Taskmaster.py", line 186, in prepare executor.prepare() - File "SCons/Executor.py", line 424, in prepare + File "SCons/Executor.py", line 418, in prepare raise SCons.Errors.StopError(msg % (s, self.batches[0].targets[0])) diff --git a/doc/generated/functions.gen b/doc/generated/functions.gen index a43ed1a203..5554802194 100644 --- a/doc/generated/functions.gen +++ b/doc/generated/functions.gen @@ -2124,261 +2124,242 @@ file is found. env.GetOption(name) This function provides a way to query the value of -SCons options set on scons command line -(or set using the -&f-link-SetOption; -function). -The options supported are: +options which can be set via the command line or using the +&f-link-SetOption; function. - - - - -cache_debug - - -which corresponds to ; - - - - -cache_disable - - -which corresponds to ; - - - - -cache_force - - -which corresponds to ; - - - - -cache_show - - -which corresponds to ; - - - - -clean - - -which corresponds to , -and ; - - - - -config - - -which corresponds to ; - - - - -directory - - -which corresponds to and ; - - - - -diskcheck - - -which corresponds to ; - - - - -duplicate - - -which corresponds to ; - - - - -file - - -which corresponds to , , and ; - - - - -help - - -which corresponds to and ; - - - - -ignore_errors - - -which corresponds to ; - - - - -implicit_cache - - -which corresponds to ; - - - - -implicit_deps_changed - - -which corresponds to ; - - - - -implicit_deps_unchanged - - -which corresponds to ; - - - - -interactive - - -which corresponds to and ; - - - - -keep_going - - -which corresponds to and ; - - - - -max_drift - - -which corresponds to ; - - - - -no_exec - - -which corresponds to , -, , - and ; - - - - -no_site_dir - - -which corresponds to ; - - - - -num_jobs - - -which corresponds to and ; - - - - -profile_file - - -which corresponds to ; - - - - -question - - -which corresponds to and ; - - - - -random - - -which corresponds to ; - - - - -repository - - -which corresponds to , and ; - - - - -silent - - -which corresponds to , and ; - - - - -site_dir - - -which corresponds to ; - - - - -stack_size - - -which corresponds to ; - - - - -taskmastertrace_file - -which corresponds to ; and - - - - -warn - - -which corresponds to and . - - - - +name can be an entry from the following table, +which shows the corresponding command line arguments +that could affect the value. +name can be also be the destination +variable name from a project-specific option added using the +&f-link-AddOption; function, as long as the addition +happens prior to the &f-GetOption; call in the SConscript files. + + + + + Query name + Command-line options + Notes + + + + + + cache_debug + + + + cache_disable + + , + + + + + cache_force + + , + + + + + cache_readonly + + + + cache_show + + + + clean + + , + , + + + + + climb_up + + + + + + + + + + config + + + + debug + + + + directory + , + + + diskcheck + + + + duplicate + + + + enable_virtualenv + + + + experimental + + since 4.2 + + + file + + , + , + , + + + + + hash_format + + since 4.2 + + + help + , + + + ignore_errors + , + + + ignore_virtualenv + + + + implicit_cache + + + + implicit_deps_changed + + + + implicit_deps_unchanged + + + + include_dir + , + + + install_sandbox + + Available only if the &t-link-install; tool has been called + + + keep_going + , + + + max_drift + + + + md5_chunksize + + , + + + since 4.2 + + + no_exec + + , + , + , + , + + + + + no_progress + + + + num_jobs + , + + + package_type + + Available only if the &t-link-packaging; tool has been called + + + profile_file + + + + question + , + + + random + + + + repository + + , + , + + + + + silent + + , + , + + + + + site_dir + , + + + stack_size + + + + taskmastertrace_file + + + + tree_printers + + + + warn + , + + + + + + See the documentation for the corresponding command line option for information about each specific @@ -3879,15 +3860,33 @@ if 'FOO' not in env: Sets &scons; option variable name to value. These options are all also settable via -&scons; command-line options but the variable name -may differ from the command-line option name (see table). +command-line options but the variable name +may differ from the command-line option name - +see the table for correspondences. A value set via command-line option will take precedence over one set with &f-SetOption;, which allows setting a project default in the scripts and temporarily overriding it via command line. +&f-SetOption; calls can also be placed in the +site_init.py file. + + + +See the documentation in the manpage for the +corresponding command line option for information about each specific option. +The value parameter is mandatory, +for option values which are boolean in nature +(that is, the command line option does not take an argument) +use a value +which evaluates to true (e.g. True, +1) or false (e.g. False, +0). + + + Options which affect the reading and processing of SConscript files -are not settable this way, since those files must -be read in order to find the &f-SetOption; call. +are not settable using &f-SetOption; since those files must +be read in order to find the &f-SetOption; call in the first place. @@ -3895,126 +3894,161 @@ The settable variables with their associated command-line options are: - + -VariableCommand-line options + + Settable name + Command-line options + Notes + + - -clean - -, , - - -diskcheck - - - - - -duplicate - - - - - - - - experimental - - - - - - - - -help - -, - - -implicit_cache - - - - - -max_drift - - - - -md5_chunksize - - - - -no_exec - -, , -, , - - - -no_progress - - - - -num_jobs - -, - - -random - - - - -silent - -. - - -stack_size - - - - -warn - -. - + + clean + + , + , + + + + + + diskcheck + + + + + duplicate + + + + + experimental + + since 4.2 + + + + hash_chunksize + + + Actually sets md5_chunksize. + since 4.2 + + + + + hash_format + + since 4.2 + + + + help + , + + + + implicit_cache + + + + + implicit_deps_changed + + + Also sets implicit_cache. + (settable since 4.2) + + + + + implicit_deps_unchanged + + + Also sets implicit_cache. + (settable since 4.2) + + + + + max_drift + + + + + md5_chunksize + + + + + no_exec + + , + , + , + , + + + + + + no_progress + + See + + If no_progress is set via &f-SetOption; + in an SConscript file + (but not if set in a site_init.py file) + there will still be an initial status message about + reading SConscript files since &SCons; has + to start reading them before it can see the + &f-SetOption;. + + + + + + + num_jobs + , + + + + random + + + + + silent + + , + , + + + + + + stack_size + + + + + warn + + + - -See the documentation in the manpage for the -corresponding command line option for information about each specific option. -Option values which are boolean in nature (that is, they are -either on or off) should be set to a true value (True, -1) or a false value (False, -0). - - - - -If no_progress is set via &f-SetOption; -there will still be initial progress output as &SCons; has -to start reading SConscript files before it can see the -&f-SetOption; in an SConscript file: -scons: Reading SConscript files ... - - - Example: -SetOption('max_drift', True) +SetOption('max_drift', 0) diff --git a/doc/generated/tools.gen b/doc/generated/tools.gen index dc15b6e5bc..16d13e3cfa 100644 --- a/doc/generated/tools.gen +++ b/doc/generated/tools.gen @@ -811,6 +811,19 @@ Sets construction variables for the Sets: &cv-link-AS;, &cv-link-ASCOM;, &cv-link-ASFLAGS;, &cv-link-ASPPCOM;, &cv-link-ASPPFLAGS;.Uses: &cv-link-ASCOMSTR;, &cv-link-ASPPCOMSTR;. + + ninja + + Sets up &b-link-Ninja; builder which generates a ninja build file, and then optionally runs &ninja;. + + + This is an experimental feature. + + This functionality is subject to change and/or removal without deprecation cycle. + + + Sets: &cv-link-IMPLICIT_COMMAND_DEPENDENCIES;, &cv-link-NINJA_ALIAS_NAME;, &cv-link-NINJA_COMPDB_EXPAND;, &cv-link-NINJA_DIR;, &cv-link-NINJA_DISABLE_AUTO_RUN;, &cv-link-NINJA_ENV_VAR_CACHE;, &cv-link-NINJA_FILE_NAME;, &cv-link-NINJA_GENERATED_SOURCE_SUFFIXES;, &cv-link-NINJA_MSVC_DEPS_PREFIX;, &cv-link-NINJA_POOL;, &cv-link-NINJA_REGENERATE_DEPS;, &cv-link-NINJA_SYNTAX;, &cv-link-_NINJA_REGENERATE_DEPS_FUNC;, &cv-link-__NINJA_NO;.Uses: &cv-link-AR;, &cv-link-ARCOM;, &cv-link-ARFLAGS;, &cv-link-CC;, &cv-link-CCCOM;, &cv-link-CCFLAGS;, &cv-link-CXX;, &cv-link-CXXCOM;, &cv-link-ESCAPE;, &cv-link-LINK;, &cv-link-LINKCOM;, &cv-link-PLATFORM;, &cv-link-PRINT_CMD_LINE_FUNC;, &cv-link-PROGSUFFIX;, &cv-link-RANLIB;, &cv-link-RANLIBCOM;, &cv-link-SHCCCOM;, &cv-link-SHCXXCOM;, &cv-link-SHLINK;, &cv-link-SHLINKCOM;. + packaging diff --git a/doc/generated/tools.mod b/doc/generated/tools.mod index 78aa9ef982..35eea5e2b4 100644 --- a/doc/generated/tools.mod +++ b/doc/generated/tools.mod @@ -79,6 +79,7 @@ THIS IS AN AUTOMATICALLY-GENERATED FILE. DO NOT EDIT. mwcc"> mwld"> nasm"> +ninja"> packaging"> pdf"> pdflatex"> @@ -184,6 +185,7 @@ THIS IS AN AUTOMATICALLY-GENERATED FILE. DO NOT EDIT. mwcc"> mwld"> nasm"> +ninja"> packaging"> pdf"> pdflatex"> diff --git a/doc/generated/variables.gen b/doc/generated/variables.gen index a24fd34ae9..c56217be7b 100644 --- a/doc/generated/variables.gen +++ b/doc/generated/variables.gen @@ -22,6 +22,16 @@ if &cv-link-LDMODULEVERSION; is set. Othervise it evaluates to an empty string. + + + __NINJA_NO + + + Internal flag. Used to tell SCons whether or not to try to import pypi's ninja python package. + This is set to True when being called by Ninja? + + + __SHLIBVERSIONFLAGS @@ -5156,6 +5166,150 @@ Specfies the name of the project to package. See the &b-link-Package; builder. + + + NINJA_ALIAS_NAME + + + Name of the Alias() which is will cause SCons to create the ninja.build file, and + then (optionally) run ninja. + + + + + + NINJA_COMPDB_EXPAND + + + Boolean value (True|False) to instruct ninja to expand the command line arguments normally put into + response files. + This prevents lines in the compilation database like gcc @rsp_file and instead yields + gcc -c -o myfile.o myfile.c -Ia -DXYZ + + + Ninja's compdb tool added the -x flag in Ninja V1.9.0 + + + + + + NINJA_DIR + + + This propagates directly into the generated ninja.build file. + From Ninja's docs: +
+ + builddir + A directory for some Ninja output files. ... (You can also store other build output in this + directory.) + +
+
+
+
+ + + NINJA_DISABLE_AUTO_RUN + + + Boolean (True|False). Default: False + When True, SCons will not run ninja automatically after creating the ninja.build file. + + + + + + + NINJA_ENV_VAR_CACHE + + + A string that sets the environment 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. + + If not explicitly specified, SCons will generate this dynamically from the Environment()'s 'ENV' + env['ENV'] + where those values differ from the existing shell.. + + + + + + NINJA_FILE_NAME + + + The filename for the generated Ninja build file defaults to + ninja.build + + + + + + NINJA_GENERATED_SOURCE_SUFFIXES + + + The list of source file suffixes which are generated by SCons build steps. + All source files which match these suffixes will be added to the _generated_sources alias in the output + ninja.build file. + Then all other source files will be made to depend on this in the ninja.build file, forcing the + generated sources to be built first. + + + + + + NINJA_MSVC_DEPS_PREFIX + + + This propagates directly into the generated ninja.build file. + From Ninja's docs + defines the string which should be stripped from msvc’s /showIncludes output + + + + + + NINJA_POOL + + + Set the ninja_pool for this or all targets in scope for this env var. + + + + + + NINJA_REGENERATE_DEPS + + + A generator function used to create a ninja depsfile which includes all the files which would require + SCons to be invoked if they change. + Or a list of said files. + + + + + + _NINJA_REGENERATE_DEPS_FUNC + + + Internal value used to specify the function to call with argument env to generate the list of files + which if changed would require the ninja file to be regenerated. + + + + + + NINJA_SYNTAX + + + Theres also NINJA_SYNTAX which is the path to a custom ninja_syntax.py file which is used in generation. + The tool currently assumes you have ninja installed through pip, and grabs the syntax file from that + installation if none specified. + + + no_import_lib diff --git a/doc/generated/variables.mod b/doc/generated/variables.mod index a3dbc0d8f2..08aa21dbf3 100644 --- a/doc/generated/variables.mod +++ b/doc/generated/variables.mod @@ -9,6 +9,7 @@ THIS IS AN AUTOMATICALLY-GENERATED FILE. DO NOT EDIT. --> $__LDMODULEVERSIONFLAGS"> +$__NINJA_NO"> $__SHLIBVERSIONFLAGS"> $APPLELINK_COMPATIBILITY_VERSION"> $_APPLELINK_COMPATIBILITY_VERSION"> @@ -349,6 +350,18 @@ THIS IS AN AUTOMATICALLY-GENERATED FILE. DO NOT EDIT. $MWCW_VERSION"> $MWCW_VERSIONS"> $NAME"> +$NINJA_ALIAS_NAME"> +$NINJA_COMPDB_EXPAND"> +$NINJA_DIR"> +$NINJA_DISABLE_AUTO_RUN"> +$NINJA_ENV_VAR_CACHE"> +$NINJA_FILE_NAME"> +$NINJA_GENERATED_SOURCE_SUFFIXES"> +$NINJA_MSVC_DEPS_PREFIX"> +$NINJA_POOL"> +$NINJA_REGENERATE_DEPS"> +$_NINJA_REGENERATE_DEPS_FUNC"> +$NINJA_SYNTAX"> $no_import_lib"> $OBJPREFIX"> $OBJSUFFIX"> @@ -649,6 +662,7 @@ THIS IS AN AUTOMATICALLY-GENERATED FILE. DO NOT EDIT. --> $__LDMODULEVERSIONFLAGS"> +$__NINJA_NO"> $__SHLIBVERSIONFLAGS"> $APPLELINK_COMPATIBILITY_VERSION"> $_APPLELINK_COMPATIBILITY_VERSION"> @@ -989,6 +1003,18 @@ THIS IS AN AUTOMATICALLY-GENERATED FILE. DO NOT EDIT. $MWCW_VERSION"> $MWCW_VERSIONS"> $NAME"> +$NINJA_ALIAS_NAME"> +$NINJA_COMPDB_EXPAND"> +$NINJA_DIR"> +$NINJA_DISABLE_AUTO_RUN"> +$NINJA_ENV_VAR_CACHE"> +$NINJA_FILE_NAME"> +$NINJA_GENERATED_SOURCE_SUFFIXES"> +$NINJA_MSVC_DEPS_PREFIX"> +$NINJA_POOL"> +$NINJA_REGENERATE_DEPS"> +$_NINJA_REGENERATE_DEPS_FUNC"> +$NINJA_SYNTAX"> $no_import_lib"> $OBJPREFIX"> $OBJSUFFIX"> diff --git a/doc/scons.mod b/doc/scons.mod index 57004549f6..72adf68240 100644 --- a/doc/scons.mod +++ b/doc/scons.mod @@ -57,6 +57,7 @@ m4"> Make"> Make++"> +ninja"> pdflatex"> pdftex"> Python"> diff --git a/doc/user/external.xml b/doc/user/external.xml index 9900e93688..5f88f5adbb 100644 --- a/doc/user/external.xml +++ b/doc/user/external.xml @@ -1,286 +1,359 @@ - %scons; + + %scons; - - %builders-mod; - - %functions-mod; - - %tools-mod; - - %variables-mod; + + %builders-mod; + + %functions-mod; + + %tools-mod; + + %variables-mod; -]> + ]> -Using SCons with other build tools + Using SCons with other build tools - + 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. - - - Sometimes a project needs to interact with other projects - in various ways. For example, many open source projects - make use of components from other open source projects, - and want to use those in their released form, not recode - their builds into &SCons;. As another example, sometimes - the flexibility and power of &SCons; is useful for managing the - overall project, but developers might like faster incremental - builds when making small changes by using a different tool. - - - - - - This chapter shows some techniques for interacting with other - projects and tools effectively from within &SCons;. - - - -
- Creating a Compilation Database + --> - Tooling to perform analysis and modification - of source code often needs to know not only the source code - itself, but also how it will be compiled, as the compilation line - affects the behavior of macros, includes, etc. &SCons; has a - record of this information once it has run, in the form of - Actions associated with the sources, and can emit this information - so tools can use it. + Sometimes a project needs to interact with other projects + in various ways. For example, many open source projects + make use of components from other open source projects, + and want to use those in their released form, not recode + their builds into &SCons;. As another example, sometimes + the flexibility and power of &SCons; is useful for managing the + overall project, but developers might like faster incremental + builds when making small changes by using a different tool. - The Clang project has defined a JSON Compilation Database. - This database is in common use as input into Clang tools - and many IDEs and editors as well. - See - - JSON Compilation Database Format Specification - - for complete information. &SCons; can emit a - compilation database in this format - by enabling the &t-link-compilation_db; tool - and calling the &b-link-CompilationDatabase; builder - (available since &scons; 4.0). - - - - - - The compilation database can be populated with - source and output files either with paths relative - to the top of the build, or using absolute paths. - This is controlled by - COMPILATIONDB_USE_ABSPATH=(True|False) - which defaults to False. - The entries in this file can be filtered by using - - COMPILATIONDB_PATH_FILTER='pattern' - where the filter pattern is a string following the Python - - fnmatch - - syntax. - This filtering can be used for outputting different - build variants to different compilation database files. - - - - - - The following example illustrates generating a compilation - database containing absolute paths: - - - - - -env = Environment(COMPILATIONDB_USE_ABSPATH=True) -env.Tool('compilation_db') -env.CompilationDatabase() -env.Program('hello.c') - - -int main( int argc, char* argv[] ) -{ - return 0; -} - - - - - scons -Q - - - compile_commands.json contains: - - -[ - { - "command": "gcc -o hello.o -c hello.c", - "directory": "/home/user/sandbox", - "file": "/home/user/sandbox/hello.c", - "output": "/home/user/sandbox/hello.o" - } -] - - - - - Notice that the generated database contains only an entry for - the hello.c/hello.o pairing, - and nothing for the generation of the final executable - hello - the transformation of - hello.o to hello - does not have any information that affects interpretation - of the source code, - so it is not interesting to the compilation database. - - - - - - Although it can be a little surprising at first glance, - a compilation database target is, like any other target, - subject to &scons; target selection rules. - This means if you set a default target (that does not - include the compilation database), or use command-line - targets, it might not be selected for building. - This can actually be an advantage, since you don't - necessarily want to regenerate the compilation database - every build. - The following example - shows selecting relative paths (the default) - for output and source, - and also giving a non-default name to the database. - In order to be able to generate the database separately from building, - an alias is set referring to the database, - which can then be used as a target - here we are only - building the compilation database target, not the code. - - - - - -env = Environment() -env.Tool('compilation_db') -cdb = env.CompilationDatabase('compile_database.json') -Alias('cdb', cdb) -env.Program('test_main.c') - - -#include "test_main.h" -int main( int argc, char* argv[] ) -{ - return 0; -} - - -/* dummy include file */ - - - - - scons -Q cdb - - - compile_database.json contains: - - -[ - { - "command": "gcc -o test_main.o -c test_main.c", - "directory": "/home/user/sandbox", - "file": "test_main.c", - "output": "test_main.o" - } -] - - - - - The following (incomplete) example shows using filtering - to separate build variants. - In the case of using variants, - you want different compilation databases for each, - since the build parameters differ, so the code analysis - needs to see the correct build lines for the 32-bit build - and 64-bit build hinted at here. - For simplicity of presentation, - the example omits the setup details of the variant directories: + This chapter shows some techniques for interacting with other + projects and tools effectively from within &SCons;. - -env = Environment() -env.Tool('compilation_db') - -env1 = env.Clone() -env1['COMPILATIONDB_PATH_FILTER'] = 'build/linux32/*' -env1.CompilationDatabase('compile_commands-linux32.json') - -env2 = env.Clone() -env2['COMPILATIONDB_PATH_FILTER'] = 'build/linux64/*' -env2.CompilationDatabase('compile_commands-linux64.json') - - - compile_commands-linux32.json contains: - - -[ - { - "command": "gcc -m32 -o build/linux32/test_main.o -c test_main.c", - "directory": "/home/user/sandbox", - "file": "test_main.c", - "output": "build/linux32/test_main.o" - } -] - - - compile_commands-linux64.json contains: - - -[ - { - "command": "gcc -m64 -o build/linux64/test_main.o -c test_main.c", - "directory": "/home/user/sandbox", - "file": "test_main.c", - "output": "build/linux64/test_main.o" - } -] - - -
+
+ Creating a Compilation Database + + + + Tooling to perform analysis and modification + of source code often needs to know not only the source code + itself, but also how it will be compiled, as the compilation line + affects the behavior of macros, includes, etc. &SCons; has a + record of this information once it has run, in the form of + Actions associated with the sources, and can emit this information + so tools can use it. + + + + + + The Clang project has defined a JSON Compilation Database. + This database is in common use as input into Clang tools + and many IDEs and editors as well. + See + + JSON Compilation Database Format Specification + + for complete information. &SCons; can emit a + compilation database in this format + by enabling the &t-link-compilation_db; tool + and calling the &b-link-CompilationDatabase; builder + (available since &scons; 4.0). + + + + + + The compilation database can be populated with + source and output files either with paths relative + to the top of the build, or using absolute paths. + This is controlled by + COMPILATIONDB_USE_ABSPATH=(True|False) + which defaults to False. + The entries in this file can be filtered by using + + COMPILATIONDB_PATH_FILTER='pattern' + where the filter pattern is a string following the Python + + fnmatch + + syntax. + This filtering can be used for outputting different + build variants to different compilation database files. + + + + + + The following example illustrates generating a compilation + database containing absolute paths: + + + + + + env = Environment(COMPILATIONDB_USE_ABSPATH=True) + env.Tool('compilation_db') + env.CompilationDatabase() + env.Program('hello.c') + + + int main( int argc, char* argv[] ) + { + return 0; + } + + + + + scons -Q + + + + compile_commands.json + contains: + + + + [ + { + "command": "gcc -o hello.o -c hello.c", + "directory": "/home/user/sandbox", + "file": "/home/user/sandbox/hello.c", + "output": "/home/user/sandbox/hello.o" + } + ] + + + + + Notice that the generated database contains only an entry for + the hello.c/hello.o pairing, + and nothing for the generation of the final executable + hello + - the transformation of + hello.o + to + hello + does not have any information that affects interpretation + of the source code, + so it is not interesting to the compilation database. + + + + + + Although it can be a little surprising at first glance, + a compilation database target is, like any other target, + subject to &scons; target selection rules. + This means if you set a default target (that does not + include the compilation database), or use command-line + targets, it might not be selected for building. + This can actually be an advantage, since you don't + necessarily want to regenerate the compilation database + every build. + The following example + shows selecting relative paths (the default) + for output and source, + and also giving a non-default name to the database. + In order to be able to generate the database separately from building, + an alias is set referring to the database, + which can then be used as a target - here we are only + building the compilation database target, not the code. + + + + + + env = Environment() + env.Tool('compilation_db') + cdb = env.CompilationDatabase('compile_database.json') + Alias('cdb', cdb) + env.Program('test_main.c') + + + #include "test_main.h" + int main( int argc, char* argv[] ) + { + return 0; + } + + + /* dummy include file */ + + + + + scons -Q cdb + + + + compile_database.json + contains: + + + + [ + { + "command": "gcc -o test_main.o -c test_main.c", + "directory": "/home/user/sandbox", + "file": "test_main.c", + "output": "test_main.o" + } + ] + + + + + The following (incomplete) example shows using filtering + to separate build variants. + In the case of using variants, + you want different compilation databases for each, + since the build parameters differ, so the code analysis + needs to see the correct build lines for the 32-bit build + and 64-bit build hinted at here. + For simplicity of presentation, + the example omits the setup details of the variant directories: + + + + + env = Environment() + env.Tool('compilation_db') + + env1 = env.Clone() + env1['COMPILATIONDB_PATH_FILTER'] = 'build/linux32/*' + env1.CompilationDatabase('compile_commands-linux32.json') + + env2 = env.Clone() + env2['COMPILATIONDB_PATH_FILTER'] = 'build/linux64/*' + env2.CompilationDatabase('compile_commands-linux64.json') + + + + compile_commands-linux32.json + contains: + + + + [ + { + "command": "gcc -m32 -o build/linux32/test_main.o -c test_main.c", + "directory": "/home/user/sandbox", + "file": "test_main.c", + "output": "build/linux32/test_main.o" + } + ] + + + + compile_commands-linux64.json + contains: + + + + [ + { + "command": "gcc -m64 -o build/linux64/test_main.o -c test_main.c", + "directory": "/home/user/sandbox", + "file": "test_main.c", + "output": "build/linux64/test_main.o" + } + ] + + +
+ +
+ Ninja Build Generator + + + + This is an experimental new feature. It is subject to change and/or removal without depreciation cycle + + + To use this tool you must install pypi's ninja + package. + This can be done via + pip install ninja + + + To enable this feature you'll need to use one of the following + + + # On the command line + --experimental=ninja + + # Or in your SConstruct + SetOption('experimental', 'ninja') + + + + + + This tool will enabled creating a ninja build file from your SCons based build system. It can then invoke + ninja to run your build. For most builds ninja will be significantly faster, but you may have to give up + some accuracy. You are NOT advised to use this for production builds. It can however significantly speed up + your build/debug/compile iterations. + + + It's not expected that the ninja builder will work for all builds at this point. It's still under active + development. If you find that your build doesn't work with ninja please bring this to the users mailing list + or devel channel on our discord server. + + + Specifically if your build has many (or even any) python function actions you may find that the ninja build + will be slower as it will run ninja, which will then run SCons for each target created by a python action. + To alleviate some of these, especially those python based actions built into SCons there is special logic to + implement those actions via shell commands in the ninja build file. + + + + Ninja Build System + + + + + + Ninja File Format Specification + + +
diff --git a/requirements.txt b/requirements.txt index 520b90bb74..4093f19278 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,4 @@ sphinx<3.5.0 sphinx_rtd_theme lxml==4.6.3 rst2pdf +ninja \ No newline at end of file diff --git a/test/ninja/build_libraries.py b/test/ninja/build_libraries.py new file mode 100644 index 0000000000..eb8eb74590 --- /dev/null +++ b/test/ninja/build_libraries.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python +# +# 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 TestSCons +from TestCmd import IS_WINDOWS, IS_MACOS + +test = TestSCons.TestSCons() + +try: + import ninja +except ImportError: + test.skip_test("Could not find ninja module in python") + +ninja_binary = test.where_is('ninja') +if not ninja_binary: + test.skip_test("Could not find ninja. Skipping test.") + +_python_ = TestSCons._python_ +_exe = TestSCons._exe + +ninja_bin = os.path.abspath(os.path.join( + ninja.__file__, + os.pardir, + 'data', + 'bin', + 'ninja' + _exe)) + +test.dir_fixture('ninja-fixture') + +if IS_WINDOWS: + lib_suffix = '.lib' + staticlib_suffix = '.lib' + lib_prefix = '' + win32 = ", 'WIN32'" +else: + lib_suffix = '.so' + staticlib_suffix = '.a' + lib_prefix = 'lib' + win32 = '' + +if IS_MACOS: + lib_suffix = '.dylib' + +test.write('SConstruct', """ +SetOption('experimental','ninja') +DefaultEnvironment(tools=[]) +env = Environment() +env.Tool('ninja') +env['NINJA'] = "%(ninja_bin)s" + +shared_lib = env.SharedLibrary(target = 'test_impl', source = 'test_impl.c', CPPDEFINES=['LIBRARY_BUILD'%(win32)s]) +env.Program(target = 'test', source = 'test1.c', LIBS=['test_impl'], LIBPATH=['.'], RPATH='.') + +static_obj = env.Object(target = 'test_impl_static', source = 'test_impl.c') +static_lib = env.StaticLibrary(target = 'test_impl_static', source = static_obj) +static_obj = env.Object(target = 'test_static', source = 'test1.c') +env.Program(target = 'test_static', source = static_obj, LIBS=[static_lib], LIBPATH=['.']) +""" % locals()) +# generate simple build +test.run(stdout=None) +test.must_contain_all_lines(test.stdout(), ['Generating: build.ninja']) +test.must_contain_all(test.stdout(), 'Executing:') +test.must_contain_all(test.stdout(), 'ninja%(_exe)s -f' % locals()) +test.run(program=test.workpath('test'), stdout="library_function") +test.run(program=test.workpath('test_static'), stdout="library_function") + +# clean build and ninja files +test.run(arguments='-c', stdout=None) +test.must_contain_all_lines(test.stdout(), [ + ('Removed %stest_impl' % lib_prefix) + lib_suffix, + 'Removed test' + _exe, + ('Removed %stest_impl_static' % lib_prefix) + staticlib_suffix, + 'Removed test_static' + _exe, + 'Removed build.ninja']) + +# only generate the ninja file +test.run(arguments='--disable-execute-ninja', stdout=None) +test.must_contain_all_lines(test.stdout(), ['Generating: build.ninja']) +test.must_not_exist(test.workpath('test')) +test.must_not_exist(test.workpath('test_static')) + +# run ninja independently +program = test.workpath('run_ninja_env.bat') if IS_WINDOWS else ninja_bin +test.run(program=program, stdout=None) +test.run(program=test.workpath('test'), stdout="library_function") +test.run(program=test.workpath('test_static'), stdout="library_function") + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/test/ninja/copy_function_command.py b/test/ninja/copy_function_command.py new file mode 100644 index 0000000000..7e999b33e5 --- /dev/null +++ b/test/ninja/copy_function_command.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python +# +# 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 TestSCons +from TestCmd import IS_WINDOWS + +test = TestSCons.TestSCons() + +try: + import ninja +except ImportError: + test.skip_test("Could not find module in python") + +_python_ = TestSCons._python_ +_exe = TestSCons._exe + +ninja_bin = os.path.abspath(os.path.join( + ninja.__file__, + os.pardir, + 'data', + 'bin', + 'ninja' + _exe)) + +test.dir_fixture('ninja-fixture') + +test.write('SConstruct', """ +SetOption('experimental','ninja') +DefaultEnvironment(tools=[]) +env = Environment() +env.Tool('ninja') +env.Command('foo2.c', ['foo.c'], Copy('$TARGET','$SOURCE')) +env.Program(target = 'foo', source = 'foo2.c') +""") + +# generate simple build +test.run(stdout=None) +test.must_contain_all_lines(test.stdout(), ['Generating: build.ninja']) +test.must_contain_all(test.stdout(), 'Executing:') +test.must_contain_all(test.stdout(), 'ninja%(_exe)s -f' % locals()) +test.run(program=test.workpath('foo'), stdout="foo.c") + +# clean build and ninja files +test.run(arguments='-c', stdout=None) +test.must_contain_all_lines(test.stdout(), [ + 'Removed foo2.o', + 'Removed foo2.c', + 'Removed foo' + _exe, + 'Removed build.ninja']) + +# only generate the ninja file +test.run(arguments='--disable-execute-ninja', stdout=None) +test.must_contain_all_lines(test.stdout(), ['Generating: build.ninja']) +test.must_not_exist(test.workpath('foo')) + +# run ninja independently +program = test.workpath('run_ninja_env.bat') if IS_WINDOWS else ninja_bin +test.run(program=program, stdout=None) +test.run(program=test.workpath('foo'), stdout="foo.c") + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/test/ninja/generate_and_build.py b/test/ninja/generate_and_build.py new file mode 100644 index 0000000000..91be108176 --- /dev/null +++ b/test/ninja/generate_and_build.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python +# +# 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 TestSCons +from TestCmd import IS_WINDOWS + +test = TestSCons.TestSCons() + +try: + import ninja +except ImportError: + test.skip_test("Could not find module in python") + +_python_ = TestSCons._python_ +_exe = TestSCons._exe + +ninja_bin = os.path.abspath(os.path.join( + ninja.__file__, + os.pardir, + 'data', + 'bin', + 'ninja' + _exe)) + +test.dir_fixture('ninja-fixture') + +test.file_fixture('ninja_test_sconscripts/sconstruct_generate_and_build', 'SConstruct') + +# generate simple build +test.run(stdout=None) +test.must_contain_all_lines(test.stdout(), ['Generating: build.ninja']) +test.must_contain_all(test.stdout(), 'Executing:') +test.must_contain_all(test.stdout(), 'ninja%(_exe)s -f' % locals()) +test.run(program=test.workpath('foo' + _exe), stdout="foo.c") + +# clean build and ninja files +test.run(arguments='-c', stdout=None) +test.must_contain_all_lines(test.stdout(), [ + 'Removed foo.o', + 'Removed foo', + 'Removed build.ninja']) + +# only generate the ninja file +test.run(arguments='--disable-execute-ninja', stdout=None) +test.must_contain_all_lines(test.stdout(), ['Generating: build.ninja']) +test.must_not_exist(test.workpath('foo' + _exe)) + +# run ninja independently +program = test.workpath('run_ninja_env.bat') if IS_WINDOWS else ninja_bin +test.run(program=program, stdout=None) +test.run(program=test.workpath('foo' + _exe), stdout="foo.c") + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/test/ninja/generate_and_build_cxx.py b/test/ninja/generate_and_build_cxx.py new file mode 100644 index 0000000000..074a5cb9af --- /dev/null +++ b/test/ninja/generate_and_build_cxx.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python +# +# 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 TestSCons +from TestCmd import IS_WINDOWS + +test = TestSCons.TestSCons() + +try: + import ninja +except ImportError: + test.skip_test("Could not find module in python") + +_python_ = TestSCons._python_ +_exe = TestSCons._exe + +ninja_bin = os.path.abspath(os.path.join( + ninja.__file__, + os.pardir, + 'data', + 'bin', + 'ninja' + _exe)) + +test.dir_fixture('ninja-fixture') + +test.file_fixture('ninja_test_sconscripts/sconstruct_generate_and_build_cxx', + 'SConstruct') + +# generate simple build +test.run() +test.must_contain_all_lines(test.stdout(), ['Generating: build.ninja']) +test.must_contain_all(test.stdout(), 'Executing:') +test.must_contain_all(test.stdout(), 'ninja%(_exe)s -f' % locals()) +test.run(program=test.workpath('test2' + _exe), stdout="print_function") + +# clean build and ninja files +test.run(arguments='-c', stdout=None) +test.must_contain_all_lines(test.stdout(), [ + 'Removed test2.o', + 'Removed test2', + 'Removed build.ninja']) + +# only generate the ninja file +test.run(arguments='--disable-execute-ninja', stdout=None) +test.must_contain_all_lines(test.stdout(), ['Generating: build.ninja']) +test.must_not_exist(test.workpath('test2' + _exe)) + +# run ninja independently +program = test.workpath('run_ninja_env.bat') if IS_WINDOWS else ninja_bin +test.run(program=program, stdout=None) +test.run(program=test.workpath('test2' + _exe), stdout="print_function") + +test.write('test2.hpp', """ +#include +#include + +class Foo +{ +public: + int print_function(); + int print_function2(){ + std::cout << "2"; + return 0; + }; +}; +""") + +# generate simple build +test.run(program=program, stdout=None) +test.run(program=test.workpath('test2' + _exe), stdout="print_function2") + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/test/ninja/generate_source.py b/test/ninja/generate_source.py new file mode 100644 index 0000000000..8300176c2e --- /dev/null +++ b/test/ninja/generate_source.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python +# +# 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 TestSCons +from TestCmd import IS_WINDOWS + +test = TestSCons.TestSCons() + +try: + import ninja +except ImportError: + test.skip_test("Could not find module in python") + +_python_ = TestSCons._python_ +_exe = TestSCons._exe + +ninja_bin = os.path.abspath(os.path.join( + ninja.__file__, + os.pardir, + 'data', + 'bin', + 'ninja' + _exe)) + +test.dir_fixture('ninja-fixture') + +shell = '' if IS_WINDOWS else './' + +test.write('SConstruct', """ +SetOption('experimental','ninja') +DefaultEnvironment(tools=[]) + +env = Environment() +env.Tool('ninja') +prog = env.Program(target = 'generate_source', source = 'generate_source.c') +env.Command('generated_source.c', prog, '%(shell)sgenerate_source%(_exe)s') +env.Program(target = 'generated_source', source = 'generated_source.c') +""" % locals()) + +test.write('generate_source.c', """ +#include + +int main(int argc, char *argv[]) { + FILE *fp; + + fp = fopen("generated_source.c", "w"); + fprintf(fp, "#include \\n"); + fprintf(fp, "#include \\n"); + fprintf(fp, "\\n"); + fprintf(fp, "int\\n"); + fprintf(fp, "main(int argc, char *argv[])\\n"); + fprintf(fp, "{\\n"); + fprintf(fp, " printf(\\"generated_source.c\\");\\n"); + fprintf(fp, " exit (0);\\n"); + fprintf(fp, "}\\n"); + fclose(fp); +} +""") + +# generate simple build +test.run(stdout=None) +test.must_contain_all_lines(test.stdout(), ['Generating: build.ninja']) +test.must_contain_all(test.stdout(), 'Executing:') +test.must_contain_all(test.stdout(), 'ninja%(_exe)s -f' % locals()) +test.run(program=test.workpath('generated_source' + _exe), stdout="generated_source.c") + +# clean build and ninja files +test.run(arguments='-c', stdout=None) +test.must_contain_all_lines(test.stdout(), [ + 'Removed generate_source.o', + 'Removed generate_source' + _exe, + 'Removed generated_source.c', + 'Removed generated_source.o', + 'Removed generated_source' + _exe, + 'Removed build.ninja']) + +# only generate the ninja file +test.run(arguments='--disable-execute-ninja', stdout=None) +test.must_contain_all_lines(test.stdout(), ['Generating: build.ninja']) +test.must_not_exist(test.workpath('generated_source' + _exe)) + +# run ninja independently +program = test.workpath('run_ninja_env.bat') if IS_WINDOWS else ninja_bin +test.run(program=program, stdout=None) +test.run(program=test.workpath('generated_source' + _exe), stdout="generated_source.c") + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/test/ninja/iterative_speedup.py b/test/ninja/iterative_speedup.py new file mode 100644 index 0000000000..05e372cf56 --- /dev/null +++ b/test/ninja/iterative_speedup.py @@ -0,0 +1,244 @@ +#!/usr/bin/env python +# +# 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 random +import time + +import TestSCons +from TestCmd import IS_WINDOWS + +test = TestSCons.TestSCons() + +try: + import ninja +except ImportError: + test.skip_test("Could not find module in python") + +_python_ = TestSCons._python_ +_exe = TestSCons._exe + +ninja_bin = os.path.abspath(os.path.join( + ninja.__file__, + os.pardir, + 'data', + 'bin', + 'ninja' + _exe)) + +test.dir_fixture('ninja-fixture') + +test.write('source_0.c', """ +#include +#include +#include "source_0.h" + +int +print_function0() +{ + printf("main print"); + return 0; +} +""") + +test.write('source_0.h', """ +#include +#include + +int +print_function0(); +""") + + +def get_num_cpus(): + """ + Function to get the number of CPUs the system has. + """ + # Linux, Unix and MacOS: + if hasattr(os, "sysconf"): + if 'SC_NPROCESSORS_ONLN' in os.sysconf_names: + # Linux & Unix: + ncpus = os.sysconf("SC_NPROCESSORS_ONLN") + if isinstance(ncpus, int) and ncpus > 0: + return ncpus + # OSX: + return int(os.popen("sysctl -n hw.ncpu")[1].read()) + # Windows: + if 'NUMBER_OF_PROCESSORS' in os.environ: + ncpus = int(os.environ["NUMBER_OF_PROCESSORS"]) + if ncpus > 0: + return ncpus + # Default + return 1 + + +def generate_source(parent_source, current_source): + test.write('source_{}.c'.format(current_source), """ + #include + #include + #include "source_%(parent_source)s.h" + #include "source_%(current_source)s.h" + + int + print_function%(current_source)s() + { + return print_function%(parent_source)s(); + } + """ % locals()) + + test.write('source_{}.h'.format(current_source), """ + #include + #include + + int + print_function%(current_source)s(); + """ % locals()) + + +def mod_source_return(test_num): + parent_source = test_num - 1 + test.write('source_{}.c'.format(test_num), """ + #include + #include + #include "source_%(parent_source)s.h" + #include "source_%(test_num)s.h" + + int + print_function%(test_num)s() + { + int test = 5 + 5; + print_function%(parent_source)s(); + return test; + } + """ % locals()) + + +def mod_source_orig(test_num): + parent_source = test_num - 1 + test.write('source_{}.c'.format(test_num), """ + #include + #include + #include "source_%(parent_source)s.h" + #include "source_%(test_num)s.h" + + int + print_function%(test_num)s() + { + return print_function%(parent_source)s(); + } + """ % locals()) + + +num_source = 200 +for i in range(1, num_source + 1): + generate_source(i - 1, i) + +test.write('main.c', """ +#include +#include +#include "source_%(num_source)s.h" +int +main() +{ + print_function%(num_source)s(); + exit(0); +} +""" % locals()) + +test.write('SConstruct', """ +SetOption('experimental','ninja') +DefaultEnvironment(tools=[]) + +env = Environment() +env.Tool('ninja') +sources = ['main.c'] + env.Glob('source*.c') +env.Program(target = 'print_bin', source = sources) +""") + +test.write('SConstruct_no_ninja', """ +env = Environment() +sources = ['main.c'] + env.Glob('source*.c') +env.Program(target = 'print_bin', source = sources) +""") + +tests_mods = [] +ninja_times = [] +scons_times = [] +for _ in range(10): + tests_mods += [random.randrange(1, num_source, 1)] +jobs = '-j' + str(get_num_cpus()) + +ninja_program = [test.workpath('run_ninja_env.bat'), jobs] if IS_WINDOWS else [ninja_bin, jobs] + +start = time.perf_counter() +test.run(arguments='--disable-execute-ninja', stdout=None) +test.run(program=ninja_program, stdout=None) +stop = time.perf_counter() +ninja_times += [stop - start] +test.run(program=test.workpath('print_bin'), stdout="main print") + +for test_mod in tests_mods: + mod_source_return(test_mod) + start = time.perf_counter() + test.run(program=ninja_program, stdout=None) + stop = time.perf_counter() + ninja_times += [stop - start] + +for test_mod in tests_mods: + mod_source_orig(test_mod) + +# clean build and ninja files +test.run(arguments='-c', stdout=None) +test.must_contain_all_lines(test.stdout(), [ + 'Removed build.ninja']) + +start = time.perf_counter() +test.run(arguments=["-f", "SConstruct_no_ninja", jobs], stdout=None) +stop = time.perf_counter() +scons_times += [stop - start] +test.run(program=test.workpath('print_bin'), stdout="main print") + +for test_mod in tests_mods: + mod_source_return(test_mod) + start = time.perf_counter() + test.run(arguments=["-f", "SConstruct_no_ninja", jobs], stdout=None) + stop = time.perf_counter() + scons_times += [stop - start] + +full_build_print = True +for ninja_time, scons_time in zip(ninja_times, scons_times): + if ninja_time > scons_time: + test.fail_test(message="Ninja was slower than SCons: SCons: {:.3f}s Ninja: {:.3f}s".format(scons_time, ninja_time)) + if full_build_print: + full_build_print = False + print("Clean build {} files - SCons: {:.3f}s Ninja: {:.3f}s".format(num_source, scons_time, ninja_time)) + else: + print("Single File Rebuild - SCons: {:.3f}s Ninja: {:.3f}s".format(scons_time, ninja_time)) + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/test/ninja/multi_env.py b/test/ninja/multi_env.py new file mode 100644 index 0000000000..e5da6cf885 --- /dev/null +++ b/test/ninja/multi_env.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python +# +# 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 TestSCons +from TestCmd import IS_WINDOWS + +test = TestSCons.TestSCons() + +try: + import ninja +except ImportError: + test.skip_test("Could not find module in python") + +_python_ = TestSCons._python_ +_exe = TestSCons._exe + +ninja_bin = os.path.abspath(os.path.join( + ninja.__file__, + os.pardir, + 'data', + 'bin', + 'ninja' + _exe)) + +test = TestSCons.TestSCons() + +test.dir_fixture('ninja-fixture') + +test.write('SConstruct', """ +SetOption('experimental','ninja') +DefaultEnvironment(tools=[]) + +env = Environment() +env.Tool('ninja') +env.Program(target = 'foo', source = 'foo.c') + +env2 = Environment() +env2.Program(target = 'bar', source = 'bar.c') +""") + +# generate simple build +test.run(stdout=None) +test.must_contain_all_lines(test.stdout(), ['Generating: build.ninja']) +test.must_contain_all(test.stdout(), 'Executing:') +test.must_contain_all(test.stdout(), 'ninja%(_exe)s -f' % locals()) +test.run(program=test.workpath('foo' + _exe), stdout="foo.c") +test.run(program=test.workpath('bar' + _exe), stdout="bar.c") + +# clean build and ninja files +test.run(arguments='-c', stdout=None) +test.must_contain_all_lines(test.stdout(), [ + 'Removed foo.o', + 'Removed foo' + _exe, + 'Removed bar.o', + 'Removed bar' + _exe, + 'Removed build.ninja']) + +# only generate the ninja file +test.run(arguments='--disable-execute-ninja', stdout=None) +test.must_contain_all_lines(test.stdout(), ['Generating: build.ninja']) +test.must_not_exist(test.workpath('foo' + _exe)) +test.must_not_exist(test.workpath('bar' + _exe)) + +# run ninja independently +program = test.workpath('run_ninja_env.bat') if IS_WINDOWS else ninja_bin +test.run(program=program, stdout=None) +test.run(program=test.workpath('foo' + _exe), stdout="foo.c") +test.run(program=test.workpath('bar' + _exe), stdout="bar.c") + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/test/ninja/ninja-fixture/bar.c b/test/ninja/ninja-fixture/bar.c new file mode 100644 index 0000000000..15b2ecc46a --- /dev/null +++ b/test/ninja/ninja-fixture/bar.c @@ -0,0 +1,10 @@ +#include +#include + +int +main(int argc, char *argv[]) +{ + argv[argc++] = "--"; + printf("bar.c"); + exit (0); +} diff --git a/test/ninja/ninja-fixture/foo.c b/test/ninja/ninja-fixture/foo.c new file mode 100644 index 0000000000..ba35c687a2 --- /dev/null +++ b/test/ninja/ninja-fixture/foo.c @@ -0,0 +1,10 @@ +#include +#include + +int +main(int argc, char *argv[]) +{ + argv[argc++] = "--"; + printf("foo.c"); + exit (0); +} diff --git a/test/ninja/ninja-fixture/test1.c b/test/ninja/ninja-fixture/test1.c new file mode 100644 index 0000000000..678461f508 --- /dev/null +++ b/test/ninja/ninja-fixture/test1.c @@ -0,0 +1,21 @@ +#include +#include + +#ifdef WIN32 +#ifdef LIBRARY_BUILD +#define DLLEXPORT __declspec(dllexport) +#else +#define DLLEXPORT __declspec(dllimport) +#endif +#else +#define DLLEXPORT +#endif + +DLLEXPORT extern int library_function(void); + +int +main(int argc, char *argv[]) +{ + library_function(); + exit(0); +} diff --git a/test/ninja/ninja-fixture/test2.cpp b/test/ninja/ninja-fixture/test2.cpp new file mode 100644 index 0000000000..69b54c97f7 --- /dev/null +++ b/test/ninja/ninja-fixture/test2.cpp @@ -0,0 +1,16 @@ +#include "test2.hpp" + +int +main(int argc, char *argv[]) +{ + Foo* test = new Foo(); + test->print_function(); + test->print_function2(); + return 0; +} + +int Foo::print_function() +{ + std::cout << "print_function"; + return 0; +} \ No newline at end of file diff --git a/test/ninja/ninja-fixture/test2.hpp b/test/ninja/ninja-fixture/test2.hpp new file mode 100644 index 0000000000..f0583fc547 --- /dev/null +++ b/test/ninja/ninja-fixture/test2.hpp @@ -0,0 +1,9 @@ +#include +#include + +class Foo +{ +public: + int print_function(); + int print_function2(){return 0;}; +}; diff --git a/test/ninja/ninja-fixture/test_impl.c b/test/ninja/ninja-fixture/test_impl.c new file mode 100644 index 0000000000..ac3fd88656 --- /dev/null +++ b/test/ninja/ninja-fixture/test_impl.c @@ -0,0 +1,20 @@ +#include +#include + +#ifdef WIN32 +#ifdef LIBRARY_BUILD +#define DLLEXPORT __declspec(dllexport) +#else +#define DLLEXPORT __declspec(dllimport) +#endif +#else +#define DLLEXPORT +#endif + + +DLLEXPORT int +library_function(void) +{ + printf("library_function"); + return 0; +} diff --git a/test/ninja/ninja_test_sconscripts/sconstest.skip b/test/ninja/ninja_test_sconscripts/sconstest.skip new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/ninja/ninja_test_sconscripts/sconstruct_generate_and_build b/test/ninja/ninja_test_sconscripts/sconstruct_generate_and_build new file mode 100644 index 0000000000..81a4366755 --- /dev/null +++ b/test/ninja/ninja_test_sconscripts/sconstruct_generate_and_build @@ -0,0 +1,6 @@ +SetOption('experimental','ninja') +DefaultEnvironment(tools=[]) + +env = Environment() +env.Tool('ninja') +env.Program(target='foo', source='foo.c') diff --git a/test/ninja/ninja_test_sconscripts/sconstruct_generate_and_build_cxx b/test/ninja/ninja_test_sconscripts/sconstruct_generate_and_build_cxx new file mode 100644 index 0000000000..f7137dfb6e --- /dev/null +++ b/test/ninja/ninja_test_sconscripts/sconstruct_generate_and_build_cxx @@ -0,0 +1,5 @@ +SetOption('experimental','ninja') +DefaultEnvironment(tools=[]) +env = Environment() +env.Tool('ninja') +env.Program(target = 'test2', source = 'test2.cpp') diff --git a/test/ninja/shell_command.py b/test/ninja/shell_command.py new file mode 100644 index 0000000000..a6926c778d --- /dev/null +++ b/test/ninja/shell_command.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python +# +# 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 TestSCons +from TestCmd import IS_WINDOWS + +test = TestSCons.TestSCons() + +try: + import ninja +except ImportError: + test.skip_test("Could not find module in python") + +_python_ = TestSCons._python_ +_exe = TestSCons._exe + +ninja_bin = os.path.abspath(os.path.join( + ninja.__file__, + os.pardir, + 'data', + 'bin', + 'ninja' + _exe)) + +test.dir_fixture('ninja-fixture') + +shell = '' if IS_WINDOWS else './' + +test.write('SConstruct', """ +SetOption('experimental','ninja') +DefaultEnvironment(tools=[]) + +env = Environment() +env.Tool('ninja') +prog = env.Program(target = 'foo', source = 'foo.c') +env.Command('foo.out', prog, '%(shell)sfoo%(_exe)s > foo.out') +""" % locals()) + +# generate simple build +test.run(stdout=None) +test.must_contain_all_lines(test.stdout(), ['Generating: build.ninja']) +test.must_contain_all(test.stdout(), 'Executing:') +test.must_contain_all(test.stdout(), 'ninja%(_exe)s -f' % locals()) +test.must_match('foo.out', 'foo.c') + +# clean build and ninja files +test.run(arguments='-c', stdout=None) +test.must_contain_all_lines(test.stdout(), [ + 'Removed foo.o', + 'Removed foo%(_exe)s' % locals(), + 'Removed foo.out', + 'Removed build.ninja']) + +# only generate the ninja file +test.run(arguments='--disable-execute-ninja', stdout=None) +test.must_contain_all_lines(test.stdout(), ['Generating: build.ninja']) +test.must_not_exist(test.workpath('foo.out')) + +# run ninja independently +program = test.workpath('run_ninja_env.bat') if IS_WINDOWS else ninja_bin +test.run(program=program, stdout=None) +test.must_match('foo.out', 'foo.c') + +test.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/test/option/option--experimental.py b/test/option/option--experimental.py index 0f0e9ec677..2e06dc3777 100644 --- a/test/option/option--experimental.py +++ b/test/option/option--experimental.py @@ -35,12 +35,13 @@ tests = [ ('.', []), - ('--experimental=all', ['transporter', 'warp_speed']), + ('--experimental=ninja', ['ninja']), + ('--experimental=all', ['ninja', 'transporter', 'warp_speed']), ('--experimental=none', []), ] for args, exper in tests: - read_string = """All Features=transporter,warp_speed + read_string = """All Features=ninja,transporter,warp_speed Experimental=%s """ % (exper) test.run(arguments=args, @@ -49,7 +50,7 @@ test.run(arguments='--experimental=warp_drive', stderr="""usage: scons [OPTION] [TARGET] ... -SCons Error: option --experimental: invalid choice: 'warp_drive' (choose from 'all','none','transporter','warp_speed') +SCons Error: option --experimental: invalid choice: 'warp_drive' (choose from 'all','none','ninja','transporter','warp_speed') """, status=2) diff --git a/testing/framework/TestCmd.py b/testing/framework/TestCmd.py index 333901c6a9..903bee693a 100644 --- a/testing/framework/TestCmd.py +++ b/testing/framework/TestCmd.py @@ -314,6 +314,7 @@ IS_WINDOWS = sys.platform == 'win32' +IS_MACOS = sys.platform == 'darwin' IS_64_BIT = sys.maxsize > 2**32 IS_PYPY = hasattr(sys, 'pypy_translation_info')