Merge from Chromium at DEPS revision 290040

This commit was generated by merge_to_master.py.

Change-Id: Ia957cd5569a5c62711d273132c342efa9c54f05b
diff --git a/pylib/gyp/generator/analyzer.py b/pylib/gyp/generator/analyzer.py
index dc55da6..8a8ac70 100644
--- a/pylib/gyp/generator/analyzer.py
+++ b/pylib/gyp/generator/analyzer.py
@@ -64,16 +64,17 @@
                'CONFIGURATION_NAME']:
   generator_default_variables[unused] = ''
 
-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 __ResolveParent(path, base_path_components):
+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
+  string if the path shouldn't be considered. See _AddSources() for a
   description of |base_path_components|."""
   depth = 0
   while path.startswith('../'):
@@ -88,7 +89,8 @@
   return '/'.join(base_path_components[0:len(base_path_components) - depth]) + \
       '/' + path
 
-def __AddSources(sources, base_path, base_path_components, result):
+
+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
@@ -103,7 +105,7 @@
     org_source = source
     source = source[0] + source[1:].replace('//', '/')
     if source.startswith('../'):
-      source = __ResolveParent(source, base_path_components)
+      source = _ResolveParent(source, base_path_components)
       if len(source):
         result.append(source)
       continue
@@ -111,19 +113,18 @@
     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 __ExtractSources(target, target_dict, toplevel_dir):
+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, 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.
-  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 + '/'):
@@ -131,7 +132,7 @@
   base_path = posixpath.dirname(base_path)
   base_path_components = base_path.split('/')
 
-  # Add a trailing '/' so that __AddSources() can easily build paths.
+  # Add a trailing '/' so that _AddSources() can easily build paths.
   if len(base_path):
     base_path += '/'
 
@@ -140,20 +141,21 @@
 
   results = []
   if 'sources' in target_dict:
-    __AddSources(target_dict['sources'], base_path, base_path_components,
-                 results)
+    _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)
+      _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)
+      _ExtractSourcesFromAction(rule, base_path, base_path_components, results)
 
   return results
 
+
 class Target(object):
   """Holds information about a particular target:
   deps: set of the names of direct dependent targets.
@@ -162,6 +164,7 @@
     self.deps = set()
     self.match_status = MATCH_STATUS_TBD
 
+
 class Config(object):
   """Details what we're looking for
   look_for_dependency_only: if true only search for a target listing any of
@@ -216,7 +219,32 @@
     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 +257,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 +268,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)
@@ -250,6 +294,7 @@
 
   return targets, matched
 
+
 def _GetUnqualifiedToQualifiedMapping(all_targets, to_find):
   """Returns a mapping (dictionary) from unqualified name to qualified name for
   all the targets in |to_find|."""
@@ -266,6 +311,7 @@
         return result
   return result
 
+
 def _DoesTargetDependOn(target, all_targets):
   """Returns true if |target| or any of its dependencies matches the supplied
   set of paths. This updates |matches| of the Targets as it recurses.
@@ -285,6 +331,7 @@
     dep_target.match_status = MATCH_STATUS_DOESNT_MATCH
   return False
 
+
 def _GetTargetsDependingOn(all_targets, possible_targets):
   """Returns the list of targets in |possible_targets| that depend (either
   directly on indirectly) on the matched files.
@@ -297,6 +344,7 @@
       found.append(gyp.common.ParseQualifiedTarget(target)[1])
   return found
 
+
 def _WriteOutput(params, **values):
   """Writes the output, either to stdout or a file is specified."""
   output_path = params.get('generator_flags', {}).get(
@@ -311,6 +359,7 @@
   except IOError as e:
     print 'Error writing to output file', output_path, str(e)
 
+
 def CalculateVariables(default_variables, params):
   """Calculate additional variables for use in the build (called by gyp)."""
   flavor = gyp.common.GetFlavor(params)
@@ -333,6 +382,7 @@
       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."""
   config = Config()
@@ -345,15 +395,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 +424,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/pylib/gyp/generator/ninja.py b/pylib/gyp/generator/ninja.py
index 3d33d0a..4eafb71 100644
--- a/pylib/gyp/generator/ninja.py
+++ b/pylib/gyp/generator/ninja.py
@@ -530,7 +530,7 @@
   def WriteWinIdlFiles(self, spec, prebuild):
     """Writes rules to match MSVS's implicit idl handling."""
     assert self.flavor == 'win'
-    if self.msvs_settings.HasExplicitIdlRules(spec):
+    if self.msvs_settings.HasExplicitIdlRulesOrActions(spec):
       return []
     outputs = []
     for source in filter(lambda x: x.endswith('.idl'), spec['sources']):
