From 40e9bbdbd2a8193a61b8f44f0ab2b108b83ef257 Mon Sep 17 00:00:00 2001 From: Danny Thomas Date: Thu, 29 Oct 2026 05:59:40 +0000 Subject: [PATCH 1/2] Disable module commands without java.se --- README.md | 2 +- .../ja/test/CommandAvailabilityTest.java | 16 ++++ .../com/netflix/tools/ja/test/JaToolTest.java | 34 ++++++++ .../com/netflix/tools/ja/AotWarmup.java | 86 +++++++++++-------- .../netflix/tools/ja/CommandAvailability.java | 36 ++++++++ .../com/netflix/tools/ja/CommandRunner.java | 1 + .../com/netflix/tools/ja/Ja.java | 34 +++++++- 7 files changed, 170 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 9a8f3a7..adbb622 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ curl -fsSL https://raw.githubusercontent.com/Netflix/ja/main/install.sh | -Installation Standalone ``` -The installer selects the correct platform archive, installs it alongside applications managed by `ja install`, and activates `ja` through the same user command directory. The launcher uses `JAVA_HOME` when set, then falls back to `java` on `PATH`. The archive contains the development tools, but not a JDK. Use a plain JDK: the launcher rejects system modules whose names overlap the bundled application modules, rather than silently using the JDK's copies. +The installer selects the correct platform archive, installs it alongside applications managed by `ja install`, and activates `ja` through the same user command directory. The launcher uses `JAVA_HOME` when set, then falls back to `java` on `PATH`. The archive contains the development tools, but not a JDK. Use a plain JDK: the launcher rejects system modules whose names overlap the bundled application modules, rather than silently using the JDK's copies. When the selected runtime does not contain `java.se`, Ja keeps its development and code-insight commands available and reports module development commands as unavailable. Neither installation modifies shell configuration. The installer prints the activation and completion commands to add to your shell profile. See the [installation guide](https://github.com/Netflix/ja/wiki/Installation) for default locations, custom locations, version pinning, shell setup, and environment-manager guidance. diff --git a/src/com.netflix.tools.ja.test/com/netflix/tools/ja/test/CommandAvailabilityTest.java b/src/com.netflix.tools.ja.test/com/netflix/tools/ja/test/CommandAvailabilityTest.java index 279af03..602e63d 100644 --- a/src/com.netflix.tools.ja.test/com/netflix/tools/ja/test/CommandAvailabilityTest.java +++ b/src/com.netflix.tools.ja.test/com/netflix/tools/ja/test/CommandAvailabilityTest.java @@ -14,6 +14,7 @@ package com.netflix.tools.ja.test; +import java.lang.module.ModuleFinder; import java.nio.file.Path; import java.util.List; import java.util.Optional; @@ -37,6 +38,21 @@ import static org.junit.jupiter.api.Assertions.assertThrows; class CommandAvailabilityTest { + @Test + void moduleCommandsRequireJavaSe() { + IllegalArgumentException failure = assertThrows(IllegalArgumentException.class, + () -> CommandAvailability.require(commandLine(BuiltinCommand.COMPILE), ToolRuntime.of(), + new ToolCatalog(List.of()), ModuleFinder.of())); + + assertEquals("compile is unavailable; java.se is not present in this runtime", failure.getMessage()); + } + + @Test + void developmentCommandsDoNotRequireJavaSe() { + CommandAvailability.require(commandLine(BuiltinCommand.PROJECTS), ToolRuntime.of(), + new ToolCatalog(List.of()), ModuleFinder.of()); + } + @Test void reportsTheUpstreamToolForAnUnavailableTypedCommand() { var commandLine = commandLine(BuiltinCommand.FMT); diff --git a/src/com.netflix.tools.ja.test/com/netflix/tools/ja/test/JaToolTest.java b/src/com.netflix.tools.ja.test/com/netflix/tools/ja/test/JaToolTest.java index 3da8a57..5c8a49f 100644 --- a/src/com.netflix.tools.ja.test/com/netflix/tools/ja/test/JaToolTest.java +++ b/src/com.netflix.tools.ja.test/com/netflix/tools/ja/test/JaToolTest.java @@ -18,6 +18,7 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintWriter; +import java.lang.module.ModuleFinder; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -38,6 +39,7 @@ import com.netflix.tools.cli.CommandLine.ToolOption; import com.netflix.tools.ja.Ja; import com.netflix.tools.ja.JaTool; +import com.netflix.tools.ja.ToolCatalog; import com.netflix.tools.ja.ToolRuntime; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -172,6 +174,32 @@ void describesAndCompletesItsCommands() { .size()); } + @Test + void reducedRuntimesExposeOnlyDevelopmentCommands() { + var output = new ByteArrayOutputStream(); + Ja.help(new java.io.PrintStream(output), ToolRuntime.of(), new ToolCatalog(List.of()), ModuleFinder.of()); + + String help = output.toString(StandardCharsets.UTF_8); + assertTrue(help.contains("Development commands:"), help); + assertTrue(help.contains("projects"), help); + assertFalse(help.contains("Module commands:"), help); + assertFalse(help.contains(" compile "), help); + assertTrue(help.contains("Module commands are unavailable; java.se is not present in this runtime."), help); + + assertEquals(List.of(), + Ja.complete(new CompletionRequest(ToolInvocation.of(), "co"), ToolRuntime.of(), ModuleFinder.of()).stream() + .map(Completion::value) + .toList()); + assertEquals(List.of("projects"), + Ja.complete(new CompletionRequest(ToolInvocation.of(), "pro"), ToolRuntime.of(), ModuleFinder.of()).stream() + .map(Completion::value) + .toList()); + assertEquals(List.of(), + Ja.complete(new CompletionRequest(ToolInvocation.of("compile"), "--r"), ToolRuntime.of(), ModuleFinder.of()).stream() + .map(Completion::value) + .toList()); + } + @Test void builtInCommandsExposeExplicitInterfacesWhileToolDelegates() { var described = assertInstanceOf(JaTool.class, ja); @@ -261,6 +289,12 @@ void composesAvailableToolCompletions() { assertEquals("", result.error()); } + @Test + void doesNotRequireJavaSe() { + assertFalse(Ja.class.getModule().getDescriptor().requires().stream() + .anyMatch(candidate -> candidate.name().equals("java.se"))); + } + @Test void declaresLauncherMetadata() throws Exception { String resource = "META-INF/com.netflix.tools/tools/ja.properties"; diff --git a/src/com.netflix.tools.ja/com/netflix/tools/ja/AotWarmup.java b/src/com.netflix.tools.ja/com/netflix/tools/ja/AotWarmup.java index 4e674e0..f8bae7e 100644 --- a/src/com.netflix.tools.ja/com/netflix/tools/ja/AotWarmup.java +++ b/src/com.netflix.tools.ja/com/netflix/tools/ja/AotWarmup.java @@ -18,6 +18,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.PrintStream; +import java.lang.module.ModuleFinder; import java.nio.file.Files; /** Warms Ja's startup paths without invoking delegated tool warmups. */ @@ -28,6 +29,8 @@ static int run(PrintStream err) throws IOException { var layer = AotWarmup.class.getModule().getLayer(); var tools = ToolRuntime.load(layer); var catalog = ToolCatalog.load(layer).withDiscovered(tools); + var systemModules = ModuleFinder.ofSystem(); + boolean moduleDevelopmentAvailable = CommandAvailability.moduleDevelopmentAvailable(systemModules); try (var work = TemporaryDirectory.create(); var out = new PrintStream(OutputStream.nullOutputStream())) { int result = invoke(work, out, err, "--help"); @@ -36,25 +39,32 @@ static int run(PrintStream err) throws IOException { } for (var command : Ja.commandLine().commands()) { + var builtin = BuiltinCommand.find(command.name()); + if (builtin.isPresent() && !CommandAvailability.available(builtin.orElseThrow(), systemModules)) { + continue; + } result = invoke(work, out, err, command.name(), "--help"); if (result != 0) { return result; } } - result = invoke(work, out, err, "__complete", "co"); - if (result != 0) { - return result; - } - result = invoke(work, out, err, "__complete", "compile", "--r"); + result = invoke(work, out, err, "__complete", moduleDevelopmentAvailable ? "co" : "de"); if (result != 0) { return result; } - - result = invoke(work, out, err, "init", "--main-class", "com.netflix.tools.ja.warmup.Main", - "com.netflix.tools.ja.warmup"); - if (result != 0) { - return result; + if (moduleDevelopmentAvailable) { + result = invoke(work, out, err, "__complete", "compile", "--r"); + if (result != 0) { + return result; + } + result = invoke(work, out, err, "init", "--main-class", "com.netflix.tools.ja.warmup.Main", + "com.netflix.tools.ja.warmup"); + if (result != 0) { + return result; + } + } else { + Files.writeString(work.root().resolve("module-info.java"), "module com.netflix.tools.ja.warmup {}\n"); } var main = work.root().resolve("com/netflix/tools/ja/warmup/Main.java"); Files.createDirectories(main.getParent()); @@ -72,13 +82,15 @@ public static void main(String[] args) {} return result; } } - result = invoke(work, out, err, "compile"); - if (result != 0) { - return result; - } - result = invoke(work, out, err, "compile"); - if (result != 0) { - return result; + if (moduleDevelopmentAvailable) { + result = invoke(work, out, err, "compile"); + if (result != 0) { + return result; + } + result = invoke(work, out, err, "compile"); + if (result != 0) { + return result; + } } if (available(BuiltinCommand.SOURCE, tools, catalog)) { result = invoke(work, out, err, "source", "java.lang.String.isEmpty"); @@ -92,25 +104,27 @@ public static void main(String[] args) {} return result; } } - result = invoke( - work, - out, - err, - "tool", - "jar", - "--create", - "--file", - work.root() - .resolve("warmup.jar") - .toString(), - "-C", - work.root().toString(), - "module-info.java", - "-C", - work.root().toString(), - "com"); - if (result != 0) { - return result; + if (tools.contains("jar")) { + result = invoke( + work, + out, + err, + "tool", + "jar", + "--create", + "--file", + work.root() + .resolve("warmup.jar") + .toString(), + "-C", + work.root().toString(), + "module-info.java", + "-C", + work.root().toString(), + "com"); + if (result != 0) { + return result; + } } result = invoke(work, out, err, "tool"); if (result != 0) { diff --git a/src/com.netflix.tools.ja/com/netflix/tools/ja/CommandAvailability.java b/src/com.netflix.tools.ja/com/netflix/tools/ja/CommandAvailability.java index 257dab6..4c08b49 100644 --- a/src/com.netflix.tools.ja/com/netflix/tools/ja/CommandAvailability.java +++ b/src/com.netflix.tools.ja/com/netflix/tools/ja/CommandAvailability.java @@ -14,10 +14,12 @@ package com.netflix.tools.ja; +import java.lang.module.ModuleFinder; import java.util.ArrayList; import java.util.List; import com.netflix.tools.ja.BuiltinCommand.ToolRequirement; +import com.netflix.tools.ja.Command.Category; import com.netflix.tools.ja.BuiltinCommand.ToolRequirement.Provider; /** @@ -25,9 +27,17 @@ * catalog. */ public final class CommandAvailability { + private static final String JAVA_SE = "java.se"; + private CommandAvailability() {} public static void require(JaInvocation commandLine, ToolRuntime tools, ToolCatalog catalog) { + require(commandLine, tools, catalog, ModuleFinder.ofSystem()); + } + + public static void require(JaInvocation commandLine, ToolRuntime tools, ToolCatalog catalog, + ModuleFinder systemModules) { + requireRuntime(commandLine, systemModules); var missing = missingTools(commandLine, tools, catalog); if (missing.isEmpty()) { return; @@ -35,6 +45,32 @@ public static void require(JaInvocation commandLine, ToolRuntime tools, ToolCata throw new IllegalArgumentException(message(commandLine.command().commandName(), missing)); } + public static void requireRuntime(JaInvocation commandLine) { + requireRuntime(commandLine, ModuleFinder.ofSystem()); + } + + public static void requireRuntime(JaInvocation commandLine, ModuleFinder systemModules) { + BuiltinCommand.from(commandLine.command()).ifPresent(command -> requireRuntime(command, systemModules)); + } + + public static void requireRuntime(BuiltinCommand command, ModuleFinder systemModules) { + if (!available(command, systemModules)) { + throw new IllegalArgumentException(moduleDevelopmentMessage(command.commandName())); + } + } + + public static boolean available(BuiltinCommand command, ModuleFinder systemModules) { + return command.category() != Category.MODULE || moduleDevelopmentAvailable(systemModules); + } + + public static boolean moduleDevelopmentAvailable(ModuleFinder systemModules) { + return systemModules.find(JAVA_SE).isPresent(); + } + + public static String moduleDevelopmentMessage(String command) { + return command + " is unavailable; java.se is not present in this runtime"; + } + public static List missingTools(BuiltinCommand command, ToolRuntime tools, ToolCatalog catalog) { return missingTools(command.requiredTools(), tools, catalog); } diff --git a/src/com.netflix.tools.ja/com/netflix/tools/ja/CommandRunner.java b/src/com.netflix.tools.ja/com/netflix/tools/ja/CommandRunner.java index 7fb691a..edc05bc 100644 --- a/src/com.netflix.tools.ja/com/netflix/tools/ja/CommandRunner.java +++ b/src/com.netflix.tools.ja/com/netflix/tools/ja/CommandRunner.java @@ -121,6 +121,7 @@ private CommandRunner(ModuleLayer layer, ToolRuntime tools, ToolCatalogProvider public int run(JaInvocation commandLine, InputStream in, PrintStream out, PrintStream err) throws IOException { + CommandAvailability.requireRuntime(commandLine); if (commandLine.verbose() && !tools.verbose()) { var catalog = usesToolCatalog(commandLine) ? catalogProvider.load() : new ToolCatalog(List.of()); return new CommandRunner(layer, tools.withVerbose(catalog.definitions(), err), () -> catalog, temporaryDirectoryProvider, verbose(javaLauncher, err), installer) diff --git a/src/com.netflix.tools.ja/com/netflix/tools/ja/Ja.java b/src/com.netflix.tools.ja/com/netflix/tools/ja/Ja.java index b84f196..ffb1f58 100644 --- a/src/com.netflix.tools.ja/com/netflix/tools/ja/Ja.java +++ b/src/com.netflix.tools.ja/com/netflix/tools/ja/Ja.java @@ -20,6 +20,7 @@ import java.io.PrintStream; import java.io.PrintWriter; import java.io.UncheckedIOException; +import java.lang.module.ModuleFinder; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; @@ -73,11 +74,13 @@ static int run(InputStream in, PrintStream out, PrintStream err, } if (args.length == commandIndex || args[commandIndex].equals("-h") || args[commandIndex].equals("--help")) { var layer = Ja.class.getModule().getLayer(); - help(out, ToolRuntime.load(layer), ToolCatalog.load(layer)); + help(out, ToolRuntime.load(layer), ToolCatalog.load(layer), ModuleFinder.ofSystem()); return 0; } var commandHelp = commandHelp(args, commandIndex); if (commandHelp.isPresent()) { + BuiltinCommand.find(args[commandIndex]) + .ifPresent(command -> CommandAvailability.requireRuntime(command, ModuleFinder.ofSystem())); var command = commandHelp.orElseThrow(); out.print(command.commandLine() .help("ja " + command.name())); @@ -104,12 +107,21 @@ static int run(InputStream in, PrintStream out, PrintStream err, } public static void help(PrintStream out, ToolRuntime tools, ToolCatalog catalog) { + help(out, tools, catalog, ModuleFinder.ofSystem()); + } + + public static void help(PrintStream out, ToolRuntime tools, ToolCatalog catalog, + ModuleFinder systemModules) { out.println("Usage: ja [OPTIONS] [command-arguments]"); out.println(" ja [OPTIONS] tool [ [tool-arguments]]"); out.println(); + boolean moduleDevelopmentAvailable = CommandAvailability.moduleDevelopmentAvailable(systemModules); var unavailable = new ArrayList(); for (Category category : Category.values()) { + if (category == Category.MODULE && !moduleDevelopmentAvailable) { + continue; + } var available = new ArrayList(); for (BuiltinCommand command : BuiltinCommand.values()) { if (command.category() != category) { @@ -134,6 +146,10 @@ public static void help(PrintStream out, ToolRuntime tools, ToolCatalog catalog) } out.println(); } + if (!moduleDevelopmentAvailable) { + out.println("Module commands are unavailable; java.se is not present in this runtime."); + out.println(); + } if (!unavailable.isEmpty()) { out.println("Unavailable commands:"); for (BuiltinCommand command : unavailable) { @@ -192,10 +208,15 @@ static CommandLine commandLine() { static List complete(CompletionRequest request) { return complete(request, ToolRuntime.load(Ja.class .getModule() - .getLayer())); + .getLayer()), ModuleFinder.ofSystem()); } public static List complete(CompletionRequest request, ToolRuntime tools) { + return complete(request, tools, ModuleFinder.ofSystem()); + } + + public static List complete(CompletionRequest request, ToolRuntime tools, + ModuleFinder systemModules) { var prepared = COMMAND_LINE.prepare(request.invocation()); var arguments = prepared.arguments(); if (!arguments.isEmpty() && (arguments.getLast().equals("-p") || arguments.getLast().equals("--project"))) { @@ -209,6 +230,12 @@ public static List complete(CompletionRequest request, ToolRuntime t .toList(); } int commandIndex = JaInvocation.commandIndex(arguments); + if (arguments.size() > commandIndex) { + var command = BuiltinCommand.find(arguments.get(commandIndex)); + if (command.isPresent() && !CommandAvailability.available(command.orElseThrow(), systemModules)) { + return List.of(); + } + } if (arguments.size() > commandIndex && arguments.get(commandIndex).equals("tool")) { if (arguments.size() == commandIndex + 1) { return tools.names().stream() @@ -262,6 +289,9 @@ public static List complete(CompletionRequest request, ToolRuntime t } return COMMAND_LINE.complete(new CompletionRequest(prepared, request.current())).stream() .filter(completion -> !completion.value().equals("completion")) + .filter(completion -> BuiltinCommand.find(completion.value()) + .map(command -> CommandAvailability.available(command, systemModules)) + .orElse(true)) .toList(); } From acf7a4a33edaefdd415ca2f9c53964df33d416c2 Mon Sep 17 00:00:00 2001 From: Danny Thomas Date: Thu, 29 Oct 2026 06:04:08 +0000 Subject: [PATCH 2/2] Clarify reduced runtime capabilities --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index adbb622..7da4ad9 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ curl -fsSL https://raw.githubusercontent.com/Netflix/ja/main/install.sh | -Installation Standalone ``` -The installer selects the correct platform archive, installs it alongside applications managed by `ja install`, and activates `ja` through the same user command directory. The launcher uses `JAVA_HOME` when set, then falls back to `java` on `PATH`. The archive contains the development tools, but not a JDK. Use a plain JDK: the launcher rejects system modules whose names overlap the bundled application modules, rather than silently using the JDK's copies. When the selected runtime does not contain `java.se`, Ja keeps its development and code-insight commands available and reports module development commands as unavailable. +The installer selects the correct platform archive, installs it alongside applications managed by `ja install`, and activates `ja` through the same user command directory. The launcher uses `JAVA_HOME` when set, then falls back to `java` on `PATH`. The archive contains the development tools, but not a JDK. Use a plain JDK: the launcher rejects system modules whose names overlap the bundled application modules, rather than silently using the JDK's copies. When the selected runtime does not contain `java.se`, Ja keeps development commands available subject to the modules in that runtime and reports module development commands as unavailable. Neither installation modifies shell configuration. The installer prints the activation and completion commands to add to your shell profile. See the [installation guide](https://github.com/Netflix/ja/wiki/Installation) for default locations, custom locations, version pinning, shell setup, and environment-manager guidance.