| Index: pylib/gyp/generator/analyzer.py
|
| diff --git a/pylib/gyp/generator/analyzer.py b/pylib/gyp/generator/analyzer.py
|
| index c093f1c6bda4fdcd466775a7af7755eec43f86ab..f1a03ccdad00a0edce2eb1ecdeec4ea9854cbe13 100644
|
| --- a/pylib/gyp/generator/analyzer.py
|
| +++ b/pylib/gyp/generator/analyzer.py
|
| @@ -7,19 +7,29 @@ This script is intended for use as a GYP_GENERATOR. It takes as input (by way of
|
| the generator flag config_path) the path of a json file that dictates the files
|
| and targets to search for. The following keys are supported:
|
| files: list of paths (relative) of the files to search for.
|
| -targets: list of targets to search for. The target names are unqualified.
|
| +test_targets: unqualified target names to search for. Any target in this list
|
| +that depends upon a file in |files| is output regardless of the type of target
|
| +or chain of dependencies.
|
| +additional_compile_targets: Unqualified targets to search for in addition to
|
| +test_targets. Targets in the combined list that depend upon a file in |files|
|
| +are not necessarily output. For example, if the target is of type none then the
|
| +target is not output (but one of the descendants of the target will be).
|
|
|
| The following is output:
|
| error: only supplied if there is an error.
|
| -build_targets: minimal set of targets that directly depend on the changed
|
| - files and need to be built. The expectation is this set of targets is passed
|
| - into a build step. The returned values are either values in the supplied
|
| - targets, or have a dependency on one of the supplied targets.
|
| +compile_targets: minimal set of targets that directly or indirectly (for
|
| + targets of type none) depend on the files in |files| and is one of the
|
| + supplied targets or a target that one of the supplied targets depends on.
|
| + The expectation is this set of targets is passed into a build step.
|
| +test_targets: set of targets from the supplied |test_targets| that either
|
| + directly or indirectly depend upon a file in |files|. This list if useful
|
| + if additional processing needs to be done for certain targets after the
|
| + build, such as running tests.
|
| status: outputs one of three values: none of the supplied files were found,
|
| one of the include files changed so that it should be assumed everything
|
| - changed (in this case targets and build_targets are not output) or at
|
| + changed (in this case test_targets and compile_targets are not output) or at
|
| least one file was found.
|
| -invalid_targets: list of supplied targets thare were not found.
|
| +invalid_targets: list of supplied targets that were not found.
|
|
|
| Example:
|
| Consider a graph like the following:
|
| @@ -28,17 +38,27 @@ Consider a graph like the following:
|
| B C
|
| A depends upon both B and C, A is of type none and B and C are executables.
|
| D is an executable, has no dependencies and nothing depends on it.
|
| -If |targets| = ["A"] and files = ["b.cc", "d.cc"] (B depends upon b.cc and D
|
| -depends upon d.cc), then the following is output:
|
| -|build_targets| = ["B"] B must built as it depends upon the changed file b.cc
|
| +If |additional_compile_targets| = ["A"], |test_targets| = ["B", "C"] and
|
| +files = ["b.cc", "d.cc"] (B depends upon b.cc and D depends upon d.cc), then
|
| +the following is output:
|
| +|compile_targets| = ["B"] B must built as it depends upon the changed file b.cc
|
| and the supplied target A depends upon it. A is not output as a build_target
|
| as it is of type none with no rules and actions.
|
| +|test_targets| = ["B"] B directly depends upon the change file b.cc.
|
|
|
| Even though the file d.cc, which D depends upon, has changed D is not output
|
| -as none of the supplied targets (A) depend upon D.
|
| +as it was not supplied by way of |additional_compile_targets| or |test_targets|.
|
|
|
| If the generator flag analyzer_output_path is specified, output is written
|
| there. Otherwise output is written to stdout.
|
| +
|
| +In Gyp the "all" target is shorthand for the root targets in the files passed
|
| +to gyp. For example, if file "a.gyp" contains targets "a1" and
|
| +"a2", and file "b.gyp" contains targets "b1" and "b2" and "a2" has a dependency
|
| +on "b2" and gyp is supplied "a.gyp" then "all" consists of "a1" and "a2".
|
| +Notice that "b1" and "b2" are not in the "all" target as "b.gyp" was not
|
| +directly supplied to gyp. OTOH if both "a.gyp" and "b.gyp" are supplied to gyp
|
| +then the "all" target includes "b1" and "b2".
|
| """
|
|
|
| import gyp.common
|
| @@ -225,6 +245,10 @@ class Config(object):
|
| def __init__(self):
|
| self.files = []
|
| self.targets = set()
|
| + self.additional_compile_target_names = set()
|
| + self.test_target_names = set()
|
| + # Needed until recipes are updated.
|
| + self.deprecated_mode = False
|
|
|
| def Init(self, params):
|
| """Initializes Config. This is a separate method as it raises an exception
|
| @@ -244,7 +268,13 @@ class Config(object):
|
| if not isinstance(config, dict):
|
| raise Exception('config_path must be a JSON file containing a dictionary')
|
| self.files = config.get('files', [])
|
| - self.targets = set(config.get('targets', []))
|
| + if config.get('targets'):
|
| + self.targets = set(config.get('targets'))
|
| + self.deprecated_mode = True
|
| + else:
|
| + self.additional_compile_target_names = set(
|
| + config.get('additional_compile_targets', []))
|
| + self.test_target_names = set(config.get('test_targets', []))
|
|
|
|
|
| def _WasBuildFileModified(build_file, data, files, toplevel_dir):
|
| @@ -295,13 +325,13 @@ def _GenerateTargets(data, target_list, target_dicts, toplevel_dir, files,
|
| """Returns a tuple of the following:
|
| . A dictionary mapping from fully qualified name to Target.
|
| . A list of the targets that have a source file in |files|.
|
| - . Set of root Targets reachable from the the files |build_files|. This
|
| - is the set of targets built by the 'all' target.
|
| + . Targets that constitute the 'all' target. See description at top of file
|
| + for details on the 'all' target.
|
| This sets the |match_status| of the targets that contain any of the source
|
| files in |files| to MATCH_STATUS_MATCHES.
|
| |toplevel_dir| is the root of the source tree."""
|
| # Maps from target name to Target.
|
| - targets = {}
|
| + name_to_target = {}
|
|
|
| # Targets that matched.
|
| matching_targets = []
|
| @@ -321,7 +351,8 @@ def _GenerateTargets(data, target_list, target_dicts, toplevel_dir, files,
|
|
|
| while len(targets_to_visit) > 0:
|
| target_name = targets_to_visit.pop()
|
| - created_target, target = _GetOrCreateTargetByName(targets, target_name)
|
| + created_target, target = _GetOrCreateTargetByName(name_to_target,
|
| + target_name)
|
| if created_target:
|
| roots.add(target)
|
| elif target.visited:
|
| @@ -364,22 +395,25 @@ def _GenerateTargets(data, target_list, target_dicts, toplevel_dir, files,
|
| for dep in target_dicts[target_name].get('dependencies', []):
|
| targets_to_visit.append(dep)
|
|
|
| - created_dep_target, dep_target = _GetOrCreateTargetByName(targets, dep)
|
| + created_dep_target, dep_target = _GetOrCreateTargetByName(name_to_target,
|
| + dep)
|
| if not created_dep_target:
|
| roots.discard(dep_target)
|
|
|
| target.deps.add(dep_target)
|
| dep_target.back_deps.add(target)
|
|
|
| - return targets, matching_targets, roots & build_file_targets
|
| + return name_to_target, matching_targets, roots & build_file_targets
|
|
|
|
|
| def _GetUnqualifiedToTargetMapping(all_targets, to_find):
|
| - """Returns a mapping (dictionary) from unqualified name to Target for all the
|
| - Targets in |to_find|."""
|
| + """Returns a tuple of the following:
|
| + . mapping (dictionary) from unqualified name to Target for all the
|
| + Targets in |to_find|.
|
| + . any target names not found. If this is empty all targets were found."""
|
| result = {}
|
| if not to_find:
|
| - return result
|
| + return {}, []
|
| to_find = set(to_find)
|
| for target_name in all_targets.keys():
|
| extracted = gyp.common.ParseQualifiedTarget(target_name)
|
| @@ -387,11 +421,11 @@ def _GetUnqualifiedToTargetMapping(all_targets, to_find):
|
| to_find.remove(extracted[1])
|
| result[extracted[1]] = all_targets[target_name]
|
| if not to_find:
|
| - return result
|
| - return result
|
| + return result, []
|
| + return result, [x for x in to_find]
|
|
|
|
|
| -def _AddBuildTargets(target, roots, result):
|
| +def _AddBuildTargetsDeprecated(target, roots, result):
|
| """Recurses through all targets that depend on |target|, adding all targets
|
| that need to be built (and are in |roots|) to |result|.
|
| roots: set of root targets.
|
| @@ -403,7 +437,7 @@ def _AddBuildTargets(target, roots, result):
|
| target.in_roots = target in roots
|
|
|
| for back_dep_target in target.back_deps:
|
| - _AddBuildTargets(back_dep_target, roots, result)
|
| + _AddBuildTargetsDeprecated(back_dep_target, roots, result)
|
| target.added_to_compile_targets |= back_dep_target.added_to_compile_targets
|
| target.in_roots |= back_dep_target.in_roots
|
| target.is_or_has_linked_ancestor |= (
|
| @@ -429,14 +463,99 @@ def _AddBuildTargets(target, roots, result):
|
| target.added_to_compile_targets = True
|
|
|
|
|
| -def _GetBuildTargets(matching_targets, roots):
|
| +def _GetBuildTargetsDeprecated(matching_targets, roots):
|
| """Returns the set of Targets that require a build.
|
| matching_targets: targets that changed and need to be built.
|
| roots: set of root targets in the build files to search from."""
|
| result = set()
|
| for target in matching_targets:
|
| print '\tfinding build targets for match', target.name
|
| - _AddBuildTargets(target, roots, result)
|
| + _AddBuildTargetsDeprecated(target, roots, result)
|
| + return result
|
| +
|
| +
|
| +def _DoesTargetDependOnMatchingTargets(target):
|
| + """Returns true if |target| or any of its dependencies is one of the
|
| + targets containing the files supplied as input to analyzer. This updates
|
| + |matches| of the Targets as it recurses.
|
| + target: the Target to look for."""
|
| + if target.match_status == MATCH_STATUS_DOESNT_MATCH:
|
| + return False
|
| + if target.match_status == MATCH_STATUS_MATCHES or \
|
| + target.match_status == MATCH_STATUS_MATCHES_BY_DEPENDENCY:
|
| + return True
|
| + for dep in target.deps:
|
| + if _DoesTargetDependOnMatchingTargets(dep):
|
| + target.match_status = MATCH_STATUS_MATCHES_BY_DEPENDENCY
|
| + print '\t', target.name, 'matches by dep', dep.name
|
| + return True
|
| + target.match_status = MATCH_STATUS_DOESNT_MATCH
|
| + return False
|
| +
|
| +
|
| +def _GetTargetsDependingOnMatchingTargets(possible_targets):
|
| + """Returns the list of Targets in |possible_targets| that depend (either
|
| + directly on indirectly) on at least one of the targets containing the files
|
| + supplied as input to analyzer.
|
| + possible_targets: targets to search from."""
|
| + found = []
|
| + print 'Targets that matched by dependency:'
|
| + for target in possible_targets:
|
| + if _DoesTargetDependOnMatchingTargets(target):
|
| + found.append(target)
|
| + return found
|
| +
|
| +
|
| +def _AddCompileTargets(target, roots, add_if_no_ancestor, result):
|
| + """Recurses through all targets that depend on |target|, adding all targets
|
| + that need to be built (and are in |roots|) to |result|.
|
| + roots: set of root targets.
|
| + add_if_no_ancestor: If true and there are no ancestors of |target| then add
|
| + |target| to |result|. |target| must still be in |roots|.
|
| + result: targets that need to be built are added here."""
|
| + if target.visited:
|
| + return
|
| +
|
| + target.visited = True
|
| + target.in_roots = target in roots
|
| +
|
| + for back_dep_target in target.back_deps:
|
| + _AddCompileTargets(back_dep_target, roots, False, result)
|
| + target.added_to_compile_targets |= back_dep_target.added_to_compile_targets
|
| + target.in_roots |= back_dep_target.in_roots
|
| + target.is_or_has_linked_ancestor |= (
|
| + back_dep_target.is_or_has_linked_ancestor)
|
| +
|
| + # Always add 'executable' targets. Even though they may be built by other
|
| + # targets that depend upon them it makes detection of what is going to be
|
| + # built easier.
|
| + # And always add static_libraries that have no dependencies on them from
|
| + # linkables. This is necessary as the other dependencies on them may be
|
| + # static libraries themselves, which are not compile time dependencies.
|
| + if target.in_roots and \
|
| + (target.is_executable or
|
| + (not target.added_to_compile_targets and
|
| + (add_if_no_ancestor or target.requires_build)) or
|
| + (target.is_static_library and add_if_no_ancestor and
|
| + not target.is_or_has_linked_ancestor)):
|
| + print '\t\tadding to compile targets', target.name, 'executable', \
|
| + target.is_executable, 'added_to_compile_targets', \
|
| + target.added_to_compile_targets, 'add_if_no_ancestor', \
|
| + add_if_no_ancestor, 'requires_build', target.requires_build, \
|
| + 'is_static_library', target.is_static_library, \
|
| + 'is_or_has_linked_ancestor', target.is_or_has_linked_ancestor
|
| + result.add(target)
|
| + target.added_to_compile_targets = True
|
| +
|
| +
|
| +def _GetCompileTargets(matching_targets, supplied_targets):
|
| + """Returns the set of Targets that require a build.
|
| + matching_targets: targets that changed and need to be built.
|
| + supplied_targets: set of targets supplied to analyzer to search from."""
|
| + result = set()
|
| + for target in matching_targets:
|
| + print 'finding compile targets for match', target.name
|
| + _AddCompileTargets(target, supplied_targets, True, result)
|
| return result
|
|
|
|
|
| @@ -461,6 +580,16 @@ def _WriteOutput(params, **values):
|
| print 'Targets that require a build:'
|
| for target in values['build_targets']:
|
| print '\t', target
|
| + if 'compile_targets' in values:
|
| + values['compile_targets'].sort()
|
| + print 'Targets that need to be built:'
|
| + for target in values['compile_targets']:
|
| + print '\t', target
|
| + if 'test_targets' in values:
|
| + values['test_targets'].sort()
|
| + print 'Test targets:'
|
| + for target in values['test_targets']:
|
| + print '\t', target
|
|
|
| output_path = params.get('generator_flags', {}).get(
|
| 'analyzer_output_path', None)
|
| @@ -520,60 +649,189 @@ def CalculateVariables(default_variables, params):
|
| default_variables.setdefault('OS', operating_system)
|
|
|
|
|
| +def _GenerateOutputDeprecated(target_list, target_dicts, data, params, config):
|
| + """Old deprecated behavior, will be nuked shortly."""
|
| + toplevel_dir = _ToGypPath(os.path.abspath(params['options'].toplevel_dir))
|
| +
|
| + if _WasGypIncludeFileModified(params, config.files):
|
| + result_dict = { 'status': all_changed_string,
|
| + 'targets': list(config.targets) }
|
| + _WriteOutput(params, **result_dict)
|
| + return
|
| +
|
| + all_targets, matching_targets, root_targets = _GenerateTargets(
|
| + data, target_list, target_dicts, toplevel_dir, frozenset(config.files),
|
| + params['build_files'])
|
| +
|
| + unqualified_mapping, invalid_targets = _GetUnqualifiedToTargetMapping(
|
| + all_targets, config.targets)
|
| +
|
| + if matching_targets:
|
| + search_targets = _LookupTargets(config.targets, unqualified_mapping)
|
| + print 'supplied targets'
|
| + for target in config.targets:
|
| + print '\t', target
|
| + print 'expanded supplied targets'
|
| + for target in search_targets:
|
| + print '\t', target.name
|
| + # Reset the visited status for _GetBuildTargets.
|
| + for target in all_targets.itervalues():
|
| + target.visited = False
|
| + build_targets = _GetBuildTargetsDeprecated(matching_targets, search_targets)
|
| + build_targets = [gyp.common.ParseQualifiedTarget(target.name)[1]
|
| + for target in build_targets]
|
| + else:
|
| + build_targets = []
|
| +
|
| + result_dict = { 'targets': build_targets,
|
| + 'status': found_dependency_string if matching_targets else
|
| + no_dependency_string,
|
| + 'build_targets': build_targets}
|
| + if invalid_targets:
|
| + result_dict['invalid_targets'] = invalid_targets
|
| + _WriteOutput(params, **result_dict)
|
| +
|
| +
|
| +class TargetCalculator(object):
|
| + """Calculates the matching test_targets and matching compile_targets."""
|
| + def __init__(self, files, additional_compile_target_names, test_target_names,
|
| + data, target_list, target_dicts, toplevel_dir, build_files):
|
| + self._additional_compile_target_names = set(additional_compile_target_names)
|
| + self._test_target_names = set(test_target_names)
|
| + self._name_to_target, self._changed_targets, self._root_targets = (
|
| + _GenerateTargets(data, target_list, target_dicts, toplevel_dir,
|
| + frozenset(files), build_files))
|
| + self._unqualified_mapping, self.invalid_targets = (
|
| + _GetUnqualifiedToTargetMapping(self._name_to_target,
|
| + self._supplied_target_names_no_all()))
|
| +
|
| + def _supplied_target_names(self):
|
| + return self._additional_compile_target_names | self._test_target_names
|
| +
|
| + def _supplied_target_names_no_all(self):
|
| + """Returns the supplied test targets without 'all'."""
|
| + result = self._supplied_target_names();
|
| + result.discard('all')
|
| + return result
|
| +
|
| + def is_build_impacted(self):
|
| + """Returns true if the supplied files impact the build at all."""
|
| + return self._changed_targets
|
| +
|
| + def find_matching_test_target_names(self):
|
| + """Returns the set of output test targets."""
|
| + assert self.is_build_impacted()
|
| + # Find the test targets first. 'all' is special cased to mean all the
|
| + # root targets. To deal with all the supplied |test_targets| are expanded
|
| + # to include the root targets during lookup. If any of the root targets
|
| + # match, we remove it and replace it with 'all'.
|
| + test_target_names_no_all = set(self._test_target_names)
|
| + test_target_names_no_all.discard('all')
|
| + test_targets_no_all = _LookupTargets(test_target_names_no_all,
|
| + self._unqualified_mapping)
|
| + test_target_names_contains_all = 'all' in self._test_target_names
|
| + if test_target_names_contains_all:
|
| + test_targets = [x for x in (set(test_targets_no_all) |
|
| + set(self._root_targets))]
|
| + else:
|
| + test_targets = [x for x in test_targets_no_all]
|
| + print 'supplied test_targets'
|
| + for target_name in self._test_target_names:
|
| + print '\t', target_name
|
| + print 'found test_targets'
|
| + for target in test_targets:
|
| + print '\t', target.name
|
| + print 'searching for matching test targets'
|
| + matching_test_targets = _GetTargetsDependingOnMatchingTargets(test_targets)
|
| + matching_test_targets_contains_all = (test_target_names_contains_all and
|
| + set(matching_test_targets) &
|
| + set(self._root_targets))
|
| + if matching_test_targets_contains_all:
|
| + # Remove any of the targets for all that were not explicitly supplied,
|
| + # 'all' is subsequentely added to the matching names below.
|
| + matching_test_targets = [x for x in (set(matching_test_targets) &
|
| + set(test_targets_no_all))]
|
| + print 'matched test_targets'
|
| + for target in matching_test_targets:
|
| + print '\t', target.name
|
| + matching_target_names = [gyp.common.ParseQualifiedTarget(target.name)[1]
|
| + for target in matching_test_targets]
|
| + if matching_test_targets_contains_all:
|
| + matching_target_names.append('all')
|
| + print '\tall'
|
| + return matching_target_names
|
| +
|
| + def find_matching_compile_target_names(self):
|
| + """Returns the set of output compile targets."""
|
| + assert self.is_build_impacted();
|
| + # Compile targets are found by searching up from changed targets.
|
| + # Reset the visited status for _GetBuildTargets.
|
| + for target in self._name_to_target.itervalues():
|
| + target.visited = False
|
| +
|
| + supplied_targets = _LookupTargets(self._supplied_target_names_no_all(),
|
| + self._unqualified_mapping)
|
| + if 'all' in self._supplied_target_names():
|
| + supplied_targets = [x for x in (set(supplied_targets) |
|
| + set(self._root_targets))]
|
| + print 'Supplied test_targets & compile_targets'
|
| + for target in supplied_targets:
|
| + print '\t', target.name
|
| + print 'Finding compile targets'
|
| + compile_targets = _GetCompileTargets(self._changed_targets,
|
| + supplied_targets)
|
| + return [gyp.common.ParseQualifiedTarget(target.name)[1]
|
| + for target in compile_targets]
|
| +
|
| +
|
| def GenerateOutput(target_list, target_dicts, data, params):
|
| """Called by gyp as the final stage. Outputs results."""
|
| config = Config()
|
| try:
|
| config.Init(params)
|
| +
|
| if not config.files:
|
| raise Exception('Must specify files to analyze via config_path generator '
|
| 'flag')
|
|
|
| + if config.deprecated_mode:
|
| + _GenerateOutputDeprecated(target_list, target_dicts, data, params,
|
| + config)
|
| + return
|
| +
|
| toplevel_dir = _ToGypPath(os.path.abspath(params['options'].toplevel_dir))
|
| if debug:
|
| print 'toplevel_dir', toplevel_dir
|
|
|
| if _WasGypIncludeFileModified(params, config.files):
|
| result_dict = { 'status': all_changed_string,
|
| - 'targets': list(config.targets) }
|
| + 'test_targets': list(config.test_target_names),
|
| + 'compile_targets': list(
|
| + config.additional_compile_target_names |
|
| + config.test_target_names) }
|
| _WriteOutput(params, **result_dict)
|
| return
|
|
|
| - all_targets, matching_targets, _ = _GenerateTargets(
|
| - data, target_list, target_dicts, toplevel_dir, frozenset(config.files),
|
| - params['build_files'])
|
| -
|
| - unqualified_mapping = _GetUnqualifiedToTargetMapping(all_targets,
|
| - config.targets)
|
| - invalid_targets = None
|
| - if len(unqualified_mapping) != len(config.targets):
|
| - invalid_targets = _NamesNotIn(config.targets, unqualified_mapping)
|
| -
|
| - if matching_targets:
|
| - search_targets = _LookupTargets(config.targets, unqualified_mapping)
|
| - print 'supplied targets'
|
| - for target in config.targets:
|
| - print '\t', target
|
| - print 'expanded supplied targets'
|
| - for target in search_targets:
|
| - print '\t', target.name
|
| - # Reset the visited status for _GetBuildTargets.
|
| - for target in all_targets.itervalues():
|
| - target.visited = False
|
| - print 'Finding build targets'
|
| - build_targets = _GetBuildTargets(matching_targets, search_targets)
|
| - build_targets = [gyp.common.ParseQualifiedTarget(target.name)[1]
|
| - for target in build_targets]
|
| - else:
|
| - build_targets = []
|
| -
|
| - # TODO(sky): nuke 'targets'.
|
| - result_dict = { 'targets': build_targets,
|
| - 'status': found_dependency_string if matching_targets else
|
| - no_dependency_string,
|
| - 'build_targets': build_targets}
|
| - if invalid_targets:
|
| - result_dict['invalid_targets'] = invalid_targets
|
| + calculator = TargetCalculator(config.files,
|
| + config.additional_compile_target_names,
|
| + config.test_target_names, data,
|
| + target_list, target_dicts, toplevel_dir,
|
| + params['build_files'])
|
| + if not calculator.is_build_impacted():
|
| + _WriteOutput(params, test_targets=[], compile_targets=[],
|
| + status=no_dependency_string,
|
| + invalid_targets=calculator.invalid_targets)
|
| + return
|
| +
|
| + test_target_names = calculator.find_matching_test_target_names()
|
| + compile_target_names = calculator.find_matching_compile_target_names()
|
| + found_at_least_one_target = compile_target_names or test_target_names
|
| + result_dict = { 'test_targets': test_target_names,
|
| + 'status': found_dependency_string if
|
| + found_at_least_one_target else no_dependency_string,
|
| + 'compile_targets': compile_target_names}
|
| + if calculator.invalid_targets:
|
| + result_dict['invalid_targets'] = calculator.invalid_targets
|
| _WriteOutput(params, **result_dict)
|
|
|
| except Exception as e:
|
|
|