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

Rework plugin structure to replace pluggable with a xml file #91

Merged
merged 11 commits into from
Jul 21, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Generate plugin.xml metadata file in the sbt create task.
  • Loading branch information
hlxid committed Jul 16, 2019
commit 71ce0490aa2f0a1e8cae90a5b79ee36910308b82
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ pluginTargetFolderNames := List("plugins", s"target/scala-$scalaMajorVersion/plu
apiProjectPath := "api"
guiProjectPath := "gui"

create := BuildUtility(streams.value.log).createPluginTask(pluginFolderNames.value)
create := PluginCreateWizard(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)
Expand Down
103 changes: 6 additions & 97 deletions project/BuildUtility.scala
Original file line number Diff line number Diff line change
Expand Up @@ -131,86 +131,6 @@ class BuildUtility(logger: ManagedLogger) {
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)
Expand All @@ -234,13 +154,13 @@ class BuildUtility(logger: ManagedLogger) {
/**
* Download the dependencies of the gui using npm.
*
* @param guiDir the directory of the gui.
* @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."
Expand All @@ -266,8 +186,8 @@ class BuildUtility(logger: ManagedLogger) {

/**
* Builds the gui using npm.
*
* @param guiDir the directory of the gui.
*
* @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.
*/
Expand Down Expand Up @@ -326,25 +246,14 @@ class BuildUtility(logger: ManagedLogger) {
Set(f)
}
}

private def withTaskInfo(taskName: String)(task: Unit): Unit = BuildUtility.withTaskInfo(taskName, logger)(task)
}

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.
*
Expand Down
55 changes: 46 additions & 9 deletions project/Plugin.scala
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import java.io.File
import java.io.{File, FileOutputStream}

import scala.xml.PrettyPrinter

