Skip to content

Commit

Permalink
http-client-java, validate JDK and Maven (#5374)
Browse files Browse the repository at this point in the history
fix #5365

run on no JDK/Maven

```
error http-client-java: Java Development Kit (JDK) is not found in PATH. Please install JDK 17 or above. Microsoft Build of OpenJDK can be downloaded from https://learn.microsoft.com/java/openjdk/download
error http-client-java: Apache Maven is not found in PATH. Apache Maven can be downloaded from https://maven.apache.org/download.cgi

Found 2 errors.
```

fix #5366

run on JDK version too old
```
error http-client-java: Java Development Kit (JDK) in PATH is version 8. Please install JDK 17 or above. Microsoft Build of OpenJDK can be downloaded from https://learn.microsoft.com/java/openjdk/download

Found 1 error.
```

Currently validation only happen on `onEmit`.

I didn't add it to postinstall script, as this could be flagged as
security warning.
  • Loading branch information
weidongxu-microsoft authored Dec 17, 2024
1 parent 3bd6a87 commit 821b7f9
Show file tree
Hide file tree
Showing 7 changed files with 199 additions and 114 deletions.
1 change: 1 addition & 0 deletions cspell.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ words:
- itor
- ivar
- Jacoco
- javac
- jdwp
- jobject
- Johan
Expand Down
2 changes: 1 addition & 1 deletion packages/http-client-java/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Install [Node.js](https://nodejs.org/) 20 or above. (Verify by running `node --v

Install [Java](https://docs.microsoft.com/java/openjdk/download) 17 or above. (Verify by running `java --version`)

Install [Maven](https://maven.apache.org/install.html). (Verify by running `mvn --version`)
Install [Maven](https://maven.apache.org/download.cgi). (Verify by running `mvn --version`)

## Getting started

Expand Down
171 changes: 62 additions & 109 deletions packages/http-client-java/emitter/src/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import {
JSONSchemaType,
resolvePath,
} from "@typespec/compiler";
import { spawn } from "child_process";
import { promises } from "fs";
import { dump } from "js-yaml";
import { dirname } from "path";
import { fileURLToPath } from "url";
import { CodeModelBuilder } from "./code-model-builder.js";
import { logError } from "./utils.js";
import { logError, spawnAsync } from "./utils.js";
import { JDK_NOT_FOUND_MESSAGE, validateDependencies } from "./validate.js";

export interface EmitterOptions {
namespace?: string;
Expand Down Expand Up @@ -113,127 +113,80 @@ export const $lib = createTypeSpecLibrary({

export async function $onEmit(context: EmitContext<EmitterOptions>) {
const program = context.program;
const options = context.options;
if (!options["flavor"]) {
if (options["package-dir"]?.toLocaleLowerCase().startsWith("azure")) {
// Azure package
options["flavor"] = "azure";
await validateDependencies(program, true);

if (!program.hasError()) {
const options = context.options;
if (!options["flavor"]) {
if (options["package-dir"]?.toLocaleLowerCase().startsWith("azure")) {
// Azure package
options["flavor"] = "azure";
}
}
}
const builder = new CodeModelBuilder(program, context);
const codeModel = await builder.build();
const builder = new CodeModelBuilder(program, context);
const codeModel = await builder.build();

if (!program.compilerOptions.noEmit && !program.hasError()) {
const __dirname = dirname(fileURLToPath(import.meta.url));
const moduleRoot = resolvePath(__dirname, "..", "..");
if (!program.hasError() && !program.compilerOptions.noEmit) {
const __dirname = dirname(fileURLToPath(import.meta.url));
const moduleRoot = resolvePath(__dirname, "..", "..");

const outputPath = options["output-dir"] ?? context.emitterOutputDir;
options["output-dir"] = getNormalizedAbsolutePath(outputPath, undefined);
const outputPath = options["output-dir"] ?? context.emitterOutputDir;
options["output-dir"] = getNormalizedAbsolutePath(outputPath, undefined);

(options as any)["arm"] = codeModel.arm;
(options as any)["arm"] = codeModel.arm;

const codeModelFileName = resolvePath(outputPath, "./code-model.yaml");
const codeModelFileName = resolvePath(outputPath, "./code-model.yaml");

await promises.mkdir(outputPath, { recursive: true }).catch((err) => {
if (err.code !== "EISDIR" && err.code !== "EEXIST") {
logError(program, `Failed to create output directory: ${outputPath}`);
return;
}
});
await promises.mkdir(outputPath, { recursive: true }).catch((err) => {
if (err.code !== "EISDIR" && err.code !== "EEXIST") {
logError(program, `Failed to create output directory: ${outputPath}`);
return;
}
});

await program.host.writeFile(codeModelFileName, dump(codeModel));
await program.host.writeFile(codeModelFileName, dump(codeModel));

program.trace("http-client-java", `Code model file written to ${codeModelFileName}`);
program.trace("http-client-java", `Code model file written to ${codeModelFileName}`);

const emitterOptions = JSON.stringify(options);
program.trace("http-client-java", `Emitter options ${emitterOptions}`);
const emitterOptions = JSON.stringify(options);
program.trace("http-client-java", `Emitter options ${emitterOptions}`);

const jarFileName = resolvePath(
moduleRoot,
"generator/http-client-generator/target",
"emitter.jar",
);
program.trace("http-client-java", `Exec JAR ${jarFileName}`);
const jarFileName = resolvePath(
moduleRoot,
"generator/http-client-generator/target",
"emitter.jar",
);
program.trace("http-client-java", `Exec JAR ${jarFileName}`);

const javaArgs: string[] = [];
javaArgs.push(`-DemitterOptions=${emitterOptions}`);
if (options["dev-options"]?.debug) {
javaArgs.push("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5005");
}
if (options["dev-options"]?.loglevel) {
javaArgs.push("-Dorg.slf4j.simpleLogger.defaultLogLevel=" + options["dev-options"]?.loglevel);
}
if (options["dev-options"]?.["java-temp-dir"]) {
javaArgs.push("-Dcodegen.java.temp.directory=" + options["dev-options"]?.["java-temp-dir"]);
}
javaArgs.push("-jar");
javaArgs.push(jarFileName);
javaArgs.push(codeModelFileName);
try {
type SpawnReturns = {
stdout: string;
stderr: string;
};
await new Promise<SpawnReturns>((resolve, reject) => {
const childProcess = spawn("java", javaArgs, { stdio: "inherit" });

let error: Error | undefined = undefined;

// std
const stdout: string[] = [];
const stderr: string[] = [];
if (childProcess.stdout) {
childProcess.stdout.on("data", (data) => {
stdout.push(data.toString());
});
}
if (childProcess.stderr) {
childProcess.stderr.on("data", (data) => {
stderr.push(data.toString());
});
const javaArgs: string[] = [];
javaArgs.push(`-DemitterOptions=${emitterOptions}`);
if (options["dev-options"]?.debug) {
javaArgs.push("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5005");
}
if (options["dev-options"]?.loglevel) {
javaArgs.push(
"-Dorg.slf4j.simpleLogger.defaultLogLevel=" + options["dev-options"]?.loglevel,
);
}
if (options["dev-options"]?.["java-temp-dir"]) {
javaArgs.push("-Dcodegen.java.temp.directory=" + options["dev-options"]?.["java-temp-dir"]);
}
javaArgs.push("-jar");
javaArgs.push(jarFileName);
javaArgs.push(codeModelFileName);
try {
await spawnAsync("java", javaArgs, { stdio: "inherit" });
} catch (error: any) {
if (error && "code" in error && error["code"] === "ENOENT") {
logError(program, JDK_NOT_FOUND_MESSAGE);
} else {
logError(program, error.message);
}

// failed to spawn the process
childProcess.on("error", (e) => {
error = e;
});

// process exits with error
childProcess.on("exit", (code, signal) => {
if (code !== 0) {
if (code) {
error = new Error(`JAR ended with code '${code}'.`);
} else {
error = new Error(`JAR terminated by signal '${signal}'.`);
}
}
});

// close and complete Promise
childProcess.on("close", () => {
if (error) {
reject(error);
} else {
resolve({
stdout: stdout.join(""),
stderr: stderr.join(""),
});
}
});
});

// as stdio: "inherit", std is not captured by spawn
// program.trace("http-client-java", output.stdout ? output.stdout : output.stderr);
} catch (error: any) {
if (error && "code" in error && error["code"] === "ENOENT") {
logError(program, "'java' is not on PATH. Please install JDK 11 or above.");
} else {
logError(program, error.message);
}
}

if (!options["dev-options"]?.["generate-code-model"]) {
await program.host.rm(codeModelFileName);
if (!options["dev-options"]?.["generate-code-model"]) {
await program.host.rm(codeModelFileName);
}
}
}
}
60 changes: 60 additions & 0 deletions packages/http-client-java/emitter/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { NoTarget, Program, Type } from "@typespec/compiler";
import { spawn, SpawnOptions } from "child_process";

export function logError(program: Program, msg: string) {
trace(program, msg);
Expand Down Expand Up @@ -63,3 +64,62 @@ export function removeClientSuffix(clientName: string): string {
const clientSuffix = "Client";
return clientName.endsWith(clientSuffix) ? clientName.slice(0, -clientSuffix.length) : clientName;
}

export type SpawnReturns = {
stdout: string;
stderr: string;
};

export async function spawnAsync(
command: string,
args: readonly string[],
options: SpawnOptions,
): Promise<SpawnReturns> {
return new Promise<SpawnReturns>((resolve, reject) => {
const childProcess = spawn(command, args, options);

let error: Error | undefined = undefined;

// std
const stdout: string[] = [];
const stderr: string[] = [];
if (childProcess.stdout) {
childProcess.stdout.on("data", (data) => {
stdout.push(data.toString());
});
}
if (childProcess.stderr) {
childProcess.stderr.on("data", (data) => {
stderr.push(data.toString());
});
}

// failed to spawn the process
childProcess.on("error", (e) => {
error = e;
});

// process exits with error
childProcess.on("exit", (code, signal) => {
if (code !== 0) {
if (code) {
error = new Error(`${command} ended with code '${code}'.`);
} else {
error = new Error(`${command} terminated by signal '${signal}'.`);
}
}
});

// close and complete Promise
childProcess.on("close", () => {
if (error) {
reject(error);
} else {
resolve({
stdout: stdout.join(""),
stderr: stderr.join(""),
});
}
});
});
}
71 changes: 71 additions & 0 deletions packages/http-client-java/emitter/src/validate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Program } from "@typespec/compiler";
import { logError, spawnAsync } from "./utils.js";

export const JDK_NOT_FOUND_MESSAGE =
"Java Development Kit (JDK) is not found in PATH. Please install JDK 17 or above. Microsoft Build of OpenJDK can be downloaded from https://learn.microsoft.com/java/openjdk/download";

export async function validateDependencies(
program: Program | undefined,
logDiagnostic: boolean = false,
) {
// Check JDK and version
try {
const result = await spawnAsync("javac", ["-version"], { stdio: "pipe" });
const javaVersion = findJavaVersion(result.stdout) ?? findJavaVersion(result.stderr);
if (javaVersion) {
if (javaVersion < 11) {
// the message is JDK 17, because clientcore depends on JDK 17
// emitter only require JDK 11
const message = `Java Development Kit (JDK) in PATH is version ${javaVersion}. Please install JDK 17 or above. Microsoft Build of OpenJDK can be downloaded from https://learn.microsoft.com/java/openjdk/download`;
// // eslint-disable-next-line no-console
// console.log("[ERROR] " + message);
if (program && logDiagnostic) {
logError(program, message);
}
}
}
} catch (error: any) {
let message = error.message;
if (error && "code" in error && error["code"] === "ENOENT") {
message = JDK_NOT_FOUND_MESSAGE;
}
// // eslint-disable-next-line no-console
// console.log("[ERROR] " + message);
if (program && logDiagnostic) {
logError(program, message);
}
}

// Check Maven
// nodejs does not allow spawn of .cmd on win32
const shell = process.platform === "win32";
try {
await spawnAsync("mvn", ["-v"], { stdio: "pipe", shell: shell });
} catch (error: any) {
let message = error.message;
if (shell || (error && "code" in error && error["code"] === "ENOENT")) {
message =
"Apache Maven is not found in PATH. Apache Maven can be downloaded from https://maven.apache.org/download.cgi";
}
// // eslint-disable-next-line no-console
// console.log("[ERROR] " + message);
if (program && logDiagnostic) {
logError(program, message);
}
}
}

function findJavaVersion(output: string): number | undefined {
const regex = /javac (\d+)\.(\d+)\..*/;
const matches = output.match(regex);
if (matches && matches.length > 2) {
if (matches[1] === "1") {
// "javac 1.8.0_422" -> 8
return +matches[2];
} else {
// "javac 21.0.3" -> 21
return +matches[1];
}
}
return undefined;
}
2 changes: 1 addition & 1 deletion packages/http-client-java/generator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ The **Microsoft Java client generator** tool generates client libraries for acce
## Prerequisites

- [Java 17 or above](https://docs.microsoft.com/java/openjdk/download)
- [Maven](https://maven.apache.org/install.html)
- [Maven](https://maven.apache.org/download.cgi)

## Build

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,17 +216,17 @@ protected void writeStandAlonePlugins(XmlBlock pluginsBlock) {
pluginsBlock.block("plugin", pluginBlock -> {
pluginBlock.tag("groupId", "org.apache.maven.plugins");
pluginBlock.tag("artifactId", "maven-compiler-plugin");
pluginBlock.tag("version", "3.10.1");
pluginBlock.tag("version", "3.13.0");
pluginBlock.block("configuration", configurationBlock -> {
configurationBlock.tag("release", "11");
configurationBlock.tag("release", JavaSettings.getInstance().isBranded() ? "11" : "17");
});
});

// maven-source-plugin
pluginsBlock.block("plugin", pluginBlock -> {
pluginBlock.tag("groupId", "org.apache.maven.plugins");
pluginBlock.tag("artifactId", "maven-source-plugin");
pluginBlock.tag("version", "3.3.0");
pluginBlock.tag("version", "3.3.1");
pluginBlock.block("executions", executionsBlock -> {
executionsBlock.block("execution", executionBlock -> {
executionBlock.tag("id", "attach-sources");
Expand Down

0 comments on commit 821b7f9

Please sign in to comment.