Skip to content

Commit a273145

Browse files
feat(apple): doctor issue with asdf (#6062)
If the user is using ASDF the 'doctor' command may fail at the Cocoapods stage as it will be using the global tool definitions instead of those specified by the project (if any). This fixes the problem by generating a .tool-versions file with the correct settings in the temp directory used for the Cocoapods test. Also resolve the asdf Ruby version with an explicit cwd so it reflects the project even when `ns doctor` is invoked outside the project directory, and newline-terminate the `.tool-versions` entry so it cannot merge with existing content. Adds unit tests for the asdf-present and asdf-absent paths. --------- Co-authored-by: Nathan Walker <walkerrunpdx@gmail.com>
1 parent 354903e commit a273145

3 files changed

Lines changed: 178 additions & 2 deletions

File tree

packages/doctor/src/sys-info.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,39 @@ export class SysInfo implements NativeScriptDoctor.ISysInfo {
424424
tempDirectory,
425425
);
426426
const xcodeProjectDir = path.join(tempDirectory, "cocoapods");
427+
428+
// If asdf version manager is installed, get the current Ruby version for the project directory and write it to the temporary project directory.
429+
// Resolve relative to the directory `ns doctor` was invoked from, since it can be run outside of a project directory.
430+
const asdfResult = await this.childProcess.spawnFromEvent(
431+
"asdf",
432+
["current", "ruby"],
433+
"exit",
434+
{ ignoreError: true, spawnOptions: { cwd: process.cwd() } },
435+
);
436+
437+
if (asdfResult.exitCode === 0) {
438+
const asdfVersionMatch = (asdfResult.stdout as string).match(
439+
SysInfo.VERSION_REGEXP,
440+
);
441+
442+
if (asdfVersionMatch?.[0]) {
443+
const asdfVersion = asdfVersionMatch[0];
444+
const asdfConfigPath = path.join(
445+
xcodeProjectDir,
446+
".tool-versions",
447+
);
448+
const wroteASDFConfig = this.fileSystem.appendFile(
449+
asdfConfigPath,
450+
`ruby ${asdfVersion}\n`,
451+
);
452+
if (!wroteASDFConfig) {
453+
console.warn(
454+
`CocoaPods invocation may fail, check asdf config`,
455+
);
456+
}
457+
}
458+
}
459+
427460
const spawnResult = await this.childProcess.spawnFromEvent(
428461
"pod",
429462
["install"],
@@ -432,6 +465,7 @@ export class SysInfo implements NativeScriptDoctor.ISysInfo {
432465
);
433466
return !spawnResult.exitCode;
434467
} catch (err) {
468+
console.log(`Pod command failed - ${err}`);
435469
return false;
436470
} finally {
437471
this.fileSystem.deleteEntry(tempDirectory);

packages/doctor/src/wrappers/file-system.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,17 @@ export class FileSystem {
88
return fs.existsSync(path.resolve(filePath));
99
}
1010

11+
public appendFile(filePath: string, text: string): boolean {
12+
let success = false;
13+
try {
14+
fs.appendFileSync(path.resolve(filePath), text);
15+
success = true;
16+
} catch (err) {
17+
console.error(`appendFile failed with ${err}`);
18+
}
19+
return success;
20+
}
21+
1122
public extractZip(pathToZip: string, outputDir: string): Promise<void> {
1223
return new Promise((resolve, reject) => {
1324
yauzl.open(
@@ -46,7 +57,7 @@ export class FileSystem {
4657
zipFile.once("end", () => resolve());
4758

4859
zipFile.readEntry();
49-
}
60+
},
5061
);
5162
});
5263
}
@@ -57,7 +68,7 @@ export class FileSystem {
5768

5869
public readJson<T>(
5970
filePath: string,
60-
options?: { encoding?: null; flag?: string }
71+
options?: { encoding?: null; flag?: string },
6172
): T {
6273
const content = fs.readFileSync(filePath, options);
6374
return JSON.parse(content.toString());

packages/doctor/test/sys-info.ts

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as assert from "assert";
2+
import * as fs from "fs";
23
import * as path from "path";
34
import { EOL } from "os";
45
import { SysInfo } from "../src/sys-info";
@@ -910,4 +911,134 @@ Java HotSpot(TM) 64-Bit Server VM (build 25.202-b08, mixed mode)`),
910911
});
911912
});
912913
});
914+
915+
describe("isCocoaPodsWorkingCorrectly", () => {
916+
interface ICocoaPodsMockOptions {
917+
// Mimics the ChildProcess result for `asdf current ruby`.
918+
// When omitted, asdf is treated as not installed.
919+
asdfResult?: {
920+
stdout?: string;
921+
stderr?: string;
922+
exitCode?: number | string;
923+
};
924+
podExitCode?: number;
925+
}
926+
927+
const createCocoaPodsSysInfo = (options: ICocoaPodsMockOptions) => {
928+
const appendedFiles: { filePath: string; text: string }[] = [];
929+
const spawnCalls: {
930+
command: string;
931+
options?: ISpawnFromEventOptions;
932+
}[] = [];
933+
934+
const childProcess: any = {
935+
spawnFromEvent: async (
936+
command: string,
937+
args: string[],
938+
event: string,
939+
spawnFromEventOptions?: ISpawnFromEventOptions,
940+
) => {
941+
const fullCommand = `${command} ${args.join(" ")}`;
942+
spawnCalls.push({
943+
command: fullCommand,
944+
options: spawnFromEventOptions,
945+
});
946+
947+
if (fullCommand === "asdf current ruby") {
948+
// Mirror the ChildProcess wrapper: with `ignoreError` it always
949+
// resolves, surfacing a non-zero exitCode instead of throwing when
950+
// asdf is missing/misconfigured.
951+
return (
952+
options.asdfResult || {
953+
stdout: "",
954+
stderr: "spawn asdf ENOENT",
955+
exitCode: "ENOENT",
956+
}
957+
);
958+
}
959+
960+
return {
961+
stdout: "",
962+
stderr: "",
963+
exitCode: options.podExitCode ?? 0,
964+
};
965+
},
966+
exec: async () => ({ stdout: "", stderr: "" }),
967+
execFile: async (): Promise<any> => undefined,
968+
execSync: (): string => null,
969+
};
970+
971+
const fileSystem: any = {
972+
exists: () => true,
973+
extractZip: () => Promise.resolve(),
974+
readDirectory: () => [],
975+
appendFile: (filePath: string, text: string) => {
976+
appendedFiles.push({ filePath, text });
977+
return true;
978+
},
979+
deleteEntry: (filePath: string) =>
980+
fs.rmSync(filePath, { recursive: true, force: true }),
981+
};
982+
983+
const hostInfo: any = {
984+
isDarwin: true,
985+
isWindows: false,
986+
isLinux: false,
987+
};
988+
989+
const helpers = new Helpers(hostInfo);
990+
const sysInfo = new SysInfo(
991+
childProcess,
992+
fileSystem,
993+
helpers,
994+
hostInfo,
995+
null,
996+
androidToolsInfo,
997+
);
998+
999+
return { sysInfo, appendedFiles, spawnCalls };
1000+
};
1001+
1002+
it("writes the active Ruby version to .tool-versions when asdf is available", async () => {
1003+
const { sysInfo, appendedFiles, spawnCalls } = createCocoaPodsSysInfo({
1004+
asdfResult: {
1005+
stdout:
1006+
"ruby 3.2.1 /Users/user/app/.tool-versions",
1007+
exitCode: 0,
1008+
},
1009+
});
1010+
1011+
const result = await sysInfo.isCocoaPodsWorkingCorrectly();
1012+
1013+
assert.deepEqual(result, true);
1014+
assert.deepEqual(appendedFiles.length, 1);
1015+
assert.ok(
1016+
appendedFiles[0].filePath.endsWith(
1017+
path.join("cocoapods", ".tool-versions"),
1018+
),
1019+
);
1020+
// The entry must be newline-terminated so it does not merge with existing content.
1021+
assert.deepEqual(appendedFiles[0].text, "ruby 3.2.1\n");
1022+
1023+
const asdfCall = spawnCalls.find(
1024+
(c) => c.command === "asdf current ruby",
1025+
);
1026+
assert.ok(asdfCall, "expected asdf to be probed");
1027+
// The probe must not throw when asdf is missing/misconfigured...
1028+
assert.deepEqual(asdfCall.options.ignoreError, true);
1029+
// ...and it must resolve the version relative to the invocation directory.
1030+
assert.deepEqual(asdfCall.options.spawnOptions.cwd, process.cwd());
1031+
});
1032+
1033+
it("does not write .tool-versions and stays healthy when asdf is not installed", async () => {
1034+
const { sysInfo, appendedFiles } = createCocoaPodsSysInfo({
1035+
// asdf missing -> wrapper resolves with a non-zero (ENOENT) exit code.
1036+
});
1037+
1038+
const result = await sysInfo.isCocoaPodsWorkingCorrectly();
1039+
1040+
assert.deepEqual(result, true);
1041+
assert.deepEqual(appendedFiles.length, 0);
1042+
});
1043+
});
9131044
});

0 commit comments

Comments
 (0)