Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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";
Expand Down
86 changes: 50 additions & 36 deletions src/com.netflix.tools.ja/com/netflix/tools/ja/AotWarmup.java
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -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");
Expand All @@ -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());
Expand All @@ -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");
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,63 @@

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;

/**
* Checks that the tools required to run a command are available in the selected
* 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;
}
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<String> missingTools(BuiltinCommand command, ToolRuntime tools, ToolCatalog catalog) {
return missingTools(command.requiredTools(), tools, catalog);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
34 changes: 32 additions & 2 deletions src/com.netflix.tools.ja/com/netflix/tools/ja/Ja.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()));
Expand All @@ -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> [command-arguments]");
out.println(" ja [OPTIONS] tool [<name> [tool-arguments]]");
out.println();

boolean moduleDevelopmentAvailable = CommandAvailability.moduleDevelopmentAvailable(systemModules);
var unavailable = new ArrayList<BuiltinCommand>();
for (Category category : Category.values()) {
if (category == Category.MODULE && !moduleDevelopmentAvailable) {
continue;
}
var available = new ArrayList<BuiltinCommand>();
for (BuiltinCommand command : BuiltinCommand.values()) {
if (command.category() != category) {
Expand All @@ -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) {
Expand Down Expand Up @@ -192,10 +208,15 @@ static CommandLine commandLine() {
static List<Completion> complete(CompletionRequest request) {
return complete(request, ToolRuntime.load(Ja.class
.getModule()
.getLayer()));
.getLayer()), ModuleFinder.ofSystem());
}

public static List<Completion> complete(CompletionRequest request, ToolRuntime tools) {
return complete(request, tools, ModuleFinder.ofSystem());
}

public static List<Completion> 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"))) {
Expand All @@ -209,6 +230,12 @@ public static List<Completion> 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()
Expand Down Expand Up @@ -262,6 +289,9 @@ public static List<Completion> 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();
}

Expand Down
Loading