From 821b7f90daa96e40decb4b422d5ab25b0d7ead4b Mon Sep 17 00:00:00 2001 From: Weidong Xu Date: Tue, 17 Dec 2024 09:54:09 +0800 Subject: [PATCH] http-client-java, validate JDK and Maven (#5374) fix https://github.com/microsoft/typespec/issues/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 https://github.com/microsoft/typespec/issues/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. --- cspell.yaml | 1 + packages/http-client-java/README.md | 2 +- .../http-client-java/emitter/src/emitter.ts | 171 +++++++----------- .../http-client-java/emitter/src/utils.ts | 60 ++++++ .../http-client-java/emitter/src/validate.ts | 71 ++++++++ packages/http-client-java/generator/README.md | 2 +- .../generator/core/template/PomTemplate.java | 6 +- 7 files changed, 199 insertions(+), 114 deletions(-) create mode 100644 packages/http-client-java/emitter/src/validate.ts diff --git a/cspell.yaml b/cspell.yaml index 8bd06fe5d0..ecc77a9a92 100644 --- a/cspell.yaml +++ b/cspell.yaml @@ -95,6 +95,7 @@ words: - itor - ivar - Jacoco + - javac - jdwp - jobject - Johan diff --git a/packages/http-client-java/README.md b/packages/http-client-java/README.md index b36be08cae..9a68507f65 100644 --- a/packages/http-client-java/README.md +++ b/packages/http-client-java/README.md @@ -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 diff --git a/packages/http-client-java/emitter/src/emitter.ts b/packages/http-client-java/emitter/src/emitter.ts index 8aef1d72d4..625e87e957 100644 --- a/packages/http-client-java/emitter/src/emitter.ts +++ b/packages/http-client-java/emitter/src/emitter.ts @@ -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; @@ -113,127 +113,80 @@ export const $lib = createTypeSpecLibrary({ export async function $onEmit(context: EmitContext) { 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((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); + } } } } diff --git a/packages/http-client-java/emitter/src/utils.ts b/packages/http-client-java/emitter/src/utils.ts index f54da2e7b1..1a13fc70fb 100644 --- a/packages/http-client-java/emitter/src/utils.ts +++ b/packages/http-client-java/emitter/src/utils.ts @@ -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); @@ -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 { + return new Promise((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(""), + }); + } + }); + }); +} diff --git a/packages/http-client-java/emitter/src/validate.ts b/packages/http-client-java/emitter/src/validate.ts new file mode 100644 index 0000000000..cc0bb704e8 --- /dev/null +++ b/packages/http-client-java/emitter/src/validate.ts @@ -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; +} diff --git a/packages/http-client-java/generator/README.md b/packages/http-client-java/generator/README.md index 6efa989f3b..e8f687a3fa 100644 --- a/packages/http-client-java/generator/README.md +++ b/packages/http-client-java/generator/README.md @@ -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 diff --git a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/PomTemplate.java b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/PomTemplate.java index cfed128e1c..6d59263b13 100644 --- a/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/PomTemplate.java +++ b/packages/http-client-java/generator/http-client-generator-core/src/main/java/com/microsoft/typespec/http/client/generator/core/template/PomTemplate.java @@ -216,9 +216,9 @@ 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"); }); }); @@ -226,7 +226,7 @@ protected void writeStandAlonePlugins(XmlBlock pluginsBlock) { 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");