summaryrefslogtreecommitdiff
path: root/tools/gyp/pylib/gyp/generator/analyzer.py
blob: 88d6c72d7ada616c7ae72c9f791c2ded3d2d86cf (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
# Copyright (c) 2014 Google Inc. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""
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.
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.
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. This list
  always contains the output of test_targets as well.
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 test_targets and compile_targets are not output) or at
  least one file was found.
invalid_targets: list of supplied targets that were not found.

Example:
Consider a graph like the following:
  A     D
 / \
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 |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 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".
"""

from __future__ import print_function

import gyp.common
import gyp.ninja_syntax as ninja_syntax
import json
import os
import posixpath
import sys

debug = False

found_dependency_string = 'Found dependency'
no_dependency_string = 'No dependencies'
# Status when it should be assumed that everything has changed.
all_changed_string = 'Found dependency (all)'

# MatchStatus is used indicate if and how a target depends upon the supplied
# sources.
# The target's sources contain one of the supplied paths.
MATCH_STATUS_MATCHES = 1
# The target has a dependency on another target that contains one of the
# supplied paths.
MATCH_STATUS_MATCHES_BY_DEPENDENCY = 2
# The target's sources weren't in the supplied paths and none of the target's
# dependencies depend upon a target that matched.
MATCH_STATUS_DOESNT_MATCH = 3
# The target doesn't contain the source, but the dependent targets have not yet
# been visited to determine a more specific status yet.
MATCH_STATUS_TBD = 4

generator_supports_multiple_toolsets = gyp.common.CrossCompileRequested()

generator_wants_static_library_dependencies_adjusted = False

generator_default_variables = {
}
for dirname in ['INTERMEDIATE_DIR', 'SHARED_INTERMEDIATE_DIR', 'PRODUCT_DIR',
                'LIB_DIR', 'SHARED_LIB_DIR']:
  generator_default_variables[dirname] = '!!!'

for unused in ['RULE_INPUT_PATH', 'RULE_INPUT_ROOT', 'RULE_INPUT_NAME',
               'RULE_INPUT_DIRNAME', 'RULE_INPUT_EXT',
               'EXECUTABLE_PREFIX', 'EXECUTABLE_SUFFIX',
               'STATIC_LIB_PREFIX', 'STATIC_LIB_SUFFIX',
               'SHARED_LIB_PREFIX', 'SHARED_LIB_SUFFIX',
               'CONFIGURATION_NAME']:
  generator_default_variables[unused] = ''


def _ToGypPath(path):
  """Converts a path to the format used by gyp."""
  if os.sep == '\\' and os.altsep == '/':
    return path.replace('\\', '/')
  return path


def _ResolveParent(path, base_path_components):
  """Resolves |path|, which starts with at least one '../'. Returns an empty
  string if the path shouldn't be considered. See _AddSources() for a
  description of |base_path_components|."""
  depth = 0
  while path.startswith('../'):
    depth += 1
    path = path[3:]
  # Relative includes may go outside the source tree. For example, an action may
  # have inputs in /usr/include, which are not in the source tree.
  if depth > len(base_path_components):
    return ''
  if depth == len(base_path_components):
    return path
  return '/'.join(base_path_components[0:len(base_path_components) - depth]) + \
      '/' + path


def _AddSources(sources, base_path, base_path_components, result):
  """Extracts valid sources from |sources| and adds them to |result|. Each
  source file is relative to |base_path|, but may contain '..'. To make
  resolving '..' easier |base_path_components| contains each of the
  directories in |base_path|. Additionally each source may contain variables.
  Such sources are ignored as it is assumed dependencies on them are expressed
  and tracked in some other means."""
  # NOTE: gyp paths are always posix style.
  for source in sources:
    if not len(source) or source.startswith('!!!') or source.startswith('$'):
      continue
    # variable expansion may lead to //.
    org_source = source
    source = source[0] + source[1:].replace('//', '/')
    if source.startswith('../'):
      source = _ResolveParent(source, base_path_components)
      if len(source):
        result.append(source)
      continue
    result.append(base_path + source)
    if debug:
      print('AddSource', org_source, result[len(result) - 1])


def _ExtractSourcesFromAction(action, base_path, base_path_components,
                              results):
  if 'inputs' in action:
    _AddSources(action['inputs'], base_path, base_path_components, results)


def _ToLocalPath(toplevel_dir, path):
  """Converts |path| to a path relative to |toplevel_dir|."""
  if path == toplevel_dir:
    return ''
  if path.startswith(toplevel_dir + '/'):
    return path[len(toplevel_dir) + len('/'):]
  return path


def _ExtractSources(target, target_dict, toplevel_dir):
  # |target| is either absolute or relative and in the format of the OS. Gyp
  # source paths are always posix. Convert |target| to a posix path relative to
  # |toplevel_dir_|. This is done to make it easy to build source paths.
  base_path = posixpath.dirname(_ToLocalPath(toplevel_dir, _ToGypPath(target)))
  base_path_components = base_path.split('/')

  # Add a trailing '/' so that _AddSources() can easily build paths.
  if len(base_path):
    base_path += '/'

  if debug:
    print('ExtractSources', target, base_path)

  results = []
  if 'sources' in target_dict:
    _AddSources(target_dict['sources'], base_path, base_path_components,
                results)
  # Include the inputs from any actions. Any changes to these affect the
  # resulting output.
  if 'actions' in target_dict:
    for action in target_dict['actions']:
      _ExtractSourcesFromAction(action, base_path, base_path_components,
                                results)
  if 'rules' in target_dict:
    for rule in target_dict['rules']:
      _ExtractSourcesFromAction(rule, base_path, base_path_components, results)

  return results


class Target(object):
  """Holds information about a particular target:
  deps: set of Targets this Target depends upon. This is not recursive, only the
    direct dependent Targets.
  match_status: one of the MatchStatus values.
  back_deps: set of Targets that have a dependency on this Target.
  visited: used during iteration to indicate whether we've visited this target.
    This is used for two iterations, once in building the set of Targets and
    again in _GetBuildTargets().
  name: fully qualified name of the target.
  requires_build: True if the target type is such that it needs to be built.
    See _DoesTargetTypeRequireBuild for details.
  added_to_compile_targets: used when determining if the target was added to the
    set of targets that needs to be built.
  in_roots: true if this target is a descendant of one of the root nodes.
  is_executable: true if the type of target is executable.
  is_static_library: true if the type of target is static_library.
  is_or_has_linked_ancestor: true if the target does a link (eg executable), or
    if there is a target in back_deps that does a link."""
  def __init__(self, name):
    self.deps = set()
    self.match_status = MATCH_STATUS_TBD
    self.back_deps = set()
    self.name = name
    # TODO(sky): I don't like hanging this off Target. This state is specific
    # to certain functions and should be isolated there.
    self.visited = False
    self.requires_build = False
    self.added_to_compile_targets = False
    self.in_roots = False
    self.is_executable = False
    self.is_static_library = False
    self.is_or_has_linked_ancestor = False


class Config(object):
  """Details what we're looking for
  files: set of files to search for
  targets: see file description for details."""
  def __init__(self):
    self.files = []
    self.targets = set()
    self.additional_compile_target_names = set()
    self.test_target_names = set()

  def Init(self, params):
    """Initializes Config. This is a separate method as it raises an exception
    if there is a parse error."""
    generator_flags = params.get('generator_flags', {})
    config_path = generator_flags.get('config_path', None)
    if not config_path:
      return
    try:
      f = open(config_path, 'r')
      config = json.load(f)
      f.close()
    except IOError:
      raise Exception('Unable to open file ' + config_path)
    except ValueError as e:
      raise Exception('Unable to parse config file ' + config_path + str(e))
    if not isinstance(config, dict):
      raise Exception('config_path must be a JSON file containing a dictionary')
    self.files = config.get('files', [])
    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):
  """Returns true if the build file |build_file| is either in |files| or
  one of the files included by |build_file| is in |files|. |toplevel_dir| is
  the root of the source tree."""
  if _ToLocalPath(toplevel_dir, _ToGypPath(build_file)) in files:
    if debug:
      print('gyp file modified', build_file)
    return True

  # First element of included_files is the file itself.
  if len(data[build_file]['included_files']) <= 1:
    return False

  for include_file in data[build_file]['included_files'][1:]:
    # |included_files| are relative to the directory of the |build_file|.
    rel_include_file = \
        _ToGypPath(gyp.common.UnrelativePath(include_file, build_file))
    if _ToLocalPath(toplevel_dir, rel_include_file) in files:
      if debug:
        print('included gyp file modified, gyp_file=', build_file,
              'included file=', rel_include_file)
      return True
  return False


def _GetOrCreateTargetByName(targets, target_name):
  """Creates or returns the Target at targets[target_name]. If there is no
  Target for |target_name| one is created. Returns a tuple of whether a new
  Target was created and the Target."""
  if target_name in targets:
    return False, targets[target_name]
  target = Target(target_name)
  targets[target_name] = target
  return True, target


def _DoesTargetTypeRequireBuild(target_dict):
  """Returns true if the target type is such that it needs to be built."""
  # If a 'none' target has rules or actions we assume it requires a build.
  return bool(target_dict['type'] != 'none' or
              target_dict.get('actions') or target_dict.get('rules'))


def _GenerateTargets(data, target_list, target_dicts, toplevel_dir, files,
                     build_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|.
  . 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.
  name_to_target = {}

  # Targets that matched.
  matching_targets = []

  # Queue of targets to visit.
  targets_to_visit = target_list[:]

  # Maps from build file to a boolean indicating whether the build file is in
  # |files|.
  build_file_in_files = {}

  # Root targets across all files.
  roots = set()

  # Set of Targets in |build_files|.
  build_file_targets = set()

  while len(targets_to_visit) > 0:
    target_name = targets_to_visit.pop()
    created_target, target = _GetOrCreateTargetByName(name_to_target,
                                                      target_name)
    if created_target:
      roots.add(target)
    elif target.visited:
      continue

    target.visited = True
    target.requires_build = _DoesTargetTypeRequireBuild(
        target_dicts[target_name])
    target_type = target_dicts[target_name]['type']
    target.is_executable = target_type == 'executable'
    target.is_static_library = target_type == 'static_library'
    target.is_or_has_linked_ancestor = (target_type == 'executable' or
                                        target_type == 'shared_library')

    build_file = gyp.common.ParseQualifiedTarget(target_name)[0]
    if not build_file in build_file_in_files:
      build_file_in_files[build_file] = \
          _WasBuildFileModified(build_file, data, files, toplevel_dir)

    if build_file in build_files:
      build_file_targets.add(target)

    # If a build file (or any of its included files) is modified we assume all
    # targets in the file are modified.
    if build_file_in_files[build_file]:
      print('matching target from modified build file', target_name)
      target.match_status = MATCH_STATUS_MATCHES
      matching_targets.append(target)
    else:
      sources = _ExtractSources(target_name, target_dicts[target_name],
                                toplevel_dir)
      for source in sources:
        if _ToGypPath(os.path.normpath(source)) in files:
          print('target', target_name, 'matches', source)
          target.match_status = MATCH_STATUS_MATCHES
          matching_targets.append(target)
          break

    # Add dependencies to visit as well as updating back pointers for deps.
    for dep in target_dicts[target_name].get('dependencies', []):
      targets_to_visit.append(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 name_to_target, matching_targets, roots & build_file_targets


def _GetUnqualifiedToTargetMapping(all_targets, 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 {}, []
  to_find = set(to_find)
  for target_name in all_targets.keys():
    extracted = gyp.common.ParseQualifiedTarget(target_name)
    if len(extracted) > 1 and extracted[1] in to_find:
      to_find.remove(extracted[1])
      result[extracted[1]] = all_targets[target_name]
      if not to_find:
        return result, []
  return result, [x for x in to_find]


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


def _WriteOutput(params, **values):
  """Writes the output, either to stdout or a file is specified."""
  if 'error' in values:
    print('Error:', values['error'])
  if 'status' in values:
    print(values['status'])
  if 'targets' in values:
    values['targets'].sort()
    print('Supplied targets that depend on changed files:')
    for target in values['targets']:
      print('\t', target)
  if 'invalid_targets' in values:
    values['invalid_targets'].sort()
    print('The following targets were not found:')
    for target in values['invalid_targets']:
      print('\t', target)
  if 'build_targets' in values:
    values['build_targets'].sort()
    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)
  if not output_path:
    print(json.dumps(values))
    return
  try:
    f = open(output_path, 'w')
    f.write(json.dumps(values) + '\n')
    f.close()
  except IOError as e:
    print('Error writing to output file', output_path, str(e))


def _WasGypIncludeFileModified(params, files):
  """Returns true if one of the files in |files| is in the set of included
  files."""
  if params['options'].includes:
    for include in params['options'].includes:
      if _ToGypPath(os.path.normpath(include)) in files:
        print('Include file modified, assuming all changed', include)
        return True
  return False


def _NamesNotIn(names, mapping):
  """Returns a list of the values in |names| that are not in |mapping|."""
  return [name for name in names if name not in mapping]


def _LookupTargets(names, mapping):
  """Returns a list of the mapping[name] for each value in |names| that is in
  |mapping|."""
  return [mapping[name] for name in names if name in mapping]


def CalculateVariables(default_variables, params):
  """Calculate additional variables for use in the build (called by gyp)."""
  flavor = gyp.common.GetFlavor(params)
  if flavor == 'mac':
    default_variables.setdefault('OS', 'mac')
  elif flavor == 'win':
    default_variables.setdefault('OS', 'win')
    # Copy additional generator configuration data from VS, which is shared
    # by the Windows Ninja generator.
    import gyp.generator.msvs as msvs_generator
    generator_additional_non_configuration_keys = getattr(msvs_generator,
        'generator_additional_non_configuration_keys', [])
    generator_additional_path_sections = getattr(msvs_generator,
        'generator_additional_path_sections', [])

    gyp.msvs_emulation.CalculateCommonVariables(default_variables, params)
  else:
    operating_system = flavor
    if flavor == 'android':
      operating_system = 'linux'  # Keep this legacy behavior for now.
    default_variables.setdefault('OS', operating_system)


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')

    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,
                      'test_targets': list(config.test_target_names),
                      'compile_targets': list(
                        config.additional_compile_target_names |
                        config.test_target_names) }
      _WriteOutput(params, **result_dict)
      return

    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():
      result_dict = { 'status': no_dependency_string,
                      'test_targets': [],
                      'compile_targets': [] }
      if calculator.invalid_targets:
        result_dict['invalid_targets'] = calculator.invalid_targets
      _WriteOutput(params, **result_dict)
      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': list(
                        set(compile_target_names) |
                        set(test_target_names)) }
    if calculator.invalid_targets:
      result_dict['invalid_targets'] = calculator.invalid_targets
    _WriteOutput(params, **result_dict)

  except Exception as e:
    _WriteOutput(params, error=str(e))