1# Copyright (c) 2014 Google Inc. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""
6This script is intended for use as a GYP_GENERATOR. It takes as input (by way of
7the generator flag config_path) the path of a json file that dictates the files
8and targets to search for. The following keys are supported:
9files: list of paths (relative) of the files to search for.
10test_targets: unqualified target names to search for. Any target in this list
11that depends upon a file in |files| is output regardless of the type of target
12or chain of dependencies.
13additional_compile_targets: Unqualified targets to search for in addition to
14test_targets. Targets in the combined list that depend upon a file in |files|
15are not necessarily output. For example, if the target is of type none then the
16target is not output (but one of the descendants of the target will be).
17
18The following is output:
19error: only supplied if there is an error.
20compile_targets: minimal set of targets that directly or indirectly (for
21  targets of type none) depend on the files in |files| and is one of the
22  supplied targets or a target that one of the supplied targets depends on.
23  The expectation is this set of targets is passed into a build step. This list
24  always contains the output of test_targets as well.
25test_targets: set of targets from the supplied |test_targets| that either
26  directly or indirectly depend upon a file in |files|. This list if useful
27  if additional processing needs to be done for certain targets after the
28  build, such as running tests.
29status: outputs one of three values: none of the supplied files were found,
30  one of the include files changed so that it should be assumed everything
31  changed (in this case test_targets and compile_targets are not output) or at
32  least one file was found.
33invalid_targets: list of supplied targets that were not found.
34
35Example:
36Consider a graph like the following:
37  A     D
38 / \
39B   C
40A depends upon both B and C, A is of type none and B and C are executables.
41D is an executable, has no dependencies and nothing depends on it.
42If |additional_compile_targets| = ["A"], |test_targets| = ["B", "C"] and
43files = ["b.cc", "d.cc"] (B depends upon b.cc and D depends upon d.cc), then
44the following is output:
45|compile_targets| = ["B"] B must built as it depends upon the changed file b.cc
46and the supplied target A depends upon it. A is not output as a build_target
47as it is of type none with no rules and actions.
48|test_targets| = ["B"] B directly depends upon the change file b.cc.
49
50Even though the file d.cc, which D depends upon, has changed D is not output
51as it was not supplied by way of |additional_compile_targets| or |test_targets|.
52
53If the generator flag analyzer_output_path is specified, output is written
54there. Otherwise output is written to stdout.
55
56In Gyp the "all" target is shorthand for the root targets in the files passed
57to gyp. For example, if file "a.gyp" contains targets "a1" and
58"a2", and file "b.gyp" contains targets "b1" and "b2" and "a2" has a dependency
59on "b2" and gyp is supplied "a.gyp" then "all" consists of "a1" and "a2".
60Notice that "b1" and "b2" are not in the "all" target as "b.gyp" was not
61directly supplied to gyp. OTOH if both "a.gyp" and "b.gyp" are supplied to gyp
62then the "all" target includes "b1" and "b2".
63"""
64
65
66import gyp.common
67import json
68import os
69import posixpath
70
71debug = False
72
73found_dependency_string = "Found dependency"
74no_dependency_string = "No dependencies"
75# Status when it should be assumed that everything has changed.
76all_changed_string = "Found dependency (all)"
77
78# MatchStatus is used indicate if and how a target depends upon the supplied
79# sources.
80# The target's sources contain one of the supplied paths.
81MATCH_STATUS_MATCHES = 1
82# The target has a dependency on another target that contains one of the
83# supplied paths.
84MATCH_STATUS_MATCHES_BY_DEPENDENCY = 2
85# The target's sources weren't in the supplied paths and none of the target's
86# dependencies depend upon a target that matched.
87MATCH_STATUS_DOESNT_MATCH = 3
88# The target doesn't contain the source, but the dependent targets have not yet
89# been visited to determine a more specific status yet.
90MATCH_STATUS_TBD = 4
91
92generator_supports_multiple_toolsets = gyp.common.CrossCompileRequested()
93
94generator_wants_static_library_dependencies_adjusted = False
95
96generator_default_variables = {}
97for dirname in [
98    "INTERMEDIATE_DIR",
99    "SHARED_INTERMEDIATE_DIR",
100    "PRODUCT_DIR",
101    "LIB_DIR",
102    "SHARED_LIB_DIR",
103]:
104    generator_default_variables[dirname] = "!!!"
105
106for unused in [
107    "RULE_INPUT_PATH",
108    "RULE_INPUT_ROOT",
109    "RULE_INPUT_NAME",
110    "RULE_INPUT_DIRNAME",
111    "RULE_INPUT_EXT",
112    "EXECUTABLE_PREFIX",
113    "EXECUTABLE_SUFFIX",
114    "STATIC_LIB_PREFIX",
115    "STATIC_LIB_SUFFIX",
116    "SHARED_LIB_PREFIX",
117    "SHARED_LIB_SUFFIX",
118    "CONFIGURATION_NAME",
119]:
120    generator_default_variables[unused] = ""
121
122
123def _ToGypPath(path):
124    """Converts a path to the format used by gyp."""
125    if os.sep == "\\" and os.altsep == "/":
126        return path.replace("\\", "/")
127    return path
128
129
130def _ResolveParent(path, base_path_components):
131    """Resolves |path|, which starts with at least one '../'. Returns an empty
132  string if the path shouldn't be considered. See _AddSources() for a
133  description of |base_path_components|."""
134    depth = 0
135    while path.startswith("../"):
136        depth += 1
137        path = path[3:]
138    # Relative includes may go outside the source tree. For example, an action may
139    # have inputs in /usr/include, which are not in the source tree.
140    if depth > len(base_path_components):
141        return ""
142    if depth == len(base_path_components):
143        return path
144    return (
145        "/".join(base_path_components[0 : len(base_path_components) - depth])
146        + "/"
147        + path
148    )
149
150
151def _AddSources(sources, base_path, base_path_components, result):
152    """Extracts valid sources from |sources| and adds them to |result|. Each
153  source file is relative to |base_path|, but may contain '..'. To make
154  resolving '..' easier |base_path_components| contains each of the
155  directories in |base_path|. Additionally each source may contain variables.
156  Such sources are ignored as it is assumed dependencies on them are expressed
157  and tracked in some other means."""
158    # NOTE: gyp paths are always posix style.
159    for source in sources:
160        if not len(source) or source.startswith("!!!") or source.startswith("$"):
161            continue
162        # variable expansion may lead to //.
163        org_source = source
164        source = source[0] + source[1:].replace("//", "/")
165        if source.startswith("../"):
166            source = _ResolveParent(source, base_path_components)
167            if len(source):
168                result.append(source)
169            continue
170        result.append(base_path + source)
171        if debug:
172            print("AddSource", org_source, result[len(result) - 1])
173
174
175def _ExtractSourcesFromAction(action, base_path, base_path_components, results):
176    if "inputs" in action:
177        _AddSources(action["inputs"], base_path, base_path_components, results)
178
179
180def _ToLocalPath(toplevel_dir, path):
181    """Converts |path| to a path relative to |toplevel_dir|."""
182    if path == toplevel_dir:
183        return ""
184    if path.startswith(toplevel_dir + "/"):
185        return path[len(toplevel_dir) + len("/") :]
186    return path
187
188
189def _ExtractSources(target, target_dict, toplevel_dir):
190    # |target| is either absolute or relative and in the format of the OS. Gyp
191    # source paths are always posix. Convert |target| to a posix path relative to
192    # |toplevel_dir_|. This is done to make it easy to build source paths.
193    base_path = posixpath.dirname(_ToLocalPath(toplevel_dir, _ToGypPath(target)))
194    base_path_components = base_path.split("/")
195
196    # Add a trailing '/' so that _AddSources() can easily build paths.
197    if len(base_path):
198        base_path += "/"
199
200    if debug:
201        print("ExtractSources", target, base_path)
202
203    results = []
204    if "sources" in target_dict:
205        _AddSources(target_dict["sources"], base_path, base_path_components, results)
206    # Include the inputs from any actions. Any changes to these affect the
207    # resulting output.
208    if "actions" in target_dict:
209        for action in target_dict["actions"]:
210            _ExtractSourcesFromAction(action, base_path, base_path_components, results)
211    if "rules" in target_dict:
212        for rule in target_dict["rules"]:
213            _ExtractSourcesFromAction(rule, base_path, base_path_components, results)
214
215    return results
216
217
218class Target:
219    """Holds information about a particular target:
220  deps: set of Targets this Target depends upon. This is not recursive, only the
221    direct dependent Targets.
222  match_status: one of the MatchStatus values.
223  back_deps: set of Targets that have a dependency on this Target.
224  visited: used during iteration to indicate whether we've visited this target.
225    This is used for two iterations, once in building the set of Targets and
226    again in _GetBuildTargets().
227  name: fully qualified name of the target.
228  requires_build: True if the target type is such that it needs to be built.
229    See _DoesTargetTypeRequireBuild for details.
230  added_to_compile_targets: used when determining if the target was added to the
231    set of targets that needs to be built.
232  in_roots: true if this target is a descendant of one of the root nodes.
233  is_executable: true if the type of target is executable.
234  is_static_library: true if the type of target is static_library.
235  is_or_has_linked_ancestor: true if the target does a link (eg executable), or
236    if there is a target in back_deps that does a link."""
237
238    def __init__(self, name):
239        self.deps = set()
240        self.match_status = MATCH_STATUS_TBD
241        self.back_deps = set()
242        self.name = name
243        # TODO(sky): I don't like hanging this off Target. This state is specific
244        # to certain functions and should be isolated there.
245        self.visited = False
246        self.requires_build = False
247        self.added_to_compile_targets = False
248        self.in_roots = False
249        self.is_executable = False
250        self.is_static_library = False
251        self.is_or_has_linked_ancestor = False
252
253
254class Config:
255    """Details what we're looking for
256  files: set of files to search for
257  targets: see file description for details."""
258
259    def __init__(self):
260        self.files = []
261        self.targets = set()
262        self.additional_compile_target_names = set()
263        self.test_target_names = set()
264
265    def Init(self, params):
266        """Initializes Config. This is a separate method as it raises an exception
267    if there is a parse error."""
268        generator_flags = params.get("generator_flags", {})
269        config_path = generator_flags.get("config_path", None)
270        if not config_path:
271            return
272        try:
273            f = open(config_path)
274            config = json.load(f)
275            f.close()
276        except OSError:
277            raise Exception("Unable to open file " + config_path)
278        except ValueError as e:
279            raise Exception("Unable to parse config file " + config_path + str(e))
280        if not isinstance(config, dict):
281            raise Exception("config_path must be a JSON file containing a dictionary")
282        self.files = config.get("files", [])
283        self.additional_compile_target_names = set(
284            config.get("additional_compile_targets", [])
285        )
286        self.test_target_names = set(config.get("test_targets", []))
287
288
289def _WasBuildFileModified(build_file, data, files, toplevel_dir):
290    """Returns true if the build file |build_file| is either in |files| or
291  one of the files included by |build_file| is in |files|. |toplevel_dir| is
292  the root of the source tree."""
293    if _ToLocalPath(toplevel_dir, _ToGypPath(build_file)) in files:
294        if debug:
295            print("gyp file modified", build_file)
296        return True
297
298    # First element of included_files is the file itself.
299    if len(data[build_file]["included_files"]) <= 1:
300        return False
301
302    for include_file in data[build_file]["included_files"][1:]:
303        # |included_files| are relative to the directory of the |build_file|.
304        rel_include_file = _ToGypPath(
305            gyp.common.UnrelativePath(include_file, build_file)
306        )
307        if _ToLocalPath(toplevel_dir, rel_include_file) in files:
308            if debug:
309                print(
310                    "included gyp file modified, gyp_file=",
311                    build_file,
312                    "included file=",
313                    rel_include_file,
314                )
315            return True
316    return False
317
318
319def _GetOrCreateTargetByName(targets, target_name):
320    """Creates or returns the Target at targets[target_name]. If there is no
321  Target for |target_name| one is created. Returns a tuple of whether a new
322  Target was created and the Target."""
323    if target_name in targets:
324        return False, targets[target_name]
325    target = Target(target_name)
326    targets[target_name] = target
327    return True, target
328
329
330def _DoesTargetTypeRequireBuild(target_dict):
331    """Returns true if the target type is such that it needs to be built."""
332    # If a 'none' target has rules or actions we assume it requires a build.
333    return bool(
334        target_dict["type"] != "none"
335        or target_dict.get("actions")
336        or target_dict.get("rules")
337    )
338
339
340def _GenerateTargets(data, target_list, target_dicts, toplevel_dir, files, build_files):
341    """Returns a tuple of the following:
342  . A dictionary mapping from fully qualified name to Target.
343  . A list of the targets that have a source file in |files|.
344  . Targets that constitute the 'all' target. See description at top of file
345    for details on the 'all' target.
346  This sets the |match_status| of the targets that contain any of the source
347  files in |files| to MATCH_STATUS_MATCHES.
348  |toplevel_dir| is the root of the source tree."""
349    # Maps from target name to Target.
350    name_to_target = {}
351
352    # Targets that matched.
353    matching_targets = []
354
355    # Queue of targets to visit.
356    targets_to_visit = target_list[:]
357
358    # Maps from build file to a boolean indicating whether the build file is in
359    # |files|.
360    build_file_in_files = {}
361
362    # Root targets across all files.
363    roots = set()
364
365    # Set of Targets in |build_files|.
366    build_file_targets = set()
367
368    while len(targets_to_visit) > 0:
369        target_name = targets_to_visit.pop()
370        created_target, target = _GetOrCreateTargetByName(name_to_target, target_name)
371        if created_target:
372            roots.add(target)
373        elif target.visited:
374            continue
375
376        target.visited = True
377        target.requires_build = _DoesTargetTypeRequireBuild(target_dicts[target_name])
378        target_type = target_dicts[target_name]["type"]
379        target.is_executable = target_type == "executable"
380        target.is_static_library = target_type == "static_library"
381        target.is_or_has_linked_ancestor = (
382            target_type == "executable" or target_type == "shared_library"
383        )
384
385        build_file = gyp.common.ParseQualifiedTarget(target_name)[0]
386        if build_file not in build_file_in_files:
387            build_file_in_files[build_file] = _WasBuildFileModified(
388                build_file, data, files, toplevel_dir
389            )
390
391        if build_file in build_files:
392            build_file_targets.add(target)
393
394        # If a build file (or any of its included files) is modified we assume all
395        # targets in the file are modified.
396        if build_file_in_files[build_file]:
397            print("matching target from modified build file", target_name)
398            target.match_status = MATCH_STATUS_MATCHES
399            matching_targets.append(target)
400        else:
401            sources = _ExtractSources(
402                target_name, target_dicts[target_name], toplevel_dir
403            )
404            for source in sources:
405                if _ToGypPath(os.path.normpath(source)) in files:
406                    print("target", target_name, "matches", source)
407                    target.match_status = MATCH_STATUS_MATCHES
408                    matching_targets.append(target)
409                    break
410
411        # Add dependencies to visit as well as updating back pointers for deps.
412        for dep in target_dicts[target_name].get("dependencies", []):
413            targets_to_visit.append(dep)
414
415            created_dep_target, dep_target = _GetOrCreateTargetByName(
416                name_to_target, dep
417            )
418            if not created_dep_target:
419                roots.discard(dep_target)
420
421            target.deps.add(dep_target)
422            dep_target.back_deps.add(target)
423
424    return name_to_target, matching_targets, roots & build_file_targets
425
426
427def _GetUnqualifiedToTargetMapping(all_targets, to_find):
428    """Returns a tuple of the following:
429  . mapping (dictionary) from unqualified name to Target for all the
430    Targets in |to_find|.
431  . any target names not found. If this is empty all targets were found."""
432    result = {}
433    if not to_find:
434        return {}, []
435    to_find = set(to_find)
436    for target_name in all_targets.keys():
437        extracted = gyp.common.ParseQualifiedTarget(target_name)
438        if len(extracted) > 1 and extracted[1] in to_find:
439            to_find.remove(extracted[1])
440            result[extracted[1]] = all_targets[target_name]
441            if not to_find:
442                return result, []
443    return result, [x for x in to_find]
444
445
446def _DoesTargetDependOnMatchingTargets(target):
447    """Returns true if |target| or any of its dependencies is one of the
448  targets containing the files supplied as input to analyzer. This updates
449  |matches| of the Targets as it recurses.
450  target: the Target to look for."""
451    if target.match_status == MATCH_STATUS_DOESNT_MATCH:
452        return False
453    if (
454        target.match_status == MATCH_STATUS_MATCHES
455        or target.match_status == MATCH_STATUS_MATCHES_BY_DEPENDENCY
456    ):
457        return True
458    for dep in target.deps:
459        if _DoesTargetDependOnMatchingTargets(dep):
460            target.match_status = MATCH_STATUS_MATCHES_BY_DEPENDENCY
461            print("\t", target.name, "matches by dep", dep.name)
462            return True
463    target.match_status = MATCH_STATUS_DOESNT_MATCH
464    return False
465
466
467def _GetTargetsDependingOnMatchingTargets(possible_targets):
468    """Returns the list of Targets in |possible_targets| that depend (either
469  directly on indirectly) on at least one of the targets containing the files
470  supplied as input to analyzer.
471  possible_targets: targets to search from."""
472    found = []
473    print("Targets that matched by dependency:")
474    for target in possible_targets:
475        if _DoesTargetDependOnMatchingTargets(target):
476            found.append(target)
477    return found
478
479
480def _AddCompileTargets(target, roots, add_if_no_ancestor, result):
481    """Recurses through all targets that depend on |target|, adding all targets
482  that need to be built (and are in |roots|) to |result|.
483  roots: set of root targets.
484  add_if_no_ancestor: If true and there are no ancestors of |target| then add
485  |target| to |result|. |target| must still be in |roots|.
486  result: targets that need to be built are added here."""
487    if target.visited:
488        return
489
490    target.visited = True
491    target.in_roots = target in roots
492
493    for back_dep_target in target.back_deps:
494        _AddCompileTargets(back_dep_target, roots, False, result)
495        target.added_to_compile_targets |= back_dep_target.added_to_compile_targets
496        target.in_roots |= back_dep_target.in_roots
497        target.is_or_has_linked_ancestor |= back_dep_target.is_or_has_linked_ancestor
498
499    # Always add 'executable' targets. Even though they may be built by other
500    # targets that depend upon them it makes detection of what is going to be
501    # built easier.
502    # And always add static_libraries that have no dependencies on them from
503    # linkables. This is necessary as the other dependencies on them may be
504    # static libraries themselves, which are not compile time dependencies.
505    if target.in_roots and (
506        target.is_executable
507        or (
508            not target.added_to_compile_targets
509            and (add_if_no_ancestor or target.requires_build)
510        )
511        or (
512            target.is_static_library
513            and add_if_no_ancestor
514            and not target.is_or_has_linked_ancestor
515        )
516    ):
517        print(
518            "\t\tadding to compile targets",
519            target.name,
520            "executable",
521            target.is_executable,
522            "added_to_compile_targets",
523            target.added_to_compile_targets,
524            "add_if_no_ancestor",
525            add_if_no_ancestor,
526            "requires_build",
527            target.requires_build,
528            "is_static_library",
529            target.is_static_library,
530            "is_or_has_linked_ancestor",
531            target.is_or_has_linked_ancestor,
532        )
533        result.add(target)
534        target.added_to_compile_targets = True
535
536
537def _GetCompileTargets(matching_targets, supplied_targets):
538    """Returns the set of Targets that require a build.
539  matching_targets: targets that changed and need to be built.
540  supplied_targets: set of targets supplied to analyzer to search from."""
541    result = set()
542    for target in matching_targets:
543        print("finding compile targets for match", target.name)
544        _AddCompileTargets(target, supplied_targets, True, result)
545    return result
546
547
548def _WriteOutput(params, **values):
549    """Writes the output, either to stdout or a file is specified."""
550    if "error" in values:
551        print("Error:", values["error"])
552    if "status" in values:
553        print(values["status"])
554    if "targets" in values:
555        values["targets"].sort()
556        print("Supplied targets that depend on changed files:")
557        for target in values["targets"]:
558            print("\t", target)
559    if "invalid_targets" in values:
560        values["invalid_targets"].sort()
561        print("The following targets were not found:")
562        for target in values["invalid_targets"]:
563            print("\t", target)
564    if "build_targets" in values:
565        values["build_targets"].sort()
566        print("Targets that require a build:")
567        for target in values["build_targets"]:
568            print("\t", target)
569    if "compile_targets" in values:
570        values["compile_targets"].sort()
571        print("Targets that need to be built:")
572        for target in values["compile_targets"]:
573            print("\t", target)
574    if "test_targets" in values:
575        values["test_targets"].sort()
576        print("Test targets:")
577        for target in values["test_targets"]:
578            print("\t", target)
579
580    output_path = params.get("generator_flags", {}).get("analyzer_output_path", None)
581    if not output_path:
582        print(json.dumps(values))
583        return
584    try:
585        f = open(output_path, "w")
586        f.write(json.dumps(values) + "\n")
587        f.close()
588    except OSError as e:
589        print("Error writing to output file", output_path, str(e))
590
591
592def _WasGypIncludeFileModified(params, files):
593    """Returns true if one of the files in |files| is in the set of included
594  files."""
595    if params["options"].includes:
596        for include in params["options"].includes:
597            if _ToGypPath(os.path.normpath(include)) in files:
598                print("Include file modified, assuming all changed", include)
599                return True
600    return False
601
602
603def _NamesNotIn(names, mapping):
604    """Returns a list of the values in |names| that are not in |mapping|."""
605    return [name for name in names if name not in mapping]
606
607
608def _LookupTargets(names, mapping):
609    """Returns a list of the mapping[name] for each value in |names| that is in
610  |mapping|."""
611    return [mapping[name] for name in names if name in mapping]
612
613
614def CalculateVariables(default_variables, params):
615    """Calculate additional variables for use in the build (called by gyp)."""
616    flavor = gyp.common.GetFlavor(params)
617    if flavor == "mac":
618        default_variables.setdefault("OS", "mac")
619    elif flavor == "win":
620        default_variables.setdefault("OS", "win")
621        gyp.msvs_emulation.CalculateCommonVariables(default_variables, params)
622    else:
623        operating_system = flavor
624        if flavor == "android":
625            operating_system = "linux"  # Keep this legacy behavior for now.
626        default_variables.setdefault("OS", operating_system)
627
628
629class TargetCalculator:
630    """Calculates the matching test_targets and matching compile_targets."""
631
632    def __init__(
633        self,
634        files,
635        additional_compile_target_names,
636        test_target_names,
637        data,
638        target_list,
639        target_dicts,
640        toplevel_dir,
641        build_files,
642    ):
643        self._additional_compile_target_names = set(additional_compile_target_names)
644        self._test_target_names = set(test_target_names)
645        (
646            self._name_to_target,
647            self._changed_targets,
648            self._root_targets,
649        ) = _GenerateTargets(
650            data, target_list, target_dicts, toplevel_dir, frozenset(files), build_files
651        )
652        (
653            self._unqualified_mapping,
654            self.invalid_targets,
655        ) = _GetUnqualifiedToTargetMapping(
656            self._name_to_target, self._supplied_target_names_no_all()
657        )
658
659    def _supplied_target_names(self):
660        return self._additional_compile_target_names | self._test_target_names
661
662    def _supplied_target_names_no_all(self):
663        """Returns the supplied test targets without 'all'."""
664        result = self._supplied_target_names()
665        result.discard("all")
666        return result
667
668    def is_build_impacted(self):
669        """Returns true if the supplied files impact the build at all."""
670        return self._changed_targets
671
672    def find_matching_test_target_names(self):
673        """Returns the set of output test targets."""
674        assert self.is_build_impacted()
675        # Find the test targets first. 'all' is special cased to mean all the
676        # root targets. To deal with all the supplied |test_targets| are expanded
677        # to include the root targets during lookup. If any of the root targets
678        # match, we remove it and replace it with 'all'.
679        test_target_names_no_all = set(self._test_target_names)
680        test_target_names_no_all.discard("all")
681        test_targets_no_all = _LookupTargets(
682            test_target_names_no_all, self._unqualified_mapping
683        )
684        test_target_names_contains_all = "all" in self._test_target_names
685        if test_target_names_contains_all:
686            test_targets = [
687                x for x in (set(test_targets_no_all) | set(self._root_targets))
688            ]
689        else:
690            test_targets = [x for x in test_targets_no_all]
691        print("supplied test_targets")
692        for target_name in self._test_target_names:
693            print("\t", target_name)
694        print("found test_targets")
695        for target in test_targets:
696            print("\t", target.name)
697        print("searching for matching test targets")
698        matching_test_targets = _GetTargetsDependingOnMatchingTargets(test_targets)
699        matching_test_targets_contains_all = test_target_names_contains_all and set(
700            matching_test_targets
701        ) & set(self._root_targets)
702        if matching_test_targets_contains_all:
703            # Remove any of the targets for all that were not explicitly supplied,
704            # 'all' is subsequentely added to the matching names below.
705            matching_test_targets = [
706                x for x in (set(matching_test_targets) & set(test_targets_no_all))
707            ]
708        print("matched test_targets")
709        for target in matching_test_targets:
710            print("\t", target.name)
711        matching_target_names = [
712            gyp.common.ParseQualifiedTarget(target.name)[1]
713            for target in matching_test_targets
714        ]
715        if matching_test_targets_contains_all:
716            matching_target_names.append("all")
717            print("\tall")
718        return matching_target_names
719
720    def find_matching_compile_target_names(self):
721        """Returns the set of output compile targets."""
722        assert self.is_build_impacted()
723        # Compile targets are found by searching up from changed targets.
724        # Reset the visited status for _GetBuildTargets.
725        for target in self._name_to_target.values():
726            target.visited = False
727
728        supplied_targets = _LookupTargets(
729            self._supplied_target_names_no_all(), self._unqualified_mapping
730        )
731        if "all" in self._supplied_target_names():
732            supplied_targets = [
733                x for x in (set(supplied_targets) | set(self._root_targets))
734            ]
735        print("Supplied test_targets & compile_targets")
736        for target in supplied_targets:
737            print("\t", target.name)
738        print("Finding compile targets")
739        compile_targets = _GetCompileTargets(self._changed_targets, supplied_targets)
740        return [
741            gyp.common.ParseQualifiedTarget(target.name)[1]
742            for target in compile_targets
743        ]
744
745
746def GenerateOutput(target_list, target_dicts, data, params):
747    """Called by gyp as the final stage. Outputs results."""
748    config = Config()
749    try:
750        config.Init(params)
751
752        if not config.files:
753            raise Exception(
754                "Must specify files to analyze via config_path generator " "flag"
755            )
756
757        toplevel_dir = _ToGypPath(os.path.abspath(params["options"].toplevel_dir))
758        if debug:
759            print("toplevel_dir", toplevel_dir)
760
761        if _WasGypIncludeFileModified(params, config.files):
762            result_dict = {
763                "status": all_changed_string,
764                "test_targets": list(config.test_target_names),
765                "compile_targets": list(
766                    config.additional_compile_target_names | config.test_target_names
767                ),
768            }
769            _WriteOutput(params, **result_dict)
770            return
771
772        calculator = TargetCalculator(
773            config.files,
774            config.additional_compile_target_names,
775            config.test_target_names,
776            data,
777            target_list,
778            target_dicts,
779            toplevel_dir,
780            params["build_files"],
781        )
782        if not calculator.is_build_impacted():
783            result_dict = {
784                "status": no_dependency_string,
785                "test_targets": [],
786                "compile_targets": [],
787            }
788            if calculator.invalid_targets:
789                result_dict["invalid_targets"] = calculator.invalid_targets
790            _WriteOutput(params, **result_dict)
791            return
792
793        test_target_names = calculator.find_matching_test_target_names()
794        compile_target_names = calculator.find_matching_compile_target_names()
795        found_at_least_one_target = compile_target_names or test_target_names
796        result_dict = {
797            "test_targets": test_target_names,
798            "status": found_dependency_string
799            if found_at_least_one_target
800            else no_dependency_string,
801            "compile_targets": list(set(compile_target_names) | set(test_target_names)),
802        }
803        if calculator.invalid_targets:
804            result_dict["invalid_targets"] = calculator.invalid_targets
805        _WriteOutput(params, **result_dict)
806
807    except Exception as e:
808        _WriteOutput(params, error=str(e))
809