ninja/mac: Support iOS codesign for ninja builds.

Also:
Warn for unimplemented code sign keys
(Resource Rules, Entitlements, and Other)
Add partial support for conditional keys for loading
CODE_SIGN_IDENTITY[sdk=iphoneos*]
Fix UIDeviceFamily extra plist bug, should be per target, not global.
Add sig_test target which will only run if valid certs are found.

R=thakis@chromium.org

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



git-svn-id: http://gyp.googlecode.com/svn/trunk@1756 78cadc50-ecff-11dd-a971-7dbc132099af
diff --git a/pylib/gyp/generator/make.py b/pylib/gyp/generator/make.py
index 952b0ce..6dedc2b 100644
--- a/pylib/gyp/generator/make.py
+++ b/pylib/gyp/generator/make.py
@@ -1414,7 +1414,7 @@
 
           # TARGET_POSTBUILDS_$(BUILDTYPE) is added to postbuilds later on.
           gyp_to_build = gyp.common.InvertRelativePath(self.path)
-          target_postbuild = self.xcode_settings.GetTargetPostbuilds(
+          target_postbuild = self.xcode_settings.AddImplicitPostbuilds(
               configname,
               QuoteSpaces(os.path.normpath(os.path.join(gyp_to_build,
                                                         self.output))),
diff --git a/pylib/gyp/generator/ninja.py b/pylib/gyp/generator/ninja.py
index 229cba4..8917b38 100644
--- a/pylib/gyp/generator/ninja.py
+++ b/pylib/gyp/generator/ninja.py
@@ -1203,17 +1203,16 @@
     if not self.xcode_settings or spec['type'] == 'none' or not output:
       return ''
     output = QuoteShellArgument(output, self.flavor)
-    target_postbuilds = []
+    postbuilds = gyp.xcode_emulation.GetSpecPostbuildCommands(spec, quiet=True)
     if output_binary is not None:
-      target_postbuilds = self.xcode_settings.GetTargetPostbuilds(
+      postbuilds = self.xcode_settings.AddImplicitPostbuilds(
           self.config_name,
           os.path.normpath(os.path.join(self.base_to_build, output)),
           QuoteShellArgument(
               os.path.normpath(os.path.join(self.base_to_build, output_binary)),
               self.flavor),
-          quiet=True)
-    postbuilds = gyp.xcode_emulation.GetSpecPostbuildCommands(spec, quiet=True)
-    postbuilds = target_postbuilds + postbuilds
+          postbuilds, quiet=True)
+
     if not postbuilds:
       return ''
     # Postbuilds expect to be run in the gyp file's directory, so insert an
diff --git a/pylib/gyp/xcode_emulation.py b/pylib/gyp/xcode_emulation.py
index 6871104..f172d6d 100644
--- a/pylib/gyp/xcode_emulation.py
+++ b/pylib/gyp/xcode_emulation.py
@@ -27,6 +27,10 @@
   # cached at class-level for efficiency.
   _plist_cache = {}
 
+  # Populated lazily by GetIOSPostbuilds.  Shared by all XcodeSettings, so
+  # cached at class-level for efficiency.
+  _codesigning_key_cache = {}
+
   def __init__(self, spec):
     self.spec = spec
 
@@ -40,25 +44,34 @@
     configs = spec['configurations']
     for configname, config in configs.iteritems():
       self.xcode_settings[configname] = config.get('xcode_settings', {})
+      self._ConvertConditionalKeys(configname)
       if self.xcode_settings[configname].get('IPHONEOS_DEPLOYMENT_TARGET',
                                              None):
         self.isIOS = True
 
-      # If you need this, speak up at http://crbug.com/122592
-      conditional_keys = [key for key in self.xcode_settings[configname]
-                          if key.endswith(']')]
-      if conditional_keys:
-        print 'Warning: Conditional keys not implemented, ignoring:', \
-              ' '.join(conditional_keys)
-        for key in conditional_keys:
-          del self.xcode_settings[configname][key]
-
     # This is only non-None temporarily during the execution of some methods.
     self.configname = None
 
     # Used by _AdjustLibrary to match .a and .dylib entries in libraries.
     self.library_re = re.compile(r'^lib([^/]+)\.(a|dylib)$')
 
+  def _ConvertConditionalKeys(self, configname):
+    """Converts or warns on conditional keys.  Xcode supports conditional keys,
+    such as CODE_SIGN_IDENTITY[sdk=iphoneos*].  This is a partial implementation
+    with some keys converted while the rest force a warning."""
+    settings = self.xcode_settings[configname]
+    conditional_keys = [key for key in settings if key.endswith(']')]
+    for key in conditional_keys:
+      # If you need more, speak up at http://crbug.com/122592
+      if key.endswith("[sdk=iphoneos*]"):
+        if configname.endswith("iphoneos"):
+          new_key = key.split("[")[0]
+          settings[new_key] = settings[key]
+      else:
+        print 'Warning: Conditional keys not implemented, ignoring:', \
+              ' '.join(conditional_keys)
+      del settings[key]
+
   def _Settings(self):
     assert self.configname
     return self.xcode_settings[self.configname]
@@ -744,7 +757,8 @@
     self.configname = None
     return result
 
-  def GetTargetPostbuilds(self, configname, output, output_binary, quiet=False):
+  def _GetTargetPostbuilds(self, configname, output, output_binary,
+                           quiet=False):
     """Returns a list of shell commands that contain the shell commands
     to run as postbuilds for this target, before the actual postbuilds."""
     # dSYMs need to build before stripping happens.
@@ -752,6 +766,50 @@
         self._GetDebugInfoPostbuilds(configname, output, output_binary, quiet) +
         self._GetStripPostbuilds(configname, output_binary, quiet))
 
+  def _GetIOSPostbuilds(self, configname, output_binary):
+    """Return a shell command to codesign the iOS output binary so it can
+    be deployed to a device.  This should be run as the very last step of the
+    build."""
+    if not (self.isIOS and self.spec['type'] == "executable"):
+      return []
+
+    identity = self.xcode_settings[configname].get('CODE_SIGN_IDENTITY', '')
+    if identity == '':
+      return []
+    if identity not in XcodeSettings._codesigning_key_cache:
+      proc = subprocess.Popen(['security', 'find-identity', '-p', 'codesigning',
+                               '-v'], stdout=subprocess.PIPE)
+      output = proc.communicate()[0].strip()
+      key = None
+      for item in output.split("\n"):
+        if identity in item:
+          assert key == None, (
+              "Multiple codesigning identities for identity: %s" %
+              identity)
+          key = item.split(' ')[1]
+      XcodeSettings._codesigning_key_cache[identity] = key
+    key = XcodeSettings._codesigning_key_cache[identity]
+    if key:
+      # Warn for any unimplemented signing xcode keys.
+      unimpl = ['CODE_SIGN_RESOURCE_RULES_PATH', 'OTHER_CODE_SIGN_FLAGS',
+                'CODE_SIGN_ENTITLEMENTS']
+      keys = set(self.xcode_settings[configname].keys())
+      unimpl = set(unimpl) & keys
+      if unimpl:
+        print 'Warning: Some codesign keys not implemented, ignoring:', \
+            ' '.join(unimpl)
+      return ['codesign --force --sign %s %s' % (key, output_binary)]
+    return []
+
+  def AddImplicitPostbuilds(self, configname, output, output_binary,
+                            postbuilds=[], quiet=False):
+    """Returns a list of shell commands that should run before and after
+    |postbuilds|."""
+    assert output_binary is not None
+    pre = self._GetTargetPostbuilds(configname, output, output_binary, quiet)
+    post = self._GetIOSPostbuilds(configname, output_binary)
+    return pre + postbuilds + post
+
   def _AdjustLibrary(self, library, config_name=None):
     if library.endswith('.framework'):
       l = '-framework ' + os.path.splitext(os.path.basename(library))[0]
@@ -815,13 +873,18 @@
         cache['DTSDKBuild'] = cache['BuildMachineOSBuild']
 
       if self.isIOS:
-        cache['UIDeviceFamily'] = self._XcodeIOSDeviceFamily(configname)
         if configname.endswith("iphoneos"):
           cache['CFBundleSupportedPlatforms'] = ['iPhoneOS']
         else:
           cache['CFBundleSupportedPlatforms'] = ['iPhoneSimulator']
       XcodeSettings._plist_cache[configname] = cache
-    return XcodeSettings._plist_cache[configname]
+
+    # Include extra plist items that are per-target, not per global
+    # XcodeSettings.
+    items = dict(XcodeSettings._plist_cache[configname])
+    if self.isIOS:
+      items['UIDeviceFamily'] = self._XcodeIOSDeviceFamily(configname)
+    return items
 
 
 class MacPrefixHeader(object):
diff --git a/test/ios/app-bundle/TestApp/check_no_signature.py b/test/ios/app-bundle/TestApp/check_no_signature.py
new file mode 100644
index 0000000..4f6e340
--- /dev/null
+++ b/test/ios/app-bundle/TestApp/check_no_signature.py
@@ -0,0 +1,13 @@
+#!/usr/bin/python
+
+import os
+import subprocess
+import sys
+
+p = os.path.join(os.environ['BUILT_PRODUCTS_DIR'],os.environ['EXECUTABLE_PATH'])
+proc = subprocess.Popen(['codesign', '-v', p],
+                        stderr=subprocess.STDOUT, stdout=subprocess.PIPE)
+o = proc.communicate()[0].strip()
+if "code object is not signed at all" not in o:
+  sys.stderr.write('File should not already be signed.')
+  sys.exit(1)
diff --git a/test/ios/app-bundle/test-device.gyp b/test/ios/app-bundle/test-device.gyp
index 6f08cbe..28cdbb3 100644
--- a/test/ios/app-bundle/test-device.gyp
+++ b/test/ios/app-bundle/test-device.gyp
@@ -36,5 +36,44 @@
         'CONFIGURATION_BUILD_DIR':'build/Default',
       },
     },
