ninja/mac: Insert a few synthesized Info.plist entries.

Namely, insert BuildMachineOSBuild, DTSDKName, DTSDKBuild, DTXcode, DTXcodeBuild

The values for the synthesized keys are collected at gyp time. The logic for
this is in XcodeEmulation, which makes it add support for this to the make
generator too if someone feels motivated.

The `mac_tool copy-info-plist` command grows support for an arbitrary number
of [key value] arguments, and these values blindly overwrite potential
existing entries from the plist file (this matches Xcode).

BUG=280718
R=scottmg@chromium.org

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

git-svn-id: http://gyp.googlecode.com/svn/trunk@1734 78cadc50-ecff-11dd-a971-7dbc132099af
diff --git a/pylib/gyp/generator/ninja.py b/pylib/gyp/generator/ninja.py
index 407f054..f49701c 100644
--- a/pylib/gyp/generator/ninja.py
+++ b/pylib/gyp/generator/ninja.py
@@ -762,15 +762,17 @@
       intermediate_plist = self.GypPathToUniqueOutput(
           os.path.basename(info_plist))
       defines = ' '.join([Define(d, self.flavor) for d in defines])
-      info_plist = self.ninja.build(intermediate_plist, 'infoplist', info_plist,
-                                    variables=[('defines',defines)])
+      info_plist = self.ninja.build(
+          intermediate_plist, 'preprocess_infoplist', info_plist,
+          variables=[('defines',defines)])
 
     env = self.GetSortedXcodeEnv(additional_settings=extra_env)
     env = self.ComputeExportEnvString(env)
 
-    self.ninja.build(out, 'mac_tool', info_plist,
-                     variables=[('mactool_cmd', 'copy-info-plist'),
-                                ('env', env)])
+    keys = self.xcode_settings.GetExtraPlistItems()
+    keys = [QuoteShellArgument(v, self.flavor) for v in sum(keys.items(), ())]
+    self.ninja.build(out, 'copy_infoplist', info_plist,
+                     variables=[('env', env), ('keys', keys)])
     bundle_depends.append(out)
 
   def WriteSources(self, ninja_file, config_name, config, sources, predepends,
@@ -1972,11 +1974,15 @@
                '$in $solibs $libs$postbuilds'),
       pool='link_pool')
     master_ninja.rule(
-      'infoplist',
-      description='INFOPLIST $out',
+      'preprocess_infoplist',
+      description='PREPROCESS INFOPLIST $out',
       command=('$cc -E -P -Wno-trigraphs -x c $defines $in -o $out && '
                'plutil -convert xml1 $out $out'))
     master_ninja.rule(
+      'copy_infoplist',
+      description='COPY INFOPLIST $in',
+      command='$env ./gyp-mac-tool copy-info-plist $in $out $keys')
+    master_ninja.rule(
       'mac_tool',
       description='MACTOOL $mactool_cmd $in',
       command='$env ./gyp-mac-tool $mactool_cmd $in $out')
diff --git a/pylib/gyp/mac_tool.py b/pylib/gyp/mac_tool.py
index a968322..3a28a17 100755
--- a/pylib/gyp/mac_tool.py
+++ b/pylib/gyp/mac_tool.py
@@ -116,13 +116,18 @@
     else:
       return None
 
-  def ExecCopyInfoPlist(self, source, dest):
+  def ExecCopyInfoPlist(self, source, dest, *keys):
     """Copies the |source| Info.plist to the destination directory |dest|."""
     # Read the source Info.plist into memory.
     fd = open(source, 'r')
     lines = fd.read()
     fd.close()
 
+    # Insert synthesized key/value pairs (e.g. BuildMachineOSBuild).
+    plist = plistlib.readPlistFromString(lines)
+    plist = dict(plist.items() + zip(keys[::2], keys[1::2]))
+    lines = plistlib.writePlistToString(plist)
+
     # Go through all the environment variables and replace them as variables in
     # the file.
     IDENT_RE = re.compile('[/\s]')
