Skip to content
This repository was archived by the owner on Aug 18, 2020. It is now read-only.

Commit e9d7d55

Browse files
authored
Merge pull request #17 from daniel0611/bundle-gui
Add gui to the build process
2 parents 4771db6 + 062b809 commit e9d7d55

9 files changed

+151
-7
lines changed

.gitignore

+4-1
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,7 @@ project/plugins/project/
3535
/wiki/
3636

3737
# Plugin Data
38-
data/
38+
data/
39+
40+
# Built gui
41+
/src/main/resources/chatoverflow-gui

.idea/runConfigurations/Build_GUI__sbt_gui_.xml

+11
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/runConfigurations/_Advanced__Full_Reload_and_Run_ChatOverflow.xml

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/runConfigurations/_Deploy__Generate_Bootstrap_Launcher_and_deploy.xml

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/runConfigurations/_Simple__Rebuild_plugins_and_Run_ChatOverflow.xml

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Makefile

+4-1
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,21 @@
33
advanced_run:
44
sbt clean
55
sbt compile
6+
sbt gui
67
sbt fetch
78
sbt reload
89
sbt version
910
sbt package copy
1011

1112
simple_run:
1213
sbt compile
14+
sbt gui
1315
sbt package copy
1416

1517
bootstrap_deploy:
16-
sbt compile
1718
sbt clean
19+
sbt compile
20+
sbt gui
1821
sbt package copy
1922
sbt bs "project bootstrapProject" assembly
2023
sbt deploy

build.sbt

+9-1
Original file line numberDiff line numberDiff line change
@@ -73,25 +73,29 @@ lazy val pluginBuildFileName = settingKey[String]("The filename of the plugin bu
7373
lazy val pluginFolderNames = settingKey[List[String]]("The folder names of all plugin source directories.")
7474
lazy val pluginTargetFolderNames = settingKey[List[String]]("The folder names of compiled and packaged plugins. Remember to gitignore these!")
7575
lazy val apiProjectPath = settingKey[String]("The path to the api sub project. Remember to gitignore it!")
76+
lazy val guiProjectPath = settingKey[String]("The path of the Angular gui.")
7677

7778
// Plugin framework tasks
7879
lazy val create = TaskKey[Unit]("create", "Creates a new plugin. Interactive command using the console.")
7980
lazy val fetch = TaskKey[Unit]("fetch", "Searches for plugins in plugin directories, builds the plugin build file.")
8081
lazy val copy = TaskKey[Unit]("copy", "Copies all packaged plugin jars to the target plugin folder.")
8182
lazy val bs = TaskKey[Unit]("bs", "Updates the bootstrap project with current dependencies and chat overflow jars.")
8283
lazy val deploy = TaskKey[Unit]("deploy", "Prepares the environment for deployment, fills deploy folder.")
84+
lazy val gui = TaskKey[Unit]("gui", "Installs GUI dependencies and builds it using npm.")
8385

8486
pluginBuildFileName := "plugins.sbt"
8587
pluginFolderNames := List("plugins-public")
8688
pluginTargetFolderNames := List("plugins", s"target/scala-$scalaMajorVersion/plugins")
8789
apiProjectPath := "api"
90+
guiProjectPath := "gui"
8891

8992
create := BuildUtility(streams.value.log).createPluginTask(pluginFolderNames.value)
9093
fetch := BuildUtility(streams.value.log).fetchPluginsTask(pluginFolderNames.value, pluginBuildFileName.value,
9194
pluginTargetFolderNames.value, apiProjectPath.value)
9295
copy := BuildUtility(streams.value.log).copyPluginsTask(pluginFolderNames.value, pluginTargetFolderNames.value, scalaMajorVersion)
9396
bs := BootstrapUtility.bootstrapGenTask(streams.value.log, s"$scalaMajorVersion$scalaMinorVersion", getDependencyList.value)
9497
deploy := BootstrapUtility.prepareDeploymentTask(streams.value.log, scalaMajorVersion)
98+
gui := BuildUtility(streams.value.log).guiTask(guiProjectPath.value, streams.value.cacheDirectory / "gui")
9599

96100
// ---------------------------------------------------------------------------------------------------------------------
97101
// UTIL
@@ -107,4 +111,8 @@ lazy val getDependencyList = Def.task[List[ModuleID]] {
107111
} else {
108112
updateReport.get.modules.map(m => m.module).toList
109113
}
110-
}
114+
}
115+
116+
// Clears the built GUI dirs on clean
117+
cleanFiles += baseDirectory.value / guiProjectPath.value / "dist"
118+
cleanFiles += baseDirectory.value / "src" / "main" / "resources" / "chatoverflow-gui"

project/BuildUtility.scala

+118
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import java.io.{File, IOException}
22
import java.nio.file.Files
33

44
import sbt.internal.util.ManagedLogger
5+
import sbt.util.{FileFunction, FilesInfo}
56