+    {
+      'target_name': 'sig_test',
+      'product_name': 'sig_test',
+      'type': 'executable',
+      'product_extension': 'bundle',
+      'mac_bundle': 1,
+      'sources': [
+        'TestApp/main.m',
+      ],
+      'mac_bundle_resources': [
+        'TestApp/English.lproj/InfoPlist.strings',
+        'TestApp/English.lproj/MainMenu.xib',
+      ],
+      'link_settings': {
+        'libraries': [
+          '$(SDKROOT)/System/Library/Frameworks/Foundation.framework',
+          '$(SDKROOT)/System/Library/Frameworks/UIKit.framework',
+        ],
+      },
+      'postbuilds': [
+        {
+          'postbuild_name': 'Verify no signature',
+          'action': [
+            'python',
+            'TestApp/check_no_signature.py'
+          ],
+        },
+      ],
+      'xcode_settings': {
+        'OTHER_CFLAGS': [
+          '-fobjc-abi-version=2',
+        ],
+        'SDKROOT': 'iphonesimulator',  # -isysroot
+        'CODE_SIGN_IDENTITY[sdk=iphoneos*]': 'iPhone Developer',
+        'INFOPLIST_FILE': 'TestApp/TestApp-Info.plist',
+        'IPHONEOS_DEPLOYMENT_TARGET': '4.2',
+        'CONFIGURATION_BUILD_DIR':'buildsig/Default',
+      },
+    },
   ],
 }
