Currently I've special cased gyp* file modifications higher in the
stack. By that I mean if a gyp* file has been modified I don't run
analyze and assume everything has changed. This change adds support
for modification to gyp* files. If a gyp* file has changed it assumes
all targets in the file are modified. Similarly if an included file
has been modified all targets in the file that did the include are
considered modified. Lastly, if one of the modified files is specified
on the command line via -I the I early out and assume everything needs
to be recompiled.

BUG=109173
R=scottmg@chromium.org

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

git-svn-id: http://gyp.googlecode.com/svn/trunk@1962 78cadc50-ecff-11dd-a971-7dbc132099af
diff --git a/pylib/gyp/generator/analyzer.py b/pylib/gyp/generator/analyzer.py
index dc55da6..2648e7d 100644
--- a/pylib/gyp/generator/analyzer.py
+++ b/pylib/gyp/generator/analyzer.py
@@ -64,6 +64,12 @@
                '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 __ExtractBasePath(target):
   """Extracts the path components of the specified gyp target path."""
   last_index = target.rfind('/')
@@ -120,10 +126,7 @@
   # |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.
-  if os.sep == '\\' and os.altsep == '/':
-    base_path = target.replace('\\', '/')
-  else:
-    base_path = target
+  base_path = _ToGypPath(target)
   if base_path == toplevel_dir:
     base_path = ''
   elif base_path.startswith(toplevel_dir + '/'):
@@ -216,7 +219,30 @@
     except IOError:
       raise Exception('Unable to open file', file_path)
 
