1
1
package com .typesafe .sbt .packager .archetypes
2
2
package jlink
3
3
4
- import scala .sys .process .{Process , ProcessBuilder }
4
+ import scala .sys .process .{BasicIO , Process , ProcessBuilder }
5
5
import sbt ._
6
6
import sbt .Keys ._
7
7
import com .typesafe .sbt .SbtNativePackager .{Debian , Universal }
@@ -49,15 +49,31 @@ object JlinkPlugin extends AutoPlugin {
49
49
jlinkModules := (jlinkModules ?? Nil ).value,
50
50
jlinkModules ++= {
51
51
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) _
53
54
val paths = fullClasspath.in(jlinkBuildImage).value.map(_.data.getPath)
54
55
val shouldIgnore = jlinkIgnoreMissingDependency.value
55
56
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
+
56
72
// Jdeps has a few convenient options (like --print-module-deps), but those
57
73
// 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)
59
75
60
- val deps = output .linesIterator
76
+ val deps = jdepsOutput .linesIterator
61
77
// There are headers in some of the lines - ignore those.
62
78
.flatMap(PackageDependency .parse(_).iterator)
63
79
.toSeq
@@ -109,12 +125,13 @@ object JlinkPlugin extends AutoPlugin {
109
125
},
110
126
jlinkBuildImage := {
111
127
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) _
113
130
val outDir = target.in(jlinkBuildImage).value
114
131
115
132
IO .delete(outDir)
116
133
117
- run(" jlink" , jlinkOptions.value) !! log
134
+ runForOutput( run(" jlink" , jlinkOptions.value), log)
118
135
119
136
outDir
120
137
},
@@ -130,21 +147,43 @@ object JlinkPlugin extends AutoPlugin {
130
147
mappings in Universal ++= mappings.in(jlinkBuildImage).value
131
148
)
132
149
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
+
133
155
// TODO: deduplicate with UniversalPlugin and DebianPlugin
134
156
/** Finds all files in a directory. */
135
157
private def findFiles (dir : File ): Seq [(File , String )] =
136
158
((PathFinder (dir) ** AllPassFilter ) --- dir)
137
159
.pair(file => IO .relativize(dir, file))
138
160
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
142
166
143
167
log.info(" Running: " + (exe +: args).mkString(" " ))
144
168
145
169
Process (exe, args)
146
170
}
147
171
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
+
148
187
private object JlinkOptions {
149
188
@ deprecated(" 1.3.24" , " " )
150
189
def apply (addModules : Seq [String ] = Nil , output : Option [File ] = None ): Seq [String ] =
0 commit comments