Last active
June 14, 2019 01:52
-
-
Save Sophrinix/7819604 to your computer and use it in GitHub Desktop.
complete file 3.2.0.v20131205165947/iphone/cli/commands/_build.js lines 2774 to 2783 fix the issue of Settings.bundle failing to load.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* iOS build command. | |
* | |
* @module cli/_build | |
* | |
* @copyright | |
* Copyright (c) 2009-2013 by Appcelerator, Inc. All Rights Reserved. | |
* | |
* @license | |
* Licensed under the terms of the Apache Public License | |
* Please see the LICENSE included with this distribution for details. | |
*/ | |
var appc = require('node-appc'), | |
async = require('async'), | |
Builder = require('titanium-sdk/lib/builder'), | |
cleanCSS = require('clean-css'), | |
crypto = require('crypto'), | |
detect = require('../lib/detect'), | |
ejs = require('ejs'), | |
fields = require('fields'), | |
fs = require('fs'), | |
humanize = require('humanize'), | |
jsanalyze = require('titanium-sdk/lib/jsanalyze'), | |
moment = require('moment'), | |
path = require('path'), | |
spawn = require('child_process').spawn, | |
ti = require('titanium-sdk'), | |
util = require('util'), | |
uuid = require('node-uuid'), | |
wrench = require('wrench'), | |
__ = appc.i18n(__dirname).__, | |
afs = appc.fs, | |
parallel = appc.async.parallel, | |
series = appc.async.series, | |
version = appc.version; | |
function hash(s) { | |
return crypto.createHash('md5').update(s || '').digest('hex'); | |
} | |
function iOSBuilder() { | |
Builder.apply(this, arguments); | |
this.devices = null; // set by findTargetDevices() during 'config' phase | |
this.minSupportedIosSdk = parseInt(version.parseMin(this.packageJson.vendorDependencies['ios sdk'])); | |
this.maxSupportedIosSdk = parseInt(version.parseMax(this.packageJson.vendorDependencies['ios sdk'])); | |
this.deployTypes = { | |
'simulator': 'development', | |
'device': 'test', | |
'dist-appstore': 'production', | |
'dist-adhoc': 'production' | |
}; | |
this.targets = ['simulator', 'device', 'dist-appstore', 'dist-adhoc']; | |
this.deviceFamilies = { | |
iphone: '1', | |
ipad: '2', | |
universal: '1,2' | |
}; | |
this.deviceFamilyNames = { | |
iphone: ['ios', 'iphone'], | |
ipad: ['ios', 'ipad'], | |
universal: ['ios', 'iphone', 'ipad'] | |
}; | |
this.xcodeTargetSuffixes = { | |
iphone: '', | |
ipad: '-iPad', | |
universal: '-universal' | |
}; | |
this.simTypes = { | |
iphone: 'iPhone', | |
ipad: 'iPad' | |
}; | |
this.blacklistDirectories = [ | |
'contents', | |
'resources' | |
]; | |
this.graylistDirectories = [ | |
'frameworks', | |
'plugins' | |
]; | |
this.ipadSplashImages = [ | |
'Default-Landscape.png', | |
'Default-Landscape@2x.png', | |
'Default-Portrait.png', | |
'Default-Portrait@2x.png', | |
'Default-LandscapeLeft.png', | |
'Default-LandscapeLeft@2x.png', | |
'Default-LandscapeRight.png', | |
'Default-LandscapeRight@2x.png', | |
'Default-PortraitUpsideDown.png', | |
'Default-PortraitUpsideDown@2x.png' | |
]; | |
this.tiSymbols = {}; | |
} | |
util.inherits(iOSBuilder, Builder); | |
iOSBuilder.prototype.assertIssue = function assertIssue(issues, name) { | |
var i = 0, | |
len = issues.length; | |
for (; i < len; i++) { | |
if ((typeof name == 'string' && issues[i].id == name) || (typeof name == 'object' && name.test(issues[i].id))) { | |
this.logger.banner(); | |
appc.string.wrap(issues[i].message, this.config.get('cli.width', 100)).split('\n').forEach(function (line, i) { | |
this.logger.error(line.replace(/(__(.+?)__)/g, '$2'.bold)); | |
if (!i) this.logger.log(); | |
}, this); | |
this.logger.log(); | |
process.exit(1); | |
} | |
} | |
}; | |
/** | |
* Returns iOS build-specific configuration options. | |
* @param {Object} logger - The logger instance | |
* @param {Object} config - The CLI config | |
* @param {Object} cli - The CLI instance | |
* @returns {Function|undefined} A function that returns the config info or undefined | |
*/ | |
iOSBuilder.prototype.config = function config(logger, config, cli) { | |
Builder.prototype.config.apply(this, arguments); | |
var _t = this; | |
this.ignoreDirs = new RegExp(config.get('cli.ignoreDirs')); | |
this.ignoreFiles = new RegExp(config.get('cli.ignoreFiles')); | |
var targetDeviceCache = {}; | |
findTargetDevices = function findTargetDevices(target, callback) { | |
if (targetDeviceCache[target]) { | |
return callback(null, targetDeviceCache[target]); | |
} | |
if (target == 'device') { | |
detect.detectDevices(function (err, devices) { | |
if (err) { | |
callback(err); | |
} else { | |
if (devices.length > 2) { | |
// we have more than 1 device plus itunes, so we should show 'all' | |
devices.push({ | |
id: 'all', | |
name: 'All Devices' | |
}); | |
} | |
_t.devices = targetDeviceCache[target] = devices; | |
callback(null, devices); | |
} | |
}); | |
} else if (target == 'simulator') { | |
detect.detectSimulators(config, function (err, sims) { | |
if (err) { | |
callback(err); | |
} else { | |
_t.devices = targetDeviceCache[target] = sims; | |
callback(null, sims); | |
} | |
}); | |
} else { | |
callback(); | |
} | |
}; | |
return function (done) { | |
detect.detect(config, null, function (iosInfo) { | |
detect.detectSimulators(config, null, function (err, simInfo) { | |
this.iosInfo = iosInfo; | |
// check that the iOS environment is found and sane | |
this.assertIssue(iosInfo.issues, 'IOS_XCODE_NOT_INSTALLED'); | |
this.assertIssue(iosInfo.issues, 'IOS_NO_SUPPORTED_XCODE_FOUND'); | |
this.assertIssue(iosInfo.issues, 'IOS_NO_IOS_SDKS'); | |
this.assertIssue(iosInfo.issues, 'IOS_NO_IOS_SIMS'); | |
// get the all installed iOS SDKs and Simulators across all Xcode versions | |
var allSdkVersions = {}, | |
sdkVersions = {}, | |
simVersions = {}; | |
Object.keys(iosInfo.xcode).forEach(function (ver) { | |
iosInfo.xcode[ver].sdks.forEach(function (sdk) { | |
allSdkVersions[sdk] = 1; | |
if (version.gte(sdk, this.minSupportedIosSdk)) { | |
sdkVersions[sdk] = 1; | |
} | |
}, this); | |
iosInfo.xcode[ver].sims.forEach(function (sim) { | |
simVersions[sim] = 1; | |
}); | |
}, this); | |
allSdkVersions = this.iosAllSdkVersions = version.sort(Object.keys(allSdkVersions)); | |
sdkVersions = this.iosSdkVersions = version.sort(Object.keys(sdkVersions)); | |
simVersions = this.iosSimVersions = version.sort(Object.keys(simVersions)); | |
// if we're running from Xcode, determine the default --ios-version | |
var defaultIosVersion = undefined, | |
sdkRoot = process.env.SDKROOT || process.env.SDK_DIR; | |
if (sdkRoot) { | |
var m = sdkRoot.match(/\/iphone(?:os|simulator)(\d.\d).sdk/i); | |
if (m) { | |
defaultIosVersion = m[1]; | |
var file = path.join(sdkRoot, 'System', 'Library', 'CoreServices', 'SystemVersion.plist'); | |
if (fs.existsSync(file)) { | |
var p = new appc.plist(file); | |
if (p.ProductVersion) { | |
defaultIosVersion = p.ProductVersion; | |
} | |
} | |
} | |
} | |
// create the lookup maps for validating developer/distribution certs from the cli args | |
var developerCertLookup = {}, | |
distributionCertLookup = {}; | |
Object.keys(iosInfo.certs.keychains).forEach(function (keychain) { | |
(iosInfo.certs.keychains[keychain].developer || []).forEach(function (d) { | |
if (!d.invalid) { | |
developerCertLookup[d.name.toLowerCase()] = d.name; | |
} | |
}); | |
(iosInfo.certs.keychains[keychain].distribution || []).forEach(function (d) { | |
if (!d.invalid) { | |
distributionCertLookup[d.name.toLowerCase()] = d.name; | |
} | |
}); | |
}); | |
var provisioningProfileLookup = {}; | |
cli.createHook('build.ios.config', function (callback) { | |
callback({ | |
flags: { | |
'force-copy': { | |
desc: __('forces files to be copied instead of symlinked for %s builds only', 'simulator'.cyan) | |
}, | |
'force-copy-all': { | |
desc: __('identical to the %s flag, except this will also copy the %s libTiCore.a file', '--force-copy', | |
humanize.filesize(fs.statSync(path.join(_t.platformPath, 'libTiCore.a')).size, 1024, 1).toUpperCase().cyan) | |
}, | |
'retina': { | |
desc: __('use the retina version of the iOS Simulator') | |
}, | |
'sim-64bit': { | |
desc: __('use the 64-bit version of the iOS Simulator') | |
}, | |
'tall': { | |
desc: __('in combination with %s flag, start the tall version of the retina device', '--retina'.cyan) | |
}, | |
'xcode': { | |
// secret flag to perform Xcode pre-compile build step | |
hidden: true | |
} | |
}, | |
options: { | |
'debug-host': { | |
hidden: true | |
}, | |
'deploy-type': { | |
abbr: 'D', | |
desc: __('the type of deployment; only used when target is %s or %s', 'simulator'.cyan, 'device'.cyan), | |
hint: __('type'), | |
order: 100, | |
values: ['test', 'development'] | |
}, | |
'device-id': { | |
abbr: 'C', | |
desc: __('the name of the iOS simulator or the device udid to install the application to; for %s builds %s; for %s builds %s', | |
'simulator'.cyan, ('[' + simInfo.map(function (s) { return s.id; }).join(', ') + ']').grey, | |
'device'.cyan, ('[' + 'itunes'.bold + ', <udid>, all]').grey), | |
hint: __('name'), | |
order: 210, | |
prompt: function (callback) { | |
findTargetDevices(cli.argv.target, function (err, results) { | |
var maxName = 0, | |
title, | |
promptLabel; | |
results.forEach(function (d) { | |
if (d.id != 'itunes') { | |
maxName = Math.max(d.name.length, maxName); | |
} | |
}); | |
// we need to sort all results into groups for the select field | |
if (cli.argv.target == 'device') { | |
title = __('Which device do you want to install your app on?'); | |
promptLabel = __('Select an device by number or name'); | |
} else if (cli.argv.target == 'simulator') { | |
title = __('Which simulator do you want to launch your app in?'); | |
promptLabel = __('Select an simulator by number or name'); | |
} | |
callback(fields.select({ | |
title: title, | |
promptLabel: promptLabel, | |
formatters: { | |
option: function (opt, idx, num) { | |
return ' ' + num + appc.string.rpad(opt.name, maxName).cyan | |
+ (opt.deviceClass | |
? ' ' + opt.deviceClass + ' (' + opt.productVersion + ')' | |
: ''); | |
} | |
}, | |
default: '1', // just default to the first one, whatever that will be | |
autoSelectOne: true, | |
margin: '', | |
optionLabel: 'name', | |
optionValue: 'id', | |
numbered: true, | |
relistOnError: true, | |
complete: true, | |
suggest: true, | |
options: results | |
})); | |
}); | |
}, | |
required: true, | |
validate: function (device, callback) { | |
var dev = device.toLowerCase(); | |
findTargetDevices(cli.argv.target, function (err, devices) { | |
if (cli.argv.target == 'device' && dev == 'all') { | |
// we let 'all' slide by | |
return callback(null, dev); | |
} | |
var i = 0, | |
l = devices.length; | |
for (; i < l; i++) { | |
if (devices[i].id.toLowerCase() == dev) { | |
return callback(null, devices[i].id); | |
} | |
} | |
callback(new Error(cli.argv.target ? __('Invalid iOS device "%s"', device) : __('Invalid iOS simulator "%s"', device))); | |
}); | |
}, | |
verifyIfRequired: function (callback) { | |
if (cli.argv['build-only']) { | |
// not required if we're build only | |
return callback(); | |
} else if (cli.argv['device-id'] == undefined && config.get('ios.autoSelectDevice', true)) { | |
findTargetDevices(cli.argv.target, function (err, devices) { | |
if (cli.argv.target == 'device') { | |
cli.argv['device-id'] = 'itunes'; | |
callback(); | |
} else if (cli.argv.target == 'simulator') { | |
// for simulator builds, --device-id is a simulator profile and is not | |
// required and we always try to best match | |
callback(); | |
} else { | |
callback(true); | |
} | |
}); | |
return; | |
} | |
// yup, still required | |
callback(true); | |
} | |
}, | |
'developer-name': { | |
abbr: 'V', | |
default: process.env.CODE_SIGN_IDENTITY && process.env.CODE_SIGN_IDENTITY.replace(/^iPhone Developer(?:\: )?/, '') || config.get('ios.developerName'), | |
desc: __('the iOS Developer Certificate to use; required when target is %s', 'device'.cyan), | |
hint: 'name', | |
order: 170, | |
prompt: function (callback) { | |
var developerCerts = {}, | |
maxDevCertLen = 0; | |
Object.keys(iosInfo.certs.keychains).forEach(function (keychain) { | |
(iosInfo.certs.keychains[keychain].developer || []).forEach(function (d) { | |
if (!d.invalid) { | |
Array.isArray(developerCerts[keychain]) || (developerCerts[keychain] = []); | |
developerCerts[keychain].push(d); | |
maxDevCertLen = Math.max(d.name.length, maxDevCertLen); | |
} | |
}); | |
}); | |
// sort the certs | |
Object.keys(developerCerts).forEach(function (keychain) { | |
developerCerts[keychain] = developerCerts[keychain].sort(function (a, b) { | |
return a.name == b.name ? 0 : a.name < b.name ? -1 : 1; | |
}); | |
}); | |
callback(fields.select({ | |
title: __("Which developer certificate would you like to use?"), | |
promptLabel: __('Select a certificate by number or name'), | |
formatters: { | |
option: function (opt, idx, num) { | |
var expires = moment(opt.after), | |
day = expires.format('D'), | |
hour = expires.format('h'); | |
return ' ' + num + appc.string.rpad(opt.name, maxDevCertLen + 1).cyan | |
+ (opt.after ? (' (' + __('expires %s', expires.format('MMM') + ' ' | |
+ (day.length == 1 ? ' ' : '') + day + ', ' + expires.format('YYYY') + ' ' | |
+ (hour.length == 1 ? ' ' : '') + hour + ':' + expires.format('mm:ss a')) | |
+ ')').grey : ''); | |
} | |
}, | |
margin: '', | |
optionLabel: 'name', | |
optionValue: 'name', | |
numbered: true, | |
relistOnError: true, | |
complete: true, | |
suggest: false, | |
options: developerCerts | |
})); | |
}, | |
validate: function (value, callback) { | |
if (cli.argv.target != 'device') { | |
return callback(null, value); | |
} | |
if (value) { | |
var v = developerCertLookup[value.toLowerCase()]; | |
if (v) { | |
return callback(null, v); | |
} | |
} | |
callback(new Error(__('Invalid developer certificate "%s"', value))); | |
} | |
}, | |
'distribution-name': { | |
abbr: 'R', | |
default: process.env.CODE_SIGN_IDENTITY && process.env.CODE_SIGN_IDENTITY.replace(/^iPhone Distribution(?:\: )?/, '') || config.get('ios.distributionName'), | |
desc: __('the iOS Distribution Certificate to use; required when target is %s or %s', 'dist-appstore'.cyan, 'dist-adhoc'.cyan), | |
hint: 'name', | |
order: 180, | |
prompt: function (callback) { | |
var distributionCerts = {}, | |
maxDistCertLen = 0; | |
Object.keys(iosInfo.certs.keychains).forEach(function (keychain) { | |
(iosInfo.certs.keychains[keychain].distribution || []).forEach(function (d) { | |
if (!d.invalid) { | |
Array.isArray(distributionCerts[keychain]) || (distributionCerts[keychain] = []); | |
distributionCerts[keychain].push(d); | |
maxDistCertLen = Math.max(d.name.length, maxDistCertLen); | |
} | |
}); | |
}); | |
// sort the certs | |
Object.keys(distributionCerts).forEach(function (keychain) { | |
distributionCerts[keychain] = distributionCerts[keychain].sort(function (a, b) { | |
return a.name == b.name ? 0 : a.name < b.name ? -1 : 1; | |
}); | |
}); | |
callback(fields.select({ | |
title: __("Which distribution certificate would you like to use?"), | |
promptLabel: __('Select a certificate by number or name'), | |
formatters: { | |
option: function (opt, idx, num) { | |
var expires = moment(opt.after), | |
day = expires.format('D'), | |
hour = expires.format('h'); | |
return ' ' + num + appc.string.rpad(opt.name, maxDistCertLen + 1).cyan | |
+ (opt.after ? (' (' + __('expires %s', expires.format('MMM') + ' ' | |
+ (day.length == 1 ? ' ' : '') + day + ', ' + expires.format('YYYY') + ' ' | |
+ (hour.length == 1 ? ' ' : '') + hour + ':' + expires.format('mm:ss a')) | |
+ ')').grey : ''); | |
} | |
}, | |
margin: '', | |
optionLabel: 'name', | |
optionValue: 'name', | |
numbered: true, | |
relistOnError: true, | |
complete: true, | |
suggest: false, | |
options: distributionCerts | |
})); | |
}, | |
validate: function (value, callback) { | |
if (cli.argv.target != 'dist-appstore' && cli.argv.target != 'dist-adhoc') { | |
return callback(null, value); | |
} | |
if (value) { | |
var v = distributionCertLookup[value.toLowerCase()]; | |
if (v) { | |
return callback(null, v); | |
} | |
} | |
callback(new Error(__('Invalid distribution certificate "%s"', value))); | |
} | |
}, | |
'device-family': { | |
abbr: 'F', | |
desc: __('the device family to build for'), | |
order: 120, | |
values: Object.keys(_t.deviceFamilies) | |
}, | |
'ios-version': { | |
abbr: 'I', | |
callback: function (value) { | |
try { | |
if (value && allSdkVersions.indexOf(value) != -1 && version.lt(value, _t.minSupportedIosSdk)) { | |
logger.banner(); | |
logger.error(__('The specified iOS SDK version "%s" is not supported by Titanium %s', value, _t.titaniumSdkVersion) + '\n'); | |
if (sdkVersions.length) { | |
logger.log(__('Available supported iOS SDKs:')); | |
sdkVersions.forEach(function (ver) { | |
logger.log(' ' + ver.cyan); | |
}); | |
logger.log(); | |
} | |
process.exit(1); | |
} | |
} catch (e) { | |
// squelch and let the cli detect the bad version | |
} | |
}, | |
default: defaultIosVersion, | |
desc: __('iOS SDK version to build with'), | |
order: 130, | |
prompt: function (callback) { | |
callback(fields.select({ | |
title: __("Which iOS SDK version would you like to build with?"), | |
promptLabel: __('Select an iOS SDK version by number or name'), | |
margin: '', | |
numbered: true, | |
relistOnError: true, | |
complete: true, | |
suggest: false, | |
options: sdkVersions | |
})); | |
}, | |
values: sdkVersions | |
}, | |
'keychain': { | |
abbr: 'K', | |
desc: __('path to the distribution keychain to use instead of the system default; only used when target is %s, %s, or %s', 'device'.cyan, 'dist-appstore'.cyan, 'dist-adhoc'.cyan), | |
hideValues: true | |
}, | |
'launch-url': { | |
// url for the application to launch in mobile Safari, as soon as the app boots up | |
hidden: true | |
}, | |
'output-dir': { | |
abbr: 'O', | |
desc: __('the output directory when using %s', 'dist-adhoc'.cyan), | |
hint: 'dir', | |
order: 200, | |
prompt: function (callback) { | |
callback(fields.file({ | |
promptLabel: __('Where would you like the output IPA file saved?'), | |
default: cli.argv['project-dir'] && afs.resolvePath(cli.argv['project-dir'], 'dist'), | |
complete: true, | |
showHidden: true, | |
ignoreDirs: _t.ignoreDirs, | |
ignoreFiles: /.*/, | |
validate: _t.conf.options['output-dir'].validate.bind(_t) | |
})); | |
}, | |
validate: function (outputDir, callback) { | |
callback(outputDir || !_t.conf.options['output-dir'].required ? null : new Error(__('Invalid output directory')), outputDir); | |
} | |
}, | |
'pp-uuid': { | |
abbr: 'P', | |
default: process.env.PROVISIONING_PROFILE, | |
desc: __('the provisioning profile uuid; required when target is %s, %s, or %s', 'device'.cyan, 'dist-appstore'.cyan, 'dist-adhoc'.cyan), | |
hint: 'uuid', | |
order: 190, | |
prompt: function (callback) { | |
var provisioningProfiles = {}, | |
appId = cli.tiapp.id, | |
maxAppId = 0, | |
pp; | |
function prep(a) { | |
return a.filter(function (p) { | |
if (!p.expired) { | |
var re = new RegExp(p.appId.replace(/\./g, '\\.').replace(/\*/g, '.*')); | |
if (re.test(appId)) { | |
var label = p.name; | |
if (label.indexOf(p.appId) == -1) { | |
label += ': ' + p.appId; | |
} | |
p.label = label; | |
maxAppId = Math.max(p.label.length, maxAppId); | |
return true; | |
} | |
} | |
}).sort(function (a, b) { | |
return a.label.toLowerCase().localeCompare(b.label.toLowerCase()); | |
}); | |
} | |
if (cli.argv.target == 'device') { | |
if (iosInfo.provisioningProfiles.development.length) { | |
pp = prep(iosInfo.provisioningProfiles.development); | |
if (pp.length) { | |
provisioningProfiles[__('Available Development UUIDs:')] = pp; | |
} else { | |
logger.error(__('Unable to find any non-expired development provisioning profiles that match the app id "%s"', appId) + '\n'); | |
logger.log(__('You will need to login into %s with your Apple Download account, then create, download, and install a profile.', | |
'http://appcelerator.com/ios-dev-certs'.cyan) + '\n'); | |
process.exit(1); | |
} | |
} else { | |
logger.error(__('Unable to find any development provisioning profiles') + '\n'); | |
logger.log(__('You will need to login into %s with your Apple Download account, then create, download, and install a profile.', | |
'http://appcelerator.com/ios-dev-certs'.cyan) + '\n'); | |
process.exit(1); | |
} | |
} else if (cli.argv.target == 'dist-appstore' || cli.argv.target == 'dist-adhoc') { | |
if (iosInfo.provisioningProfiles.distribution.length || iosInfo.provisioningProfiles.adhoc.length) { | |
pp = prep(iosInfo.provisioningProfiles.distribution); | |
var valid = pp.length; | |
if (pp.length) { | |
provisioningProfiles[__('Available Distribution UUIDs:')] = pp; | |
} | |
pp = prep(iosInfo.provisioningProfiles.adhoc); | |
valid += pp.length; | |
if (pp.length) { | |
provisioningProfiles[__('Available Adhoc UUIDs:')] = pp; | |
} | |
if (!valid) { | |
logger.error(__('Unable to find any non-expired distribution or adhoc provisioning profiles that match the app id "%s".', appId) + '\n'); | |
logger.log(__('You will need to login into %s with your Apple Download account, then create, download, and install a profile.', | |
'http://appcelerator.com/ios-dist-certs'.cyan) + '\n'); | |
process.exit(1); | |
} | |
} else { | |
logger.error(__('Unable to find any distribution or adhoc provisioning profiles')); | |
logger.log(__('You will need to login into %s with your Apple Download account, then create, download, and install a profile.', | |
'http://appcelerator.com/ios-dist-certs'.cyan) + '\n'); | |
process.exit(1); | |
} | |
} | |
callback(fields.select({ | |
title: __("Which provisioning profile would you like to use?"), | |
promptLabel: __('Select a provisioning profile UUID by number or name'), | |
formatters: { | |
option: function (opt, idx, num) { | |
var expires = moment(opt.expirationDate), | |
day = expires.format('D'), | |
hour = expires.format('h'); | |
return ' ' + num + String(opt.uuid).cyan + ' ' | |
+ appc.string.rpad(opt.label, maxAppId + 1) | |
+ (opt.expirationDate ? (' (' + __('expires %s', expires.format('MMM') + ' ' | |
+ (day.length == 1 ? ' ' : '') + day + ', ' + expires.format('YYYY') + ' ' | |
+ (hour.length == 1 ? ' ' : '') + hour + ':' + expires.format('mm:ss a')) | |
+ ')').grey : ''); | |
} | |
}, | |
margin: '', | |
optionLabel: 'name', | |
optionValue: 'uuid', | |
numbered: true, | |
relistOnError: true, | |
complete: true, | |
suggest: false, | |
options: provisioningProfiles | |
})); | |
}, | |
validate: function (value, callback) { | |
if (cli.argv.target == 'simulator') { | |
return callback(null, value); | |
} | |
if (value) { | |
var v = provisioningProfileLookup[value.toLowerCase()]; | |
if (v) { | |
return callback(null, v); | |
} | |
} | |
callback(new Error(__('Invalid provisioning profile UUID "%s"', value))); | |
} | |
}, | |
'profiler-host': { | |
hidden: true | |
}, | |
'sim-type': { | |
abbr: 'Y', | |
desc: __('iOS Simulator type; only used when target is %s', 'simulator'.cyan), | |
hint: 'type', | |
order: 150, | |
values: Object.keys(_t.simTypes) | |
}, | |
'sim-version': { | |
abbr: 'S', | |
desc: __('iOS Simulator version; only used when target is %s', 'simulator'.cyan), | |
hint: 'version', | |
order: 160, | |
values: simVersions | |
}, | |
'target': { | |
abbr: 'T', | |
callback: function (value) { | |
if (value != 'simulator') { | |
_t.assertIssue(logger, iosInfo.issues, 'IOS_NO_KEYCHAINS_FOUND'); | |
_t.assertIssue(logger, iosInfo.issues, 'IOS_NO_WWDR_CERT_FOUND'); | |
} | |
// as soon as we know the target, toggle required options for validation | |
switch (value) { | |
case 'device': | |
_t.assertIssue(logger, iosInfo.issues, 'IOS_NO_VALID_DEV_CERTS_FOUND'); | |
_t.assertIssue(logger, iosInfo.issues, 'IOS_NO_VALID_DEVELOPMENT_PROVISIONING_PROFILES'); | |
iosInfo.provisioningProfiles.development.forEach(function (d) { | |
provisioningProfileLookup[d.uuid.toLowerCase()] = d.uuid; | |
}); | |
_t.conf.options['developer-name'].required = true; | |
_t.conf.options['pp-uuid'].required = true; | |
break; | |
case 'dist-adhoc': | |
_t.assertIssue(logger, iosInfo.issues, 'IOS_NO_VALID_DIST_CERTS_FOUND'); | |
// TODO: assert there is at least one distribution or adhoc provisioning profile | |
_t.conf.options['output-dir'].required = true; | |
// purposely fall through! | |
case 'dist-appstore': | |
_t.conf.options['device-id'].required = false; | |
_t.conf.options['distribution-name'].required = true; | |
_t.conf.options['pp-uuid'].required = true; | |
// build lookup maps | |
iosInfo.provisioningProfiles.distribution.forEach(function (d) { | |
provisioningProfileLookup[d.uuid.toLowerCase()] = d.uuid; | |
}); | |
iosInfo.provisioningProfiles.adhoc.forEach(function (d) { | |
provisioningProfileLookup[d.uuid.toLowerCase()] = d.uuid; | |
}); | |
} | |
}, | |
default: process.env.CURRENT_ARCH && process.env.CURRENT_ARCH != 'i386' ? 'device' : 'simulator', | |
desc: __('the target to build for'), | |
order: 110, | |
required: true, | |
values: _t.targets | |
} | |
} | |
}); | |
})(function (err, results, result) { | |
done(_t.conf = result); | |
}); | |
}.bind(this)); | |
}.bind(this)); | |
}.bind(this); | |
}; | |
iOSBuilder.prototype.validate = function (logger, config, cli) { | |
this.target = cli.argv.target; | |
if (cli.argv.xcode) { | |
this.deployType = cli.argv['deploy-type'] || this.deployTypes[this.target]; | |
} else { | |
this.deployType = /^device|simulator$/.test(this.target) && cli.argv['deploy-type'] ? cli.argv['deploy-type'] : this.deployTypes[this.target]; | |
} | |
// manually inject the build profile settings into the tiapp.xml | |
switch (this.deployType) { | |
case 'production': | |
this.minifyJS = true; | |
this.encryptJS = true; | |
this.allowDebugging = false; | |
this.allowProfiling = false; | |
this.includeAllTiModules = false; | |
this.compileI18N = true; | |
this.compileJSS = true; | |
break; | |
case 'test': | |
this.minifyJS = true; | |
this.encryptJS = true; | |
this.allowDebugging = true; | |
this.allowProfiling = true; | |
this.includeAllTiModules = false; | |
this.compileI18N = true; | |
this.compileJSS = true; | |
break; | |
case 'development': | |
default: | |
this.minifyJS = false; | |
this.encryptJS = false; | |
this.allowDebugging = true; | |
this.allowProfiling = true; | |
this.includeAllTiModules = true; | |
this.compileI18N = false; | |
this.compileJSS = false; | |
} | |
if (cli.argv['skip-js-minify']) { | |
this.minifyJS = false; | |
} | |
// at this point we've validated everything except underscores in the app id | |
if (!config.get('ios.skipAppIdValidation')) { | |
if (!/^([a-zA-Z_]{1}[a-zA-Z0-9_-]*(\.[a-zA-Z0-9_-]*)*)$/.test(cli.tiapp.id)) { | |
logger.error(__('tiapp.xml contains an invalid app id "%s"', cli.tiapp.id)); | |
logger.error(__('The app id must consist only of letters, numbers, dashes, and underscores.')); | |
logger.error(__('Note: iOS does not allow underscores.')); | |
logger.error(__('The first character must be a letter or underscore.')); | |
logger.error(__("Usually the app id is your company's reversed Internet domain name. (i.e. com.example.myapp)") + '\n'); | |
process.exit(1); | |
} | |
if (cli.tiapp.id.indexOf('_') != -1) { | |
logger.error(__('tiapp.xml contains an invalid app id "%s"', cli.tiapp.id)); | |
logger.error(__('The app id must consist of letters, numbers, and dashes.')); | |
logger.error(__('The first character must be a letter.')); | |
logger.error(__("Usually the app id is your company's reversed Internet domain name. (i.e. com.example.myapp)") + '\n'); | |
process.exit(1); | |
} | |
} | |
if (!cli.argv['ios-version']) { | |
if (this.iosSdkVersions.length) { | |
// set the latest version | |
cli.argv['ios-version'] = this.iosSdkVersions[this.iosSdkVersions.length-1]; | |
} else { | |
// this should not be possible, but you never know | |
logger.error(cli.argv['ios-version'] ? __('Unable to find iOS SDK %s', cli.argv['ios-version']) + '\n' : __('Missing iOS SDK') + '\n'); | |
logger.log(__('Available iOS SDK versions:')); | |
this.iosSdkVersions.forEach(function (ver) { | |
logger.log(' ' + ver.cyan); | |
}); | |
logger.log(); | |
process.exit(1); | |
} | |
} | |
this.iosSdkVersion = cli.argv['ios-version']; | |
// figure out the min-ios-ver that this app is going to support | |
var defaultMinIosSdk = this.packageJson.minIosVersion; | |
this.minIosVer = cli.tiapp.ios && cli.tiapp.ios['min-ios-ver'] || defaultMinIosSdk; | |
if (version.gte(this.iosSdkVersion, '6.0') && version.lt(this.minIosVer, defaultMinIosSdk)) { | |
this.minIosVer = defaultMinIosSdk; | |
} else if (version.lt(this.minIosVer, defaultMinIosSdk)) { | |
this.minIosVer = defaultMinIosSdk; | |
} else if (version.gt(this.minIosVer, this.iosSdkVersion)) { | |
this.minIosVer = this.iosSdkVersion; | |
} | |
var deviceId = this.deviceId = cli.argv['device-id']; | |
// if we're doing a simulator build and we have a --device-id, then set the | |
// args based on the sim profile values | |
if ((this.target == 'device' || this.target == 'simulator') && deviceId) { | |
for (var i = 0, l = this.devices.length; i < l; i++) { | |
if (this.devices[i].id == deviceId) { | |
if (this.target == 'device') { | |
if (this.devices[i].id != 'itunes' && version.lt(this.devices[i].productVersion, this.minIosVer)) { | |
logger.error(__('This app does not support the device "%s"', this.devices[i].name) + '\n'); | |
logger.log(__("The device is running iOS %s, however the app's the minimum iOS version is set to %s", this.devices[i].productVersion.cyan, version.format(this.minIosVer, 2, 3).cyan)); | |
logger.log(__('In order to install this app on this device, lower the %s to %s in the tiapp.xml:', '<min-ios-ver>'.cyan, version.format(this.devices[i].productVersion, 2, 2).cyan)); | |
logger.log(); | |
logger.log('<ti:app xmlns:ti="http://ti.appcelerator.org">'.grey); | |
logger.log(' <ios>'.grey); | |
logger.log((' <min-ios-ver>' + version.format(this.devices[i].productVersion, 2, 2) + '</min-ios-ver>').magenta); | |
logger.log(' </ios>'.grey); | |
logger.log('</ti:app>'.grey); | |
logger.log(); | |
process.exit(0); | |
} | |
} else if (this.target == 'simulator') { | |
cli.argv.retina = !!this.devices[i].retina; | |
cli.argv.tall = !!this.devices[i].tall; | |
cli.argv['sim-64bit'] = !!this.devices[i]['64bit']; | |
cli.argv['sim-type'] = this.devices[i].type; | |
} | |
break; | |
} | |
} | |
} | |
// make sure the app doesn't have any blacklisted directories in the Resources directory and warn about graylisted names | |
var resourcesDir = path.join(cli.argv['project-dir'], 'Resources'); | |
if (fs.existsSync(resourcesDir)) { | |
fs.readdirSync(resourcesDir).forEach(function (filename) { | |
var lcaseFilename = filename.toLowerCase(), | |
isDir = fs.statSync(path.join(resourcesDir, filename)).isDirectory(); | |
if (this.blacklistDirectories.indexOf(lcaseFilename) != -1) { | |
if (isDir) { | |
logger.error(__('Found blacklisted directory in the Resources directory') + '\n'); | |
logger.error(__('The directory "%s" is a reserved word.', filename)); | |
logger.error(__('You must rename this directory to something else.') + '\n'); | |
} else { | |
logger.error(__('Found blacklisted file in the Resources directory') + '\n'); | |
logger.error(__('The file "%s" is a reserved word.', filename)); | |
logger.error(__('You must rename this file to something else.') + '\n'); | |
} | |
process.exit(1); | |
} else if (this.graylistDirectories.indexOf(lcaseFilename) != -1) { | |
if (isDir) { | |
logger.warn(__('Found graylisted directory in the Resources directory')); | |
logger.warn(__('The directory "%s" is potentially a reserved word.', filename)); | |
logger.warn(__('There is a good chance your app will be rejected by Apple.')); | |
logger.warn(__('It is highly recommended you rename this directory to something else.')); | |
} else { | |
logger.warn(__('Found graylisted file in the Resources directory')); | |
logger.warn(__('The file "%s" is potentially a reserved word.', filename)); | |
logger.warn(__('There is a good chance your app will be rejected by Apple.')); | |
logger.warn(__('It is highly recommended you rename this file to something else.')); | |
} | |
} | |
}, this); | |
} | |
// we have an ios sdk version, find the best xcode version to use | |
this.xcodeEnv = null; | |
Object.keys(this.iosInfo.xcode).forEach(function (ver) { | |
if (ver != '__selected__' && (!this.xcodeEnv || this.iosInfo.xcode[ver].selected) && this.iosInfo.xcode[ver].sdks.some(function (sdk) { return version.eq(sdk, cli.argv['ios-version']); }, this)) { | |
this.xcodeEnv = this.iosInfo.xcode[ver]; | |
} | |
}, this); | |
if (!this.xcodeEnv) { | |
// this should never happen | |
logger.error(__('Unable to find suitable Xcode install that supports iOS SDK %s', cli.argv['ios-version']) + '\n'); | |
process.exit(1); | |
} | |
// check if we are running from Xcode | |
if (cli.argv.xcode) { | |
cli.argv['skip-js-minify'] = true; // never minify Xcode builds | |
cli.argv['force-copy'] = true; // if building from xcode, we'll force files to be copied instead of symlinked | |
cli.argv['force-copy-all'] = false; // we don't want to copy the big libTiCore.a file around by default | |
} | |
// if in the prepare phase and doing a device/dist build... | |
if (!cli.argv.xcode && cli.argv.target != 'simulator') { | |
// make sure they have Apple's WWDR cert installed | |
if (!this.iosInfo.certs.wwdr) { | |
logger.error(__('WWDR Intermediate Certificate not found') + '\n'); | |
logger.log(__('Download and install the certificate from %s', 'http://appcelerator.com/ios-wwdr'.cyan) + '\n'); | |
process.exit(1); | |
} | |
// validate keychain | |
var keychain = cli.argv.keychain ? afs.resolvePath(cli.argv.keychain) : null; | |
if (keychain && !fs.existsSync(keychain)) { | |
logger.error(__('Unable to find keychain "%s"', keychain) + '\n'); | |
logger.log(__('Available keychains:')); | |
Object.keys(this.iosInfo.certs.keychains).forEach(function (kc) { | |
logger.log(' ' + kc.cyan); | |
}); | |
logger.log(); | |
appc.string.suggest(keychain, Object.keys(this.iosInfo.certs.keychains), logger.log); | |
process.exit(1); | |
} | |
} | |
var deviceFamily = cli.argv['device-family'], | |
deploymentTargets = cli.tiapp['deployment-targets']; | |
if (!deviceFamily && process.env.TARGETED_DEVICE_FAMILY) { | |
// device family was not specified at the command line, but we did get it via an environment variable! | |
deviceFamily = process.env.TARGETED_DEVICE_FAMILY === '1' ? 'iphone' : process.env.TARGETED_DEVICE_FAMILY == '2' ? 'ipad' : 'universal'; | |
} | |
if (!deviceFamily && deploymentTargets) { | |
// device family was not an environment variable, construct via the tiapp.xml's deployment targets | |
if (deploymentTargets.iphone && deploymentTargets.ipad) { | |
deviceFamily = cli.argv.$originalPlatform == 'ipad' ? 'ipad' : 'universal'; | |
} else if (deploymentTargets.iphone) { | |
deviceFamily = 'iphone'; | |
} else if (deploymentTargets.ipad) { | |
deviceFamily = 'ipad'; | |
} | |
} | |
if (!deviceFamily) { | |
logger.info(__('No device family specified, defaulting to %s', 'universal')); | |
deviceFamily = 'universal'; | |
} | |
if (!this.deviceFamilies[deviceFamily]) { | |
logger.error(__('Invalid device family "%s"', deviceFamily) + '\n'); | |
appc.string.suggest(deviceFamily, Object.keys(this.deviceFamilies), logger.log, 3); | |
process.exit(1); | |
} | |
// device family may have been modified, so set it back in the args | |
cli.argv['device-family'] = deviceFamily; | |
if (cli.argv.target == 'simulator') { | |
if (!cli.argv['sim-version']) { | |
cli.argv['sim-version'] = cli.argv['ios-version']; | |
} | |
// check that the sim version exists | |
if (!this.xcodeEnv.sims || this.xcodeEnv.sims.indexOf(cli.argv['sim-version']) == -1) { | |
// the preferred Xcode install we selected doesn't have this simulator, search the all again | |
this.xcodeEnv = null; | |
Object.keys(this.iosInfo.xcode).forEach(function (ver) { | |
if (ver != '__selected__' | |
&& !this.xcodeEnv | |
&& this.iosInfo.xcode[ver].sdks.some(function (sdk) { return version.eq(sdk, cli.argv['ios-version']); }) | |
&& this.iosInfo.xcode[ver].sims.some(function (sim) { return version.eq(sim, cli.argv['sim-version']); }) | |
) { | |
this.xcodeEnv = this.iosInfo.xcode[ver]; | |
} | |
}, this); | |
if (!this.xcodeEnv) { | |
// this should never happen | |
logger.error(__('Unable to find any Xcode installs that has iOS SDK %s and iOS Simulator %s', cli.argv['ios-version'], cli.argv['sim-version']) + '\n'); | |
process.exit(1); | |
} | |
} | |
if (!cli.argv['sim-type']) { | |
cli.argv['sim-type'] = cli.argv['device-family'] == 'ipad' ? 'ipad' : 'iphone'; | |
} | |
} | |
if (cli.argv.target != 'dist-appstore') { | |
var tool = []; | |
this.allowDebugging && tool.push('debug'); | |
this.allowProfiling && tool.push('profiler'); | |
tool.forEach(function (type) { | |
if (cli.argv[type + '-host']) { | |
if (typeof cli.argv[type + '-host'] == 'number') { | |
logger.error(__('Invalid %s host "%s"', type, cli.argv[type + '-host']) + '\n'); | |
logger.log(__('The %s host must be in the format "host:port".', type) + '\n'); | |
process.exit(1); | |
} | |
var parts = cli.argv[type + '-host'].split(':'); | |
if ((cli.argv.target == 'simulator' && parts.length < 2) || (cli.argv.target != 'simulator' && parts.length < 4)) { | |
logger.error(__('Invalid ' + type + ' host "%s"', cli.argv[type + '-host']) + '\n'); | |
if (cli.argv.target == 'simulator') { | |
logger.log(__('The %s host must be in the format "host:port".', type) + '\n'); | |
} else { | |
logger.log(__('The %s host must be in the format "host:port:airkey:hosts".', type) + '\n'); | |
} | |
process.exit(1); | |
} | |
if (parts.length > 1 && parts[1]) { | |
var port = parseInt(parts[1]); | |
if (isNaN(port) || port < 1 || port > 65535) { | |
logger.error(__('Invalid ' + type + ' host "%s"', cli.argv[type + '-host']) + '\n'); | |
logger.log(__('The port must be a valid integer between 1 and 65535.') + '\n'); | |
process.exit(1); | |
} | |
} | |
} | |
}); | |
} | |
return function (finished) { | |
// validate modules | |
var moduleSearchPaths = [ cli.argv['project-dir'] ], | |
customModulePaths = config.get('paths.modules'), | |
addSearchPath = function (p) { | |
p = afs.resolvePath(p); | |
if (fs.existsSync(p) && moduleSearchPaths.indexOf(p) == -1) { | |
moduleSearchPaths.push(p); | |
} | |
}; | |
cli.env.os.sdkPaths.forEach(addSearchPath); | |
Array.isArray(customModulePaths) && customModulePaths.forEach(addSearchPath); | |
appc.timodule.find(cli.tiapp.modules, ['ios', 'iphone'], this.deployType, this.titaniumSdkVersion, moduleSearchPaths, logger, function (modules) { | |
if (modules.missing.length) { | |
logger.error(__('Could not find all required Titanium Modules:')) | |
modules.missing.forEach(function (m) { | |
logger.error(' id: ' + m.id + '\t version: ' + (m.version || 'latest') + '\t platform: ' + m.platform + '\t deploy-type: ' + m.deployType); | |
}, this); | |
logger.log(); | |
process.exit(1); | |
} | |
if (modules.incompatible.length) { | |
logger.error(__('Found incompatible Titanium Modules:')); | |
modules.incompatible.forEach(function (m) { | |
logger.error(' id: ' + m.id + '\t version: ' + (m.version || 'latest') + '\t platform: ' + m.platform + '\t min sdk: ' + m.minsdk); | |
}, this); | |
logger.log(); | |
process.exit(1); | |
} | |
if (modules.conflict.length) { | |
logger.error(__('Found conflicting Titanium modules:')); | |
modules.conflict.forEach(function (m) { | |
logger.error(' ' + __('Titanium module "%s" requested for both iOS and CommonJS platforms, but only one may be used at a time.', m.id)); | |
}, this); | |
logger.log(); | |
process.exit(1); | |
} | |
this.modules = modules.found; | |
this.commonJsModules = []; | |
this.nativeLibModules = []; | |
var nativeHashes = []; | |
modules.found.forEach(function (module) { | |
if (module.platform.indexOf('commonjs') != -1) { | |
module.native = false; | |
module.libFile = path.join(module.modulePath, module.id + '.js'); | |
if (!fs.existsSync(module.libFile)) { | |
this.logger.error(__('Module %s version %s is missing module file: %s', module.id.cyan, (module.manifest.version || 'latest').cyan, module.libFile.cyan) + '\n'); | |
process.exit(1); | |
} | |
this.commonJsModules.push(module); | |
} else { | |
module.native = true; | |
module.libName = 'lib' + module.id.toLowerCase() + '.a', | |
module.libFile = path.join(module.modulePath, module.libName); | |
if (!fs.existsSync(module.libFile)) { | |
this.logger.error(__('Module %s version %s is missing library file: %s', module.id.cyan, (module.manifest.version || 'latest').cyan, module.libFile.cyan) + '\n'); | |
process.exit(1); | |
} | |
nativeHashes.push(module.hash = hash(fs.readFileSync(module.libFile))); | |
this.nativeLibModules.push(module); | |
} | |
// scan the module for any CLI hooks | |
cli.scanHooks(path.join(module.modulePath, 'hooks')); | |
}, this); | |
this.modulesNativeHash = hash(nativeHashes.length ? nativeHashes.sort().join(',') : ''); | |
finished(); | |
}.bind(this)); // end timodule.find() | |
}.bind(this); // end returned callback | |
}; | |
iOSBuilder.prototype.run = function (logger, config, cli, finished) { | |
Builder.prototype.run.apply(this, arguments); | |
// force the platform to "ios" just in case it was "iphone" so that plugins can reference it | |
cli.argv.platform = 'ios'; | |
// if in the xcode phase, bypass the pre, post, and finalize hooks for xcode builds | |
if (cli.argv.xcode) { | |
series(this, [ | |
'initialize', | |
'loginfo', | |
'xcodePrecompilePhase', | |
'optimizeImages' | |
], function () { | |
finished(); | |
}); | |
return; | |
} | |
series(this, [ | |
function (next) { | |
cli.emit('build.pre.construct', this, next); | |
}, | |
'doAnalytics', | |
'initialize', | |
'loginfo', | |
'readBuildManifest', | |
'checkIfNeedToRecompile', | |
'preparePhase', | |
function (next) { | |
cli.emit('build.pre.compile', this, next); | |
}, | |
function (next) { | |
// Make sure we have an app.js. This used to be validated in validate(), but since plugins like | |
// Alloy generate an app.js, it may not have existed during validate(), but should exist now | |
// that build.pre.compile was fired. | |
ti.validateAppJsExists(this.projectDir, this.logger, ['iphone', 'ios']); | |
next(); | |
}, | |
'createInfoPlist', | |
'createEntitlementsPlist', | |
'initBuildDir', | |
'injectModulesIntoXcodeProject', | |
'injectApplicationDefaults', // if ApplicationDefaults.m was modified, forceRebuild will be set to true | |
'copyTitaniumLibraries', | |
'copyItunesArtwork', | |
'copyGraphics', | |
function (next) { | |
// this is a hack... for non-deployment builds we need to force xcode so that the pre-compile phase | |
// is run and the ApplicationRouting.m gets updated | |
if (!this.forceRebuild && this.deployType != 'development') { | |
this.logger.info(__('Forcing rebuild: deploy type is %s, so need to recompile ApplicationRouting.m', this.deployType)); | |
this.forceRebuild = true; | |
} | |
this.xcodePrecompilePhase(function () { | |
if (this.forceRebuild || !fs.existsSync(this.xcodeAppDir, this.tiapp.name)) { | |
// we're not being called from Xcode, so we can call the pre-compile phase now | |
// and save us several seconds | |
parallel(this, [ | |
'optimizeImages', | |
'invokeXcodeBuild' | |
], next); | |
} else { | |
this.logger.info(__('Skipping xcodebuild')); | |
next(); | |
} | |
}.bind(this)); | |
}, | |
'writeBuildManifest', | |
function (next) { | |
if (!this.buildOnly && this.target == 'simulator') { | |
var delta = appc.time.prettyDiff(this.cli.startTime, Date.now()); | |
this.logger.info(__('Finished building the application in %s', delta.cyan)); | |
} | |
cli.emit('build.post.compile', this, next); | |
} | |
], function (err) { | |
cli.emit('build.finalize', this, function () { | |
finished(err); | |
}); | |
}); | |
}; | |
iOSBuilder.prototype.doAnalytics = function doAnalytics(next) { | |
var cli = this.cli, | |
eventName = cli.argv['device-family'] + '.' + cli.argv.target; | |
if (cli.argv.target == 'dist-appstore' || cli.argv.target == 'dist-adhoc') { | |
eventName = cli.argv['device-family'] + '.distribute.' + cli.argv.target.replace('dist-', ''); | |
} else if (this.allowDebugging && cli.argv['debug-host']) { | |
eventName += '.debug'; | |
} else if (this.allowProfiling && cli.argv['profiler-host']) { | |
eventName += '.profile'; | |
} else { | |
eventName += '.run'; | |
} | |
cli.addAnalyticsEvent(eventName, { | |
dir: cli.argv['project-dir'], | |
name: cli.tiapp.name, | |
publisher: cli.tiapp.publisher, | |
url: cli.tiapp.url, | |
image: cli.tiapp.icon, | |
appid: cli.tiapp.id, | |
description: cli.tiapp.description, | |
type: cli.argv.type, | |
guid: cli.tiapp.guid, | |
version: cli.tiapp.version, | |
copyright: cli.tiapp.copyright, | |
date: (new Date()).toDateString() | |
}); | |
next(); | |
}; | |
iOSBuilder.prototype.initialize = function initialize(next) { | |
var argv = this.cli.argv; | |
this.titaniumIosSdkPath = afs.resolvePath(__dirname, '..', '..'); | |
this.titaniumSdkVersion = path.basename(path.join(this.titaniumIosSdkPath, '..')); | |
this.templatesDir = path.join(this.titaniumIosSdkPath, 'templates', 'build'); | |
this.platformName = path.basename(this.titaniumIosSdkPath); // the name of the actual platform directory which will some day be "ios" | |
this.moduleSearchPaths = [ this.projectDir, afs.resolvePath(this.titaniumIosSdkPath, '..', '..', '..', '..') ]; | |
if (this.config.paths && Array.isArray(this.config.paths.modules)) { | |
this.moduleSearchPaths = this.moduleSearchPaths.concat(this.config.paths.modules); | |
} | |
this.provisioningProfileUUID = argv['pp-uuid']; | |
this.buildOnly = argv['build-only']; | |
this.debugHost = this.allowDebugging && argv['debug-host']; | |
this.profilerHost = this.allowProfiling && argv['profiler-host']; | |
this.launchUrl = argv['launch-url']; | |
this.keychain = argv.keychain; | |
this.xcodeTarget = process.env.CONFIGURATION || (/^device|simulator$/.test(this.target) ? 'Debug' : 'Release'); | |
this.iosSimVersion = argv['sim-version']; | |
this.iosSimType = argv['sim-type']; | |
this.deviceFamily = argv['device-family']; | |
this.xcodeTargetOS = (this.target == 'simulator' ? 'iphonesimulator' : 'iphoneos') + version.format(this.iosSdkVersion, 2, 2); | |
this.iosBuildDir = path.join(this.buildDir, 'build', this.xcodeTarget + '-' + (this.target == 'simulator' ? 'iphonesimulator' : 'iphoneos')); | |
this.xcodeAppDir = argv.xcode ? path.join(process.env.TARGET_BUILD_DIR, process.env.CONTENTS_FOLDER_PATH) : path.join(this.iosBuildDir, this.tiapp.name + '.app'); | |
this.xcodeProjectConfigFile = path.join(this.buildDir, 'project.xcconfig'); | |
this.certDeveloperName = argv['developer-name']; | |
this.certDistributionName = argv['distribution-name']; | |
this.forceCopy = !!argv['force-copy']; | |
this.forceCopyAll = !!argv['force-copy-all']; | |
this.forceRebuild = false; | |
this.buildAssetsDir = path.join(this.buildDir, 'assets'); | |
this.buildManifestFile = path.join(this.buildDir, 'build-manifest.json'); | |
// make sure we have an icon | |
if (!this.tiapp.icon || !['Resources', 'Resources/iphone', 'Resources/ios'].some(function (p) { | |
return fs.existsSync(this.projectDir, p, this.tiapp.icon); | |
}, this)) { | |
this.tiapp.icon = 'appicon.png'; | |
} | |
this.architectures = 'armv6 armv7 i386'; | |
// no armv6 support above 4.3 or with 6.0+ SDK | |
if (version.gte(this.iosSdkVersion, '6.0')) { | |
this.architectures = 'armv7 armv7s i386'; | |
} else if (version.gte(this.minIosVer, '4.3')) { | |
this.architectures = 'armv7 i386'; | |
} | |
this.imagesOptimizedFile = path.join(this.buildDir, 'images_optimized'); | |
fs.existsSync(this.imagesOptimizedFile) && fs.unlinkSync(this.imagesOptimizedFile); | |
next(); | |
}; | |
iOSBuilder.prototype.loginfo = function loginfo(next) { | |
this.logger.debug(__('Titanium SDK iOS directory: %s', this.platformPath.cyan)); | |
this.logger.info(__('Deploy type: %s', this.deployType.cyan)); | |
this.logger.info(__('Building for target: %s', this.target.cyan)); | |
this.logger.info(__('Building using iOS SDK: %s', version.format(this.iosSdkVersion, 2).cyan)); | |
if (this.buildOnly) { | |
this.logger.info(__('Performing build only')); | |
} else { | |
if (this.target == 'simulator') { | |
this.logger.info(__('Building for iOS %s Simulator: %s', this.simTypes[this.iosSimType], this.iosSimVersion.cyan)); | |
} else if (this.target == 'device') { | |
this.logger.info(__('Building for device: %s', this.deviceId.cyan)); | |
} | |
} | |
this.logger.info(__('Building for device family: %s', this.deviceFamily.cyan)); | |
this.logger.debug(__('Setting Xcode target to %s', this.xcodeTarget.cyan)); | |
this.logger.debug(__('Setting Xcode build OS to %s', this.xcodeTargetOS.cyan)); | |
this.logger.debug(__('Xcode installation: %s', this.xcodeEnv.path.cyan)); | |
this.logger.debug(__('iOS WWDR certificate: %s', this.iosInfo.certs.wwdr ? __('installed').cyan : __('not found').cyan)); | |
this.logger.debug(__('Building for the following architectures: %s', this.architectures.cyan)); | |
if (this.target == 'device') { | |
this.logger.info(__('iOS Development Certificate: %s', this.certDeveloperName.cyan)); | |
} else if (/^dist-appstore|dist\-adhoc$/.test(this.target)) { | |
this.logger.info(__('iOS Distribution Certificate: %s', this.certDistributionName.cyan)); | |
} | |
// validate the min-ios-ver from the tiapp.xml | |
this.logger.info(__('Minimum iOS version: %s', version.format(this.minIosVer, 2, 3).cyan)); | |
if (/^device|dist\-appstore|dist\-adhoc$/.test(this.target)) { | |
if (this.keychain) { | |
this.logger.info(__('Using keychain: %s', this.keychain)); | |
} else { | |
this.logger.info(__('Using default keychain')); | |
} | |
} | |
if (this.debugHost && this.target != 'dist-appstore') { | |
this.logger.info(__('Debugging enabled via debug host: %s', this.debugHost.cyan)); | |
} else { | |
this.logger.info(__('Debugging disabled')); | |
} | |
if (this.profilerHost && this.target != 'dist-appstore') { | |
this.logger.info(__('Profiler enabled via profiler host: %s', this.profilerHost.cyan)); | |
} else { | |
this.logger.info(__('Profiler disabled')); | |
} | |
next(); | |
}; | |
iOSBuilder.prototype.readBuildManifest = function readBuildManifest(next) { | |
// read the build manifest from the last build, if exists, so we | |
// can determine if we need to do a full rebuild | |
this.buildManifest = {}; | |
if (fs.existsSync(this.buildManifestFile)) { | |
try { | |
this.buildManifest = JSON.parse(fs.readFileSync(this.buildManifestFile)) || {}; | |
} catch (e) {} | |
} | |
next(); | |
}; | |
iOSBuilder.prototype.checkIfShouldForceRebuild = function checkIfShouldForceRebuild() { | |
var manifest = this.buildManifest; | |
if (this.cli.argv.force) { | |
this.logger.info(__('Forcing rebuild: %s flag was set', '--force'.cyan)); | |
return true; | |
} | |
if (!fs.existsSync(this.buildManifestFile)) { | |
// if no .version file, rebuild! | |
this.logger.info(__('Forcing rebuild: %s does not exist', this.buildManifestFile.cyan)); | |
return true; | |
} | |
// check if the target changed | |
if (this.target != manifest.target) { | |
this.logger.info(__('Forcing rebuild: target changed since last build')); | |
this.logger.info(' ' + __('Was: %s', this.buildManifest.target)); | |
this.logger.info(' ' + __('Now: %s', this.target)); | |
return true; | |
} | |
if (fs.existsSync(this.xcodeProjectConfigFile)) { | |
// we have a previous build, see if the Titanium SDK changed | |
var conf = fs.readFileSync(this.xcodeProjectConfigFile).toString(), | |
versionMatch = conf.match(/TI_VERSION\=([^\n]*)/), | |
idMatch = conf.match(/TI_APPID\=([^\n]*)/); | |
if (versionMatch && !appc.version.eq(versionMatch[1], this.titaniumSdkVersion)) { | |
this.logger.info(__("Forcing rebuild: last build was under Titanium SDK version %s and we're compiling for version %s", versionMatch[1].cyan, this.titaniumSdkVersion.cyan)); | |
return true; | |
} | |
if (idMatch && idMatch[1] != this.tiapp.id) { | |
this.logger.info(__("Forcing rebuild: app id changed from %s to %s", idMatch[1].cyan, this.tiapp.id.cyan)); | |
return true; | |
} | |
} | |
if (!fs.existsSync(this.xcodeAppDir)) { | |
this.logger.info(__('Forcing rebuild: %s does not exist', this.xcodeAppDir.cyan)); | |
return true; | |
} | |
// check that we have a libTiCore hash | |
if (!manifest.tiCoreHash) { | |
this.logger.info(__('Forcing rebuild: incomplete version file %s', this.buildVersionFile.cyan)); | |
return true; | |
} | |
// check if the libTiCore hashes are different | |
if (this.libTiCoreHash != manifest.tiCoreHash) { | |
this.logger.info(__('Forcing rebuild: libTiCore hash changed since last build')); | |
this.logger.info(' ' + __('Was: %s', manifest.tiCoreHash)); | |
this.logger.info(' ' + __('Now: %s', this.libTiCoreHash)); | |
return true; | |
} | |
// check if the titanium sdk paths are different | |
if (manifest.iosSdkPath != this.titaniumIosSdkPath) { | |
this.logger.info(__('Forcing rebuild: Titanium SDK path changed since last build')); | |
this.logger.info(' ' + __('Was: %s', manifest.iosSdkPath)); | |
this.logger.info(' ' + __('Now: %s', this.titaniumIosSdkPath)); | |
return true; | |
} | |
// check if the device family has changed (i.e. was universal, now iphone) | |
if (manifest.deviceFamily != this.deviceFamily) { | |
this.logger.info(__('Forcing rebuild: device family changed since last build')); | |
this.logger.info(' ' + __('Was: %s', manifest.deviceFamily)); | |
this.logger.info(' ' + __('Now: %s', this.deviceFamily)); | |
return true; | |
} | |
// check the git hashes are different | |
if (!manifest.gitHash || manifest.gitHash != ti.manifest.githash) { | |
this.logger.info(__('Forcing rebuild: githash changed since last build')); | |
this.logger.info(' ' + __('Was: %s', manifest.gitHash)); | |
this.logger.info(' ' + __('Now: %s', ti.manifest.githash)); | |
return true; | |
} | |
// if encryption is enabled, then we must recompile | |
if (this.encryptJS) { | |
this.logger.info(__('Forcing rebuild: JavaScript files need to be re-encrypted')); | |
return true; | |
} | |
// if encryptJS changed, then we need to recompile | |
if (this.encryptJS != manifest.encryptJS) { | |
this.logger.info(__('Forcing rebuild: JavaScript encryption flag changed')); | |
this.logger.info(' ' + __('Was: %s', manifest.encryptJS)); | |
this.logger.info(' ' + __('Now: %s', this.encryptJS)); | |
return true; | |
} | |
// check if the modules hashes are different | |
if (this.modulesHash != manifest.modulesHash) { | |
this.logger.info(__('Forcing rebuild: modules hash changed since last build')); | |
this.logger.info(' ' + __('Was: %s', manifest.modulesHash)); | |
this.logger.info(' ' + __('Now: %s', this.modulesHash)); | |
return true; | |
} | |
if (this.modulesNativeHash != manifest.modulesNativeHash) { | |
this.logger.info(__('Forcing rebuild: native modules hash changed since last build')); | |
this.logger.info(' ' + __('Was: %s', manifest.modulesNativeHash)); | |
this.logger.info(' ' + __('Now: %s', this.modulesNativeHash)); | |
return true; | |
} | |
// next we check if any tiapp.xml values changed so we know if we need to reconstruct the main.m | |
if (this.tiapp.name != manifest.name) { | |
this.logger.info(__('Forcing rebuild: tiapp.xml project name changed since last build')); | |
this.logger.info(' ' + __('Was: %s', manifest.name)); | |
this.logger.info(' ' + __('Now: %s', this.tiapp.name)); | |
return true; | |
} | |
if (this.tiapp.id != manifest.id) { | |
this.logger.info(__('Forcing rebuild: tiapp.xml app id changed since last build')); | |
this.logger.info(' ' + __('Was: %s', manifest.id)); | |
this.logger.info(' ' + __('Now: %s', this.tiapp.id)); | |
return true; | |
} | |
if (!this.tiapp.analytics != !manifest.analytics) { | |
this.logger.info(__('Forcing rebuild: tiapp.xml analytics flag changed since last build')); | |
this.logger.info(' ' + __('Was: %s', !!manifest.analytics)); | |
this.logger.info(' ' + __('Now: %s', !!this.tiapp.analytics)); | |
return true; | |
} | |
if (this.tiapp.publisher != manifest.publisher) { | |
this.logger.info(__('Forcing rebuild: tiapp.xml publisher changed since last build')); | |
this.logger.info(' ' + __('Was: %s', manifest.publisher)); | |
this.logger.info(' ' + __('Now: %s', this.tiapp.publisher)); | |
return true; | |
} | |
if (this.tiapp.url != manifest.url) { | |
this.logger.info(__('Forcing rebuild: tiapp.xml url changed since last build')); | |
this.logger.info(' ' + __('Was: %s', manifest.url)); | |
this.logger.info(' ' + __('Now: %s', this.tiapp.url)); | |
return true; | |
} | |
if (this.tiapp.version != manifest.version) { | |
this.logger.info(__('Forcing rebuild: tiapp.xml version changed since last build')); | |
this.logger.info(' ' + __('Was: %s', manifest.version)); | |
this.logger.info(' ' + __('Now: %s', this.tiapp.version)); | |
return true; | |
} | |
if (this.tiapp.description != manifest.description) { | |
this.logger.info(__('Forcing rebuild: tiapp.xml description changed since last build')); | |
this.logger.info(' ' + __('Was: %s', manifest.description)); | |
this.logger.info(' ' + __('Now: %s', this.tiapp.description)); | |
return true; | |
} | |
if (this.tiapp.copyright != manifest.copyright) { | |
this.logger.info(__('Forcing rebuild: tiapp.xml copyright changed since last build')); | |
this.logger.info(' ' + __('Was: %s', manifest.copyright)); | |
this.logger.info(' ' + __('Now: %s', this.tiapp.copyright)); | |
return true; | |
} | |
if (this.tiapp.guid != manifest.guid) { | |
this.logger.info(__('Forcing rebuild: tiapp.xml guid changed since last build')); | |
this.logger.info(' ' + __('Was: %s', manifest.guid)); | |
this.logger.info(' ' + __('Now: %s', this.tiapp.guid)); | |
return true; | |
} | |
if (this.forceCopy != manifest.forceCopy) { | |
this.logger.info(__('Forcing rebuild: force copy flag changed since last build')); | |
this.logger.info(' ' + __('Was: %s', manifest.forceCopy)); | |
this.logger.info(' ' + __('Now: %s', this.forceCopy)); | |
return true; | |
} | |
if (this.forceCopyAll != manifest.forceCopyAll) { | |
this.logger.info(__('Forcing rebuild: force copy all flag changed since last build')); | |
this.logger.info(' ' + __('Was: %s', manifest.forceCopyAll)); | |
this.logger.info(' ' + __('Now: %s', this.forceCopyAll)); | |
return true; | |
} | |
return false; | |
}; | |
iOSBuilder.prototype.checkIfNeedToRecompile = function checkIfNeedToRecompile(next) { | |
// determine the libTiCore hash | |
this.libTiCoreHash = hash(fs.readFileSync(path.join(this.titaniumIosSdkPath, 'libTiCore.a'))); | |
// figure out all of the modules currently in use | |
this.modulesHash = hash(this.tiapp.modules ? this.tiapp.modules.filter(function (m) { | |
return !m.platform || /^iphone|ipad|ios|commonjs$/.test(m.platform); | |
}).map(function (m) { | |
return m.id + ',' + m.platform + ',' + m.version; | |
}).join('|') : ''); | |
// check if we need to do a rebuild | |
this.forceRebuild = this.checkIfShouldForceRebuild(); | |
// now that we've read the build manifest, delete it so if this build | |
// becomes incomplete, the next build will be a full rebuild | |
fs.existsSync(this.buildManifestFile) && fs.unlinkSync(this.buildManifestFile); | |
next(); | |
}; | |
iOSBuilder.prototype.copyDirSync = function copyDirSync(src, dest, opts) { | |
afs.copyDirSyncRecursive(src, dest, opts || { | |
preserve: true, | |
logger: this.logger.debug, | |
ignoreDirs: this.ignoreDirs, | |
ignoreFiles: this.ignoreFiles | |
}); | |
}; | |
iOSBuilder.prototype.copyDirAsync = function copyDirAsync(src, dest, callback, opts) { | |
afs.copyDirRecursive(src, dest, callback, opts || { | |
preserve: true, | |
logger: this.logger.debug, | |
ignoreDirs: this.ignoreDirs, | |
ignoreFiles: this.ignoreFiles | |
}); | |
}; | |
iOSBuilder.prototype.preparePhase = function preparePhase(next) { | |
this.logger.info(__('Initiating prepare phase')); | |
// recreate the build directory (<project dir>/build/[iphone|ios]/assets) | |
fs.existsSync(this.buildAssetsDir) && wrench.rmdirSyncRecursive(this.buildAssetsDir); | |
wrench.mkdirSyncRecursive(this.buildAssetsDir); | |
next(); | |
}; | |
iOSBuilder.prototype.initBuildDir = function initBuildDir(next) { | |
if (this.forceRebuild) { | |
var xcodeBuildDir = path.join(this.buildDir, 'build'); | |
if (fs.existsSync(xcodeBuildDir)) { | |
this.logger.info(__('Cleaning old build directory')); | |
// wipe the actual Xcode build dir, not the Titanium build dir | |
wrench.rmdirSyncRecursive(xcodeBuildDir, true); | |
wrench.mkdirSyncRecursive(xcodeBuildDir); | |
} | |
this.logger.info(__('Performing full rebuild')); | |
this.createXcodeProject(); | |
this.populateIosFiles(); | |
} | |
// create the actual .app dir if it doesn't exist | |
wrench.mkdirSyncRecursive(this.xcodeAppDir); | |
wrench.mkdirSyncRecursive(path.join(this.buildDir, 'Classes')); | |
next(); | |
}; | |
iOSBuilder.prototype.createInfoPlist = function createInfoPlist(next) { | |
var src = this.projectDir + '/Info.plist', | |
dest = this.buildDir + '/Info.plist', | |
plist = this.infoPlist = new appc.plist(), | |
iphone = this.tiapp.iphone, | |
ios = this.tiapp.ios, | |
defaultInfoPlist = path.join(this.titaniumIosSdkPath, 'Info.plist'), | |
fbAppId = this.tiapp.properties && this.tiapp.properties['ti.facebook.appid'] && this.tiapp.properties['ti.facebook.appid']['value'], | |
iconName = this.tiapp.icon.replace(/(.+)(\..*)$/, '$1'), // note: this is basically stripping the file extension | |
consts = { | |
'__APPICON__': iconName, | |
'__PROJECT_NAME__': this.tiapp.name, | |
'__PROJECT_ID__': this.tiapp.id, | |
'__URL__': this.tiapp.id, | |
'__URLSCHEME__': this.tiapp.name.replace(/\./g, '_').replace(/ /g, '').toLowerCase(), | |
'__ADDITIONAL_URL_SCHEMES__': fbAppId ? '<string>fb' + fbAppId + '</string>' : '' | |
}; | |
function merge(src, dest) { | |
Object.keys(src).forEach(function (prop) { | |
if (!/^\+/.test(prop)) { | |
if (Object.prototype.toString.call(src[prop]) == '[object Object]') { | |
dest.hasOwnProperty(prop) || (dest[prop] = {}); | |
merge(src[prop], dest[prop]); | |
} else { | |
dest[prop] = src[prop]; | |
} | |
} | |
}); | |
} | |
// default info.plist | |
if (fs.existsSync(defaultInfoPlist)) { | |
plist.parse(fs.readFileSync(defaultInfoPlist).toString().replace(/(__.+__)/g, function (match, key, format) { | |
return consts.hasOwnProperty(key) ? consts[key] : '<!-- ' + key + ' -->'; // if they key is not a match, just comment out the key | |
})); | |
} | |
// if the user has a Info.plist in their project directory, consider that a custom override | |
if (fs.existsSync(src)) { | |
this.logger.info(__('Copying custom Info.plist from project directory')); | |
var custom = new appc.plist().parse(fs.readFileSync(src).toString()); | |
if (custom.CFBundleIdentifier != this.tiapp.id) { | |
this.logger.info(__('Forcing rebuild: custom Info.plist CFBundleIdentifier not equal to tiapp.xml <id>')); | |
this.forceRebuild = true; | |
} | |
merge(custom, plist); | |
} | |
plist.UIRequiresPersistentWiFi = this.tiapp.hasOwnProperty('persistent-wifi') ? !!this.tiapp['persistent-wifi'] : false; | |
plist.UIPrerenderedIcon = this.tiapp.hasOwnProperty('prerendered-icon') ? !!this.tiapp['prerendered-icon'] : false; | |
plist.UIStatusBarHidden = this.tiapp.hasOwnProperty('statusbar-hidden') ? !!this.tiapp['statusbar-hidden'] : false; | |
plist.UIStatusBarStyle = 'UIStatusBarStyleDefault'; | |
if (/opaque_black|opaque|black/.test(this.tiapp['statusbar-style'])) { | |
plist.UIStatusBarStyle = 'UIStatusBarStyleBlackOpaque'; | |
} else if (/translucent_black|transparent|translucent/.test(this.tiapp['statusbar-style'])) { | |
plist.UIStatusBarStyle = 'UIStatusBarStyleBlackTranslucent'; | |
} | |
if (iphone) { | |
if (iphone.orientations) { | |
var orientationsMap = { | |
'PORTRAIT': 'UIInterfaceOrientationPortrait', | |
'UPSIDE_PORTRAIT': 'UIInterfaceOrientationPortraitUpsideDown', | |
'LANDSCAPE_LEFT': 'UIInterfaceOrientationLandscapeLeft', | |
'LANDSCAPE_RIGHT': 'UIInterfaceOrientationLandscapeRight' | |
}; | |
Object.keys(iphone.orientations).forEach(function (key) { | |
var entry = 'UISupportedInterfaceOrientations' + (key == 'ipad' ? '~ipad' : ''); | |
Array.isArray(plist[entry]) || (plist[entry] = []); | |
iphone.orientations[key].forEach(function (name) { | |
var value = orientationsMap[name.split('.').pop().toUpperCase()] || name; | |
// name should be in the format Ti.UI.PORTRAIT, so pop the last part and see if it's in the map | |
if (plist[entry].indexOf(value) == -1) { | |
plist[entry].push(value); | |
} | |
}); | |
}); | |
} | |
if (iphone.backgroundModes) { | |
plist.UIBackgroundModes = (plist.UIBackgroundModes || []).concat(iphone.backgroundModes); | |
} | |
if (iphone.requires) { | |
plist.UIRequiredDeviceCapabilities = (plist.UIRequiredDeviceCapabilities || []).concat(iphone.requiredFeatures); | |
} | |
if (iphone.types) { | |
Array.isArray(plist.CFBundleDocumentTypes) || (plist.CFBundleDocumentTypes = []); | |
iphone.types.forEach(function (type) { | |
var types = plist.CFBundleDocumentTypes, | |
match = false, | |
i = 0; | |
for (; i < types.length; i++) { | |
if (types[i].CFBundleTypeName == type.name) { | |
types[i].CFBundleTypeIconFiles = type.icon; | |
types[i].LSItemContentTypes = type.uti; | |
types[i].LSHandlerRank = type.owner ? 'Owner' : 'Alternate'; | |
match = true; | |
break; | |
} | |
} | |
if (!match) { | |
types.push({ | |
CFBundleTypeName: type.name, | |
CFBundleTypeIconFiles: type.icon, | |
LSItemContentTypes: type.uti, | |
LSHandlerRank: type.owner ? 'Owner' : 'Alternate' | |
}); | |
} | |
}); | |
} | |
} | |
ios && ios.plist && merge(ios.plist, plist); | |
plist.CFBundleIdentifier = this.tiapp.id; | |
// device builds require an additional token to ensure uniquiness so that iTunes will detect an updated app to sync | |
if (this.target == 'device') { | |
plist.CFBundleVersion = appc.version.format(this.tiapp.version, 3, 3) + '.' + (new Date).getTime(); | |
} else { | |
plist.CFBundleVersion = appc.version.format(this.tiapp.version, 3, 3); | |
} | |
plist.CFBundleShortVersionString = plist.CFBundleVersion; | |
Array.isArray(plist.CFBundleIconFiles) || (plist.CFBundleIconFiles = []); | |
['.png', '@2x.png', '-72.png', '-60.png', '-60@2x.png', '-76.png', '-76@2x.png', '-Small-50.png', '-72@2x.png', '-Small-50@2x.png', '-Small.png', '-Small@2x.png', '-Small-40.png', '-Small-40@2x.png'].forEach(function (name) { | |
name = iconName + name; | |
if (fs.existsSync(this.projectDir, 'Resources', name) || | |
fs.existsSync(this.projectDir, 'Resources', 'iphone', name) || | |
fs.existsSync(this.projectDir, 'Resources', this.platformName, name)) { | |
if (plist.CFBundleIconFiles.indexOf(name) == -1) { | |
plist.CFBundleIconFiles.push(name); | |
} | |
} | |
}, this); | |
// scan for ttf and otf font files | |
var fontMap = {}, | |
resourceDir = path.join(this.projectDir, 'Resources'), | |
iphoneDir = path.join(resourceDir, 'iphone'), | |
iosDir = path.join(resourceDir, 'ios'); | |
(plist.UIAppFonts || []).forEach(function (f) { | |
fontMap[f] = 1; | |
}); | |
(function scanFonts(dir, isRoot) { | |
fs.existsSync(dir) && fs.readdirSync(dir).forEach(function (file) { | |
var p = path.join(dir, file); | |
if (fs.statSync(p).isDirectory() && (!isRoot || file == 'iphone' || file == 'ios' || ti.availablePlatformsNames.indexOf(file) == -1)) { | |
scanFonts(p); | |
} else if (/\.(otf|ttf)$/i.test(file)) { | |
fontMap['/' + p.replace(iphoneDir, '').replace(iosDir, '').replace(resourceDir, '').replace(/^\//, '')] = 1; | |
} | |
}); | |
}(resourceDir, true)); | |
var fonts = Object.keys(fontMap); | |
fonts.length && (plist.UIAppFonts = fonts); | |
// write the Info.plist | |
fs.writeFile(dest, plist.toString('xml'), next); | |
}; | |
iOSBuilder.prototype.createEntitlementsPlist = function createEntitlementsPlist(next) { | |
if (/device|dist\-appstore|dist\-adhoc/.test(this.target)) { | |
// allow the project to have its own custom entitlements | |
var entitlementsFile = path.join(this.projectDir, 'Entitlements.plist'), | |
contents = '', | |
pp; | |
if (fs.existsSync(entitlementsFile)) { | |
this.logger.info(__('Found custom entitlements: %s', entitlementsFile)); | |
contents = fs.readFileSync(entitlementsFile).toString(); | |
} else { | |
function getPP(list, uuid) { | |
for (var i = 0, l = list.length; i < l; i++) { | |
if (list[i].uuid == uuid) { | |
return list[i]; | |
} | |
} | |
} | |
var pp; | |
if (this.target == 'device') { | |
pp = getPP(this.iosInfo.provisioningProfiles.development, this.provisioningProfileUUID); | |
} else { | |
pp = getPP(this.iosInfo.provisioningProfiles.distribution, this.provisioningProfileUUID); | |
if (!pp) { | |
pp = getPP(this.iosInfo.provisioningProfiles.adhoc, this.provisioningProfileUUID); | |
} | |
} | |
if (pp) { | |
// attempt to customize it by reading provisioning profile | |
var plist = new appc.plist(); | |
plist['get-task-allow'] = !!pp.getTaskAllow; | |
pp.apsEnvironment && (plist['aps-environment'] = pp.apsEnvironment); | |
plist['application-identifier'] = pp.appPrefix + '.' + this.tiapp.id; | |
plist['keychain-access-groups'] = [ plist['application-identifier'] ]; | |
contents = plist.toString('xml'); | |
} | |
} | |
fs.writeFile(path.join(this.buildDir, 'Entitlements.plist'), contents, next); | |
} else { | |
next(); | |
} | |
}; | |
iOSBuilder.prototype.createXcodeProject = function createXcodeProject() { | |
var xcodeDir = path.join(this.buildDir, this.tiapp.name + '.xcodeproj'), | |
namespace = (function (name) { | |
name = name.replace(/-/g, '_').replace(/\W/g, '') | |
return /^[0-9]/.test(name) ? 'k' + name : name; | |
}(this.tiapp.name)), | |
copyFileRegExps = [ | |
// note: order of regexps matters | |
[/TitaniumViewController/g, namespace + '$ViewController'], | |
[/TitaniumModule/g, namespace + '$Module'], | |
[/Titanium|Appcelerator/g, namespace], | |
[/titanium/g, '_' + namespace.toLowerCase()], | |
[/(org|com)\.appcelerator/g, '$1.' + namespace.toLowerCase()], | |
[new RegExp('\\* ' + namespace + ' ' + namespace + ' Mobile', 'g'), '* Appcelerator Titanium Mobile'], | |
[new RegExp('\\* Copyright \\(c\\) \\d{4}(-\\d{4})? by ' + namespace + ', Inc\\.', 'g'), '* Copyright (c) 2009-' + (new Date).getFullYear() + ' by Appcelerator, Inc.'], | |
[/(\* Please see the LICENSE included with this distribution for details.\n)(?! \*\s*\* WARNING)/g, '$1 * \n * WARNING: This is generated code. Modify at your own risk and without support.\n'] | |
], | |
extRegExp = /\.(c|cpp|h|m|mm|pbxproj)$/, | |
copyOpts = { | |
preserve: true, | |
logger: this.logger.debug, | |
ignoreDirs: this.ignoreDirs, | |
ignoreFiles: /^(bridge\.txt|libTitanium\.a|\.gitignore|\.npmignore|\.cvsignore|\.DS_Store|\._.*|[Tt]humbs.db|\.vspscc|\.vssscc|\.sublime-project|\.sublime-workspace|\.project|\.tmproj)$'/, | |
callback: function (src, dest, contents, logger) { | |
if (extRegExp.test(src) && src.indexOf('TiCore') == -1) { | |
logger && logger(__('Processing %s', src.cyan)); | |
for (var i = 0, l = copyFileRegExps.length; i < l; i++) { | |
contents = contents.toString().replace(copyFileRegExps[i][0], copyFileRegExps[i][1]); | |
} | |
} | |
return contents; | |
} | |
}; | |
this.logger.info(__('Copying Xcode iOS files')); | |
['Classes', 'headers'].forEach(function (dir) { | |
afs.copyDirSyncRecursive( | |
path.join(this.titaniumIosSdkPath, dir), | |
path.join(this.buildDir, dir), | |
copyOpts | |
); | |
}, this); | |
afs.copyFileSync( | |
path.join(this.titaniumIosSdkPath, this.platformName, 'Titanium_Prefix.pch'), | |
path.join(this.buildDir, this.tiapp.name + '_Prefix.pch'), | |
{ | |
logger: this.logger.debug | |
} | |
); | |
this.logger.info(__('Creating Xcode project directory: %s', xcodeDir.cyan)); | |
wrench.mkdirSyncRecursive(xcodeDir); | |
function injectCompileShellScript(str, sectionName, shellScript) { | |
var p = 0; | |
while (p != -1) { | |
p = str.indexOf('name = "' + sectionName + '"', p); | |
if (p != -1) { | |
p = str.indexOf('shellScript = ', p); | |
if (p != -1) { | |
str = str.substring(0, p) + 'shellScript = "' + shellScript + '";' + str.substring(str.indexOf('\n', p)); | |
} | |
} | |
} | |
return str; | |
} | |
this.logger.info(__('Writing Xcode project data file: %s', 'Titanium.xcodeproj/project.pbxproj'.cyan)); | |
var proj = fs.readFileSync(path.join(this.titaniumIosSdkPath, this.platformName, 'Titanium.xcodeproj', 'project.pbxproj')).toString(); | |
proj = proj.replace(/\.\.\/Classes/g, 'Classes') | |
.replace(/\.\.\/Resources/g, 'Resources') | |
.replace(/\.\.\/headers/g, 'headers') | |
.replace(/\.\.\/lib/g, 'lib') | |
.replace(/Titanium\.plist/g, 'Info.plist') | |
.replace(/Titanium\-KitchenSink/g, this.tiapp.name) | |
.replace(/path \= Titanium.app;/g, 'path = "' + this.tiapp.name + '.app";') | |
.replace(/Titanium.app/g, this.tiapp.name + '.app') | |
.replace(/PRODUCT_NAME \= ['"]?Titanium(-iPad|-universal)?['"]?/g, 'PRODUCT_NAME = "' + this.tiapp.name + '$1"') // note: there are no PRODUCT_NAMEs with -iPad and -universal | |
.replace(/path \= Titanium_Prefix\.pch;/g, 'path = "' + this.tiapp.name + '_Prefix.pch";') | |
.replace(/GCC_PREFIX_HEADER \= Titanium_Prefix\.pch;/g, 'GCC_PREFIX_HEADER = "' + this.tiapp.name + '_Prefix.pch";') | |
.replace(/Titanium_Prefix\.pch/g, this.tiapp.name + '_Prefix.pch') | |
.replace(/Titanium/g, namespace); | |
proj = injectCompileShellScript( | |
proj, | |
'Pre-Compile', | |
'if [ \\"x$TITANIUM_CLI_XCODEBUILD\\" == \\"x\\" ]; then\\n' | |
+ ' ' + (process.execPath || 'node') + ' \\"' + this.cli.argv.$0.replace(/^(.+\/)*node /, '') + '\\" build --platform ' + this.platformName + ' --sdk ' + this.titaniumSdkVersion + ' --no-prompt --no-progress-bars --no-banner --no-colors --build-only --xcode\\n' | |
+ ' exit $?\\n' | |
+ 'else\\n' | |
+ ' echo \\"skipping pre-compile phase\\"\\n' | |
+ 'fi' | |
); | |
proj = injectCompileShellScript( | |
proj, | |
'Post-Compile', | |
"echo 'Xcode Post-Compile Phase: Touching important files'\\n" | |
+ 'touch -c Classes/ApplicationRouting.h Classes/ApplicationRouting.m Classes/ApplicationDefaults.m Classes/ApplicationMods.m Classes/defines.h\\n' | |
+ 'if [ \\"x$TITANIUM_CLI_IMAGES_OPTIMIZED\\" != \\"x\\" ]; then\\n' | |
+ ' if [ -f \\"$TITANIUM_CLI_IMAGES_OPTIMIZED\\" ]; then\\n' | |
+ ' echo \\"Xcode Post-Compile Phase: Image optimization finished before xcodebuild finished, continuing\\"\\n' | |
+ ' else\\n' | |
+ ' echo \\"Xcode Post-Compile Phase: Waiting for image optimization to complete\\"\\n' | |
+ ' echo \\"Xcode Post-Compile Phase: $TITANIUM_CLI_IMAGES_OPTIMIZED\\"\\n' | |
+ ' while [ ! -f \\"$TITANIUM_CLI_IMAGES_OPTIMIZED\\" ]\\n' | |
+ ' do\\n' | |
+ ' sleep 1\\n' | |
+ ' done\\n' | |
+ " echo 'Xcode Post-Compile Phase: Image optimization complete, continuing'\\n" | |
+ ' fi\\n' | |
+ 'fi' | |
); | |
fs.writeFileSync(path.join(this.buildDir, this.tiapp.name + '.xcodeproj', 'project.pbxproj'), proj); | |
this.logger.info(__('Writing Xcode project configuration: %s', 'project.xcconfig'.cyan)); | |
fs.writeFileSync(this.xcodeProjectConfigFile, [ | |
'TI_VERSION=' + this.titaniumSdkVersion, | |
'TI_SDK_DIR=' + this.titaniumIosSdkPath.replace(this.titaniumSdkVersion, '$(TI_VERSION)'), | |
'TI_APPID=' + this.tiapp.id, | |
'OTHER_LDFLAGS[sdk=iphoneos*]=$(inherited) -weak_framework iAd', | |
'OTHER_LDFLAGS[sdk=iphonesimulator*]=$(inherited) -weak_framework iAd', | |
'#include "module"' | |
].join('\n') + '\n'); | |
this.logger.info(__('Writing Xcode module configuration: %s', 'module.xcconfig'.cyan)); | |
fs.writeFileSync(path.join(this.buildDir, 'module.xcconfig'), '// this is a generated file - DO NOT EDIT\n\n'); | |
}; | |
iOSBuilder.prototype.injectApplicationDefaults = function injectApplicationDefaults(next) { | |
var file = path.join(this.buildDir, 'Classes', 'ApplicationDefaults.m'), | |
exists = fs.existsSync(file), | |
contents = ejs.render(fs.readFileSync(path.join(this.templatesDir, 'ApplicationDefaults.m')).toString(), { | |
props: this.tiapp.properties || {}, | |
deployType: this.deployType, | |
launchUrl: this.launchUrl | |
}); | |
if (!exists || fs.readFileSync(file).toString() != contents) { | |
if (!exists) { | |
this.logger.info(__('Forcing rebuild: ApplicationDefaults.m does not exist')); | |
} else { | |
this.logger.info(__('Forcing rebuild: ApplicationDefaults.m has changed since last build')); | |
} | |
this.forceRebuild = true; | |
this.logger.info(__('Writing application defaults: %s', file.cyan)); | |
fs.writeFile(file, contents, next); | |
} else { | |
next(); | |
} | |
}; | |
iOSBuilder.prototype.copyItunesArtwork = function copyItunesArtwork(next) { | |
// note: iTunesArtwork is a png image WITHOUT the file extension and the | |
// purpose of this function is to copy it from the root of the project. | |
// The preferred location of this file is <project-dir>/Resources/iphone | |
// or <project-dir>/platform/iphone. | |
if (/device|dist\-appstore|dist\-adhoc/.test(this.target)) { | |
this.logger.info(__('Copying iTunes artwork')); | |
fs.readdirSync(this.projectDir).forEach(function (file) { | |
var src = path.join(this.projectDir, file), | |
m = file.match(/^iTunesArtwork(@2x)?$/i); | |
if (m && fs.statSync(src).isFile()) { | |
afs.copyFileSync(src, path.join(this.xcodeAppDir, 'iTunesArtwork' + (m[1] ? m[1].toLowerCase() : '')), { | |
logger: this.logger.debug | |
}); | |
} | |
}, this); | |
} | |
next(); | |
}; | |
iOSBuilder.prototype.copyGraphics = function copyGraphics(next) { | |
var paths = [ | |
path.join(this.projectDir, 'Resources', 'iphone'), | |
path.join(this.projectDir, 'Resources', 'ios'), | |
path.join(this.titaniumIosSdkPath, 'resources') | |
], | |
len = paths.length, | |
i, src; | |
for (i = 0; i < len; i++) { | |
if (fs.existsSync(src = path.join(paths[i], this.tiapp.icon))) { | |
afs.copyFileSync(src, this.xcodeAppDir, { | |
logger: this.logger.debug | |
}); | |
break; | |
} | |
} | |
next(); | |
}; | |
iOSBuilder.prototype.writeBuildManifest = function writeBuildManifest(next) { | |
this.cli.createHook('build.ios.writeBuildManifest', this, function (manifest, cb) { | |
fs.existsSync(this.buildDir) || wrench.mkdirSyncRecursive(this.buildDir); | |
fs.existsSync(this.buildManifestFile) && fs.unlinkSync(this.buildManifestFile); | |
fs.writeFile(this.buildManifestFile, JSON.stringify(this.buildManifest = manifest, null, '\t'), function () { | |
cb(); | |
}); | |
})({ | |
target: this.target, | |
deployType: this.deployType, | |
deviceFamily: this.deviceFamily, | |
developerName: this.certDeveloperName, | |
distributionName: this.certDistributionName, | |
iosSdkPath: this.titaniumIosSdkPath, | |
tiCoreHash: this.libTiCoreHash, | |
modulesHash: this.modulesHash, | |
modulesNativeHash: this.modulesNativeHash, | |
gitHash: ti.manifest.githash, | |
outputDir: this.cli.argv['output-dir'], | |
name: this.tiapp.name, | |
id: this.tiapp.id, | |
analytics: this.tiapp.analytics, | |
publisher: this.tiapp.publisher, | |
url: this.tiapp.url, | |
version: this.tiapp.version, | |
description: this.tiapp.description, | |
copyright: this.tiapp.copyright, | |
guid: this.tiapp.guid, | |
skipJSMinification: !!this.cli.argv['skip-js-minify'], | |
forceCopy: !!this.forceCopy, | |
forceCopyAll: !!this.forceCopyAll, | |
encryptJS: !!this.encryptJS | |
}, function (err, results, result) { | |
next(); | |
}); | |
}; | |
iOSBuilder.prototype.compileI18NFiles = function compileI18NFiles(next) { | |
var data = ti.i18n.load(this.projectDir, this.logger); | |
parallel(this, | |
Object.keys(data).map(function (lang) { | |
return function (done) { | |
var contents = [ | |
'/**', | |
' * Appcelerator Titanium', | |
' * this is a generated file - DO NOT EDIT', | |
' */', | |
'' | |
], | |
dir = path.join(this.xcodeAppDir, lang + '.lproj'), | |
tasks = []; | |
wrench.mkdirSyncRecursive(dir); | |
function add(obj, filename, map) { | |
obj && tasks.push(function (next) { | |
var dest = path.join(dir, filename); | |
fs.writeFileSync(dest, contents.concat(Object.keys(obj).map(function (name) { | |
return '"' + (map && map[name] || name).replace(/\\"/g, '"').replace(/"/g, '\\"') + | |
'" = "' + (''+obj[name]).replace(/%s/g, '%@').replace(/\\"/g, '"').replace(/"/g, '\\"') + '";'; | |
})).join('\n')); | |
if (this.compileI18N) { | |
appc.subprocess.run('/usr/bin/plutil', ['-convert', 'binary1', dest], function (code, out, err) { | |
next(); | |
}); | |
} else { | |
next(); | |
} | |
}); | |
} | |
add(data[lang].app, 'InfoPlist.strings', { appname: 'CFBundleDisplayName' }); | |
add(data[lang].strings, 'Localizable.strings'); | |
parallel(this, tasks, done); | |
}; | |
}, this), | |
next | |
); | |
}; | |
iOSBuilder.prototype.copyLocalizedSplashScreens = function copyLocalizedSplashScreens(next) { | |
ti.i18n.splashScreens(this.projectDir, this.logger).forEach(function (splashImage) { | |
var token = splashImage.split('/'), | |
file = token.pop(), | |
lang = token.pop(), | |
lprojDir = path.join(this.xcodeAppDir, lang + '.lproj'), | |
globalFile = path.join(this.xcodeAppDir, file); | |
// this would never need to run. But just to be safe | |
if (!fs.existsSync(lprojDir)) { | |
this.logger.debug(__('Creating lproj folder %s', lprojDir.cyan)); | |
wrench.mkdirSyncRecursive(lprojDir); | |
} | |
// check for it in the root of the xcode build folder | |
if (fs.existsSync(globalFile)) { | |
this.logger.debug(__('Removing File %s, as it is being localized', globalFile.cyan)); | |
fs.unlinkSync(globalFile); | |
} | |
afs.copyFileSync(splashImage, lprojDir, { | |
logger: this.logger.debug | |
}); | |
}, this); | |
next(); | |
}; | |
iOSBuilder.prototype.injectModulesIntoXcodeProject = function injectModulesIntoXcodeProject(next) { | |
if (!this.nativeLibModules.length) { | |
return next(); | |
} | |
var projectFile = path.join(this.buildDir, this.tiapp.name + '.xcodeproj', 'project.pbxproj'), | |
projectOrigContents = fs.readFileSync(projectFile).toString(), | |
projectContents = projectOrigContents; | |
targetLibs = []; | |
this.nativeLibModules.forEach(function (lib) { | |
projectContents.indexOf(lib.libName) == -1 && targetLibs.push(lib); | |
}, this); | |
if (targetLibs.length) { | |
// we have some libraries to add to the project file | |
this.logger.info(__('Injecting native libraries into Xcode project file')); | |
var fileMarkers = [], | |
fileMarkers2FileRefs = {}, | |
refMarkers = [], | |
frameworkMarkers = [], | |
groupMarkers = [], | |
groupUUID; | |
function makeUUID() { | |
return uuid.v4().toUpperCase().replace(/-/g, '').substring(0, 24); | |
} | |
projectContents.split('\n').forEach(function (line) { | |
line.indexOf('/* libTiCore.a */;') != -1 && fileMarkers.push(line); | |
line.indexOf('/* libTiCore.a */ =') != -1 && refMarkers.push(line); | |
line.indexOf('/* libTiCore.a in Frameworks */,') != -1 && frameworkMarkers.push(line); | |
line.indexOf('/* libTiCore.a */,') != -1 && groupMarkers.push(line); | |
}); | |
fileMarkers.forEach(function (marker) { | |
var m = marker.match(/([0-9a-zA-Z]+) \/\*/); | |
if (m) { | |
fileMarkers2FileRefs[m[1].trim()] = makeUUID(); | |
!groupUUID && (m = marker.match(/fileRef \= ([0-9a-zA-Z]+) /)) && (groupUUID = m[1]); | |
} | |
}); | |
targetLibs.forEach(function (lib) { | |
var newGroupUUID = makeUUID(); | |
fileMarkers.forEach(function (marker) { | |
var begin = projectContents.indexOf(marker), | |
end = begin + marker.length, | |
m = marker.match(/([0-9a-zA-Z]+) \/\*/), | |
newUUID = makeUUID(), | |
line = projectContents | |
.substring(begin, end) | |
.replace(/libTiCore\.a/g, lib.libName) | |
.replace(new RegExp(groupUUID, 'g'), newGroupUUID) | |
.replace(new RegExp(m[1].trim(), 'g'), newUUID); | |
fileMarkers2FileRefs[m[1].trim()] = newUUID; | |
projectContents = projectContents.substring(0, end) + '\n' + line + '\n' + projectContents.substring(end + 1); | |
}); | |
refMarkers.forEach(function (marker) { | |
var begin = projectContents.indexOf(marker), | |
end = begin + marker.length, | |
m = marker.match(/([0-9a-zA-Z]+) \/\*/), | |
line = projectContents | |
.substring(begin, end) | |
.replace(/lib\/libTiCore\.a/g, '"' + lib.libFile.replace(/"/g, '\\"') + '"') | |
.replace(/libTiCore\.a/g, lib.libName) | |
.replace(/SOURCE_ROOT/g, '"<absolute>"') | |
.replace(new RegExp(m[1].trim(), 'g'), newGroupUUID); | |
projectContents = projectContents.substring(0, end) + '\n' + line + '\n' + projectContents.substring(end + 1); | |
}); | |
groupMarkers.forEach(function (marker) { | |
var begin = projectContents.indexOf(marker), | |
end = begin + marker.length, | |
line = projectContents | |
.substring(begin, end) | |
.replace(/libTiCore\.a/g, lib.libName) | |
.replace(new RegExp(groupUUID, 'g'), newGroupUUID); | |
projectContents = projectContents.substring(0, end) + '\n' + line + '\n' + projectContents.substring(end + 1); | |
}); | |
frameworkMarkers.forEach(function (marker) { | |
var begin = projectContents.indexOf(marker), | |
end = begin + marker.length, | |
m = marker.match(/([0-9a-zA-Z]+) \/\*/), | |
line = projectContents | |
.substring(begin, end) | |
.replace(/libTiCore\.a/g, lib.libName) | |
.replace(new RegExp(m[1].trim(), 'g'), fileMarkers2FileRefs[m[1].trim()]); | |
projectContents = projectContents.substring(0, end) + '\n' + line + '\n' + projectContents.substring(end + 1); | |
}); | |
(function (libPath) { | |
var begin = projectContents.indexOf(libPath), | |
end, line; | |
while (begin != -1) { | |
end = begin + libPath.length; | |
line = projectContents.substring(begin, end).replace(libPath, '"\\"' + path.dirname(lib.libFile) + '\\"",'); | |
projectContents = projectContents.substring(0, end) + '\n ' + line + '\n' + projectContents.substring(end + 1); | |
begin = projectContents.indexOf(libPath, end + line.length); | |
} | |
}('"\\"$(SRCROOT)/lib\\"",')); | |
}, this); | |
if (projectContents != projectOrigContents) { | |
this.logger.debug(__('Writing %s', projectFile.cyan)); | |
fs.writeFileSync(projectFile, projectContents); | |
} | |
} | |
next(); | |
}; | |
iOSBuilder.prototype.populateIosFiles = function populateIosFiles(next) { | |
var consts = { | |
'__PROJECT_NAME__': this.tiapp.name, | |
'__PROJECT_ID__': this.tiapp.id, | |
'__DEPLOYTYPE__': this.deployType, | |
'__APP_ID__': this.tiapp.id, | |
'__APP_ANALYTICS__': '' + (this.tiapp.hasOwnProperty('analytics') ? !!this.tiapp.analytics : true), | |
'__APP_PUBLISHER__': this.tiapp.publisher, | |
'__APP_URL__': this.tiapp.url, | |
'__APP_NAME__': this.tiapp.name, | |
'__APP_VERSION__': this.tiapp.version, | |
'__APP_DESCRIPTION__': this.tiapp.description, | |
'__APP_COPYRIGHT__': this.tiapp.copyright, | |
'__APP_GUID__': this.tiapp.guid, | |
'__APP_RESOURCE_DIR__': '' | |
}, | |
dest, | |
variables = {}, | |
mainContents = fs.readFileSync(path.join(this.titaniumIosSdkPath, 'main.m')).toString().replace(/(__.+?__)/g, function (match, key, format) { | |
var s = consts.hasOwnProperty(key) ? consts[key] : key; | |
return typeof s == 'string' ? s.replace(/"/g, '\\"').replace(/\n/g, '\\n') : s; | |
}), | |
xcconfigContents = [ | |
'// this is a generated file - DO NOT EDIT', | |
'' | |
]; | |
dest = path.join(this.buildDir, 'main.m'); | |
if (!fs.existsSync(dest) || fs.readFileSync(dest).toString() != mainContents) { | |
this.logger.debug(__('Writing %s', dest.cyan)); | |
fs.writeFileSync(dest, mainContents); | |
} | |
if (this.modules.length) { | |
// add the modules to the xcconfig file | |
this.modules.forEach(function (m) { | |
var moduleId = m.manifest.moduleid.toLowerCase(), | |
moduleName = m.manifest.name.toLowerCase(), | |
prefix = m.manifest.moduleid.toUpperCase().replace(/\./g, '_'); | |
[ path.join(m.modulePath, 'module.xcconfig'), | |
path.join(this.projectDir, 'modules', 'iphone', moduleName + '.xcconfig') | |
].forEach(function (file) { | |
if (fs.existsSync(file)) { | |
var xc = new appc.xcconfig(file); | |
Object.keys(xc).forEach(function (key) { | |
var name = (prefix + '_' + key).replace(/[^\w]/g, '_'); | |
variables[key] || (variables[key] = []); | |
variables[key].push(name); | |
xcconfigContents.push((name + '=' + xc[key]).replace(new RegExp('\$\(' + key + '\)', 'g'), '$(' + name + ')')); | |
}); | |
} | |
}); | |
}, this); | |
// write the ApplicationMods.m file | |
var applicationModsContents = ejs.render(fs.readFileSync(path.join(this.templatesDir, 'ApplicationMods.m')).toString(), { | |
modules: this.modules | |
}), | |
applicationModsFile = path.join(this.buildDir, 'Classes', 'ApplicationMods.m'); | |
if (!fs.existsSync(applicationModsFile) || fs.readFileSync(applicationModsFile).toString() != applicationModsContents) { | |
this.logger.debug(__('Writing application modules source file: %s', applicationModsFile.cyan)); | |
fs.writeFileSync(applicationModsFile, applicationModsContents); | |
} else { | |
this.logger.debug(__('Application modules source file already up-to-date: %s', applicationModsFile.cyan)); | |
} | |
} | |
// write the module.xcconfig file | |
Object.keys(variables).forEach(function (v) { | |
xcconfigContents.push(v + '=$(inherited) ' + variables[v].map(function (x) { return '$(' + x + ') '; }).join('')); | |
}); | |
xcconfigContents = xcconfigContents.join('\n'); | |
dest = path.join(this.buildDir, 'module.xcconfig'); | |
if (!fs.existsSync(dest) || fs.readFileSync(dest).toString() != xcconfigContents) { | |
this.logger.debug(__('Writing module xcconfig file: %s', dest.cyan)); | |
fs.writeFileSync(dest, xcconfigContents); | |
} else { | |
this.logger.debug(__('Module xccconfig file already up-to-date: %s', dest.cyan)); | |
} | |
}; | |
iOSBuilder.prototype.copyTitaniumLibraries = function copyTitaniumLibraries(next) { | |
// check to see if the symlink exists and that it points to the right version of the library | |
var dir = path.join(this.buildDir, 'lib'), | |
dest; | |
wrench.mkdirSyncRecursive(dir); | |
dest = path.join(dir, 'libTiCore.a'); | |
if (this.cli.argv['force-copy-all']) { | |
fs.existsSync(dest) || afs.copyFileSync(path.join(this.titaniumIosSdkPath, 'libTiCore.a'), dest, { logger: this.logger.debug }); | |
} else { | |
if (!fs.existsSync(dest) || !fs.lstatSync(dest).isSymbolicLink() || fs.readlinkSync(dest).indexOf(this.titaniumSdkVersion) == -1) { | |
try { | |
fs.unlinkSync(dest); | |
} catch (e) {} | |
fs.symlinkSync(path.join(this.titaniumIosSdkPath, 'libTiCore.a'), dest); | |
} | |
} | |
dest = path.join(dir, 'libtiverify.a'); | |
fs.existsSync(dest) || afs.copyFileSync(path.join(this.titaniumIosSdkPath, 'libtiverify.a'), dest, { logger: this.logger.debug }); | |
dest = path.join(dir, 'libti_ios_debugger.a'); | |
fs.existsSync(dest) || afs.copyFileSync(path.join(this.titaniumIosSdkPath, 'libti_ios_debugger.a'), dest, { logger: this.logger.debug }); | |
dest = path.join(dir, 'libti_ios_profiler.a'); | |
fs.existsSync(dest) || afs.copyFileSync(path.join(this.titaniumIosSdkPath, 'libti_ios_profiler.a'), dest, { logger: this.logger.debug }); | |
next(); | |
}; | |
iOSBuilder.prototype.compileJSSFiles = function compileJSSFiles(next) { | |
ti.jss.load(path.join(this.projectDir, 'Resources'), this.deviceFamilyNames[this.deviceFamily], this.logger, function (results) { | |
var appStylesheet = path.join(this.xcodeAppDir, 'stylesheet.plist'), | |
plist = new appc.plist(); | |
appc.util.mix(plist, results); | |
fs.writeFile(appStylesheet, plist.toString('xml'), function () { | |
if (this.compileJSS) { | |
// compile plist into binary format so it's faster to load, we can be slow on simulator | |
appc.subprocess.run('/usr/bin/plutil', ['-convert', 'binary1', appStylesheet], function (code, out, err) { | |
next(); | |
}); | |
} else { | |
next(); | |
} | |
}.bind(this)); | |
}.bind(this)); | |
}; | |
iOSBuilder.prototype.invokeXcodeBuild = function invokeXcodeBuild(next) { | |
this.logger.info(__('Invoking xcodebuild')); | |
var xcodeArgs = [ | |
'-target', this.tiapp.name + this.xcodeTargetSuffixes[this.deviceFamily], | |
'-configuration', this.xcodeTarget, | |
'-sdk', this.xcodeTargetOS, | |
'IPHONEOS_DEPLOYMENT_TARGET=' + appc.version.format(this.minIosVer, 2), | |
'TARGETED_DEVICE_FAMILY=' + this.deviceFamilies[this.deviceFamily], | |
'VALID_ARCHS=' + this.architectures | |
], | |
gccDefs = [ 'DEPLOYTYPE=' + this.deployType ]; | |
if (this.target == 'simulator') { | |
gccDefs.push('__LOG__ID__=' + this.tiapp.guid); | |
gccDefs.push('DEBUG=1'); | |
gccDefs.push('TI_VERSION=' + this.titaniumSdkVersion); | |
} | |
if (/simulator|device|dist\-adhoc/.test(this.target)) { | |
this.tiapp.ios && this.tiapp.ios.enablecoverage && gccDefs.push('KROLL_COVERAGE=1'); | |
} | |
xcodeArgs.push('GCC_PREPROCESSOR_DEFINITIONS=' + gccDefs.join(' ')); | |
if (/device|dist\-appstore|dist\-adhoc/.test(this.target)) { | |
xcodeArgs.push('PROVISIONING_PROFILE=' + this.provisioningProfileUUID); | |
xcodeArgs.push('DEPLOYMENT_POSTPROCESSING=YES'); | |
if (this.keychain) { | |
xcodeArgs.push('OTHER_CODE_SIGN_FLAGS=--keychain ' + this.keychain); | |
} | |
xcodeArgs.push('CODE_SIGN_ENTITLEMENTS=Entitlements.plist'); | |
} | |
if (this.target == 'device') { | |
xcodeArgs.push('CODE_SIGN_IDENTITY=iPhone Developer: ' + this.certDeveloperName); | |
} | |
if (/dist-appstore|dist\-adhoc/.test(this.target)) { | |
xcodeArgs.push('CODE_SIGN_IDENTITY=iPhone Distribution: ' + this.certDistributionName); | |
} | |
var xcodebuildHook = this.cli.createHook('build.ios.xcodebuild', this, function (exe, args, opts, done) { | |
var p = spawn(exe, args, opts), | |
out = [], | |
err = [], | |
stopOutputting = false; | |
p.stdout.on('data', function (data) { | |
data.toString().split('\n').forEach(function (line) { | |
if (line.length) { | |
out.push(line); | |
if (line.indexOf('Failed to minify') != -1) { | |
stopOutputting = true; | |
} | |
if (!stopOutputting) { | |
this.logger.trace(line); | |
} | |
} | |
}, this); | |
}.bind(this)); | |
p.stderr.on('data', function (data) { | |
data.toString().split('\n').forEach(function (line) { | |
if (line.length) { | |
err.push(line); | |
} | |
}, this); | |
}.bind(this)); | |
p.on('close', function (code, signal) { | |
if (code) { | |
// first see if we errored due to a dependency issue | |
if (err.join('\n').indexOf('Check dependencies') != -1) { | |
var len = out.length; | |
for (var i = len - 1; i >= 0; i--) { | |
if (out[i].indexOf('Check dependencies') != -1) { | |
if (out[out.length - 1].indexOf('Command /bin/sh failed with exit code') != -1) { | |
len--; | |
} | |
for (var j = i + 1; j < len; j++) { | |
this.logger.error(__('Error details: %s', out[j])); | |
} | |
this.logger.log(); | |
process.exit(1); | |
} | |
} | |
} | |
// next see if it was a minification issue | |
var len = out.length; | |
for (var i = len - 1, k = 0; i >= 0 && k < 10; i--, k++) { | |
if (out[i].indexOf('Failed to minify') != -1) { | |
if (out[out.length - 1].indexOf('Command /bin/sh failed with exit code') != -1) { | |
len--; | |
} | |
while (i < len) { | |
this.logger.log(out[i++]); | |
} | |
this.logger.log(); | |
process.exit(1); | |
} | |
} | |
// just print the entire error buffer | |
err.forEach(function (line) { | |
this.logger.error(line); | |
}, this); | |
this.logger.log(); | |
process.exit(1); | |
} | |
// end of the line | |
done(code); | |
}.bind(this)); | |
}); | |
xcodebuildHook( | |
this.xcodeEnv.xcodebuild, | |
xcodeArgs, | |
{ | |
cwd: this.buildDir, | |
env: { | |
DEVELOPER_DIR: this.xcodeEnv.path, | |
TMPDIR: process.env.TMPDIR, | |
HOME: process.env.HOME, | |
PATH: process.env.PATH, | |
TITANIUM_CLI_XCODEBUILD: 'Enjoy hacking? http://jobs.appcelerator.com/', | |
TITANIUM_CLI_IMAGES_OPTIMIZED: this.target == 'simulator' ? '' : this.imagesOptimizedFile | |
} | |
}, | |
next | |
); | |
}; | |
iOSBuilder.prototype.xcodePrecompilePhase = function xcodePrecompilePhase(finished) { | |
this.logger.info(__('Initiating Xcode pre-compile phase')); | |
series(this, [ | |
'copyResources', | |
'processTiSymbols', | |
'writeDebugProfilePlists', | |
'compileJSSFiles', | |
'compileI18NFiles', | |
'copyLocalizedSplashScreens', | |
function (next) { | |
// if not production and running from Xcode | |
if (this.deployType != 'production') { | |
var appDefaultsFile = path.join(this.buildDir, 'Classes', 'ApplicationDefaults.m'); | |
fs.writeFileSync(appDefaultsFile, fs.readFileSync(appDefaultsFile).toString().replace(/return \[NSDictionary dictionaryWithObjectsAndKeys\:\[TiUtils stringValue\:@".+"\], @"application-launch-url", nil];/, 'return nil;')); | |
} | |
next(); | |
} | |
], function () { | |
finished(); | |
}); | |
}; | |
iOSBuilder.prototype.writeDebugProfilePlists = function writeDebugProfilePlists(next) { | |
function processPlist(filename, host) { | |
var dest = path.join(this.xcodeAppDir, filename), | |
parts = (host || '').split(':'); | |
fs.writeFileSync(dest, ejs.render(fs.readFileSync(path.join(this.templatesDir, filename)).toString(), { | |
host: parts.length > 0 ? parts[0] : '', | |
port: parts.length > 1 ? parts[1] : '', | |
airkey: parts.length > 2 ? parts[2] : '', | |
hosts: parts.length > 3 ? parts[3] : '' | |
})); | |
} | |
processPlist.call(this, 'debugger.plist', this.debugHost); | |
processPlist.call(this, 'profiler.plist', this.profilerHost); | |
next(); | |
}; | |
iOSBuilder.prototype.copyResources = function copyResources(finished) { | |
var ignoreDirs = this.ignoreDirs, | |
ignoreFiles = this.ignoreFiles, | |
extRegExp = /\.(\w+)$/, | |
icon = (this.tiapp.icon || 'appicon.png').match(/^(.*)\.(.+)$/), | |
unsymlinkableFileRegExp = new RegExp("^Default.*\.png|.+\.(otf|ttf)|iTunesArtwork" + (icon ? '|' + icon[1].replace(/\./g, '\\.') + '.*\\.' + icon[2] : '') + "$"), | |
jsFiles = {}, | |
jsFilesToEncrypt = this.jsFilesToEncrypt = [], | |
htmlJsFiles = this.htmlJsFiles = {}, | |
symlinkFiles = this.target == 'simulator' && this.config.get('ios.symlinkResources', true) && !this.forceCopy && !this.forceCopyAll; | |
//symlinkResources(path.join(this.projectDir, 'platform', 'ios'), this.xcodeAppDir, false); | |
//symlinkResources(path.join(this.projectDir, 'platform', 'iphone'), this.xcodeAppDir, false); | |
function copyDir(opts, callback) { | |
if (opts && opts.src && fs.existsSync(opts.src) && opts.dest) { | |
opts.origSrc = opts.src; | |
opts.origDest = opts.dest; | |
recursivelyCopy.call(this, opts.src, opts.dest, opts.ignoreRootDirs, opts, callback); | |
} else { | |
callback(); | |
} | |
} | |
function copyFile(from, to, next) { | |
var d = path.dirname(to); | |
fs.existsSync(d) || wrench.mkdirSyncRecursive(d); | |
if (symlinkFiles && !unsymlinkableFileRegExp.test(path.basename(to))) { | |
fs.existsSync(to) && fs.unlinkSync(to); | |
this.logger.debug(__('Symlinking %s => %s', from.cyan, to.cyan)); | |
if (next) { | |
fs.symlink(from, to, next); | |
} else { | |
fs.symlinkSync(from, to); | |
} | |
} else { | |
this.logger.debug(__('Copying %s => %s', from.cyan, to.cyan)); | |
if (next) { | |
fs.readFile(from, function (err, data) { | |
if (err) throw err; | |
fs.writeFile(to, data, next); | |
}); | |
} else { | |
fs.writeFileSync(to, fs.readFileSync(from)); | |
} | |
} | |
} | |
function recursivelyCopy(src, dest, ignoreRootDirs, opts, done) { | |
var files; | |
if (fs.statSync(src).isDirectory()) { | |
files = fs.readdirSync(src); | |
} else { | |
// we have a file, so fake a directory listing | |
files = [ path.basename(src) ]; | |
src = path.dirname(src); | |
} | |
series(this, files.map(function (filename) { | |
return function (next) { | |
var from = path.join(src, filename), | |
to = path.join(dest, filename); | |
// check that the file actually exists and isn't a broken symlink | |
if (!fs.existsSync(from)) return next(); | |
var isDir = fs.statSync(from).isDirectory(); | |
// check if we are ignoring this file | |
if ((isDir && ignoreRootDirs && ignoreRootDirs.indexOf(filename) != -1) || (isDir ? ignoreDirs : ignoreFiles).test(filename)) { | |
this.logger.debug(__('Ignoring %s', from.cyan)); | |
return next(); | |
} | |
// if this is a directory, recurse | |
if (isDir) return recursivelyCopy.call(this, from, path.join(dest, filename), null, opts, next); | |
// we have a file, now we need to see what sort of file | |
// if the destination directory does not exists, create it | |
fs.existsSync(dest) || wrench.mkdirSyncRecursive(dest); | |
var ext = filename.match(extRegExp), | |
relPath = to.replace(opts.origDest, '').replace(/^\//, ''); | |
switch (ext && ext[1]) { | |
case 'css': | |
// if we encounter a css file, check if we should minify it | |
if (this.minifyCSS) { | |
this.logger.debug(__('Copying and minifying %s => %s', from.cyan, to.cyan)); | |
fs.readFile(from, function (err, data) { | |
if (err) throw err; | |
fs.writeFile(to, cleanCSS.process(data.toString()), next); | |
}); | |
} else { | |
copyFile.call(this, from, to, next); | |
} | |
break; | |
case 'html': | |
// find all js files referenced in this html file | |
var relPath = from.replace(opts.origSrc, '').replace(/\\/g, '/').replace(/^\//, '').split('/'); | |
relPath.pop(); // remove the filename | |
relPath = relPath.join('/'); | |
jsanalyze.analyzeHtmlFile(from, relPath).forEach(function (file) { | |
htmlJsFiles[file] = 1; | |
}); | |
copyFile.call(this, from, to, next); | |
break; | |
case 'js': | |
// track each js file so we can copy/minify later | |
// we use the destination file name minus the path to the assets dir as the id | |
// which will eliminate dupes | |
var id = to.replace(opts.origDest, '').replace(/^\//, ''); | |
if (!jsFiles[relPath] || !opts || !opts.onJsConflict || opts.onJsConflict(from, to, relPath)) { | |
jsFiles[relPath] = from; | |
} | |
next(); | |
break; | |
case 'jss': | |
// ignore, these will be compiled later by compileJSS() | |
next(); | |
break; | |
default: | |
// if the device family is iphone, then don't copy iPad specific images | |
if (this.deviceFamily != 'iphone' || this.ipadSplashImages.indexOf(relPath) == -1) { | |
// normal file, just copy it into the build/iphone/bin/assets directory | |
this.cli.createHook('build.ios.copyResource', this, function (from, to, cb) { | |
copyFile.call(this, from, to, cb); | |
})(from, to, next); | |
} else { | |
next(); | |
} | |
} | |
}; | |
}), done); | |
} | |
var tasks = [ | |
// first task is to copy all files in the Resources directory, but ignore | |
// any directory that is the name of a known platform | |
function (cb) { | |
copyDir.call(this, { | |
src: path.join(this.projectDir, 'Resources'), | |
dest: this.xcodeAppDir, | |
ignoreRootDirs: ti.availablePlatformsNames | |
}, cb); | |
}, | |
// next copy all files from the iOS specific Resources directory | |
function (cb) { | |
copyDir.call(this, { | |
src: path.join(this.projectDir, 'Resources', 'iphone'), | |
dest: this.xcodeAppDir | |
}, cb); | |
}, | |
function (cb) { | |
copyDir.call(this, { | |
src: path.join(this.projectDir, 'Resources', 'ios'), | |
dest: this.xcodeAppDir | |
}, cb); | |
}, | |
// last let's move that Settings.bundle into the xcodeAppDir, where it belongs. | |
//symlinkResources(path.join(this.projectDir, 'platform', 'iphone'), this.xcodeAppDir, false); | |
function (cb) { | |
copyDir.call(this, { | |
src: path.join(this.projectDir, 'platform', 'iphone'), | |
dest: this.xcodeAppDir | |
}, cb); | |
}, | |
]; | |
// copy all commonjs modules | |
this.commonJsModules.forEach(function (module) { | |
// copy the main module | |
tasks.push(function (cb) { | |
copyDir.call(this, { | |
src: module.libFile, | |
dest: this.xcodeAppDir, | |
onJsConflict: function (src, dest, id) { | |
this.logger.error(__('There is a project resource "%s" that conflicts with a CommonJS module', id)); | |
this.logger.error(__('Please rename the file, then rebuild') + '\n'); | |
process.exit(1); | |
}.bind(this) | |
}, cb); | |
}); | |
}); | |
// copy all module assets | |
this.modules.forEach(function (module) { | |
// copy the assets | |
tasks.push(function (cb) { | |
copyDir.call(this, { | |
src: path.join(module.modulePath, 'assets'), | |
dest: path.join(this.xcodeAppDir, 'modules', module.id.toLowerCase()) | |
}, cb); | |
}); | |
}); | |
var platformPaths = [ | |
path.join(this.projectDir, 'platform', 'iphone'), | |
path.join(this.projectDir, 'platform', 'ios') | |
]; | |
// WARNING! This is pretty dangerous, but yes, we're intentionally copying | |
// every file from platform/iphone|ios and all modules into the build dir | |
this.modules.forEach(function (module) { | |
platformPaths.push( | |
path.join(module.modulePath, 'platform', 'iphone'), | |
path.join(module.modulePath, 'platform', 'ios') | |
); | |
}); | |
platformPaths.forEach(function (dir) { | |
if (fs.existsSync(dir)) { | |
tasks.push(function (cb) { | |
copyDir.call(this, { | |
src: dir, | |
dest: this.buildDir | |
}, cb); | |
}); | |
} | |
}, this); | |
series(this, tasks, function (err, results) { | |
// copy js files into assets directory and minify if needed | |
this.logger.info(__('Processing JavaScript files')); | |
series(this, Object.keys(jsFiles).map(function (id) { | |
return function (done) { | |
var from = jsFiles[id], | |
to = path.join(this.xcodeAppDir, id); | |
if (htmlJsFiles[id]) { | |
// this js file is referenced from an html file, so don't minify or encrypt | |
return copyFile.call(this, from, to, done); | |
} | |
// we have a js file that may be minified or encrypted | |
id = id.replace(/\./g, '_'); | |
// if we're encrypting the JavaScript, copy the files to the assets dir | |
// for processing later | |
if (this.encryptJS) { | |
to = path.join(this.buildAssetsDir, id); | |
jsFilesToEncrypt.push(id); | |
} | |
// if we're not minifying the JavaScript and we're not forcing all | |
// Titanium modules to be included, then parse the AST and detect | |
// all Titanium symbols | |
if (this.minifyJS || !this.includeAllTiModules) { | |
var r = jsanalyze.analyzeJsFile(from, { minify: this.minifyJS }); | |
// we want to sort by the "to" filename so that we correctly handle file overwriting | |
this.tiSymbols[to] = r.symbols; | |
this.logger.debug(__('Copying and minifying %s => %s', from.cyan, to.cyan)); | |
this.cli.createHook('build.ios.compileJsFile', this, function (r, from, to, cb) { | |
var dir = path.dirname(to); | |
fs.existsSync(dir) || wrench.mkdirSyncRecursive(dir); | |
fs.writeFile(to, r.contents, cb); | |
})(r, from, to, done); | |
} else { | |
// no need to parse the AST, so just copy the file | |
this.cli.createHook('build.ios.copyResource', this, function (from, to, cb) { | |
copyFile.call(this, from, to, cb); | |
})(from, to, done); | |
} | |
}; | |
}), function () { | |
// write the properties file | |
var appPropsFile = this.encryptJS ? path.join(this.buildAssetsDir, '_app_props__json') : path.join(this.xcodeAppDir, '_app_props_.json'), | |
props = {}; | |
this.tiapp.properties && Object.keys(this.tiapp.properties).forEach(function (prop) { | |
props[prop] = this.tiapp.properties[prop].value; | |
}, this); | |
fs.writeFileSync( | |
appPropsFile, | |
JSON.stringify(props) | |
); | |
this.encryptJS && jsFilesToEncrypt.push('_app_props__json'); | |
if (!jsFilesToEncrypt.length) { | |
// nothing to encrypt, continue | |
return finished(); | |
} | |
this.cli.fireHook('build.ios.prerouting', this, function (err) { | |
var titaniumPrepHook = this.cli.createHook('build.ios.titaniumprep', this, function (exe, args, opts, done) { | |
var tries = 0, | |
completed = false; | |
this.logger.info('Encrypting JavaScript files: %s', (exe + ' "' + args.join('" "') + '"').cyan); | |
jsFilesToEncrypt.forEach(function (file) { | |
this.logger.debug(__('Preparing %s', file.cyan)); | |
}, this); | |
async.whilst( | |
function () { | |
if (tries > 3) { | |
// we failed 3 times, so just give up | |
this.logger.error(__('titanium_prep failed to complete successfully')); | |
this.logger.error(__('Try cleaning this project and build again') + '\n'); | |
process.exit(1); | |
} | |
return !completed; | |
}, | |
function (cb) { | |
var child = spawn(exe, args, opts), | |
out = ''; | |
child.stdin.write(jsFilesToEncrypt.join('\n')); | |
child.stdin.end(); | |
child.stdout.on('data', function (data) { | |
out += data.toString(); | |
}); | |
child.on('close', function (code) { | |
if (code) { | |
this.logger.error(__('titanium_prep failed to run (%s)', code) + '\n'); | |
process.exit(1); | |
} | |
if (out.indexOf('initWithObjectsAndKeys') != -1) { | |
// success! | |
var file = path.join(this.buildDir, 'Classes', 'ApplicationRouting.m'); | |
this.logger.debug(__('Writing application routing source file: %s', file.cyan)); | |
fs.writeFileSync( | |
file, | |
ejs.render(fs.readFileSync(path.join(this.templatesDir, 'ApplicationRouting.m')).toString(), { | |
bytes: out | |
}) | |
); | |
completed = true; | |
} else { | |
// failure, maybe it was a fluke, try again | |
this.logger.warn(__('titanium_prep failed to complete successfully, trying again')); | |
tries++; | |
} | |
cb(); | |
}.bind(this)); | |
}.bind(this), | |
done | |
); | |
}); | |
titaniumPrepHook( | |
path.join(this.titaniumIosSdkPath, 'titanium_prep'), | |
[this.tiapp.id, this.buildAssetsDir], | |
{}, | |
finished | |
); | |
}.bind(this)); | |
}); | |
}); | |
}; | |
iOSBuilder.prototype.processTiSymbols = function processTiSymbols(finished) { | |
// if we're including all titanium modules, then there's no point writing the defines.h | |
if (this.includeAllTiModules) { | |
return finished(); | |
} | |
var namespaces = { | |
'analytics': 1, | |
'api': 1, | |
'network': 1, | |
'platform': 1, | |
'ui': 1 | |
}, | |
symbols = {}; | |
// generate the default symbols | |
Object.keys(namespaces).forEach(function (ns) { | |
symbols[ns.toUpperCase()] = 1; | |
}); | |
function addSymbol(symbol) { | |
var parts = symbol.replace(/^(Ti|Titanium)./, '').split('.'); | |
if (parts.length) { | |
namespaces[parts[0].toLowerCase()] = 1; | |
while (parts.length) { | |
symbols[parts.join('.').replace(/\.create/gi, '').replace(/\./g, '').replace(/\-/g, '_').toUpperCase()] = 1; | |
parts.pop(); | |
} | |
} | |
} | |
// add the symbols we found | |
Object.keys(this.tiSymbols).forEach(function (file) { | |
this.tiSymbols[file].forEach(addSymbol); | |
}, this); | |
// for each module, if it has a metadata.json file, add its symbols | |
this.modules.forEach(function (m) { | |
var file = path.join(m.modulePath, 'metadata.json'); | |
if (fs.existsSync(file)) { | |
try { | |
var metadata = JSON.parse(fs.readFileSync(file)); | |
if (metadata && typeof metadata == 'object' && Array.isArray(metadata.exports)) { | |
metadata.exports.forEach(addSymbol); | |
} | |
} catch (e) {} | |
} | |
}); | |
// for each Titanium namespace, copy any resources | |
this.logger.info(__('Processing Titanium namespace resources')); | |
Object.keys(namespaces).forEach(function (ns) { | |
var src = path.join(this.titaniumIosSdkPath, 'modules', ns, 'images'); | |
if (fs.existsSync(src)) { | |
this.copyDirSync(src, path.join(this.xcodeAppDir, 'modules', ns, 'images')); | |
} | |
}, this); | |
// if we're doing a simulator build, return now since we don't care about writing the defines.h | |
if (this.target == 'simulator') { | |
return finished(); | |
} | |
// build the defines.h file | |
var dest = path.join(this.buildDir, 'Classes', 'defines.h'), | |
contents = [ | |
'// Warning: this is generated file. Do not modify!', | |
'', | |
'#define TI_VERSION ' + this.titaniumSdkVersion | |
]; | |
contents = contents.concat(Object.keys(symbols).sort().map(function (s) { | |
return '#define USE_TI_' + s; | |
})); | |
if (Array.isArray(this.infoPlist.UIBackgroundModes) && this.infoPlist.UIBackgroundModes.indexOf('remote-notification') != -1) { | |
contents.push('#define USE_TI_SILENTPUSH'); | |
} | |
if (Array.isArray(this.infoPlist.UIBackgroundModes) && this.infoPlist.UIBackgroundModes.indexOf('fetch') != -1) { | |
contents.push('#define USE_TI_FETCH'); | |
} | |
contents.push( | |
'#ifdef USE_TI_UILISTVIEW', | |
'#define USE_TI_UILABEL', | |
'#define USE_TI_UIBUTTON', | |
'#define USE_TI_UIIMAGEVIEW', | |
'#define USE_TI_UIPROGRESSBAR', | |
'#define USE_TI_UIACTIVITYINDICATOR', | |
'#define USE_TI_UISWITCH', | |
'#define USE_TI_UISLIDER', | |
'#define USE_TI_UITEXTFIELD', | |
'#define USE_TI_UITEXTAREA', | |
'#endif' | |
); | |
contents = contents.join('\n'); | |
if (!fs.existsSync(dest) || fs.readFileSync(dest).toString() != contents) { | |
this.logger.debug(__('Writing Titanium symbol file: %s', dest.cyan)); | |
fs.writeFileSync(dest, contents); | |
} else { | |
this.logger.debug(__('Titanium symbol file already up-to-date: %s', dest.cyan)); | |
} | |
finished(); | |
}; | |
iOSBuilder.prototype.optimizeImages = function optimizeImages(next) { | |
// if we're doing a simulator build, return now since we don't care about optimizing images | |
if (this.target == 'simulator') { | |
return next(); | |
} | |
var tool = path.join(this.xcodeEnv.path, 'Platforms', 'iPhoneOS.platform', 'Developer', 'usr', 'bin', 'iphoneos-optimize'); | |
if (fs.existsSync(tool)) { | |
this.logger.info(__('Optimizing all images in %s', this.xcodeAppDir.cyan)); | |
appc.subprocess.run(tool, this.xcodeAppDir, function (code, out, err) { | |
// remove empty directories | |
this.logger.debug(__('Removing empty directories')); | |
appc.subprocess.run('find', ['.', '-type', 'd', '-empty', '-delete'], { | |
cwd: this.xcodeAppDir | |
}, function (code, out, err) { | |
this.logger.info(__('Image optimization complete')); | |
afs.touch(this.imagesOptimizedFile); | |
next(); | |
}.bind(this)); | |
}.bind(this)); | |
} else { | |
this.logger.warn(__('Unable to find iphoneos-optimize, skipping image optimization')); | |
afs.touch(this.imagesOptimizedFile); | |
next(); | |
} | |
}; | |
// create the builder instance and expose the public api | |
(function (iosBuilder) { | |
exports.config = iosBuilder.config.bind(iosBuilder); | |
exports.validate = iosBuilder.validate.bind(iosBuilder); | |
exports.run = iosBuilder.run.bind(iosBuilder); | |
}(new iOSBuilder(module))); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment