Skip to content

fix: Project's Podfile is regenerated incorrectly #3900

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 1 commit into from
Sep 20, 2018
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
7 changes: 7 additions & 0 deletions lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,3 +215,10 @@ export class AddPlaformErrors {

export const PLUGIN_BUILD_DATA_FILENAME = "plugin-data.json";
export const PLUGINS_BUILD_DATA_FILENAME = ".ns-plugins-build-data.json";

export class PluginNativeDirNames {
public static iOS = "ios";
public static Android = "android";
}

export const PODFILE_NAME = "Podfile";
32 changes: 27 additions & 5 deletions lib/definitions/project.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -464,10 +464,32 @@ interface ICocoaPodsService {
getPodfileFooter(): string;

/**
* Merges the content of hooks with the provided name if there are more than one hooks with this name in the Podfile.
* @param {string} hookName The name of the hook.
* @param {string} pathToPodfile The path to the Podfile.
* @return {void}
* Prepares the Podfile content of a plugin and merges it in the project's Podfile.
* @param {IPluginData} pluginData Information about the plugin.
* @param {IProjectData} projectData Information about the project.
* @param {string} nativeProjectPath Path to the native Xcode project.
* @returns {Promise<void>}
*/
applyPluginPodfileToProject(pluginData: IPluginData, projectData: IProjectData, nativeProjectPath: string): Promise<void>;

/**
* Removes plugins Podfile content from the project.
* @param {IPluginData} pluginData Information about the plugin.
* @param {IProjectData} projectData Information about the project.
* @param {string} nativeProjectPath Path to the native Xcode project.
* @returns {void}
*/
mergePodfileHookContent(sectionName: string, pathToPodfile: string): void
removePluginPodfileFromProject(pluginData: IPluginData, projectData: IProjectData, nativeProjectPath: string): void;

/**
* Gives the path to project's Podfile.
* @param {string} nativeProjectPath Path to the native Xcode project.
* @returns {string} Path to project's Podfile.
*/
getProjectPodfilePath(nativeProjectPath: string): string;
}

interface IRubyFunction {
functionName: string;
functionParameters?: string;
}
170 changes: 144 additions & 26 deletions lib/services/cocoapods-service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { EOL } from "os";

interface IRubyFunction {
functionName: string;
functionParameters?: string;
}
import * as path from "path";
import { PluginNativeDirNames, PODFILE_NAME } from "../constants";

export class CocoaPodsService implements ICocoaPodsService {
private static PODFILE_POST_INSTALL_SECTION_NAME = "post_install";
private static INSTALLER_BLOCK_PARAMETER_NAME = "installer";

constructor(private $fs: IFileSystem) { }

public getPodfileHeader(targetName: string): string {
Expand All @@ -16,20 +16,133 @@ export class CocoaPodsService implements ICocoaPodsService {
return `${EOL}end`;
}

public mergePodfileHookContent(hookName: string, pathToPodfile: string): void {
if (!this.$fs.exists(pathToPodfile)) {
throw new Error(`The Podfile ${pathToPodfile} does not exist.`);
public getProjectPodfilePath(projectRoot: string): string {
return path.join(projectRoot, PODFILE_NAME);
}

public async applyPluginPodfileToProject(pluginData: IPluginData, projectData: IProjectData, nativeProjectPath: string): Promise<void> {
const pluginPodFilePath = this.getPluginPodfilePath(pluginData);
if (!this.$fs.exists(pluginPodFilePath)) {
return;
}

const { pluginPodfileContent, replacedFunctions } = this.buildPodfileContent(pluginPodFilePath, pluginData.name);
const pathToProjectPodfile = this.getProjectPodfilePath(nativeProjectPath);
const projectPodfileContent = this.$fs.exists(pathToProjectPodfile) ? this.$fs.readText(pathToProjectPodfile).trim() : "";

if (projectPodfileContent.indexOf(pluginPodfileContent) === -1) {
// Remove old occurences of the plugin from the project's Podfile.
this.removePluginPodfileFromProject(pluginData, projectData, nativeProjectPath);
let finalPodfileContent = this.$fs.exists(pathToProjectPodfile) ? this.getPodfileContentWithoutTarget(projectData, this.$fs.readText(pathToProjectPodfile)) : "";

if (pluginPodfileContent.indexOf(CocoaPodsService.PODFILE_POST_INSTALL_SECTION_NAME) !== -1) {
finalPodfileContent = this.addPostInstallHook(replacedFunctions, finalPodfileContent, pluginPodfileContent);
}

finalPodfileContent = `${pluginPodfileContent}${EOL}${finalPodfileContent}`;
this.saveProjectPodfile(projectData, finalPodfileContent, nativeProjectPath);
}
}

public removePluginPodfileFromProject(pluginData: IPluginData, projectData: IProjectData, projectRoot: string): void {
const pluginPodfilePath = this.getPluginPodfilePath(pluginData);

if (this.$fs.exists(pluginPodfilePath) && this.$fs.exists(this.getProjectPodfilePath(projectRoot))) {
let projectPodFileContent = this.$fs.readText(this.getProjectPodfilePath(projectRoot));
// Remove the data between #Begin Podfile and #EndPodfile
const regExpToRemove = new RegExp(`${this.getPluginPodfileHeader(pluginPodfilePath)}[\\s\\S]*?${this.getPluginPodfileEnd()}`, "mg");
projectPodFileContent = projectPodFileContent.replace(regExpToRemove, "");
projectPodFileContent = this.removePostInstallHook(pluginData, projectPodFileContent);

const defaultPodfileBeginning = this.getPodfileHeader(projectData.projectName);
const defaultContentWithPostInstallHook = `${defaultPodfileBeginning}${EOL}${this.getPostInstallHookHeader()}end${EOL}end`;
const defaultContentWithoutPostInstallHook = `${defaultPodfileBeginning}end`;
const trimmedProjectPodFileContent = projectPodFileContent.trim();
if (!trimmedProjectPodFileContent || trimmedProjectPodFileContent === defaultContentWithPostInstallHook || trimmedProjectPodFileContent === defaultContentWithoutPostInstallHook) {
this.$fs.deleteFile(this.getProjectPodfilePath(projectRoot));
} else {
this.$fs.writeFile(this.getProjectPodfilePath(projectRoot), projectPodFileContent);
}
}
}

private getPluginPodfilePath(pluginData: IPluginData): string {
const pluginPlatformsFolderPath = pluginData.pluginPlatformsFolderPath(PluginNativeDirNames.iOS);
const pluginPodFilePath = path.join(pluginPlatformsFolderPath, PODFILE_NAME);
return pluginPodFilePath;
}

const podfileContent = this.$fs.readText(pathToPodfile);
private addPostInstallHook(replacedFunctions: IRubyFunction[], finalPodfileContent: string, pluginPodfileContent: string): string {
const postInstallHookStart = this.getPostInstallHookHeader();
let postInstallHookContent = "";
_.each(replacedFunctions, rubyFunction => {
let functionExecution = rubyFunction.functionName;
if (rubyFunction.functionParameters && rubyFunction.functionParameters.length) {
functionExecution = `${functionExecution} ${CocoaPodsService.INSTALLER_BLOCK_PARAMETER_NAME}`;
}

postInstallHookContent += ` ${functionExecution}${EOL}`;
});

if (postInstallHookContent) {
const index = finalPodfileContent.indexOf(postInstallHookStart);
if (index !== -1) {
finalPodfileContent = finalPodfileContent.replace(postInstallHookStart, `${postInstallHookStart}${postInstallHookContent}`);
} else {
const postInstallHook = `${postInstallHookStart}${postInstallHookContent}end`;
finalPodfileContent = `${finalPodfileContent}${postInstallHook}`;
}
}

return finalPodfileContent;
}

private getPodfileContentWithoutTarget(projectData: IProjectData, projectPodfileContent: string): string {
const podFileHeader = this.getPodfileHeader(projectData.projectName);

if (_.startsWith(projectPodfileContent, podFileHeader)) {
projectPodfileContent = projectPodfileContent.substr(podFileHeader.length);

const podFileFooter = this.getPodfileFooter();
// Only remove the final end in case the file starts with the podFileHeader
if (_.endsWith(projectPodfileContent, podFileFooter)) {
projectPodfileContent = projectPodfileContent.substr(0, projectPodfileContent.length - podFileFooter.length);
}
}

return projectPodfileContent.trim();
}

private saveProjectPodfile(projectData: IProjectData, projectPodfileContent: string, projectRoot: string): void {
projectPodfileContent = this.getPodfileContentWithoutTarget(projectData, projectPodfileContent);
const podFileHeader = this.getPodfileHeader(projectData.projectName);
const podFileFooter = this.getPodfileFooter();
const contentToWrite = `${podFileHeader}${projectPodfileContent}${podFileFooter}`;
const projectPodfilePath = this.getProjectPodfilePath(projectRoot);
this.$fs.writeFile(projectPodfilePath, contentToWrite);
}

private removePostInstallHook(pluginData: IPluginData, projectPodFileContent: string): string {
const regExp = new RegExp(`^.*?${this.getHookBasicFuncNameForPlugin(CocoaPodsService.PODFILE_POST_INSTALL_SECTION_NAME, pluginData.name)}.*?$\\r?\\n`, "gm");
projectPodFileContent = projectPodFileContent.replace(regExp, "");
return projectPodFileContent;
}

private getHookBasicFuncNameForPlugin(hookName: string, pluginName: string): string {
// nativescript-hook and nativescript_hook should have different names, so replace all _ with ___ first and then replace all special symbols with _
// This will lead to a clash in case plugins are called nativescript-hook and nativescript___hook
const replacedPluginName = pluginName.replace(/_/g, "___").replace(/[^A-Za-z0-9_]/g, "_");
return `${hookName}${replacedPluginName}`;
}

private replaceHookContent(hookName: string, podfileContent: string, pluginName: string): { replacedContent: string, newFunctions: IRubyFunction[] } {
const hookStart = `${hookName} do`;

const hookDefinitionRegExp = new RegExp(`${hookStart} *(\\|(\\w+)\\|)?`, "g");
let newFunctionNameIndex = 1;
const newFunctions: IRubyFunction[] = [];

const replacedContent = podfileContent.replace(hookDefinitionRegExp, (substring: string, firstGroup: string, secondGroup: string, index: number): string => {
const newFunctionName = `${hookName}${newFunctionNameIndex++}`;
const newFunctionName = `${this.getHookBasicFuncNameForPlugin(hookName, pluginName)}_${newFunctions.length}`;
let newDefinition = `def ${newFunctionName}`;

const rubyFunction: IRubyFunction = { functionName: newFunctionName };
Expand All @@ -43,26 +156,31 @@ export class CocoaPodsService implements ICocoaPodsService {
return newDefinition;
});

if (newFunctions.length > 1) {
// Execute all methods in the hook and pass the parameter to them.
const blokParameterName = "installer";
let mergedHookContent = `${hookStart} |${blokParameterName}|${EOL}`;
return { replacedContent, newFunctions };
}

_.each(newFunctions, (rubyFunction: IRubyFunction) => {
let functionExecution = rubyFunction.functionName;
if (rubyFunction.functionParameters && rubyFunction.functionParameters.length) {
functionExecution = `${functionExecution} ${blokParameterName}`;
}
private getPluginPodfileHeader(pluginPodFilePath: string): string {
return `# Begin Podfile - ${pluginPodFilePath}`;
}

mergedHookContent = `${mergedHookContent} ${functionExecution}${EOL}`;
});
private getPluginPodfileEnd(): string {
return `# End Podfile${EOL}`;
}

mergedHookContent = `${mergedHookContent}end`;
private getPostInstallHookHeader() {
return `${CocoaPodsService.PODFILE_POST_INSTALL_SECTION_NAME} do |${CocoaPodsService.INSTALLER_BLOCK_PARAMETER_NAME}|${EOL}`;
}

const newPodfileContent = `${replacedContent}${EOL}${mergedHookContent}`;
this.$fs.writeFile(pathToPodfile, newPodfileContent);
}
private buildPodfileContent(pluginPodFilePath: string, pluginName: string): { pluginPodfileContent: string, replacedFunctions: IRubyFunction[] } {
const pluginPodfileContent = this.$fs.readText(pluginPodFilePath);
const { replacedContent, newFunctions: replacedFunctions } = this.replaceHookContent(CocoaPodsService.PODFILE_POST_INSTALL_SECTION_NAME, pluginPodfileContent, pluginName);

return {
pluginPodfileContent: `${this.getPluginPodfileHeader(pluginPodFilePath)}${EOL}${replacedContent}${EOL}${this.getPluginPodfileEnd()}`,
replacedFunctions
};
}

}

$injector.register("cocoapodsService", CocoaPodsService);
77 changes: 9 additions & 68 deletions lib/services/ios-project-service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import * as path from "path";
import * as shell from "shelljs";
import * as os from "os";
import * as semver from "semver";
import * as constants from "../constants";
import * as helpers from "../common/helpers";
Expand Down Expand Up @@ -28,11 +27,6 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ
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");
}

constructor($fs: IFileSystem,
private $childProcess: IChildProcess,
Expand Down Expand Up @@ -900,10 +894,6 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f
return path.join(this.getPlatformData(projectData).projectRoot, projectData.projectName + IOSProjectService.XCODE_PROJECT_EXT_NAME);
}

private getProjectPodFilePath(projectData: IProjectData): string {
return path.join(this.getPlatformData(projectData).projectRoot, "Podfile");
}

private getPluginsDebugXcconfigFilePath(projectData: IProjectData): string {
return path.join(this.getPlatformData(projectData).projectRoot, "plugins-debug.xcconfig");
}
Expand Down Expand Up @@ -951,7 +941,7 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f
await this.prepareResources(pluginPlatformsFolderPath, pluginData, projectData);
await this.prepareFrameworks(pluginPlatformsFolderPath, pluginData, projectData);
await this.prepareStaticLibs(pluginPlatformsFolderPath, pluginData, projectData);
await this.prepareCocoapods(pluginPlatformsFolderPath, projectData);
await this.prepareCocoapods(pluginPlatformsFolderPath, pluginData, projectData);
}

public async removePluginNativeCode(pluginData: IPluginData, projectData: IProjectData): Promise<void> {
Expand All @@ -960,20 +950,14 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f
this.removeNativeSourceCode(pluginPlatformsFolderPath, pluginData, projectData);
this.removeFrameworks(pluginPlatformsFolderPath, pluginData, projectData);
this.removeStaticLibs(pluginPlatformsFolderPath, pluginData, projectData);
this.removeCocoapods(pluginPlatformsFolderPath, projectData);
const projectRoot = this.getPlatformData(projectData).projectRoot;

this.$cocoapodsService.removePluginPodfileFromProject(pluginData, projectData, projectRoot);
}

public async afterPrepareAllPlugins(projectData: IProjectData): Promise<void> {
if (this.$fs.exists(this.getProjectPodFilePath(projectData))) {
const projectPodfileContent = this.$fs.readText(this.getProjectPodFilePath(projectData));
this.$logger.trace("Project Podfile content");
this.$logger.trace(projectPodfileContent);

const firstPostInstallIndex = projectPodfileContent.indexOf(IOSProjectService.PODFILE_POST_INSTALL_SECTION_NAME);
if (firstPostInstallIndex !== -1 && firstPostInstallIndex !== projectPodfileContent.lastIndexOf(IOSProjectService.PODFILE_POST_INSTALL_SECTION_NAME)) {
this.$cocoapodsService.mergePodfileHookContent(IOSProjectService.PODFILE_POST_INSTALL_SECTION_NAME, this.getProjectPodFilePath(projectData));
}

const projectRoot = this.getPlatformData(projectData).projectRoot;
if (this.$fs.exists(this.$cocoapodsService.getProjectPodfilePath(projectRoot))) {
const xcuserDataPath = path.join(this.getXcodeprojPath(projectData), "xcuserdata");
const sharedDataPath = path.join(this.getXcodeprojPath(projectData), "xcshareddata");

Expand Down Expand Up @@ -1169,38 +1153,15 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f
}
}

private async prepareCocoapods(pluginPlatformsFolderPath: string, projectData: IProjectData, opts?: any): Promise<void> {
private async prepareCocoapods(pluginPlatformsFolderPath: string, pluginData: IPluginData, projectData: IProjectData, opts?: any): Promise<void> {
const projectRoot = this.getPlatformData(projectData).projectRoot;
await this.$cocoapodsService.applyPluginPodfileToProject(pluginData, projectData, projectRoot);
const pluginPodFilePath = path.join(pluginPlatformsFolderPath, "Podfile");
if (this.$fs.exists(pluginPodFilePath)) {
const pluginPodFileContent = this.$fs.readText(pluginPodFilePath);
const pluginPodFilePreparedContent = this.buildPodfileContent(pluginPodFilePath, pluginPodFileContent);
let projectPodFileContent = this.$fs.exists(this.getProjectPodFilePath(projectData)) ? this.$fs.readText(this.getProjectPodFilePath(projectData)) : "";

if (!~projectPodFileContent.indexOf(pluginPodFilePreparedContent)) {
const podFileHeader = this.$cocoapodsService.getPodfileHeader(projectData.projectName),
podFileFooter = this.$cocoapodsService.getPodfileFooter();

if (_.startsWith(projectPodFileContent, podFileHeader)) {
projectPodFileContent = projectPodFileContent.substr(podFileHeader.length);
}

if (_.endsWith(projectPodFileContent, podFileFooter)) {
projectPodFileContent = projectPodFileContent.substr(0, projectPodFileContent.length - podFileFooter.length);
}

const contentToWrite = `${podFileHeader}${projectPodFileContent}${pluginPodFilePreparedContent}${podFileFooter}`;
this.$fs.writeFile(this.getProjectPodFilePath(projectData), contentToWrite);

const project = this.createPbxProj(projectData);
this.savePbxProj(project, projectData);
}
}

if (opts && opts.executePodInstall && this.$fs.exists(pluginPodFilePath)) {
await this.executePodInstall(projectData);
}
}

private removeNativeSourceCode(pluginPlatformsFolderPath: string, pluginData: IPluginData, projectData: IProjectData): void {
const project = this.createPbxProj(projectData);
const group = this.getRootGroup(pluginData.name, pluginPlatformsFolderPath);
Expand Down Expand Up @@ -1235,26 +1196,6 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f
this.savePbxProj(project, projectData);
}

private removeCocoapods(pluginPlatformsFolderPath: string, projectData: IProjectData): void {
const pluginPodFilePath = path.join(pluginPlatformsFolderPath, "Podfile");

if (this.$fs.exists(pluginPodFilePath) && this.$fs.exists(this.getProjectPodFilePath(projectData))) {
const pluginPodFileContent = this.$fs.readText(pluginPodFilePath);
let projectPodFileContent = this.$fs.readText(this.getProjectPodFilePath(projectData));
const contentToRemove = this.buildPodfileContent(pluginPodFilePath, pluginPodFileContent);
projectPodFileContent = helpers.stringReplaceAll(projectPodFileContent, contentToRemove, "");
if (projectPodFileContent.trim() === `use_frameworks!${os.EOL}${os.EOL}target "${projectData.projectName}" do${os.EOL}${os.EOL}end`) {
this.$fs.deleteFile(this.getProjectPodFilePath(projectData));
} else {
this.$fs.writeFile(this.getProjectPodFilePath(projectData), projectPodFileContent);
}
}
}

private buildPodfileContent(pluginPodFilePath: string, pluginPodFileContent: string): string {
return `# Begin Podfile - ${pluginPodFilePath} ${os.EOL} ${pluginPodFileContent} ${os.EOL} # End Podfile ${os.EOL}`;
}

private generateModulemap(headersFolderPath: string, libraryName: string): void {
const headersFilter = (fileName: string, containingFolderPath: string) => (path.extname(fileName) === ".h" && this.$fs.getFsStats(path.join(containingFolderPath, fileName)).isFile());
const headersFolderContents = this.$fs.readDirectory(headersFolderPath);
Expand Down
Loading