Skip to content

Commit 4f3561b

Browse files
committed
Implement ability to use static libraries in iOS projects. Implement tests.
1 parent 4027186 commit 4f3561b

File tree

4 files changed

+196
-30
lines changed

4 files changed

+196
-30
lines changed

lib/definitions/xcode.d.ts

+10-6
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
declare module "xcode" {
2-
interface FrameworkOptions {
2+
interface Options {
33
[key: string]: any;
44

55
customFramework?: boolean;
6-
76
embed?: boolean;
7+
relativePath?: string;
88
}
9-
9+
1010
class project {
1111
constructor(filename: string);
1212

@@ -15,9 +15,13 @@ declare module "xcode" {
1515

1616
writeSync(): string;
1717

18-
addFramework(filepath: string, options?: FrameworkOptions): void;
19-
removeFramework(filePath: string, options?: FrameworkOptions): void;
20-
18+
addFramework(filepath: string, options?: Options): void;
19+
removeFramework(filePath: string, options?: Options): void;
20+
21+
addPbxGroup(filePathsArray: any[], name: string, path: string, sourceTree: string): void;
22+
23+
addToHeaderSearchPaths(options?: Options): void;
24+
removeFromHeaderSearchPaths(options?: Options): void;
2125
updateBuildProperty(key: string, value: any): void;
2226
}
2327
}

lib/services/ios-project-service.ts

+118-23
Original file line numberDiff line numberDiff line change
@@ -197,35 +197,68 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ
197197
public isPlatformPrepared(projectRoot: string): IFuture<boolean> {
198198
return this.$fs.exists(path.join(projectRoot, this.$projectData.projectName, constants.APP_FOLDER_NAME));
199199
}
200-
200+
201201
public addLibrary(libraryPath: string): IFuture<void> {
202202
return (() => {
203-
this.validateFramework(libraryPath).wait();
204-
203+
let extension = path.extname(libraryPath);
204+
if (extension === ".framework") {
205+
this.addDynamicFramework(libraryPath);
206+
} else {
207+
this.$errors.failWithoutHelp("The bundle at %s does not appear to be a dynamic framework package.", libraryPath);
208+
}
209+
}).future<void>()();
210+
}
211+
212+
private addDynamicFramework(frameworkPath: string): IFuture<void> {
213+
return (() => {
214+
this.validateFramework(frameworkPath).wait();
215+
205216
let targetPath = path.join("lib", this.platformData.normalizedPlatformName);
206217
let fullTargetPath = path.join(this.$projectData.projectDir, targetPath);
207218
this.$fs.ensureDirectoryExists(fullTargetPath).wait();
208-
shell.cp("-R", libraryPath, fullTargetPath);
219+
shell.cp("-R", frameworkPath, fullTargetPath);
209220

210221
let project = this.createPbxProj();
211-
212-
let frameworkName = path.basename(libraryPath, path.extname(libraryPath));
213-
let frameworkBinaryPath = path.join(libraryPath, frameworkName);
222+
let frameworkName = path.basename(frameworkPath, path.extname(frameworkPath));
223+
let frameworkBinaryPath = path.join(frameworkPath, frameworkName);
214224
let isDynamic = _.contains(this.$childProcess.exec(`otool -Vh ${frameworkBinaryPath}`).wait(), " DYLIB ");
215225

216-
let frameworkAddOptions: xcode.FrameworkOptions = { customFramework: true };
226+
let frameworkAddOptions: xcode.Options = { customFramework: true };
217227

218228
if(isDynamic) {
219229
frameworkAddOptions["embed"] = true;
220230
project.updateBuildProperty("IPHONEOS_DEPLOYMENT_TARGET", "8.0");
221231
this.$logger.info("The iOS Deployment Target is now 8.0 in order to support Cocoa Touch Frameworks.");
222232
}
223233

224-
let frameworkPath = this.getFrameworkRelativePath(libraryPath);
225-
project.addFramework(frameworkPath, frameworkAddOptions);
234+
let frameworkRelativePath = this.getLibSubpathRelativeToProjectPath(path.basename(frameworkPath));
235+
project.addFramework(frameworkRelativePath, { customFramework: true, embed: true });
226236
this.savePbxProj(project).wait();
227237
}).future<void>()();
228238
}
239+
240+
private addStaticLibrary(staticLibPath: string): IFuture<void> {
241+
return (() => {
242+
this.validateStaticLibrary(staticLibPath).wait();
243+
// Copy files to lib folder.
244+
let libDestinationPath = path.join(this.$projectData.projectDir, path.join("lib", this.platformData.normalizedPlatformName));
245+
let headersSubpath = path.join("include", path.basename(staticLibPath, ".a"));
246+
this.$fs.ensureDirectoryExists(path.join(libDestinationPath, headersSubpath)).wait();
247+
shell.cp("-Rf", staticLibPath, libDestinationPath);
248+
shell.cp("-Rf", path.join(path.dirname(staticLibPath), headersSubpath), path.join(libDestinationPath, "include"));
249+
250+
// Add static library to project file and setup header search paths
251+
let project = this.createPbxProj();
252+
let relativeStaticLibPath = this.getLibSubpathRelativeToProjectPath(path.basename(staticLibPath));
253+
project.addFramework(relativeStaticLibPath);
254+
255+
let relativeHeaderSearchPath = path.join(this.getLibSubpathRelativeToProjectPath(headersSubpath));
256+
project.addToHeaderSearchPaths({ relativePath: relativeHeaderSearchPath });
257+
258+
this.generateMobulemap(path.join(libDestinationPath, headersSubpath), path.basename(staticLibPath, ".a"));
259+
this.savePbxProj(project).wait();
260+
}).future<void>()();
261+
}
229262

230263
public canUpdatePlatform(currentVersion: string, newVersion: string): IFuture<boolean> {
231264
return (() => {
@@ -307,10 +340,9 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ
307340
return name.replace(/\\\"/g, "\"");
308341
}
309342

310-
private getFrameworkRelativePath(libraryPath: string): string {
311-
let frameworkName = path.basename(libraryPath, path.extname(libraryPath));
343+
private getLibSubpathRelativeToProjectPath(subPath: string): string {
312344
let targetPath = path.join("lib", this.platformData.normalizedPlatformName);
313-
let frameworkPath = path.relative("platforms/ios", path.join(targetPath, frameworkName + ".framework"));
345+
let frameworkPath = path.relative("platforms/ios", path.join(targetPath, subPath));
314346
return frameworkPath;
315347
}
316348

@@ -332,15 +364,19 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ
332364
public preparePluginNativeCode(pluginData: IPluginData, opts?: any): IFuture<void> {
333365
return (() => {
334366
let pluginPlatformsFolderPath = pluginData.pluginPlatformsFolderPath(IOSProjectService.IOS_PLATFORM_NAME);
367+
335368
this.prepareFrameworks(pluginPlatformsFolderPath, pluginData).wait();
336-
this.prepareCocoapods(pluginPlatformsFolderPath, opts).wait();
369+
this.prepareStaticLibs(pluginPlatformsFolderPath, pluginData).wait();
370+
this.prepareCocoapods(pluginPlatformsFolderPath).wait();
337371
}).future<void>()();
338372
}
339373

340374
public removePluginNativeCode(pluginData: IPluginData): IFuture<void> {
341375
return (() => {
342376
let pluginPlatformsFolderPath = pluginData.pluginPlatformsFolderPath(IOSProjectService.IOS_PLATFORM_NAME);
377+
343378
this.removeFrameworks(pluginPlatformsFolderPath, pluginData).wait();
379+
this.removeStaticLibs(pluginPlatformsFolderPath, pluginData).wait();
344380
this.removeCocoapods(pluginPlatformsFolderPath).wait();
345381
}).future<void>()();
346382
}
@@ -377,11 +413,11 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ
377413
}).future<void>()();
378414
}
379415

380-
private getAllFrameworksForPlugin(pluginData: IPluginData): IFuture<string[]> {
381-
let filterCallback = (fileName: string, pluginPlatformsFolderPath: string) => path.extname(fileName) === ".framework";
416+
private getAllLibsForPluginWithFileExtension(pluginData: IPluginData, fileExtension: string): IFuture<string[]> {
417+
let filterCallback = (fileName: string, pluginPlatformsFolderPath: string) => path.extname(fileName) === fileExtension;
382418
return this.getAllNativeLibrariesForPlugin(pluginData, IOSProjectService.IOS_PLATFORM_NAME, filterCallback);
383-
}
384-
419+
};
420+
385421
private buildPathToXcodeProjectFile(version: string): string {
386422
return path.join(this.$npmInstallationManager.getCachedPackagePath(this.platformData.frameworkPackageName, version), constants.PROJECT_FRAMEWORK_FOLDER_NAME, util.format("%s.xcodeproj", IOSProjectService.IOS_PROJECT_NAME_PLACEHOLDER), "project.pbxproj");
387423
}
@@ -399,6 +435,23 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ
399435
}
400436
}).future<void>()();
401437
}
438+
439+
private validateStaticLibrary(libraryPath: string): IFuture<void> {
440+
return (() => {
441+
if (path.extname(libraryPath) !== ".a") {
442+
this.$errors.failWithoutHelp("The bundle at %s does not contain valid '.a' extension.", libraryPath);
443+
}
444+
445+
let expectedArchs = ["armv7", "arm64", "x86_64", "i386"];
446+
let archsInTheFatFile = this.$childProcess.exec("lipo -i " + libraryPath).wait();
447+
448+
expectedArchs.forEach(expectedArch => {
449+
if (archsInTheFatFile.indexOf(expectedArch) < 0) {
450+
this.$errors.failWithoutHelp("The static library at %s is not build for all required architectures - %s.", libraryPath, expectedArchs);
451+
}
452+
});
453+
}).future<void>()();
454+
}
402455

