Skip to content

Cocoapods support #769

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Aug 21, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/definitions/plugins.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ interface IPluginsService {
prepare(pluginData: IDependencyData): IFuture<void>;
getAllInstalledPlugins(): IFuture<IPluginData[]>;
ensureAllDependenciesAreInstalled(): IFuture<void>;
afterPrepareAllPlugins(): IFuture<void>;
}

interface IPluginData extends INodeModuleData {
Expand Down
1 change: 1 addition & 0 deletions lib/definitions/project.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ interface IPlatformProjectService {
updatePlatform(currentVersion: string, newVersion: string): IFuture<void>;
preparePluginNativeCode(pluginData: IPluginData): IFuture<void>;
removePluginNativeCode(pluginData: IPluginData): IFuture<void>;
afterPrepareAllPlugins(): IFuture<void>;
}

interface IAndroidProjectPropertiesManager {
Expand Down
4 changes: 4 additions & 0 deletions lib/services/android-project-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,10 @@ class AndroidProjectService extends projectServiceBaseLib.PlatformProjectService
}).future<void>()();
}

public afterPrepareAllPlugins(): IFuture<void> {
return Future.fromResult();
}

private getLibraryRelativePath(basePath: string, libraryPath: string): string {
return path.relative(basePath, libraryPath).split("\\").join("/");
}
Expand Down
108 changes: 92 additions & 16 deletions lib/services/ios-project-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,21 @@
"use strict";

import Future = require("fibers/future");
import path = require("path");
import shell = require("shelljs");
import util = require("util");
import xcode = require("xcode");
import * as path from "path";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Almost all other imports can also be converted (Future can't).

import * as shell from "shelljs";
import * as util from "util";
import * as os from "os";
import * as xcode from "xcode";
import constants = require("./../constants");
import helpers = require("./../common/helpers");
import projectServiceBaseLib = require("./platform-project-service-base");

class IOSProjectService extends projectServiceBaseLib.PlatformProjectServiceBase implements IPlatformProjectService {
export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServiceBase implements IPlatformProjectService {
private static XCODE_PROJECT_EXT_NAME = ".xcodeproj";
private static XCODEBUILD_MIN_VERSION = "6.0";
private static IOS_PROJECT_NAME_PLACEHOLDER = "__PROJECT_NAME__";
private static IOS_PLATFORM_NAME = "ios";
private static PODFILE_POST_INSTALL_SECTION_NAME = "post_install";

private get $npmInstallationManager(): INpmInstallationManager {
return this.$injector.resolve("npmInstallationManager");
Expand Down Expand Up @@ -262,14 +264,17 @@ class IOSProjectService extends projectServiceBaseLib.PlatformProjectServiceBase

this.savePbxProj(project).wait();
}

}).future<void>()();
}

public prepareAppResources(appResourcesDirectoryPath: string): IFuture<void> {
return this.$fs.deleteDirectory(this.platformData.appResourcesDestinationDirectoryPath);
}

private get projectPodFilePath(): string {
return path.join(this.platformData.projectRoot, "Podfile");
}

