Skip to content

Commit e50c9b4

Browse files
committed
Merge pull request #885 from NativeScript/tpopov/static-libs
Support static libraries in plugins for iOS.
2 parents 4027186 + 81be681 commit e50c9b4

File tree

4 files changed

+209
-26
lines changed

4 files changed

+209
-26
lines changed

lib/definitions/xcode.d.ts

+9-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
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
}
99

1010
class project {
@@ -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

+115-20
Original file line numberDiff line numberDiff line change
@@ -200,29 +200,63 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ
200200

201201
public addLibrary(libraryPath: string): IFuture<void> {
202202
return (() => {
203-
this.validateFramework(libraryPath).wait();
203+
let extension = path.extname(libraryPath);
204+
if (extension === ".framework") {
205+
this.addDynamicFramework(libraryPath);
206+
} else {
207+
this.$errors.failWithoutHelp(`The bundle at ${libraryPath} does not appear to be a dynamic framework package.`);
208+
}
209+
}).future<void>()();
210+
}
211+
212+
private addDynamicFramework(frameworkPath: string): IFuture<void> {
213+
return (() => {
214+
this.validateFramework(frameworkPath).wait();
204215

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, frameworkAddOptions);
236+
this.savePbxProj(project).wait();
237+
}).future<void>()();
238+
}
239+
240+
private addStaticLibrary(staticLibPath: string): IFuture<void> {
241+
return (() => {
242+
this.validateStaticLibrary(staticLibPath).wait();
243+
// Copy files to lib folder.
244+
let libraryName = path.basename(staticLibPath, ".a");
245+
let libDestinationPath = path.join(this.$projectData.projectDir, path.join("lib", this.platformData.normalizedPlatformName));
246+
let headersSubpath = path.join("include", libraryName);
247+
this.$fs.ensureDirectoryExists(path.join(libDestinationPath, headersSubpath)).wait();
248+
shell.cp("-Rf", staticLibPath, libDestinationPath);
249+
shell.cp("-Rf", path.join(path.dirname(staticLibPath), headersSubpath), path.join(libDestinationPath, "include"));
250+
251+
// Add static library to project file and setup header search paths
252+
let project = this.createPbxProj();
253+
let relativeStaticLibPath = this.getLibSubpathRelativeToProjectPath(path.basename(staticLibPath));
254+
project.addFramework(relativeStaticLibPath);
255+
256+
let relativeHeaderSearchPath = path.join(this.getLibSubpathRelativeToProjectPath(headersSubpath));
257+
project.addToHeaderSearchPaths({ relativePath: relativeHeaderSearchPath });
258+
259+
this.generateMobulemap(path.join(libDestinationPath, headersSubpath), libraryName);
226260
this.savePbxProj(project).wait();
227261
}).future<void>()();
228262
}
@@ -307,10 +341,9 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ
307341
return name.replace(/\\\"/g, "\"");
308342
}
309343

310-
private getFrameworkRelativePath(libraryPath: string): string {
311-
let frameworkName = path.basename(libraryPath, path.extname(libraryPath));
344+
private getLibSubpathRelativeToProjectPath(subPath: string): string {
312345
let targetPath = path.join("lib", this.platformData.normalizedPlatformName);
313-
let frameworkPath = path.relative("platforms/ios", path.join(targetPath, frameworkName + ".framework"));
346+
let frameworkPath = path.relative("platforms/ios", path.join(targetPath, subPath));
314347
return frameworkPath;
315348
}
316349

@@ -332,15 +365,19 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ
332365
public preparePluginNativeCode(pluginData: IPluginData, opts?: any): IFuture<void> {
333366
return (() => {
334367
let pluginPlatformsFolderPath = pluginData.pluginPlatformsFolderPath(IOSProjectService.IOS_PLATFORM_NAME);
368+
335369
this.prepareFrameworks(pluginPlatformsFolderPath, pluginData).wait();
336-
this.prepareCocoapods(pluginPlatformsFolderPath, opts).wait();
370+
this.prepareStaticLibs(pluginPlatformsFolderPath, pluginData).wait();
371+
this.prepareCocoapods(pluginPlatformsFolderPath).wait();
337372
}).future<void>()();
338373
}
339374

340375
public removePluginNativeCode(pluginData: IPluginData): IFuture<void> {
341376
return (() => {
342377
let pluginPlatformsFolderPath = pluginData.pluginPlatformsFolderPath(IOSProjectService.IOS_PLATFORM_NAME);
378+
343379
this.removeFrameworks(pluginPlatformsFolderPath, pluginData).wait();
380+
this.removeStaticLibs(pluginPlatformsFolderPath, pluginData).wait();
344381
this.removeCocoapods(pluginPlatformsFolderPath).wait();
345382
}).future<void>()();
346383
}
@@ -377,10 +414,10 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ
377414
}).future<void>()();
378415
}
379416

