#!/usr/bin/env python
import logging
import os
import citadel.nodes.node
import citadel.tools
[docs]class Xcode(citadel.nodes.node.Base):
""":synopsis: Runs Xcode on the current directory.
:requirements: Xcode executable, osx_pprofile
:platform: OSX
:param scheme: The scheme to be built.
:type scheme: required
:param archivePath: The path where the application's binary will reside.
:type archivePath: required
:param workspace: The workspace to build (mutually exclusive with project).
:type workspace: required
:param project: The project to build (mutually exclusive with project).
:type project: required
:param keychain: The path to the keychain to use when signing the binary.
:type keychain: optional
:param keychain_password: The password to unlock the keychain.
:type keychain_password: required if keychain is specified
:param app_id: The application's identifier (com.company.app)
:type app_id: optional
:param lifecycle: The lifecycle use when building (default: clean archive)
:type lifecycle: optional
:param OTHER_CODE_SIGN_FLAGS: Additional options to pass to xcodebuild
:type OTHER_CODE_SIGN_FLAGS: optional
:param CODE_SIGN_IDENTITY: The code signing identity to use when signing
:type CODE_SIGN_IDENTITY: optional
:param DEVELOPMENT_TEAM: The development team to use when signing.
:type DEVELOPMENT_TEAM: optional
:param PROVISIONING_PROFILE_SPECIFIER: The provisioning profile specifier.
:type PROVISIONING_PROFILE_SPECIFIER: optional
**Usage**
.. code-block:: yaml
:linenos:
rbenv:
ruby: 2.3.0
cocoapods: 1.1.0
build:
script:
- rm -fr build
- chmod -R +w *
xcode:
app_id: com.company.app
lifecycle: clean archive
scheme: SomeName
workspace: SomeName.xcworkspace
archivePath: build/SomeName.xcarchive
configuration: Debug
keychain: /Users/jenkins/Library/Keychains/default.keychain
keychain_password: $KEYCHAIN_PASSWORD
ENABLE_BITCODE: NO
IPHONEOS_DEPLOYMENT_TARGET: 6.0
.. warning::
This module will delete the ~/Library/Developer/CoreSimulator and the
~/Library/Developer/Xcode/DerivedData directories regardless. Make
sure there are no builds being executed concurrently.
The keychain password should not be written directly into the citadel.yml
file for security reasons. Pass the value as an environment variable:
``citadel-generate -e "KEYCHAIN_PASSWORD=securestring"``.
The Xcode module is extremely complex due to the requirements it has when
invoking it from the command line. From the GUI everything seems a bit
magical, but what's actually happening underneath is far from it.
Most of the above options should be known to you if you're developing
applications with Xcode and have a reasonable degree of knowledge about
the options it provides. As such, those details will not be discussed
here.
If no app_id is specified, the module will attempt to find a
wildcard provisioning profile and corresponding certificate to sign the
application.
Given the keychain and app_id, it will use an utility to look for the
best matching provisioning profile/certificate to be used. This is a
heuristic and may not match the best. If bugs are found, please contact
the author.
The provisioning profile search is done using
`<https://github.com/grilo/ppbuddy>`_.
Any unknown options will be treated as Xcode options. The following:
.. code-block:: yaml
:linenos:
xcode:
[...]
ENABLE_BITCODE: NO
IPHONEOS_DEPLOYMENT_TARGET: 6.0
Would be passed down as:
.. code-block:: bash
:linenos:
xcodebuild clean archive \\
[...] \\
ENABLE_BITCODE=NO \\
IPHONEOS_DEPLOYMENT_TARGET=6.0
"""
def __init__(self, yml, path):
super(Xcode, self).__init__(yml, path)
xcode_exec = citadel.tools.find_executable('xcodebuild')
if not 'build' in path:
logging.critical('Xcode can only run during the "build" stage.')
return
self.parser.add_default('app_id', None)
self.parser.add_default('lifecycle', 'clean archive')
self.parser.add_default('OTHER_CODE_SIGN_FLAGS', '')
self.parser.add_default('CODE_SIGN_IDENTITY', '$CODE_SIGN_IDENTITY')
self.parser.add_default('DEVELOPMENT_TEAM', '$TEAM_ID')
self.parser.add_default('PROVISIONING_PROFILE_SPECIFIER', '$TEAM_ID/$UUID')
self.parser.is_required('scheme')
self.parser.is_required('archivePath')
self.parser.is_required('configuration')
self.parser.at_most_one(['workspace', 'project'])
self.parser.if_one_then_all(['keychain', 'keychain_password'])
errors, parsed, ignored = self.parser.validate()
self.errors.extend(errors)
if len(errors):
return
# Set exportPath if it doesn't exist already
if 'exportPath' in yml.keys():
parsed['exportPath'] = yml['exportPath']
else:
parsed['exportPath'] = os.path.join(os.path.dirname(parsed['archivePath']),
parsed['scheme'] + '.ipa')
logging.debug('Setting exportPath to: %s', parsed['exportPath'])
self.output.append(citadel.tools.template('xcode_header'))
cmd = ['%s' % (xcode_exec)]
lifecycle = parsed['lifecycle']
if not parsed['archivePath'].startswith('/') and not parsed['archivePath'].startswith('$'):
parsed['archivePath'] = os.path.join(os.getcwd(), parsed['archivePath'])
cmd.append(' %s' % (lifecycle))
cmd.append('-scheme "%s"' % (parsed['scheme']))
cmd.append('-archivePath "%s"' % (parsed['archivePath']))
cmd.append('-configuration "%s"' % (parsed['configuration']))
if 'workspace' in parsed.keys():
cmd.append('-workspace "%s"' % (parsed['workspace']))
elif 'project' in parsed.keys():
cmd.append('-project "%s"' % (parsed['project']))
if 'target' in ignored.keys():
cmd.append('-target "%s"' % (ignored['target']))
del ignored['target']
if 'keychain' in parsed.keys():
parsed['OTHER_CODE_SIGN_FLAGS'] += ' --keychain \'%s\'' % (parsed['keychain'])
cmd.append('OTHER_CODE_SIGN_FLAGS="%s"' %
(parsed['OTHER_CODE_SIGN_FLAGS']))
self.output.append(self.unlock_keychain(
parsed['keychain'], parsed['keychain_password']))
self.output.append(self.get_provisioning_profile(parsed['app_id'], parsed['keychain']))
else:
logging.warning('No "keychain" found, assuming it\'s already prepared.')
cmd.append('CODE_SIGN_IDENTITY="%s"' % (parsed['CODE_SIGN_IDENTITY']))
cmd.append('DEVELOPMENT_TEAM="%s"' % (parsed['DEVELOPMENT_TEAM']))
cmd.append('PROVISIONING_PROFILE_SPECIFIER="%s"' %
(parsed['PROVISIONING_PROFILE_SPECIFIER']))
for key, value in ignored.items():
cmd.append('%s="%s"' % (key, value))
self.output.append('echo "Building..."')
self.output.append(citadel.tools.format_cmd(cmd))
self.output.append('echo "Generating IPA file..."')
export_cmd = ['%s' % (xcode_exec)]
export_cmd.append('-exportArchive')
export_cmd.append('-exportFormat ipa')
export_cmd.append('-exportProvisioningProfile "$PP_NAME"')
export_cmd.append('-archivePath "%s"' % (parsed['archivePath']))
export_cmd.append('-exportPath "%s"' % (parsed['exportPath']))
self.output.append(citadel.tools.format_cmd(export_cmd))
self.output.append(self.codesign_verify(parsed['exportPath']))
[docs] def get_provisioning_profile(self, app_id, keychain):
"""Download ppbuddy.py and run it.
Obtains the best provisioning profile/certificate combo."""
cmd = 'python ppbuddy/ppbuddy.py -k %s' % (keychain)
if app_id:
cmd += ' -a %s' % (app_id)
return citadel.tools.template('xcode_provisioningprofile', {
'command': cmd
})
[docs] def unlock_keychain(self, keychain, password):
"""Unlocks the keychain, required to digitally sign apps."""
return citadel.tools.template('xcode_unlockkeychain', {
'keychain': keychain, 'password': password
})
[docs] def codesign_verify(self, ipafile):
"""Ensure the code signing was done properly."""
return citadel.tools.template('xcode_codesignverify', {
'ipafile': ipafile
})