Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
81589b3
report nice error message when dotnet is not installed
chunyu3 Jan 3, 2025
57ca9c0
change the diagonosic code
chunyu3 Jan 3, 2025
e68c4d9
Merge branch 'main' of https://github.com/microsoft/typespec into Fix…
chunyu3 Jan 3, 2025
176c45f
Merge branch 'main' into Fix5364
chunyu3 Jan 6, 2025
3adef56
Merge branch 'main' of https://github.com/microsoft/typespec into Fix…
chunyu3 Jan 8, 2025
a6d54ca
check dotnet runtime when generate fail
chunyu3 Jan 12, 2025
4dee9a5
use semver to parse and compare version
chunyu3 Jan 12, 2025
80bc6c7
Merge branch 'main' into Fix5364
chunyu3 Jan 13, 2025
1a3e0a2
Update packages/http-client-csharp/emitter/src/lib/lib.ts
chunyu3 Jan 15, 2025
35adcaa
Update packages/http-client-csharp/emitter/src/constants.ts
chunyu3 Jan 15, 2025
e9c0ed2
Update packages/http-client-csharp/emitter/src/emitter.ts
chunyu3 Jan 15, 2025
a477162
Update packages/http-client-csharp/emitter/src/lib/lib.ts
chunyu3 Jan 15, 2025
10cd636
Update packages/http-client-csharp/emitter/src/emitter.ts
chunyu3 Jan 15, 2025
6127a1b
change text
chunyu3 Jan 15, 2025
caedb3f
Update packages/http-client-csharp/emitter/src/emitter.ts
chunyu3 Jan 15, 2025
0942fdb
Merge branch 'main' into Fix5364
chunyu3 Jan 15, 2025
e34ce6f
update comment
chunyu3 Jan 15, 2025
e5c7d12
Merge branch 'Fix5364' of https://github.com/chunyu3/typespec into Fi…
chunyu3 Jan 15, 2025
75ef4ef
update the dotnet download url
chunyu3 Jan 16, 2025
8934ce3
use dotnet --version to check dotnet sdk
chunyu3 Jan 17, 2025
eee758f
Merge branch 'main' into Fix5364
chunyu3 Jan 17, 2025
f45887f
compare as number
chunyu3 Jan 17, 2025
f6b3410
remove extra quote in error message
chunyu3 Jan 17, 2025
8b0bb82
add Unit test for validateDotNetSDK
chunyu3 Jan 17, 2025
0fff783
Merge branch 'main' into Fix5364
chunyu3 Jan 17, 2025
81a4380
Update packages/http-client-csharp/emitter/src/lib/lib.ts
chunyu3 Jan 18, 2025
3694213
typo
chunyu3 Jan 20, 2025
e87f80f
mock dotnet not installed scenario
chunyu3 Jan 20, 2025
bca3546
Update packages/http-client-csharp/emitter/src/emitter.ts
chunyu3 Jan 20, 2025
057cece
format
chunyu3 Jan 20, 2025
4650160
add UT for different dotnet sdk installation scenarios with mock
chunyu3 Jan 20, 2025
b4eb6df
simple mock
chunyu3 Jan 20, 2025
2c9ab1f
Merge branch 'main' into Fix5364
chunyu3 Jan 20, 2025
1729cfa
Merge branch 'main' into Fix5364
chunyu3 Jan 21, 2025
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
1 change: 1 addition & 0 deletions packages/http-client-csharp/emitter/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export const projectedNameClientKey = "client";
export const mockApiVersion = "0000-00-00";
export const tspOutputFileName = "tspCodeModel.json";
export const configurationFileName = "Configuration.json";
export const minSupportedDotNetSdkVersion = 8;
154 changes: 100 additions & 54 deletions packages/http-client-csharp/emitter/src/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,25 @@ import {
getDirectoryPath,
joinPaths,
logDiagnostics,
NoTarget,
Program,
resolvePath,
} from "@typespec/compiler";

