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

project directory cleanup #111

Merged
merged 3 commits into from
Aug 7, 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
40 changes: 34 additions & 6 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,25 @@
// PROJECT INFORMATION
// ---------------------------------------------------------------------------------------------------------------------

/*
* A brief introduction of the sbt related folder structure:
* root
* | build.sbt
* | plugins.sbt (produced by the fetch task)
* | -> api project (required to build plugins and the framework)
* | -> a plugin source directory
* | -> -> a plugin folder = plugin
* | -> -> -> build.sbt
* | -> -> -> source etc.
* | -> -> another folder = another plugin
* | -> -> -> build.sbt
* | -> -> -> source etc.
* | -> another plugin source directory (optional)
* | -> gui project (build will be skipped, if missing)
* | -> bootstrap launcher (for end-user deployments)
* | -> build project (contains code for all sbt tasks and sbt related things)
*/

name := "ChatOverflow"
version := "0.3"
mainClass := Some("org.codeoverflow.chatoverflow.Launcher")
Expand All @@ -17,6 +36,9 @@ inThisBuild(List(
// Link the bootstrap launcher
lazy val bootstrapProject = project in file("bootstrap")

// not actually used. Just required to say IntelliJ to mark the build directory as a sbt project, otherwise it wouldn't detect it.
lazy val buildProject = project in file("build")

// ---------------------------------------------------------------------------------------------------------------------
// LIBRARY DEPENDENCIES
// ---------------------------------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -66,6 +88,7 @@ libraryDependencies += "com.fazecast" % "jSerialComm" % "[2.0.0,3.0.0)"

// Socket.io
libraryDependencies += "io.socket" % "socket.io-client" % "1.0.0"

// ---------------------------------------------------------------------------------------------------------------------
// PLUGIN FRAMEWORK DEFINITIONS
// ---------------------------------------------------------------------------------------------------------------------
Expand All @@ -92,17 +115,22 @@ pluginTargetFolderNames := List("plugins", s"target/scala-$scalaMajorVersion/plu
apiProjectPath := "api"
guiProjectPath := "gui"

create := PluginCreateWizard(streams.value.log).createPluginTask(pluginFolderNames.value)
fetch := BuildUtility(streams.value.log).fetchPluginsTask(pluginFolderNames.value, pluginBuildFileName.value,

import org.codeoverflow.chatoverflow.build.GUIUtility
import org.codeoverflow.chatoverflow.build.deployment.BootstrapUtility
import org.codeoverflow.chatoverflow.build.plugins.{PluginUtility, PluginCreateWizard}

create := new PluginCreateWizard(streams.value.log).createPluginTask(pluginFolderNames.value)
fetch := new PluginUtility(streams.value.log).fetchPluginsTask(pluginFolderNames.value, pluginBuildFileName.value,
pluginTargetFolderNames.value, apiProjectPath.value)
copy := BuildUtility(streams.value.log).copyPluginsTask(pluginFolderNames.value, pluginTargetFolderNames.value, scalaMajorVersion)
copy := new PluginUtility(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)
deployDev := BootstrapUtility.prepareDevDeploymentTask(streams.value.log, scalaMajorVersion, apiProjectPath.value, libraryDependencies.value.toList)
gui := BuildUtility(streams.value.log).guiTask(guiProjectPath.value, streams.value.cacheDirectory / "gui")
gui := new GUIUtility(streams.value.log).guiTask(guiProjectPath.value, streams.value.cacheDirectory / "gui")

Compile / packageBin := {
BuildUtility(streams.value.log).packageGUITask(guiProjectPath.value, scalaMajorVersion, crossTarget.value)
new GUIUtility(streams.value.log).packageGUITask(guiProjectPath.value, scalaMajorVersion, crossTarget.value)
(Compile / packageBin).value
}

Expand All @@ -127,5 +155,5 @@ lazy val getDependencyList = Def.task[List[ModuleID]] {
}
}

// Clears the built GUI dirs on clean
// Clears the built GUI dir on clean
cleanFiles += baseDirectory.value / guiProjectPath.value / "dist"
5 changes: 5 additions & 0 deletions build/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
name := "chatoverflow-build"
sbtPlugin := true

// JSON lib (Jackson) used for parsing the GUI version in the package.json file
libraryDependencies += "org.json4s" %% "json4s-jackson" % "3.5.2"
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package org.codeoverflow.chatoverflow.build

import java.io.File

import sbt.internal.util.ManagedLogger

object BuildUtils {

/**
* This method can be used to create better readable sbt console output by declaring start and stop of a custom task.
*
* @param taskName the name of the task (use caps for better readability)
* @param logger the sbt logger of the task
* @param task the task itself
*/
def withTaskInfo(taskName: String, logger: ManagedLogger)(task: => Unit): Unit = {

// Info when task started (better log comprehension)
logger info s"Started custom task: $taskName"

// Doing the actual work
task

// Info when task stopped (better log comprehension)
logger info s"Finished custom task: $taskName"
}

/**
* Creates a file listing with all files including files in any sub-dir.
*
* @param dir the directory for which the file listing needs to be created.
* @return the file listing as a set of files.
*/
def getAllDirectoryChilds(dir: File): Set[File] = {
val dirEntries = dir.listFiles()
(dirEntries.filter(_.isFile) ++ dirEntries.filter(_.isDirectory).flatMap(getAllDirectoryChilds)).toSet
}

/**
* Checks whether the current os is windows.
*
* @return true if running on any windows version, false otherwise
*/
def isRunningOnWindows: Boolean = System.getProperty("os.name").toLowerCase().contains("win")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package org.codeoverflow.chatoverflow.build

import java.io.File
import java.util.jar.Manifest

import com.fasterxml.jackson.databind.ObjectMapper
import org.codeoverflow.chatoverflow.build.BuildUtils.withTaskInfo
import sbt.internal.util.ManagedLogger
import sbt.util.{FileFunction, FilesInfo}

import scala.io.Source

class GUIUtility(logger: ManagedLogger) {

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

val packageJson = new File(guiDir, "package.json")

if (!executeNpmCommand(guiDir, cacheDir, Set(packageJson), "install",
() => logger error "GUI dependencies couldn't be installed, please check above log for further details.",
() => new File(guiDir, "node_modules")
)) {
return // early return on failure, error has already been displayed
}

val srcFiles = BuildUtils.getAllDirectoryChilds(new File(guiDir, "src"))
val outDir = new File(guiDir, "dist")

executeNpmCommand(guiDir, cacheDir, srcFiles + packageJson, "run build",
() => logger error "GUI couldn't be built, please check above log for further details.",
() => outDir
)
}
}

/**
* Executes a npm command in the given directory and skips executing the given command
* if no input files have changed and the output file still exists.
*
* @param workDir the directory in which npm should be executed
* @param cacheDir a directory required for caching using sbt
* @param inputs the input files, which will be used for caching.
* If any one of these files change the cache is invalidated.
* @param command the npm command to execute
* @param failed called if npm returned an non-zero exit code
* @param success called if npm returned successfully. Needs to return a file for caching.
* If the returned file doesn't exist the npm command will ignore the cache.
* @return true if npm returned zero as a exit code and false otherwise
*/
private def executeNpmCommand(workDir: File, cacheDir: File, inputs: Set[File], command: String,
failed: () => Unit, success: () => File): Boolean = {
// 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 cachedFn = FileFunction.cached(new File(cacheDir, command), FilesInfo.hash) { _ => {
val exitCode = new ProcessBuilder(getNpmCommand ++ command.split("\\s+"): _*)
.inheritIO()
.directory(workDir)
.start()
.waitFor()

if (exitCode != 0) {
failed()
return false
} else {
Set(success())
}
}
}

cachedFn(inputs)
true
}

private def getNpmCommand: List[String] = {
if (BuildUtils.isRunningOnWindows) {
List("cmd.exe", "/C", "npm")
} else {
List("npm")
}
}

def packageGUITask(guiProjectPath: String, scalaMajorVersion: String, crossTargetDir: File): Unit = {
val dir = new File(guiProjectPath, "dist")
if (!dir.exists()) {
logger info "GUI hasn't been compiled. Won't create a jar for it."
return
}

val files = BuildUtils.getAllDirectoryChilds(dir)

// contains tuples with the actual file as the first value and the name with directory in the jar as the second value
val jarEntries = files.map(file => file -> s"/chatoverflow-gui/${dir.toURI.relativize(file.toURI).toString}")

val guiVersion = getGUIVersion(guiProjectPath).getOrElse("unknown")

sbt.IO.jar(jarEntries, new File(crossTargetDir, s"chatoverflow-gui_$scalaMajorVersion-$guiVersion.jar"), new Manifest())
}

private def getGUIVersion(guiProjectPath: String): Option[String] = {
val packageJson = new File(s"$guiProjectPath/package.json")
if (!packageJson.exists()) {
logger error "The package.json file of the GUI doesn't exist. Have you cloned the GUI in the correct directory?"
return None
}

val content = Source.fromFile(packageJson)
val version = new ObjectMapper().reader().readTree(content.mkString).get("version").asText()

content.close()

if (version.isEmpty) {
logger warn "The GUI version couldn't be loaded from the package.json."
None
} else {
Option(version)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package org.codeoverflow.chatoverflow.build

import java.io.{BufferedWriter, File, FileWriter, IOException}

import org.codeoverflow.chatoverflow.build.plugins.Plugin
import sbt.librarymanagement.{CrossVersion, ModuleID}

/**
Expand All @@ -15,16 +18,16 @@ import sbt.librarymanagement.{CrossVersion, ModuleID}
class SbtFile(val name: String, val version: String, val plugins: List[Plugin], val apiProjectPath: String,
val defineRoot: Boolean, dependencies: List[ModuleID]) {
/**
* Represents a simple sbt files content and methods to create a new sbt file. Not intended to open/read sbt files.
*
* @param name the name of a sbt project
* @param version the version of a sbt project
*/
* Represents a simple sbt files content and methods to create a new sbt file. Not intended to open/read sbt files.
*
* @param name the name of a sbt project
* @param version the version of a sbt project
*/
def this(name: String, version: String) = this(name, version, List(), "", false, List())

/**
* Represents a simple sbt files content and methods to create a new sbt file. Not intended to open/read sbt files.
*/
* Represents a simple sbt files content and methods to create a new sbt file. Not intended to open/read sbt files.
*/
def this() = this("", "")

/**
Expand All @@ -35,11 +38,11 @@ class SbtFile(val name: String, val version: String, val plugins: List[Plugin],
def this(dependencies: List[ModuleID]) = this("", "", List(), "", false, dependencies)

/**
* Tries to save the sbt files content into a defined directory.
*
* @param pathAndFileName the path of the sbt file (incl. file name)
* @return true, if the save process was successful
*/
* Tries to save the sbt files content into a defined directory.
*
* @param pathAndFileName the path of the sbt file (incl. file name)
* @return true, if the save process was successful
*/
def save(pathAndFileName: String): Boolean = {

val buildFile = new File(pathAndFileName)
Expand All @@ -57,10 +60,10 @@ class SbtFile(val name: String, val version: String, val plugins: List[Plugin],
}

/**
* Returns the string representation of the sbt files content in valid sbt/scala syntax
*
* @return a multiline string with all defined attributes
*/
* Returns the string representation of the sbt files content in valid sbt/scala syntax
*
* @return a multiline string with all defined attributes
*/
override def toString: String = {

val sbtContent = new StringBuilder("// GENERATED FILE USING THE CHAT OVERFLOW PLUGIN FRAMEWORK\n")
Expand Down
Loading