Skip to content

Commit 6612503

Browse files
committed
feat(cli): support javadoc and jar tools
feat(cli): implement `jar` tool support feat(cli): implement `javadoc` tool support Signed-off-by: Sam Gammon <[email protected]>
1 parent 2ee45e7 commit 6612503

File tree

15 files changed

+1263
-5
lines changed

15 files changed

+1263
-5
lines changed

packages/cli/build.gradle.kts

+13-1
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,8 @@ val jvmCompileArgs = listOfNotNull(
230230
"--add-modules=jdk.unsupported",
231231
"--add-exports=java.base/jdk.internal.misc=ALL-UNNAMED",
232232
"--add-exports=java.base/jdk.internal.jrtfs=ALL-UNNAMED",
233+
"--add-exports=jdk.jartool/sun.tools.jar=ALL-UNNAMED",
234+
"--add-exports=jdk.javadoc/jdk.javadoc.internal.tool=ALL-UNNAMED",
233235
).plus(if (enableJpms) listOf(
234236
"--add-reads=elide.cli=ALL-UNNAMED",
235237
"--add-reads=elide.graalvm=ALL-UNNAMED",
@@ -900,6 +902,16 @@ val initializeAtRuntime: List<String> = listOfNotNull(
900902
// pkl needs this
901903
"org.msgpack.core.buffer.DirectBufferAccess",
902904

905+
// --- JDK Tooling -----
906+
907+
"elide.tool.cli.cmd.tool.jar.JarTool",
908+
"elide.tool.cli.cmd.tool.javadoc.JavadocTool",
909+
"jdk.tools.jlink.internal.Main",
910+
"jdk.tools.jlink.internal.Utils",
911+
"jdk.tools.jlink.internal.Main\$JlinkToolProvider",
912+
"jdk.tools.jlink.internal.plugins.LegalNoticeFilePlugin",
913+
"jdk.jpackage.internal.JPackageToolProvider",
914+
903915
// --- JNA -----
904916

905917
"com.sun.jna.Structure${'$'}FFIType",
@@ -1127,7 +1139,7 @@ val commonNativeArgs = listOfNotNull(
11271139
"--install-exit-handlers",
11281140
// @TODO breaks with old configs, and new configs can't use the agent.
11291141
// "--exact-reachability-metadata",
1130-
"--enable-url-protocols=http,https,jar",
1142+
"--enable-url-protocols=http,https,jar,resource",
11311143
"--color=always",
11321144
"--initialize-at-build-time",
11331145
"--link-at-build-time=elide",

packages/cli/src/main/kotlin/elide/tool/cli/CommandContext.kt

+5-1
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,11 @@ sealed interface CommandContext : CoroutineScope {
127127
* @param out String builder to emit.
128128
*/
129129
fun output(out: StringBuilder) {
130-
println(out.toString())
130+
out.toString().let {
131+
if (it.isNotEmpty() && it.isNotBlank()) {
132+
println(it)
133+
}
134+
}
131135
System.out.flush()
132136
}
133137

packages/cli/src/main/kotlin/elide/tool/cli/Elide.kt

+5-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ import elide.tool.cli.cmd.pkl.ToolPklCommand
4646
import elide.tool.cli.cmd.project.ToolProjectCommand
4747
import elide.tool.cli.cmd.tool.ToolInvokeCommand
4848
import elide.tool.cli.cmd.repl.ToolShellCommand
49+
import elide.tool.cli.cmd.tool.jar.JarTool
4950
import elide.tool.cli.cmd.tool.javac.JavaCompiler
51+
import elide.tool.cli.cmd.tool.javadoc.JavadocTool
5052
import elide.tool.cli.cmd.tool.kotlinc.KotlinCompiler
5153
import elide.tool.cli.state.CommandState
5254
import elide.tool.engine.NativeEngine
@@ -98,6 +100,8 @@ internal const val ELIDE_HEADER = ("@|bold,fg(magenta)%n" +
98100
ToolProjectCommand::class,
99101
KotlinCompiler.KotlinCliTool::class,
100102
JavaCompiler.JavacCliTool::class,
103+
JarTool.JarCliTool::class,
104+
JavadocTool.JavadocCliTool::class,
101105
],
102106
customSynopsis = [
103107
"",
@@ -112,7 +116,7 @@ internal const val ELIDE_HEADER = ("@|bold,fg(magenta)%n" +
112116
" or: elide @|bold,fg(cyan) run|repl|@ --js [OPTIONS]",
113117
" or: elide @|bold,fg(cyan) run|repl|@ --language=[@|bold,fg(green) JS|@] [OPTIONS] [FILE] [ARG...]",
114118
" or: elide @|bold,fg(cyan) run|repl|@ --languages=[@|bold,fg(green) JS|@,@|bold,fg(green) PYTHON|@,...] [OPTIONS] [FILE] [ARG...]",
115-
" or: elide @|bold,fg(cyan) javac|kotlinc|...|@ [OPTIONS] [SOURCES...]",
119+
" or: elide @|bold,fg(cyan) javac|kotlinc|jar|javadoc|...|@ [OPTIONS] [SOURCES...]",
116120
],
117121
)
118122
@Suppress("MemberVisibilityCanBePrivate")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
/*
2+
* Copyright (c) 2024-2025 Elide Technologies, Inc.
3+
*
4+
* Licensed under the MIT license (the "License"); you may not use this file except in compliance
5+
* with the License. You may obtain a copy of the License at
6+
*
7+
* https://opensource.org/license/mit/
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
11+
* License for the specific language governing permissions and limitations under the License.
12+
*/
13+
14+
package elide.tool.cli.cmd.tool
15+
16+
import java.io.PrintWriter
17+
import java.io.StringWriter
18+
import java.util.LinkedList
19+
import java.util.SortedSet
20+
import java.util.concurrent.atomic.AtomicLong
21+
import elide.runtime.Logger
22+
import elide.tool.Argument
23+
import elide.tool.Arguments
24+
import elide.tool.MutableArguments
25+
import elide.tool.Tool
26+
import elide.tool.cli.CommandContext
27+
28+
/**
29+
* ## Abstract Tool (Generic)
30+
*/
31+
abstract class AbstractGenericTool<T, I, O>(info: Tool.CommandLineTool) : AbstractTool(info) {
32+
/**
33+
* Create the tool provider instance.
34+
*
35+
* @return Generic tool provider.
36+
*/
37+
abstract fun createTool(): T
38+
39+
/**
40+
* Logger to use for this tool.
41+
*/
42+
abstract val toolLogger: Logger
43+
44+
/**
45+
* Generic description for this task.
46+
*/
47+
abstract val taskDescription: String
48+
49+
/**
50+
* Amend the arguments to be passed to the tool.
51+
*
52+
* @param args Arguments to amend.
53+
*/
54+
open fun amendArgs(args: MutableArguments): Unit = Unit
55+
56+
/**
57+
* Implementation to run the generic tool.
58+
*/
59+
abstract fun toolRun(out: PrintWriter, err: PrintWriter, vararg args: String): Int
60+
61+
/**
62+
* Wrapped execution of the tool itself, to customize before/after actions.
63+
*
64+
* @param block Block to execute
65+
*/
66+
open suspend fun <R> toolExec(block: suspend () -> R): R = block()
67+
68+
/**
69+
* Inputs for this tool.
70+
*/
71+
abstract val inputs: I
72+
73+
/**
74+
* Outputs for this tool.
75+
*/
76+
abstract val outputs: O
77+
78+
/**
79+
* Resolve inputs for this tool.
80+
*
81+
* @return Resolved inputs.
82+
*/
83+
open fun resolveInputs(): I = inputs
84+
85+
/**
86+
* Resolve outputs for this tool.
87+
*
88+
* @param out Standard output stream.
89+
* @param err Standard error stream.
90+
* @param ms Milliseconds since the tool started.
91+
* @return Resolved outputs.
92+
*/
93+
open suspend fun CommandContext.resolveOutputs(out: StringWriter, err: StringWriter, ms: Int): O = outputs.also {
94+
val regularOutput = out.toString()
95+
val regularError = err.toString()
96+
97+
output {
98+
append(regularOutput)
99+
append(regularError)
100+
append("[${info.name}] Ran in ${ms}ms")
101+
}
102+
}
103+
104+
// Supported by default.
105+
override fun supported(): Boolean = true
106+
107+
override suspend fun CommandContext.invoke(state: EmbeddedToolState): Tool.Result {
108+
val args = MutableArguments.from(info.args).also { amendArgs(it) }
109+
110+
output {
111+
"""
112+
Invoking `${info.name}` with:
113+
114+
-- Arguments:
115+
$args
116+
117+
-- Inputs:
118+
$inputs
119+
120+
-- Outputs:
121+
$outputs
122+
123+
-- Environment:
124+
${info.environment}
125+
""".trimIndent().also {
126+
toolLogger.debug(it)
127+
if (state.cmd.state.output.verbose) {
128+
append(it)
129+
}
130+
}
131+
}
132+
133+
toolLogger.debug { "Resolving inputs" }
134+
resolveInputs()
135+
val toolStart = System.currentTimeMillis()
136+
val toolEnd = AtomicLong(0L)
137+
val out = StringWriter()
138+
val outPrinter = PrintWriter(out, true)
139+
val err = StringWriter()
140+
val errPrinter = PrintWriter(err, true)
141+
val finalizdArgs = args.asArgumentList().toTypedArray()
142+
143+
val exitCode = toolExec {
144+
@Suppress("TooGenericExceptionCaught", "SpreadOperator")
145+
try {
146+
toolLogger.debug { "Preparing generic tool execution task '$taskDescription'" }
147+
toolRun(outPrinter, errPrinter, *finalizdArgs)
148+
} catch (rxe: RuntimeException) {
149+
toolLogger.debug { "Exception while executing task: $rxe" }
150+
151+
embeddedToolError(
152+
info,
153+
"$taskDescription failed: ${rxe.message}",
154+
cause = rxe,
155+
)
156+
} finally {
157+
toolLogger.debug { "$taskDescription job finished" }
158+
toolEnd.set(System.currentTimeMillis())
159+
}
160+
}
161+
162+
out.flush()
163+
err.flush()
164+
val totalMs = toolEnd.get() - toolStart
165+
toolLogger.debug { "$taskDescription job completed in ${totalMs}ms (exit code: $exitCode)" }
166+
resolveOutputs(out, err, totalMs.toInt())
167+
return Tool.Result.Success
168+
}
169+
170+
companion object {
171+
// Gather options, inputs, and outputs for an invocation of the jar tool.
172+
@JvmStatic internal fun gatherArgs(
173+
expectValues: SortedSet<String>,
174+
args: Arguments,
175+
): Pair<LinkedList<Argument>, LinkedList<String>> {
176+
val effective = LinkedList<Argument>()
177+
val likelyInputs = LinkedList<String>()
178+
var nextIsValueForKey: String? = null
179+
180+
for (arg in args.asArgumentList()) {
181+
if (nextIsValueForKey != null) {
182+
effective.add(Argument.of(nextIsValueForKey to arg))
183+
nextIsValueForKey = null
184+
continue
185+
}
186+
val argNormalized = arg.lowercase().trim()
187+
val argSplit = argNormalized.split('=')
188+
val argToken = argSplit.first()
189+
if (argToken in expectValues) {
190+
if (argSplit.size > 1) {
191+
// Split on `=`.
192+
val value = argSplit[1]
193+
effective.add(Argument.of(argToken to value))
194+
} else {
195+
// Next token is a value for this key.
196+
nextIsValueForKey = argToken
197+
}
198+
} else {
199+
// Just add the token as-is.
200+
effective.add(Argument.of(arg))
201+
if (!argToken.startsWith("-") && !argToken.startsWith("@")) {
202+
// this is a likely input
203+
likelyInputs.add(arg)
204+
}
205+
}
206+
}
207+
return (effective to likelyInputs)
208+
}
209+
}
210+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright (c) 2024-2025 Elide Technologies, Inc.
3+
*
4+
* Licensed under the MIT license (the "License"); you may not use this file except in compliance
5+
* with the License. You may obtain a copy of the License at
6+
*
7+
* https://opensource.org/license/mit/
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
11+
* License for the specific language governing permissions and limitations under the License.
12+
*/
13+
14+
package elide.tool.cli.cmd.tool
15+
16+
import elide.tool.Tool
17+
18+
/**
19+
* ## Delegated Tool Command (Generic)
20+
*
21+
* Implements generic support for tools which use the JDK's built-in [java.util.spi.ToolProvider] interface; such tools
22+
* provide a [java.util.spi.ToolProvider.run] method.
23+
*
24+
* @param T Tool adapter implementation
25+
*/
26+
abstract class DelegatedGenericToolCommand<T> (
27+
info: Tool.CommandLineTool,
28+
configurator: ToolCommandConfigurator<T>? = null,
29+
): DelegatedToolCommand<T>(info, configurator) where T: AbstractTool {
30+
31+
}

packages/cli/src/main/kotlin/elide/tool/cli/cmd/tool/DelegatedToolCommand.kt

+5
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,11 @@ abstract class DelegatedToolCommand<T> (
167167
errExitCode = exc.exitCode
168168
exc.message?.let { exitMessage = it }
169169
}
170+
else -> {
171+
logging.error("Failed to call tool: ${info.name}", exc)
172+
errExitCode = 1
173+
exitMessage = it.message ?: "Unknown error"
174+
}
170175
}
171176
}.getOrElse {
172177
err(exitMessage, exitCode = errExitCode)

0 commit comments

Comments
 (0)