First crack at adding a GYP_GENERATOR for determining various things

This version reads a file to get a list of input files, then outputs
if any of the input files are contained as sources (or inputs to
actions/rules) in any targets.

R=mark@chromium.org

Patch by Scott Violet <sky@chromium.org>

Review URL: https://codereview.chromium.org/330053003

git-svn-id: http://gyp.googlecode.com/svn/trunk@1936 78cadc50-ecff-11dd-a971-7dbc132099af
diff --git a/pylib/gyp/generator/analyzer.py b/pylib/gyp/generator/analyzer.py
new file mode 100644
index 0000000..007d17d
--- /dev/null
+++ b/pylib/gyp/generator/analyzer.py
@@ -0,0 +1,192 @@
+# 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 file_path) the list of relative file paths to consider. If
+any target has at least one of the paths as a source (or input to an action or
+rule) then 'Found dependency' is output, otherwise 'No dependencies' is output.
+"""
+
+import gyp.common
+import gyp.ninja_syntax as ninja_syntax
+import os
+import posixpath
+
+generator_supports_multiple_toolsets = True
+
+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 __MakeRelativeTargetName(path):
+  """Converts a gyp target name into a relative name. For example, the path to a
+  gyp file may be something like c:\foo\bar.gyp:target, this converts it to
+  bar.gyp.
+  """
+  prune_path = os.getcwd()
+  if path.startswith(prune_path):
+    path = path[len(prune_path):]
+  # Gyp paths are always posix style.
+  path = path.replace('\\', '/')
+  if path.endswith('#target'):
+    path = path[0:len(path) - len('#target')]
+  return path
+
+def __ExtractBasePath(target):
+  """Extracts the path components of the specified gyp target path."""
+  last_index = target.rfind('/')
+  if last_index == -1:
+    return ''
+  return target[0:(last_index + 1)]
+
+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 //.
+    source = source[0] + source[1:].replace('//', '/')
+    if source.startswith('../'):
+      path_components = base_path_components[:]
+      # Resolve relative paths.
+      while source.startswith('../'):
+        path_components.pop(len(path_components) - 1)
+        source = source[3:]
+      result.append('/'.join(path_components) + source)
+      continue
+    result.append(base_path + source)
+
+def __ExtractSourcesFromAction(action, base_path, base_path_components,
+                               results):
+  if 'inputs' in action:
+    __AddSources(action['inputs'], base_path, base_path_components, results)
+
+def __ExtractSources(target, target_dict):
+  base_path = posixpath.dirname(target)
+  base_path_components = base_path.split('/')
+  # Add a trailing '/' so that __AddSources() can easily build paths.
+  if len(base_path):
+    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 effect 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:
+  sources: set of source files defined by this target. This includes inputs to
+           actions and rules.
+  deps: list of direct dependencies."""
+  def __init__(self):
+    self.sources = []
+    self.deps = []
+
+def __GenerateTargets(target_list, target_dicts):
+  """Generates a dictionary with the key the name of a target and the value a
+  Target."""
+  targets = {}
+
+  # Queue of targets to visit.
+  targets_to_visit = target_list[:]
+
+  while len(targets_to_visit) > 0:
+    absolute_target_name = targets_to_visit.pop()
+    # |absolute_target| may be an absolute path and may include #target.
+    # References to targets are relative, so we need to clean the name.
+    relative_target_name = __MakeRelativeTargetName(absolute_target_name)
+    if relative_target_name in targets:
+      continue
+
+    target = Target()
+    targets[relative_target_name] = target
+    target.sources.extend(__ExtractSources(relative_target_name,
+                                           target_dicts[absolute_target_name]))
+
+    for dep in target_dicts[absolute_target_name].get('dependencies', []):
+      targets[relative_target_name].deps.append(__MakeRelativeTargetName(dep))
+      targets_to_visit.append(dep)
+
+  return targets
+
+def __GetFiles(params):
+  """Returns the list of files to analyze, or None if none specified."""
+  generator_flags = params.get('generator_flags', {})
+  file_path = generator_flags.get('file_path', None)
+  if not file_path:
+    return None
+  try:
+    f = open(file_path, 'r')
+    result = []
+    for file_name in f:
+      if file_name.endswith('\n'):
+        file_name = file_name[0:len(file_name) - 1]
+      if len(file_name):
+        result.append(file_name)
+    f.close()
+    return result
+  except IOError:
+    print 'Unable to open file', file_path
+  return None
+
+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')
+  else:
+    operating_system = flavor
+    if flavor == 'android':
+      operating_system = 'linux'  # Keep this legacy behavior for now.
+    default_variables.setdefault('OS', operating_system)
+
+def GenerateOutput(target_list, target_dicts, data, params):
+  """Called by gyp as the final stage. Outputs results."""
+  files = __GetFiles(params)
+  if not files:
+    print 'Must specify files to analyze via file_path generator flag'
+    return
+
+  targets = __GenerateTargets(target_list, target_dicts)
+
+  files_set = frozenset(files)
+  found_in_all_sources = 0
+  for target_name, target in targets.iteritems():
+    sources = files_set.intersection(target.sources)
+    if len(sources):
+      print 'Found dependency'
+      return
+
+  print 'No dependencies'
diff --git a/test/analyzer/gyptest-analyzer.py b/test/analyzer/gyptest-analyzer.py
new file mode 100644
index 0000000..a42748f
--- /dev/null
+++ b/test/analyzer/gyptest-analyzer.py
@@ -0,0 +1,76 @@
+#!/usr/bin/env python
+# 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.
+
+"""Tests for analyzer
+"""
+
+import TestGyp
+
+found = 'Found dependency\n'
+not_found = 'No dependencies\n'
+
+def __CreateTestFile(files):
+  f = open('test_file', 'w')
+  for file in files:
+    f.write(file + '\n')
+  f.close()
+
+test = TestGyp.TestGypCustom(format='analyzer')
+
+# Verifies file_path must be specified.
+test.run_gyp('test.gyp',
+             stdout='Must specify files to analyze via file_path generator '
+             'flag\n')
+
+# Trivial test of a source.
+__CreateTestFile(['foo.c'])
+test.run_gyp('test.gyp', '-Gfile_path=test_file', stdout=found)
+
+# Conditional source that is excluded.
+__CreateTestFile(['conditional_source.c'])
+test.run_gyp('test.gyp', '-Gfile_path=test_file', stdout=not_found)
+
+# Conditional source that is included by way of argument.
+__CreateTestFile(['conditional_source.c'])
+test.run_gyp('test.gyp', '-Gfile_path=test_file', '-Dtest_variable=1',
+             stdout=found)
+
+# Two unknown files.
+__CreateTestFile(['unknown1.c', 'unoknow2.cc'])
+test.run_gyp('test.gyp', '-Gfile_path=test_file', stdout=not_found)
+
+# Two unknown files.
+__CreateTestFile(['unknown1.c', 'subdir/subdir_sourcex.c'])
+test.run_gyp('test.gyp', '-Gfile_path=test_file', stdout=not_found)
+
+# Included dependency
+__CreateTestFile(['unknown1.c', 'subdir/subdir_source.c'])
+test.run_gyp('test.gyp', '-Gfile_path=test_file', stdout=found)
+
+# Included inputs to actions.
+__CreateTestFile(['action_input.c'])
+test.run_gyp('test.gyp', '-Gfile_path=test_file', stdout=found)
+
+# Don't consider outputs.
+__CreateTestFile(['action_output.c'])
+test.run_gyp('test.gyp', '-Gfile_path=test_file', stdout=not_found)
+
+# Rule inputs.
+__CreateTestFile(['rule_input.c'])
+test.run_gyp('test.gyp', '-Gfile_path=test_file', stdout=found)
+
+# Ignore patch specified with PRODUCT_DIR.
+__CreateTestFile(['product_dir_input.c'])
+test.run_gyp('test.gyp', '-Gfile_path=test_file', stdout=not_found)
+
+# Path specified via a variable.
+__CreateTestFile(['subdir/subdir_source2.c'])
+test.run_gyp('test.gyp', '-Gfile_path=test_file', stdout=found)
+
+# Verifies paths with // are fixed up correctly.
+__CreateTestFile(['parent_source.c'])
+test.run_gyp('test.gyp', '-Gfile_path=test_file', stdout=found)
+
+test.pass_test()
diff --git a/test/analyzer/subdir/subdir.gyp b/test/analyzer/subdir/subdir.gyp
new file mode 100644
index 0000000..cd0f013
--- /dev/null
+++ b/test/analyzer/subdir/subdir.gyp
@@ -0,0 +1,19 @@
+# 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.
+
+{
+  'variables': {
+    'trailing_dir_path': '../',
+   },
+  'targets': [
+    {
+      'target_name': 'foo',
+      'type': 'static_library',
+      'sources': [
+        'subdir_source.c',
+        '<(trailing_dir_path)/parent_source.c',
+      ],
+    },
+  ],
+}
diff --git a/test/analyzer/test.gyp b/test/analyzer/test.gyp
new file mode 100644
index 0000000..b3fcdd3
--- /dev/null
+++ b/test/analyzer/test.gyp
@@ -0,0 +1,54 @@
+# 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.
+
+{
+  'variables': {
+    'test_variable%': 0,
+    'variable_path': 'subdir',
+   },
+  'targets': [
+    {
+      'target_name': 'exe',
+      'type': 'executable',
+      'dependencies': [
+        'subdir/subdir.gyp:foo',
+      ],
+      'sources': [
+        'foo.c',
+        '<(variable_path)/subdir_source2.c',
+      ],
+      'conditions': [
+        ['test_variable==1', {
+          'sources': [
+            'conditional_source.c',
+          ],
+        }],
+      ],
+      'actions': [
+        {
+          'action_name': 'action',
+          'inputs': [
+            '<(PRODUCT_DIR)/product_dir_input.c',
+            'action_input.c',
+          ],
+          'outputs': [
+            'action_output.c',
+          ],
+        },
+      ],
+      'rules': [
+        {
+          'rule_name': 'rule',
+          'extension': 'pdf',
+          'inputs': [
+            'rule_input.c',
+          ],
+          'outputs': [
+            'rule_output.pdf',
+          ],
+        },
+      ],
+    },
+  ],
+}