Açıklama Yok

applyCustomConfig.js 43KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186
  1. #!/usr/bin/env node
  2. /**********
  3. * Globals
  4. **********/
  5. var TAG = "cordova-custom-config";
  6. var SCRIPT_NAME = "applyCustomConfig.js";
  7. // Pre-existing Cordova npm modules
  8. var deferral, path, cwd;
  9. // Npm dependencies
  10. var logger,
  11. fs,
  12. _ ,
  13. et,
  14. plist,
  15. xcode,
  16. tostr,
  17. os,
  18. fileUtils;
  19. // Other globals
  20. var hooksPath;
  21. var applyCustomConfig = (function(){
  22. /**********************
  23. * Internal properties
  24. *********************/
  25. /*
  26. * Constants
  27. */
  28. var defaultHook = "after_prepare";
  29. var elementPrefix = "custom-";
  30. var androidActivityNames = [
  31. "CordovaApp", // Cordova <= 4.2.0
  32. "MainActivity" // Cordova >= 4.3.0
  33. ];
  34. // Tags that can appear multiple times
  35. // Specified by parent and distinguished by name or label
  36. var androidMultiples = [
  37. {
  38. tag: "uses-permission",
  39. parent: "./",
  40. uniqueBy: "name"
  41. },
  42. {
  43. tag: "permission",
  44. parent: "./",
  45. uniqueBy: "name"
  46. },
  47. {
  48. tag: "permission-tree",
  49. parent: "./",
  50. uniqueBy: "name"
  51. },
  52. {
  53. tag: "permission-group",
  54. parent: "./",
  55. uniqueBy: "name"
  56. },
  57. {
  58. tag: "instrumentation",
  59. parent: "./",
  60. uniqueBy: "name"
  61. },
  62. {
  63. tag: "uses-configuration",
  64. parent: "./",
  65. uniqueBy: "name"
  66. },
  67. {
  68. tag: "uses-feature",
  69. parent: "./",
  70. uniqueBy: "name"
  71. },
  72. {
  73. tag: "compatible-screens",
  74. parent: "./",
  75. uniqueBy: "name"
  76. },
  77. {
  78. tag: "activity",
  79. parent: "./application",
  80. uniqueBy: "name"
  81. },
  82. {
  83. tag: "activity-alias",
  84. parent: "./application",
  85. uniqueBy: "name"
  86. },
  87. {
  88. tag: "service",
  89. parent: "./application",
  90. uniqueBy: "name"
  91. },
  92. {
  93. tag: "receiver",
  94. parent: "./application",
  95. uniqueBy: "name"
  96. },
  97. {
  98. tag: "provider",
  99. parent: "./application",
  100. uniqueBy: "name"
  101. },
  102. {
  103. tag: "uses-library",
  104. parent: "./application",
  105. uniqueBy: "name"
  106. },
  107. {
  108. tag: "meta-data",
  109. parent: "./application",
  110. uniqueBy: "name"
  111. },
  112. {
  113. tag: "intent-filter",
  114. parent: "./application/activity/[@android:name='MainActivity']",
  115. uniqueBy: "label"
  116. },
  117. {
  118. tag: "meta-data",
  119. parent: "./application/activity/[@android:name='MainActivity']",
  120. uniqueBy: "name"
  121. }
  122. ];
  123. var xcconfigs = ["build.xcconfig", "build-extras.xcconfig", "build-debug.xcconfig", "build-release.xcconfig"];
  124. var manifestPath = {
  125. cordovaAndroid6: 'platforms/android/AndroidManifest.xml',
  126. cordovaAndroid7: 'platforms/android/app/src/main/AndroidManifest.xml'
  127. };
  128. // Variables
  129. var applyCustomConfig = {}, rootdir, plugindir, context, configXml, projectName, settings = {}, updatedFiles = {};
  130. var preferencesData;
  131. var resources;
  132. var syncOperationsComplete = false;
  133. var asyncOperationsRemaining = 0;
  134. // Filepath to AndroidManifest.xml in android platform project
  135. var androidManifestFilePath;
  136. // Indicates if project is using new cordova@7 structure
  137. var isNewCordovaAndroid;
  138. /*********************
  139. * Internal functions
  140. *********************/
  141. // Converts an elementtree object to an xml string. Since this is used for plist values, we don't care about attributes
  142. function eltreeToXmlString(data) {
  143. var tag = data.tag;
  144. var el = '<' + tag + '>';
  145. if(data.text && data.text.trim()) {
  146. el += data.text.trim();
  147. } else {
  148. _.each(data.getchildren(), function (child) {
  149. el += eltreeToXmlString(child);
  150. });
  151. }
  152. el += '</' + tag + '>';
  153. return el;
  154. }
  155. function getElements(elementName, pathPrefix){
  156. var path = (pathPrefix || '') + elementPrefix + elementName;
  157. logger.debug("Searching config.xml for prefixed elements: " + path);
  158. var els = configXml.findall(path);
  159. if(settings["parse_unprefixed"] === 'true' || (!isNewCordovaAndroid && settings["parse_unprefixed"] !== 'false')){
  160. path = (pathPrefix || '') + elementName;
  161. logger.debug("Searching config.xml for unprefixed elements: " + path);
  162. els = els.concat(configXml.findall(path));
  163. }
  164. return els;
  165. }
  166. /**
  167. * Retrieves all <preferences ..> from config.xml and returns a map of preferences with platform as the key.
  168. * If a platform is supplied, common prefs + platform prefs will be returned, otherwise just common prefs are returned.
  169. */
  170. function getPlatformPreferences(platform) {
  171. //init common config.xml prefs if we haven't already
  172. if(!preferencesData) {
  173. preferencesData = {
  174. common: getElements('preference')
  175. };
  176. }
  177. var prefs = preferencesData.common || [];
  178. if(platform) {
  179. if(!preferencesData[platform]) {
  180. preferencesData[platform] = getElements('preference','platform[@name=\'' + platform + '\']/');
  181. }
  182. prefs = prefs.concat(preferencesData[platform]);
  183. }
  184. return prefs;
  185. }
  186. /**
  187. * Retrieves all <resource> from config.xml and returns a map of resources with platform as the key.
  188. */
  189. function getPlatformResources(platform) {
  190. if(!resources) {
  191. resources = {};
  192. }
  193. if(!resources[platform]) {
  194. resources[platform] = getElements('resource','platform[@name=\'' + platform + '\']/');
  195. }
  196. return resources[platform];
  197. }
  198. /**
  199. * Implementation of _.keyBy so old versions of lodash (<2.0.0) don't cause issues
  200. */
  201. function keyBy(arr, fn){
  202. var result = {};
  203. arr.forEach(function(v){
  204. result[fn(v)] = v;
  205. });
  206. return result;
  207. }
  208. /**
  209. * Retrieves all configured xml for a specific platform/target/parent element nested inside a platforms config-file
  210. * element within the config.xml. The config-file elements are then indexed by target|parent so if there are
  211. * any config-file elements per platform that have the same target and parent, the last config-file element is used.
  212. */
  213. function getConfigFilesByTargetAndParent(platform) {
  214. var configFileData = getElements('config-file','platform[@name=\'' + platform + '\']');
  215. var result = keyBy(configFileData, function(item) {
  216. var parent = item.attrib.parent;
  217. var mode;
  218. if (item.attrib.add){
  219. logger.warn("add=\"true\" is deprecated. Change to mode=\"add\".");
  220. mode = "add";
  221. }
  222. if (item.attrib.mode){
  223. mode = item.attrib.mode;
  224. }
  225. //if parent attribute is undefined /* or */, set parent to top level elementree selector
  226. if(!parent || parent === '/*' || parent === '*/') {
  227. parent = './';
  228. }
  229. return item.attrib.target + '|' + parent + '|' + mode;
  230. });
  231. return result;
  232. }
  233. /**
  234. * Parses the config.xml's preferences and config-file elements for a given platform
  235. * @param platform
  236. * @returns {{}}
  237. */
  238. function parseConfigXml(platform) {
  239. var configData = {};
  240. parsePlatformPreferences(configData, platform);
  241. parseConfigFiles(configData, platform);
  242. parseResources(configData, platform);
  243. return configData;
  244. }
  245. /**
  246. * Retrieves the config.xml's preferences for a given platform and parses them into JSON data
  247. * @param configData
  248. * @param platform
  249. */
  250. function parsePlatformPreferences(configData, platform) {
  251. var preferences = getPlatformPreferences(platform);
  252. switch(platform){
  253. case "ios":
  254. parseiOSPreferences(preferences, configData);
  255. break;
  256. case "android":
  257. parseAndroidPreferences(preferences, configData);
  258. break;
  259. }
  260. }
  261. /**
  262. * Parses iOS preferences into project.pbxproj
  263. * @param preferences
  264. * @param configData
  265. */
  266. function parseiOSPreferences(preferences, configData){
  267. var hasPbxProjPrefs = false;
  268. _.each(preferences, function (preference) {
  269. if(preference.attrib.name.match(new RegExp("^ios-"))){
  270. hasPbxProjPrefs = true;
  271. var parts = preference.attrib.name.split("-"),
  272. target = "project.pbxproj",
  273. prefData = {
  274. type: parts[1],
  275. name: parts[2],
  276. value: preference.attrib.value
  277. };
  278. if(preference.attrib.buildType){
  279. prefData["buildType"] = preference.attrib.buildType;
  280. }
  281. if(preference.attrib.quote){
  282. prefData["quote"] = preference.attrib.quote;
  283. }
  284. if(preference.attrib.func){
  285. prefData["func"] = preference.attrib.func;
  286. prefData["args"] = [];
  287. _.each(preference.getchildren(), function (arg) {
  288. if (arg.tag === "arg") {
  289. var value;
  290. switch (arg.attrib.type) {
  291. case "Null":
  292. value = null;
  293. break;
  294. case "Undefined":
  295. value = undefined;
  296. break;
  297. case "Object":
  298. value = JSON.parse(arg.attrib.value);
  299. break;
  300. case "Number":
  301. value = Number(arg.attrib.value);
  302. break;
  303. case "String":
  304. value = String(arg.attrib.value);
  305. break;
  306. case "Symbol":
  307. value = Symbol(arg.attrib.value);
  308. break;
  309. default:
  310. value = arg.attrib.value;
  311. break;
  312. }
  313. if (arg.attrib.flag !== undefined) {
  314. switch (arg.attrib.flag) {
  315. case "path":
  316. value = path.isAbsolute(value) ? value : path.join("../../", value);
  317. break;
  318. }
  319. }
  320. prefData["args"].push(value);
  321. }
  322. });
  323. }
  324. prefData["xcconfigEnforce"] = preference.attrib.xcconfigEnforce ? preference.attrib.xcconfigEnforce : null;
  325. if(!configData[target]) {
  326. configData[target] = [];
  327. }
  328. configData[target].push(prefData);
  329. }
  330. });
  331. if(hasPbxProjPrefs){
  332. asyncOperationsRemaining++;
  333. }
  334. }
  335. /**
  336. * Parses supported Android preferences using the preference mapping into the appropriate XML elements in AndroidManifest.xml
  337. * @param preferences
  338. * @param configData
  339. */
  340. function parseAndroidPreferences(preferences, configData){
  341. var type = 'preference';
  342. _.each(preferences, function (preference) {
  343. // Extract pre-defined preferences (deprecated)
  344. var target,
  345. prefData;
  346. if(preference.attrib.name.match(/^android-manifest\//)){
  347. // Extract manifest Xpath preferences
  348. var parts = preference.attrib.name.split("/"),
  349. destination = parts.pop();
  350. parts.shift();
  351. prefData = {
  352. parent: parts.join("/") || "./",
  353. type: type,
  354. destination: destination,
  355. data: preference
  356. };
  357. target = "AndroidManifest.xml";
  358. }
  359. if(prefData){
  360. if(!configData[target]) {
  361. configData[target] = [];
  362. }
  363. configData[target].push(prefData);
  364. }
  365. });
  366. }
  367. /**
  368. * Retrieves the config.xml's config-file elements for a given platform and parses them into JSON data
  369. * @param configData
  370. * @param platform
  371. */
  372. function parseConfigFiles(configData, platform) {
  373. var configFiles = getConfigFilesByTargetAndParent(platform),
  374. type = 'configFile';
  375. _.each(configFiles, function (configFile, key) {
  376. var keyParts = key.split('|');
  377. var target = keyParts[0];
  378. var parent = keyParts[1];
  379. var mode = keyParts[2];
  380. var items = configData[target] || [];
  381. var children = configFile.getchildren();
  382. if(children.length > 0){
  383. _.each(children, function (element) {
  384. items.push({
  385. parent: parent,
  386. type: type,
  387. destination: element.tag,
  388. data: element,
  389. mode: mode
  390. });
  391. });
  392. }else{
  393. items.push({
  394. parent: parent,
  395. type: type,
  396. mode: mode
  397. });
  398. }
  399. configData[target] = items;
  400. });
  401. }
  402. /**
  403. * Retrieves the config.xml's resources for a given platform and parses them into JSON data
  404. * @param configData
  405. * @param platform
  406. */
  407. function parseResources(configData, platform) {
  408. var resources = getPlatformResources(platform);
  409. switch(platform){
  410. case "ios":
  411. parseiOSResources(resources, configData);
  412. break;
  413. case "android":
  414. break;
  415. }
  416. }
  417. /**
  418. * Parses supported iOS resources
  419. * @param resources
  420. * @param configData
  421. */
  422. function parseiOSResources(resources, configData){
  423. _.each(resources, function (resource) {
  424. var resourceData, catalog;
  425. if(resource.attrib.type === "image"){
  426. catalog = resource.attrib.catalog;
  427. target = "asset_catalog."+catalog;
  428. resourceData = {
  429. type: resource.attrib.type,
  430. catalog: catalog,
  431. src: resource.attrib.src,
  432. scale: resource.attrib.scale,
  433. idiom: resource.attrib.idiom
  434. };
  435. }
  436. if(resourceData){
  437. if(!configData[target]) {
  438. configData[target] = [];
  439. }
  440. configData[target].push(resourceData);
  441. }
  442. });
  443. }
  444. /**
  445. * @description Create paths if it's not existing
  446. *
  447. * @param {object} root - root element
  448. * @param {object} item - element to add
  449. *
  450. * @returns {object}
  451. */
  452. function createPath(root, item) {
  453. var paths = item.parent.split('/'),
  454. dir, prevEl, el;
  455. if (paths && paths.length) {
  456. paths.forEach(function (path, index) {
  457. dir = paths.slice(0, index + 1).join('/');
  458. el = root.find(dir);
  459. if (!el) {
  460. el = et.SubElement(prevEl ? prevEl : root, path, {});
  461. }
  462. prevEl = el;
  463. });
  464. }
  465. return root.find(item.parent || root.find('*/' + item.parent));
  466. }
  467. /**
  468. * Updates the AndroidManifest.xml target file with data from config.xml
  469. * @param targetFilePath
  470. * @param configItems
  471. */
  472. function updateAndroidManifest(targetFilePath, configItems) {
  473. var tempManifest = fileUtils.parseElementtreeSync(targetFilePath),
  474. root = tempManifest.getroot();
  475. var isAllowedMultiple = function(tag, parent){
  476. var multipleConfig = null;
  477. _.each(androidMultiples, function(multiple){
  478. if(multiple.tag === tag && multiple.parent === parent){
  479. multipleConfig = multiple;
  480. }
  481. });
  482. return multipleConfig;
  483. };
  484. _.each(configItems, function (item) {
  485. // if parent is not found on the root, child/grandchild nodes are searched
  486. var parentEl = root.find(item.parent) || root.find('*/' + item.parent),
  487. parentSelector,
  488. data = item.data,
  489. childSelector = item.destination,
  490. childEl;
  491. _.each(androidActivityNames, function(activityName){
  492. if(parentEl){
  493. return;
  494. }
  495. parentSelector = item.parent.replace("{ActivityName}", activityName);
  496. parentEl = root.find(parentSelector) || root.find('*/' + parentSelector);
  497. });
  498. if (item.type === 'preference' && !parentEl) {
  499. parentEl = createPath(root, item);
  500. }
  501. if (!parentEl) {
  502. return;
  503. }
  504. if (item.type === 'preference') {
  505. logger.debug("**PREFERENCE"); //logger.dump(item);
  506. logger.debug("**parentEl"); //logger.dump(parentEl);
  507. if(data.attrib['delete'] === 'true') {
  508. logger.debug("Deleting preference");
  509. childEl = parentEl.find(childSelector);
  510. logger.debug("**childEl"); //logger.dump(childEl);
  511. if(childEl) {
  512. parentEl.remove(childEl);
  513. logger.debug("Deleted preference from parent");
  514. } else {
  515. childEl = root.find('*/' + childSelector);
  516. if (childEl) {
  517. root.remove(childEl);
  518. logger.debug("Deleted preference from root");
  519. }
  520. }
  521. } else {
  522. parentEl.attrib[childSelector.replace("@",'')] = data.attrib['value'];
  523. }
  524. } else { // item.type === 'configFile'
  525. logger.debug("**CONFIG-FILE");
  526. //logger.dump(item);
  527. logger.debug("childSelector: " + childSelector);
  528. var multiple = isAllowedMultiple(childSelector, item.parent);
  529. logger.debug("isAllowedMultiple: "+ !!multiple);
  530. var copyDataToElement = function(el){
  531. // copy all config.xml data except for the generated _id property
  532. _.each(data, function (prop, propName) {
  533. if(propName !== '_id') {
  534. el[propName] = prop;
  535. }
  536. });
  537. };
  538. if(multiple){
  539. var uniqueSelector = childSelector + "[@android:"+multiple.uniqueBy+"='" + data.attrib["android:"+multiple.uniqueBy] + "']";
  540. childEl = parentEl.find(uniqueSelector);
  541. // if child el is not found by unique selector, compare child contents
  542. if(!childEl){
  543. var similarEls = parentEl.findall(childSelector);
  544. if(similarEls){
  545. var targetEl = new et.Element(item.destination);
  546. copyDataToElement(targetEl);
  547. var compareEls; compareEls = function(el1, el2){
  548. if(el1.tag !== el2.tag) return false;
  549. if(el1.text !== el2.text) return false;
  550. for(var name in el1.attrib){
  551. if(!el2.attrib[name] || el1.attrib[name] !== el2.attrib[name]) return false;
  552. }
  553. if(el1._children.length !== el2._children.length) return false;
  554. if(el1._children.length > 0){
  555. for(var i=0; i<el1._children.length; i++){
  556. if(compareEls(el1._children[i], el2._children[i]) === false){
  557. return false;
  558. }
  559. }
  560. }
  561. return true;
  562. };
  563. similarEls.forEach(function(similarEl){
  564. if(compareEls(targetEl, similarEl) === true){
  565. childEl = similarEl;
  566. return false;
  567. }
  568. });
  569. }
  570. }
  571. }else{
  572. childEl = parentEl.find(childSelector);
  573. }
  574. logger.debug("**childEl"); //logger.dump(childEl);
  575. // if child element doesnt exist, create new element
  576. if(!childEl || item.mode === 'add') {
  577. childEl = new et.Element(item.destination);
  578. parentEl.append(childEl);
  579. }
  580. copyDataToElement(childEl);
  581. }
  582. });
  583. fs.writeFileSync(targetFilePath, tempManifest.write({indent: 4}), 'utf-8');
  584. logger.verbose("Wrote file " + targetFilePath);
  585. }
  586. /**
  587. * Updates target file with data from config.xml
  588. * @param targetFilePath
  589. * @param configItems
  590. */
  591. function updateWp8Manifest(targetFilePath, configItems) {
  592. var tempManifest = fileUtils.parseElementtreeSync(targetFilePath),
  593. root = tempManifest.getroot();
  594. _.each(configItems, function (item) {
  595. // if parent is not found on the root, child/grandchild nodes are searched
  596. var parentEl = root.find(item.parent) || root.find('*/' + item.parent),
  597. parentSelector,
  598. data = item.data,
  599. childSelector = item.destination,
  600. childEl;
  601. if(!parentEl) {
  602. return;
  603. }
  604. _.each(data.attrib, function (prop, propName) {
  605. childSelector += '[@'+propName+'="'+prop+'"]';
  606. });
  607. childEl = parentEl.find(childSelector);
  608. // if child element doesnt exist, create new element
  609. if(!childEl) {
  610. childEl = new et.Element(item.destination);
  611. parentEl.append(childEl);
  612. }
  613. // copy all config.xml data except for the generated _id property
  614. _.each(data, function (prop, propName) {
  615. if(propName !== '_id') {
  616. childEl[propName] = prop;
  617. }
  618. });
  619. });
  620. fs.writeFileSync(targetFilePath, tempManifest.write({indent: 4}), 'utf-8');
  621. logger.verbose("Wrote file " + targetFilePath);
  622. }
  623. /**
  624. * Updates the *-Info.plist file with data from config.xml by parsing to an xml string, then using the plist
  625. * module to convert the data to a map. The config.xml data is then replaced or appended to the original plist file
  626. * @param targetFilePath
  627. * @param configItems
  628. */
  629. function updateIosPlist (targetFilePath, configItems) {
  630. var infoPlist = plist.parse(fs.readFileSync(targetFilePath, 'utf-8')),
  631. tempInfoPlist;
  632. _.each(configItems, function (item) {
  633. var key = item.parent;
  634. var plistXml = '<plist><dict><key>' + key + '</key>';
  635. var value;
  636. if(item.data){
  637. plistXml += eltreeToXmlString(item.data) + '</dict></plist>';
  638. var configPlistObj = plist.parse(plistXml);
  639. value = configPlistObj[key];
  640. if (!value && item.data.tag === "string") {
  641. value = "";
  642. }
  643. }
  644. //logger.dump(item);
  645. if (item.mode === 'delete') {
  646. delete infoPlist[key];
  647. }else if (item.data.tag === "array" && infoPlist[key] && item.mode !== 'replace') {
  648. infoPlist[key] = infoPlist[key].concat(value).filter(onlyUnique);
  649. } else {
  650. infoPlist[key] = value;
  651. }
  652. logger.verbose("Wrote to plist; key=" + key + "; value=" + tostr(infoPlist[key]));
  653. });
  654. tempInfoPlist = plist.build(infoPlist);
  655. tempInfoPlist = tempInfoPlist.replace(/<string>[\s\r\n]*<\/string>/g,'<string></string>');
  656. fs.writeFileSync(targetFilePath, tempInfoPlist, 'utf-8');
  657. logger.verbose("Wrote file " + targetFilePath);
  658. }
  659. /**
  660. * Updates the *-Prefix.pch file file with data from config.xml
  661. */
  662. function updateIosPch (targetFilePath, configItems) {
  663. var content = fs.readFileSync(targetFilePath, 'utf-8');
  664. var strings = [];
  665. _.each(configItems, function (item) {
  666. if (item.data.tag === "string") {
  667. item.data.text && content.indexOf(item.data.text.trim()) === -1 && strings.push(item.data.text.trim());
  668. } else if (item.data.tag === "array") {
  669. _.each(item.data.getchildren(), function (child) {
  670. child.text && content.indexOf(child.text.trim()) === -1 && strings.push(child.text.trim());
  671. });
  672. }
  673. if (strings.length) {
  674. fs.appendFileSync(targetFilePath, os.EOL + strings.join(os.EOL) + os.EOL, { encoding: 'utf-8' });
  675. }
  676. });
  677. }
  678. /**
  679. * Updates the project.pbxproj file with data from config.xml
  680. * @param {String} xcodeProjectPath - path to XCode project file
  681. * @param {Array} configItems - config items to update project file with
  682. */
  683. function updateIosPbxProj(xcodeProjectPath, configItems) {
  684. var xcodeProject = xcode.project(xcodeProjectPath);
  685. xcodeProject.parse(function(err){
  686. if(err){
  687. // shell is undefined if android platform has been removed and added with a new package id but ios stayed the same.
  688. var msg = 'An error occurred during parsing of [' + xcodeProjectPath + ']: ' + JSON.stringify(err);
  689. if(typeof shell !== "undefined" && shell !== null){
  690. shell.echo(msg);
  691. } else{
  692. logger.error(msg + ' - Maybe you forgot to remove/add the ios platform?');
  693. }
  694. }else{
  695. _.each(configItems, function (item) {
  696. switch(item.type){
  697. case "XCBuildConfiguration":
  698. var buildConfig = xcodeProject.pbxXCBuildConfigurationSection();
  699. var replaced = updateXCBuildConfiguration(item, buildConfig, "replace");
  700. if(!replaced){
  701. updateXCBuildConfiguration(item, buildConfig, "add");
  702. }
  703. break;
  704. case "xcodefunc":
  705. if (typeof (xcodeProject[item.func]) === "function") {
  706. xcodeProject[item.func].apply(xcodeProject, item.args);
  707. }
  708. break;
  709. }
  710. });
  711. fs.writeFileSync(xcodeProjectPath, xcodeProject.writeSync(), 'utf-8');
  712. logger.verbose("Wrote file " + xcodeProjectPath);
  713. }
  714. asyncOperationsRemaining--;
  715. checkComplete();
  716. });
  717. }
  718. /**
  719. * Updates an XCode build configuration setting with the given item.
  720. * @param {Object} item - configuration item containing setting data
  721. * @param {Object} buildConfig - XCode build config object
  722. * @param {String} mode - update mode: "replace" to replace only existing keys or "add" to add a new key to every block
  723. * @returns {boolean} true if buildConfig was modified
  724. */
  725. function updateXCBuildConfiguration(item, buildConfig, mode){
  726. var modified = false;
  727. for(var blockName in buildConfig){
  728. var block = buildConfig[blockName];
  729. if(typeof(block) !== "object" || !(block["buildSettings"])) continue;
  730. var literalMatch = !!block["buildSettings"][item.name],
  731. quotedMatch = !!block["buildSettings"][quoteEscape(item.name)],
  732. match = literalMatch || quotedMatch;
  733. if((match || mode === "add") &&
  734. (!item.buildType || item.buildType.toLowerCase() === block['name'].toLowerCase())){
  735. var name;
  736. if(match){
  737. name = literalMatch ? item.name : quoteEscape(item.name);
  738. }else{
  739. // adding
  740. name = (item.quote && (item.quote === "none" || item.quote === "value")) ? item.name : quoteEscape(item.name);
  741. }
  742. var value = (item.quote && (item.quote === "none" || item.quote === "key")) ? item.value : quoteEscape(item.value);
  743. block["buildSettings"][name] = value;
  744. modified = true;
  745. logger.verbose(mode+" XCBuildConfiguration key={ "+name+" } to value={ "+value+" } for build type='"+block['name']+"' in block='"+blockName+"'");
  746. }
  747. }
  748. return modified;
  749. }
  750. /**
  751. * Checks if Cordova's .xcconfig files contain overrides for the given setting, and if so overwrites the value in the .xcconfig file(s).
  752. */
  753. function updateXCConfigs(configItems, platformPath){
  754. xcconfigs.forEach(function(fileName){
  755. updateXCConfig(platformPath, fileName, configItems);
  756. });
  757. }
  758. function updateXCConfig(platformPath, targetFileName, configItems){
  759. var modified = false,
  760. targetFilePath = path.join(platformPath, 'cordova', targetFileName);
  761. // Read file contents
  762. logger.verbose("Reading "+targetFileName);
  763. var fileContents = fs.readFileSync(targetFilePath, 'utf-8');
  764. _.each(configItems, function (item) {
  765. // some keys have name===undefined; ignore these.
  766. if (item.name) {
  767. var escapedName = regExpEscape(item.name);
  768. var fileBuildType = "none";
  769. if(targetFileName.match("release")){
  770. fileBuildType = "release";
  771. }else if(targetFileName.match("debug")){
  772. fileBuildType = "debug";
  773. }
  774. var itemBuildType = item.buildType ? item.buildType.toLowerCase() : "none";
  775. var name = item.name;
  776. var value = item.value;
  777. var doReplace = function(){
  778. fileContents = fileContents.replace(new RegExp("\n\"?"+escapedName+"\"?.*"), "\n"+name+" = "+value);
  779. logger.verbose("Overwrote "+item.name+" with '"+item.value+"' in "+targetFileName);
  780. modified = true;
  781. };
  782. // If item's target build type matches the xcconfig build type
  783. if(itemBuildType === fileBuildType)
  784. {
  785. // If config.xml contains any #include statements for use in .xcconfig files
  786. if(item.name.match("#INCLUDE") && !fileContents.match(value)) {
  787. fileContents += '\n#include "' + value + '"';
  788. modified = true;
  789. } else {
  790. // If file contains the item, replace it with configured value
  791. if (fileContents.match(escapedName) && item.xcconfigEnforce !== "false") {
  792. doReplace();
  793. } else // presence of item is being enforced, so add it to the relevant .xcconfig
  794. if (item.xcconfigEnforce === "true") {
  795. fileContents += "\n" + name + " = " + value;
  796. modified = true;
  797. }
  798. }
  799. }else
  800. // if item is a Debug CODE_SIGNING_IDENTITY, this is a special case: Cordova places its default Debug CODE_SIGNING_IDENTITY in build.xcconfig (not build-debug.xcconfig)
  801. // so if buildType="debug", want to overrwrite in build.xcconfig
  802. if(item.name.match("CODE_SIGN_IDENTITY") && itemBuildType === "debug" && fileBuildType === "none" && !item.xcconfigEnforce){
  803. doReplace();
  804. }
  805. }
  806. });
  807. if(modified){
  808. ensureBackup(targetFilePath, 'ios', targetFileName);
  809. fs.writeFileSync(targetFilePath, fileContents, 'utf-8');
  810. logger.verbose("Overwrote "+targetFileName);
  811. }
  812. }
  813. function deployAssetCatalog(targetName, targetDirPath, configItems){
  814. var contents;
  815. var contentsFilePath = path.join(targetDirPath, "Contents.json");
  816. if(!fileUtils.directoryExists(targetDirPath)){
  817. fileUtils.createDirectory(targetDirPath);
  818. contents = fs.readFileSync(path.join(plugindir, "templates", "ios", "Contents.json"), 'utf-8');
  819. }else{
  820. contents = fs.readFileSync(contentsFilePath);
  821. }
  822. try{
  823. contents = JSON.parse(contents);
  824. }catch(e){
  825. logger.error("Unable to parse Contents.json of asset catalog '" + targetName + "' - aborting deployment ");
  826. return;
  827. }
  828. _.each(configItems, function (item) {
  829. var srcImgFilePath = path.join(cwd, item.src);
  830. if(!fileUtils.fileExists(srcImgFilePath)){
  831. logger.error("Resource file not found: "+item.src+" ("+srcImgFilePath+")");
  832. return;
  833. }
  834. var srcImgFileName = srcImgFilePath.split(path.sep).pop();
  835. var targetImgFilePath = path.join(targetDirPath, srcImgFileName);
  836. if(fileUtils.fileExists(targetImgFilePath)){
  837. logger.verbose("Resource file already exists: "+item.src+" ("+targetImgFilePath+")");
  838. return;
  839. }
  840. // Copy source image
  841. fileUtils.copySync(srcImgFilePath, targetImgFilePath);
  842. // Create JSON entry
  843. if(!item.scale || !item.scale.match(/^[1-3]{1}x$/)){
  844. logger.error("scale must be specified as 1x, 2x or 3x for "+srcImgFileName+" in "+targetName+" asset catalog - skipping image");
  845. return;
  846. }
  847. var entry = {
  848. filename: srcImgFileName,
  849. scale: item.scale,
  850. idiom: item.idiom || "universal"
  851. };
  852. contents.images.push(entry);
  853. fs.writeFileSync(contentsFilePath, JSON.stringify(contents));
  854. });
  855. }
  856. function regExpEscape(literal_string) {
  857. return literal_string.replace(/[-[\]{}()*+!<=:?.\/\\^$|#\s,]/g, '\\$&');
  858. }
  859. function quoteEscape(value){
  860. return '"'+value+'"';
  861. }
  862. function onlyUnique(value, index, self){
  863. return self.indexOf(value) === index;
  864. }
  865. function ensureBackup(targetFilePath, platform, targetFileName){
  866. var backupDirPath = path.join(plugindir, "backup"),
  867. backupPlatformPath = path.join(backupDirPath, platform),
  868. backupFilePath = path.join(backupPlatformPath, targetFileName);
  869. var backupDirExists = fileUtils.directoryExists(backupDirPath);
  870. if(!backupDirExists){
  871. fileUtils.createDirectory(backupDirPath);
  872. logger.verbose("Created backup directory: "+backupDirPath);
  873. }
  874. var backupPlatformExists = fileUtils.directoryExists(backupPlatformPath);
  875. if(!backupPlatformExists){
  876. fileUtils.createDirectory(backupPlatformPath);
  877. logger.verbose("Created backup platform directory: "+backupPlatformPath);
  878. }
  879. var backupFileExists = fileUtils.fileExists(backupFilePath);
  880. if(!backupFileExists){
  881. fileUtils.copySync(targetFilePath, backupFilePath);
  882. logger.verbose("Backed up "+targetFilePath+" to "+backupFilePath);
  883. }else{
  884. logger.verbose("Backup exists for '"+targetFileName+"' at: "+backupFilePath);
  885. }
  886. if(!updatedFiles[targetFilePath]){
  887. logger.log("Applied custom config from config.xml to "+targetFilePath);
  888. updatedFiles[targetFilePath] = true;
  889. }
  890. }
  891. /**
  892. * Parses config.xml data, and update each target file for a specified platform
  893. * @param platform
  894. */
  895. function updatePlatformConfig(platform) {
  896. if(platform === "android"){
  897. getAndroidManifestFilePath();
  898. }
  899. var configData = parseConfigXml(platform),
  900. platformPath = path.join(rootdir, 'platforms', platform);
  901. _.each(configData, function (configItems, targetName) {
  902. var targetFilePath;
  903. if (platform === 'ios') {
  904. if (targetName.indexOf("Info.plist") > -1) {
  905. targetName = projectName + '-Info.plist';
  906. targetFilePath = path.join(platformPath, projectName, targetName);
  907. ensureBackup(targetFilePath, platform, targetName);
  908. updateIosPlist(targetFilePath, configItems);
  909. }else if (targetName === "project.pbxproj") {
  910. targetFilePath = path.join(platformPath, projectName + '.xcodeproj', targetName);
  911. ensureBackup(targetFilePath, platform, targetName);
  912. updateIosPbxProj(targetFilePath, configItems);
  913. updateXCConfigs(configItems, platformPath);
  914. }else if (targetName.indexOf("Entitlements-Release.plist") > -1) {
  915. targetFilePath = path.join(platformPath, projectName, targetName);
  916. ensureBackup(targetFilePath, platform, targetName);
  917. updateIosPlist(targetFilePath, configItems);
  918. }else if (targetName.indexOf("Entitlements-Debug.plist") > -1) {
  919. targetFilePath = path.join(platformPath, projectName, targetName);
  920. ensureBackup(targetFilePath, platform, targetName);
  921. updateIosPlist(targetFilePath, configItems);
  922. }else if (targetName.indexOf("Prefix.pch") > -1) {
  923. targetName = projectName + '-Prefix.pch';
  924. targetFilePath = path.join(platformPath, projectName, targetName);
  925. ensureBackup(targetFilePath, platform, targetName);
  926. updateIosPch(targetFilePath, configItems);
  927. }else if (targetName.indexOf("asset_catalog") > -1) {
  928. targetName = targetName.split('.')[1];
  929. var targetDirPath = path.join(platformPath, projectName, "Images.xcassets", targetName+".imageset");
  930. deployAssetCatalog(targetName, targetDirPath, configItems);
  931. }
  932. } else if (platform === 'android' && targetName === 'AndroidManifest.xml') {
  933. targetFilePath = androidManifestFilePath;
  934. ensureBackup(targetFilePath, platform, targetName);
  935. updateAndroidManifest(targetFilePath, configItems);
  936. } else if (platform === 'wp8') {
  937. targetFilePath = path.join(platformPath, targetName);
  938. ensureBackup(targetFilePath, platform, targetName);
  939. updateWp8Manifest(targetFilePath, configItems);
  940. }
  941. });
  942. }
  943. function getAndroidManifestFilePath(){
  944. var cordovaAndroid6Path = path.join(rootdir, manifestPath.cordovaAndroid6);
  945. var cordovaAndroid7Path = path.join(rootdir, manifestPath.cordovaAndroid7);
  946. if(fileUtils.fileExists(cordovaAndroid7Path)){
  947. isNewCordovaAndroid = true;
  948. androidManifestFilePath = cordovaAndroid7Path;
  949. }else if(fileUtils.fileExists(cordovaAndroid6Path)){
  950. isNewCordovaAndroid = false;
  951. androidManifestFilePath = cordovaAndroid6Path;
  952. }else{
  953. throw "Can't find AndroidManifest.xml in platforms/Android";
  954. }
  955. }
  956. /**
  957. * Script operations are complete, so resolve deferred promises
  958. */
  959. function complete(){
  960. logger.verbose("Finished applying platform config");
  961. deferral.resolve();
  962. }
  963. function checkComplete(){
  964. if(syncOperationsComplete && asyncOperationsRemaining === 0){
  965. complete();
  966. }
  967. }
  968. /*************
  969. * Public API
  970. *************/
  971. applyCustomConfig.loadDependencies = function(ctx){
  972. fs = require('fs'),
  973. _ = require('lodash'),
  974. et = require('elementtree'),
  975. plist = require('plist'),
  976. xcode = require('xcode'),
  977. tostr = require('tostr'),
  978. os = require('os'),
  979. fileUtils = require(path.resolve(hooksPath, "fileUtils.js"))(ctx);
  980. logger.verbose("Loaded module dependencies");
  981. };
  982. applyCustomConfig.init = function(ctx){
  983. context = ctx;
  984. rootdir = context.opts.projectRoot;
  985. plugindir = path.join(cwd, 'plugins', context.opts.plugin.id);
  986. configXml = fileUtils.getConfigXml();
  987. projectName = fileUtils.getProjectName();
  988. settings = fileUtils.getSettings();
  989. var runHook = settings.hook ? settings.hook : defaultHook;
  990. if(context.hook !== runHook){
  991. logger.debug("Aborting applyCustomConfig.js because current hook '"+context.hook+"' is not configured hook '"+runHook+"'");
  992. return complete();
  993. }
  994. // go through each of the context platforms
  995. _.each(context.opts.platforms, function (platform, index) {
  996. platform = platform.trim().toLowerCase();
  997. try{
  998. updatePlatformConfig(platform);
  999. if(index === context.opts.platforms.length - 1){
  1000. syncOperationsComplete = true;
  1001. checkComplete();
  1002. }
  1003. }catch(e){
  1004. var msg = "Error updating config for platform '"+platform+"': "+ e.message;
  1005. logger.error(msg);
  1006. logger.dump(e);
  1007. if(settings.stoponerror){
  1008. deferral.reject(TAG + ": " +msg);
  1009. }
  1010. }
  1011. });
  1012. };
  1013. return applyCustomConfig;
  1014. })();
  1015. // Main
  1016. module.exports = function(ctx) {
  1017. try{
  1018. deferral = require('q').defer();
  1019. path = require('path');
  1020. cwd = path.resolve();
  1021. hooksPath = path.resolve(ctx.opts.projectRoot, "plugins", ctx.opts.plugin.id, "hooks");
  1022. logger = require(path.resolve(hooksPath, "logger.js"))(ctx);
  1023. applyCustomConfig.loadDependencies(ctx);
  1024. }catch(e){
  1025. e.message = TAG + ": Error loading dependencies for "+SCRIPT_NAME+" - ensure the plugin has been installed via cordova-fetch or run 'npm install cordova-custom-config': "+e.message;
  1026. if(typeof deferral !== "undefined"){
  1027. deferral.reject(e.message);
  1028. return deferral.promise;
  1029. }
  1030. throw e;
  1031. }
  1032. try{
  1033. logger.verbose("Running " + SCRIPT_NAME);
  1034. applyCustomConfig.init(ctx);
  1035. }catch(e){
  1036. e.message = TAG + ": Error running "+SCRIPT_NAME+": "+e.message;
  1037. if(typeof deferral !== "undefined"){
  1038. deferral.reject(e.message);
  1039. return deferral.promise;
  1040. }
  1041. throw e;
  1042. }
  1043. return deferral.promise;
  1044. };