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

Commit f2b1f37

Browse files
authored
Merge pull request #111 from codeoverflow-org/fix/108-project-cleanup
project directory cleanup
2 parents d85d12b + c4057d6 commit f2b1f37

20 files changed

+697
-625
lines changed

build.sbt

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,25 @@
22
// PROJECT INFORMATION
33
// ---------------------------------------------------------------------------------------------------------------------
44

5+
/*
6+
* A brief introduction of the sbt related folder structure:
7+
* root
8+
* | build.sbt
9+
* | plugins.sbt (produced by the fetch task)
10+
* | -> api project (required to build plugins and the framework)
11+
* | -> a plugin source directory
12+
* | -> -> a plugin folder = plugin
13+
* | -> -> -> build.sbt
14+
* | -> -> -> source etc.
15+
* | -> -> another folder = another plugin
16+
* | -> -> -> build.sbt
17+
* | -> -> -> source etc.
18+
* | -> another plugin source directory (optional)
19+
* | -> gui project (build will be skipped, if missing)
20+
* | -> bootstrap launcher (for end-user deployments)
21+
* | -> build project (contains code for all sbt tasks and sbt related things)
22+
*/
23+
524
name := "ChatOverflow"
625
version := "0.3"
726
mainClass := Some("org.codeoverflow.chatoverflow.Launcher")
@@ -17,6 +36,9 @@ inThisBuild(List(
1736
// Link the bootstrap launcher
1837
lazy val bootstrapProject = project in file("bootstrap")
1938

39+
// not actually used. Just required to say IntelliJ to mark the build directory as a sbt project, otherwise it wouldn't detect it.
40+
lazy val buildProject = project in file("build")
41+
2042
// ---------------------------------------------------------------------------------------------------------------------
2143
// LIBRARY DEPENDENCIES
2244
// ---------------------------------------------------------------------------------------------------------------------
@@ -66,6 +88,7 @@ libraryDependencies += "com.fazecast" % "jSerialComm" % "[2.0.0,3.0.0)"
6688

6789
// Socket.io
6890
libraryDependencies += "io.socket" % "socket.io-client" % "1.0.0"
91+
6992
// ---------------------------------------------------------------------------------------------------------------------
7093
// PLUGIN FRAMEWORK DEFINITIONS
7194
// ---------------------------------------------------------------------------------------------------------------------
@@ -92,17 +115,22 @@ pluginTargetFolderNames := List("plugins", s"target/scala-$scalaMajorVersion/plu
92115
apiProjectPath := "api"
93116
guiProjectPath := "gui"
94117

95-
create := PluginCreateWizard(streams.value.log).createPluginTask(pluginFolderNames.value)
96-
fetch := BuildUtility(streams.value.log).fetchPluginsTask(pluginFolderNames.value, pluginBuildFileName.value,
118+
119+
import org.codeoverflow.chatoverflow.build.GUIUtility
120+
import org.codeoverflow.chatoverflow.build.deployment.BootstrapUtility
121+
import org.codeoverflow.chatoverflow.build.plugins.{PluginUtility, PluginCreateWizard}
122+
123+
create := new PluginCreateWizard(streams.value.log).createPluginTask(pluginFolderNames.value)
124+
fetch := new PluginUtility(streams.value.log).fetchPluginsTask(pluginFolderNames.value, pluginBuildFileName.value,
97125
pluginTargetFolderNames.value, apiProjectPath.value)
98-
copy := BuildUtility(streams.value.log).copyPluginsTask(pluginFolderNames.value, pluginTargetFolderNames.value, scalaMajorVersion)
126+
copy := new PluginUtility(streams.value.log).copyPluginsTask(pluginFolderNames.value, pluginTargetFolderNames.value, scalaMajorVersion)
99127
bs := BootstrapUtility.bootstrapGenTask(streams.value.log, s"$scalaMajorVersion$scalaMinorVersion", getDependencyList.value)
100128
deploy := BootstrapUtility.prepareDeploymentTask(streams.value.log, scalaMajorVersion)
101129
deployDev := BootstrapUtility.prepareDevDeploymentTask(streams.value.log, scalaMajorVersion, apiProjectPath.value, libraryDependencies.value.toList)
102-
gui := BuildUtility(streams.value.log).guiTask(guiProjectPath.value, streams.value.cacheDirectory / "gui")
130+
gui := new GUIUtility(streams.value.log).guiTask(guiProjectPath.value, streams.value.cacheDirectory / "gui")
103131

104132
Compile / packageBin := {
105-
BuildUtility(streams.value.log).packageGUITask(guiProjectPath.value, scalaMajorVersion, crossTarget.value)
133+
new GUIUtility(streams.value.log).packageGUITask(guiProjectPath.value, scalaMajorVersion, crossTarget.value)
106134
(Compile / packageBin).value
107135
}
108136

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

130-
// Clears the built GUI dirs on clean
158+
// Clears the built GUI dir on clean
131159
cleanFiles += baseDirectory.value / guiProjectPath.value / "dist"

build/build.sbt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
name := "chatoverflow-build"
2+
sbtPlugin := true
3+
4+
// JSON lib (Jackson) used for parsing the GUI version in the package.json file
5+
libraryDependencies += "org.json4s" %% "json4s-jackson" % "3.5.2"
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package org.codeoverflow.chatoverflow.build
2+
3+
import java.io.File
4+
5+
import sbt.internal.util.ManagedLogger
6+
7+
object BuildUtils {
8+
9+
/**
10+
* This method can be used to create better readable sbt console output by declaring start and stop of a custom task.
11+
*
12+
* @param taskName the name of the task (use caps for better readability)
13+
* @param logger the sbt logger of the task
14+
* @param task the task itself
15+
*/
16+
def withTaskInfo(taskName: String, logger: ManagedLogger)(task: => Unit): Unit = {
17+
18+
// Info when task started (better log comprehension)
19+
logger info s"Started custom task: $taskName"
20+
21+
// Doing the actual work
22+
task
23+
24+
// Info when task stopped (better log comprehension)
25+
logger info s"Finished custom task: $taskName"
26+
}
27+
28+
/**
29+
* Creates a file listing with all files including files in any sub-dir.
30+
*
31+
* @param dir the directory for which the file listing needs to be created.
32+
* @return the file listing as a set of files.
33+
*/
34+
def getAllDirectoryChilds(dir: File): Set[File] = {
35+
val dirEntries = dir.listFiles()
36+
(dirEntries.filter(_.isFile) ++ dirEntries.filter(_.isDirectory).flatMap(getAllDirectoryChilds)).toSet
37+
}
38+
39+
/**
40+
* Checks whether the current os is windows.
41+
*
42+
* @return true if running on any windows version, false otherwise
43+
*/
44+
def isRunningOnWindows: Boolean = System.getProperty("os.name").toLowerCase().contains("win")
45+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package org.codeoverflow.chatoverflow.build
2+
3+
import java.io.File
4+
import java.util.jar.Manifest
5+
6+
import com.fasterxml.jackson.databind.ObjectMapper
7+
import org.codeoverflow.chatoverflow.build.BuildUtils.withTaskInfo
8+
import sbt.internal.util.ManagedLogger
9+
import sbt.util.{FileFunction, FilesInfo}
10+
11+
import scala.io.Source
12+
13+
class GUIUtility(logger: ManagedLogger) {
14+
15+
def guiTask(guiProjectPath: String, cacheDir: File): Unit = {
16+
withTaskInfo("BUILD GUI", logger) {
17+
val guiDir = new File(guiProjectPath)
18+
if (!guiDir.exists()) {
19+
logger warn s"GUI not found at $guiProjectPath, ignoring GUI build."
20+
return
21+
}
22+
23+
val packageJson = new File(guiDir, "package.json")
24+
25+
if (!executeNpmCommand(guiDir, cacheDir, Set(packageJson), "install",
26+
() => logger error "GUI dependencies couldn't be installed, please check above log for further details.",
27+
() => new File(guiDir, "node_modules")
28+
)) {
29+
return // early return on failure, error has already been displayed
30+
}
31+
32+
val srcFiles = BuildUtils.getAllDirectoryChilds(new File(guiDir, "src"))
33+
val outDir = new File(guiDir, "dist")
34+
35+
executeNpmCommand(guiDir, cacheDir, srcFiles + packageJson, "run build",
36+
() => logger error "GUI couldn't be built, please check above log for further details.",
37+
() => outDir
38+
)
39+
}
40+
}
41+
42+
/**
43+
* Executes a npm command in the given directory and skips executing the given command
44+
* if no input files have changed and the output file still exists.
45+
*
46+
* @param workDir the directory in which npm should be executed
47+
* @param cacheDir a directory required for caching using sbt
48+
* @param inputs the input files, which will be used for caching.
49+
* If any one of these files change the cache is invalidated.
50+
* @param command the npm command to execute
51+
* @param failed called if npm returned an non-zero exit code
52+
* @param success called if npm returned successfully. Needs to return a file for caching.
53+
* If the returned file doesn't exist the npm command will ignore the cache.
54+
* @return true if npm returned zero as a exit code and false otherwise
55+
*/
56+
private def executeNpmCommand(workDir: File, cacheDir: File, inputs: Set[File], command: String,
57+
failed: () => Unit, success: () => File): Boolean = {
58+
// sbt allows easily to cache our external build using FileFunction.cached
59+
// sbt will only invoke the passed function when at least one of the input files (passed in the last line of this method)
60+
// has been modified. For the gui these input files are all files in the src directory of the gui and the package.json.
61+
// sbt passes these input files to the passed function, but they aren't used, we just instruct npm to build the gui.
62+
// sbt invalidates the cache as well if any of the output files (returned by the passed function) doesn't exist anymore.
63+
val cachedFn = FileFunction.cached(new File(cacheDir, command), FilesInfo.hash) { _ => {
64+
val exitCode = new ProcessBuilder(getNpmCommand ++ command.split("\\s+"): _*)
65+
.inheritIO()
66+
.directory(workDir)
67+
.start()
68+
.waitFor()
69+
70+
if (exitCode != 0) {
71+
failed()
72+
return false
73+
} else {
74+
Set(success())
75+
}
76+
}
77+
}
78+
79+
cachedFn(inputs)
80+
true
81+
}
82+
83+
private def getNpmCommand: List[String] = {
84+
if (BuildUtils.isRunningOnWindows) {
85+
List("cmd.exe", "/C", "npm")
86+
} else {
87+
List("npm")
88+
}
89+
}
90+
91+
def packageGUITask(guiProjectPath: String, scalaMajorVersion: String, crossTargetDir: File): Unit = {
92+
val dir = new File(guiProjectPath, "dist")
93+
if (!dir.exists()) {
94+
logger info "GUI hasn't been compiled. Won't create a jar for it."
95+
return
96+
}
97+
98+
val files = BuildUtils.getAllDirectoryChilds(dir)
99+
100+
// contains tuples with the actual file as the first value and the name with directory in the jar as the second value
101+
val jarEntries = files.map(file => file -> s"/chatoverflow-gui/${dir.toURI.relativize(file.toURI).toString}")
102+
103+
val guiVersion = getGUIVersion(guiProjectPath).getOrElse("unknown")
104+
105+
sbt.IO.jar(jarEntries, new File(crossTargetDir, s"chatoverflow-gui_$scalaMajorVersion-$guiVersion.jar"), new Manifest())
106+
}
107+
108+
private def getGUIVersion(guiProjectPath: String): Option[String] = {
109+
val packageJson = new File(s"$guiProjectPath/package.json")
110+
if (!packageJson.exists()) {
111+
logger error "The package.json file of the GUI doesn't exist. Have you cloned the GUI in the correct directory?"
112+
return None
113+
}
114+
115+
val content = Source.fromFile(packageJson)
116+
val version = new ObjectMapper().reader().readTree(content.mkString).get("version").asText()
117+
118+
content.close()
119+
120+
if (version.isEmpty) {
121+
logger warn "The GUI version couldn't be loaded from the package.json."
122+
None
123+
} else {
124+
Option(version)
125+
}
126+
}
127+
}

project/SbtFile.scala renamed to build/src/main/scala/org/codeoverflow/chatoverflow/build/SbtFile.scala

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
package org.codeoverflow.chatoverflow.build
2+
13
import java.io.{BufferedWriter, File, FileWriter, IOException}
24

5+
import org.codeoverflow.chatoverflow.build.plugins.Plugin
36
import sbt.librarymanagement.{CrossVersion, ModuleID}
47

58
/**
@@ -15,16 +18,16 @@ import sbt.librarymanagement.{CrossVersion, ModuleID}
1518
class SbtFile(val name: String, val version: String, val plugins: List[Plugin], val apiProjectPath: String,
1619
val defineRoot: Boolean, dependencies: List[ModuleID]) {
1720
/**
18-
* Represents a simple sbt files content and methods to create a new sbt file. Not intended to open/read sbt files.
19-
*
20-
* @param name the name of a sbt project
21-
* @param version the version of a sbt project
22-
*/
21+
* Represents a simple sbt files content and methods to create a new sbt file. Not intended to open/read sbt files.
22+
*
23+
* @param name the name of a sbt project
24+
* @param version the version of a sbt project
25+
*/
2326
def this(name: String, version: String) = this(name, version, List(), "", false, List())
2427

2528
/**
26-
* Represents a simple sbt files content and methods to create a new sbt file. Not intended to open/read sbt files.
27-
*/
29+
* Represents a simple sbt files content and methods to create a new sbt file. Not intended to open/read sbt files.
30+
*/
2831
def this() = this("", "")
2932

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

3740
/**
38-
* Tries to save the sbt files content into a defined directory.
39-
*
40-
* @param pathAndFileName the path of the sbt file (incl. file name)
41-
* @return true, if the save process was successful
42-
*/
41+
* Tries to save the sbt files content into a defined directory.
42+
*
43+
* @param pathAndFileName the path of the sbt file (incl. file name)
44+
* @return true, if the save process was successful
45+
*/
4346
def save(pathAndFileName: String): Boolean = {
4447

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

5962
/**
60-
* Returns the string representation of the sbt files content in valid sbt/scala syntax
61-
*
62-
* @return a multiline string with all defined attributes
63-
*/
63+
* Returns the string representation of the sbt files content in valid sbt/scala syntax
64+
*
65+
* @return a multiline string with all defined attributes
66+
*/
6467
override def toString: String = {
6568

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

0 commit comments

Comments
 (0)