Fix msvs-ninja OutputDirectory path.

_FixPath is designed to take gyp paths and convert them to msvs project paths.
This translates a <gyp_dir>"<gyp_dir_to_X>" to
<msvs_project_dir>"<msvs_project_dir_to_gyp_dir>/<gyp_dir_to_X>".

The OutputDirectory when using ninja as the external builder with the ninja
generator generated build files needs to be the path
<msvs_project_dir>"<msvs_project_dir_to_ninja_build_config>".
Since this is specified on a per target basis and will be run though _FixPath,
the external builder directory in the target must be specified as
<gyp_dir>"<gyp_dir_to_toplevel_dir>/<toplevel_dir_to_ninja_build>/<config>".

Chromium currently does not see any issue as it does not set generator_output.
When generator_output is not set, _GetPathOfProject sets fix_prefix to None.
Also, Chromium appears to be using an absolute path for
msvs_external_builder_out_dir.

This is, however, affecting Skia, which sets generator_output to 'out' and
places all of its gyp files in a 'gyp' directory. As a result Skia is seeing
the OutputDirectory set to "../../gyp/out/$(Configuration)". This change fixes
this to "../../out/$(Configuration)".

R=scottmg@chromium.org

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

git-svn-id: http://gyp.googlecode.com/svn/trunk@1926 78cadc50-ecff-11dd-a971-7dbc132099af
diff --git a/buildbot/buildbot_run.py b/buildbot/buildbot_run.py
index 979073c..cc3a25a 100755
--- a/buildbot/buildbot_run.py
+++ b/buildbot/buildbot_run.py
@@ -107,7 +107,7 @@
       cwd=ANDROID_DIR)
 
 
-def GypTestFormat(title, format=None, msvs_version=None):
+def GypTestFormat(title, format=None, msvs_version=None, tests=[]):
   """Run the gyp tests for a given format, emitting annotator tags.
 
   See annotator docs at:
@@ -131,7 +131,7 @@
        '--passed',
        '--format', format,
        '--path', CMAKE_BIN_DIR,
-       '--chdir', 'trunk'])
+       '--chdir', 'trunk'] + tests)
   if format == 'android':
     # gyptest needs the environment setup from envsetup/lunch in order to build
     # using the 'android' backend, so this is done in a single shell.
@@ -173,6 +173,12 @@
   elif sys.platform == 'win32':
     retcode += GypTestFormat('ninja')
     if os.environ['BUILDBOT_BUILDERNAME'] == 'gyp-win64':
+      retcode += GypTestFormat('msvs-ninja-2012', format='msvs-ninja',
+                               msvs_version='2012',
+                               tests=[
+                                   'test\generator-output\gyptest-actions.py',
+                                   'test\generator-output\gyptest-relocate.py',
+                                   'test\generator-output\gyptest-rules.py'])
       retcode += GypTestFormat('msvs-2010', format='msvs', msvs_version='2010')
       retcode += GypTestFormat('msvs-2012', format='msvs', msvs_version='2012')
   else:
diff --git a/pylib/gyp/generator/msvs.py b/pylib/gyp/generator/msvs.py
index 843f97f..a4ebd7c 100644
--- a/pylib/gyp/generator/msvs.py
+++ b/pylib/gyp/generator/msvs.py
@@ -12,6 +12,7 @@
 
 import gyp.common
 import gyp.easy_xml as easy_xml
+import gyp.generator.ninja as ninja_generator
 import gyp.MSVSNew as MSVSNew
 import gyp.MSVSProject as MSVSProject
 import gyp.MSVSSettings as MSVSSettings
@@ -1787,7 +1788,7 @@
   return projects
 
 
-def _InitNinjaFlavor(options, target_list, target_dicts):
+def _InitNinjaFlavor(params, target_list, target_dicts):
   """Initialize targets for the ninja flavor.
 
   This sets up the necessary variables in the targets to generate msvs projects
@@ -1795,7 +1796,7 @@
   if they have not been set. This allows individual specs to override the
   default values initialized here.
   Arguments:
