Skip to content
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
34 changes: 34 additions & 0 deletions packages/doctor/src/sys-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,39 @@ export class SysInfo implements NativeScriptDoctor.ISysInfo {
tempDirectory,
);
const xcodeProjectDir = path.join(tempDirectory, "cocoapods");

// If asdf version manager is installed, get the current Ruby version for the project directory and write it to the temporary project directory.
// Resolve relative to the directory `ns doctor` was invoked from, since it can be run outside of a project directory.
const asdfResult = await this.childProcess.spawnFromEvent(
"asdf",
["current", "ruby"],
"exit",
{ ignoreError: true, spawnOptions: { cwd: process.cwd() } },
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This should set cwd since ns doctor can be run outside of project directories.


if (asdfResult.exitCode === 0) {
const asdfVersionMatch = (asdfResult.stdout as string).match(
SysInfo.VERSION_REGEXP,
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if (asdfVersionMatch?.[0]) {
const asdfVersion = asdfVersionMatch[0];
const asdfConfigPath = path.join(
xcodeProjectDir,
".tool-versions",
);
const wroteASDFConfig = this.fileSystem.appendFile(
asdfConfigPath,
`ruby ${asdfVersion}\n`,
);
if (!wroteASDFConfig) {
console.warn(
`CocoaPods invocation may fail, check asdf config`,
);
}
}
}

const spawnResult = await this.childProcess.spawnFromEvent(
"pod",
["install"],
Expand All @@ -432,6 +465,7 @@ export class SysInfo implements NativeScriptDoctor.ISysInfo {
);
return !spawnResult.exitCode;
} catch (err) {
console.log(`Pod command failed - ${err}`);
return false;
} finally {
this.fileSystem.deleteEntry(tempDirectory);
Expand Down
15 changes: 13 additions & 2 deletions packages/doctor/src/wrappers/file-system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ export class FileSystem {
return fs.existsSync(path.resolve(filePath));
}

public appendFile(filePath: string, text: string): boolean {
let success = false;
try {
fs.appendFileSync(path.resolve(filePath), text);
success = true;
} catch (err) {
console.error(`appendFile failed with ${err}`);
}
return success;
}

public extractZip(pathToZip: string, outputDir: string): Promise<void> {
return new Promise((resolve, reject) => {
yauzl.open(
Expand Down Expand Up @@ -46,7 +57,7 @@ export class FileSystem {
zipFile.once("end", () => resolve());

zipFile.readEntry();
}
},
);
});
}
Expand All @@ -57,7 +68,7 @@ export class FileSystem {

public readJson<T>(
filePath: string,
options?: { encoding?: null; flag?: string }
options?: { encoding?: null; flag?: string },
): T {
const content = fs.readFileSync(filePath, options);
return JSON.parse(content.toString());
Expand Down
131 changes: 131 additions & 0 deletions packages/doctor/test/sys-info.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as assert from "assert";
import * as fs from "fs";
import * as path from "path";
import { EOL } from "os";
import { SysInfo } from "../src/sys-info";
Expand Down Expand Up @@ -910,4 +911,134 @@ Java HotSpot(TM) 64-Bit Server VM (build 25.202-b08, mixed mode)`),
});
});
});

describe("isCocoaPodsWorkingCorrectly", () => {
interface ICocoaPodsMockOptions {
// Mimics the ChildProcess result for `asdf current ruby`.
// When omitted, asdf is treated as not installed.
asdfResult?: {
stdout?: string;
stderr?: string;
exitCode?: number | string;
};
podExitCode?: number;
}

const createCocoaPodsSysInfo = (options: ICocoaPodsMockOptions) => {
const appendedFiles: { filePath: string; text: string }[] = [];
const spawnCalls: {
command: string;
options?: ISpawnFromEventOptions;
}[] = [];

const childProcess: any = {
spawnFromEvent: async (
command: string,
args: string[],
event: string,
spawnFromEventOptions?: ISpawnFromEventOptions,
) => {
const fullCommand = `${command} ${args.join(" ")}`;
spawnCalls.push({
command: fullCommand,
options: spawnFromEventOptions,
});

if (fullCommand === "asdf current ruby") {
// Mirror the ChildProcess wrapper: with `ignoreError` it always
// resolves, surfacing a non-zero exitCode instead of throwing when
// asdf is missing/misconfigured.
return (
options.asdfResult || {
stdout: "",
stderr: "spawn asdf ENOENT",
exitCode: "ENOENT",
}
);
}

return {
stdout: "",
stderr: "",
exitCode: options.podExitCode ?? 0,
};
},
exec: async () => ({ stdout: "", stderr: "" }),
execFile: async (): Promise<any> => undefined,
execSync: (): string => null,
};

const fileSystem: any = {
exists: () => true,
extractZip: () => Promise.resolve(),
readDirectory: () => [],
appendFile: (filePath: string, text: string) => {
appendedFiles.push({ filePath, text });
return true;
},
deleteEntry: (filePath: string) =>
fs.rmSync(filePath, { recursive: true, force: true }),
};

const hostInfo: any = {
isDarwin: true,
isWindows: false,
isLinux: false,
};

const helpers = new Helpers(hostInfo);
const sysInfo = new SysInfo(
childProcess,
fileSystem,
helpers,
hostInfo,
null,
androidToolsInfo,
);

return { sysInfo, appendedFiles, spawnCalls };
};

it("writes the active Ruby version to .tool-versions when asdf is available", async () => {
const { sysInfo, appendedFiles, spawnCalls } = createCocoaPodsSysInfo({
asdfResult: {
stdout:
"ruby 3.2.1 /Users/user/app/.tool-versions",
exitCode: 0,
},
});

const result = await sysInfo.isCocoaPodsWorkingCorrectly();

assert.deepEqual(result, true);
assert.deepEqual(appendedFiles.length, 1);
assert.ok(
appendedFiles[0].filePath.endsWith(
path.join("cocoapods", ".tool-versions"),
),
);
// The entry must be newline-terminated so it does not merge with existing content.
assert.deepEqual(appendedFiles[0].text, "ruby 3.2.1\n");

const asdfCall = spawnCalls.find(
(c) => c.command === "asdf current ruby",
);
assert.ok(asdfCall, "expected asdf to be probed");
// The probe must not throw when asdf is missing/misconfigured...
assert.deepEqual(asdfCall.options.ignoreError, true);
// ...and it must resolve the version relative to the invocation directory.
assert.deepEqual(asdfCall.options.spawnOptions.cwd, process.cwd());
});

it("does not write .tool-versions and stays healthy when asdf is not installed", async () => {
const { sysInfo, appendedFiles } = createCocoaPodsSysInfo({
// asdf missing -> wrapper resolves with a non-zero (ENOENT) exit code.
});

const result = await sysInfo.isCocoaPodsWorkingCorrectly();

assert.deepEqual(result, true);
assert.deepEqual(appendedFiles.length, 0);
});
});
});