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

Add gui to the build process #17

Merged
merged 5 commits into from
Jun 16, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,7 @@ project/plugins/project/
/wiki/

# Plugin Data
data/
data/

# Built gui
/src/main/resources/chatoverflow-gui
11 changes: 11 additions & 0 deletions .idea/runConfigurations/Build_GUI__sbt_gui_.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,21 @@
advanced_run:
sbt clean
sbt compile
sbt gui
sbt fetch
sbt reload
sbt version
sbt package copy

simple_run:
sbt compile
sbt gui
sbt package copy

bootstrap_deploy:
sbt compile
sbt clean
sbt compile
sbt gui
sbt package copy
sbt bs "project bootstrapProject" assembly
sbt deploy
Expand Down
10 changes: 9 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
118 changes: 118 additions & 0 deletions project/BuildUtility.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -20,6 +21,7 @@ import sbt.internal.util.ManagedLogger
* | -> -> -> build.sbt
* | -> -> -> source etc.
* | -> another plugin source directory (optional)
* | -> gui project
*
*/
class BuildUtility(logger: ManagedLogger) {
Expand Down Expand Up @@ -219,6 +221,122 @@ 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(getNpmCommand :+ "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(getNpmCommand :+ "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
}

private def getNpmCommand: List[String] = {
if (System.getProperty("os.name").toLowerCase().contains("win")) {
List("cmd.exe", "/C", "npm")
} else {
List("npm")
}
}

/**
* 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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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)

Expand Down