private replace(name: string): string {
if(_.startsWith(name, '"')) {
name = name.substr(1, name.length-2);
Expand Down Expand Up @@ -303,22 +308,41 @@ class IOSProjectService extends projectServiceBaseLib.PlatformProjectServiceBase
public preparePluginNativeCode(pluginData: IPluginData): IFuture<void> {
return (() => {
let pluginPlatformsFolderPath = pluginData.pluginPlatformsFolderPath(IOSProjectService.IOS_PLATFORM_NAME);
_.each(this.getAllDynamicFrameworksForPlugin(pluginData).wait(), fileName => this.addLibrary(path.join(pluginPlatformsFolderPath, fileName)).wait());
this.prepareDynamicFrameworks(pluginPlatformsFolderPath, pluginData).wait();
this.prepareCocoapods(pluginPlatformsFolderPath).wait();
}).future<void>()();
}

public removePluginNativeCode(pluginData: IPluginData): IFuture<void> {
return (() => {
let pluginPlatformsFolderPath = pluginData.pluginPlatformsFolderPath(IOSProjectService.IOS_PLATFORM_NAME);
let project = this.createPbxProj();

_.each(this.getAllDynamicFrameworksForPlugin(pluginData).wait(), fileName => {
let fullFrameworkPath = path.join(pluginPlatformsFolderPath, fileName);
let relativeFrameworkPath = this.getFrameworkRelativePath(fullFrameworkPath);
project.removeFramework(relativeFrameworkPath, { customFramework: true, embed: true })
});

this.savePbxProj(project).wait();
this.removeDynamicFrameworks(pluginPlatformsFolderPath, pluginData).wait();
this.removeCocoapods(pluginPlatformsFolderPath).wait();
}).future<void>()();
}

public afterPrepareAllPlugins(): IFuture<void> {
return (() => {
if(this.$fs.exists(this.projectPodFilePath).wait()) {
// Check availability
try {
this.$childProcess.exec("gem which cocoapods").wait();
} catch(e) {
this.$errors.failWithoutHelp("CocoaPods are not installed. Run `sudo gem install cocoapods` and try again.");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The message on my machine is ERROR: Can't find ruby library file or shared library cocoapods. We must find another way to check for existence. Checking the exit code looks promising.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We discussed the issue in person and we decided that the code is correct.

}

let projectPodfileContent = this.$fs.readText(this.projectPodFilePath).wait();
this.$logger.trace("Project Podfile content");
this.$logger.trace(projectPodfileContent);

let firstPostInstallIndex = projectPodfileContent.indexOf(IOSProjectService.PODFILE_POST_INSTALL_SECTION_NAME);
if(firstPostInstallIndex !== -1 && firstPostInstallIndex !== projectPodfileContent.lastIndexOf(IOSProjectService.PODFILE_POST_INSTALL_SECTION_NAME)) {
this.$logger.warn(`Podfile contains more than one post_install sections. You need to open ${this.projectPodFilePath} file and manually resolve this issue.`);
}

this.$logger.info("Installing pods...");
this.$childProcess.exec("pod install", { cwd: this.platformData.projectRoot }).wait();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does happen when this program fails? We should check its exit code or error output.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the command fails the error will be printed on the console.

}
}).future<void>()();
}

Expand Down Expand Up @@ -379,5 +403,57 @@ class IOSProjectService extends projectServiceBaseLib.PlatformProjectServiceBase
this.$fs.rename(path.join(fileRootLocation, oldFileName), path.join(fileRootLocation, newFileName)).wait();
}).future<void>()();
}

private prepareDynamicFrameworks(pluginPlatformsFolderPath: string, pluginData: IPluginData): IFuture<void> {
return (() => {
_.each(this.getAllDynamicFrameworksForPlugin(pluginData).wait(), fileName => this.addLibrary(path.join(pluginPlatformsFolderPath, fileName)).wait());
}).future<void>()();
}

private prepareCocoapods(pluginPlatformsFolderPath: string): IFuture<void> {
return (() => {
let pluginPodFilePath = path.join(pluginPlatformsFolderPath, "Podfile");
if(this.$fs.exists(pluginPodFilePath).wait()) {
let pluginPodFileContent = this.$fs.readText(pluginPodFilePath).wait();
let contentToWrite = this.buildPodfileContent(pluginPodFilePath, pluginPodFileContent);
this.$fs.appendFile(this.projectPodFilePath, contentToWrite).wait();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This potentially generates invalid podfile.

  • two dependencies may have incompatible versions, see spec. For example, requiring both < 0.9 and >1.0
  • two hooks like post_install

Are we going to address these? perhaps a warning to check the resulting podfile?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we have two dependencies with incompatible versions pod install command fails with descriptive message.

If Podfile has more than one post_install sections only the last one is executed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should document this behavior and give a warning to the user. Ping @ikoevska

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a warning message.

}
}).future<void>()();
}

private removeDynamicFrameworks(pluginPlatformsFolderPath: string, pluginData: IPluginData): IFuture<void> {
return (() => {
let project = this.createPbxProj();

_.each(this.getAllDynamicFrameworksForPlugin(pluginData).wait(), fileName => {
let fullFrameworkPath = path.join(pluginPlatformsFolderPath, fileName);
let relativeFrameworkPath = this.getFrameworkRelativePath(fullFrameworkPath);
project.removeFramework(relativeFrameworkPath, { customFramework: true, embed: true })
});

this.savePbxProj(project).wait();
}).future<void>()();
}