diff --git a/pylib/gyp/msvs_emulation.py b/pylib/gyp/msvs_emulation.py
index 1b82adb..5f71e9e 100644
--- a/pylib/gyp/msvs_emulation.py
+++ b/pylib/gyp/msvs_emulation.py
@@ -760,10 +760,16 @@
         return True
     return False
 
-  def HasExplicitIdlRules(self, spec):
-    """Determine if there's an explicit rule for idl files. When there isn't we
-    need to generate implicit rules to build MIDL .idl files."""
-    return self._HasExplicitRuleForExtension(spec, 'idl')
+  def _HasExplicitIdlActions(self, spec):
+    """Determine if an action should not run midl for .idl files."""
+    return any([action.get('explicit_idl_action', 0)
+                for action in spec.get('actions', [])])
+
+  def HasExplicitIdlRulesOrActions(self, spec):
+    """Determine if there's an explicit rule or action for idl files. When
+    there isn't we need to generate implicit rules to build MIDL .idl files."""
+    return (self._HasExplicitRuleForExtension(spec, 'idl') or
+            self._HasExplicitIdlActions(spec))
 
   def HasExplicitAsmRules(self, spec):
     """Determine if there's an explicit rule for asm files. When there isn't we
diff --git a/pylib/gyp/xcode_ninja.py b/pylib/gyp/xcode_ninja.py
index 2a89fa9..a005dfd 100644
--- a/pylib/gyp/xcode_ninja.py
+++ b/pylib/gyp/xcode_ninja.py
@@ -80,7 +80,8 @@
 
   if 'configurations' in old_spec:
     for config in old_spec['configurations'].iterkeys():
-      old_xcode_settings = old_spec['configurations'][config]['xcode_settings']
+      old_xcode_settings = \
+        old_spec['configurations'][config].get('xcode_settings', {})
       if 'IPHONEOS_DEPLOYMENT_TARGET' in old_xcode_settings:
         new_xcode_settings['CODE_SIGNING_REQUIRED'] = "NO"
         new_xcode_settings['IPHONEOS_DEPLOYMENT_TARGET'] = \
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',
+      ],
+    },
+  ],
+}
diff --git a/test/generator-output/gyptest-actions.py b/test/generator-output/gyptest-actions.py
index 2bd09c8..8c912e4 100755
--- a/test/generator-output/gyptest-actions.py
+++ b/test/generator-output/gyptest-actions.py
@@ -18,6 +18,7 @@
 test.writable(test.workpath('actions'), False)
 test.run_gyp('actions.gyp',
              '--generator-output=' + test.workpath('gypfiles'),
+             '-G', 'xcode_ninja_target_pattern=^pull_in_all_actions$',
              chdir='actions')
 
 test.writable(test.workpath('actions'), True)
diff --git a/test/generator-output/gyptest-copies.py b/test/generator-output/gyptest-copies.py
index 7524b17..909aebf 100755
--- a/test/generator-output/gyptest-copies.py
+++ b/test/generator-output/gyptest-copies.py
@@ -18,6 +18,7 @@
 
 test.run_gyp('copies.gyp',
              '--generator-output=' + test.workpath('gypfiles'),
+             '-G', 'xcode_ninja_target_pattern=^(?!copies_null)',
              chdir='copies')
 
 test.writable(test.workpath('copies'), True)
@@ -39,7 +40,7 @@
 
 if test.format == 'xcode':
   chdir = 'relocate/copies/build'
-elif test.format in ['make', 'ninja', 'cmake']:
+elif test.format in ['make', 'ninja', 'xcode-ninja', 'cmake']:
   chdir = 'relocate/gypfiles/out'
 else:
   chdir = 'relocate/gypfiles'
@@ -50,7 +51,7 @@
 
 if test.format == 'xcode':
   chdir = 'relocate/copies/subdir/build'
-elif test.format in ['make', 'ninja', 'cmake']:
+elif test.format in ['make', 'ninja', 'xcode-ninja', 'cmake']:
   chdir = 'relocate/gypfiles/out'
 else:
   chdir = 'relocate/gypfiles'
diff --git a/test/generator-output/gyptest-rules.py b/test/generator-output/gyptest-rules.py
index 324d5c2..b95e005 100755
--- a/test/generator-output/gyptest-rules.py
+++ b/test/generator-output/gyptest-rules.py
@@ -17,6 +17,7 @@
 
 test.run_gyp('rules.gyp',
              '--generator-output=' + test.workpath('gypfiles'),
+             '-G', 'xcode_ninja_target_pattern=^pull_in_all_actions$',
              chdir='rules')
 
 test.writable(test.workpath('rules'), True)
diff --git a/test/win/idl-rules/Window.idl b/test/win/idl-rules/Window.idl
new file mode 100644
index 0000000..d8ea01b
--- /dev/null
+++ b/test/win/idl-rules/Window.idl
@@ -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.
+
+[
+    WillBeGarbageCollected,
+] interface Window {
+    void alert();
+};
diff --git a/test/win/idl-rules/basic-idl.gyp b/test/win/idl-rules/basic-idl.gyp
index 9c08327..b74622a 100644
--- a/test/win/idl-rules/basic-idl.gyp
+++ b/test/win/idl-rules/basic-idl.gyp
@@ -1,42 +1,67 @@
-# Copyright (c) 2012 The Chromium Authors. All rights reserved.

-# Use of this source code is governed by a BSD-style license that can be

-# found in the LICENSE file.

-

-{

-  'variables': {

-    'midl_out_dir': '<(SHARED_INTERMEDIATE_DIR)',

-  },

-  'target_defaults': {

-    'configurations': {

-      'Debug': {

-        'msvs_configuration_platform': 'Win32',

-      },

-      'Debug_x64': {

-        'inherit_from': ['Debug'],

-        'msvs_configuration_platform': 'x64',

-      },

-    },

-  },

-  'targets': [

-    {

-      'target_name': 'idl_test',

-      'type': 'executable',

-      'sources': [

-        'history_indexer.idl',

-        '<(midl_out_dir)/history_indexer.h',

-        '<(midl_out_dir)/history_indexer_i.c',

-        'history_indexer_user.cc',

-      ],

-      'libraries': ['ole32.lib'],

-      'include_dirs': [

-        '<(midl_out_dir)',

-      ],

-      'msvs_settings': {

-        'VCMIDLTool': {

-          'OutputDirectory': '<(midl_out_dir)',

-          'HeaderFileName': '<(RULE_INPUT_ROOT).h',

-         },

-      },

-    },

-  ],

-}

+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+{
+  'variables': {
+    'midl_out_dir': '<(SHARED_INTERMEDIATE_DIR)',
+  },
+  'target_defaults': {
+    'configurations': {
+      'Debug': {
+        'msvs_configuration_platform': 'Win32',
+      },
+      'Debug_x64': {
+        'inherit_from': ['Debug'],
+        'msvs_configuration_platform': 'x64',
+      },
+    },
+  },
+  'targets': [
+    {
+      'target_name': 'idl_test',
+      'type': 'executable',
+      'sources': [
+        'history_indexer.idl',
+        '<(midl_out_dir)/history_indexer.h',
+        '<(midl_out_dir)/history_indexer_i.c',
+        'history_indexer_user.cc',
+      ],
+      'libraries': ['ole32.lib'],
+      'include_dirs': [
+        '<(midl_out_dir)',
+      ],
+      'msvs_settings': {
+        'VCMIDLTool': {
+          'OutputDirectory': '<(midl_out_dir)',
+          'HeaderFileName': '<(RULE_INPUT_ROOT).h',
+         },
+      },
+    },
+    {
+      'target_name': 'idl_explicit_action',
+      'type': 'none',
+      'sources': [
+        'Window.idl',
+      ],
+      'actions': [{
+        'action_name': 'blink_idl',
+        'explicit_idl_action': 1,
+        'msvs_cygwin_shell': 0,
+        'inputs': [
+          'Window.idl',
+          'idl_compiler.py',
+        ],
+        'outputs': [
+          'Window.cpp',
+          'Window.h',
+        ],
+        'action': [
+          'python',
+          'idl_compiler.py',
+          'Window.idl',
+        ],
+      }],
+    },
+  ],
+}
diff --git a/test/win/idl-rules/idl_compiler.py b/test/win/idl-rules/idl_compiler.py
new file mode 100644
index 0000000..a12b274
--- /dev/null
+++ b/test/win/idl-rules/idl_compiler.py
@@ -0,0 +1,17 @@
+#!/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.
+
+# mock, just outputs empty .h/.cpp files
+
+import os
+import sys
+
+if len(sys.argv) == 2:
+  basename, ext = os.path.splitext(sys.argv[1])
+  with open('%s.h' % basename, 'w') as f:
+    f.write('// %s.h\n' % basename)
+  with open('%s.cpp' % basename, 'w') as f:
+    f.write('// %s.cpp\n' % basename)