UNPKG

13.1 kBJavaScriptView Raw
1/*
2 Licensed to the Apache Software Foundation (ASF) under one
3 or more contributor license agreements. See the NOTICE file
4 distributed with this work for additional information
5 regarding copyright ownership. The ASF licenses this file
6 to you under the Apache License, Version 2.0 (the
7 "License"); you may not use this file except in compliance
8 with the License. You may obtain a copy of the License at
9
10 http://www.apache.org/licenses/LICENSE-2.0
11
12 Unless required by applicable law or agreed to in writing,
13 software distributed under the License is distributed on an
14 "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 KIND, either express or implied. See the License for the
16 specific language governing permissions and limitations
17 under the License.
18*/
19'use strict';
20
21const fs = require('fs-extra');
22const path = require('path');
23const util = require('util');
24const execa = require('execa');
25const { CordovaError, events } = require('cordova-common');
26
27Podfile.FILENAME = 'Podfile';
28Podfile.declarationRegexpMap = {
29 'use_frameworks!': 'use[-_]frameworks!?',
30 'inhibit_all_warnings!': 'inhibit[-_]all[-_]warnings!?'
31};
32
33function Podfile (podFilePath, projectName, minDeploymentTarget) {
34 this.declarationToken = '##INSERT_DECLARATION##';
35 this.sourceToken = '##INSERT_SOURCE##';
36 this.podToken = '##INSERT_POD##';
37
38 this.path = podFilePath;
39 this.projectName = projectName;
40 this.minDeploymentTarget = minDeploymentTarget || '11.0';
41 this.contents = null;
42 this.sources = null;
43 this.declarations = null;
44 this.pods = null;
45 this.__dirty = false;
46
47 // check whether it is named Podfile
48 const filename = this.path.split(path.sep).pop();
49 if (filename !== Podfile.FILENAME) {
50 throw new CordovaError(util.format('Podfile: The file at %s is not `%s`.', this.path, Podfile.FILENAME));
51 }
52
53 if (!projectName) {
54 throw new CordovaError('Podfile: The projectName was not specified in the constructor.');
55 }
56
57 if (!fs.existsSync(this.path)) {
58 events.emit('verbose', util.format('Podfile: The file at %s does not exist.', this.path));
59 events.emit('verbose', 'Creating new Podfile in platforms/ios');
60 this.clear();
61 this.write();
62 } else {
63 events.emit('verbose', 'Podfile found in platforms/ios');
64 // parse for pods
65 const fileText = fs.readFileSync(this.path, 'utf8');
66 this.declarations = this.__parseForDeclarations(fileText);
67 this.sources = this.__parseForSources(fileText);
68 this.pods = this.__parseForPods(fileText);
69 }
70}
71
72Podfile.prototype.__parseForDeclarations = function (text) {
73 // split by \n
74 const arr = text.split('\n');
75
76 // getting lines between "platform :ios, '11.0'"" and "target 'HelloCordova'" do
77 const declarationsPreRE = /platform :ios,\s+'[^']+'/;
78 const declarationsPostRE = /target\s+'[^']+'\s+do/;
79 const declarationRE = /^\s*[^#]/;
80
81 return arr.reduce((acc, line) => {
82 switch (acc.state) {
83 case 0:
84 if (declarationsPreRE.exec(line)) {
85 acc.state = 1; // Start to read
86 }
87 break;
88 case 1:
89 if (declarationsPostRE.exec(line)) {
90 acc.state = 2; // Finish to read
91 } else {
92 acc.lines.push(line);
93 }
94 break;
95 case 2:
96 default:
97 // do nothing;
98 }
99 return acc;
100 }, { state: 0, lines: [] })
101 .lines
102 .filter(line => declarationRE.exec(line))
103 .reduce((obj, line) => {
104 obj[line] = line;
105 return obj;
106 }, {});
107};
108
109Podfile.prototype.__parseForSources = function (text) {
110 // split by \n
111 const arr = text.split('\n');
112
113 const sourceRE = /source '(.*)'/;
114 return arr.filter(line => {
115 const m = sourceRE.exec(line);
116
117 return (m !== null);
118 })
119 .reduce((obj, line) => {
120 const m = sourceRE.exec(line);
121 if (m !== null) {
122 const source = m[1];
123 obj[source] = source;
124 }
125 return obj;
126 }, {});
127};
128
129Podfile.prototype.__parseForPods = function (text) {
130 // split by \n
131 const arr = text.split('\n');
132
133 // aim is to match (space insignificant around the comma, comma optional):
134 // pod 'Foobar', '1.2'
135 // pod 'Foobar', 'abc 123 1.2'
136 // pod 'PonyDebugger', :configurations => ['Debug', 'Beta']
137 // var podRE = new RegExp('pod \'([^\']*)\'\\s*,?\\s*(.*)');
138 const podRE = /pod '([^']*)'\s*(?:,\s*'([^']*)'\s*)?,?\s*(.*)/;
139
140 // only grab lines that don't have the pod spec'
141 return arr.filter(line => {
142 const m = podRE.exec(line);
143
144 return (m !== null);
145 })
146 .reduce((obj, line) => {
147 const m = podRE.exec(line);
148
149 if (m !== null) {
150 const podspec = {
151 name: m[1]
152 };
153 if (m[2]) {
154 podspec.spec = m[2];
155 }
156 if (m[3]) {
157 podspec.options = m[3];
158 }
159 obj[m[1]] = podspec; // i.e pod 'Foo', '1.2' ==> { 'Foo' : '1.2'}
160 }
161
162 return obj;
163 }, {});
164};
165
166Podfile.prototype.escapeSingleQuotes = function (string) {
167 return string.replace(/'/g, '\\\'');
168};
169
170Podfile.prototype.getTemplate = function () {
171 // Escaping possible ' in the project name
172 const projectName = this.escapeSingleQuotes(this.projectName);
173 return util.format(
174 '# DO NOT MODIFY -- auto-generated by Apache Cordova\n' +
175 '%s\n' +
176 'platform :ios, \'%s\'\n' +
177 '%s\n' +
178 'target \'%s\' do\n' +
179 '\tproject \'%s.xcodeproj\'\n' +
180 '%s\n' +
181 'end\n',
182 this.sourceToken, this.minDeploymentTarget, this.declarationToken, projectName, projectName, this.podToken);
183};
184
185Podfile.prototype.addSpec = function (name, spec) {
186 name = name || '';
187 // optional
188 spec = spec; /* eslint no-self-assign : 0 */
189
190 if (!name.length) { // blank names are not allowed
191 throw new CordovaError('Podfile addSpec: name is not specified.');
192 }
193
194 if (typeof spec === 'string') {
195 if (spec.startsWith(':')) {
196 spec = { name, options: spec };
197 } else {
198 spec = { name, spec };
199 }
200 }
201
202 this.pods[name] = spec;
203 this.__dirty = true;
204
205 events.emit('verbose', util.format('Added pod line for `%s`', name));
206};
207
208Podfile.prototype.removeSpec = function (name) {
209 if (this.existsSpec(name)) {
210 delete this.pods[name];
211 this.__dirty = true;
212 }
213
214 events.emit('verbose', util.format('Removed pod line for `%s`', name));
215};
216
217Podfile.prototype.getSpec = function (name) {
218 return this.pods[name];
219};
220
221Podfile.prototype.existsSpec = function (name) {
222 return (name in this.pods);
223};
224
225Podfile.prototype.addSource = function (src) {
226 this.sources[src] = src;
227 this.__dirty = true;
228
229 events.emit('verbose', util.format('Added source line for `%s`', src));
230};
231
232Podfile.prototype.removeSource = function (src) {
233 if (this.existsSource(src)) {
234 delete this.sources[src];
235 this.__dirty = true;
236 }
237
238 events.emit('verbose', util.format('Removed source line for `%s`', src));
239};
240
241Podfile.prototype.existsSource = function (src) {
242 return (src in this.sources);
243};
244
245Podfile.prototype.addDeclaration = function (declaration) {
246 this.declarations[declaration] = declaration;
247 this.__dirty = true;
248
249 events.emit('verbose', util.format('Added declaration line for `%s`', declaration));
250};
251
252Podfile.prototype.removeDeclaration = function (declaration) {
253 if (this.existsDeclaration(declaration)) {
254 delete this.declarations[declaration];
255 this.__dirty = true;
256 }
257
258 events.emit('verbose', util.format('Removed source line for `%s`', declaration));
259};
260
261Podfile.proofDeclaration = declaration => {
262 const list = Object.keys(Podfile.declarationRegexpMap).filter(key => {
263 const regexp = new RegExp(Podfile.declarationRegexpMap[key]);
264 return regexp.test(declaration);
265 });
266 if (list.length > 0) {
267 return list[0];
268 }
269 return declaration;
270};
271
272Podfile.prototype.existsDeclaration = function (declaration) {
273 return (declaration in this.declarations);
274};
275
276Podfile.prototype.clear = function () {
277 this.sources = {};
278 this.declarations = {};
279 this.pods = {};
280 this.__dirty = true;
281};
282
283Podfile.prototype.destroy = function () {
284 fs.unlinkSync(this.path);
285 events.emit('verbose', util.format('Deleted `%s`', this.path));
286};
287
288Podfile.prototype.write = function () {
289 let text = this.getTemplate();
290
291 const podsString =
292 Object.keys(this.pods).map(key => {
293 const name = key;
294 const json = this.pods[key];
295
296 if (typeof json === 'string') { // compatibility for using framework tag.
297 const spec = json;
298 if (spec.length) {
299 if (spec.indexOf(':') === 0) {
300 // don't quote it, it's a specification (starts with ':')
301 return util.format('\tpod \'%s\', %s', name, spec);
302 } else {
303 // quote it, it's a version
304 return util.format('\tpod \'%s\', \'%s\'', name, spec);
305 }
306 } else {
307 return util.format('\tpod \'%s\'', name);
308 }
309 } else {
310 const list = [`'${name}'`];
311 if ('spec' in json && json.spec.length) {
312 list.push(`'${json.spec}'`);
313 }
314
315 let options = ['tag', 'branch', 'commit', 'git', 'podspec']
316 .filter(tag => tag in json)
317 .map(tag => `:${tag} => '${json[tag]}'`);
318
319 if ('configurations' in json) {
320 options.push(`:configurations => [${json.configurations.split(',').map(conf => `'${conf.trim()}'`).join(',')}]`);
321 }
322 if ('options' in json) {
323 options = [json.options];
324 }
325 if (options.length > 0) {
326 list.push(options.join(', '));
327 }
328 return util.format('\tpod %s', list.join(', '));
329 }
330 }).join('\n');
331
332 const sourcesString =
333 Object.keys(this.sources).map(key => {
334 const source = this.sources[key];
335 return util.format('source \'%s\'', source);
336 }).join('\n');
337
338 const declarationString =
339 Object.keys(this.declarations).map(key => {
340 const declaration = this.declarations[key];
341 return declaration;
342 }).join('\n');
343
344 text = text.replace(this.podToken, podsString)
345 .replace(this.sourceToken, sourcesString)
346 .replace(this.declarationToken, declarationString);
347
348 fs.writeFileSync(this.path, text, 'utf8');
349 this.__dirty = false;
350
351 events.emit('verbose', 'Wrote to Podfile.');
352};
353
354Podfile.prototype.isDirty = function () {
355 return this.__dirty;
356};
357
358Podfile.prototype.before_install = function (toolOptions) {
359 toolOptions = toolOptions || {};
360
361 // Template tokens in order: project name, project name, debug | release
362 const template =
363 '// DO NOT MODIFY -- auto-generated by Apache Cordova\n' +
364 '#include "Pods/Target Support Files/Pods-%s/Pods-%s.%s.xcconfig"';
365
366 const debugContents = util.format(template, this.projectName, this.projectName, 'debug');
367 const releaseContents = util.format(template, this.projectName, this.projectName, 'release');
368
369 const debugConfigPath = path.join(this.path, '..', 'pods-debug.xcconfig');
370 const releaseConfigPath = path.join(this.path, '..', 'pods-release.xcconfig');
371
372 fs.writeFileSync(debugConfigPath, debugContents, 'utf8');
373 fs.writeFileSync(releaseConfigPath, releaseContents, 'utf8');
374
375 return Promise.resolve(toolOptions);
376};
377
378Podfile.prototype.install = function (requirementsCheckerFunction) {
379 const opts = {};
380 opts.cwd = path.join(this.path, '..'); // parent path of this Podfile
381 opts.stderr = 'inherit';
382
383 if (!requirementsCheckerFunction) {
384 requirementsCheckerFunction = Promise.resolve();
385 }
386
387 return requirementsCheckerFunction()
388 .then(toolOptions => this.before_install(toolOptions))
389 .then(toolOptions => {
390 events.emit('verbose', '==== pod install start ====\n');
391
392 if (toolOptions.ignore) {
393 events.emit('verbose', toolOptions.ignoreMessage);
394 return;
395 }
396
397 const subprocess = execa('pod', ['install', '--verbose'], opts);
398
399 // FIXME: data emitted is not necessarily a complete line
400 subprocess.stdout.on('data', data => {
401 events.emit('verbose', data);
402 });
403
404 return subprocess;
405 })
406 .then(() => { // done
407 events.emit('verbose', '==== pod install end ====\n');
408 });
409};
410
411module.exports.Podfile = Podfile;