private removeCocoapods(pluginPlatformsFolderPath: string): IFuture<void> {
return (() => {
let pluginPodFilePath = path.join(pluginPlatformsFolderPath, "Podfile");
if(this.$fs.exists(pluginPodFilePath).wait()) {
let pluginPodFileContent = this.$fs.readText(pluginPodFilePath).wait();
let projectPodFileContent = this.$fs.readText(this.projectPodFilePath).wait();
let contentToRemove= this.buildPodfileContent(pluginPodFilePath, pluginPodFileContent);
projectPodFileContent = helpers.stringReplaceAll(projectPodFileContent, contentToRemove, "");
if(_.isEmpty(projectPodFileContent)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may trim the content - there is no need to keep files consisting of empty space. This may happen when the user manually adds empty space.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer to keep the content as is from original Podfile.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue is when the user has manually edit the podfile.
In this case, we can leave the code as is and let the users manually remove the empty file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We discussed the issue in person and decided to keep the current code.

this.$fs.deleteFile(this.projectPodFilePath).wait();
} else {
this.$fs.writeFile(this.projectPodFilePath, projectPodFileContent).wait();
}
}
}).future<void>()();
}

private buildPodfileContent(pluginPodFilePath: string, pluginPodFileContent: string): string {
return `# Begin Podfile - ${pluginPodFilePath} ${os.EOL} ${pluginPodFileContent} ${os.EOL} # End Podfile ${os.EOL}`;
}
}
$injector.register("iOSProjectService", IOSProjectService);
9 changes: 8 additions & 1 deletion lib/services/plugins-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@ export class PluginsService implements IPluginsService {
this.validateXml(resultXml);
this.$fs.writeFile(configurationFilePath, resultXml).wait();
}


this.$projectFilesManager.processPlatformSpecificFiles(pluginDestinationPath, platform).wait();

Expand Down Expand Up @@ -145,6 +144,14 @@ export class PluginsService implements IPluginsService {
}).future<IPluginData[]>()();
}

public afterPrepareAllPlugins(): IFuture<void> {
let action = (pluginDestinationPath: string, platform: string, platformData: IPlatformData) => {
return platformData.platformProjectService.afterPrepareAllPlugins();
};

return this.executeForAllInstalledPlatforms(action);
}

private get nodeModulesPath(): string {
return path.join(this.$projectData.projectDir, "node_modules");
}
Expand Down
4 changes: 4 additions & 0 deletions lib/tools/broccoli/node-modules-dest-copy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ export class DestCopy implements IBroccoliPlugin {
this.$pluginsService.prepare(dependency).wait();
}
});

if(!_.isEmpty(this.dependencies)) {
this.$pluginsService.afterPrepareAllPlugins().wait();
}
}

