Skip to content
Permalink
Browse files

Bundle gui with main framework

  • Loading branch information...
daniel0611 committed Jun 14, 2019
1 parent c028b72 commit e0de5ea3383543a0823aa7c7a2aa7156c0e1aca5
Showing with 125 additions and 5 deletions.
  1. +4 −1 .gitignore
  2. +9 −1 build.sbt
  3. +110 −0 project/BuildUtility.scala
  4. +2 −3 src/main/scala/org/codeoverflow/chatoverflow/ui/web/Server.scala
@@ -35,4 +35,7 @@ project/plugins/project/
/wiki/

# Plugin Data
data/
data/

# Built gui
/src/main/resources/chatoverflow-gui
@@ -73,25 +73,29 @@ lazy val pluginBuildFileName = settingKey[String]("The filename of the plugin bu
lazy val pluginFolderNames = settingKey[List[String]]("The folder names of all plugin source directories.")
lazy val pluginTargetFolderNames = settingKey[List[String]]("The folder names of compiled and packaged plugins. Remember to gitignore these!")
lazy val apiProjectPath = settingKey[String]("The path to the api sub project. Remember to gitignore it!")
lazy val guiProjectPath = settingKey[String]("The path of the Angular gui.")

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

pluginBuildFileName := "plugins.sbt"
pluginFolderNames := List("plugins-public")
pluginTargetFolderNames := List("plugins", s"target/scala-$scalaMajorVersion/plugins")
apiProjectPath := "api"
guiProjectPath := "gui"

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

// ---------------------------------------------------------------------------------------------------------------------
// UTIL
@@ -107,4 +111,8 @@ lazy val getDependencyList = Def.task[List[ModuleID]] {
} else {
updateReport.get.modules.map(m => m.module).toList
}
}
}

// Clears the built GUI dirs on clean
cleanFiles += baseDirectory.value / guiProjectPath.value / "dist"
cleanFiles += baseDirectory.value / "src" / "main" / "resources" / "chatoverflow-gui"
@@ -2,6 +2,7 @@ import java.io.{File, IOException}
import java.nio.file.Files

import sbt.internal.util.ManagedLogger
import sbt.util.{FileFunction, FilesInfo}

/**
* A build utility instance handles build tasks and prints debug information using the managed logger.
@@ -20,6 +21,7 @@ import sbt.internal.util.ManagedLogger
* | -> -> -> build.sbt
* | -> -> -> source etc.
* | -> another plugin source directory (optional)
* | -> gui project
*
*/
class BuildUtility(logger: ManagedLogger) {
@@ -219,6 +221,114 @@ class BuildUtility(logger: ManagedLogger) {
}
}
}

def guiTask(guiProjectPath: String, cacheDir: File): Unit = {
withTaskInfo("BUILD GUI") {
val guiDir = new File(guiProjectPath)
if (!guiDir.exists()) {
logger warn s"GUI not found at $guiProjectPath, ignoring GUI build."
return
}

if (installGuiDeps(guiDir, cacheDir).isEmpty)
return // Early return on failure, error has already been displayed

val outDir = buildGui(guiDir, cacheDir)
if (outDir.isEmpty)
return // Again early return on failure

// Copy built gui into resources, will be included in the classpath on execution of the framework
sbt.IO.copyDirectory(outDir.get, new File("src/main/resources/chatoverflow-gui"))
}
}

/**
* Download the dependencies of the gui using npm.
*
* @param guiDir the directory of the gui.
* @param cacheDir a dir, where sbt can store files for caching in the "install" sub-dir.
* @return None, if a error occurs which will be displayed, otherwise the output directory with the built gui.
*/
private def installGuiDeps(guiDir: File, cacheDir: File): Option[File] = {
// Check buildGui for a explanation, it's almost the same.

val install = FileFunction.cached(new File(cacheDir, "install"), FilesInfo.hash)(_ => {

logger info "Installing GUI dependencies."

val exitCode = new ProcessBuilder("npm", "install")
.inheritIO()
.directory(guiDir)
.start()
.waitFor()

if (exitCode != 0) {
logger error "GUI dependencies couldn't be installed, please check above log for further details."
return None
} else {
logger info "GUI dependencies successfully installed."
Set(new File(guiDir, "node_modules"))
}
})

val input = new File(guiDir, "package.json")
install(Set(input)).headOption
}

/**
* Builds the gui using npm.
*
* @param guiDir the directory of the gui.
* @param cacheDir a dir, where sbt can store files for caching in the "build" sub-dir.
* @return None, if a error occurs which will be displayed, otherwise the output directory with the built gui.
*/
private def buildGui(guiDir: File, cacheDir: File): Option[File] = {
// sbt allows easily to cache our external build using FileFunction.cached
// sbt will only invoke the passed function when at least one of the input files (passed in the last line of this method)
// has been modified. For the gui these input files are all files in the src directory of the gui and the package.json.
// sbt passes these input files to the passed function, but they aren't used, we just instruct npm to build the gui.
// sbt invalidates the cache as well if any of the output files (returned by the passed function) doesn't exist anymore.

val build = FileFunction.cached(new File(cacheDir, "build"), FilesInfo.hash)(_ => {

logger info "Building GUI."

val buildExitCode = new ProcessBuilder("npm", "run", "build")
.inheritIO()
.directory(guiDir)
.start()
.waitFor()

if (buildExitCode != 0) {
logger error "GUI couldn't be built, please check above log for further details."
return None
} else {
logger info "GUI successfully built."
Set(new File(guiDir, "dist"))
}
})


val srcDir = new File(guiDir, "src")
val packageJson = new File(guiDir, "package.json")
val inputs = recursiveFileListing(srcDir) + packageJson

build(inputs).headOption
}

/**
* Creates a file listing with all files including files in any sub-dir.
*
* @param f the directory for which the file listing needs to be created.
* @return the file listing as a set of files.
*/
private def recursiveFileListing(f: File): Set[File] = {
if (f.isDirectory) {
f.listFiles().flatMap(recursiveFileListing).toSet
} else {
Set(f)
}
}
}

object BuildUtility {
@@ -1,7 +1,7 @@
package org.codeoverflow.chatoverflow.ui.web

import org.codeoverflow.chatoverflow.{ChatOverflow, WithLogger}
import org.eclipse.jetty.servlet.ServletHandler.Default404Servlet
import org.eclipse.jetty.util.resource.Resource
import org.eclipse.jetty.webapp.WebAppContext
import org.scalatra.servlet.ScalatraListener

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

server.setHandler(context)

0 comments on commit e0de5ea

Please sign in to comment.
You can’t perform that action at this time.