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.
34 from os
.path
import splitext
35 from tempfile
import NamedTemporaryFile
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
53 """Maintains state of Ninja build system as it's translated from SCons."""
55 def __init__(self
, env
, ninja_file
, ninja_syntax
) -> None:
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(
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
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
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
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())
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
111 if os
.path
.basename(sys
.argv
[0]) == 'scons.py':
112 python_bin
= ninja_syntax
.escape(scons_escape(sys
.executable
))
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(
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
))
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:")
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",
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
157 "command": "$env$CC @$out.rsp",
158 "description": "Compiling $out",
159 "rspfile": "$out.rsp",
160 "rspfile_content": "$rspc",
163 "command": "$env$CXX @$out.rsp",
164 "description": "Compiling $out",
165 "rspfile": "$out.rsp",
166 "rspfile_content": "$rspc",
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
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",
191 "command": "$env$CC $rspc",
192 "description": "Compiling $out",
195 "command": "$env$CXX $rspc",
196 "description": "Compiling $out",
199 "command": "$env$LINK $rspc",
200 "description": "Linking $out",
201 "pool": "local_pool",
204 "command": "{}$env$AR $rspc".format(
205 '' if sys
.platform
== "win32" else "rm -f $out && "
207 "description": "Archiving $out",
208 "pool": "local_pool",
212 "cmd /c mklink $out $in"
213 if sys
.platform
== "win32"
214 else "ln -s $in $out"
216 "description": "Symlink $in -> $out",
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.
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",
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",
245 "command": "$SCONS_INVOCATION $out",
246 "description": "$SCONS_INVOCATION $out",
247 "pool": "scons_pool",
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
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",
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
290 "command": "$SCONS_INVOCATION_W_TARGETS",
291 "description": "Regenerating $self",
298 if env
['PLATFORM'] == 'darwin' and env
.get('AR', "") == '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():
310 if isinstance(node
, SCons
.Node
.Python
.Value
):
313 if isinstance(node
, SCons
.Node
.Alias
.Alias
):
314 build
= alias_to_ninja_build(node
)
316 build
= self
.translator
.action_to_ninja_build(node
)
318 # Some things are unbuild-able or need not be built in Ninja
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"
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
345 raise InternalError("Node {} added to ninja build state more than once".format(node_string
))
346 self
.builds
[node_string
] = build
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
):
365 # pylint: disable=too-many-branches,too-many-locals
368 Generate the build.ninja.
370 This should only be called once for the lifetime of this object.
375 num_jobs
= self
.env
.get('NINJA_MAX_JOBS', self
.env
.GetOption("num_jobs"))
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"
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
})
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':
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
):
441 build
!= generated_sources_build
442 and set(build
["outputs"]).isdisjoint(generated_source_files
)
445 generated_sources_build
= None
446 generated_source_files
= sorted({
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"
463 outputs
=generated_sources_alias
,
465 implicit
=generated_source_files
468 def check_generated_source_deps(build
):
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
:
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
)
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
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
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
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
= (
555 build
["outputs"][1:],
558 if remaining_outputs
:
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()
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
]
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'])
602 outputs
=ninja_file_path
,
604 implicit
=regenerate_deps
,
606 "self": ninja_file_path
612 outputs
=regenerate_deps
,
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.
625 outputs
="compile_commands.json",
628 implicit
=[str(self
.ninja_file
)],
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 ''
643 outputs
="compiledb", rule
="phony", implicit
=["compile_commands.json"],
648 outputs
=["run_ninja_scons_daemon_phony", scons_daemon_dirty
],
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
]
663 all_targets
= list(all_targets
)
665 if len(all_targets
) == 0:
666 all_targets
= ["phony_default"]
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
)
682 daemon_dir
= pathlib
.Path(tempfile
.gettempdir()) / ('scons_daemon_' + str(hashlib
.md5(str(get_path(self
.env
["NINJA_DIR"])).encode()).hexdigest()))
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'
690 with
open(pidfile
) as f
:
691 pid
= int(f
.readline())
693 os
.kill(pid
, signal
.SIGINT
)
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:
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
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:
745 action
= node
.builder
.action
747 if node
.env
and node
.env
.get("NINJA_SKIP"):
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
757 if SCons
.Tool
.ninja
.NINJA_STATE
.ninja_file
== str(node
):
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
)
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
)
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
798 if name
== 'ninja_builder':
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
)
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"""
830 self
.action_to_ninja_build(node
, action
=act
)
831 for act
in action
.list
835 result
for result
in results
if result
is not None and result
["outputs"]
840 # No need to process the results if we only got a single result
841 if len(results
) == 1:
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":
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"):
860 cmdstr
= cmd
["variables"]["cmd"].strip()
864 # Skip duplicate commands
865 if cmdstr
in cmdline
:
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()
880 if hasattr(node
, "target_peers"):
881 targets
= node
.target_peers
885 # Make sure we didn't generate an empty cmdline
888 "outputs": all_outputs
,
889 "rule": get_rule(node
, "GENERATED_CMD"),
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"]
902 elif results
[0]["rule"] == "phony":
904 "outputs": all_outputs
,
906 "implicit": dependencies
,
909 elif results
[0]["rule"] == "INSTALL":
911 "outputs": all_outputs
,
912 "rule": get_rule(node
, "INSTALL"),
913 "inputs": get_inputs(node
),
914 "implicit": dependencies
,
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),