diff --git a/pylib/gyp/xcode_emulation.py b/pylib/gyp/xcode_emulation.py
index 92cba63..b4af0fd 100644
--- a/pylib/gyp/xcode_emulation.py
+++ b/pylib/gyp/xcode_emulation.py
@@ -22,6 +22,10 @@
   # at class-level for efficiency.
   _sdk_path_cache = {}
 
+  # Populated lazily by GetExtraPlistItems(). Shared by all XcodeSettings, so
+  # cached at class-level for efficiency.
+  _plist_cache = {}
+
   def __init__(self, spec):
     self.spec = spec
 
@@ -246,17 +250,22 @@
     # CURRENT_ARCH / NATIVE_ARCH env vars?
     return self.xcode_settings[configname].get('ARCHS', ['i386'])
 
-  def _GetSdkVersionInfoItem(self, sdk, infoitem):
-    job = subprocess.Popen(['xcodebuild', '-version', '-sdk', sdk, infoitem],
-                           stdout=subprocess.PIPE)
+  def _GetStdout(self, cmdlist):
+    job = subprocess.Popen(cmdlist, stdout=subprocess.PIPE)
     out = job.communicate()[0]
     if job.returncode != 0:
       sys.stderr.write(out + '\n')
-      raise GypError('Error %d running xcodebuild' % job.returncode)
+      raise GypError('Error %d running %s' % (job.returncode, cmdlist[0]))
     return out.rstrip('\n')
 
+  def _GetSdkVersionInfoItem(self, sdk, infoitem):
+    return self._GetStdout(['xcodebuild', '-version', '-sdk', sdk, infoitem])
+
+  def _SdkRoot(self):
+    return self.GetPerTargetSetting('SDKROOT', default='')
+
   def _SdkPath(self):
-    sdk_root = self.GetPerTargetSetting('SDKROOT', default='macosx')
+    sdk_root = self._SdkRoot()
     if sdk_root.startswith('/'):
       return sdk_root
     if sdk_root not in XcodeSettings._sdk_path_cache:
@@ -659,12 +668,12 @@
   def GetPerTargetSetting(self, setting, default=None):
     """Tries to get xcode_settings.setting from spec. Assumes that the setting
        has the same value in all configurations and throws otherwise."""
-    first_pass = True
+    is_first_pass = True
     result = None
     for configname in sorted(self.xcode_settings.keys()):
-      if first_pass:
+      if is_first_pass:
         result = self.xcode_settings[configname].get(setting, None)
