11import java .io .{File , IOException }
22import java .nio .file .{Files , StandardCopyOption }
3+ import java .util .jar .Manifest
34
5+ import com .fasterxml .jackson .databind .ObjectMapper
46import sbt .internal .util .ManagedLogger
57import sbt .util .{FileFunction , FilesInfo }
68
9+ import scala .io .Source
10+
711/**
812 * A build utility instance handles build tasks and prints debug information using the managed logger.
913 *
@@ -139,90 +143,64 @@ class BuildUtility(logger: ManagedLogger) {
139143 return
140144 }
141145
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" )
163147
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
180153 }
181- })
182154
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+ }
185163 }
186164
187165 /**
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 = {
195181 // sbt allows easily to cache our external build using FileFunction.cached
196182 // sbt will only invoke the passed function when at least one of the input files (passed in the last line of this method)
197183 // has been modified. For the gui these input files are all files in the src directory of the gui and the package.json.
198184 // sbt passes these input files to the passed function, but they aren't used, we just instruct npm to build the gui.
199185 // 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+" ): _* )
206188 .inheritIO()
207- .directory(guiDir )
189+ .directory(workDir )
208190 .start()
209191 .waitFor()
210192
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
214196 } else {
215- logger info " GUI successfully built."
216- Set (new File (guiDir, " dist" ))
197+ Set (success())
217198 }
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+ }
224201
225- build(inputs).headOption
202+ cachedFn(inputs)
203+ true
226204 }
227205
228206 private def getNpmCommand : List [String ] = {
@@ -233,6 +211,43 @@ class BuildUtility(logger: ManagedLogger) {
233211 }
234212 }
235213
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+
236251 /**
237252 * Creates a file listing with all files including files in any sub-dir.
238253 *
0 commit comments