Skip to content

Commit 6e6eca0

Browse files
nigredo-torimuuki88
authored andcommitted
JlinkPlugin: support multi-release dependencies (#1244)
* Fix error logging in JlinkPlugin (#1243) * Add a test case for multi-release dependency (#1243) * Add support for multi-release dependencies (#1243)
1 parent 659e0a9 commit 6e6eca0

File tree

5 files changed

+90
-9
lines changed

5 files changed

+90
-9
lines changed

src/main/scala/com/typesafe/sbt/packager/archetypes/jlink/JlinkPlugin.scala

+48-9
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package com.typesafe.sbt.packager.archetypes
22
package jlink
33

4-
import scala.sys.process.{Process, ProcessBuilder}
4+
import scala.sys.process.{BasicIO, Process, ProcessBuilder}
55
import sbt._
66
import sbt.Keys._
77
import com.typesafe.sbt.SbtNativePackager.{Debian, Universal}
@@ -49,15 +49,31 @@ object JlinkPlugin extends AutoPlugin {
4949
jlinkModules := (jlinkModules ?? Nil).value,
5050
jlinkModules ++= {
5151
val log = streams.value.log
52-
val run = runJavaTool(javaHome.in(jlinkBuildImage).value, log) _
52+
val javaHome0 = javaHome.in(jlinkBuildImage).value.getOrElse(defaultJavaHome)
53+
val run = runJavaTool(javaHome0, log) _
5354
val paths = fullClasspath.in(jlinkBuildImage).value.map(_.data.getPath)
5455
val shouldIgnore = jlinkIgnoreMissingDependency.value
5556

57+
// We can find the java toolchain version by parsing the `release` file. This
58+
// only works for Java 9+, but so does this whole plugin.
59+
// Alternatives:
60+
// - Parsing `java -version` output - the format is not standardized, so there
61+
// are a lot of weird incompatibilities.
62+
// - Parsing `java -XshowSettings:properties` output - the format is nicer,
63+
// but the command itself is subject to change without notice.
64+
val releaseFile = javaHome0 / "release"
65+
val javaVersion = IO
66+
.readLines(releaseFile)
67+
.collectFirst {
68+
case javaVersionPattern(feature) => feature
69+
}
70+
.getOrElse(sys.error("JAVA_VERSION not found in ${releaseFile.getAbsolutePath}"))
71+
5672
// Jdeps has a few convenient options (like --print-module-deps), but those
5773
// are not flexible enough - we need to parse the full output.
58-
val output = run("jdeps", "-R" +: paths) !! log
74+
val jdepsOutput = runForOutput(run("jdeps", "--multi-release" +: javaVersion +: "-R" +: paths), log)
5975

60-
val deps = output.linesIterator
76+
val deps = jdepsOutput.linesIterator
6177
// There are headers in some of the lines - ignore those.
6278
.flatMap(PackageDependency.parse(_).iterator)
6379
.toSeq
@@ -109,12 +125,13 @@ object JlinkPlugin extends AutoPlugin {
109125
},
110126
jlinkBuildImage := {
111127
val log = streams.value.log
112-
val run = runJavaTool(javaHome.in(jlinkBuildImage).value, log) _
128+
val javaHome0 = javaHome.in(jlinkBuildImage).value.getOrElse(defaultJavaHome)
129+
val run = runJavaTool(javaHome0, log) _
113130
val outDir = target.in(jlinkBuildImage).value
114131

115132
IO.delete(outDir)
116133

117-
run("jlink", jlinkOptions.value) !! log
134+
runForOutput(run("jlink", jlinkOptions.value), log)
118135

119136
outDir
120137
},
@@ -130,21 +147,43 @@ object JlinkPlugin extends AutoPlugin {
130147
mappings in Universal ++= mappings.in(jlinkBuildImage).value
131148
)
132149

150+
// Extracts java version from a release file line (`JAVA_VERSION` property):
151+
// - if the feature version is 1, yield the minor version number (e.g. 1.9.0 -> 9);
152+
// - otherwise yield the major version number (e.g. 11.0.3 -> 11).
153+
private[jlink] val javaVersionPattern = """JAVA_VERSION="(?:1\.)?(\d+).*?"""".r
154+
133155
// TODO: deduplicate with UniversalPlugin and DebianPlugin
134156
/** Finds all files in a directory. */
135157
private def findFiles(dir: File): Seq[(File, String)] =
136158
((PathFinder(dir) ** AllPassFilter) --- dir)
137159
.pair(file => IO.relativize(dir, file))
138160

139-
private def runJavaTool(jvm: Option[File], log: Logger)(exeName: String, args: Seq[String]): ProcessBuilder = {
140-
val jh = jvm.getOrElse(file(sys.props.getOrElse("java.home", sys.error("no java.home"))))
141-
val exe = (jh / "bin" / exeName).getAbsolutePath
161+
private lazy val defaultJavaHome: File =
162+
file(sys.props.getOrElse("java.home", sys.error("no java.home")))
163+
164+
private def runJavaTool(jvm: File, log: Logger)(exeName: String, args: Seq[String]): ProcessBuilder = {
165+
val exe = (jvm / "bin" / exeName).getAbsolutePath
142166

143167
log.info("Running: " + (exe +: args).mkString(" "))
144168

145169
Process(exe, args)
146170
}
147171

172+
// Like `ProcessBuilder.!!`, but this logs the output in case of a non-zero
173+
// exit code. We need this since some Java tools write their errors to stdout.
174+
// This uses `scala.sys.process.ProcessLogger` instead of the SBT `Logger`
175+
// to make it a drop-in replacement for `ProcessBuilder.!!`.
176+
private def runForOutput(builder: ProcessBuilder, log: scala.sys.process.ProcessLogger): String = {
177+
val buffer = new StringBuffer
178+
val code = builder.run(BasicIO(false, buffer, Some(log))).exitValue()
179+
180+
if (code == 0) buffer.toString
181+
else {
182+
log.out(buffer.toString)
183+
scala.sys.error("Nonzero exit value: " + code)
184+
}
185+
}
186+
148187
private object JlinkOptions {
149188
@deprecated("1.3.24", "")
150189
def apply(addModules: Seq[String] = Nil, output: Option[File] = None): Seq[String] =
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Various JlinkPlugin test cases that don't warrant setting up separate
2+
// `scripted` tests.
3+
4+
import scala.sys.process.Process
5+
import com.typesafe.sbt.packager.Compat._
6+
7+
val runChecks = taskKey[Unit]("Run checks for a specific issue")
8+
9+
// Exclude Scala by default to simplify the test.
10+
autoScalaLibrary in ThisBuild := false
11+
12+
// Should succeed for multi-release artifacts
13+
val issue1243 = project
14+
.enablePlugins(JlinkPlugin)
15+
.settings(
16+
libraryDependencies ++= List(
17+
// An arbitrary multi-release artifact
18+
"org.apache.logging.log4j" % "log4j-core" % "2.12.0"
19+
),
20+
// Don't bother with providing dependencies.
21+
jlinkIgnoreMissingDependency := JlinkIgnore.everything,
22+
runChecks := jlinkBuildImage.value
23+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
val pluginVersion = sys.props("project.version")
3+
if (pluginVersion == null)
4+
throw new RuntimeException("""|The system property 'project.version' is not defined.
5+
|Specify this property using the scriptedLaunchOpts -D.""".stripMargin)
6+
else
7+
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % sys.props("project.version"))
8+
}
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# These tasks can be aggregated, but running them one by one means
2+
# more granular output in case of a failure.
3+
> issue1243/runChecks

src/test/scala/com/typesafe/sbt/packager/archetypes/jlink/JlinkSpec.scala

+8
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.typesafe.sbt.packager.archetypes.jlink
22

33
import org.scalatest.{FlatSpec, Matchers}
44
import JlinkPlugin.Ignore.byPackagePrefix
5+
import JlinkPlugin.javaVersionPattern
56

67
class JlinkSpec extends FlatSpec with Matchers {
78
"Ignore.byPackagePrefix()" should "match as expected for sample examples" in {
@@ -20,4 +21,11 @@ class JlinkSpec extends FlatSpec with Matchers {
2021
byPackagePrefix("foo" -> "bar", "" -> "")("baz" -> "qux") should be(true)
2122
byPackagePrefix("foo" -> "", "" -> "bar")("baz" -> "qux") should be(false)
2223
}
24+
25+
"javaVersionPattern" should "match known examples" in {
26+
"""JAVA_VERSION="11.0.3"""" should fullyMatch regex (javaVersionPattern withGroup "11")
27+
// Haven't seen this in the wild, but JEP220 has this example, so we might
28+
// as well handle it.
29+
"""JAVA_VERSION="1.9.0"""" should fullyMatch regex (javaVersionPattern withGroup "9")
30+
}
2331
}

0 commit comments

Comments
 (0)