-        first_pass = False
+        is_first_pass = False
       else:
         assert result == self.xcode_settings[configname].get(setting, None), (
             "Expected per-target setting for '%s', got per-config setting "
@@ -752,6 +761,41 @@
     libraries = [ self._AdjustLibrary(library) for library in libraries]
     return libraries
 
+  def _BuildMachineOSBuild(self):
+    return self._GetStdout(['sw_vers', '-buildVersion'])
+
+  def _XcodeVersion(self):
+    # `xcodebuild -version` output looks like
+    #    Xcode 4.6.3
+    #    Build version 4H1503
+    # Convert that to '0463', '4H1503'.
+    version, build = self._GetStdout(['xcodebuild', '-version']).splitlines()
+    # Be careful to convert "4.2" to "0420":
+    version = version.split()[-1].replace('.', '')
+    version = (version + '0' * (3 - len(version))).zfill(4)
+    build = build.split()[-1]
+    return version, build
+
+  def GetExtraPlistItems(self):
+    """Returns a dictionary with extra items to insert into Info.plist."""
+    if not XcodeSettings._plist_cache:
+      cache = XcodeSettings._plist_cache
+      cache['BuildMachineOSBuild'] = self._BuildMachineOSBuild()
+
+      xcode, xcode_build = self._XcodeVersion()
+      cache['DTXcode'] = xcode
+      cache['DTXcodeBuild'] = xcode_build
+
+      sdk_root = self._SdkRoot()
+      cache['DTSDKName'] = sdk_root
+      if xcode >= '0430':
+        cache['DTSDKBuild'] = self._GetSdkVersionInfoItem(
+            sdk_root, 'ProductBuildVersion')
+      else:
+        cache['DTSDKBuild'] = cache['BuildMachineOSBuild']
+
+    return XcodeSettings._plist_cache
+
 
 class MacPrefixHeader(object):
   """A class that helps with emulating Xcode's GCC_PREFIX_HEADER feature.
diff --git a/test/mac/app-bundle/TestApp/TestApp-Info.plist b/test/mac/app-bundle/TestApp/TestApp-Info.plist
index 8f86b1a..e005852 100644
--- a/test/mac/app-bundle/TestApp/TestApp-Info.plist
+++ b/test/mac/app-bundle/TestApp/TestApp-Info.plist
@@ -2,6 +2,8 @@
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <dict>
+	<key>BuildMachineOSBuild</key>
+	<string>Doesn't matter, will be overwritten</string>
 	<key>CFBundleDevelopmentRegion</key>
 	<string>English</string>
 	<key>CFBundleExecutable</key>
diff --git a/test/mac/gyptest-app.py b/test/mac/gyptest-app.py
index fc319b8..c84c92f 100755
--- a/test/mac/gyptest-app.py
+++ b/test/mac/gyptest-app.py
@@ -11,8 +11,18 @@
 import TestGyp
 
 import os
+import plistlib
+import subprocess
 import sys
 
+def GetStdout(cmdlist):
+  return subprocess.Popen(cmdlist,
+                          stdout=subprocess.PIPE).communicate()[0].rstrip('\n')
+
+def ExpectEq(expected, actual):
+  if expected != actual:
+    print >>sys.stderr, 'Expected "%s", got "%s"' % (expected, actual)
+    test.fail_test()
 
 def ls(path):
   '''Returns a list of all files in a directory, relative to the directory.'''
@@ -41,6 +51,25 @@
   test.must_contain(info_plist, 'com.google.Test-App-Gyp')  # Variable expansion
   test.must_not_contain(info_plist, '${MACOSX_DEPLOYMENT_TARGET}');
 
+  if test.format != 'make':
+    # TODO: Synthesized plist entries aren't hooked up in the make generator.
+    plist = plistlib.readPlist(info_plist)
+    ExpectEq(GetStdout(['sw_vers', '-buildVersion']),
+             plist['BuildMachineOSBuild'])
+    ExpectEq('', plist['DTSDKName'])
+    sdkbuild = GetStdout(
+        ['xcodebuild', '-version', '-sdk', '', 'ProductBuildVersion'])
+    if not sdkbuild:
+      # Above command doesn't work in Xcode 4.2.
+      sdkbuild = plist['BuildMachineOSBuild']
+    ExpectEq(sdkbuild, plist['DTSDKBuild'])
+    xcode, build = GetStdout(['xcodebuild', '-version']).splitlines()
+    xcode = xcode.split()[-1].replace('.', '')
+    xcode = (xcode + '0' * (3 - len(xcode))).zfill(4)
+    build = build.split()[-1]
+    ExpectEq(xcode, plist['DTXcode'])
+    ExpectEq(build, plist['DTXcodeBuild'])
+
   # Resources
   strings_files = ['InfoPlist.strings', 'utf-16be.strings', 'utf-16le.strings']
   for f in strings_files:
diff --git a/test/mac/gyptest-sdkroot.py b/test/mac/gyptest-sdkroot.py
index da20654..20edd36 100644
--- a/test/mac/gyptest-sdkroot.py
+++ b/test/mac/gyptest-sdkroot.py
@@ -18,7 +18,7 @@
   test = TestGyp.TestGyp(formats=['ninja', 'make', 'xcode'])
 
   # Make sure this works on the bots, which only have the 10.6 sdk, and on
-  # dev machines, who usually don't have the 10.6 sdk.
+  # dev machines, which usually don't have the 10.6 sdk.
   sdk = '10.6'
   DEVNULL = open(os.devnull, 'wb')
   proc = subprocess.Popen(['xcodebuild', '-version', '-sdk', 'macosx' + sdk],