67
/**
78
* A build utility instance handles build tasks and prints debug information using the managed logger.
@@ -20,6 +21,7 @@ import sbt.internal.util.ManagedLogger
2021
* | -> -> -> build.sbt
2122
* | -> -> -> source etc.
2223
* | -> another plugin source directory (optional)
24+
* | -> gui project
2325
*
2426
*/
2527
class BuildUtility(logger: ManagedLogger) {
@@ -219,6 +221,122 @@ class BuildUtility(logger: ManagedLogger) {
219221
}
220222
}
221223
}
224+
225+
def guiTask(guiProjectPath: String, cacheDir: File): Unit = {
226+
withTaskInfo("BUILD GUI") {
227+
val guiDir = new File(guiProjectPath)
228+
if (!guiDir.exists()) {
229+
logger warn s"GUI not found at $guiProjectPath, ignoring GUI build."
230+
return
231+
}
232+
233+
if (installGuiDeps(guiDir, cacheDir).isEmpty)
234+
return // Early return on failure, error has already been displayed
235+
236+
val outDir = buildGui(guiDir, cacheDir)
237+
if (outDir.isEmpty)
238+
return // Again early return on failure
239+
240+
// Copy built gui into resources, will be included in the classpath on execution of the framework
241+
sbt.IO.copyDirectory(outDir.get, new File("src/main/resources/chatoverflow-gui"))
242+
}
243+
}
244+
245+
/**
246+
* Download the dependencies of the gui using npm.
247+
*
248+
* @param guiDir the directory of the gui.
249+
* @param cacheDir a dir, where sbt can store files for caching in the "install" sub-dir.
250+
* @return None, if a error occurs which will be displayed, otherwise the output directory with the built gui.
251+
*/
252+
private def installGuiDeps(guiDir: File, cacheDir: File): Option[File] = {
253+
// Check buildGui for a explanation, it's almost the same.
254+
255+
val install = FileFunction.cached(new File(cacheDir, "install"), FilesInfo.hash)(_ => {
256+
257+
logger info "Installing GUI dependencies."
258+
259+
val exitCode = new ProcessBuilder(getNpmCommand :+ "install": _*)
260+
.inheritIO()
261+
.directory(guiDir)
262+
.start()
263+
.waitFor()
264+
265+
if (exitCode != 0) {
266+
logger error "GUI dependencies couldn't be installed, please check above log for further details."
267+
return None
268+
} else {
269+
logger info "GUI dependencies successfully installed."
270+
Set(new File(guiDir, "node_modules"))
271+
}
272+
})
273+
274+
val input = new File(guiDir, "package.json")
275+
install(Set(input)).headOption
276+
}
277+
278+
/**
279+
* Builds the gui using npm.
280+
*
281+
* @param guiDir the directory of the gui.
282+
* @param cacheDir a dir, where sbt can store files for caching in the "build" sub-dir.
283+
* @return None, if a error occurs which will be displayed, otherwise the output directory with the built gui.
284+
*/
285+
private def buildGui(guiDir: File, cacheDir: File): Option[File] = {
286+
// sbt allows easily to cache our external build using FileFunction.cached
287+
// sbt will only invoke the passed function when at least one of the input files (passed in the last line of this method)
288+
// has been modified. For the gui these input files are all files in the src directory of the gui and the package.json.
289+
// sbt passes these input files to the passed function, but they aren't used, we just instruct npm to build the gui.
290+
// sbt invalidates the cache as well if any of the output files (returned by the passed function) doesn't exist anymore.
291+
292+
val build = FileFunction.cached(new File(cacheDir, "build"), FilesInfo.hash)(_ => {
293+
294+
logger info "Building GUI."
295+
296+
val buildExitCode = new ProcessBuilder(getNpmCommand :+ "run" :+ "build": _*)
297+
.inheritIO()
298+
.directory(guiDir)
299+
.start()
300+
.waitFor()
301+
302+
if (buildExitCode != 0) {
303+
logger error "GUI couldn't be built, please check above log for further details."
304+
return None
305+
} else {
306+
logger info "GUI successfully built."
307+
Set(new File(guiDir, "dist"))
308+
}
309+
})
310+
311+
312+
val srcDir = new File(guiDir, "src")
313+
val packageJson = new File(guiDir, "package.json")
314+
val inputs = recursiveFileListing(srcDir) + packageJson
315+
316+
build(inputs).headOption
317+
}
318+
319+
private def getNpmCommand: List[String] = {
320+
if (System.getProperty("os.name").toLowerCase().contains("win")) {
321+
List("cmd.exe", "/C", "npm")
322+
} else {
323+
List("npm")
324+
}
325+
}
326+
327+
/**
328+
* Creates a file listing with all files including files in any sub-dir.
329+
*
330+
* @param f the directory for which the file listing needs to be created.
331+
* @return the file listing as a set of files.
332+
*/
333+
private def recursiveFileListing(f: File): Set[File] = {
334+
if (f.isDirectory) {
335+
f.listFiles().flatMap(recursiveFileListing).toSet
336+
} else {
337+
Set(f)
338+
}
339+
}
222340
}
223341

224342
object BuildUtility {

src/main/scala/org/codeoverflow/chatoverflow/ui/web/Server.scala

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package org.codeoverflow.chatoverflow.ui.web
22

33
import org.codeoverflow.chatoverflow.{ChatOverflow, WithLogger}
4-
import org.eclipse.jetty.servlet.ServletHandler.Default404Servlet
4+
import org.eclipse.jetty.util.resource.Resource
55
import org.eclipse.jetty.webapp.WebAppContext
66
import org.scalatra.servlet.ScalatraListener
77

@@ -17,9 +17,8 @@ class Server(val chatOverflow: ChatOverflow, val port: Int) extends WithLogger {
1717
private val context = new WebAppContext()
1818
context.setInitParameter("org.eclipse.jetty.servlet.Default.dirAllowed", "false")
1919
context setContextPath "/"
20-
context.setResourceBase("/")
20+
context.setBaseResource(Resource.newClassPathResource("/chatoverflow-gui/"))
2121
context.addEventListener(new ScalatraListener)
22-
context.addServlet(classOf[Default404Servlet], "/")
2322

2423
server.setHandler(context)
2524

0 commit comments

Comments
 (0)