public rebuild(treeDiff: IDiffResult): void {
Expand Down
149 changes: 149 additions & 0 deletions test/ios-project-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/// <reference path=".d.ts" />
"use strict";

import Future = require("fibers/future");
import * as path from "path";
import temp = require("temp");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This leaks the created directories. Consider executing temp.track()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually temp.track is used from another tests and the created directories are removed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other tests can be rewritten in the future. Do not depend on side effects of unrelated code. If you go this route, you can move temp.track in test's bootstrapper and remove it from all other places.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've decided to implement this in another PR.

temp.track();

import ChildProcessLib = require("../lib/common/child-process");
import ConfigLib = require("../lib/config");
import ErrorsLib = require("../lib/common/errors");
import FileSystemLib = require("../lib/common/file-system");
import HostInfoLib = require("../lib/common/host-info");
import iOSProjectServiceLib = require("../lib/services/ios-project-service");
import LoggerLib = require("../lib/common/logger");
import OptionsLib = require("../lib/options");
import ProjectDataLib = require("../lib/project-data");

import yok = require("../lib/common/yok");

import { assert } from "chai";

function createTestInjector(projectPath: string, projectName: string): IInjector {
let testInjector = new yok.Yok();
testInjector.register("childProcess", ChildProcessLib.ChildProcess);
testInjector.register("config", ConfigLib.Configuration);
testInjector.register("errors", ErrorsLib.Errors);
testInjector.register("fs", FileSystemLib.FileSystem);
testInjector.register("hostInfo", HostInfoLib.HostInfo);
testInjector.register("injector", testInjector);
testInjector.register("iOSEmulatorServices", {});
testInjector.register("iOSProjectService", iOSProjectServiceLib.IOSProjectService);
testInjector.register("logger", LoggerLib.Logger);
testInjector.register("options", OptionsLib.Options);
testInjector.register("projectData", {
platformsDir: projectPath,
projectName: projectName
});
testInjector.register("projectHelper", {});
testInjector.register("staticConfig", ConfigLib.StaticConfig);

return testInjector;
}

describe("Cocoapods support", () => {
it("adds plugin with Podfile", () => {
let projectName = "projectDirectory";
let projectPath = temp.mkdirSync(projectName);

let testInjector = createTestInjector(projectPath, projectName);
let fs: IFileSystem = testInjector.resolve("fs");

let packageJsonData = {
"name": "myProject",
"version": "0.1.0",
"nativescript": {
"id": "org.nativescript.myProject",
"tns-android": {
"version": "1.0.0"
}
}
};
fs.writeJson(path.join(projectPath, "package.json"), packageJsonData).wait();

let platformsFolderPath = path.join(projectPath, "ios");
fs.createDirectory(platformsFolderPath).wait();

let iOSProjectService = testInjector.resolve("iOSProjectService");
iOSProjectService.prepareDynamicFrameworks = (pluginPlatformsFolderPath: string, pluginData: IPluginData): IFuture<void> => {
return Future.fromResult();
};

let pluginPath = temp.mkdirSync("pluginDirectory");
let pluginPlatformsFolderPath = path.join(pluginPath, "platforms", "ios");
let pluginPodfilePath = path.join(pluginPlatformsFolderPath, "Podfile");
let pluginPodfileContent = ["source 'https://github.com/CocoaPods/Specs.git'", "platform :ios, '8.1'", "pod 'GoogleMaps'"].join("\n");
fs.writeFile(pluginPodfilePath, pluginPodfileContent).wait();

let pluginData = {
pluginPlatformsFolderPath(platform: string): string {
return pluginPlatformsFolderPath;
}
};

iOSProjectService.preparePluginNativeCode(pluginData).wait();

let projectPodfilePath = path.join(platformsFolderPath, "Podfile");
assert.isTrue(fs.exists(projectPodfilePath).wait());

let actualProjectPodfileContent = fs.readText(projectPodfilePath).wait();
let expectedProjectPodfileContent = [`# Begin Podfile - ${pluginPodfilePath} `, ` ${pluginPodfileContent} `, " # End Podfile \n"].join("\n");
assert.equal(actualProjectPodfileContent, expectedProjectPodfileContent);
});
it("adds and removes plugin with Podfile", () => {
let projectName = "projectDirectory2";
let projectPath = temp.mkdirSync(projectName);

let testInjector = createTestInjector(projectPath, projectName);
let fs: IFileSystem = testInjector.resolve("fs");

let packageJsonData = {
"name": "myProject2",
"version": "0.1.0",
"nativescript": {
"id": "org.nativescript.myProject2",
"tns-android": {
"version": "1.0.0"
}
}
};
fs.writeJson(path.join(projectPath, "package.json"), packageJsonData).wait();

let platformsFolderPath = path.join(projectPath, "ios");
fs.createDirectory(platformsFolderPath).wait();

let iOSProjectService = testInjector.resolve("iOSProjectService");
iOSProjectService.prepareDynamicFrameworks = (pluginPlatformsFolderPath: string, pluginData: IPluginData): IFuture<void> => {
return Future.fromResult();
};
iOSProjectService.removeDynamicFrameworks = (pluginPlatformsFolderPath: string, pluginData: IPluginData): IFuture<void> => {
return Future.fromResult();
}

let pluginPath = temp.mkdirSync("pluginDirectory");
let pluginPlatformsFolderPath = path.join(pluginPath, "platforms", "ios");
let pluginPodfilePath = path.join(pluginPlatformsFolderPath, "Podfile");
let pluginPodfileContent = ["source 'https://github.com/CocoaPods/Specs.git'", "platform :ios, '8.1'", "pod 'GoogleMaps'"].join("\n");
fs.writeFile(pluginPodfilePath, pluginPodfileContent).wait();

let pluginData = {
pluginPlatformsFolderPath(platform: string): string {
return pluginPlatformsFolderPath;
}
};

iOSProjectService.preparePluginNativeCode(pluginData).wait();

let projectPodfilePath = path.join(platformsFolderPath, "Podfile");
assert.isTrue(fs.exists(projectPodfilePath).wait());

let actualProjectPodfileContent = fs.readText(projectPodfilePath).wait();
let expectedProjectPodfileContent = [`# Begin Podfile - ${pluginPodfilePath} `, ` ${pluginPodfileContent} `, " # End Podfile \n"].join("\n");
assert.equal(actualProjectPodfileContent, expectedProjectPodfileContent);

iOSProjectService.removePluginNativeCode(pluginData).wait();

assert.isFalse(fs.exists(projectPodfilePath).wait());
});
});
3 changes: 2 additions & 1 deletion test/npm-support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ function setupProject(): IFuture<any> {
normalizedPlatformName: "Android",
platformProjectService: {
prepareProject: () => Future.fromResult(),
prepareAppResources: () => Future.fromResult()
prepareAppResources: () => Future.fromResult(),
afterPrepareAllPlugins: () => Future.fromResult()
}
}
};
Expand Down
Loading