Skip to content

Commit

Permalink
fix: CLI breaks process when pod install has not failed
Browse files Browse the repository at this point in the history
In some cases `pod install` command prints data on the `stderr` (for example when setup the repo for the first time or when updating it). CLI detects the data on stderr and fails the operation. However these are not real errors.
Fix this by passing `{ throwError: false }` to `spawnFromEvent` method. This way it will not fail in any case. Check the exit code of the operation and if it is not 0, just fail.
Pipe the stderr of the `pod install` process to stdout of CLI, this way we'll not polute CLI's stderr with some unrelevant info.
NOTE: `pod install` command prints the real errors on stdout, not stderr.
  • Loading branch information
rosen-vladimirov committed Sep 26, 2018
1 parent c578acc commit a970cbe
Show file tree
Hide file tree
Showing 4 changed files with 173 additions and 19 deletions.
3 changes: 1 addition & 2 deletions lib/definitions/project.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -496,12 +496,11 @@ interface ICocoaPodsService {

/**
* Executes `pod install` or `sanboxpod install` in the passed project.
* @param {IProjectData} projectData Information about the project.
* @param {string} projectRoot The root directory of the native iOS project.
* @param {string} xcodeProjPath The full path to the .xcodeproj file.
* @returns {Promise<ISpawnResult>} Information about the spawned process.
*/
executePodInstall(projectData: IProjectData, projectRoot: string, xcodeProjPath: string): Promise<ISpawnResult>;
executePodInstall(projectRoot: string, xcodeProjPath: string): Promise<ISpawnResult>;
}

interface IRubyFunction {
Expand Down
22 changes: 6 additions & 16 deletions lib/services/cocoapods-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export class CocoaPodsService implements ICocoaPodsService {
return path.join(projectRoot, PODFILE_NAME);
}

public async executePodInstall(projectData: IProjectData, projectRoot: string, xcodeProjPath: string): Promise<ISpawnResult> {
public async executePodInstall(projectRoot: string, xcodeProjPath: string): Promise<ISpawnResult> {
// Check availability
try {
await this.$childProcess.exec("which pod");
Expand All @@ -38,21 +38,11 @@ export class CocoaPodsService implements ICocoaPodsService {

this.$logger.info("Installing pods...");
const podTool = this.$config.USE_POD_SANDBOX ? "sandbox-pod" : "pod";
const podInstallResult = await this.$childProcess.spawnFromEvent(podTool, ["install"], "close", { cwd: projectRoot, stdio: ['pipe', process.stdout, 'pipe'] });
if (podInstallResult.stderr) {
const warnings = podInstallResult.stderr.match(/(\u001b\[(?:\d*;){0,5}\d*m[\s\S]+?\u001b\[(?:\d*;){0,5}\d*m)|(\[!\].*?\n)|(.*?warning.*)/gi);
_.each(warnings, (warning: string) => {
this.$logger.warnWithLabel(warning.replace("\n", ""));
});

let errors = podInstallResult.stderr;
_.each(warnings, warning => {
errors = errors.replace(warning, "");
});

if (errors.trim()) {
this.$errors.failWithoutHelp(`Pod install command failed. Error output: ${errors}`);
}
// cocoapods print a lot of non-error information on stderr. Pipe the `stderr` to `stdout`, so we won't polute CLI's stderr output.
const podInstallResult = await this.$childProcess.spawnFromEvent(podTool, ["install"], "close", { cwd: projectRoot, stdio: ['pipe', process.stdout, process.stdout] }, { throwError: false });

if (podInstallResult.exitCode !== 0) {
this.$errors.failWithoutHelp(`'${podTool} install' command failed.${podInstallResult.stderr ? " Error is: " + podInstallResult.stderr : ""}`);
}

if ((await this.$xcprojService.getXcprojInfo()).shouldUseXcproj) {
Expand Down
2 changes: 1 addition & 1 deletion lib/services/ios-project-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -969,7 +969,7 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f
await this.$childProcess.exec(createSchemeRubyScript, { cwd: this.getPlatformData(projectData).projectRoot });
}

await this.$cocoapodsService.executePodInstall(projectData, projectRoot, xcodeProjPath);
await this.$cocoapodsService.executePodInstall(projectRoot, xcodeProjPath);
}
}

Expand Down
165 changes: 165 additions & 0 deletions test/cocoapods-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -682,4 +682,169 @@ end`
});
});
});

describe("executePodInstall", () => {
const projectRoot = "nativeProjectRoot";
const xcodeProjPath = "xcodeProjectPath";

beforeEach(() => {
const childProcess = testInjector.resolve<IChildProcess>("childProcess");
childProcess.exec = async (command: string, options?: any, execOptions?: IExecOptions): Promise<any> => null;
childProcess.spawnFromEvent = async (command: string, args: string[], event: string, options?: any, spawnFromEventOptions?: ISpawnFromEventOptions): Promise<ISpawnResult> => ({
stdout: "",
stderr: "",
exitCode: 0
});

const xcprojService = testInjector.resolve<IXcprojService>("xcprojService");
xcprojService.verifyXcproj = async (shouldFail: boolean): Promise<boolean> => false;
xcprojService.getXcprojInfo = async (): Promise<IXcprojInfo> => (<any>{});
});

it("fails when pod executable is not found", async () => {
const childProcess = testInjector.resolve<IChildProcess>("childProcess");
childProcess.exec = async (command: string, options?: any, execOptions?: IExecOptions): Promise<any> => {
assert.equal(command, "which pod");
throw new Error("Missing pod executable");
};

await assert.isRejected(cocoapodsService.executePodInstall(projectRoot, xcodeProjPath), "CocoaPods or ruby gem 'xcodeproj' is not installed. Run `sudo gem install cocoapods` and try again.");
});

it("fails when xcodeproj executable is not found", async () => {
const childProcess = testInjector.resolve<IChildProcess>("childProcess");
childProcess.exec = async (command: string, options?: any, execOptions?: IExecOptions): Promise<any> => {
if (command === "which pod") {
return;
}

assert.equal(command, "which xcodeproj");
throw new Error("Missing xcodeproj executable");

};

await assert.isRejected(cocoapodsService.executePodInstall(projectRoot, xcodeProjPath), "CocoaPods or ruby gem 'xcodeproj' is not installed. Run `sudo gem install cocoapods` and try again.");
});

it("fails with correct error when xcprojService.verifyXcproj throws", async () => {
const expectedError = new Error("err");
const xcprojService = testInjector.resolve<IXcprojService>("xcprojService");
xcprojService.verifyXcproj = async (shouldFail: boolean): Promise<boolean> => {
throw expectedError;
};

await assert.isRejected(cocoapodsService.executePodInstall(projectRoot, xcodeProjPath), expectedError);
});

["pod", "sandbox-pod"].forEach(podExecutable => {
it(`uses ${podExecutable} executable when USE_POD_SANDBOX is ${podExecutable === "sandbox-pod"}`, async () => {
const config = testInjector.resolve<IConfiguration>("config");
config.USE_POD_SANDBOX = podExecutable === "sandbox-pod";
const childProcess = testInjector.resolve<IChildProcess>("childProcess");
let commandCalled = "";
childProcess.spawnFromEvent = async (command: string, args: string[], event: string, options?: any, spawnFromEventOptions?: ISpawnFromEventOptions): Promise<ISpawnResult> => {
commandCalled = command;
return {
stdout: "",
stderr: "",
exitCode: 0
};
};

await cocoapodsService.executePodInstall(projectRoot, xcodeProjPath);
assert.equal(commandCalled, podExecutable);
});
});

it("calls pod install spawnFromEvent with correct arguments", async () => {
const childProcess = testInjector.resolve<IChildProcess>("childProcess");
let commandCalled = "";
childProcess.spawnFromEvent = async (command: string, args: string[], event: string, options?: any, spawnFromEventOptions?: ISpawnFromEventOptions): Promise<ISpawnResult> => {
commandCalled = command;
assert.deepEqual(args, ["install"]);
assert.equal(event, "close");
assert.deepEqual(options, { cwd: projectRoot, stdio: ['pipe', process.stdout, process.stdout] });
assert.deepEqual(spawnFromEventOptions, { throwError: false });
return {
stdout: "",
stderr: "",
exitCode: 0
};
};

await cocoapodsService.executePodInstall(projectRoot, xcodeProjPath);
assert.equal(commandCalled, "pod");
});

it("fails when pod install exits with code that is not 0", async () => {
const childProcess = testInjector.resolve<IChildProcess>("childProcess");
childProcess.spawnFromEvent = async (command: string, args: string[], event: string, options?: any, spawnFromEventOptions?: ISpawnFromEventOptions): Promise<ISpawnResult> => {
return {
stdout: "",
stderr: "",
exitCode: 1
};
};

await assert.isRejected(cocoapodsService.executePodInstall(projectRoot, xcodeProjPath), "'pod install' command failed.");
});

it("returns the result of the pod install spawnFromEvent methdo", async () => {
const childProcess = testInjector.resolve<IChildProcess>("childProcess");
const expectedResult = {
stdout: "pod install finished",
stderr: "",
exitCode: 0
};
childProcess.spawnFromEvent = async (command: string, args: string[], event: string, options?: any, spawnFromEventOptions?: ISpawnFromEventOptions): Promise<ISpawnResult> => {
return expectedResult;
};

const result = await cocoapodsService.executePodInstall(projectRoot, xcodeProjPath);
assert.deepEqual(result, expectedResult);
});

it("executes xcproj command with correct arguments when is true", async () => {
const xcprojService = testInjector.resolve<IXcprojService>("xcprojService");
xcprojService.getXcprojInfo = async (): Promise<IXcprojInfo> => (<any>{
shouldUseXcproj: true
});

const spawnFromEventCalls: any[] = [];
const childProcess = testInjector.resolve<IChildProcess>("childProcess");
childProcess.spawnFromEvent = async (command: string, args: string[], event: string, options?: any, spawnFromEventOptions?: ISpawnFromEventOptions): Promise<ISpawnResult> => {
spawnFromEventCalls.push({
command,
args,
event,
options,
spawnFromEventOptions
});
return {
stdout: "",
stderr: "",
exitCode: 0
};
};

await cocoapodsService.executePodInstall(projectRoot, xcodeProjPath);
assert.deepEqual(spawnFromEventCalls, [
{
command: "pod",
args: ["install"],
event: "close",
options: { cwd: projectRoot, stdio: ['pipe', process.stdout, process.stdout] },
spawnFromEventOptions: { throwError: false }
},
{
command: "xcproj",
args: ["--project", xcodeProjPath, "touch"],
event: "close",
options: undefined,
spawnFromEventOptions: undefined
}
]);

});
});
});

0 comments on commit a970cbe

Please sign in to comment.