380-
private getAllFrameworksForPlugin(pluginData: IPluginData): IFuture<string[]> {
381-
let filterCallback = (fileName: string, pluginPlatformsFolderPath: string) => path.extname(fileName) === ".framework";
417+
private getAllLibsForPluginWithFileExtension(pluginData: IPluginData, fileExtension: string): IFuture<string[]> {
418+
let filterCallback = (fileName: string, pluginPlatformsFolderPath: string) => path.extname(fileName) === fileExtension;
382419
return this.getAllNativeLibrariesForPlugin(pluginData, IOSProjectService.IOS_PLATFORM_NAME, filterCallback);
383-
}
420+
};
384421

385422
private buildPathToXcodeProjectFile(version: string): string {
386423
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");
@@ -400,6 +437,24 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ
400437
}).future<void>()();
401438
}
402439

440+
private validateStaticLibrary(libraryPath: string): IFuture<void> {
441+
return (() => {
442+
if (path.extname(libraryPath) !== ".a") {
443+
this.$errors.failWithoutHelp(`The bundle at ${libraryPath} does not contain a valid static library in the '.a' file format.`);
444+
}
445+
446+
let expectedArchs = ["armv7", "arm64", "i386"];
447+
let archsInTheFatFile = this.$childProcess.exec("lipo -i " + libraryPath).wait();
448+
449+
expectedArchs.forEach(expectedArch => {
450+
if (archsInTheFatFile.indexOf(expectedArch) < 0) {
451+
this.$errors.failWithoutHelp(`The static library at ${libraryPath} is not built for one or more of the following required architectures:
452+
${expectedArchs.join(", ")}. The static library must be built for all required architectures.`);
453+
}
454+
});
455+
}).future<void>()();
456+
}
457+
403458
private replaceFileContent(file: string): IFuture<void> {
404459
return (() => {
405460
let fileContent = this.$fs.readText(file).wait();
@@ -424,13 +479,20 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ
424479

425480
private prepareFrameworks(pluginPlatformsFolderPath: string, pluginData: IPluginData): IFuture<void> {
426481
return (() => {
427-
_.each(this.getAllFrameworksForPlugin(pluginData).wait(), fileName => this.addLibrary(path.join(pluginPlatformsFolderPath, fileName)).wait());
482+
_.each(this.getAllLibsForPluginWithFileExtension(pluginData, ".framework").wait(), fileName => this.addDynamicFramework(path.join(pluginPlatformsFolderPath, fileName)).wait());
483+
}).future<void>()();
484+
}
485+
486+
private prepareStaticLibs(pluginPlatformsFolderPath: string, pluginData: IPluginData): IFuture<void> {
487+
return (() => {
488+
_.each(this.getAllLibsForPluginWithFileExtension(pluginData, ".a").wait(), fileName => this.addStaticLibrary(path.join(pluginPlatformsFolderPath, fileName)).wait());
428489
}).future<void>()();
429490
}
430491

431492
private prepareCocoapods(pluginPlatformsFolderPath: string, opts?: any): IFuture<void> {
432493
return (() => {
433494
let pluginPodFilePath = path.join(pluginPlatformsFolderPath, "Podfile");
495+
434496
if(this.$fs.exists(pluginPodFilePath).wait()) {
435497
if(!this.$fs.exists(this.projectPodFilePath).wait()) {
436498
this.$fs.writeFile(this.projectPodFilePath, "use_frameworks!\n").wait();
@@ -450,17 +512,33 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ
450512
private removeFrameworks(pluginPlatformsFolderPath: string, pluginData: IPluginData): IFuture<void> {
451513
return (() => {
452514
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);
515+
_.each(this.getAllLibsForPluginWithFileExtension(pluginData, ".framework").wait(), fileName => {
516+
let relativeFrameworkPath = this.getLibSubpathRelativeToProjectPath(fileName);
457517
project.removeFramework(relativeFrameworkPath, { customFramework: true, embed: true });
458518
});
459519

460520
this.savePbxProj(project).wait();
461521
}).future<void>()();
462522
}
463523

524+
private removeStaticLibs(pluginPlatformsFolderPath: string, pluginData: IPluginData): IFuture<void> {
525+
return (() => {
526+
let project = this.createPbxProj();
527+
528+
_.each(this.getAllLibsForPluginWithFileExtension(pluginData, ".a").wait(), fileName => {
529+
let staticLibPath = path.join(pluginPlatformsFolderPath, fileName);
530+
let relativeStaticLibPath = this.getLibSubpathRelativeToProjectPath(path.basename(staticLibPath));
531+
project.removeFramework(relativeStaticLibPath);
532+
533+
let headersSubpath = path.join("include", path.basename(staticLibPath, ".a"));
534+
let relativeHeaderSearchPath = path.join(this.getLibSubpathRelativeToProjectPath(headersSubpath));
535+
project.removeFromHeaderSearchPaths({ relativePath: relativeHeaderSearchPath });
536+
});
537+
538+
this.savePbxProj(project).wait();
539+
}).future<void>()();
540+
}
541+
464542
private removeCocoapods(pluginPlatformsFolderPath: string): IFuture<void> {
465543
return (() => {
466544
let pluginPodFilePath = path.join(pluginPlatformsFolderPath, "Podfile");
@@ -481,5 +559,22 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ
481559
private buildPodfileContent(pluginPodFilePath: string, pluginPodFileContent: string): string {
482560
return `# Begin Podfile - ${pluginPodFilePath} ${os.EOL} ${pluginPodFileContent} ${os.EOL} # End Podfile ${os.EOL}`;
483561
}
562+
563+
private generateMobulemap(headersFolderPath: string, libraryName: string): void {
564+
let headersFilter = (fileName: string, containingFolderPath: string) => (path.extname(fileName) === ".h" && this.$fs.getFsStats(path.join(containingFolderPath, fileName)).wait().isFile());
565+
let headersFolderContents = this.$fs.readDirectory(headersFolderPath).wait();
566+
let headers = _(headersFolderContents).filter(item => headersFilter(item, headersFolderPath)).value();
567+
568+
if (!headers.length) {
569+
this.$fs.deleteFile(path.join(headersFolderPath, "module.modulemap")).wait();
570+
return;
571+
}
572+
573+
headers = _.map(headers, value => `header "${value}"`);
574+
575+
let modulemap = `module ${libraryName} { explicit module ${libraryName} { ${headers.join(" ")} } }`;
576+
this.$fs.writeFile(path.join(headersFolderPath, "module.modulemap"), modulemap).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

+84
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ describe("Cocoapods support", () => {
7272
iOSProjectService.prepareFrameworks = (pluginPlatformsFolderPath: string, pluginData: IPluginData): IFuture<void> => {
7373
return Future.fromResult();
7474
};
75+
iOSProjectService.prepareStaticLibs = (pluginPlatformsFolderPath: string, pluginData: IPluginData): IFuture<void> => {
76+
return Future.fromResult();
77+
};
7578

7679
let pluginPath = temp.mkdirSync("pluginDirectory");
7780
let pluginPlatformsFolderPath = path.join(pluginPath, "platforms", "ios");
@@ -124,9 +127,15 @@ describe("Cocoapods support", () => {
124127
iOSProjectService.prepareFrameworks = (pluginPlatformsFolderPath: string, pluginData: IPluginData): IFuture<void> => {
125128
return Future.fromResult();
126129
};
130+
iOSProjectService.prepareStaticLibs = (pluginPlatformsFolderPath: string, pluginData: IPluginData): IFuture<void> => {
131+
return Future.fromResult();
132+
};
127133
iOSProjectService.removeFrameworks = (pluginPlatformsFolderPath: string, pluginData: IPluginData): IFuture<void> => {
128134
return Future.fromResult();
129135
};
136+
iOSProjectService.removeStaticLibs = (pluginPlatformsFolderPath: string, pluginData: IPluginData): IFuture<void> => {
137+
return Future.fromResult();
138+
};
130139

131140
let pluginPath = temp.mkdirSync("pluginDirectory");
132141
let pluginPlatformsFolderPath = path.join(pluginPath, "platforms", "ios");
@@ -159,3 +168,78 @@ describe("Cocoapods support", () => {
159168
});
160169
}
161170
});
171+
172+
describe("Static libraries support", () => {
173+
if (require("os").platform() !== "darwin") {
174+
console.log("Skipping static library tests. They work only on darwin.");
175+
return;
176+
}
177+
178+
let projectName = "projectDirectory";
179+
let projectPath = temp.mkdirSync(projectName);
180+
let libraryName = "testLibrary1";
181+
let headers = ["TestHeader1.h", "TestHeader2.h"];
182+
let testInjector = createTestInjector(projectPath, projectName);
183+
let fs: IFileSystem = testInjector.resolve("fs");
184+
let staticLibraryPath = path.join(path.join(temp.mkdirSync("pluginDirectory"), "platforms", "ios"));
185+
let staticLibraryHeadersPath = path.join(staticLibraryPath,"include", libraryName);
186+
187+
it("checks validation of header files", () => {
188+
let iOSProjectService = testInjector.resolve("iOSProjectService");
189+
fs.ensureDirectoryExists(staticLibraryHeadersPath).wait();
190+
_.each(headers, header => { fs.writeFile(path.join(staticLibraryHeadersPath, header), "").wait(); });
191+
192+
// Add all header files.
193+
fs.writeFile(path.join(staticLibraryHeadersPath, libraryName + ".a"), "").wait();
194+
195+
let error: any;
196+
try {
197+
iOSProjectService.validateStaticLibrary(path.join(staticLibraryPath, libraryName + ".a")).wait();
198+
} catch(err) {
199+
error = err;
200+
}
201+
202+
assert.instanceOf(error, Error, "Expect to fail, the .a file is not a static library.");
203+
});
204+
205+
it("checks generation of modulemaps", () => {
206+
let iOSProjectService = testInjector.resolve("iOSProjectService");
207+
fs.ensureDirectoryExists(staticLibraryHeadersPath).wait();
208+
_.each(headers, header => { fs.writeFile(path.join(staticLibraryHeadersPath, header), "").wait(); });
209+
210+
iOSProjectService.generateMobulemap(staticLibraryHeadersPath, libraryName);
211+
// Read the generated modulemap and verify it.
212+
let modulemap = fs.readFile(path.join(staticLibraryHeadersPath, "module.modulemap")).wait();
213+
let headerCommands = _.map(headers, value => `header "${value}"`);
214+
let modulemapExpectation = `module ${libraryName} { explicit module ${libraryName} { ${headerCommands.join(" ")} } }`;
215+
216+
assert.equal(modulemap, modulemapExpectation);
217+
218+
// Delete all header files. And try to regenerate modulemap.
219+
_.each(headers, header => { fs.deleteFile(path.join(staticLibraryHeadersPath, header)).wait(); });
220+
iOSProjectService.generateMobulemap(staticLibraryHeadersPath, libraryName);
221+
222+
let error: any;
223+
try {
224+
modulemap = fs.readFile(path.join(staticLibraryHeadersPath, "module.modulemap")).wait();
225+
} catch(err) {
226+
error = err;
227+
}
228+
229+
assert.instanceOf(error, Error, "Expect to fail, there shouldn't be a module.modulemap file.");
230+
});
231+
});
232+
233+
describe("Relative paths", () => {
234+
it("checks for correct calculation of relative paths", () => {
235+
let projectName = "projectDirectory";
236+
let projectPath = temp.mkdirSync(projectName);
237+
let subpath = "sub/path";
238+
239+
let testInjector = createTestInjector(projectPath, projectName);
240+
let iOSProjectService = testInjector.resolve("iOSProjectService");
241+
242+
let result = iOSProjectService.getLibSubpathRelativeToProjectPath(subpath);
243+
assert.equal(result, path.join("../../lib/iOS/", subpath));
244+
});
245+
});

0 commit comments

Comments
 (0)