StephanS
StephanS

Reputation: 851

can't find jpackage via ToolProvider

short version: I'm trying to invoke jpackage from a gradle task but the ToolProvider returns null (or better a failed Optional). This is the case on AdoptOpenJDK 14.0.0 (sdkman identifier 14.0.0.hs-adpt) as well as on Java.net (i think that's Oracle OpenJDK!?) 14.0.1 (sdkman identifier 14.0.1-open). I'm using Gradle 6.3 (but this doesn't feel like a gradle issue).

long version: I'm following this talk on jpackage, where at 12:12 the code to invoke jpackage from a build tool is displayed. (The official jpackage page also mentions: In addition to the command-line interface, jpackage is accessible via the ToolProvider API (java.util.spi.ToolProvider) under the name "jpackage".)

And still my (Kotlin) code (lying in buildSrc/src/main/kotlin)

object JPackage {
  fun buildInstaller( 
    ...
  ): Int {
    val jpackageTool: ToolProvider = ToolProvider.findFirst("jpackage").orElseThrow {
      val javaVersion: String = System.getProperty("java.version")
      IllegalStateException(
        "jpackage not found (expected JDK version: 14 or above, detected: $javaVersion)"
      )
    }
    val arguments: Array<String> = ...
    return jpackageTool.run(System.out, System.err, *arguments)
  }
}

invoked by a new Gradle task

tasks {
  register("buildInstaller") {
    JPackage.buildInstaller(
      ...
    )
    dependsOn("build")
  }
}

fails with stating

> Could not create task ':buildInstaller'.
> jpackage not found (expected JDK version: 14 or above, detected: 14.0.1)

I should add that I have no issues invoking jpackage from the command line.

UPDATE: I verified that this has nothing to do with either Kotlin or Gradle. This basic Java-14 program produces the same exception:

public class Main {
    public static void main(String[] args) {
        java.util.spi.ToolProvider.findFirst("jpackage").orElseThrow(() -> {
            String javaVersion = System.getProperty("java.version");
            return new IllegalStateException("jpackage not found (expected JDK version: 14 or above, detected: " + javaVersion + ")");
        });
        System.out.println("success");
    }
}

Solution: (incorporating Slaw's answer) Since jpackage is in "incubating" and therefor not readily available for my non-modular application I decided to invoke it by creating a new process:

object JPackage {
  fun buildInstaller( 
    ...
  ): Int {
    val arguments: Array<String> = ...
    return execJpackageViaRuntime(arguments)
  }

  private fun execJpackageViaRuntime(arguments: Array<String>): Int {
    val cmdAndArgs = ArrayList<String>(arguments.size+1).let {
      it.add("jpackage")
      it.addAll(arguments)
      it.toTypedArray()
    }
    return try {
      val process: Process = Runtime.getRuntime().exec(cmdAndArgs)
      process.waitFor(3L, TimeUnit.MINUTES)
      return process.exitValue()
    } catch (e: Exception) {
      1
    }
  }
}

and my task definition looks like this:

tasks {
    register("buildInstaller") {
        dependsOn("build")

        doLast {
            if (JavaVersion.current() < JavaVersion.VERSION_14) {
                throw GradleException("Require Java 14+ to run 'jpackage' (currently ${JavaVersion.current()})")
            }
            JPackage.buildInstaller(
                ...
            )
        }
    }
}

I can't execute the task from within IntelliJ since it seems to invoke Gradle with the JDK11 it itself comes bundled, but at least IntelliJ can compile the build script itself (since the version check is in the doLast-block and not directly in the register-block). Alternatively you can change the JDK that IntelliJ uses to invoke Gradle, scroll down to Slaw's comment under his answer to see how.

Btw: I'm pretty sure Gradle version 6.3 is a hard requirement for this to work, since it's the first Gradle version to be compatible with Java 14.

Upvotes: 4

Views: 544

Answers (1)

Slaw
Slaw

Reputation: 45806

The problem is related to JEP 11: Incubator Modules. That JEP states:

An incubator module is identified by the jdk.incubator. prefix in its module name, whether the module exports an incubating API or contains an incubating tool.

In Java 14, and probably for the next few releases, the jpackage tool is contained in a module named jdk.incubator.jpackage. From the name's prefix, we can see that module is an incubator module. The same JEP later states:

Incubator modules are part of the JDK run-time image produced by the standard JDK build. However, by default, incubator modules are not resolved for applications on the class path. Furthermore, by default, incubator modules do not participate in service binding for applications on the class path or the module path [emphasis added].

Applications on the class path must use the --add-modules command-line option to request that an incubator module be resolved. Applications developed as modules can specify requires or requires transitive dependences [sic] upon an incubator module directly. (Some incubator modules, such as those offering command-line tools, may forego exporting any packages, and instead provide service implementations so that tools can be accessed programatically [sic]. It is usually not recommended to require a module that exports no packages, but it is necessary in this case in order to resolve the incubator module, and have its service providers participate in service binding.)

Since incubator modules do not participate in service binding, the jdk.incubator.jpackage module is not resolved at runtime. This unfortunately means that the tool can not be located via ToolProvider unless you either:

  1. Launch the Java process with --add-modules jdk.incubator.jpackage,
  2. Or make your code modular and include a requires jdk.incubator.jpackage; directive in the module-info file.

Since you're attempting to invoke jpackage from a Gradle task I presume option two is not feasible. However, the first option does seem to be viable; when executing Gradle you can specify an appropriate org.gradle.jvmargs system property. For example:

./gradlew "-Dorg.gradle.jvmargs=--add-modules=jdk.incubator.jpackage" buildInstaller

You don't necessarily have to type that in the command-line every time. Check out this Gradle documentation to see where else you can define that system property. Might be able to do something through your IDE as well, though not sure.

That said, it should also be possible to invoke jpackage as another process using an Exec task. For example:

tasks {
    val build by existing

    register<Exec>("buildInstaller") {
        if (JavaVersion.current() < JavaVersion.VERSION_14) {
            throw GradleException("Require Java 14+ to run 'jpackage'")
        }
        dependsOn(build)

        executable = "jpackage"
        args(/* your args */)
    }
}

Upvotes: 3

Related Questions