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