summaryrefslogtreecommitdiff
path: root/SCons/Tool/ninja/NinjaState.py
blob: 707a9e20d5bcb7f9da175c399258a747c5147226 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
# 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 pathlib
import signal
import tempfile
import shutil
import sys
import random
import filecmp
from os.path import splitext
from tempfile import NamedTemporaryFile
import ninja
import hashlib

import SCons
from SCons.Script import COMMAND_LINE_TARGETS
from SCons.Util import wait_for_process_to_die
from SCons.Errors import InternalError
from .Globals import COMMAND_TYPES, NINJA_RULES, NINJA_POOLS, \
    NINJA_CUSTOM_HANDLERS, NINJA_DEFAULT_TARGETS
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, to_escaped_list, ninja_sorted_build
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, ninja_syntax) -> None:
        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.ninja_syntax = ninja_syntax
        self.writer_class = ninja_syntax.Writer
        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()

        # 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
        scons_escape = env.get("ESCAPE", lambda x: x)

        # The daemon port should be the same across runs, unless explicitly set
        # or if the portfile is deleted. This ensures the ninja file is deterministic
        # across regen's if nothings changed. The construction var should take preference,
        # then portfile is next, and then otherwise create a new random port to persist in
        # use.
        scons_daemon_port = None
        os.makedirs(get_path(self.env.get("NINJA_DIR")), exist_ok=True)
        scons_daemon_port_file = str(pathlib.Path(get_path(self.env.get("NINJA_DIR"))) / "scons_daemon_portfile")

        if env.get('NINJA_SCONS_DAEMON_PORT') is not None:
            scons_daemon_port = int(env.get('NINJA_SCONS_DAEMON_PORT'))
        elif os.path.exists(scons_daemon_port_file):
            with open(scons_daemon_port_file) as f:
                scons_daemon_port = int(f.read())
        else:
            scons_daemon_port = random.randint(10000, 60000)

        with open(scons_daemon_port_file, 'w') as f:
            f.write(str(scons_daemon_port))

        # 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 = ninja_syntax.escape(scons_escape(sys.executable))
        self.variables = {
            "COPY": "cmd.exe /c 1>NUL copy" if sys.platform == "win32" else "cp",
            'PORT': scons_daemon_port,
            'NINJA_DIR_PATH': env.get('NINJA_DIR').abspath,
            'PYTHON_BIN': sys.executable,
            'NINJA_TOOL_DIR': pathlib.Path(__file__).parent,
            'NINJA_SCONS_DAEMON_KEEP_ALIVE': str(env.get('NINJA_SCONS_DAEMON_KEEP_ALIVE')),
            "SCONS_INVOCATION": '{} {} --disable-ninja __NINJA_NO=1 $out'.format(
                python_bin,
                " ".join(
                    [ninja_syntax.escape(scons_escape(arg)) for arg in sys.argv if arg not in COMMAND_LINE_TARGETS]
                ),
            ),
            "SCONS_INVOCATION_W_TARGETS": "{} {} NINJA_DISABLE_AUTO_RUN=1".format(
                python_bin, " ".join([
                    ninja_syntax.escape(scons_escape(arg))
                    for arg in sys.argv
                    if arg != 'NINJA_DISABLE_AUTO_RUN=1'])
            ),
            # 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_RSP": {
                "command": "$env$CC @$out.rsp",
                "description": "Compiling $out",
                "rspfile": "$out.rsp",
                "rspfile_content": "$rspc",
            },
            "CXX_RSP": {
                "command": "$env$CXX @$out.rsp",
                "description": "Compiling $out",
                "rspfile": "$out.rsp",
                "rspfile_content": "$rspc",
            },
            "LINK_RSP": {
                "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_RSP": {
                "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",
            },
            "CC": {
                "command": "$env$CC $rspc",
                "description": "Compiling $out",
            },
            "CXX": {
                "command": "$env$CXX $rspc",
                "description": "Compiling $out",
            },
            "LINK": {
                "command": "$env$LINK $rspc",
                "description": "Linking $out",
                "pool": "local_pool",
            },
            "AR": {
                "command": "{}$env$AR $rspc".format(
                    '' if sys.platform == "win32" else "rm -f $out && "
                ),
                "description": "Archiving $out",
                "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": "$PYTHON_BIN $NINJA_TOOL_DIR/ninja_daemon_build.py $PORT $NINJA_DIR_PATH $out",
                "description": "Defer to SCons to build $out",
                "pool": "local_pool",
                "restat": 1
            },
            "EXIT_SCONS_DAEMON": {
                "command": "$PYTHON_BIN $NINJA_TOOL_DIR/ninja_daemon_build.py $PORT $NINJA_DIR_PATH --exit",
                "description": "Shutting down ninja scons daemon server",
                "pool": "local_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,
            },

            "SCONS_DAEMON": {
                "command": "$PYTHON_BIN $NINJA_TOOL_DIR/ninja_run_daemon.py $PORT $NINJA_DIR_PATH $NINJA_SCONS_DAEMON_KEEP_ALIVE $SCONS_INVOCATION",
                "description": "Starting scons daemon...",
                "pool": "local_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 $self",
                "generator": 1,
                "pool": "console",
                "restat": 1,
            },
        }

        if env['PLATFORM'] == 'darwin' and env.get('AR', "") == 'ar':
            self.rules["AR"] = {
                "command": "rm -f $out && $env$AR $rspc",
                "description": "Archiving $out",
                "pool": "local_pool",
            }
        self.pools = {"scons_pool": 1}

    def add_build(self, node) -> bool:
        if not node.has_builder():
            return False

        if isinstance(node, SCons.Node.Python.Value):
            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:
            # TODO: If we work out a way to handle Alias() with same name as file this logic can be removed
            # This works around adding Alias with the same name as a Node.
            # It's not great way to workaround because it force renames the alias,
            # but the alternative is broken ninja support.
            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."
            if isinstance(node, SCons.Node.Alias.Alias):
                for i, output in enumerate(build["outputs"]):
                    if output == node_string:
                        build["outputs"][i] += "_alias"
                node_string += "_alias"
                print(warn_msg)
            elif self.builds[node_string]["rule"] == "phony":
                for i, output in enumerate(self.builds[node_string]["outputs"]):
                    if output == node_string:
                        self.builds[node_string]["outputs"][i] += "_alias"
                tmp_build = self.builds[node_string].copy()
                del self.builds[node_string]
                node_string += "_alias"
                self.builds[node_string] = tmp_build
                print(warn_msg)
            else:
                raise InternalError("Node {} added to ninja build state more than once".format(node_string))
        self.builds[node_string] = build
        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) -> bool:
        """Check if output ends with a known generated suffix."""
        _, suffix = splitext(output)
        return suffix in self.generated_suffixes

    def has_generated_sources(self, output) -> bool:
        """
        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

        num_jobs = self.env.get('NINJA_MAX_JOBS', self.env.GetOption("num_jobs"))
        self.pools.update({
            "local_pool": num_jobs,
            "install_pool": num_jobs / 2,
        })

        deps_format = self.env.get("NINJA_DEPFILE_PARSE_FORMAT", 'msvc' if self.env['PLATFORM'] == 'win32' else 'gcc')
        for rule in ["CC", "CXX"]:
            if deps_format == "msvc":
                self.rules[rule]["deps"] = "msvc"
            elif deps_format == "gcc" or deps_format == "clang":
                self.rules[rule]["deps"] = "gcc"
                self.rules[rule]["depfile"] = "$out.d"
            else:
                raise Exception(f"Unknown 'NINJA_DEPFILE_PARSE_FORMAT'={self.env['NINJA_DEPFILE_PARSE_FORMAT']}, use 'mvsc', 'gcc', or 'clang'.")

        for key, rule in self.env.get(NINJA_RULES, {}).items():
            # make a non response file rule for users custom response file rules.
            if rule.get('rspfile') is not None:
                self.rules.update({key + '_RSP': rule})
                non_rsp_rule = rule.copy()
                del non_rsp_rule['rspfile']
                del non_rsp_rule['rspfile_content']
                self.rules.update({key: non_rsp_rule})
            else:
                self.rules.update({key: rule})
        
        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.Dir(self.env['NINJA_DIR']).path))

        for pool_name, size in sorted(self.pools.items()):
            ninja.pool(pool_name, min(self.env.get('NINJA_MAX_JOBS', size), size))

        for var, val in sorted(self.variables.items()):
            ninja.variable(var, val)

        for rule, kwargs in sorted(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)

        # If the user supplied an alias to determine generated sources, use that, otherwise
        # determine what the generated sources are dynamically.
        generated_sources_alias = self.env.get('NINJA_GENERATED_SOURCE_ALIAS_NAME')
        generated_sources_build = None

        if generated_sources_alias:
            generated_sources_build = self.builds.get(generated_sources_alias)
            if generated_sources_build is None or generated_sources_build["rule"] != 'phony':
                raise Exception(
                    "ERROR: 'NINJA_GENERATED_SOURCE_ALIAS_NAME' set, but no matching Alias object found."
                )

        if generated_sources_alias and generated_sources_build:
            generated_source_files = sorted(
                [] if not generated_sources_build else generated_sources_build['implicit']
            )
            
            def check_generated_source_deps(build):
                return (
                    build != generated_sources_build
                    and set(build["outputs"]).isdisjoint(generated_source_files)
                )
        else:
            generated_sources_build = None
            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:
                generated_sources_alias = "_ninja_generated_sources"
                ninja.build(
                    outputs=generated_sources_alias,
                    rule="phony",
                    implicit=generated_source_files
                )
                
                def check_generated_source_deps(build):
                    return (
                        not build["rule"] == "INSTALL"
                        and set(build["outputs"]).isdisjoint(generated_source_files)
                        and set(build.get("implicit", [])).isdisjoint(generated_source_files)
                    )

        template_builders = []
        scons_compiledb = False

        if SCons.Script._Get_Default_Targets == SCons.Script._Set_Default_Targets_Has_Not_Been_Called:
            all_targets = set()
        else:
            all_targets = None

        for build in [self.builds[key] for key in sorted(self.builds.keys())]:
            if "compile_commands.json" in build["outputs"]:
                scons_compiledb = True

            # this is for the no command line targets, no SCons default case. We want this default
            # to just be all real files in the build.
            if all_targets is not None and build['rule'] != 'phony':
                all_targets = all_targets | set(build["outputs"])

            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 check_generated_source_deps(build)
            ):
                # 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_alias)
                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_sorted_build(
                        ninja,
                        outputs=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_sorted_build(
                ninja,
                **build
            )

        scons_daemon_dirty = str(pathlib.Path(get_path(self.env.get("NINJA_DIR"))) / "scons_daemon_dirty")
        for template_builder in template_builders:
            template_builder["implicit"] += [scons_daemon_dirty]
            ninja_sorted_build(
                ninja,
                **template_builder
            )

        # 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.
        ninja_file_path = self.env.File(self.ninja_file).path
        regenerate_deps = to_escaped_list(self.env, self.env['NINJA_REGENERATE_DEPS'])

        ninja_sorted_build(
            ninja,
            outputs=ninja_file_path,
            rule="REGENERATE",
            implicit=regenerate_deps,
            variables={
                "self": ninja_file_path
            }
        )

        ninja_sorted_build(
            ninja,
            outputs=regenerate_deps,
            rule="phony",
            variables={
                "self": ninja_file_path,
            }
        )

        if not scons_compiledb:
            # 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_sorted_build(
                ninja,
                outputs="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_sorted_build(
                ninja,
                outputs="compiledb", rule="phony", implicit=["compile_commands.json"],
            )

        ninja_sorted_build(
            ninja,
            outputs=["run_ninja_scons_daemon_phony", scons_daemon_dirty],
            rule="SCONS_DAEMON",
        )

        ninja.build(
            "shutdown_ninja_scons_daemon_phony",
            rule="EXIT_SCONS_DAEMON",
        )


        if all_targets is None:
            # Look in SCons's list of DEFAULT_TARGETS, find the ones that
            # we generated a ninja build rule for.
            all_targets = [str(node) for node in NINJA_DEFAULT_TARGETS]
        else:
            all_targets = list(all_targets)
        
        if len(all_targets) == 0:
            all_targets = ["phony_default"]
            ninja_sorted_build(
                ninja,
                outputs=all_targets,
                rule="phony",
            )
        
        ninja.default([self.ninja_syntax.escape_path(path) for path in sorted(all_targets)])

        with NamedTemporaryFile(delete=False, mode='w') as temp_ninja_file:
            temp_ninja_file.write(content.getvalue())

        if self.env.GetOption('skip_ninja_regen') and os.path.exists(ninja_file_path) and filecmp.cmp(temp_ninja_file.name, ninja_file_path):
            os.unlink(temp_ninja_file.name)
        else:

            daemon_dir = pathlib.Path(tempfile.gettempdir()) / ('scons_daemon_' + str(hashlib.md5(str(get_path(self.env["NINJA_DIR"])).encode()).hexdigest()))
            pidfile = None
            if os.path.exists(scons_daemon_dirty):
                pidfile = scons_daemon_dirty
            elif os.path.exists(daemon_dir / 'pidfile'):
                pidfile = daemon_dir / 'pidfile'

            if pidfile:
                with open(pidfile) as f:
                    pid = int(f.readline())
                    try:
                        os.kill(pid, signal.SIGINT)
                    except OSError:
                        pass

                # wait for the server process to fully killed
                # TODO: update wait_for_process_to_die() to handle timeout and then catch exception
                #       here and do something smart.
                wait_for_process_to_die(pid)

            if os.path.exists(scons_daemon_dirty):
                os.unlink(scons_daemon_dirty)

            shutil.move(temp_ninja_file.name, ninja_file_path)

        self.__generated = True


class SConsToNinjaTranslator:
    """Translates SCons Actions into Ninja build objects."""

    def __init__(self, env) -> None:
        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,
            "Mkdir": _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:
            return {
                "rule": "TEMPLATE",
                "order_only": get_order_only(node),
                "outputs": get_outputs(node),
                "inputs": get_inputs(node),
                "implicit": get_dependencies(node, skip_sources=True),
            }

        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.get("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.
                if not cmd.get("variables") or not cmd["variables"].get("cmd"):
                    continue

                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()
            env = node.env if node.env else self.env
            executor = node.get_executor()
            if executor is not None:
                targets = executor.get_all_targets()
            else:
                if hasattr(node, "target_peers"):
                    targets = node.target_peers
                else:
                    targets = [node]

            # 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(env, targets, node.sources),
                    },
                    "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,
            }

        return {
            "rule": "TEMPLATE",
            "order_only": get_order_only(node),
            "outputs": get_outputs(node),
            "inputs": get_inputs(node),
            "implicit": get_dependencies(node, skip_sources=True),
        }