import { spawn, SpawnOptions } from "child_process";
import fs, { statSync } from "fs";
import { PreserveType, stringifyRefs } from "json-serialize-refs";
import { dirname } from "path";
import { fileURLToPath } from "url";
import { configurationFileName, tspOutputFileName } from "./constants.js";
import {
configurationFileName,
minSupportedDotNetSdkVersion,
tspOutputFileName,
} from "./constants.js";
import { createModel } from "./lib/client-model-builder.js";
import { reportDiagnostic } from "./lib/lib.js";
import { LoggerLevel } from "./lib/log-level.js";
import { Logger } from "./lib/logger.js";
import { execAsync } from "./lib/utils.js";
import { NetEmitterOptions, resolveOptions, resolveOutputFolder } from "./options.js";
import { defaultSDKContextOptions } from "./sdk-context-options.js";
import { Configuration } from "./type/configuration.js";
Expand Down Expand Up @@ -150,25 +156,38 @@ export async function $onEmit(context: EmitContext<NetEmitterOptions>) {
const command = `dotnet --roll-forward Major ${generatorPath} ${outputFolder} -p ${options["plugin-name"]}${constructCommandArg(newProjectOption)}${constructCommandArg(existingProjectOption)}${constructCommandArg(debugFlag)}`;
Logger.getInstance().info(command);

const result = await execAsync(
"dotnet",
[
"--roll-forward",
"Major",
generatorPath,
outputFolder,
"-p",
options["plugin-name"],
newProjectOption,
existingProjectOption,
debugFlag,
],
{ stdio: "inherit" },
);
if (result.exitCode !== 0) {
if (result.stderr) Logger.getInstance().error(result.stderr);
if (result.stdout) Logger.getInstance().verbose(result.stdout);
throw new Error(`Failed to generate SDK. Exit code: ${result.exitCode}`);
try {
const result = await execAsync(
"dotnet",
[
"--roll-forward",
"Major",
generatorPath,
outputFolder,
"-p",
options["plugin-name"],
newProjectOption,
existingProjectOption,
debugFlag,
],
{ stdio: "inherit" },
);
if (result.exitCode !== 0) {
const isValid = await validateDotNetSdk(
sdkContext.program,
minSupportedDotNetSdkVersion,
);
// if the dotnet sdk is valid, the error is not dependency issue, log it as normal
if (isValid) {
if (result.stderr) Logger.getInstance().error(result.stderr);
if (result.stdout) Logger.getInstance().verbose(result.stdout);
throw new Error(`Failed to generate the library. Exit code: ${result.exitCode}`);
}
}
} catch (error: any) {
const isValid = await validateDotNetSdk(sdkContext.program, minSupportedDotNetSdkVersion);
// if the dotnet sdk is valid, the error is not dependency issue, log it as normal
if (isValid) throw new Error(error);
}
if (!options["save-inputs"]) {
// delete
Expand All @@ -180,43 +199,70 @@ export async function $onEmit(context: EmitContext<NetEmitterOptions>) {
}
}

function constructCommandArg(arg: string): string {
return arg !== "" ? ` ${arg}` : "";
/** check the dotnet sdk installation.
* Report diagnostic if dotnet sdk is not installed or its version does not meet prerequisite
* @param program The typespec compiler program
* @param minVersionRequisite The minimum required major version
*/
export async function validateDotNetSdk(
program: Program,
minMajorVersion: number,
): Promise<boolean> {
try {
const result = await execAsync("dotnet", ["--version"], { stdio: "pipe" });
return validateDotNetSdkVersion(program, result.stdout, minMajorVersion);
} catch (error: any) {
if (error && "code" in (error as {}) && error["code"] === "ENOENT") {
reportDiagnostic(program, {
code: "invalid-dotnet-sdk-dependency",
messageId: "missing",
format: {
dotnetMajorVersion: `${minMajorVersion}`,
downloadUrl: "https://dotnet.microsoft.com/",
},
target: NoTarget,
});
}
return false;
}
}

async function execAsync(
command: string,
args: string[] = [],
options: SpawnOptions = {},
): Promise<{ exitCode: number; stdio: string; stdout: string; stderr: string; proc: any }> {
const child = spawn(command, args, options);
function validateDotNetSdkVersion(
program: Program,
version: string,
minMajorVersion: number,
): boolean {
if (version) {
const dotIndex = version.indexOf(".");
const firstPart = dotIndex === -1 ? version : version.substring(0, dotIndex);
const major = Number(firstPart);

return new Promise((resolve, reject) => {
child.on("error", (error) => {
reject(error);
});
const stdio: Buffer[] = [];
const stdout: Buffer[] = [];
const stderr: Buffer[] = [];
child.stdout?.on("data", (data) => {
stdout.push(data);
stdio.push(data);
});
child.stderr?.on("data", (data) => {
stderr.push(data);
stdio.push(data);
});

child.on("exit", (exitCode) => {
resolve({
exitCode: exitCode ?? -1,
stdio: Buffer.concat(stdio).toString(),
stdout: Buffer.concat(stdout).toString(),
stderr: Buffer.concat(stderr).toString(),
proc: child,
if (isNaN(major)) {
Logger.getInstance().error("Invalid .NET SDK version.");
return false;
}
if (major < minMajorVersion) {
reportDiagnostic(program, {
code: "invalid-dotnet-sdk-dependency",
messageId: "invalidVersion",
format: {
installedVersion: version,
dotnetMajorVersion: `${minMajorVersion}`,
downloadUrl: "https://dotnet.microsoft.com/",
},
target: NoTarget,
});
});
});
return false;
}
return true;
} else {
Logger.getInstance().error("Cannot get the installed .NET SDK version.");
return false;
}
}

function constructCommandArg(arg: string): string {
return arg !== "" ? ` ${arg}` : "";
}

function transformJSONProperties(this: any, key: string, value: any): any {
Expand Down
8 changes: 8 additions & 0 deletions packages/http-client-csharp/emitter/src/lib/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ const $lib = createTypeSpecLibrary({
default: paramMessage`${"message"}`,
},
},
"invalid-dotnet-sdk-dependency": {
severity: "error",
messages: {
default: paramMessage`Invalid .NET SDK installed.`,
missing: paramMessage`The dotnet command was not found in the PATH. Please install the .NET SDK version ${"dotnetMajorVersion"} or above. Guidance for installing the .NET SDK can be found at ${"downloadUrl"}.`,
invalidVersion: paramMessage`The .NET SDK found is version ${"installedVersion"}. Please install the .NET SDK ${"dotnetMajorVersion"} or above and ensure there is no global.json in the file system requesting a lower version. Guidance for installing the .NET SDK can be found at ${"downloadUrl"}.`,
},
},
"no-root-client": {
severity: "error",
messages: {
Expand Down
40 changes: 38 additions & 2 deletions packages/http-client-csharp/emitter/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import {
getLibraryName,
getSdkModel,
SdkContext,
SdkModelPropertyTypeBase,
SdkPathParameter,
getLibraryName,
getSdkModel,
} from "@azure-tools/typespec-client-generator-core";
import { Enum, EnumMember, Model, ModelProperty, Operation, Scalar } from "@typespec/compiler";
import { spawn, SpawnOptions } from "child_process";
import { InputConstant } from "../type/input-constant.js";
import { InputOperationParameterKind } from "../type/input-operation-parameter-kind.js";
import { InputParameter } from "../type/input-parameter.js";
Expand Down Expand Up @@ -80,3 +81,38 @@ export function isSdkPathParameter(
): parameter is SdkPathParameter {
return (parameter as SdkPathParameter).kind === "path";
}

export async function execAsync(
command: string,
args: string[] = [],
options: SpawnOptions = {},
): Promise<{ exitCode: number; stdio: string; stdout: string; stderr: string; proc: any }> {
const child = spawn(command, args, options);

return new Promise((resolve, reject) => {
child.on("error", (error) => {
reject(error);
});
const stdio: Buffer[] = [];
const stdout: Buffer[] = [];
const stderr: Buffer[] = [];
child.stdout?.on("data", (data) => {
stdout.push(data);
stdio.push(data);
});
child.stderr?.on("data", (data) => {
stderr.push(data);
stdio.push(data);
});

child.on("exit", (exitCode) => {
resolve({
exitCode: exitCode ?? -1,
stdio: Buffer.concat(stdio).toString(),
stdout: Buffer.concat(stdout).toString(),
stderr: Buffer.concat(stderr).toString(),
proc: child,
});
});
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { Program } from "@typespec/compiler";
import { TestHost } from "@typespec/compiler/testing";
import { strictEqual } from "assert";
import { SpawnOptions } from "child_process";
import { afterEach, beforeEach, describe, expect, it, Mock, vi } from "vitest";
import { validateDotNetSdk } from "../../src/emitter.js";
import { execAsync } from "../../src/lib/utils.js";
import { createEmitterTestHost, typeSpecCompile } from "./utils/test-util.js";

describe("Test validateDotNetSdk", () => {
let runner: TestHost;
let program: Program;
const minVersion = 8;

vi.mock("../../src/lib/utils.js", () => ({
execAsync: vi.fn(),
}));

beforeEach(async () => {
runner = await createEmitterTestHost();
program = await typeSpecCompile(
`
op test(
@query
@encode(DurationKnownEncoding.ISO8601)
input: duration
): NoContentResponse;
`,
runner,
);
});

afterEach(() => {
// Restore all mocks after each test
vi.restoreAllMocks();
});

it("should return false and report diagnostic when dotnet SDK is not installed.", async () => {
/* mock the scenario that dotnet SDK is not installed, so execAsync will throw exception with error ENOENT */
const error: any = new Error("ENOENT: no such file or directory");
error.code = "ENOENT";
(execAsync as Mock).mockRejectedValue(error);
const result = await validateDotNetSdk(program, minVersion);
expect(result).toBe(false);
strictEqual(program.diagnostics.length, 1);
strictEqual(
program.diagnostics[0].code,
"@typespec/http-client-csharp/invalid-dotnet-sdk-dependency",
);
strictEqual(
program.diagnostics[0].message,
"The dotnet command was not found in the PATH. Please install the .NET SDK version 8 or above. Guidance for installing the .NET SDK can be found at https://dotnet.microsoft.com/.",
);
});

it("should return true for installed SDK version whose major equals min supported version", async () => {
/* mock the scenario that the installed SDK version whose major equals min supported version */
(execAsync as Mock).mockResolvedValue({
exitCode: 0,
stdio: "",
stdout: "8.0.204",
stderr: "",
proc: { pid: 0, output: "", stdout: "", stderr: "", stdin: "" },
});
const result = await validateDotNetSdk(program, minVersion);
expect(result).toBe(true);
/* no diagnostics */
strictEqual(program.diagnostics.length, 0);
});

it("should return true for installed SDK version whose major greaters than min supported version", async () => {
/* mock the scenario that the installed SDK version whose major greater than min supported version */
(execAsync as Mock).mockImplementation(
(command: string, args: string[] = [], options: SpawnOptions = {}) => {
return {
exitCode: 0,
stdio: "",
stdout: "9.0.102",
stderr: "",
proc: { pid: 0, output: "", stdout: "", stderr: "", stdin: "" },
};
},
);
const result = await validateDotNetSdk(program, minVersion);
expect(result).toBe(true);
/* no diagnostics */
strictEqual(program.diagnostics.length, 0);
});

it("should return false and report diagnostic for invalid .NET SDK version", async () => {
/* mock the scenario that the installed SDK version whose major less than min supported version */
(execAsync as Mock).mockResolvedValue({
exitCode: 0,
stdio: "",
stdout: "5.0.408",
stderr: "",
proc: { pid: 0, output: "", stdout: "", stderr: "", stdin: "" },
});
const result = await validateDotNetSdk(program, minVersion);
expect(result).toBe(false);
strictEqual(program.diagnostics.length, 1);
strictEqual(
program.diagnostics[0].code,
"@typespec/http-client-csharp/invalid-dotnet-sdk-dependency",
);
strictEqual(
program.diagnostics[0].message,
"The .NET SDK found is version 5.0.408. Please install the .NET SDK 8 or above and ensure there is no global.json in the file system requesting a lower version. Guidance for installing the .NET SDK can be found at https://dotnet.microsoft.com/.",
);
});
});
Loading