mirror of
https://github.com/ethauvin/kobalt.git
synced 2025-04-26 08:27:12 -07:00
Merge branch 'master' of github.com:cbeust/kobalt
This commit is contained in:
commit
8acd8b8fce
133 changed files with 861 additions and 125 deletions
13
modules/kobalt-plugin-api/kobalt-plugin-api.iml
Normal file
13
modules/kobalt-plugin-api/kobalt-plugin-api.iml
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/kotlin" isTestSource="false" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" name="kobalt (Compile)" level="project" />
|
||||
<orderEntry type="library" name="kobalt-plugin-api (Compile)" level="project" />
|
||||
<orderEntry type="library" scope="TEST" name="kobalt-plugin-api (Test)" level="project" />
|
||||
</component>
|
||||
</module>
|
|
@ -0,0 +1,60 @@
|
|||
package com.beust.kobalt
|
||||
|
||||
import com.beust.jcommander.Parameter
|
||||
|
||||
class Args {
|
||||
@Parameter
|
||||
var targets: List<String> = arrayListOf()
|
||||
|
||||
@Parameter(names = arrayOf("-bf", "--buildFile"), description = "The build file")
|
||||
var buildFile: String? = null
|
||||
|
||||
@Parameter(names = arrayOf("--checkVersions"), description = "Check if there are any newer versions of the " +
|
||||
"dependencies")
|
||||
var checkVersions = false
|
||||
|
||||
@Parameter(names = arrayOf("--client"))
|
||||
var client: Boolean = false
|
||||
|
||||
@Parameter(names = arrayOf("--dev"), description = "Turn of dev mode, resulting in a more verbose log output")
|
||||
var dev: Boolean = false
|
||||
|
||||
@Parameter(names = arrayOf("--download"), description = "Force a download from the downloadUrl in the wrapper")
|
||||
var download: Boolean = false
|
||||
|
||||
@Parameter(names = arrayOf("--dryRun"), description = "Display all the tasks that will get run without " +
|
||||
"actually running them")
|
||||
var dryRun: Boolean = false
|
||||
|
||||
@Parameter(names = arrayOf("--help", "--usage"), description = "Display the help")
|
||||
var usage: Boolean = false
|
||||
|
||||
@Parameter(names = arrayOf("-i", "--init"), description = "Create a build file based on the current project")
|
||||
var init: Boolean = false
|
||||
|
||||
@Parameter(names = arrayOf("--log"), description = "Define the log level (1-3)")
|
||||
var log: Int = 1
|
||||
|
||||
companion object {
|
||||
const val DEFAULT_SERVER_PORT = 1234
|
||||
}
|
||||
|
||||
@Parameter(names = arrayOf("--port"), description = "Port, if --server was specified")
|
||||
var port: Int = DEFAULT_SERVER_PORT
|
||||
|
||||
@Parameter(names = arrayOf("--profiles"), description = "Comma-separated list of profiles to run")
|
||||
var profiles: String? = null
|
||||
|
||||
@Parameter(names = arrayOf("--resolve"), description = "Resolve the given dependency and display its tree")
|
||||
var dependency: String? = null
|
||||
|
||||
@Parameter(names = arrayOf("--server"), description = "Run in server mode")
|
||||
var serverMode: Boolean = false
|
||||
|
||||
@Parameter(names = arrayOf("--tasks"), description = "Display the tasks available for this build")
|
||||
var tasks: Boolean = false
|
||||
|
||||
@Parameter(names = arrayOf("--update"), description = "Update to the latest version of Kobalt")
|
||||
var update: Boolean = false
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
package com.beust.kobalt
|
||||
|
||||
import com.beust.kobalt.misc.log
|
||||
import java.util.*
|
||||
|
||||
|
||||
class AsciiArt {
|
||||
companion object {
|
||||
private val BANNERS = arrayOf(
|
||||
" __ __ __ __ __ \n" +
|
||||
" / //_/ ____ / /_ ____ _ / / / /_\n" +
|
||||
" / ,< / __ \\ / __ \\ / __ `/ / / / __/\n" +
|
||||
" / /| | / /_/ / / /_/ // /_/ / / / / /_ \n" +
|
||||
" /_/ |_| \\____/ /_.___/ \\__,_/ /_/ \\__/ ",
|
||||
|
||||
" _ __ _ _ _ \n" +
|
||||
" | |/ / ___ | |__ __ _ | | | |_ \n" +
|
||||
" | ' / / _ \\ | '_ \\ / _` | | | | __|\n" +
|
||||
" | . \\ | (_) | | |_) | | (_| | | | | |_ \n" +
|
||||
" |_|\\_\\ \\___/ |_.__/ \\__,_| |_| \\__| "
|
||||
)
|
||||
|
||||
val banner : String get() = BANNERS[Random().nextInt(BANNERS.size)]
|
||||
|
||||
// fun box(s: String) : List<String> = box(listOf(s))
|
||||
|
||||
val horizontalSingleLine = "\u2500\u2500\u2500\u2500\u2500"
|
||||
val horizontalDoubleLine = "\u2550\u2550\u2550\u2550\u2550"
|
||||
|
||||
// fun horizontalLine(n: Int) = StringBuffer().apply {
|
||||
// repeat(n, { append("\u2500") })
|
||||
// }.toString()
|
||||
|
||||
fun box(strings: List<String>) : List<String> {
|
||||
val ul = "\u2554"
|
||||
val ur = "\u2557"
|
||||
val h = "\u2550"
|
||||
val v = "\u2551"
|
||||
val bl = "\u255a"
|
||||
val br = "\u255d"
|
||||
|
||||
fun r(n: Int, w: String) : String {
|
||||
with(StringBuffer()) {
|
||||
repeat(n, { append(w) })
|
||||
return toString()
|
||||
}
|
||||
}
|
||||
|
||||
val maxString: String = strings.maxBy { it.length } ?: ""
|
||||
val max = maxString.length
|
||||
val result = arrayListOf(ul + r(max + 2, h) + ur)
|
||||
result.addAll(strings.map { "$v ${center(it, max - 2)} $v" })
|
||||
result.add(bl + r(max + 2, h) + br)
|
||||
return result
|
||||
}
|
||||
|
||||
private fun fill(n: Int) = StringBuffer().apply { repeat(n, { append(" ")})}.toString()
|
||||
|
||||
val defaultLog : (s: String) -> Unit = { log(1, " $it") }
|
||||
|
||||
fun logBox(strings: List<String>, print: (String) -> Unit = defaultLog) {
|
||||
box(strings).forEach {
|
||||
print(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun logBox(s: String, print: (String) -> Unit = defaultLog) {
|
||||
logBox(listOf(s), print)
|
||||
}
|
||||
|
||||
fun center(s: String, width: Int) : String {
|
||||
val diff = width - s.length
|
||||
val spaces = diff / 2 + 1
|
||||
return fill(spaces) + s + fill(spaces + if (diff % 2 == 1) 1 else 0)
|
||||
}
|
||||
|
||||
const val RESET = "\u001B[0m"
|
||||
const val BLACK = "\u001B[30m"
|
||||
const val RED = "\u001B[31m"
|
||||
const val GREEN = "\u001B[32m"
|
||||
const val YELLOW = "\u001B[33m";
|
||||
const val BLUE = "\u001B[34m"
|
||||
const val PURPLE = "\u001B[35m"
|
||||
const val CYAN = "\u001B[36m"
|
||||
const val WHITE = "\u001B[37m"
|
||||
|
||||
private fun wrap(s: String, color: String) = color + s + RESET
|
||||
private fun blue(s: String) = wrap(s, BLUE)
|
||||
private fun red(s: String) = wrap(s, RED)
|
||||
private fun yellow(s: String) = wrap(s, YELLOW)
|
||||
|
||||
fun taskColor(s: String) = s
|
||||
fun errorColor(s: String) = red(s)
|
||||
fun warnColor(s: String) = red(s)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.beust.kobalt
|
||||
|
||||
import com.beust.kobalt.api.IPlugin
|
||||
import com.beust.kobalt.api.PluginTask
|
||||
import com.beust.kobalt.api.Project
|
||||
|
||||
public abstract class BasePluginTask(override val plugin: IPlugin,
|
||||
override val name: String,
|
||||
override val doc: String,
|
||||
override val project: Project)
|
||||
: PluginTask()
|
|
@ -0,0 +1,61 @@
|
|||
package com.beust.kobalt
|
||||
|
||||
import com.beust.kobalt.api.IClasspathDependency
|
||||
import com.beust.kobalt.api.Kobalt
|
||||
import com.beust.kobalt.api.annotation.Directive
|
||||
import com.beust.kobalt.maven.DepFactory
|
||||
import com.beust.kobalt.maven.dependency.FileDependency
|
||||
import java.io.File
|
||||
|
||||
@Directive
|
||||
fun homeDir(vararg dirs: String) : String = SystemProperties.homeDir +
|
||||
File.separator + dirs.toArrayList().joinToString(File.separator)
|
||||
|
||||
@Directive
|
||||
fun localMavenRepo() = homeDir(".m2" + File.separator + "repository/")
|
||||
|
||||
@Directive
|
||||
fun file(file: String) : String = FileDependency.PREFIX_FILE + file
|
||||
|
||||
@Directive
|
||||
fun plugins(vararg dependency : IClasspathDependency) {
|
||||
dependency.forEach { Plugins.addDynamicPlugin(it) }
|
||||
}
|
||||
|
||||
@Directive
|
||||
fun plugins(vararg dependencies : String) {
|
||||
val factory = Kobalt.INJECTOR.getInstance(DepFactory::class.java)
|
||||
dependencies.forEach {
|
||||
Plugins.addDynamicPlugin(factory.create(it))
|
||||
}
|
||||
}
|
||||
|
||||
data class HostConfig(var url: String = "", var username: String? = null, var password: String? = null) {
|
||||
fun hasAuth() : Boolean {
|
||||
return (! username.isNullOrBlank()) && (! password.isNullOrBlank())
|
||||
}
|
||||
|
||||
override fun toString() : String {
|
||||
return url + if (username != null) {
|
||||
"username: $username, password: ***"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Directive
|
||||
fun repos(vararg repos : String) {
|
||||
repos.forEach { Kobalt.addRepo(HostConfig(it)) }
|
||||
}
|
||||
|
||||
@Directive
|
||||
fun authRepos(vararg repos : HostConfig) {
|
||||
repos.forEach { Kobalt.addRepo(it) }
|
||||
}
|
||||
|
||||
@Directive
|
||||
fun authRepo(init: HostConfig.() -> Unit) = HostConfig().apply { init() }
|
||||
|
||||
@Directive
|
||||
fun glob(g: String) : IFileSpec.Glob = IFileSpec.Glob(g)
|
|
@ -0,0 +1,14 @@
|
|||
package com.beust.kobalt
|
||||
|
||||
object Constants {
|
||||
val BUILD_FILE_NAME = "Build.kt"
|
||||
val BUILD_FILE_DIRECTORY = "kobalt/src"
|
||||
|
||||
internal val DEFAULT_REPOS = listOf(
|
||||
"http://repo1.maven.org/maven2/",
|
||||
"https://maven-central.storage.googleapis.com/",
|
||||
"https://repository.jboss.org/nexus/content/repositories/root_repository/",
|
||||
"https://jcenter.bintray.com/"
|
||||
)
|
||||
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package com.beust.kobalt
|
||||
|
||||
import com.beust.kobalt.misc.log
|
||||
import java.io.File
|
||||
import java.nio.file.*
|
||||
import java.nio.file.attribute.BasicFileAttributes
|
||||
|
||||
sealed class IFileSpec {
|
||||
abstract fun toFiles(directory: String): List<File>
|
||||
|
||||
class FileSpec(val spec: String) : IFileSpec() {
|
||||
override public fun toFiles(directory: String) = arrayListOf(File(spec))
|
||||
|
||||
override public fun toString() = spec
|
||||
}
|
||||
|
||||
class Glob(val spec: String) : IFileSpec() {
|
||||
override public fun toFiles(directory: String): List<File> {
|
||||
val result = arrayListOf<File>()
|
||||
|
||||
val matcher = FileSystems.getDefault().getPathMatcher("glob:${spec}")
|
||||
Files.walkFileTree(Paths.get(directory), object : SimpleFileVisitor<Path>() {
|
||||
override public fun visitFile(path: Path, attrs: BasicFileAttributes): FileVisitResult {
|
||||
val rel = Paths.get(directory).relativize(path)
|
||||
if (matcher.matches(rel)) {
|
||||
log(3, "Adding ${rel.toFile()}")
|
||||
result.add(rel.toFile())
|
||||
}
|
||||
return FileVisitResult.CONTINUE
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
override public fun toString() = spec
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package com.beust.kobalt
|
||||
|
||||
import java.io.File
|
||||
|
||||
abstract public class JavaInfo {
|
||||
public var javaExecutable: File? = null
|
||||
get() = findExecutable("java")
|
||||
public var javacExecutable: File? = null
|
||||
get() = findExecutable("javac")
|
||||
public var javadocExecutable: File? = null
|
||||
get() = findExecutable("javadoc")
|
||||
abstract public var javaHome: File?
|
||||
abstract public var runtimeJar: File?
|
||||
abstract public var toolsJar: File?
|
||||
|
||||
abstract public fun findExecutable(command: String) : File
|
||||
|
||||
companion object {
|
||||
fun create(javaBase: File?): Jvm {
|
||||
val vendor = System.getProperty("java.vm.vendor")
|
||||
if (vendor.toLowerCase().startsWith("apple inc.")) {
|
||||
return AppleJvm(OperatingSystem.Companion.current(), javaBase!!)
|
||||
}
|
||||
if (vendor.toLowerCase().startsWith("ibm corporation")) {
|
||||
return IbmJvm(OperatingSystem.Companion.current(), javaBase!!)
|
||||
}
|
||||
return Jvm(OperatingSystem.Companion.current(), javaBase)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,160 @@
|
|||
package com.beust.kobalt
|
||||
|
||||
import com.beust.kobalt.misc.log
|
||||
import com.beust.kobalt.misc.warn
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
public open class Jvm constructor(
|
||||
val os: OperatingSystem,
|
||||
var javaBase: File? = null) : JavaInfo() {
|
||||
|
||||
private var _javaHome: File? = null
|
||||
override public var javaHome: File? = null
|
||||
get() = _javaHome!!
|
||||
override public var runtimeJar: File? = null
|
||||
private fun findRuntimeJar() : File? {
|
||||
var runtimeJar = File(javaBase, "lib/rt.jar")
|
||||
if (runtimeJar.exists()) {
|
||||
return runtimeJar
|
||||
}
|
||||
runtimeJar = File(javaBase, "jre/lib/rt.jar")
|
||||
return if (runtimeJar.exists()) runtimeJar else null
|
||||
}
|
||||
override public var toolsJar: File? = null
|
||||
|
||||
private var userSupplied: Boolean? = false
|
||||
private var javaVersion: String? = null
|
||||
|
||||
init {
|
||||
if (javaBase == null) {
|
||||
//discover based on what's in the sys. property
|
||||
try {
|
||||
javaBase = File(System.getProperty("java.home")).canonicalFile
|
||||
} catch (e: IOException) {
|
||||
throw KobaltException(e)
|
||||
}
|
||||
|
||||
_javaHome = findJavaHome(javaBase!!)
|
||||
javaVersion = SystemProperties.Companion.javaVersion
|
||||
userSupplied = false
|
||||
} else {
|
||||
//precisely use what the user wants and validate strictly further on
|
||||
_javaHome = javaBase!!
|
||||
userSupplied = true
|
||||
javaVersion = null
|
||||
}
|
||||
toolsJar = findToolsJar(javaBase!!)
|
||||
runtimeJar = findRuntimeJar()
|
||||
}
|
||||
|
||||
private fun findJavaHome(javaBase: File): File {
|
||||
val toolsJar = findToolsJar(javaBase)
|
||||
if (toolsJar != null) {
|
||||
return toolsJar.parentFile.parentFile
|
||||
} else if (javaBase.name.equals("jre", true) && File(javaBase.parentFile,
|
||||
"bin/java").exists()) {
|
||||
return javaBase.parentFile
|
||||
} else {
|
||||
return javaBase
|
||||
}
|
||||
}
|
||||
|
||||
private fun findToolsJar(jh: File): File? {
|
||||
javaHome = jh
|
||||
var toolsJar = File(javaHome, "lib/tools.jar")
|
||||
if (toolsJar.exists()) {
|
||||
return toolsJar
|
||||
}
|
||||
if (javaHome!!.name.equals("jre", true)) {
|
||||
javaHome = javaHome!!.parentFile
|
||||
toolsJar = File(javaHome, "lib/tools.jar")
|
||||
if (toolsJar.exists()) {
|
||||
return toolsJar
|
||||
}
|
||||
}
|
||||
|
||||
if (os.isWindows()) {
|
||||
val version = SystemProperties.Companion.javaVersion
|
||||
if (javaHome!!.name.toRegex().matches("jre\\d+")
|
||||
|| javaHome!!.name == "jre$version") {
|
||||
javaHome = File(javaHome!!.parentFile, "jdk$version")
|
||||
toolsJar = File(javaHome, "lib/tools.jar")
|
||||
if (toolsJar.exists()) {
|
||||
return toolsJar
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// open public fun isIbmJvm(): Boolean {
|
||||
// return false
|
||||
// }
|
||||
|
||||
override public fun findExecutable(command: String): File {
|
||||
if (javaHome != null) {
|
||||
val jdkHome = if (javaHome!!.endsWith("jre")) javaHome!!.parentFile else javaHome
|
||||
val exec = File(jdkHome, "bin/" + command)
|
||||
var executable = File(os.getExecutableName(exec.absolutePath))
|
||||
if (executable.isFile) {
|
||||
return executable
|
||||
}
|
||||
}
|
||||
|
||||
// if (userSupplied) {
|
||||
// //then we want to validate strictly
|
||||
// throw JavaHomeException(String.format("The supplied javaHome seems to be invalid." + " I cannot find the %s executable. Tried location: %s", command, executable.getAbsolutePath()))
|
||||
// }
|
||||
|
||||
val pathExecutable = os.findInPath(command)
|
||||
if (pathExecutable != null) {
|
||||
log(1, "Unable to find the $command executable using home: " +
|
||||
"$javaHome but found it on the PATH: $pathExecutable.")
|
||||
return pathExecutable
|
||||
}
|
||||
|
||||
warn("Unable to find the $command executable. Tried the java home: $javaHome" +
|
||||
" and the PATH. We will assume the executable can be ran in the current " +
|
||||
"working folder.")
|
||||
return java.io.File(os.getExecutableName(command))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class AppleJvm : Jvm {
|
||||
override var runtimeJar: File? = File(javaHome!!.getParentFile(), "Classes/classes.jar")
|
||||
override var toolsJar: File? = File(javaHome!!.getParentFile(), "Classes/tools.jar")
|
||||
|
||||
constructor(os: OperatingSystem) : super(os) {
|
||||
}
|
||||
|
||||
constructor(current: OperatingSystem, javaHome: File) : super(current, javaHome) {
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
// fun getInheritableEnvironmentVariables(envVars: Map<String, *>): Map<String, *> {
|
||||
// val vars = HashMap<String, Any>()
|
||||
// for (entry in envVars.entrySet()) {
|
||||
// if (entry.getKey().toRegex().matches("APP_NAME_\\d+") ||
|
||||
// entry.getKey().toRegex().matches("JAVA_MAIN_CLASS_\\d+")) {
|
||||
// continue
|
||||
// }
|
||||
// vars.put(entry.getKey(), entry.getValue())
|
||||
// }
|
||||
// return vars
|
||||
// }
|
||||
}
|
||||
|
||||
class IbmJvm(os: OperatingSystem, suppliedJavaBase: File) : Jvm(os, suppliedJavaBase) {
|
||||
override var runtimeJar: File? = throw IllegalArgumentException("Not implemented")
|
||||
override var toolsJar: File? = throw IllegalArgumentException("Not implemented")
|
||||
|
||||
// override fun isIbmJvm(): Boolean {
|
||||
// return true
|
||||
// }
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.beust.kobalt
|
||||
|
||||
class KobaltException(s: String? = null, ex: Throwable? = null, val docUrl: String? = null)
|
||||
: RuntimeException(s, ex) {
|
||||
constructor(ex: Throwable?) : this(null, ex, null)
|
||||
}
|
||||
|
|
@ -0,0 +1,298 @@
|
|||
package com.beust.kobalt
|
||||
|
||||
import java.io.File
|
||||
|
||||
abstract class OperatingSystem {
|
||||
|
||||
override fun toString(): String {
|
||||
return getName() + " " + getVersion() + " " + System.getProperty("os.arch")
|
||||
}
|
||||
|
||||
fun getName(): String {
|
||||
return System.getProperty("os.name")
|
||||
}
|
||||
|
||||
fun getVersion(): String {
|
||||
return System.getProperty("os.version")
|
||||
}
|
||||
|
||||
open fun isWindows(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
open fun isUnix(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
open fun isMacOsX(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
open fun isLinux(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
abstract fun getNativePrefix(): String
|
||||
|
||||
abstract fun getScriptName(scriptPath: String): String
|
||||
|
||||
abstract fun getExecutableName(executablePath: String): String
|
||||
|
||||
abstract fun getSharedLibraryName(libraryName: String): String
|
||||
|
||||
abstract fun getStaticLibraryName(libraryName: String): String
|
||||
|
||||
abstract fun getFamilyName(): String
|
||||
|
||||
/**
|
||||
* Locates the given executable in the system path. Returns null if not found.
|
||||
*/
|
||||
fun findInPath(name: String): File? {
|
||||
val exeName = getExecutableName(name)
|
||||
if (exeName.contains(File.separator)) {
|
||||
val candidate = File(exeName)
|
||||
if (candidate.isFile) {
|
||||
return candidate
|
||||
}
|
||||
return null
|
||||
}
|
||||
for (dir in path) {
|
||||
val candidate = File(dir, exeName)
|
||||
if (candidate.isFile) {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// fun findAllInPath(name: String): List<File> {
|
||||
// val all = LinkedList<File>()
|
||||
//
|
||||
// for (dir in getPath()) {
|
||||
// val candidate = File(dir, name)
|
||||
// if (candidate.isFile) {
|
||||
// all.add(candidate)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return all
|
||||
// }
|
||||
|
||||
val path: List<File>
|
||||
get() {
|
||||
return (System.getenv(getPathVar()) ?: return emptyList()).split(File.pathSeparator).map { File(it) }
|
||||
}
|
||||
|
||||
open fun getPathVar(): String {
|
||||
return "PATH"
|
||||
}
|
||||
|
||||
class Windows : OperatingSystem() {
|
||||
override fun isWindows(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun getFamilyName(): String {
|
||||
return "windows"
|
||||
}
|
||||
|
||||
override fun getScriptName(scriptPath: String): String {
|
||||
return withSuffix(scriptPath, ".bat")
|
||||
}
|
||||
|
||||
override fun getExecutableName(executablePath: String): String {
|
||||
return withSuffix(executablePath, ".exe")
|
||||
}
|
||||
|
||||
override fun getSharedLibraryName(libraryName: String): String {
|
||||
return withSuffix(libraryName, ".dll")
|
||||
}
|
||||
|
||||
override fun getStaticLibraryName(libraryName: String): String {
|
||||
return withSuffix(libraryName, ".lib")
|
||||
}
|
||||
|
||||
override fun getNativePrefix(): String {
|
||||
var arch = System.getProperty("os.arch")
|
||||
if ("i386" == arch) {
|
||||
arch = "x86"
|
||||
}
|
||||
return "win32-" + arch
|
||||
}
|
||||
|
||||
private fun withSuffix(executablePath: String, extension: String): String {
|
||||
if (executablePath.toLowerCase().endsWith(extension)) {
|
||||
return executablePath
|
||||
}
|
||||
return removeExtension(executablePath) + extension
|
||||
}
|
||||
|
||||
private fun removeExtension(executablePath: String): String {
|
||||
val fileNameStart = Math.max(executablePath.lastIndexOf('/'), executablePath.lastIndexOf('\\'))
|
||||
val extensionPos = executablePath.lastIndexOf('.')
|
||||
|
||||
if (extensionPos > fileNameStart) {
|
||||
return executablePath.substring(0, extensionPos)
|
||||
}
|
||||
return executablePath
|
||||
}
|
||||
|
||||
|
||||
override fun getPathVar(): String {
|
||||
return "Path"
|
||||
}
|
||||
}
|
||||
|
||||
open class Unix : OperatingSystem() {
|
||||
override fun getScriptName(scriptPath: String): String {
|
||||
return scriptPath
|
||||
}
|
||||
|
||||
override fun getFamilyName(): String {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
override fun getExecutableName(executablePath: String): String {
|
||||
return executablePath
|
||||
}
|
||||
|
||||
override fun getSharedLibraryName(libraryName: String): String {
|
||||
return getLibraryName(libraryName, getSharedLibSuffix())
|
||||
}
|
||||
|
||||
private fun getLibraryName(libraryName: String, suffix: String): String {
|
||||
if (libraryName.endsWith(suffix)) {
|
||||
return libraryName
|
||||
}
|
||||
val pos = libraryName.lastIndexOf('/')
|
||||
if (pos >= 0) {
|
||||
return libraryName.substring(0, pos + 1) + "lib" + libraryName.substring(pos + 1) + suffix
|
||||
} else {
|
||||
return "lib" + libraryName + suffix
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun getSharedLibSuffix(): String {
|
||||
return ".so"
|
||||
}
|
||||
|
||||
override fun getStaticLibraryName(libraryName: String): String {
|
||||
return getLibraryName(libraryName, ".a")
|
||||
}
|
||||
|
||||
override fun isUnix(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun getNativePrefix(): String {
|
||||
val arch = getArch()
|
||||
var osPrefix = getOsPrefix()
|
||||
osPrefix += "-" + arch
|
||||
return osPrefix
|
||||
}
|
||||
|
||||
protected open fun getArch(): String {
|
||||
var arch = System.getProperty("os.arch")
|
||||
if ("x86" == arch) {
|
||||
arch = "i386"
|
||||
}
|
||||
if ("x86_64" == arch) {
|
||||
arch = "amd64"
|
||||
}
|
||||
if ("powerpc" == arch) {
|
||||
arch = "ppc"
|
||||
}
|
||||
return arch
|
||||
}
|
||||
|
||||
protected open fun getOsPrefix(): String {
|
||||
var osPrefix = getName().toLowerCase()
|
||||
val space = osPrefix.indexOf(" ")
|
||||
if (space != -1) {
|
||||
osPrefix = osPrefix.substring(0, space)
|
||||
}
|
||||
return osPrefix
|
||||
}
|
||||
}
|
||||
|
||||
class MacOs : Unix() {
|
||||
override fun isMacOsX(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun getFamilyName(): String {
|
||||
return "os x"
|
||||
}
|
||||
|
||||
override fun getSharedLibSuffix(): String {
|
||||
return ".dylib"
|
||||
}
|
||||
|
||||
override fun getNativePrefix(): String {
|
||||
return "darwin"
|
||||
}
|
||||
}
|
||||
|
||||
class Linux : Unix() {
|
||||
override fun isLinux(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun getFamilyName(): String {
|
||||
return "linux"
|
||||
}
|
||||
}
|
||||
|
||||
class FreeBSD : Unix()
|
||||
|
||||
class Solaris : Unix() {
|
||||
override fun getFamilyName(): String {
|
||||
return "solaris"
|
||||
}
|
||||
|
||||
override fun getOsPrefix(): String {
|
||||
return "sunos"
|
||||
}
|
||||
|
||||
override fun getArch(): String {
|
||||
val arch = System.getProperty("os.arch")
|
||||
if (arch == "i386" || arch == "x86") {
|
||||
return "x86"
|
||||
}
|
||||
return super.getArch()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val WINDOWS: Windows = Windows()
|
||||
val MAC_OS: MacOs = MacOs()
|
||||
val SOLARIS: Solaris = Solaris()
|
||||
val LINUX: Linux = Linux()
|
||||
val FREE_BSD: FreeBSD = FreeBSD()
|
||||
val UNIX: Unix = Unix()
|
||||
|
||||
fun current(): OperatingSystem {
|
||||
return forName(System.getProperty("os.name"))
|
||||
}
|
||||
|
||||
fun forName(os: String): OperatingSystem {
|
||||
val osName = os.toLowerCase()
|
||||
if (osName.contains("windows")) {
|
||||
return WINDOWS
|
||||
} else if (osName.contains("mac os x") || osName.contains("darwin") || osName.contains("osx")) {
|
||||
return MAC_OS
|
||||
} else if (osName.contains("sunos") || osName.contains("solaris")) {
|
||||
return SOLARIS
|
||||
} else if (osName.contains("linux")) {
|
||||
return LINUX
|
||||
} else if (osName.contains("freebsd")) {
|
||||
return FREE_BSD
|
||||
} else {
|
||||
// Not strictly true
|
||||
return UNIX
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
package com.beust.kobalt
|
||||
|
||||
import com.beust.kobalt.api.*
|
||||
import com.beust.kobalt.api.annotation.Task
|
||||
import com.beust.kobalt.internal.PluginInfo
|
||||
import com.beust.kobalt.internal.TaskManager
|
||||
import com.beust.kobalt.maven.DepFactory
|
||||
import com.beust.kobalt.maven.LocalRepo
|
||||
import com.beust.kobalt.misc.KFiles
|
||||
import com.beust.kobalt.misc.KobaltExecutors
|
||||
import com.beust.kobalt.misc.log
|
||||
import com.beust.kobalt.misc.JarUtils
|
||||
import com.google.inject.Provider
|
||||
import java.lang.reflect.Method
|
||||
import java.lang.reflect.Modifier
|
||||
import java.util.*
|
||||
import java.util.jar.JarFile
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
public class Plugins @Inject constructor (val taskManagerProvider : Provider<TaskManager>,
|
||||
val files: KFiles,
|
||||
val depFactory: DepFactory,
|
||||
val localRepo: LocalRepo,
|
||||
val executors: KobaltExecutors,
|
||||
val pluginInfo: PluginInfo,
|
||||
val taskManager: TaskManager) {
|
||||
|
||||
companion object {
|
||||
private var pluginMap = hashMapOf<String, IPlugin>()
|
||||
|
||||
fun addPluginInstance(plugin: IPlugin) {
|
||||
pluginMap.put(plugin.name, plugin)
|
||||
}
|
||||
|
||||
val plugins : List<IPlugin>
|
||||
get() = ArrayList(pluginMap.values)
|
||||
|
||||
/**
|
||||
* The list of plugins found in the build file.
|
||||
*/
|
||||
val dynamicPlugins : ArrayList<IClasspathDependency> = arrayListOf()
|
||||
fun addDynamicPlugin(plugin: IClasspathDependency) = dynamicPlugins.add(plugin)
|
||||
|
||||
fun findPlugin(name: String) : IPlugin? = pluginMap[name]
|
||||
}
|
||||
|
||||
fun applyPlugins(context: KobaltContext, projects: List<Project>) {
|
||||
plugins.forEach { plugin ->
|
||||
addPluginInstance(plugin)
|
||||
// We could inject this in the plug-in but since these will be written by external users,
|
||||
// I want to keep the verbosity of plugins to a minimum, so instead, we do the injection
|
||||
// manually here
|
||||
if (plugin is BasePlugin) {
|
||||
plugin.taskManager = taskManagerProvider.get()
|
||||
plugin.plugins = this
|
||||
}
|
||||
|
||||
// Call apply() on each plug-in that accepts a project
|
||||
log(2, "Applying plug-in \"${plugin.name}\"")
|
||||
projects.filter { plugin.accept(it) }.forEach { project ->
|
||||
plugin.apply(project, context)
|
||||
}
|
||||
|
||||
var currentClass : Class<in Any> = plugin.javaClass
|
||||
|
||||
// Tasks can come from two different places: plugin classes and build files.
|
||||
// When a task is read from a build file, ScriptCompiler adds it right away to plugin.methodTasks.
|
||||
// The following loop introspects the current plugin, finds all the tasks using the @Task annotation
|
||||
// and adds them to plugin.methodTasks
|
||||
while (! (currentClass.equals(Any::class.java))) {
|
||||
currentClass.declaredMethods.map {
|
||||
Pair(it, it.getAnnotation(Task::class.java))
|
||||
}.filter {
|
||||
it.second != null
|
||||
}.filter {
|
||||
isValidTaskMethod(it.first)
|
||||
}.forEach {
|
||||
if (Modifier.isPrivate(it.first.modifiers)) {
|
||||
throw KobaltException("A task method cannot be private: ${it.first}")
|
||||
}
|
||||
val annotation = it.second
|
||||
|
||||
taskManager.staticTasks.add(TaskManager.StaticTask(plugin, it.first, annotation))
|
||||
}
|
||||
|
||||
currentClass = currentClass.superclass
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Collect all the tasks from the task contributors
|
||||
context.pluginInfo.taskContributors.forEach {
|
||||
taskManager.dynamicTasks.addAll(it.tasksFor(context).map { TaskManager.PluginDynamicTask(it.plugin, it) })
|
||||
}
|
||||
|
||||
// Now that we have collected all static and dynamic tasks, turn them all into plug-in tasks
|
||||
taskManager.computePluginTasks(plugins, projects)
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure this task method has the right signature.
|
||||
*/
|
||||
private fun isValidTaskMethod(method: Method): Boolean {
|
||||
val t = "Task ${method.declaringClass.simpleName}.${method.name}: "
|
||||
|
||||
if (method.returnType != TaskResult::class.java) {
|
||||
throw IllegalArgumentException("${t}should return a TaskResult")
|
||||
}
|
||||
if (method.parameterTypes.size != 1) {
|
||||
throw IllegalArgumentException("${t}should take exactly one parameter of type a Project")
|
||||
}
|
||||
with(method.parameterTypes) {
|
||||
if (! Project::class.java.isAssignableFrom(get(0))) {
|
||||
throw IllegalArgumentException("${t}first parameter should be of type Project," +
|
||||
"not ${get(0)}")
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
val dependencies = arrayListOf<IClasspathDependency>()
|
||||
|
||||
fun installPlugins(dependencies: List<IClasspathDependency>, classLoader: ClassLoader) {
|
||||
val executor = executors.newExecutor("Plugins", 5)
|
||||
dependencies.forEach {
|
||||
//
|
||||
// Load all the jar files synchronously (can't compile the build script until
|
||||
// they are installed locally).
|
||||
depFactory.create(it.id, executor)
|
||||
|
||||
//
|
||||
// Open the jar, parse its kobalt-plugin.xml and add the resulting PluginInfo to pluginInfo
|
||||
//
|
||||
val pluginXml = JarUtils.extractTextFile(JarFile(it.jarFile.get()), PluginInfo.PLUGIN_XML)
|
||||
if (pluginXml != null) {
|
||||
val thisPluginInfo = PluginInfo.readPluginXml(pluginXml, classLoader)
|
||||
pluginInfo.addPluginInfo(thisPluginInfo)
|
||||
thisPluginInfo.plugins.forEach {
|
||||
Plugins.addPluginInstance(it)
|
||||
}
|
||||
} else {
|
||||
throw KobaltException("Plugin $it doesn't contain a ${PluginInfo.PLUGIN_XML} file")
|
||||
}
|
||||
}
|
||||
executor.shutdown()
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
package com.beust.kobalt
|
||||
|
||||
import com.beust.kobalt.api.IClasspathDependency
|
||||
import com.beust.kobalt.maven.LocalRepo
|
||||
import com.beust.kobalt.maven.MavenId
|
||||
import com.beust.kobalt.maven.RepoFinder
|
||||
import com.beust.kobalt.maven.SimpleDep
|
||||
import com.beust.kobalt.maven.dependency.MavenDependency
|
||||
import com.beust.kobalt.misc.Node
|
||||
import com.beust.kobalt.misc.log
|
||||
import com.google.inject.Inject
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Display information about a Maven id.
|
||||
*/
|
||||
class ResolveDependency @Inject constructor(val repoFinder: RepoFinder, val localRepo: LocalRepo) {
|
||||
val increment = 8
|
||||
val leftFirst = "\u2558"
|
||||
val leftMiddle = "\u255f"
|
||||
val leftLast = "\u2559"
|
||||
val vertical = "\u2551"
|
||||
|
||||
class Dep(val dep: IClasspathDependency, val level: Int)
|
||||
|
||||
fun run(id: String) {
|
||||
val repoResult = repoFinder.findCorrectRepo(id)
|
||||
|
||||
val indent = -1
|
||||
val originalDep = MavenDependency.create(id)
|
||||
// We want to display the dependencies of the id we found, not the one we queries
|
||||
val dep = MavenDependency.create(originalDep.shortId + repoResult.version)
|
||||
val root = Node(Dep(dep, indent))
|
||||
val seen = hashSetOf(id)
|
||||
root.addChildren(findChildren(root, seen))
|
||||
|
||||
val simpleDep = SimpleDep(MavenId.create(id))
|
||||
val url = repoResult.hostConfig.url + simpleDep.toJarFile(repoResult)
|
||||
val localFile = localRepo.toFullPath(simpleDep.toJarFile(repoResult))
|
||||
AsciiArt.logBox(listOf(id, url, localFile).map { " $it" }, {s -> println(s) })
|
||||
|
||||
display(root.children)
|
||||
println("")
|
||||
}
|
||||
|
||||
private fun display(nodes: List<Node<Dep>>) {
|
||||
nodes.withIndex().forEach { indexNode ->
|
||||
val node = indexNode.value
|
||||
with(node.value) {
|
||||
val left =
|
||||
if (indexNode.index == nodes.size - 1) leftLast
|
||||
else leftMiddle
|
||||
val indent = level * increment
|
||||
for(i in 0..indent - 2) {
|
||||
if (i % increment == 0) print(vertical)
|
||||
else print(" ")
|
||||
}
|
||||
println(left + " " + dep.id)
|
||||
display(node.children)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun findChildren(root: Node<Dep>, seen: HashSet<String>): List<Node<Dep>> {
|
||||
val result = arrayListOf<Node<Dep>>()
|
||||
root.value.dep.directDependencies().forEach {
|
||||
if (! seen.contains(it.id)) {
|
||||
val dep = Dep(it, root.value.level + 1)
|
||||
val node = Node(dep)
|
||||
log(2, "Found dependency ${dep.dep.id} level: ${dep.level}")
|
||||
result.add(node)
|
||||
seen.add(it.id)
|
||||
node.addChildren(findChildren(node, seen))
|
||||
}
|
||||
}
|
||||
log(2, "Children for ${root.value.dep.id}: ${result.size}")
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.beust.kobalt
|
||||
|
||||
public class SystemProperties {
|
||||
companion object {
|
||||
val javaBase = System.getProperty("java.home") ?:
|
||||
(System.getenv("JAVA_HOME") ?: throw IllegalArgumentException("JAVA_HOME not defined"))
|
||||
val javaVersion = System.getProperty("java.version")
|
||||
val homeDir = System.getProperty("user.home")
|
||||
val tmpDir = System.getProperty("java.io.tmpdir")
|
||||
val currentDir = System.getProperty("user.dir")
|
||||
val username = System.getProperty("user.name")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package com.beust.kobalt
|
||||
|
||||
open public class TaskResult(val success: Boolean = true, val errorMessage: String? = null)
|
|
@ -0,0 +1,20 @@
|
|||
package com.beust.kobalt
|
||||
|
||||
import java.io.BufferedReader
|
||||
import java.io.PrintWriter
|
||||
import java.io.Reader
|
||||
|
||||
public class Template(val reader: Reader, val writer: PrintWriter, val map: Map<String, Any>) {
|
||||
|
||||
public fun execute() {
|
||||
BufferedReader(reader).let {
|
||||
it.forEachLine { line ->
|
||||
var replacedLine = line
|
||||
map.keys.forEach { key ->
|
||||
replacedLine = replacedLine.replace("{{$key}}", map.get(key).toString(), false)
|
||||
}
|
||||
writer.append(replacedLine).append("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package com.beust.kobalt
|
||||
|
||||
import com.beust.kobalt.api.Project
|
||||
import com.beust.kobalt.api.annotation.Directive
|
||||
|
||||
class TestConfig(val project: Project) {
|
||||
fun args(vararg arg: String) {
|
||||
project.testArgs.addAll(arg)
|
||||
}
|
||||
|
||||
fun jvmArgs(vararg arg: String) {
|
||||
project.testJvmArgs.addAll(arg)
|
||||
}
|
||||
}
|
||||
|
||||
@Directive
|
||||
fun Project.test(init: TestConfig.() -> Unit) : TestConfig {
|
||||
val result = TestConfig(this)
|
||||
result.init()
|
||||
return result
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,201 @@
|
|||
package com.beust.kobalt
|
||||
|
||||
import com.beust.kobalt.api.*
|
||||
import com.beust.kobalt.misc.KFiles
|
||||
import com.beust.kobalt.misc.log
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Capture the product flavor and the build type of a build.
|
||||
*/
|
||||
class Variant(val initialProductFlavor: ProductFlavorConfig? = null,
|
||||
val initialBuildType: BuildTypeConfig? = null) {
|
||||
|
||||
val productFlavor: ProductFlavorConfig by lazy {
|
||||
initialProductFlavor ?: Variant.DEFAULT_PRODUCT_FLAVOR
|
||||
}
|
||||
val buildType: BuildTypeConfig by lazy {
|
||||
initialBuildType ?: Variant.DEFAULT_BUILD_TYPE
|
||||
}
|
||||
|
||||
val isDefault : Boolean
|
||||
get() = productFlavor == DEFAULT_PRODUCT_FLAVOR && buildType == DEFAULT_BUILD_TYPE
|
||||
|
||||
fun toTask(taskName: String) = taskName + productFlavor.name.capitalize() + buildType.name.capitalize()
|
||||
|
||||
/**
|
||||
* for {internal, release}, return [internalRelease, internal, release]
|
||||
*/
|
||||
fun allDirectories(project: Project): List<String> {
|
||||
val result = arrayListOf<String>()
|
||||
result.add(toCamelcaseDir())
|
||||
if (productFlavor != null) result.add(productFlavor.name)
|
||||
if (buildType != null) result.add(buildType.name)
|
||||
return result
|
||||
}
|
||||
|
||||
fun resDirectories(project: Project) : List<File> = sourceDirectories(project, "res")
|
||||
|
||||
fun sourceDirectories(project: Project) : List<File> =
|
||||
sourceDirectories(project, project.projectInfo.sourceDirectory)
|
||||
|
||||
/**
|
||||
* suffix is either "java" (to find source files) or "res" (to find resources)
|
||||
*/
|
||||
private fun sourceDirectories(project: Project, suffix: String) : List<File> {
|
||||
val result = arrayListOf<File>()
|
||||
val sourceDirectories = project.sourceDirectories.map { File(it) }
|
||||
if (isDefault) {
|
||||
result.addAll(sourceDirectories)
|
||||
} else {
|
||||
result.addAll(allDirectories(project).map {
|
||||
File(KFiles.joinDir("src", it, suffix))
|
||||
}.filter {
|
||||
it.exists()
|
||||
})
|
||||
|
||||
// // The ordering of files is: 1) build type 2) product flavor 3) default
|
||||
buildType.let {
|
||||
val dir = File(KFiles.joinDir("src", it.name, project.projectInfo.sourceDirectory))
|
||||
log(3, "Adding source for build type ${it.name}: ${dir.path}")
|
||||
result.add(dir)
|
||||
}
|
||||
productFlavor.let {
|
||||
val dir = File(KFiles.joinDir("src", it.name, project.projectInfo.sourceDirectory))
|
||||
log(3, "Adding source for product flavor ${it.name}: ${dir.path}")
|
||||
result.add(dir)
|
||||
}
|
||||
|
||||
// Now that all the variant source directories have been added, add the project's default ones
|
||||
result.addAll(sourceDirectories)
|
||||
return result
|
||||
}
|
||||
|
||||
// Generated directory, if applicable
|
||||
generatedSourceDirectory?.let {
|
||||
result.add(it)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
fun archiveName(project: Project, archiveName: String?, suffix: String) : String {
|
||||
val result =
|
||||
if (isDefault) {
|
||||
archiveName ?: project.name + "-" + project.version + suffix
|
||||
} else {
|
||||
val base = if (archiveName != null) archiveName.substring(0, archiveName.length - suffix.length)
|
||||
else project.name + "-" + project.version
|
||||
val result: String =
|
||||
base + "-${productFlavor.name}" + "-${buildType.name}"
|
||||
|
||||
result
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
val shortArchiveName = if (isDefault) "" else "-" + productFlavor.name + "-" + buildType.name
|
||||
|
||||
var generatedSourceDirectory: File? = null
|
||||
|
||||
private fun findBuildTypeBuildConfig(project: Project, variant: Variant?) : BuildConfig? {
|
||||
val buildTypeName = variant?.buildType?.name
|
||||
return project.buildTypes.getRaw(buildTypeName)?.buildConfig ?: null
|
||||
}
|
||||
|
||||
private fun findProductFlavorBuildConfig(project: Project, variant: Variant?) : BuildConfig? {
|
||||
val buildTypeName = variant?.productFlavor?.name
|
||||
return project.productFlavors.getRaw(buildTypeName)?.buildConfig ?: null
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of the BuildConfigs found on the productFlavor{}, buildType{} and project{} (in that order).
|
||||
*/
|
||||
private fun findBuildConfigs(project: Project, variant: Variant?) : List<BuildConfig> {
|
||||
val result = listOf(
|
||||
findBuildTypeBuildConfig(project, variant),
|
||||
findProductFlavorBuildConfig(project, variant),
|
||||
project.buildConfig)
|
||||
.filterNotNull()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate BuildConfig.java if requested. Also look up if any BuildConfig is defined on the current build type,
|
||||
* product flavor or main project, and use them to generate any additional field (in that order to
|
||||
* respect the priorities). Return the generated file if it was generated, null otherwise.
|
||||
*/
|
||||
fun maybeGenerateBuildConfig(project: Project, context: KobaltContext) : File? {
|
||||
val buildConfigs = findBuildConfigs(project, this)
|
||||
|
||||
if (buildConfigs.size > 0) {
|
||||
val pkg = project.packageName ?: project.group
|
||||
?: throw KobaltException(
|
||||
"packageName needs to be defined on the project in order to generate BuildConfig")
|
||||
|
||||
val code = project.projectInfo.generateBuildConfig(project, context, pkg, this, buildConfigs)
|
||||
val result = KFiles.makeDir(KFiles.generatedSourceDir(project, this, "buildConfig"))
|
||||
// Make sure the generatedSourceDirectory doesn't contain the project.directory since
|
||||
// that directory will be added when trying to find recursively all the sources in it
|
||||
generatedSourceDirectory = File(result.relativeTo(File(project.directory)))
|
||||
val outputGeneratedSourceDirectory = File(result, pkg.replace('.', File.separatorChar))
|
||||
val outputDir = File(outputGeneratedSourceDirectory, "BuildConfig" + project.sourceSuffix)
|
||||
KFiles.saveFile(outputDir, code)
|
||||
log(2, "Generated ${outputDir.path}")
|
||||
return result
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString() = toTask("")
|
||||
|
||||
companion object {
|
||||
val DEFAULT_PRODUCT_FLAVOR = ProductFlavorConfig("")
|
||||
val DEFAULT_BUILD_TYPE = BuildTypeConfig(null, "")
|
||||
|
||||
fun allVariants(project: Project): List<Variant> {
|
||||
val result = arrayListOf<Variant>()
|
||||
|
||||
if (project.buildTypes.size > 0) {
|
||||
project.buildTypes.keys.forEach {
|
||||
val bt = project.buildTypes[it]
|
||||
if (project.productFlavors.size > 0) {
|
||||
project.productFlavors.keys.forEach {
|
||||
result.add(Variant(project.productFlavors[it], bt))
|
||||
}
|
||||
} else {
|
||||
result.add(Variant(null, bt))
|
||||
}
|
||||
}
|
||||
} else if (project.productFlavors.size > 0) {
|
||||
project.productFlavors.keys.forEach {
|
||||
val pf = project.productFlavors[it]
|
||||
if (project.buildTypes.size > 0) {
|
||||
project.buildTypes.keys.forEach {
|
||||
result.add(Variant(pf, project.buildTypes[it]))
|
||||
}
|
||||
} else {
|
||||
result.add(Variant(pf, null))
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
fun toCamelcaseDir() : String {
|
||||
val pfName = productFlavor.name
|
||||
val btName = buildType.name
|
||||
return pfName[0].toLowerCase() + pfName.substring(1) + btName[0].toUpperCase() + btName.substring(1)
|
||||
}
|
||||
|
||||
fun toIntermediateDir() : String {
|
||||
if (isDefault) {
|
||||
return ""
|
||||
} else {
|
||||
return KFiles.joinDir(productFlavor.name, buildType.name)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package com.beust.kobalt.api
|
||||
|
||||
import com.beust.kobalt.Plugins
|
||||
import com.beust.kobalt.internal.TaskManager
|
||||
|
||||
abstract public class BasePlugin : IPlugin {
|
||||
lateinit var context: KobaltContext
|
||||
|
||||
override fun accept(project: Project) = true
|
||||
|
||||
override fun apply(project: Project, context: KobaltContext) {
|
||||
this.context = context
|
||||
}
|
||||
|
||||
/**
|
||||
* The list of projects depended upon (e.g. val p = javaProject(dependentProject)).
|
||||
*/
|
||||
protected val projects = arrayListOf<ProjectDescription>()
|
||||
|
||||
fun addProject(project: Project, dependsOn: Array<out Project>) =
|
||||
projects.add(ProjectDescription(project, dependsOn.toList()))
|
||||
|
||||
override lateinit var taskManager: TaskManager
|
||||
lateinit var plugins: Plugins
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package com.beust.kobalt.api
|
||||
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Describe the information necessary to run a compiler.
|
||||
*/
|
||||
data class CompilerActionInfo(val directory: String?,
|
||||
val dependencies: List<IClasspathDependency>,
|
||||
val sourceFiles: List<String>,
|
||||
val outputDir: File,
|
||||
val compilerArgs: List<String>)
|
||||
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.beust.kobalt.api
|
||||
|
||||
/**
|
||||
* A plug-in that has some per-project configuration in the build file.
|
||||
*/
|
||||
abstract public class ConfigPlugin<T> : BasePlugin() {
|
||||
private val configurations = hashMapOf<String, T>()
|
||||
|
||||
fun configurationFor(project: Project?) = if (project != null) configurations[project.name] else null
|
||||
|
||||
fun addConfiguration(project: Project, configuration: T) = configurations.put(project.name, configuration)
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package com.beust.kobalt.api
|
||||
|
||||
import com.google.common.collect.ArrayListMultimap
|
||||
|
||||
/**
|
||||
* A plug-in that has some per-project list of configurations in the build file.
|
||||
*/
|
||||
abstract public class ConfigsPlugin<T> : BasePlugin() {
|
||||
private val configurations = ArrayListMultimap.create<String, T>()
|
||||
|
||||
fun configurationFor(project: Project) = configurations[project.name]
|
||||
|
||||
fun addConfiguration(project: Project, configuration: T) = configurations.put(project.name, configuration)
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package com.beust.kobalt.api
|
||||
|
||||
/**
|
||||
* Base interface for affinity interfaces.
|
||||
*/
|
||||
interface IAffinity {
|
||||
companion object {
|
||||
/**
|
||||
* The recommended default affinity if your plug-in can run this project. Use a higher
|
||||
* number if you expect to compete against other actors and you'd like to win over them.
|
||||
*/
|
||||
const val DEFAULT_POSITIVE_AFFINITY = 100
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.beust.kobalt.api
|
||||
|
||||
class BuildConfigField(val type: String, val name: String, val value: Any)
|
||||
|
||||
/**
|
||||
* Plug-ins that want to add fields to BuildConfig need to implement this interface.
|
||||
*/
|
||||
interface IBuildConfigFieldContributor {
|
||||
fun fieldsFor(project: Project, context: KobaltContext) : List<BuildConfigField>
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package com.beust.kobalt.api
|
||||
|
||||
/**
|
||||
* Plug-ins can alter the build directory by implementing this interface.
|
||||
*/
|
||||
interface IBuildDirectoryIncerceptor : IInterceptor {
|
||||
fun intercept(project: Project, context: KobaltContext, buildDirectory: String) : String
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.beust.kobalt.api
|
||||
|
||||
import com.beust.kobalt.api.IClasspathDependency
|
||||
|
||||
/**
|
||||
* Plugins that export classpath entries need to implement this interface.
|
||||
*/
|
||||
interface IClasspathContributor : IContributor {
|
||||
fun entriesFor(project: Project?) : Collection<IClasspathDependency>
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.beust.kobalt.api
|
||||
|
||||
import org.apache.maven.model.Dependency
|
||||
import java.io.File
|
||||
import java.util.concurrent.Future
|
||||
|
||||
/**
|
||||
* Encapsulate a dependency that can be put on the classpath. This interface
|
||||
* has two subclasses: FileDependency, a physical file, and MavenDependency,
|
||||
* which represents a dependency living in a Maven repo.
|
||||
*/
|
||||
interface IClasspathDependency {
|
||||
/** Identifier for this dependency */
|
||||
val id: String
|
||||
|
||||
/** Absolute path to the jar file on the local file system */
|
||||
val jarFile: Future<File>
|
||||
|
||||
/** Convert to a Maven <dependency> model tag */
|
||||
fun toMavenDependencies() : Dependency
|
||||
|
||||
/** The list of dependencies for this element (not the transitive closure */
|
||||
fun directDependencies(): List<IClasspathDependency>
|
||||
|
||||
/** Used to only keep the most recent version for an artifact if no version was specified */
|
||||
val shortId: String
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package com.beust.kobalt.api
|
||||
|
||||
/**
|
||||
* Modify a list of dependencies before Kobalt starts using them.
|
||||
*/
|
||||
interface IClasspathInterceptor : IInterceptor {
|
||||
fun intercept(project: Project, dependencies: List<IClasspathDependency>) : List<IClasspathDependency>
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package com.beust.kobalt.api
|
||||
|
||||
import com.beust.kobalt.TaskResult
|
||||
import com.beust.kobalt.api.IClasspathDependency
|
||||
import java.io.File
|
||||
|
||||
interface ICompilerContributor : IProjectAffinity {
|
||||
fun compile(project: Project, context: KobaltContext, info: CompilerActionInfo) : TaskResult
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package com.beust.kobalt.api
|
||||
|
||||
/**
|
||||
* Plugins that add compiler flags.
|
||||
*/
|
||||
interface ICompilerFlagContributor : IContributor {
|
||||
fun flagsFor(project: Project, context: KobaltContext, currentFlags: List<String>): List<String>
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package com.beust.kobalt.api
|
||||
|
||||
/**
|
||||
* Plug-ins can alter what is passed to the compiler by implementing this interface.
|
||||
*/
|
||||
interface ICompilerInterceptor : IInterceptor {
|
||||
fun intercept(project: Project, context: KobaltContext, actionInfo: CompilerActionInfo) : CompilerActionInfo
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package com.beust.kobalt.api
|
||||
|
||||
import com.beust.kobalt.TaskResult
|
||||
|
||||
interface IDocContributor : IProjectAffinity {
|
||||
fun generateDoc(project: Project, context: KobaltContext, info: CompilerActionInfo) : TaskResult
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.beust.kobalt.api
|
||||
|
||||
/**
|
||||
* The factory function to use to instantiate all the contributors and other entities
|
||||
* found in kobalt-plugin.xml.
|
||||
*/
|
||||
interface IFactory {
|
||||
fun <T> instanceOf(c: Class<T>) : T
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.beust.kobalt.api
|
||||
|
||||
import java.io.OutputStream
|
||||
|
||||
/**
|
||||
* Plugins that want to participate in the --init process (they can generate files to initialize
|
||||
* a new project).
|
||||
*/
|
||||
interface IInitContributor<T> : ISimpleAffinity<T> {
|
||||
/**
|
||||
* Generate the Build.kt file into the given OutputStream.
|
||||
*/
|
||||
fun generateBuildFile(os: OutputStream)
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.beust.kobalt.api
|
||||
|
||||
import com.beust.kobalt.maven.MavenId
|
||||
|
||||
/**
|
||||
* Plug-ins can rewrite Maven id's before Kobalt sees them with this interface.
|
||||
*/
|
||||
interface IMavenIdInterceptor: IInterceptor {
|
||||
fun intercept(mavenId: MavenId) : MavenId
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package com.beust.kobalt.api
|
||||
|
||||
import com.beust.kobalt.internal.TaskManager
|
||||
|
||||
public interface IPlugin : IPluginActor {
|
||||
val name: String
|
||||
fun accept(project: Project) : Boolean
|
||||
fun apply(project: Project, context: KobaltContext) {}
|
||||
var taskManager : TaskManager
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package com.beust.kobalt.api
|
||||
|
||||
interface IPluginActor
|
||||
|
||||
interface IContributor : IPluginActor
|
||||
|
||||
interface IInterceptor : IPluginActor
|
||||
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.beust.kobalt.api
|
||||
|
||||
/**
|
||||
* An affinity interface that uses a project and a context to calculate its affinity.
|
||||
*/
|
||||
interface IProjectAffinity : IAffinity {
|
||||
/**
|
||||
* @return an integer indicating the affinity of your actor for the given project. The actor that returns
|
||||
* the highest affinity gets selected.
|
||||
*/
|
||||
fun affinity(project: Project, context: KobaltContext) : Int
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package com.beust.kobalt.api
|
||||
|
||||
/**
|
||||
* Plugins that create projects need to implement this interface.
|
||||
*/
|
||||
interface IProjectContributor : IContributor {
|
||||
fun projects() : List<ProjectDescription>
|
||||
}
|
||||
|
||||
class ProjectDescription(val project: Project, val dependsOn: List<Project>)
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.beust.kobalt.api
|
||||
|
||||
import com.beust.kobalt.HostConfig
|
||||
|
||||
/**
|
||||
* Plugins that add their own repos.
|
||||
*/
|
||||
interface IRepoContributor : IContributor {
|
||||
/**
|
||||
* Note that the project passed might be null because this contributor is called twice:
|
||||
* before the build file gets parsed (so we don't have any projects yet) and after the
|
||||
* build file has been parsed (then it gets called once for each project discovered).
|
||||
*/
|
||||
fun reposFor(project: Project?) : List<HostConfig>
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.beust.kobalt.api
|
||||
|
||||
import com.beust.kobalt.TaskResult
|
||||
import com.beust.kobalt.api.IClasspathDependency
|
||||
|
||||
/**
|
||||
* Plugins that can run a project (task "run" or "test") should implement this interface.
|
||||
*/
|
||||
interface IRunnerContributor : IContributor, IProjectAffinity {
|
||||
/**
|
||||
* Run the project.
|
||||
*/
|
||||
fun run(project: Project, context: KobaltContext, classpath: List<IClasspathDependency>) : TaskResult
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package com.beust.kobalt.api
|
||||
|
||||
/**
|
||||
* An affinity interface that gets run without a project nor a context.
|
||||
*/
|
||||
interface ISimpleAffinity<T> : IAffinity {
|
||||
/**
|
||||
* @return an integer indicating the affinity of your actor. The actor that returns
|
||||
* the highest affinity gets selected.
|
||||
*/
|
||||
fun affinity(arg: T) : Int
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package com.beust.kobalt.api
|
||||
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Plug-ins can alter the source directories by implementing this interface.
|
||||
*/
|
||||
interface ISourceDirectoryIncerceptor : IInterceptor {
|
||||
fun intercept(project: Project, context: KobaltContext, sourceDirectories: List<File>) : List<File>
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.beust.kobalt.api
|
||||
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Plug-ins that add source directories to be compiled need to implement this interface.
|
||||
*/
|
||||
interface ISourceDirectoryContributor {
|
||||
fun sourceDirectoriesFor(project: Project, context: KobaltContext): List<File>
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package com.beust.kobalt.api
|
||||
|
||||
import com.beust.kobalt.TaskResult
|
||||
|
||||
/**
|
||||
* Plug-ins that need to add dynamic tasks (tasks that are not methods annotated with @Task) need
|
||||
* to implement this interface.
|
||||
*/
|
||||
interface ITaskContributor : IContributor {
|
||||
fun tasksFor(context: KobaltContext) : List<DynamicTask>
|
||||
}
|
||||
|
||||
class DynamicTask(val plugin: IPlugin, val name: String, val description: String = "",
|
||||
val runBefore: List<String> = listOf<String>(),
|
||||
val runAfter: List<String> = listOf<String>(),
|
||||
val alwaysRunAfter: List<String> = listOf<String>(),
|
||||
val closure: (Project) -> TaskResult)
|
|
@ -0,0 +1,14 @@
|
|||
package com.beust.kobalt.api
|
||||
|
||||
import com.beust.kobalt.TaskResult
|
||||
|
||||
/**
|
||||
* Plugins that can run a project (task "run" or "test") should implement this interface.
|
||||
*/
|
||||
interface ITestRunnerContributor : IContributor, IProjectAffinity {
|
||||
/**
|
||||
* Run the project.
|
||||
*/
|
||||
fun run(project: Project, context: KobaltContext, classpath: List<IClasspathDependency>) : TaskResult
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.beust.kobalt.api
|
||||
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Plug-ins that add tets source directories to be compiled need to implement this interface.
|
||||
*/
|
||||
interface ITestSourceDirectoryContributor {
|
||||
fun testSourceDirectoriesFor(project: Project, context: KobaltContext): List<File>
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.beust.kobalt.api
|
||||
|
||||
import com.beust.kobalt.maven.DepFactory
|
||||
import com.beust.kobalt.misc.KobaltExecutors
|
||||
import java.io.File
|
||||
import java.util.concurrent.Future
|
||||
|
||||
public class JarFinder {
|
||||
companion object {
|
||||
/**
|
||||
* @return a Future for the jar file corresponding to this id.
|
||||
*/
|
||||
fun byIdFuture(id: String) : Future<File> {
|
||||
val executor = Kobalt.INJECTOR.getInstance(KobaltExecutors::class.java).miscExecutor
|
||||
val depFactory = Kobalt.INJECTOR.getInstance(DepFactory::class.java)
|
||||
return depFactory.create(id, executor).jarFile
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the jar file corresponding to this id. This might cause a network call.
|
||||
*/
|
||||
fun byId(id: String) = byIdFuture(id).get()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
package com.beust.kobalt.api
|
||||
|
||||
import com.beust.kobalt.Constants
|
||||
import com.beust.kobalt.HostConfig
|
||||
import com.beust.kobalt.Plugins
|
||||
import com.google.inject.Injector
|
||||
import java.io.InputStream
|
||||
import java.util.*
|
||||
|
||||
public class Kobalt {
|
||||
companion object {
|
||||
// This injector will eventually be replaced with a different injector initialized with the
|
||||
// correct arguments (or with a TestModule) but it's necessary to give it a default value
|
||||
// here so the kobalt-plugin.xml file can be read since this is done very early
|
||||
lateinit var INJECTOR : Injector
|
||||
|
||||
var context: KobaltContext? = null
|
||||
|
||||
val repos = HashSet<HostConfig>(Constants.DEFAULT_REPOS.map { HostConfig(it) })
|
||||
|
||||
fun addRepo(repo: HostConfig) = repos.add(
|
||||
if (repo.url.endsWith("/")) repo
|
||||
else repo.copy(url = (repo.url + "/")))
|
||||
|
||||
private val PROPERTY_KOBALT_VERSION = "kobalt.version"
|
||||
private val KOBALT_PROPERTIES = "kobalt.properties"
|
||||
|
||||
/** kobalt.properties */
|
||||
private val kobaltProperties: Properties by lazy { readProperties() }
|
||||
|
||||
/**
|
||||
* Read the content of kobalt.properties
|
||||
*/
|
||||
private fun readProperties() : Properties {
|
||||
val result = Properties()
|
||||
|
||||
// kobalt.properties is internal to Kobalt
|
||||
val url = Kobalt::class.java.classLoader.getResource(KOBALT_PROPERTIES)
|
||||
if (url != null) {
|
||||
readProperties(result, url.openConnection().inputStream)
|
||||
} else {
|
||||
throw AssertionError("Couldn't find $KOBALT_PROPERTIES")
|
||||
}
|
||||
|
||||
// local.properties can be used by external users
|
||||
// Paths.get(LOCAL_PROPERTIES).let { path ->
|
||||
// if (Files.exists(path)) {
|
||||
// Files.newInputStream(path).use {
|
||||
// readProperties(result, it)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private fun readProperties(properties: Properties, ins: InputStream) {
|
||||
properties.load(ins)
|
||||
ins.close()
|
||||
properties.forEach { es -> System.setProperty(es.key.toString(), es.value.toString()) }
|
||||
}
|
||||
|
||||
val version = kobaltProperties.getProperty(PROPERTY_KOBALT_VERSION)
|
||||
|
||||
fun findPlugin(name: String) = Plugins.findPlugin(name)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package com.beust.kobalt.api
|
||||
|
||||
import com.beust.kobalt.Args
|
||||
import com.beust.kobalt.Plugins
|
||||
import com.beust.kobalt.Variant
|
||||
import com.beust.kobalt.internal.PluginInfo
|
||||
import com.beust.kobalt.maven.DependencyManager
|
||||
import com.beust.kobalt.misc.KobaltExecutors
|
||||
|
||||
public class KobaltContext(val args: Args) {
|
||||
var variant: Variant = Variant()
|
||||
val profiles = arrayListOf<String>()
|
||||
|
||||
init {
|
||||
args.profiles?.split(",")?.filterNotNull()?.forEach {
|
||||
profiles.add(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun findPlugin(name: String) = Plugins.findPlugin(name)
|
||||
|
||||
//
|
||||
// Injected
|
||||
//
|
||||
lateinit var pluginInfo: PluginInfo
|
||||
lateinit var pluginProperties: PluginProperties
|
||||
lateinit var dependencyManager: DependencyManager
|
||||
lateinit var executors: KobaltExecutors
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.beust.kobalt.api
|
||||
|
||||
import com.google.inject.Inject
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Plugins can publish to and read from this object in order to exchange information. Keys stored in
|
||||
* these maps should be annotated with @ExportedPluginProperty.
|
||||
*/
|
||||
class PluginProperties @Inject constructor() {
|
||||
private val pluginProperties = hashMapOf<String, HashMap<String, Any>>()
|
||||
|
||||
fun put(pluginName: String, key: String, value: Any) =
|
||||
pluginProperties.getOrPut(pluginName) { hashMapOf<String, Any>() }.put(key, value)
|
||||
|
||||
fun get(pluginName: String, key: String) = pluginProperties[pluginName]?.get(key)
|
||||
|
||||
fun getString(pluginName: String, key: String) = get(pluginName, key) as String
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package com.beust.kobalt.api
|
||||
|
||||
import com.beust.kobalt.internal.TaskResult2
|
||||
import com.beust.kobalt.misc.toString
|
||||
import java.util.concurrent.Callable
|
||||
|
||||
abstract public class PluginTask : Callable<TaskResult2<PluginTask>> {
|
||||
abstract val plugin: IPlugin
|
||||
open val name: String = ""
|
||||
open val doc: String = ""
|
||||
abstract val project: Project
|
||||
|
||||
override public fun toString() : String {
|
||||
return toString("PluginTask", "id", project.name + ":" + name)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,208 @@
|
|||
package com.beust.kobalt.api
|
||||
|
||||
import com.beust.kobalt.api.annotation.Directive
|
||||
import com.beust.kobalt.internal.IProjectInfo
|
||||
import com.beust.kobalt.maven.dependency.MavenDependency
|
||||
import com.beust.kobalt.misc.KFiles
|
||||
import java.util.*
|
||||
|
||||
open class Project(
|
||||
@Directive open var name: String,
|
||||
@Directive open var version: String? = null,
|
||||
@Directive open var directory: String = ".",
|
||||
@Directive open var buildDirectory: String = KFiles.KOBALT_BUILD_DIR,
|
||||
@Directive open var group: String? = null,
|
||||
@Directive open var artifactId: String? = null,
|
||||
@Directive open var packaging: String? = null,
|
||||
@Directive open var dependencies: Dependencies? = null,
|
||||
@Directive open var sourceSuffix : String = "",
|
||||
@Directive open var description : String = "",
|
||||
@Directive open var scm : Scm? = null,
|
||||
@Directive open var url: String? = null,
|
||||
@Directive open var licenses: List<License> = arrayListOf<License>(),
|
||||
@Directive open var packageName: String? = group,
|
||||
val projectInfo: IProjectInfo) : IBuildConfig {
|
||||
|
||||
override var buildConfig : BuildConfig? = null //BuildConfig()
|
||||
|
||||
val testArgs = arrayListOf<String>()
|
||||
val testJvmArgs = arrayListOf<String>()
|
||||
|
||||
val projectProperties = ProjectProperties()
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return name == (other as Project).name
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return name.hashCode()
|
||||
}
|
||||
|
||||
//
|
||||
// Directories
|
||||
//
|
||||
|
||||
@Directive
|
||||
fun sourceDirectories(init: Sources.() -> Unit) : Sources {
|
||||
val sources = Sources(this, sourceDirectories)
|
||||
sources.init()
|
||||
return sources
|
||||
}
|
||||
|
||||
var sourceDirectories : HashSet<String> = hashSetOf()
|
||||
get() = if (field.isEmpty()) projectInfo.defaultSourceDirectories else field
|
||||
set(value) {
|
||||
field = value
|
||||
}
|
||||
|
||||
@Directive
|
||||
fun sourceDirectoriesTest(init: Sources.() -> Unit) : Sources {
|
||||
val sources = Sources(this, sourceDirectoriesTest)
|
||||
sources.init()
|
||||
return sources
|
||||
}
|
||||
|
||||
var sourceDirectoriesTest : HashSet<String> = hashSetOf()
|
||||
get() = if (field.isEmpty()) projectInfo.defaultTestDirectories
|
||||
else field
|
||||
set(value) {
|
||||
field = value
|
||||
}
|
||||
|
||||
//
|
||||
// Dependencies
|
||||
//
|
||||
|
||||
@Directive
|
||||
fun dependencies(init: Dependencies.() -> Unit) : Dependencies {
|
||||
dependencies = Dependencies(this, compileDependencies, compileProvidedDependencies, compileRuntimeDependencies)
|
||||
dependencies!!.init()
|
||||
return dependencies!!
|
||||
}
|
||||
|
||||
val compileDependencies : ArrayList<IClasspathDependency> = arrayListOf()
|
||||
val compileProvidedDependencies : ArrayList<IClasspathDependency> = arrayListOf()
|
||||
val compileRuntimeDependencies : ArrayList<IClasspathDependency> = arrayListOf()
|
||||
|
||||
@Directive
|
||||
fun dependenciesTest(init: Dependencies.() -> Unit) : Dependencies {
|
||||
dependencies = Dependencies(this, testDependencies, testProvidedDependencies, compileRuntimeDependencies)
|
||||
dependencies!!.init()
|
||||
return dependencies!!
|
||||
}
|
||||
|
||||
val testDependencies : ArrayList<IClasspathDependency> = arrayListOf()
|
||||
val testProvidedDependencies : ArrayList<IClasspathDependency> = arrayListOf()
|
||||
|
||||
/** Used to disambiguate various name properties */
|
||||
@Directive
|
||||
val projectName: String get() = name
|
||||
|
||||
val productFlavors = hashMapOf<String, ProductFlavorConfig>()
|
||||
|
||||
fun addProductFlavor(name: String, pf: ProductFlavorConfig) {
|
||||
productFlavors.put(name, pf)
|
||||
}
|
||||
|
||||
val buildTypes = hashMapOf<String, BuildTypeConfig>()
|
||||
|
||||
fun addBuildType(name: String, bt: BuildTypeConfig) {
|
||||
buildTypes.put(name, bt)
|
||||
}
|
||||
|
||||
fun classesDir(context: KobaltContext): String {
|
||||
val initial = KFiles.joinDir(buildDirectory, "classes")
|
||||
val result = context.pluginInfo.buildDirectoryInterceptors.fold(initial, { dir, intercept ->
|
||||
intercept.intercept(this, context, dir)
|
||||
})
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
class Sources(val project: Project, val sources: HashSet<String>) {
|
||||
@Directive
|
||||
fun path(vararg paths: String) {
|
||||
sources.addAll(paths)
|
||||
}
|
||||
}
|
||||
|
||||
class Dependencies(val project: Project, val dependencies: ArrayList<IClasspathDependency>,
|
||||
val providedDependencies: ArrayList<IClasspathDependency>,
|
||||
val runtimeDependencies: ArrayList<IClasspathDependency>) {
|
||||
@Directive
|
||||
fun compile(vararg dep: String) {
|
||||
dep.forEach { dependencies.add(MavenDependency.create(it)) }
|
||||
}
|
||||
|
||||
@Directive
|
||||
fun provided(vararg dep: String) {
|
||||
dep.forEach { providedDependencies.add(MavenDependency.create(it))}
|
||||
}
|
||||
|
||||
@Directive
|
||||
fun runtime(vararg dep: String) {
|
||||
dep.forEach { runtimeDependencies.add(MavenDependency.create(it))}
|
||||
}
|
||||
}
|
||||
|
||||
class Scm(val connection: String, val developerConnection: String, val url: String)
|
||||
|
||||
class License(val name: String, val url: String) {
|
||||
fun toMavenLicense() : org.apache.maven.model.License {
|
||||
val result = org.apache.maven.model.License()
|
||||
result.name = name
|
||||
result.url = url
|
||||
return result
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class BuildConfig {
|
||||
class Field(val name: String, val type: String, val value: Any)
|
||||
|
||||
val fields = arrayListOf<Field>()
|
||||
|
||||
fun field(type: String, name: String, value: Any) {
|
||||
fields.add(Field(name, type, value))
|
||||
}
|
||||
}
|
||||
|
||||
interface IBuildConfig {
|
||||
var buildConfig: BuildConfig?
|
||||
|
||||
fun buildConfig(init: BuildConfig.() -> Unit) {
|
||||
buildConfig = BuildConfig().apply {
|
||||
init()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ProductFlavorConfig(val name: String) : IBuildConfig {
|
||||
var applicationId: String? = null
|
||||
override var buildConfig : BuildConfig? = BuildConfig()
|
||||
}
|
||||
|
||||
@Directive
|
||||
fun Project.productFlavor(name: String, init: ProductFlavorConfig.() -> Unit) = ProductFlavorConfig(name).apply {
|
||||
init()
|
||||
addProductFlavor(name, this)
|
||||
}
|
||||
|
||||
class BuildTypeConfig(val project: Project?, val name: String) : IBuildConfig {
|
||||
var minifyEnabled = false
|
||||
var applicationIdSuffix: String? = null
|
||||
var proguardFile: String? = null
|
||||
|
||||
// fun getDefaultProguardFile(name: String) : String {
|
||||
// val androidPlugin = Plugins.findPlugin(AndroidPlugin.PLUGIN_NAME) as AndroidPlugin
|
||||
// return Proguard(androidPlugin.androidHome(project)).getDefaultProguardFile(name)
|
||||
// }
|
||||
|
||||
override var buildConfig : BuildConfig? = BuildConfig()
|
||||
}
|
||||
|
||||
@Directive
|
||||
fun Project.buildType(name: String, init: BuildTypeConfig.() -> Unit) = BuildTypeConfig(this, name).apply {
|
||||
init()
|
||||
addBuildType(name, this)
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package com.beust.kobalt.api
|
||||
|
||||
/**
|
||||
* Plugins can add and read properties from the project by using this class, found on the Project class.
|
||||
* Keys stored in this map by plug-ins should be annotated with @ExportedProjectProperty.
|
||||
*/
|
||||
class ProjectProperties {
|
||||
private val properties = hashMapOf<String, Any>()
|
||||
|
||||
fun put(key: String, value: Any) = properties.put(key, value)
|
||||
|
||||
fun get(key: String) = properties[key]
|
||||
|
||||
fun getString(key: String) : String? {
|
||||
val result = get(key)
|
||||
return if (result != null) result as String
|
||||
else null
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.beust.kobalt.api
|
||||
|
||||
import com.beust.kobalt.misc.toString
|
||||
|
||||
data public class Task(val pluginName: String, val taskName: String) {
|
||||
override public fun toString() : String {
|
||||
return toString("Task", pluginName, taskName)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
package com.beust.kobalt.api
|
||||
|
||||
import com.beust.kobalt.TaskResult
|
||||
import com.beust.kobalt.Variant
|
||||
|
||||
/**
|
||||
* Plug-ins that are ITaskContributor can use this class to manage their collection of tasks and
|
||||
* implement the interface by delegating to an instance of this class (if injection permits).
|
||||
*/
|
||||
class TaskContributor : ITaskContributor {
|
||||
val dynamicTasks = arrayListOf<DynamicTask>()
|
||||
|
||||
/**
|
||||
* Register dynamic tasks corresponding to the variants found in the project,e.g. assembleDevDebug,
|
||||
* assembleDevRelease, etc...
|
||||
*
|
||||
* TODO: this should be done automatically so that users don't have to invoke it themselves.
|
||||
* Certain tasks could have a boolean flag "hasVariants" and any task that depends on it automatically
|
||||
* depends on variants of that task.
|
||||
*/
|
||||
fun addVariantTasks(plugin: IPlugin, project: Project, context: KobaltContext, taskName: String,
|
||||
runBefore : List<String> = emptyList(),
|
||||
runAfter : List<String> = emptyList(),
|
||||
alwaysRunAfter : List<String> = emptyList(),
|
||||
runTask: (Project) -> TaskResult) {
|
||||
Variant.allVariants(project).forEach { variant ->
|
||||
val variantTaskName = variant.toTask(taskName)
|
||||
dynamicTasks.add(DynamicTask(plugin, variantTaskName, variantTaskName,
|
||||
runBefore = runBefore.map { variant.toTask(it) },
|
||||
runAfter = runAfter.map { variant.toTask(it) },
|
||||
alwaysRunAfter = alwaysRunAfter.map { variant.toTask(it) },
|
||||
closure = { p: Project ->
|
||||
context.variant = variant
|
||||
runTask(project)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
override fun tasksFor(context: KobaltContext) : List<DynamicTask> = dynamicTasks
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package com.beust.kobalt.api.annotation
|
||||
|
||||
/**
|
||||
* Plugins that export directives should annotated those with this annotation so they can be documented and also
|
||||
* receive special treatment for auto completion in the plug-in.
|
||||
*/
|
||||
annotation class Directive
|
||||
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class Task(
|
||||
val name: String,
|
||||
val description: String = "",
|
||||
|
||||
/** Tasks that this task depends on */
|
||||
val runBefore: Array<String> = arrayOf(),
|
||||
|
||||
/** Tasks that this task will run after if they get run */
|
||||
val runAfter: Array<String> = arrayOf(),
|
||||
|
||||
/** Tasks that this task will always run after */
|
||||
val alwaysRunAfter: Array<String> = arrayOf()
|
||||
)
|
||||
|
||||
/**
|
||||
* Plugins that export properties should annotate those with this annotation so they can be documented.
|
||||
*/
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class ExportedPluginProperty(
|
||||
/** Documentation for this property */
|
||||
val doc: String = "",
|
||||
|
||||
/** The type of this property */
|
||||
val type: String = ""
|
||||
)
|
||||
|
||||
/**
|
||||
* Plugins that export properties on the Project instance should annotate those with this annotation so
|
||||
* they can be documented.
|
||||
*/
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class ExportedProjectProperty(
|
||||
/** Documentation for this property */
|
||||
val doc: String = "",
|
||||
|
||||
/** The type of this property */
|
||||
val type: String = ""
|
||||
)
|
|
@ -0,0 +1,22 @@
|
|||
package com.beust.kobalt.internal
|
||||
|
||||
import com.beust.kobalt.api.IProjectAffinity
|
||||
import com.beust.kobalt.api.ISimpleAffinity
|
||||
import com.beust.kobalt.api.KobaltContext
|
||||
import com.beust.kobalt.api.Project
|
||||
|
||||
class ActorUtils {
|
||||
companion object {
|
||||
/**
|
||||
* Return the plug-in actor with the highest affinity.
|
||||
*/
|
||||
fun <T : IProjectAffinity> selectAffinityActor(project: Project, context: KobaltContext, actors: List<T>)
|
||||
= actors.maxBy { it.affinity(project, context) }
|
||||
|
||||
/**
|
||||
* Return the plug-in actor with the highest affinity.
|
||||
*/
|
||||
fun <T : ISimpleAffinity<A>, A> selectAffinityActor(actors: List<T>, arg: A) = actors.maxBy { it.affinity(arg) }
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package com.beust.kobalt.internal
|
||||
|
||||
class DocUrl {
|
||||
companion object {
|
||||
private const val HOST = "http://beust.com/kobalt/"
|
||||
private fun url(path: String) = HOST + path
|
||||
|
||||
val PUBLISH_PLUGIN_URL = url("plug-ins/index.html#publishing")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,285 @@
|
|||
package com.beust.kobalt.internal
|
||||
|
||||
import com.beust.kobalt.KobaltException
|
||||
import com.beust.kobalt.TaskResult
|
||||
import com.beust.kobalt.misc.NamedThreadFactory
|
||||
import com.beust.kobalt.misc.error
|
||||
import com.beust.kobalt.misc.log
|
||||
import com.beust.kobalt.misc.toString
|
||||
import com.google.common.collect.HashMultimap
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.util.concurrent.*
|
||||
|
||||
open class TaskResult2<T>(success: Boolean, errorMessage: String?, val value: T) : TaskResult(success, errorMessage) {
|
||||
override fun toString() = toString("TaskResult", "value", value, "success", success)
|
||||
}
|
||||
|
||||
public interface IWorker<T> : Callable<TaskResult2<T>> {
|
||||
/**
|
||||
* @return list of tasks this worker is working on.
|
||||
*/
|
||||
// val tasks : List<T>
|
||||
|
||||
/**
|
||||
* @return the priority of this task.
|
||||
*/
|
||||
val priority : Int
|
||||
}
|
||||
|
||||
public interface IThreadWorkerFactory<T> {
|
||||
|
||||
/**
|
||||
* Creates {@code IWorker} for specified set of tasks. It is not necessary that
|
||||
* number of workers returned be same as number of tasks entered.
|
||||
*
|
||||
* @param nodes tasks that need to be executed
|
||||
* @return list of workers
|
||||
*/
|
||||
fun createWorkers(nodes: List<T>) : List<IWorker<T>>
|
||||
}
|
||||
|
||||
public class DynamicGraphExecutor<T>(val graph: DynamicGraph<T>,
|
||||
val factory: IThreadWorkerFactory<T>) {
|
||||
val executor = Executors.newFixedThreadPool(5, NamedThreadFactory("DynamicGraphExecutor"))
|
||||
val completion = ExecutorCompletionService<TaskResult2<T>>(executor)
|
||||
|
||||
/**
|
||||
* @return 0 if all went well, > 0 otherwise
|
||||
*/
|
||||
public fun run() : Int {
|
||||
var lastResult = TaskResult()
|
||||
var gotError = false
|
||||
var nodesRunning = 0
|
||||
while (graph.freeNodes.size > 0 && ! gotError) {
|
||||
log(3, "Current node count: ${graph.nodeCount}")
|
||||
synchronized(graph) {
|
||||
val freeNodes = graph.freeNodes
|
||||
freeNodes.forEach { graph.setStatus(it, DynamicGraph.Status.RUNNING)}
|
||||
log(3, " ==> Submitting " + freeNodes)
|
||||
val callables : List<IWorker<T>> = factory.createWorkers(freeNodes)
|
||||
callables.forEach { completion.submit(it) }
|
||||
nodesRunning += callables.size
|
||||
|
||||
// When a callable ends, see if it freed a node. If not, keep looping
|
||||
while (graph.nodesRunning.size > 0 && graph.freeNodes.size == 0 && ! gotError) {
|
||||
try {
|
||||
val future = completion.take()
|
||||
val taskResult = future.get(2, TimeUnit.SECONDS)
|
||||
lastResult = taskResult
|
||||
log(3, " <== Received task result $taskResult")
|
||||
graph.setStatus(taskResult.value,
|
||||
if (taskResult.success) {
|
||||
DynamicGraph.Status.FINISHED
|
||||
} else {
|
||||
DynamicGraph.Status.ERROR
|
||||
})
|
||||
} catch(ex: TimeoutException) {
|
||||
log(2, "Time out")
|
||||
} catch(ex: Exception) {
|
||||
if (ex.cause is InvocationTargetException) {
|
||||
val ite = ex.cause
|
||||
if (ite.targetException is KobaltException) {
|
||||
throw (ex.cause as InvocationTargetException).targetException
|
||||
} else {
|
||||
error("Error: ${ite.cause?.message}", ite.cause)
|
||||
gotError = true
|
||||
}
|
||||
} else {
|
||||
error("Error: ${ex.message}", ex)
|
||||
gotError = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
executor.shutdown()
|
||||
if (graph.freeNodes.size == 0 && graph.nodesReady.size > 0) {
|
||||
throw KobaltException("Couldn't find any free nodes but a few nodes still haven't run, there is " +
|
||||
"a cycle in the dependencies.\n Nodes left: " + graph.dump(graph.nodesReady))
|
||||
}
|
||||
return if (lastResult.success) 0 else 1
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Representation of the graph of methods.
|
||||
*/
|
||||
public class DynamicGraph<T> {
|
||||
val nodesReady = linkedSetOf<T>()
|
||||
val nodesRunning = linkedSetOf<T>()
|
||||
private val nodesFinished = linkedSetOf<T>()
|
||||
private val nodesInError = linkedSetOf<T>()
|
||||
private val nodesSkipped = linkedSetOf<T>()
|
||||
private val dependedUpon = HashMultimap.create<T, T>()
|
||||
private val dependingOn = HashMultimap.create<T, T>()
|
||||
|
||||
/**
|
||||
* Define a comparator for the nodes of this graph, which will be used
|
||||
* to order the free nodes when they are asked.
|
||||
*/
|
||||
// public val comparator : Comparator<T>? = null
|
||||
|
||||
enum class Status {
|
||||
READY, RUNNING, FINISHED, ERROR, SKIPPED
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a node to the graph.
|
||||
*/
|
||||
public fun addNode(value: T) : T {
|
||||
nodes.add(value)
|
||||
nodesReady.add(value)
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an edge between two nodes, which don't have to already be in the graph
|
||||
* (they will be added by this method). Makes "to" depend on "from".
|
||||
*/
|
||||
public fun addEdge(from: T, to: T) {
|
||||
nodes.add(from)
|
||||
nodes.add(to)
|
||||
val fromNode = addNode(from)
|
||||
val toNode = addNode(to)
|
||||
dependingOn.put(toNode, fromNode)
|
||||
dependedUpon.put(fromNode, toNode)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a set of all the nodes that don't depend on any other nodes.
|
||||
*/
|
||||
public val freeNodes : List<T>
|
||||
get() {
|
||||
val result = arrayListOf<T>()
|
||||
nodesReady.forEach { m ->
|
||||
// A node is free if...
|
||||
|
||||
val du = dependedUpon.get(m)
|
||||
// - no other nodes depend on it
|
||||
if (! dependedUpon.containsKey(m)) {
|
||||
result.add(m)
|
||||
} else if (getUnfinishedNodes(du).size == 0) {
|
||||
result.add(m)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the free nodes if requested (e.g. priorities)
|
||||
// if (! result.isEmpty()) {
|
||||
// if (comparator != null) {
|
||||
// Collections.sort(result, comparator)
|
||||
// debug("Nodes after sorting:" + result.get(0))
|
||||
// }
|
||||
// }
|
||||
|
||||
log(3, " freeNodes: $result")
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a list of all the nodes that have a status other than FINISHED.
|
||||
*/
|
||||
private fun getUnfinishedNodes(nodes: Set<T>) : Collection<T> {
|
||||
val result = hashSetOf<T>()
|
||||
nodes.forEach { node ->
|
||||
if (nodesReady.contains(node) || nodesRunning.contains(node)) {
|
||||
result.add(node);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the status for a set of nodes.
|
||||
*/
|
||||
public fun setStatus(nodes: Collection<T>, status: Status) {
|
||||
nodes.forEach { setStatus(it, status) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all dependees of this node SKIPPED
|
||||
*/
|
||||
private fun setSkipStatus(node: T, status: Status) {
|
||||
dependingOn.get(node).forEach {
|
||||
if (! nodesSkipped.contains(it)) {
|
||||
log(3, "Node skipped: $it")
|
||||
nodesSkipped.add(it)
|
||||
nodesReady.remove(it)
|
||||
setSkipStatus(it, status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the status for a node.
|
||||
*/
|
||||
public fun setStatus(node: T, status: Status) {
|
||||
removeNode(node);
|
||||
when(status) {
|
||||
Status.READY -> nodesReady.add(node)
|
||||
Status.RUNNING -> nodesRunning.add(node)
|
||||
Status.FINISHED -> nodesFinished.add(node)
|
||||
Status.ERROR -> {
|
||||
log(3, "Node in error: $node")
|
||||
nodesReady.remove(node)
|
||||
nodesInError.add(node)
|
||||
setSkipStatus(node, status)
|
||||
}
|
||||
else -> {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeNode(node: T) {
|
||||
if (! nodesReady.remove(node)) {
|
||||
if (! nodesRunning.remove(node)) {
|
||||
nodesFinished.remove(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the number of nodes in this graph.
|
||||
*/
|
||||
public val nodeCount: Int
|
||||
get() = nodesReady.size + nodesRunning.size + nodesFinished.size
|
||||
|
||||
override public fun toString() : String {
|
||||
val result = StringBuilder("[DynamicGraph ")
|
||||
result.append("\n Ready:" + nodesReady)
|
||||
result.append("\n Running:" + nodesRunning)
|
||||
result.append("\n Finished:" + nodesFinished)
|
||||
result.append("\n Edges:\n")
|
||||
// dependingOn.entrySet().forEach { es ->
|
||||
// result.append(" " + es.getKey() + "\n");
|
||||
// es.getValue().forEach { t ->
|
||||
// result.append(" " + t + "\n");
|
||||
// }
|
||||
// }
|
||||
result.append("]");
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
val nodes = hashSetOf<T>()
|
||||
|
||||
fun dump(nodes: Collection<T>) : String {
|
||||
val result = StringBuffer()
|
||||
result.append("************ Graph dump ***************\n")
|
||||
val free = arrayListOf<T>()
|
||||
nodes.forEach { node ->
|
||||
val d = dependedUpon.get(node)
|
||||
if (d == null || d.isEmpty()) {
|
||||
free.add(node)
|
||||
}
|
||||
}
|
||||
|
||||
result.append("Free nodes: $free").append("\n Dependent nodes:\n")
|
||||
nodes.forEach {
|
||||
result.append(" $it -> ${dependedUpon.get(it)}\n")
|
||||
}
|
||||
return result.toString()
|
||||
}
|
||||
|
||||
fun dump() = dump(nodesReady)
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
package com.beust.kobalt.internal
|
||||
|
||||
import com.beust.kobalt.JavaInfo
|
||||
import com.beust.kobalt.SystemProperties
|
||||
import com.beust.kobalt.TaskResult
|
||||
import com.beust.kobalt.api.*
|
||||
import com.beust.kobalt.misc.KFiles
|
||||
import com.beust.kobalt.misc.log
|
||||
import java.io.File
|
||||
import java.net.URLClassLoader
|
||||
|
||||
/**
|
||||
* Base class for testing frameworks that are invoked from a main class with arguments. Test runners can
|
||||
* subclass this class and override mainClass, args and the name of the dependency that should trigger this runner.
|
||||
*/
|
||||
abstract class GenericTestRunner : ITestRunnerContributor {
|
||||
abstract val dependencyName : String
|
||||
abstract val mainClass: String
|
||||
abstract fun args(project: Project, classpath: List<IClasspathDependency>) : List<String>
|
||||
|
||||
override fun run(project: Project, context: KobaltContext, classpath: List<IClasspathDependency>)
|
||||
= TaskResult(runTests(project, classpath))
|
||||
|
||||
override fun affinity(project: Project, context: KobaltContext) =
|
||||
if (project.testDependencies.any { it.id.contains(dependencyName)}) IAffinity.DEFAULT_POSITIVE_AFFINITY
|
||||
else 0
|
||||
|
||||
protected fun findTestClasses(project: Project, classpath: List<IClasspathDependency>): List<String> {
|
||||
val path = KFiles.joinDir(project.directory, project.buildDirectory, KFiles.TEST_CLASSES_DIR)
|
||||
val result = KFiles.findRecursively(File(path), arrayListOf(File(".")), {
|
||||
file -> file.endsWith(".class")
|
||||
}).map {
|
||||
it.replace("/", ".").replace("\\", ".").replace(".class", "").substring(2)
|
||||
}.filter {
|
||||
try {
|
||||
// Only keep classes with a parameterless constructor
|
||||
val urls = arrayOf(File(path).toURI().toURL()) +
|
||||
classpath.map { it.jarFile.get().toURI().toURL() }
|
||||
URLClassLoader(urls).loadClass(it).getConstructor()
|
||||
true
|
||||
} catch(ex: Exception) {
|
||||
log(2, "Skipping non test class $it: ${ex.message}")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
log(2, "Found ${result.size} test classes")
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if all the tests passed
|
||||
*/
|
||||
fun runTests(project: Project, classpath: List<IClasspathDependency>) : Boolean {
|
||||
val jvm = JavaInfo.create(File(SystemProperties.javaBase))
|
||||
val java = jvm.javaExecutable
|
||||
val args = args(project, classpath)
|
||||
if (args.size > 0) {
|
||||
val allArgs = arrayListOf<String>().apply {
|
||||
add(java!!.absolutePath)
|
||||
addAll(project.testJvmArgs)
|
||||
add("-classpath")
|
||||
add(classpath.map { it.jarFile.get().absolutePath }.joinToString(File.pathSeparator))
|
||||
add(mainClass)
|
||||
addAll(args)
|
||||
}
|
||||
|
||||
val pb = ProcessBuilder(allArgs)
|
||||
pb.directory(File(project.directory))
|
||||
pb.inheritIO()
|
||||
log(1, "Running tests with classpath size ${classpath.size}")
|
||||
log(2, "Launching " + allArgs.joinToString(" "))
|
||||
val process = pb.start()
|
||||
val errorCode = process.waitFor()
|
||||
if (errorCode == 0) {
|
||||
log(1, "All tests passed")
|
||||
} else {
|
||||
log(1, "Test failures")
|
||||
}
|
||||
return errorCode == 0
|
||||
} else {
|
||||
log(2, "Couldn't find any test classes")
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.beust.kobalt.internal
|
||||
|
||||
import com.beust.kobalt.api.IClasspathDependency
|
||||
import com.beust.kobalt.api.Project
|
||||
|
||||
open public class JUnitRunner() : GenericTestRunner() {
|
||||
|
||||
override val mainClass = "org.junit.runner.JUnitCore"
|
||||
|
||||
override val dependencyName = "junit"
|
||||
|
||||
override fun args(project: Project, classpath: List<IClasspathDependency>) = findTestClasses(project, classpath)
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package com.beust.kobalt.internal
|
||||
|
||||
import com.beust.kobalt.KobaltException
|
||||
import com.beust.kobalt.TaskResult
|
||||
import com.beust.kobalt.api.CompilerActionInfo
|
||||
import com.beust.kobalt.api.KobaltContext
|
||||
import com.beust.kobalt.api.Project
|
||||
import com.beust.kobalt.maven.DependencyManager
|
||||
import com.google.inject.Inject
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Abstract the compilation process by running an ICompilerAction parameterized by a CompilerActionInfo.
|
||||
* Also validates the classpath and run all the contributors.
|
||||
*/
|
||||
class JvmCompiler @Inject constructor(val dependencyManager: DependencyManager) {
|
||||
|
||||
/**
|
||||
* Take the given CompilerActionInfo and enrich it with all the applicable contributors and
|
||||
* then pass it to the ICompilerAction.
|
||||
*/
|
||||
fun doCompile(project: Project?, context: KobaltContext?, action: ICompilerAction, info: CompilerActionInfo)
|
||||
: TaskResult {
|
||||
|
||||
// Dependencies
|
||||
val allDependencies = (info.dependencies
|
||||
+ dependencyManager.calculateDependencies(project, context!!, allDependencies = info.dependencies))
|
||||
.distinct()
|
||||
|
||||
// Plugins that add flags to the compiler
|
||||
val contributorFlags = if (project != null) {
|
||||
context.pluginInfo.compilerFlagContributors.flatMap {
|
||||
it.flagsFor(project, context, info.compilerArgs)
|
||||
}
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
val addedFlags = contributorFlags + ArrayList(info.compilerArgs)
|
||||
|
||||
validateClasspath(allDependencies.map { it.jarFile.get().absolutePath })
|
||||
return action.compile(info.copy(dependencies = allDependencies, compilerArgs = addedFlags))
|
||||
}
|
||||
|
||||
private fun validateClasspath(cp: List<String>) {
|
||||
cp.forEach {
|
||||
if (! File(it).exists()) {
|
||||
throw KobaltException("Couldn't find $it")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface ICompilerAction {
|
||||
fun compile(info: CompilerActionInfo): TaskResult
|
||||
}
|
|
@ -0,0 +1,226 @@
|
|||
package com.beust.kobalt.internal
|
||||
|
||||
import com.beust.kobalt.KobaltException
|
||||
import com.beust.kobalt.TaskResult
|
||||
import com.beust.kobalt.api.*
|
||||
import com.beust.kobalt.api.annotation.ExportedProjectProperty
|
||||
import com.beust.kobalt.api.annotation.Task
|
||||
import com.beust.kobalt.maven.DepFactory
|
||||
import com.beust.kobalt.maven.DependencyManager
|
||||
import com.beust.kobalt.maven.LocalRepo
|
||||
import com.beust.kobalt.misc.KFiles
|
||||
import com.beust.kobalt.misc.KobaltExecutors
|
||||
import com.beust.kobalt.misc.log
|
||||
import com.beust.kobalt.misc.warn
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Base classes for plug-ins that compile files on the JVM. This base class requires the bare minimum
|
||||
* contributors (source files, projects and tasks). Subclasses can add more as they see fit (e.g. test
|
||||
* source directory, etc...).
|
||||
*/
|
||||
@Singleton
|
||||
abstract class JvmCompilerPlugin @Inject constructor(
|
||||
open val localRepo: LocalRepo,
|
||||
open val files: KFiles,
|
||||
open val depFactory: DepFactory,
|
||||
open val dependencyManager: DependencyManager,
|
||||
open val executors: KobaltExecutors,
|
||||
open val jvmCompiler: JvmCompiler,
|
||||
val taskContributor : TaskContributor = TaskContributor())
|
||||
: BasePlugin(), ISourceDirectoryContributor, IProjectContributor, ITaskContributor by taskContributor {
|
||||
|
||||
companion object {
|
||||
@ExportedProjectProperty(doc = "Projects this project depends on", type = "List<ProjectDescription>")
|
||||
const val DEPENDENT_PROJECTS = "dependentProjects"
|
||||
|
||||
@ExportedProjectProperty(doc = "Compiler args", type = "List<String>")
|
||||
const val COMPILER_ARGS = "compilerArgs"
|
||||
|
||||
const val TASK_COMPILE = "compile"
|
||||
const val TASK_COMPILE_TEST = "compileTest"
|
||||
const val TASK_CLEAN = "clean"
|
||||
const val TASK_TEST = "test"
|
||||
|
||||
const val SOURCE_SET_MAIN = "main"
|
||||
const val SOURCE_SET_TEST = "test"
|
||||
const val DOCS_DIRECTORY = "docs/javadoc"
|
||||
}
|
||||
|
||||
/**
|
||||
* Log with a project.
|
||||
*/
|
||||
protected fun lp(project: Project, s: String) {
|
||||
log(2, "${project.name}: $s")
|
||||
}
|
||||
|
||||
override fun apply(project: Project, context: KobaltContext) {
|
||||
super.apply(project, context)
|
||||
project.projectProperties.put(DEPENDENT_PROJECTS, projects())
|
||||
taskContributor.addVariantTasks(this, project, context, "compile", runTask = { taskCompile(project) })
|
||||
}
|
||||
|
||||
@Task(name = TASK_TEST, description = "Run the tests",
|
||||
runAfter = arrayOf(JvmCompilerPlugin.TASK_COMPILE, JvmCompilerPlugin.TASK_COMPILE_TEST))
|
||||
fun taskTest(project: Project) : TaskResult {
|
||||
lp(project, "Running tests")
|
||||
|
||||
val runContributor = ActorUtils.selectAffinityActor(project, context,
|
||||
context.pluginInfo.testRunnerContributors)
|
||||
if (runContributor != null && runContributor.affinity(project, context) > 0) {
|
||||
return runContributor.run(project, context, dependencyManager.testDependencies(project, context,
|
||||
projects()))
|
||||
} else {
|
||||
log(1, "Couldn't find a test runner for project ${project.name}, not running any tests")
|
||||
return TaskResult()
|
||||
}
|
||||
}
|
||||
|
||||
@Task(name = TASK_CLEAN, description = "Clean the project", runBefore = arrayOf("compile"))
|
||||
fun taskClean(project : Project ) : TaskResult {
|
||||
java.io.File(project.directory, project.buildDirectory).let { dir ->
|
||||
if (! dir.deleteRecursively()) {
|
||||
warn("Couldn't delete $dir")
|
||||
}
|
||||
}
|
||||
return TaskResult()
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the resources from a source directory to the build one
|
||||
*/
|
||||
protected fun copyResources(project: Project, sourceSet: String) {
|
||||
val sourceDirs: ArrayList<String> = arrayListOf()
|
||||
var outputDir: String?
|
||||
if (sourceSet == JvmCompilerPlugin.SOURCE_SET_MAIN) {
|
||||
sourceDirs.addAll(project.sourceDirectories.filter { it.contains("resources") })
|
||||
outputDir = KFiles.CLASSES_DIR
|
||||
} else if (sourceSet == JvmCompilerPlugin.SOURCE_SET_TEST) {
|
||||
sourceDirs.addAll(project.sourceDirectoriesTest.filter { it.contains("resources") })
|
||||
outputDir = KFiles.TEST_CLASSES_DIR
|
||||
} else {
|
||||
throw IllegalArgumentException("Custom source sets not supported yet: $sourceSet")
|
||||
}
|
||||
|
||||
if (sourceDirs.size > 0) {
|
||||
lp(project, "Copying $sourceSet resources")
|
||||
val absOutputDir = File(KFiles.joinDir(project.directory, project.buildDirectory, outputDir))
|
||||
sourceDirs.map { File(project.directory, it) }.filter {
|
||||
it.exists()
|
||||
} .forEach {
|
||||
log(2, "Copying from $sourceDirs to $absOutputDir")
|
||||
KFiles.copyRecursively(it, absOutputDir, deleteFirst = true)
|
||||
}
|
||||
} else {
|
||||
lp(project, "No resources to copy for $sourceSet")
|
||||
}
|
||||
}
|
||||
|
||||
protected fun compilerArgsFor(project: Project) : List<String> {
|
||||
val result = project.projectProperties.get(COMPILER_ARGS)
|
||||
if (result != null) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return result as List<String>
|
||||
} else {
|
||||
return emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
fun addCompilerArgs(project: Project, vararg args: String) {
|
||||
project.projectProperties.put(COMPILER_ARGS, arrayListOf(*args))
|
||||
}
|
||||
|
||||
@Task(name = JvmCompilerPlugin.TASK_COMPILE, description = "Compile the project")
|
||||
fun taskCompile(project: Project) : TaskResult {
|
||||
// Set up the source files now that we have the variant
|
||||
sourceDirectories.addAll(context.variant.sourceDirectories(project))
|
||||
|
||||
val sourceDirectory = context.variant.maybeGenerateBuildConfig(project, context)
|
||||
if (sourceDirectory != null) {
|
||||
sourceDirectories.add(sourceDirectory)
|
||||
}
|
||||
val info = createCompilerActionInfo(project, context, isTest = false)
|
||||
val compiler = ActorUtils.selectAffinityActor(project, context, context.pluginInfo.compilerContributors)
|
||||
if (compiler != null) {
|
||||
return compiler.compile(project, context, info)
|
||||
} else {
|
||||
throw KobaltException("Couldn't find any compiler for project ${project.name}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun projects() = projects
|
||||
|
||||
@Task(name = "doc", description = "Generate the documentation for the project")
|
||||
fun taskJavadoc(project: Project) : TaskResult {
|
||||
val docGenerator = ActorUtils.selectAffinityActor(project, context, context.pluginInfo.docContributors)
|
||||
if (docGenerator != null) {
|
||||
return docGenerator.generateDoc(project, context, createCompilerActionInfo(project, context,
|
||||
isTest = false))
|
||||
} else {
|
||||
warn("Couldn't find any doc contributor for project ${project.name}")
|
||||
return TaskResult()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a CompilerActionInfo (all the information that a compiler needs to know) for the given parameters.
|
||||
* Runs all the contributors and interceptors relevant to that task.
|
||||
*/
|
||||
protected fun createCompilerActionInfo(project: Project, context: KobaltContext, isTest: Boolean) :
|
||||
CompilerActionInfo {
|
||||
copyResources(project, JvmCompilerPlugin.SOURCE_SET_MAIN)
|
||||
|
||||
val classpath = if (isTest)
|
||||
dependencyManager.testDependencies(project, context, projects)
|
||||
else
|
||||
dependencyManager.dependencies(project, context, projects)
|
||||
|
||||
val projectDirectory = File(project.directory)
|
||||
val buildDirectory = if (isTest) KFiles.makeOutputTestDir(project)
|
||||
else File(project.classesDir(context))
|
||||
buildDirectory.mkdirs()
|
||||
|
||||
val initialSourceDirectories = arrayListOf<File>()
|
||||
|
||||
// Source directories from the contributors
|
||||
initialSourceDirectories.addAll(
|
||||
if (isTest) {
|
||||
context.pluginInfo.testSourceDirContributors.flatMap { it.testSourceDirectoriesFor(project, context) }
|
||||
} else {
|
||||
context.pluginInfo.sourceDirContributors.flatMap { it.sourceDirectoriesFor(project, context) }
|
||||
})
|
||||
|
||||
// Transform them with the interceptors, if any
|
||||
val sourceDirectories = if (isTest) {
|
||||
initialSourceDirectories
|
||||
} else {
|
||||
context.pluginInfo.sourceDirectoriesInterceptors.fold(initialSourceDirectories.toList(),
|
||||
{ sd, interceptor -> interceptor.intercept(project, context, sd) })
|
||||
}.filter {
|
||||
File(project.directory, it.path).exists()
|
||||
}
|
||||
|
||||
// Now that we have the final list of source dirs, find source files in them
|
||||
val sourceFiles = files.findRecursively(projectDirectory, sourceDirectories,
|
||||
{ it .endsWith(project.sourceSuffix) })
|
||||
.map { File(projectDirectory, it).path }
|
||||
|
||||
// Finally, alter the info with the compiler interceptors before returning it
|
||||
val initialActionInfo = CompilerActionInfo(projectDirectory.path, classpath, sourceFiles, buildDirectory,
|
||||
emptyList())
|
||||
val result = context.pluginInfo.compilerInterceptors.fold(initialActionInfo, { ai, interceptor ->
|
||||
interceptor.intercept(project, context, ai)
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
val sourceDirectories = arrayListOf<File>()
|
||||
|
||||
// ISourceDirectoryContributor
|
||||
override fun sourceDirectoriesFor(project: Project, context: KobaltContext)
|
||||
= if (accept(project)) sourceDirectories else arrayListOf()
|
||||
}
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
package com.beust.kobalt.internal
|
||||
|
||||
import com.beust.kobalt.api.*
|
||||
import com.beust.kobalt.misc.log
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import javax.xml.bind.JAXBContext
|
||||
import javax.xml.bind.annotation.XmlElement
|
||||
import javax.xml.bind.annotation.XmlRootElement
|
||||
|
||||
//
|
||||
// Operations related to the parsing of kobalt-plugin.xml: XML parsing, PluginInfo, etc...
|
||||
//
|
||||
|
||||
/**
|
||||
* If a plug-in didn't specify a factory, we use our own injector to instantiate all its components.
|
||||
*/
|
||||
class GuiceFactory : IFactory {
|
||||
override fun <T> instanceOf(c: Class<T>) : T = Kobalt.INJECTOR.getInstance(c)
|
||||
}
|
||||
|
||||
/////
|
||||
// XML parsing
|
||||
//
|
||||
// The following classes are used by JAXB to parse the kobalt-plugin.xml file.
|
||||
|
||||
/**
|
||||
* The root element of kobalt-plugin.xml
|
||||
*/
|
||||
@XmlRootElement(name = "kobalt-plugin")
|
||||
class KobaltPluginXml {
|
||||
@XmlElement @JvmField
|
||||
var name: String? = null
|
||||
|
||||
@XmlElement(name = "plugin-actors") @JvmField
|
||||
var pluginActors : ClassNameXml? = null
|
||||
|
||||
@XmlElement(name = "factory-class-name") @JvmField
|
||||
var factoryClassName: String? = null
|
||||
}
|
||||
|
||||
class ContributorXml {
|
||||
@XmlElement @JvmField
|
||||
val name: String? = null
|
||||
}
|
||||
|
||||
class ClassNameXml {
|
||||
@XmlElement(name = "class-name") @JvmField
|
||||
var className: List<String> = arrayListOf()
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn a KobaltPluginXml (the raw content of kobalt-plugin.xml mapped to POJO's) into a PluginInfo object, which
|
||||
* contains all the contributors instantiated and other information that Kobalt can actually use. Kobalt code that
|
||||
* needs to access plug-in info can then just inject a PluginInfo object.
|
||||
*/
|
||||
class PluginInfo(val xml: KobaltPluginXml, val classLoader: ClassLoader?) {
|
||||
val plugins = arrayListOf<IPlugin>()
|
||||
val projectContributors = arrayListOf<IProjectContributor>()
|
||||
val classpathContributors = arrayListOf<IClasspathContributor>()
|
||||
val initContributors = arrayListOf<IInitContributor<File>>()
|
||||
val repoContributors = arrayListOf<IRepoContributor>()
|
||||
val compilerFlagContributors = arrayListOf<ICompilerFlagContributor>()
|
||||
val compilerInterceptors = arrayListOf<ICompilerInterceptor>()
|
||||
val sourceDirectoriesInterceptors = arrayListOf<ISourceDirectoryIncerceptor>()
|
||||
val buildDirectoryInterceptors = arrayListOf<IBuildDirectoryIncerceptor>()
|
||||
val runnerContributors = arrayListOf<IRunnerContributor>()
|
||||
val testRunnerContributors = arrayListOf<ITestRunnerContributor>()
|
||||
val classpathInterceptors = arrayListOf<IClasspathInterceptor>()
|
||||
val compilerContributors = arrayListOf<ICompilerContributor>()
|
||||
val docContributors = arrayListOf<IDocContributor>()
|
||||
val sourceDirContributors = arrayListOf<ISourceDirectoryContributor>()
|
||||
val testSourceDirContributors = arrayListOf<ITestSourceDirectoryContributor>()
|
||||
val buildConfigFieldContributors = arrayListOf<IBuildConfigFieldContributor>()
|
||||
val taskContributors = arrayListOf<ITaskContributor>()
|
||||
|
||||
val mavenIdInterceptors = arrayListOf<IMavenIdInterceptor>()
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Where plug-ins define their plug-in actors.
|
||||
*/
|
||||
val PLUGIN_XML = "META-INF/kobalt-plugin.xml"
|
||||
|
||||
/**
|
||||
* Kobalt's core XML file needs to be different from kobalt-plugin.xml because classloaders
|
||||
* can put a plug-in's jar file in front of Kobalt's, which means we'll read
|
||||
* that one instead of the core one.
|
||||
*/
|
||||
val PLUGIN_CORE_XML = "META-INF/kobalt-core-plugin.xml"
|
||||
|
||||
/**
|
||||
* Read Kobalt's own kobalt-plugin.xml.
|
||||
*/
|
||||
fun readKobaltPluginXml(): PluginInfo {
|
||||
// Note: use forward slash here since we're looking up this file in a .jar file
|
||||
val url = Kobalt::class.java.classLoader.getResource(PLUGIN_CORE_XML)
|
||||
log(2, "URL for core kobalt-plugin.xml: $url")
|
||||
if (url != null) {
|
||||
return readPluginXml(url.openConnection().inputStream)
|
||||
} else {
|
||||
throw AssertionError("Couldn't find $PLUGIN_XML")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a general kobalt-plugin.xml.
|
||||
*/
|
||||
fun readPluginXml(ins: InputStream, classLoader: ClassLoader? = null): PluginInfo {
|
||||
val jaxbContext = JAXBContext.newInstance(KobaltPluginXml::class.java)
|
||||
val kotlinPlugin: KobaltPluginXml = jaxbContext.createUnmarshaller().unmarshal(ins)
|
||||
as KobaltPluginXml
|
||||
log(2, "Parsed plugin XML file, found: " + kotlinPlugin.name)
|
||||
return PluginInfo(kotlinPlugin, classLoader)
|
||||
}
|
||||
|
||||
fun readPluginXml(s: String, classLoader: ClassLoader? = null)
|
||||
= readPluginXml(ByteArrayInputStream(s.toByteArray(Charsets.UTF_8)), classLoader)
|
||||
}
|
||||
|
||||
init {
|
||||
val factory = if (xml.factoryClassName != null) {
|
||||
Class.forName(xml.factoryClassName).newInstance() as IFactory
|
||||
} else {
|
||||
GuiceFactory()
|
||||
}
|
||||
|
||||
fun forName(className: String) =
|
||||
if (classLoader != null) classLoader.loadClass(className)
|
||||
else Class.forName(className)
|
||||
|
||||
//
|
||||
// Populate pluginInfo with what was found in Kobalt's own kobalt-plugin.xml
|
||||
//
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
xml.pluginActors?.className?.forEach {
|
||||
with(factory.instanceOf(forName(it))) {
|
||||
// Note: can't use "when" here since the same instance can implement multiple interfaces
|
||||
if (this is IBuildConfigFieldContributor) buildConfigFieldContributors.add(this)
|
||||
if (this is IBuildDirectoryIncerceptor) buildDirectoryInterceptors.add(this)
|
||||
if (this is IClasspathContributor) classpathContributors.add(this)
|
||||
if (this is IClasspathInterceptor) classpathInterceptors.add(this)
|
||||
if (this is ICompilerContributor) compilerContributors.add(this)
|
||||
if (this is ICompilerFlagContributor) compilerFlagContributors.add(this)
|
||||
if (this is ICompilerInterceptor) compilerInterceptors.add(this)
|
||||
if (this is IDocContributor) docContributors.add(this)
|
||||
if (this is IInitContributor<*>) initContributors.add(this as IInitContributor<File>)
|
||||
if (this is IPlugin) plugins.add(this)
|
||||
if (this is IProjectContributor) projectContributors.add(this)
|
||||
if (this is IRepoContributor) repoContributors.add(this)
|
||||
if (this is IRunnerContributor) runnerContributors.add(this)
|
||||
if (this is ISourceDirectoryContributor) sourceDirContributors.add(this)
|
||||
if (this is ISourceDirectoryIncerceptor) sourceDirectoriesInterceptors.add(this)
|
||||
if (this is ITaskContributor) taskContributors.add(this)
|
||||
if (this is ITestRunnerContributor) testRunnerContributors.add(this)
|
||||
|
||||
// Not documented yet
|
||||
if (this is IMavenIdInterceptor) mavenIdInterceptors.add(this)
|
||||
if (this is ITestSourceDirectoryContributor) testSourceDirContributors.add(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate pluginInfo with what was found in the plug-in's kobalt-plugin.xml
|
||||
*/
|
||||
fun addPluginInfo(pluginInfo: PluginInfo) {
|
||||
log(2, "Found new plug-in, adding it to pluginInfo: $pluginInfo")
|
||||
|
||||
plugins.addAll(pluginInfo.plugins)
|
||||
classpathContributors.addAll(pluginInfo.classpathContributors)
|
||||
projectContributors.addAll(pluginInfo.projectContributors)
|
||||
initContributors.addAll(pluginInfo.initContributors)
|
||||
repoContributors.addAll(pluginInfo.repoContributors)
|
||||
compilerFlagContributors.addAll(pluginInfo.compilerFlagContributors)
|
||||
compilerInterceptors.addAll(pluginInfo.compilerInterceptors)
|
||||
sourceDirectoriesInterceptors.addAll(pluginInfo.sourceDirectoriesInterceptors)
|
||||
buildDirectoryInterceptors.addAll(pluginInfo.buildDirectoryInterceptors)
|
||||
runnerContributors.addAll(pluginInfo.runnerContributors)
|
||||
testRunnerContributors.addAll(pluginInfo.testRunnerContributors)
|
||||
classpathInterceptors.addAll(pluginInfo.classpathInterceptors)
|
||||
compilerContributors.addAll(pluginInfo.compilerContributors)
|
||||
docContributors.addAll(pluginInfo.docContributors)
|
||||
sourceDirContributors.addAll(pluginInfo.sourceDirContributors)
|
||||
buildConfigFieldContributors.addAll(pluginInfo.buildConfigFieldContributors)
|
||||
taskContributors.addAll(pluginInfo.taskContributors)
|
||||
testSourceDirContributors.addAll(pluginInfo.testSourceDirContributors)
|
||||
mavenIdInterceptors.addAll(pluginInfo.mavenIdInterceptors)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.beust.kobalt.internal
|
||||
|
||||
import java.util.jar.JarInputStream
|
||||
|
||||
public class PluginLoader(val jarStream: JarInputStream) {
|
||||
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package com.beust.kobalt.internal
|
||||
|
||||
import com.beust.kobalt.Variant
|
||||
import com.beust.kobalt.api.BuildConfig
|
||||
import com.beust.kobalt.api.BuildConfigField
|
||||
import com.beust.kobalt.api.KobaltContext
|
||||
import com.beust.kobalt.api.Project
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Data that is useful for projects to have but should not be specified in the DSL.
|
||||
*/
|
||||
interface IProjectInfo {
|
||||
/** Used to determine the last directory segment of the flavored sources, e.g. src/main/JAVA */
|
||||
val sourceDirectory : String
|
||||
|
||||
val defaultSourceDirectories: HashSet<String>
|
||||
val defaultTestDirectories: HashSet<String>
|
||||
|
||||
/**
|
||||
* If at least one build config was found either on the project or the variant, this function
|
||||
* will be used to generate the BuildConfig file with the correct language.
|
||||
*/
|
||||
fun generateBuildConfig(project: Project, context: KobaltContext, packageName: String, variant: Variant,
|
||||
buildConfigs: List<BuildConfig>) : String
|
||||
}
|
||||
|
||||
interface BaseProjectInfo : IProjectInfo {
|
||||
fun generate(field: BuildConfigField) : String
|
||||
fun generate(type: String, name: String, value: Any) = generate(BuildConfigField(type, name, value))
|
||||
|
||||
fun generateFieldsFromContributors(project: Project, context: KobaltContext)
|
||||
= context.pluginInfo.buildConfigFieldContributors.flatMap {
|
||||
it.fieldsFor(project, context)
|
||||
}.map {
|
||||
generate(it)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package com.beust.kobalt.internal
|
||||
|
||||
/**
|
||||
* SpekRunner triggers if it finds a dependency on org.jetbrains.spek but other than that, it just
|
||||
* uses the regular JUnitRunner.
|
||||
*/
|
||||
public class SpekRunner() : JUnitRunner() {
|
||||
override val dependencyName = "org.jetbrains.spek"
|
||||
}
|
||||
|
|
@ -0,0 +1,323 @@
|
|||
package com.beust.kobalt.internal
|
||||
|
||||
import com.beust.kobalt.*
|
||||
import com.beust.kobalt.api.DynamicTask
|
||||
import com.beust.kobalt.api.IPlugin
|
||||
import com.beust.kobalt.api.PluginTask
|
||||
import com.beust.kobalt.api.Project
|
||||
import com.beust.kobalt.api.annotation.Task
|
||||
import com.beust.kobalt.Args
|
||||
import com.beust.kobalt.misc.log
|
||||
import com.google.common.collect.ArrayListMultimap
|
||||
import com.google.common.collect.Multimap
|
||||
import com.google.common.collect.TreeMultimap
|
||||
import java.lang.reflect.Method
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
public class TaskManager @Inject constructor(val args: Args) {
|
||||
private val runBefore = TreeMultimap.create<String, String>()
|
||||
private val alwaysRunAfter = TreeMultimap.create<String, String>()
|
||||
|
||||
/**
|
||||
* Called by plugins to indicate task dependencies defined at runtime. Keys depend on values.
|
||||
* Declare that `task1` depends on `task2`.
|
||||
*
|
||||
* Note: there is no runAfter on this class since a runAfter(a, b) in a task simply translates
|
||||
* to a runBefore(b, a) here.
|
||||
*/
|
||||
fun runBefore(task1: String, task2: String) {
|
||||
runBefore.put(task1, task2)
|
||||
}
|
||||
|
||||
fun alwaysRunAfter(task1: String, task2: String) {
|
||||
alwaysRunAfter.put(task1, task2)
|
||||
}
|
||||
|
||||
data class TaskInfo(val id: String) {
|
||||
constructor(project: String, task: String) : this(project + ":" + task)
|
||||
|
||||
val project: String?
|
||||
get() = if (id.contains(":")) id.split(":")[0] else null
|
||||
val taskName: String
|
||||
get() = if (id.contains(":")) id.split(":")[1] else id
|
||||
fun matches(projectName: String) = project == null || project == projectName
|
||||
}
|
||||
|
||||
public fun runTargets(taskNames: List<String>, projects: List<Project>) : Int {
|
||||
var result = 0
|
||||
projects.forEach { project ->
|
||||
val projectName = project.name
|
||||
// There can be multiple tasks by the same name (e.g. PackagingPlugin and AndroidPlugin both
|
||||
// define "install"), so use a multimap
|
||||
val tasksByNames = ArrayListMultimap.create<String, PluginTask>()
|
||||
tasks.filter {
|
||||
it.project.name == project.name
|
||||
}.forEach {
|
||||
tasksByNames.put(it.name, it)
|
||||
}
|
||||
|
||||
AsciiArt.logBox("Building project ${project.name}")
|
||||
|
||||
log(3, "Tasks:")
|
||||
tasksByNames.keys().forEach {
|
||||
log(3, " $it: " + tasksByNames.get(it))
|
||||
}
|
||||
val graph = DynamicGraph<PluginTask>()
|
||||
taskNames.forEach { taskName ->
|
||||
val ti = TaskInfo(taskName)
|
||||
if (! tasksByNames.keys().contains(ti.taskName)) {
|
||||
throw KobaltException("Unknown task: $taskName")
|
||||
}
|
||||
|
||||
if (ti.matches(projectName)) {
|
||||
tasksByNames[ti.taskName].forEach { task ->
|
||||
if (task != null && task.plugin.accept(project)) {
|
||||
val reverseAfter = hashMapOf<String, String>()
|
||||
alwaysRunAfter.keys().forEach { from ->
|
||||
val tasks = alwaysRunAfter.get(from)
|
||||
tasks.forEach {
|
||||
reverseAfter.put(it, from)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// If the current target is free, add it as a single node to the graph
|
||||
//
|
||||
val allFreeTasks = calculateFreeTasks(tasksByNames, reverseAfter)
|
||||
val currentFreeTask = allFreeTasks.filter {
|
||||
TaskInfo(projectName, it.name).taskName == task.name
|
||||
}
|
||||
if (currentFreeTask.size == 1) {
|
||||
currentFreeTask[0].let {
|
||||
graph.addNode(it)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Add the transitive closure of the current task as edges to the graph
|
||||
//
|
||||
val transitiveClosure = calculateTransitiveClosure(project, tasksByNames, ti)
|
||||
transitiveClosure.forEach { pluginTask ->
|
||||
val rb = runBefore.get(pluginTask.name)
|
||||
rb.forEach {
|
||||
val tos = tasksByNames[it]
|
||||
if (tos != null && tos.size > 0) {
|
||||
tos.forEach { to ->
|
||||
graph.addEdge(pluginTask, to)
|
||||
}
|
||||
} else {
|
||||
log(2, "Couldn't find node $it: not applicable to project ${project.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// If any of the nodes in the graph has an "alwaysRunAfter", add that edge too
|
||||
//
|
||||
val allNodes = arrayListOf<PluginTask>()
|
||||
allNodes.addAll(graph.nodes)
|
||||
allNodes.forEach { node ->
|
||||
val other = alwaysRunAfter.get(node.name)
|
||||
other?.forEach { o ->
|
||||
tasksByNames[o]?.forEach {
|
||||
graph.addEdge(it, node)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Now that we have a full graph, run it
|
||||
//
|
||||
log(3, "About to run graph:\n ${graph.dump()} ")
|
||||
|
||||
val factory = object : IThreadWorkerFactory<PluginTask> {
|
||||
override public fun createWorkers(nodes: List<PluginTask>): List<IWorker<PluginTask>> {
|
||||
val thisResult = arrayListOf<IWorker<PluginTask>>()
|
||||
nodes.forEach {
|
||||
thisResult.add(TaskWorker(arrayListOf(it), args.dryRun))
|
||||
}
|
||||
return thisResult
|
||||
}
|
||||
}
|
||||
|
||||
val executor = DynamicGraphExecutor(graph, factory)
|
||||
val thisResult = executor.run()
|
||||
if (result == 0) {
|
||||
result = thisResult
|
||||
}
|
||||
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the free tasks of the graph.
|
||||
*/
|
||||
private fun calculateFreeTasks(tasksByNames: Multimap<String, PluginTask>, reverseAfter: HashMap<String, String>)
|
||||
: Collection<PluginTask> {
|
||||
val freeTaskMap = hashMapOf<String, PluginTask>()
|
||||
tasksByNames.keys().forEach {
|
||||
if (! runBefore.containsKey(it) && ! reverseAfter.containsKey(it)) {
|
||||
tasksByNames[it].forEach { t ->
|
||||
freeTaskMap.put(it, t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return freeTaskMap.values
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the transitive closure for the given TaskInfo
|
||||
*/
|
||||
private fun calculateTransitiveClosure(project: Project, tasksByNames: Multimap<String, PluginTask>, ti: TaskInfo):
|
||||
HashSet<PluginTask> {
|
||||
log(3, "Processing ${ti.taskName}")
|
||||
|
||||
val transitiveClosure = hashSetOf<PluginTask>()
|
||||
val seen = hashSetOf(ti.taskName)
|
||||
val toProcess = hashSetOf(ti)
|
||||
var done = false
|
||||
while (! done) {
|
||||
val newToProcess = hashSetOf<TaskInfo>()
|
||||
log(3, "toProcess size: " + toProcess.size)
|
||||
toProcess.forEach { target ->
|
||||
|
||||
val currentTask = TaskInfo(project.name, target.taskName)
|
||||
val thisTask = tasksByNames[currentTask.taskName]
|
||||
if (thisTask != null) {
|
||||
transitiveClosure.addAll(thisTask)
|
||||
val dependencyNames = runBefore.get(currentTask.taskName)
|
||||
dependencyNames.forEach { dependencyName ->
|
||||
if (!seen.contains(dependencyName)) {
|
||||
newToProcess.add(currentTask)
|
||||
seen.add(dependencyName)
|
||||
}
|
||||
}
|
||||
|
||||
dependencyNames.forEach {
|
||||
newToProcess.add(TaskInfo(project.name, it))
|
||||
}
|
||||
} else {
|
||||
log(2, "Couldn't find task ${currentTask.taskName}: not applicable to project ${project.name}")
|
||||
}
|
||||
}
|
||||
done = newToProcess.isEmpty()
|
||||
toProcess.clear()
|
||||
toProcess.addAll(newToProcess)
|
||||
}
|
||||
|
||||
return transitiveClosure
|
||||
}
|
||||
|
||||
/////
|
||||
// Manage the tasks
|
||||
//
|
||||
|
||||
class StaticTask(val plugin: IPlugin, val method: Method, val taskAnnotation: Task)
|
||||
class PluginDynamicTask(val plugin: IPlugin, val task: DynamicTask)
|
||||
|
||||
val tasks = arrayListOf<PluginTask>()
|
||||
val staticTasks = arrayListOf<StaticTask>()
|
||||
val dynamicTasks = arrayListOf<PluginDynamicTask>()
|
||||
|
||||
/**
|
||||
* Turn all the static and dynamic tasks into plug-in tasks, which are then suitable to be executed.
|
||||
*/
|
||||
fun computePluginTasks(plugins: List<IPlugin>, projects: List<Project>) {
|
||||
addStaticTasks(projects)
|
||||
addDynamicTasks(projects)
|
||||
}
|
||||
|
||||
private fun addDynamicTasks(projects: List<Project>) {
|
||||
dynamicTasks.forEach { dynamicTask ->
|
||||
val task = dynamicTask.task
|
||||
projects.filter { dynamicTask.plugin.accept(it) }.forEach { project ->
|
||||
addTask(dynamicTask.plugin, project, task.name, task.description, task.runBefore, task.runAfter,
|
||||
task.alwaysRunAfter, task.closure)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addStaticTasks(projects: List<Project>) {
|
||||
staticTasks.forEach { staticTask ->
|
||||
val method = staticTask.method
|
||||
val annotation = staticTask.taskAnnotation
|
||||
|
||||
val methodName = method.declaringClass.toString() + "." + method.name
|
||||
log(3, " Found task:${annotation.name} method: $methodName")
|
||||
|
||||
fun toTask(m: Method, project: Project, plugin: IPlugin): (Project) -> TaskResult {
|
||||
val result: (Project) -> TaskResult = {
|
||||
m.invoke(plugin, project) as TaskResult
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
val plugin = staticTask.plugin
|
||||
projects.filter { plugin.accept(it) }.forEach { project ->
|
||||
addStaticTask(plugin, project, staticTask.taskAnnotation, toTask(method, project, plugin))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addStaticTask(plugin: IPlugin, project: Project, annotation: Task, task: (Project) -> TaskResult) {
|
||||
addTask(plugin, project, annotation.name, annotation.description, annotation.runBefore.toList(),
|
||||
annotation.runAfter.toList(), annotation.alwaysRunAfter.toList(), task)
|
||||
}
|
||||
|
||||
fun addTask(plugin: IPlugin, project: Project, name: String, description: String = "",
|
||||
runBefore: List<String> = listOf<String>(),
|
||||
runAfter: List<String> = listOf<String>(),
|
||||
alwaysRunAfter: List<String> = listOf<String>(),
|
||||
task: (Project) -> TaskResult) {
|
||||
tasks.add(
|
||||
object : BasePluginTask(plugin, name, description, project) {
|
||||
override fun call(): TaskResult2<PluginTask> {
|
||||
val taskResult = task(project)
|
||||
return TaskResult2(taskResult.success, taskResult.errorMessage, this)
|
||||
}
|
||||
})
|
||||
runBefore.forEach { runBefore(it, name) }
|
||||
runAfter.forEach { runBefore(name, it) }
|
||||
alwaysRunAfter.forEach { alwaysRunAfter(it, name)}
|
||||
}
|
||||
|
||||
//
|
||||
//
|
||||
/////
|
||||
|
||||
}
|
||||
|
||||
class TaskWorker(val tasks: List<PluginTask>, val dryRun: Boolean) : IWorker<PluginTask> {
|
||||
// override fun compareTo(other: IWorker2<PluginTask>): Int {
|
||||
// return priority.compareTo(other.priority)
|
||||
// }
|
||||
|
||||
override fun call() : TaskResult2<PluginTask> {
|
||||
if (tasks.size > 0) {
|
||||
tasks[0].let {
|
||||
log(1, AsciiArt.taskColor(AsciiArt.horizontalSingleLine + " ${it.project.name}:${it.name}"))
|
||||
}
|
||||
}
|
||||
var success = true
|
||||
val errorMessages = arrayListOf<String>()
|
||||
tasks.forEach {
|
||||
val tr = if (dryRun) TaskResult() else it.call()
|
||||
success = success and tr.success
|
||||
if (tr.errorMessage != null) errorMessages.add(tr.errorMessage)
|
||||
}
|
||||
return TaskResult2(success, errorMessages.joinToString("\n"), tasks[0])
|
||||
}
|
||||
|
||||
// override val timeOut : Long = 10000
|
||||
|
||||
override val priority: Int = 0
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package com.beust.kobalt.internal
|
||||
|
||||
import com.beust.kobalt.api.IClasspathDependency
|
||||
import com.beust.kobalt.api.Project
|
||||
import com.beust.kobalt.misc.KFiles
|
||||
import java.io.File
|
||||
|
||||
public class TestNgRunner() : GenericTestRunner() {
|
||||
|
||||
override val mainClass = "org.testng.TestNG"
|
||||
|
||||
override val dependencyName = "org.testng"
|
||||
|
||||
override fun args(project: Project, classpath: List<IClasspathDependency>) = arrayListOf<String>().apply {
|
||||
if (project.testArgs.size > 0) {
|
||||
addAll(project.testArgs)
|
||||
} else {
|
||||
val testngXml = File(project.directory, KFiles.joinDir("src", "test", "resources", "testng.xml"))
|
||||
if (testngXml.exists()) {
|
||||
add(testngXml.absolutePath)
|
||||
} else {
|
||||
add("-testclass")
|
||||
add(findTestClasses(project, classpath).joinToString(","))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package com.beust.kobalt.internal.build
|
||||
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.attribute.BasicFileAttributes
|
||||
/**
|
||||
* Sometimes, build files are moved to temporary files, so we give them a specific name for clarity.
|
||||
*/
|
||||
class BuildFile(val path: Path, val name: String) {
|
||||
public fun exists() : Boolean = Files.exists(path)
|
||||
|
||||
public val lastModified : Long
|
||||
get() = Files.readAttributes(path, BasicFileAttributes::class.java).lastModifiedTime().toMillis()
|
||||
|
||||
public val directory : File get() = path.toFile().directory
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package com.beust.kobalt.internal.build
|
||||
|
||||
import com.beust.kobalt.api.Kobalt
|
||||
import com.beust.kobalt.misc.KFiles
|
||||
import java.io.File
|
||||
|
||||
class VersionFile {
|
||||
companion object {
|
||||
private val VERSION_FILE = "version.txt"
|
||||
|
||||
fun generateVersionFile(directory: File) {
|
||||
KFiles.saveFile(File(directory, VERSION_FILE), Kobalt.version)
|
||||
}
|
||||
|
||||
fun isSameVersionFile(directory: File) =
|
||||
with(File(directory, VERSION_FILE)) {
|
||||
! exists() || (exists() && readText() == Kobalt.version)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package com.beust.kobalt.internal.remote
|
||||
|
||||
import com.beust.kobalt.Constants
|
||||
import java.io.PrintWriter
|
||||
import java.net.Socket
|
||||
|
||||
fun main(argv: Array<String>) {
|
||||
val socket = Socket("localhost", 1234)
|
||||
(PrintWriter(socket.outputStream, true)).use { out ->
|
||||
out.println("""{ "name" : "getDependencies", "buildFile":
|
||||
"/Users/beust/kotlin/kobalt/kobalt/src/${Constants.BUILD_FILE_NAME}"}""")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package com.beust.kobalt.internal.remote
|
||||
|
||||
import com.google.gson.JsonObject
|
||||
|
||||
/**
|
||||
* All commands implement this interface.
|
||||
*/
|
||||
interface ICommand {
|
||||
/**
|
||||
* The name of this command.
|
||||
*/
|
||||
val name: String
|
||||
|
||||
/**
|
||||
* Run this command based on the information received from the client. When done, use
|
||||
* the sender object to send back a response.
|
||||
*/
|
||||
fun run(sender: ICommandSender, received: JsonObject)
|
||||
|
||||
fun toCommandData(data: String) = CommandData(name, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Passed to a command in its `run` method so it can send information back to the caller.
|
||||
* @param The string content that will be sent in the "data" field.
|
||||
*/
|
||||
interface ICommandSender {
|
||||
fun sendData(commandData: CommandData)
|
||||
}
|
||||
|
||||
/**
|
||||
* The JSON payload that commands exchange follow the following pattern:
|
||||
* {
|
||||
* name: "nameOfTheCommand"
|
||||
* data: a JSON string containing the payload itself
|
||||
* }
|
||||
* This allows commands to be tested for their name first, after which each command can
|
||||
* decode its own specific payload by parsing the JSON in the "data" field and mapping
|
||||
* it into a Kotlin *Data class. The downside of this approach is a double parsing,
|
||||
* but since the data part is parsed as a string first, this is probably not a huge deal.
|
||||
*/
|
||||
class CommandData(val name: String, val data: String?, val error: String? = null)
|
||||
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.beust.kobalt.internal.remote
|
||||
|
||||
import com.beust.kobalt.misc.log
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.JsonObject
|
||||
|
||||
/**
|
||||
* A simple command that returns its own content.
|
||||
* Payload:
|
||||
* {
|
||||
* "name" : "ping"
|
||||
* }
|
||||
*
|
||||
*/
|
||||
class PingCommand() : ICommand {
|
||||
override val name = "ping"
|
||||
|
||||
override fun run(sender: ICommandSender, received: JsonObject) {
|
||||
sender.sendData(toCommandData(Gson().toJson(PingData(received.toString()))))
|
||||
}
|
||||
|
||||
class PingData(val received: String)
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package com.beust.kobalt.kotlin
|
||||
|
||||
import java.net.URL
|
||||
import java.net.URLClassLoader
|
||||
|
||||
/**
|
||||
* A parent-last classloader that will try the child classloader first and then the parent.
|
||||
* Used by the wrapper to launch a new Kobalt with not interferences from its own classes.
|
||||
* Will probably be made obsolete by making the wrapper a standalone module instead of
|
||||
* being inside Kobalt itself.
|
||||
*/
|
||||
public class ParentLastClassLoader(val classpath: List<URL>)
|
||||
: ClassLoader(Thread.currentThread().contextClassLoader) {
|
||||
private val childClassLoader: ChildURLClassLoader
|
||||
|
||||
init {
|
||||
val urls : Array<URL> = classpath.toTypedArray()
|
||||
childClassLoader = ChildURLClassLoader(urls, FindClassClassLoader(this.parent))
|
||||
}
|
||||
|
||||
/**
|
||||
* This class makes it possible to call findClass on a classloader
|
||||
*/
|
||||
private class FindClassClassLoader(parent: ClassLoader) : ClassLoader(parent) {
|
||||
override public fun findClass(name: String) = super.findClass(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* This class delegates (child then parent) for the findClass method for a URLClassLoader.
|
||||
* We need this because findClass is protected in URLClassLoader
|
||||
*/
|
||||
private class ChildURLClassLoader(urls: Array<URL>, val realParent: FindClassClassLoader)
|
||||
: URLClassLoader(urls, null) {
|
||||
|
||||
override public fun findClass(name: String) : Class<*> {
|
||||
try {
|
||||
// first try to use the URLClassLoader findClass
|
||||
return super.findClass(name)
|
||||
} catch(e: ClassNotFoundException) {
|
||||
// if that fails, we ask our real parent classloader to load the class (we give up)
|
||||
return realParent.loadClass(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override public @Synchronized fun loadClass(name: String, resolve: Boolean) : Class<*> {
|
||||
try {
|
||||
// first we try to find a class inside the child classloader
|
||||
return childClassLoader.findClass(name)
|
||||
} catch(e: ClassNotFoundException) {
|
||||
// didn't find it, try the parent
|
||||
return super.loadClass(name, resolve)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
package com.beust.kobalt.maven
|
||||
|
||||
import com.beust.kobalt.HostConfig
|
||||
import com.beust.kobalt.misc.KFiles
|
||||
import com.beust.kobalt.misc.log
|
||||
import com.beust.kobalt.misc.warn
|
||||
import com.google.common.cache.CacheBuilder
|
||||
import com.google.common.cache.CacheLoader
|
||||
import com.google.common.cache.LoadingCache
|
||||
import com.google.inject.assistedinject.Assisted
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
import java.nio.file.StandardCopyOption
|
||||
import java.util.concurrent.Callable
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Future
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class DownloadManager @Inject constructor(val factory: ArtifactFetcher.IFactory) {
|
||||
class Key(val hostInfo: HostConfig, val fileName: String, val executor: ExecutorService) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return (other as Key).hostInfo.url == hostInfo.url
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return hostInfo.url.hashCode()
|
||||
}
|
||||
}
|
||||
|
||||
private val CACHE : LoadingCache<Key, Future<File>> = CacheBuilder.newBuilder()
|
||||
.build(object : CacheLoader<Key, Future<File>>() {
|
||||
override fun load(key: Key): Future<File> {
|
||||
return key.executor.submit(factory.create(key.hostInfo, key.fileName))
|
||||
}
|
||||
})
|
||||
|
||||
fun download(hostInfo: HostConfig, fileName: String, executor: ExecutorService)
|
||||
: Future<File> = CACHE.get(Key(hostInfo, fileName, executor))
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches an artifact (a file in a Maven repo, .jar, -javadoc.jar, ...) to the given local file.
|
||||
*/
|
||||
class ArtifactFetcher @Inject constructor(@Assisted("hostInfo") val hostInfo: HostConfig,
|
||||
@Assisted("fileName") val fileName: String,
|
||||
val files: KFiles) : Callable<File> {
|
||||
interface IFactory {
|
||||
fun create(@Assisted("hostInfo") hostInfo: HostConfig, @Assisted("fileName") fileName: String) : ArtifactFetcher
|
||||
}
|
||||
|
||||
override fun call() : File {
|
||||
val k = Kurl(hostInfo.copy(url = hostInfo.url + ".md5"))
|
||||
val remoteMd5 =
|
||||
if (k.exists) k.string.trim(' ', '\t', '\n').substring(0, 32)
|
||||
else null
|
||||
|
||||
val tmpFile = Paths.get(fileName + ".tmp")
|
||||
val file = Paths.get(fileName)
|
||||
val url = hostInfo.url
|
||||
if (! Files.exists(file)) {
|
||||
with(tmpFile.toFile()) {
|
||||
parentFile.mkdirs()
|
||||
Kurl(hostInfo).toFile(this)
|
||||
}
|
||||
log(2, "Done downloading, renaming $tmpFile to $file")
|
||||
Files.move(tmpFile, file, StandardCopyOption.REPLACE_EXISTING)
|
||||
log(1, " Downloaded $url")
|
||||
log(2, " to $file")
|
||||
}
|
||||
|
||||
val localMd5 = Md5.toMd5(file.toFile())
|
||||
if (remoteMd5 != null && remoteMd5 != localMd5) {
|
||||
warn("MD5 not matching for $url")
|
||||
} else {
|
||||
log(2, "No md5 found for $url, skipping md5 check")
|
||||
}
|
||||
|
||||
return file.toFile()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package com.beust.kobalt.maven
|
||||
|
||||
import java.util.concurrent.Future
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
public class CompletedFuture<T>(val value: T) : Future<T> {
|
||||
override fun cancel(mayInterruptIfRunning: Boolean) = true
|
||||
override fun get(): T = value
|
||||
override fun get(timeout: Long, unit: TimeUnit): T = value
|
||||
override fun isCancelled(): Boolean = false
|
||||
override fun isDone(): Boolean = true
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package com.beust.kobalt.maven
|
||||
|
||||
import com.beust.kobalt.KobaltException
|
||||
import com.beust.kobalt.api.IClasspathDependency
|
||||
import com.beust.kobalt.api.Kobalt
|
||||
import com.beust.kobalt.maven.dependency.FileDependency
|
||||
import com.beust.kobalt.maven.dependency.MavenDependency
|
||||
import com.beust.kobalt.misc.DependencyExecutor
|
||||
import com.beust.kobalt.misc.KobaltExecutors
|
||||
import com.google.inject.Key
|
||||
import java.util.concurrent.ExecutorService
|
||||
import javax.inject.Inject
|
||||
|
||||
public class DepFactory @Inject constructor(val localRepo: LocalRepo,
|
||||
val remoteRepo: RepoFinder,
|
||||
val executors: KobaltExecutors,
|
||||
val downloadManager: DownloadManager,
|
||||
val pomFactory: Pom.IFactory) {
|
||||
|
||||
companion object {
|
||||
val defExecutor : ExecutorService by lazy {
|
||||
Kobalt.INJECTOR.getInstance(Key.get(ExecutorService::class.java, DependencyExecutor::class.java))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the id and return the correct IClasspathDependency
|
||||
*/
|
||||
public fun create(id: String, executor: ExecutorService = defExecutor, localFirst : Boolean = true)
|
||||
: IClasspathDependency {
|
||||
if (id.startsWith(FileDependency.PREFIX_FILE)) {
|
||||
return FileDependency(id.substring(FileDependency.PREFIX_FILE.length))
|
||||
} else {
|
||||
val mavenId = MavenId.create(id)
|
||||
var tentativeVersion = mavenId.version
|
||||
var packaging = mavenId.packaging
|
||||
var repoResult: RepoFinder.RepoResult?
|
||||
|
||||
val version =
|
||||
if (tentativeVersion != null && ! MavenId.isRangedVersion(tentativeVersion)) tentativeVersion
|
||||
else {
|
||||
var localVersion: String? = tentativeVersion
|
||||
if (localFirst) localVersion = localRepo.findLocalVersion(mavenId.groupId, mavenId.artifactId, mavenId.packaging)
|
||||
if (localFirst && localVersion != null) {
|
||||
localVersion
|
||||
} else {
|
||||
repoResult = remoteRepo.findCorrectRepo(id)
|
||||
if (!repoResult.found) {
|
||||
throw KobaltException("Couldn't resolve $id")
|
||||
} else {
|
||||
repoResult.version?.version
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return MavenDependency(MavenId.create(mavenId.groupId, mavenId.artifactId, packaging, version),
|
||||
executor, localRepo, remoteRepo, pomFactory, downloadManager)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
package com.beust.kobalt.maven
|
||||
|
||||
import com.beust.kobalt.api.*
|
||||
import com.beust.kobalt.maven.dependency.FileDependency
|
||||
import com.beust.kobalt.misc.KFiles
|
||||
import com.beust.kobalt.misc.KobaltExecutors
|
||||
import com.google.common.collect.ArrayListMultimap
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
public class DependencyManager @Inject constructor(val executors: KobaltExecutors,
|
||||
val depFactory: DepFactory){
|
||||
|
||||
/**
|
||||
* @return the classpath for this project, including the IClasspathContributors.
|
||||
* allDependencies is typically either compileDependencies or testDependencies
|
||||
*/
|
||||
fun calculateDependencies(project: Project?, context: KobaltContext,
|
||||
dependentProjects: List<ProjectDescription> = emptyList(),
|
||||
vararg allDependencies: List<IClasspathDependency>): List<IClasspathDependency> {
|
||||
var result = arrayListOf<IClasspathDependency>()
|
||||
allDependencies.forEach { dependencies ->
|
||||
result.addAll(transitiveClosure(dependencies))
|
||||
}
|
||||
result.addAll(runClasspathContributors(project, context))
|
||||
result.addAll(dependentProjectDependencies(dependentProjects, project, context))
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private fun runClasspathContributors(project: Project?, context: KobaltContext) :
|
||||
Collection<IClasspathDependency> {
|
||||
val result = hashSetOf<IClasspathDependency>()
|
||||
context.pluginInfo.classpathContributors.forEach { it: IClasspathContributor ->
|
||||
result.addAll(it.entriesFor(project))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the transitive closure of the dependencies *without* running the classpath contributors.
|
||||
* TODO: This should be private, everyone should be calling calculateDependencies().
|
||||
*/
|
||||
fun transitiveClosure(dependencies : List<IClasspathDependency>): List<IClasspathDependency> {
|
||||
var executor = executors.newExecutor("JvmCompiler}", 10)
|
||||
|
||||
var result = hashSetOf<IClasspathDependency>()
|
||||
|
||||
dependencies.forEach { projectDependency ->
|
||||
result.add(projectDependency)
|
||||
projectDependency.id.let {
|
||||
result.add(depFactory.create(it, executor))
|
||||
val downloaded = transitiveClosure(projectDependency.directDependencies())
|
||||
|
||||
result.addAll(downloaded)
|
||||
}
|
||||
}
|
||||
|
||||
val result2 = reorderDependencies(result).filter {
|
||||
// Only keep existent files (nonexistent files are probably optional dependencies or parent poms
|
||||
// that point to other poms but don't have a jar file themselves)
|
||||
it.jarFile.get().exists()
|
||||
}
|
||||
|
||||
executor.shutdown()
|
||||
|
||||
return result2
|
||||
}
|
||||
|
||||
/**
|
||||
* Reorder dependencies so that if an artifact appears several times, only the one with the higest version
|
||||
* is included.
|
||||
*/
|
||||
public fun reorderDependencies(dependencies: Collection<IClasspathDependency>): List<IClasspathDependency> {
|
||||
val result = arrayListOf<IClasspathDependency>()
|
||||
val map : ArrayListMultimap<String, IClasspathDependency> = ArrayListMultimap.create()
|
||||
// The multilist maps each artifact to a list of all the versions found
|
||||
// (e.g. {org.testng:testng -> (6.9.5, 6.9.4, 6.1.1)}), then we return just the first one
|
||||
dependencies.forEach {
|
||||
map.put(it.shortId, it)
|
||||
}
|
||||
for (k in map.keySet()) {
|
||||
val l = map.get(k)
|
||||
Collections.sort(l, Collections.reverseOrder())
|
||||
result.add(l.get(0))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* If this project depends on other projects, we need to include their jar file and also
|
||||
* their own dependencies
|
||||
*/
|
||||
private fun dependentProjectDependencies(projectDescriptions: List<ProjectDescription>,
|
||||
project: Project?, context: KobaltContext) :
|
||||
List<IClasspathDependency> {
|
||||
val result = arrayListOf<IClasspathDependency>()
|
||||
projectDescriptions.filter {
|
||||
it.project.name == project?.name
|
||||
}.forEach { pd ->
|
||||
pd.dependsOn.forEach { p ->
|
||||
result.add(FileDependency(KFiles.joinDir(p.directory, p.classesDir(context))))
|
||||
val otherDependencies = calculateDependencies(p, context, projectDescriptions,
|
||||
p.compileDependencies)
|
||||
result.addAll(otherDependencies)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the compile dependencies for this project, including the contributors.
|
||||
*/
|
||||
fun dependencies(project: Project, context: KobaltContext,
|
||||
projects: List<ProjectDescription>) : List<IClasspathDependency> {
|
||||
val result = arrayListOf<IClasspathDependency>()
|
||||
result.add(FileDependency(KFiles.makeOutputDir(project).absolutePath))
|
||||
result.add(FileDependency(KFiles.makeOutputTestDir(project).absolutePath))
|
||||
with(project) {
|
||||
arrayListOf(compileDependencies, compileProvidedDependencies).forEach {
|
||||
result.addAll(calculateDependencies(project, context, projects, it))
|
||||
}
|
||||
}
|
||||
val result2 = reorderDependencies(result)
|
||||
return result2
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the test dependencies for this project, including the contributors.
|
||||
*/
|
||||
fun testDependencies(project: Project, context: KobaltContext,
|
||||
projects: List<ProjectDescription>) : List<IClasspathDependency> {
|
||||
val result = arrayListOf<IClasspathDependency>()
|
||||
result.add(FileDependency(KFiles.makeOutputDir(project).absolutePath))
|
||||
result.add(FileDependency(KFiles.makeOutputTestDir(project).absolutePath))
|
||||
with(project) {
|
||||
arrayListOf(compileDependencies, compileProvidedDependencies, testDependencies,
|
||||
testProvidedDependencies).forEach {
|
||||
result.addAll(calculateDependencies(project, context, projects, it))
|
||||
}
|
||||
}
|
||||
val result2 = reorderDependencies(result)
|
||||
return result2
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
package com.beust.kobalt.maven
|
||||
|
||||
import com.beust.kobalt.OperatingSystem
|
||||
import com.beust.kobalt.misc.error
|
||||
import com.beust.kobalt.misc.log
|
||||
import com.beust.kobalt.misc.warn
|
||||
import com.google.inject.Singleton
|
||||
import java.io.BufferedReader
|
||||
import java.io.File
|
||||
import java.io.InputStreamReader
|
||||
|
||||
@Singleton
|
||||
public class Gpg {
|
||||
val COMMANDS = listOf("gpg", "gpg2")
|
||||
|
||||
fun findGpgCommand() : String? {
|
||||
val path = System.getenv("PATH")
|
||||
if (path != null) {
|
||||
path.split(File.pathSeparator).forEach { dir ->
|
||||
COMMANDS.map {
|
||||
File(dir, if (OperatingSystem.current().isWindows()) it + ".exe" else it)
|
||||
}.firstOrNull {
|
||||
it.exists()
|
||||
}?.let {
|
||||
return it.absolutePath
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the .asc files
|
||||
*/
|
||||
fun runGpg(files: List<File>) : List<File> {
|
||||
val result = arrayListOf<File>()
|
||||
val gpg = findGpgCommand()
|
||||
if (gpg != null) {
|
||||
val directory = files.get(0).parentFile.absoluteFile
|
||||
files.forEach { file ->
|
||||
val ascFile = File(file.absolutePath + ".asc")
|
||||
ascFile.delete()
|
||||
val allArgs = arrayListOf<String>()
|
||||
allArgs.add(gpg)
|
||||
allArgs.add("-ab")
|
||||
allArgs.add(file.absolutePath)
|
||||
|
||||
val pb = ProcessBuilder(allArgs)
|
||||
pb.directory(directory)
|
||||
log(2, "Signing file: " + allArgs.joinToString(" "))
|
||||
val process = pb.start()
|
||||
|
||||
val br = BufferedReader(InputStreamReader(process.errorStream))
|
||||
val errorCode = process.waitFor()
|
||||
if (errorCode != 0) {
|
||||
var line = br.readLine()
|
||||
while (line != null) {
|
||||
error(line)
|
||||
line = br.readLine()
|
||||
}
|
||||
warn("Couldn't sign file $file")
|
||||
} else {
|
||||
result.add(ascFile)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
} else {
|
||||
warn("Couldn't find the gpg command, make sure it is on your PATH")
|
||||
warn("Signing of artifacts with PGP (.asc) disabled")
|
||||
return arrayListOf()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
package com.beust.kobalt.maven
|
||||
|
||||
import com.beust.kobalt.KobaltException
|
||||
import com.beust.kobalt.misc.CountingFileRequestBody
|
||||
import com.beust.kobalt.misc.log
|
||||
import com.squareup.okhttp.*
|
||||
import retrofit.mime.TypedFile
|
||||
import java.io.IOException
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
public class Http {
|
||||
public fun get(user: String?, password: String?, url: String) : Response {
|
||||
val client = OkHttpClient();
|
||||
val request = Request.Builder().url(url)
|
||||
if (user != null) {
|
||||
request.header("Authorization", Credentials.basic(user, password))
|
||||
}
|
||||
|
||||
try {
|
||||
return client.newCall(request.build()).execute()
|
||||
} catch(ex: IOException) {
|
||||
throw KobaltException("Could not load URL $url, error: " + ex.message, ex)
|
||||
}
|
||||
}
|
||||
|
||||
public fun get(url: String) : Response {
|
||||
return get(null, null, url)
|
||||
}
|
||||
|
||||
fun percentProgressCallback(totalSize: Long) : (Long) -> Unit {
|
||||
return { num: Long ->
|
||||
val progress = num * 100 / totalSize
|
||||
log(1, "\rUploaded: $progress%", newLine = false)
|
||||
}
|
||||
}
|
||||
|
||||
val DEFAULT_ERROR_RESPONSE = { r: Response ->
|
||||
error("Couldn't upload file: " + r.message())
|
||||
}
|
||||
|
||||
public fun uploadFile(user: String? = null, password: String? = null, url: String, file: TypedFile,
|
||||
post: Boolean,
|
||||
progressCallback: (Long) -> Unit = {},
|
||||
headers: Headers = Headers.of(),
|
||||
success: (Response) -> Unit = {},
|
||||
error: (Response) -> Unit = DEFAULT_ERROR_RESPONSE) {
|
||||
|
||||
val fullHeaders = Headers.Builder()
|
||||
fullHeaders.set("Content-Type", file.mimeType())
|
||||
headers.names().forEach { fullHeaders.set(it, headers.get(it)) }
|
||||
|
||||
user?.let {
|
||||
fullHeaders.set("Authorization", Credentials.basic(user, password))
|
||||
}
|
||||
|
||||
val requestBuilder = Request.Builder()
|
||||
.headers(fullHeaders.build())
|
||||
.url(url)
|
||||
val request =
|
||||
(if (post)
|
||||
requestBuilder.post(CountingFileRequestBody(file.file(), file.mimeType(), progressCallback))
|
||||
else
|
||||
requestBuilder.put(CountingFileRequestBody(file.file(), file.mimeType(), progressCallback)))
|
||||
.build()
|
||||
|
||||
log(2, "Uploading $file to $url")
|
||||
val response = OkHttpClient().newCall(request).execute()
|
||||
if (! response.isSuccessful) {
|
||||
error(response)
|
||||
} else {
|
||||
success(response)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,137 @@
|
|||
package com.beust.kobalt.maven
|
||||
|
||||
import com.beust.kobalt.HostConfig
|
||||
import com.beust.kobalt.KobaltException
|
||||
import com.beust.kobalt.maven.dependency.FileDependency
|
||||
import com.beust.kobalt.misc.LocalProperties
|
||||
import java.io.*
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.net.URLConnection
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Abstracts a URL so that it works transparently on either http:// or file://
|
||||
*/
|
||||
class Kurl(val hostInfo: HostConfig) {
|
||||
companion object {
|
||||
const val KEY = "authUrl"
|
||||
const val VALUE_USER = "username"
|
||||
const val VALUE_PASSWORD = "password"
|
||||
}
|
||||
|
||||
init {
|
||||
// See if the URL needs to be authenticated. Look in local.properties for keys
|
||||
// of the format authUrl.<host>.user=xxx and authUrl.<host>.password=xxx
|
||||
val properties = LocalProperties().localProperties
|
||||
val host = java.net.URL(hostInfo.url).host
|
||||
properties.entries.forEach {
|
||||
val key = it.key.toString()
|
||||
if (key == "$KEY.$host.$VALUE_USER") {
|
||||
hostInfo.username = properties.getProperty(key)
|
||||
} else if (key == "$KEY.$host.$VALUE_PASSWORD") {
|
||||
hostInfo.password = properties.getProperty(key)
|
||||
}
|
||||
}
|
||||
fun error(s1: String, s2: String) {
|
||||
throw KobaltException("Found \"$s1\" but not \"$s2\" in local.properties for $KEY.$host",
|
||||
docUrl = "http://beust.com/kobalt/documentation/index.html#maven-repos-authenticated")
|
||||
}
|
||||
if (! hostInfo.username.isNullOrBlank() && hostInfo.password.isNullOrBlank()) {
|
||||
error("username", "password")
|
||||
} else if(hostInfo.username.isNullOrBlank() && ! hostInfo.password.isNullOrBlank()) {
|
||||
error("password", "username")
|
||||
}
|
||||
}
|
||||
|
||||
val connection : URLConnection
|
||||
get() {
|
||||
val result = URL(hostInfo.url).openConnection()
|
||||
if (hostInfo.hasAuth()) {
|
||||
val userPass = hostInfo.username + ":" + hostInfo.password
|
||||
val basicAuth = "Basic " + String(Base64.getEncoder().encode(userPass.toByteArray()))
|
||||
result.setRequestProperty("Authorization", basicAuth)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
val inputStream : InputStream by lazy {
|
||||
connection.inputStream
|
||||
}
|
||||
|
||||
val exists : Boolean
|
||||
get() {
|
||||
val url = hostInfo.url
|
||||
val result =
|
||||
if (connection is HttpURLConnection) {
|
||||
val responseCode = (connection as HttpURLConnection).responseCode
|
||||
checkResponseCode(responseCode)
|
||||
responseCode == 200
|
||||
} else if (url.startsWith(FileDependency.PREFIX_FILE)) {
|
||||
val fileName = url.substring(FileDependency.PREFIX_FILE.length)
|
||||
File(fileName).exists()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun checkResponseCode(responseCode: Int) {
|
||||
if (responseCode == 401) {
|
||||
if (hostInfo.hasAuth()) {
|
||||
error("Bad credentials supplied for ${hostInfo.url}")
|
||||
} else {
|
||||
error("This repo requires authentication: ${hostInfo.url}")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/** The Kotlin compiler is about 17M and downloading it with the default buffer size takes forever */
|
||||
private val estimatedSize: Int
|
||||
get() = if (hostInfo.url.contains("kotlin-compiler")) 18000000 else 1000000
|
||||
|
||||
fun toOutputStream(os: OutputStream, progress: (Long) -> Unit) = copy(inputStream, os, progress)
|
||||
|
||||
fun toFile(file: File, progress: (Long) -> Unit = {}) = FileOutputStream(file).use {
|
||||
toOutputStream(it, progress)
|
||||
}
|
||||
|
||||
private fun copy(from: InputStream, to: OutputStream, progress: (Long) -> Unit = {}) : Long {
|
||||
val estimate =
|
||||
if (connection is HttpURLConnection) {
|
||||
(connection as HttpURLConnection).let {
|
||||
it.contentLength
|
||||
}
|
||||
} else {
|
||||
estimatedSize
|
||||
}
|
||||
|
||||
val buf = ByteArray(estimatedSize)
|
||||
var total: Long = 0
|
||||
while (true) {
|
||||
val r = from.read(buf)
|
||||
if (r == -1) {
|
||||
break
|
||||
}
|
||||
to.write(buf, 0, r)
|
||||
total += r.toLong()
|
||||
progress(total * 100 / estimate)
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
val string: String
|
||||
get() {
|
||||
val sb = StringBuilder()
|
||||
val reader = BufferedReader(InputStreamReader(inputStream))
|
||||
|
||||
var line: String? = reader.readLine()
|
||||
while (line != null) {
|
||||
sb.append(line).append('\n')
|
||||
line = reader.readLine()
|
||||
}
|
||||
|
||||
return sb.toString()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package com.beust.kobalt.maven
|
||||
|
||||
import com.beust.kobalt.maven.CompletedFuture
|
||||
import com.beust.kobalt.misc.Strings
|
||||
import java.io.File
|
||||
import java.util.concurrent.Future
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
open public class LocalDep(override val mavenId: MavenId, open val localRepo: LocalRepo)
|
||||
: SimpleDep(mavenId) {
|
||||
|
||||
fun toAbsoluteJarFilePath(v: String) = localRepo.toFullPath(toJarFile(v))
|
||||
|
||||
fun toAbsolutePomFile(v: String): String {
|
||||
return localRepo.toFullPath(Strings.Companion.join("/", arrayListOf(toPomFile(v))))
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
package com.beust.kobalt.maven
|
||||
|
||||
import com.beust.kobalt.misc.KFiles
|
||||
import com.beust.kobalt.misc.Versions
|
||||
import java.io.File
|
||||
import java.util.Collections
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
open public class LocalRepo(open val localRepo: String = KFiles.localRepo) {
|
||||
init {
|
||||
val l = File(localRepo)
|
||||
if (! l.exists()) {
|
||||
l.mkdirs()
|
||||
}
|
||||
}
|
||||
|
||||
fun existsPom(d: LocalDep, v: String) : Boolean {
|
||||
return File(d.toAbsolutePomFile(v)).exists()
|
||||
}
|
||||
|
||||
fun existsJar(d: LocalDep, v: String) : Boolean {
|
||||
return File(d.toAbsoluteJarFilePath(v)).exists()
|
||||
}
|
||||
|
||||
/**
|
||||
* If the dependency is local, return the correct version for it
|
||||
*/
|
||||
fun findLocalVersion(groupId: String, artifactId: String, packaging: String? = null) : String? {
|
||||
// No version: look at all the directories under group/artifactId, pick the latest and see
|
||||
// if it contains a maven and jar file
|
||||
val dir = toFullPath(KFiles.joinDir(groupId.replace(".", File.separator), artifactId))
|
||||
val files = File(dir).listFiles()
|
||||
|
||||
if (files != null) {
|
||||
val directories = files.filter { it.isDirectory }
|
||||
if (directories.size > 0) {
|
||||
Collections.sort(directories, { f1, f2 ->
|
||||
val v1 = Versions.toLongVersion(f1.name)
|
||||
val v2 = Versions.toLongVersion(f2.name)
|
||||
v2.compareTo(v1) // we want the most recent at position 0
|
||||
})
|
||||
val result = directories[0].name
|
||||
val newDep = LocalDep(MavenId.create(groupId, artifactId, packaging, result), this)
|
||||
if (existsPom(newDep, result) && existsJar(newDep, result)) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun toFullPath(path: String) : String {
|
||||
return localRepo + File.separatorChar + path
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
package com.beust.kobalt.maven
|
||||
|
||||
import com.beust.kobalt.api.Kobalt
|
||||
|
||||
/**
|
||||
* Encapsulate a Maven id captured in one string, as used by Gradle or Ivy, e.g. "org.testng:testng:6.9.9".
|
||||
* These id's are somewhat painful to manipulate because on top of containing groupId, artifactId
|
||||
* and version, they also accept an optional packaging (e.g. "aar") and qualifier (e.g. "no_aop").
|
||||
* Determining which is which in an untyped string separated by colons is not clearly defined so
|
||||
* this class does a best attempt at deconstructing an id but there's surely room for improvement.
|
||||
*
|
||||
* This class accepts a versionless id, which needs to end with a :, e.g. "com.beust:jcommander:" (which
|
||||
* usually means "latest version") but it doesn't handle version ranges yet.
|
||||
*/
|
||||
class MavenId private constructor(val groupId: String, val artifactId: String, val packaging: String?,
|
||||
val version: String?) {
|
||||
|
||||
companion object {
|
||||
fun isMavenId(id: String) = with(id.split(":")) {
|
||||
size == 3 || size == 4
|
||||
}
|
||||
|
||||
private fun isVersion(s: String): Boolean {
|
||||
return Character.isDigit(s[0]) || isRangedVersion(s)
|
||||
}
|
||||
|
||||
fun isRangedVersion(s: String): Boolean {
|
||||
return s.first() in listOf('[', '(') && s.last() in listOf(']', ')')
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to create(MavenId) but don't run IMavenIdInterceptors.
|
||||
*/
|
||||
fun createNoInterceptors(id: String) : MavenId {
|
||||
var groupId: String? = null
|
||||
var artifactId: String? = null
|
||||
var version: String? = null
|
||||
var packaging: String? = null
|
||||
if (!isMavenId(id)) {
|
||||
throw IllegalArgumentException("Illegal id: $id")
|
||||
}
|
||||
|
||||
val c = id.split(":")
|
||||
groupId = c[0]
|
||||
artifactId = c[1]
|
||||
if (!c[2].isEmpty()) {
|
||||
val split = c[2].split('@')
|
||||
if (isVersion(split[0])) {
|
||||
version = split[0]
|
||||
}
|
||||
packaging = if (split.size == 2) split[1] else null
|
||||
}
|
||||
|
||||
return MavenId(groupId, artifactId, packaging, version)
|
||||
}
|
||||
|
||||
/**
|
||||
* The main entry point to create Maven Id's. Id's created by this function
|
||||
* will run through IMavenIdInterceptors.
|
||||
*/
|
||||
fun create(id: String) : MavenId {
|
||||
var originalMavenId = createNoInterceptors(id)
|
||||
var interceptedMavenId = originalMavenId
|
||||
val interceptors = Kobalt.context?.pluginInfo?.mavenIdInterceptors
|
||||
if (interceptors != null) {
|
||||
interceptedMavenId = interceptors.fold(originalMavenId, {
|
||||
id, interceptor -> interceptor.intercept(id) })
|
||||
}
|
||||
|
||||
return interceptedMavenId
|
||||
}
|
||||
|
||||
fun create(groupId: String, artifactId: String, packaging: String?, version: String?) =
|
||||
create(toId(groupId, artifactId, packaging, version))
|
||||
|
||||
fun toId(groupId: String, artifactId: String, packaging: String? = null, version: String?) =
|
||||
"$groupId:$artifactId:$version" +
|
||||
(if (packaging != null) "@$packaging" else "")
|
||||
}
|
||||
|
||||
|
||||
val hasVersion = version != null
|
||||
|
||||
val toId = MavenId.toId(groupId, artifactId, packaging, version)
|
||||
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package com.beust.kobalt.maven
|
||||
|
||||
import java.io.File
|
||||
import java.security.MessageDigest
|
||||
import javax.xml.bind.DatatypeConverter
|
||||
|
||||
public class Md5 {
|
||||
companion object {
|
||||
fun toMd5(file: File) = MessageDigest.getInstance("MD5").let { md5 ->
|
||||
file.forEachBlock { bytes, size ->
|
||||
md5.update(bytes, 0, size)
|
||||
}
|
||||
DatatypeConverter.printHexBinary(md5.digest()).toLowerCase()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
package com.beust.kobalt.maven
|
||||
|
||||
import com.beust.kobalt.misc.log
|
||||
import com.beust.kobalt.misc.toString
|
||||
import com.google.inject.assistedinject.Assisted
|
||||
import kotlinx.dom.childElements
|
||||
import org.w3c.dom.Element
|
||||
import org.w3c.dom.NodeList
|
||||
import org.xml.sax.InputSource
|
||||
import java.io.FileReader
|
||||
import javax.xml.xpath.XPathConstants
|
||||
|
||||
public class Pom @javax.inject.Inject constructor(@Assisted val id: String,
|
||||
@Assisted documentFile: java.io.File) {
|
||||
val XPATH_FACTORY = javax.xml.xpath.XPathFactory.newInstance()
|
||||
val XPATH = XPATH_FACTORY.newXPath()
|
||||
var groupId: String? = null
|
||||
var artifactId: String? = null
|
||||
var packaging: String? = null
|
||||
var version: String? = null
|
||||
var name: String? = null
|
||||
var properties = sortedMapOf<String, String>()
|
||||
var repositories = listOf<String>()
|
||||
|
||||
public interface IFactory {
|
||||
fun create(@Assisted id: String, @Assisted documentFile: java.io.File): Pom
|
||||
}
|
||||
|
||||
data public class Dependency(val groupId: String, val artifactId: String, val packaging: String?,
|
||||
val version: String, val optional: Boolean = false, val scope: String? = null) {
|
||||
|
||||
/** When a variable is used in a maven file, e.g. ${version} */
|
||||
private val VAR = "$" + "{"
|
||||
|
||||
val mustDownload: Boolean
|
||||
get() = !optional && "provided" != scope && "test" != scope
|
||||
|
||||
val isValid: Boolean
|
||||
get() {
|
||||
var result = false
|
||||
if (version.contains(VAR)) {
|
||||
log(3, "Skipping variable version ${this}")
|
||||
} else if (groupId.contains(VAR)) {
|
||||
log(3, "Skipping variable groupId ${this}")
|
||||
} else if (artifactId.contains(VAR)) {
|
||||
log(3, "Skipping variable artifactId ${this}")
|
||||
} else {
|
||||
result = true
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
val id: String = "$groupId:$artifactId:$version"
|
||||
}
|
||||
|
||||
val dependencies = arrayListOf<Dependency>()
|
||||
|
||||
init {
|
||||
val DEPENDENCIES = XPATH.compile("/project/dependencies/dependency")
|
||||
|
||||
val document = kotlinx.dom.parseXml(InputSource(FileReader(documentFile)))
|
||||
groupId = XPATH.compile("/project/groupId").evaluate(document)
|
||||
artifactId = XPATH.compile("/project/artifactId").evaluate(document)
|
||||
version = XPATH.compile("/project/version").evaluate(document)
|
||||
name = XPATH.compile("/project/name").evaluate(document)
|
||||
var repositoriesList = XPATH.compile("/project/repositories").evaluate(document, XPathConstants.NODESET)
|
||||
as NodeList
|
||||
var repoElem = repositoriesList.item(0) as Element?
|
||||
repositories = repoElem.childElements().map({ it.getElementsByTagName("url").item(0).textContent })
|
||||
|
||||
val propertiesList = XPATH.compile("/project/properties").evaluate(document, XPathConstants.NODESET) as NodeList
|
||||
var propsElem = propertiesList.item(0) as Element?
|
||||
propsElem.childElements().forEach {
|
||||
properties.put(it.nodeName, it.textContent)
|
||||
}
|
||||
|
||||
val deps = DEPENDENCIES.evaluate(document, XPathConstants.NODESET) as NodeList
|
||||
for (i in 0..deps.length - 1) {
|
||||
val d = deps.item(i) as NodeList
|
||||
var groupId: String? = null
|
||||
var artifactId: String? = null
|
||||
var packaging: String? = null
|
||||
var version: String = ""
|
||||
var optional: Boolean? = false
|
||||
var scope: String? = null
|
||||
for (j in 0..d.length - 1) {
|
||||
val e = d.item(j)
|
||||
if (e is Element) {
|
||||
when (e.tagName) {
|
||||
"groupId" -> groupId = e.textContent
|
||||
"artifactId" -> artifactId = e.textContent
|
||||
"type" -> packaging = e.textContent
|
||||
"version" -> version = e.textContent
|
||||
"optional" -> optional = "true".equals(e.textContent, true)
|
||||
"scope" -> scope = e.textContent
|
||||
}
|
||||
}
|
||||
}
|
||||
log(3, "Done parsing: $groupId $artifactId $version")
|
||||
val tmpDependency = Dependency(groupId!!, artifactId!!, packaging, version, optional!!, scope)
|
||||
dependencies.add(tmpDependency)
|
||||
}
|
||||
}
|
||||
|
||||
override public fun toString() = toString("Pom", id, "id")
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
package com.beust.kobalt.maven
|
||||
|
||||
import com.beust.kobalt.SystemProperties
|
||||
import com.beust.kobalt.api.Project
|
||||
import com.beust.kobalt.misc.KFiles
|
||||
import com.beust.kobalt.misc.log
|
||||
import com.google.inject.assistedinject.Assisted
|
||||
import org.apache.maven.model.Developer
|
||||
import org.apache.maven.model.Model
|
||||
import org.apache.maven.model.Scm
|
||||
import org.apache.maven.model.io.xpp3.MavenXpp3Writer
|
||||
import java.io.File
|
||||
import java.io.StringWriter
|
||||
import java.nio.charset.Charset
|
||||
import javax.inject.Inject
|
||||
|
||||
public class PomGenerator @Inject constructor(@Assisted val project: Project) {
|
||||
interface IFactory {
|
||||
fun create(project: Project) : PomGenerator
|
||||
}
|
||||
|
||||
fun generate() {
|
||||
requireNotNull(project.version, { "version mandatory on project ${project.name}" })
|
||||
requireNotNull(project.group, { "group mandatory on project ${project.name}" })
|
||||
requireNotNull(project.artifactId, { "artifactId mandatory on project ${project.name}" })
|
||||
|
||||
val m = Model().apply {
|
||||
name = project.name
|
||||
artifactId = project.artifactId
|
||||
groupId = project.group
|
||||
version = project.version
|
||||
description = project.description
|
||||
licenses = project.licenses.map { it.toMavenLicense() }
|
||||
url = project.url
|
||||
scm = Scm().apply {
|
||||
project.scm?.let {
|
||||
url = it.url
|
||||
connection = it.connection
|
||||
developerConnection = it.developerConnection
|
||||
}
|
||||
}
|
||||
}
|
||||
with(Developer()) {
|
||||
name = SystemProperties.username
|
||||
m.addDeveloper(this)
|
||||
}
|
||||
|
||||
val dependencies = arrayListOf<org.apache.maven.model.Dependency>()
|
||||
m.dependencies = dependencies
|
||||
project.compileDependencies.forEach { dep ->
|
||||
dependencies.add(dep.toMavenDependencies())
|
||||
}
|
||||
|
||||
val s = StringWriter()
|
||||
MavenXpp3Writer().write(s, m)
|
||||
|
||||
val buildDir = KFiles.makeDir(project.directory, project.buildDirectory)
|
||||
val outputDir = KFiles.makeDir(buildDir.path, "libs")
|
||||
val mavenId = MavenId.create(project.group!!, project.artifactId!!, project.packaging, project.version!!)
|
||||
val pomFile = SimpleDep(mavenId).toPomFileName()
|
||||
val outputFile = File(outputDir, pomFile)
|
||||
outputFile.writeText(s.toString(), Charset.defaultCharset())
|
||||
log(1, " Wrote $outputFile")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,213 @@
|
|||
package com.beust.kobalt.maven
|
||||
|
||||
import com.beust.kobalt.HostConfig
|
||||
import com.beust.kobalt.api.Kobalt
|
||||
import com.beust.kobalt.maven.dependency.FileDependency
|
||||
import com.beust.kobalt.misc.*
|
||||
import com.google.common.cache.CacheBuilder
|
||||
import com.google.common.cache.CacheLoader
|
||||
import com.google.common.cache.LoadingCache
|
||||
import kotlinx.dom.asElementList
|
||||
import kotlinx.dom.parseXml
|
||||
import org.w3c.dom.NodeList
|
||||
import java.io.File
|
||||
import java.util.concurrent.Callable
|
||||
import java.util.concurrent.ExecutorCompletionService
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
import javax.xml.xpath.XPathConstants
|
||||
import javax.xml.xpath.XPathFactory
|
||||
|
||||
/**
|
||||
* Find the repo that contains the given dependency among a list of repos. Searches are performed in parallel and
|
||||
* cached so we never make a network call for the same dependency more than once.
|
||||
*/
|
||||
public class RepoFinder @Inject constructor(val executors: KobaltExecutors) {
|
||||
public fun findCorrectRepo(id: String): RepoResult {
|
||||
return FOUND_REPOS.get(id)
|
||||
}
|
||||
|
||||
data class RepoResult(val hostConfig: HostConfig, val found: Boolean, val version: Version? = null,
|
||||
val hasJar: Boolean = true, val snapshotVersion: Version? = null)
|
||||
|
||||
private val FOUND_REPOS: LoadingCache<String, RepoResult> = CacheBuilder.newBuilder()
|
||||
.build(object : CacheLoader<String, RepoResult>() {
|
||||
override fun load(key: String): RepoResult {
|
||||
return loadCorrectRepo(key)
|
||||
}})
|
||||
|
||||
/**
|
||||
* Schedule an HTTP request to each repo in its own thread.
|
||||
*/
|
||||
private fun loadCorrectRepo(id: String): RepoResult {
|
||||
val executor = executors.newExecutor("RepoFinder-$id", Kobalt.repos.size)
|
||||
val cs = ExecutorCompletionService<RepoResult>(executor)
|
||||
|
||||
val results = arrayListOf<RepoResult>()
|
||||
try {
|
||||
log(2, "Looking for $id")
|
||||
Kobalt.repos.forEach { cs.submit(RepoFinderCallable(id, it)) }
|
||||
for (i in 0..Kobalt.repos.size - 1) {
|
||||
try {
|
||||
val result = cs.take().get(2000, TimeUnit.MILLISECONDS)
|
||||
log(2, " Result for repo #$i: $result")
|
||||
if (result.found) {
|
||||
log(2, "Located $id in ${result.hostConfig.url}")
|
||||
results.add(result)
|
||||
}
|
||||
} catch(ex: Exception) {
|
||||
warn("Error: $ex")
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
executor.shutdownNow()
|
||||
}
|
||||
|
||||
if (results.size > 0) {
|
||||
// results.sortByDescending { Versions.toLongVersion(it.version) }
|
||||
results.sort({ left, right -> left.version!!.compareTo(right.version!!) })
|
||||
return results[0]
|
||||
} else {
|
||||
return RepoResult(HostConfig(""), false, Version.of(id))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a single HTTP request to one repo.
|
||||
*/
|
||||
inner class RepoFinderCallable(val id: String, val repo: HostConfig) : Callable<RepoResult> {
|
||||
override fun call(): RepoResult {
|
||||
val repoUrl = repo.url
|
||||
log(2, " Checking $repoUrl for $id")
|
||||
|
||||
val mavenId = MavenId.create(id)
|
||||
val groupId = mavenId.groupId
|
||||
val artifactId = mavenId.artifactId
|
||||
|
||||
if (mavenId.version == null) {
|
||||
val ud = UnversionedDep(groupId, artifactId)
|
||||
val isLocal = repoUrl.startsWith(FileDependency.PREFIX_FILE)
|
||||
val foundVersion = findCorrectVersionRelease(ud.toMetadataXmlPath(false, isLocal), repoUrl)
|
||||
if (foundVersion != null) {
|
||||
return RepoResult(repo, true, Version.of(foundVersion))
|
||||
} else {
|
||||
return RepoResult(repo, false)
|
||||
}
|
||||
} else {
|
||||
val version = Version.of(mavenId.version)
|
||||
if (version.isSnapshot()) {
|
||||
val dep = SimpleDep(mavenId)
|
||||
val isLocal = repoUrl.startsWith(FileDependency.PREFIX_FILE)
|
||||
val snapshotVersion = if (isLocal) version
|
||||
else findSnapshotVersion(dep.toMetadataXmlPath(false, isLocal, version.version), repoUrl)
|
||||
if (snapshotVersion != null) {
|
||||
return RepoResult(repo, true, version, true /* hasJar, potential bug here */,
|
||||
snapshotVersion)
|
||||
} else {
|
||||
return RepoResult(repo, false)
|
||||
}
|
||||
} else if (version.isRangedVersion() ) {
|
||||
val foundVersion = findRangedVersion(SimpleDep(mavenId), repoUrl)
|
||||
if (foundVersion != null) {
|
||||
return RepoResult(repo, true, foundVersion)
|
||||
} else {
|
||||
return RepoResult(repo, false)
|
||||
}
|
||||
|
||||
} else {
|
||||
val dep = SimpleDep(mavenId)
|
||||
// Try to find the jar file
|
||||
val urlJar = repo.copy(url = repo.url + dep.toJarFile(dep.version))
|
||||
val hasJar = Kurl(urlJar).exists
|
||||
val found =
|
||||
if (! hasJar) {
|
||||
// No jar, try to find the directory
|
||||
val url = repo.copy(url = repoUrl
|
||||
+ File(dep.toJarFile(dep.version)).parentFile.path.replace("\\", "/"))
|
||||
Kurl(url).exists
|
||||
} else {
|
||||
true
|
||||
}
|
||||
log(2, "Result for $repoUrl for $id: $found")
|
||||
return RepoResult(repo, found, Version.of(dep.version), hasJar)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val XPATH_FACTORY = XPathFactory.newInstance();
|
||||
val XPATH = XPATH_FACTORY.newXPath();
|
||||
|
||||
fun findCorrectVersionRelease(metadataPath: String, repoUrl: String): String? {
|
||||
val XPATHS = arrayListOf(
|
||||
XPATH.compile("/metadata/version"),
|
||||
XPATH.compile("/metadata/versioning/latest"),
|
||||
XPATH.compile("/metadata/versioning/release"))
|
||||
// No version in this dependency, find out the most recent one by parsing maven-metadata.xml, if it exists
|
||||
val url = repoUrl + metadataPath
|
||||
try {
|
||||
val doc = parseXml(url)
|
||||
arrayListOf(XPATHS.forEach {
|
||||
val result = it.evaluate(doc, XPathConstants.STRING) as String
|
||||
if (! result.isEmpty()) {
|
||||
return result
|
||||
}
|
||||
})
|
||||
} catch(ex: Exception) {
|
||||
log(2, "Couldn't find metadata at $url: ${ex.message}")
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun findRangedVersion(dep: SimpleDep, repoUrl: String): Version? {
|
||||
val l = listOf(dep.groupId.replace(".", "/"), dep.artifactId.replace(".", "/"), "maven-metadata.xml")
|
||||
var metadataPath = l.joinToString("/")
|
||||
|
||||
val versionsXpath = XPATH.compile("/metadata/versioning/versions/version")
|
||||
|
||||
// No version in this dependency, find out the most recent one by parsing maven-metadata.xml, if it exists
|
||||
val url = repoUrl + metadataPath
|
||||
try {
|
||||
val doc = parseXml(url)
|
||||
val version = Version.of(dep.version)
|
||||
if(version.isRangedVersion()) {
|
||||
val versions = (versionsXpath.evaluate(doc, XPathConstants.NODESET) as NodeList)
|
||||
.asElementList().map { Version.of(it.textContent) }
|
||||
return version.select(versions)
|
||||
} else {
|
||||
return Version.of(XPATH.compile("/metadata/versioning/versions/version/$version")
|
||||
.evaluate(doc, XPathConstants.STRING) as String)
|
||||
}
|
||||
} catch(ex: Exception) {
|
||||
log(2, "Couldn't find metadata at ${url}")
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun findSnapshotVersion(metadataPath: String, repoUrl: String): Version? {
|
||||
val timestamp = XPATH.compile("/metadata/versioning/snapshot/timestamp")
|
||||
val buildNumber = XPATH.compile("/metadata/versioning/snapshot/buildNumber")
|
||||
// No version in this dependency, find out the most recent one by parsing maven-metadata.xml, if it exists
|
||||
val url = repoUrl + metadataPath
|
||||
try {
|
||||
val doc = parseXml(url)
|
||||
val ts = timestamp.evaluate(doc, XPathConstants.STRING)
|
||||
val bn = buildNumber.evaluate(doc, XPathConstants.STRING)
|
||||
if (! Strings.isEmpty(ts.toString()) && ! Strings.isEmpty(bn.toString())) {
|
||||
return Version.of(ts.toString() + "-" + bn.toString())
|
||||
} else {
|
||||
val lastUpdated = XPATH.compile("/metadata/versioning/lastUpdated")
|
||||
if (! lastUpdated.toString().isEmpty()) {
|
||||
return Version.of(lastUpdated.toString())
|
||||
}
|
||||
|
||||
}
|
||||
} catch(ex: Exception) {
|
||||
log(2, "Couldn't find metadata at $url")
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package com.beust.kobalt.maven
|
||||
|
||||
import com.beust.kobalt.misc.Strings
|
||||
|
||||
open class SimpleDep(open val mavenId: MavenId) : UnversionedDep(mavenId.groupId, mavenId.artifactId) {
|
||||
companion object {
|
||||
fun create(id: String) = MavenId.create(id).let {
|
||||
SimpleDep(it)
|
||||
}
|
||||
}
|
||||
|
||||
val version: String get() = mavenId.version!!
|
||||
|
||||
private fun toFile(v: String, snapshotVersion: String?, suffix: String) : String {
|
||||
val fv = if (v.contains("SNAPSHOT")) v.replace("SNAPSHOT", "") else v
|
||||
val result = Strings.join("/", arrayListOf(toDirectory(v, false) +
|
||||
artifactId + "-" + fv + (snapshotVersion ?: "") + suffix))
|
||||
return result
|
||||
}
|
||||
|
||||
fun toPomFile(v: String) = toFile(v, "", ".pom")
|
||||
|
||||
fun toPomFile(r: RepoFinder.RepoResult) = toFile(r.version!!.version, r.snapshotVersion?.version, ".pom")
|
||||
|
||||
fun toJarFile(v: String = version) = toFile(v, "", suffix)
|
||||
|
||||
fun toJarFile(r: RepoFinder.RepoResult) = toFile(r.version!!.version, r.snapshotVersion?.version, suffix)
|
||||
|
||||
fun toPomFileName() = "$artifactId-$version.pom"
|
||||
|
||||
val suffix : String
|
||||
get() {
|
||||
val packaging = mavenId.packaging
|
||||
return if (packaging != null && ! packaging.isNullOrBlank()) ".$packaging" else ".jar"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package com.beust.kobalt.maven
|
||||
|
||||
import com.beust.kobalt.misc.Strings
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Represents a dependency that doesn't have a version: "org.testng:testng:". Such dependencies
|
||||
* eventually resolve to the latest version of the artifact.
|
||||
*/
|
||||
open class UnversionedDep(open val groupId: String, open val artifactId: String) {
|
||||
open fun toMetadataXmlPath(fileSystem: Boolean = true, isLocal: Boolean, version: String? = null) : String {
|
||||
var result = toDirectory("", fileSystem) + if (isLocal) "maven-metadata-local.xml" else "maven-metadata.xml"
|
||||
if (! File(result).exists() && version != null) {
|
||||
result = toDirectory("", fileSystem) + version + File.separator +
|
||||
if (isLocal) "maven-metadata-local.xml" else "maven-metadata.xml"
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn this dependency to a directory. If fileSystem is true, use the file system
|
||||
* dependent path separator, otherwise, use '/' (used to create URL's). The returned
|
||||
* string always ends with the path separator.
|
||||
*/
|
||||
fun toDirectory(v: String, fileSystem: Boolean = true): String {
|
||||
val sep = if (fileSystem) File.separator else "/"
|
||||
val l = listOf(groupId.replace(".", sep), artifactId, v)
|
||||
val result = Strings.Companion.join(sep, l)
|
||||
return if (result.endsWith(sep)) result else result + sep
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package com.beust.kobalt.maven.dependency
|
||||
|
||||
import com.beust.kobalt.api.IClasspathDependency
|
||||
import com.beust.kobalt.maven.CompletedFuture
|
||||
import org.apache.maven.model.Dependency
|
||||
import java.io.File
|
||||
|
||||
open public class FileDependency(open val fileName: String) : IClasspathDependency, Comparable<FileDependency> {
|
||||
companion object {
|
||||
val PREFIX_FILE: String = "file://"
|
||||
}
|
||||
|
||||
override val id = PREFIX_FILE + fileName
|
||||
|
||||
override val jarFile = CompletedFuture(File(fileName))
|
||||
|
||||
override fun toMavenDependencies(): Dependency {
|
||||
with(Dependency()) {
|
||||
systemPath = jarFile.get().absolutePath
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
override val shortId = fileName
|
||||
|
||||
override fun directDependencies() = arrayListOf<IClasspathDependency>()
|
||||
|
||||
override fun compareTo(other: FileDependency) = fileName.compareTo(other.fileName)
|
||||
|
||||
override fun toString() = fileName
|
||||
|
||||
override fun equals(other: Any?): Boolean{
|
||||
if (this === other) return true
|
||||
if (other?.javaClass != javaClass) return false
|
||||
|
||||
other as FileDependency
|
||||
|
||||
if (id != other.id) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode() = id.hashCode()
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
package com.beust.kobalt.maven.dependency
|
||||
|
||||
import com.beust.kobalt.HostConfig
|
||||
import com.beust.kobalt.KobaltException
|
||||
import com.beust.kobalt.api.IClasspathDependency
|
||||
import com.beust.kobalt.api.Kobalt
|
||||
import com.beust.kobalt.maven.*
|
||||
import com.beust.kobalt.misc.DependencyExecutor
|
||||
import com.beust.kobalt.misc.Versions
|
||||
import com.beust.kobalt.misc.warn
|
||||
import com.google.inject.Key
|
||||
import org.apache.maven.model.Dependency
|
||||
import java.io.File
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Future
|
||||
import javax.inject.Inject
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
public class MavenDependency @Inject constructor(mavenId: MavenId,
|
||||
val executor: ExecutorService,
|
||||
override val localRepo: LocalRepo,
|
||||
val repoFinder: RepoFinder,
|
||||
val pomFactory: Pom.IFactory,
|
||||
val downloadManager: DownloadManager)
|
||||
: LocalDep(mavenId, localRepo), IClasspathDependency, Comparable<MavenDependency> {
|
||||
override var jarFile: Future<File> by Delegates.notNull()
|
||||
var pomFile: Future<File> by Delegates.notNull()
|
||||
|
||||
init {
|
||||
val jar = File(localRepo.toFullPath(toJarFile(version)))
|
||||
val pom = File(localRepo.toFullPath(toPomFile(version)))
|
||||
if (jar.exists() && pom.exists()) {
|
||||
jarFile = CompletedFuture(jar)
|
||||
pomFile = CompletedFuture(pom)
|
||||
} else {
|
||||
val repoResult = repoFinder.findCorrectRepo(mavenId.toId)
|
||||
if (repoResult.found) {
|
||||
jarFile =
|
||||
if (repoResult.hasJar) {
|
||||
downloadManager.download(HostConfig(url = repoResult.hostConfig.url + toJarFile(repoResult)),
|
||||
jar.absolutePath, executor)
|
||||
} else {
|
||||
CompletedFuture(File("nonexistentFile")) // will be filtered out
|
||||
}
|
||||
pomFile = downloadManager.download(HostConfig(url = repoResult.hostConfig.url + toPomFile(repoResult)),
|
||||
pom.absolutePath, executor)
|
||||
} else {
|
||||
throw KobaltException("Couldn't resolve ${mavenId.toId}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val executor = Kobalt.INJECTOR.getInstance(Key.get(ExecutorService::class.java, DependencyExecutor::class.java))
|
||||
val depFactory = Kobalt.INJECTOR.getInstance(DepFactory::class.java)
|
||||
|
||||
fun create(id: String, ex: ExecutorService = executor) = depFactory.create(id, ex)
|
||||
|
||||
fun create(mavenId: MavenId, ex: ExecutorService = executor) = depFactory.create(mavenId.toId, ex)
|
||||
}
|
||||
|
||||
override fun toString() = mavenId.toId
|
||||
|
||||
override val id = mavenId.toId
|
||||
|
||||
override fun toMavenDependencies(): Dependency {
|
||||
return Dependency().apply {
|
||||
setGroupId(groupId)
|
||||
setArtifactId(artifactId)
|
||||
setVersion(version)
|
||||
}
|
||||
}
|
||||
|
||||
override fun compareTo(other: MavenDependency): Int {
|
||||
return Versions.toLongVersion(version).compareTo(Versions.toLongVersion(other.version))
|
||||
}
|
||||
|
||||
override val shortId = "$groupId:$artifactId:"
|
||||
|
||||
override fun directDependencies() : List<IClasspathDependency> {
|
||||
val result = arrayListOf<IClasspathDependency>()
|
||||
try {
|
||||
pomFactory.create(id, pomFile.get()).dependencies.filter {
|
||||
it.mustDownload && it.isValid
|
||||
}.forEach {
|
||||
result.add(create(MavenId.toId(it.groupId, it.artifactId, it.packaging, it.version)))
|
||||
}
|
||||
} catch(ex: Exception) {
|
||||
warn("Exception when trying to resolve dependencies for $id: " + ex.message)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.beust.kobalt.misc
|
||||
|
||||
public fun benchmark(run: () -> Unit) : Long {
|
||||
val start = System.currentTimeMillis()
|
||||
run()
|
||||
return (System.currentTimeMillis() - start) / 1000
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package com.beust.kobalt.misc
|
||||
|
||||
import com.beust.kobalt.KobaltException
|
||||
import com.beust.kobalt.api.Project
|
||||
import com.beust.kobalt.maven.DepFactory
|
||||
import com.beust.kobalt.maven.MavenId
|
||||
import com.beust.kobalt.maven.dependency.MavenDependency
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Find out if any newer versions of the dependencies are available.
|
||||
*/
|
||||
public class CheckVersions @Inject constructor(val depFactory : DepFactory,
|
||||
val executors : KobaltExecutors) {
|
||||
|
||||
fun run(projects: List<Project>) {
|
||||
val executor = executors.newExecutor("CheckVersions", 5)
|
||||
|
||||
val newVersions = hashSetOf<String>()
|
||||
projects.forEach {
|
||||
listOf(it.compileDependencies, it.testDependencies).forEach { cds ->
|
||||
cds.forEach { compileDependency ->
|
||||
if (MavenId.isMavenId(compileDependency.id)) {
|
||||
try {
|
||||
val dep = depFactory.create(compileDependency.shortId, executor, localFirst = false)
|
||||
if (dep is MavenDependency) {
|
||||
val other = compileDependency as MavenDependency
|
||||
if (dep.id != compileDependency.id
|
||||
&& Versions.toLongVersion(dep.version) > Versions.toLongVersion(other.version)) {
|
||||
newVersions.add(dep.id)
|
||||
}
|
||||
}
|
||||
} catch(e: KobaltException) {
|
||||
log(1, " Cannot resolve ${compileDependency.shortId}. ignoring")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newVersions.size > 0) {
|
||||
log(1, "New versions found:")
|
||||
newVersions.forEach { log(1, " $it") }
|
||||
} else {
|
||||
log(1, "All dependencies up to date")
|
||||
}
|
||||
executor.shutdown()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
package com.beust.kobalt.misc
|
||||
|
||||
import com.squareup.okhttp.MediaType
|
||||
import com.squareup.okhttp.RequestBody
|
||||
import okio.BufferedSink
|
||||
import okio.Okio
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* An OkHttp RequestBody subclass that counts the outgoing bytes and offers progress callbacks.
|
||||
*/
|
||||
class CountingFileRequestBody(val file: File, val contentType: String,
|
||||
val listenerCallback: (Long) -> Unit) : RequestBody() {
|
||||
|
||||
val SEGMENT_SIZE = 4096L
|
||||
|
||||
override fun contentLength() = file.length()
|
||||
|
||||
override fun contentType() = MediaType.parse(contentType)
|
||||
|
||||
override fun writeTo(sink: BufferedSink) {
|
||||
Okio.source(file).use { source ->
|
||||
var total = 0L
|
||||
var read: Long = source.read(sink.buffer(), SEGMENT_SIZE)
|
||||
|
||||
while (read != -1L) {
|
||||
total += read
|
||||
sink.flush();
|
||||
listenerCallback(total)
|
||||
read = source.read(sink.buffer(), SEGMENT_SIZE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// companion object {
|
||||
// private val MEDIA_TYPE_BINARY = MediaType.parse("application/octet-stream")
|
||||
//
|
||||
// fun progressUpload(file: File, url: String) {
|
||||
// val totalSize = file.length()
|
||||
//
|
||||
// val progressListener = object : ProgressListener {
|
||||
// override fun transferred(num: Long) {
|
||||
// val progress: Float = (num.toFloat() * 100) / totalSize
|
||||
// print("\rProgress: $progress")
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// val request = Request.Builder()
|
||||
// .url(url)
|
||||
// // .post(RequestBody.create(MEDIA_TYPE_BINARY, file))
|
||||
// .put(CountingFileRequestBody(file, "application/octet-stream", progressListener))
|
||||
// // .post(requestBody)
|
||||
// .build();
|
||||
//
|
||||
// val response = OkHttpClient().newCall(request).execute()
|
||||
// if (! response.isSuccessful) {
|
||||
// println("ERROR")
|
||||
// } else {
|
||||
// println("SUCCESS")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
package com.beust.kobalt.misc
|
||||
|
||||
import com.beust.kobalt.KobaltException
|
||||
import com.beust.kobalt.internal.DocUrl
|
||||
import com.beust.kobalt.maven.Http
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.okhttp.Headers
|
||||
import com.squareup.okhttp.OkHttpClient
|
||||
import retrofit.RestAdapter
|
||||
import retrofit.RetrofitError
|
||||
import retrofit.client.OkClient
|
||||
import retrofit.http.*
|
||||
import retrofit.mime.TypedByteArray
|
||||
import retrofit.mime.TypedFile
|
||||
import rx.Observable
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
import java.util.concurrent.Callable
|
||||
import java.util.concurrent.Future
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Retrieve Kobalt's latest release version from github.
|
||||
*/
|
||||
public class GithubApi @Inject constructor(val executors: KobaltExecutors,
|
||||
val localProperties: LocalProperties, val http: Http) {
|
||||
companion object {
|
||||
const val PROPERTY_ACCESS_TOKEN = "github.accessToken"
|
||||
const val PROPERTY_USERNAME = "github.username"
|
||||
}
|
||||
|
||||
class RetrofitErrorResponse(val code: String?, val field: String?)
|
||||
class RetrofitErrorsResponse(val message: String?, val errors: List<RetrofitErrorResponse>)
|
||||
|
||||
private val DOC_URL = DocUrl.PUBLISH_PLUGIN_URL
|
||||
|
||||
private fun parseRetrofitError(e: Throwable) : RetrofitErrorsResponse {
|
||||
val re = e as RetrofitError
|
||||
val json = String((re.response.body as TypedByteArray).bytes)
|
||||
return Gson().fromJson(json, RetrofitErrorsResponse::class.java)
|
||||
}
|
||||
|
||||
fun uploadRelease(packageName: String, tagName: String, zipFile: File) {
|
||||
log(1, "Uploading release ${zipFile.name}")
|
||||
|
||||
val username = localProperties.get(PROPERTY_USERNAME, DOC_URL)
|
||||
val accessToken = localProperties.get(PROPERTY_ACCESS_TOKEN, DOC_URL)
|
||||
try {
|
||||
service.createRelease(username, accessToken, packageName, CreateRelease(tagName))
|
||||
.flatMap { response ->
|
||||
uploadAsset(accessToken, response.uploadUrl!!, TypedFile("application/zip", zipFile),
|
||||
tagName)
|
||||
}
|
||||
.toBlocking()
|
||||
.forEach { action ->
|
||||
log(1, "\n${zipFile.name} successfully uploaded")
|
||||
}
|
||||
} catch(e: RetrofitError) {
|
||||
val error = parseRetrofitError(e)
|
||||
throw KobaltException("Couldn't upload release, ${error.message}: "
|
||||
+ error.errors[0].code + " field: " + error.errors[0].field)
|
||||
}
|
||||
}
|
||||
|
||||
private fun uploadAsset(token: String, uploadUrl: String, typedFile: TypedFile, tagName: String)
|
||||
: Observable<UploadAssetResponse> {
|
||||
val strippedUrl = uploadUrl.substring(0, uploadUrl.indexOf("{"))
|
||||
val fileName = typedFile.file().name
|
||||
val url = "$strippedUrl?name=$fileName&label=$fileName"
|
||||
val headers = Headers.of("Authorization", "token $token")
|
||||
val totalSize = typedFile.file().length()
|
||||
http.uploadFile(url = url, file = typedFile, headers = headers, post = true, // Github requires POST
|
||||
progressCallback = http.percentProgressCallback(totalSize))
|
||||
|
||||
return Observable.just(UploadAssetResponse(tagName, tagName))
|
||||
}
|
||||
|
||||
//
|
||||
// Read only Api
|
||||
//
|
||||
|
||||
private val service = RestAdapter.Builder()
|
||||
// .setLogLevel(RestAdapter.LogLevel.FULL)
|
||||
.setClient(OkClient(OkHttpClient()))
|
||||
.setEndpoint("https://api.github.com")
|
||||
.build()
|
||||
.create(Api::class.java)
|
||||
|
||||
//
|
||||
// JSON mapped classes that get sent up and down
|
||||
//
|
||||
class CreateRelease(@SerializedName("tag_name") var tagName: String? = null,
|
||||
var name: String? = tagName)
|
||||
class CreateReleaseResponse(var id: String? = null, @SerializedName("upload_url") var uploadUrl: String?)
|
||||
class UploadAssetResponse(var id: String? = null, val name: String? = null)
|
||||
class ReleasesResponse(@SerializedName("tag_name") var tagName: String? = null,
|
||||
var name: String? = tagName)
|
||||
|
||||
interface Api {
|
||||
@POST("/repos/{owner}/{repo}/releases")
|
||||
fun createRelease(@Path("owner") owner: String,
|
||||
@Query("access_token") accessToken: String,
|
||||
@Path("repo") repo: String,
|
||||
@Body createRelease: CreateRelease): Observable<CreateReleaseResponse>
|
||||
|
||||
@GET("/repos/{owner}/{repo}/releases")
|
||||
fun getReleases(@Path("owner") owner: String,
|
||||
@Query("access_token") accessToken: String,
|
||||
@Path("repo") repo: String): List<ReleasesResponse>
|
||||
|
||||
@GET("/repos/{owner}/{repo}/releases")
|
||||
fun getReleasesNoAuth(@Path("owner") owner: String,
|
||||
@Path("repo") repo: String): List<ReleasesResponse>
|
||||
}
|
||||
|
||||
val latestKobaltVersion: Future<String>
|
||||
get() {
|
||||
val callable = Callable<String> {
|
||||
var result = "0"
|
||||
|
||||
val username = localProperties.getNoThrows(PROPERTY_USERNAME, DOC_URL)
|
||||
val accessToken = localProperties.getNoThrows(PROPERTY_ACCESS_TOKEN, DOC_URL)
|
||||
try {
|
||||
val releases =
|
||||
if (username != null && accessToken != null) {
|
||||
service.getReleases(username, accessToken, "kobalt")
|
||||
} else {
|
||||
service.getReleasesNoAuth("cbeust", "kobalt")
|
||||
}
|
||||
releases.firstOrNull()?.let {
|
||||
try {
|
||||
result = listOf(it.name, it.tagName).filterNotNull().first { !it.isBlank() }
|
||||
} catch(ex: NoSuchElementException) {
|
||||
throw KobaltException("Couldn't find the latest release")
|
||||
}
|
||||
}
|
||||
} catch(e: RetrofitError) {
|
||||
val error = parseRetrofitError(e)
|
||||
throw KobaltException("Couldn't retrieve releases, ${error.message}: "
|
||||
+ error.errors[0].code + " field: " + error.errors[0].field)
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
return executors.miscExecutor.submit(callable)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,160 @@
|
|||
package com.beust.kobalt.misc
|
||||
|
||||
import com.beust.kobalt.IFileSpec
|
||||
import com.google.common.io.CharStreams
|
||||
import java.io.*
|
||||
import java.util.jar.JarEntry
|
||||
import java.util.jar.JarFile
|
||||
import java.util.jar.JarInputStream
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipFile
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
public class JarUtils {
|
||||
companion object {
|
||||
val DEFAULT_HANDLER: (Exception) -> Unit = { ex: Exception ->
|
||||
// Ignore duplicate entry exceptions
|
||||
if (! ex.message?.contains("duplicate")!!) {
|
||||
throw ex
|
||||
}
|
||||
}
|
||||
|
||||
public fun addFiles(directory: String, files: List<IncludedFile>, target: ZipOutputStream,
|
||||
expandJarFiles: Boolean,
|
||||
onError: (Exception) -> Unit = DEFAULT_HANDLER) {
|
||||
files.forEach {
|
||||
addSingleFile(directory, it, target, expandJarFiles, onError)
|
||||
}
|
||||
}
|
||||
|
||||
private val DEFAULT_JAR_EXCLUDES = arrayListOf(
|
||||
IFileSpec.Glob("META-INF/*.SF"),
|
||||
IFileSpec.Glob("META-INF/*.DSA"),
|
||||
IFileSpec.Glob("META-INF/*.RSA"))
|
||||
|
||||
public fun addSingleFile(directory: String, file: IncludedFile, outputStream: ZipOutputStream,
|
||||
expandJarFiles: Boolean, onError: (Exception) -> Unit = DEFAULT_HANDLER) {
|
||||
file.specs.forEach { spec ->
|
||||
val path = spec.toString()
|
||||
spec.toFiles(directory + "/" + file.from).forEach { source ->
|
||||
if (source.isDirectory) {
|
||||
log(2, "Writing contents of directory ${source}")
|
||||
|
||||
// Directory
|
||||
var name = path
|
||||
if (!name.isEmpty()) {
|
||||
if (!name.endsWith("/")) name += "/"
|
||||
val entry = JarEntry(name)
|
||||
entry.time = source.lastModified()
|
||||
try {
|
||||
outputStream.putNextEntry(entry)
|
||||
} finally {
|
||||
outputStream.closeEntry()
|
||||
}
|
||||
}
|
||||
val includedFile = IncludedFile(From(source.path), To(""), listOf(IFileSpec.Glob("**")))
|
||||
addSingleFile(".", includedFile, outputStream, expandJarFiles)
|
||||
} else {
|
||||
if (expandJarFiles and source.name.endsWith(".jar")) {
|
||||
log(2, "Writing contents of jar file ${source}")
|
||||
val stream = JarInputStream(FileInputStream(source))
|
||||
var entry = stream.nextEntry
|
||||
while (entry != null) {
|
||||
if (! entry.isDirectory && ! KFiles.isExcluded(entry.name, DEFAULT_JAR_EXCLUDES)) {
|
||||
val ins = JarFile(source).getInputStream(entry)
|
||||
addEntry(ins, JarEntry(entry), outputStream, onError)
|
||||
}
|
||||
entry = stream.nextEntry
|
||||
}
|
||||
} else {
|
||||
val entry = JarEntry((file.to + source.path).replace("\\", "/"))
|
||||
entry.time = source.lastModified()
|
||||
val fromPath = (file.from + "/" + source.path).replace("\\", "/")
|
||||
val entryFile = File(directory, fromPath)
|
||||
if (! entryFile.exists()) {
|
||||
throw AssertionError("File should exist: ${entryFile}")
|
||||
}
|
||||
addEntry(FileInputStream(entryFile), entry, outputStream, onError)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addEntry(inputStream: InputStream, entry: ZipEntry, outputStream: ZipOutputStream,
|
||||
onError: (Exception) -> Unit = DEFAULT_HANDLER) {
|
||||
var bis: BufferedInputStream? = null
|
||||
try {
|
||||
outputStream.putNextEntry(entry)
|
||||
bis = BufferedInputStream(inputStream)
|
||||
|
||||
val buffer = ByteArray(50 * 1024)
|
||||
while (true) {
|
||||
val count = bis.read(buffer)
|
||||
if (count == -1) break
|
||||
outputStream.write(buffer, 0, count)
|
||||
}
|
||||
outputStream.closeEntry()
|
||||
} catch(ex: Exception) {
|
||||
onError(ex)
|
||||
} finally {
|
||||
bis?.close()
|
||||
}
|
||||
}
|
||||
|
||||
fun extractTextFile(zip : ZipFile, fileName: String) : String? {
|
||||
val enumEntries = zip.entries()
|
||||
while (enumEntries.hasMoreElements()) {
|
||||
val file = enumEntries.nextElement()
|
||||
if (file.name == fileName) {
|
||||
log(2, "Found $fileName in ${zip.name}")
|
||||
zip.getInputStream(file).use { ins ->
|
||||
return CharStreams.toString(InputStreamReader(ins, "UTF-8"))
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun extractJarFile(jarFile: File, destDir: File) {
|
||||
val jar = JarFile(jarFile)
|
||||
val enumEntries = jar.entries()
|
||||
while (enumEntries.hasMoreElements()) {
|
||||
val file = enumEntries.nextElement()
|
||||
val f = File(destDir.path + File.separator + file.name)
|
||||
if (file.isDirectory) {
|
||||
f.mkdir()
|
||||
continue
|
||||
}
|
||||
|
||||
jar.getInputStream(file).use { ins ->
|
||||
f.parentFile.mkdirs()
|
||||
FileOutputStream(f).use { fos ->
|
||||
while (ins.available() > 0) {
|
||||
fos.write(ins.read())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open class Direction(open val p: String) {
|
||||
override public fun toString() = path
|
||||
public val path: String get() = if (p.isEmpty() or p.endsWith("/")) p else p + "/"
|
||||
}
|
||||
|
||||
class IncludedFile(val fromOriginal: From, val toOriginal: To, val specs: List<IFileSpec>) {
|
||||
constructor(specs: List<IFileSpec>) : this(From(""), To(""), specs)
|
||||
public val from: String get() = fromOriginal.path.replace("\\", "/")
|
||||
public val to: String get() = toOriginal.path.replace("\\", "/")
|
||||
override public fun toString() = toString("IncludedFile",
|
||||
"files", specs.map { it.toString() }.joinToString(", "),
|
||||
"from", from,
|
||||
"to", to)
|
||||
}
|
||||
|
||||
class From(override val p: String) : Direction(p)
|
||||
|
||||
class To(override val p: String) : Direction(p)
|
|
@ -0,0 +1,300 @@
|
|||
package com.beust.kobalt.misc
|
||||
|
||||
import com.beust.kobalt.IFileSpec
|
||||
import com.beust.kobalt.SystemProperties
|
||||
import com.beust.kobalt.Variant
|
||||
import com.beust.kobalt.api.Kobalt
|
||||
import com.beust.kobalt.api.Project
|
||||
import com.beust.kobalt.homeDir
|
||||
import com.beust.kobalt.internal.build.BuildFile
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.nio.file.*
|
||||
import kotlin.io.FileAlreadyExistsException
|
||||
import kotlin.io.FileSystemException
|
||||
import kotlin.io.NoSuchFileException
|
||||
|
||||
class KFiles {
|
||||
/**
|
||||
* This actually returns a list of strings because in development mode, we are not pointing to a single
|
||||
* jar file but to a set of /classes directories.
|
||||
*/
|
||||
val kobaltJar : List<String>
|
||||
get() {
|
||||
val envJar = System.getenv("KOBALT_JAR")
|
||||
if (envJar != null) {
|
||||
debug("Using kobalt jar $envJar")
|
||||
return listOf(File(envJar).absolutePath)
|
||||
} else {
|
||||
val jar = joinDir(distributionsDir, Kobalt.version, "kobalt/wrapper/kobalt-" + Kobalt.version + ".jar")
|
||||
val jarFile = File(jar)
|
||||
if (jarFile.exists()) {
|
||||
return listOf(jarFile.absolutePath)
|
||||
} else {
|
||||
// Will only happen when building kobalt itself: the jar file might not be in the dist/ directory
|
||||
// yet since we're currently building it. Instead, use the classes directly
|
||||
val result = listOf("kobalt", "kobalt-plugin-api", "kobalt-wrapper").map {
|
||||
File(homeDir(KFiles.joinDir("kotlin", "kobalt", "out", "production", it))).absolutePath
|
||||
}
|
||||
debug("Couldn't find ${jarFile.absolutePath}, using\n " + result.joinToString(" "))
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
File(KOBALT_DOT_DIR).mkdirs()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KOBALT_DOT_DIR : String = ".kobalt"
|
||||
const val KOBALT_DIR : String = "kobalt"
|
||||
const val KOBALT_BUILD_DIR = "kobaltBuild"
|
||||
|
||||
// Directories under ~/.kobalt
|
||||
val localRepo = homeDir(KOBALT_DOT_DIR, "repository")
|
||||
|
||||
/** Where all the .zip files are extracted */
|
||||
val distributionsDir = homeDir(KOBALT_DOT_DIR, "wrapper", "dist")
|
||||
|
||||
// Directories under ./.kobalt
|
||||
val SCRIPT_BUILD_DIR : String = "build"
|
||||
val CLASSES_DIR : String = "classes"
|
||||
|
||||
/** Where build file and support source files go, under KOBALT_DIR */
|
||||
private val SRC = "src"
|
||||
|
||||
val TEST_CLASSES_DIR : String = "test-classes"
|
||||
|
||||
private fun generatedDir(project: Project, variant: Variant)
|
||||
= KFiles.joinDir(project.directory, project.buildDirectory, "generated", variant.toIntermediateDir())
|
||||
|
||||
fun generatedSourceDir(project: Project, variant: Variant, name: String) =
|
||||
KFiles.joinDir(project.directory, project.buildDirectory, "generated", "source", name,
|
||||
variant.toIntermediateDir())
|
||||
|
||||
fun buildDir(project: Project) = KFiles.makeDir(project.directory, project.buildDirectory)
|
||||
|
||||
/**
|
||||
* Join the paths elements with the file separator.
|
||||
*/
|
||||
fun joinDir(paths: List<String>): String = paths.joinToString(File.separator)
|
||||
|
||||
/**
|
||||
* Join the paths elements with the file separator.
|
||||
*/
|
||||
fun joinDir(vararg ts: String): String = ts.toArrayList().joinToString(File.separator)
|
||||
|
||||
/**
|
||||
* The paths elements are expected to be a directory. Make that directory and join the
|
||||
* elements with the file separator.
|
||||
*/
|
||||
fun joinAndMakeDir(paths: List<String>) = joinDir(paths).apply { File(this).mkdirs() }
|
||||
|
||||
/**
|
||||
* The paths elements are expected to be a directory. Make that directory and join the
|
||||
* elements with the file separator.
|
||||
*/
|
||||
fun joinAndMakeDir(vararg ts: String) = joinAndMakeDir(ts.toList())
|
||||
|
||||
/**
|
||||
* The paths elements are expected to be a file. Make that parent directory of that file and join the
|
||||
* elements with the file separator.
|
||||
*/
|
||||
fun joinFileAndMakeDir(vararg ts: String) = joinDir(joinAndMakeDir(ts.slice(0..ts.size - 2)), ts[ts.size - 1])
|
||||
|
||||
fun makeDir(dir: String, s: String? = null) =
|
||||
(if (s != null) File(dir, s) else File(dir)).apply { mkdirs() }
|
||||
|
||||
fun findRecursively(rootDir: File) : List<String> =
|
||||
findRecursively(rootDir, arrayListOf(), { s -> true })
|
||||
|
||||
fun findRecursively(rootDir: File, directories: List<File>,
|
||||
function: Function1<String, Boolean>): List<String> {
|
||||
var result = arrayListOf<String>()
|
||||
|
||||
val allDirs = arrayListOf<File>()
|
||||
if (directories.isEmpty()) {
|
||||
allDirs.add(rootDir)
|
||||
} else {
|
||||
allDirs.addAll(directories.map { File(rootDir, it.path) })
|
||||
}
|
||||
|
||||
val seen = hashSetOf<java.nio.file.Path>()
|
||||
allDirs.forEach { dir ->
|
||||
if (! dir.exists()) {
|
||||
log(2, "Couldn't find directory $dir")
|
||||
} else {
|
||||
val files = findRecursively(dir, function)
|
||||
files.map { Paths.get(it) }.forEach {
|
||||
val rel = Paths.get(dir.path).relativize(it)
|
||||
if (! seen.contains(rel)) {
|
||||
result.add(File(dir, rel.toFile().path).path)
|
||||
seen.add(rel)
|
||||
} else {
|
||||
log(2, "Skipped file already seen in previous flavor: $rel")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Return files relative to rootDir
|
||||
val r = result.map { it.substring(rootDir.path.length + 1)}
|
||||
return r
|
||||
}
|
||||
|
||||
fun findRecursively(directory: File, function: Function1<String, Boolean>): List<String> {
|
||||
var result = arrayListOf<String>()
|
||||
directory.listFiles().forEach {
|
||||
if (it.isFile && function(it.path)) {
|
||||
result.add(it.path)
|
||||
} else if (it.isDirectory) {
|
||||
result.addAll(findRecursively(it, function))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun copyRecursively(from: File, to: File, replaceExisting: Boolean = true, deleteFirst: Boolean = false,
|
||||
onError: (File, IOException) -> OnErrorAction = { file, exception -> throw exception }) {
|
||||
// Need to wait until copyRecursively supports an overwrite: Boolean = false parameter
|
||||
// Until then, wipe everything first
|
||||
if (deleteFirst) to.deleteRecursively()
|
||||
// to.mkdirs()
|
||||
hackCopyRecursively(from, to, replaceExisting, onError)
|
||||
}
|
||||
|
||||
/** Private exception class, used to terminate recursive copying */
|
||||
private class TerminateException(file: File) : FileSystemException(file) {}
|
||||
|
||||
/**
|
||||
* Copy/pasted from kotlin/io/Utils.kt to add support for overwriting.
|
||||
*/
|
||||
private fun hackCopyRecursively(from: File, dst: File,
|
||||
replaceExisting: Boolean,
|
||||
onError: (File, IOException) -> OnErrorAction =
|
||||
{ file, exception -> throw exception }
|
||||
): Boolean {
|
||||
if (!from.exists()) {
|
||||
return onError(from, NoSuchFileException(file = from, reason = "The source file doesn't exist")) !=
|
||||
OnErrorAction.TERMINATE
|
||||
}
|
||||
try {
|
||||
// We cannot break for loop from inside a lambda, so we have to use an exception here
|
||||
for (src in from.walkTopDown().fail { f, e -> if (onError(f, e) == OnErrorAction.TERMINATE) throw TerminateException(f) }) {
|
||||
if (!src.exists()) {
|
||||
if (onError(src, NoSuchFileException(file = src, reason = "The source file doesn't exist")) ==
|
||||
OnErrorAction.TERMINATE)
|
||||
return false
|
||||
} else {
|
||||
val relPath = src.relativeTo(from)
|
||||
val dstFile = File(dst, relPath)
|
||||
if (dstFile.exists() && !replaceExisting && !(src.isDirectory() && dstFile.isDirectory())) {
|
||||
if (onError(dstFile, FileAlreadyExistsException(file = src,
|
||||
other = dstFile,
|
||||
reason = "The destination file already exists")) == OnErrorAction.TERMINATE)
|
||||
return false
|
||||
} else if (src.isDirectory()) {
|
||||
dstFile.mkdirs()
|
||||
} else {
|
||||
if (src.copyTo(dstFile, true) != src.length()) {
|
||||
if (onError(src, IOException("src.length() != dst.length()")) == OnErrorAction.TERMINATE)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
} catch (e: TerminateException) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
fun findDotDir(startDir: File) : File {
|
||||
var result = startDir
|
||||
while (result != null && ! File(result, KOBALT_DOT_DIR).exists()) {
|
||||
result = result.parentFile
|
||||
}
|
||||
if (result == null) {
|
||||
throw IllegalArgumentException("Couldn't locate $KOBALT_DOT_DIR in $startDir")
|
||||
}
|
||||
return File(result, KOBALT_DOT_DIR)
|
||||
}
|
||||
|
||||
/**
|
||||
* The build location for build scripts is .kobalt/build
|
||||
*/
|
||||
fun findBuildScriptLocation(buildFile: BuildFile, jarFile: String) : String {
|
||||
val result = joinDir(findDotDir(buildFile.directory).absolutePath, KFiles.SCRIPT_BUILD_DIR, jarFile)
|
||||
log(2, "Script jar file: $result")
|
||||
return result
|
||||
}
|
||||
|
||||
fun saveFile(file: File, text: String) {
|
||||
file.absoluteFile.parentFile.mkdirs()
|
||||
file.writeText(text)
|
||||
log(2, "Wrote $file")
|
||||
}
|
||||
|
||||
private fun isWindows() = System.getProperty("os.name").contains("Windows");
|
||||
|
||||
fun copy(from: Path?, to: Path?, option: StandardCopyOption = StandardCopyOption.REPLACE_EXISTING) {
|
||||
if (isWindows() && to!!.toFile().exists()) {
|
||||
log(2, "Windows detected, not overwriting ${to}")
|
||||
} else {
|
||||
try {
|
||||
log(2, "Copy from $from to ${to!!}")
|
||||
Files.copy(from, to, option)
|
||||
} catch(ex: IOException) {
|
||||
// Windows is anal about this
|
||||
log(1, "Couldn't copy $from to $to: ${ex.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun createTempFile(suffix : String = "", deleteOnExit: Boolean = false) : File =
|
||||
File.createTempFile("kobalt", suffix, File(SystemProperties.tmpDir)).let {
|
||||
if (deleteOnExit) it.deleteOnExit()
|
||||
return it
|
||||
}
|
||||
|
||||
fun src(filePath: String): String = KFiles.joinDir(KOBALT_DIR, SRC, filePath)
|
||||
|
||||
fun makeDir(project: Project, suffix: String) : File {
|
||||
return File(project.directory, project.buildDirectory + File.separator + suffix).apply { mkdirs() }
|
||||
}
|
||||
|
||||
fun makeOutputDir(project: Project) : File = makeDir(project, KFiles.CLASSES_DIR)
|
||||
|
||||
fun makeOutputTestDir(project: Project) : File = makeDir(project, KFiles.TEST_CLASSES_DIR)
|
||||
|
||||
fun isExcluded(file: File, excludes: List<IFileSpec.Glob>) = isExcluded(file.path, excludes)
|
||||
|
||||
fun isExcluded(file: String, excludes: List<IFileSpec.Glob>) : Boolean {
|
||||
if (excludes.isEmpty()) {
|
||||
return false
|
||||
} else {
|
||||
val ex = arrayListOf<PathMatcher>()
|
||||
excludes.forEach {
|
||||
ex.add(FileSystems.getDefault().getPathMatcher("glob:${it.spec}"))
|
||||
}
|
||||
ex.forEach {
|
||||
if (it.matches(Paths.get(file))) {
|
||||
log(3, "Excluding $file")
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun findRecursively(directory: File, function: Function1<String, Boolean>): List<String> {
|
||||
return KFiles.findRecursively(directory, function)
|
||||
}
|
||||
|
||||
fun findRecursively(rootDir: File, directories: List<File>,
|
||||
function: Function1<String, Boolean>): List<String> {
|
||||
return KFiles.findRecursively(rootDir, directories, function)
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue