Skip to content

Commit a970cbe

Browse files
committedSep 26, 2018
fix: CLI breaks process when pod install has not failed
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.
1 parent c578acc commit a970cbe

File tree

4 files changed

+173
-19
lines changed

4 files changed

+173
-19
lines changed
 

‎lib/definitions/project.d.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -496,12 +496,11 @@ interface ICocoaPodsService {
496496

497497
/**
498498
* Executes `pod install` or `sanboxpod install` in the passed project.
499-
* @param {IProjectData} projectData Information about the project.
500499
* @param {string} projectRoot The root directory of the native iOS project.
501500
* @param {string} xcodeProjPath The full path to the .xcodeproj file.
502501
* @returns {Promise<ISpawnResult>} Information about the spawned process.
503502
*/
504-
executePodInstall(projectData: IProjectData, projectRoot: string, xcodeProjPath: string): Promise<ISpawnResult>;
503+
executePodInstall(projectRoot: string, xcodeProjPath: string): Promise<ISpawnResult>;
505504
}
506505

507506
interface IRubyFunction {

‎lib/services/cocoapods-service.ts

+6-16
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export class CocoaPodsService implements ICocoaPodsService {
2525
return path.join(projectRoot, PODFILE_NAME);
2626
}
2727

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

3939
this.$logger.info("Installing pods...");
4040
const podTool = this.$config.USE_POD_SANDBOX ? "sandbox-pod" : "pod";
41-
const podInstallResult = await this.$childProcess.spawnFromEvent(podTool, ["install"], "close", { cwd: projectRoot, stdio: ['pipe', process.stdout, 'pipe'] });
42-
if (podInstallResult.stderr) {
43-
const warnings = podInstallResult.stderr.match(/(\u001b\[(?:\d*;){0,5}\d*m[\s\S]+?\u001b\[(?:\d*;){0,5}\d*m)|(\[!\].*?\n)|(.*?warning.*)/gi);
44-
_.each(warnings, (warning: string) => {
45-
this.$logger.warnWithLabel(warning.replace("\n", ""));
46-
});
47-
48-
let errors = podInstallResult.stderr;
49-
_.each(warnings, warning => {
50-
errors = errors.replace(warning, "");
51-
});
52-
53-
if (errors.trim()) {
54-
this.$errors.failWithoutHelp(`Pod install command failed. Error output: ${errors}`);
55-
}
41+
// cocoapods print a lot of non-error information on stderr. Pipe the `stderr` to `stdout`, so we won't polute CLI's stderr output.
42+
const podInstallResult = await this.$childProcess.spawnFromEvent(podTool, ["install"], "close", { cwd: projectRoot, stdio: ['pipe', process.stdout, process.stdout] }, { throwError: false });
43+
44+
if (podInstallResult.exitCode !== 0) {
45+
this.$errors.failWithoutHelp(`'${podTool} install' command failed.${podInstallResult.stderr ? " Error is: " + podInstallResult.stderr : ""}`);
5646
}
5747

5848
if ((await this.$xcprojService.getXcprojInfo()).shouldUseXcproj) {

‎lib/services/ios-project-service.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -969,7 +969,7 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f
969969
await this.$childProcess.exec(createSchemeRubyScript, { cwd: this.getPlatformData(projectData).projectRoot });
970970
}
971971

972-
await this.$cocoapodsService.executePodInstall(projectData, projectRoot, xcodeProjPath);
972+
await this.$cocoapodsService.executePodInstall(projectRoot, xcodeProjPath);
973973
}
974974
}
975975

‎test/cocoapods-service.ts

+165
Original file line numberDiff line numberDiff line change
@@ -682,4 +682,169 @@ end`
682682
});
683683
});
684684
});
685+
686+
describe("executePodInstall", () => {
687+
const projectRoot = "nativeProjectRoot";
688+
const xcodeProjPath = "xcodeProjectPath";
689+
690+
beforeEach(() => {
691+
const childProcess = testInjector.resolve<IChildProcess>("childProcess");
692+
childProcess.exec = async (command: string, options?: any, execOptions?: IExecOptions): Promise<any> => null;
693+
childProcess.spawnFromEvent = async (command: string, args: string[], event: string, options?: any, spawnFromEventOptions?: ISpawnFromEventOptions): Promise<ISpawnResult> => ({
694+
stdout: "",
695+
stderr: "",
696+
exitCode: 0
697+
});
698+
699+
const xcprojService = testInjector.resolve<IXcprojService>("xcprojService");
700+
xcprojService.verifyXcproj = async (shouldFail: boolean): Promise<boolean> => false;
701+
xcprojService.getXcprojInfo = async (): Promise<IXcprojInfo> => (<any>{});
702+
});
703+
704+
it("fails when pod executable is not found", async () => {
705+
const childProcess = testInjector.resolve<IChildProcess>("childProcess");
706+
childProcess.exec = async (command: string, options?: any, execOptions?: IExecOptions): Promise<any> => {
707+
assert.equal(command, "which pod");
708+
throw new Error("Missing pod executable");
709+
};
710+
711+
await assert.isRejected(cocoapodsService.executePodInstall(projectRoot, xcodeProjPath), "CocoaPods or ruby gem 'xcodeproj' is not installed. Run `sudo gem install cocoapods` and try again.");
712+
});
713+
714+
it("fails when xcodeproj executable is not found", async () => {
715+
const childProcess = testInjector.resolve<IChildProcess>("childProcess");
716+
childProcess.exec = async (command: string, options?: any, execOptions?: IExecOptions): Promise<any> => {
717+
if (command === "which pod") {
718+
return;
719+
}
720+
721+
assert.equal(command, "which xcodeproj");
722+
throw new Error("Missing xcodeproj executable");
723+
724+
};
725+
726+
await assert.isRejected(cocoapodsService.executePodInstall(projectRoot, xcodeProjPath), "CocoaPods or ruby gem 'xcodeproj' is not installed. Run `sudo gem install cocoapods` and try again.");
727+
});
728+
729+
it("fails with correct error when xcprojService.verifyXcproj throws", async () => {
730+
const expectedError = new Error("err");
731+
const xcprojService = testInjector.resolve<IXcprojService>("xcprojService");
732+
xcprojService.verifyXcproj = async (shouldFail: boolean): Promise<boolean> => {
733+
throw expectedError;
734+
};
735+
736+
await assert.isRejected(cocoapodsService.executePodInstall(projectRoot, xcodeProjPath), expectedError);
737+
});
738+
739+
["pod", "sandbox-pod"].forEach(podExecutable => {
740+
it(`uses ${podExecutable} executable when USE_POD_SANDBOX is ${podExecutable === "sandbox-pod"}`, async () => {
741+
const config = testInjector.resolve<IConfiguration>("config");
742+
config.USE_POD_SANDBOX = podExecutable === "sandbox-pod";
743+
const childProcess = testInjector.resolve<IChildProcess>("childProcess");
744+
let commandCalled = "";
745+
childProcess.spawnFromEvent = async (command: string, args: string[], event: string, options?: any, spawnFromEventOptions?: ISpawnFromEventOptions): Promise<ISpawnResult> => {
746+
commandCalled = command;
747+
return {
748+
stdout: "",
749+
stderr: "",
750+
exitCode: 0
751+
};
752+
};
753+
754+
await cocoapodsService.executePodInstall(projectRoot, xcodeProjPath);
755+
assert.equal(commandCalled, podExecutable);
756+
});
757+
});
758+
759+
it("calls pod install spawnFromEvent with correct arguments", async () => {
760+
const childProcess = testInjector.resolve<IChildProcess>("childProcess");
761+
let commandCalled = "";
762+
childProcess.spawnFromEvent = async (command: string, args: string[], event: string, options?: any, spawnFromEventOptions?: ISpawnFromEventOptions): Promise<ISpawnResult> => {
763+
commandCalled = command;
764+
assert.deepEqual(args, ["install"]);
765+
assert.equal(event, "close");
766+
assert.deepEqual(options, { cwd: projectRoot, stdio: ['pipe', process.stdout, process.stdout] });
767+
assert.deepEqual(spawnFromEventOptions, { throwError: false });
768+
return {
769+
stdout: "",
770+
stderr: "",
771+
exitCode: 0
772+
};
773+
};
774+
775+
await cocoapodsService.executePodInstall(projectRoot, xcodeProjPath);
776+
assert.equal(commandCalled, "pod");
777+
});
778+
779+
it("fails when pod install exits with code that is not 0", async () => {
780+
const childProcess = testInjector.resolve<IChildProcess>("childProcess");
781+
childProcess.spawnFromEvent = async (command: string, args: string[], event: string, options?: any, spawnFromEventOptions?: ISpawnFromEventOptions): Promise<ISpawnResult> => {
782+
return {
783+
stdout: "",
784+
stderr: "",
785+
exitCode: 1
786+
};
787+
};
788+
789+
await assert.isRejected(cocoapodsService.executePodInstall(projectRoot, xcodeProjPath), "'pod install' command failed.");
790+
});
791+
792+
it("returns the result of the pod install spawnFromEvent methdo", async () => {
793+
const childProcess = testInjector.resolve<IChildProcess>("childProcess");
794+
const expectedResult = {
795+
stdout: "pod install finished",
796+
stderr: "",
797+
exitCode: 0
798+
};
799+
childProcess.spawnFromEvent = async (command: string, args: string[], event: string, options?: any, spawnFromEventOptions?: ISpawnFromEventOptions): Promise<ISpawnResult> => {
800+
return expectedResult;
801+
};
802+
803+
const result = await cocoapodsService.executePodInstall(projectRoot, xcodeProjPath);
804+
assert.deepEqual(result, expectedResult);
805+
});
806+
807+
it("executes xcproj command with correct arguments when is true", async () => {
808+
const xcprojService = testInjector.resolve<IXcprojService>("xcprojService");
809+
xcprojService.getXcprojInfo = async (): Promise<IXcprojInfo> => (<any>{
810+
shouldUseXcproj: true
811+
});
812+
813+
const spawnFromEventCalls: any[] = [];
814+
const childProcess = testInjector.resolve<IChildProcess>("childProcess");
815+
childProcess.spawnFromEvent = async (command: string, args: string[], event: string, options?: any, spawnFromEventOptions?: ISpawnFromEventOptions): Promise<ISpawnResult> => {
816+
spawnFromEventCalls.push({
817+
command,
818+
args,
819+
event,
820+
options,
821+
spawnFromEventOptions
822+
});
823+
return {
824+
stdout: "",
825+
stderr: "",
826+
exitCode: 0
827+
};
828+
};
829+
830+
await cocoapodsService.executePodInstall(projectRoot, xcodeProjPath);
831+
assert.deepEqual(spawnFromEventCalls, [
832+
{
833+
command: "pod",
834+
args: ["install"],
835+
event: "close",
836+
options: { cwd: projectRoot, stdio: ['pipe', process.stdout, process.stdout] },
837+
spawnFromEventOptions: { throwError: false }
838+
},
839+
{
840+
command: "xcproj",
841+
args: ["--project", xcodeProjPath, "touch"],
842+
event: "close",
843+
options: undefined,
844+
spawnFromEventOptions: undefined
845+
}
846+
]);
847+
848+
});
849+
});
685850
});

0 commit comments

Comments
 (0)