-def __GenerateTargets(target_list, target_dicts, toplevel_dir, files):
+def _WasBuildFileModified(build_file, data, files):
+  """Returns true if the build file |build_file| is either in |files| or
+  one of the files included by |build_file| is in |files|."""
+  if _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 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 __GenerateTargets(data, target_list, target_dicts, toplevel_dir, files):
   """Generates a dictionary with the key the name of a target and the value a
   Target. |toplevel_dir| is the root of the source tree. If the sources of
   a target match that of |files|, then |target.matched| is set to True.
@@ -229,6 +255,10 @@
 
   matched = False
 
+  # Maps from build file to a boolean indicating whether the build file is in
+  # |files|.
+  build_file_in_files = {}
+
   while len(targets_to_visit) > 0:
     target_name = targets_to_visit.pop()
     if target_name in targets:
@@ -236,13 +266,25 @@
 
     target = Target()
     targets[target_name] = target
-    sources = __ExtractSources(target_name, target_dicts[target_name],
-                               toplevel_dir)
-    for source in sources:
-      if source in files:
-        target.match_status = MATCH_STATUS_MATCHES
-        matched = True
-        break
+
+    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)
+
+    # 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]:
+      target.match_status = MATCH_STATUS_MATCHES
+      matched = True
+    else:
+      sources = __ExtractSources(target_name, target_dicts[target_name],
+                                 toplevel_dir)
+      for source in sources:
+        if source in files:
+          target.match_status = MATCH_STATUS_MATCHES
+          matched = True
+          break
 
     for dep in target_dicts[target_name].get('dependencies', []):
       targets[target_name].deps.add(dep)
@@ -345,15 +387,28 @@
       raise Exception('Must specify files to analyze via config_path generator '
                       'flag')
 
-    toplevel_dir = os.path.abspath(params['options'].toplevel_dir)
-    if os.sep == '\\' and os.altsep == '/':
-      toplevel_dir = toplevel_dir.replace('\\', '/')
+    toplevel_dir = _ToGypPath(os.path.abspath(params['options'].toplevel_dir))
     if debug:
       print 'toplevel_dir', toplevel_dir
 
-    all_targets, matched = __GenerateTargets(target_list, target_dicts,
-                                             toplevel_dir,
-                                             frozenset(config.files))
+    matched = False
+    matched_include = False
+
+    # If one of the modified files is an include file then everything is
+    # affected.
+    if params['options'].includes:
+      for include in params['options'].includes:
+        if _ToGypPath(include) in config.files:
+          if debug:
+            print 'include path modified', include
+          matched_include = True
+          matched = True
+          break
+
+    if not matched:
+      all_targets, matched = __GenerateTargets(data, target_list, target_dicts,
+                                               toplevel_dir,
+                                               frozenset(config.files))
 
     # Set of targets that refer to one of the files.
     if config.look_for_dependency_only:
@@ -361,7 +416,9 @@
       return
 
     warning = None
-    if matched:
+    if matched_include:
+      output_targets = config.targets
+    elif matched:
       unqualified_mapping = _GetUnqualifiedToQualifiedMapping(
           all_targets, config.targets)
       if len(unqualified_mapping) != len(config.targets):
diff --git a/test/analyzer/common.gypi b/test/analyzer/common.gypi
new file mode 100644
index 0000000..7c664e4
--- /dev/null
+++ b/test/analyzer/common.gypi
@@ -0,0 +1,6 @@
+# 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.
+
+{
+}
diff --git a/test/analyzer/gyptest-analyzer.new.py b/test/analyzer/gyptest-analyzer.new.py
index db7e125..b736867 100644
--- a/test/analyzer/gyptest-analyzer.new.py
+++ b/test/analyzer/gyptest-analyzer.new.py
@@ -42,8 +42,14 @@
            '-Ganalyzer_output_path=analyzer_output')
   test.run_gyp('test.gyp', *args, **kw)
 
+def run_analyzer2(*args, **kw):
+  """Runs the test specifying a particular config and output path."""
+  args += ('-Gconfig_path=test_file',
+           '-Ganalyzer_output_path=analyzer_output')
+  test.run_gyp('test2.gyp', *args, **kw)
+
 def EnsureContains(targets=set(), matched=False):
-  """Verifies output contains |targets| and |direct_targets|."""
+  """Verifies output contains |targets|."""
   result = _ReadOutputFileContents()
   if result.get('error', None):
     print 'unexpected error', result.get('error')
@@ -196,4 +202,28 @@
 run_analyzer()
 EnsureContains(matched=True)
 
+# Assertions when modifying build (gyp/gypi) files, especially when said files
+# are included.
+_CreateTestFile(['subdir2/d.cc'], ['exe', 'exe2', 'foo', 'exe3'])
+run_analyzer2()
+EnsureContains(matched=True, targets={'exe', 'foo'})
+
+_CreateTestFile(['subdir2/subdir.includes.gypi'],
+                ['exe', 'exe2', 'foo', 'exe3'])
+run_analyzer2()
+EnsureContains(matched=True, targets={'exe', 'foo'})
+
+_CreateTestFile(['subdir2/subdir.gyp'], ['exe', 'exe2', 'foo', 'exe3'])
+run_analyzer2()
+EnsureContains(matched=True, targets={'exe', 'foo'})
+
+_CreateTestFile(['test2.includes.gypi'], ['exe', 'exe2', 'foo', 'exe3'])
+run_analyzer2()
+EnsureContains(matched=True, targets={'exe', 'exe2', 'exe3'})
+
+# Verify modifying a file included makes all targets dirty.
+_CreateTestFile(['common.gypi'], ['exe', 'exe2', 'foo', 'exe3'])
+run_analyzer2('-Icommon.gypi')
+EnsureContains(matched=True, targets={'exe', 'foo', 'exe2', 'exe3'})
+
 test.pass_test()
diff --git a/test/analyzer/subdir2/subdir.gyp b/test/analyzer/subdir2/subdir.gyp
new file mode 100644
index 0000000..d6c709c
--- /dev/null
+++ b/test/analyzer/subdir2/subdir.gyp
@@ -0,0 +1,18 @@
+# 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.
+
+{
+  'targets': [
+    {
+      'target_name': 'foo',
+      'type': 'static_library',
+      'sources': [
+        'subdir_source.c',
+      ],
+      'includes': [
+        'subdir.includes.gypi',
+      ],
+    },
+  ],
+}
diff --git a/test/analyzer/subdir2/subdir.includes.gypi b/test/analyzer/subdir2/subdir.includes.gypi
new file mode 100644
index 0000000..324e92b
--- /dev/null
+++ b/test/analyzer/subdir2/subdir.includes.gypi
@@ -0,0 +1,9 @@
+# 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.
+
+{
+  'sources': [
+    'd.cc'
+  ],
+}
diff --git a/test/analyzer/test2.gyp b/test/analyzer/test2.gyp
new file mode 100644
index 0000000..782b6e6
--- /dev/null
+++ b/test/analyzer/test2.gyp
@@ -0,0 +1,25 @@
+# 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.
+
+{
+  'targets': [
+    {
+      'target_name': 'exe',
+      'type': 'executable',
+      'dependencies': [
+        'subdir2/subdir.gyp:foo',
+      ],
+    },
+    {
+      'target_name': 'exe2',
+      'type': 'executable',
+      'includes': [
+        'test2.includes.gypi',
+      ],
+    },
+  ],
+  'includes': [
+    'test2.toplevel_includes.gypi',
+  ],
+}
diff --git a/test/analyzer/test2.includes.gypi b/test/analyzer/test2.includes.gypi
new file mode 100644
index 0000000..3e21de2
--- /dev/null
+++ b/test/analyzer/test2.includes.gypi
@@ -0,0 +1,13 @@
+# 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.
+
+{
+  'sources': [
+    'a.cc',
+    'b.cc'
+  ],
+  'includes': [
+    'test2.includes.includes.gypi',
+  ],
+}
diff --git a/test/analyzer/test2.includes.includes.gypi b/test/analyzer/test2.includes.includes.gypi
new file mode 100644
index 0000000..de3a025
--- /dev/null
+++ b/test/analyzer/test2.includes.includes.gypi
@@ -0,0 +1,9 @@
+# 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.
+
+{
+  'sources': [
+    'c.cc'
+  ],
+}
diff --git a/test/analyzer/test2.toplevel_includes.gypi b/test/analyzer/test2.toplevel_includes.gypi
new file mode 100644
index 0000000..54fa453
--- /dev/null
+++ b/test/analyzer/test2.toplevel_includes.gypi
@@ -0,0 +1,15 @@
+# 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.
+
+{
+  'targets': [
+    {
+      'target_name': 'exe3',
+      'type': 'executable',
+      'sources': [
+        'e.cc',
+      ],
+    },
+  ],
+}