Skip to content

Commit a96bf3a

Browse files
committed
Add InstallPods command for iOS dependencies
- Add a VS Code command to run `pod install` - Support CocoaPods discovery via rbenv, rvm, Homebrew, and system paths - Ensure a consistent execution environment (PATH, RBENV_ROOT, locale) - Provide clearer diagnostics and guidance for CocoaPods failures - Keep command output defaults consistent (English)
1 parent 0f3d941 commit a96bf3a

6 files changed

Lines changed: 264 additions & 0 deletions

File tree

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,12 @@
474474
"category": "React Native",
475475
"enablement": "!config.security.workspace.trust.enabled || isWorkspaceTrusted"
476476
},
477+
{
478+
"command": "reactNative.installPods",
479+
"title": "Install CocoaPods dependencies",
480+
"category": "React Native",
481+
"enablement": "!config.security.workspace.trust.enabled || isWorkspaceTrusted"
482+
},
477483
{
478484
"command": "reactNative.killPort",
479485
"title": "Kill Port",

src/common/error/errorStrings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,4 +426,5 @@ export const ERROR_STRINGS = {
426426
[InternalErrorCode.FaiedToSetNewArch]: "Failed to set New Architecture",
427427
[InternalErrorCode.FailedToToggleNetworkView]: "Failed to config network view",
428428
[InternalErrorCode.FailedToRunEasBuild]: "Failed to run eas build",
429+
[InternalErrorCode.FailedToInstallPods]: "Failed to install pods",
429430
};

src/common/error/internalErrorCode.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export enum InternalErrorCode {
4040
FaiedToSetNewArch = 135,
4141
FailedToToggleNetworkView = 136,
4242
FailedToRunEasBuild = 137,
43+
FailedToInstallPods = 138,
4344

4445
// Device Deployer errors
4546
IOSDeployNotFound = 201,

src/extension/commands/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export * from "./openEASProject";
2929
export * from "./revertOpenModule";
3030
export * from "./openRNUpgradeHelper";
3131
export * from "./installExpoGoApplication";
32+
export * from "./installPods";
3233
export * from "./prebuild";
3334
export * from "./prebuildClean";
3435
export * from "./reopenQRCode";
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for details.
3+
4+
import * as assert from "assert";
5+
import * as fs from "fs";
6+
import * as os from "os";
7+
import * as path from "path";
8+
import * as url from "url";
9+
import * as vscode from "vscode";
10+
import { ErrorHelper } from "../../common/error/errorHelper";
11+
import { InternalErrorCode } from "../../common/error/internalErrorCode";
12+
import { ChildProcess } from "../../common/node/childProcess";
13+
import { OutputChannelLogger } from "../log/OutputChannelLogger";
14+
import { Command } from "./util/command";
15+
16+
const logger = OutputChannelLogger.getMainChannel();
17+
const childProcess = new ChildProcess();
18+
19+
export class InstallPods extends Command {
20+
codeName = "installPods";
21+
label = "Install CocoaPods dependencies";
22+
error = ErrorHelper.getInternalError(InternalErrorCode.FailedToInstallPods);
23+
requiresTrust = true;
24+
25+
async baseFn(): Promise<void> {
26+
assert(this.project);
27+
if (os.platform() !== "darwin") {
28+
void vscode.window.showWarningMessage("CocoaPods is only supported on macOS.");
29+
return;
30+
}
31+
const projectPath = this.project.getPackager().getProjectPath();
32+
const iosPath = path.join(projectPath, "ios");
33+
if (!fs.existsSync(iosPath)) {
34+
const errorMsg =
35+
"iOS directory not found. Make sure this is a React Native project with iOS support.";
36+
logger.error(errorMsg);
37+
void vscode.window.showErrorMessage(errorMsg);
38+
return;
39+
}
40+
const podfilePath = path.join(iosPath, "Podfile");
41+
if (!fs.existsSync(podfilePath)) {
42+
const errorMsg = "Podfile not found in the ios directory.";
43+
logger.error(errorMsg);
44+
void vscode.window.showErrorMessage(errorMsg);
45+
return;
46+
}
47+
logger.info("Installing CocoaPods dependencies...");
48+
logger.info(`Working directory: ${iosPath}`);
49+
try {
50+
const enhancedEnv = this.getEnhancedEnvironment();
51+
const podCommand = this.findPodCommand();
52+
logger.info(`Using pod command: ${podCommand}`);
53+
try {
54+
const versionResult = await childProcess.exec(`${podCommand} --version`, {
55+
env: enhancedEnv,
56+
timeout: 10000,
57+
});
58+
const versionOutput = await versionResult.outcome;
59+
logger.info(`Pod version: ${versionOutput.trim()}`);
60+
} catch (versionError) {
61+
const errorMsg =
62+
"Cannot execute pod command. Please ensure CocoaPods is properly installed.";
63+
logger.error(errorMsg);
64+
void vscode.window.showErrorMessage(errorMsg);
65+
throw new Error(errorMsg);
66+
}
67+
logger.info(`Executing: ${podCommand} install`);
68+
const installResult = await childProcess.exec(`${podCommand} install`, {
69+
cwd: iosPath,
70+
env: enhancedEnv,
71+
maxBuffer: 1024 * 1024 * 10,
72+
timeout: 300000,
73+
});
74+
const stdout = await installResult.outcome;
75+
if (stdout) {
76+
logger.info(stdout);
77+
}
78+
logger.info("CocoaPods installation completed successfully");
79+
void vscode.window.showInformationMessage(
80+
"CocoaPods dependencies installed successfully.",
81+
);
82+
} catch (error: any) {
83+
let errorMessage = "Unknown error";
84+
let stderr = "";
85+
let stdout = "";
86+
if (error instanceof Error) {
87+
errorMessage = error.message || "Unknown error";
88+
}
89+
if (error && typeof error === "object") {
90+
if ("stderr" in error && error.stderr) {
91+
stderr = String(error.stderr);
92+
}
93+
if ("stdout" in error && error.stdout) {
94+
stdout = String(error.stdout);
95+
}
96+
}
97+
const suggestion = this.getSuggestionForError(`${errorMessage} ${stderr}`);
98+
const baseMessage = "Failed to install CocoaPods dependencies.";
99+
let fullErrorMessage = `${baseMessage}\n${errorMessage}`;
100+
if (stderr) {
101+
fullErrorMessage = `${fullErrorMessage}\n\nError details:\n${stderr}`;
102+
}
103+
if (suggestion) {
104+
fullErrorMessage = `${fullErrorMessage}\n\n${suggestion}`;
105+
}
106+
logger.error(fullErrorMessage);
107+
if (stdout) {
108+
logger.info(`Command output: ${stdout}`);
109+
}
110+
if (error instanceof Error && error.stack) {
111+
logger.error(`Stack trace: ${error.stack}`);
112+
}
113+
void vscode.window.showErrorMessage(fullErrorMessage);
114+
const enhancedError = new Error(fullErrorMessage);
115+
if (error instanceof Error && error.stack) {
116+
enhancedError.stack = error.stack;
117+
}
118+
throw enhancedError;
119+
}
120+
}
121+
122+
private findPodCommand(): string {
123+
const homeDir = os.homedir();
124+
const possiblePodPaths = [
125+
`${homeDir}/.rbenv/shims/pod`,
126+
`${homeDir}/.rvm/bin/pod`,
127+
"/opt/homebrew/bin/pod",
128+
"/usr/local/bin/pod",
129+
"/Library/Ruby/Gems/2.6.0/bin/pod",
130+
"/Library/Ruby/Gems/3.0.0/bin/pod",
131+
"/Library/Ruby/Gems/3.3.0/bin/pod",
132+
];
133+
logger.info("Searching for pod command...");
134+
for (const possiblePath of possiblePodPaths) {
135+
if (fs.existsSync(possiblePath)) {
136+
try {
137+
fs.accessSync(possiblePath, fs.constants.X_OK);
138+
logger.info(`Found executable pod at: ${possiblePath}`);
139+
return possiblePath;
140+
} catch (accessError) {
141+
logger.warning(`Found pod at ${possiblePath} but it's not executable`);
142+
}
143+
}
144+
}
145+
logger.warning("Pod command not found in common locations, using 'pod' from PATH");
146+
return "pod";
147+
}
148+
149+
private getEnhancedEnvironment(): { [key: string]: string } {
150+
const env = { ...process.env } as { [key: string]: string };
151+
const homeDir = os.homedir();
152+
logger.info(`Using HOME directory: ${homeDir}`);
153+
const rbenvRoot = process.env.RBENV_ROOT || `${homeDir}/.rbenv`;
154+
env.RBENV_ROOT = rbenvRoot;
155+
logger.info(`RBENV_ROOT: ${rbenvRoot}`);
156+
const rbenvShims = `${rbenvRoot}/shims`;
157+
const rbenvBin = `${rbenvRoot}/bin`;
158+
const additionalPaths = [
159+
rbenvShims,
160+
rbenvBin,
161+
"/usr/local/bin",
162+
"/opt/homebrew/bin",
163+
"/opt/homebrew/sbin",
164+
`${homeDir}/.rvm/bin`,
165+
`${homeDir}/.gem/ruby/2.6.0/bin`,
166+
`${homeDir}/.gem/ruby/3.0.0/bin`,
167+
`${homeDir}/.gem/ruby/3.3.0/bin`,
168+
"/Library/Ruby/Gems/2.6.0/bin",
169+
"/Library/Ruby/Gems/3.0.0/bin",
170+
"/Library/Ruby/Gems/3.3.0/bin",
171+
"/System/Library/Frameworks/Ruby.framework/Versions/Current/usr/bin",
172+
"/usr/bin",
173+
"/bin",
174+
"/usr/sbin",
175+
"/sbin",
176+
];
177+
const originalPath = env.PATH || "";
178+
const originalPathArray = originalPath
179+
.split(":")
180+
.filter(p => !p.includes(".rbenv") && p.trim() !== "");
181+
const allPaths = [...additionalPaths, ...originalPathArray];
182+
const uniquePaths = Array.from(new Set(allPaths)).filter(Boolean);
183+
env.PATH = uniquePaths.join(":");
184+
logger.info(`Enhanced PATH: ${env.PATH}`);
185+
if (!env.SHELL) {
186+
env.SHELL = "/bin/zsh";
187+
}
188+
if (process.env.RBENV_VERSION) {
189+
env.RBENV_VERSION = process.env.RBENV_VERSION;
190+
}
191+
if (process.env.GEM_HOME) {
192+
env.GEM_HOME = process.env.GEM_HOME;
193+
}
194+
if (process.env.GEM_PATH) {
195+
env.GEM_PATH = process.env.GEM_PATH;
196+
}
197+
env.LC_ALL = env.LC_ALL || "en_US.UTF-8";
198+
env.LANG = env.LANG || "en_US.UTF-8";
199+
return env;
200+
}
201+
202+
private isCDNError(errorMessage: string): boolean {
203+
const cdnDomains = ["trunk.cocoapods.org", "cdn.cocoapods.org"];
204+
if (errorMessage.toLowerCase().includes("cdn")) {
205+
return true;
206+
}
207+
const urlPattern = /https?:\/\/\S+/gi;
208+
const urls = errorMessage.match(urlPattern);
209+
if (!urls) {
210+
return cdnDomains.some(domain => errorMessage.includes(domain));
211+
}
212+
for (const urlString of urls) {
213+
try {
214+
const parsedUrl = new url.URL(urlString);
215+
const hostname = parsedUrl.hostname.toLowerCase();
216+
for (const domain of cdnDomains) {
217+
if (hostname === domain || hostname.endsWith(`.${domain}`)) {
218+
return true;
219+
}
220+
}
221+
} catch (e) {
222+
continue;
223+
}
224+
}
225+
return false;
226+
}
227+
228+
private getSuggestionForError(errorMessage: string): string {
229+
if (
230+
errorMessage.includes("command not found") ||
231+
errorMessage.includes("pod: not found") ||
232+
errorMessage.includes("'pod' is not recognized") ||
233+
errorMessage.includes("Cannot execute pod command")
234+
) {
235+
return "CocoaPods may not be installed or not accessible. Install it via:\n • System Ruby: sudo gem install cocoapods\n • Homebrew: brew install cocoapods\n • rbenv: gem install cocoapods\nAfter installation, please restart VS Code to refresh the environment.";
236+
}
237+
if (this.isCDNError(errorMessage)) {
238+
return "CDN error. Try running 'pod repo update' in the terminal.";
239+
}
240+
if (errorMessage.includes("Xcode") || errorMessage.includes("xcrun")) {
241+
return "Xcode command line tools may be missing. Run 'xcode-select --install' in the terminal.";
242+
}
243+
if (errorMessage.includes("ruby") || errorMessage.includes("Ruby")) {
244+
return "Ruby environment issue. Check your Ruby installation and version with 'ruby --version'.";
245+
}
246+
if (errorMessage.includes("permission") || errorMessage.includes("Permission")) {
247+
return "Permission denied. Try checking directory permissions or running the command with appropriate privileges.";
248+
}
249+
if (errorMessage.includes("Gem::") || errorMessage.includes("gem")) {
250+
return "Ruby gem issue. Try updating your gems with 'gem update --system' or 'sudo gem update --system'.";
251+
}
252+
return "";
253+
}
254+
}

test/extension/rn-extension.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ suite("rn-extension", function () {
174174
"reactNative.revertOpenModule",
175175
"reactNative.openRNUpgradeHelper",
176176
"reactNative.installExpoGoApplication",
177+
"reactNative.installPods",
177178
"reactNative.expoPrebuild",
178179
"reactNative.expoPrebuildClean",
179180
"reactNative.reopenQRCode",

0 commit comments

Comments
 (0)