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 *
@@ -141,90 +145,64 @@ class BuildUtility(logger: ManagedLogger) {
141145 return
142146 }
143147
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" )
175149
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
182155 }
183- })
184156
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+ }
187165 }
188166
189167 /**
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 = {
197183 // sbt allows easily to cache our external build using FileFunction.cached
198184 // sbt will only invoke the passed function when at least one of the input files (passed in the last line of this method)
199185 // has been modified. For the gui these input files are all files in the src directory of the gui and the package.json.
200186 // sbt passes these input files to the passed function, but they aren't used, we just instruct npm to build the gui.
201187 // 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+" ): _* )
208190 .inheritIO()
209- .directory(guiDir )
191+ .directory(workDir )
210192 .start()
211193 .waitFor()
212194
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
216198 } else {
217- logger info " GUI successfully built."
218- Set (new File (guiDir, " dist" ))
199+ Set (success())
219200 }
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+ }
226203
227- build(inputs).headOption
204+ cachedFn(inputs)
205+ true
228206 }
229207
230208 private def getNpmCommand : List [String ] = {
@@ -235,6 +213,43 @@ class BuildUtility(logger: ManagedLogger) {
235213 }
236214 }
237215
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+
238253 /**
239254 * Creates a file listing with all files including files in any sub-dir.
240255 *
0 commit comments