From e0de5ea3383543a0823aa7c7a2aa7156c0e1aca5 Mon Sep 17 00:00:00 2001 From: Daniel Huber Date: Fri, 14 Jun 2019 18:37:47 +0200 Subject: [PATCH 1/5] Bundle gui with main framework --- .gitignore | 5 +- build.sbt | 10 +- project/BuildUtility.scala | 110 ++++++++++++++++++ .../chatoverflow/ui/web/Server.scala | 5 +- 4 files changed, 125 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 53ac882c..0e4abe60 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,7 @@ project/plugins/project/ /wiki/ # Plugin Data -data/ \ No newline at end of file +data/ + +# Built gui +/src/main/resources/chatoverflow-gui \ No newline at end of file diff --git a/build.sbt b/build.sbt index 28ea907c..bfb3852a 100644 --- a/build.sbt +++ b/build.sbt @@ -73,6 +73,7 @@ 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.") @@ -80,11 +81,13 @@ lazy val fetch = TaskKey[Unit]("fetch", "Searches for plugins in plugin director 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, @@ -92,6 +95,7 @@ fetch := BuildUtility(streams.value.log).fetchPluginsTask(pluginFolderNames.valu 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 } -} \ No newline at end of file +} + +// Clears the built GUI dirs on clean +cleanFiles += baseDirectory.value / guiProjectPath.value / "dist" +cleanFiles += baseDirectory.value / "src" / "main" / "resources" / "chatoverflow-gui" \ No newline at end of file diff --git a/project/BuildUtility.scala b/project/BuildUtility.scala index 21f23b5e..e4e6ea3b 100644 --- a/project/BuildUtility.scala +++ b/project/BuildUtility.scala @@ -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 { diff --git a/src/main/scala/org/codeoverflow/chatoverflow/ui/web/Server.scala b/src/main/scala/org/codeoverflow/chatoverflow/ui/web/Server.scala index 5a89cab0..d8d65a1e 100644 --- a/src/main/scala/org/codeoverflow/chatoverflow/ui/web/Server.scala +++ b/src/main/scala/org/codeoverflow/chatoverflow/ui/web/Server.scala @@ -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) From 55a476b0cbbeda1d191a2916795dc7902818faa5 Mon Sep 17 00:00:00 2001 From: Daniel Huber Date: Fri, 14 Jun 2019 18:38:18 +0200 Subject: [PATCH 2/5] Include gui build in run configs and makefile --- .../_Advanced__Full_Reload_and_Run_ChatOverflow.xml | 1 + .../_Deploy__Generate_Bootstrap_Launcher_and_deploy.xml | 1 + .../_Simple__Rebuild_plugins_and_Run_ChatOverflow.xml | 1 + Makefile | 5 ++++- 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.idea/runConfigurations/_Advanced__Full_Reload_and_Run_ChatOverflow.xml b/.idea/runConfigurations/_Advanced__Full_Reload_and_Run_ChatOverflow.xml index 401c9115..c34bbadd 100644 --- a/.idea/runConfigurations/_Advanced__Full_Reload_and_Run_ChatOverflow.xml +++ b/.idea/runConfigurations/_Advanced__Full_Reload_and_Run_ChatOverflow.xml @@ -11,6 +11,7 @@