/**
* A plugin represents a directory in a plugin source directory. Every plugin has its own build file and source folder.
Expand Down Expand Up @@ -27,19 +29,16 @@ class Plugin(val pluginSourceDirectoryName: String, val name: String) {
}

/**
* Creates the plugin src folder inside of a plugin folder
* Creates the plugin src and resources folder inside of a plugin folder
*
* @note Make sure to create the plugin folder first!
* @return true, if the process was successful
*/
def createSrcFolder(): Boolean = {
if (new File(s"$pluginDirectoryPath/src").mkdir() &&
def createSrcFolders(): Boolean = {
new File(s"$pluginDirectoryPath/src").mkdir() &&
new File(s"$pluginDirectoryPath/src/main").mkdir() &&
new File(s"$pluginDirectoryPath/src/main/scala").mkdir()) {
true
} else {
false
}
new File(s"$pluginDirectoryPath/src/main/scala").mkdir() &&
new File(s"$pluginDirectoryPath/src/main/resources").mkdir()
}

/**
Expand All @@ -55,6 +54,44 @@ class Plugin(val pluginSourceDirectoryName: String, val name: String) {
sbtFile.save(s"$pluginDirectoryPath/$name.sbt")
}

/**
* Generates the plugin.xml file in the resources of the plugin.
*
* @param metadata the metadata for this plugin
* @param author author of this plugin, used by the framework to identify it
*/
def createPluginXMLFile(metadata: PluginMetadata, author: String, version: String): Boolean = {
val xml = <plugin>
<name>
{name}
</name>
<author>
{author}
</author>
<version>
{version}
</version>
<api>
<major>3</major>
<minor>0</minor>
</api>{metadata.toXML}
</plugin>

val file = new FileOutputStream(s"$pluginDirectoryPath/src/main/resources/plugin.xml")
try {
val trimmed = scala.xml.Utility.trim(xml)
val prettyXml = new PrettyPrinter(100, 2).format(trimmed)
file.write(prettyXml.getBytes)
true
} catch {
case e: Throwable =>
e.printStackTrace()
false
} finally {
file.close()
}
}

/**
* Fetches the build plugin jar from the target folder of a given scala version.
*
Expand Down
129 changes: 129 additions & 0 deletions project/PluginCreateWizard.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import java.io.File

import BuildUtility._
import PluginCreateWizard._
import sbt.internal.util.ManagedLogger


class PluginCreateWizard(logger: ManagedLogger) {

/**
* 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", logger) {

// 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."
return
}

println("Welcome to the \"create plugin\"-wizard. Please answer all the following questions to automatically create a plugin.")

// Plugin name
val name = askForInput(
"Please specify the name of the plugin. Do only use characters allowed for directories and files of your OS.",
"Plugin name",
s => s.nonEmpty
)

val author = askForInput(
"Please specify the name of the author of the plugin. Can be a real name or alias.",
"Author name",
s => s.nonEmpty
)

// Plugin version (default: 0.1)
val version = askForInput(
"Please specify the version of the plugin. Just press enter for version \"0.1\".",
"Plugin version", "0.1"
)

// Plugin folder name (must be defined in build.sbt)
val pluginFolderName = askForInput(
s"Please specify the plugin source directory. Available directories: ${pluginFolderNames.mkString("[", ", ", "]")}",
"Plugin source directory",
s => s.nonEmpty && pluginFolderNames.contains(s)
)

// Plugin metadata
val description = askForInput("Please specify a optional description of what this plugin does.", "Description")
val licence = askForInput("Please specify the SPDX short identifier of the used license, if any e.g. 'MIT' or 'EPL-2.0'.", "License")
val website = askForInput("Please specify a optional website about you or the plugin", "Website")
val sourceRepo = askForInput("The repository where the code of this plugin is hosted, if published.", "Source repo")
val bugtracker = askForInput("A optional bug tracker or other support website.", "Bug tracker")

val metadata = PluginMetadata(description, licence, website, sourceRepo, bugtracker)

createPlugin(name, author, version, pluginFolderName, metadata)
}
}

private def createPlugin(name: String, author: String, version: String, pluginFolderName: String, metadata: PluginMetadata): 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.createSrcFolders()) {
logger info "Successfully created source folder."

if (plugin.createPluginXMLFile(metadata, author, version)) {
logger info "Successfully created plugin.xml containing metadata."
} else {
logger warn "Unable to create plugin.xml containing metadata in plugin resources."
}
} 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."
}
}
}
}
}

object PluginCreateWizard {

def apply(logger: ManagedLogger): PluginCreateWizard = new PluginCreateWizard(logger)

private def askForInput(information: String, description: String, validate: String => Boolean): String = {
println(information)
print(s"$description > ")

val input = scala.io.Source.fromInputStream(System.in).bufferedReader().readLine()
println("")

if (validate(input))
input
else
askForInput(information, description, validate)
}

private def askForInput(information: String, description: String): String = askForInput(information, description, _ => true)

private def askForInput(information: String, description: String, default: String): String = {
val input = askForInput(information, description, _ => true)

if (input.isEmpty) default
else input
}
}
39 changes: 39 additions & 0 deletions project/PluginMetadata.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import scala.xml.Node

/**
* Equivalent to the one in the main framework, just with a toXML method, rather than a fromXML method and an all string constructor.
* The website, sourceRepo and bug tracker strings have to be checked with the validateURL method in the companion first,
* because this class just assumes that those are valid urls.
* If those aren't the framework will be unable to load these urls, but the actual plugin will work fine.
*
* Check the metadata class in the framework for more information about these properties.
*/
case class PluginMetadata(description: String,
license: String,
website: String,
sourceRepo: String,
bugtracker: String) {

/**
* Converts the metadata into a scala xml object. Empty properties are ignored.
*
* @return the metadata as a scala xml node.
* It has a root tag with the name plugin and all metadata properties are tags in this root tag.
*/
def toXML: List[Node] = {
// Map of tag names to variables. Add new vars here and in the constructor.
Map(
"description" -> description,
"licence" -> license,
"website" -> website,
"sourceRepo" -> sourceRepo,
"bugtracker" -> bugtracker
).filter(_._2.nonEmpty) // filters not specified options
.map(entry => {
// just dummy tag name, replaced afterwards
<value>
{entry._2}
</value>.copy(label = entry._1) // update the tag name with the correct one
}).toList
}
}
Loading