-    options: Options provided to the generator.
+    params: Params provided to the generator.
     target_list: List of target pairs: 'base/base.gyp:base'.
     target_dicts: Dict of target properties keyed on target pair.
   """
@@ -1809,8 +1810,12 @@
 
     spec['msvs_external_builder'] = 'ninja'
     if not spec.get('msvs_external_builder_out_dir'):
-      spec['msvs_external_builder_out_dir'] = \
-        options.depth + '/out/$(Configuration)'
+      gyp_file, _, _ = gyp.common.ParseQualifiedTarget(qualified_target)
+      gyp_dir = os.path.dirname(gyp_file)
+      spec['msvs_external_builder_out_dir'] = os.path.join(
+          gyp.common.RelativePath(params['options'].toplevel_dir, gyp_dir),
+          ninja_generator.ComputeOutputDir(params),
+          '$(Configuration)')
     if not spec.get('msvs_external_builder_build_cmd'):
       spec['msvs_external_builder_build_cmd'] = [
         path_to_ninja,
@@ -1905,7 +1910,7 @@
 
   # Optionally configure each spec to use ninja as the external builder.
   if params.get('flavor') == 'ninja':
-    _InitNinjaFlavor(options, target_list, target_dicts)
+    _InitNinjaFlavor(params, target_list, target_dicts)
 
   # Prepare the set of configurations.
   configs = set()
diff --git a/test/ios/gyptest-xcode-ninja.py b/test/ios/gyptest-xcode-ninja.py
index d2ff333..609db8c 100644
--- a/test/ios/gyptest-xcode-ninja.py
+++ b/test/ios/gyptest-xcode-ninja.py
@@ -16,12 +16,8 @@
 if sys.platform == 'darwin':
   test = TestGyp.TestGyp(formats=['xcode'])
 
-  # Run ninja first
-  test.format = 'ninja'
-  test.run_gyp('test.gyp', chdir='app-bundle')
-
-  # Then run xcode-ninja
-  test.format = 'xcode-ninja'
+  # Run ninja and xcode-ninja
+  test.formats = ['ninja', 'xcode-ninja']
   test.run_gyp('test.gyp', chdir='app-bundle')
 
   # If it builds the target, it works.
diff --git a/test/lib/TestGyp.py b/test/lib/TestGyp.py
index 36b6281..cde04ca 100644
--- a/test/lib/TestGyp.py
+++ b/test/lib/TestGyp.py
@@ -78,6 +78,7 @@
   configuration and to run executables generated by those builds.
   """
 
+  formats = []
   build_tool = None
   build_tool_list = []
 
@@ -113,6 +114,8 @@
     self.gyp = os.path.abspath(gyp)
     self.no_parallel = False
 
+    self.formats = [self.format]
+
     self.initialize_build_tool()
 
     kw.setdefault('match', TestCommon.match_exact)
@@ -130,10 +133,11 @@
 
     super(TestGypBase, self).__init__(*args, **kw)
 
+    real_format = self.format.split('-')[-1]
     excluded_formats = set([f for f in formats if f[0] == '!'])
     included_formats = set(formats) - excluded_formats
-    if ('!'+self.format in excluded_formats or
-        included_formats and self.format not in included_formats):
+    if ('!'+real_format in excluded_formats or
+        included_formats and real_format not in included_formats):
       msg = 'Invalid test for %r format; skipping test.\n'
       self.skip_test(msg % self.format)
 
@@ -272,9 +276,13 @@
 
     # TODO:  --depth=. works around Chromium-specific tree climbing.
     depth = kw.pop('depth', '.')
-    run_args = ['--depth='+depth, '--format='+self.format, gyp_file]
+    run_args = ['--depth='+depth]
+    run_args.extend(['--format='+f for f in self.formats]);
+    run_args.append(gyp_file)
     if self.no_parallel:
       run_args += ['--no-parallel']
+    # TODO: if extra_args contains a '--build' flag
+    # we really want that to only apply to the last format (self.format).
     run_args.extend(self.extra_args)
     run_args.extend(args)
     return self.run(program=self.gyp, arguments=run_args, **kw)
@@ -747,6 +755,53 @@
   return path
 
 
