Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1008)

Unified Diff: pylib/gyp/generator/analyzer.py

Issue 1431343002: Changes semantics of analyzer (Closed) Base URL: https://chromium.googlesource.com/external/gyp@master
Patch Set: fix all modified output Created 5 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « no previous file | test/analyzer/gyptest-analyzer.py » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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:
« no previous file with comments | « no previous file | test/analyzer/gyptest-analyzer.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698