diff --git a/test/ios/gyptest-per-config-settings.py b/test/ios/gyptest-per-config-settings.py
index 51c8d26..c7c704a 100644
--- a/test/ios/gyptest-per-config-settings.py
+++ b/test/ios/gyptest-per-config-settings.py
@@ -22,6 +22,20 @@
     print 'File: Expected %s, got %s' % (expected, o)
     test.fail_test()
 
+def HasCerts():
+  # Because the bots do not have certs, don't check them if there are no
+  # certs available.
+  proc = subprocess.Popen(['security','find-identity','-p', 'codesigning',
+                           '-v'], stdout=subprocess.PIPE)
+  return "0 valid identities found" not in proc.communicate()[0].strip()
+
+def CheckSignature(file):
+  proc = subprocess.Popen(['codesign', '-v', file], stdout=subprocess.PIPE)
+  o = proc.communicate()[0].strip()
+  assert not proc.returncode
+  if "code object is not signed at all" in o:
+    print 'File %s not properly signed.' % (file)
+    test.fail_test()
 
 def CheckPlistvalue(plist, key, expected):
   if key not in plist:
@@ -50,7 +64,7 @@
 
   for configuration in test_configs:
     test.set_configuration(configuration)
-    test.build('test-device.gyp', test.ALL, chdir='app-bundle')
+    test.build('test-device.gyp', 'test_app', chdir='app-bundle')
     result_file = test.built_file_path('Test App Gyp.bundle/Test App Gyp',
                                        chdir='app-bundle')
     test.must_exist(result_file)
@@ -72,4 +86,15 @@
       CheckFileType(result_file, 'i386')
       CheckPlistvalue(plist, 'CFBundleSupportedPlatforms', ['iPhoneSimulator'])
 
+    if HasCerts() and configuration == 'Default-iphoneos':
+      test.build('test-device.gyp', 'sig_test', chdir='app-bundle')
+      result_file = test.built_file_path('sig_test.bundle/sig_test',
+                                       chdir='app-bundle')
+      CheckSignature(result_file)
+      info_plist = test.built_file_path('sig_test.bundle/Info.plist',
+                                        chdir='app-bundle')
+
+      plist = plistlib.readPlist(info_plist)
+      CheckPlistvalue(plist, 'UIDeviceFamily', [1])
+
   test.pass_test()