Using jpx from Java
Everything the jpx command does is available to a Java program: jpx itself is a main method around a
small API, and that API is public. Reach for it when a tool of your own has to run a published module - an
IDE plugin launching a formatter, a test harness driving a released version of the program under test, or a
script that resolves a tool once and then runs it many times.
Where the class lives
The type is build.jenesis.Jpx, in the module build.jenesis - the same artifact the build tool
publishes, build.jenesis:build.jenesis on Maven Central. Declare it the way you declare any dependency:
module demo.tooling {
requires build.jenesis;
}
A project that already embeds Jenesis as source under build/jenesis/ has the class in the tree, so an entry
point next to Project.java can call it with nothing resolved at all.
Install, then launch
Both halves of a jpx invocation are one call each: install the target, launch what it installed.
Jpx jpx = new Jpx(PathPlacement.INFERRED);
int status = jpx.install("org.junit.platform.console@6.1.3").launch(List.of("--version"));
install takes the target grammar the command line takes, <name>[@<version>][/<main-class>].
It performs the resolution and installation described earlier, reuses an existing
install when there is one, and returns an Installation: a handle on the folder it landed in. A
malformed target throws IllegalArgumentException rather than resolving something unexpected.
The constructor's argument is the placement, which is what --modular selects on the command line.
PathPlacement.INFERRED resolves a module name through its published coordinates and places each jar as it
describes a module. PathPlacement.MODULE_PATH resolves purely over module descriptors and places every
jar of the closure on the module path. The placement belongs to the instance rather than to the call because
it also decides which repository and resolver the instance carries, so one Jpx answers one way for every
target you hand it. A Maven coordinate is the one target it does not govern: it names an artifact rather
than a module, and runs on the class path whichever placement the instance was built with.
launch starts a java process from the JVM that runs your program, with the installed paths and entry
point. It inherits the current process's streams, waits, and returns the child's exit code. The
single-argument form uses the installation's own entry point; a launch(mainClass, arguments) overload
names a different one, the way a /<main-class> suffix does on the command line.
When you want the parts of a target separately - to log the version, or to route on the name - parse it yourself and hand the result to the same method:
Jpx.Command command = Jpx.Command.parse("org.junit.platform.console@6.1.3/org.junit.platform.console.ConsoleLauncher");
Jpx.Installation installation = jpx.install(command);
int status = installation.launch(command.mainClass(), List.of("--version"));
Command gives you name(), version(), and mainClass(). The name is always present; the other two are
null where the target left them out. The string form of install is exactly install(Command.parse(target)).
launch call that has to receive it. A /<main-class> written into a target
string therefore parses fine and then goes unused unless you pass command.mainClass() along, as
above.
Reading what was installed
An Installation exposes its folder and the descriptor written beside the jars, so you can inspect a target
without running it:
SequencedProperties descriptor = installation.properties();
String module = descriptor.getProperty("mainModule"); // null for a class-path target
String entry = descriptor.getProperty("mainClass"); // null when the target declares none
String digest = descriptor.getProperty("checksum"); // "SHA-256/…" over all jars
Path folder = installation.folder();
The modulepath and classpath entries are comma-separated file names, relative to that folder. Which of
the two a jar lands in follows from how the target was named: a module name is placed jar by jar as each
describes a module, a Maven coordinate on the class path in full. So mainModule is absent for a coordinate,
as it is for a module name whose jars declare none. A target with no entry point at all is a legitimate
install too: mainClass is absent, and launch then fails unless you name a class yourself.
To find an install that is already on disk without resolving anything, ask for the newest one by name:
Optional<Jpx.Installation> installed = jpx.latestInstalled("org.junit.platform.console");
That is the same lookup an unversioned target performs first, and it is what makes an offline run possible.
Verifying before you launch
verify is the API behind --hash. It recomputes the digest over the
installed jars and throws IllegalStateException when the result does not start with the prefix you passed,
so a launch guarded by it never runs unvetted bytes:
jpx.install("org.junit.platform.console@6.1.3")
.verify("9b60dfc3d10f0b4fdf69050eec7b7332")
.launch(List.of("--version"));
It returns the installation, so it chains between install and launch. The prefix rules are the command
line's: at least 32 hex characters, optionally written with the leading SHA-256/ the descriptor records.
Launching in a container, or building the command yourself
To run the program in a container, with resolution and installation still on the host, hand launch a
DockerizedJava from build.jenesis.docker:
Path workingDirectory = Path.of("").toAbsolutePath();
installation.launch(command.mainClass(), List.of("--version"), new DockerizedJava(workingDirectory));
The single-argument constructor builds and reuses the minimal hardened image that --docker uses;
new DockerizedJava(directory, "<image>") names an image of your own, which then runs without the
hardening flags. Either way the container runs the host's Java home, mounted read-only, so the image only
has to provide a compatible operating system, not a JDK. The working directory is mounted read-write at its
host path.
When you would rather start the process yourself - a different working directory, a redirected stream, an
extra JVM flag - ask for the argument list instead of a launch. javaArguments returns everything that
follows java, given a file it may spill long paths into:
Path arguments = Files.createTempFile("jpx.", ".args");
try {
List<String> command = new ArrayList<>();
command.add(Path.of(System.getProperty("java.home"), "bin", "java").toString());
command.addAll(installation.javaArguments(null, List.of("--version"), arguments));
new ProcessBuilder(command).inheritIO().start().waitFor();
} finally {
Files.deleteIfExists(arguments);
}
Choosing where things come from
Jpx is a record of five values - the storage folder, the repositories, the resolvers, the hash function,
and the placement - and it has two constructors. The short one takes a placement and fills the rest in the
way the command does, installing under ~/.jenesis/jpx:
Jpx jpx = new Jpx(PathPlacement.INFERRED); // resolve by published coordinates
Jpx modular = new Jpx(PathPlacement.MODULE_PATH); // resolve over module descriptors
PathPlacement.CLASS_PATH is the third value, and the one a Maven coordinate runs under however the instance
was built.
The long constructor names all five. A storage folder of your own keeps installs out of ~/.jenesis/jpx,
which is what a test, a sandboxed tool, or a demo wants; installations land under a layout folder inside it,
the same way they do under the default root. Here are the defaults spelled out, with
target/jpx as the storage folder:
MavenPomResolver maven = new MavenPomResolver();
Repository modules = JenesisModuleRepository.of(JenesisRepository.Scope.ARTIFACT);
Jpx jpx = new Jpx(Path.of("target", "jpx"),
Map.of("maven", MavenDefaultRepository.of(), "module", modules),
Map.<String, Resolver>of("maven", maven, "module", new MavenModuleResolver("maven", maven, modules)),
new HashDigestFunction("SHA-256"),
PathPlacement.INFERRED);
Replacing the repositories is the way to resolve from a private mirror, or from a local folder with no
network at all. MavenDefaultRepository takes the mirror's URI, a local folder to cache downloads in (null
for none), the checksum URIs to validate against, a callback for each fetch, and an optional token sent as
the Authorization header:
URI mirror = URI.create("https://nexus.example.com/maven2/");
Jpx jpx = new Jpx(storage,
Map.of("maven", new MavenDefaultRepository(mirror, local, Map.of("SHA256", mirror), _ -> { }, token)),
Map.of("maven", new MavenPomResolver()),
new HashDigestFunction("SHA-256"),
PathPlacement.INFERRED);
Repositories are keyed by the kind of coordinate they serve: maven for Maven coordinates and module for
module names. A map that carries only maven, as above, supports only Maven coordinates. Resolving a module
name under PathPlacement.INFERRED needs both entries, because the module index discovers the
coordinates and the Maven repository supplies the jars. A target that needs a missing entry fails rather than
silently reaching the public default.
MAVEN_REPOSITORY_URI and JENESIS_REPOSITORY_URI - and their token and local-folder
companions - already redirect a plain new Jpx(PathPlacement.INFERRED) at a mirror. Construct
the repositories yourself when the choice has to come from your program rather than from its environment.
install every time and pay for the download only on the
first run. The
jpx demo is this whole chapter as
one runnable file - it installs the JUnit console launcher into its own target/jpx/, verifies
it, and launches it.