diff --git a/.gitignore b/.gitignore index 796bd5b..5ef95a1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ node_modules/* .DS_Store npm-debug.log package-lock.json +.ns-build-pbxgroup-data.json +*.tgz \ No newline at end of file diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 65e3ba2..0000000 --- a/.npmignore +++ /dev/null @@ -1 +0,0 @@ -test/ diff --git a/.ratignore b/.ratignore deleted file mode 100644 index eecfa9a..0000000 --- a/.ratignore +++ /dev/null @@ -1,2 +0,0 @@ -fixtures -*.pbxproj diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 14a176e..0000000 --- a/.travis.yml +++ /dev/null @@ -1,19 +0,0 @@ -language: node_js -sudo: false - -git: - depth: 10 - -node_js: - - 6 - - 8 - - 10 - -install: - - nvm --version - - node --version - - npm --version - - npm install - -script: - - npm test diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..f280792 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "skipFiles": ["/**"], + "runtimeExecutable": "nodeunit", + "runtimeArgs": ["test/parser", "test"] + } + ] +} diff --git a/LICENSE b/LICENSE index d645695..4794b43 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,3 @@ - Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -187,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright (c) 2015-2019 Progress Software Corporation Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 814a221..6f9060e 100644 --- a/README.md +++ b/README.md @@ -19,34 +19,31 @@ # --> -# cordova-node-xcode - -[![NPM](https://nodei.co/npm/xcode.png?compact=true)](https://nodei.co/npm/xcode/) -[![Build Status](https://travis-ci.org/apache/cordova-node-xcode.svg?branch=master)](https://travis-ci.org/apache/cordova-node-xcode) +# nativescript-dev-xcode Parser utility for xcodeproj project files Allows you to edit xcodeproject files and write them back out. -based on donated code from [alunny / node-xcode](https://github.com/alunny/node-xcode) +Forked from: [apache/cordova-node-xcode](https://github.com/apache/cordova-node-xcode) ## Example ```js // API is a bit wonky right now -var xcode = require('xcode'), - fs = require('fs'), - projectPath = 'myproject.xcodeproj/project.pbxproj', - myProj = xcode.project(projectPath); +var xcode = require("xcode"), + fs = require("fs"), + projectPath = "myproject.xcodeproj/project.pbxproj", + myProj = xcode.project(projectPath); // parsing is async, in a different process myProj.parse(function (err) { - myProj.addHeaderFile('foo.h'); - myProj.addSourceFile('foo.m'); - myProj.addFramework('FooKit.framework'); - - fs.writeFileSync(projectPath, myProj.writeSync()); - console.log('new project written'); + myProj.addHeaderFile("foo.h"); + myProj.addSourceFile("foo.m"); + myProj.addFramework("FooKit.framework"); + + fs.writeFileSync(projectPath, myProj.writeSync()); + console.log("new project written"); }); ``` @@ -61,7 +58,9 @@ grammar. Other tests will use the prebuilt parser (`lib/parser/pbxproj.js`). To rebuild the parser js file after editing the grammar, run: - npm run pegjs +``` +npm run pegjs +``` (and be sure to restore the Apache license notice in `lib/parser/pbxproj.js` before committing) diff --git a/lib/constants.js b/lib/constants.js new file mode 100644 index 0000000..68d0b15 --- /dev/null +++ b/lib/constants.js @@ -0,0 +1,147 @@ +var DEFAULT_SOURCETREE = '""', + DEFAULT_PRODUCT_SOURCETREE = 'BUILT_PRODUCTS_DIR', + DEFAULT_FILEENCODING = 4, + DEFAULT_GROUP = 'Resources', + DEFAULT_FILETYPE = 'unknown', + HEADER_FILE_TYPE_SUFFIX = ".h", + ENTITLEMENTS_FILE_TYPE_SUFFIX = ".entitlements", + SOURCE_CODE_FILE_TYPE_PREFIX = "sourcecode."; + +var FILETYPE_BY_EXTENSION = { + a: 'archive.ar', + app: 'wrapper.application', + appex: 'wrapper.app-extension', + bundle: 'wrapper.plug-in', + c: 'sourcecode.c.c', + cc: 'sourcecode.cpp.cpp', + cpp: 'sourcecode.cpp.cpp', + cxx: 'sourcecode.cpp.cpp', + 'c++': 'sourcecode.cpp.cpp', + dylib: 'compiled.mach-o.dylib', + framework: 'wrapper.framework', + h: 'sourcecode.c.h', + hh: 'sourcecode.cpp.h', + hpp: 'sourcecode.cpp.h', + hxx: 'sourcecode.cpp.h', + 'h++': 'sourcecode.cpp.h', + m: 'sourcecode.c.objc', + mm: 'sourcecode.cpp.objcpp', + markdown: 'text', + mdimporter: 'wrapper.cfbundle', + octest: 'wrapper.cfbundle', + pch: 'sourcecode.c.h', + plist: 'text.plist.xml', + entitlements: 'text.plist.entitlements', + png: "image.png", + sh: 'text.script.sh', + swift: 'sourcecode.swift', + tbd: 'sourcecode.text-based-dylib-definition', + xcassets: 'folder.assetcatalog', + xcconfig: 'text.xcconfig', + xcdatamodel: 'wrapper.xcdatamodel', + xcodeproj: 'wrapper.pb-project', + xctest: 'wrapper.cfbundle', + xib: 'file.xib', + strings: 'text.plist.strings', + modulemap: 'sourcecode.module-map' + }, + GROUP_BY_FILETYPE = { + 'archive.ar': 'Frameworks', + 'compiled.mach-o.dylib': 'Frameworks', + 'sourcecode.text-based-dylib-definition': 'Frameworks', + 'wrapper.framework': 'Frameworks', + 'embedded.framework': 'Embed Frameworks', + 'sourcecode.c.h': 'Resources', + 'sourcecode.c.c': 'Sources', + 'sourcecode.c.objc': 'Sources', + 'sourcecode.swift': 'Sources', + 'sourcecode.cpp.cpp': 'Sources', + 'sourcecode.cpp.objcpp': 'Sources' + }, + PATH_BY_FILETYPE = { + 'compiled.mach-o.dylib': 'usr/lib/', + 'sourcecode.text-based-dylib-definition': 'usr/lib/', + 'wrapper.framework': 'System/Library/Frameworks/' + }, + SOURCETREE_BY_FILETYPE = { + 'compiled.mach-o.dylib': 'SDKROOT', + 'sourcecode.text-based-dylib-definition': 'SDKROOT', + 'wrapper.framework': 'SDKROOT' + }, + ENCODING_BY_FILETYPE = { + 'sourcecode.c.h': 4, + 'sourcecode.c.h': 4, + 'sourcecode.cpp.h': 4, + 'sourcecode.c.c': 4, + 'sourcecode.c.objc': 4, + 'sourcecode.cpp.cpp': 4, + 'sourcecode.cpp.objcpp': 4, + 'sourcecode.swift': 4, + 'text': 4, + 'text.plist.xml': 4, + 'text.script.sh': 4, + 'text.xcconfig': 4, + 'text.plist.strings': 4 + }; + +function isHeaderFileType(fileType) { + return fileType.endsWith(HEADER_FILE_TYPE_SUFFIX); +} + +function isSourceFileType(fileType) { + return fileType.startsWith(SOURCE_CODE_FILE_TYPE_PREFIX) && !isHeaderFileType(fileType); +} + +function isAssetFileType(fileType) { + return fileType === FILETYPE_BY_EXTENSION.xcassets; +} + +function isResource(group) { + return group === "Resources"; +} + +function isEntitlementFileType(fileType) { + return fileType.endsWith(ENTITLEMENTS_FILE_TYPE_SUFFIX); +} + +function isPlistFileType(fileType) { + return fileType === FILETYPE_BY_EXTENSION.plist; +} + +function unquoted(text) { + return text == null ? '' : text.replace (/(^")|("$)/g, '') +} + +function quoteIfNeeded(name) { + const quotedName = (name.indexOf(" ") >= 0 || name.indexOf("@") >= 0) && name[0] !== `"` ? `"${name}"` : name; + return quotedName; +} + +function isModuleMapFileType(fileType) { + return fileType === FILETYPE_BY_EXTENSION.modulemap; +} + +module.exports = { + DEFAULT_SOURCETREE, + DEFAULT_PRODUCT_SOURCETREE, + DEFAULT_FILEENCODING, + DEFAULT_GROUP, + DEFAULT_FILETYPE, + HEADER_FILE_TYPE_SUFFIX, + ENTITLEMENTS_FILE_TYPE_SUFFIX, + SOURCE_CODE_FILE_TYPE_PREFIX, + FILETYPE_BY_EXTENSION, + GROUP_BY_FILETYPE, + PATH_BY_FILETYPE, + SOURCETREE_BY_FILETYPE, + ENCODING_BY_FILETYPE, + isHeaderFileType, + isSourceFileType, + isAssetFileType, + isResource, + isEntitlementFileType, + isPlistFileType, + isModuleMapFileType, + unquoted, + quoteIfNeeded +} \ No newline at end of file diff --git a/lib/guidMapper.js b/lib/guidMapper.js new file mode 100644 index 0000000..98300ff --- /dev/null +++ b/lib/guidMapper.js @@ -0,0 +1,52 @@ +const fs = require('fs'); +const path = require('path'); + +function guidMapper(filePath) { + this.filePath = filePath; + this.data = this.loadFromFile(); +} + +guidMapper.prototype.loadFromFile = function () { + try { + const rawData = fs.readFileSync(this.filePath, 'utf8'); + return JSON.parse(rawData); + } catch (error) { + // If file doesn't exist or there's an error parsing it, initialize with an empty object. + return {}; + } +}; + +guidMapper.prototype.writeFileSync = function () { + const jsonData = JSON.stringify(this.data, null, 2); + fs.writeFileSync(this.filePath, jsonData, 'utf8'); +}; + +guidMapper.prototype.addEntry = function (guid, path, name) { + if(!!guid && !! path && !!name){ + this.data[guid] = { path: path, name: name }; + } +}; + +guidMapper.prototype.removeEntry = function (guid) { + if (this.data[guid]) { + delete this.data[guid]; + } +}; + +guidMapper.prototype.getEntries = function () { + return this.data; +}; + +guidMapper.prototype.findEntryGuid = function (name, path) { + for (const guid in this.data) { + if (this.data.hasOwnProperty(guid)) { + const entry = this.data[guid]; + if (entry.path === path && entry.name === name) { + return guid; + } + } + } + return null; +}; + +module.exports = guidMapper; \ No newline at end of file diff --git a/lib/pbxFile.js b/lib/pbxFile.js index 9553bfb..1b52d25 100644 --- a/lib/pbxFile.js +++ b/lib/pbxFile.js @@ -16,89 +16,24 @@ */ var path = require('path'), - util = require('util'); - -var DEFAULT_SOURCETREE = '""', - DEFAULT_PRODUCT_SOURCETREE = 'BUILT_PRODUCTS_DIR', - DEFAULT_FILEENCODING = 4, - DEFAULT_GROUP = 'Resources', - DEFAULT_FILETYPE = 'unknown'; - -var FILETYPE_BY_EXTENSION = { - a: 'archive.ar', - app: 'wrapper.application', - appex: 'wrapper.app-extension', - bundle: 'wrapper.plug-in', - dylib: 'compiled.mach-o.dylib', - framework: 'wrapper.framework', - h: 'sourcecode.c.h', - m: 'sourcecode.c.objc', - markdown: 'text', - mdimporter: 'wrapper.cfbundle', - octest: 'wrapper.cfbundle', - pch: 'sourcecode.c.h', - plist: 'text.plist.xml', - sh: 'text.script.sh', - swift: 'sourcecode.swift', - tbd: 'sourcecode.text-based-dylib-definition', - xcassets: 'folder.assetcatalog', - xcconfig: 'text.xcconfig', - xcdatamodel: 'wrapper.xcdatamodel', - xcodeproj: 'wrapper.pb-project', - xctest: 'wrapper.cfbundle', - xib: 'file.xib', - strings: 'text.plist.strings' - }, - GROUP_BY_FILETYPE = { - 'archive.ar': 'Frameworks', - 'compiled.mach-o.dylib': 'Frameworks', - 'sourcecode.text-based-dylib-definition': 'Frameworks', - 'wrapper.framework': 'Frameworks', - 'embedded.framework': 'Embed Frameworks', - 'sourcecode.c.h': 'Resources', - 'sourcecode.c.objc': 'Sources', - 'sourcecode.swift': 'Sources' - }, - PATH_BY_FILETYPE = { - 'compiled.mach-o.dylib': 'usr/lib/', - 'sourcecode.text-based-dylib-definition': 'usr/lib/', - 'wrapper.framework': 'System/Library/Frameworks/' - }, - SOURCETREE_BY_FILETYPE = { - 'compiled.mach-o.dylib': 'SDKROOT', - 'sourcecode.text-based-dylib-definition': 'SDKROOT', - 'wrapper.framework': 'SDKROOT' - }, - ENCODING_BY_FILETYPE = { - 'sourcecode.c.h': 4, - 'sourcecode.c.h': 4, - 'sourcecode.c.objc': 4, - 'sourcecode.swift': 4, - 'text': 4, - 'text.plist.xml': 4, - 'text.script.sh': 4, - 'text.xcconfig': 4, - 'text.plist.strings': 4 - }; - - -function unquoted(text){ - return text == null ? '' : text.replace (/(^")|("$)/g, '') -} + util = require('util'), + constants = require('./constants'), + unquoted = constants.unquoted, + FILETYPE_BY_EXTENSION = constants.FILETYPE_BY_EXTENSION; function detectType(filePath) { var extension = path.extname(filePath).substring(1), filetype = FILETYPE_BY_EXTENSION[unquoted(extension)]; if (!filetype) { - return DEFAULT_FILETYPE; + return constants.DEFAULT_FILETYPE; } return filetype; } function defaultExtension(fileRef) { - var filetype = fileRef.lastKnownFileType && fileRef.lastKnownFileType != DEFAULT_FILETYPE ? + var filetype = fileRef.lastKnownFileType && fileRef.lastKnownFileType != constants.DEFAULT_FILETYPE ? fileRef.lastKnownFileType : fileRef.explicitFileType; for(var extension in FILETYPE_BY_EXTENSION) { @@ -111,7 +46,7 @@ function defaultExtension(fileRef) { function defaultEncoding(fileRef) { var filetype = fileRef.lastKnownFileType || fileRef.explicitFileType, - encoding = ENCODING_BY_FILETYPE[unquoted(filetype)]; + encoding = constants.ENCODING_BY_FILETYPE[unquoted(filetype)]; if (encoding) { return encoding; @@ -121,18 +56,18 @@ function defaultEncoding(fileRef) { function detectGroup(fileRef, opt) { var extension = path.extname(fileRef.basename).substring(1), filetype = fileRef.lastKnownFileType || fileRef.explicitFileType, - groupName = GROUP_BY_FILETYPE[unquoted(filetype)]; + groupName = constants.GROUP_BY_FILETYPE[unquoted(filetype)]; if (extension === 'xcdatamodeld') { return 'Sources'; } if (opt.customFramework && opt.embed) { - return GROUP_BY_FILETYPE['embedded.framework']; + return constants.GROUP_BY_FILETYPE['embedded.framework']; } if (!groupName) { - return DEFAULT_GROUP; + return constants.DEFAULT_GROUP; } return groupName; @@ -141,18 +76,18 @@ function detectGroup(fileRef, opt) { function detectSourcetree(fileRef) { var filetype = fileRef.lastKnownFileType || fileRef.explicitFileType, - sourcetree = SOURCETREE_BY_FILETYPE[unquoted(filetype)]; + sourcetree = constants.SOURCETREE_BY_FILETYPE[unquoted(filetype)]; if (fileRef.explicitFileType) { - return DEFAULT_PRODUCT_SOURCETREE; + return constants.DEFAULT_PRODUCT_SOURCETREE; } if (fileRef.customFramework) { - return DEFAULT_SOURCETREE; + return constants.DEFAULT_SOURCETREE; } if (!sourcetree) { - return DEFAULT_SOURCETREE; + return constants.DEFAULT_SOURCETREE; } return sourcetree; @@ -160,7 +95,7 @@ function detectSourcetree(fileRef) { function defaultPath(fileRef, filePath) { var filetype = fileRef.lastKnownFileType || fileRef.explicitFileType, - defaultPath = PATH_BY_FILETYPE[unquoted(filetype)]; + defaultPath = constants.PATH_BY_FILETYPE[unquoted(filetype)]; if (fileRef.customFramework) { return filePath; @@ -173,20 +108,10 @@ function defaultPath(fileRef, filePath) { return filePath; } -function defaultGroup(fileRef) { - var groupName = GROUP_BY_FILETYPE[fileRef.lastKnownFileType]; - - if (!groupName) { - return DEFAULT_GROUP; - } - - return defaultGroup; -} - function pbxFile(filepath, opt) { - var opt = opt || {}; + opt = opt || {}; - this.basename = path.basename(filepath); + this.basename = opt.basename || path.basename(filepath); this.lastKnownFileType = opt.lastKnownFileType || detectType(filepath); this.group = detectGroup(this, opt); @@ -230,4 +155,4 @@ function pbxFile(filepath, opt) { } } -module.exports = pbxFile; +module.exports = pbxFile; \ No newline at end of file diff --git a/lib/pbxProject.js b/lib/pbxProject.js index 0e0f8f9..70f82ea 100644 --- a/lib/pbxProject.js +++ b/lib/pbxProject.js @@ -18,21 +18,31 @@ var util = require('util'), f = util.format, EventEmitter = require('events').EventEmitter, - path = require('path'), - uuid = require('uuid'), + $path = require('path'), + $uuid = require('uuid'), fork = require('child_process').fork, pbxWriter = require('./pbxWriter'), pbxFile = require('./pbxFile'), + constants = require('./constants'), fs = require('fs'), parser = require('./parser/pbxproj'), plist = require('simple-plist'), - COMMENT_KEY = /_comment$/ + COMMENT_KEY = /_comment$/, + NO_SPECIAL_SYMBOLS = /^[a-zA-Z0-9_\.\$]+\.[a-zA-Z]+$/, + isSourceFileType = constants.isSourceFileType, + isHeaderFileType = constants.isHeaderFileType, + isResource = constants.isResource, + isEntitlementFileType = constants.isEntitlementFileType, + isAssetFileType = constants.isAssetFileType, + isPlistFileType = constants.isPlistFileType, + isModuleMapFileType = constants.isModuleMapFileType, + guidMapper = require('./guidMapper'); function pbxProject(filename) { if (!(this instanceof pbxProject)) return new pbxProject(filename); - this.filepath = path.resolve(filename) + this.filepath = $path.resolve(filename) } util.inherits(pbxProject, EventEmitter) @@ -65,6 +75,10 @@ pbxProject.prototype.parseSync = function() { } pbxProject.prototype.writeSync = function(options) { + if(this.pbxGroupTracker){ + this.pbxGroupTracker.writeFileSync(); + } + this.writer = new pbxWriter(this.hash, options); return this.writer.writeSync(); } @@ -74,7 +88,7 @@ pbxProject.prototype.allUuids = function() { uuids = [], section; - for (key in sections) { + for (var key in sections) { section = sections[key] uuids = uuids.concat(Object.keys(section)) } @@ -87,9 +101,9 @@ pbxProject.prototype.allUuids = function() { } pbxProject.prototype.generateUuid = function() { - var id = uuid.v4() + var id = $uuid.v4() .replace(/-/g, '') - .substr(0, 24) + .substring(0, 24) .toUpperCase() if (this.allUuids().indexOf(id) >= 0) { @@ -329,32 +343,43 @@ pbxProject.prototype.addFramework = function(fpath, opt) { 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 + var fileReference = this.hasFile(file.path); + if (fileReference) { + var key = this.getFileKey(file.path); + file.fileRef = key; + } else { + this.addToPbxFileReferenceSection(file); // PBXFileReference + this.addToFrameworksPbxGroup(file); // PBXGroup + } if (link) { - this.addToPbxFrameworksBuildPhase(file); // PBXFrameworksBuildPhase + const buildFileUuid = this.addToPbxFrameworksBuildPhase(file); + if(buildFileUuid === file.uuid) { // PBXFrameworksBuildPhase) + this.addToPbxBuildFileSection(file); // PBXBuildFile + } else { + file.uuid = buildFileUuid; + } } 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 + opt.embed = embed; + var embeddedFile = new pbxFile(fpath, opt); + + embeddedFile.uuid = this.generateUuid(); + embeddedFile.fileRef = file.fileRef; + embeddedFile.target = file.target; + const embedBuildFileUuid = this.addToPbxEmbedFrameworksBuildPhase(embeddedFile); + if(embedBuildFileUuid === embeddedFile.uuid) { // PBXCopyFilesBuildPhase + //keeping a separate PBXBuildFile entry for Embed Frameworks + this.addToPbxBuildFileSection(embeddedFile); // PBXBuildFile + } else { + embeddedFile.uuid = embedBuildFileUuid; + } - return embeddedFile; + return embeddedFile; } } @@ -415,8 +440,8 @@ 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); +pbxProject.prototype.addToPbxCopyfilesBuildPhase = function(file, comment, target) { + var sources = this.buildPhaseObject('PBXCopyFilesBuildPhase', comment || 'Copy Files', target || file.target); sources.files.push(pbxBuildPhaseObj(file)); } @@ -433,7 +458,7 @@ pbxProject.prototype.removeCopyfile = function(fpath, opt) { pbxProject.prototype.removeFromPbxCopyfilesBuildPhase = function(file) { var sources = this.pbxCopyfilesBuildPhaseObj(file.target); - for (i in sources.files) { + for (var i in sources.files) { if (sources.files[i].comment == longComment(file)) { sources.files.splice(i, 1); break; @@ -478,33 +503,88 @@ pbxProject.prototype.addToPbxBuildFileSection = function(file) { } pbxProject.prototype.removeFromPbxBuildFileSection = function(file) { - var uuid; + var fileUuid; + + for (fileUuid in this.pbxBuildFileSection()) { + if (this.pbxBuildFileSection()[fileUuid].fileRef_comment == file.basename) { + file.uuid = fileUuid; + this.removeFromPbxBuildFileSectionByUuid(fileUuid); + } + } +} + +pbxProject.prototype.removeFromPbxBuildFileSectionByFileRef = function(file) { + var fileUuid; + var pbxBuildFileSection = this.pbxBuildFileSection(); + + for (fileUuid in pbxBuildFileSection) { + if (pbxBuildFileSection[fileUuid].fileRef == file.uuid) { + this.removeFromPbxBuildFileSectionByUuid(fileUuid); + } + } +} - for (uuid in this.pbxBuildFileSection()) { - if (this.pbxBuildFileSection()[uuid].fileRef_comment == file.basename) { - file.uuid = uuid; - delete this.pbxBuildFileSection()[uuid]; +pbxProject.prototype.removeFromPbxBuildFileSectionByUuid = function(itemUuid) { + var buildSection = this.pbxBuildFileSection(); + removeItemAndCommentFromSectionByUuid(buildSection, itemUuid); +} - var commentKey = f("%s_comment", uuid); - delete this.pbxBuildFileSection()[commentKey]; +pbxProject.prototype.findMainPbxGroup = function () { + var groups = this.hash.project.objects['PBXGroup']; + var candidates = []; + for (var key in groups) { + if (!groups[key].path && !groups[key].name && groups[key].isa) { + candidates.push(groups[key]); } } + if (candidates.length == 1) { + return candidates[0]; + } + + return null; +} +pbxProject.prototype.getPbxGroupTracker = function (path) { + + if(!this.pbxGroupTracker){ + this.pbxGroupTracker = new guidMapper($path.join(path, '.ns-build-pbxgroup-data.json')); + } + + return this.pbxGroupTracker; } -pbxProject.prototype.addPbxGroup = function(filePathsArray, name, path, sourceTree) { +pbxProject.prototype.addPbxGroup = function (filePathsArray, name, path, sourceTree, opt) { + opt = opt || {}; + var srcRootPath = $path.dirname($path.dirname(this.filepath)); + + var existingGroupId = this.getPbxGroupTracker(srcRootPath).findEntryGuid(name, path); + if(existingGroupId){ + if(this.getPBXGroupByKey(existingGroupId)){ + this.removePbxGroupByKey(existingGroupId, path); + } + this.pbxGroupTracker.removeEntry(existingGroupId); + } + var groups = this.hash.project.objects['PBXGroup'], - pbxGroupUuid = this.generateUuid(), + pbxGroupUuid = opt.uuid || this.generateUuid(), commentKey = f("%s_comment", pbxGroupUuid), + groupName = constants.quoteIfNeeded(name), pbxGroup = { isa: 'PBXGroup', children: [], - name: name, - path: path, + name: groupName, sourceTree: sourceTree ? sourceTree : '""' }, fileReferenceSection = this.pbxFileReferenceSection(), filePathToReference = {}; + //path is mandatory only for the main group + if(!opt.filesRelativeToProject && path) { + pbxGroup.path = path; + } + + // save to group to the tracker + this.pbxGroupTracker.addEntry(pbxGroupUuid, path, name); + for (var key in fileReferenceSection) { // only look for comments if (!COMMENT_KEY.test(key)) continue; @@ -526,35 +606,161 @@ pbxProject.prototype.addPbxGroup = function(filePathsArray, name, path, sourceTr continue; } - var file = new pbxFile(filePath); + var relativePath = $path.relative(srcRootPath, filePath); + var file = new pbxFile(opt.filesRelativeToProject ? relativePath : filePath); file.uuid = this.generateUuid(); file.fileRef = this.generateUuid(); - this.addToPbxFileReferenceSection(file); // PBXFileReference - this.addToPbxBuildFileSection(file); // PBXBuildFile - pbxGroup.children.push(pbxGroupChild(file)); + if(opt.target) { + file.target = opt.target; + } + // if the file is a symLink, isDirectory() returns false + // but isFile() too so we need to see if the realPath is a directory + const exists = fs.existsSync(filePath); + const stats = exists && fs.lstatSync(filePath); + const isSymlink = exists && fs.lstatSync(filePath).isSymbolicLink(); + let symRealPath = isSymlink && fs.realpathSync(filePath); + const isRealDir = stats && stats.isDirectory() || (isSymlink && fs.lstatSync(symRealPath).isDirectory()); + if (exists && isRealDir && !isAssetFileType(file.lastKnownFileType)) { + if($path.extname(filePath) === ".lproj") { + continue; + } + file.fileRef = file.uuid; + var files = fs.readdirSync(filePath).map(p => $path.join(filePath, p)); + this.addToPbxFileReferenceSection(file); // PBXFileReference + this.addToPbxBuildFileSection(file); + pbxGroup.children.push(pbxGroupChild(file)); + this.addPbxGroup(files, $path.basename(filePath), filePath, null, {uuid: file.uuid, filesRelativeToProject: opt.filesRelativeToProject, target: opt.target}); + } else { + this.addToPbxFileReferenceSection(file); // PBXFileReference + pbxGroup.children.push(pbxGroupChild(file)); + if(isHeaderFileType(file.lastKnownFileType) || isPlistFileType(file.lastKnownFileType) || isModuleMapFileType(file.lastKnownFileType)) { + continue; + } + + if(isEntitlementFileType(file.lastKnownFileType)) { + this.addToBuildSettings('CODE_SIGN_ENTITLEMENTS', constants.quoteIfNeeded(file.path), opt.target); + continue; + } + + if (isSourceFileType(file.lastKnownFileType)) { // PBXBuildFile + this.addToPbxSourcesBuildPhase(file); + } else if(isResource(file.group)) { + this.addToPbxResourcesBuildPhase(file) + } + + this.addToPbxBuildFileSection(file); + } } + handleLocalization.call(this, filePathsArray, pbxGroup, srcRootPath, opt); + if (groups) { groups[pbxGroupUuid] = pbxGroup; groups[commentKey] = name; } - return { uuid: pbxGroupUuid, pbxGroup: pbxGroup }; + if (opt.isMain) { + let mainGroup = this.findMainPbxGroup(); + if (mainGroup) { + var file = new pbxFile($path.relative(this.filepath, path), {basename: name}); + file.fileRef = pbxGroupUuid; + mainGroup.children.push(pbxGroupChild(file)); + } + } + + return {uuid: pbxGroupUuid, pbxGroup: pbxGroup}; +} + +function handleLocalization(files, pbxGroup, srcRootPath, opt) { + var storyboardNames = {}; + var allNames = {}; + var regions = {}; + + for (let i = 0; i < files.length; i++) { + const filePath = files[i]; + const parsedPath = $path.parse(filePath); + if($path.extname(filePath) === ".lproj") { + var regionName = parsedPath.name; + var region = regions[regionName] = {}; + var regionFiles = fs.readdirSync(filePath); + this.addKnownRegion(regionName); + for (let j = 0; j < regionFiles.length; j++) { + var regionFilePath = regionFiles[j]; + var parsedRegionFilePath = $path.parse(regionFilePath); + var regionFileName = parsedRegionFilePath.name; + if(parsedRegionFilePath.ext === ".storyboard") { + storyboardNames[parsedRegionFilePath.name] = true; + } + var fileRegions = allNames[parsedRegionFilePath.name] = allNames[parsedRegionFilePath.name] || []; + fileRegions.push(regionName); + region[regionFileName] = $path.join(filePath, regionFilePath); + } + } + } + + for (var name in allNames) { + var fileRegionsForName = allNames[name] + var variantGroupName = storyboardNames[name] ? name + ".storyboard" : name + ".strings"; + + var variantGroup = this.addLocalizationVariantGroup(variantGroupName, { target: opt.target, skipAddToResourcesGroup: true }); + pbxGroup.children.push(pbxGroupChild(variantGroup)); + for (let k = 0; k < fileRegionsForName.length; k++) { + var file = regions[fileRegionsForName[k]][name]; + var refFile = new pbxFile($path.relative(srcRootPath, file), {basename: fileRegionsForName[k]}); + refFile.fileRef = this.generateUuid(); + this.addToPbxFileReferenceSection(refFile); + this.addToPbxVariantGroup(refFile, variantGroup.fileRef); + } + } +}; + +pbxProject.prototype.removePbxGroup = function(groupName, path) { + var groupKey = this.findPBXGroupKey({name: groupName}) || this.findPBXVariantGroupKey({name: groupName}); + if (!groupKey) { + return; + } + + this.removePbxGroupByKey(groupKey, path); } -pbxProject.prototype.removePbxGroup = function (groupName) { - var section = this.hash.project.objects['PBXGroup'], - key, itemKey; +pbxProject.prototype.removePbxGroupByKey = function(groupKey, path) { + var group = this.getPBXGroupByKey(groupKey) || this.getPBXVariantGroupByKey(groupKey) - for (key in section) { - // only look for comments - if (!COMMENT_KEY.test(key)) continue; + if (!group) { + return; + } - if (section[key] == groupName) { - itemKey = key.split(COMMENT_KEY)[0]; - delete section[itemKey]; + path = path || ""; + var children = group.children; + + for(i in children) { + var file = new pbxFile($path.join(path, children[i].comment)); + file.fileRef = children[i].value; + file.uuid = file.fileRef; + this.removePbxGroupByKey(children[i].value, $path.join(path, children[i].comment)); + this.removeFromPbxFileReferenceSectionByUuid(children[i].value); + this.removeFromPbxBuildFileSectionByFileRef(file); + this.removeFromPbxSourcesBuildPhase(file); + } + + var mainGroup = this.findMainPbxGroup(); + if(mainGroup) { + var mainGroupChildren = mainGroup.children, i; + for(i in mainGroupChildren) { + if (mainGroupChildren[i].value == groupKey) { + mainGroupChildren.splice(i, 1); + } } } + + var section, key, itemKey; + if(unquote(group.isa) === "PBXVariantGroup") { + section = this.hash.project.objects['PBXVariantGroup']; + } else { + section = this.hash.project.objects['PBXGroup']; + } + + removeItemAndCommentFromSectionByUuid(section, groupKey); } pbxProject.prototype.addToPbxProjectSection = function(target) { @@ -603,6 +809,12 @@ pbxProject.prototype.removeFromPbxFileReferenceSection = function(file) { return file; } +pbxProject.prototype.removeFromPbxFileReferenceSectionByUuid = function(fileUuid) { + var section = this.pbxFileReferenceSection(); + + removeItemAndCommentFromSectionByUuid(section, fileUuid); +} + 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"); @@ -615,12 +827,12 @@ pbxProject.prototype.addToXcVersionGroupSection = function(file) { isa: 'XCVersionGroup', children: file.models.map(function (el) { return el.fileRef; }), currentVersion: file.currentModel.fileRef, - name: path.basename(file.path), + name: $path.basename(file.path), path: file.path, sourceTree: '""', versionGroupType: 'wrapper.xcdatamodel' }; - this.xcVersionGroupSection()[commentKey] = path.basename(file.path); + this.xcVersionGroupSection()[commentKey] = $path.basename(file.path); } } @@ -673,7 +885,7 @@ pbxProject.prototype.removeFromResourcesPbxGroup = function(file) { pbxProject.prototype.addToFrameworksPbxGroup = function(file) { var pluginsGroup = this.pbxGroupByName('Frameworks'); if (!pluginsGroup) { - this.addPbxGroup([file.path], 'Frameworks'); + this.addPbxGroup([file.path], 'Frameworks', 'Frameworks', null, { isMain: true, filesRelativeToProject: true}); } else { pluginsGroup.children.push(pbxGroupChild(file)); } @@ -685,7 +897,7 @@ pbxProject.prototype.removeFromFrameworksPbxGroup = function(file) { } var pluginsGroupChildren = this.pbxGroupByName('Frameworks').children; - for (i in pluginsGroupChildren) { + for (var i in pluginsGroupChildren) { if (pbxGroupChild(file).value == pluginsGroupChildren[i].value && pbxGroupChild(file).comment == pluginsGroupChildren[i].comment) { pluginsGroupChildren.splice(i, 1); @@ -694,10 +906,26 @@ pbxProject.prototype.removeFromFrameworksPbxGroup = function(file) { } } +function getReferenceInPbxBuildFile(buildFileReferences, fileReference) { + var buildFileSection = this.pbxBuildFileSection(); + for(let buildFileReference of buildFileReferences) { + if(buildFileSection[buildFileReference.value] && buildFileSection[buildFileReference.value].fileRef === fileReference.fileRef){ + return buildFileReference.value; + } + } +} + pbxProject.prototype.addToPbxEmbedFrameworksBuildPhase = function (file) { var sources = this.pbxEmbedFrameworksBuildPhaseObj(file.target); + if (sources) { + var referenceUuid = getReferenceInPbxBuildFile.call(this, sources.files, file) + if(referenceUuid){ + return referenceUuid; + } + sources.files.push(pbxBuildPhaseObj(file)); + return file.uuid; } } @@ -705,7 +933,7 @@ pbxProject.prototype.removeFromPbxEmbedFrameworksBuildPhase = function (file) { var sources = this.pbxEmbedFrameworksBuildPhaseObj(file.target); if (sources) { var files = []; - for (i in sources.files) { + for (var i in sources.files) { if (sources.files[i].comment != longComment(file)) { files.push(sources.files[i]); } @@ -771,12 +999,21 @@ pbxProject.prototype.removeFromPbxResourcesBuildPhase = function(file) { pbxProject.prototype.addToPbxFrameworksBuildPhase = function(file) { var sources = this.pbxFrameworksBuildPhaseObj(file.target); - sources.files.push(pbxBuildPhaseObj(file)); + + if (sources) { + var frameworkBuildUuid = getReferenceInPbxBuildFile.call(this, sources.files, file); + if (frameworkBuildUuid) { + return frameworkBuildUuid; + } + + sources.files.push(pbxBuildPhaseObj(file)); + return file.uuid; + } } pbxProject.prototype.removeFromPbxFrameworksBuildPhase = function(file) { var sources = this.pbxFrameworksBuildPhaseObj(file.target); - for (i in sources.files) { + for (var i in sources.files) { if (sources.files[i].comment == longComment(file)) { sources.files.splice(i, 1); break; @@ -834,6 +1071,14 @@ pbxProject.prototype.addTargetDependency = function(target, dependencyTargets) { pbxTargetDependencySection = this.hash.project.objects[pbxTargetDependency], pbxContainerItemProxySection = this.hash.project.objects[pbxContainerItemProxy]; + if(!pbxTargetDependencySection){ + pbxTargetDependencySection = this.hash.project.objects[pbxTargetDependency] = {}; + } + + if(!pbxContainerItemProxySection){ + pbxContainerItemProxySection = this.hash.project.objects[pbxContainerItemProxy] = {}; + } + for (var index = 0; index < dependencyTargets.length; index++) { var dependencyTargetUuid = dependencyTargets[index], dependencyTargetCommentKey = f("%s_comment", dependencyTargetUuid), @@ -869,6 +1114,35 @@ pbxProject.prototype.addTargetDependency = function(target, dependencyTargets) { return { uuid: target, target: nativeTargets[target] }; } +pbxProject.prototype.removeBuildPhase = function(comment, target) { // Build phase files should be removed separately + var buildPhaseUuid = undefined, + buildPhaseTargetUuid = target || this.getFirstTarget().uuid + + if (this.hash.project.objects['PBXNativeTarget'][buildPhaseTargetUuid]['buildPhases']) { + let phases = this.hash.project.objects['PBXNativeTarget'][buildPhaseTargetUuid]['buildPhases']; + for (let i = 0; i < phases.length; i++) { + const phase = phases[i]; + if (phase.comment === comment) { + buildPhaseUuid = phase.value; + let commentKey = f("%s_comment", buildPhaseUuid) + if (this.hash.project.objects['PBXCopyFilesBuildPhase']) { + let phase = this.hash.project.objects['PBXCopyFilesBuildPhase'][commentKey] + delete phase + } + + if (this.hash.project.objects['PBXShellScriptBuildPhase']) { + let phase = this.hash.project.objects['PBXShellScriptBuildPhase'][commentKey] + delete phase + } + + phases.splice(i, 1); + } + } + + } + +} + pbxProject.prototype.addBuildPhase = function(filePathsArray, buildPhaseType, comment, target, optionsOrFolderType, subfolderPath) { var buildPhaseSection, fileReferenceSection = this.pbxFileReferenceSection(), @@ -913,12 +1187,12 @@ pbxProject.prototype.addBuildPhase = function(filePathsArray, buildPhaseType, co if (!COMMENT_KEY.test(key)) continue; var buildFileKey = key.split(COMMENT_KEY)[0], - buildFile = buildFileSection[buildFileKey]; - fileReference = fileReferenceSection[buildFile.fileRef]; + buildFile = buildFileSection[buildFileKey], + fileReference = fileReferenceSection[buildFile.fileRef]; if (!fileReference) continue; - var pbxFileObj = new pbxFile(fileReference.path); + var pbxFileObj = new pbxFile(fileReference.path || ""); filePathToBuildFile[fileReference.path] = { uuid: buildFileKey, basename: pbxFileObj.basename, group: pbxFileObj.group }; } @@ -1046,11 +1320,23 @@ pbxProject.prototype.pbxResourcesBuildPhaseObj = function(target) { } pbxProject.prototype.pbxFrameworksBuildPhaseObj = function(target) { - return this.buildPhaseObject('PBXFrameworksBuildPhase', 'Frameworks', target); + let buildPhase = this.buildPhaseObject('PBXFrameworksBuildPhase', 'Frameworks', target); + if (!buildPhase) { + // Create Frameworks phase in parent target + const phase = this.addBuildPhase([], 'PBXFrameworksBuildPhase', 'Frameworks', target, 'frameworks'); + buildPhase = phase.buildPhase; + } + return buildPhase; } pbxProject.prototype.pbxEmbedFrameworksBuildPhaseObj = function (target) { - return this.buildPhaseObject('PBXCopyFilesBuildPhase', 'Embed Frameworks', target); + let buildPhase = this.buildPhaseObject('PBXCopyFilesBuildPhase', 'Embed Frameworks', target); + if (!buildPhase) { + // Create CopyFiles phase in parent target + const phase = this.addBuildPhase([], 'PBXCopyFilesBuildPhase', 'Embed Frameworks', target, 'frameworks'); + buildPhase = phase.buildPhase; + } + return buildPhase; }; // Find Build Phase from group/target @@ -1070,8 +1356,8 @@ pbxProject.prototype.buildPhase = function(group, target) { 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], @@ -1094,13 +1380,13 @@ pbxProject.prototype.buildPhaseObject = function(name, group, target) { return null; } -pbxProject.prototype.addBuildProperty = function(prop, value, build_name) { +pbxProject.prototype.addBuildProperty = function(prop, value, build_name, productName) { var configurations = nonComments(this.pbxXCBuildConfigurationSection()), key, configuration; for (key in configurations){ configuration = configurations[key]; - if (!build_name || configuration.name === build_name){ + if ((!build_name || configuration.name === build_name) && (!productName || configuration.buildSettings.PRODUCT_NAME === productName || configuration.buildSettings.PRODUCT_NAME === `"${productName}"`)) { configuration.buildSettings[prop] = value; } } @@ -1183,8 +1469,10 @@ pbxProject.prototype.addToFrameworkSearchPaths = function(file) { || buildSettings['FRAMEWORK_SEARCH_PATHS'] === INHERITED) { buildSettings['FRAMEWORK_SEARCH_PATHS'] = [INHERITED]; } - - buildSettings['FRAMEWORK_SEARCH_PATHS'].push(searchPathForFile(file, this)); + var searchPath = searchPathForFile(file, this); + if(buildSettings['FRAMEWORK_SEARCH_PATHS'].indexOf(searchPath) < 0){ + buildSettings['FRAMEWORK_SEARCH_PATHS'].push(searchPath); + } } } @@ -1232,11 +1520,7 @@ pbxProject.prototype.addToLibrarySearchPaths = function(file) { buildSettings['LIBRARY_SEARCH_PATHS'] = [INHERITED]; } - if (typeof file === 'string') { - buildSettings['LIBRARY_SEARCH_PATHS'].push(file); - } else { - buildSettings['LIBRARY_SEARCH_PATHS'].push(searchPathForFile(file, this)); - } + buildSettings['LIBRARY_SEARCH_PATHS'].push(searchPathForFile(file, this)); } } @@ -1245,7 +1529,7 @@ pbxProject.prototype.removeFromHeaderSearchPaths = function(file) { INHERITED = '"$(inherited)"', SEARCH_PATHS = 'HEADER_SEARCH_PATHS', config, buildSettings, searchPaths; - var new_path = searchPathForFile(file, this); + var new_path = searchPathForFile(file, this); for (config in configurations) { buildSettings = configurations[config].buildSettings; @@ -1265,25 +1549,27 @@ pbxProject.prototype.removeFromHeaderSearchPaths = function(file) { } } -pbxProject.prototype.addToHeaderSearchPaths = function(file) { +pbxProject.prototype.addToHeaderSearchPaths = function(file, productName) { var configurations = nonComments(this.pbxXCBuildConfigurationSection()), INHERITED = '"$(inherited)"', config, buildSettings, searchPaths; + productName = unquote(productName || this.productName); + for (config in configurations) { buildSettings = configurations[config].buildSettings; - if (unquote(buildSettings['PRODUCT_NAME']) != this.productName) + if (unquote(buildSettings['PRODUCT_NAME']) != 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)); + // Check if the search path is already in the HEADER_SEARCH_PATHS and add it if it's not. + const searchPath = searchPathForFile(file, this); + if (buildSettings['HEADER_SEARCH_PATHS'].indexOf(searchPath) < 0) { + buildSettings['HEADER_SEARCH_PATHS'].push(searchPath); } } } @@ -1333,14 +1619,30 @@ pbxProject.prototype.removeFromOtherLinkerFlags = function (flag) { } } -pbxProject.prototype.addToBuildSettings = function (buildSetting, value) { +pbxProject.prototype.addToBuildSettings = function (buildSetting, value, targetUuid) { var configurations = nonComments(this.pbxXCBuildConfigurationSection()), + buildConfigurationsUuids = [], config, buildSettings; + if(targetUuid) { + var targets = this.hash.project.objects['PBXNativeTarget'] || []; + var target = targets[targetUuid] || {}; + var buildConfigurationList = target["buildConfigurationList"]; + var pbxXCConfigurationListSection = this.pbxXCConfigurationList() || {}; + var xcConfigurationList = pbxXCConfigurationListSection[buildConfigurationList] || {}; + var buildConfigurations = xcConfigurationList.buildConfigurations || []; + for(var configurationUuid in buildConfigurations){ + buildConfigurationsUuids.push(buildConfigurations[configurationUuid].value); + } + + } + for (config in configurations) { - buildSettings = configurations[config].buildSettings; + if(!target || buildConfigurationsUuids.indexOf(config) >= 0) { + buildSettings = configurations[config].buildSettings; - buildSettings[buildSetting] = value; + buildSettings[buildSetting] = value; + } } } @@ -1385,7 +1687,20 @@ pbxProject.prototype.hasFile = function(filePath) { return false; } -pbxProject.prototype.addTarget = function(name, type, subfolder) { +pbxProject.prototype.getFileKey = function(filePath) { + var files = nonComments(this.pbxFileReferenceSection()), + file, id; + for (id in files) { + file = files[id]; + if (file.path == filePath || file.path == ('"' + filePath + '"')) { + return id; + } + } + + return false; +} + +pbxProject.prototype.addTarget = function(name, type, subfolder, parentTarget) { // Setup uuid and name of new target var targetUuid = this.generateUuid(), @@ -1415,7 +1730,7 @@ pbxProject.prototype.addTarget = function(name, type, subfolder) { isa: 'XCBuildConfiguration', buildSettings: { GCC_PREPROCESSOR_DEFINITIONS: ['"DEBUG=1"', '"$(inherited)"'], - INFOPLIST_FILE: '"' + path.join(targetSubfolder, targetSubfolder + '-Info.plist' + '"'), + INFOPLIST_FILE: '"' + $path.join(targetSubfolder, 'Info.plist' + '"'), LD_RUNPATH_SEARCH_PATHS: '"$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"', PRODUCT_NAME: '"' + targetName + '"', SKIP_INSTALL: 'YES' @@ -1425,7 +1740,7 @@ pbxProject.prototype.addTarget = function(name, type, subfolder) { name: 'Release', isa: 'XCBuildConfiguration', buildSettings: { - INFOPLIST_FILE: '"' + path.join(targetSubfolder, targetSubfolder + '-Info.plist' + '"'), + INFOPLIST_FILE: '"' + $path.join(targetSubfolder, 'Info.plist' + '"'), LD_RUNPATH_SEARCH_PATHS: '"$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"', PRODUCT_NAME: '"' + targetName + '"', SKIP_INSTALL: 'YES' @@ -1464,33 +1779,238 @@ pbxProject.prototype.addTarget = function(name, type, subfolder) { }; // Target: Add to PBXNativeTarget section - this.addToPbxNativeTargetSection(target) - - // Product: Embed (only for "extension"-type targets) - if (targetType === 'app_extension') { + this.addToPbxNativeTargetSection(target); - // Create CopyFiles phase in first target - this.addBuildPhase([], 'PBXCopyFilesBuildPhase', 'Copy Files', this.getFirstTarget().uuid, targetType) + if (targetType === 'app_extension' || targetType === 'watch_extension' || targetType === 'watch_app') { + const isWatchApp = targetType === 'watch_app'; + const copyTargetUuid = parentTarget || this.getFirstTarget().uuid; + const phaseComment = targetType === 'watch_app' ? 'Embed Watch Content' : 'Copy Files'; + let destination; - // Add product to CopyFiles phase - this.addToPbxCopyfilesBuildPhase(productFile) + if(isWatchApp) { + destination = '"$(CONTENTS_FOLDER_PATH)/Watch"'; + } - // this.addBuildPhaseToTarget(newPhase.buildPhase, this.getFirstTarget().uuid) + // Create CopyFiles phase in parent target + this.addBuildPhase([], 'PBXCopyFilesBuildPhase', phaseComment, copyTargetUuid, targetType, destination); - }; + // Add product to CopyFiles phase + this.addToPbxCopyfilesBuildPhase(productFile, phaseComment, copyTargetUuid); + } // 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]); + this.addTargetDependency(parentTarget || this.getFirstTarget().uuid, [target.uuid]); // Return target on success return target; +}; + +pbxProject.prototype.removeTargetsByProductType = function(targetProductType) { + var nativeTargetsNonComments = nonComments(this.pbxNativeTargetSection()); + + for (var nativeTargetUuid in nativeTargetsNonComments) { + var target = nativeTargetsNonComments[nativeTargetUuid]; + if(target.productType === targetProductType || target.productType === `"${targetProductType}"`) { + this.removeTarget(target, nativeTargetUuid); + } + } +} + +pbxProject.prototype.removeTarget = function(target, targetKey) { + let files = []; + var pbxBuildFileSection = this.pbxBuildFileSection(); + var fileReferenceSection = this.pbxFileReferenceSection(); + // iterate all buildPhases and collect all files that should be removed + // remove the phase from the appropriate section + var buildPhases = target["buildPhases"]; + + for (let i = 0; i < buildPhases.length; i++) { + var buildPhase = buildPhases[i]; + var sectionUuid = buildPhase.value; + var section = {}; //in case we don't recognise the section + if(buildPhase.comment === buildPhaseNameForIsa("PBXSourcesBuildPhase")){ + section = this.hash.project.objects["PBXSourcesBuildPhase"]; + files = files.concat(section[sectionUuid].files); + } else if (buildPhase.comment === buildPhaseNameForIsa("PBXResourcesBuildPhase")) { + section = this.hash.project.objects["PBXResourcesBuildPhase"]; + files = files.concat(section[sectionUuid].files); + } else if (buildPhase.comment === buildPhaseNameForIsa("PBXFrameworksBuildPhase")) { + section = this.hash.project.objects["PBXFrameworksBuildPhase"]; + var frameworkFiles = section[sectionUuid].files; + for (let currentBuildFile of frameworkFiles) { + var currentBuildFileUuid = currentBuildFile.value; + var fileRef = pbxBuildFileSection[currentBuildFileUuid].fileRef; + var stillReferenced = false; + for (var buildFileUuid in nonComments(pbxBuildFileSection)) { + if(pbxBuildFileSection[buildFileUuid].fileRef === fileRef && buildFileUuid !== currentBuildFileUuid){ + stillReferenced = true; + break; + } + } + + if(!stillReferenced) { + var frameworkFileRef = fileReferenceSection[fileRef]; + if (frameworkFileRef?.path) { + // when working with widgets, the framework might be in a different group + var fileToRemove = new pbxFile(unquote(frameworkFileRef.path), {basename: frameworkFileRef.name}); + fileToRemove.fileRef = fileRef; + this.removeFromFrameworksPbxGroup(fileToRemove); + removeItemAndCommentFromSectionByUuid(fileReferenceSection, fileRef); + } + } + } + files = files.concat(frameworkFiles); + } + + removeItemAndCommentFromSectionByUuid(section, sectionUuid); + } + + //remove files from all build phases from PBXBuildFile section + for (let k = 0; k < files.length; k++) { + const fileUuid = files[k].value; + this.removeFromPbxBuildFileSectionByUuid(fileUuid); + } + + //remove target from the project itself + var targets = this.pbxProjectSection()[this.getFirstProject()['uuid']]['targets'] + for (let l = 0; l < targets.length; l++) { + if(targets[l].value === targetKey){ + targets.splice(l,1); + } + } + + //remove target build configurations + //get configurationList object and get all configuration uuids + var buildConfigurationList = target["buildConfigurationList"]; + var pbxXCConfigurationListSection = this.pbxXCConfigurationList(); + var xcConfigurationList = pbxXCConfigurationListSection[buildConfigurationList] || {}; + var buildConfigurations = xcConfigurationList.buildConfigurations || []; + + //remove all configurations from XCBuildConfiguration section + var pbxBuildConfigurationSection = this.pbxXCBuildConfigurationSection() + for (let m = 0; m < buildConfigurations.length; m++) { + const configuration = buildConfigurations[m]; + removeItemAndCommentFromSectionByUuid(pbxBuildConfigurationSection, configuration.value); + } + + //remove the XCConfigurationList from the section + removeItemAndCommentFromSectionByUuid(pbxXCConfigurationListSection, buildConfigurationList); + + //get target product information + var productUuid = ""; + + var productReferenceUuid = target.productReference; + + // the productReference is the uuid from the PBXFileReference Section, but we need the one in PBXBuildFile section + // check the fileRef of all records until we find the product + for (var uuid in nonComments(pbxBuildFileSection)) { + if (this.pbxBuildFileSection()[uuid].fileRef == productReferenceUuid) { + productUuid = uuid; + } + } + + //remove copy phase + var pbxCopySection = this.hash.project.objects["PBXCopyFilesBuildPhase"]; + var noCommentsCopySection = nonComments(pbxCopySection); + for (var copyPhaseId in noCommentsCopySection) { + var copyPhase = noCommentsCopySection[copyPhaseId]; + if(copyPhase.files) { + + //check if the product of the target is part of this copy phase files + for (let p = 0; p < copyPhase.files.length; p++) { + const copyFile = copyPhase.files[p]; + if(copyFile.value === productUuid) { + //if this is the only file in the copy phase - delete the whole phase and remove it from all targets + if(copyPhase.files.length === 1) { + var nativeTargetsnoComments = nonComments(this.pbxNativeTargetSection()); + for (var nativeTargetUuid in nativeTargetsnoComments) { + const nativeTarget = nativeTargetsnoComments[nativeTargetUuid]; + for (var phaseIndex in nativeTarget.buildPhases) { + if (nativeTarget.buildPhases[phaseIndex].value == copyPhaseId) { + //remove copy build phase from containing target + nativeTarget.buildPhases.splice(phaseIndex, 1); + break; + } + } + } + + //remove from copySection + removeItemAndCommentFromSectionByUuid(pbxCopySection, copyPhaseId); + } else { + //if there are other files in the copy phase, just remove the product + copyPhase.files.splice(p, 1); + } + break; + } + } + } + } + + //remove the product from the PBXBuildFile section + removeItemAndCommentFromSectionByUuid(pbxBuildFileSection, productUuid); + + + //remove the product from the Products PBXGroup + var productReference = fileReferenceSection[productReferenceUuid]; + var productFile = new pbxFile(productReference.path); + productFile.fileRef = productReferenceUuid; + productFile.uuid = productReferenceUuid; + this.removeFromProductsPbxGroup(productFile); + + //remove the product from the PBXFileReference section + removeItemAndCommentFromSectionByUuid(fileReferenceSection, productReferenceUuid); + + + //find all PBXTargetDependency that refer the target and remove them with the PBXContainerItemProxy + var pbxTargetDependency = 'PBXTargetDependency'; + var pbxContainerItemProxy = 'PBXContainerItemProxy'; + var pbxTargetDependencySection = this.hash.project.objects[pbxTargetDependency]; + var pbxTargetDependencySectionNoComments = nonComments(pbxTargetDependencySection); + var pbxContainerItemProxySection = this.hash.project.objects[pbxContainerItemProxy]; + + for(var targetDependencyUuid in pbxTargetDependencySectionNoComments) { + var targetDependency = pbxTargetDependencySectionNoComments[targetDependencyUuid]; + if(targetDependency.target === targetKey) { + //remove the PBXContainerItemProxy + removeItemAndCommentFromSectionByUuid(pbxContainerItemProxySection, targetDependency.targetProxy); + //remove the PBXTargetDependency from dependencies from all targets + for (var nativeTargetKey in nativeTargetsnoComments) { + const nativeTarget = nativeTargetsnoComments[nativeTargetKey]; + for (var dependencyIndex in nativeTarget.dependencies) { + if (nativeTarget.dependencies[dependencyIndex].value == targetDependencyUuid) { + nativeTarget.dependencies.splice(dependencyIndex, 1); + } + } + } + //remove the PBXTargetDependency + removeItemAndCommentFromSectionByUuid(pbxTargetDependencySection, targetDependencyUuid); + } + } + + //remove targetAttributes for target + var attributes = this.getFirstProject()['firstProject']['attributes']; + if (attributes['TargetAttributes']) { + delete attributes['TargetAttributes'][targetKey]; + } + + //remove the target from PBXNativeTarget section + var nativeTargets = this.pbxNativeTargetSection(); + removeItemAndCommentFromSectionByUuid(nativeTargets, targetKey); + + this.removePbxGroup(unquote(target.name)); }; +function removeItemAndCommentFromSectionByUuid(section, itemUuid) { + var commentKey = f("%s_comment", itemUuid) + delete section[commentKey]; + delete section[itemUuid]; +} + // helper recursive prop search+replace function propReplace(obj, prop, value) { var o = {}; @@ -1520,8 +2040,8 @@ function pbxBuildFileObj(file) { function pbxFileReferenceObj(file) { var fileObject = { isa: "PBXFileReference", - name: "\"" + file.basename + "\"", - path: "\"" + file.path.replace(/\\/g, '/') + "\"", + name: file.basename, + path: file.path, sourceTree: file.sourceTree, fileEncoding: file.fileEncoding, lastKnownFileType: file.lastKnownFileType, @@ -1529,6 +2049,19 @@ function pbxFileReferenceObj(file) { includeInIndex: file.includeInIndex }; + if(fileObject.name && fileObject.name.indexOf("\"") !== -1) { + fileObject.name = fileObject.name.replace(/\"/g, "\\\""); + fileObject.path = fileObject.path.replace(/\"/g, "\\\""); + } + + if(file.basename && !file.basename.match(NO_SPECIAL_SYMBOLS)) { + fileObject.name = "\"" + fileObject.name + "\""; + } + + if(!file.path.match(NO_SPECIAL_SYMBOLS)) { + fileObject.path = "\"" + fileObject.path + "\""; + } + return fileObject; } @@ -1563,7 +2096,7 @@ function pbxCopyFilesBuildPhaseObj(obj, folderType, subfolderPath, phaseName) { frameworks: 'frameworks', static_library: 'products_directory', unit_test_bundle: 'wrapper', - watch_app: 'wrapper', + watch_app: 'products_directory', watch_extension: 'plugins' } var SUBFOLDERSPEC_BY_DESTINATION = { @@ -1602,7 +2135,7 @@ function pbxBuildFileComment(file) { } function pbxFileReferenceComment(file) { - return file.basename || path.basename(file.path); + return file.basename || $path.basename(file.path); } function pbxNativeTargetComment(target) { @@ -1629,16 +2162,39 @@ function correctForFrameworksPath(file, project) { function correctForPath(file, project, group) { var r_group_dir = new RegExp('^' + group + '[\\\\/]'); - if (project.pbxGroupByName(group).path) + if (project.pbxGroupByName(group)?.path) file.path = file.path.replace(r_group_dir, ''); return file; } function searchPathForFile(file, proj) { + const getPathString = (filePath) => { + return `"\\"${filePath}\\""`; + } + + const getRelativePathString = (filePath) => { + return getPathString(`$(SRCROOT)/${filePath}`); + } + + if (typeof file === 'string') { + let relativeFilePath = file; + + if ($path.isAbsolute(file)) { + const srcRoot = $path.dirname($path.dirname(proj.filepath)); + relativeFilePath = $path.relative(srcRoot, file); + } + + return getRelativePathString(relativeFilePath); + } + + if (file.relativePath) { + return getRelativePathString(file.relativePath); + } + var plugins = proj.pbxGroupByName('Plugins'), pluginsPath = plugins ? plugins.path : null, - fileDir = path.dirname(file.path); + fileDir = $path.dirname(file.path); if (fileDir == '.') { fileDir = ''; @@ -1647,11 +2203,11 @@ function searchPathForFile(file, proj) { } if (file.plugin && pluginsPath) { - return '"\\"$(SRCROOT)/' + unquote(pluginsPath) + '\\""'; + return getRelativePathString(unquote(pluginsPath)); } else if (file.customFramework && file.dirname) { - return '"\\"' + file.dirname + '\\""'; + return getPathString(file.dirname); } else { - return '"\\"$(SRCROOT)/' + proj.productName + fileDir + '\\""'; + return getRelativePathString(proj.productName + fileDir); } } @@ -1675,7 +2231,7 @@ function unquote(str) { function buildPhaseNameForIsa (isa) { - BUILDPHASENAME_BY_ISA = { + var BUILDPHASENAME_BY_ISA = { PBXCopyFilesBuildPhase: 'Copy Files', PBXResourcesBuildPhase: 'Resources', PBXSourcesBuildPhase: 'Sources', @@ -1687,7 +2243,7 @@ function buildPhaseNameForIsa (isa) { function producttypeForTargettype (targetType) { - PRODUCTTYPE_BY_TARGETTYPE = { + var PRODUCTTYPE_BY_TARGETTYPE = { application: 'com.apple.product-type.application', app_extension: 'com.apple.product-type.app-extension', bundle: 'com.apple.product-type.bundle', @@ -1696,8 +2252,8 @@ function producttypeForTargettype (targetType) { 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' + watch_app: 'com.apple.product-type.application.watchapp2', + watch_extension: 'com.apple.product-type.watchkit2-extension' }; return PRODUCTTYPE_BY_TARGETTYPE[targetType] @@ -1705,7 +2261,7 @@ function producttypeForTargettype (targetType) { function filetypeForProducttype (productType) { - FILETYPE_BY_PRODUCTTYPE = { + var 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"', @@ -1714,8 +2270,8 @@ function filetypeForProducttype (productType) { '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"' + 'com.apple.product-type.application.watchapp2': '"wrapper.application"', + 'com.apple.product-type.watchkit2-extension': '"wrapper.app-extension"' }; return FILETYPE_BY_PRODUCTTYPE[productType] @@ -1856,7 +2412,7 @@ pbxProject.prototype.getPBXGroupByKey = function(key) { }; pbxProject.prototype.getPBXVariantGroupByKey = function(key) { - return this.hash.project.objects['PBXVariantGroup'][key]; + return this.hash.project.objects['PBXVariantGroup']?.[key]; }; @@ -1871,7 +2427,7 @@ pbxProject.prototype.findPBXGroupKeyAndType = function(criteria, groupType) { var group = groups[key]; if (criteria && criteria.path && criteria.name) { - if (criteria.path === group.path && criteria.name === group.name) { + if (criteria.path === group.path && (criteria.name === group.name || `"${criteria.name}"` === group.name)) { target = key; break } @@ -1883,7 +2439,7 @@ pbxProject.prototype.findPBXGroupKeyAndType = function(criteria, groupType) { } } else if (criteria && criteria.name) { - if (criteria.name === group.name) { + if (criteria.name === group.name || `"${criteria.name}"` === group.name) { target = key; break } @@ -1901,16 +2457,24 @@ pbxProject.prototype.findPBXVariantGroupKey = function(criteria) { return this.findPBXGroupKeyAndType(criteria, 'PBXVariantGroup'); } -pbxProject.prototype.addLocalizationVariantGroup = function(name) { +pbxProject.prototype.addLocalizationVariantGroup = function(name, ops) { + ops = ops || {}; var groupKey = this.pbxCreateVariantGroup(name); - var resourceGroupKey = this.findPBXGroupKey({name: 'Resources'}); - this.addToPbxGroup(groupKey, resourceGroupKey); + if(!ops.skipAddToResourcesGroup) { + var resourcesGroupKey = this.findPBXGroupKey({name: 'Resources'}); + this.addToPbxGroup(groupKey, resourcesGroupKey); + } var localizationVariantGroup = { uuid: this.generateUuid(), fileRef: groupKey, - basename: name + basename: name, + group: "Resources", + children: [] + } + if(ops.target) { + localizationVariantGroup.target = ops.target; } this.addToPbxBuildFileSection(localizationVariantGroup); // PBXBuildFile this.addToPbxResourcesBuildPhase(localizationVariantGroup); //PBXResourcesBuildPhase @@ -2053,7 +2617,7 @@ pbxProject.prototype.addDataModelDocument = function(filePath, group, opt) { var modelFiles = fs.readdirSync(file.path); for (var index in modelFiles) { var modelFileName = modelFiles[index]; - var modelFilePath = path.join(filePath, modelFileName); + var modelFilePath = $path.join(filePath, modelFileName); if (modelFileName == '.xccurrentversion') { currentVersionName = plist.readFileSync(modelFilePath)._XCCurrentVersionName; diff --git a/package.json b/package.json index b25fa48..69b2110 100644 --- a/package.json +++ b/package.json @@ -1,48 +1,30 @@ { - "author": "Apache Software Foundation", - "name": "xcode", + "author": "NativeScript TSC ", + "name": "nativescript-dev-xcode", "description": "parser for xcodeproj/project.pbxproj files", - "version": "2.0.1-dev", "main": "index.js", + "version": "0.8.1", + "files": [ + "lib", + "!lib/parser/pbxproj.pegjs" + ], "repository": { - "url": "https://github.com/apache/cordova-node-xcode.git" + "url": "https://github.com/NativeScript/nativescript-dev-xcode.git" }, "engines": { - "node": ">=6.0.0" + "node": ">=14.0.0" }, "dependencies": { - "simple-plist": "^1.0.0", - "uuid": "^3.3.2" + "simple-plist": "1.3.1", + "uuid": "9.0.1" }, "devDependencies": { - "nodeunit": "^0.11.3", - "pegjs": "^0.10.0" + "nodeunit": "0.11.3", + "pegjs": "0.10.0" }, "scripts": { - "pegjs": "node_modules/.bin/pegjs lib/parser/pbxproj.pegjs", - "test": "node_modules/.bin/nodeunit test/parser test" + "pegjs": "pegjs lib/parser/pbxproj.pegjs", + "test": "nodeunit test/parser test" }, - "license": "Apache-2.0", - "contributors": [ - { - "name": "Andrew Lunny", - "email": "alunny@gmail.com" - }, - { - "name": "Anis Kadri" - }, - { - "name": "Mike Reinstein" - }, - { - "name": "Filip Maj" - }, - { - "name": "Brett Rudd", - "email": "goya@apache.org" - }, - { - "name": "Bob Easterday" - } - ] + "license": "Apache-2.0" } diff --git a/test/addFramework.js b/test/addFramework.js index c3c3200..b0bbcab 100644 --- a/test/addFramework.js +++ b/test/addFramework.js @@ -98,7 +98,7 @@ exports.addFramework = { test.equal(fileRefEntry.isa, 'PBXFileReference'); test.equal(fileRefEntry.lastKnownFileType, 'compiled.mach-o.dylib'); - test.equal(fileRefEntry.name, '"libsqlite3.dylib"'); + test.equal(fileRefEntry.name, 'libsqlite3.dylib'); test.equal(fileRefEntry.path, '"usr/lib/libsqlite3.dylib"'); test.equal(fileRefEntry.sourceTree, 'SDKROOT'); @@ -188,10 +188,20 @@ exports.addFramework = { test.done(); }, 'duplicate entries': { - 'should return false': function (test) { + 'should return same build file': function (test) { var newFile = proj.addFramework('libsqlite3.dylib'); + var sameFile = proj.addFramework('libsqlite3.dylib'); - test.ok(!proj.addFramework('libsqlite3.dylib')); + test.equal(newFile.uuid, sameFile.uuid); + test.equal(newFile.fileRef, sameFile.fileRef); + test.done(); + }, + 'should return different build file with same ref for different target': function (test) { + var newFile = proj.addFramework('libsqlite3.dylib'); + var differentFile = proj.addFramework('libsqlite3.dylib', { target: "1D6058900D05DD3D006BFB54"}); + + test.notEqual(newFile.uuid, differentFile.uuid); + test.equal(newFile.fileRef, differentFile.fileRef); test.done(); } }, diff --git a/test/addHeaderFile.js b/test/addHeaderFile.js index f9bd71a..2373405 100644 --- a/test/addHeaderFile.js +++ b/test/addHeaderFile.js @@ -70,8 +70,8 @@ exports.addHeaderFile = { test.equal(fileRefEntry.isa, 'PBXFileReference'); test.equal(fileRefEntry.fileEncoding, 4); test.equal(fileRefEntry.lastKnownFileType, 'sourcecode.c.h'); - test.equal(fileRefEntry.name, '"file.h"'); - test.equal(fileRefEntry.path, '"file.h"'); + test.equal(fileRefEntry.name, 'file.h'); + test.equal(fileRefEntry.path, 'file.h'); test.equal(fileRefEntry.sourceTree, '""'); test.done(); diff --git a/test/addRemovePbxGroup.js b/test/addRemovePbxGroup.js index 8212f91..74006da 100644 --- a/test/addRemovePbxGroup.js +++ b/test/addRemovePbxGroup.js @@ -169,7 +169,6 @@ exports.addRemovePbxGroup = { proj.removePbxGroup(groupName); var pbxGroupInPbx = proj.pbxGroupByName(groupName); - console.log(pbxGroupInPbx); test.ok(!pbxGroupInPbx); test.done() diff --git a/test/addResourceFile.js b/test/addResourceFile.js index 36afdb2..3c48c59 100644 --- a/test/addResourceFile.js +++ b/test/addResourceFile.js @@ -108,7 +108,7 @@ exports.addResourceFile = { test.equal(fileRefEntry.isa, 'PBXFileReference'); test.equal(fileRefEntry.fileEncoding, undefined); test.equal(fileRefEntry.lastKnownFileType, 'wrapper.plug-in'); - test.equal(fileRefEntry.name, '"assets.bundle"'); + test.equal(fileRefEntry.name, 'assets.bundle'); test.equal(fileRefEntry.path, '"Resources/assets.bundle"'); test.equal(fileRefEntry.sourceTree, '""'); @@ -169,7 +169,7 @@ exports.addResourceFile = { test.equal(fileRefEntry.isa, 'PBXFileReference'); test.equal(fileRefEntry.fileEncoding, undefined); test.equal(fileRefEntry.lastKnownFileType, 'wrapper.plug-in'); - test.equal(fileRefEntry.name, '"assets.bundle"'); + test.equal(fileRefEntry.name, 'assets.bundle'); test.equal(fileRefEntry.path, '"Plugins/assets.bundle"'); test.equal(fileRefEntry.sourceTree, '""'); test.done(); diff --git a/test/addSourceFile.js b/test/addSourceFile.js index d614d62..612a0de 100644 --- a/test/addSourceFile.js +++ b/test/addSourceFile.js @@ -106,8 +106,8 @@ exports.addSourceFile = { test.equal(fileRefEntry.isa, 'PBXFileReference'); test.equal(fileRefEntry.fileEncoding, 4); test.equal(fileRefEntry.lastKnownFileType, 'sourcecode.c.objc'); - test.equal(fileRefEntry.name, '"file.m"'); - test.equal(fileRefEntry.path, '"file.m"'); + test.equal(fileRefEntry.name, 'file.m'); + test.equal(fileRefEntry.path, 'file.m'); test.equal(fileRefEntry.sourceTree, '""'); test.done(); diff --git a/test/addStaticLibrary.js b/test/addStaticLibrary.js index d1b0ec6..b157a08 100644 --- a/test/addStaticLibrary.js +++ b/test/addStaticLibrary.js @@ -140,8 +140,8 @@ exports.addStaticLibrary = { test.equal(fileRefEntry.isa, 'PBXFileReference'); test.equal(fileRefEntry.lastKnownFileType, 'archive.ar'); - test.equal(fileRefEntry.name, '"libGoogleAnalytics.a"'); - test.equal(fileRefEntry.path, '"libGoogleAnalytics.a"'); + test.equal(fileRefEntry.name, 'libGoogleAnalytics.a'); + test.equal(fileRefEntry.path, 'libGoogleAnalytics.a'); test.equal(fileRefEntry.sourceTree, '""'); test.done(); diff --git a/test/addToPbxFileReferenceSection.js b/test/addToPbxFileReferenceSection.js index d69abbc..494f223 100644 --- a/test/addToPbxFileReferenceSection.js +++ b/test/addToPbxFileReferenceSection.js @@ -39,8 +39,8 @@ exports['addToPbxFileReferenceSection function'] = { test.equal(myProj.pbxFileReferenceSection()[file.fileRef].isa, 'PBXFileReference'); test.equal(myProj.pbxFileReferenceSection()[file.fileRef].lastKnownFileType, 'sourcecode.c.objc'); - test.equal(myProj.pbxFileReferenceSection()[file.fileRef].name, '"file.m"'); - test.equal(myProj.pbxFileReferenceSection()[file.fileRef].path, '"file.m"'); + test.equal(myProj.pbxFileReferenceSection()[file.fileRef].name, 'file.m'); + test.equal(myProj.pbxFileReferenceSection()[file.fileRef].path, 'file.m'); test.equal(myProj.pbxFileReferenceSection()[file.fileRef].sourceTree, '""'); test.equal(myProj.pbxFileReferenceSection()[file.fileRef].fileEncoding, 4); test.equal(myProj.pbxFileReferenceSection()[file.fileRef + "_comment"], 'file.m'); diff --git a/test/fixtures/full-project.json b/test/fixtures/full-project.json index 09ec88e..dc8cf6f 100644 --- a/test/fixtures/full-project.json +++ b/test/fixtures/full-project.json @@ -564,7 +564,15 @@ ], "runOnlyForDeploymentPostprocessing": 0 }, - "1D60588F0D05DD3D006BFB54_comment": "Frameworks" + "1D60588F0D05DD3D006BFB54_comment": "Frameworks", + "2D60588F0D05DD3D006BFB55": { + "isa": "PBXFrameworksBuildPhase", + "buildActionMask": 2147483647, + "files": [ + ], + "runOnlyForDeploymentPostprocessing": 0 + }, + "2D60588F0D05DD3D006BFB55_comment": "Frameworks" }, "PBXGroup": { "080E96DDFE201D6D7F000001": { @@ -866,7 +874,7 @@ "comment": "Sources" }, { - "value": "1D60588F0D05DD3D006BFB54", + "value": "2D60588F0D05DD3D006BFB55", "comment": "Frameworks" } ], diff --git a/test/guidMapper.js b/test/guidMapper.js new file mode 100644 index 0000000..8c9cb72 --- /dev/null +++ b/test/guidMapper.js @@ -0,0 +1,69 @@ +var guidMapper = require('../lib/guidMapper'); +const fs = require('fs'); +const $uuid = require('uuid'); +const TEST_FILE_NAME = 'test/.ns-build-pbxgroup-data.json'; +const goodGUID = $uuid.v4(); +const goodName = "goodName"; +const badName = 'badName'; +const goodPath = "goodPath"; +const badPath = "badPath"; +exports.setUp = function(callback) { + if(fs.existsSync(TEST_FILE_NAME)){ + fs.rmSync(TEST_FILE_NAME); + } + callback(); +} +exports.tearDown = function(callback) { + if(fs.existsSync(TEST_FILE_NAME)){ + fs.rmSync(TEST_FILE_NAME); + } + callback(); +} +function addTestData(){ + var mapper = new guidMapper(TEST_FILE_NAME); + mapper.addEntry(goodGUID, goodPath, goodName); + mapper.writeFileSync(); +} +exports.operations = { + 'should be able to add to map': function(test) { + var mapper = new guidMapper(TEST_FILE_NAME); + mapper.addEntry(goodGUID, goodPath, goodName); + mapper.writeFileSync(); + mapper = new guidMapper(TEST_FILE_NAME); + const result = mapper.findEntryGuid(goodName, goodPath); + + test.ok(result === goodGUID) + test.done(); + }, + 'should not match only on name': function(test) { + addTestData(); + var mapper = new guidMapper(TEST_FILE_NAME); + + const result = mapper.findEntryGuid(goodName, badPath); + + test.ok(result === null) + test.done(); + }, + 'should not match only on path': function(test) { + addTestData(); + var mapper = new guidMapper(TEST_FILE_NAME); + + const result = mapper.findEntryGuid(badName, goodPath); + + test.ok(result === null) + test.done(); + }, + 'can remove': function(test) { + addTestData(); + var mapper = new guidMapper(TEST_FILE_NAME); + mapper.removeEntry(goodGUID); + var result = mapper.findEntryGuid(goodName, goodPath); + + test.ok(result === null); + mapper.writeFileSync(); + result = mapper.findEntryGuid(goodName, goodPath); + test.ok(result === null) + + test.done(); + } +} \ No newline at end of file diff --git a/test/parser/projects/expected/with_omit_empty_values_disabled_expected.pbxproj b/test/parser/projects/expected/with_omit_empty_values_disabled_expected.pbxproj index 409a2d4..7fd8b95 100644 --- a/test/parser/projects/expected/with_omit_empty_values_disabled_expected.pbxproj +++ b/test/parser/projects/expected/with_omit_empty_values_disabled_expected.pbxproj @@ -261,8 +261,8 @@ children = ( ); name = CustomGroup; - path = undefined; sourceTree = ""; + path = undefined; }; /* End PBXGroup section */ diff --git a/test/pbxProject.js b/test/pbxProject.js index c076b96..860c4f4 100644 --- a/test/pbxProject.js +++ b/test/pbxProject.js @@ -18,9 +18,14 @@ var pbx = require('../lib/pbxProject'), buildConfig = require('./fixtures/buildFiles'), jsonProject = require('./fixtures/full-project'), + fullProjectStr = JSON.stringify(jsonProject), fs = require('fs'), project; + function getCleanHash() { + return JSON.parse(fullProjectStr); + } + exports['creation'] = { 'should create a pbxProject with the new operator': function (test) { var myProj = new pbx('test/parser/projects/hash.pbxproj'); @@ -328,3 +333,120 @@ exports['hasFile'] = { test.done() } } + +exports['addToPbxFileReferenceSection'] = { + 'should not quote name when no special characters present in basename': function (test) { + var newProj = new pbx('.'); + newProj.hash = getCleanHash(), + file = { + uuid: newProj.generateUuid(), + fileRef: newProj.generateUuid(), + isa: 'PBXFileReference', + explicitFileType: 'wrapper.application', + includeInIndex: 0, + basename: "SomeFile.m", + path: "SomePath.m", + sourceTree: 'BUILT_PRODUCTS_DIR' + }, + fileRefSection = newProj.pbxFileReferenceSection(); + + newProj.addToPbxFileReferenceSection(file); + test.equal(fileRefSection[file.fileRef].name, "SomeFile.m"); + test.done(); + }, + 'should quote name when special characters present in basename': function (test) { + var newProj = new pbx('.'); + newProj.hash = getCleanHash(), + file = { + uuid: newProj.generateUuid(), + fileRef: newProj.generateUuid(), + isa: 'PBXFileReference', + explicitFileType: 'wrapper.application', + includeInIndex: 0, + basename: "Some File.m", + path: "SomePath.m", + sourceTree: 'BUILT_PRODUCTS_DIR' + }, + fileRefSection = newProj.pbxFileReferenceSection(); + + newProj.addToPbxFileReferenceSection(file); + test.equal(fileRefSection[file.fileRef].name, '"Some File.m"'); + test.done(); + }, + 'should not quote path when no special characters present in path': function (test) { + var newProj = new pbx('.'); + newProj.hash = getCleanHash(), + file = { + uuid: newProj.generateUuid(), + fileRef: newProj.generateUuid(), + isa: 'PBXFileReference', + explicitFileType: 'wrapper.application', + includeInIndex: 0, + basename: "SomeFile.m", + path: "SomePath.m", + sourceTree: 'BUILT_PRODUCTS_DIR' + }, + fileRefSection = newProj.pbxFileReferenceSection(); + + newProj.addToPbxFileReferenceSection(file); + test.equal(fileRefSection[file.fileRef].path, "SomePath.m"); + test.done(); + }, + 'should quote path when special characters present in path': function (test) { + var newProj = new pbx('.'); + newProj.hash = getCleanHash(), + file = { + uuid: newProj.generateUuid(), + fileRef: newProj.generateUuid(), + isa: 'PBXFileReference', + explicitFileType: 'wrapper.application', + includeInIndex: 0, + basename: "SomeFile.m", + path: "SomeFolder/Some Path.m", + sourceTree: 'BUILT_PRODUCTS_DIR' + }, + fileRefSection = newProj.pbxFileReferenceSection(); + + newProj.addToPbxFileReferenceSection(file); + test.equal(fileRefSection[file.fileRef].path, '"SomeFolder/Some Path.m"'); + test.done(); + }, + 'should quote path and name when special characters present in path and basename': function (test) { + var newProj = new pbx('.'); + newProj.hash = getCleanHash(), + file = { + uuid: newProj.generateUuid(), + fileRef: newProj.generateUuid(), + isa: 'PBXFileReference', + explicitFileType: 'wrapper.application', + includeInIndex: 0, + basename: "Some File.m", + path: "SomeFolder/Some Path.m", + sourceTree: 'BUILT_PRODUCTS_DIR' + }, + fileRefSection = newProj.pbxFileReferenceSection(); + + newProj.addToPbxFileReferenceSection(file); + test.equal(fileRefSection[file.fileRef].name, '"Some File.m"'); + test.equal(fileRefSection[file.fileRef].path, '"SomeFolder/Some Path.m"'); + test.done(); + } +} + + +exports['addPbxGroup'] = { + 'should not add the same group twice': function (test) { + var newProj = new pbx('test/parser/projects/group.pbxproj'); + newProj.parse(function (err, hash) { + this.hash.project.objects['PBXVariantGroup']={}; + var group1 = newProj.addPbxGroup(['test/somefile'], "TestGroup", "test/somepath", null, null); + var group2 = newProj.addPbxGroup(['test/somefile'], "TestGroup", "test/somepath", null, null); + test.equal(newProj.getPBXGroupByKey(group1.uuid), null); + test.equal(newProj.getPBXGroupByKey(group2.uuid).name, "TestGroup"); + test.equal(newProj.getPbxGroupTracker().getEntries().hasOwnProperty(group1.uuid), false); + test.equal(newProj.getPbxGroupTracker().getEntries().hasOwnProperty(group2.uuid), true); + + test.done(); + }); + } +} \ No newline at end of file diff --git a/test/removeBuildPhase.js b/test/removeBuildPhase.js new file mode 100644 index 0000000..bd35c24 --- /dev/null +++ b/test/removeBuildPhase.js @@ -0,0 +1,43 @@ +/** + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + 'License'); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +var fullProject = require('./fixtures/full-project') + fullProjectStr = JSON.stringify(fullProject), + pbx = require('../lib/pbxProject'), + proj = new pbx('.'); + +function cleanHash() { + return JSON.parse(fullProjectStr); +} + +exports.setUp = function (callback) { + proj.hash = cleanHash(); + callback(); +} + +exports.removeBuildPhase = { + + 'should remove a pbxBuildPhase': function (test) { + const comment = 'My build phase'; + var buildPhase = proj.addBuildPhase([], 'PBXSourcesBuildPhase', comment); + proj.removeBuildPhase(comment) + let phases = proj.hash.project.objects['PBXNativeTarget'][proj.getFirstTarget().uuid]['buildPhases']; + + test.ok(phases.map(p => p.comment).indexOf(comment) === -1); + test.done() + }, +} diff --git a/test/removeHeaderFile.js b/test/removeHeaderFile.js index acda161..358a88a 100644 --- a/test/removeHeaderFile.js +++ b/test/removeHeaderFile.js @@ -94,8 +94,8 @@ exports.removeHeaderFile = { test.equal(fileRefEntry.isa, 'PBXFileReference'); test.equal(fileRefEntry.fileEncoding, 4); test.equal(fileRefEntry.lastKnownFileType, 'sourcecode.c.h'); - test.equal(fileRefEntry.name, '"file.h"'); - test.equal(fileRefEntry.path, '"file.h"'); + test.equal(fileRefEntry.name, 'file.h'); + test.equal(fileRefEntry.path, 'file.h'); test.equal(fileRefEntry.sourceTree, '""'); var deletedFile = proj.removeHeaderFile('Plugins/file.h'), diff --git a/test/removeResourceFile.js b/test/removeResourceFile.js index bda48f9..f6d6488 100644 --- a/test/removeResourceFile.js +++ b/test/removeResourceFile.js @@ -158,7 +158,7 @@ exports.removeResourceFile = { test.equal(fileRefEntry.isa, 'PBXFileReference'); test.equal(fileRefEntry.fileEncoding, undefined); test.equal(fileRefEntry.lastKnownFileType, 'wrapper.plug-in'); - test.equal(fileRefEntry.name, '"assets.bundle"'); + test.equal(fileRefEntry.name, 'assets.bundle'); test.equal(fileRefEntry.path, '"Resources/assets.bundle"'); test.equal(fileRefEntry.sourceTree, '""'); diff --git a/test/removeSourceFile.js b/test/removeSourceFile.js index de7895e..92fb49b 100644 --- a/test/removeSourceFile.js +++ b/test/removeSourceFile.js @@ -83,6 +83,7 @@ exports.removeSourceFile = { test.done(); }, 'should remove 2 fields from the PBXFileReference section': function (test) { + debugger proj.addSourceFile('file.m'); var newFile = proj.removeSourceFile('file.m'), fileRefSection = proj.pbxFileReferenceSection(),