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