403456
private replaceFileContent(file: string): IFuture<void> {
404457
return (() => {
@@ -424,13 +477,20 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ
424477

425478
private prepareFrameworks(pluginPlatformsFolderPath: string, pluginData: IPluginData): IFuture<void> {
426479
return (() => {
427-
_.each(this.getAllFrameworksForPlugin(pluginData).wait(), fileName => this.addLibrary(path.join(pluginPlatformsFolderPath, fileName)).wait());
480+
_.each(this.getAllLibsForPluginWithFileExtension(pluginData, ".framework").wait(), fileName => this.addDynamicFramework(path.join(pluginPlatformsFolderPath, fileName)).wait());
481+
}).future<void>()();
482+
}
483+
484+
private prepareStaticLibs(pluginPlatformsFolderPath: string, pluginData: IPluginData): IFuture<void> {
485+
return (() => {
486+
_.each(this.getAllLibsForPluginWithFileExtension(pluginData, ".a").wait(), fileName => this.addStaticLibrary(path.join(pluginPlatformsFolderPath, fileName)).wait());
428487
}).future<void>()();
429488
}
430489

431490
private prepareCocoapods(pluginPlatformsFolderPath: string, opts?: any): IFuture<void> {
432491
return (() => {
433492
let pluginPodFilePath = path.join(pluginPlatformsFolderPath, "Podfile");
493+
434494
if(this.$fs.exists(pluginPodFilePath).wait()) {
435495
if(!this.$fs.exists(this.projectPodFilePath).wait()) {
436496
this.$fs.writeFile(this.projectPodFilePath, "use_frameworks!\n").wait();
@@ -450,16 +510,32 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ
450510
private removeFrameworks(pluginPlatformsFolderPath: string, pluginData: IPluginData): IFuture<void> {
451511
return (() => {
452512
let project = this.createPbxProj();
453-
454-
_.each(this.getAllFrameworksForPlugin(pluginData).wait(), fileName => {
455-
let fullFrameworkPath = path.join(pluginPlatformsFolderPath, fileName);
456-
let relativeFrameworkPath = this.getFrameworkRelativePath(fullFrameworkPath);
513+
_.each(this.getAllLibsForPluginWithFileExtension(pluginData, ".framework").wait(), fileName => {
514+
let relativeFrameworkPath = this.getLibSubpathRelativeToProjectPath(fileName);
457515
project.removeFramework(relativeFrameworkPath, { customFramework: true, embed: true });
458516
});
459517

460518
this.savePbxProj(project).wait();
461519
}).future<void>()();
462520
}
521+
522+
private removeStaticLibs(pluginPlatformsFolderPath: string, pluginData: IPluginData): IFuture<void> {
523+
return (() => {
524+
let project = this.createPbxProj();
525+
526+
_.each(this.getAllLibsForPluginWithFileExtension(pluginData, ".a").wait(), fileName => {
527+
let staticLibPath = path.join(pluginPlatformsFolderPath, fileName);
528+
let relativeStaticLibPath = this.getLibSubpathRelativeToProjectPath(path.basename(staticLibPath));
529+
project.removeFramework(relativeStaticLibPath);
530+
531+
let headersSubpath = path.join("include", path.basename(staticLibPath, ".a"));
532+
let relativeHeaderSearchPath = path.join(this.getLibSubpathRelativeToProjectPath(headersSubpath));
533+
project.removeFromHeaderSearchPaths({ relativePath: relativeHeaderSearchPath });
534+
});
535+
536+
this.savePbxProj(project).wait();
537+
}).future<void>()();
538+
}
463539

464540
private removeCocoapods(pluginPlatformsFolderPath: string): IFuture<void> {
465541
return (() => {
@@ -481,5 +557,24 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ
481557
private buildPodfileContent(pluginPodFilePath: string, pluginPodFileContent: string): string {
482558
return `# Begin Podfile - ${pluginPodFilePath} ${os.EOL} ${pluginPodFileContent} ${os.EOL} # End Podfile ${os.EOL}`;
483559
}
560+
561+
private generateMobulemap(headersFolderPath: string, libraryName: string): void {
562+
let headersFilter = (fileName: string, containingFolderPath: string) => (path.extname(fileName) === ".h" && this.$fs.getFsStats(path.join(containingFolderPath, fileName)).wait().isFile());
563+
let headersFolderContents = this.$fs.readDirectory(headersFolderPath).wait();
564+
let headers = _(headersFolderContents).filter(item => headersFilter(item, headersFolderPath)).value();
565+
566+
if (!headers.length) {
567+
this.$fs.deleteFile(path.join(headersFolderPath, "module.modulemap")).wait();
568+
return;
569+
}
570+
571+
headers.forEach(function(currentValue, index, array) {
572+
array[index] = "header \"" + currentValue + "\"";
573+
});
574+
575+
let modulemap = `module ${libraryName} { explicit module ${libraryName} { ${headers.join(" ")} } }`;
576+
this.$fs.writeFile(path.join(headersFolderPath, "module.modulemap"), modulemap, "utf8").wait();
577+
}
484578
}
579+
485580
$injector.register("iOSProjectService", IOSProjectService);

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
"vinyl-filter-since": "2.0.2",
7373
"winreg": "0.0.12",
7474
"ws": "0.7.1",
75-
"xcode": "https://github.com/NativeScript/node-xcode/archive/NativeScript-1.2.2.tar.gz",
75+
"xcode": "https://github.com/NativeScript/node-xcode/archive/1.4.0.tar.gz",
7676
"xmldom": "0.1.19",
7777
"xmlhttprequest": "https://github.com/telerik/node-XMLHttpRequest/tarball/master",
7878
"xmlmerge-js": "0.2.4",

test/ios-project-service.ts

+67
Original file line numberDiff line numberDiff line change
@@ -159,3 +159,70 @@ describe("Cocoapods support", () => {
159159
});
160160
}
161161
});
162+
163+
describe("Static libraries support", () => {
164+
if (require("os").platform() !== "darwin") {
165+
console.log("Skipping static library tests. They cannot work on windows.");
166+
} else {
167+
it("checks static libraries support", () => {
168+
let projectName = "projectDirectory";
169+
let projectPath = temp.mkdirSync(projectName);
170+
let libraryName = "testLibrary1";
171+
let headers = ["TestHeader1.h", "TestHeader2.h"];
172+
173+
let testInjector = createTestInjector(projectPath, projectName);
174+
let fs: IFileSystem = testInjector.resolve("fs");
175+
let iOSProjectService = testInjector.resolve("iOSProjectService");
176+
177+
let staticLibraryPath = path.join(path.join(temp.mkdirSync("pluginDirectory"), "platforms", "ios"));
178+
let staticLibraryHeadersPath = path.join(staticLibraryPath,"include", libraryName);
179+
fs.createDirectory(staticLibraryHeadersPath).wait();
180+
181+
_.each(headers, header => { fs.writeFile(path.join(staticLibraryHeadersPath, header), "").wait(); });
182+
// Add all header files.
183+
fs.writeFile(path.join(staticLibraryHeadersPath, libraryName + ".a"), "").wait();
184+
185+
try {
186+
iOSProjectService.validateStaticLibrary(path.join(staticLibraryPath, libraryName + ".a")).wait();
187+
} catch(error) {
188+
// Expect to fail, the .a file is not a static library.
189+
assert.isTrue(error !== null);
190+
}
191+
192+
iOSProjectService.generateMobulemap(staticLibraryHeadersPath, libraryName);
193+
let modulemap = fs.readFile(path.join(staticLibraryHeadersPath, "module.modulemap")).wait();
194+
let headerCommands: string[] = [];
195+
headers.forEach(function(currentValue, index, array) {
196+
headerCommands.push("header \"" + currentValue + "\"");
197+
});
198+
199+
let modulemapExpectation = `module ${libraryName} { explicit module ${libraryName} { ${headerCommands.join(" ")} } }`;
200+
assert.equal(modulemap, modulemapExpectation);
201+
202+
// Delete all header files.
203+
_.each(headers, header => { fs.deleteFile(path.join(staticLibraryHeadersPath, header)).wait(); });
204+
iOSProjectService.generateMobulemap(staticLibraryHeadersPath, libraryName);
205+
try {
206+
modulemap = fs.readFile(path.join(staticLibraryHeadersPath, "module.modulemap")).wait();
207+
} catch(error) {
208+
// Expect to fail, there shouldn't be a module.modulemap file.
209+
assert.isTrue(error !== null);
210+
}
211+
});
212+
}
213+
});
214+
215+
describe("Relative paths", () => {
216+
it("checks for correct calculation of relative paths", () => {
217+
let projectName = "projectDirectory";
218+
let projectPath = temp.mkdirSync(projectName);
219+
let subpath = "sub/path";
220+
221+
let testInjector = createTestInjector(projectPath, projectName);
222+
let fs: IFileSystem = testInjector.resolve("fs");
223+
let iOSProjectService = testInjector.resolve("iOSProjectService");
224+
225+
let result = iOSProjectService.getLibSubpathRelativeToProjectPath(subpath);
226+
assert.equal(result, path.join("../../lib/iOS/", subpath));
227+
});
228+
});

0 commit comments

Comments
 (0)