+def FindMSBuildInstallation(msvs_version = 'auto'):
+  """Returns path to MSBuild for msvs_version or latest available.
+
+  Looks in the registry to find install location of MSBuild.
+  MSBuild before v4.0 will not build c++ projects, so only use newer versions.
+  """
+  import TestWin
+  registry = TestWin.Registry()
+
+  msvs_to_msbuild = {
+      '2013': r'12.0',
+      '2012': r'4.0',  # Really v4.0.30319 which comes with .NET 4.5.
+      '2010': r'4.0'}
+
+  msbuild_basekey = r'HKLM\SOFTWARE\Microsoft\MSBuild\ToolsVersions'
+  if not registry.KeyExists(msbuild_basekey):
+    print 'Error: could not find MSBuild base registry entry'
+    return None
+
+  msbuild_version = None
+  if msvs_version in msvs_to_msbuild:
+    msbuild_test_version = msvs_to_msbuild[msvs_version]
+    if registry.KeyExists(msbuild_basekey + '\\' + msbuild_test_version):
+      msbuild_version = msbuild_test_version
+    else:
+      print ('Warning: Environment variable GYP_MSVS_VERSION specifies "%s" '
+             'but corresponding MSBuild "%s" was not found.' %
+             (msvs_version, msbuild_version))
+  if not msbuild_version:
+    for msvs_version in sorted(msvs_to_msbuild, reverse=True):
+      msbuild_test_version = msvs_to_msbuild[msvs_version]
+      if registry.KeyExists(msbuild_basekey + '\\' + msbuild_test_version):
+        msbuild_version = msbuild_test_version
+        break
+  if not msbuild_version:
+    print 'Error: could not find MSBuild registry entry'
+    return None
+
+  msbuild_path = registry.GetValue(msbuild_basekey + '\\' + msbuild_version,
+                                   'MSBuildToolsPath')
+  if not msbuild_path:
+    print 'Error: could not get MSBuild registry entry value'
+    return None
+
+  return os.path.join(msbuild_path, 'MSBuild.exe')
+
+
 def FindVisualStudioInstallation():
   """Returns appropriate values for .build_tool and .uses_msbuild fields
   of TestGypBase for Visual Studio.
@@ -772,39 +827,28 @@
     msvs_version = flag.split('=')[-1]
   msvs_version = os.environ.get('GYP_MSVS_VERSION', msvs_version)
 
-  build_tool = None
   if msvs_version in possible_paths:
     # Check that the path to the specified GYP_MSVS_VERSION exists.
     path = possible_paths[msvs_version]
     for r in possible_roots:
-      bt = os.path.join(r, path)
-      if os.path.exists(bt):
-        build_tool = bt
+      build_tool = os.path.join(r, path)
+      if os.path.exists(build_tool):
         uses_msbuild = msvs_version >= '2010'
-        return build_tool, uses_msbuild
+        msbuild_path = FindMSBuildInstallation(msvs_version)
+        return build_tool, uses_msbuild, msbuild_path
     else:
       print ('Warning: Environment variable GYP_MSVS_VERSION specifies "%s" '
               'but corresponding "%s" was not found.' % (msvs_version, path))
-  if build_tool:
-    # We found 'devenv' on the path, use that and try to guess the version.
-    for version, path in possible_paths.iteritems():
-      if build_tool.find(path) >= 0:
-        uses_msbuild = version >= '2010'
-        return build_tool, uses_msbuild
-    else:
-      # If not, assume not MSBuild.
-      uses_msbuild = False
-    return build_tool, uses_msbuild
   # Neither GYP_MSVS_VERSION nor the path help us out.  Iterate through
   # the choices looking for a match.
   for version in sorted(possible_paths, reverse=True):
     path = possible_paths[version]
     for r in possible_roots:
-      bt = os.path.join(r, path)
-      if os.path.exists(bt):
-        build_tool = bt
+      build_tool = os.path.join(r, path)
+      if os.path.exists(build_tool):
         uses_msbuild = msvs_version >= '2010'
-        return build_tool, uses_msbuild
+        msbuild_path = FindMSBuildInstallation(msvs_version)
+        return build_tool, uses_msbuild, msbuild_path
   print 'Error: could not find devenv'
   sys.exit(1)
 
@@ -822,7 +866,8 @@
   def initialize_build_tool(self):
     super(TestGypOnMSToolchain, self).initialize_build_tool()
     if sys.platform in ('win32', 'cygwin'):
-      self.devenv_path, self.uses_msbuild = FindVisualStudioInstallation()
+      build_tools = FindVisualStudioInstallation()
+      self.devenv_path, self.uses_msbuild, self.msbuild_path = build_tools
       self.vsvars_path = TestGypOnMSToolchain._ComputeVsvarsPath(
           self.devenv_path)
 
@@ -1002,6 +1047,56 @@
     return self.workpath(*result)
 
 
+class TestGypMSVSNinja(TestGypNinja):
+  """
+  Subclass for testing the GYP Visual Studio Ninja generator.
+  """
+  format = 'msvs-ninja'
+
+  def initialize_build_tool(self):
+    super(TestGypMSVSNinja, self).initialize_build_tool()
+    # When using '--build', make sure ninja is first in the format list.
+    self.formats.insert(0, 'ninja')
+
+  def build(self, gyp_file, target=None, rebuild=False, clean=False, **kw):
+    """
+    Runs a Visual Studio build using the configuration generated
+    from the specified gyp_file.
+    """
+    arguments = kw.get('arguments', [])[:]
+    if target in (None, self.ALL, self.DEFAULT):
+      # Note: the Visual Studio generator doesn't add an explicit 'all' target.
+      # This will build each project. This will work if projects are hermetic,
+      # but may fail if they are not (a project may run more than once).
+      # It would be nice to supply an all.metaproj for MSBuild.
+      arguments.extend([gyp_file.replace('.gyp', '.sln')])
+    else:
+      # MSBuild documentation claims that one can specify a sln but then build a
+      # project target like 'msbuild a.sln /t:proj:target' but this format only
+      # supports 'Clean', 'Rebuild', and 'Publish' (with none meaning Default).
+      # This limitation is due to the .sln -> .sln.metaproj conversion.
+      # The ':' is not special, 'proj:target' is a target in the metaproj.
+      arguments.extend([target+'.vcxproj'])
+
+    if clean:
+      build = 'Clean'
+    elif rebuild:
+      build = 'Rebuild'
+    else:
+      build = 'Build'
+    arguments.extend(['/target:'+build])
+    configuration = self.configuration_buildname()
+    config = configuration.split('|')
+    arguments.extend(['/property:Configuration='+config[0]])
+    if len(config) > 1:
+      arguments.extend(['/property:Platform='+config[1]])
+    arguments.extend(['/property:BuildInParallel=false'])
+    arguments.extend(['/verbosity:minimal'])
+
+    kw['arguments'] = arguments
+    return self.run(program=self.msbuild_path, **kw)
+
+
 class TestGypXcode(TestGypBase):
   """
   Subclass for testing the GYP Xcode generator.
@@ -1118,6 +1213,7 @@
   TestGypCMake,
   TestGypMake,
   TestGypMSVS,
+  TestGypMSVSNinja,
   TestGypNinja,
   TestGypXcode,
 ]
diff --git a/test/lib/TestWin.py b/test/lib/TestWin.py
new file mode 100644
index 0000000..1571c85
--- /dev/null
+++ b/test/lib/TestWin.py
@@ -0,0 +1,101 @@
+# 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.
+
+"""
+TestWin.py:  a collection of helpers for testing on Windows.
+"""
+
+import errno
+import os
+import re
+import sys
+import subprocess
+
+class Registry(object):
+  def _QueryBase(self, sysdir, key, value):
+    """Use reg.exe to read a particular key.
+
+    While ideally we might use the win32 module, we would like gyp to be
+    python neutral, so for instance cygwin python lacks this module.
+
+    Arguments:
+      sysdir: The system subdirectory to attempt to launch reg.exe from.
+      key: The registry key to read from.
+      value: The particular value to read.
+    Return:
+      stdout from reg.exe, or None for failure.
+    """
+    # Skip if not on Windows or Python Win32 setup issue
+    if sys.platform not in ('win32', 'cygwin'):
+      return None
+    # Setup params to pass to and attempt to launch reg.exe
+    cmd = [os.path.join(os.environ.get('WINDIR', ''), sysdir, 'reg.exe'),
+           'query', key]
+    if value:
+      cmd.extend(['/v', value])
+    p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+    # Get the stdout from reg.exe, reading to the end so p.returncode is valid
+    # Note that the error text may be in [1] in some cases
+    text = p.communicate()[0]
+    # Check return code from reg.exe; officially 0==success and 1==error
+    if p.returncode:
+      return None
+    return text
+
+  def Query(self, key, value=None):
+    """Use reg.exe to read a particular key through _QueryBase.
+
+    First tries to launch from %WinDir%\Sysnative to avoid WoW64 redirection. If
+    that fails, it falls back to System32.  Sysnative is available on Vista and
+    up and available on Windows Server 2003 and XP through KB patch 942589. Note
+    that Sysnative will always fail if using 64-bit python due to it being a
+    virtual directory and System32 will work correctly in the first place.
+
+    KB 942589 - http://support.microsoft.com/kb/942589/en-us.
+
+    Arguments:
+      key: The registry key.
+      value: The particular registry value to read (optional).
+    Return:
+      stdout from reg.exe, or None for failure.
+    """
+    text = None
+    try:
+      text = self._QueryBase('Sysnative', key, value)
+    except OSError, e:
+      if e.errno == errno.ENOENT:
+        text = self._QueryBase('System32', key, value)
+      else:
+        raise
+    return text
+
+  def GetValue(self, key, value):
+    """Use reg.exe to obtain the value of a registry key.
+
+    Args:
+      key: The registry key.
+      value: The particular registry value to read.
+    Return:
+      contents of the registry key's value, or None on failure.
+    """
+    text = self.Query(key, value)
+    if not text:
+      return None
+    # Extract value.
+    match = re.search(r'REG_\w+\s+([^\r]+)\r\n', text)
+    if not match:
+      return None
+    return match.group(1)
+
+  def KeyExists(self, key):
+    """Use reg.exe to see if a key exists.
+
+    Args:
+      key: The registry key to check.
+    Return:
+      True if the key exists
+    """
+    if not self.Query(key):
+      return False
+    return True