var util = require('util'), f = util.format, EventEmitter = require('events').EventEmitter, path = require('path'), uuid = require('uuid'), fork = require('child_process').fork, pbxWriter = require('./pbxWriter'), pbxFile = require('./pbxFile'), fs = require('fs'), parser = require('./parser/pbxproj'), plist = require('simple-plist'), COMMENT_KEY = /_comment$/ function pbxProject(filename) { if (!(this instanceof pbxProject)) return new pbxProject(filename); this.filepath = path.resolve(filename) } util.inherits(pbxProject, EventEmitter) pbxProject.prototype.parse = function(cb) { var worker = fork(__dirname + '/parseJob.js', [this.filepath]) worker.on('message', function(msg) { if (msg.name == 'SyntaxError' || msg.code) { this.emit('error', msg); } else { this.hash = msg; this.emit('end', null, msg) } }.bind(this)); if (cb) { this.on('error', cb); this.on('end', cb); } return this; } pbxProject.prototype.parseSync = function() { var file_contents = fs.readFileSync(this.filepath, 'utf-8'); this.hash = parser.parse(file_contents); return this; } pbxProject.prototype.writeSync = function() { this.writer = new pbxWriter(this.hash); return this.writer.writeSync(); } pbxProject.prototype.allUuids = function() { var sections = this.hash.project.objects, uuids = [], section; for (key in sections) { section = sections[key] uuids = uuids.concat(Object.keys(section)) } uuids = uuids.filter(function(str) { return !COMMENT_KEY.test(str) && str.length == 24; }); return uuids; } pbxProject.prototype.generateUuid = function() { var id = uuid.v4() .replace(/-/g, '') .substr(0, 24) .toUpperCase() if (this.allUuids().indexOf(id) >= 0) { return this.generateUuid(); } else { return id; } } pbxProject.prototype.addPluginFile = function(path, opt) { var file = new pbxFile(path, opt); file.plugin = true; // durr correctForPluginsPath(file, this); // null is better for early errors if (this.hasFile(file.path)) return null; file.fileRef = this.generateUuid(); this.addToPbxFileReferenceSection(file); // PBXFileReference this.addToPluginsPbxGroup(file); // PBXGroup return file; } pbxProject.prototype.removePluginFile = function(path, opt) { var file = new pbxFile(path, opt); correctForPluginsPath(file, this); this.removeFromPbxFileReferenceSection(file); // PBXFileReference this.removeFromPluginsPbxGroup(file); // PBXGroup return file; } pbxProject.prototype.addProductFile = function(targetPath, opt) { var file = new pbxFile(targetPath, opt); file.includeInIndex = 0; file.fileRef = this.generateUuid(); file.target = opt ? opt.target : undefined; file.group = opt ? opt.group : undefined; file.uuid = this.generateUuid(); file.path = file.basename; this.addToPbxFileReferenceSection(file); this.addToProductsPbxGroup(file); // PBXGroup return file; } pbxProject.prototype.removeProductFile = function(path, opt) { var file = new pbxFile(path, opt); this.removeFromProductsPbxGroup(file); // PBXGroup return file; } /** * * @param path {String} * @param opt {Object} see pbxFile for avail options * @param group {String} group key * @returns {Object} file; see pbxFile */ pbxProject.prototype.addSourceFile = function (path, opt, group) { var file; if (group) { file = this.addFile(path, group, opt); } else { file = this.addPluginFile(path, opt); } if (!file) return false; file.target = opt ? opt.target : undefined; file.uuid = this.generateUuid(); this.addToPbxBuildFileSection(file); // PBXBuildFile this.addToPbxSourcesBuildPhase(file); // PBXSourcesBuildPhase return file; } /** * * @param path {String} * @param opt {Object} see pbxFile for avail options * @param group {String} group key * @returns {Object} file; see pbxFile */ pbxProject.prototype.removeSourceFile = function (path, opt, group) { var file; if (group) { file = this.removeFile(path, group, opt); } else { file = this.removePluginFile(path, opt); } file.target = opt ? opt.target : undefined; this.removeFromPbxBuildFileSection(file); // PBXBuildFile this.removeFromPbxSourcesBuildPhase(file); // PBXSourcesBuildPhase return file; } /** * * @param path {String} * @param opt {Object} see pbxFile for avail options * @param group {String} group key * @returns {Object} file; see pbxFile */ pbxProject.prototype.addHeaderFile = function (path, opt, group) { if (group) { return this.addFile(path, group, opt); } else { return this.addPluginFile(path, opt); } } /** * * @param path {String} * @param opt {Object} see pbxFile for avail options * @param group {String} group key * @returns {Object} file; see pbxFile */ pbxProject.prototype.removeHeaderFile = function (path, opt, group) { if (group) { return this.removeFile(path, group, opt); } else { return this.removePluginFile(path, opt); } } /** * * @param path {String} * @param opt {Object} see pbxFile for avail options * @param group {String} group key * @returns {Object} file; see pbxFile */ pbxProject.prototype.addResourceFile = function(path, opt, group) { opt = opt || {}; var file; if (opt.plugin) { file = this.addPluginFile(path, opt); if (!file) return false; } else { file = new pbxFile(path, opt); if (this.hasFile(file.path)) return false; } file.uuid = this.generateUuid(); file.target = opt ? opt.target : undefined; if (!opt.plugin) { correctForResourcesPath(file, this); file.fileRef = this.generateUuid(); } if (!opt.variantGroup) { this.addToPbxBuildFileSection(file); // PBXBuildFile this.addToPbxResourcesBuildPhase(file); // PBXResourcesBuildPhase } if (!opt.plugin) { this.addToPbxFileReferenceSection(file); // PBXFileReference if (group) { if (this.getPBXGroupByKey(group)) { this.addToPbxGroup(file, group); //Group other than Resources (i.e. 'splash') } else if (this.getPBXVariantGroupByKey(group)) { this.addToPbxVariantGroup(file, group); // PBXVariantGroup } } else { this.addToResourcesPbxGroup(file); // PBXGroup } } return file; } /** * * @param path {String} * @param opt {Object} see pbxFile for avail options * @param group {String} group key * @returns {Object} file; see pbxFile */ pbxProject.prototype.removeResourceFile = function(path, opt, group) { var file = new pbxFile(path, opt); file.target = opt ? opt.target : undefined; correctForResourcesPath(file, this); this.removeFromPbxBuildFileSection(file); // PBXBuildFile this.removeFromPbxFileReferenceSection(file); // PBXFileReference if (group) { if (this.getPBXGroupByKey(group)) { this.removeFromPbxGroup(file, group); //Group other than Resources (i.e. 'splash') } else if (this.getPBXVariantGroupByKey(group)) { this.removeFromPbxVariantGroup(file, group); // PBXVariantGroup } } else { this.removeFromResourcesPbxGroup(file); // PBXGroup } this.removeFromPbxResourcesBuildPhase(file); // PBXResourcesBuildPhase return file; } pbxProject.prototype.addFramework = function(fpath, opt) { var customFramework = opt && opt.customFramework == true; var link = !opt || (opt.link == undefined || opt.link); //defaults to true if not specified var embed = opt && opt.embed; //defaults to false if not specified if (opt) { delete opt.embed; } var file = new pbxFile(fpath, opt); file.uuid = this.generateUuid(); file.fileRef = this.generateUuid(); file.target = opt ? opt.target : undefined; if (this.hasFile(file.path)) return false; this.addToPbxBuildFileSection(file); // PBXBuildFile this.addToPbxFileReferenceSection(file); // PBXFileReference this.addToFrameworksPbxGroup(file); // PBXGroup if (link) { this.addToPbxFrameworksBuildPhase(file); // PBXFrameworksBuildPhase } if (customFramework) { this.addToFrameworkSearchPaths(file); if (embed) { opt.embed = embed; var embeddedFile = new pbxFile(fpath, opt); embeddedFile.uuid = this.generateUuid(); embeddedFile.fileRef = file.fileRef; //keeping a separate PBXBuildFile entry for Embed Frameworks this.addToPbxBuildFileSection(embeddedFile); // PBXBuildFile this.addToPbxEmbedFrameworksBuildPhase(embeddedFile); // PBXCopyFilesBuildPhase return embeddedFile; } } return file; } pbxProject.prototype.removeFramework = function(fpath, opt) { var embed = opt && opt.embed; if (opt) { delete opt.embed; } var file = new pbxFile(fpath, opt); file.target = opt ? opt.target : undefined; this.removeFromPbxBuildFileSection(file); // PBXBuildFile this.removeFromPbxFileReferenceSection(file); // PBXFileReference this.removeFromFrameworksPbxGroup(file); // PBXGroup this.removeFromPbxFrameworksBuildPhase(file); // PBXFrameworksBuildPhase if (opt && opt.customFramework) { this.removeFromFrameworkSearchPaths(file); } opt = opt || {}; opt.embed = true; var embeddedFile = new pbxFile(fpath, opt); embeddedFile.fileRef = file.fileRef; this.removeFromPbxBuildFileSection(embeddedFile); // PBXBuildFile this.removeFromPbxEmbedFrameworksBuildPhase(embeddedFile); // PBXCopyFilesBuildPhase return file; } pbxProject.prototype.addCopyfile = function(fpath, opt) { var file = new pbxFile(fpath, opt); // catch duplicates if (this.hasFile(file.path)) { file = this.hasFile(file.path); } file.fileRef = file.uuid = this.generateUuid(); file.target = opt ? opt.target : undefined; this.addToPbxBuildFileSection(file); // PBXBuildFile this.addToPbxFileReferenceSection(file); // PBXFileReference this.addToPbxCopyfilesBuildPhase(file); // PBXCopyFilesBuildPhase return file; } pbxProject.prototype.pbxCopyfilesBuildPhaseObj = function(target) { return this.buildPhaseObject('PBXCopyFilesBuildPhase', 'Copy Files', target); } pbxProject.prototype.addToPbxCopyfilesBuildPhase = function(file) { var sources = this.buildPhaseObject('PBXCopyFilesBuildPhase', 'Copy Files', file.target); sources.files.push(pbxBuildPhaseObj(file)); } pbxProject.prototype.removeCopyfile = function(fpath, opt) { var file = new pbxFile(fpath, opt); file.target = opt ? opt.target : undefined; this.removeFromPbxBuildFileSection(file); // PBXBuildFile this.removeFromPbxFileReferenceSection(file); // PBXFileReference this.removeFromPbxCopyfilesBuildPhase(file); // PBXFrameworksBuildPhase return file; } pbxProject.prototype.removeFromPbxCopyfilesBuildPhase = function(file) { var sources = this.pbxCopyfilesBuildPhaseObj(file.target); for (i in sources.files) { if (sources.files[i].comment == longComment(file)) { sources.files.splice(i, 1); break; } } } pbxProject.prototype.addStaticLibrary = function(path, opt) { opt = opt || {}; var file; if (opt.plugin) { file = this.addPluginFile(path, opt); if (!file) return false; } else { file = new pbxFile(path, opt); if (this.hasFile(file.path)) return false; } file.uuid = this.generateUuid(); file.target = opt ? opt.target : undefined; if (!opt.plugin) { file.fileRef = this.generateUuid(); this.addToPbxFileReferenceSection(file); // PBXFileReference } this.addToPbxBuildFileSection(file); // PBXBuildFile this.addToPbxFrameworksBuildPhase(file); // PBXFrameworksBuildPhase this.addToLibrarySearchPaths(file); // make sure it gets built! return file; } // helper addition functions pbxProject.prototype.addToPbxBuildFileSection = function(file) { var commentKey = f("%s_comment", file.uuid); this.pbxBuildFileSection()[file.uuid] = pbxBuildFileObj(file); this.pbxBuildFileSection()[commentKey] = pbxBuildFileComment(file); } pbxProject.prototype.removeFromPbxBuildFileSection = function(file) { var uuid; for (uuid in this.pbxBuildFileSection()) { if (this.pbxBuildFileSection()[uuid].fileRef_comment == file.basename) { file.uuid = uuid; delete this.pbxBuildFileSection()[uuid]; } } var commentKey = f("%s_comment", file.uuid); delete this.pbxBuildFileSection()[commentKey]; } pbxProject.prototype.addPbxGroup = function(filePathsArray, name, path, sourceTree) { var groups = this.hash.project.objects['PBXGroup'], pbxGroupUuid = this.generateUuid(), commentKey = f("%s_comment", pbxGroupUuid), pbxGroup = { isa: 'PBXGroup', children: [], name: name, path: path, sourceTree: sourceTree ? sourceTree : '""' }, fileReferenceSection = this.pbxFileReferenceSection(), filePathToReference = {}; for (var key in fileReferenceSection) { // only look for comments if (!COMMENT_KEY.test(key)) continue; var fileReferenceKey = key.split(COMMENT_KEY)[0], fileReference = fileReferenceSection[fileReferenceKey]; filePathToReference[fileReference.path] = { fileRef: fileReferenceKey, basename: fileReferenceSection[key] }; } for (var index = 0; index < filePathsArray.length; index++) { var filePath = filePathsArray[index], filePathQuoted = "\"" + filePath + "\""; if (filePathToReference[filePath]) { pbxGroup.children.push(pbxGroupChild(filePathToReference[filePath])); continue; } else if (filePathToReference[filePathQuoted]) { pbxGroup.children.push(pbxGroupChild(filePathToReference[filePathQuoted])); continue; } var file = new pbxFile(filePath); file.uuid = this.generateUuid(); file.fileRef = this.generateUuid(); this.addToPbxFileReferenceSection(file); // PBXFileReference this.addToPbxBuildFileSection(file); // PBXBuildFile pbxGroup.children.push(pbxGroupChild(file)); } if (groups) { groups[pbxGroupUuid] = pbxGroup; groups[commentKey] = name; } return { uuid: pbxGroupUuid, pbxGroup: pbxGroup }; } pbxProject.prototype.removePbxGroup = function (groupName) { var section = this.hash.project.objects['PBXGroup'], key, itemKey; for (key in section) { // only look for comments if (!COMMENT_KEY.test(key)) continue; if (section[key] == groupName) { itemKey = key.split(COMMENT_KEY)[0]; delete section[itemKey]; } } } pbxProject.prototype.addToPbxProjectSection = function(target) { var newTarget = { value: target.uuid, comment: pbxNativeTargetComment(target.pbxNativeTarget) }; this.pbxProjectSection()[this.getFirstProject()['uuid']]['targets'].push(newTarget); } pbxProject.prototype.addToPbxNativeTargetSection = function(target) { var commentKey = f("%s_comment", target.uuid); this.pbxNativeTargetSection()[target.uuid] = target.pbxNativeTarget; this.pbxNativeTargetSection()[commentKey] = target.pbxNativeTarget.name; } pbxProject.prototype.addToPbxFileReferenceSection = function(file) { var commentKey = f("%s_comment", file.fileRef); this.pbxFileReferenceSection()[file.fileRef] = pbxFileReferenceObj(file); this.pbxFileReferenceSection()[commentKey] = pbxFileReferenceComment(file); } pbxProject.prototype.removeFromPbxFileReferenceSection = function(file) { var i; var refObj = pbxFileReferenceObj(file); for (i in this.pbxFileReferenceSection()) { if (this.pbxFileReferenceSection()[i].name == refObj.name || ('"' + this.pbxFileReferenceSection()[i].name + '"') == refObj.name || this.pbxFileReferenceSection()[i].path == refObj.path || ('"' + this.pbxFileReferenceSection()[i].path + '"') == refObj.path) { file.fileRef = file.uuid = i; delete this.pbxFileReferenceSection()[i]; break; } } var commentKey = f("%s_comment", file.fileRef); if (this.pbxFileReferenceSection()[commentKey] != undefined) { delete this.pbxFileReferenceSection()[commentKey]; } return file; } pbxProject.prototype.addToXcVersionGroupSection = function(file) { if (!file.models || !file.currentModel) { throw new Error("Cannot create a XCVersionGroup section from not a data model document file"); } var commentKey = f("%s_comment", file.fileRef); if (!this.xcVersionGroupSection()[file.fileRef]) { this.xcVersionGroupSection()[file.fileRef] = { isa: 'XCVersionGroup', children: file.models.map(function (el) { return el.fileRef; }), currentVersion: file.currentModel.fileRef, name: path.basename(file.path), path: file.path, sourceTree: '""', versionGroupType: 'wrapper.xcdatamodel' }; this.xcVersionGroupSection()[commentKey] = path.basename(file.path); } } pbxProject.prototype.addToPluginsPbxGroup = function(file) { var pluginsGroup = this.pbxGroupByName('Plugins'); pluginsGroup.children.push(pbxGroupChild(file)); } pbxProject.prototype.removeFromPluginsPbxGroup = function(file) { var pluginsGroupChildren = this.pbxGroupByName('Plugins').children, i; for (i in pluginsGroupChildren) { if (pbxGroupChild(file).value == pluginsGroupChildren[i].value && pbxGroupChild(file).comment == pluginsGroupChildren[i].comment) { pluginsGroupChildren.splice(i, 1); break; } } } pbxProject.prototype.addToResourcesPbxGroup = function(file) { var pluginsGroup = this.pbxGroupByName('Resources'); pluginsGroup.children.push(pbxGroupChild(file)); } pbxProject.prototype.removeFromResourcesPbxGroup = function(file) { var pluginsGroupChildren = this.pbxGroupByName('Resources').children, i; for (i in pluginsGroupChildren) { if (pbxGroupChild(file).value == pluginsGroupChildren[i].value && pbxGroupChild(file).comment == pluginsGroupChildren[i].comment) { pluginsGroupChildren.splice(i, 1); break; } } } pbxProject.prototype.addToFrameworksPbxGroup = function(file) { var pluginsGroup = this.pbxGroupByName('Frameworks'); pluginsGroup.children.push(pbxGroupChild(file)); } pbxProject.prototype.removeFromFrameworksPbxGroup = function(file) { var pluginsGroupChildren = this.pbxGroupByName('Frameworks').children; for (i in pluginsGroupChildren) { if (pbxGroupChild(file).value == pluginsGroupChildren[i].value && pbxGroupChild(file).comment == pluginsGroupChildren[i].comment) { pluginsGroupChildren.splice(i, 1); break; } } } pbxProject.prototype.addToPbxEmbedFrameworksBuildPhase = function (file) { var sources = this.pbxEmbedFrameworksBuildPhaseObj(file.target); if (sources) { sources.files.push(pbxBuildPhaseObj(file)); } } pbxProject.prototype.removeFromPbxEmbedFrameworksBuildPhase = function (file) { var sources = this.pbxEmbedFrameworksBuildPhaseObj(file.target); if (sources) { var files = []; for (i in sources.files) { if (sources.files[i].comment != longComment(file)) { files.push(sources.files[i]); } } sources.files = files; } } pbxProject.prototype.addToProductsPbxGroup = function(file) { var productsGroup = this.pbxGroupByName('Products'); productsGroup.children.push(pbxGroupChild(file)); } pbxProject.prototype.removeFromProductsPbxGroup = function(file) { var productsGroupChildren = this.pbxGroupByName('Products').children, i; for (i in productsGroupChildren) { if (pbxGroupChild(file).value == productsGroupChildren[i].value && pbxGroupChild(file).comment == productsGroupChildren[i].comment) { productsGroupChildren.splice(i, 1); break; } } } pbxProject.prototype.addToPbxSourcesBuildPhase = function(file) { var sources = this.pbxSourcesBuildPhaseObj(file.target); sources.files.push(pbxBuildPhaseObj(file)); } pbxProject.prototype.removeFromPbxSourcesBuildPhase = function(file) { var sources = this.pbxSourcesBuildPhaseObj(file.target), i; for (i in sources.files) { if (sources.files[i].comment == longComment(file)) { sources.files.splice(i, 1); break; } } } pbxProject.prototype.addToPbxResourcesBuildPhase = function(file) { var sources = this.pbxResourcesBuildPhaseObj(file.target); sources.files.push(pbxBuildPhaseObj(file)); } pbxProject.prototype.removeFromPbxResourcesBuildPhase = function(file) { var sources = this.pbxResourcesBuildPhaseObj(file.target), i; for (i in sources.files) { if (sources.files[i].comment == longComment(file)) { sources.files.splice(i, 1); break; } } } pbxProject.prototype.addToPbxFrameworksBuildPhase = function(file) { var sources = this.pbxFrameworksBuildPhaseObj(file.target); sources.files.push(pbxBuildPhaseObj(file)); } pbxProject.prototype.removeFromPbxFrameworksBuildPhase = function(file) { var sources = this.pbxFrameworksBuildPhaseObj(file.target); for (i in sources.files) { if (sources.files[i].comment == longComment(file)) { sources.files.splice(i, 1); break; } } } pbxProject.prototype.addXCConfigurationList = function(configurationObjectsArray, defaultConfigurationName, comment) { var pbxBuildConfigurationSection = this.pbxXCBuildConfigurationSection(), pbxXCConfigurationListSection = this.pbxXCConfigurationList(), xcConfigurationListUuid = this.generateUuid(), commentKey = f("%s_comment", xcConfigurationListUuid), xcConfigurationList = { isa: 'XCConfigurationList', buildConfigurations: [], defaultConfigurationIsVisible: 0, defaultConfigurationName: defaultConfigurationName }; for (var index = 0; index < configurationObjectsArray.length; index++) { var configuration = configurationObjectsArray[index], configurationUuid = this.generateUuid(), configurationCommentKey = f("%s_comment", configurationUuid); pbxBuildConfigurationSection[configurationUuid] = configuration; pbxBuildConfigurationSection[configurationCommentKey] = configuration.name; xcConfigurationList.buildConfigurations.push({ value: configurationUuid, comment: configuration.name }); } if (pbxXCConfigurationListSection) { pbxXCConfigurationListSection[xcConfigurationListUuid] = xcConfigurationList; pbxXCConfigurationListSection[commentKey] = comment; } return { uuid: xcConfigurationListUuid, xcConfigurationList: xcConfigurationList }; } pbxProject.prototype.addTargetDependency = function(target, dependencyTargets) { if (!target) return undefined; var nativeTargets = this.pbxNativeTargetSection(); if (typeof nativeTargets[target] == "undefined") throw new Error("Invalid target: " + target); for (var index = 0; index < dependencyTargets.length; index++) { var dependencyTarget = dependencyTargets[index]; if (typeof nativeTargets[dependencyTarget] == "undefined") throw new Error("Invalid target: " + dependencyTarget); } var pbxTargetDependency = 'PBXTargetDependency', pbxContainerItemProxy = 'PBXContainerItemProxy', pbxTargetDependencySection = this.hash.project.objects[pbxTargetDependency], pbxContainerItemProxySection = this.hash.project.objects[pbxContainerItemProxy]; for (var index = 0; index < dependencyTargets.length; index++) { var dependencyTargetUuid = dependencyTargets[index], dependencyTargetCommentKey = f("%s_comment", dependencyTargetUuid), targetDependencyUuid = this.generateUuid(), targetDependencyCommentKey = f("%s_comment", targetDependencyUuid), itemProxyUuid = this.generateUuid(), itemProxyCommentKey = f("%s_comment", itemProxyUuid), itemProxy = { isa: pbxContainerItemProxy, containerPortal: this.hash.project['rootObject'], containerPortal_comment: this.hash.project['rootObject_comment'], proxyType: 1, remoteGlobalIDString: dependencyTargetUuid, remoteInfo: nativeTargets[dependencyTargetUuid].name }, targetDependency = { isa: pbxTargetDependency, target: dependencyTargetUuid, target_comment: nativeTargets[dependencyTargetCommentKey], targetProxy: itemProxyUuid, targetProxy_comment: pbxContainerItemProxy }; if (pbxContainerItemProxySection && pbxTargetDependencySection) { pbxContainerItemProxySection[itemProxyUuid] = itemProxy; pbxContainerItemProxySection[itemProxyCommentKey] = pbxContainerItemProxy; pbxTargetDependencySection[targetDependencyUuid] = targetDependency; pbxTargetDependencySection[targetDependencyCommentKey] = pbxTargetDependency; nativeTargets[target].dependencies.push({ value: targetDependencyUuid, comment: pbxTargetDependency }) } } return { uuid: target, target: nativeTargets[target] }; } pbxProject.prototype.addBuildPhase = function(filePathsArray, buildPhaseType, comment, target, optionsOrFolderType, subfolderPath) { var buildPhaseSection, fileReferenceSection = this.pbxFileReferenceSection(), buildFileSection = this.pbxBuildFileSection(), buildPhaseUuid = this.generateUuid(), buildPhaseTargetUuid = target || this.getFirstTarget().uuid, commentKey = f("%s_comment", buildPhaseUuid), buildPhase = { isa: buildPhaseType, buildActionMask: 2147483647, files: [], runOnlyForDeploymentPostprocessing: 0 }, filePathToBuildFile = {}; if (buildPhaseType === 'PBXCopyFilesBuildPhase') { buildPhase = pbxCopyFilesBuildPhaseObj(buildPhase, optionsOrFolderType, subfolderPath, comment); } else if (buildPhaseType === 'PBXShellScriptBuildPhase') { buildPhase = pbxShellScriptBuildPhaseObj(buildPhase, optionsOrFolderType, comment) } if (!this.hash.project.objects[buildPhaseType]) { this.hash.project.objects[buildPhaseType] = new Object(); } if (!this.hash.project.objects[buildPhaseType][buildPhaseUuid]) { this.hash.project.objects[buildPhaseType][buildPhaseUuid] = buildPhase; this.hash.project.objects[buildPhaseType][commentKey] = comment; } if (this.hash.project.objects['PBXNativeTarget'][buildPhaseTargetUuid]['buildPhases']) { this.hash.project.objects['PBXNativeTarget'][buildPhaseTargetUuid]['buildPhases'].push({ value: buildPhaseUuid, comment: comment }) } for (var key in buildFileSection) { // only look for comments if (!COMMENT_KEY.test(key)) continue; var buildFileKey = key.split(COMMENT_KEY)[0], buildFile = buildFileSection[buildFileKey]; fileReference = fileReferenceSection[buildFile.fileRef]; if (!fileReference) continue; var pbxFileObj = new pbxFile(fileReference.path); filePathToBuildFile[fileReference.path] = { uuid: buildFileKey, basename: pbxFileObj.basename, group: pbxFileObj.group }; } for (var index = 0; index < filePathsArray.length; index++) { var filePath = filePathsArray[index], filePathQuoted = "\"" + filePath + "\"", file = new pbxFile(filePath); if (filePathToBuildFile[filePath]) { buildPhase.files.push(pbxBuildPhaseObj(filePathToBuildFile[filePath])); continue; } else if (filePathToBuildFile[filePathQuoted]) { buildPhase.files.push(pbxBuildPhaseObj(filePathToBuildFile[filePathQuoted])); continue; } file.uuid = this.generateUuid(); file.fileRef = this.generateUuid(); this.addToPbxFileReferenceSection(file); // PBXFileReference this.addToPbxBuildFileSection(file); // PBXBuildFile buildPhase.files.push(pbxBuildPhaseObj(file)); } if (buildPhaseSection) { buildPhaseSection[buildPhaseUuid] = buildPhase; buildPhaseSection[commentKey] = comment; } return { uuid: buildPhaseUuid, buildPhase: buildPhase }; } // helper access functions pbxProject.prototype.pbxProjectSection = function() { return this.hash.project.objects['PBXProject']; } pbxProject.prototype.pbxBuildFileSection = function() { return this.hash.project.objects['PBXBuildFile']; } pbxProject.prototype.pbxXCBuildConfigurationSection = function() { return this.hash.project.objects['XCBuildConfiguration']; } pbxProject.prototype.pbxFileReferenceSection = function() { return this.hash.project.objects['PBXFileReference']; } pbxProject.prototype.pbxNativeTargetSection = function() { return this.hash.project.objects['PBXNativeTarget']; } pbxProject.prototype.xcVersionGroupSection = function () { if (typeof this.hash.project.objects['XCVersionGroup'] !== 'object') { this.hash.project.objects['XCVersionGroup'] = {}; } return this.hash.project.objects['XCVersionGroup']; } pbxProject.prototype.pbxXCConfigurationList = function() { return this.hash.project.objects['XCConfigurationList']; } pbxProject.prototype.pbxGroupByName = function(name) { var groups = this.hash.project.objects['PBXGroup'], key, groupKey; for (key in groups) { // only look for comments if (!COMMENT_KEY.test(key)) continue; if (groups[key] == name) { groupKey = key.split(COMMENT_KEY)[0]; return groups[groupKey]; } } return null; } pbxProject.prototype.pbxTargetByName = function(name) { return this.pbxItemByComment(name, 'PBXNativeTarget'); } pbxProject.prototype.findTargetKey = function(name) { var targets = this.hash.project.objects['PBXNativeTarget']; for (var key in targets) { // only look for comments if (COMMENT_KEY.test(key)) continue; var target = targets[key]; if (target.name === name) { return key; } } return null; } pbxProject.prototype.pbxItemByComment = function(name, pbxSectionName) { var section = this.hash.project.objects[pbxSectionName], key, itemKey; for (key in section) { // only look for comments if (!COMMENT_KEY.test(key)) continue; if (section[key] == name) { itemKey = key.split(COMMENT_KEY)[0]; return section[itemKey]; } } return null; } pbxProject.prototype.pbxSourcesBuildPhaseObj = function(target) { return this.buildPhaseObject('PBXSourcesBuildPhase', 'Sources', target); } pbxProject.prototype.pbxResourcesBuildPhaseObj = function(target) { return this.buildPhaseObject('PBXResourcesBuildPhase', 'Resources', target); } pbxProject.prototype.pbxFrameworksBuildPhaseObj = function(target) { return this.buildPhaseObject('PBXFrameworksBuildPhase', 'Frameworks', target); } pbxProject.prototype.pbxEmbedFrameworksBuildPhaseObj = function (target) { return this.buildPhaseObject('PBXCopyFilesBuildPhase', 'Embed Frameworks', target); }; // Find Build Phase from group/target pbxProject.prototype.buildPhase = function(group, target) { if (!target) return undefined; var nativeTargets = this.pbxNativeTargetSection(); if (typeof nativeTargets[target] == "undefined") throw new Error("Invalid target: " + target); var nativeTarget = nativeTargets[target]; var buildPhases = nativeTarget.buildPhases; for(var i in buildPhases) { var buildPhase = buildPhases[i]; if (buildPhase.comment==group) return buildPhase.value + "_comment"; } } pbxProject.prototype.buildPhaseObject = function(name, group, target) { var section = this.hash.project.objects[name], obj, sectionKey, key; var buildPhase = this.buildPhase(group, target); for (key in section) { // only look for comments if (!COMMENT_KEY.test(key)) continue; // select the proper buildPhase if (buildPhase && buildPhase!=key) continue; if (section[key] == group) { sectionKey = key.split(COMMENT_KEY)[0]; return section[sectionKey]; } } return null; } pbxProject.prototype.addBuildProperty = function(prop, value, build_name) { var configurations = nonComments(this.pbxXCBuildConfigurationSection()), key, configuration; for (key in configurations){ configuration = configurations[key]; if (!build_name || configuration.name === build_name){ configuration.buildSettings[prop] = value; } } } pbxProject.prototype.removeBuildProperty = function(prop, build_name) { var configurations = nonComments(this.pbxXCBuildConfigurationSection()), key, configuration; for (key in configurations){ configuration = configurations[key]; if (configuration.buildSettings[prop] && !build_name || configuration.name === build_name){ delete configuration.buildSettings[prop]; } } } /** * * @param prop {String} * @param value {String|Array|Object|Number|Boolean} * @param build {String} Release or Debug */ pbxProject.prototype.updateBuildProperty = function(prop, value, build) { var configs = this.pbxXCBuildConfigurationSection(); for (var configName in configs) { if (!COMMENT_KEY.test(configName)) { var config = configs[configName]; if ( (build && config.name === build) || (!build) ) { config.buildSettings[prop] = value; } } } } pbxProject.prototype.updateProductName = function(name) { this.updateBuildProperty('PRODUCT_NAME', '"' + name + '"'); } pbxProject.prototype.removeFromFrameworkSearchPaths = function(file) { var configurations = nonComments(this.pbxXCBuildConfigurationSection()), INHERITED = '"$(inherited)"', SEARCH_PATHS = 'FRAMEWORK_SEARCH_PATHS', config, buildSettings, searchPaths; var new_path = searchPathForFile(file, this); for (config in configurations) { buildSettings = configurations[config].buildSettings; if (unquote(buildSettings['PRODUCT_NAME']) != this.productName) continue; searchPaths = buildSettings[SEARCH_PATHS]; if (searchPaths) { var matches = searchPaths.filter(function(p) { return p.indexOf(new_path) > -1; }); matches.forEach(function(m) { var idx = searchPaths.indexOf(m); searchPaths.splice(idx, 1); }); } } } pbxProject.prototype.addToFrameworkSearchPaths = function(file) { var configurations = nonComments(this.pbxXCBuildConfigurationSection()), INHERITED = '"$(inherited)"', config, buildSettings, searchPaths; for (config in configurations) { buildSettings = configurations[config].buildSettings; if (unquote(buildSettings['PRODUCT_NAME']) != this.productName) continue; if (!buildSettings['FRAMEWORK_SEARCH_PATHS'] || buildSettings['FRAMEWORK_SEARCH_PATHS'] === INHERITED) { buildSettings['FRAMEWORK_SEARCH_PATHS'] = [INHERITED]; } buildSettings['FRAMEWORK_SEARCH_PATHS'].push(searchPathForFile(file, this)); } } pbxProject.prototype.removeFromLibrarySearchPaths = function(file) { var configurations = nonComments(this.pbxXCBuildConfigurationSection()), INHERITED = '"$(inherited)"', SEARCH_PATHS = 'LIBRARY_SEARCH_PATHS', config, buildSettings, searchPaths; var new_path = searchPathForFile(file, this); for (config in configurations) { buildSettings = configurations[config].buildSettings; if (unquote(buildSettings['PRODUCT_NAME']) != this.productName) continue; searchPaths = buildSettings[SEARCH_PATHS]; if (searchPaths) { var matches = searchPaths.filter(function(p) { return p.indexOf(new_path) > -1; }); matches.forEach(function(m) { var idx = searchPaths.indexOf(m); searchPaths.splice(idx, 1); }); } } } pbxProject.prototype.addToLibrarySearchPaths = function(file) { var configurations = nonComments(this.pbxXCBuildConfigurationSection()), INHERITED = '"$(inherited)"', config, buildSettings, searchPaths; for (config in configurations) { buildSettings = configurations[config].buildSettings; if (unquote(buildSettings['PRODUCT_NAME']) != this.productName) continue; if (!buildSettings['LIBRARY_SEARCH_PATHS'] || buildSettings['LIBRARY_SEARCH_PATHS'] === INHERITED) { buildSettings['LIBRARY_SEARCH_PATHS'] = [INHERITED]; } if (typeof file === 'string') { buildSettings['LIBRARY_SEARCH_PATHS'].push(file); } else { buildSettings['LIBRARY_SEARCH_PATHS'].push(searchPathForFile(file, this)); } } } pbxProject.prototype.removeFromHeaderSearchPaths = function(file) { var configurations = nonComments(this.pbxXCBuildConfigurationSection()), INHERITED = '"$(inherited)"', SEARCH_PATHS = 'HEADER_SEARCH_PATHS', config, buildSettings, searchPaths; var new_path = searchPathForFile(file, this); for (config in configurations) { buildSettings = configurations[config].buildSettings; if (unquote(buildSettings['PRODUCT_NAME']) != this.productName) continue; if (buildSettings[SEARCH_PATHS]) { var matches = buildSettings[SEARCH_PATHS].filter(function(p) { return p.indexOf(new_path) > -1; }); matches.forEach(function(m) { var idx = buildSettings[SEARCH_PATHS].indexOf(m); buildSettings[SEARCH_PATHS].splice(idx, 1); }); } } } pbxProject.prototype.addToHeaderSearchPaths = function(file) { var configurations = nonComments(this.pbxXCBuildConfigurationSection()), INHERITED = '"$(inherited)"', config, buildSettings, searchPaths; for (config in configurations) { buildSettings = configurations[config].buildSettings; if (unquote(buildSettings['PRODUCT_NAME']) != this.productName) continue; if (!buildSettings['HEADER_SEARCH_PATHS']) { buildSettings['HEADER_SEARCH_PATHS'] = [INHERITED]; } if (typeof file === 'string') { buildSettings['HEADER_SEARCH_PATHS'].push(file); } else { buildSettings['HEADER_SEARCH_PATHS'].push(searchPathForFile(file, this)); } } } pbxProject.prototype.addToOtherLinkerFlags = function (flag) { var configurations = nonComments(this.pbxXCBuildConfigurationSection()), INHERITED = '"$(inherited)"', OTHER_LDFLAGS = 'OTHER_LDFLAGS', config, buildSettings; for (config in configurations) { buildSettings = configurations[config].buildSettings; if (unquote(buildSettings['PRODUCT_NAME']) != this.productName) continue; if (!buildSettings[OTHER_LDFLAGS] || buildSettings[OTHER_LDFLAGS] === INHERITED) { buildSettings[OTHER_LDFLAGS] = [INHERITED]; } buildSettings[OTHER_LDFLAGS].push(flag); } } pbxProject.prototype.removeFromOtherLinkerFlags = function (flag) { var configurations = nonComments(this.pbxXCBuildConfigurationSection()), OTHER_LDFLAGS = 'OTHER_LDFLAGS', config, buildSettings; for (config in configurations) { buildSettings = configurations[config].buildSettings; if (unquote(buildSettings['PRODUCT_NAME']) != this.productName) { continue; } if (buildSettings[OTHER_LDFLAGS]) { var matches = buildSettings[OTHER_LDFLAGS].filter(function (p) { return p.indexOf(flag) > -1; }); matches.forEach(function (m) { var idx = buildSettings[OTHER_LDFLAGS].indexOf(m); buildSettings[OTHER_LDFLAGS].splice(idx, 1); }); } } } pbxProject.prototype.addToBuildSettings = function (buildSetting, value) { var configurations = nonComments(this.pbxXCBuildConfigurationSection()), config, buildSettings; for (config in configurations) { buildSettings = configurations[config].buildSettings; buildSettings[buildSetting] = value; } } pbxProject.prototype.removeFromBuildSettings = function (buildSetting) { var configurations = nonComments(this.pbxXCBuildConfigurationSection()), config, buildSettings; for (config in configurations) { buildSettings = configurations[config].buildSettings; if (buildSettings[buildSetting]) { delete buildSettings[buildSetting]; } } } // a JS getter. hmmm pbxProject.prototype.__defineGetter__("productName", function() { var configurations = nonComments(this.pbxXCBuildConfigurationSection()), config, productName; for (config in configurations) { productName = configurations[config].buildSettings['PRODUCT_NAME']; if (productName) { return unquote(productName); } } }); // check if file is present pbxProject.prototype.hasFile = function(filePath) { var files = nonComments(this.pbxFileReferenceSection()), file, id; for (id in files) { file = files[id]; if (file.path == filePath || file.path == ('"' + filePath + '"')) { return file; } } return false; } pbxProject.prototype.addTarget = function(name, type, subfolder) { // Setup uuid and name of new target var targetUuid = this.generateUuid(), targetType = type, targetSubfolder = subfolder || name, targetName = name.trim(); // Check type against list of allowed target types if (!targetName) { throw new Error("Target name missing."); } // Check type against list of allowed target types if (!targetType) { throw new Error("Target type missing."); } // Check type against list of allowed target types if (!producttypeForTargettype(targetType)) { throw new Error("Target type invalid: " + targetType); } // Build Configuration: Create var buildConfigurationsList = [ { name: 'Debug', isa: 'XCBuildConfiguration', buildSettings: { GCC_PREPROCESSOR_DEFINITIONS: ['"DEBUG=1"', '"$(inherited)"'], INFOPLIST_FILE: '"' + path.join(targetSubfolder, targetSubfolder + '-Info.plist' + '"'), LD_RUNPATH_SEARCH_PATHS: '"$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"', PRODUCT_NAME: '"' + targetName + '"', SKIP_INSTALL: 'YES' } }, { name: 'Release', isa: 'XCBuildConfiguration', buildSettings: { INFOPLIST_FILE: '"' + path.join(targetSubfolder, targetSubfolder + '-Info.plist' + '"'), LD_RUNPATH_SEARCH_PATHS: '"$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"', PRODUCT_NAME: '"' + targetName + '"', SKIP_INSTALL: 'YES' } } ]; // Build Configuration: Add var buildConfigurations = this.addXCConfigurationList(buildConfigurationsList, 'Release', 'Build configuration list for PBXNativeTarget "' + targetName +'"'); // Product: Create var productName = targetName, productType = producttypeForTargettype(targetType), productFileType = filetypeForProducttype(productType), productFile = this.addProductFile(productName, { group: 'Copy Files', 'target': targetUuid, 'explicitFileType': productFileType}), productFileName = productFile.basename; // Product: Add to build file list this.addToPbxBuildFileSection(productFile); // Target: Create var target = { uuid: targetUuid, pbxNativeTarget: { isa: 'PBXNativeTarget', name: '"' + targetName + '"', productName: '"' + targetName + '"', productReference: productFile.fileRef, productType: '"' + producttypeForTargettype(targetType) + '"', buildConfigurationList: buildConfigurations.uuid, buildPhases: [], buildRules: [], dependencies: [] } }; // Target: Add to PBXNativeTarget section this.addToPbxNativeTargetSection(target) // Product: Embed (only for "extension"-type targets) if (targetType === 'app_extension') { // Create CopyFiles phase in first target this.addBuildPhase([], 'PBXCopyFilesBuildPhase', 'Copy Files', this.getFirstTarget().uuid, targetType) // Add product to CopyFiles phase this.addToPbxCopyfilesBuildPhase(productFile) // this.addBuildPhaseToTarget(newPhase.buildPhase, this.getFirstTarget().uuid) }; // Target: Add uuid to root project this.addToPbxProjectSection(target); // Target: Add dependency for this target to first (main) target this.addTargetDependency(this.getFirstTarget().uuid, [target.uuid]); // Return target on success return target; }; // helper recursive prop search+replace function propReplace(obj, prop, value) { var o = {}; for (var p in obj) { if (o.hasOwnProperty.call(obj, p)) { if (typeof obj[p] == 'object' && !Array.isArray(obj[p])) { propReplace(obj[p], prop, value); } else if (p == prop) { obj[p] = value; } } } } // helper object creation functions function pbxBuildFileObj(file) { var obj = Object.create(null); obj.isa = 'PBXBuildFile'; obj.fileRef = file.fileRef; obj.fileRef_comment = file.basename; if (file.settings) obj.settings = file.settings; return obj; } function pbxFileReferenceObj(file) { var fileObject = { isa: "PBXFileReference", name: "\"" + file.basename + "\"", path: "\"" + file.path.replace(/\\/g, '/') + "\"", sourceTree: file.sourceTree, fileEncoding: file.fileEncoding, lastKnownFileType: file.lastKnownFileType, explicitFileType: file.explicitFileType, includeInIndex: file.includeInIndex }; return fileObject; } function pbxGroupChild(file) { var obj = Object.create(null); obj.value = file.fileRef; obj.comment = file.basename; return obj; } function pbxBuildPhaseObj(file) { var obj = Object.create(null); obj.value = file.uuid; obj.comment = longComment(file); return obj; } function pbxCopyFilesBuildPhaseObj(obj, folderType, subfolderPath, phaseName) { // Add additional properties for 'CopyFiles' build phase var DESTINATION_BY_TARGETTYPE = { application: 'wrapper', app_extension: 'plugins', bundle: 'wrapper', command_line_tool: 'wrapper', dynamic_library: 'products_directory', framework: 'shared_frameworks', frameworks: 'frameworks', static_library: 'products_directory', unit_test_bundle: 'wrapper', watch_app: 'wrapper', watch_extension: 'plugins' } var SUBFOLDERSPEC_BY_DESTINATION = { absolute_path: 0, executables: 6, frameworks: 10, java_resources: 15, plugins: 13, products_directory: 16, resources: 7, shared_frameworks: 11, shared_support: 12, wrapper: 1, xpc_services: 0 } obj.name = '"' + phaseName + '"'; obj.dstPath = subfolderPath || '""'; obj.dstSubfolderSpec = SUBFOLDERSPEC_BY_DESTINATION[DESTINATION_BY_TARGETTYPE[folderType]]; return obj; } function pbxShellScriptBuildPhaseObj(obj, options, phaseName) { obj.name = '"' + phaseName + '"'; obj.inputPaths = options.inputPaths || []; obj.outputPaths = options.outputPaths || []; obj.shellPath = options.shellPath; obj.shellScript = '"' + options.shellScript.replace(/"/g, '\\"') + '"'; return obj; } function pbxBuildFileComment(file) { return longComment(file); } function pbxFileReferenceComment(file) { return file.basename || path.basename(file.path); } function pbxNativeTargetComment(target) { return target.name; } function longComment(file) { return f("%s in %s", file.basename, file.group); } // respect path function correctForPluginsPath(file, project) { return correctForPath(file, project, 'Plugins'); } function correctForResourcesPath(file, project) { return correctForPath(file, project, 'Resources'); } function correctForFrameworksPath(file, project) { return correctForPath(file, project, 'Frameworks'); } function correctForPath(file, project, group) { var r_group_dir = new RegExp('^' + group + '[\\\\/]'); if (project.pbxGroupByName(group).path) file.path = file.path.replace(r_group_dir, ''); return file; } function searchPathForFile(file, proj) { var plugins = proj.pbxGroupByName('Plugins'), pluginsPath = plugins ? plugins.path : null, fileDir = path.dirname(file.path); if (fileDir == '.') { fileDir = ''; } else { fileDir = '/' + fileDir; } if (file.plugin && pluginsPath) { return '"\\"$(SRCROOT)/' + unquote(pluginsPath) + '\\""'; } else if (file.customFramework && file.dirname) { return '"\\"' + file.dirname + '\\""'; } else { return '"\\"$(SRCROOT)/' + proj.productName + fileDir + '\\""'; } } function nonComments(obj) { var keys = Object.keys(obj), newObj = {}, i = 0; for (i; i < keys.length; i++) { if (!COMMENT_KEY.test(keys[i])) { newObj[keys[i]] = obj[keys[i]]; } } return newObj; } function unquote(str) { if (str) return str.replace(/^"(.*)"$/, "$1"); } function buildPhaseNameForIsa (isa) { BUILDPHASENAME_BY_ISA = { PBXCopyFilesBuildPhase: 'Copy Files', PBXResourcesBuildPhase: 'Resources', PBXSourcesBuildPhase: 'Sources', PBXFrameworksBuildPhase: 'Frameworks' } return BUILDPHASENAME_BY_ISA[isa] } function producttypeForTargettype (targetType) { PRODUCTTYPE_BY_TARGETTYPE = { application: 'com.apple.product-type.application', app_extension: 'com.apple.product-type.app-extension', bundle: 'com.apple.product-type.bundle', command_line_tool: 'com.apple.product-type.tool', dynamic_library: 'com.apple.product-type.library.dynamic', framework: 'com.apple.product-type.framework', static_library: 'com.apple.product-type.library.static', unit_test_bundle: 'com.apple.product-type.bundle.unit-test', watch_app: 'com.apple.product-type.application.watchapp', watch_extension: 'com.apple.product-type.watchkit-extension' }; return PRODUCTTYPE_BY_TARGETTYPE[targetType] } function filetypeForProducttype (productType) { FILETYPE_BY_PRODUCTTYPE = { 'com.apple.product-type.application': '"wrapper.application"', 'com.apple.product-type.app-extension': '"wrapper.app-extension"', 'com.apple.product-type.bundle': '"wrapper.plug-in"', 'com.apple.product-type.tool': '"compiled.mach-o.dylib"', 'com.apple.product-type.library.dynamic': '"compiled.mach-o.dylib"', 'com.apple.product-type.framework': '"wrapper.framework"', 'com.apple.product-type.library.static': '"archive.ar"', 'com.apple.product-type.bundle.unit-test': '"wrapper.cfbundle"', 'com.apple.product-type.application.watchapp': '"wrapper.application"', 'com.apple.product-type.watchkit-extension': '"wrapper.app-extension"' }; return FILETYPE_BY_PRODUCTTYPE[productType] } pbxProject.prototype.getFirstProject = function() { // Get pbxProject container var pbxProjectContainer = this.pbxProjectSection(); // Get first pbxProject UUID var firstProjectUuid = Object.keys(pbxProjectContainer)[0]; // Get first pbxProject var firstProject = pbxProjectContainer[firstProjectUuid]; return { uuid: firstProjectUuid, firstProject: firstProject } } pbxProject.prototype.getFirstTarget = function() { // Get first targets UUID var firstTargetUuid = this.getFirstProject()['firstProject']['targets'][0].value; // Get first pbxNativeTarget var firstTarget = this.pbxNativeTargetSection()[firstTargetUuid]; return { uuid: firstTargetUuid, firstTarget: firstTarget } } /*** NEW ***/ pbxProject.prototype.addToPbxGroupType = function (file, groupKey, groupType) { var group = this.getPBXGroupByKeyAndType(groupKey, groupType); if (group && group.children !== undefined) { if (typeof file === 'string') { //Group Key var childGroup = { value:file, }; if (this.getPBXGroupByKey(file)) { childGroup.comment = this.getPBXGroupByKey(file).name; } else if (this.getPBXVariantGroupByKey(file)) { childGroup.comment = this.getPBXVariantGroupByKey(file).name; } group.children.push(childGroup); } else { //File Object group.children.push(pbxGroupChild(file)); } } } pbxProject.prototype.addToPbxVariantGroup = function (file, groupKey) { this.addToPbxGroupType(file, groupKey, 'PBXVariantGroup'); } pbxProject.prototype.addToPbxGroup = function (file, groupKey) { this.addToPbxGroupType(file, groupKey, 'PBXGroup'); } pbxProject.prototype.pbxCreateGroupWithType = function(name, pathName, groupType) { //Create object var model = { isa: '"' + groupType + '"', children: [], name: name, sourceTree: '""' }; if (pathName) model.path = pathName; var key = this.generateUuid(); //Create comment var commendId = key + '_comment'; //add obj and commentObj to groups; var groups = this.hash.project.objects[groupType]; if (!groups) { groups = this.hash.project.objects[groupType] = new Object(); } groups[commendId] = name; groups[key] = model; return key; } pbxProject.prototype.pbxCreateVariantGroup = function(name) { return this.pbxCreateGroupWithType(name, undefined, 'PBXVariantGroup') } pbxProject.prototype.pbxCreateGroup = function(name, pathName) { return this.pbxCreateGroupWithType(name, pathName, 'PBXGroup'); } pbxProject.prototype.removeFromPbxGroupAndType = function (file, groupKey, groupType) { var group = this.getPBXGroupByKeyAndType(groupKey, groupType); if (group) { var groupChildren = group.children, i; for(i in groupChildren) { if(pbxGroupChild(file).value == groupChildren[i].value && pbxGroupChild(file).comment == groupChildren[i].comment) { groupChildren.splice(i, 1); break; } } } } pbxProject.prototype.removeFromPbxGroup = function (file, groupKey) { this.removeFromPbxGroupAndType(file, groupKey, 'PBXGroup'); } pbxProject.prototype.removeFromPbxVariantGroup = function (file, groupKey) { this.removeFromPbxGroupAndType(file, groupKey, 'PBXVariantGroup'); } pbxProject.prototype.getPBXGroupByKeyAndType = function(key, groupType) { return this.hash.project.objects[groupType][key]; }; pbxProject.prototype.getPBXGroupByKey = function(key) { return this.hash.project.objects['PBXGroup'][key]; }; pbxProject.prototype.getPBXVariantGroupByKey = function(key) { return this.hash.project.objects['PBXVariantGroup'][key]; }; pbxProject.prototype.findPBXGroupKeyAndType = function(criteria, groupType) { var groups = this.hash.project.objects[groupType]; var target; for (var key in groups) { // only look for comments if (COMMENT_KEY.test(key)) continue; var group = groups[key]; if (criteria && criteria.path && criteria.name) { if (criteria.path === group.path && criteria.name === group.name) { target = key; break } } else if (criteria && criteria.path) { if (criteria.path === group.path) { target = key; break } } else if (criteria && criteria.name) { if (criteria.name === group.name) { target = key; break } } } return target; } pbxProject.prototype.findPBXGroupKey = function(criteria) { return this.findPBXGroupKeyAndType(criteria, 'PBXGroup'); } pbxProject.prototype.findPBXVariantGroupKey = function(criteria) { return this.findPBXGroupKeyAndType(criteria, 'PBXVariantGroup'); } pbxProject.prototype.addLocalizationVariantGroup = function(name) { var groupKey = this.pbxCreateVariantGroup(name); var resourceGroupKey = this.findPBXGroupKey({name: 'Resources'}); this.addToPbxGroup(groupKey, resourceGroupKey); var localizationVariantGroup = { uuid: this.generateUuid(), fileRef: groupKey, basename: name } this.addToPbxBuildFileSection(localizationVariantGroup); // PBXBuildFile this.addToPbxResourcesBuildPhase(localizationVariantGroup); //PBXResourcesBuildPhase return localizationVariantGroup; }; pbxProject.prototype.addKnownRegion = function (name) { if (!this.pbxProjectSection()[this.getFirstProject()['uuid']]['knownRegions']) { this.pbxProjectSection()[this.getFirstProject()['uuid']]['knownRegions'] = []; } if (!this.hasKnownRegion(name)) { this.pbxProjectSection()[this.getFirstProject()['uuid']]['knownRegions'].push(name); } } pbxProject.prototype.removeKnownRegion = function (name) { var regions = this.pbxProjectSection()[this.getFirstProject()['uuid']]['knownRegions']; if (regions) { for (var i = 0; i < regions.length; i++) { if (regions[i] === name) { regions.splice(i, 1); break; } } this.pbxProjectSection()[this.getFirstProject()['uuid']]['knownRegions'] = regions; } } pbxProject.prototype.hasKnownRegion = function (name) { var regions = this.pbxProjectSection()[this.getFirstProject()['uuid']]['knownRegions']; if (regions) { for (var i in regions) { if (regions[i] === name) { return true; } } } return false; } pbxProject.prototype.getPBXObject = function(name) { return this.hash.project.objects[name]; } pbxProject.prototype.addFile = function (path, group, opt) { var file = new pbxFile(path, opt); // null is better for early errors if (this.hasFile(file.path)) return null; file.fileRef = this.generateUuid(); this.addToPbxFileReferenceSection(file); // PBXFileReference if (this.getPBXGroupByKey(group)) { this.addToPbxGroup(file, group); // PBXGroup } else if (this.getPBXVariantGroupByKey(group)) { this.addToPbxVariantGroup(file, group); // PBXVariantGroup } return file; } pbxProject.prototype.removeFile = function (path, group, opt) { var file = new pbxFile(path, opt); this.removeFromPbxFileReferenceSection(file); // PBXFileReference if (this.getPBXGroupByKey(group)) { this.removeFromPbxGroup(file, group); // PBXGroup } else if (this.getPBXVariantGroupByKey(group)) { this.removeFromPbxVariantGroup(file, group); // PBXVariantGroup } return file; } pbxProject.prototype.getBuildProperty = function(prop, build) { var target; var configs = this.pbxXCBuildConfigurationSection(); for (var configName in configs) { if (!COMMENT_KEY.test(configName)) { var config = configs[configName]; if ( (build && config.name === build) || (build === undefined) ) { if (config.buildSettings[prop] !== undefined) { target = config.buildSettings[prop]; } } } } return target; } pbxProject.prototype.getBuildConfigByName = function(name) { var target = {}; var configs = this.pbxXCBuildConfigurationSection(); for (var configName in configs) { if (!COMMENT_KEY.test(configName)) { var config = configs[configName]; if (config.name === name) { target[configName] = config; } } } return target; } pbxProject.prototype.addDataModelDocument = function(filePath, group, opt) { if (!group) { group = 'Resources'; } if (!this.getPBXGroupByKey(group)) { group = this.findPBXGroupKey({ name: group }); } var file = new pbxFile(filePath, opt); if (!file || this.hasFile(file.path)) return null; file.fileRef = this.generateUuid(); this.addToPbxGroup(file, group); if (!file) return false; file.target = opt ? opt.target : undefined; file.uuid = this.generateUuid(); this.addToPbxBuildFileSection(file); this.addToPbxSourcesBuildPhase(file); file.models = []; var currentVersionName; var modelFiles = fs.readdirSync(file.path); for (var index in modelFiles) { var modelFileName = modelFiles[index]; var modelFilePath = path.join(filePath, modelFileName); if (modelFileName == '.xccurrentversion') { currentVersionName = plist.readFileSync(modelFilePath)._XCCurrentVersionName; continue; } var modelFile = new pbxFile(modelFilePath); modelFile.fileRef = this.generateUuid(); this.addToPbxFileReferenceSection(modelFile); file.models.push(modelFile); if (currentVersionName && currentVersionName === modelFileName) { file.currentModel = modelFile; } } if (!file.currentModel) { file.currentModel = file.models[0]; } this.addToXcVersionGroupSection(file); return file; } pbxProject.prototype.addTargetAttribute = function(prop, value, target) { var attributes = this.getFirstProject()['firstProject']['attributes']; if (attributes['TargetAttributes'] === undefined) { attributes['TargetAttributes'] = {}; } target = target || this.getFirstTarget(); if (attributes['TargetAttributes'][target.uuid] === undefined) { attributes['TargetAttributes'][target.uuid] = {}; } attributes['TargetAttributes'][target.uuid][prop] = value; } pbxProject.prototype.removeTargetAttribute = function(prop, target) { var attributes = this.getFirstProject()['firstProject']['attributes']; target = target || this.getFirstTarget(); if (attributes['TargetAttributes'] && attributes['TargetAttributes'][target.uuid]) { delete attributes['TargetAttributes'][target.uuid][prop]; } } module.exports = pbxProject;