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 # without limitation the rights to use, copy, modify, merge, publish,
9 # distribute, sublicense, and/or sell copies of the Software, and to
10 # permit persons to whom the Software is furnished to do so, subject to
11 # the following conditions:
13 # The above copyright notice and this permission notice shall be included
14 # in all copies or substantial portions of the Software.
16 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
17 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
18 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25 from os
.path
import join
as joinpath
26 from collections
import OrderedDict
29 from SCons
.Action
import get_default_ENV
, _string_from_cmd_list
30 from SCons
.Script
import AddOption
31 from SCons
.Util
import is_List
, flatten_sequence
33 class NinjaExperimentalWarning(SCons
.Warnings
.WarningOnByDefault
):
37 def ninja_add_command_line_options() -> None:
39 Add additional command line arguments to SCons specific to the ninja tool
41 AddOption('--disable-execute-ninja',
42 dest
='disable_execute_ninja',
46 help='Disable automatically running ninja after scons')
48 AddOption('--disable-ninja',
53 help='Disable ninja generation and build with scons even if tool is loaded. '+
54 'Also used by ninja to build targets which only scons can build.')
56 AddOption('--skip-ninja-regen',
57 dest
='skip_ninja_regen',
61 help='Allow scons to skip regeneration of the ninja file and restarting of the daemon. ' +
62 'Care should be taken in cases where Glob is in use or SCons generated files are used in ' +
66 def is_valid_dependent_node(node
) -> bool:
68 Return True if node is not an alias or is an alias that has children
70 This prevents us from making phony targets that depend on other
71 phony targets that will never have an associated ninja build
74 We also have to specify that it's an alias when doing the builder
75 check because some nodes (like src files) won't have builders but
76 are valid implicit dependencies.
78 if isinstance(node
, SCons
.Node
.Alias
.Alias
):
79 return bool(node
.children())
81 return not node
.get_env().get("NINJA_SKIP")
84 def alias_to_ninja_build(node
):
85 """Convert an Alias node into a Ninja phony target"""
87 "outputs": get_outputs(node
),
90 get_path(src_file(n
)) for n
in node
.children() if is_valid_dependent_node(n
)
95 def check_invalid_ninja_node(node
) -> bool:
96 return not isinstance(node
, (SCons
.Node
.FS
.Base
, SCons
.Node
.Alias
.Alias
))
99 def filter_ninja_nodes(node_list
):
101 for node
in node_list
:
102 if isinstance(node
, (SCons
.Node
.FS
.Base
, SCons
.Node
.Alias
.Alias
)) and not node
.get_env().get('NINJA_SKIP'):
103 ninja_nodes
.append(node
)
109 def get_input_nodes(node
):
110 if node
.get_executor() is not None:
111 inputs
= node
.get_executor().get_all_sources()
113 inputs
= node
.sources
117 def invalid_ninja_nodes(node
, targets
):
119 for node_list
in [node
.prerequisites
, get_input_nodes(node
), node
.children(), targets
]:
121 result
= result
or any([check_invalid_ninja_node(node
) for node
in node_list
])
125 def get_order_only(node
):
126 """Return a list of order only dependencies for node."""
127 if node
.prerequisites
is None:
129 return [get_path(src_file(prereq
)) for prereq
in filter_ninja_nodes(node
.prerequisites
)]
132 def get_dependencies(node
, skip_sources
: bool=False):
133 """Return a list of dependencies for node."""
136 get_path(src_file(child
))
137 for child
in filter_ninja_nodes(node
.children())
138 if child
not in node
.sources
140 return [get_path(src_file(child
)) for child
in filter_ninja_nodes(node
.children())]
143 def get_inputs(node
):
144 """Collect the Ninja inputs for node."""
145 return [get_path(src_file(o
)) for o
in filter_ninja_nodes(get_input_nodes(node
))]
148 def get_outputs(node
):
149 """Collect the Ninja outputs for node."""
150 executor
= node
.get_executor()
151 if executor
is not None:
152 outputs
= executor
.get_all_targets()
154 if hasattr(node
, "target_peers"):
155 outputs
= node
.target_peers
159 outputs
= [get_path(o
) for o
in filter_ninja_nodes(outputs
)]
164 def get_targets_sources(node
):
165 executor
= node
.get_executor()
166 if executor
is not None:
167 tlist
= executor
.get_all_targets()
168 slist
= executor
.get_all_sources()
170 if hasattr(node
, "target_peers"):
171 tlist
= node
.target_peers
176 # Retrieve the repository file for all sources
177 slist
= [rfile(s
) for s
in slist
]
183 Return a fake path if necessary.
185 As an example Aliases use this as their target name in Ninja.
187 if hasattr(node
, "get_path"):
188 return node
.get_path()
194 Return the repository file for node if it has one. Otherwise return node
196 if hasattr(node
, "rfile"):
202 """Returns the src code file if it exists."""
203 if hasattr(node
, "srcnode"):
205 if src
.stat() is not None:
207 return get_path(node
)
210 def get_rule(node
, rule
):
211 tlist
, slist
= get_targets_sources(node
)
212 if invalid_ninja_nodes(node
, tlist
):
218 def to_escaped_list(env
, lst
):
220 Ninja tool function for returning an escaped list of strings from a subst
223 env_var arg can be a list or a subst generator which returns a list.
226 # subst_list will take in either a raw list or a subst callable which generates
227 # a list, and return a list of CmdStringHolders which can be converted into raw strings.
228 # If a raw list was passed in, then scons_list will make a list of lists from the original
229 # values and even subst items in the list if they are substitutable. Flatten will flatten
230 # the list in that case, to ensure for either input we have a list of CmdStringHolders.
231 deps_list
= env
.Flatten(env
.subst_list(lst
))
233 # Now that we have the deps in a list as CmdStringHolders, we can convert them into raw strings
234 # and make sure to escape the strings to handle spaces in paths. We also will sort the result
235 # keep the order of the list consistent.
236 return sorted([dep
.escape(env
.get("ESCAPE", lambda x
: x
)) for dep
in deps_list
])
239 def generate_depfile(env
, node
, dependencies
) -> None:
241 Ninja tool function for writing a depfile. The depfile should include
242 the node path followed by all the dependent files in a makefile format.
244 dependencies arg can be a list or a subst generator which returns a list.
247 depfile
= os
.path
.join(get_path(env
['NINJA_DIR']), str(node
) + '.depfile')
249 depfile_contents
= str(node
) + ": " + ' '.join(to_escaped_list(env
, dependencies
))
253 with
open(depfile
, 'r') as f
:
254 need_rewrite
= (f
.read() != depfile_contents
)
255 except FileNotFoundError
:
259 os
.makedirs(os
.path
.dirname(depfile
) or '.', exist_ok
=True)
260 with
open(depfile
, 'w') as f
:
261 f
.write(depfile_contents
)
264 def ninja_noop(*_args
, **_kwargs
):
266 A general purpose no-op function.
268 There are many things that happen in SCons that we don't need and
269 also don't return anything. We use this to disable those functions
270 instead of creating multiple definitions of the same thing.
274 def ninja_recursive_sorted_dict(build
):
275 sorted_dict
= OrderedDict()
276 for key
, val
in sorted(build
.items()):
277 if isinstance(val
, dict):
278 sorted_dict
[key
] = ninja_recursive_sorted_dict(val
)
280 sorted_dict
[key
] = val
284 def ninja_sorted_build(ninja
, **build
) -> None:
285 sorted_dict
= ninja_recursive_sorted_dict(build
)
286 ninja
.build(**sorted_dict
)
289 def get_command_env(env
, target
, source
):
291 Return a string that sets the environment for any environment variables that
292 differ between the OS environment and the SCons command ENV.
294 It will be compatible with the default shell of the operating system.
297 return env
["NINJA_ENV_VAR_CACHE"]
301 # Scan the ENV looking for any keys which do not exist in
302 # os.environ or differ from it. We assume if it's a new or
303 # differing key from the process environment then it's
304 # important to pass down to commands in the Ninja file.
305 ENV
= SCons
.Action
._resolve
_shell
_env
(env
, target
, source
)
306 scons_specified_env
= {
308 for key
, value
in ENV
.items()
309 # TODO: Remove this filter, unless there's a good reason to keep. SCons's behavior shouldn't depend on shell's.
310 if key
not in os
.environ
or os
.environ
.get(key
, None) != value
313 windows
= env
["PLATFORM"] == "win32"
315 scons_specified_env
= SCons
.Util
.sanitize_shell_env(scons_specified_env
)
316 for key
, value
in scons_specified_env
.items():
318 command_env
+= "set '{}={}' && ".format(key
, value
)
320 # We address here *only* the specific case that a user might have
321 # an environment variable which somehow gets included and has
322 # spaces in the value. These are escapes that Ninja handles. This
323 # doesn't make builds on paths with spaces (Ninja and SCons issues)
324 # nor expanding response file paths with spaces (Ninja issue) work.
325 value
= value
.replace(r
' ', r
'$ ')
326 command_env
+= "export {}='{}';".format(key
, value
)
328 env
["NINJA_ENV_VAR_CACHE"] = command_env
332 def get_comstr(env
, action
, targets
, sources
):
333 """Get the un-substituted string for action."""
334 # Despite being having "list" in it's name this member is not
335 # actually a list. It's the pre-subst'd string of the command. We
336 # use it to determine if the command we're about to generate needs
337 # to use a custom Ninja rule. By default this redirects CC, CXX,
338 # AR, SHLINK, and LINK commands to their respective rules but the
339 # user can inject custom Ninja rules and tie them to commands by
340 # using their pre-subst'd string.
341 if hasattr(action
, "process"):
342 return action
.cmd_list
344 return action
.genstring(targets
, sources
, env
)
347 def generate_command(env
, node
, action
, targets
, sources
, executor
=None):
348 # Actions like CommandAction have a method called process that is
349 # used by SCons to generate the cmd_line they need to run. So
350 # check if it's a thing like CommandAction and call it if we can.
351 if hasattr(action
, "process"):
352 cmd_list
, _
, _
= action
.process(targets
, sources
, env
, executor
=executor
)
353 cmd
= _string_from_cmd_list(cmd_list
[0])
355 # Anything else works with genstring, this is most commonly hit by
356 # ListActions which essentially call process on all of their
357 # commands and concatenate it for us.
358 genstring
= action
.genstring(targets
, sources
, env
)
359 if executor
is not None:
360 cmd
= env
.subst(genstring
, executor
=executor
)
362 cmd
= env
.subst(genstring
, targets
, sources
)
364 cmd
= cmd
.replace("\n", " && ").strip()
365 if cmd
.endswith("&&"):
366 cmd
= cmd
[0:-2].strip()
368 # Escape dollars as necessary
369 return cmd
.replace("$", "$$")
372 def ninja_csig(original
):
373 """Return a dummy csig"""
376 if isinstance(self
, SCons
.Node
.Node
) and self
.is_sconscript():
377 return original(self
)
378 return "dummy_ninja_csig"
383 def ninja_contents(original
):
384 """Return a dummy content without doing IO"""
387 if isinstance(self
, SCons
.Node
.Node
) and (self
.is_sconscript() or self
.is_conftest()):
388 return original(self
)
389 return bytes("dummy_ninja_contents", encoding
="utf-8")
394 def ninja_stat(_self
, path
):
396 Eternally memoized stat call.
398 SCons is very aggressive about clearing out cached values. For our
399 purposes everything should only ever call stat once since we're
400 running in a no_exec build the file system state should not
401 change. For these reasons we patch SCons.Node.FS.LocalFS.stat to
402 use our eternal memoized dictionary.
406 return SCons
.Tool
.ninja
.Globals
.NINJA_STAT_MEMO
[path
]
409 result
= os
.stat(path
)
413 SCons
.Tool
.ninja
.Globals
.NINJA_STAT_MEMO
[path
] = result
417 def ninja_whereis(thing
, *_args
, **_kwargs
):
418 """Replace env.WhereIs with a much faster version"""
420 # Optimize for success, this gets called significantly more often
421 # when the value is already memoized than when it's not.
423 return SCons
.Tool
.ninja
.Globals
.NINJA_WHEREIS_MEMO
[thing
]
425 # TODO: Fix this to respect env['ENV']['PATH']... WPD
426 # We do not honor any env['ENV'] or env[*] variables in the
427 # generated ninja file. Ninja passes your raw shell environment
428 # down to it's subprocess so the only sane option is to do the
429 # same during generation. At some point, if and when we try to
430 # upstream this, I'm sure a sticking point will be respecting
431 # env['ENV'] variables and such but it's actually quite
432 # complicated. I have a naive version but making it always work
433 # with shell quoting is nigh impossible. So I've decided to
434 # cross that bridge when it's absolutely required.
435 path
= shutil
.which(thing
)
436 SCons
.Tool
.ninja
.Globals
.NINJA_WHEREIS_MEMO
[thing
] = path
440 def ninja_print_conf_log(s
, target
, source
, env
) -> None:
441 """Command line print only for conftest to generate a correct conf log."""
442 if target
and target
[0].is_conftest():
443 action
= SCons
.Action
._ActionAction
()
444 action
.print_cmd_line(s
, target
, source
, env
)