This repository was archived by the owner on Aug 18, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 12
/
Copy pathBuildUtility.scala
378 lines (308 loc) · 13.7 KB
/
BuildUtility.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
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.
*
* @param logger The current logger instance. Usually: {{{streams.value.log}}}
* @note A brief introduction of the folder structure:
* root
* | build.sbt
* | plugins.sbt
* | -> api project
* | -> 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
*
*/
class BuildUtility(logger: ManagedLogger) {
/**
* Searches for plugins in plugin directories, builds the plugin build file.
*
* @param pluginSourceFolderNames All folder names, containing plugin source code. Defined in build.sbt.
* @param pluginBuildFileName The generated sbt build file, containing all sub project references. Defined in build.sbt.
*/
def fetchPluginsTask(pluginSourceFolderNames: List[String], pluginBuildFileName: String,
pluginTargetFolderNames: List[String], apiProjectPath: String): Unit = {
withTaskInfo("FETCH PLUGINS") {
// Check validity of plugin source folders
pluginSourceFolderNames.foreach(
name => if (!Plugin.isSourceFolderNameValid(name, pluginTargetFolderNames))
logger warn s"Plugin folder '$name' is reserved for build plugin jar files!"
)
val allPlugins = getAllPlugins(pluginSourceFolderNames)
// Create a sbt file with all plugin dependencies (sub projects)
val sbtFile = new SbtFile("", "", allPlugins, apiProjectPath, defineRoot = true)
if (sbtFile.save(pluginBuildFileName)) {
logger info s"Successfully updated plugin file at '$pluginBuildFileName'."
} else {
logger error s"Unable to write plugin file at '$pluginBuildFileName'."
}
}
}
private def getAllPlugins(pluginSourceFolderNames: List[String]): List[Plugin] = {
// Get all plugins (= folders) in all plugin source directories. Flatten that list of lists
(for (pluginSourceFolderName <- pluginSourceFolderNames) yield {
logger info s"Fetching plugins from folder '$pluginSourceFolderName'."
if (!Plugin.sourceFolderExists(pluginSourceFolderName)) {
logger error s"Plugin directory '$pluginSourceFolderName' does not exist."
List[Plugin]()
} else {
val plugins = Plugin.getPlugins(pluginSourceFolderName)
logger info s"Found ${plugins.length} plugins."
plugins
}
}).flatten
}
/**
* Copies all packaged plugin jars to the target plugin folder.
*
* @param pluginSourceFolderNames All folder names, containing plugin source code. Defined in build.sbt.
* @param pluginTargetFolderNames The generated sbt build file, containing all sub project references. Defined in build.sbt.
* @param scalaMajorVersion The major part (x.x) of the scala version string. Defined in build.sbt.
*/
def copyPluginsTask(pluginSourceFolderNames: List[String], pluginTargetFolderNames: List[String], scalaMajorVersion: String): Unit = {
withTaskInfo("COPY PLUGINS") {
// Get all plugins first
val allPlugins = getAllPlugins(pluginSourceFolderNames)
// Now get all jar files in the target folders of the plugins, warn if not found
val allJarFiles = (for (plugin <- allPlugins) yield {
val jarFiles = plugin.getBuildPluginFiles(scalaMajorVersion)
if (jarFiles.isEmpty) {
logger warn s"Target jar file(s) of plugin '${plugin.name}' does not exist. Use 'sbt package' first."
Seq[File]()
} else {
jarFiles.foreach(jar => logger info s"Found archive: '${jar.getName}'.")
jarFiles
}
}).flatten
// Copy all jars to the target folders
for (pluginTargetFolderName <- pluginTargetFolderNames) copyPlugins(allJarFiles, pluginTargetFolderName)
}
}
private def copyPlugins(allJarFiles: List[File], pluginTargetFolderName: String): Unit = {
val pluginTargetFolder = new File(pluginTargetFolderName)
// Check target folder existence
if (!pluginTargetFolder.exists() && !pluginTargetFolder.mkdir()) {
logger warn s"Unable to create or find plugin folder '${pluginTargetFolder.getAbsolutePath}'."
} else {
logger info s"Found plugin folder '${pluginTargetFolder.getPath}'."
}
// Clean first
// TODO: Should this be cleaned? How to handle external plugins? Separate folder?
for (jarFile <- pluginTargetFolder.listFiles().filter(_.getName.endsWith(".jar"))) {
try {
jarFile.delete()
logger info s"Deleted plugin '${jarFile.getName}' from target."
} catch {
case e: IOException => logger warn s"Unable to delete plugin '${jarFile.getAbsolutePath}' from target. Error: ${e.getMessage}."
}
}
// Copy jars
var successCounter = 0
for (jarFile <- allJarFiles) {
try {
Files.copy(jarFile.toPath, new File(pluginTargetFolder, jarFile.getName).toPath)
logger info s"Copied plugin '${jarFile.getName}'."
successCounter = successCounter + 1
} catch {
case e: IOException => logger warn s"Unable to copy plugin '${jarFile.getName}'. Error: ${e.getMessage}."
}
}
logger info s"Successfully copied $successCounter / ${allJarFiles.length} plugins to target '${pluginTargetFolder.getPath}'!"
}
/**
* Creates a new plugin. Interactive command using the console.
*
* @param pluginFolderNames All folder names, containing plugin source code. Defined in build.sbt.
*/
def createPluginTask(pluginFolderNames: List[String]): Unit = {
withTaskInfo("CREATE PLUGIN") {
// Plugin folders have to be defined in the build.sbt file first
if (pluginFolderNames.isEmpty) {
println("Before creating a new plugin, please define at least one plugin source folder in the build.sbt file.")
logger warn "Aborting task without plugin creation."
} else {
println("Welcome to the \"create plugin\"-wizard. Please specify name, version and plugin source folder.")
// Plugin name
val name = BuildUtility.askForInput(
"Please specify the name of the plugin. Do only use characters allowed for directories and files of your OS.",
"Plugin name",
repeatIfEmpty = true
)
// Plugin version (default: 0.1)
var version = BuildUtility.askForInput(
"Please specify the version of the plugin. Just press enter for version \"0.1\".",
"Plugin version",
repeatIfEmpty = false
)
if (version == "") version = "0.1"
// Plugin folder name (must be defined in build.sbt)
var pluginFolderName = ""
while (!pluginFolderNames.contains(pluginFolderName)) {
pluginFolderName = BuildUtility.askForInput(
s"Please specify the plugin source directory. Available directories: ${pluginFolderNames.mkString("[", ", ", "]")}",
"Plugin source directory",
repeatIfEmpty = true
)
}
createPlugin(name, version, pluginFolderName)
}
}
}
private def withTaskInfo(taskName: String)(task: Unit): Unit = BuildUtility.withTaskInfo(taskName, logger)(task)
private def createPlugin(name: String, version: String, pluginFolderName: String): Unit = {
logger info s"Trying to create plugin $name (version $version) at plugin folder $pluginFolderName."
val pluginFolder = new File(pluginFolderName)
if (!pluginFolder.exists()) {
logger error "Plugin source folder does not exist. Aborting task without plugin creation."
} else {
val plugin = new Plugin(pluginFolderName, name)
if (!plugin.createPluginFolder()) {
logger error "Plugin does already exist. Aborting task without plugin creation."
} else {
logger info s"Created plugin '$name'"
if (plugin.createSrcFolder()) {
logger info "Successfully created source folder."
} else {
logger warn "Unable to create source folder."
}
if (plugin.createSbtFile(version)) {
logger info "Successfully created plugins sbt file."
} else {
logger warn "Unable to create plugins sbt file."
}
}
}
}
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 {
def apply(logger: ManagedLogger): BuildUtility = new BuildUtility(logger)
private def askForInput(information: String, description: String, repeatIfEmpty: Boolean): String = {
println(information)
print(s"$description > ")
var input = scala.io.Source.fromInputStream(System.in).bufferedReader().readLine()
println("")
if (input == "" && repeatIfEmpty)
input = askForInput(information, description, repeatIfEmpty)
input
}
/**
* 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"
}
}