Followon to PR #4348: more bool fixes
[scons.git] / SCons / Tool / ninja / NinjaState.py
blob707a9e20d5bcb7f9da175c399258a747c5147226
1 # MIT License
3 # Copyright The SCons Foundation
5 # Permission is hereby granted, free of charge, to any person obtaining
6 # a copy of this software and associated documentation files (the
7 # "Software"), to deal in the Software without restriction, including
8 # "Software"), to deal in the Software without restriction, including
9 # without limitation the rights to use, copy, modify, merge, publish,
10 # distribute, sublicense, and/or sell copies of the Software, and to
11 # permit persons to whom the Software is furnished to do so, subject to
12 # the following conditions:
14 # The above copyright notice and this permission notice shall be included
15 # in all copies or substantial portions of the Software.
17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
18 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
19 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25 import io
26 import os
27 import pathlib
28 import signal
29 import tempfile
30 import shutil
31 import sys
32 import random
33 import filecmp
34 from os.path import splitext
35 from tempfile import NamedTemporaryFile
36 import ninja
37 import hashlib
39 import SCons
40 from SCons.Script import COMMAND_LINE_TARGETS
41 from SCons.Util import wait_for_process_to_die
42 from SCons.Errors import InternalError
43 from .Globals import COMMAND_TYPES, NINJA_RULES, NINJA_POOLS, \
44 NINJA_CUSTOM_HANDLERS, NINJA_DEFAULT_TARGETS
45 from .Rules import _install_action_function, _mkdir_action_function, _lib_symlink_action_function, _copy_action_function
46 from .Utils import get_path, alias_to_ninja_build, generate_depfile, ninja_noop, get_order_only, \
47 get_outputs, get_inputs, get_dependencies, get_rule, get_command_env, to_escaped_list, ninja_sorted_build
48 from .Methods import get_command
51 # pylint: disable=too-many-instance-attributes
52 class NinjaState:
53 """Maintains state of Ninja build system as it's translated from SCons."""
55 def __init__(self, env, ninja_file, ninja_syntax) -> None:
56 self.env = env
57 self.ninja_file = ninja_file
59 self.ninja_bin_path = env.get('NINJA')
60 if not self.ninja_bin_path:
61 # default to using ninja installed with python module
62 ninja_bin = 'ninja.exe' if env["PLATFORM"] == "win32" else 'ninja'
63 self.ninja_bin_path = os.path.abspath(os.path.join(
64 ninja.__file__,
65 os.pardir,
66 'data',
67 'bin',
68 ninja_bin))
69 if not os.path.exists(self.ninja_bin_path):
70 # couldn't find it, just give the bin name and hope
71 # its in the path later
72 self.ninja_bin_path = ninja_bin
73 self.ninja_syntax = ninja_syntax
74 self.writer_class = ninja_syntax.Writer
75 self.__generated = False
76 self.translator = SConsToNinjaTranslator(env)
77 self.generated_suffixes = env.get("NINJA_GENERATED_SOURCE_SUFFIXES", [])
79 # List of generated builds that will be written at a later stage
80 self.builds = dict()
82 # SCons sets this variable to a function which knows how to do
83 # shell quoting on whatever platform it's run on. Here we use it
84 # to make the SCONS_INVOCATION variable properly quoted for things
85 # like CCFLAGS
86 scons_escape = env.get("ESCAPE", lambda x: x)
88 # The daemon port should be the same across runs, unless explicitly set
89 # or if the portfile is deleted. This ensures the ninja file is deterministic
90 # across regen's if nothings changed. The construction var should take preference,
91 # then portfile is next, and then otherwise create a new random port to persist in
92 # use.
93 scons_daemon_port = None
94 os.makedirs(get_path(self.env.get("NINJA_DIR")), exist_ok=True)
95 scons_daemon_port_file = str(pathlib.Path(get_path(self.env.get("NINJA_DIR"))) / "scons_daemon_portfile")
97 if env.get('NINJA_SCONS_DAEMON_PORT') is not None:
98 scons_daemon_port = int(env.get('NINJA_SCONS_DAEMON_PORT'))
99 elif os.path.exists(scons_daemon_port_file):
100 with open(scons_daemon_port_file) as f:
101 scons_daemon_port = int(f.read())
102 else:
103 scons_daemon_port = random.randint(10000, 60000)
105 with open(scons_daemon_port_file, 'w') as f:
106 f.write(str(scons_daemon_port))
108 # if SCons was invoked from python, we expect the first arg to be the scons.py
109 # script, otherwise scons was invoked from the scons script
110 python_bin = ''
111 if os.path.basename(sys.argv[0]) == 'scons.py':
112 python_bin = ninja_syntax.escape(scons_escape(sys.executable))
113 self.variables = {
114 "COPY": "cmd.exe /c 1>NUL copy" if sys.platform == "win32" else "cp",
115 'PORT': scons_daemon_port,
116 'NINJA_DIR_PATH': env.get('NINJA_DIR').abspath,
117 'PYTHON_BIN': sys.executable,
118 'NINJA_TOOL_DIR': pathlib.Path(__file__).parent,
119 'NINJA_SCONS_DAEMON_KEEP_ALIVE': str(env.get('NINJA_SCONS_DAEMON_KEEP_ALIVE')),
120 "SCONS_INVOCATION": '{} {} --disable-ninja __NINJA_NO=1 $out'.format(
121 python_bin,
122 " ".join(
123 [ninja_syntax.escape(scons_escape(arg)) for arg in sys.argv if arg not in COMMAND_LINE_TARGETS]
126 "SCONS_INVOCATION_W_TARGETS": "{} {} NINJA_DISABLE_AUTO_RUN=1".format(
127 python_bin, " ".join([
128 ninja_syntax.escape(scons_escape(arg))
129 for arg in sys.argv
130 if arg != 'NINJA_DISABLE_AUTO_RUN=1'])
132 # This must be set to a global default per:
133 # https://ninja-build.org/manual.html#_deps
134 # English Visual Studio will have the default below,
135 # otherwise the user can define the variable in the first environment
136 # that initialized ninja tool
137 "msvc_deps_prefix": env.get("NINJA_MSVC_DEPS_PREFIX", "Note: including file:")
140 self.rules = {
141 "CMD": {
142 "command": "cmd /c $env$cmd $in $out" if sys.platform == "win32" else "$env$cmd $in $out",
143 "description": "Building $out",
144 "pool": "local_pool",
146 "GENERATED_CMD": {
147 "command": "cmd /c $env$cmd" if sys.platform == "win32" else "$env$cmd",
148 "description": "Building $out",
149 "pool": "local_pool",
151 # We add the deps processing variables to this below. We
152 # don't pipe these through cmd.exe on Windows because we
153 # use this to generate a compile_commands.json database
154 # which can't use the shell command as it's compile
155 # command.
156 "CC_RSP": {
157 "command": "$env$CC @$out.rsp",
158 "description": "Compiling $out",
159 "rspfile": "$out.rsp",
160 "rspfile_content": "$rspc",
162 "CXX_RSP": {
163 "command": "$env$CXX @$out.rsp",
164 "description": "Compiling $out",
165 "rspfile": "$out.rsp",
166 "rspfile_content": "$rspc",
168 "LINK_RSP": {
169 "command": "$env$LINK @$out.rsp",
170 "description": "Linking $out",
171 "rspfile": "$out.rsp",
172 "rspfile_content": "$rspc",
173 "pool": "local_pool",
175 # Ninja does not automatically delete the archive before
176 # invoking ar. The ar utility will append to an existing archive, which
177 # can cause duplicate symbols if the symbols moved between object files.
178 # Native SCons will perform this operation so we need to force ninja
179 # to do the same. See related for more info:
180 # https://jira.mongodb.org/browse/SERVER-49457
181 "AR_RSP": {
182 "command": "{}$env$AR @$out.rsp".format(
183 '' if sys.platform == "win32" else "rm -f $out && "
185 "description": "Archiving $out",
186 "rspfile": "$out.rsp",
187 "rspfile_content": "$rspc",
188 "pool": "local_pool",
190 "CC": {
191 "command": "$env$CC $rspc",
192 "description": "Compiling $out",
194 "CXX": {
195 "command": "$env$CXX $rspc",
196 "description": "Compiling $out",
198 "LINK": {
199 "command": "$env$LINK $rspc",
200 "description": "Linking $out",
201 "pool": "local_pool",
203 "AR": {
204 "command": "{}$env$AR $rspc".format(
205 '' if sys.platform == "win32" else "rm -f $out && "
207 "description": "Archiving $out",
208 "pool": "local_pool",
210 "SYMLINK": {
211 "command": (
212 "cmd /c mklink $out $in"
213 if sys.platform == "win32"
214 else "ln -s $in $out"
216 "description": "Symlink $in -> $out",
218 "INSTALL": {
219 "command": "$COPY $in $out",
220 "description": "Install $out",
221 "pool": "install_pool",
222 # On Windows cmd.exe /c copy does not always correctly
223 # update the timestamp on the output file. This leads
224 # to a stuck constant timestamp in the Ninja database
225 # and needless rebuilds.
227 # Adding restat here ensures that Ninja always checks
228 # the copy updated the timestamp and that Ninja has
229 # the correct information.
230 "restat": 1,
232 "TEMPLATE": {
233 "command": "$PYTHON_BIN $NINJA_TOOL_DIR/ninja_daemon_build.py $PORT $NINJA_DIR_PATH $out",
234 "description": "Defer to SCons to build $out",
235 "pool": "local_pool",
236 "restat": 1
238 "EXIT_SCONS_DAEMON": {
239 "command": "$PYTHON_BIN $NINJA_TOOL_DIR/ninja_daemon_build.py $PORT $NINJA_DIR_PATH --exit",
240 "description": "Shutting down ninja scons daemon server",
241 "pool": "local_pool",
242 "restat": 1
244 "SCONS": {
245 "command": "$SCONS_INVOCATION $out",
246 "description": "$SCONS_INVOCATION $out",
247 "pool": "scons_pool",
248 # restat
249 # if present, causes Ninja to re-stat the command's outputs
250 # after execution of the command. Each output whose
251 # modification time the command did not change will be
252 # treated as though it had never needed to be built. This
253 # may cause the output's reverse dependencies to be removed
254 # from the list of pending build actions.
256 # We use restat any time we execute SCons because
257 # SCons calls in Ninja typically create multiple
258 # targets. But since SCons is doing it's own up to
259 # date-ness checks it may only update say one of
260 # them. Restat will find out which of the multiple
261 # build targets did actually change then only rebuild
262 # those targets which depend specifically on that
263 # output.
264 "restat": 1,
267 "SCONS_DAEMON": {
268 "command": "$PYTHON_BIN $NINJA_TOOL_DIR/ninja_run_daemon.py $PORT $NINJA_DIR_PATH $NINJA_SCONS_DAEMON_KEEP_ALIVE $SCONS_INVOCATION",
269 "description": "Starting scons daemon...",
270 "pool": "local_pool",
271 # restat
272 # if present, causes Ninja to re-stat the command's outputs
273 # after execution of the command. Each output whose
274 # modification time the command did not change will be
275 # treated as though it had never needed to be built. This
276 # may cause the output's reverse dependencies to be removed
277 # from the list of pending build actions.
279 # We use restat any time we execute SCons because
280 # SCons calls in Ninja typically create multiple
281 # targets. But since SCons is doing it's own up to
282 # date-ness checks it may only update say one of
283 # them. Restat will find out which of the multiple
284 # build targets did actually change then only rebuild
285 # those targets which depend specifically on that
286 # output.
287 "restat": 1,
289 "REGENERATE": {
290 "command": "$SCONS_INVOCATION_W_TARGETS",
291 "description": "Regenerating $self",
292 "generator": 1,
293 "pool": "console",
294 "restat": 1,
298 if env['PLATFORM'] == 'darwin' and env.get('AR', "") == 'ar':
299 self.rules["AR"] = {
300 "command": "rm -f $out && $env$AR $rspc",
301 "description": "Archiving $out",
302 "pool": "local_pool",
304 self.pools = {"scons_pool": 1}
306 def add_build(self, node) -> bool:
307 if not node.has_builder():
308 return False
310 if isinstance(node, SCons.Node.Python.Value):
311 return False
313 if isinstance(node, SCons.Node.Alias.Alias):
314 build = alias_to_ninja_build(node)
315 else:
316 build = self.translator.action_to_ninja_build(node)
318 # Some things are unbuild-able or need not be built in Ninja
319 if build is None:
320 return False
322 node_string = str(node)
323 if node_string in self.builds:
324 # TODO: If we work out a way to handle Alias() with same name as file this logic can be removed
325 # This works around adding Alias with the same name as a Node.
326 # It's not great way to workaround because it force renames the alias,
327 # but the alternative is broken ninja support.
328 warn_msg = f"Alias {node_string} name the same as File node, ninja does not support this. Renaming Alias {node_string} to {node_string}_alias."
329 if isinstance(node, SCons.Node.Alias.Alias):
330 for i, output in enumerate(build["outputs"]):
331 if output == node_string:
332 build["outputs"][i] += "_alias"
333 node_string += "_alias"
334 print(warn_msg)
335 elif self.builds[node_string]["rule"] == "phony":
336 for i, output in enumerate(self.builds[node_string]["outputs"]):
337 if output == node_string:
338 self.builds[node_string]["outputs"][i] += "_alias"
339 tmp_build = self.builds[node_string].copy()
340 del self.builds[node_string]
341 node_string += "_alias"
342 self.builds[node_string] = tmp_build
343 print(warn_msg)
344 else:
345 raise InternalError("Node {} added to ninja build state more than once".format(node_string))
346 self.builds[node_string] = build
347 return True
349 # TODO: rely on SCons to tell us what is generated source
350 # or some form of user scanner maybe (Github Issue #3624)
351 def is_generated_source(self, output) -> bool:
352 """Check if output ends with a known generated suffix."""
353 _, suffix = splitext(output)
354 return suffix in self.generated_suffixes
356 def has_generated_sources(self, output) -> bool:
358 Determine if output indicates this is a generated header file.
360 for generated in output:
361 if self.is_generated_source(generated):
362 return True
363 return False
365 # pylint: disable=too-many-branches,too-many-locals
366 def generate(self):
368 Generate the build.ninja.
370 This should only be called once for the lifetime of this object.
372 if self.__generated:
373 return
375 num_jobs = self.env.get('NINJA_MAX_JOBS', self.env.GetOption("num_jobs"))
376 self.pools.update({
377 "local_pool": num_jobs,
378 "install_pool": num_jobs / 2,
381 deps_format = self.env.get("NINJA_DEPFILE_PARSE_FORMAT", 'msvc' if self.env['PLATFORM'] == 'win32' else 'gcc')
382 for rule in ["CC", "CXX"]:
383 if deps_format == "msvc":
384 self.rules[rule]["deps"] = "msvc"
385 elif deps_format == "gcc" or deps_format == "clang":
386 self.rules[rule]["deps"] = "gcc"
387 self.rules[rule]["depfile"] = "$out.d"
388 else:
389 raise Exception(f"Unknown 'NINJA_DEPFILE_PARSE_FORMAT'={self.env['NINJA_DEPFILE_PARSE_FORMAT']}, use 'mvsc', 'gcc', or 'clang'.")
391 for key, rule in self.env.get(NINJA_RULES, {}).items():
392 # make a non response file rule for users custom response file rules.
393 if rule.get('rspfile') is not None:
394 self.rules.update({key + '_RSP': rule})
395 non_rsp_rule = rule.copy()
396 del non_rsp_rule['rspfile']
397 del non_rsp_rule['rspfile_content']
398 self.rules.update({key: non_rsp_rule})
399 else:
400 self.rules.update({key: rule})
402 self.pools.update(self.env.get(NINJA_POOLS, {}))
404 content = io.StringIO()
405 ninja = self.writer_class(content, width=100)
407 ninja.comment("Generated by scons. DO NOT EDIT.")
409 ninja.variable("builddir", get_path(self.env.Dir(self.env['NINJA_DIR']).path))
411 for pool_name, size in sorted(self.pools.items()):
412 ninja.pool(pool_name, min(self.env.get('NINJA_MAX_JOBS', size), size))
414 for var, val in sorted(self.variables.items()):
415 ninja.variable(var, val)
417 for rule, kwargs in sorted(self.rules.items()):
418 if self.env.get('NINJA_MAX_JOBS') is not None and 'pool' not in kwargs:
419 kwargs['pool'] = 'local_pool'
420 ninja.rule(rule, **kwargs)
422 # If the user supplied an alias to determine generated sources, use that, otherwise
423 # determine what the generated sources are dynamically.
424 generated_sources_alias = self.env.get('NINJA_GENERATED_SOURCE_ALIAS_NAME')
425 generated_sources_build = None
427 if generated_sources_alias:
428 generated_sources_build = self.builds.get(generated_sources_alias)
429 if generated_sources_build is None or generated_sources_build["rule"] != 'phony':
430 raise Exception(
431 "ERROR: 'NINJA_GENERATED_SOURCE_ALIAS_NAME' set, but no matching Alias object found."
434 if generated_sources_alias and generated_sources_build:
435 generated_source_files = sorted(
436 [] if not generated_sources_build else generated_sources_build['implicit']
439 def check_generated_source_deps(build):
440 return (
441 build != generated_sources_build
442 and set(build["outputs"]).isdisjoint(generated_source_files)
444 else:
445 generated_sources_build = None
446 generated_source_files = sorted({
447 output
448 # First find builds which have header files in their outputs.
449 for build in self.builds.values()
450 if self.has_generated_sources(build["outputs"])
451 for output in build["outputs"]
452 # Collect only the header files from the builds with them
453 # in their output. We do this because is_generated_source
454 # returns True if it finds a header in any of the outputs,
455 # here we need to filter so we only have the headers and
456 # not the other outputs.
457 if self.is_generated_source(output)
460 if generated_source_files:
461 generated_sources_alias = "_ninja_generated_sources"
462 ninja.build(
463 outputs=generated_sources_alias,
464 rule="phony",
465 implicit=generated_source_files
468 def check_generated_source_deps(build):
469 return (
470 not build["rule"] == "INSTALL"
471 and set(build["outputs"]).isdisjoint(generated_source_files)
472 and set(build.get("implicit", [])).isdisjoint(generated_source_files)
475 template_builders = []
476 scons_compiledb = False
478 if SCons.Script._Get_Default_Targets == SCons.Script._Set_Default_Targets_Has_Not_Been_Called:
479 all_targets = set()
480 else:
481 all_targets = None
483 for build in [self.builds[key] for key in sorted(self.builds.keys())]:
484 if "compile_commands.json" in build["outputs"]:
485 scons_compiledb = True
487 # this is for the no command line targets, no SCons default case. We want this default
488 # to just be all real files in the build.
489 if all_targets is not None and build['rule'] != 'phony':
490 all_targets = all_targets | set(build["outputs"])
492 if build["rule"] == "TEMPLATE":
493 template_builders.append(build)
494 continue
496 if "implicit" in build:
497 build["implicit"].sort()
499 # Don't make generated sources depend on each other. We
500 # have to check that none of the outputs are generated
501 # sources and none of the direct implicit dependencies are
502 # generated sources or else we will create a dependency
503 # cycle.
504 if (
505 generated_source_files
506 and check_generated_source_deps(build)
508 # Make all non-generated source targets depend on
509 # _generated_sources. We use order_only for generated
510 # sources so that we don't rebuild the world if one
511 # generated source was rebuilt. We just need to make
512 # sure that all of these sources are generated before
513 # other builds.
514 order_only = build.get("order_only", [])
515 order_only.append(generated_sources_alias)
516 build["order_only"] = order_only
517 if "order_only" in build:
518 build["order_only"].sort()
520 # When using a depfile Ninja can only have a single output
521 # but SCons will usually have emitted an output for every
522 # thing a command will create because it's caching is much
523 # more complex than Ninja's. This includes things like DWO
524 # files. Here we make sure that Ninja only ever sees one
525 # target when using a depfile. It will still have a command
526 # that will create all of the outputs but most targets don't
527 # depend directly on DWO files and so this assumption is safe
528 # to make.
529 rule = self.rules.get(build["rule"])
531 # Some rules like 'phony' and other builtins we don't have
532 # listed in self.rules so verify that we got a result
533 # before trying to check if it has a deps key.
535 # Anything using deps or rspfile in Ninja can only have a single
536 # output, but we may have a build which actually produces
537 # multiple outputs which other targets can depend on. Here we
538 # slice up the outputs so we have a single output which we will
539 # use for the "real" builder and multiple phony targets that
540 # match the file names of the remaining outputs. This way any
541 # build can depend on any output from any build.
543 # We assume that the first listed output is the 'key'
544 # output and is stably presented to us by SCons. For
545 # instance if -gsplit-dwarf is in play and we are
546 # producing foo.o and foo.dwo, we expect that outputs[0]
547 # from SCons will be the foo.o file and not the dwo
548 # file. If instead we just sorted the whole outputs array,
549 # we would find that the dwo file becomes the
550 # first_output, and this breaks, for instance, header
551 # dependency scanning.
552 if rule is not None and (rule.get("deps") or rule.get("rspfile")):
553 first_output, remaining_outputs = (
554 build["outputs"][0],
555 build["outputs"][1:],
558 if remaining_outputs:
559 ninja_sorted_build(
560 ninja,
561 outputs=remaining_outputs, rule="phony", implicit=first_output,
564 build["outputs"] = first_output
566 # Optionally a rule can specify a depfile, and SCons can generate implicit
567 # dependencies into the depfile. This allows for dependencies to come and go
568 # without invalidating the ninja file. The depfile was created in ninja specifically
569 # for dealing with header files appearing and disappearing across rebuilds, but it can
570 # be repurposed for anything, as long as you have a way to regenerate the depfile.
571 # More specific info can be found here: https://ninja-build.org/manual.html#_depfile
572 if rule is not None and rule.get('depfile') and build.get('deps_files'):
573 path = build['outputs'] if SCons.Util.is_List(build['outputs']) else [build['outputs']]
574 generate_depfile(self.env, path[0], build.pop('deps_files', []))
576 if "inputs" in build:
577 build["inputs"].sort()
579 ninja_sorted_build(
580 ninja,
581 **build
584 scons_daemon_dirty = str(pathlib.Path(get_path(self.env.get("NINJA_DIR"))) / "scons_daemon_dirty")
585 for template_builder in template_builders:
586 template_builder["implicit"] += [scons_daemon_dirty]
587 ninja_sorted_build(
588 ninja,
589 **template_builder
592 # We have to glob the SCons files here to teach the ninja file
593 # how to regenerate itself. We'll never see ourselves in the
594 # DAG walk so we can't rely on action_to_ninja_build to
595 # generate this rule even though SCons should know we're
596 # dependent on SCons files.
597 ninja_file_path = self.env.File(self.ninja_file).path
598 regenerate_deps = to_escaped_list(self.env, self.env['NINJA_REGENERATE_DEPS'])
600 ninja_sorted_build(
601 ninja,
602 outputs=ninja_file_path,
603 rule="REGENERATE",
604 implicit=regenerate_deps,
605 variables={
606 "self": ninja_file_path
610 ninja_sorted_build(
611 ninja,
612 outputs=regenerate_deps,
613 rule="phony",
614 variables={
615 "self": ninja_file_path,
619 if not scons_compiledb:
620 # If we ever change the name/s of the rules that include
621 # compile commands (i.e. something like CC) we will need to
622 # update this build to reflect that complete list.
623 ninja_sorted_build(
624 ninja,
625 outputs="compile_commands.json",
626 rule="CMD",
627 pool="console",
628 implicit=[str(self.ninja_file)],
629 variables={
630 "cmd": "{} -f {} -t compdb {}CC CXX > compile_commands.json".format(
631 # NINJA_COMPDB_EXPAND - should only be true for ninja
632 # This was added to ninja's compdb tool in version 1.9.0 (merged April 2018)
633 # https://github.com/ninja-build/ninja/pull/1223
634 # TODO: add check in generate to check version and enable this by default if it's available.
635 self.ninja_bin_path, str(self.ninja_file),
636 '-x ' if self.env.get('NINJA_COMPDB_EXPAND', True) else ''
641 ninja_sorted_build(
642 ninja,
643 outputs="compiledb", rule="phony", implicit=["compile_commands.json"],
646 ninja_sorted_build(
647 ninja,
648 outputs=["run_ninja_scons_daemon_phony", scons_daemon_dirty],
649 rule="SCONS_DAEMON",
652 ninja.build(
653 "shutdown_ninja_scons_daemon_phony",
654 rule="EXIT_SCONS_DAEMON",
658 if all_targets is None:
659 # Look in SCons's list of DEFAULT_TARGETS, find the ones that
660 # we generated a ninja build rule for.
661 all_targets = [str(node) for node in NINJA_DEFAULT_TARGETS]
662 else:
663 all_targets = list(all_targets)
665 if len(all_targets) == 0:
666 all_targets = ["phony_default"]
667 ninja_sorted_build(
668 ninja,
669 outputs=all_targets,
670 rule="phony",
673 ninja.default([self.ninja_syntax.escape_path(path) for path in sorted(all_targets)])
675 with NamedTemporaryFile(delete=False, mode='w') as temp_ninja_file:
676 temp_ninja_file.write(content.getvalue())
678 if self.env.GetOption('skip_ninja_regen') and os.path.exists(ninja_file_path) and filecmp.cmp(temp_ninja_file.name, ninja_file_path):
679 os.unlink(temp_ninja_file.name)
680 else:
682 daemon_dir = pathlib.Path(tempfile.gettempdir()) / ('scons_daemon_' + str(hashlib.md5(str(get_path(self.env["NINJA_DIR"])).encode()).hexdigest()))
683 pidfile = None
684 if os.path.exists(scons_daemon_dirty):
685 pidfile = scons_daemon_dirty
686 elif os.path.exists(daemon_dir / 'pidfile'):
687 pidfile = daemon_dir / 'pidfile'
689 if pidfile:
690 with open(pidfile) as f:
691 pid = int(f.readline())
692 try:
693 os.kill(pid, signal.SIGINT)
694 except OSError:
695 pass
697 # wait for the server process to fully killed
698 # TODO: update wait_for_process_to_die() to handle timeout and then catch exception
699 # here and do something smart.
700 wait_for_process_to_die(pid)
702 if os.path.exists(scons_daemon_dirty):
703 os.unlink(scons_daemon_dirty)
705 shutil.move(temp_ninja_file.name, ninja_file_path)
707 self.__generated = True
710 class SConsToNinjaTranslator:
711 """Translates SCons Actions into Ninja build objects."""
713 def __init__(self, env) -> None:
714 self.env = env
715 self.func_handlers = {
716 # Skip conftest builders
717 "_createSource": ninja_noop,
718 # SCons has a custom FunctionAction that just makes sure the
719 # target isn't static. We let the commands that ninja runs do
720 # this check for us.
721 "SharedFlagChecker": ninja_noop,
722 # The install builder is implemented as a function action.
723 # TODO: use command action #3573
724 "installFunc": _install_action_function,
725 "MkdirFunc": _mkdir_action_function,
726 "Mkdir": _mkdir_action_function,
727 "LibSymlinksActionFunction": _lib_symlink_action_function,
728 "Copy": _copy_action_function
731 self.loaded_custom = False
733 # pylint: disable=too-many-return-statements
734 def action_to_ninja_build(self, node, action=None):
735 """Generate build arguments dictionary for node."""
737 if not self.loaded_custom:
738 self.func_handlers.update(self.env[NINJA_CUSTOM_HANDLERS])
739 self.loaded_custom = True
741 if node.builder is None:
742 return None
744 if action is None:
745 action = node.builder.action
747 if node.env and node.env.get("NINJA_SKIP"):
748 return None
750 build = {}
751 env = node.env if node.env else self.env
753 # Ideally this should never happen, and we do try to filter
754 # Ninja builders out of being sources of ninja builders but I
755 # can't fix every DAG problem so we just skip ninja_builders
756 # if we find one
757 if SCons.Tool.ninja.NINJA_STATE.ninja_file == str(node):
758 build = None
759 elif isinstance(action, SCons.Action.FunctionAction):
760 build = self.handle_func_action(node, action)
761 elif isinstance(action, SCons.Action.LazyAction):
762 # pylint: disable=protected-access
763 action = action._generate_cache(env)
764 build = self.action_to_ninja_build(node, action=action)
765 elif isinstance(action, SCons.Action.ListAction):
766 build = self.handle_list_action(node, action)
767 elif isinstance(action, COMMAND_TYPES):
768 build = get_command(env, node, action)
769 else:
770 return {
771 "rule": "TEMPLATE",
772 "order_only": get_order_only(node),
773 "outputs": get_outputs(node),
774 "inputs": get_inputs(node),
775 "implicit": get_dependencies(node, skip_sources=True),
778 if build is not None:
779 build["order_only"] = get_order_only(node)
781 # TODO: WPD Is this testing the filename to verify it's a configure context generated file?
782 if not node.is_conftest():
783 node_callback = node.check_attributes("ninja_build_callback")
784 if callable(node_callback):
785 node_callback(env, node, build)
787 return build
789 def handle_func_action(self, node, action):
790 """Determine how to handle the function action."""
791 name = action.function_name()
792 # This is the name given by the Subst/Textfile builders. So return the
793 # node to indicate that SCons is required. We skip sources here because
794 # dependencies don't really matter when we're going to shove these to
795 # the bottom of ninja's DAG anyway and Textfile builders can have text
796 # content as their source which doesn't work as an implicit dep in
797 # ninja.
798 if name == 'ninja_builder':
799 return None
801 handler = self.func_handlers.get(name, None)
802 if handler is not None:
803 return handler(node.env if node.env else self.env, node)
804 elif name == "ActionCaller":
805 action_to_call = str(action).split('(')[0].strip()
806 handler = self.func_handlers.get(action_to_call, None)
807 if handler is not None:
808 return handler(node.env if node.env else self.env, node)
810 SCons.Warnings.SConsWarning(
811 "Found unhandled function action {}, "
812 " generating scons command to build\n"
813 "Note: this is less efficient than Ninja,"
814 " you can write your own ninja build generator for"
815 " this function using NinjaRegisterFunctionHandler".format(name)
818 return {
819 "rule": "TEMPLATE",
820 "order_only": get_order_only(node),
821 "outputs": get_outputs(node),
822 "inputs": get_inputs(node),
823 "implicit": get_dependencies(node, skip_sources=True),
826 # pylint: disable=too-many-branches
827 def handle_list_action(self, node, action):
828 """TODO write this comment"""
829 results = [
830 self.action_to_ninja_build(node, action=act)
831 for act in action.list
832 if act is not None
834 results = [
835 result for result in results if result is not None and result["outputs"]
837 if not results:
838 return None
840 # No need to process the results if we only got a single result
841 if len(results) == 1:
842 return results[0]
844 all_outputs = list({output for build in results for output in build["outputs"]})
845 dependencies = list({dep for build in results for dep in build.get("implicit", [])})
847 if results[0]["rule"] == "CMD" or results[0]["rule"] == "GENERATED_CMD":
848 cmdline = ""
849 for cmd in results:
851 # Occasionally a command line will expand to a
852 # whitespace only string (i.e. ' '). Which is not a
853 # valid command but does not trigger the empty command
854 # condition if not cmdstr. So here we strip preceding
855 # and proceeding whitespace to make strings like the
856 # above become empty strings and so will be skipped.
857 if not cmd.get("variables") or not cmd["variables"].get("cmd"):
858 continue
860 cmdstr = cmd["variables"]["cmd"].strip()
861 if not cmdstr:
862 continue
864 # Skip duplicate commands
865 if cmdstr in cmdline:
866 continue
868 if cmdline:
869 cmdline += " && "
871 cmdline += cmdstr
873 # Remove all preceding and proceeding whitespace
874 cmdline = cmdline.strip()
875 env = node.env if node.env else self.env
876 executor = node.get_executor()
877 if executor is not None:
878 targets = executor.get_all_targets()
879 else:
880 if hasattr(node, "target_peers"):
881 targets = node.target_peers
882 else:
883 targets = [node]
885 # Make sure we didn't generate an empty cmdline
886 if cmdline:
887 ninja_build = {
888 "outputs": all_outputs,
889 "rule": get_rule(node, "GENERATED_CMD"),
890 "variables": {
891 "cmd": cmdline,
892 "env": get_command_env(env, targets, node.sources),
894 "implicit": dependencies,
897 if node.env and node.env.get("NINJA_POOL", None) is not None:
898 ninja_build["pool"] = node.env["pool"]
900 return ninja_build
902 elif results[0]["rule"] == "phony":
903 return {
904 "outputs": all_outputs,
905 "rule": "phony",
906 "implicit": dependencies,
909 elif results[0]["rule"] == "INSTALL":
910 return {
911 "outputs": all_outputs,
912 "rule": get_rule(node, "INSTALL"),
913 "inputs": get_inputs(node),
914 "implicit": dependencies,
917 return {
918 "rule": "TEMPLATE",
919 "order_only": get_order_only(node),
920 "outputs": get_outputs(node),
921 "inputs": get_inputs(node),
922 "implicit": get_dependencies(node, skip_sources=True),