1
1
import java .io .{File , IOException }
2
2
import java .nio .file .{Files , StandardCopyOption }
3
+ import java .util .jar .Manifest
3
4
5
+ import com .fasterxml .jackson .databind .ObjectMapper
4
6
import sbt .internal .util .ManagedLogger
5
7
import sbt .util .{FileFunction , FilesInfo }
6
8
9
+ import scala .io .Source
10
+
7
11
/**
8
12
* A build utility instance handles build tasks and prints debug information using the managed logger.
9
13
*
@@ -139,90 +143,64 @@ class BuildUtility(logger: ManagedLogger) {
139
143
return
140
144
}
141
145
142
- if (installGuiDeps(guiDir, cacheDir).isEmpty)
143
- return // Early return on failure, error has already been displayed
144
-
145
- val outDir = buildGui(guiDir, cacheDir)
146
- if (outDir.isEmpty)
147
- return // Again early return on failure
148
-
149
- // Copy built gui into resources, will be included in the classpath on execution of the framework
150
- sbt.IO .copyDirectory(outDir.get, new File (" src/main/resources/chatoverflow-gui" ))
151
- }
152
- }
153
-
154
- /**
155
- * Download the dependencies of the gui using npm.
156
- *
157
- * @param guiDir the directory of the gui.
158
- * @param cacheDir a dir, where sbt can store files for caching in the "install" sub-dir.
159
- * @return None, if a error occurs which will be displayed, otherwise the output directory with the built gui.
160
- */
161
- private def installGuiDeps (guiDir : File , cacheDir : File ): Option [File ] = {
162
- // Check buildGui for a explanation, it's almost the same.
146
+ val packageJson = new File (guiDir, " package.json" )
163
147
164
- val install = FileFunction .cached(new File (cacheDir, " install" ), FilesInfo .hash)(_ => {
165
-
166
- logger info " Installing GUI dependencies."
167
-
168
- val exitCode = new ProcessBuilder (getNpmCommand :+ " install" : _* )
169
- .inheritIO()
170
- .directory(guiDir)
171
- .start()
172
- .waitFor()
173
-
174
- if (exitCode != 0 ) {
175
- logger error " GUI dependencies couldn't be installed, please check above log for further details."
176
- return None
177
- } else {
178
- logger info " GUI dependencies successfully installed."
179
- Set (new File (guiDir, " node_modules" ))
148
+ if (! executeNpmCommand(guiDir, cacheDir, Set (packageJson), " install" ,
149
+ () => logger error " GUI dependencies couldn't be installed, please check above log for further details." ,
150
+ () => new File (guiDir, " node_modules" )
151
+ )) {
152
+ return // early return on failure, error has already been displayed
180
153
}
181
- })
182
154
183
- val input = new File (guiDir, " package.json" )
184
- install(Set (input)).headOption
155
+ val srcFiles = recursiveFileListing(new File (guiDir, " src" ))
156
+ val outDir = new File (guiDir, " dist" )
157
+
158
+ executeNpmCommand(guiDir, cacheDir, srcFiles + packageJson, " run build" ,
159
+ () => logger error " GUI couldn't be built, please check above log for further details." ,
160
+ () => outDir
161
+ )
162
+ }
185
163
}
186
164
187
165
/**
188
- * Builds the gui using npm.
189
- *
190
- * @param guiDir the directory of the gui.
191
- * @param cacheDir a dir, where sbt can store files for caching in the "build" sub-dir.
192
- * @return None, if a error occurs which will be displayed, otherwise the output directory with the built gui.
193
- */
194
- private def buildGui (guiDir : File , cacheDir : File ): Option [File ] = {
166
+ * Executes a npm command in the given directory and skips executing the given command
167
+ * if no input files have changed and the output file still exists.
168
+ *
169
+ * @param workDir the directory in which npm should be executed
170
+ * @param cacheDir a directory required for caching using sbt
171
+ * @param inputs the input files, which will be used for caching.
172
+ * If any one of these files change the cache is invalidated.
173
+ * @param command the npm command to execute
174
+ * @param failed called if npm returned an non-zero exit code
175
+ * @param success called if npm returned successfully. Needs to return a file for caching.
176
+ * If the returned file doesn't exist the npm command will ignore the cache.
177
+ * @return true if npm returned zero as a exit code and false otherwise
178
+ */
179
+ private def executeNpmCommand (workDir : File , cacheDir : File , inputs : Set [File ], command : String ,
180
+ failed : () => Unit , success : () => File ): Boolean = {
195
181
// sbt allows easily to cache our external build using FileFunction.cached
196
182
// sbt will only invoke the passed function when at least one of the input files (passed in the last line of this method)
197
183
// has been modified. For the gui these input files are all files in the src directory of the gui and the package.json.
198
184
// sbt passes these input files to the passed function, but they aren't used, we just instruct npm to build the gui.
199
185
// sbt invalidates the cache as well if any of the output files (returned by the passed function) doesn't exist anymore.
200
-
201
- val build = FileFunction .cached(new File (cacheDir, " build" ), FilesInfo .hash)(_ => {
202
-
203
- logger info " Building GUI."
204
-
205
- val buildExitCode = new ProcessBuilder (getNpmCommand :+ " run" :+ " build" : _* )
186
+ val cachedFn = FileFunction .cached(new File (cacheDir, command), FilesInfo .hash) { _ => {
187
+ val exitCode = new ProcessBuilder (getNpmCommand ++ command.split(" \\ s+" ): _* )
206
188
.inheritIO()
207
- .directory(guiDir )
189
+ .directory(workDir )
208
190
.start()
209
191
.waitFor()
210
192
211
- if (buildExitCode != 0 ) {
212
- logger error " GUI couldn't be built, please check above log for further details. "
213
- return None
193
+ if (exitCode != 0 ) {
194
+ failed()
195
+ return false
214
196
} else {
215
- logger info " GUI successfully built."
216
- Set (new File (guiDir, " dist" ))
197
+ Set (success())
217
198
}
218
- })
219
-
220
-
221
- val srcDir = new File (guiDir, " src" )
222
- val packageJson = new File (guiDir, " package.json" )
223
- val inputs = recursiveFileListing(srcDir) + packageJson
199
+ }
200
+ }
224
201
225
- build(inputs).headOption
202
+ cachedFn(inputs)
203
+ true
226
204
}
227
205
228
206
private def getNpmCommand : List [String ] = {
@@ -233,6 +211,43 @@ class BuildUtility(logger: ManagedLogger) {
233
211
}
234
212
}
235
213
214
+ def packageGUITask (guiProjectPath : String , scalaMajorVersion : String , crossTargetDir : File ): Unit = {
215
+ val dir = new File (guiProjectPath, " dist" )
216
+ if (! dir.exists()) {
217
+ logger info " GUI hasn't been compiled. Won't create a jar for it."
218
+ return
219
+ }
220
+
221
+ val files = recursiveFileListing(dir)
222
+
223
+ // contains tuples with the actual file as the first value and the name with directory in the jar as the second value
224
+ val jarEntries = files.map(file => file -> s " /chatoverflow-gui/ ${dir.toURI.relativize(file.toURI).toString}" )
225
+
226
+ val guiVersion = getGUIVersion(guiProjectPath).getOrElse(" unknown" )
227
+
228
+ sbt.IO .jar(jarEntries, new File (crossTargetDir, s " chatoverflow-gui_ $scalaMajorVersion- $guiVersion.jar " ), new Manifest ())
229
+ }
230
+
231
+ private def getGUIVersion (guiProjectPath : String ): Option [String ] = {
232
+ val packageJson = new File (s " $guiProjectPath/package.json " )
233
+ if (! packageJson.exists()) {
234
+ logger error " The package.json file of the GUI doesn't exist. Have you cloned the GUI in the correct directory?"
235
+ return None
236
+ }
237
+
238
+ val content = Source .fromFile(packageJson)
239
+ val version = new ObjectMapper ().reader().readTree(content.mkString).get(" version" ).asText()
240
+
241
+ content.close()
242
+
243
+ if (version.isEmpty) {
244
+ logger warn " The GUI version couldn't be loaded from the package.json."
245
+ None
246
+ } else {
247
+ Option (version)
248
+ }
249
+ }
250
+
236
251
/**
237
252
* Creates a file listing with all files including files in any sub-dir.
238
253
*
0 commit comments