Move everything under kotlin-native folder

I was forced to manually do update the following files, because otherwise
they would be ignored according .gitignore settings. Probably they
should be deleted from repo.

Interop/.idea/compiler.xml
Interop/.idea/gradle.xml
Interop/.idea/libraries/Gradle__org_jetbrains_kotlin_kotlin_runtime_1_0_3.xml
Interop/.idea/libraries/Gradle__org_jetbrains_kotlin_kotlin_stdlib_1_0_3.xml
Interop/.idea/modules.xml
Interop/.idea/modules/Indexer/Indexer.iml
Interop/.idea/modules/Runtime/Runtime.iml
Interop/.idea/modules/StubGenerator/StubGenerator.iml
backend.native/backend.native.iml
backend.native/bc.frontend/bc.frontend.iml
backend.native/cli.bc/cli.bc.iml
backend.native/cli.bc/src/org/jetbrains/kotlin/cli/bc/K2Native.kt
backend.native/cli.bc/src/org/jetbrains/kotlin/cli/bc/K2NativeCompilerArguments.kt
backend.native/tests/link/lib/foo.kt
backend.native/tests/link/lib/foo2.kt
backend.native/tests/teamcity-test.property
This commit is contained in:
Stanislav Erokhin
2020-10-27 21:00:28 +03:00
parent 91e4162dad
commit f624800b84
2830 changed files with 0 additions and 0 deletions
@@ -0,0 +1,37 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.analyzer
// Report with changes of different fields.
class ChangeReport<T>(val entityName: String, val changes: List<FieldChange<T>>) {
fun renderAsTextReport(): String {
var content = ""
if (!changes.isEmpty()) {
content = "$content$entityName changes\n"
content = "$content====================\n"
changes.forEach {
content = "$content${it.renderAsText()}"
}
}
return content
}
}
// Change of report field.
class FieldChange<T>(val field: String, val previous: T, val current: T) {
companion object {
fun <T> getFieldChangeOrNull(field: String, previous: T, current: T): FieldChange<T>? {
if (previous != current) {
return FieldChange(field, previous, current)
}
return null
}
}
fun renderAsText(): String {
return "$field: $previous -> $current\n"
}
}
@@ -0,0 +1,142 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.analyzer
import org.jetbrains.report.BenchmarkResult
import org.jetbrains.report.MeanVariance
import org.jetbrains.report.MeanVarianceBenchmark
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import kotlin.math.pow
import kotlin.math.sqrt
val MeanVariance.description: String
get() {
val format = { number: Double -> number.format(2) }
return "${format(mean)} ± ${format(variance)}"
}
val MeanVarianceBenchmark.description: String
get() = "${score.format()} ± ${variance.format()}"
// Calculate difference in percentage compare to another.
fun MeanVarianceBenchmark.calcPercentageDiff(other: MeanVarianceBenchmark): MeanVariance {
assert(other.score >= 0 &&
other.variance >= 0 &&
other.score - other.variance != 0.0,
{ "Mean and variance should be positive and not equal!" })
// Analyze intervals. Calculate difference between border points.
val (bigValue, smallValue) = if (score > other.score) Pair(this, other) else Pair(other, this)
val bigValueIntervalStart = bigValue.score - bigValue.variance
val bigValueIntervalEnd = bigValue.score + bigValue.variance
val smallValueIntervalStart = smallValue.score - smallValue.variance
val smallValueIntervalEnd = smallValue.score + smallValue.variance
if (smallValueIntervalEnd > bigValueIntervalStart) {
// Interval intersect.
return MeanVariance(0.0, 0.0)
}
val mean = ((smallValueIntervalEnd - bigValueIntervalStart) / bigValueIntervalStart) *
(if (score > other.score) -1 else 1)
val maxValueChange = ((bigValueIntervalEnd - smallValueIntervalEnd) / bigValueIntervalEnd)
val minValueChange = ((bigValueIntervalStart - smallValueIntervalStart) / bigValueIntervalStart)
val variance = abs(abs(mean) - max(minValueChange, maxValueChange))
return MeanVariance(mean * 100, variance * 100)
}
// Calculate ratio value compare to another.
fun MeanVarianceBenchmark.calcRatio(other: MeanVarianceBenchmark): MeanVariance {
assert(other.score >= 0 &&
other.variance >= 0 &&
other.score - other.variance != 0.0,
{ "Mean and variance should be positive and not equal!" })
val mean = score / other.score
val minRatio = (score - variance) / (other.score + other.variance)
val maxRatio = (score + variance) / (other.score - other.variance)
val ratioConfInt = min(abs(minRatio - mean), abs(maxRatio - mean))
return MeanVariance(mean, ratioConfInt)
}
fun geometricMean(values: Collection<Double>, totalNumber: Int = values.size) =
with(values.asSequence().filter { it != 0.0 }) {
if (count() == 0) {
0.0
} else {
map { it.pow(1.0 / totalNumber) }.reduce { a, b -> a * b }
}
}
fun computeMeanVariance(samples: List<Double>): MeanVariance {
val removedBroadSamples = 0.2
val zStar = 1.67 // Critical point for 90% confidence of normal distribution.
// Skip several minimal and maximum values.
val filteredSamples = if (samples.size >= 1/removedBroadSamples) {
samples.sorted().subList((samples.size * removedBroadSamples).toInt(),
samples.size - (samples.size * removedBroadSamples).toInt())
} else {
samples
}
val mean = filteredSamples.sum() / filteredSamples.size
val variance = samples.indices.sumByDouble {
(samples[it] - mean) * (samples[it] - mean)
} / samples.size
val confidenceInterval = sqrt(variance / samples.size) * zStar
return MeanVariance(mean, confidenceInterval)
}
// Calculate average results for benchmarks (each benchmark can be run several times).
fun collectMeanResults(benchmarks: Map<String, List<BenchmarkResult>>): BenchmarksTable {
return benchmarks.map { (name, resultsSet) ->
val repeatedSequence = IntArray(resultsSet.size)
var metric = BenchmarkResult.Metric.EXECUTION_TIME
var currentStatus = BenchmarkResult.Status.PASSED
var currentWarmup = -1
// Results can be already processed.
if (resultsSet[0] is MeanVarianceBenchmark) {
assert(resultsSet.size == 1) { "Several MeanVarianceBenchmark instances." }
name to resultsSet[0] as MeanVarianceBenchmark
} else {
// Collect common benchmark values and check them.
resultsSet.forEachIndexed { index, result ->
// If there was at least one failure, summary is marked as failure.
if (result.status == BenchmarkResult.Status.FAILED) {
currentStatus = result.status
}
repeatedSequence[index] = result.repeat
if (currentWarmup != -1)
if (result.warmup != currentWarmup)
println("Check data consistency. Warmup value for benchmark '${result.name}' differs.")
currentWarmup = result.warmup
metric = result.metric
}
repeatedSequence.sort()
// Check if there are missed loop during running benchmarks.
repeatedSequence.forEachIndexed { index, element ->
if (index != 0)
if ((element - repeatedSequence[index - 1]) != 1)
println("Check data consistency. For benchmark '$name' there is no run" +
" between ${repeatedSequence[index - 1]} and $element.")
}
// Create mean and variance benchmarks result.
val scoreMeanVariance = computeMeanVariance(resultsSet.map { it.score })
val runtimeInUsMeanVariance = computeMeanVariance(resultsSet.map { it.runtimeInUs })
val meanBenchmark = MeanVarianceBenchmark(name, currentStatus, scoreMeanVariance.mean, metric,
runtimeInUsMeanVariance.mean, repeatedSequence[resultsSet.size - 1],
currentWarmup, scoreMeanVariance.variance)
name to meanBenchmark
}
}.toMap()
}
fun collectBenchmarksDurations(benchmarks: Map<String, List<BenchmarkResult>>): Map<String, Double> =
benchmarks.map { (name, resultsSet) ->
name to resultsSet.sumByDouble { it.runtimeInUs }
}.toMap()
@@ -0,0 +1,282 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.analyzer
import org.jetbrains.report.BenchmarkResult
import org.jetbrains.report.BenchmarksReport
import org.jetbrains.report.Compiler
import org.jetbrains.report.Environment
import org.jetbrains.report.MeanVariance
import org.jetbrains.report.MeanVarianceBenchmark
import kotlin.math.abs
typealias SummaryBenchmark = Pair<MeanVarianceBenchmark?, MeanVarianceBenchmark?>
typealias BenchmarksTable = Map<String, MeanVarianceBenchmark>
typealias SummaryBenchmarksTable = Map<String, SummaryBenchmark>
typealias ScoreChange = Pair<MeanVariance, MeanVariance>
// Summary report with comparasion of separate benchmarks results.
class SummaryBenchmarksReport(val currentReport: BenchmarksReport,
val previousReport: BenchmarksReport? = null,
val meaningfulChangesValue: Double = 0.5) {
// Report created by joining comparing reports.
val mergedReport: Map<String, SummaryBenchmark>
private val benchmarksDurations: Map<String, Pair<Double?, Double?>>
// Lists of benchmarks in different status.
private val benchmarksWithChangedStatus = mutableListOf<FieldChange<BenchmarkResult.Status>>()
// Maps with changes of performance.
var regressions = mapOf<String, ScoreChange>()
private set
var improvements = mapOf<String, ScoreChange>()
private set
// Summary value of report - geometric mean.
val geoMeanBenchmark: SummaryBenchmark
var geoMeanScoreChange: ScoreChange? = null
private set
// Environment and tools.
val environments: Pair<Environment, Environment?>
val compilers: Pair<Compiler, Compiler?>
// Countable properties.
val failedBenchmarks: List<String>
get() = mergedReport.filter { it.value.first?.status == BenchmarkResult.Status.FAILED }
.map { it.key }
val addedBenchmarks: List<String>
get() = mergedReport.filter { it.value.second == null }.map { it.key }
val removedBenchmarks: List<String>
get() = mergedReport.filter { it.value.first == null }.map { it.key }
val benchmarksNumber: Int
get() = mergedReport.keys.size
val currentMeanVarianceBenchmarks: List<MeanVarianceBenchmark>
get() = mergedReport.filter { it.value.first != null }.map { it.value.first!! }
val currentBenchmarksDuration: Map<String, Double>
get() = benchmarksDurations.filter { it.value.first != null }.map { it.key to it.value.first!! }.toMap()
val maximumRegression: Double
get() = getMaximumChange(regressions)
val maximumImprovement: Double
get() = getMaximumChange(improvements)
val regressionsGeometricMean: Double
get() = getGeometricMeanOfChanges(regressions)
val improvementsGeometricMean: Double
get() = getGeometricMeanOfChanges(improvements)
val envChanges: List<FieldChange<String>>
get() {
val previousEnvironment = environments.second
val currentEnvironment = environments.first
return previousEnvironment?.let {
mutableListOf<FieldChange<String>>().apply {
addFieldChange("Machine CPU", previousEnvironment.machine.cpu, currentEnvironment.machine.cpu)
addFieldChange("Machine OS", previousEnvironment.machine.os, currentEnvironment.machine.os)
addFieldChange("JDK version", previousEnvironment.jdk.version, currentEnvironment.jdk.version)
addFieldChange("JDK vendor", previousEnvironment.jdk.vendor, currentEnvironment.jdk.vendor)
}
} ?: listOf<FieldChange<String>>()
}
val kotlinChanges: List<FieldChange<String>>
get() {
val previousCompiler = compilers.second
val currentCompiler = compilers.first
return previousCompiler?.let {
mutableListOf<FieldChange<String>>().apply {
addFieldChange("Backend type", previousCompiler.backend.type.type, currentCompiler.backend.type.type)
addFieldChange("Backend version", previousCompiler.backend.version, currentCompiler.backend.version)
addFieldChange("Backend flags", previousCompiler.backend.flags.toString(),
currentCompiler.backend.flags.toString())
addFieldChange("Kotlin version", previousCompiler.kotlinVersion, currentCompiler.kotlinVersion)
}
} ?: listOf<FieldChange<String>>()
}
init {
// Count avarage values for each benchmark.
val currentBenchmarksTable = collectMeanResults(currentReport.benchmarks)
val previousBenchmarksTable = previousReport?.let {
collectMeanResults(previousReport.benchmarks)
}
mergedReport = createMergedReport(currentBenchmarksTable, previousBenchmarksTable)
benchmarksDurations = calculateBenchmarksDuration(currentReport, previousReport)
geoMeanBenchmark = calculateGeoMeanBenchmark(currentBenchmarksTable, previousBenchmarksTable)
environments = Pair(currentReport.env, previousReport?.env)
compilers = Pair(currentReport.compiler, previousReport?.compiler)
if (previousReport != null) {
// Check changes in environment and tools.
analyzePerformanceChanges()
}
}
// Get benchmark report.
fun getBenchmarksReport(takeMainReport: Boolean = true) =
if (takeMainReport)
BenchmarksReport(environments.first, mergedReport.map { (_, value) -> value.first!! }, compilers.first)
else
BenchmarksReport(environments.second!!, mergedReport.map { (_, value) -> value.second!! }, compilers.second!!)
fun getResultsByMetric(metric: BenchmarkResult.Metric, getGeoMean: Boolean = true, filter: List<String>? = null,
normalizeData: Map<String, Map<String, Double>>? = null): List<Double?> {
val benchmarks = filter?.let {
mergedReport.filter { entry ->
filter.find {
entry.key.startsWith(it)
} != null
}
} ?: mergedReport
val results = benchmarks.map { entry ->
val name = entry.key.removeSuffix(metric.suffix)
if (entry.value.first!!.metric == metric) {
val score = entry.value.first!!.score
val value = normalizeData?.let {
it.get(name)?.get("$metric")?.let { score / it }
?: error("No normalization data for benchmark $name and metric $metric")
} ?: score
name to value
} else name to null
}.toMap()
if (getGeoMean) {
return listOf(geometricMean(results.values.filterNotNull()))
}
return filter?.let { it.map { results[it] }.toList() } ?: results.values.toList()
}
private fun getMaximumChange(bucket: Map<String, ScoreChange>): Double =
// Maps of regressions and improvements are sorted.
if (bucket.isEmpty()) 0.0 else bucket.values.map { it.first.mean }.first()
private fun getGeometricMeanOfChanges(bucket: Map<String, ScoreChange>): Double {
if (bucket.isEmpty())
return 0.0
var percentsList = bucket.values.map { it.first.mean }
return if (percentsList.first() > 0.0) {
geometricMean(percentsList, benchmarksNumber)
} else {
// Geometric mean can be counted on positive numbers.
percentsList = percentsList.map { abs(it) }
-geometricMean(percentsList, benchmarksNumber)
}
}
fun getBenchmarksWithChangedStatus(): List<FieldChange<BenchmarkResult.Status>> = benchmarksWithChangedStatus
// Create geometric mean.
private fun createGeoMeanBenchmark(benchTable: BenchmarksTable): MeanVarianceBenchmark {
val geoMeanBenchmarkName = "Geometric mean"
val geoMean = geometricMean(benchTable.toList().map { (_, value) -> value.score })
val varianceGeoMean = geometricMean(benchTable.toList().map { (_, value) -> value.variance })
return MeanVarianceBenchmark(geoMeanBenchmarkName, geoMean, varianceGeoMean)
}
// Generate map with summary durations of each benchmark.
private fun calculateBenchmarksDuration(currentReport: BenchmarksReport, previousReport: BenchmarksReport?):
Map<String, Pair<Double?, Double?>> {
val currentDurations = collectBenchmarksDurations(currentReport.benchmarks)
val previousDurations = previousReport?.let {
collectBenchmarksDurations(previousReport.benchmarks)
} ?: mapOf<String, Double>()
return currentDurations.keys.union(previousDurations.keys)
.map { it to Pair(currentDurations[it], previousDurations[it]) }.toMap()
}
// Merge current and compare to report.
private fun createMergedReport(currentBenchmarks: BenchmarksTable, previousBenchmarks: BenchmarksTable?):
Map<String, SummaryBenchmark> {
val mergedTable = mutableMapOf<String, SummaryBenchmark>()
mergedTable.apply {
currentBenchmarks.forEach { (name, current) ->
// Check existance of benchmark in previous results.
if (previousBenchmarks == null || name !in previousBenchmarks) {
getOrPut(name) { SummaryBenchmark(current, null) }
} else {
val previousBenchmark = previousBenchmarks.getValue(name)
getOrPut(name) { SummaryBenchmark(current, previousBenchmarks[name]) }
// Explore change of status.
if (previousBenchmark.status != current.status) {
val statusChange = FieldChange("$name", previousBenchmark.status, current.status)
benchmarksWithChangedStatus.add(statusChange)
}
}
}
}
// Add removed benchmarks to merged report.
mergedTable.apply {
previousBenchmarks?.filter { (key, _) -> key !in currentBenchmarks }?.forEach { (key, value) ->
getOrPut(key) { SummaryBenchmark(null, value) }
}
}
return mergedTable
}
// Calculate geometric mean.
private fun calculateGeoMeanBenchmark(currentBenchmarks: BenchmarksTable, previousBenchmarks: BenchmarksTable?):
SummaryBenchmark {
// Calculate geometric mean.
val currentGeoMean = createGeoMeanBenchmark(currentBenchmarks)
val previousGeoMean = previousBenchmarks?.let { createGeoMeanBenchmark(previousBenchmarks) }
return SummaryBenchmark(currentGeoMean, previousGeoMean)
}
private fun getBenchmarkPerfomanceChange(name: String, benchmark: SummaryBenchmark): Pair<String, ScoreChange>? {
val (current, previous) = benchmark
current?.let {
previous?.let {
// Calculate metrics for showing difference.
val percent = current.calcPercentageDiff(previous)
val ratio = current.calcRatio(previous)
if (abs(percent.mean) - percent.variance >= meaningfulChangesValue) {
return Pair(name, Pair(percent, ratio))
}
}
}
return null
}
// Analyze and collect changes in performance between same becnhmarks.
private fun analyzePerformanceChanges() {
val performanceChanges = mergedReport.asSequence().map { (name, element) ->
getBenchmarkPerfomanceChange(name, element)
}.filterNotNull().groupBy {
if (it.second.first.mean > 0) "regressions" else "improvements"
}
// Sort regressions and improvements.
regressions = performanceChanges["regressions"]
?.sortedByDescending { it.second.first.mean }?.map { it.first to it.second }
?.toMap() ?: mapOf<String, ScoreChange>()
improvements = performanceChanges["improvements"]
?.sortedBy { it.second.first.mean }?.map { it.first to it.second }
?.toMap() ?: mapOf<String, ScoreChange>()
// Calculate change for geometric mean.
val (current, previous) = geoMeanBenchmark
geoMeanScoreChange = current?.let {
previous?.let {
Pair(current.calcPercentageDiff(previous), current.calcRatio(previous))
}
}
}
private fun <T> MutableList<FieldChange<T>>.addFieldChange(field: String, previous: T, current: T) {
FieldChange.getFieldChangeOrNull(field, previous, current)?.let {
add(it)
}
}
}
@@ -0,0 +1,13 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.analyzer
expect fun readFile(fileName: String): String
expect fun Double.format(decimalNumber: Int = 4): String
expect fun writeToFile(fileName: String, text: String)
expect fun assert(value: Boolean, lazyMessage: () -> Any)
expect fun sendGetRequest(url: String, user: String? = null, password: String? = null,
followLocation: Boolean = false) : String
@@ -0,0 +1,351 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.report
import org.jetbrains.report.json.*
interface JsonSerializable {
fun serializeFields(): String
fun toJson(): String {
return """
{
${serializeFields()}
}
"""
}
// Convert iterable objects arrays, lists to json.
fun <T> arrayToJson(data: Iterable<T>): String {
return data.joinToString(prefix = "[", postfix = "]") {
if (it is JsonSerializable) it.toJson() else it.toString()
}
}
}
interface EntityFromJsonFactory<T> : ConvertedFromJson {
fun create(data: JsonElement): T
}
// Parse array with benchmarks to list
fun parseBenchmarksArray(data: JsonElement): List<BenchmarkResult> {
if (data is JsonArray) {
return data.jsonArray.map {
if (MeanVarianceBenchmark.isMeanVarianceBenchmark(it))
MeanVarianceBenchmark.create(it as JsonObject)
else BenchmarkResult.create(it as JsonObject)
}
} else {
error("Benchmarks field is expected to be an array. Please, check origin files.")
}
}
// Class for benchmarks report with all information of run.
open class BenchmarksReport(val env: Environment, benchmarksList: List<BenchmarkResult>, val compiler: Compiler) :
JsonSerializable {
companion object : EntityFromJsonFactory<BenchmarksReport> {
override fun create(data: JsonElement): BenchmarksReport {
if (data is JsonObject) {
val env = Environment.create(data.getRequiredField("env"))
val benchmarksObj = data.getRequiredField("benchmarks")
val compiler = Compiler.create(data.getRequiredField("kotlin"))
val buildNumberField = data.getOptionalField("buildNumber")
val benchmarksList = parseBenchmarksArray(benchmarksObj)
val report = BenchmarksReport(env, benchmarksList, compiler)
buildNumberField?.let { report.buildNumber = (it as JsonLiteral).unquoted() }
return report
} else {
error("Top level entity is expected to be an object. Please, check origin files.")
}
}
// Made a map of becnhmarks with name as key from list.
private fun structBenchmarks(benchmarksList: List<BenchmarkResult>) =
benchmarksList.groupBy { it.name }
}
val benchmarks = structBenchmarks(benchmarksList)
var buildNumber: String? = null
override fun serializeFields(): String {
val buildNumberField = buildNumber?.let {
""",
"buildNumber": "$buildNumber"
"""
} ?: ""
return """
"env": ${env.toJson()},
"kotlin": ${compiler.toJson()},
"benchmarks": ${arrayToJson(benchmarks.flatMap { it.value })}$buildNumberField
""".trimIndent()
}
fun merge(other: BenchmarksReport): BenchmarksReport {
val mergedBenchmarks = HashMap(benchmarks)
other.benchmarks.forEach {
if (it.key in mergedBenchmarks) {
error("${it.key} already exists in report!")
}
}
mergedBenchmarks.putAll(other.benchmarks)
return BenchmarksReport(env, mergedBenchmarks.flatMap { it.value }, compiler)
}
// Concatenate benchmarks report if they have same environment and compiler.
operator fun plus(other: BenchmarksReport): BenchmarksReport {
if (compiler != other.compiler || env != other.env) {
error("It's impossible to concat reports from different machines!")
}
return merge(other)
}
}
// Class for kotlin compiler
data class Compiler(val backend: Backend, val kotlinVersion: String) : JsonSerializable {
enum class BackendType(val type: String) {
JVM("jvm"),
NATIVE("native")
}
companion object : EntityFromJsonFactory<Compiler> {
override fun create(data: JsonElement): Compiler {
if (data is JsonObject) {
val backend = Backend.create(data.getRequiredField("backend"))
val kotlinVersion = elementToString(data.getRequiredField("kotlinVersion"), "kotlinVersion")
return Compiler(backend, kotlinVersion)
} else {
error("Kotlin entity is expected to be an object. Please, check origin files.")
}
}
fun backendTypeFromString(s: String): BackendType? = BackendType.values().find { it.type == s }
}
// Class for compiler backend
data class Backend(val type: BackendType, val version: String, val flags: List<String>) : JsonSerializable {
companion object : EntityFromJsonFactory<Backend> {
override fun create(data: JsonElement): Backend {
if (data is JsonObject) {
val typeElement = data.getRequiredField("type")
if (typeElement is JsonLiteral) {
val type = backendTypeFromString(typeElement.unquoted())
?: error("Backend type should be 'jvm' or 'native'")
val version = elementToString(data.getRequiredField("version"), "version")
val flagsArray = data.getOptionalField("flags")
var flags: List<String> = emptyList()
if (flagsArray != null && flagsArray is JsonArray) {
flags = flagsArray.jsonArray.map { it.toString() }
}
return Backend(type, version, flags)
} else {
error("Backend type should be string literal.")
}
} else {
error("Backend entity is expected to be an object. Please, check origin files.")
}
}
}
override fun serializeFields(): String {
val result = """
"type": "${type.type}",
"version": "${version}""""
// Don't print flags field if there is no one.
if (flags.isEmpty()) {
return """$result
"""
} else {
return """
$result,
"flags": ${arrayToJson(flags.map { if (it.startsWith("\"")) it else "\"$it\"" })}
"""
}
}
}
override fun serializeFields(): String {
return """
"backend": ${backend.toJson()},
"kotlinVersion": "${kotlinVersion}"
"""
}
}
// Class for description of environment of benchmarks run
data class Environment(val machine: Machine, val jdk: JDKInstance) : JsonSerializable {
companion object : EntityFromJsonFactory<Environment> {
override fun create(data: JsonElement): Environment {
if (data is JsonObject) {
val machine = Machine.create(data.getRequiredField("machine"))
val jdk = JDKInstance.create(data.getRequiredField("jdk"))
return Environment(machine, jdk)
} else {
error("Environment entity is expected to be an object. Please, check origin files.")
}
}
}
// Class for description of machine used for benchmarks run.
data class Machine(val cpu: String, val os: String) : JsonSerializable {
companion object : EntityFromJsonFactory<Machine> {
override fun create(data: JsonElement): Machine {
if (data is JsonObject) {
val cpu = elementToString(data.getRequiredField("cpu"), "cpu")
val os = elementToString(data.getRequiredField("os"), "os")
return Machine(cpu, os)
} else {
error("Machine entity is expected to be an object. Please, check origin files.")
}
}
}
override fun serializeFields(): String {
return """
"cpu": "$cpu",
"os": "$os"
"""
}
}
// Class for description of jdk used for benchmarks run.
data class JDKInstance(val version: String, val vendor: String) : JsonSerializable {
companion object : EntityFromJsonFactory<JDKInstance> {
override fun create(data: JsonElement): JDKInstance {
if (data is JsonObject) {
val version = elementToString(data.getRequiredField("version"), "version")
val vendor = elementToString(data.getRequiredField("vendor"), "vendor")
return JDKInstance(version, vendor)
} else {
error("JDK entity is expected to be an object. Please, check origin files.")
}
}
}
override fun serializeFields(): String {
return """
"version": "$version",
"vendor": "$vendor"
"""
}
}
override fun serializeFields(): String {
return """
"machine": ${machine.toJson()},
"jdk": ${jdk.toJson()}
"""
}
}
open class BenchmarkResult(val name: String, val status: Status,
val score: Double, val metric: Metric, val runtimeInUs: Double,
val repeat: Int, val warmup: Int) : JsonSerializable {
enum class Metric(val suffix: String, val value: String) {
EXECUTION_TIME("", "EXECUTION_TIME"),
CODE_SIZE(".codeSize", "CODE_SIZE"),
COMPILE_TIME(".compileTime", "COMPILE_TIME"),
BUNDLE_SIZE(".bundleSize", "BUNDLE_SIZE")
}
constructor(name: String, score: Double) : this(name, Status.PASSED, score, Metric.EXECUTION_TIME, 0.0, 0, 0)
companion object : EntityFromJsonFactory<BenchmarkResult> {
override fun create(data: JsonElement): BenchmarkResult {
if (data is JsonObject) {
var name = elementToString(data.getRequiredField("name"), "name")
val metricElement = data.getOptionalField("metric")
val metric = if (metricElement != null && metricElement is JsonLiteral)
metricFromString(metricElement.unquoted()) ?: Metric.EXECUTION_TIME
else Metric.EXECUTION_TIME
name += metric.suffix
val statusElement = data.getRequiredField("status")
if (statusElement is JsonLiteral) {
val status = statusFromString(statusElement.unquoted())
?: error("Status should be PASSED or FAILED")
val score = elementToDouble(data.getRequiredField("score"), "score")
val runtimeInUs = elementToDouble(data.getRequiredField("runtimeInUs"), "runtimeInUs")
val repeat = elementToInt(data.getRequiredField("repeat"), "repeat")
val warmup = elementToInt(data.getRequiredField("warmup"), "warmup")
return BenchmarkResult(name, status, score, metric, runtimeInUs, repeat, warmup)
} else {
error("Status should be string literal.")
}
} else {
error("Benchmark entity is expected to be an object. Please, check origin files.")
}
}
fun statusFromString(s: String): Status? = Status.values().find { it.value == s }
fun metricFromString(s: String): Metric? = Metric.values().find { it.value == s }
}
enum class Status(val value: String) {
PASSED("PASSED"),
FAILED("FAILED")
}
override fun serializeFields(): String {
return """
"name": "${name.removeSuffix(metric.suffix)}",
"status": "${status.value}",
"score": ${score},
"metric": "${metric.value}",
"runtimeInUs": ${runtimeInUs},
"repeat": ${repeat},
"warmup": ${warmup}
"""
}
val shortName: String
get() = name.removeSuffix(metric.suffix)
}
// Entity to describe avarage values which conssists of mean and variance values.
data class MeanVariance(val mean: Double, val variance: Double)
// Processed benchmark result with calculated mean and variance value.
open class MeanVarianceBenchmark(name: String, status: BenchmarkResult.Status, score: Double, metric: BenchmarkResult.Metric,
runtimeInUs: Double, repeat: Int, warmup: Int, val variance: Double) :
BenchmarkResult(name, status, score, metric, runtimeInUs, repeat, warmup) {
constructor(name: String, score: Double, variance: Double) : this(name, BenchmarkResult.Status.PASSED, score,
BenchmarkResult.Metric.EXECUTION_TIME, 0.0, 0, 0, variance)
companion object : EntityFromJsonFactory<MeanVarianceBenchmark> {
fun isMeanVarianceBenchmark(data: JsonElement) = data is JsonObject && data.getOptionalField("variance") != null
override fun create(data: JsonElement): MeanVarianceBenchmark {
if (data is JsonObject) {
val baseBenchmark = BenchmarkResult.create(data)
val variance = elementToDouble(data.getRequiredField("variance"), "variance")
return MeanVarianceBenchmark(baseBenchmark.name, baseBenchmark.status, baseBenchmark.score, baseBenchmark.metric,
baseBenchmark.runtimeInUs, baseBenchmark.repeat, baseBenchmark.warmup, variance)
} else {
error("Benchmark entity is expected to be an object. Please, check origin files.")
}
}
}
override fun serializeFields(): String {
return """
${super.serializeFields()},
"variance": $variance
"""
}
}
@@ -0,0 +1,54 @@
/*
* Copyright 2010-2018 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.report.json
// Entity can be created from json description.
interface ConvertedFromJson {
// Methods for conversion to expected type with checks of possibility of such conversions.
fun elementToDouble(element: JsonElement, name: String): Double =
if (element is JsonPrimitive)
element.double
else
error("Field '$name' in '$element' is expected to be a double number. Please, check origin files.")
fun elementToInt(element: JsonElement, name: String): Int =
if (element is JsonPrimitive)
element.int
else
error("Field '$name' in '$element' is expected to be an integer number. Please, check origin files.")
fun elementToString(element: JsonElement, name:String): String =
if (element is JsonLiteral)
element.unquoted()
else
error("Field '$name' in '$element' is expected to be a string. Please, check origin files.")
fun elementToStringOrNull(element: JsonElement, name:String): String? =
when (element) {
is JsonLiteral -> element.unquoted()
is JsonNull -> null
else -> error("Field '$name' in '$element' is expected to be a string. Please, check origin files.")
}
}
fun JsonObject.getRequiredField(fieldName: String): JsonElement {
return getOrNull(fieldName) ?: error("Field '$fieldName' doesn't exist in '$this'. Please, check origin files.")
}
fun JsonObject.getOptionalField(fieldName: String): JsonElement? {
return getOrNull(fieldName)
}
@@ -0,0 +1,328 @@
/*
* Copyright 2010-2018 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.report.json
/**
* Class representing single JSON element.
* Can be [JsonPrimitive], [JsonArray] or [JsonObject].
*
* [JsonElement.toString] properly prints JSON tree as valid JSON, taking into
* account quoted values and primitives
*/
sealed class JsonElement {
/**
* Convenience method to get current element as [JsonPrimitive]
* @throws JsonElementTypeMismatchException is current element is not a [JsonPrimitive]
*/
open val primitive: JsonPrimitive
get() = error("JsonLiteral")
/**
* Convenience method to get current element as [JsonObject]
* @throws JsonElementTypeMismatchException is current element is not a [JsonObject]
*/
open val jsonObject: JsonObject
get() = error("JsonObject")
/**
* Convenience method to get current element as [JsonArray]
* @throws JsonElementTypeMismatchException is current element is not a [JsonArray]
*/
open val jsonArray: JsonArray
get() = error("JsonArray")
/**
* Convenience method to get current element as [JsonNull]
* @throws JsonElementTypeMismatchException is current element is not a [JsonNull]
*/
open val jsonNull: JsonNull
get() = error("JsonPrimitive")
/**
* Checks whether current element is [JsonNull]
*/
val isNull: Boolean
get() = this === JsonNull
private fun error(element: String): Nothing =
throw JsonElementTypeMismatchException(this::class.toString(), element)
}
/**
* Class representing JSON primitive value. Can be either [JsonLiteral] or [JsonNull].
*/
sealed class JsonPrimitive : JsonElement() {
/**
* Content of given element without quotes. For [JsonNull] this methods returns `"null"`
*/
abstract val content: String
/**
* Content of the given element without quotes or `null` if current element is [JsonNull]
*/
abstract val contentOrNull: String?
@Suppress("LeakingThis")
final override val primitive: JsonPrimitive = this
/**
* Returns content of current element as int
* @throws NumberFormatException if current element is not a valid representation of number
*/
val int: Int get() = content.toInt()
/**
* Returns content of current element as int or `null` if current element is not a valid representation of number
**/
val intOrNull: Int? get() = content.toIntOrNull()
/**
* Returns content of current element as long
* @throws NumberFormatException if current element is not a valid representation of number
*/
val long: Long get() = content.toLong()
/**
* Returns content of current element as long or `null` if current element is not a valid representation of number
*/
val longOrNull: Long? get() = content.toLongOrNull()
/**
* Returns content of current element as double
* @throws NumberFormatException if current element is not a valid representation of number
*/
val double: Double get() = content.toDouble()
/**
* Returns content of current element as double or `null` if current element is not a valid representation of number
*/
val doubleOrNull: Double? get() = content.toDoubleOrNull()
/**
* Returns content of current element as float
* @throws NumberFormatException if current element is not a valid representation of number
*/
val float: Float get() = content.toFloat()
/**
* Returns content of current element as float or `null` if current element is not a valid representation of number
*/
val floatOrNull: Float? get() = content.toFloatOrNull()
/**
* Returns content of current element as boolean
* @throws IllegalStateException if current element doesn't represent boolean
*/
val boolean: Boolean get() = content.toBooleanStrict()
/**
* Returns content of current element as boolean or `null` if current element is not a valid representation of boolean
*/
val booleanOrNull: Boolean? get() = content.toBooleanStrictOrNull()
override fun toString() = content
}
/**
* Class representing JSON literals: numbers, booleans and string.
* Strings are always quoted.
*/
data class JsonLiteral internal constructor(
private val body: Any,
private val isString: Boolean
) : JsonPrimitive() {
override val content = body.toString()
override val contentOrNull: String = content
/**
* Creates number literal
*/
constructor(number: Number) : this(number, false)
/**
* Creates boolean literal
*/
constructor(boolean: Boolean) : this(boolean, false)
/**
* Creates quoted string literal
*/
constructor(string: String) : this(string, true)
override fun toString() =
if (isString) buildString { printQuoted(content) }
else content
fun unquoted() = content
}
/**
* Class representing JSON `null` value
*/
object JsonNull : JsonPrimitive() {
override val jsonNull: JsonNull = this
override val content: String = "null"
override val contentOrNull: String? = null
}
/**
* Class representing JSON object, consisting of name-value pairs, where value is arbitrary [JsonElement]
*/
data class JsonObject(val content: Map<String, JsonElement>) : JsonElement(), Map<String, JsonElement> by content {
override val jsonObject: JsonObject = this
/**
* Returns [JsonElement] associated with given [key]
* @throws NoSuchElementException if element is not present
*/
override fun get(key: String): JsonElement = content[key] ?: throw NoSuchElementException("Element $key is missing")
/**
* Returns [JsonElement] associated with given [key] or `null` if element is not present
*/
fun getOrNull(key: String): JsonElement? = content[key]
/**
* Returns [JsonPrimitive] associated with given [key]
*
* @throws NoSuchElementException if element is not present
* @throws JsonElementTypeMismatchException if element is present, but has invalid type
*/
fun getPrimitive(key: String): JsonPrimitive = get(key) as? JsonPrimitive
?: unexpectedJson(key, "JsonPrimitive")
/**
* Returns [JsonObject] associated with given [key]
*
* @throws NoSuchElementException if element is not present
* @throws JsonElementTypeMismatchException if element is present, but has invalid type
*/
fun getObject(key: String): JsonObject = get(key) as? JsonObject
?: unexpectedJson(key, "JsonObject")
/**
* Returns [JsonArray] associated with given [key]
*
* @throws NoSuchElementException if element is not present
* @throws JsonElementTypeMismatchException if element is present, but has invalid type
*/
fun getArray(key: String): JsonArray = get(key) as? JsonArray
?: unexpectedJson(key, "JsonArray")
/**
* Returns [JsonPrimitive] associated with given [key] or `null` if element
* is not present or has different type
*/
fun getPrimitiveOrNull(key: String): JsonPrimitive? = content[key] as? JsonPrimitive
/**
* Returns [JsonObject] associated with given [key] or `null` if element
* is not present or has different type
*/
fun getObjectOrNull(key: String): JsonObject? = content[key] as? JsonObject
/**
* Returns [JsonArray] associated with given [key] or `null` if element
* is not present or has different type
*/
fun getArrayOrNull(key: String): JsonArray? = content[key] as? JsonArray
/**
* Returns [J] associated with given [key]
*
* @throws NoSuchElementException if element is not present
* @throws JsonElementTypeMismatchException if element is present, but has invalid type
*/
inline fun <reified J : JsonElement> getAs(key: String): J = get(key) as? J
?: unexpectedJson(key, J::class.toString())
/**
* Returns [J] associated with given [key] or `null` if element
* is not present or has different type
*/
inline fun <reified J : JsonElement> lookup(key: String): J? = content[key] as? J
override fun toString(): String {
return content.entries.joinToString(
prefix = "{",
postfix = "}",
transform = {(k, v) -> """"$k": $v"""}
)
}
}
data class JsonArray(val content: List<JsonElement>) : JsonElement(), List<JsonElement> by content {
override val jsonArray: JsonArray = this
/**
* Returns [index]-th element of an array as [JsonPrimitive]
* @throws JsonElementTypeMismatchException if element has invalid type
*/
fun getPrimitive(index: Int) = content[index] as? JsonPrimitive
?: unexpectedJson("at $index", "JsonPrimitive")
/**
* Returns [index]-th element of an array as [JsonObject]
* @throws JsonElementTypeMismatchException if element has invalid type
*/
fun getObject(index: Int) = content[index] as? JsonObject
?: unexpectedJson("at $index", "JsonObject")
/**
* Returns [index]-th element of an array as [JsonArray]
* @throws JsonElementTypeMismatchException if element has invalid type
*/
fun getArray(index: Int) = content[index] as? JsonArray
?: unexpectedJson("at $index", "JsonArray")
/**
* Returns [index]-th element of an array as [JsonPrimitive] or `null` if element is missing or has different type
*/
fun getPrimitiveOrNull(index: Int) = content.getOrNull(index) as? JsonPrimitive
/**
* Returns [index]-th element of an array as [JsonObject] or `null` if element is missing or has different type
*/
fun getObjectOrNull(index: Int) = content.getOrNull(index) as? JsonObject
/**
* Returns [index]-th element of an array as [JsonArray] or `null` if element is missing or has different type
*/
fun getArrayOrNull(index: Int) = content.getOrNull(index) as? JsonArray
/**
* Returns [index]-th element of an array as [J]
* @throws JsonElementTypeMismatchException if element has invalid type
*/
inline fun <reified J : JsonElement> getAs(index: Int): J = content[index] as? J
?: unexpectedJson("at $index", J::class.toString())
/**
* Returns [index]-th element of an array as [J] or `null` if element is missing or has different type
*/
inline fun <reified J : JsonElement> getAsOrNull(index: Int): J? = content.getOrNull(index) as? J
override fun toString() = content.joinToString(prefix = "[", postfix = "]")
}
fun unexpectedJson(key: String, expected: String): Nothing =
throw JsonElementTypeMismatchException(key, expected)
@@ -0,0 +1,34 @@
/*
* Copyright 2010-2018 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.report.json
class JsonInvalidValueInStrictModeException(value: Any, valueDescription: String) : Exception(
"$value is not a valid $valueDescription as per JSON spec.\n" +
"You can disable strict mode to serialize such values"
) {
constructor(floatValue: Float) : this(floatValue, "float")
constructor(doubleValue: Double) : this(doubleValue, "double")
}
class JsonUnknownKeyException(key: String) : Exception(
"Strict JSON encountered unknown key: $key\n" +
"You can disable strict mode to skip unknown keys"
)
class JsonParsingException(position: Int, message: String) : Exception("Invalid JSON at $position: $message")
class JsonElementTypeMismatchException(key: String, expected: String) : Exception("Element $key is not a $expected")
@@ -0,0 +1,291 @@
/*
* Copyright 2010-2018 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.report.json
import org.jetbrains.report.json.EscapeCharMappings.ESC2C
// special strings
internal const val NULL = "null"
// special chars
internal const val COMMA = ','
internal const val COLON = ':'
internal const val BEGIN_OBJ = '{'
internal const val END_OBJ = '}'
internal const val BEGIN_LIST = '['
internal const val END_LIST = ']'
internal const val STRING = '"'
internal const val STRING_ESC = '\\'
internal const val INVALID = 0.toChar()
internal const val UNICODE_ESC = 'u'
// token classes
internal const val TC_OTHER: Byte = 0
internal const val TC_STRING: Byte = 1
internal const val TC_STRING_ESC: Byte = 2
internal const val TC_WS: Byte = 3
internal const val TC_COMMA: Byte = 4
internal const val TC_COLON: Byte = 5
internal const val TC_BEGIN_OBJ: Byte = 6
internal const val TC_END_OBJ: Byte = 7
internal const val TC_BEGIN_LIST: Byte = 8
internal const val TC_END_LIST: Byte = 9
internal const val TC_NULL: Byte = 10
internal const val TC_INVALID: Byte = 11
internal const val TC_EOF: Byte = 12
// mapping from chars to token classes
private const val CTC_MAX = 0x7e
// mapping from escape chars real chars
private const val C2ESC_MAX = 0x5d
private const val ESC2C_MAX = 0x75
internal val C2TC = ByteArray(CTC_MAX).apply {
for (i in 0..0x20)
initC2TC(i, TC_INVALID)
initC2TC(0x09, TC_WS)
initC2TC(0x0a, TC_WS)
initC2TC(0x0d, TC_WS)
initC2TC(0x20, TC_WS)
initC2TC(COMMA, TC_COMMA)
initC2TC(COLON, TC_COLON)
initC2TC(BEGIN_OBJ, TC_BEGIN_OBJ)
initC2TC(END_OBJ, TC_END_OBJ)
initC2TC(BEGIN_LIST, TC_BEGIN_LIST)
initC2TC(END_LIST, TC_END_LIST)
initC2TC(STRING, TC_STRING)
initC2TC(STRING_ESC, TC_STRING_ESC)
}
// object instead of @SharedImmutable because there is mutual initialization in [initC2ESC]
internal object EscapeCharMappings {
internal val ESC2C = CharArray(ESC2C_MAX)
internal val C2ESC = CharArray(C2ESC_MAX).apply {
for (i in 0x00..0x1f)
initC2ESC(i, UNICODE_ESC)
initC2ESC(0x08, 'b')
initC2ESC(0x09, 't')
initC2ESC(0x0a, 'n')
initC2ESC(0x0c, 'f')
initC2ESC(0x0d, 'r')
initC2ESC('/', '/')
initC2ESC(STRING, STRING)
initC2ESC(STRING_ESC, STRING_ESC)
}
private fun CharArray.initC2ESC(c: Int, esc: Char) {
this[c] = esc
if (esc != UNICODE_ESC) ESC2C[esc.toInt()] = c.toChar()
}
private fun CharArray.initC2ESC(c: Char, esc: Char) = initC2ESC(c.toInt(), esc)
}
private fun ByteArray.initC2TC(c: Int, cl: Byte) {
this[c] = cl
}
private fun ByteArray.initC2TC(c: Char, cl: Byte) {
initC2TC(c.toInt(), cl)
}
internal fun charToTokenClass(c: Char) = if (c.toInt() < CTC_MAX) C2TC[c.toInt()] else TC_OTHER
internal fun escapeToChar(c: Int): Char = if (c < ESC2C_MAX) ESC2C[c] else INVALID
// JSON low level parser
internal class Parser(val source: String) {
var curPos: Int = 0 // position in source
private set
// updated by nextToken
var tokenPos: Int = 0
private set
var tc: Byte = TC_EOF
private set
// update by nextString/nextLiteral
private var offset = -1 // when offset >= 0 string is in source, otherwise in buf
private var length = 0 // length of string
private var buf = CharArray(16) // only used for strings with escapes
init {
nextToken()
}
internal inline fun requireTc(expected: Byte, lazyErrorMsg: () -> String) {
if (tc != expected)
fail(tokenPos, lazyErrorMsg())
}
val canBeginValue: Boolean
get() = when (tc) {
TC_BEGIN_LIST, TC_BEGIN_OBJ, TC_OTHER, TC_STRING, TC_NULL -> true
else -> false
}
@OptIn(ExperimentalStdlibApi::class)
fun takeStr(): String {
if (tc != TC_OTHER && tc != TC_STRING) fail(tokenPos, "Expected string or non-null literal")
val prevStr = if (offset < 0)
buf.concatToString(0, length) else
source.substring(offset, offset + length)
nextToken()
return prevStr
}
private fun append(ch: Char) {
if (length >= buf.size) buf = buf.copyOf(2 * buf.size)
buf[length++] = ch
}
// initializes buf usage upon the first encountered escaped char
private fun appendRange(source: String, fromIndex: Int, toIndex: Int) {
val addLen = toIndex - fromIndex
val oldLen = length
val newLen = oldLen + addLen
if (newLen > buf.size) buf = buf.copyOf(newLen.coerceAtLeast(2 * buf.size))
for (i in 0 until addLen) buf[oldLen + i] = source[fromIndex + i]
length += addLen
}
fun nextToken() {
val source = source
var curPos = curPos
val maxLen = source.length
while (true) {
if (curPos >= maxLen) {
tokenPos = curPos
tc = TC_EOF
return
}
val ch = source[curPos]
val tc = charToTokenClass(ch)
when (tc) {
TC_WS -> curPos++ // skip whitespace
TC_OTHER -> {
nextLiteral(source, curPos)
return
}
TC_STRING -> {
nextString(source, curPos)
return
}
else -> {
this.tokenPos = curPos
this.tc = tc
this.curPos = curPos + 1
return
}
}
}
}
private fun nextLiteral(source: String, startPos: Int) {
tokenPos = startPos
offset = startPos
var curPos = startPos
val maxLen = source.length
while (true) {
curPos++
if (curPos >= maxLen || charToTokenClass(source[curPos]) != TC_OTHER) break
}
this.curPos = curPos
length = curPos - offset
tc = if (rangeEquals(source, offset, length, NULL)) TC_NULL else TC_OTHER
}
private fun nextString(source: String, startPos: Int) {
tokenPos = startPos
length = 0 // in buffer
var curPos = startPos + 1
var lastPos = curPos
val maxLen = source.length
parse@ while (true) {
if (curPos >= maxLen) fail(curPos, "Unexpected end in string")
if (source[curPos] == STRING) {
break@parse
} else if (source[curPos] == STRING_ESC) {
appendRange(source, lastPos, curPos)
val newPos = appendEsc(source, curPos + 1)
curPos = newPos
lastPos = newPos
} else {
curPos++
}
}
if (lastPos == startPos + 1) {
// there was no escaped chars
this.offset = lastPos
this.length = curPos - lastPos
} else {
// some escaped chars were there
appendRange(source, lastPos, curPos)
this.offset = -1
}
this.curPos = curPos + 1
tc = TC_STRING
}
private fun appendEsc(source: String, startPos: Int): Int {
var curPos = startPos
require(curPos < source.length, curPos) { "Unexpected end after escape char" }
val curChar = source[curPos++]
if (curChar == UNICODE_ESC) {
curPos = appendHex(source, curPos)
} else {
val c = escapeToChar(curChar.toInt())
require(c != INVALID, curPos) { "Invalid escaped char '$curChar'" }
append(c)
}
return curPos
}
private fun appendHex(source: String, startPos: Int): Int {
var curPos = startPos
append(
((fromHexChar(source, curPos++) shl 12) +
(fromHexChar(source, curPos++) shl 8) +
(fromHexChar(source, curPos++) shl 4) +
fromHexChar(source, curPos++)).toChar()
)
return curPos
}
fun skipElement() {
if (tc != TC_BEGIN_OBJ && tc != TC_BEGIN_LIST) {
nextToken()
return
}
val tokenStack = mutableListOf<Byte>()
do {
when (tc) {
TC_BEGIN_LIST, TC_BEGIN_OBJ -> tokenStack.add(tc)
TC_END_LIST -> {
if (tokenStack.last() != TC_BEGIN_LIST) throw JsonParsingException(curPos, "found ] instead of }")
tokenStack.removeAt(tokenStack.size - 1)
}
TC_END_OBJ -> {
if (tokenStack.last() != TC_BEGIN_OBJ) throw JsonParsingException(curPos, "found } instead of ]")
tokenStack.removeAt(tokenStack.size - 1)
}
}
nextToken()
} while (tokenStack.isNotEmpty())
}
}
// Utility functions
private fun fromHexChar(source: String, curPos: Int): Int {
require(curPos < source.length, curPos) { "Unexpected end in unicode escape" }
val curChar = source[curPos]
return when (curChar) {
in '0'..'9' -> curChar.toInt() - '0'.toInt()
in 'a'..'f' -> curChar.toInt() - 'a'.toInt() + 10
in 'A'..'F' -> curChar.toInt() - 'A'.toInt() + 10
else -> fail(curPos, "Invalid toHexChar char '$curChar' in unicode escape")
}
}
private fun rangeEquals(source: String, start: Int, length: Int, str: String): Boolean {
val n = str.length
if (length != n) return false
for (i in 0 until n) if (source[start + i] != str[i]) return false
return true
}
internal inline fun require(condition: Boolean, pos: Int, msg: () -> String) {
if (!condition)
fail(pos, msg())
}
@Suppress("NOTHING_TO_INLINE")
internal inline fun fail(pos: Int, msg: String): Nothing {
throw JsonParsingException(pos, msg)
}
@@ -0,0 +1,85 @@
/*
* Copyright 2010-2018 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// A bit changed part of kotlinx.serialization plugin
package org.jetbrains.report.json
class JsonTreeParser internal constructor(private val p: Parser) {
companion object {
fun parse(input: String): JsonElement = JsonTreeParser(input).readFully()
}
constructor(input: String) : this(Parser(input))
private fun readObject(): JsonElement {
p.requireTc(TC_BEGIN_OBJ) { "Expected start of object" }
p.nextToken()
val result: MutableMap<String, JsonElement> = hashMapOf()
while (true) {
if (p.tc == TC_COMMA) p.nextToken()
if (!p.canBeginValue) break
val key = p.takeStr()
p.requireTc(TC_COLON) { "Expected ':'" }
p.nextToken()
val elem = read()
result[key] = elem
}
p.requireTc(TC_END_OBJ) { "Expected end of object" }
p.nextToken()
return JsonObject(result)
}
private fun readValue(isString: Boolean): JsonElement {
val str = p.takeStr()
return JsonLiteral(str, isString)
}
private fun readArray(): JsonElement {
p.requireTc(TC_BEGIN_LIST) { "Expected start of array" }
p.nextToken()
val result: MutableList<JsonElement> = arrayListOf()
while (true) {
if (p.tc == TC_COMMA) p.nextToken()
if (!p.canBeginValue) break
val elem = read()
result.add(elem)
}
p.requireTc(TC_END_LIST) { "Expected end of array" }
p.nextToken()
return JsonArray(result)
}
fun read(): JsonElement {
if (!p.canBeginValue) fail(p.curPos, "Can't begin reading value from here")
val tc = p.tc
return when (tc) {
TC_NULL -> JsonNull.also { p.nextToken() }
TC_STRING -> readValue(isString = true)
TC_OTHER -> readValue(isString = false)
TC_BEGIN_OBJ -> readObject()
TC_BEGIN_LIST -> readArray()
else -> fail(p.curPos, "Can't begin reading element")
}
}
fun readFully(): JsonElement {
val r = read()
p.requireTc(TC_EOF) { "Input wasn't consumed fully" }
return r
}
}
@@ -0,0 +1,74 @@
/*
* Copyright 2010-2018 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.report.json
private fun toHexChar(i: Int) : Char {
val d = i and 0xf
return if (d < 10) (d + '0'.toInt()).toChar()
else (d - 10 + 'a'.toInt()).toChar()
}
private val ESCAPE_CHARS: Array<String?> = arrayOfNulls<String>(128).apply {
for (c in 0..0x1f) {
val c1 = toHexChar(c shr 12)
val c2 = toHexChar(c shr 8)
val c3 = toHexChar(c shr 4)
val c4 = toHexChar(c)
this[c] = "\\u$c1$c2$c3$c4"
}
this['"'.toInt()] = "\\\""
this['\\'.toInt()] = "\\\\"
this['\t'.toInt()] = "\\t"
this['\b'.toInt()] = "\\b"
this['\n'.toInt()] = "\\n"
this['\r'.toInt()] = "\\r"
this[0x0c] = "\\f"
}
internal fun StringBuilder.printQuoted(value: String) {
append(STRING)
var lastPos = 0
val length = value.length
for (i in 0 until length) {
val c = value[i].toInt()
// Do not replace this constant with C2ESC_MAX (which is smaller than ESCAPE_CHARS size),
// otherwise JIT won't eliminate range check and won't vectorize this loop
if (c >= ESCAPE_CHARS.size) continue // no need to escape
val esc = ESCAPE_CHARS[c] ?: continue
append(value, lastPos, i) // flush prev
append(esc)
lastPos = i + 1
}
append(value, lastPos, length)
append(STRING)
}
/**
* Returns `true` if the contents of this string is equal to the word "true", ignoring case, `false` if content equals "false",
* and throws [IllegalStateException] otherwise.
*/
fun String.toBooleanStrict(): Boolean = toBooleanStrictOrNull() ?: throw IllegalStateException("$this does not represent a Boolean")
/**
* Returns `true` if the contents of this string is equal to the word "true", ignoring case, `false` if content equals "false",
* and returns `null` otherwise.
*/
fun String.toBooleanStrictOrNull(): Boolean? = when {
this.equals("true", ignoreCase = true) -> true
this.equals("false", ignoreCase = true) -> false
else -> null
}
@@ -0,0 +1,196 @@
buildscript {
ext.rootBuildDirectory = file('../..')
apply from: "$rootBuildDirectory/gradle/loadRootProperties.gradle"
apply from: "$rootBuildDirectory/gradle/kotlinGradlePlugin.gradle"
repositories {
maven {
url 'https://cache-redirector.jetbrains.com/jcenter'
}
jcenter()
maven {
url kotlinCompilerRepo
}
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
}
}
apply plugin: 'kotlin-multiplatform'
repositories {
maven {
url 'https://cache-redirector.jetbrains.com/jcenter'
}
jcenter()
maven {
url kotlinCompilerRepo
}
maven {
url buildKotlinCompilerRepo
}
}
def getHostName() {
def target = System.getProperty("os.name")
if (target == 'Linux') return 'linux'
if (target.startsWith('Windows')) return 'windows'
if (target.startsWith('Mac')) return 'macos'
return 'unknown'
}
kotlin {
sourceSets {
commonMain {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-common:$kotlinVersion"
}
kotlin.srcDir '../benchmarks/shared/src'
kotlin.srcDir 'src/main/kotlin'
kotlin.srcDir '../../endorsedLibraries/kotlinx.cli/src/main/kotlin'
}
commonTest {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-test-common:$kotlinVersion"
implementation "org.jetbrains.kotlin:kotlin-test-annotations-common:$kotlinVersion"
}
kotlin.srcDir 'src/tests'
}
jvmTest {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-test:$kotlinVersion"
implementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlinVersion"
}
}
jsTest {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-test-js:$kotlinVersion"
}
}
nativeMain {
dependsOn commonMain
kotlin.srcDir 'src/main/kotlin-native'
kotlin.srcDir '../../endorsedLibraries/kotlinx.cli/src/main/kotlin-native'
}
jvmMain {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion"
}
kotlin.srcDir 'src/main/kotlin-jvm'
kotlin.srcDir '../../endorsedLibraries/kotlinx.cli/src/main/kotlin-jvm'
}
jsMain {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlinVersion"
}
kotlin.srcDir 'src/main/kotlin-js'
kotlin.srcDir '../../endorsedLibraries/kotlinx.cli/src/main/kotlin-js'
}
linuxMain { dependsOn nativeMain }
windowsMain { dependsOn nativeMain }
macosMain {dependsOn nativeMain }
}
targets {
fromPreset(presets.jvm, 'jvm') {
compilations.all {
tasks[compileKotlinTaskName].kotlinOptions {
jvmTarget = '1.8'
}
tasks[compileKotlinTaskName].kotlinOptions.suppressWarnings = true
}
}
fromPreset(presets.mingwX64, 'windows') {
binaries.all {
linkerOpts = ["-L${getMingwPath()}/lib".toString()]
}
compilations.main.cinterops {
libcurl {
includeDirs.headerFilterOnly "${getMingwPath()}/include"
}
}
}
fromPreset(presets.linuxX64, 'linux') {
compilations.main.cinterops {
libcurl {
includeDirs.headerFilterOnly '/usr/include', '/usr/include/x86_64-linux-gnu'
}
}
}
fromPreset(presets.macosX64, 'macos') {
compilations.main.cinterops {
libcurl {
includeDirs.headerFilterOnly '/opt/local/include', '/usr/local/include'
}
}
}
fromPreset(presets.js, 'js') {
compilations.main.kotlinOptions {
main = "noCall"
}
}
configure([windows, linux, macos]) {
def isCurrentHost = (name == getHostName())
compilations.all {
cinterops.all {
project.tasks[interopProcessingTaskName].enabled = isCurrentHost
}
compileKotlinTask.enabled = isCurrentHost
}
binaries.all {
linkTask.enabled = isCurrentHost
}
binaries {
executable('benchmarksAnalyzer', [RELEASE]) {
if (org.gradle.internal.os.OperatingSystem.current().isWindows()) {
linkerOpts("-L${getMingwPath()}/lib")
}
}
}
}
}
js {
browser {
distribution {
directory = new File("$projectDir/web/")
}
dceTask {
keep 'benchmarksAnalyzer.main_kand9s$'
}
}
}
}
def getMingwPath() {
def directory = System.getenv("MINGW64_DIR")
if (directory == null)
directory = "c:/msys64/mingw64"
return directory
}
task assembleWeb(type: Sync) {
def runtimeDependencies = kotlin.targets.js.compilations.main.runtimeDependencyFiles
from(files {
runtimeDependencies.collect { File file ->
zipTree(file.absolutePath)
}
}.builtBy(runtimeDependencies)) {
includeEmptyDirs = false
include { fileTreeElement ->
def path = fileTreeElement.path
path.endsWith(".js") && (path.startsWith("META-INF/resources/") ||
!path.startsWith("META-INF/"))
}
}
from compileKotlinJs.destinationDir
into "${projectDir}/web"
}
@@ -0,0 +1,4 @@
org.jetbrains.kotlin.native.home=../../dist
org.gradle.jvmargs=-Xmx2048m
# Avoid building platform libraries by the MPP plugin.
kotlin.native.distribution.type=prebuilt
@@ -0,0 +1,41 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.analyzer
import org.w3c.xhr.*
import kotlin.browser.*
import kotlin.js.*
actual fun readFile(fileName: String): String {
error("Reading from local file for JS isn't supported")
}
actual fun Double.format(decimalNumber: Int): String =
this.asDynamic().toFixed(decimalNumber)
actual fun writeToFile(fileName: String, text: String) {
if (fileName != "html")
error("Writing to local file for JS isn't supported")
val bodyPart = text.substringAfter("<body>").substringBefore("</body>")
document.body?.innerHTML = bodyPart
}
actual fun assert(value: Boolean, lazyMessage: () -> Any) {
if (!value) error(lazyMessage)
}
actual fun sendGetRequest(url: String, user: String?, password: String?, followLocation: Boolean) : String {
val proxyServerAddress = "https://perf-proxy.labs.jb.gg/"
val newUrl = proxyServerAddress + url
val request = XMLHttpRequest()
request.open("GET", newUrl, false, user, password)
request.send()
if (request.status == 200.toShort()) {
return request.responseText
}
error("Request to $url has status ${request.status}")
}
@@ -0,0 +1,64 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.analyzer
import java.io.File
import java.io.InputStream
import java.net.HttpURLConnection
import java.net.URL
import java.util.Base64
actual fun readFile(fileName: String): String {
val inputStream = File(fileName).inputStream()
val inputString = inputStream.bufferedReader().use { it.readText() }
return inputString
}
actual fun Double.format(decimalNumber: Int): String =
"%.${decimalNumber}f".format(this)
actual fun writeToFile(fileName: String, text: String) {
File(fileName).printWriter().use { out ->
out.println(text)
}
}
actual fun assert(value: Boolean, lazyMessage: () -> Any) =
kotlin.assert(value, lazyMessage)
// Create http(-s) request.
fun getHttpRequest(url: String, user: String?, password: String?): HttpURLConnection {
val connection = URL(url).openConnection() as HttpURLConnection
if (user != null && password != null) {
val auth = Base64.getEncoder().encode((user + ":" + password).toByteArray()).toString(Charsets.UTF_8)
connection.addRequestProperty("Authorization", "Basic $auth")
}
connection.setRequestProperty("Accept", "application/json")
return connection
}
actual fun sendGetRequest(url: String, user: String?, password: String?, followLocation: Boolean) : String {
val connection = getHttpRequest(url, user, password)
connection.connect()
val responseCode = connection.responseCode
if (!followLocation) {
connection.connect()
return connection.inputStream.use { it.reader().use { reader -> reader.readText() } }
}
// Request with redirect.
if (responseCode != HttpURLConnection.HTTP_MOVED_TEMP &&
responseCode != HttpURLConnection.HTTP_MOVED_PERM &&
responseCode != HttpURLConnection.HTTP_SEE_OTHER) {
error("No opportunity to redirect, but flag for redirecting to location was provided!")
}
val newUrl = connection.getHeaderField("Location")
val cookies = connection.getHeaderField("Set-Cookie")
val redirect = getHttpRequest(newUrl, user, password)
redirect.setRequestProperty("Cookie", cookies)
redirect.connect()
return redirect.inputStream.use { it.reader().use { reader -> reader.readText() } }
}
@@ -0,0 +1,108 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.analyzer
import platform.posix.*
import kotlinx.cinterop.*
import libcurl.*
actual fun readFile(fileName: String): String {
val file = fopen(fileName, "r") ?: error("Cannot read file '$fileName'")
var buffer = ByteArray(1024)
var text = StringBuilder()
try {
while (true) {
val nextLine = fgets(buffer.refTo(0), buffer.size, file)?.toKString()
if (nextLine == null) break
text.append(nextLine)
}
} finally {
fclose(file)
}
return text.toString()
}
actual fun Double.format(decimalNumber: Int): String {
var buffer = ByteArray(1024)
snprintf(buffer.refTo(0), buffer.size.toULong(), "%.${decimalNumber}f", this)
return buffer.toKString()
}
actual fun writeToFile(fileName: String, text: String) {
val file = fopen(fileName, "wt") ?: error("Cannot write file '$fileName'")
try {
if (fputs(text, file) == EOF) throw Error("File write error")
} finally {
fclose(file)
}
}
actual fun assert(value: Boolean, lazyMessage: () -> Any) =
kotlin.assert(value, lazyMessage)
class CUrl(url: String, user: String? = null, password: String? = null, followLocation: Boolean = false) {
private val stableRef = StableRef.create(this)
private val curl = curl_easy_init()
init {
curl_easy_setopt(curl, CURLOPT_URL, url)
val writeData = staticCFunction(::collectResponse)
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeData)
curl_easy_setopt(curl, CURLOPT_WRITEDATA, stableRef.asCPointer())
if (followLocation) {
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L)
}
user ?.let {
curl_easy_setopt(curl, CURLOPT_USERNAME, it)
}
password ?.let {
curl_easy_setopt(curl, CURLOPT_PASSWORD, it)
}
}
val body = StringBuilder()
fun fetch() {
memScoped {
val res = curl_easy_perform(curl)
if (res != CURLE_OK)
error("curl_easy_perform() failed: ${curl_easy_strerror(res)?.toKString()}")
val http_code = alloc<LongVar>()
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, http_code.ptr)
if (http_code.value >= 400L) {
error("Error http code ${http_code.value}")
}
}
}
fun close() {
curl_easy_cleanup(curl)
stableRef.dispose()
}
}
fun CPointer<ByteVar>.toKString(length: Int): String {
val bytes = this.readBytes(length)
return bytes.toKString()
}
fun collectResponse(buffer: CPointer<ByteVar>?, size: size_t, nitems: size_t, userdata: COpaquePointer?): size_t {
buffer ?: return 0u
userdata ?. let {
val data = buffer.toKString((size * nitems).toInt()).trim()
val curl = userdata.asStableRef<CUrl>().get()
curl.body.append(data)
}
return size * nitems
}
actual fun sendGetRequest(url: String, user: String?, password: String?, followLocation: Boolean) : String {
val curl = CUrl(url, user, password, followLocation)
curl.fetch()
curl.close()
return curl.body.toString()
}
@@ -0,0 +1,175 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
import kotlinx.cli.*
import org.jetbrains.analyzer.sendGetRequest
import org.jetbrains.analyzer.readFile
import org.jetbrains.analyzer.SummaryBenchmarksReport
import org.jetbrains.renders.*
import org.jetbrains.report.*
import org.jetbrains.report.json.*
abstract class Connector {
abstract val connectorPrefix: String
fun isCompatible(fileName: String) =
fileName.startsWith(connectorPrefix)
abstract fun getFileContent(fileLocation: String, user: String? = null): String
}
object ArtifactoryConnector : Connector() {
override val connectorPrefix = "artifactory:"
val artifactoryUrl = "https://repo.labs.intellij.net/kotlin-native-benchmarks"
override fun getFileContent(fileLocation: String, user: String?): String {
val fileParametersSize = 3
val fileDescription = fileLocation.substringAfter(connectorPrefix)
val fileParameters = fileDescription.split(':', limit = fileParametersSize)
// Right link to Artifactory file.
if (fileParameters.size == 1) {
val accessFileUrl = "$artifactoryUrl/${fileParameters[0]}"
return sendGetRequest(accessFileUrl, followLocation = true)
}
// Used builds description format.
if (fileParameters.size != fileParametersSize) {
error("To get file from Artifactory, please, specify, build number from TeamCity and target" +
" in format artifactory:build_number:target:filename")
}
val (buildNumber, target, fileName) = fileParameters
val accessFileUrl = "$artifactoryUrl/$target/$buildNumber/$fileName"
return sendGetRequest(accessFileUrl, followLocation = true)
}
}
object TeamCityConnector : Connector() {
override val connectorPrefix = "teamcity:"
val teamCityUrl = "http://buildserver.labs.intellij.net"
override fun getFileContent(fileLocation: String, user: String?): String {
val fileDescription = fileLocation.substringAfter(connectorPrefix)
val buildLocator = fileDescription.substringBeforeLast(':')
val fileName = fileDescription.substringAfterLast(':')
if (fileDescription == fileLocation ||
fileDescription == buildLocator || fileName == fileDescription) {
error("To get file from TeamCity, please, specify, build locator and filename on TeamCity" +
" in format teamcity:build_locator:filename")
}
val accessFileUrl = "$teamCityUrl/app/rest/builds/$buildLocator/artifacts/content/$fileName"
val userName = user?.substringBefore(':')
val password = user?.substringAfter(':')
return sendGetRequest(accessFileUrl, userName, password)
}
}
object DBServerConnector : Connector() {
override val connectorPrefix = ""
val serverUrl = "https://kotlin-native-perf-summary.labs.jb.gg"
override fun getFileContent(fileLocation: String, user: String?): String {
val buildNumber = fileLocation.substringBefore(':')
val target = fileLocation.substringAfter(':')
if (target == buildNumber) {
error("To get file from database, please, specify, target and build number" +
" in format target:build_number")
}
val accessFileUrl = "$serverUrl/report/$target/$buildNumber"
return sendGetRequest(accessFileUrl)
}
}
fun getFileContent(fileName: String, user: String? = null): String {
return when {
ArtifactoryConnector.isCompatible(fileName) -> ArtifactoryConnector.getFileContent(fileName, user)
TeamCityConnector.isCompatible(fileName) -> TeamCityConnector.getFileContent(fileName, user)
fileName.endsWith(".json") -> readFile(fileName)
else -> DBServerConnector.getFileContent(fileName, user)
}
}
fun getBenchmarkReport(fileName: String, user: String? = null): List<BenchmarksReport> {
val jsonEntity = JsonTreeParser.parse(getFileContent(fileName, user))
return when (jsonEntity) {
is JsonObject -> listOf(BenchmarksReport.create(jsonEntity))
is JsonArray -> jsonEntity.map { BenchmarksReport.create(it) }
else -> error("Wrong format of report. Expected object or array of objects.")
}
}
fun parseNormalizeResults(results: String): Map<String, Map<String, Double>> {
val parsedNormalizeResults = mutableMapOf<String, MutableMap<String, Double>>()
val tokensNumber = 3
results.lines().forEach {
if (!it.isEmpty()) {
val tokens = it.split(",").map { it.trim() }
if (tokens.size != tokensNumber) {
error("Data for normalization should include benchmark name, metric name and value. Got $it")
}
parsedNormalizeResults.getOrPut(tokens[0], { mutableMapOf<String, Double>() })[tokens[1]] = tokens[2].toDouble()
}
}
return parsedNormalizeResults
}
fun mergeCompilerFlags(reports: List<BenchmarksReport>): List<String> {
val flagsMap = mutableMapOf<String, MutableList<String>>()
reports.forEach {
val benchmarks = it.benchmarks.values.flatten().asSequence().filter { it.metric == BenchmarkResult.Metric.COMPILE_TIME }
.map { it.shortName }.toList()
if (benchmarks.isNotEmpty())
(flagsMap.getOrPut("${it.compiler.backend.flags.joinToString()}") { mutableListOf<String>() }).addAll(benchmarks)
}
return flagsMap.map { (flags, benchmarks) -> "$flags for [${benchmarks.distinct().sorted().joinToString()}]" }
}
fun mergeReportsWithDetailedFlags(reports: List<BenchmarksReport>) =
if (reports.size > 1) {
// Merge reports.
val detailedFlags = mergeCompilerFlags(reports)
reports.map {
BenchmarksReport(it.env, it.benchmarks.values.flatten(),
Compiler(Compiler.Backend(it.compiler.backend.type, it.compiler.backend.version, detailedFlags),
it.compiler.kotlinVersion))
}.reduce { result, it -> result + it }
} else {
reports.first()
}
fun main(args: Array<String>) {
// Parse args.
val argParser = ArgParser("benchmarksAnalyzer")
val mainReport by argParser.argument(ArgType.String, description = "Main report for analysis")
val compareToReport by argParser.argument(ArgType.String, description = "Report to compare to").optional()
val output by argParser.option(ArgType.String, shortName = "o", description = "Output file")
val epsValue by argParser.option(ArgType.Double, "eps", "e",
"Meaningful performance changes").default(1.0)
val useShortForm by argParser.option(ArgType.Boolean, "short", "s",
"Show short version of report").default(false)
val renders by argParser.option(ArgType.Choice(listOf("text", "html", "teamcity", "statistics", "metrics")),
shortName = "r", description = "Renders for showing information").multiple().default(listOf("text"))
val user by argParser.option(ArgType.String, shortName = "u", description = "User access information for authorization")
argParser.parse(args)
// Read contents of file.
val mainBenchsReport = mergeReportsWithDetailedFlags(getBenchmarkReport(mainReport, user))
var compareToBenchsReport = compareToReport?.let {
mergeReportsWithDetailedFlags(getBenchmarkReport(it, user))
}
// Generate comparasion report.
val summaryReport = SummaryBenchmarksReport(mainBenchsReport,
compareToBenchsReport,
epsValue)
var outputFile = output
renders.forEach {
Render.getRenderByName(it).print(summaryReport, useShortForm, outputFile)
outputFile = null
}
}
@@ -0,0 +1,672 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.renders
import org.jetbrains.analyzer.*
import org.jetbrains.report.*
import kotlin.math.sin
import kotlin.math.abs
import kotlin.math.pow
private fun <T : Comparable<T>> clamp(value: T, minValue: T, maxValue: T): T =
minOf(maxOf(value, minValue), maxValue)
// Natural number.
class Natural(initValue: Int) {
val value = if (initValue > 0) initValue else error("Provided value $initValue isn't natural")
override fun toString(): String {
return value.toString()
}
}
interface Element {
fun render(builder: StringBuilder, indent: String)
}
class TextElement(val text: String) : Element {
override fun render(builder: StringBuilder, indent: String) {
builder.append("$indent$text\n")
}
}
@DslMarker
annotation class HtmlTagMarker
@HtmlTagMarker
abstract class Tag(val name: String) : Element {
val children = arrayListOf<Element>()
val attributes = hashMapOf<String, String>()
protected fun <T : Element> initTag(tag: T, init: T.() -> Unit): T {
tag.init()
children.add(tag)
return tag
}
override fun render(builder: StringBuilder, indent: String) {
builder.append("$indent<$name${renderAttributes()}>\n")
for (c in children) {
c.render(builder, indent + " ")
}
builder.append("$indent</$name>\n")
}
private fun renderAttributes(): String =
attributes.map { (attr, value) ->
"$attr=\"$value\""
}.joinToString(separator = " ", prefix = " ")
override fun toString(): String {
val builder = StringBuilder()
render(builder, "")
return builder.toString()
}
}
abstract class TagWithText(name: String) : Tag(name) {
operator fun String.unaryPlus() {
children.add(TextElement(this))
}
}
class HTML : TagWithText("html") {
fun head(init: Head.() -> Unit) = initTag(Head(), init)
fun body(init: Body.() -> Unit) = initTag(Body(), init)
}
class Head : TagWithText("head") {
fun title(init: Title.() -> Unit) = initTag(Title(), init)
fun link(init: Link.() -> Unit) = initTag(Link(), init)
fun script(init: Script.() -> Unit) = initTag(Script(), init)
}
class Title : TagWithText("title")
class Link : TagWithText("link")
class Script : TagWithText("script")
abstract class BodyTag(name: String) : TagWithText(name) {
fun b(init: B.() -> Unit) = initTag(B(), init)
fun p(init: P.() -> Unit) = initTag(P(), init)
fun h1(init: H1.() -> Unit) = initTag(H1(), init)
fun h2(init: H2.() -> Unit) = initTag(H2(), init)
fun h4(init: H4.() -> Unit) = initTag(H4(), init)
fun hr(init: HR.() -> Unit) = initTag(HR(), init)
fun a(href: String, init: A.() -> Unit) {
val a = initTag(A(), init)
a.href = href
}
fun img(src: String, init: Image.() -> Unit) {
val element = initTag(Image(), init)
element.src = src
}
fun table(init: Table.() -> Unit) = initTag(Table(), init)
fun div(classAttr: String, init: Div.() -> Unit) = initTag(Div(classAttr), init)
fun button(classAttr: String, init: Button.() -> Unit) = initTag(Button(classAttr), init)
fun header(classAttr: String, init: Header.() -> Unit) = initTag(Header(classAttr), init)
fun span(classAttr: String, init: Span.() -> Unit) = initTag(Span(classAttr), init)
}
abstract class BodyTagWithClass(name: String, val classAttr: String) : BodyTag(name) {
init {
attributes["class"] = classAttr
}
}
class Body : BodyTag("body")
class B : BodyTag("b")
class P : BodyTag("p")
class H1 : BodyTag("h1")
class H2 : BodyTag("h2")
class H4 : BodyTag("h4")
class HR : BodyTag("hr")
class Div(classAttr: String) : BodyTagWithClass("div", classAttr)
class Header(classAttr: String) : BodyTagWithClass("header", classAttr)
class Button(classAttr: String) : BodyTagWithClass("button", classAttr)
class Span(classAttr: String) : BodyTagWithClass("span", classAttr)
class A : BodyTag("a") {
var href: String by attributes
}
class Image : BodyTag("img") {
var src: String by attributes
}
abstract class TableTag(name: String) : BodyTag(name) {
fun thead(init: THead.() -> Unit) = initTag(THead(), init)
fun tbody(init: TBody.() -> Unit) = initTag(TBody(), init)
fun tfoot(init: TFoot.() -> Unit) = initTag(TFoot(), init)
}
abstract class TableBlock(name: String) : TableTag(name) {
fun tr(init: TableRow.() -> Unit) = initTag(TableRow(), init)
}
class Table : TableTag("table")
class THead : TableBlock("thead")
class TFoot : TableBlock("tfoot")
class TBody : TableBlock("tbody")
abstract class TableRowTag(name: String) : TableBlock(name) {
var colspan = Natural(1)
set(value) {
attributes["colspan"] = value.toString()
}
var rowspan = Natural(1)
set(value) {
attributes["rowspan"] = value.toString()
}
fun th(rowspan: Natural = Natural(1), colspan: Natural = Natural(1), init: TableHeadInfo.() -> Unit) {
val element = initTag(TableHeadInfo(), init)
element.rowspan = rowspan
element.colspan = colspan
}
fun td(rowspan: Natural = Natural(1), colspan: Natural = Natural(1), init: TableDataInfo.() -> Unit) {
val element = initTag(TableDataInfo(), init)
element.rowspan = rowspan
element.colspan = colspan
}
}
class TableRow : TableRowTag("tr")
class TableHeadInfo : TableRowTag("th")
class TableDataInfo : TableRowTag("td")
fun html(init: HTML.() -> Unit): HTML {
val html = HTML()
html.init()
return html
}
// Report render to html format.
class HTMLRender: Render() {
override val name: String
get() = "html"
override fun render (report: SummaryBenchmarksReport, onlyChanges: Boolean) =
html {
head {
title { +"Benchmarks report" }
// Links to bootstrap files.
link {
attributes["href"] = "https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css"
attributes["rel"] = "stylesheet"
}
script {
attributes["src"] = "https://code.jquery.com/jquery-3.3.1.slim.min.js"
}
script {
attributes["src"] = "https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.6/umd/popper.min.js"
}
script {
attributes["src"] = "https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/js/bootstrap.min.js"
}
}
body {
header("navbar navbar-expand navbar-dark flex-column flex-md-row bd-navbar") {
attributes["style"] = "background-color:#161616;"
img("https://dashboard.snapcraft.io/site_media/appmedia/2018/04/256px-kotlin-logo-svg.png") {
attributes["style"] = "width:60px;height:60px;"
}
span("navbar-brand mb-0 h1") { +"Benchmarks report" }
}
div("container-fluid") {
p{}
renderEnvironmentTable(report.environments)
renderCompilerTable(report.compilers)
hr {}
renderStatusSummary(report)
hr {}
renderPerformanceSummary(report)
renderPerformanceDetails(report, onlyChanges)
}
}
}.toString()
private fun TableRowTag.formatComparedTableData(data: String, compareToData: String?) {
td {
compareToData?. let {
// Highlight changed data.
if (it != data)
attributes["bgcolor"] = "yellow"
}
if (data.isEmpty()) {
+"-"
} else {
+data
}
}
}
private fun TableTag.renderEnvironment(environment: Environment, name: String, compareTo: Environment? = null) {
tbody {
tr {
th {
attributes["scope"] = "row"
+name
}
formatComparedTableData(environment.machine.os, compareTo?.machine?.os)
formatComparedTableData(environment.machine.cpu, compareTo?.machine?.cpu)
formatComparedTableData(environment.jdk.version, compareTo?.jdk?.version)
formatComparedTableData(environment.jdk.vendor, compareTo?.jdk?.vendor)
}
}
}
private fun BodyTag.renderEnvironmentTable(environments: Pair<Environment, Environment?>) {
h4 { +"Environment" }
table {
attributes["class"] = "table table-sm table-bordered table-hover"
attributes["style"] = "width:initial;"
val firstEnvironment = environments.first
val secondEnvironment = environments.second
// Table header.
thead {
tr {
th(rowspan = Natural(2)) { +"Run" }
th(colspan = Natural(2)) { +"Machine" }
th(colspan = Natural(2)) { +"JDK" }
}
tr {
th { + "OS" }
th { + "CPU" }
th { + "Version"}
th { + "Vendor"}
}
}
renderEnvironment(firstEnvironment, "First")
secondEnvironment?. let { renderEnvironment(it, "Second", firstEnvironment) }
}
}
private fun TableTag.renderCompiler(compiler: Compiler, name: String, compareTo: Compiler? = null) {
tbody {
tr {
th {
attributes["scope"] = "row"
+name
}
formatComparedTableData(compiler.backend.type.type, compareTo?.backend?.type?.type)
formatComparedTableData(compiler.backend.version, compareTo?.backend?.version)
formatComparedTableData(compiler.backend.flags.joinToString(), compareTo?.backend?.flags?.joinToString())
formatComparedTableData(compiler.kotlinVersion, compareTo?.kotlinVersion)
}
}
}
private fun BodyTag.renderCompilerTable(compilers: Pair<Compiler, Compiler?>) {
h4 { +"Compiler" }
table {
attributes["class"] = "table table-sm table-bordered table-hover"
attributes["style"] = "width:initial;"
val firstCompiler = compilers.first
val secondCompiler = compilers.second
// Table header.
thead {
tr {
th(rowspan = Natural(2)) { +"Run" }
th(colspan = Natural(3)) { +"Backend" }
th(rowspan = Natural(2)) { +"Kotlin" }
}
tr {
th { + "Type" }
th { + "Version" }
th { + "Flags"}
}
}
renderCompiler(firstCompiler, "First")
secondCompiler?. let { renderCompiler(it, "Second", firstCompiler) }
}
}
private fun TableBlock.renderBucketInfo(bucket: Collection<Any>, name: String) {
if (!bucket.isEmpty()) {
tr {
th {
attributes["scope"] = "row"
+name
}
td {
+"${bucket.size}"
}
}
}
}
private fun BodyTag.renderCollapsedData(name: String, isCollapsed: Boolean = false, colorStyle: String = "",
init: BodyTag.() -> Unit) {
val show = if (!isCollapsed) "show" else ""
val tagName = name.replace(' ', '_')
div("accordion") {
div("card") {
attributes["style"] = "border-bottom: 1px solid rgba(0,0,0,.125);"
div("card-header") {
attributes["id"] = "heading"
attributes["style"] = "padding: 0;$colorStyle"
button("btn btn-link") {
attributes["data-toggle"] = "collapse"
attributes["data-target"] = "#$tagName"
+name
}
}
div("collapse $show") {
attributes["id"] = tagName
div("accordion-inner") {
init()
}
}
}
}
}
private fun TableTag.renderTableFromList(list: List<String>, name: String) {
if (!list.isEmpty()) {
thead {
tr {
th { +name }
}
}
list.forEach {
tbody {
tr {
td { +it }
}
}
}
}
}
private fun BodyTag.renderStatusSummary(report: SummaryBenchmarksReport) {
h4 { +"Status Summary" }
val failedBenchmarks = report.failedBenchmarks
if (failedBenchmarks.isEmpty()) {
div("alert alert-success") {
attributes["role"] = "alert"
+"All benchmarks passed!"
}
} else {
div("alert alert-danger") {
attributes["role"] = "alert"
+"There are failed benchmarks!"
}
}
val benchmarksWithChangedStatus = report.getBenchmarksWithChangedStatus()
val newFailures = benchmarksWithChangedStatus
.filter { it.current == BenchmarkResult.Status.FAILED }
val newPasses = benchmarksWithChangedStatus
.filter { it.current == BenchmarkResult.Status.PASSED }
table {
attributes["class"] = "table table-sm table-striped table-hover"
attributes["style"] = "width:initial; font-size: 11pt;"
thead {
tr {
th { +"Status Group" }
th { +"#" }
}
}
tbody {
renderBucketInfo(failedBenchmarks, "Failed (total)")
renderBucketInfo(newFailures, "New Failures")
renderBucketInfo(newPasses, "New Passes")
renderBucketInfo(report.addedBenchmarks, "Added")
renderBucketInfo(report.removedBenchmarks, "Removed")
}
tfoot {
tr {
th { +"Total becnhmarks number" }
th { +"${report.benchmarksNumber}" }
}
}
}
if (!failedBenchmarks.isEmpty()) {
renderCollapsedData("Failures", colorStyle = "background-color: lightpink") {
table {
attributes["class"] = "table table-sm table-striped table-hover"
attributes["style"] = "width:initial; font-size: 11pt;"
val newFailuresList = newFailures.map { it.field }
renderTableFromList(newFailuresList, "New Failures")
val existingFailures = failedBenchmarks.filter { it !in newFailuresList }
renderTableFromList(existingFailures, "Existing Failures")
}
}
}
if (!newPasses.isEmpty()) {
renderCollapsedData("New Passes", colorStyle = "background-color: lightgreen") {
table {
attributes["class"] = "table table-sm table-striped table-hover"
attributes["style"] = "width:initial; font-size: 11pt;"
renderTableFromList(newPasses.map { it.field }, "New Passes")
}
}
}
if (!report.addedBenchmarks.isEmpty()) {
renderCollapsedData("Added", true) {
table {
attributes["class"] = "table table-sm table-striped table-hover"
attributes["style"] = "width:initial; font-size: 11pt;"
renderTableFromList(report.addedBenchmarks, "Added benchmarks")
}
}
}
if (!report.removedBenchmarks.isEmpty()) {
renderCollapsedData("Removed", true) {
table {
attributes["class"] = "table table-sm table-striped table-hover"
attributes["style"] = "width:initial; font-size: 11pt;"
renderTableFromList(report.removedBenchmarks, "Removed benchmarks")
}
}
}
}
private fun BodyTag.renderPerformanceSummary(report: SummaryBenchmarksReport) {
if (!report.improvements.isEmpty() || !report.regressions.isEmpty()) {
h4 { +"Performance Summary" }
table {
attributes["class"] = "table table-sm table-striped table-hover"
attributes["style"] = "width:initial;"
thead {
tr {
th { +"Change" }
th { +"#" }
th { +"Maximum" }
th { +"Geometric mean" }
}
}
val maximumRegression = report.maximumRegression
val maximumImprovement = report.maximumImprovement
val regressionsGeometricMean = report.regressionsGeometricMean
val improvementsGeometricMean = report.improvementsGeometricMean
tbody {
if (!report.regressions.isEmpty()) {
tr {
th { +"Regressions" }
td { +"${report.regressions.size}" }
td {
attributes["bgcolor"] = ColoredCell(
maximumRegression/maxOf(maximumRegression, abs(maximumImprovement)))
.backgroundStyle
+formatValue(maximumRegression, true)
}
td {
attributes["bgcolor"] = ColoredCell(
regressionsGeometricMean/maxOf(regressionsGeometricMean,
abs(improvementsGeometricMean)))
.backgroundStyle
+formatValue(report.regressionsGeometricMean, true)
}
}
}
if (!report.improvements.isEmpty()) {
tr {
th { +"Improvements" }
td { +"${report.improvements.size}" }
td {
attributes["bgcolor"] = ColoredCell(
maximumImprovement/maxOf(maximumRegression, abs(maximumImprovement)))
.backgroundStyle
+formatValue(report.maximumImprovement, true)
}
td {
attributes["bgcolor"] = ColoredCell(
improvementsGeometricMean/maxOf(regressionsGeometricMean,
abs(improvementsGeometricMean)))
.backgroundStyle
+formatValue(report.improvementsGeometricMean, true)
}
}
}
}
}
}
}
private fun TableBlock.renderBenchmarksDetails(fullSet: Map<String, SummaryBenchmark>,
bucket: Map<String, ScoreChange>? = null, rowStyle: String? = null) {
if (bucket != null && !bucket.isEmpty()) {
// Find max ratio.
val maxRatio = bucket.values.map { it.second.mean }.maxOrNull()!!
// There are changes in performance.
// Output changed benchmarks.
for ((name, change) in bucket) {
tr {
rowStyle?. let {
attributes["style"] = rowStyle
}
th { +name }
td { +"${fullSet.getValue(name).first?.description}" }
td { +"${fullSet.getValue(name).second?.description}" }
td {
attributes["bgcolor"] = ColoredCell(if (bucket.values.first().first.mean == 0.0) null
else change.first.mean / abs(bucket.values.first().first.mean))
.backgroundStyle
+"${change.first.description + " %"}"
}
td {
val scaledRatio = if (maxRatio == 0.0) null else change.second.mean / maxRatio
attributes["bgcolor"] = ColoredCell(scaledRatio,
borderPositive = { cellValue -> cellValue > 1.0 / maxRatio }).backgroundStyle
+"${change.second.description}"
}
}
}
} else if (bucket == null) {
// Output all values without performance changes.
val placeholder = "-"
for ((name, value) in fullSet) {
tr {
th { +name }
td { +"${value.first?.description ?: placeholder}" }
td { +"${value.second?.description ?: placeholder}" }
td { +placeholder }
td { +placeholder }
}
}
}
}
private fun BodyTag.renderPerformanceDetails(report: SummaryBenchmarksReport, onlyChanges: Boolean) {
if (onlyChanges) {
if (report.regressions.isEmpty() && report.improvements.isEmpty()) {
div("alert alert-success") {
attributes["role"] = "alert"
+"All becnhmarks are stable!"
}
}
}
table {
attributes["id"] = "result"
attributes["class"] = "table table-striped table-bordered"
thead {
tr {
th { +"Benchmark" }
th { +"First score" }
th { +"Second score" }
th { +"Percent" }
th { +"Ratio" }
}
}
val geoMeanChangeMap = report.geoMeanScoreChange?.
let { mapOf(report.geoMeanBenchmark.first!!.name to report.geoMeanScoreChange!!) }
tbody {
renderBenchmarksDetails(
mutableMapOf(report.geoMeanBenchmark.first!!.name to report.geoMeanBenchmark),
geoMeanChangeMap, "border-bottom: 2.3pt solid black; border-top: 2.3pt solid black")
renderBenchmarksDetails(report.mergedReport, report.regressions)
renderBenchmarksDetails(report.mergedReport, report.improvements)
if (!onlyChanges) {
// Print all remaining results.
renderBenchmarksDetails(report.mergedReport.filter { it.key !in report.regressions.keys &&
it.key !in report.improvements.keys })
}
}
}
}
data class Color(val red: Double, val green: Double, val blue: Double) {
operator fun times(coefficient: Double) =
Color(red * coefficient, green * coefficient, blue * coefficient)
operator fun plus(other: Color) =
Color(red + other.red, green + other.green, blue + other.blue)
override fun toString() =
"#" + buildString {
listOf(red, green, blue).forEach {
append(clamp((it * 255).toInt(), 0, 255).toString(16).padStart(2, '0'))
}
}
}
class ColoredCell(val scaledValue: Double?, val reverse: Boolean = false,
val borderPositive: (cellValue: Double) -> Boolean = { cellValue -> cellValue > 0 }) {
val value: Double
val neutralColor = Color(1.0,1.0 , 1.0)
val negativeColor = Color(0.0, 1.0, 0.0)
val positiveColor = Color(1.0, 0.0, 0.0)
init {
value = scaledValue?.let { if (abs(scaledValue) <= 1.0) scaledValue else error ("Value should be scaled in range [-1.0; 1.0]") }
?: 0.0
}
val backgroundStyle: String
get() = scaledValue?.let { getColor().toString() } ?: ""
fun getColor(): Color {
val currentValue = clamp(value, -1.0, 1.0)
val cellValue = if (reverse) -currentValue else currentValue
val baseColor = if (borderPositive(cellValue)) positiveColor else negativeColor
// Smooth mapping to put first 20% of change into 50% of range,
// although really we should compensate for luma.
val color = sin((abs(cellValue).pow(.477)) * kotlin.math.PI * .5)
return linearInterpolation(neutralColor, baseColor, color)
}
private fun linearInterpolation(a: Color, b: Color, coefficient: Double): Color {
val reversedCoefficient = 1.0 - coefficient
return a * reversedCoefficient + b * coefficient
}
}
}
@@ -0,0 +1,18 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.renders
import org.jetbrains.analyzer.*
import org.jetbrains.report.*
// Report render to text format.
class JsonResultsRender: Render() {
override val name: String
get() = "json"
override fun render(report: SummaryBenchmarksReport, onlyChanges: Boolean) =
report.getBenchmarksReport().toJson()
}
@@ -0,0 +1,28 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.renders
import org.jetbrains.analyzer.*
import org.jetbrains.report.BenchmarkResult
// Report render to text format.
class MetricResultsRender: Render() {
override val name: String
get() = "metrics"
override fun render(report: SummaryBenchmarksReport, onlyChanges: Boolean): String {
val results = report.mergedReport.map { entry ->
buildString {
val metric = entry.value.first!!.metric
append("{ \"benchmarkName\": \"${entry.key.removeSuffix(metric.suffix)}\",")
append("\"metric\": \"${metric}\",")
append("\"value\": \"${entry.value.first!!.score}\" }")
}
}.joinToString(", ")
return "[ $results ]"
}
}
@@ -0,0 +1,205 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.renders
import org.jetbrains.analyzer.*
import org.jetbrains.report.*
import kotlin.math.abs
// Base class for printing report in different formats.
abstract class Render {
companion object {
fun getRenderByName(name: String) =
when (name) {
"text" -> TextRender()
"html" -> HTMLRender()
"teamcity" -> TeamCityStatisticsRender()
"statistics" -> StatisticsRender()
"metrics" -> MetricResultsRender()
else -> error("Unknown render $name")
}
}
abstract val name: String
abstract fun render(report: SummaryBenchmarksReport, onlyChanges: Boolean = false): String
// Print report using render.
fun print(report: SummaryBenchmarksReport, onlyChanges: Boolean = false, outputFile: String? = null) {
val content = render(report, onlyChanges)
outputFile?.let {
writeToFile(outputFile, content)
} ?: println(content)
}
protected fun formatValue(number: Double, isPercent: Boolean = false): String =
if (isPercent) number.format(2) + "%" else number.format()
}
// Report render to text format.
class TextRender: Render() {
override val name: String
get() = "text"
private val content = StringBuilder()
private val headerSeparator = "================="
private val wideColumnWidth = 50
private val standardColumnWidth = 25
private fun append(text: String = "") {
content.append("$text\n")
}
override fun render(report: SummaryBenchmarksReport, onlyChanges: Boolean): String {
renderEnvChanges(report.envChanges, "Environment")
renderEnvChanges(report.kotlinChanges, "Compiler")
renderStatusSummary(report)
renderStatusChangesDetails(report.getBenchmarksWithChangedStatus())
renderPerformanceSummary(report)
renderPerformanceDetails(report, onlyChanges)
return content.toString()
}
private fun printBucketInfo(bucket: Collection<Any>, name: String) {
if (!bucket.isEmpty()) {
append("$name: ${bucket.size}")
}
}
private fun <T> printStatusChangeInfo(bucket: List<FieldChange<T>>, name: String) {
if (!bucket.isEmpty()) {
append("$name:")
for (change in bucket) {
append(change.renderAsText())
}
}
}
fun renderEnvChanges(envChanges: List<FieldChange<String>>, bucketName: String) {
if (!envChanges.isEmpty()) {
append(ChangeReport(bucketName, envChanges).renderAsTextReport())
}
}
fun renderStatusChangesDetails(benchmarksWithChangedStatus: List<FieldChange<BenchmarkResult.Status>>) {
if (!benchmarksWithChangedStatus.isEmpty()) {
append("Changes in status")
append(headerSeparator)
printStatusChangeInfo(benchmarksWithChangedStatus
.filter { it.current == BenchmarkResult.Status.FAILED }, "New failures")
printStatusChangeInfo(benchmarksWithChangedStatus
.filter { it.current == BenchmarkResult.Status.PASSED }, "New passes")
append()
}
}
fun renderStatusSummary(report: SummaryBenchmarksReport) {
append("Status summary")
append(headerSeparator)
val failedBenchmarks = report.failedBenchmarks
val addedBenchmarks = report.addedBenchmarks
val removedBenchmarks = report.removedBenchmarks
if (failedBenchmarks.isEmpty()) {
append("All benchmarks passed!")
}
if (!failedBenchmarks.isEmpty() || !addedBenchmarks.isEmpty() || !removedBenchmarks.isEmpty()) {
printBucketInfo(failedBenchmarks, "Failed benchmarks")
printBucketInfo(addedBenchmarks, "Added benchmarks")
printBucketInfo(removedBenchmarks, "Removed benchmarks")
}
append("Total becnhmarks number: ${report.benchmarksNumber}")
append()
}
fun renderPerformanceSummary(report: SummaryBenchmarksReport) {
if (!report.regressions.isEmpty() || !report.improvements.isEmpty()) {
append("Performance summary")
append(headerSeparator)
if (!report.regressions.isEmpty()) {
append("Regressions: Maximum = ${formatValue(report.maximumRegression, true)}," +
" Geometric mean = ${formatValue(report.regressionsGeometricMean, true)}")
}
if (!report.improvements.isEmpty()) {
append("Improvements: Maximum = ${formatValue(report.maximumImprovement, true)}," +
" Geometric mean = ${formatValue(report.improvementsGeometricMean, true)}")
}
append()
}
}
private fun formatColumn(content:String, isWide: Boolean = false): String =
content.padEnd(if (isWide) wideColumnWidth else standardColumnWidth, ' ')
private fun printBenchmarksDetails(fullSet: Map<String, SummaryBenchmark>,
bucket: Map<String, ScoreChange>? = null) {
val placeholder = "-"
if (bucket != null) {
// There are changes in performance.
// Output changed benchmarks.
for ((name, change) in bucket) {
append(formatColumn(name, true) +
formatColumn(fullSet.getValue(name).first?.description ?: placeholder) +
formatColumn(fullSet.getValue(name).second?.description ?: placeholder) +
formatColumn(change.first.description + " %") +
formatColumn(change.second.description))
}
} else {
// Output all values without performance changes.
for ((name, value) in fullSet) {
append(formatColumn(name, true) +
formatColumn(value.first?.description ?: placeholder) +
formatColumn(value.second?.description ?: placeholder) +
formatColumn(placeholder) +
formatColumn(placeholder))
}
}
}
private fun printTableLineSeparator(tableWidth: Int) =
append("${"-".padEnd(tableWidth, '-')}")
private fun printPerformanceTableHeader(): Int {
val wideColumns = listOf(formatColumn("Benchmark", true))
val standardColumns = listOf(formatColumn("First score"),
formatColumn("Second score"),
formatColumn("Percent"),
formatColumn("Ratio"))
val tableWidth = wideColumnWidth * wideColumns.size + standardColumnWidth * standardColumns.size
append("${wideColumns.joinToString(separator = "")}${standardColumns.joinToString(separator = "")}")
printTableLineSeparator(tableWidth)
return tableWidth
}
fun renderPerformanceDetails(report: SummaryBenchmarksReport, onlyChanges: Boolean = false) {
append("Performance details")
append(headerSeparator)
if (onlyChanges) {
if (report.regressions.isEmpty() && report.improvements.isEmpty()) {
append("All becnhmarks are stable.")
}
}
val tableWidth = printPerformanceTableHeader()
// Print geometric mean.
val geoMeanChangeMap = report.geoMeanScoreChange?.
let { mapOf(report.geoMeanBenchmark.first!!.name to report.geoMeanScoreChange!!) }
printBenchmarksDetails(
mutableMapOf(report.geoMeanBenchmark.first!!.name to report.geoMeanBenchmark),
geoMeanChangeMap)
printTableLineSeparator(tableWidth)
printBenchmarksDetails(report.mergedReport, report.regressions)
printBenchmarksDetails(report.mergedReport, report.improvements)
if (!onlyChanges) {
// Print all remaining results.
printBenchmarksDetails(report.mergedReport.filter { it.key !in report.regressions.keys &&
it.key !in report.improvements.keys })
}
}
}
@@ -0,0 +1,59 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.renders
import org.jetbrains.analyzer.*
import org.jetbrains.report.BenchmarkResult
enum class Status {
FAILED, FIXED, IMPROVED, REGRESSED, STABLE, UNSTABLE
}
// Report render to short summary statistics.
class StatisticsRender: Render() {
override val name: String
get() = "statistics"
private var content = StringBuilder()
override fun render(report: SummaryBenchmarksReport, onlyChanges: Boolean): String {
val benchmarksWithChangedStatus = report.getBenchmarksWithChangedStatus()
val newPasses = benchmarksWithChangedStatus
.filter { it.current == BenchmarkResult.Status.PASSED }
val newFailures = benchmarksWithChangedStatus
.filter { it.current == BenchmarkResult.Status.FAILED }
if (report.failedBenchmarks.isNotEmpty()) {
content.append("failed: ${report.failedBenchmarks.size}\n")
}
val status = when {
newFailures.isNotEmpty() -> {
content.append("new failures: ${newFailures.size}\n")
Status.FAILED
}
newPasses.isNotEmpty() -> {
content.append("new passes: ${newPasses.size}\n")
Status.FIXED
}
report.improvements.isNotEmpty() && report.regressions.isNotEmpty() -> {
content.append("regressions: ${report.regressions.size}\nimprovements: ${report.improvements.size}")
Status.UNSTABLE
}
report.improvements.isNotEmpty() && report.regressions.isEmpty() -> {
content.append("improvements: ${report.improvements.size}")
Status.IMPROVED
}
report.improvements.isEmpty() && report.regressions.isNotEmpty() -> {
content.append("regressions: ${report.regressions.size}")
Status.REGRESSED
}
else -> Status.STABLE
}
return """
status: $status
total: ${report.benchmarksNumber}
""".trimIndent() + "\n$content"
}
}
@@ -0,0 +1,58 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.renders
import org.jetbrains.analyzer.*
import org.jetbrains.report.BenchmarkResult
import org.jetbrains.report.MeanVarianceBenchmark
// Report render to text format.
class TeamCityStatisticsRender: Render() {
override val name: String
get() = "teamcity"
private var content = StringBuilder()
override fun render(report: SummaryBenchmarksReport, onlyChanges: Boolean): String {
val currentDurations = report.currentBenchmarksDuration
content.append("##teamcity[testSuiteStarted name='Benchmarks']\n")
// For current benchmarks print score as TeamCity Test Metadata
report.currentMeanVarianceBenchmarks.forEach { benchmark ->
renderBenchmark(benchmark, currentDurations[benchmark.name]!!)
renderSummaryBecnhmarkValue(benchmark)
}
content.append("##teamcity[testSuiteFinished name='Benchmarks']\n")
// Report geometric mean as build statistic value
renderGeometricMean(report.geoMeanBenchmark.first!!)
return content.toString()
}
private fun renderSummaryBecnhmarkValue(benchmark: MeanVarianceBenchmark) {
content.append("##teamcity[testMetadata testName='${benchmark.name}' name='Mean'" +
" type='number' value='${benchmark.score}']\n")
content.append("##teamcity[testMetadata testName='${benchmark.name}' name='Variance'" +
" type='number' value='${benchmark.variance}']\n")
}
// Produce benchmark as test in TeamCity
private fun renderBenchmark(benchmark: BenchmarkResult , duration: Double) {
content.append("##teamcity[testStarted name='${benchmark.name}']\n")
if (benchmark.status == BenchmarkResult.Status.FAILED) {
content.append("##teamcity[testFailed name='${benchmark.name}']\n")
}
// test_duration_in_milliseconds is set for TeamCity
content.append("##teamcity[testFinished name='${benchmark.name}' duration='${(duration / 1000).toInt()}']\n")
}
private fun renderGeometricMean(geoMeanBenchmark: MeanVarianceBenchmark) {
content.append("##teamcity[buildStatisticValue key='Geometric mean' value='${geoMeanBenchmark.score}']\n")
content.append("##teamcity[buildStatisticValue key='Geometric mean variance' value='${geoMeanBenchmark.variance}']\n")
}
}
@@ -0,0 +1,5 @@
headers = curl/curl.h
headerFilter = curl/*
linkerOpts.osx = -L/opt/local/lib -L/usr/local/opt/curl/lib -lcurl
linkerOpts.linux = -L/usr/lib64 -L/usr/lib/x86_64-linux-gnu -lcurl
linkerOpts.mingw = -lcurl
@@ -0,0 +1,63 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.analyzer
import kotlin.test.*
import kotlin.math.abs
import org.jetbrains.report.BenchmarkResult
import org.jetbrains.report.MeanVarianceBenchmark
class AnalyzerTests {
private val eps = 0.000001
private fun createMeanVarianceBenchmarks(): Pair<MeanVarianceBenchmark, MeanVarianceBenchmark> {
val first = MeanVarianceBenchmark("testBenchmark", BenchmarkResult.Status.PASSED, 9.0, BenchmarkResult.Metric.EXECUTION_TIME, 9.0, 10, 10, 0.0001)
val second = MeanVarianceBenchmark("testBenchmark", BenchmarkResult.Status.PASSED, 10.0, BenchmarkResult.Metric.EXECUTION_TIME, 10.0, 10, 10, 0.0001)
return Pair(first, second)
}
@Test
fun testGeoMean() {
val numbers = listOf(4.0, 6.0, 9.0)
val value = geometricMean(numbers)
val expected = 6.0
assertTrue(abs(value - expected) < eps)
}
@Test
fun testComputeMeanVariance() {
val numbers = listOf(10.1, 10.2, 10.3)
val value = computeMeanVariance(numbers)
val expectedMean = 10.2
val expectedVariance = 0.07872455
assertTrue(abs(value.mean - expectedMean) < eps)
assertTrue(abs(value.variance - expectedVariance) < eps)
}
@Test
fun calcPercentageDiff() {
val inputs = createMeanVarianceBenchmarks()
val percent = inputs.first.calcPercentageDiff(inputs.second)
val expectedMean = -9.99809998
val expectedVariance = 0.0021
assertTrue(abs(percent.mean - expectedMean) < eps)
//assertTrue(abs(percent.variance - expectedVariance) < eps)
}
@Test
fun calcRatio() {
val inputs = createMeanVarianceBenchmarks()
val ratio = inputs.first.calcRatio(inputs.second)
val expectedMean = 0.9
val expectedVariance = 0.00001899
assertTrue(abs(ratio.mean - expectedMean) < eps)
assertTrue(abs(ratio.variance - expectedVariance) < eps)
}
}
@@ -0,0 +1,201 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import org.jetbrains.kotlin.konan.*
buildscript {
ext.rootBuildDirectory = file('../../')
apply from: "$rootBuildDirectory/gradle/loadRootProperties.gradle"
apply from: "$rootBuildDirectory/gradle/kotlinGradlePlugin.gradle"
repositories {
maven {
url = 'https://cache-redirector.jetbrains.com/jcenter'
}
jcenter()
maven {
url "https://cache-redirector.jetbrains.com/plugins.gradle.org/m2/"
}
gradlePluginPortal()
}
dependencies {
classpath 'com.github.jengelman.gradle.plugins:shadow:4.0.4'
classpath "org.jetbrains.kotlin:kotlin-native-shared:$konanVersion"
}
}
apply plugin: 'java-gradle-plugin'
apply plugin: 'kotlin'
apply plugin: 'groovy'
apply plugin: 'com.github.johnrengelman.shadow'
group = 'org.jetbrains.kotlin'
version = CompilerVersionGeneratedKt.getCurrentCompilerVersion()
repositories {
mavenCentral()
maven {
url buildKotlinCompilerRepo
}
maven {
url kotlinCompilerRepo
}
}
configurations {
bundleDependencies {
transitive = false
}
implementation.extendsFrom shadow
compileOnly.extendsFrom bundleDependencies
testImplementation.extendsFrom bundleDependencies
}
dependencies {
shadow "org.jetbrains.kotlin:kotlin-stdlib:1.3.0"
// Bundle the serialization plugin into the final jar because we shade classes of the kotlin plugin
// while the serialization one extends them.
bundleDependencies "org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion"
bundleDependencies "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
bundleDependencies "org.jetbrains.kotlin:kotlin-gradle-plugin-api:$kotlinVersion"
bundleDependencies "org.jetbrains.kotlin:kotlin-native-utils:$kotlinVersion"
bundleDependencies "org.jetbrains.kotlin:kotlin-native-shared:$konanVersion"
bundleDependencies "org.jetbrains.kotlin:kotlin-util-io:$kotlinVersion"
bundleDependencies "org.jetbrains.kotlin:kotlin-util-klib:$kotlinVersion"
testImplementation 'junit:junit:4.12'
testImplementation "org.jetbrains.kotlin:kotlin-test:$buildKotlinVersion"
testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$buildKotlinVersion"
testImplementation "org.tools4j:tools4j-spockito:1.6"
testImplementation('org.spockframework:spock-core:1.1-groovy-2.4') {
exclude module: 'groovy-all'
}
}
shadowJar {
from sourceSets.main.output
configurations = [project.configurations.bundleDependencies]
archiveClassifier.set(null)
relocate('org.jetbrains.kotlinx', 'shadow.org.jetbrains.kotlinx')
relocate('org.jetbrains.kotlin.compilerRunner', 'shadow.org.jetbrains.kotlin.compilerRunner')
relocate('org.jetbrains.kotlin.konan', 'shadow.org.jetbrains.kotlin.konan')
relocate('org.jetbrains.kotlin.gradle', 'shadow.org.jetbrains.kotlin.gradle') {
exclude('org.jetbrains.kotlin.gradle.plugin.experimental.**')
exclude('org.jetbrains.kotlin.gradle.plugin.konan.**')
exclude('org.jetbrains.kotlin.gradle.plugin.model.**')
}
exclude {
def path = it.relativePath.pathString
if (path.startsWith("META-INF/gradle-plugins") && path.endsWith(".properties")) {
def fileName = it.name
def id = fileName.take(fileName.lastIndexOf('.'))
return project.gradlePlugin.plugins.findByName(id) == null
}
return false
}
exclude('META-INF/services/org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar')
exclude('META-INF/services/org.jetbrains.kotlin.gradle.plugin.KotlinGradleSubplugin')
}
jar {
dependsOn shadowJar
enabled = false
}
pluginUnderTestMetadata {
dependsOn shadowJar
doLast {
// Since Gradle 4.10 it isn't possible to edit the pluginUnderTest classpath.
// So we have to manually set the implementation-classpath to get the output fat-jar.
def pluginMetadata = outputDirectory.get().file(PluginUnderTestMetadata.METADATA_FILE_NAME).getAsFile()
def classpath = files(shadowJar.archivePath) + configurations.shadow
new Properties().with { properties ->
pluginMetadata.withInputStream {
properties.load(it)
}
properties.setProperty(PluginUnderTestMetadata.IMPLEMENTATION_CLASSPATH_PROP_KEY , classpath.asPath)
pluginMetadata.withOutputStream {
properties.store(it, null)
}
}
}
}
test {
dependsOn shadowJar
systemProperty("kotlin.version", kotlinVersion)
systemProperty("kotlin.repo", kotlinCompilerRepo)
if (project.hasProperty("konan.home")) {
systemProperty("konan.home", project.property("konan.home"))
systemProperty("org.jetbrains.kotlin.native.home", project.property("konan.home"))
} else if (project.hasProperty("org.jetbrains.kotlin.native.home")) {
systemProperty("org.jetbrains.kotlin.native.home", project.property("org.jetbrains.kotlin.native.home"))
} else {
// The Koltin/Native compiler must be built before test execution.
systemProperty("konan.home", distDir.absolutePath)
systemProperty("org.jetbrains.kotlin.native.home", distDir.absolutePath)
}
if (project.hasProperty("konan.jvmArgs")) {
systemProperty("konan.jvmArgs", project.property("konan.jvmArgs"))
}
// Uncomment for debugging.
//testLogging.showStandardStreams = true
if (project.hasProperty("maxParallelForks")) {
maxParallelForks=project.property("maxParallelForks")
}
if (project.hasProperty("filter")) {
filter.includeTestsMatching project.property("filter")
}
if (project.hasProperty("gradleVersion")) {
systemProperty("gradleVersion", project.property("gradleVersion"))
}
}
processResources {
from(file("$rootBuildDirectory/utilities/env_blacklist"))
}
tasks.named('compileTestGroovy') {
classpath = sourceSets.test.compileClasspath
}
tasks.named('compileTestKotlin') {
classpath += files(sourceSets.test.groovy.classesDirectory)
}
gradlePlugin {
plugins {
create('konan') {
id = 'konan'
implementationClass = 'org.jetbrains.kotlin.gradle.plugin.konan.KonanPlugin'
}
// We bundle a shaded version of kotlinx-serialization plugin
create('kotlinx-serialization-native') {
id = 'kotlinx-serialization-native'
implementationClass = 'shadow.org.jetbrains.kotlinx.serialization.gradle.SerializationGradleSubplugin'
}
create('org.jetbrains.kotlin.konan') {
id = 'org.jetbrains.kotlin.konan'
implementationClass = 'org.jetbrains.kotlin.gradle.plugin.konan.KonanPlugin'
}
}
}
@@ -0,0 +1,2 @@
rootProject.name = "kotlin-native-gradle-plugin"
includeBuild '../../shared'
@@ -0,0 +1,122 @@
/*
* Copyright 2010-2018 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.gradle.plugin.experimental.internal
import org.gradle.api.Project
import org.gradle.api.internal.component.UsageContext
import org.gradle.api.internal.project.ProjectInternal
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.Provider
import org.gradle.internal.os.OperatingSystem
import org.gradle.language.cpp.internal.NativeVariantIdentity
import org.gradle.nativeplatform.MachineArchitecture
import org.gradle.nativeplatform.OperatingSystemFamily
import org.gradle.nativeplatform.TargetMachine
import org.gradle.nativeplatform.TargetMachineFactory
import org.gradle.nativeplatform.platform.NativePlatform
import org.gradle.nativeplatform.platform.internal.*
import org.jetbrains.kotlin.gradle.utils.isGradleVersionAtLeast
import org.jetbrains.kotlin.konan.target.KonanTarget
import org.jetbrains.kotlin.konan.util.visibleName
interface KotlinNativePlatform: NativePlatform {
val target: KonanTarget
}
fun KonanTarget.getGradleOS(): OperatingSystemInternal = family.visibleName.let {
DefaultOperatingSystem(it, OperatingSystem.forName(it))
}
fun KonanTarget.getGradleOSFamily(objectFactory: ObjectFactory): OperatingSystemFamily {
return objectFactory.named(OperatingSystemFamily::class.java, family.visibleName)
}
fun KonanTarget.getGradleCPU(): ArchitectureInternal = architecture.visibleName.let {
Architectures.forInput(it)
}
fun KonanTarget.toTargetMachine(objectFactory: ObjectFactory): TargetMachine = object: TargetMachine {
override fun getOperatingSystemFamily(): OperatingSystemFamily =
getGradleOSFamily(objectFactory)
override fun getArchitecture(): MachineArchitecture =
objectFactory.named(MachineArchitecture::class.java, this@toTargetMachine.architecture.visibleName)
}
class DefaultKotlinNativePlatform(name: String, override val target: KonanTarget):
DefaultNativePlatform(name, target.getGradleOS(), target.getGradleCPU()),
KotlinNativePlatform
{
constructor(target: KonanTarget): this(target.visibleName, target)
// TODO: Extend ImmutableDefaultNativePlatform and get rid of these methods after switch to Gradle 4.8
private fun notImplemented(): Nothing = throw NotImplementedError("Not Implemented in Kotlin/Native plugin")
override fun operatingSystem(name: String?) = notImplemented()
override fun withArchitecture(architecture: ArchitectureInternal?) = notImplemented()
override fun architecture(name: String?) = notImplemented()
}
// NativeVariantIdentity constructor was changed in Gradle 5.1
// So we have to use reflection to create instance of this class in earlier versions.
internal fun compatibleVariantIdentity(
project: Project,
name: String,
baseName: Provider<String>,
group: Provider<String>,
version: Provider<String>,
debuggable: Boolean,
optimized: Boolean,
target: KonanTarget,
linkUsage: UsageContext?,
runtimeUsage: UsageContext?
): NativeVariantIdentity =
if (isGradleVersionAtLeast(5, 1)) {
val targetMachineFactory = (project as ProjectInternal).services.get(TargetMachineFactory::class.java)
NativeVariantIdentity(
name,
baseName,
group,
version,
debuggable,
optimized,
targetMachineFactory.os(target.family.name),
linkUsage,
runtimeUsage
)
} else {
NativeVariantIdentity::class.java.getConstructor(
String::class.java,
Provider::class.java,
Provider::class.java,
Provider::class.java,
Boolean::class.javaPrimitiveType,
Boolean::class.javaPrimitiveType,
OperatingSystemFamily::class.java,
UsageContext::class.java,
UsageContext::class.java
).newInstance(
name,
baseName,
group,
version,
debuggable,
optimized,
target.getGradleOSFamily(project.objects),
linkUsage,
runtimeUsage
)
}
@@ -0,0 +1,109 @@
/*
* Copyright 2010-2018 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.gradle.plugin.konan
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.plugin.konan.KonanPlugin.ProjectProperty
import java.io.File
/**
* The plugin allows an IDE to specify some building parameters. These parameters
* are passed to the plugin via environment variables. Two variables are supported:
* - CONFIGURATION_BUILD_DIR - A path to a destination directory for all compilation tasks.
* The IDE should take care about specifying different directories
* for different targets. This setting has less priority than
* an explicitly specified destination directory in the build script.
*
* - DEBUGGING_SYMBOLS - If YES, the debug support will be enabled for all artifacts. This option has less
* priority than explicitly specified enableDebug option in the build script and
* enableDebug project property.
*
* - KONAN_ENABLE_OPTIMIZATIONS - If YES, optimizations will be enabled for all artifacts by default. This option
* has less priority than explicitly specified enableOptimizations option in the
* build script.
*
* Support for environment variables should be explicitly enabled by setting a project property:
* konan.useEnvironmentVariables = true.
*/
internal interface EnvironmentVariables {
val configurationBuildDir: File?
val debuggingSymbols: Boolean
val enableOptimizations: Boolean
}
internal class EnvironmentVariablesUnused: EnvironmentVariables {
override val configurationBuildDir: File?
get() = null
override val debuggingSymbols: Boolean
get() = false
override val enableOptimizations: Boolean
get() = false
}
internal class EnvironmentVariablesImpl(val project: Project): EnvironmentVariables {
override val configurationBuildDir: File?
get() = System.getenv("CONFIGURATION_BUILD_DIR")?.let {
project.file(it)
}
override val debuggingSymbols: Boolean
get() = System.getenv("DEBUGGING_SYMBOLS")?.toUpperCase() == "YES"
override val enableOptimizations: Boolean
get() = System.getenv("KONAN_ENABLE_OPTIMIZATIONS")?.toUpperCase() == "YES"
}
/**
* Due to https://github.com/gradle/gradle/issues/3468 we cannot use environment
* variables in Java 9. Until Gradle API for environment variables is provided
* we use project properties instead of them. TODO: Return to using env vars when the issue is fixed.
*/
internal class EnvironmentVariablesFromProperties(val project: Project): EnvironmentVariables {
override val configurationBuildDir: File?
get() = project.findProperty(ProjectProperty.KONAN_CONFIGURATION_BUILD_DIR)?.let {
project.file(it)
}
override val debuggingSymbols: Boolean
get() = project.findProperty(ProjectProperty.KONAN_DEBUGGING_SYMBOLS)?.toString()?.toUpperCase().let {
it == "YES" || it == "TRUE"
}
override val enableOptimizations: Boolean
get() = project.findProperty(ProjectProperty.KONAN_OPTIMIZATIONS_ENABLE)?.toString()?.toUpperCase().let {
it == "YES" || it == "TRUE"
}
}
internal val Project.useEnvironmentVariables: Boolean
get() = findProperty(ProjectProperty.KONAN_USE_ENVIRONMENT_VARIABLES)?.toString()?.toBoolean() ?: false
/*
TODO: Return to using env vars when the issue is fixed.
Take into account the useEnvironmentVariables property (and may be rename it) in the following way:
if (useEnvironmentVariables) {
EnvironmentVariablesImpl(project)
} else {
EnvironmentVariablesUnused()
}
*/
internal val Project.environmentVariables: EnvironmentVariables
get() = EnvironmentVariablesFromProperties(project)
@@ -0,0 +1,171 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.gradle.plugin.konan
import groovy.lang.Closure
import org.gradle.api.Action
import org.gradle.api.NamedDomainObjectFactory
import org.gradle.api.internal.DefaultPolymorphicDomainObjectContainer
import org.gradle.api.internal.project.ProjectInternal
import org.gradle.internal.reflect.Instantiator
import org.gradle.util.ConfigureUtil
import kotlin.reflect.KClass
open class KonanArtifactContainer(val project: ProjectInternal)
: DefaultPolymorphicDomainObjectContainer<KonanBuildingConfig<*>>(
KonanBuildingConfig::class.java,
project.services.get(Instantiator::class.java)
) {
private inner class KonanBuildingConfigFactory<T: KonanBuildingConfig<*>>(val configClass: KClass<T>)
: NamedDomainObjectFactory<T> {
var targets: Iterable<String> = emptyList()
override fun create(name: String?): T =
instantiator.newInstance(configClass.java, name, project, targets)
}
private val factories = mutableMapOf<KClass<out KonanBuildingConfig<*>>, KonanBuildingConfigFactory<*>>()
private fun <T: KonanBuildingConfig<*>> createFactory(configClass: KClass<T>) {
val factory = KonanBuildingConfigFactory(configClass)
super.registerFactory(configClass.java, factory)
factories.put(configClass, factory)
}
init {
createFactory(KonanProgram::class)
createFactory(KonanDynamic::class)
createFactory(KonanFramework::class)
createFactory(KonanLibrary::class)
createFactory(KonanBitcode::class)
createFactory(KonanInteropLibrary::class)
}
private fun determineTargets(configClass: KClass<out KonanBuildingConfig<*>>, args: Map<String, Any?>) {
val targetsArg = args["targets"]
val targets = when {
targetsArg == null -> project.konanExtension.targets
targetsArg is Iterable<*> -> targetsArg.map { it.toString() }
else -> listOf(targetsArg.toString())
}
factories[configClass]?.targets = targets
}
private fun <T: KonanBuildingConfig<*>> create(name: String,
configClass: KClass<T>,
args: Map<String, Any?>,
configureAction: Action<T>) {
determineTargets(configClass, args)
super.create(name, configClass.java, configureAction)
}
private fun <T: KonanBuildingConfig<*>> create(name: String,
configClass: KClass<T>,
args: Map<String, Any?>,
configureAction: T.() -> Unit) {
determineTargets(configClass, args)
super.create(name, configClass.java, configureAction)
}
private fun <T: KonanBuildingConfig<*>> create(name: String,
configClass: KClass<T>,
args: Map<String, Any?>) {
determineTargets(configClass, args)
super.create(name, configClass.java)
}
fun program(args: Map<String, Any?>, name: String) = create(name, KonanProgram::class, args)
fun program(args: Map<String, Any?>, name: String, configureAction: Action<KonanProgram>) =
create(name, KonanProgram::class, args, configureAction)
fun program(args: Map<String, Any?>, name: String, configureAction: KonanProgram.() -> Unit) =
create(name, KonanProgram::class, args, configureAction)
fun program(args: Map<String, Any?>, name: String, configureAction: Closure<*>) =
program(args, name, ConfigureUtil.configureUsing(configureAction))
fun dynamic(args: Map<String, Any?>, name: String) = create(name, KonanDynamic::class, args)
fun dynamic(args: Map<String, Any?>, name: String, configureAction: Action<KonanDynamic>) =
create(name, KonanDynamic::class, args, configureAction)
fun dynamic(args: Map<String, Any?>, name: String, configureAction: KonanDynamic.() -> Unit) =
create(name, KonanDynamic::class, args, configureAction)
fun dynamic(args: Map<String, Any?>, name: String, configureAction: Closure<*>) =
dynamic(args, name, ConfigureUtil.configureUsing(configureAction))
fun framework(args: Map<String, Any?>, name: String) = create(name, KonanFramework::class, args)
fun framework(args: Map<String, Any?>, name: String, configureAction: Action<KonanFramework>) =
create(name, KonanFramework::class, args, configureAction)
fun framework(args: Map<String, Any?>, name: String, configureAction: KonanFramework.() -> Unit) =
create(name, KonanFramework::class, args, configureAction)
fun framework(args: Map<String, Any?>, name: String, configureAction: Closure<*>) =
framework(args, name, ConfigureUtil.configureUsing(configureAction))
fun library(args: Map<String, Any?>, name: String) = create(name, KonanLibrary::class, args)
fun library(args: Map<String, Any?>, name: String, configureAction: Action<KonanLibrary>) =
create(name, KonanLibrary::class, args, configureAction)
fun library(args: Map<String, Any?>, name: String, configureAction: KonanLibrary.() -> Unit) =
create(name, KonanLibrary::class, args, configureAction)
fun library(args: Map<String, Any?>, name: String, configureAction: Closure<*>) =
library(args, name, ConfigureUtil.configureUsing(configureAction))
fun bitcode(args: Map<String, Any?>, name: String) = create(name, KonanBitcode::class, args)
fun bitcode(args: Map<String, Any?>, name: String, configureAction: Action<KonanBitcode>) =
create(name, KonanBitcode::class, args, configureAction)
fun bitcode(args: Map<String, Any?>, name: String, configureAction: KonanBitcode.() -> Unit) =
create(name, KonanBitcode::class, args, configureAction)
fun bitcode(args: Map<String, Any?>, name: String, configureAction: Closure<*>) =
bitcode(args, name, ConfigureUtil.configureUsing(configureAction))
fun interop(args: Map<String, Any?>, name: String) = create(name, KonanInteropLibrary::class, args)
fun interop(args: Map<String, Any?>, name: String, configureAction: Action<KonanInteropLibrary>) =
create(name, KonanInteropLibrary::class, args, configureAction)
fun interop(args: Map<String, Any?>, name: String, configureAction: KonanInteropLibrary.() -> Unit) =
create(name, KonanInteropLibrary::class, args, configureAction)
fun interop(args: Map<String, Any?>, name: String, configureAction: Closure<*>) =
interop(args, name, ConfigureUtil.configureUsing(configureAction))
fun program(name: String) = program(emptyMap(), name)
fun program(name: String, configureAction: Action<KonanProgram>) = program(emptyMap(), name, configureAction)
fun program(name: String, configureAction: KonanProgram.() -> Unit) = program(emptyMap(), name, configureAction)
fun program(name: String, configureAction: Closure<*>) = program(emptyMap(), name, configureAction)
fun dynamic(name: String) = dynamic(emptyMap(), name)
fun dynamic(name: String, configureAction: Action<KonanDynamic>) = dynamic(emptyMap(), name, configureAction)
fun dynamic(name: String, configureAction: KonanDynamic.() -> Unit) = dynamic(emptyMap(), name, configureAction)
fun dynamic(name: String, configureAction: Closure<*>) = dynamic(emptyMap(), name, configureAction)
fun framework(name: String) = framework(emptyMap(), name)
fun framework(name: String, configureAction: Action<KonanFramework>) = framework(emptyMap(), name, configureAction)
fun framework(name: String, configureAction: KonanFramework.() -> Unit) = framework(emptyMap(), name, configureAction)
fun framework(name: String, configureAction: Closure<*>) = framework(emptyMap(), name, configureAction)
fun library(name: String) = library(emptyMap(), name)
fun library(name: String, configureAction: Action<KonanLibrary>) = library(emptyMap(), name, configureAction)
fun library(name: String, configureAction: KonanLibrary.() -> Unit) = library(emptyMap(), name, configureAction)
fun library(name: String, configureAction: Closure<*>) = library(emptyMap(), name, configureAction)
fun bitcode(name: String) = bitcode(emptyMap(), name)
fun bitcode(name: String, configureAction: Action<KonanBitcode>) = bitcode(emptyMap(), name, configureAction)
fun bitcode(name: String, configureAction: KonanBitcode.() -> Unit) = bitcode(emptyMap(), name, configureAction)
fun bitcode(name: String, configureAction: Closure<*>) = bitcode(emptyMap(), name, configureAction)
fun interop(name: String) = interop(emptyMap(), name)
fun interop(name: String, configureAction: Action<KonanInteropLibrary>) = interop(emptyMap(), name, configureAction)
fun interop(name: String, configureAction: KonanInteropLibrary.() -> Unit) = interop(emptyMap(), name, configureAction)
fun interop(name: String, configureAction: Closure<*>) = interop(emptyMap(), name, configureAction)
}
@@ -0,0 +1,190 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.gradle.plugin.konan
import groovy.lang.Closure
import org.gradle.api.*
import org.gradle.api.internal.project.ProjectInternal
import org.gradle.api.plugins.BasePlugin
import org.gradle.api.plugins.ExtensionAware
import org.gradle.api.publish.maven.MavenPom
import org.gradle.util.ConfigureUtil
import org.gradle.util.WrapUtil
import org.jetbrains.kotlin.gradle.plugin.tasks.KonanBuildingTask
import org.jetbrains.kotlin.konan.target.KonanTarget
import java.io.File
/** Base class for all Kotlin/Native artifacts. */
abstract class KonanBuildingConfig<T: KonanBuildingTask>(private val name_: String,
val type: Class<T>,
val project: ProjectInternal,
val targets: Iterable<String>)
: KonanBuildingSpec, Named, DomainObjectSet<T> by WrapUtil.toDomainObjectSet(type) {
internal val mainVariant = KonanSoftwareComponent(project)
override fun getName() = name_
protected val targetToTask = mutableMapOf<KonanTarget, T>()
internal val aggregateBuildTask: Task
internal var pomActions = mutableListOf<Action<MavenPom>>()
private val konanTargets: Iterable<KonanTarget>
get() = project.hostManager.toKonanTargets(targets).distinct()
init {
for (targetName in targets.distinct()) {
val konanTarget = project.hostManager.targetByName(targetName)
if (!project.hostManager.isEnabled(konanTarget)) {
project.logger.info("The target is not enabled on the current host: $targetName")
continue
}
if (!targetIsSupported(konanTarget)) {
project.logger.info("The target ${targetName} is not supported by the artifact $name")
continue
}
if (this[konanTarget] == null) {
val task = createTask(konanTarget)
add(task)
targetToTask[konanTarget] = task
// Allow accessing targets just by their names in Groovy DSL.
(this as? ExtensionAware)?.extensions?.add(konanTarget.visibleName, task)
}
if (targetName != konanTarget.visibleName) {
createTargetAliasTaskIfDeclared(targetName)
}
}
aggregateBuildTask = createAggregateTask()
}
protected open fun generateTaskName(target: KonanTarget) =
"compileKonan${name.capitalize()}${target.visibleName.capitalize()}"
protected open fun generateAggregateTaskName() =
"compileKonan${name.capitalize()}"
protected open fun generateTargetAliasTaskName(targetName: String) =
"compileKonan${name.capitalize()}${targetName.capitalize()}"
protected abstract fun generateTaskDescription(task: T): String
protected abstract fun generateAggregateTaskDescription(task: Task): String
protected abstract fun generateTargetAliasTaskDescription(task: Task, targetName: String): String
protected abstract val defaultBaseDir: File
protected open fun targetIsSupported(target: KonanTarget): Boolean = true
data class OutputPlacement(val destinationDir: File, val artifactName: String)
// There are two options for output placement.
// 1. Gradle's build directory. We use it by default, e.g. if user runs Gradle from command line.
// In this case all produced files has the same name but are placed in different directories
// depending on their targets (e.g. linux/foo.kexe and macbook/foo.kexe).
// 2. Custom path provided by IDE. In this case CONFIGURATION_BUILD_DIR environment variable should
// contain a path to a destination directory. All produced files are placed in this directory so IDE
// should take care about setting different CONFIGURATION_BUILD_DIR for different targets.
protected fun determineOutputPlacement(target: KonanTarget): OutputPlacement {
val configurationBuildDir = project.environmentVariables.configurationBuildDir
return if (configurationBuildDir != null) {
OutputPlacement(configurationBuildDir, name)
} else {
OutputPlacement(defaultBaseDir.targetSubdir(target), name)
}
}
protected fun createTask(target: KonanTarget): T =
project.tasks.create(generateTaskName(target), type) {
val outputDescription = determineOutputPlacement(target)
it.init(this, outputDescription.destinationDir, outputDescription.artifactName, target)
it.group = BasePlugin.BUILD_GROUP
it.description = generateTaskDescription(it)
} ?: throw Exception("Cannot create task for target: ${target.visibleName}")
protected fun createAggregateTask(): Task =
project.tasks.create(generateAggregateTaskName()) { task ->
task.group = BasePlugin.BUILD_GROUP
task.description = generateAggregateTaskDescription(task)
this.filter {
project.targetIsRequested(it.konanTarget)
}.forEach {
task.dependsOn(it)
}
project.compileAllTask.dependsOn(task)
}
protected fun createTargetAliasTaskIfDeclared(targetName: String): Task? {
val canonicalTarget = project.hostManager.targetByName(targetName)
return this[canonicalTarget]?.let { canonicalBuild ->
project.tasks.create(generateTargetAliasTaskName(targetName)) {
it.group = BasePlugin.BUILD_GROUP
it.description = generateTargetAliasTaskDescription(it, targetName)
it.dependsOn(canonicalBuild)
}
}
}
internal operator fun get(target: KonanTarget) = targetToTask[target]
fun getByTarget(target: String) = findByTarget(target) ?: throw NoSuchElementException("No such target for artifact $name: ${target}")
fun findByTarget(target: String) = this[project.hostManager.targetByName(target)]
fun getArtifactByTarget(target: String) = getByTarget(target).artifact
fun findArtifactByTarget(target: String) = findByTarget(target)?.artifact
// Common building DSL.
override fun artifactName(name: String) = forEach { it.artifactName(name) }
fun baseDir(dir: Any) = forEach { it.destinationDir(project.file(dir).targetSubdir(it.konanTarget)) }
override fun libraries(closure: Closure<Unit>) = forEach { it.libraries(closure) }
override fun libraries(action: Action<KonanLibrariesSpec>) = forEach { it.libraries(action) }
override fun libraries(configure: KonanLibrariesSpec.() -> Unit) = forEach { it.libraries(configure) }
override fun noDefaultLibs(flag: Boolean) = forEach { it.noDefaultLibs(flag) }
override fun noEndorsedLibs(flag: Boolean) = forEach { it.noEndorsedLibs(flag) }
override fun dumpParameters(flag: Boolean) = forEach { it.dumpParameters(flag) }
override fun extraOpts(vararg values: Any) = forEach { it.extraOpts(*values) }
override fun extraOpts(values: List<Any>) = forEach { it.extraOpts(values) }
fun dependsOn(vararg dependencies: Any?) = forEach { it.dependsOn(*dependencies) }
fun target(targetString: String, configureAction: T.() -> Unit) {
val target = project.hostManager.targetByName(targetString)
if (!project.hostManager.isEnabled(target)) {
project.logger.info("Target '$targetString' of artifact '$name' is not supported on the current host")
return
}
val task = this[target] ?:
throw InvalidUserDataException("Target '$targetString' is not declared. Please add it into project.konanTasks list")
task.configureAction()
}
fun target(targetString: String, configureAction: Action<T>) =
target(targetString) { configureAction.execute(this) }
fun target(targetString: String, configureAction: Closure<Unit>) =
target(targetString, ConfigureUtil.configureUsing(configureAction))
fun pom(action: Action<MavenPom>) = pomActions + action
}
@@ -0,0 +1,162 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.gradle.plugin.konan
import groovy.lang.Closure
import org.gradle.api.Task
import org.gradle.api.file.FileCollection
import org.gradle.api.internal.project.ProjectInternal
import org.jetbrains.kotlin.gradle.plugin.tasks.*
import org.jetbrains.kotlin.konan.target.Family
import org.jetbrains.kotlin.konan.target.KonanTarget
import org.jetbrains.kotlin.konan.target.KonanTarget.WASM32
import java.io.File
abstract class KonanCompileConfig<T: KonanCompileTask>(name: String,
type: Class<T>,
project: ProjectInternal,
targets: Iterable<String>)
: KonanBuildingConfig<T>(name, type, project, targets), KonanCompileSpec {
protected abstract val typeForDescription: String
override fun generateTaskDescription(task: T) =
"Build the Kotlin/Native $typeForDescription '${task.name}' for target '${task.konanTarget}'"
override fun generateAggregateTaskDescription(task: Task) =
"Build the Kotlin/Native $typeForDescription '${task.name}' for all supported and declared targets"
override fun generateTargetAliasTaskDescription(task: Task, targetName: String) =
"Build the Kotlin/Native $typeForDescription '${task.name}' for target '$targetName'"
override fun srcDir(dir: Any) = forEach { it.srcDir(dir) }
override fun srcFiles(vararg files: Any) = forEach { it.srcFiles(*files) }
override fun srcFiles(files: Collection<Any>) = forEach { it.srcFiles(files) }
override fun nativeLibrary(lib: Any) = forEach { it.nativeLibrary(lib) }
override fun nativeLibraries(vararg libs: Any) = forEach { it.nativeLibraries(*libs) }
override fun nativeLibraries(libs: FileCollection) = forEach { it.nativeLibraries(libs) }
@Deprecated("Use commonSourceSets instead", ReplaceWith("commonSourceSets(sourceSetName)"))
override fun commonSourceSet(sourceSetName: String) = forEach { it.commonSourceSets(sourceSetName) }
override fun commonSourceSets(vararg sourceSetNames: String) = forEach { it.commonSourceSets(*sourceSetNames) }
override fun enableMultiplatform(flag: Boolean) = forEach { it.enableMultiplatform(flag) }
override fun linkerOpts(values: List<String>) = forEach { it.linkerOpts(values) }
override fun linkerOpts(vararg values: String) = forEach { it.linkerOpts(*values) }
override fun enableDebug(flag: Boolean) = forEach { it.enableDebug(flag) }
override fun noStdLib(flag: Boolean) = forEach { it.noStdLib(flag) }
override fun noMain(flag: Boolean) = forEach { it.noMain(flag) }
override fun enableOptimizations(flag: Boolean) = forEach { it.enableOptimizations(flag) }
override fun enableAssertions(flag: Boolean) = forEach { it.enableAssertions(flag) }
override fun entryPoint(entryPoint: String) = forEach { it.entryPoint(entryPoint) }
override fun measureTime(flag: Boolean) = forEach { it.measureTime(flag) }
override fun dependencies(closure: Closure<Unit>) = forEach { it.dependencies(closure) }
}
open class KonanProgram(name: String,
project: ProjectInternal,
targets: Iterable<String> = project.konanExtension.targets
) : KonanCompileConfig<KonanCompileProgramTask>(name,
KonanCompileProgramTask::class.java,
project,
targets
) {
override val typeForDescription: String
get() = "executable"
override val defaultBaseDir: File
get() = project.konanBinBaseDir
}
open class KonanDynamic(name: String,
project: ProjectInternal,
targets: Iterable<String> = project.konanExtension.targets)
: KonanCompileConfig<KonanCompileDynamicTask>(name,
KonanCompileDynamicTask::class.java,
project,
targets
) {
override val typeForDescription: String
get() = "dynamic library"
override val defaultBaseDir: File
get() = project.konanBinBaseDir
override fun targetIsSupported(target: KonanTarget): Boolean = target != WASM32
}
open class KonanFramework(name: String,
project: ProjectInternal,
targets: Iterable<String> = project.konanExtension.targets)
: KonanCompileConfig<KonanCompileFrameworkTask>(name,
KonanCompileFrameworkTask::class.java,
project,
targets
) {
override val typeForDescription: String
get() = "framework"
override val defaultBaseDir: File
get() = project.konanBinBaseDir
override fun targetIsSupported(target: KonanTarget): Boolean =
target.family.isAppleFamily
}
open class KonanLibrary(name: String,
project: ProjectInternal,
targets: Iterable<String> = project.konanExtension.targets)
: KonanCompileConfig<KonanCompileLibraryTask>(name,
KonanCompileLibraryTask::class.java,
project,
targets
) {
override val typeForDescription: String
get() = "library"
override val defaultBaseDir: File
get() = project.konanLibsBaseDir
}
open class KonanBitcode(name: String,
project: ProjectInternal,
targets: Iterable<String> = project.konanExtension.targets)
: KonanCompileConfig<KonanCompileBitcodeTask>(name,
KonanCompileBitcodeTask::class.java,
project,
targets
) {
override val typeForDescription: String
get() = "bitcode"
override fun generateTaskDescription(task: KonanCompileBitcodeTask) =
"Generates bitcode for the artifact '${task.name}' and target '${task.konanTarget}'"
override fun generateAggregateTaskDescription(task: Task) =
"Generates bitcode for the artifact '${task.name}' for all supported and declared targets'"
override fun generateTargetAliasTaskDescription(task: Task, targetName: String) =
"Generates bitcode for the artifact '${task.name}' for '$targetName'"
override val defaultBaseDir: File
get() = project.konanBitcodeBaseDir
}
@@ -0,0 +1,86 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.gradle.plugin.konan
import groovy.lang.Closure
import org.gradle.api.Action
import org.gradle.api.Task
import org.gradle.api.file.FileCollection
import org.gradle.api.internal.project.ProjectInternal
import org.gradle.util.ConfigureUtil
import org.jetbrains.kotlin.gradle.plugin.konan.KonanInteropSpec.IncludeDirectoriesSpec
import org.jetbrains.kotlin.gradle.plugin.tasks.KonanInteropTask
import java.io.File
open class KonanInteropLibrary(name: String,
project: ProjectInternal,
targets: Iterable<String> = project.konanExtension.targets
) : KonanBuildingConfig<KonanInteropTask>(name, KonanInteropTask::class.java, project, targets),
KonanInteropSpec
{
override fun generateTaskDescription(task: KonanInteropTask) =
"Build the Kotlin/Native interop library '${task.name}' for target '${task.konanTarget}'"
override fun generateAggregateTaskDescription(task: Task) =
"Build the Kotlin/Native interop library '${task.name}' for all supported and declared targets'"
override fun generateTargetAliasTaskDescription(task: Task, targetName: String) =
"Build the Kotlin/Native interop library '${task.name}' for '$targetName'"
override val defaultBaseDir: File
get() = project.konanLibsBaseDir
// DSL
inner class IncludeDirectoriesSpecImpl: IncludeDirectoriesSpec {
override fun allHeaders(vararg includeDirs: Any) = allHeaders(includeDirs.toList())
override fun allHeaders(includeDirs: Collection<Any>) = forEach {
it.includeDirs.allHeaders(includeDirs)
}
override fun headerFilterOnly(vararg includeDirs: Any) = headerFilterOnly(includeDirs.toList())
override fun headerFilterOnly(includeDirs: Collection<Any>) = forEach {
it.includeDirs.headerFilterOnly(includeDirs)
}
}
val includeDirs = IncludeDirectoriesSpecImpl()
override fun defFile(file: Any) = forEach { it.defFile(file) }
override fun packageName(value: String) = forEach { it.packageName(value) }
override fun compilerOpts(vararg values: String) = forEach { it.compilerOpts(*values) }
override fun headers(vararg files: Any) = forEach { it.headers(*files) }
override fun headers(files: FileCollection) = forEach { it.headers(files) }
override fun includeDirs(vararg values: Any) = forEach { it.includeDirs(*values) }
override fun includeDirs(closure: Closure<Unit>) = includeDirs(ConfigureUtil.configureUsing(closure))
override fun includeDirs(action: Action<IncludeDirectoriesSpec>) = includeDirs { action.execute(this) }
override fun includeDirs(configure: IncludeDirectoriesSpec.() -> Unit) = includeDirs.configure()
override fun linkerOpts(values: List<String>) = forEach { it.linkerOpts(values) }
override fun linkerOpts(vararg values: String) = linkerOpts(values.toList())
override fun link(vararg files: Any) = forEach { it.link(*files) }
override fun link(files: FileCollection) = forEach { it.link(files) }
override fun dependencies(closure: Closure<Unit>) = forEach { it.dependencies(closure) }
}
@@ -0,0 +1,168 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.gradle.plugin.konan
import org.gradle.api.InvalidUserDataException
import org.gradle.api.Project
import org.gradle.api.file.FileCollection
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.Internal
import org.jetbrains.kotlin.gradle.plugin.tasks.KonanArtifactWithLibrariesTask
import org.jetbrains.kotlin.gradle.plugin.tasks.KonanBuildingTask
import org.jetbrains.kotlin.konan.*
import org.jetbrains.kotlin.konan.CompilerVersion
import org.jetbrains.kotlin.konan.library.defaultResolver
import org.jetbrains.kotlin.konan.library.impl.KonanLibraryImpl
import org.jetbrains.kotlin.konan.target.Distribution
import org.jetbrains.kotlin.konan.target.KonanTarget
import org.jetbrains.kotlin.library.SearchPathResolver
import java.io.File
open class KonanLibrariesSpec(
@Internal val task: KonanArtifactWithLibrariesTask,
@Internal val project: Project
) {
@InputFiles val files = mutableSetOf<FileCollection>()
@Input val namedKlibs = mutableSetOf<String>()
@Internal val artifacts = mutableListOf<KonanBuildingTask>()
val artifactFiles: List<File>
@InputFiles get() = artifacts.map { it.artifact }
@Internal val explicitRepos = mutableSetOf<File>()
val repos: Set<File>
@Input get() = mutableSetOf<File>().apply {
addAll(explicitRepos)
add(task.destinationDir) // TODO: Check if task is a library - create a Library interface
add(task.destinationDir) // TODO: Check if task is a library - create a Library interface
add(task.project.konanLibsBaseDir.targetSubdir(target))
addAll(artifacts.flatMap { it.libraries.repos })
addAll(task.platformConfiguration.files.map { it.parentFile })
}
val target: KonanTarget
@Internal get() = task.konanTarget
private val friendsTasks = mutableSetOf<KonanBuildingTask>()
@get:Internal // Taken into account by tasks's dependOn.
val friends: Set<File> get() = mutableSetOf<File>().apply {
addAll(friendsTasks.map { it.artifact })
}
// DSL Methods
/** Absolute path */
fun file(file: Any) = files.add(project.files(file))
fun files(vararg files: Any) = this.files.addAll(files.map { project.files(it) })
fun files(collection: FileCollection) = this.files.add(collection)
/** The compiler with search the library in repos */
fun klib(lib: String) = namedKlibs.add(lib)
fun klibs(vararg libs: String) = namedKlibs.addAll(libs)
fun klibs(libs: Iterable<String>) = namedKlibs.addAll(libs)
private fun klibInternal(lib: KonanBuildingConfig<*>, friend: Boolean) {
if (!(lib is KonanLibrary || lib is KonanInteropLibrary)) {
throw InvalidUserDataException("Config ${lib.name} is not a library")
}
val libraryTask = lib[target] ?:
throw InvalidUserDataException("Library ${lib.name} has no target ${target.visibleName}")
if (libraryTask == task) {
throw InvalidUserDataException("Attempt to use a library as its own dependency: " +
"${task.name} (in project: ${project.path})")
}
artifacts.add(libraryTask)
task.dependsOn(libraryTask)
if (friend) friendsTasks.add(libraryTask)
}
/** Direct link to a config */
fun klib(lib: KonanLibrary) = klibInternal(lib, false)
/** Direct link to a config */
fun klib(lib: KonanInteropLibrary) = klibInternal(lib, false)
/** Artifact in the specified project by name */
fun artifact(libraryProject: Project, name: String, friend: Boolean) {
project.evaluationDependsOn(libraryProject)
klibInternal(libraryProject.konanArtifactsContainer.getByName(name), friend)
}
fun artifact(libraryProject: Project, name: String) = artifact(libraryProject, name, false)
/** Artifact in the current project by name */
fun artifact(name: String, friend: Boolean) = artifact(project, name, friend)
fun artifact(name: String) = artifact(project, name, false)
/** Artifact by direct link */
fun artifact(artifact: KonanLibrary) = klib(artifact)
/** Direct link to a config */
fun artifact(artifact: KonanInteropLibrary) = klib(artifact)
private fun allArtifactsFromInternal(libraryProjects: Array<out Project>,
filter: (KonanBuildingConfig<*>) -> Boolean) {
libraryProjects.forEach { prj ->
project.evaluationDependsOn(prj)
prj.konanArtifactsContainer.filter(filter).forEach {
klibInternal(it, false)
}
}
}
/** All libraries (both interop and non-interop ones) from the projects by direct references */
fun allLibrariesFrom(vararg libraryProjects: Project) = allArtifactsFromInternal(libraryProjects) {
it is KonanLibrary || it is KonanInteropLibrary
}
/** All interop libraries from the projects by direct references */
fun allInteropLibrariesFrom(vararg libraryProjects: Project) = allArtifactsFromInternal(libraryProjects) {
it is KonanInteropLibrary
}
/** Add repo for library search */
fun useRepo(directory: Any) = explicitRepos.add(project.file(directory))
/** Add repos for library search */
fun useRepos(vararg directories: Any) = directories.forEach { useRepo(it) }
/** Add repos for library search */
fun useRepos(directories: Iterable<Any>) = directories.forEach { useRepo(it) }
private fun Project.evaluationDependsOn(another: Project) {
if (this != another) { evaluationDependsOn(another.path) }
}
fun asFiles(): List<File> = asFiles(
defaultResolver(
repos.map { it.absolutePath },
task.konanTarget,
Distribution(project.konanHome)
)
)
fun asFiles(resolver: SearchPathResolver<*>): List<File> = mutableListOf<File>().apply {
files.flatMapTo(this) { it.files }
addAll(artifactFiles)
addAll(task.platformConfiguration.files)
namedKlibs.mapTo(this) { project.file(resolver.resolve(it).libraryFile.absolutePath) }
}
}
@@ -0,0 +1,418 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.gradle.plugin.konan
import org.gradle.api.*
import org.gradle.api.component.ComponentWithVariants
import org.gradle.api.component.SoftwareComponent
import org.gradle.api.file.FileCollection
import org.gradle.api.internal.FeaturePreviews
import org.gradle.api.internal.component.SoftwareComponentInternal
import org.gradle.api.internal.component.UsageContext
import org.gradle.api.internal.project.ProjectInternal
import org.gradle.api.plugins.BasePlugin
import org.gradle.api.publish.PublishingExtension
import org.gradle.api.publish.maven.MavenPublication
import org.gradle.api.publish.maven.internal.publication.MavenPublicationInternal
import org.gradle.language.cpp.internal.NativeVariantIdentity
import org.gradle.tooling.provider.model.ToolingModelBuilderRegistry
import org.gradle.util.GradleVersion
import org.jetbrains.kotlin.gradle.plugin.konan.KonanPlugin.Companion.COMPILE_ALL_TASK_NAME
import org.jetbrains.kotlin.gradle.plugin.model.KonanToolingModelBuilder
import org.jetbrains.kotlin.gradle.plugin.tasks.*
import org.jetbrains.kotlin.konan.CURRENT
import org.jetbrains.kotlin.konan.CompilerVersion
import org.jetbrains.kotlin.konan.parseCompilerVersion
import org.jetbrains.kotlin.konan.target.HostManager
import org.jetbrains.kotlin.konan.target.KonanTarget
import org.jetbrains.kotlin.konan.target.buildDistribution
import org.jetbrains.kotlin.konan.target.customerDistribution
import org.jetbrains.kotlin.konan.util.DependencyProcessor
import java.io.File
import javax.inject.Inject
/**
* We use the following properties:
* org.jetbrains.kotlin.native.home - directory where compiler is located (aka dist in konan project output).
* org.jetbrains.kotlin.native.version - a konan compiler version for downloading.
* konan.build.targets - list of targets to build (by default all the declared targets are built).
* konan.jvmArgs - additional args to be passed to a JVM executing the compiler/cinterop tool.
*/
internal fun Project.warnAboutDeprecatedProperty(property: KonanPlugin.ProjectProperty) =
property.deprecatedPropertyName?.let { deprecated ->
if (project.hasProperty(deprecated)) {
logger.warn("Project property '$deprecated' is deprecated. Use '${property.propertyName}' instead.")
}
}
internal fun Project.hasProperty(property: KonanPlugin.ProjectProperty) = with(property) {
when {
hasProperty(propertyName) -> true
deprecatedPropertyName != null && hasProperty(deprecatedPropertyName) -> true
else -> false
}
}
internal fun Project.findProperty(property: KonanPlugin.ProjectProperty): Any? = with(property) {
return findProperty(propertyName) ?: deprecatedPropertyName?.let { findProperty(it) }
}
internal fun Project.getProperty(property: KonanPlugin.ProjectProperty) = findProperty(property)
?: throw IllegalArgumentException("No such property in the project: ${property.propertyName}")
internal fun Project.getProperty(property: KonanPlugin.ProjectProperty, defaultValue: Any) =
findProperty(property) ?: defaultValue
internal fun Project.setProperty(property: KonanPlugin.ProjectProperty, value: Any) {
extensions.extraProperties.set(property.propertyName, value)
}
// konanHome extension is set by downloadKonanCompiler task.
internal val Project.konanHome: String
get() {
assert(hasProperty(KonanPlugin.ProjectProperty.KONAN_HOME))
return project.file(getProperty(KonanPlugin.ProjectProperty.KONAN_HOME)).canonicalPath
}
internal val Project.konanVersion: CompilerVersion
get() = project.findProperty(KonanPlugin.ProjectProperty.KONAN_VERSION)
?.toString()?.let { CompilerVersion.fromString(it) }
?: CompilerVersion.CURRENT
internal val Project.konanBuildRoot get() = buildDir.resolve("konan")
internal val Project.konanBinBaseDir get() = konanBuildRoot.resolve("bin")
internal val Project.konanLibsBaseDir get() = konanBuildRoot.resolve("libs")
internal val Project.konanBitcodeBaseDir get() = konanBuildRoot.resolve("bitcode")
internal fun File.targetSubdir(target: KonanTarget) = resolve(target.visibleName)
internal val Project.konanDefaultSrcFiles get() = fileTree("${projectDir.canonicalPath}/src/main/kotlin")
internal fun Project.konanDefaultDefFile(libName: String)
= file("${projectDir.canonicalPath}/src/main/c_interop/$libName.def")
@Suppress("UNCHECKED_CAST")
internal val Project.konanArtifactsContainer: KonanArtifactContainer
get() = extensions.getByName(KonanPlugin.ARTIFACTS_CONTAINER_NAME) as KonanArtifactContainer
// TODO: The Kotlin/Native compiler is downloaded manually by a special task so the compilation tasks
// are configured without the compile distribution. After target management refactoring
// we need .properties files from the distribution to configure targets. This is worked around here
// by using HostManager instead of PlatformManager. But we need to download the compiler at the configuration
// stage (e.g. by getting it from maven as a plugin dependency) and bring back the PlatformManager here.
internal val Project.hostManager: HostManager
get() = findProperty("hostManager") as HostManager? ?:
if (hasProperty("org.jetbrains.kotlin.native.experimentalTargets"))
HostManager(buildDistribution(rootProject.rootDir.absolutePath), true)
else
HostManager(customerDistribution(konanHome))
internal val Project.konanTargets: List<KonanTarget>
get() = hostManager.toKonanTargets(konanExtension.targets)
.filter{ hostManager.isEnabled(it) }
.distinct()
@Suppress("UNCHECKED_CAST")
internal val Project.konanExtension: KonanExtension
get() = extensions.getByName(KonanPlugin.KONAN_EXTENSION_NAME) as KonanExtension
internal val Project.konanCompilerDownloadTask
get() = tasks.getByName(KonanPlugin.KONAN_DOWNLOAD_TASK_NAME)
internal val Project.requestedTargets
get() = findProperty(KonanPlugin.ProjectProperty.KONAN_BUILD_TARGETS)?.let {
it.toString().trim().split("\\s+".toRegex())
}.orEmpty()
internal val Project.jvmArgs
get() = (findProperty(KonanPlugin.ProjectProperty.KONAN_JVM_ARGS) as String?)?.split("\\s+".toRegex()).orEmpty()
internal val Project.compileAllTask
get() = getOrCreateTask(COMPILE_ALL_TASK_NAME)
internal fun Project.targetIsRequested(target: KonanTarget): Boolean {
val targets = requestedTargets
return (targets.isEmpty() || targets.contains(target.visibleName) || targets.contains("all"))
}
/** Looks for task with given name in the given project. Throws [UnknownTaskException] if there's not such task. */
private fun Project.getTask(name: String): Task = tasks.getByPath(name)
/**
* Looks for task with given name in the given project.
* If such task isn't found, will create it. Returns created/found task.
*/
private fun Project.getOrCreateTask(name: String): Task = with(tasks) {
findByPath(name) ?: create(name, DefaultTask::class.java)
}
internal fun Project.konanCompilerName(): String =
"kotlin-native-${project.simpleOsName}-${project.konanVersion}"
internal fun Project.konanCompilerDownloadDir(): String =
DependencyProcessor.localKonanDir.resolve(project.konanCompilerName()).absolutePath
// region Useful extensions and functions ---------------------------------------
internal fun MutableList<String>.addArg(parameter: String, value: String) {
add(parameter)
add(value)
}
internal fun MutableList<String>.addArgs(parameter: String, values: Iterable<String>) {
values.forEach {
addArg(parameter, it)
}
}
internal fun MutableList<String>.addArgIfNotNull(parameter: String, value: String?) {
if (value != null) {
addArg(parameter, value)
}
}
internal fun MutableList<String>.addKey(key: String, enabled: Boolean) {
if (enabled) {
add(key)
}
}
internal fun MutableList<String>.addFileArgs(parameter: String, values: FileCollection) {
values.files.forEach {
addArg(parameter, it.canonicalPath)
}
}
internal fun MutableList<String>.addFileArgs(parameter: String, values: Collection<FileCollection>) {
values.forEach {
addFileArgs(parameter, it)
}
}
// endregion
internal fun dumpProperties(task: Task) {
fun Iterable<String>.dump() = joinToString(prefix = "[", separator = ",\n${" ".repeat(22)}", postfix = "]")
fun Collection<FileCollection>.dump() = flatMap { it.files }.map { it.canonicalPath }.dump()
when (task) {
is KonanCompileTask -> with(task) {
println()
println("Compilation task: $name")
println("destinationDir : $destinationDir")
println("artifact : ${artifact.canonicalPath}")
println("srcFiles : ${srcFiles.dump()}")
println("produce : $produce")
println("libraries : ${libraries.files.dump()}")
println(" : ${libraries.artifacts.map {
it.artifact.canonicalPath
}.dump()}")
println(" : ${libraries.namedKlibs.dump()}")
println("nativeLibraries : ${nativeLibraries.dump()}")
println("linkerOpts : $linkerOpts")
println("enableDebug : $enableDebug")
println("noStdLib : $noStdLib")
println("noMain : $noMain")
println("enableOptimization : $enableOptimizations")
println("enableAssertions : $enableAssertions")
println("noDefaultLibs : $noDefaultLibs")
println("noEndorsedLibs : $noEndorsedLibs")
println("target : $target")
println("languageVersion : $languageVersion")
println("apiVersion : $apiVersion")
println("konanVersion : ${CompilerVersion.CURRENT}")
println("konanHome : $konanHome")
println()
}
is KonanInteropTask -> with(task) {
println()
println("Stub generation task: $name")
println("destinationDir : $destinationDir")
println("artifact : $artifact")
println("libraries : ${libraries.files.dump()}")
println(" : ${libraries.artifacts.map {
it.artifact.canonicalPath
}.dump()}")
println(" : ${libraries.namedKlibs.dump()}")
println("defFile : $defFile")
println("target : $target")
println("packageName : $packageName")
println("compilerOpts : $compilerOpts")
println("linkerOpts : $linkerOpts")
println("headers : ${headers.dump()}")
println("linkFiles : ${linkFiles.dump()}")
println("konanVersion : ${CompilerVersion.CURRENT}")
println("konanHome : $konanHome")
println()
}
else -> {
println("Unsupported task.")
}
}
}
open class KonanExtension {
var targets = mutableListOf("host")
var languageVersion: String? = null
var apiVersion: String? = null
var jvmArgs = mutableListOf<String>()
}
open class KonanSoftwareComponent(val project: ProjectInternal?): SoftwareComponentInternal, ComponentWithVariants {
private val usages = mutableSetOf<UsageContext>()
override fun getUsages(): MutableSet<out UsageContext> = usages
private val variants = mutableSetOf<SoftwareComponent>()
override fun getName() = "main"
override fun getVariants(): Set<SoftwareComponent> = variants
fun addVariant(component: SoftwareComponent) = variants.add(component)
}
class KonanPlugin @Inject constructor(private val registry: ToolingModelBuilderRegistry)
: Plugin<ProjectInternal> {
enum class ProjectProperty(val propertyName: String, val deprecatedPropertyName: String? = null) {
KONAN_HOME ("org.jetbrains.kotlin.native.home", "konan.home"),
KONAN_VERSION ("org.jetbrains.kotlin.native.version"),
KONAN_BUILD_TARGETS ("konan.build.targets"),
KONAN_JVM_ARGS ("konan.jvmArgs"),
KONAN_USE_ENVIRONMENT_VARIABLES("konan.useEnvironmentVariables"),
DOWNLOAD_COMPILER ("download.compiler"),
// Properties used instead of env vars until https://github.com/gradle/gradle/issues/3468 is fixed.
// TODO: Remove them when an API for env vars is provided.
KONAN_CONFIGURATION_BUILD_DIR ("konan.configuration.build.dir"),
KONAN_DEBUGGING_SYMBOLS ("konan.debugging.symbols"),
KONAN_OPTIMIZATIONS_ENABLE ("konan.optimizations.enable"),
}
companion object {
internal const val ARTIFACTS_CONTAINER_NAME = "konanArtifacts"
internal const val KONAN_DOWNLOAD_TASK_NAME = "checkKonanCompiler"
internal const val KONAN_GENERATE_CMAKE_TASK_NAME = "generateCMake"
internal const val COMPILE_ALL_TASK_NAME = "compileKonan"
internal const val KONAN_EXTENSION_NAME = "konan"
internal val REQUIRED_GRADLE_VERSION = GradleVersion.version("4.7")
}
private fun Project.cleanKonan() = project.tasks.withType(KonanBuildingTask::class.java).forEach {
project.delete(it.artifact)
}
private fun checkGradleVersion() = GradleVersion.current().let { current ->
check(current >= REQUIRED_GRADLE_VERSION) {
"Kotlin/Native Gradle plugin is incompatible with this version of Gradle.\n" +
"The minimal required version is $REQUIRED_GRADLE_VERSION\n" +
"Current version is ${current}"
}
}
override fun apply(project: ProjectInternal?) {
if (project == null) {
return
}
checkGradleVersion()
registry.register(KonanToolingModelBuilder)
project.plugins.apply("base")
// Create necessary tasks and extensions.
project.tasks.create(KONAN_DOWNLOAD_TASK_NAME, KonanCompilerDownloadTask::class.java)
project.tasks.create(KONAN_GENERATE_CMAKE_TASK_NAME, KonanGenerateCMakeTask::class.java)
project.extensions.create(KONAN_EXTENSION_NAME, KonanExtension::class.java)
val container = project.extensions.create(
KonanArtifactContainer::class.java,
ARTIFACTS_CONTAINER_NAME,
KonanArtifactContainer::class.java,
project
)
project.warnAboutDeprecatedProperty(ProjectProperty.KONAN_HOME)
// Set additional project properties like org.jetbrains.kotlin.native.home, konan.build.targets etc.
if (!project.hasProperty(ProjectProperty.KONAN_HOME)) {
project.setProperty(ProjectProperty.KONAN_HOME, project.konanCompilerDownloadDir())
project.setProperty(ProjectProperty.DOWNLOAD_COMPILER, true)
}
// Create and set up aggregate building tasks.
val compileKonanTask = project.getOrCreateTask(COMPILE_ALL_TASK_NAME).apply {
group = BasePlugin.BUILD_GROUP
description = "Compiles all the Kotlin/Native artifacts"
}
project.getTask("build").apply {
dependsOn(compileKonanTask)
}
project.getTask("clean").apply {
doLast { project.cleanKonan() }
}
val runTask = project.getOrCreateTask("run")
project.afterEvaluate {
project.konanArtifactsContainer
.filterIsInstance(KonanProgram::class.java)
.forEach { program ->
program.forEach { compile ->
compile.runTask?.let { runTask.dependsOn(it) }
}
}
}
// Enable multiplatform support
project.pluginManager.apply(KotlinNativePlatformPlugin::class.java)
project.afterEvaluate {
project.pluginManager.withPlugin("maven-publish") {
container.all { buildingConfig ->
val konanSoftwareComponent = buildingConfig.mainVariant
project.extensions.configure(PublishingExtension::class.java) {
val builtArtifact = buildingConfig.name
val mavenPublication = it.publications.maybeCreate(builtArtifact, MavenPublication::class.java)
mavenPublication.apply {
artifactId = builtArtifact
groupId = project.group.toString()
from(konanSoftwareComponent)
}
(mavenPublication as MavenPublicationInternal).publishWithOriginalFileName()
buildingConfig.pomActions.forEach {
mavenPublication.pom(it)
}
}
project.extensions.configure(PublishingExtension::class.java) {
val publishing = it
for (v in konanSoftwareComponent.variants) {
publishing.publications.create(v.name, MavenPublication::class.java) { mavenPublication ->
val coordinates = (v as NativeVariantIdentity).coordinates
project.logger.info("variant with coordinates($coordinates) and module: ${coordinates.module}")
mavenPublication.artifactId = coordinates.module.name
mavenPublication.groupId = coordinates.group
mavenPublication.version = coordinates.version
mavenPublication.from(v)
(mavenPublication as MavenPublicationInternal).publishWithOriginalFileName()
buildingConfig.pomActions.forEach {
mavenPublication.pom(it)
}
}
}
}
}
}
}
}
}
@@ -0,0 +1,111 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.gradle.plugin.konan
import groovy.lang.Closure
import org.gradle.api.Action
import org.gradle.api.file.FileCollection
interface KonanArtifactSpec {
fun artifactName(name: String)
}
interface KonanArtifactWithLibrariesSpec: KonanArtifactSpec {
fun libraries(closure: Closure<Unit>)
fun libraries(action: Action<KonanLibrariesSpec>)
fun libraries(configure: KonanLibrariesSpec.() -> Unit)
fun noDefaultLibs(flag: Boolean)
fun noEndorsedLibs(flag: Boolean)
fun dependencies(closure: Closure<Unit>)
}
interface KonanBuildingSpec: KonanArtifactWithLibrariesSpec {
fun dumpParameters(flag: Boolean)
fun extraOpts(vararg values: Any)
fun extraOpts(values: List<Any>)
}
interface KonanCompileSpec: KonanBuildingSpec {
fun srcDir(dir: Any)
fun srcFiles(vararg files: Any)
fun srcFiles(files: Collection<Any>)
// DSL. Native libraries.
fun nativeLibrary(lib: Any)
fun nativeLibraries(vararg libs: Any)
fun nativeLibraries(libs: FileCollection)
// DSL. Multiplatform projects
fun enableMultiplatform(flag: Boolean)
// TODO: Get rid of commonSourceSet in 0.7
@Deprecated("Use commonSourceSets instead", ReplaceWith("commonSourceSets(sourceSetName)"))
fun commonSourceSet(sourceSetName: String)
fun commonSourceSets(vararg sourceSetNames: String)
// DSL. Other parameters.
fun linkerOpts(vararg values: String)
fun linkerOpts(values: List<String>)
fun enableDebug(flag: Boolean)
fun noStdLib(flag: Boolean)
fun noMain(flag: Boolean)
fun enableOptimizations(flag: Boolean)
fun enableAssertions(flag: Boolean)
fun entryPoint(entryPoint: String)
fun measureTime(flag: Boolean)
}
interface KonanInteropSpec: KonanBuildingSpec {
interface IncludeDirectoriesSpec {
fun allHeaders(vararg includeDirs: Any)
fun allHeaders(includeDirs: Collection<Any>)
fun headerFilterOnly(vararg includeDirs: Any)
fun headerFilterOnly(includeDirs: Collection<Any>)
}
fun defFile(file: Any)
fun packageName(value: String)
fun compilerOpts(vararg values: String)
fun header(file: Any) = headers(file)
fun headers(vararg files: Any)
fun headers(files: FileCollection)
fun includeDirs(vararg values: Any)
fun includeDirs(closure: Closure<Unit>)
fun includeDirs(action: Action<IncludeDirectoriesSpec>)
fun includeDirs(configure: IncludeDirectoriesSpec.() -> Unit)
fun linkerOpts(vararg values: String)
fun linkerOpts(values: List<String>)
fun link(vararg files: Any)
fun link(files: FileCollection)
}
@@ -0,0 +1,155 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.gradle.plugin.konan
import org.gradle.api.Named
import org.gradle.api.Project
import org.gradle.api.file.FileCollection
import org.jetbrains.kotlin.gradle.plugin.konan.KonanPlugin.ProjectProperty.KONAN_HOME
import org.jetbrains.kotlin.konan.target.Family
import org.jetbrains.kotlin.konan.target.HostManager
import org.jetbrains.kotlin.konan.target.KonanTarget
import org.jetbrains.kotlin.konan.util.DependencyProcessor
internal interface KonanToolRunner: Named {
val mainClass: String
val classpath: FileCollection
val jvmArgs: List<String>
val environment: Map<String, Any>
fun run(args: List<String>)
fun run(vararg args: String) = run(args.toList())
}
internal abstract class KonanCliRunner(
val toolName: String,
val fullName: String,
val project: Project,
private val additionalJvmArgs: List<String>,
private val konanHome: String
): KonanToolRunner {
override val mainClass = "org.jetbrains.kotlin.cli.utilities.MainKt"
override fun getName() = toolName
// We need to unset some environment variables which are set by XCode and may potentially affect the tool executed.
protected val blacklistEnvironment: List<String> by lazy {
KonanPlugin::class.java.getResourceAsStream("/env_blacklist")?.let { stream ->
stream.reader().use { it.readLines() }
} ?: emptyList<String>()
}
protected val blacklistProperties: Set<String> =
setOf("java.endorsed.dirs")
override val classpath: FileCollection =
project.fileTree("$konanHome/konan/lib/")
.apply { include("*.jar") }
override val jvmArgs = HostManager.defaultJvmArgs.toMutableList().apply {
if (additionalJvmArgs.none { it.startsWith("-Xmx") } &&
project.jvmArgs.none { it.startsWith("-Xmx") }) {
add("-Xmx3G")
}
addAll(additionalJvmArgs)
addAll(project.jvmArgs)
}
override val environment = mutableMapOf("LIBCLANG_DISABLE_CRASH_RECOVERY" to "1")
private fun String.escapeQuotes() = replace("\"", "\\\"")
private fun Sequence<Pair<String, String>>.escapeQuotesForWindows() =
if (HostManager.hostIsMingw) {
map { (key, value) -> key.escapeQuotes() to value.escapeQuotes() }
} else {
this
}
open protected fun transformArgs(args: List<String>): List<String> = args
override fun run(args: List<String>) {
project.logger.info("Run tool: $toolName with args: ${args.joinToString(separator = " ")}")
if (classpath.isEmpty) {
throw IllegalStateException("Classpath of the tool is empty: $toolName\n" +
"Probably the '${KONAN_HOME.propertyName}' project property contains an incorrect path.\n" +
"Please change it to the compiler root directory and rerun the build.")
}
project.javaexec { spec ->
spec.main = mainClass
spec.classpath = classpath
spec.jvmArgs(jvmArgs)
spec.systemProperties(
System.getProperties().asSequence()
.map { (k, v) -> k.toString() to v.toString() }
.filter { (k, _) -> k !in blacklistProperties }
.escapeQuotesForWindows()
.toMap()
)
spec.args(listOf(toolName) + transformArgs(args))
blacklistEnvironment.forEach { spec.environment.remove(it) }
spec.environment(environment)
}
}
}
internal class KonanInteropRunner(
project: Project,
additionalJvmArgs: List<String> = emptyList(),
konanHome: String = project.konanHome
) : KonanCliRunner("cinterop", "Kotlin/Native cinterop tool", project, additionalJvmArgs, konanHome) {
init {
if (HostManager.host == KonanTarget.MINGW_X64) {
//TODO: Oh-ho-ho fix it in more convinient way.
environment.put("PATH", DependencyProcessor.defaultDependenciesRoot.absolutePath +
"\\msys2-mingw-w64-x86_64-clang-llvm-lld-compiler_rt-8.0.1" +
"\\bin;${environment.get("PATH")}")
}
}
}
internal class KonanCompilerRunner(
project: Project,
additionalJvmArgs: List<String> = emptyList(),
val useArgFile: Boolean = true,
konanHome: String = project.konanHome
) : KonanCliRunner("konanc", "Kotlin/Native compiler", project, additionalJvmArgs, konanHome)
{
override fun transformArgs(args: List<String>): List<String> {
if (!useArgFile) {
return args
}
val argFile = createTempFile(prefix = "konancArgs", suffix = ".lst").apply {
deleteOnExit()
}
argFile.printWriter().use { writer ->
args.forEach {
writer.println(it)
}
}
return listOf("@${argFile.absolutePath}")
}
}
internal class KonanKlibRunner(
project: Project,
additionalJvmArgs: List<String> = emptyList(),
konanHome: String = project.konanHome
) : KonanCliRunner("klib", "Klib management tool", project, additionalJvmArgs, konanHome)
@@ -0,0 +1,40 @@
package org.jetbrains.kotlin.gradle.plugin.konan
import org.gradle.api.Named
import org.gradle.api.NamedDomainObjectContainer
import org.gradle.api.Project
import org.gradle.api.artifacts.Configuration
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformImplementationPluginBase
import org.jetbrains.kotlin.gradle.plugin.tasks.KonanCompileTask
import javax.inject.Inject
open class KotlinNativePlatformPlugin: KotlinPlatformImplementationPluginBase("native") {
private val Project.konanMultiplatformTasks: Collection<KonanCompileTask>
get() = tasks.withType(KonanCompileTask::class.java).filter { it.enableMultiplatform }
override fun configurationsForCommonModuleDependency(project: Project) = emptyList<Configuration>()
open class RequestedCommonSourceSet @Inject constructor(private val name: String): Named {
override fun getName() = name
}
override fun addCommonSourceSetToPlatformSourceSet(commonSourceSet: Named, platformProject: Project) {
val commonSourceSetName = commonSourceSet.name
platformProject.konanMultiplatformTasks
.filter { it.commonSourceSets.contains(commonSourceSetName) }
.forEach { task: KonanCompileTask ->
getKotlinSourceDirectorySetSafe(commonSourceSet)!!.srcDirs.forEach {
task.commonSrcDir(it)
}
}
}
override fun namedSourceSetsContainer(project: Project): NamedDomainObjectContainer<*> =
project.container(RequestedCommonSourceSet::class.java).apply {
project.konanMultiplatformTasks.forEach { task ->
task.commonSourceSets.forEach { maybeCreate(it) }
}
}
}
@@ -0,0 +1,189 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.gradle.plugin.tasks
import groovy.lang.Closure
import org.gradle.api.Action
import org.gradle.api.DefaultTask
import org.gradle.api.Project
import org.gradle.api.artifacts.*
import org.gradle.api.attributes.Attribute
import org.gradle.api.attributes.AttributeContainer
import org.gradle.api.attributes.Usage
import org.gradle.api.capabilities.Capability
import org.gradle.api.internal.component.UsageContext
import org.gradle.api.internal.tasks.DefaultTaskDependency
import org.gradle.api.tasks.*
import org.gradle.language.cpp.CppBinary
import org.gradle.language.cpp.internal.DefaultUsageContext
import org.gradle.nativeplatform.Linkage
import org.gradle.util.ConfigureUtil
import org.jetbrains.kotlin.gradle.plugin.experimental.internal.compatibleVariantIdentity
import org.jetbrains.kotlin.gradle.plugin.konan.*
import org.jetbrains.kotlin.konan.target.HostManager
import org.jetbrains.kotlin.konan.target.KonanTarget
import java.io.File
import java.util.*
internal val Project.host
get() = HostManager.host.visibleName
internal val Project.simpleOsName
get() = HostManager.simpleOsName()
/** A task with a KonanTarget specified. */
abstract class KonanTargetableTask: DefaultTask() {
@get:Input
val konanTargetName: String
get() = konanTarget.name
@get:Internal
internal lateinit var konanTarget: KonanTarget
internal open fun init(target: KonanTarget) {
this.konanTarget = target
}
val isCrossCompile: Boolean
@Internal get() = (konanTarget != HostManager.host)
val target: String
@Internal get() = konanTarget.visibleName
}
/** A task building an artifact. */
abstract class KonanArtifactTask: KonanTargetableTask(), KonanArtifactSpec {
open val artifact: File
@OutputFile get() = destinationDir.resolve(artifactFullName)
@Internal lateinit var destinationDir: File
@Internal lateinit var artifactName: String
@Internal lateinit var platformConfiguration: Configuration
@Internal lateinit var configuration: Configuration
protected val artifactFullName: String
@Internal get() = "$artifactPrefix$artifactName$artifactSuffix"
val artifactPath: String
@Internal get() = artifact.canonicalPath
protected abstract val artifactSuffix: String
@Internal get
protected abstract val artifactPrefix: String
@Internal get
internal open fun init(config: KonanBuildingConfig<*>, destinationDir: File, artifactName: String, target: KonanTarget) {
super.init(target)
this.destinationDir = destinationDir
this.artifactName = artifactName
configuration = project.configurations.maybeCreate("artifact$artifactName")
platformConfiguration = project.configurations.create("artifact${artifactName}_${target.name}")
platformConfiguration.extendsFrom(configuration)
platformConfiguration.attributes{
it.attribute(Usage.USAGE_ATTRIBUTE, project.objects.named(Usage::class.java, Usage.NATIVE_LINK))
it.attribute(CppBinary.LINKAGE_ATTRIBUTE, Linkage.STATIC)
it.attribute(CppBinary.OPTIMIZED_ATTRIBUTE, false)
it.attribute(CppBinary.DEBUGGABLE_ATTRIBUTE, false)
it.attribute(Attribute.of("org.gradle.native.kotlin.platform", String::class.java), target.name)
}
val artifactNameWithoutSuffix = artifact.name.removeSuffix("$artifactSuffix")
project.pluginManager.withPlugin("maven-publish") {
platformConfiguration.artifacts.add(object: PublishArtifact {
override fun getName(): String = artifactNameWithoutSuffix
override fun getExtension() = if (artifactSuffix.startsWith('.')) artifactSuffix.substring(1) else artifactSuffix
override fun getType() = artifactSuffix
override fun getClassifier():String? = target.name
override fun getFile() = artifact
override fun getDate() = Date(artifact.lastModified())
override fun getBuildDependencies(): TaskDependency =
DefaultTaskDependency().apply { add(this@KonanArtifactTask) }
})
val objectFactory = project.objects
val linkUsage = objectFactory.named(Usage::class.java, Usage.NATIVE_LINK)
val konanSoftwareComponent = config.mainVariant
val variantName = "${artifactNameWithoutSuffix}_${target.name}"
val context = DefaultUsageContext(object:UsageContext {
override fun getUsage(): Usage = linkUsage
override fun getName(): String = "${variantName}Link"
override fun getCapabilities(): MutableSet<out Capability> = mutableSetOf()
override fun getDependencies(): MutableSet<out ModuleDependency> = mutableSetOf()
override fun getDependencyConstraints(): MutableSet<out DependencyConstraint> = mutableSetOf()
override fun getArtifacts(): MutableSet<out PublishArtifact> = platformConfiguration.allArtifacts
override fun getAttributes(): AttributeContainer = platformConfiguration.attributes
override fun getGlobalExcludes(): Set<ExcludeRule> = emptySet()
}, platformConfiguration.allArtifacts, platformConfiguration)
konanSoftwareComponent.addVariant(
compatibleVariantIdentity(
project,
variantName,
project.provider{ artifactName },
project.provider{ project.group.toString() },
project.provider{ project.version.toString() },
false,
false,
target,
context,
null
)
)
}
}
fun dependencies(closure: Closure<Unit>) {
if (konanTarget in project.konanTargets)
project.dependencies(closure)
}
// DSL.
override fun artifactName(name: String) {
artifactName = name
}
fun destinationDir(dir: Any) {
destinationDir = project.file(dir)
}
}
/** Task building an artifact with libraries */
abstract class KonanArtifactWithLibrariesTask: KonanArtifactTask(), KonanArtifactWithLibrariesSpec {
@Nested
val libraries = KonanLibrariesSpec(this, project)
@Input
var noDefaultLibs = false
@Input
var noEndorsedLibs = false
// DSL
override fun libraries(closure: Closure<Unit>) = libraries(ConfigureUtil.configureUsing(closure))
override fun libraries(action: Action<KonanLibrariesSpec>) = libraries { action.execute(this) }
override fun libraries(configure: KonanLibrariesSpec.() -> Unit) { libraries.configure() }
override fun noDefaultLibs(flag: Boolean) {
noDefaultLibs = flag
}
override fun noEndorsedLibs(flag: Boolean) {
noEndorsedLibs = flag
}
}
@@ -0,0 +1,65 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.gradle.plugin.tasks
import org.gradle.api.tasks.Console
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.TaskAction
import org.jetbrains.kotlin.gradle.plugin.konan.*
import org.jetbrains.kotlin.gradle.plugin.model.KonanModelArtifact
import org.jetbrains.kotlin.konan.target.KonanTarget
import java.io.File
/** Base class for both interop and compiler tasks. */
abstract class KonanBuildingTask: KonanArtifactWithLibrariesTask(), KonanBuildingSpec {
@get:Internal
internal abstract val toolRunner: KonanToolRunner
internal abstract fun toModelArtifact(): KonanModelArtifact
override fun init(config: KonanBuildingConfig<*>, destinationDir: File, artifactName: String, target: KonanTarget) {
dependsOn(project.konanCompilerDownloadTask)
super.init(config, destinationDir, artifactName, target)
}
@Console
var dumpParameters: Boolean = false
@Input
val extraOpts = mutableListOf<String>()
val konanHome
@Input get() = project.konanHome
val konanVersion
@Input get() = project.konanVersion.toString(true, true)
@TaskAction
abstract fun run()
// DSL.
override fun dumpParameters(flag: Boolean) {
dumpParameters = flag
}
override fun extraOpts(vararg values: Any) = extraOpts(values.toList())
override fun extraOpts(values: List<Any>) {
extraOpts.addAll(values.map { it.toString() })
}
}
@@ -0,0 +1,69 @@
package org.jetbrains.kotlin.gradle.plugin.konan.tasks
import org.gradle.api.DefaultTask
import org.gradle.api.provider.Property
import org.gradle.api.tasks.*
import org.jetbrains.kotlin.gradle.plugin.konan.KonanCompilerRunner
import org.jetbrains.kotlin.gradle.plugin.konan.hostManager
import org.jetbrains.kotlin.gradle.plugin.konan.konanHome
import org.jetbrains.kotlin.konan.target.CompilerOutputKind
import org.jetbrains.kotlin.konan.target.HostManager
import java.io.File
enum class KonanCacheKind(val outputKind: CompilerOutputKind) {
STATIC(CompilerOutputKind.STATIC_CACHE),
DYNAMIC(CompilerOutputKind.DYNAMIC_CACHE)
}
open class KonanCacheTask: DefaultTask() {
@InputDirectory
lateinit var originalKlib: File
// Taken into account by the [cacheFile] property.
@Internal
lateinit var cacheRoot: File
@get:Input
lateinit var target: String
@get:Internal
// TODO: Reuse NativeCacheKind from Big Kotlin plugin when it is available.
val cacheDirectory: File
get() = cacheRoot.resolve("$target-g$cacheKind")
@get:OutputDirectory
protected val cacheFile: File
get() {
val klibName = originalKlib.let {
if (it.isDirectory) it.name else it.nameWithoutExtension
}
return cacheDirectory.resolve("${klibName}-cache")
}
@Input
var cacheKind: KonanCacheKind = KonanCacheKind.STATIC
@Input
/** Path to a compiler distribution that is used to build this cache. */
val compilerDistributionPath: Property<File> = project.objects.property(File::class.java).apply {
set(project.provider { project.file(project.konanHome) })
}
@TaskAction
fun compile() {
// Compiler doesn't create a cache if the cacheFile already exists. So we need to remove it manually.
if (cacheFile.exists()) {
val deleted = cacheFile.deleteRecursively()
check(deleted) { "Cannot delete stale cache: ${cacheFile.absolutePath}" }
}
val args = listOf(
"-g",
"-target", target,
"-produce", cacheKind.outputKind.name.toLowerCase(),
"-Xadd-cache=${originalKlib.absolutePath}",
"-Xcache-directory=${cacheDirectory.absolutePath}"
)
KonanCompilerRunner(project, konanHome = compilerDistributionPath.get().absolutePath).run(args)
}
}
@@ -0,0 +1,432 @@
/*
* Copyright 2010-2019 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.gradle.plugin.tasks
import groovy.lang.Closure
import org.codehaus.groovy.runtime.GStringImpl
import org.gradle.api.file.ConfigurableFileTree
import org.gradle.api.file.FileCollection
import org.gradle.api.tasks.*
import org.gradle.process.CommandLineArgumentProvider
import org.jetbrains.kotlin.gradle.plugin.konan.*
import org.jetbrains.kotlin.gradle.plugin.model.KonanModelArtifact
import org.jetbrains.kotlin.gradle.plugin.model.KonanModelArtifactImpl
import org.jetbrains.kotlin.konan.library.defaultResolver
import org.jetbrains.kotlin.konan.target.CompilerOutputKind
import org.jetbrains.kotlin.konan.target.Distribution
import org.jetbrains.kotlin.konan.target.KonanTarget
import java.io.File
/**
* A task compiling the target executable/library using Kotlin/Native compiler
*/
abstract class KonanCompileTask: KonanBuildingTask(), KonanCompileSpec {
@get:Internal
override val toolRunner = KonanCompilerRunner(project, project.konanExtension.jvmArgs)
abstract val produce: CompilerOutputKind
@Internal get
// Output artifact --------------------------------------------------------
override val artifactSuffix: String
@Internal get() = produce.suffix(konanTarget)
override val artifactPrefix: String
@Internal get() = produce.prefix(konanTarget)
// Multiplatform support --------------------------------------------------
@Input var commonSourceSets = listOf("main")
@Internal var enableMultiplatform = false
private val commonSrcFiles_ = mutableSetOf<FileCollection>()
val commonSrcFiles: Collection<FileCollection>
@Internal get() = if (enableMultiplatform) commonSrcFiles_ else emptyList()
// Other compilation parameters -------------------------------------------
private val srcFiles_ = mutableSetOf<FileCollection>()
val srcFiles: Collection<FileCollection>
@Internal get() = srcFiles_.takeIf { !it.isEmpty() } ?: listOf(project.konanDefaultSrcFiles)
val allSources: Collection<FileCollection>
@InputFiles get() = listOf(srcFiles, commonSrcFiles).flatten()
private val allSourceFiles: List<File>
get() = allSources
.flatMap { it.files }
.filter { it.name.endsWith(".kt") }
@InputFiles val nativeLibraries = mutableSetOf<FileCollection>()
@Input val linkerOpts = mutableListOf<String>()
@Input var enableDebug = project.findProperty("enableDebug")?.toString()?.toBoolean()
?: project.environmentVariables.debuggingSymbols
@Input var noStdLib = false
@Input var noMain = false
@Input var enableOptimizations = project.environmentVariables.enableOptimizations
@Input var enableAssertions = false
@Optional @Input var entryPoint: String? = null
@Console var measureTime = false
val languageVersion : String?
@Optional @Input get() = project.konanExtension.languageVersion
val apiVersion : String?
@Optional @Input get() = project.konanExtension.apiVersion
/**
* Is the two-stage compilation enabled.
*
* In regular (one-stage) compilation, sources are directly compiled into a final native binary.
* In two-stage compilation, sources are compiled into a klib first and then a final native binary is produced from this klib.
*/
@get:Input
abstract val enableTwoStageCompilation: Boolean
protected fun directoryToKt(dir: Any) = project.fileTree(dir).apply {
include("**/*.kt")
exclude { it.file.startsWith(project.buildDir) }
}
// Command line ------------------------------------------------------------
// Exclude elements matching the predicate.
private fun List<String>.excludeFlags(predicate: (String) -> Boolean) = filterNot(predicate)
// Exclude the listed elements.
private fun List<String>.excludeFlags(vararg keys: String) = keys.toSet().let { keysToExclude ->
excludeFlags { it in keysToExclude }
}
// Exclude the arguments passed by the given keys.
// E.g. if the list contains the following elements: ["-l", "foo", "-r", "bar"],
// call exclude("-r") returns the following list: ["-l", "foo"].
private fun List<String>.excludeArguments(vararg args: String): List<String> {
val argsToExclude = args.toSet()
val xPrefixesToExclude = argsToExclude.filter { it.startsWith("-X") }.map { "$it=" }
val result = mutableListOf<String>()
var i = 0
while (i < size) {
val key = this[i]
when {
key in argsToExclude -> {
// Skip the key and the following arg.
i++
}
// Support args passed as -X<arg>=<value>.
xPrefixesToExclude.any { key.startsWith(it) } -> { /* Skip the key. */ }
else -> result += key
}
i++
}
return result
}
// Don't include coverage flags into the first stage because they are not supported when compiling a klib.
private fun firstStageExtraOpts() = extraOpts
.excludeFlags("-Xcoverage")
.excludeArguments("-Xcoverage-file", "-Xlibrary-to-cover")
// Don't include the -Xemit-lazy-objc-header flag into
// the second stage because this stage have no sources.
private fun secondStageExtraOpts() = extraOpts
.excludeArguments("-Xemit-lazy-objc-header")
/** Args passed to the compiler at the first stage of two-stage compilation (klib building). */
protected fun buildFirstStageArgs(klibPath: String) = mutableListOf<String>().apply {
addArg("-output", klibPath)
addArg("-produce", CompilerOutputKind.LIBRARY.name.toLowerCase())
addAll(buildCommonArgs())
addAll(firstStageExtraOpts())
allSourceFiles.mapTo(this) { it.absolutePath }
commonSrcFiles
.flatMap { it.files }
.mapTo(this) { "-Xcommon-sources=${it.absolutePath}" }
}
/** Args passed to the compiler at the second stage of two-stage compilation (producing a final binary from the klib). */
protected fun buildSecondStageArgs(klibPath: String) = mutableListOf<String>().apply {
addArg("-output", artifact.canonicalPath)
addArg("-produce", produce.name.toLowerCase())
addArgIfNotNull("-entry", entryPoint)
addAll(buildCommonArgs())
addFileArgs("-native-library", nativeLibraries)
linkerOpts.forEach {
addArg("-linker-option", it)
}
addAll(secondStageExtraOpts())
add("-Xinclude=${klibPath}")
}
/** Args passed to the compiler at both stages of the two-stage compilation and during the singe-stage compilation. */
protected fun buildCommonArgs() = mutableListOf<String>().apply {
addArgs("-repo", libraries.repos.map { it.canonicalPath })
if (platformConfiguration.files.isNotEmpty()) {
platformConfiguration.files.filter { it.name.endsWith(".klib") }.forEach {
// The library's directory is added in libraries.repos.
addArg("-library", it.nameWithoutExtension)
}
}
addFileArgs("-library", libraries.files)
addArgs("-library", libraries.namedKlibs)
// The library's directory is added in libraries.repos.
addArgs("-library", libraries.artifacts.map { it.artifact.nameWithoutExtension })
addArgIfNotNull("-target", konanTarget.visibleName)
addArgIfNotNull("-language-version", languageVersion)
addArgIfNotNull("-api-version", apiVersion)
addArgIfNotNull("-entry", entryPoint)
addKey("-g", enableDebug)
addKey("-nostdlib", noStdLib)
addKey("-nomain", noMain)
addKey("-opt", enableOptimizations)
addKey("-ea", enableAssertions)
addKey("-Xtime", measureTime)
addKey("-Xprofile-phases", measureTime)
addKey("-no-default-libs", noDefaultLibs)
addKey("-no-endorsed-libs", noEndorsedLibs)
addKey("-Xmulti-platform", enableMultiplatform)
if (libraries.friends.isNotEmpty())
addArg("-friend-modules", libraries.friends.joinToString(File.pathSeparator))
}
/** Args passed to the compiler if the two-stage compilation is disabled. */
fun buildSingleStageArgs() = mutableListOf<String>().apply {
addArg("-output", artifact.canonicalPath)
addArg("-produce", produce.name.toLowerCase())
addArgIfNotNull("-entry", entryPoint)
addAll(buildCommonArgs())
addFileArgs("-native-library", nativeLibraries)
linkerOpts.forEach {
addArg("-linker-option", it)
}
addAll(extraOpts)
allSourceFiles.mapTo(this) { it.absolutePath }
commonSrcFiles
.flatMap { it.files }
.mapTo(this) { "-Xcommon-sources=${it.absolutePath}" }
}
// region DSL.
// DSL. Input/output files.
override fun srcDir(dir: Any) {
srcFiles_.add(directoryToKt(dir))
}
override fun srcFiles(vararg files: Any) {
srcFiles_.add(project.files(files))
}
override fun srcFiles(files: Collection<Any>) = srcFiles(*files.toTypedArray())
// DSL. Native libraries.
override fun nativeLibrary(lib: Any) = nativeLibraries(lib)
override fun nativeLibraries(vararg libs: Any) {
nativeLibraries.add(project.files(*libs))
}
override fun nativeLibraries(libs: FileCollection) {
nativeLibraries.add(libs)
}
// DSL. Multiplatform projects.
override fun enableMultiplatform(flag: Boolean) {
enableMultiplatform = flag
}
@Deprecated("Use commonSourceSets instead", ReplaceWith("commonSourceSets(sourceSetName)"))
override fun commonSourceSet(sourceSetName: String) {
commonSourceSets = listOf(sourceSetName)
enableMultiplatform(true)
}
override fun commonSourceSets(vararg sourceSetNames: String) {
commonSourceSets = sourceSetNames.toList()
enableMultiplatform(true)
}
internal fun commonSrcDir(dir: Any) {
commonSrcFiles_.add(directoryToKt(dir))
}
// DSL. Other parameters.
override fun linkerOpts(values: List<String>) = linkerOpts(*values.toTypedArray())
override fun linkerOpts(vararg values: String) {
linkerOpts.addAll(values)
}
override fun enableDebug(flag: Boolean) {
enableDebug = flag
}
override fun noStdLib(flag: Boolean) {
noStdLib = flag
}
override fun noMain(flag: Boolean) {
noMain = flag
}
override fun enableOptimizations(flag: Boolean) {
enableOptimizations = flag
}
override fun enableAssertions(flag: Boolean) {
enableAssertions = flag
}
override fun entryPoint(entryPoint: String) {
this.entryPoint = entryPoint
}
override fun measureTime(flag: Boolean) {
measureTime = flag
}
// endregion
// region IDE model
override fun toModelArtifact(): KonanModelArtifact {
val repos = libraries.repos
val resolver = defaultResolver(
repos.map { it.absolutePath },
konanTarget,
Distribution(project.konanHome)
)
return KonanModelArtifactImpl(
artifactName,
artifact,
produce,
konanTarget.name,
name,
allSources.filterIsInstance(ConfigurableFileTree::class.java).map { it.dir },
allSourceFiles,
libraries.asFiles(resolver),
repos.toList()
)
}
// endregion
override fun run() {
destinationDir.mkdirs()
if (dumpParameters) {
dumpProperties(this)
}
if (enableTwoStageCompilation) {
logger.info("Start two-stage compilation")
val intermediateDir = project.konanBuildRoot
.resolve("intermediate")
.targetSubdir(konanTarget)
.apply { mkdirs() }
val klibPrefix = CompilerOutputKind.LIBRARY.prefix(konanTarget)
val klibSuffix = CompilerOutputKind.LIBRARY.suffix(konanTarget)
val intermediateKlib = intermediateDir.resolve("$klibPrefix$artifactName$klibSuffix").absolutePath
logger.info("Start first stage")
toolRunner.run(buildFirstStageArgs(intermediateKlib))
logger.info("Start second stage")
toolRunner.run(buildSecondStageArgs(intermediateKlib))
} else {
toolRunner.run(buildSingleStageArgs())
}
}
}
abstract class KonanCompileNativeBinary: KonanCompileTask() {
@Input
override var enableTwoStageCompilation: Boolean = false
}
open class KonanCompileProgramTask: KonanCompileNativeBinary() {
override val produce: CompilerOutputKind get() = CompilerOutputKind.PROGRAM
@Internal
var runTask: Exec? = null
inner class RunArgumentProvider(): CommandLineArgumentProvider {
override fun asArguments() = project.findProperty("runArgs")?.let {
it.toString().split(' ')
} ?: emptyList()
}
// Create tasks to run supported executables.
override fun init(config: KonanBuildingConfig<*>, destinationDir: File, artifactName: String, target: KonanTarget) {
super.init(config, destinationDir, artifactName, target)
if (!isCrossCompile && !project.hasProperty("konanNoRun")) {
runTask = project.tasks.create("run${artifactName.capitalize()}", Exec::class.java).apply {
group = "run"
dependsOn(this@KonanCompileProgramTask)
val artifactPathClosure = object : Closure<String>(this) {
override fun call() = artifactPath
}
// Use GString to evaluate a path to the artifact lazily thus allow changing it at configuration phase.
val lazyArtifactPath = GStringImpl(arrayOf(artifactPathClosure), arrayOf(""))
executable(lazyArtifactPath)
// Add values passed in the runArgs project property as arguments.
argumentProviders.add(RunArgumentProvider())
}
}
}
}
open class KonanCompileDynamicTask: KonanCompileNativeBinary() {
override val produce: CompilerOutputKind get() = CompilerOutputKind.DYNAMIC
val headerFile: File
@OutputFile get() = destinationDir.resolve("$artifactPrefix${artifactName}_api.h")
}
open class KonanCompileFrameworkTask: KonanCompileNativeBinary() {
override val produce: CompilerOutputKind get() = CompilerOutputKind.FRAMEWORK
override val artifact
@OutputDirectory get() = super.artifact
}
open class KonanCompileLibraryTask: KonanCompileTask() {
override val produce: CompilerOutputKind get() = CompilerOutputKind.LIBRARY
override val enableTwoStageCompilation: Boolean = false
}
open class KonanCompileBitcodeTask: KonanCompileNativeBinary() {
override val produce: CompilerOutputKind get() = CompilerOutputKind.BITCODE
}
@@ -0,0 +1,78 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.gradle.plugin.tasks
import org.gradle.api.DefaultTask
import org.gradle.api.GradleScriptException
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.TaskAction
import org.jetbrains.kotlin.gradle.plugin.konan.*
import org.jetbrains.kotlin.konan.MetaVersion
import org.jetbrains.kotlin.konan.util.DependencyProcessor
import org.jetbrains.kotlin.konan.util.DependencySource
import java.io.IOException
open class KonanCompilerDownloadTask : DefaultTask() {
internal companion object {
internal const val BASE_DOWNLOAD_URL = "https://download.jetbrains.com/kotlin/native/builds"
}
/**
* If true the task will also download dependencies for targets specified by the konan.targets project extension.
*/
@Internal var downloadDependencies: Boolean = false
@TaskAction
fun downloadAndExtract() {
if (!project.hasProperty(KonanPlugin.ProjectProperty.DOWNLOAD_COMPILER)) {
val konanHome = project.getProperty(KonanPlugin.ProjectProperty.KONAN_HOME)
logger.info("Use a user-defined compiler path: $konanHome")
} else {
try {
val downloadUrlDirectory = buildString {
append("$BASE_DOWNLOAD_URL/")
val version = project.konanVersion
when (version.meta) {
MetaVersion.DEV -> append("dev/")
else -> append("releases/")
}
append("$version/")
append(project.simpleOsName)
}
val konanCompiler = project.konanCompilerName()
val parentDir = DependencyProcessor.localKonanDir
logger.info("Downloading Kotlin/Native compiler from $downloadUrlDirectory/$konanCompiler into $parentDir")
DependencyProcessor(
parentDir,
downloadUrlDirectory,
mapOf(konanCompiler to listOf(DependencySource.Remote.Public))
).run()
} catch (e: IOException) {
throw GradleScriptException("Cannot download Kotlin/Native compiler", e)
}
}
// Download dependencies if a user said so.
if (downloadDependencies) {
val runner = KonanCompilerRunner(project, project.konanExtension.jvmArgs)
project.konanTargets.forEach {
runner.run("-Xcheck_dependencies", "-target", it.visibleName)
}
}
}
}
@@ -0,0 +1,147 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.gradle.plugin.tasks
import org.gradle.api.DefaultTask
import org.gradle.api.file.FileCollection
import org.gradle.api.tasks.TaskAction
import org.jetbrains.kotlin.gradle.plugin.konan.KonanInteropLibrary
import org.jetbrains.kotlin.gradle.plugin.konan.KonanLibrary
import org.jetbrains.kotlin.gradle.plugin.konan.KonanProgram
import org.jetbrains.kotlin.gradle.plugin.konan.konanArtifactsContainer
import org.jetbrains.kotlin.konan.target.Family
import org.jetbrains.kotlin.konan.target.HostManager
import java.io.File
open class KonanGenerateCMakeTask : DefaultTask() {
@Suppress("unused")
@TaskAction
fun generateCMake() {
val interops = project.konanArtifactsContainer.toList().filterIsInstance<KonanInteropLibrary>()
val libraries = project.konanArtifactsContainer.toList().filterIsInstance<KonanLibrary>()
val programs = project.konanArtifactsContainer.toList().filterIsInstance<KonanProgram>()
val cMakeLists = generateCMakeLists(
project.name,
interops,
libraries,
programs
)
File(project.projectDir, "CMakeLists.txt")
.writeText(cMakeLists)
// This directory is filled out by the IDE
File(project.projectDir, "KotlinCMakeModule")
.mkdir()
}
private val host = HostManager.host
private fun generateCMakeLists(
projectName: String,
interops: List<KonanInteropLibrary>,
libraries: List<KonanLibrary>,
programs: List<KonanProgram>
): String {
val cMakeCurrentListDir = "$" + "{CMAKE_CURRENT_LIST_DIR}"
return buildString {
appendln("""
cmake_minimum_required(VERSION 3.8)
set(CMAKE_MODULE_PATH $cMakeCurrentListDir/KotlinCMakeModule)
project($projectName Kotlin)
""".trimIndent())
appendln()
for (interop in interops) {
val task = interop[host] ?: continue
appendln(
Call("cinterop")
.arg("NAME", interop.name)
.arg("DEF_FILE", task.defFile.relativePath.toString().crossPlatformPath)
.arg("COMPILER_OPTS", task.cMakeCompilerOpts)
)
}
for (library in libraries) {
val task = library[host] ?: continue
appendln(
Call("konanc_library")
.arg("NAME", library.name)
.arg("SOURCES", task.cMakeSources)
.arg("LIBRARIES", task.cMakeLibraries)
.arg("LINKER_OPTS", task.cMakeLinkerOpts))
}
for (program in programs) {
val task = program[host] ?: continue
appendln(
Call("konanc_executable")
.arg("NAME", program.name)
.arg("SOURCES", task.cMakeSources)
.arg("LIBRARIES", task.cMakeLibraries)
.arg("LINKER_OPTS", task.cMakeLinkerOpts))
}
}
}
private val File.relativePath get() = relativeTo(project.projectDir)
private val String.crossPlatformPath get() =
if (host.family == Family.MINGW) replace('\\', '/') else this
private val FileCollection.asCMakeSourceList: List<String>
get() = files.map { it.relativePath.toString().crossPlatformPath }
private val KonanInteropTask.cMakeCompilerOpts: String
get() = (compilerOpts + includeDirs.allHeadersDirs.map { "-I${it.absolutePath.crossPlatformPath}" })
.joinToString(" ")
private val KonanCompileTask.cMakeSources: String
get() = allSources.flatMap { it.asCMakeSourceList }.joinToString(" ")
private val KonanCompileTask.cMakeLibraries: String
get() = mutableListOf<String>().apply {
addAll(libraries.artifacts.map { it.artifactName })
addAll(libraries.namedKlibs)
addAll(libraries.files.flatMap { it.files }.map { it.canonicalPath.crossPlatformPath })
}.joinToString(" ")
private val KonanCompileTask.cMakeLinkerOpts: String
get() = linkerOpts.joinToString(" ").replace('\\', '/')
}
private class Call(val name: String) {
private val args: MutableList<Pair<String, String>> = mutableListOf()
fun arg(key: String, value: String?): Call {
if (value != null && value.isNotBlank()) args += key to value
return this
}
override fun toString(): String =
buildString {
append(name)
append("(")
for ((key, value) in args) {
appendln()
append(" $key $value")
}
appendln(")")
}
}
@@ -0,0 +1,227 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.gradle.plugin.tasks
import groovy.lang.Closure
import org.gradle.api.Action
import org.gradle.api.file.FileCollection
import org.gradle.api.tasks.*
import org.gradle.util.ConfigureUtil
import org.gradle.workers.IsolationMode
import org.gradle.workers.WorkAction
import org.gradle.workers.WorkParameters
import org.gradle.workers.WorkerExecutor
import org.jetbrains.kotlin.gradle.plugin.konan.*
import org.jetbrains.kotlin.gradle.plugin.konan.KonanInteropSpec.IncludeDirectoriesSpec
import org.jetbrains.kotlin.gradle.plugin.model.KonanModelArtifact
import org.jetbrains.kotlin.gradle.plugin.model.KonanModelArtifactImpl
import org.jetbrains.kotlin.konan.CURRENT
import org.jetbrains.kotlin.konan.CompilerVersion
import org.jetbrains.kotlin.konan.library.defaultResolver
import org.jetbrains.kotlin.konan.target.CompilerOutputKind
import org.jetbrains.kotlin.konan.target.Distribution
import org.jetbrains.kotlin.konan.target.KonanTarget
import java.io.File
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
/**
* A task executing cinterop tool with the given args and compiling the stubs produced by this tool.
*/
open class KonanInteropTask @Inject constructor(@Internal val workerExecutor: WorkerExecutor) : KonanBuildingTask(), KonanInteropSpec {
@get:Internal
override val toolRunner: KonanToolRunner = KonanInteropRunner(project, project.konanExtension.jvmArgs)
override fun init(config: KonanBuildingConfig<*>, destinationDir: File, artifactName: String, target: KonanTarget) {
super.init(config, destinationDir, artifactName, target)
this.defFile = project.konanDefaultDefFile(artifactName)
}
// Output directories -----------------------------------------------------
override val artifactSuffix: String
@Internal get() = ".klib"
override val artifactPrefix: String
@Internal get() = ""
// Interop stub generator parameters -------------------------------------
@Internal var enableParallel: Boolean = false
@InputFile lateinit var defFile: File
@Optional @Input var packageName: String? = null
@Input val compilerOpts = mutableListOf<String>()
@Input val linkerOpts = mutableListOf<String>()
@Nested val includeDirs = IncludeDirectoriesSpecImpl()
@InputFiles val headers = mutableSetOf<FileCollection>()
@InputFiles val linkFiles = mutableSetOf<FileCollection>()
fun buildArgs() = mutableListOf<String>().apply {
addArg("-o", artifact.canonicalPath)
addArgIfNotNull("-target", konanTarget.visibleName)
addArgIfNotNull("-def", defFile.canonicalPath)
addArgIfNotNull("-pkg", packageName)
addFileArgs("-header", headers)
compilerOpts.forEach {
addArg("-compiler-option", it)
}
val linkerOpts = mutableListOf<String>().apply { addAll(linkerOpts) }
linkFiles.forEach {
linkerOpts.addAll(it.files.map { it.canonicalPath })
}
linkerOpts.forEach {
addArg("-linker-option", it)
}
addArgs("-compiler-option", includeDirs.allHeadersDirs.map { "-I${it.absolutePath}" })
addArgs("-headerFilterAdditionalSearchPrefix", includeDirs.headerFilterDirs.map { it.absolutePath })
addArgs("-repo", libraries.repos.map { it.canonicalPath })
addFileArgs("-library", libraries.files)
addArgs("-library", libraries.namedKlibs)
addArgs("-library", libraries.artifacts.map { it.artifact.canonicalPath })
addKey("-no-default-libs", noDefaultLibs)
addKey("-no-endorsed-libs", noEndorsedLibs)
addAll(extraOpts)
}
// region DSL.
inner class IncludeDirectoriesSpecImpl: IncludeDirectoriesSpec {
@Input val allHeadersDirs = mutableSetOf<File>()
@Input val headerFilterDirs = mutableSetOf<File>()
override fun allHeaders(vararg includeDirs: Any) = allHeaders(includeDirs.toList())
override fun allHeaders(includeDirs: Collection<Any>) {
allHeadersDirs.addAll(includeDirs.map { project.file(it) })
}
override fun headerFilterOnly(vararg includeDirs: Any) = headerFilterOnly(includeDirs.toList())
override fun headerFilterOnly(includeDirs: Collection<Any>) {
headerFilterDirs.addAll(includeDirs.map { project.file(it) })
}
}
override fun defFile(file: Any) {
defFile = project.file(file)
}
override fun packageName(value: String) {
packageName = value
}
override fun compilerOpts(vararg values: String) {
compilerOpts.addAll(values)
}
override fun header(file: Any) = headers(file)
override fun headers(vararg files: Any) {
headers.add(project.files(files))
}
override fun headers(files: FileCollection) {
headers.add(files)
}
override fun includeDirs(vararg values: Any) = includeDirs.allHeaders(values.toList())
override fun includeDirs(closure: Closure<Unit>) = includeDirs(ConfigureUtil.configureUsing(closure))
override fun includeDirs(action: Action<IncludeDirectoriesSpec>) = includeDirs { action.execute(this) }
override fun includeDirs(configure: IncludeDirectoriesSpec.() -> Unit) = includeDirs.configure()
override fun linkerOpts(vararg values: String) = linkerOpts(values.toList())
override fun linkerOpts(values: List<String>) {
linkerOpts.addAll(values)
}
override fun link(vararg files: Any) {
linkFiles.add(project.files(files))
}
override fun link(files: FileCollection) {
linkFiles.add(files)
}
// endregion
// region IDE model
override fun toModelArtifact(): KonanModelArtifact {
val repos = libraries.repos
val resolver = defaultResolver(
repos.map { it.absolutePath },
konanTarget,
Distribution(project.konanHome)
)
return KonanModelArtifactImpl(
artifactName,
artifact,
CompilerOutputKind.LIBRARY,
konanTarget.name,
name,
listOfNotNull(defFile.parentFile),
listOf(defFile),
libraries.asFiles(resolver),
repos.toList()
)
}
// endregion
internal interface RunToolParameters: WorkParameters {
var taskName: String
var args: List<String>
}
internal abstract class RunTool @Inject constructor() : WorkAction<RunToolParameters> {
override fun execute() {
val toolRunner = interchangeBox.remove(parameters.taskName) ?: error(":(")
toolRunner.run(parameters.args)
}
}
override fun run() {
destinationDir.mkdirs()
if (dumpParameters) {
dumpProperties(this)
}
val args = buildArgs()
if (enableParallel) {
val workQueue = workerExecutor.noIsolation()
interchangeBox[this.path] = toolRunner
workQueue.submit(RunTool::class.java) {
it.taskName = this.path
it.args = args
}
} else {
toolRunner.run(args)
}
}
}
internal val interchangeBox = ConcurrentHashMap<String, KonanToolRunner>()
@@ -0,0 +1,50 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.gradle.plugin.model
import org.jetbrains.kotlin.konan.CompilerVersion
import org.jetbrains.kotlin.konan.target.CompilerOutputKind
import java.io.File
import java.io.Serializable
/**
* An immutable representation of Konan's project model for gradle tooling API.
* This model is shared with the client processes such as an IDE.
*/
interface KonanModel : Serializable {
val konanHome: File
val konanVersion: CompilerVersion
val languageVersion: String?
val apiVersion: String?
val artifacts: List<KonanModelArtifact>
}
/**
* A representation of a single binary produced by Kotlin/Native for a cetrain target.
*/
interface KonanModelArtifact : Serializable {
val name: String
val type: CompilerOutputKind
val targetPlatform: String
val file: File
val buildTaskName: String
val srcDirs: List<File>
val srcFiles: List<File>
val libraries: List<File>
val searchPaths: List<File>
}
@@ -0,0 +1,79 @@
/*
* Copyright 2010-2018 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.gradle.plugin.model
import org.gradle.api.Project
import org.gradle.tooling.provider.model.ToolingModelBuilder
import org.jetbrains.kotlin.gradle.plugin.konan.konanArtifactsContainer
import org.jetbrains.kotlin.gradle.plugin.konan.konanExtension
import org.jetbrains.kotlin.gradle.plugin.konan.konanHome
import org.jetbrains.kotlin.gradle.plugin.konan.konanVersion
import org.jetbrains.kotlin.konan.CURRENT
import org.jetbrains.kotlin.konan.CompilerVersion
import org.jetbrains.kotlin.konan.target.CompilerOutputKind
import java.io.File
object KonanToolingModelBuilder : ToolingModelBuilder {
override fun canBuild(modelName: String) = KonanModel::class.java.name == modelName
private fun buildModelKonan(project: Project): KonanModel {
val artifacts = project.konanArtifactsContainer.flatten().toList().map { it.toModelArtifact() }
return KonanModelImpl(
artifacts,
project.file(project.konanHome),
project.konanVersion,
// TODO: Provide defaults for these versions.
project.konanExtension.languageVersion,
project.konanExtension.apiVersion
)
}
private val Project.hasKonanPlugin: Boolean
get() = with(pluginManager) {
hasPlugin("konan") ||
hasPlugin("org.jetbrains.kotlin.konan")
}
override fun buildAll(modelName: String, project: Project): KonanModel =
when {
project.hasKonanPlugin -> buildModelKonan(project)
else -> throw IllegalStateException("The project '${project.path}' has no Kotlin/Native plugin")
}
}
internal data class KonanModelImpl(
override val artifacts: List<KonanModelArtifact>,
override val konanHome: File,
override val konanVersion: CompilerVersion,
override val languageVersion: String?,
override val apiVersion: String?
) : KonanModel
internal data class KonanModelArtifactImpl(
override val name: String,
override val file: File,
override val type: CompilerOutputKind,
override val targetPlatform: String,
override val buildTaskName: String,
override val srcDirs: List<File>,
override val srcFiles: List<File>,
override val libraries: List<File>,
override val searchPaths: List<File>
) : KonanModelArtifact
@@ -0,0 +1,17 @@
#
# Copyright 2010-2017 JetBrains s.r.o.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
shadow.org.jetbrains.kotlinx.serialization.gradle.SerializationKotlinGradleSubplugin
@@ -0,0 +1,29 @@
/*
* Copyright 2010-2018 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.gradle.plugin.test
import org.junit.Rule
import org.junit.rules.TemporaryFolder
import spock.lang.Specification
class BaseKonanSpecification extends Specification {
@Rule
TemporaryFolder tmpFolder = new TemporaryFolder()
File getProjectDirectory() { return tmpFolder.root }
}
@@ -0,0 +1,42 @@
/*
* Copyright 2010-2018 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.gradle.plugin.test
import org.gradle.testkit.runner.TaskOutcome
class DefaultSpecification extends BaseKonanSpecification {
def 'Plugin should build a project without additional settings'() {
when:
def project = KonanProject.createEmpty(projectDirectory) { KonanProject it ->
it.buildFile.write("""
plugins { id 'konan' }
konanArtifacts {
interop('stdio')
library('main')
}
""".stripIndent())
it.generateDefFile("stdio.def", "")
it.generateSrcFile("main.kt")
}
def result = project.createRunner().withArguments('build').build()
then:
!result.tasks.collect { it.outcome }.contains(TaskOutcome.FAILED)
}
}
@@ -0,0 +1,336 @@
/*
* Copyright 2010-2018 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.gradle.plugin.test
import org.jetbrains.kotlin.konan.target.Family
import org.jetbrains.kotlin.konan.target.HostManager
import org.jetbrains.kotlin.konan.target.KonanTarget
import spock.lang.Ignore
import spock.lang.Unroll
import static org.jetbrains.kotlin.gradle.plugin.test.KonanProject.escapeBackSlashes
// TODO: Rewrite tests using Kotlin.
class EnvVariableSpecification extends BaseKonanSpecification {
class WrapperResult {
private int exitValue;
private String stdout;
private String stderr;
WrapperResult(Process process) {
exitValue = process.exitValue()
stdout = process.getInputStream().readLines().join("\n")
stderr = process.getErrorStream().readLines().join("\n")
}
int getExitValue() { return exitValue }
String getStdout() { return stdout }
String getStderr() { return stderr }
WrapperResult printStdout() { println(stdout); return this }
WrapperResult printStderr() { println(stderr); return this }
}
private KonanProject createProjectWithWrapper() {
def project = KonanProject.createEmpty(projectDirectory)
def runner = project.createRunner()
// Gradle TestKit doesn't support setting environment variables for runners.
// So we use the following hack: we create a gradle wrapper, start it as a separate
// process with custom environment variables and check its exit code and output.
runner.withArguments("wrapper").build()
def classpath = runner.pluginClasspath.collect { "'${escapeBackSlashes(it.absolutePath)}'" }.join(", ")
project.buildFile.write("""\
buildscript {
dependencies {
classpath files($classpath)
}
}
""".stripIndent())
return project
}
private WrapperResult runWrapper(KonanProject project,
List<String> tasks,
Map<String, String> environment = [:],
Map<String, String> properties = ["konan.useEnvironmentVariables": 'true']) {
def wrapper = (HostManager.host.family == Family.MINGW) ? "gradlew.bat" : "gradlew"
def command = ["$project.projectDir.absolutePath/$wrapper".toString()]
command.addAll(tasks)
command.addAll(properties.collect { "-P${it.key}=${it.value}".toString() })
def projectBuilder = new ProcessBuilder()
.directory(project.projectDir)
.command(command)
projectBuilder.environment().putAll(environment)
def process = projectBuilder.start()
process.waitFor()
return new WrapperResult(process)
}
private WrapperResult runWrapper(KonanProject project,
String task,
Map<String, String> environment = [:],
Map<String, String> properties = ["konan.useEnvironmentVariables": 'true']) {
return runWrapper(project, [task], environment, properties)
}
private String artifactFileName(String baseName, ArtifactType type, KonanTarget target = HostManager.host) {
String suffix = ""
String prefix = ""
switch (type) {
case ArtifactType.PROGRAM:
suffix = target.family.exeSuffix
break
case ArtifactType.INTEROP:
case ArtifactType.LIBRARY:
suffix = "klib"
break
case ArtifactType.BITCODE:
suffix = "bc"
break;
case ArtifactType.DYNAMIC:
prefix = target.family.dynamicPrefix
suffix = target.family.dynamicSuffix
break
case ArtifactType.STATIC:
prefix = target.family.staticPrefix
suffix = target.family.staticSuffix
break
case ArtifactType.FRAMEWORK:
suffix = "framework"
}
return "$prefix${baseName}.$suffix"
}
@Ignore("The plugin doesn't use env vars until https://github.com/gradle/gradle/issues/3468 is fixed.")
@Unroll("Plugin should support #action via an env variable")
def 'Plugin should support enabling/disabling debug/opt via an env variable'() {
when:
def project = createProjectWithWrapper()
project.buildFile.append("""\
apply plugin: 'konan'
konanArtifacts {
library('main')
}
task assertEnableDebug {
doLast {
konanArtifacts.main.forEach {
if (!($assertion)) throw new AssertionError("$message for \${it.name}")
}
}
}
""".stripIndent())
def result = runWrapper(project,"assertEnableDebug", [(variable): value])
.printStderr()
.getExitValue()
then:
result == 0
where:
action |variable |value |assertion |message
"enabling debug" |"DEBUGGING_SYMBOLS" |"YES" |"it.enableDebug" |"Debug should be enabled"
"disabling debug" |"DEBUGGING_SYMBOLS" |"NO" |"!it.enableDebug" |"Debug should be disabled"
"enabling opt" |"KONAN_ENABLE_OPTIMIZATIONS" |"YES" |"it.enableOptimizations" |"Opts should be enabled"
"disabling opt" |"KONAN_ENABLE_OPTIMIZATIONS" |"NO" |"!it.enableOptimizations" |"Opts should be disabled"
}
@Ignore("The plugin doesn't use env vars until https://github.com/gradle/gradle/issues/3468 is fixed.")
def 'Plugin should support setting destination directory via an env variable'() {
when:
def project = createProjectWithWrapper()
def newDestinationDir = project.createSubDir("newDestination")
def newDestinationPath = newDestinationDir.absolutePath
project.buildFile.append("""\
apply plugin: 'konan'
konanArtifacts {
program('program')
library('library')
dynamic('dynamic')
framework('framework')
}
task assertDestinationDir {
doLast {
konanArtifacts.forEach { artifact ->
artifact.forEach {
if (it.destinationDir.absolutePath != '${escapeBackSlashes(newDestinationPath)}'){
throw new AssertionError("Unexpected destination dir for \$it.name\\n" +
"expected: ${escapeBackSlashes(newDestinationPath)}\\n" +
"actual: \$it.destinationDir")
}
}
}
}
}
""".stripIndent())
project.generateSrcFile("main.kt")
def assertResult = runWrapper(project, "assertDestinationDir", ["CONFIGURATION_BUILD_DIR": newDestinationPath])
.printStderr()
.getExitValue()
def buildResult = runWrapper(project, "build", ["CONFIGURATION_BUILD_DIR": newDestinationPath])
.printStderr()
.getExitValue()
def files = newDestinationDir.list()
then:
assertResult == 0
buildResult == 0
files.contains(artifactFileName("program", ArtifactType.PROGRAM))
files.contains(artifactFileName("library", ArtifactType.LIBRARY))
files.contains(artifactFileName("dynamic", ArtifactType.DYNAMIC))
files.contains(artifactFileName("static", ArtifactType.STATIC))
if (HostManager.hostIsMac) {
files.contains(artifactFileName("framework", ArtifactType.FRAMEWORK))
}
}
@Ignore("The plugin doesn't use env vars until https://github.com/gradle/gradle/issues/3468 is fixed.")
def 'Plugin should rerun tasks if CONFIGURATION_BUILD_DIR has been changed'() {
when:
def project = createProjectWithWrapper()
def destination1 = project.createSubDir("destination1", "subdir")
def destination2 = project.createSubDir("destination2", "subdir")
project.buildFile.append("""\
apply plugin: 'konan'
konanArtifacts {
library('main')
}
""".stripIndent())
project.generateSrcFile("main.kt")
def buildResult1 = runWrapper(project, "build", ["CONFIGURATION_BUILD_DIR": destination1.absolutePath])
.printStderr()
.getExitValue()
def buildResult2 = runWrapper(project, "build", ["CONFIGURATION_BUILD_DIR": destination2.absolutePath])
.printStderr()
.getExitValue()
def files1 = destination1.list()
def files2 = destination2.list()
then:
buildResult1 == 0
buildResult2 == 0
destination1.exists()
destination2.exists()
files1.contains(artifactFileName("main", ArtifactType.LIBRARY))
files2.contains(artifactFileName("main", ArtifactType.LIBRARY))
}
@Ignore("The plugin doesn't use env vars until https://github.com/gradle/gradle/issues/3468 is fixed.")
def 'Plugin should ignore environmentVariables if konan.useEnvironmentVariables is false or is not set'() {
when:
def project = createProjectWithWrapper()
def newDestinationDir = project.createSubDir("newDestination")
def newDestinationPath = newDestinationDir.absolutePath
project.buildFile.append("""\
apply plugin: 'konan'
konanArtifacts {
program('program')
library('library')
dynamic('dynamic')
framework('framework')
}
task assertNoOverrides {
doLast {
konanArtifacts.forEach { artifact ->
artifact.forEach {
if (it.destinationDir.absolutePath == '${escapeBackSlashes(newDestinationPath)}'){
throw new AssertionError("CONFIGURATION_BUILD_DIR overrides a default output path " +
"when it shouldn't.\\n" +
"Task: \${it.name}, Path: \${it.destinationDir}")
}
if (it.enableDebug) {
throw new AssertionError("DEBUGGING_SYMBOLS overrides a default value " +
"when it shouldn't\\n" +
"Task: \${it.name}")
}
}
}
}
}
""".stripIndent())
def resultNoProp = runWrapper(project,
"assertNoOverrides",
["DEBUGGING_SYMBOLS": "true", "CONFIGURATION_BUILD_DIR": newDestinationPath], [:])
.printStderr()
.getExitValue()
def resultFalseValue = runWrapper(project,
"assertNoOverrides",
["DEBUGGING_SYMBOLS": "true", "CONFIGURATION_BUILD_DIR": newDestinationPath],
["konan.useEnvironmentVariables": "false"])
.printStderr()
.getExitValue()
then:
resultNoProp == 0
resultFalseValue == 0
}
@Ignore("The plugin doesn't use env vars until https://github.com/gradle/gradle/issues/3468 is fixed.")
def 'Up-to-date checks should work with different directories for different targets'() {
when:
def project = createProjectWithWrapper()
def fooDir = project.createSubDir("foo")
def barDir = project.createSubDir("bar")
project.buildFile.append("""\
apply plugin: 'konan'
konanArtifacts {
library('foo')
library('bar')
}
task assertUpToDate {
dependsOn 'compileKonanFoo'
doLast {
if (!konanArtifacts.foo.getByTarget('host').state.upToDate) {
throw new AssertionError("Compilation task is not up-to-date")
}
}
}
""".stripIndent())
project.generateSrcFile("main.kt")
def buildResult1 = runWrapper(project, "compileKonanFoo",
["CONFIGURATION_BUILD_DIR": fooDir.absolutePath])
.printStderr()
.getExitValue()
def buildResult2 = runWrapper(project, "compileKonanBar",
["CONFIGURATION_BUILD_DIR": barDir.absolutePath])
.printStderr()
.getExitValue()
def buildResult3 = runWrapper(project,
"assertUpToDate",
["CONFIGURATION_BUILD_DIR": fooDir.absolutePath])
.printStderr()
.getExitValue()
then:
buildResult1 == 0
buildResult2 == 0
buildResult3 == 0
}
}
@@ -0,0 +1,312 @@
/*
* Copyright 2010-2018 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.gradle.plugin.test
import org.gradle.testkit.runner.BuildResult
import org.gradle.testkit.runner.TaskOutcome
import spock.lang.Unroll
class IncrementalSpecification extends BaseKonanSpecification {
Tuple buildTwice(KonanProject project, String task = 'build', Closure change) {
def runner = project.createRunner().withArguments(task)
def firstResult = runner.build()
change(project)
def secondResult = runner.build()
return new Tuple(project, firstResult, secondResult)
}
Tuple buildTwice(ArtifactType mainArtifactType = ArtifactType.LIBRARY, String task = 'build', Closure change) {
return buildTwice(KonanProject.createWithInterop(projectDirectory, mainArtifactType), change)
}
Boolean noRecompilationHappened(KonanProject project, BuildResult firstResult, BuildResult secondResult) {
return project.with {
firstResult.tasks.collect { it.path }.containsAll(buildingTasks) &&
firstResult.taskPaths(TaskOutcome.SUCCESS).containsAll(buildingTasks) &&
secondResult.taskPaths(TaskOutcome.UP_TO_DATE).containsAll(buildingTasks) &&
firstResult.task(downloadTask).outcome == TaskOutcome.SUCCESS &&
secondResult.task(downloadTask).outcome == TaskOutcome.SUCCESS
}
}
Boolean onlyRecompilationHappened(KonanProject project, BuildResult firstResult, BuildResult secondResult) {
return project.with {
firstResult.taskPaths(TaskOutcome.SUCCESS).containsAll(buildingTasks) &&
secondResult.taskPaths(TaskOutcome.SUCCESS).containsAll(compilationTasks) &&
secondResult.taskPaths(TaskOutcome.UP_TO_DATE).containsAll(interopTasks)
}
}
Boolean recompilationAndInteropProcessingHappened(KonanProject project, BuildResult firstResult, BuildResult secondResult) {
return project.with {
firstResult.taskPaths(TaskOutcome.SUCCESS).containsAll(buildingTasks) &&
secondResult.taskPaths(TaskOutcome.SUCCESS).containsAll(buildingTasks)
}
}
//region tests =====================================================================================================
def 'Compilation is up-to-date if there is no changes'() {
when:
def results = buildTwice {}
then:
noRecompilationHappened(*results)
}
def 'Source change should cause only recompilation'() {
when:
def results = buildTwice { KonanProject project ->
project.srcFiles[0].append("\n // Some change in the source file")
}
then:
onlyRecompilationHappened(*results)
}
def 'Def-file change should cause recompilation and interop reprocessing'() {
when:
def results = buildTwice { KonanProject project ->
project.defFiles[0].append("\n # Some change in the def-file")
}
then:
recompilationAndInteropProcessingHappened(*results)
}
@Unroll("#parameter change for a compilation task should cause only recompilation")
def 'Parameter changes should cause only recompilaton'() {
when:
def results = buildTwice { KonanProject project ->
project.addSetting("main", parameter, value)
}
then:
onlyRecompilationHappened(*results)
where:
parameter | value
"baseDir" | "'build/new/outputDir'"
"enableOptimizations" | "true"
"linkerOpts" | "'--help'"
"enableAssertions" | "true"
"enableDebug" | "true"
"artifactName" | "'foo'"
"extraOpts" | "'-Xtime'"
"noDefaultLibs" | "true"
"noEndorsedLibs" | "true"
}
def 'Plugin should support a custom entry point and recompile an artifact if it changes'() {
when:
def project = KonanProject.createEmpty(projectDirectory) { KonanProject it ->
it.addCompilerArtifact("main", """
|fun main(args: Array<String>) { println("default main") }
|
""".stripMargin())
}
def results = buildTwice(project) { KonanProject it ->
it.srcFiles[0].write("""
|package foo
|
|fun bar(args: Array<String>) { println("changed main") }
|
""".stripMargin())
it.addSetting("main", "entryPoint", "'foo.bar'")
}
then:
onlyRecompilationHappened(*results)
}
def 'srcFiles change for a compilation task should cause only recompilation'() {
when:
def project = KonanProject.createWithInterop(projectDirectory, ArtifactType.LIBRARY) { KonanProject it ->
it.generateSrcFile(["src", "foo", "kotlin"], 'bar.kt', """
fun foo(args: Array<String>) { println("Hello!") }
""".stripIndent())
}
def results = buildTwice(project) { KonanProject it ->
it.addSetting("main", "srcFiles", "project.fileTree('src/foo/kotlin')")
}
then:
onlyRecompilationHappened(*results)
}
def 'Library change for a compilation task should cause only recompilation'() {
when:
def project = KonanProject.create(projectDirectory, ArtifactType.LIBRARY) { KonanProject it ->
it.generateSrcFile(["src", "lib", "kotlin"], "lib.kt", "fun bar() { println(\"Hello!\") }")
it.buildFile.append("""
konanArtifacts {
library('lib') {
srcFiles fileTree('src/lib/kotlin')
}
}
""".stripIndent())
}
def results = buildTwice(project) { KonanProject it ->
it.addLibraryToArtifact("main", 'lib')
}
then:
onlyRecompilationHappened(*results)
}
def 'Native library change for a compilation task should cause only recompilaton'() {
when:
def project = KonanProject.createWithInterop(projectDirectory, ArtifactType.LIBRARY) { KonanProject it ->
it.generateSrcFile(["src", "lib", "kotlin"], "lib.kt", "fun bar() { println(\"Hello!\") }")
it.buildFile.append("""
konanArtifacts {
bitcode('lib') {
srcFiles fileTree('src/lib/kotlin')
}
}
""".stripIndent())
}
def results = buildTwice(project) { KonanProject it ->
it.addSetting("main", "nativeLibrary", "compileKonanLib${KonanProject.HOST.capitalize()}.artifact")
}
then:
onlyRecompilationHappened(*results)
}
// TODO: Test library for incremental compilation.
@Unroll("#parameter change for an interop task should cause recompilation and interop reprocessing")
def 'Parameter change for an interop task should cause recompilation and interop reprocessing'() {
when:
def results = buildTwice { KonanProject project ->
project.addSetting("stdio", parameter, value)
}
then:
recompilationAndInteropProcessingHappened(*results)
where:
parameter | value
"packageName" | "'org.sample'"
"compilerOpts" | "'-g'"
"linkerOpts" | "'--help'"
"includeDirs" | "'src'"
"includeDirs.allHeaders" | "'src'"
"extraOpts" | "'-verbose'"
"noDefaultLibs" | "true"
"noEndorsedLibs" | "true"
}
def 'includeDirs.headerFilterOnly change should cause recompilation and interop reprocessing'() {
when:
def project = KonanProject.createWithInterop(projectDirectory) { KonanProject it ->
it.defFiles.first().write("headers = stdio.h\nheaderFilter = stdio.h")
}
def results = buildTwice(project) { KonanProject it ->
it.addSetting(KonanProject.DEFAULT_INTEROP_NAME, "includeDirs.headerFilterOnly", "'.'")
}
then:
recompilationAndInteropProcessingHappened(*results)
}
def 'defFile change for an interop task should cause recompilation and interop reprocessing'() {
when:
def project = KonanProject.createWithInterop(projectDirectory, ArtifactType.LIBRARY)
def defFile = project.generateDefFile("foo.def", "#some content")
def results = buildTwice(project) { KonanProject it ->
it.addSetting("stdio", "defFile", defFile)
}
then:
recompilationAndInteropProcessingHappened(*results)
}
def 'header change for an interop task should cause recompilation and interop reprocessing'() {
when:
def project = KonanProject.createWithInterop(projectDirectory, ArtifactType.LIBRARY)
def header = project.generateSrcFile('header.h', "#define CONST 1")
def results = buildTwice(project) { KonanProject it ->
it.addSetting("stdio", "headers", header)
}
then:
recompilationAndInteropProcessingHappened(*results)
}
def 'link change for an interop task should cause recompilation and interop reprocessing'() {
when:
def project = KonanProject.createWithInterop(projectDirectory, ArtifactType.LIBRARY) { KonanProject it ->
it.generateSrcFile(["src", "lib", "kotlin"], 'lib.kt', 'fun foo() { println(42) }')
it.buildFile.append("""
konanArtifacts {
bitcode('lib') {
srcFiles fileTree('src/lib/kotlin')
}
}
""".stripIndent())
}
def results = buildTwice(project) { KonanProject it ->
it.addSetting("stdio", "dependsOn", "konanArtifacts.lib.${KonanProject.HOST}")
it.addSetting("stdio", "link", "files(konanArtifacts.lib.${KonanProject.HOST}.artifactPath)")
}
then:
recompilationAndInteropProcessingHappened(*results)
}
def 'Common source change should cause recompilation'() {
when:
File commonSource
def project = KonanProject.createEmpty(projectDirectory) { KonanProject it ->
def commonDirectory = MultiplatformSpecification.createCommonProject(it)
commonSource = MultiplatformSpecification.createCommonSource(commonDirectory,
["src", "main", "kotlin"],
"common.kt",
"expect fun foo(): Int")
println(it.settingsFile.text)
it.generateSrcFile("actual.kt", "actual fun foo() = 42")
it.buildFile.append("""
konanArtifacts {
library('foo') {
enableMultiplatform true
}
}
dependencies {
expectedBy project(':common')
}
""".stripIndent())
it.buildingTasks.addAll([":compileKonanFoo", ":compileKonanFoo${KonanProject.HOST}}"])
}
def results = buildTwice(project, ':build') { KonanProject ->
commonSource.append("\nfun bar() = 43")
}
then:
onlyRecompilationHappened(*results)
}
// TODO: Add incremental tests for the 'libraries' block.
//endregion
}
@@ -0,0 +1,377 @@
/*
* Copyright 2010-2018 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.gradle.plugin.test
import org.gradle.testkit.runner.GradleRunner
import org.gradle.util.GradleVersion
import org.jetbrains.kotlin.konan.target.HostManager
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
enum ArtifactType {
PROGRAM("program"),
LIBRARY("library"),
BITCODE("bitcode"),
INTEROP("interop"),
DYNAMIC("dynamic"),
STATIC("static"),
FRAMEWORK("framework")
String type
ArtifactType(String type) { this.type = type }
String toString() { return type }
}
class KonanProject {
static String DEFAULT_ARTIFACT_NAME = 'main'
static String DEFAULT_INTEROP_NAME = "stdio"
static String HOST = HostManager.hostName
File projectDir
Path projectPath
File konanBuildDir
String konanHome
String gradleVersion
File buildFile
File propertiesFile
File settingsFile
Set<File> srcFiles = []
Set<File> defFiles = []
List<String> interopTasks = []
List<String> compilationTasks = []
String downloadTask = ":checkKonanCompiler"
List<String> targets
List<String> getBuildingTasks() { return compilationTasks + interopTasks }
List<String> getKonanTasks() { return getBuildingTasks() + downloadTask }
static String DEFAULT_SRC_CONTENT = """
fun main(args: Array<String>) {
println(42)
}
"""
static String DEFAULT_DEF_CONTENT = """
headers = stdio.h
""".stripIndent()
protected KonanProject(File projectDir){
this(projectDir, [HOST])
}
protected KonanProject(File projectDir, List<String> targets) {
this.projectDir = projectDir
this.targets = targets
projectPath = projectDir.toPath()
konanBuildDir = projectPath.resolve('build/konan').toFile()
def konanHomeDir = new File(getKonanHome())
if (!konanHomeDir.exists() || !konanHomeDir.directory) {
throw new IllegalStateException("konan.home doesn't exist or is not a directory: $konanHomeDir.canonicalPath")
}
// Escape windows path separator
this.konanHome = escapeBackSlashes(konanHomeDir.canonicalPath)
this.gradleVersion = System.getProperty("gradleVersion") ?: GradleVersion.current().version
}
GradleRunner createRunner(boolean withDebug = true) {
return GradleRunner.create()
.withProjectDir(projectDir)
.withPluginClasspath()
.withDebug(withDebug)
.withGradleVersion(gradleVersion)
}
/** Creates a subdirectory specified by the given path. */
File createSubDir(String ... path) {
return createSubDir(Paths.get(*path))
}
/** Creates a subdirectory specified by the given path. */
File createSubDir(Path path) {
return Files.createDirectories(projectPath.resolve(path)).toFile()
}
/** Creates a file with the given content in project subdirectory specified by parentDirectory. */
File createFile(Path parentDirectory = projectPath, String fileName, String content) {
def parent = projectPath.resolve(parentDirectory)
Files.createDirectories(parent)
def result = parent.resolve(fileName).toFile()
result.createNewFile()
result.write(content)
return result
}
/** Creates a file with the given content in project subdirectory specified by parentPath. */
File createFile(List<String> parentPath, String fileName, String content) {
return createFile(Paths.get(*parentPath), fileName, content)
}
/** Creates a folder for project source files (src/main/kotlin). */
void generateFolders() {
createSubDir("src", "main", "kotlin")
createSubDir("src", "main", "c_interop")
}
/** Generates a build.gradle file in the root project directory with the given content. */
File generateBuildFile(String content) {
buildFile = createFile(projectPath, "build.gradle", content)
return buildFile
}
/** Generates a settings.gradle file in the root project directory with the given content. */
File generateSettingsFile(String content) {
settingsFile = createFile(projectPath, "settings.gradle", content)
return settingsFile
}
/**
* Generates a build.gradle file in root project directory with the default content (see below)
* and fills the compilationTasks array.
*
* plugins { id 'konan' }
*
* konanArtifacts {
* program('$DEFAULT_ARTIFACT_NAME')
* }
*/
File generateBuildFile() {
def result = generateBuildFile("""
|plugins { id 'konan' }
|
|konan.targets = [${targets.collect { "'$it'" }.join(", ")}]
|""".stripMargin()
)
compilationTasks = [":compileKonan", ":build"]
return result
}
/** Generates a source file with the given name and content in the given directory and adds it into srcFiles */
File generateSrcFile(Path parentDirectory, String fileName, String content) {
def result = createFile(parentDirectory, fileName, content)
srcFiles.add(result)
return result
}
/** Generates a source file with the given name and content in the given directory and adds it into srcFiles */
File generateSrcFile(List<String> parentPath, String fileName, String content) {
return generateSrcFile(Paths.get(*parentPath), fileName, content)
}
/** Generates a source file with the given name and content in 'src/main/kotlin' and adds it into srcFiles */
File generateSrcFile(String fileName, String content) {
return generateSrcFile(["src", "main", "kotlin"], fileName, content)
}
/**
* Generates a source file with the given name and default content (see below) in src/main/kotlin
* and adds it into srcFiles.
*
* fun main(args: Array<String>) {
* println(42)
* }
*/
File generateSrcFile(String fileName) {
return generateSrcFile(fileName, DEFAULT_SRC_CONTENT)
}
/** Creates a def-file with the given name and content in src/main/c_interop directory and adds it to defFiles. */
File generateDefFile(String fileName, String content) {
def result = createFile(["src", "main", "c_interop"], fileName, content)
defFiles.add(result)
return result
}
/**
* Creates a def-file with the given name and the default content (see below) in src/main/c_interop directory
* and adds it to defFiles.
*
* headers = stdio.h stdlib.h string.h
*/
File generateDefFile(String fileName = "${DEFAULT_INTEROP_NAME}.def") {
return generateDefFile(fileName, DEFAULT_DEF_CONTENT)
}
/** Generates gradle.properties file with the konan.home and konan.jvmArgs properties set. */
File generatePropertiesFile(String konanHome, String konanJvmArgs = System.getProperty("konan.jvmArgs") ?: "") {
propertiesFile = createFile(projectPath, "gradle.properties", """\
org.jetbrains.kotlin.native.home=$konanHome
${!konanJvmArgs.isEmpty() ? "konan.jvmArgs=$konanJvmArgs\n" : ""}
""".stripIndent())
return propertiesFile
}
/**
* Sets the given setting of the given project extension.
* In other words adds the following string in the build file:
*
* $container.$section.$parameter $value
*/
protected void addSetting(String container, String section, String parameter, String value) {
buildFile.append("$container.$section.$parameter $value\n")
}
/**
* Sets the given setting of the given project extension using the path of the file as a value.
* In other words adds the following string in the build file:
*
* $container.$section.$parameter ${value.canonicalPath.replace(\, \\)}
*/
protected void addSetting(String container, String section, String parameter, File value) {
addSetting(container, section, parameter, "'${escapeBackSlashes(value.canonicalPath)}'")
}
/** Sets the given setting of the given konanArtifact */
void addSetting(String artifactName = DEFAULT_ARTIFACT_NAME, String parameter, String value) {
addSetting("konanArtifacts", artifactName, parameter, value)
}
/** Sets the given setting of the given konanArtifact using the path of the file as a value. */
void addSetting(String artifactName = DEFAULT_ARTIFACT_NAME, String parameter, File value) {
addSetting("konanArtifacts", artifactName, parameter, value)
}
void addLibraryToArtifact(String artifactName = DEFAULT_ARTIFACT_NAME, String library = DEFAULT_INTEROP_NAME) {
addLibraryToArtifactCustom(artifactName, "artifact '$library'")
}
void addLibraryToArtifactCustom(String artifactName = DEFAULT_ARTIFACT_NAME, String closureContent) {
buildFile.append("konanArtifacts.${artifactName}.libraries { $closureContent }\n")
}
/** Returns the path of compileKonan... task for the default artifact. */
static String defaultCompilationTask(String target = HOST) {
return compilationTask(DEFAULT_ARTIFACT_NAME, target)
}
static String defaultInteropTask(String target = HOST) {
return compilationTask(DEFAULT_INTEROP_NAME, target)
}
/** Returns the path of compileKonan... task for the artifact specified. */
static String compilationTask(String artifactName, String target = HOST) {
return ":compileKonan${artifactName.capitalize()}${target.capitalize()}"
}
static String defaultCompilationConfig() {
return artifactConfig(DEFAULT_ARTIFACT_NAME)
}
static String defaultInteropConfig() {
return artifactConfig(DEFAULT_INTEROP_NAME)
}
static String artifactConfig(String artifactName) {
return "konanArtifacts.$artifactName"
}
static String outputAccessCode(String artifact, String target = HOST) {
return "${artifactConfig(artifact)}.${target}.artifact"
}
void addCompilerArtifact(String name, String content = "", ArtifactType type = ArtifactType.PROGRAM) {
def newTasks = targets.collect { compilationTask(name, it) } + ":compileKonan${name.capitalize()}".toString()
buildFile.append("konanArtifacts { $type('$name') }\n")
if (type == ArtifactType.INTEROP) {
defFiles += generateDefFile("${name}.def", content)
interopTasks += newTasks
} else {
def src = generateSrcFile(projectPath.resolve("src/$name/kotlin"), "source.kt", content)
addSetting(name, "srcFiles", src)
srcFiles += src
compilationTasks += newTasks
}
}
/** Creates a project with default build and source files. */
static KonanProject create(File projectDir,
ArtifactType artifactType = ArtifactType.PROGRAM,
List<String> targets = [HOST]) {
return createEmpty(projectDir, targets) { KonanProject p ->
p.addCompilerArtifact(DEFAULT_ARTIFACT_NAME, DEFAULT_SRC_CONTENT, artifactType)
}
}
/** Creates a project with default build and source files. */
static KonanProject create(File projectDir,
ArtifactType artifactType = ArtifactType.PROGRAM,
List<String> targets = [HOST],
Closure config) {
def result = create(projectDir, artifactType, targets)
config(result)
return result
}
static KonanProject createWithInterop(File projectDir,
ArtifactType mainArtifactType = ArtifactType.PROGRAM,
List<String> targets = [HOST]) {
return create(projectDir, mainArtifactType, targets) { KonanProject p ->
p.addCompilerArtifact(DEFAULT_INTEROP_NAME, DEFAULT_DEF_CONTENT, ArtifactType.INTEROP)
p.addLibraryToArtifact()
}
}
static KonanProject createWithInterop(File projectDir,
ArtifactType mainArtifactType = ArtifactType.PROGRAM,
List<String> targets = [HOST],
Closure config) {
def result = createWithInterop(projectDir, mainArtifactType, targets)
config(result)
return result
}
/** Creates a project with the default build file and without any source files. */
static KonanProject createEmpty(File projectDir, List<String> targets = [HOST]) {
def result = new KonanProject(projectDir, targets)
result.with {
generateFolders()
generateBuildFile()
generatePropertiesFile(konanHome)
generateSettingsFile("")
}
return result
}
/** Creates a project with the default build file and without any source files. */
static KonanProject createEmpty(File projectDir, List<String> targets = [HOST], Closure config) {
def result = createEmpty(projectDir, targets)
config(result)
return result
}
static String escapeBackSlashes(String value) {
return value.replace('\\', '\\\\')
}
static String getKonanHome() {
def konanHome = System.getProperty("konan.home") ?: System.getProperty("org.jetbrains.kotlin.native.home")
if (konanHome == null) {
throw new IllegalStateException("konan.home isn't specified")
}
return konanHome
}
}
@@ -0,0 +1,330 @@
/*
* Copyright 2010-2018 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.gradle.plugin.test
class LibrarySpecification extends BaseKonanSpecification {
def libraries = [
[manualDependsOn: true , code: { l1, l2 ->
"file ${KonanProject.outputAccessCode(l1)}\nfile ${KonanProject.outputAccessCode(l2)}"
}],
[manualDependsOn: true , code: { l1, l2 ->
"files ${KonanProject.outputAccessCode(l1)}, ${KonanProject.outputAccessCode(l2)}"
}],
[manualDependsOn: true , code: { l1, l2 ->
"files project.files(${KonanProject.outputAccessCode(l1)}, ${KonanProject.outputAccessCode(l2)})"
}],
[manualDependsOn: true , code: { l1, l2 -> "klib '$l1'\nklib '$l2'" }],
[manualDependsOn: true , code: { l1, l2 -> "klibs '$l1', '$l2'" }],
[manualDependsOn: false, code: { l1, l2 -> "artifact '$l1'\nartifact '$l2'" }],
[manualDependsOn: false, code: { l1, l2 -> "artifact konanArtifacts.$l1\nartifact konanArtifacts.$l2" }],
]
String createMainWithCalls(List<Tuple2<String, String>> functions, Closure<String> callBuilder) {
def result = new StringBuilder("""
|fun main(args: Array<String>) {\n
""".stripMargin())
functions.forEach {
result.append(callBuilder(it.first))
result.append(callBuilder(it.second))
}
result.append("}")
return result.toString()
}
void createLibraryWithFunction(KonanProject project, String name) {
project.addCompilerArtifact(name, """
|package $name
|
|fun $name() {
| println("$name")
|}
""".stripMargin(), ArtifactType.LIBRARY)
project.addSetting(name, "noDefaultLibs", "true")
project.addSetting(name, "noEndorsedLibs", "true")
}
void createInteropLibrary(KonanProject project, String name) {
project.addCompilerArtifact(name, "headers = math.h", ArtifactType.INTEROP)
project.addSetting(name, "noDefaultLibs", "true")
project.addSetting(name, "noEndorsedLibs", "true")
}
KonanProject createProjectWithLibraries(Closure createLibraryFunction, Closure callBuilder) {
def result = KonanProject.createEmpty(projectDirectory)
def libraryNames = new ArrayList<Tuple2<String, String>>()
for (int i = 0; i < libraries.size(); i++) {
libraryNames.add(new Tuple2("foo$i", "bar$i"))
}
libraryNames.forEach {
createLibraryFunction(result, it.first)
createLibraryFunction(result, it.second)
}
result.addCompilerArtifact("main", createMainWithCalls(libraryNames, callBuilder))
result.addSetting("main", "noDefaultLibs", "true")
result.addSetting("main", "noEndorsedLibs", "true")
for (int i = 0; i < libraries.size(); i++) {
def foo = libraryNames[i].first
def bar = libraryNames[i].second
result.addLibraryToArtifactCustom("main", libraries[i].code(foo, bar) )
if (libraries[i].manualDependsOn) {
result.addSetting("main", "dependsOn", "konanArtifacts.$foo")
result.addSetting("main", "dependsOn", "konanArtifacts.$bar")
}
}
return result
}
KonanProject createProjectWithSimpleLibraries() {
return createProjectWithLibraries(
{ p, n -> createLibraryWithFunction(p, n) },
{ "$it.$it()\n" } )
}
KonanProject createProjectWithInteropLibraries() {
return createProjectWithLibraries(
{ p, n -> createInteropLibrary(p, n) },
{ "println(${it}.cos(0.0))\n" }
)
}
def 'Plugin should support libraries from the same project'() {
expect:
createProjectWithSimpleLibraries()
.createRunner()
.withArguments(KonanProject.compilationTask("main"))
.build()
}
def 'Plugin should support interop libraries from the same project'() {
expect:
createProjectWithInteropLibraries()
.createRunner()
.withArguments(KonanProject.compilationTask("main"))
.build()
}
def 'Plugin should support allLibrariesFrom method for the current project'() {
expect:
def project = KonanProject.createEmpty(projectDirectory) { KonanProject it ->
it.addCompilerArtifact("foo", "fun foo() { println(42) }", ArtifactType.LIBRARY)
it.addSetting("foo", "noDefaultLibs", "true")
it.addSetting("foo", "noEndorsedLibs", "true")
it.addCompilerArtifact("bar", "fun bar() { println(43) }", ArtifactType.LIBRARY)
it.addSetting("bar", "noDefaultLibs", "true")
it.addSetting("bar", "noEndorsedLibs", "true")
it.addCompilerArtifact("main" ,"fun main(args: Array<String>) { foo(); bar() }")
it.addSetting("main", "noDefaultLibs", "true" )
it.addSetting("main", "noEndorsedLibs", "true" )
it.addLibraryToArtifactCustom("main", "allLibrariesFrom project")
}
project.createRunner()
.withArguments(KonanProject.compilationTask("main"))
.build()
}
def 'Plugin should support allLibrariesFrom method for another project'() {
expect:
def project = KonanProject.createEmpty(projectDirectory)
def subproject = KonanProject.createEmpty(project.createSubDir("subproject")) { KonanProject it ->
it.buildFile.write("apply plugin: 'konan'\n")
}
project.settingsFile.append("include ':subproject'")
project.addCompilerArtifact("wrongFoo","fun foo() { println(24) }", ArtifactType.LIBRARY)
project.addSetting("wrongFoo", "noDefaultLibs", "true")
project.addSetting("wrongFoo", "noEndorsedLibs", "true")
subproject.addCompilerArtifact("foo", "fun foo() { println(42) }", ArtifactType.LIBRARY)
subproject.addSetting("foo", "noDefaultLibs", "true")
subproject.addSetting("foo", "noEndorsedLibs", "true")
subproject.addCompilerArtifact("bar", "fun bar() { println(43) }", ArtifactType.LIBRARY)
subproject.addSetting("bar", "noDefaultLibs", "true")
subproject.addSetting("bar", "noEndorsedLibs", "true")
project.addCompilerArtifact("main" ,"fun main(args: Array<String>) { foo(); bar() }")
project.addSetting("main", "noDefaultLibs", "true" )
project.addSetting("main", "noEndorsedLibs", "true" )
project.addLibraryToArtifactCustom("main", "allLibrariesFrom project('subproject')")
project.createRunner()
.withArguments(KonanProject.compilationTask("main"))
.build()
}
def 'Plugin should support allInteropLibrariesFrom method for the current project'() {
expect:
def project = KonanProject.createEmpty(projectDirectory)
def subproject = KonanProject.createEmpty(project.createSubDir("subproject")) { KonanProject it ->
it.buildFile.write("apply plugin: 'konan'\n")
}
project.settingsFile.append("include ':subproject'")
project.addCompilerArtifact("wrongFoo1", "fun foo() { println(42) }", ArtifactType.LIBRARY)
project.addSetting("wrongFoo1", "noDefaultLibs", "true")
project.addSetting("wrongFoo1", "noEndorsedLibs", "true")
subproject.addCompilerArtifact("wrongFoo2", "fun foo() { println(42) }", ArtifactType.LIBRARY)
subproject.addSetting("wrongFoo2", "noDefaultLibs", "true")
subproject.addSetting("wrongFoo2", "noEndorsedLibs", "true")
subproject.addCompilerArtifact("math1", "headers = math.h", ArtifactType.INTEROP)
subproject.addSetting("math1", "noDefaultLibs", "true")
subproject.addSetting("math1", "noEndorsedLibs", "true")
subproject.addCompilerArtifact("math2", "headers = math.h", ArtifactType.INTEROP)
subproject.addSetting("math2", "noDefaultLibs", "true")
subproject.addSetting("math2", "noEndorsedLibs", "true")
project.addCompilerArtifact("main" ,"""
|fun foo() {}
|
|fun main(args: Array<String>) { foo(); math1.cos(0.0); math2.cos(0.0) }
""".stripMargin())
project.addSetting("main", "noDefaultLibs", "true" )
project.addSetting("main", "noEndorsedLibs", "true" )
project.addLibraryToArtifactCustom("main", "allInteropLibrariesFrom project('subproject')")
project.createRunner()
.withArguments(KonanProject.compilationTask("main"))
.build()
}
def 'Plugin should support allInteropLibrariesFrom method for another projecct'() {
expect:
def project = KonanProject.createEmpty(projectDirectory) { KonanProject it ->
it.addCompilerArtifact("wrongFoo", "fun foo() { println(42) }", ArtifactType.LIBRARY)
it.addSetting("wrongFoo", "noDefaultLibs", "true")
it.addSetting("wrongFoo", "noEndorsedLibs", "true")
it.addCompilerArtifact("math1", "headers = math.h", ArtifactType.INTEROP)
it.addSetting("math1", "noDefaultLibs", "true")
it.addSetting("math1", "noEndorsedLibs", "true")
it.addCompilerArtifact("math2", "headers = math.h", ArtifactType.INTEROP)
it.addSetting("math2", "noDefaultLibs", "true")
it.addSetting("math2", "noEndorsedLibs", "true")
it.addCompilerArtifact("main" ,"""
|fun foo() {}
|
|fun main(args: Array<String>) { foo(); math1.cos(0.0); math2.cos(0.0) }
""".stripMargin())
it.addSetting("main", "noDefaultLibs", "true" )
it.addSetting("main", "noEndorsedLibs", "true" )
it.addLibraryToArtifactCustom("main", "allInteropLibrariesFrom project")
}
project.createRunner()
.withArguments(KonanProject.compilationTask("main"))
.build()
}
def 'Plugin should support custom repositories for libraries'() {
expect:
def project = KonanProject.createEmpty(projectDirectory) { KonanProject it ->
it.addCompilerArtifact("foo", "fun foo() { println(42) }", ArtifactType.LIBRARY)
it.addSetting("foo", "noDefaultLibs", "true")
it.addSetting("foo", "noEndorsedLibs", "true")
it.addSetting("foo", "baseDir", "file('out')")
it.addCompilerArtifact("main" ,"fun main(args: Array<String>) { foo() }")
it.addSetting("main", "noDefaultLibs", "true" )
it.addSetting("main", "noEndorsedLibs", "true" )
it.addSetting("main", "dependsOn", "konanArtifacts.foo.$KonanProject.HOST")
it.addLibraryToArtifactCustom("main", "klib 'foo'")
it.addLibraryToArtifactCustom("main", "useRepo 'out/$KonanProject.HOST'")
}
project.createRunner()
.withArguments(KonanProject.compilationTask("main"), "-i")
.build()
}
def 'Plugin should support library dependencies in the same project'() {
expect:
def project = KonanProject.createEmpty(projectDirectory) { KonanProject it ->
it.addCompilerArtifact("foo", "fun foo() { println(42) }", ArtifactType.LIBRARY)
it.addSetting("foo", "noDefaultLibs", "true")
it.addSetting("foo", "noEndorsedLibs", "true")
it.addCompilerArtifact("bar", "fun bar() { println(43) }", ArtifactType.LIBRARY)
it.addSetting("bar", "noDefaultLibs", "true")
it.addSetting("bar", "noEndorsedLibs", "true")
it.addLibraryToArtifact("bar", "foo")
it.addCompilerArtifact("main" ,"fun main(args: Array<String>) { foo(); bar() }")
it.addSetting("main", "noDefaultLibs", "true" )
it.addSetting("main", "noEndorsedLibs", "true" )
it.addLibraryToArtifact("main", "bar")
}
project.createRunner()
.withArguments(KonanProject.compilationTask("main"), "-i")
.build()
}
def 'Plugin should support library dependencies from other projects'() {
expect:
def project = KonanProject.createEmpty(projectDirectory)
def subproject1 = KonanProject.createEmpty(project.createSubDir("subproject1")) { KonanProject it ->
it.buildFile.write("apply plugin: 'konan'\n")
}
def subproject2 = KonanProject.createEmpty(project.createSubDir("subproject2")) { KonanProject it ->
it.buildFile.write("apply plugin: 'konan'\n")
}
project.settingsFile.append("include ':subproject1'\ninclude ':subproject2'")
subproject1.addCompilerArtifact("foo", "fun foo() { println(42) }", ArtifactType.LIBRARY)
subproject1.addSetting("foo", "noDefaultLibs", "true")
subproject1.addSetting("foo", "noEndorsedLibs", "true")
subproject2.addCompilerArtifact("bar", "fun bar() { println(43) }", ArtifactType.LIBRARY)
subproject2.addSetting("bar", "noDefaultLibs", "true")
subproject2.addSetting("bar", "noEndorsedLibs", "true")
subproject2.addLibraryToArtifactCustom(
"bar", "artifact rootProject.project('subproject1'), 'foo'"
)
project.addCompilerArtifact("main" ,"fun main(args: Array<String>) { foo(); bar() }")
project.addSetting("main", "noDefaultLibs", "true" )
project.addSetting("main", "noEndorsedLibs", "true" )
project.addLibraryToArtifactCustom(
"main", "artifact project('subproject2'), 'bar'"
)
project.createRunner()
.withArguments(KonanProject.compilationTask("main"))
.build()
}
// TODO: Add tests for incorrect cases (e.g. attempt to use an executable as a library)
}
@@ -0,0 +1,392 @@
/*
* Copyright 2010-2018 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.gradle.plugin.test
import spock.lang.Ignore
import java.nio.file.Files
import java.nio.file.Paths
class MultiplatformSpecification extends BaseKonanSpecification {
public static final String KOTLIN_VERSION = System.getProperty("kotlin.version")
public static final String KOTLIN_REPO = System.getProperty("kotlin.repo")
public static final String DEFAULT_COMMON_BUILD_FILE_CONTENT = """\
buildscript {
repositories {
maven {
url = '$KOTLIN_REPO'
}
maven {
url = 'https://cache-redirector.jetbrains.com/jcenter'
}
jcenter()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$KOTLIN_VERSION"
}
}
apply plugin: 'kotlin-platform-common'
repositories {
maven {
url = '$KOTLIN_REPO'
}
maven {
url = 'https://cache-redirector.jetbrains.com/jcenter'
}
jcenter()
}
dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib-common:$KOTLIN_VERSION"
}
""".stripIndent()
static File createCommonProject(KonanProject platformProject,
String commonProjectName = "common",
String commonBuildFileContent = DEFAULT_COMMON_BUILD_FILE_CONTENT) {
def commonDirectory = platformProject.createSubDir(commonProjectName)
def commonBuildFile = Paths.get(commonDirectory.absolutePath, "build.gradle")
commonBuildFile.write(commonBuildFileContent)
platformProject.settingsFile.append("include ':$commonProjectName'\n")
return commonDirectory
}
static File createCommonSource(File commonDirectory,
Iterable<String> subdirectory,
String fileName,
String content) {
def commonSrcDir = commonDirectory.toPath().resolve(Paths.get(*subdirectory))
def commonSource = commonSrcDir.resolve(fileName)
Files.createDirectories(commonSrcDir)
commonSource.write(content)
return commonSource.toFile()
}
def 'Plugin should support multiplatform projects'() {
expect:
def project = KonanProject.createEmpty(projectDirectory) { KonanProject it ->
def commonDirectory = createCommonProject(it)
createCommonSource(commonDirectory,
["src", "main", "kotlin"],
"common.kt",
"""\
@file:Suppress("EXPERIMENTAL_API_USAGE_ERROR")
@OptionalExpectation
expect annotation class Optional()
@Optional
fun opt() = 42
expect fun foo(): Int
""".stripIndent()
)
it.generateSrcFile("platform.kt", "actual fun foo() = 42")
it.buildFile.append("""
konanArtifacts {
library('foo') {
enableMultiplatform true
}
}
dependencies {
expectedBy project(':common')
}
""".stripIndent())
}
project.createRunner().withArguments(":build").build()
}
def 'Multiplatform projects should be disabled by default'() {
expect:
def project = KonanProject.createEmpty(projectDirectory) { KonanProject it ->
def commonDirectory = createCommonProject(it)
createCommonSource(commonDirectory,
["src", "main", "kotlin"],
"common.kt",
"expect fun foo(): Int")
it.generateSrcFile("platform.kt", "fun foo() = 42")
it.buildFile.append("""
konanArtifacts {
library('foo') {}
}
dependencies {
expectedBy project(':common')
}
""".stripIndent())
}
project.createRunner().withArguments(":build").build()
}
def 'Plugin should use the \'main\' source set as a default common source set'() {
expect:
def project = KonanProject.createEmpty(projectDirectory) { KonanProject it ->
def commonDirectory = createCommonProject(it)
Paths.get(commonDirectory.absolutePath, "build.gradle").append("""
sourceSets {
common.kotlin.srcDir 'src/common/kotlin'
}
""".stripIndent())
createCommonSource(commonDirectory,
["src", "main", "kotlin"],
"common.kt",
"expect fun foo(): Int")
createCommonSource(commonDirectory,
["src", "common", "kotlin"],
"common.kt",
"expect fun bar(): Int")
it.generateSrcFile("platform.kt", "actual fun foo() = 42")
it.buildFile.append("""
konanArtifacts {
library('foo') {
enableMultiplatform true
}
}
dependencies {
expectedBy project(':common')
}
""".stripIndent())
}
project.createRunner().withArguments(":build").build()
}
def 'Plugin should allow a user to specify custom common source set'() {
expect:
def project = KonanProject.createEmpty(projectDirectory) { KonanProject it ->
def commonDirectory = createCommonProject(it)
Paths.get(commonDirectory.absolutePath, "build.gradle").append("""
sourceSets {
common.kotlin.srcDir 'src/common/kotlin'
}
""".stripIndent())
createCommonSource(commonDirectory,
["src", "common", "kotlin"],
"common.kt",
"expect fun bar(): Int")
it.generateSrcFile("platform.kt", "actual fun bar() = 42")
it.buildFile.append("""
konanArtifacts {
library('foo') {
enableMultiplatform true
commonSourceSets 'common'
}
}
dependencies {
expectedBy project(':common')
}
""".stripIndent())
}
project.createRunner().withArguments(":build").build()
}
def 'Plugin should allow setting several common source sets'() {
expect:
def project = KonanProject.createEmpty(projectDirectory) { KonanProject it ->
def commonDirectory = createCommonProject(it)
Paths.get(commonDirectory.absolutePath, "build.gradle").append("""
sourceSets {
common.kotlin.srcDir 'src/common/kotlin'
}
""".stripIndent())
createCommonSource(commonDirectory,
["src", "common", "kotlin"],
"common.kt",
"expect fun bar(): Int")
createCommonSource(commonDirectory,
["src", "main", "kotlin"],
"main.kt",
"expect fun foo() : Int")
it.generateSrcFile("platform.kt", "actual fun bar() = 42\nactual fun foo() = 43")
it.buildFile.append("""
konanArtifacts {
library('foo') {
enableMultiplatform true
commonSourceSets 'common', 'main'
}
}
dependencies {
expectedBy project(':common')
}
""".stripIndent())
}
project.createRunner().withArguments(":build").build()
}
def 'Build should fail if the expectedBy dependency is not a project one'() {
when:
def project = KonanProject.createEmpty(projectDirectory) { KonanProject it ->
def commonDirectory = createCommonProject(it)
createCommonSource(commonDirectory,
["src", "main", "kotlin"],
"common.kt",
"expect fun foo(): Int")
it.generateSrcFile("platform.kt", "actual fun foo() = 42")
it.buildFile.append("""
konanArtifacts {
library('foo') {
enableMultiplatform true
}
}
dependencies {
expectedBy files('common/src/main/kotlin/common.kt')
}
""".stripIndent())
}
def result = project.createRunner().withArguments(":build").buildAndFail()
then:
result.output.contains("dependency is not a project: ")
}
def 'Build should support several expectedBy-dependencies'() {
expect:
def project = KonanProject.createEmpty(projectDirectory) { KonanProject it ->
def commonDirectory = createCommonProject(it, "commonFoo")
createCommonSource(commonDirectory,
["src", "main", "kotlin"],
"common.kt",
"expect fun foo(): Int")
commonDirectory = createCommonProject(it, "commonBar")
createCommonSource(commonDirectory,
["src", "main", "kotlin"],
"common.kt",
"expect fun bar(): Int")
it.generateSrcFile("platform.kt", "actual fun foo() = 0\nactual fun bar() = 0")
it.buildFile.append("""
konanArtifacts {
library('foo') {
enableMultiplatform true
}
}
dependencies {
expectedBy project(':commonFoo')
expectedBy project(':commonBar')
}
""".stripIndent())
}
project.createRunner().withArguments(":build").build()
}
def 'Build should fail if the common project has no common plugin'() {
when:
def project = KonanProject.createEmpty(projectDirectory) { KonanProject it ->
def commonDirectory = createCommonProject(it,"common", "")
createCommonSource(commonDirectory,
["src", "main", "kotlin"],
"common.kt",
"expect fun foo(): Int")
it.generateSrcFile("platform.kt", "actual fun bar() = 42")
it.buildFile.append("""
konanArtifacts {
library('foo') {
enableMultiplatform true
}
}
dependencies {
expectedBy project(':common')
}
""".stripIndent())
}
def result = project.createRunner().withArguments(":build").buildAndFail()
then:
result.output.contains("has an 'expectedBy' dependency to non-common project")
}
@Ignore("TODO in the Big Kotlin plugin")
def 'Build should fail if custom common source set doesn\'t exist'() {
when:
def project = KonanProject.createEmpty(projectDirectory) { KonanProject it ->
def commonDirectory = createCommonProject(it)
createCommonSource(commonDirectory,
["src", "main", "kotlin"],
"common.kt",
"expect fun foo(): Int")
it.generateSrcFile("platform.kt", "actual fun bar() = 42")
it.buildFile.append("""
konanArtifacts {
library('foo') {
enableMultiplatform true
commonSourceSets 'common'
}
}
dependencies {
expectedBy project(':common')
}
""".stripIndent())
}
def result = project.createRunner().withArguments(":build").buildAndFail()
then:
result.output.contains("Cannot find a source set with name 'common' in a common project")
}
def 'Setting custom source set should enable the multiplatform support'() {
expect:
def project = KonanProject.createEmpty(projectDirectory) { KonanProject it ->
def commonDirectory = createCommonProject(it)
Paths.get(commonDirectory.absolutePath, "build.gradle").append("""
sourceSets {
common.kotlin.srcDir 'src/common/kotlin'
}
""".stripIndent())
createCommonSource(commonDirectory,
["src", "common", "kotlin"],
"common.kt",
"expect fun bar(): Int")
it.generateSrcFile("platform.kt", "actual fun bar() = 42")
it.buildFile.append("""
konanArtifacts {
library('foo') {
commonSourceSets 'common'
}
}
dependencies {
expectedBy project(':common')
}
""".stripIndent())
}
project.createRunner().withArguments(":build").build()
}
}
@@ -0,0 +1,130 @@
/*
* Copyright 2010-2018 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.gradle.plugin.test
import org.gradle.testkit.runner.TaskOutcome
import org.jetbrains.kotlin.konan.target.Distribution
import org.jetbrains.kotlin.konan.target.PlatformManager
class PathSpecification extends BaseKonanSpecification {
boolean fileExists(KonanProject project, String path) {
project.konanBuildDir.toPath().resolve(path).toFile().exists()
}
def platformManager = new PlatformManager(new Distribution(KonanProject.konanHome, false, null), false)
def 'Plugin should provide a correct path to the artifacts created'() {
expect:
def project = KonanProject.createEmpty(
projectDirectory,
platformManager.filteredOutEnabledButNotSupported.collect { t -> t.visibleName }
) { KonanProject it ->
it.generateSrcFile("main.kt")
it.generateDefFile("interop.def", "")
it.buildFile.append("""
konanArtifacts {
program('program')
library('library')
bitcode('bitcode')
interop('interop')
framework('framework')
dynamic('dynamic')
}
task checkArtifacts(type: DefaultTask) {
dependsOn(':build')
doLast {
for(artifact in konanArtifacts) {
for (target in artifact) {
if (!target.artifact.exists()) throw new Exception("Artifact doesn't exist. Type: \${artifact.name}, target: \${target.target}")
}
}
for (target in konanArtifacts.dynamic) {
if (!target.headerFile.exists()) throw new Exception("Header file doesn't exist. Target: \${target.target}")
}
}
}
""".stripIndent())
}
project.createRunner().withArguments("checkArtifacts").build()
}
def 'Plugin should create all necessary directories'() {
when:
def project = KonanProject.createWithInterop(projectDirectory)
project.addCompilerArtifact("lib", "fun foo() {}", ArtifactType.LIBRARY)
project.addCompilerArtifact("bit", "fun bar() {}", ArtifactType.BITCODE)
project.createRunner().withArguments('build').build()
then:
project.konanBuildDir.toPath().resolve("bin/$KonanProject.HOST").toFile().listFiles().findAll {
File it -> it.file && it.name.matches("^${KonanProject.DEFAULT_ARTIFACT_NAME}\\.[^.]+")
}.size() > 0
fileExists(project, "libs/$KonanProject.HOST/${KonanProject.DEFAULT_INTEROP_NAME}.klib")
fileExists(project, "libs/$KonanProject.HOST/lib.klib")
fileExists(project, "bitcode/$KonanProject.HOST/bit.bc")
}
def 'Plugin should stop building if the compiler classpath is empty'() {
when:
def project = KonanProject.create(projectDirectory)
project.propertiesFile.write("konan.home=fakepath")
def result = project.createRunner().withArguments('build').buildAndFail()
def task = result.task(project.defaultCompilationTask())
then:
task == null || task.outcome == TaskOutcome.FAILED
}
def 'Plugin should stop building if the stub generator classpath is empty'() {
when:
def project = KonanProject.createWithInterop(projectDirectory)
project.propertiesFile.write("konan.home=fakepath")
def result = project.createRunner().withArguments('build').buildAndFail()
def task = result.task(project.compilationTask(KonanProject.DEFAULT_INTEROP_NAME))
then:
task == null || task.outcome == TaskOutcome.FAILED
}
def 'Plugin should remove custom output directories'() {
when:
def customOutputDir = projectDirectory.toPath().resolve("foo").toFile()
def project = KonanProject.create(projectDirectory, ArtifactType.LIBRARY) { KonanProject it ->
it.addSetting("baseDir", customOutputDir)
}
def res1 = project.createRunner().withArguments("build").build()
def artifactExistsAfterBuild = customOutputDir.toPath()
.resolve("${KonanProject.HOST}/${KonanProject.DEFAULT_ARTIFACT_NAME}.klib").toFile()
.exists()
def res2 = project.createRunner().withArguments("clean").build()
def artifactExistsAfterClean = customOutputDir.toPath()
.resolve("${KonanProject.HOST}/${KonanProject.DEFAULT_ARTIFACT_NAME}.klib").toFile()
.exists()
then:
res1.taskPaths(TaskOutcome.SUCCESS).containsAll(project.buildingTasks)
res2.taskPaths(TaskOutcome.SUCCESS).contains(":clean")
artifactExistsAfterBuild
!artifactExistsAfterClean
}
}
@@ -0,0 +1,73 @@
/*
* Copyright 2010-2018 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.gradle.plugin.test
import org.gradle.testkit.runner.TaskOutcome
class RegressionSpecification extends BaseKonanSpecification {
def 'KT-19916'() {
when:
def project = KonanProject.createEmpty(getProjectDirectory()) { KonanProject prj ->
prj.generateSettingsFile("include ':subproject'")
def subprojectDir = prj.projectPath.resolve("subproject").toFile()
subprojectDir.mkdirs()
subprojectDir.toPath().resolve("build.gradle").write("""
dependencies {
libs gradleApi()
}
""".stripIndent())
prj.buildFile.append("""
subprojects {
apply plugin: 'konan'
apply plugin: Foo
}
class Foo implements Plugin<Project> {
void apply(Project project) {
project.configurations.maybeCreate("libs")
}
}
""".stripIndent())
}
def result = project.createRunner().withArguments('tasks').build()
then:
result.task(':tasks').outcome == TaskOutcome.SUCCESS
}
// Ensure gradle plugin fails in case of linker errors.
def 'KT-20192'() {
when:
def project = KonanProject.createEmpty(getProjectDirectory()) { KonanProject prj ->
prj.addCompilerArtifact(KonanProject.DEFAULT_ARTIFACT_NAME,"""
external fun foo()
fun main(args: Array<String>) {
foo()
}
""", ArtifactType.PROGRAM)
}
def result = project.createRunner().withArguments('build').buildAndFail()
then:
result.taskPaths(TaskOutcome.FAILED).contains(project.defaultCompilationTask())
}
}
@@ -0,0 +1,167 @@
/*
* Copyright 2010-2018 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.gradle.plugin.test
import org.gradle.testkit.runner.BuildResult
import org.gradle.testkit.runner.TaskOutcome
import org.jetbrains.kotlin.konan.target.HostManager
import org.jetbrains.kotlin.konan.target.KonanTarget
import spock.lang.Requires
import spock.lang.Unroll
class TaskSpecification extends BaseKonanSpecification {
def 'Configs should allow user to add dependencies to them'() {
when:
def project = KonanProject.createWithInterop(projectDirectory, ArtifactType.LIBRARY)
project.buildFile.append("""
task beforeInterop(type: DefaultTask) { doLast { println("Before Interop") } }
task beforeCompilation(type: DefaultTask) { doLast { println("Before compilation") } }
""".stripIndent())
project.addSetting(KonanProject.DEFAULT_INTEROP_NAME,"dependsOn", "beforeInterop")
project.addSetting("dependsOn", "beforeCompilation")
def result = project.createRunner().withArguments('build').build()
then:
def beforeInterop = result.task(":beforeInterop")
beforeInterop != null && beforeInterop.outcome == TaskOutcome.SUCCESS
def beforeCompilation = result.task(":beforeCompilation")
beforeCompilation != null && beforeCompilation.outcome == TaskOutcome.SUCCESS
}
def 'Compiler should print time measurements if measureTime flag is set'() {
when:
def project = KonanProject.create(projectDirectory, ArtifactType.LIBRARY)
project.addSetting("measureTime", "true")
def result = project.createRunner().withArguments('build').build()
then:
result.output.findAll(~/Frontend builds AST:\s+\d+\s+msec/).size() == 1
result.output.findAll(~/IR Lowering:\s+\d+\s+msec/).size() == 1
}
@Unroll('Plugin should support #option option for cinterop')
def 'Plugin should support includeDir option for cinterop'() {
expect:
def project = KonanProject.createEmpty(projectDirectory) { KonanProject it ->
it.addCompilerArtifact("interopLib", "headers=foo.h\n$headerFilter", ArtifactType.INTEROP)
it.generateSrcFile(it.projectPath, "foo.h", "#include <bar.h>")
def fooDir = it.projectPath.resolve("foo")
it.generateSrcFile(fooDir, "bar.h", "const int foo = 5;")
it.addSetting("interopLib", option, fooDir.toFile())
it.addSetting("interopLib", option, it.projectDir)
}
project.createRunner().withArguments("build").build()
where:
option | headerFilter
"includeDirs.headerFilterOnly" | "headerFilter=foo.h bar.h"
"includeDirs.allHeaders" | ""
"includeDirs" | ""
}
@Requires({ HostManager.host instanceof KonanTarget.MACOS_X64 })
def 'Plugin should create framework tasks only for Apple targets'() {
when:
def project = KonanProject.createEmpty(projectDirectory) { KonanProject it ->
it.buildFile.append("""
konan.targets = ['wasm32', 'macbook', 'iphone', 'iphone_sim']
konanArtifacts {
framework('foo')
}
""".stripIndent())
}
def result = project.createRunner().withArguments('tasks', '--all').build()
then:
!compilationTaskExists(result,'foo', 'wasm32')
compilationTaskExists (result,'foo', 'macbook')
compilationTaskExists (result,'foo', 'iphone')
compilationTaskExists (result,'foo', 'iphone_sim')
}
def 'Plugin should support different targets for different artifacts'() {
when:
def project = KonanProject.createEmpty(projectDirectory, ['host']) { KonanProject it ->
it.buildFile.append("""
konanArtifacts {
program('defaultTarget')
program('customTarget', targets: ['wasm32'])
program('customTargets', targets: ['host', 'wasm32'])
}
""".stripIndent())
}
def result = project.createRunner().withArguments('tasks', '--all').build()
def hostName = HostManager.hostName
then:
compilationTaskExists (result, 'defaultTarget', hostName)
!compilationTaskExists(result, 'defaultTarget', 'wasm32')
!compilationTaskExists(result, 'customTarget', hostName)
compilationTaskExists (result, 'customTarget', 'wasm32')
compilationTaskExists (result, 'customTargets', hostName)
compilationTaskExists (result, 'customTargets', 'wasm32')
}
def 'Plugin should not create dynamic task for wasm'() {
when:
def project = KonanProject.createEmpty(projectDirectory) { KonanProject it ->
it.buildFile.append("""
konan.targets = ['wasm32']
konanArtifacts {
dynamic('foo')
}
""".stripIndent())
}
def result = project.createRunner().withArguments('tasks', '--all').build()
then:
!compilationTaskExists(result, 'foo', 'wasm32')
}
boolean taskExists(BuildResult result, String taskName) {
def taskNameForSearch = taskName.startsWith(':') ? taskName.substring(1) : taskName
return result.output =~ "\\s$taskNameForSearch\\s"
}
boolean compilationTaskExists(BuildResult result, String artifactName, String targetName) {
return taskExists(result, KonanProject.compilationTask(artifactName, targetName))
}
BuildResult failOnPropertyAccess(KonanProject project, String property) {
project.buildFile.append("""
task testTask(type: DefaultTask) {
doLast {
println(${project.defaultInteropConfig()}.$property)
}
}
""".stripIndent())
return project.createRunner().withArguments("testTask").buildAndFail()
}
BuildResult failOnTaskAccess(KonanProject project, String task) {
project.buildFile.append("""
task testTask(type: DefaultTask) {
dependsOn $task
}
""".stripIndent())
return project.createRunner().withArguments("testTask").buildAndFail()
}
}
@@ -0,0 +1,48 @@
/*
* Copyright 2010-2018 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.gradle.plugin.test
import org.junit.Rule
import org.junit.rules.TemporaryFolder
import java.io.File
import java.net.URI
import kotlin.test.Test
import kotlin.test.assertTrue
open class CompatibilityTests {
val tmpFolder = TemporaryFolder()
@Rule get
val projectDirectory: File
get() = tmpFolder.root
@Test
fun `Plugin should fail if running with Gradle prior to the required one`() {
val project = KonanProject.createEmpty(projectDirectory)
val result = project
.createRunner()
.withGradleDistribution(URI.create(
"https://cache-redirector.jetbrains.com/services.gradle.org/distributions/gradle-4.5-bin.zip"
))
.withArguments("tasks")
.buildAndFail()
println(result.output)
assertTrue("Build doesn't show the warning message") {
result.output.contains("Kotlin/Native Gradle plugin is incompatible with this version of Gradle.")
}
}
}
@@ -0,0 +1,204 @@
/*
* Copyright 2010-2018 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.gradle.plugin.test
import org.jetbrains.kotlin.gradle.plugin.test.KonanProject.escapeBackSlashes
import org.jetbrains.kotlin.konan.target.HostManager
import org.jetbrains.kotlin.konan.target.KonanTarget
import org.junit.Rule
import org.junit.rules.TemporaryFolder
import org.junit.runner.RunWith
import org.tools4j.spockito.Spockito
import java.io.File
import kotlin.test.Test
@RunWith(Spockito::class)
open class PropertiesAsEnvVariablesTest {
val tmpFolder = TemporaryFolder()
@Rule get
val projectDirectory: File
get() = tmpFolder.root
private fun artifactFileName(baseName: String, type: ArtifactType, target: KonanTarget = HostManager.host): String {
var suffix = ""
var prefix = ""
when (type) {
ArtifactType.PROGRAM -> suffix = target.family.exeSuffix
ArtifactType.INTEROP,
ArtifactType.LIBRARY -> suffix = "klib"
ArtifactType.BITCODE -> suffix = "bc"
ArtifactType.FRAMEWORK -> suffix = "framework"
ArtifactType.DYNAMIC -> {
prefix = target.family.dynamicPrefix
suffix = target.family.dynamicSuffix
}
ArtifactType.STATIC -> {
prefix = target.family.staticPrefix
suffix = target.family.staticSuffix
}
}
return "$prefix${baseName}.$suffix"
}
private fun assertFileExists(directory: File, filename: String) = assert(directory.list().contains(filename)) {
"No such file: $filename in directory: ${directory.absolutePath}"
}
@Test
@Spockito.Unroll(
"|property |value |assertion |message |",
"|konan.debugging.symbols |YES |it.enableDebug |Debug should be enabled |",
"|konan.debugging.symbols |true |it.enableDebug |Debug should be enabled |",
"|konan.debugging.symbols |NO |!it.enableDebug |Debug should be disabled |",
"|konan.debugging.symbols |false |!it.enableDebug |Debug should be disabled |",
"|konan.optimizations.enable |YES |it.enableOptimizations |Opts should be enabled |",
"|konan.optimizations.enable |true |it.enableOptimizations |Opts should be enabled |",
"|konan.optimizations.enable |NO |!it.enableOptimizations |Opts should be disabled |",
"|konan.optimizations.enable |false |!it.enableOptimizations |Opts should be disabled |"
)
@Spockito.Name("[{row}]: {variable}={value}")
fun `Plugin should support enabling and disabling debug and opt options via a project property`(
property: String,
value: String,
assertion: String,
message: String
) {
val project = KonanProject.createEmpty(projectDirectory)
project.buildFile.appendText("""
apply plugin: 'konan'
konanArtifacts {
library('main')
}
task assertEnableDebug {
doLast {
konanArtifacts.main.forEach {
if (!($assertion)) throw new AssertionError("$message for ${'$'}it.name")
}
}
}
""".trimIndent())
project.createRunner()
.withArguments("assertEnableDebug", "-P${property}=${value}")
.build()
}
@Test
fun `Plugin should support setting destination directory via a project property`() {
val project = KonanProject.createEmpty(projectDirectory)
val newDestinationDir = project.createSubDir("newDestination")
val newDestinationPath = newDestinationDir.absolutePath
project.buildFile.appendText("""
apply plugin: 'konan'
konanArtifacts {
program('program')
library('library')
dynamic('dynamic')
framework('framework')
}
task assertDestinationDir {
doLast {
konanArtifacts.forEach { artifact ->
artifact.forEach {
if (it.destinationDir.absolutePath != '${escapeBackSlashes(newDestinationPath)}'){
throw new AssertionError("Unexpected destination dir for ${'$'}it.name\\n" +
"expected: ${escapeBackSlashes(newDestinationPath)}\\n" +
"actual: ${'$'}it.destinationDir")
}
}
}
}
}
""".trimIndent())
project.generateSrcFile("main.kt")
project.createRunner()
.withArguments("assertDestinationDir", "build", "-Pkonan.configuration.build.dir=$newDestinationPath")
.build()
assertFileExists(newDestinationDir, artifactFileName("program", ArtifactType.PROGRAM))
assertFileExists(newDestinationDir, artifactFileName("library", ArtifactType.LIBRARY))
assertFileExists(newDestinationDir, artifactFileName("dynamic", ArtifactType.DYNAMIC))
if (HostManager.hostIsMac) {
assertFileExists(newDestinationDir, artifactFileName("framework", ArtifactType.FRAMEWORK))
}
}
@Test
fun `Plugin should rerun tasks if konan_configuration_build_dir has been changed`() {
val project = KonanProject.createEmpty(projectDirectory)
val destination1 = project.createSubDir("destination1", "subdir")
val destination2 = project.createSubDir("destination2", "subdir")
project.buildFile.appendText("""
apply plugin: 'konan'
konanArtifacts {
library('main')
}
""".trimIndent())
project.generateSrcFile("main.kt")
project.createRunner()
.withArguments("build", "-Pkonan.configuration.build.dir=${destination1.absolutePath}")
.build()
project.createRunner()
.withArguments("build", "-Pkonan.configuration.build.dir=${destination2.absolutePath}")
.build()
assertFileExists(destination1, artifactFileName("main", ArtifactType.LIBRARY))
assertFileExists(destination2, artifactFileName("main", ArtifactType.LIBRARY))
}
@Test
fun `Up-to-date checks should work with different directories for different targets`() {
val project = KonanProject.createEmpty(projectDirectory)
val fooDir = project.createSubDir("foo")
val barDir = project.createSubDir("bar")
project.buildFile.appendText("""
apply plugin: 'konan'
konanArtifacts {
library('foo')
library('bar')
}
task assertUpToDate {
dependsOn 'compileKonanFoo'
doLast {
if (!konanArtifacts.foo.getByTarget('host').state.upToDate) {
throw new AssertionError("Compilation task is not up-to-date")
}
}
}
""".trimIndent())
project.generateSrcFile("main.kt")
project.createRunner()
.withArguments("compileKonanFoo", "-Pkonan.configuration.build.dir=${fooDir.absolutePath}")
.build()
project.createRunner()
.withArguments("compileKonanBar", "-Pkonan.configuration.build.dir=${barDir.absolutePath}")
.build()
project.createRunner()
.withArguments("assertUpToDate", "-Pkonan.configuration.build.dir=${fooDir.absolutePath}")
.build()
}
}
@@ -0,0 +1,67 @@
/*
* Copyright 2010-2018 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.gradle.plugin.test
import org.junit.Rule
import org.junit.rules.TemporaryFolder
import java.io.File
import kotlin.test.Test
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class TaskTests {
val tmpFolder = TemporaryFolder()
@Rule get
val projectDirectory: File
get() = tmpFolder.root
@Test
fun `Plugin should support separate run tasks for different binaries`() {
val project = KonanProject.createEmpty(projectDirectory).apply {
buildFile.appendText("""
konanArtifacts {
program('foo') {
srcDir 'src/foo/kotlin'
}
program('bar') {
srcDir 'src/bar/kotlin'
}
}
""".trimIndent())
}
project.generateSrcFile(
listOf("src", "foo", "kotlin"),
"main.kt",
"fun main(args: Array<String>) = println(\"Run Foo: \${args[0]}, \${args[1]}\")")
project.generateSrcFile(
listOf("src", "bar", "kotlin"),
"main.kt",
"fun main(args: Array<String>) = println(\"Run Bar: \${args[0]}, \${args[1]}\")")
val resultFoo = project.createRunner()
.withArguments("runFoo", "-PrunArgs=arg1 arg2")
.build()
val resultAll = project.createRunner()
.withArguments("run", "-PrunArgs=arg1 arg2")
.build()
assertTrue(resultFoo.output.contains("Run Foo: arg1, arg2"), "No Foo output for 'runFoo'")
assertFalse(resultFoo.output.contains("Run Bar: "), "There is Bar output for 'runFoo'")
assertTrue(resultAll.output.contains("Run Foo: arg1, arg2"), "No Foo output for 'run'")
assertTrue(resultAll.output.contains("Run Bar: arg1, arg2"), "No Bar output for 'run'")
}
}
@@ -0,0 +1,267 @@
/*
* Copyright 2010-2018 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.gradle.plugin.test
import org.gradle.api.internal.project.ProjectInternal
import org.gradle.testfixtures.ProjectBuilder
import org.jetbrains.kotlin.gradle.plugin.konan.KonanPlugin
import org.jetbrains.kotlin.gradle.plugin.konan.konanArtifactsContainer
import org.jetbrains.kotlin.gradle.plugin.model.KonanToolingModelBuilder
import org.jetbrains.kotlin.konan.CURRENT
import org.jetbrains.kotlin.konan.CompilerVersion
import org.junit.Ignore
import org.junit.Rule
import org.junit.rules.TemporaryFolder
import java.io.File
import java.nio.file.Paths
import kotlin.test.Test
import kotlin.test.assertEquals
open class ToolingModelTests {
val tmpFolder = TemporaryFolder()
@Rule get
val projectDirectory: File
get() = tmpFolder.root
fun String.escapeBackSlashes() = KonanProject.escapeBackSlashes(this)
@Ignore
@Test
fun `The model should be serialized without exceptions`() {
val project = KonanProject.createEmpty(projectDirectory).apply {
buildFile.appendText("""
konanArtifacts {
library('foo') {
srcDir 'src/foo/kotlin'
}
interop('bar')
program('main') {
libraries {
artifact konanArtifacts.foo
artifact konanArtifacts.bar
klib 'posix'
}
}
}
import org.jetbrains.kotlin.gradle.plugin.model.*
task testSerialization {
doLast {
def model = KonanToolingModelBuilder.INSTANCE.buildAll("KonanModel", project)
file("model.bin").withObjectOutputStream {
it.writeObject(model)
}
KonanModelImpl deserializedModel
file("model.bin").withObjectInputStream(model.getClass().getClassLoader()) { stream ->
deserializedModel = (KonanModelImpl) stream.readObject()
}
if (!deserializedModel.equals(model)) {
throw new AssertionError("The deserialized model doesn't equal to the initial one")
}
}
}
""".trimIndent())
generateSrcFile("main.kt")
generateSrcFile(Paths.get("src", "foo", "kotlin"), "foo.kt", "fun foo() = 1")
generateSrcFile(Paths.get("src", "bar", "kotlin"), "bar.kt", "fun bar() = 1")
generateDefFile("baz.def", "")
generateDefFile("qux.def", "")
}
project.createRunner().withArguments("testSerialization").build()
}
@Ignore
@Test
fun `The model should contain the same data as the Gradle tasks`() {
val project = KonanProject.createEmpty(projectDirectory, listOf("host", "wasm32")).apply {
val konanVersion = CompilerVersion.CURRENT.toString()
generateSrcFile(listOf("src", "foo1"), "foo1.kt", "fun foo1() = 0")
generateSrcFile(listOf("src", "foo1"), "foo11.kt", "fun foo11() = 0")
generateSrcFile(listOf("src", "foo2"), "foo2.kt", "fun foo2() = 0")
generateSrcFile(listOf("defs_bar"), "bar.def", "")
generateSrcFile(listOf("src", "baz"), "baz.kt", "fun baz() = 0")
generateSrcFile("main.kt")
createSubDir("custom_repo")
buildFile.appendText("""
konanArtifacts {
library('foo') {
srcDir 'src/foo1'
srcDir 'src/foo2'
}
interop('bar') {
defFile 'defs_bar/bar.def'
}
library('baz') {
srcDir 'src/baz'
}
program('main') {
libraries {
artifact konanArtifacts.foo
artifact konanArtifacts.bar
klib 'baz'
useRepo 'custom_repo'
}
}
}
konan {
languageVersion = "1.2"
apiVersion = "1.2"
}
import org.jetbrains.kotlin.gradle.plugin.model.*
import shadow.org.jetbrains.kotlin.konan.target.CompilerOutputKind
public <T> void assertEquals(
T actual,
T expected,
String message = "${'$'}expected expected but ${'$'}actual found") {
if (expected != actual) {
throw new AssertionError(message)
}
}
public <T> void assertContentEquals(
Collection<T> actual,
Collection<T> expected,
String message = "${'$'}expected\nexpected but\n${'$'}actual\nfound") {
if (actual.size() != expected.size() || !actual.containsAll(expected)) {
throw new AssertionError(message)
}
}
task testModelData {
dependsOn('compileKonanBaz')
doLast {
def model = KonanToolingModelBuilder.INSTANCE.buildAll("KonanModel", project)
assertEquals(model.konanHome, file(project.getProperty('org.jetbrains.kotlin.native.home')))
assertEquals(model.konanVersion.toString(), "$konanVersion")
assertEquals(model.languageVersion, "1.2")
assertEquals(model.apiVersion, "1.2")
model.artifacts.each {
def konanArtifact = konanArtifacts[it.name]
def target = it.targetPlatform
def task = konanArtifact.getByTarget(target)
assertEquals(it.file, task.artifact)
assertEquals(it.buildTaskName, task.name)
switch(it.name) {
case 'foo':
assertEquals(it.type, CompilerOutputKind.valueOf('LIBRARY'))
assertContentEquals(it.srcDirs, [file('src/foo1'), file('src/foo2')])
assertContentEquals(it.srcFiles, [
file('src/foo1/foo1.kt'),
file('src/foo1/foo11.kt'),
file('src/foo2/foo2.kt')])
assertContentEquals(it.libraries, [])
assertContentEquals(it.searchPaths, [task.artifact.parentFile])
break
case 'bar':
assertEquals(it.type, CompilerOutputKind.valueOf('LIBRARY'))
assertContentEquals(it.srcDirs, [file('defs_bar')])
assertContentEquals(it.srcFiles, [file('defs_bar/bar.def')])
assertContentEquals(it.libraries, [])
assertContentEquals(it.searchPaths, [task.artifact.parentFile])
break
case 'main':
assertEquals(it.type, CompilerOutputKind.valueOf('PROGRAM'))
assertContentEquals(it.srcDirs, [file('src/main/kotlin')])
assertContentEquals(it.srcFiles, [file('src/main/kotlin/main.kt')])
assertContentEquals(it.libraries, [
konanArtifacts['foo'].getByTarget(target).artifact,
konanArtifacts['bar'].getByTarget(target).artifact,
konanArtifacts['baz'].getByTarget(target).artifact
])
assertContentEquals(it.searchPaths, [
file('custom_repo'),
task.artifact.parentFile,
konanArtifacts.foo.getByTarget(target).artifact.parentFile
])
break
}
}
}
}
""".trimIndent())
}
project.createRunner().withArguments("testModelData").build()
}
@Test
fun `The model should support maven libraries`() {
val repoDir = projectDirectory.resolve("repo").absolutePath.escapeBackSlashes()
val dependency = KonanProject.createEmpty(projectDirectory).apply {
buildFile.appendText("""
group 'test'
version '1.0'
konanArtifacts {
library('foo')
}
apply plugin: 'maven-publish'
publishing {
repositories {
maven {
url = '$repoDir'
}
}
}
""".trimIndent())
generateSrcFile("main.kt")
}
dependency.createRunner().withArguments("build", "publish").build()
val dependentDir = projectDirectory.resolve("dependent").apply {
mkdirs()
}
val dependent = ProjectBuilder.builder().withProjectDir(dependentDir).build() as ProjectInternal
with(dependent) {
pluginManager.apply(KonanPlugin::class.java)
konanArtifactsContainer.library("bar")
repositories.maven {
it.setUrl(repoDir)
}
dependencies.apply {
add("artifactbar", "test:foo:1.0")
}
}
val model = KonanToolingModelBuilder.buildAll("konanModel", dependent)
assertEquals(1, model.artifacts.size, "Incorrect number of artifacts.")
val libraries = model.artifacts[0].libraries
assertEquals(1, libraries.size, "Incorrect number of libraries.")
val library = libraries[0].name
assertEquals("foo.klib", library, "Incorrect library name.")
}
}
@@ -0,0 +1,66 @@
buildscript {
ext.rootBuildDirectory = file('../..')
apply from: "$rootBuildDirectory/gradle/loadRootProperties.gradle"
apply from: "$rootBuildDirectory/gradle/kotlinGradlePlugin.gradle"
repositories {
maven {
url 'https://cache-redirector.jetbrains.com/jcenter'
}
jcenter()
maven {
url kotlinCompilerRepo
}
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
}
}
apply plugin: 'kotlin-multiplatform'
repositories {
maven {
url 'https://cache-redirector.jetbrains.com/jcenter'
}
jcenter()
maven {
url kotlinCompilerRepo
}
maven {
url buildKotlinCompilerRepo
}
}
kotlin {
sourceSets {
commonMain {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-common:$kotlinVersion"
}
kotlin.srcDir '../benchmarks/shared/src'
}
jsMain {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlinVersion"
implementation(npm("aws-sdk", "~2.670.0"))
}
kotlin.srcDir 'src/main/kotlin'
kotlin.srcDir 'src/main/kotlin-js'
kotlin.srcDir 'shared/src/main/kotlin'
}
}
targets {
fromPreset(presets.js, 'js') {
nodejs()
compilations.main.kotlinOptions {
outputFile = "${projectDir}/server/app.js"
moduleKind = "commonjs"
sourceMap = true
}
}
}
}
@@ -0,0 +1 @@
org.jetbrains.kotlin.native.home=../../dist
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+183
View File
@@ -0,0 +1,183 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"
+100
View File
@@ -0,0 +1,100 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
+529
View File
@@ -0,0 +1,529 @@
{
"name": "performance-server",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"accepts": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz",
"integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=",
"requires": {
"mime-types": "~2.1.18",
"negotiator": "0.6.1"
}
},
"array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
},
"aws-sdk": {
"version": "2.670.0",
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.670.0.tgz",
"integrity": "sha512-hGRnZtp1wDUh6hZRBHO0Ki7thx/xbRlIEiTKlWes+f/0E1Nhm3KpelsBZ3L/Q6y1ragwkQd4Q720AmWEqemLyA==",
"requires": {
"buffer": "4.9.1",
"events": "1.1.1",
"ieee754": "1.1.13",
"jmespath": "0.15.0",
"querystring": "0.2.0",
"sax": "1.2.1",
"url": "0.10.3",
"uuid": "3.3.2",
"xml2js": "0.4.19"
}
},
"base64-js": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
"integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g=="
},
"body-parser": {
"version": "1.18.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz",
"integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=",
"requires": {
"bytes": "3.0.0",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "~1.1.2",
"http-errors": "~1.6.3",
"iconv-lite": "0.4.23",
"on-finished": "~2.3.0",
"qs": "6.5.2",
"raw-body": "2.3.3",
"type-is": "~1.6.16"
},
"dependencies": {
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
}
}
},
"buffer": {
"version": "4.9.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
"integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=",
"requires": {
"base64-js": "^1.0.2",
"ieee754": "^1.1.4",
"isarray": "^1.0.0"
}
},
"bytes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
"integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg="
},
"content-disposition": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
"integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ="
},
"content-type": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
"integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
},
"cookie": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
"integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
},
"cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
},
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"requires": {
"ms": "^2.1.1"
},
"dependencies": {
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
}
}
},
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
},
"destroy": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
},
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
},
"ejs": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-2.6.1.tgz",
"integrity": "sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ=="
},
"encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
},
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
},
"etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
},
"events": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz",
"integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ="
},
"express": {
"version": "4.16.4",
"resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz",
"integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==",
"requires": {
"accepts": "~1.3.5",
"array-flatten": "1.1.1",
"body-parser": "1.18.3",
"content-disposition": "0.5.2",
"content-type": "~1.0.4",
"cookie": "0.3.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~1.1.2",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "1.1.1",
"fresh": "0.5.2",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "~2.3.0",
"parseurl": "~1.3.2",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.4",
"qs": "6.5.2",
"range-parser": "~1.2.0",
"safe-buffer": "5.1.2",
"send": "0.16.2",
"serve-static": "1.13.2",
"setprototypeof": "1.1.0",
"statuses": "~1.4.0",
"type-is": "~1.6.16",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"dependencies": {
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"statuses": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
"integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
}
}
},
"finalhandler": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz",
"integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==",
"requires": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"on-finished": "~2.3.0",
"parseurl": "~1.3.2",
"statuses": "~1.4.0",
"unpipe": "~1.0.0"
},
"dependencies": {
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"statuses": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
"integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
}
}
},
"forwarded": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
"integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
},
"fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
},
"http-errors": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
"integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
"requires": {
"depd": "~1.1.2",
"inherits": "2.0.3",
"setprototypeof": "1.1.0",
"statuses": ">= 1.4.0 < 2"
}
},
"iconv-lite": {
"version": "0.4.23",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz",
"integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
},
"ieee754": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
"integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="
},
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"ipaddr.js": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz",
"integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4="
},
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"jmespath": {
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz",
"integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc="
},
"kotlin": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/kotlin/-/kotlin-1.4.0.tgz",
"integrity": "sha512-q+Ts9Xr72eT3QkmLjHDObPAHbsKbZ/Vn0ozqrNNr7UbtpLxa26+Hn8ILORPLz1UIUTespZyfXztXJ7AO5Xl/Gg=="
},
"media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
},
"merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
},
"methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
},
"mime": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz",
"integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ=="
},
"mime-db": {
"version": "1.38.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz",
"integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg=="
},
"mime-types": {
"version": "2.1.22",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz",
"integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==",
"requires": {
"mime-db": "~1.38.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"negotiator": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
"integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk="
},
"node-fetch": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
},
"on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
"requires": {
"ee-first": "1.1.1"
}
},
"parseurl": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz",
"integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M="
},
"path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
},
"proxy-addr": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz",
"integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==",
"requires": {
"forwarded": "~0.1.2",
"ipaddr.js": "1.8.0"
}
},
"punycode": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
"integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0="
},
"qs": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
},
"querystring": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
"integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA="
},
"range-parser": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
"integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4="
},
"raw-body": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz",
"integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==",
"requires": {
"bytes": "3.0.0",
"http-errors": "1.6.3",
"iconv-lite": "0.4.23",
"unpipe": "1.0.0"
}
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"sax": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz",
"integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o="
},
"send": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz",
"integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==",
"requires": {
"debug": "2.6.9",
"depd": "~1.1.2",
"destroy": "~1.0.4",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "~1.6.2",
"mime": "1.4.1",
"ms": "2.0.0",
"on-finished": "~2.3.0",
"range-parser": "~1.2.0",
"statuses": "~1.4.0"
},
"dependencies": {
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"statuses": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
"integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
}
}
},
"serve-static": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz",
"integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==",
"requires": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.2",
"send": "0.16.2"
}
},
"setprototypeof": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
"integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="
},
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
},
"type-is": {
"version": "1.6.16",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz",
"integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==",
"requires": {
"media-typer": "0.3.0",
"mime-types": "~2.1.18"
}
},
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
},
"url": {
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz",
"integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=",
"requires": {
"punycode": "1.3.2",
"querystring": "0.2.0"
}
},
"utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
},
"uuid": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
"integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
},
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
},
"xml2js": {
"version": "0.4.19",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz",
"integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==",
"requires": {
"sax": ">=0.6.0",
"xmlbuilder": "~9.0.1"
}
},
"xmlbuilder": {
"version": "9.0.7",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz",
"integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0="
}
}
}
@@ -0,0 +1,17 @@
{
"name": "performance-server",
"version": "1.0.0",
"main": "server/app.js",
"scripts": {
"start": "node server/app.js"
},
"dependencies": {
"aws-sdk": "~2.670.0",
"body-parser": "~1.18.3",
"debug": "~4.1.1",
"ejs": "~2.6.1",
"express": "~4.16.4",
"kotlin": "~1.4.0",
"node-fetch": "~2.6.1"
}
}
@@ -0,0 +1,54 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.buildInfo
import org.jetbrains.report.*
import org.jetbrains.report.json.*
data class Build(val buildNumber: String, val startTime: String, val finishTime: String, val branch: String,
val commits: String, val failuresNumber: Int) {
companion object : EntityFromJsonFactory<Build> {
override fun create(data: JsonElement): Build {
if (data is JsonObject) {
val buildNumber = elementToString(data.getRequiredField("buildNumber"), "buildNumber").replace("\"", "")
val startTime = elementToString(data.getRequiredField("startTime"), "startTime").replace("\"", "")
val finishTime = elementToString(data.getRequiredField("finishTime"), "finishTime").replace("\"", "")
val branch = elementToString(data.getRequiredField("branch"), "branch").replace("\"", "")
val commits = elementToString(data.getRequiredField("commits"), "commits")
val failuresNumber = elementToInt(data.getRequiredField("failuresNumber"), "failuresNumber")
return Build(buildNumber, startTime, finishTime, branch, commits, failuresNumber)
} else {
error("Top level entity is expected to be an object. Please, check origin files.")
}
}
}
private fun formatTime(time: String, targetZone: Int = 3): String {
val matchResult = "^\\d{8}T(\\d{2})(\\d{2})\\d{2}((\\+|-)\\d{2})".toRegex().find(time)?.groupValues
matchResult?.let {
val timeZone = matchResult[3].toInt()
val timeDifference = targetZone - timeZone
var hours = (matchResult[1].toInt() + timeDifference)
if (hours > 23) {
hours -= 24
}
return "${if (hours < 10) "0$hours" else "$hours"}:${matchResult[2]}"
} ?: error { "Wrong format of time $startTime" }
}
val date: String by lazy {
val matchResult = "^(\\d{4})(\\d{2})(\\d{2})".toRegex().find(startTime)?.groupValues
matchResult?.let { "${matchResult[3]}/${matchResult[2]}/${matchResult[1]}" }
?: error { "Wrong format of time $startTime" }
}
val formattedStartTime: String by lazy {
formatTime(startTime)
}
val formattedFinishTime: String by lazy {
formatTime(finishTime)
}
}
@@ -0,0 +1,18 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.elastic
import kotlin.js.Promise // TODO - migrate to multiplatform.
import org.jetbrains.report.json.*
import org.jetbrains.network.*
// Connector with InfluxDB.
class ElasticSearchConnector(private val connector: NetworkConnector,
private val user: String? = null, private val password: String? = null) {
// Execute ElasticSearch request.
fun request(method: RequestMethod, path: String, acceptJsonContentType: Boolean = true, body: String? = null) =
connector.sendRequest(method, path, user, password, acceptJsonContentType, body)
}
@@ -0,0 +1,202 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.elastic
import org.jetbrains.report.*
import org.jetbrains.report.json.*
import org.jetbrains.report.MeanVarianceBenchmark
import org.jetbrains.network.*
import kotlin.js.Promise // TODO - migrate to multiplatform.
data class Commit(val revision: String, val developer: String) : JsonSerializable {
override fun toString() = "$revision by $developer"
override fun serializeFields() = """
"revision": "$revision",
"developer": "$developer"
"""
companion object : EntityFromJsonFactory<Commit> {
fun parse(description: String) = if (description != "...") {
description.split(" by ").let {
val (currentRevision, currentDeveloper) = it
Commit(currentRevision, currentDeveloper)
}
} else {
Commit("unknown", "unknown")
}
override fun create(data: JsonElement): Commit {
if (data is JsonObject) {
val revision = elementToString(data.getRequiredField("revision"), "revision")
val developer = elementToString(data.getRequiredField("developer"), "developer")
return Commit(revision, developer)
} else {
error("Top level entity is expected to be an object. Please, check origin files.")
}
}
}
}
// List of commits.
class CommitsList : ConvertedFromJson, JsonSerializable {
val commits: List<Commit>
constructor(data: JsonElement) {
if (data !is JsonObject) {
error("Commits description is expected to be a JSON object!")
}
val changesElement = data.getOptionalField("change")
commits = changesElement?.let {
if (changesElement !is JsonArray) {
error("Change field is expected to be an array. Please, check source.")
}
changesElement.jsonArray.map {
with(it as JsonObject) {
Commit(elementToString(getRequiredField("version"), "version"),
elementToString(getRequiredField("username"), "username")
)
}
}
} ?: listOf<Commit>()
}
constructor(_commits: List<Commit>) {
commits = _commits
}
override fun toString(): String =
commits.toString()
companion object {
fun parse(description: String) = CommitsList(description.split(";").filter { it.isNotEmpty() }.map {
Commit.parse(it)
})
}
override fun serializeFields() = """
"commits": ${arrayToJson(commits)}
"""
}
data class BuildInfo(val buildNumber: String, val startTime: String, val endTime: String, val commitsList: CommitsList,
val branch: String,
val agentInfo: String /* Important agent information often used in requests.*/) : JsonSerializable {
override fun serializeFields() = """
"buildNumber": "$buildNumber",
"startTime": "$startTime",
"endTime": "$endTime",
${commitsList.serializeFields()},
"branch": "$branch",
"agentInfo": "$agentInfo"
"""
companion object : EntityFromJsonFactory<BuildInfo> {
override fun create(data: JsonElement): BuildInfo {
if (data is JsonObject) {
val buildNumber = elementToString(data.getRequiredField("buildNumber"), "buildNumber")
val startTime = elementToString(data.getRequiredField("startTime"), "startTime")
val endTime = elementToString(data.getRequiredField("endTime"), "endTime")
val branch = elementToString(data.getRequiredField("branch"), "branch")
val commitsList = data.getRequiredField("commits")
val commits = if (commitsList is JsonArray) {
commitsList.jsonArray.map { Commit.create(it as JsonObject) }
} else {
error("benchmarksSets field is expected to be an array. Please, check origin files.")
}
val agentInfoElement = data.getOptionalField("agentInfo")
val agentInfo = agentInfoElement?.let {
elementToString(agentInfoElement, "agentInfo")
} ?: ""
return BuildInfo(buildNumber, startTime, endTime, CommitsList(commits), branch, agentInfo)
} else {
error("Top level entity is expected to be an object. Please, check origin files.")
}
}
}
}
enum class ElasticSearchType {
TEXT, KEYWORD, DATE, LONG, DOUBLE, BOOLEAN, OBJECT, NESTED
}
abstract class ElasticSearchIndex(val indexName: String, val connector: ElasticSearchConnector) {
// Insert data.
fun insert(data: JsonSerializable): Promise<String> {
val description = data.toJson()
val writePath = "$indexName/_doc/"
return connector.request(RequestMethod.POST, writePath, body = description)
}
// Delete data.
fun delete(data: String): Promise<String> {
val writePath = "$indexName/_delete_by_query"
return connector.request(RequestMethod.POST, writePath, body = data)
}
// Make search request.
fun search(requestJson: String, filterPathes: List<String> = emptyList()): Promise<String> {
val path = "$indexName/_search?pretty${if (filterPathes.isNotEmpty())
"&filter_path=" + filterPathes.joinToString(",") else ""}"
return connector.request(RequestMethod.POST, path, body = requestJson)
}
abstract val mapping: Map<String, ElasticSearchType>
val mappingDescription: String
get() = """
{
"mappings": {
"properties": {
${mapping.map { (property, type) ->
"\"${property}\": { \"type\": \"${type.name.toLowerCase()}\"${if (type == ElasticSearchType.DATE) "," +
"\"format\": \"basic_date_time_no_millis\"" else ""} }"
}.joinToString()}}
}
}
""".trimIndent()
fun createMapping() =
connector.request(RequestMethod.PUT, indexName, body = mappingDescription)
}
class BenchmarksIndex(name: String, connector: ElasticSearchConnector) : ElasticSearchIndex(name, connector) {
override val mapping: Map<String, ElasticSearchType>
get() = mapOf("buildNumber" to ElasticSearchType.KEYWORD,
"benchmarks" to ElasticSearchType.NESTED,
"env" to ElasticSearchType.NESTED,
"kotlin" to ElasticSearchType.NESTED)
}
class GoldenResultsIndex(connector: ElasticSearchConnector) : ElasticSearchIndex("golden", connector) {
override val mapping: Map<String, ElasticSearchType>
get() = mapOf("buildNumber" to ElasticSearchType.KEYWORD,
"benchmarks" to ElasticSearchType.NESTED,
"env" to ElasticSearchType.NESTED,
"kotlin" to ElasticSearchType.NESTED)
}
class BuildInfoIndex(connector: ElasticSearchConnector) : ElasticSearchIndex("builds", connector) {
override val mapping: Map<String, ElasticSearchType>
get() = mapOf("buildNumber" to ElasticSearchType.KEYWORD,
"startTime" to ElasticSearchType.DATE,
"endTime" to ElasticSearchType.DATE,
"commits" to ElasticSearchType.NESTED)
}
// Processed benchmark result with calculated mean, variance and normalized reult.
class NormalizedMeanVarianceBenchmark(name: String, status: BenchmarkResult.Status, score: Double, metric: BenchmarkResult.Metric,
runtimeInUs: Double, repeat: Int, warmup: Int, variance: Double, val normalizedScore: Double) :
MeanVarianceBenchmark(name, status, score, metric, runtimeInUs, repeat, warmup, variance) {
override fun serializeFields(): String {
return """
${super.serializeFields()},
"normalizedScore": $normalizedScore
"""
}
}
@@ -0,0 +1,46 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.network
import kotlin.js.Promise // TODO - migrate to multiplatform.
import kotlin.js.json // TODO - migrate to multiplatform.
// Now implemenation for network connection only for Node.js. TODO - multiplatform.
external fun require(module: String): dynamic
enum class RequestMethod {
POST, GET, PUT
}
// Abstract class for working with network.
abstract class NetworkConnector {
fun getAuth(user: String, password: String): String {
val buffer = js("Buffer").from(user + ":" + password)
val based64String = buffer.toString("base64")
return "Basic " + based64String
}
protected abstract fun <T : String?> sendBaseRequest(method: RequestMethod, path: String, user: String? = null,
password: String? = null, acceptJsonContentType: Boolean = true,
body: String? = null,
errorHandler: (url: String, response: dynamic) -> Nothing?): Promise<T>
open fun sendRequest(method: RequestMethod, path: String, user: String? = null, password: String? = null,
acceptJsonContentType: Boolean = true, body: String? = null): Promise<String> =
sendBaseRequest<String>(method, path, user, password, acceptJsonContentType, body) { url, response ->
error("Error during getting response from $url\n$response")
}
open fun sendOptionalRequest(method: RequestMethod, path: String, user: String? = null, password: String? = null,
acceptJsonContentType: Boolean = true, body: String? = null): Promise<String?> =
sendBaseRequest<String?>(method, path, user, password, acceptJsonContentType, body) { url, response ->
println("Error during getting response from $url\n$response")
null
}
}
@@ -0,0 +1,48 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.network
import kotlin.js.Promise // TODO - migrate to multiplatform.
import kotlin.js.json // TODO - migrate to multiplatform.
// Network connector to work with basic url requests.
class UrlNetworkConnector(private val host: String, private val port: Int? = null) : NetworkConnector() {
private val url = "$host${port?.let { ":$port" } ?: ""}"
override fun <T : String?> sendBaseRequest(method: RequestMethod, path: String, user: String?, password: String?,
acceptJsonContentType: Boolean, body: String?,
errorHandler: (url: String, response: dynamic) -> Nothing?): Promise<T> {
val fullUrl = "$url/$path"
val request = require("node-fetch")
val headers = mutableListOf<Pair<String, String>>()
if (user != null && password != null) {
headers.add("Authorization" to getAuth(user, password))
}
if (acceptJsonContentType) {
headers.add("Accept" to "application/json")
headers.add("Content-Type" to "application/json")
}
return request(fullUrl,
json(
"method" to method.toString(),
"headers" to json(*(headers.toTypedArray())),
"body" to body
)
).then { response ->
if (!response.ok) {
println(JSON.stringify(response))
errorHandler(fullUrl, response)
} else {
response.text()
}
}
}
}
@@ -0,0 +1,29 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.analyzer
import org.w3c.xhr.*
import kotlin.browser.*
import kotlin.js.*
actual fun readFile(fileName: String): String {
error("Reading from local file for JS isn't supported")
}
actual fun writeToFile(fileName: String, text: String) {
error("Writing to local file for JS isn't supported")
}
actual fun Double.format(decimalNumber: Int): String =
this.asDynamic().toFixed(decimalNumber)
actual fun assert(value: Boolean, lazyMessage: () -> Any) {
if (!value) error(lazyMessage)
}
actual fun sendGetRequest(url: String, user: String?, password: String?, followLocation: Boolean) : String {
error("Unsupported")
}
@@ -0,0 +1,365 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.database
import kotlin.js.Promise
import org.jetbrains.elastic.*
import org.jetbrains.utils.*
import org.jetbrains.report.json.*
import org.jetbrains.report.*
fun <T> Iterable<T>.isEmpty() = count() == 0
fun <T> Iterable<T>.isNotEmpty() = !isEmpty()
inline fun <T: Any> T?.str(block: (T) -> String): String =
if (this != null) block(this)
else ""
// Dispatcher to create and control benchmarks indexes separated by some feature.
// Feature can be choosen as often used as filtering entity in case there is no need in separate indexes.
// Default behaviour of dispatcher is working with one index (case when separating isn't needed).
class BenchmarksIndexesDispatcher(connector: ElasticSearchConnector, val feature: String,
featureValues: Iterable<String> = emptyList()) {
// Becnhmarks indexes to work with in case of existing feature values.
private val benchmarksIndexes =
if (featureValues.isNotEmpty())
featureValues.map { it to BenchmarksIndex("benchmarks_${it.replace(" ", "_").toLowerCase()}", connector) }
.toMap()
else emptyMap()
// Single benchmark index.
private val benchmarksSingleInstance =
if (featureValues.isEmpty()) BenchmarksIndex("benchmarks", connector) else null
// Get right index in ES.
private fun getIndex(featureValue: String = "") =
benchmarksSingleInstance ?: benchmarksIndexes[featureValue]
?: error("Used wrong feature value $featureValue. Indexes are separated using next values: ${benchmarksIndexes.keys}")
// Used filter to get data with needed feature value.
var featureFilter: ((String) -> String)? = null
// Get benchmark reports corresponding to needed build number.
fun getBenchmarksReports(buildNumber: String, featureValue: String): Promise<List<String>> {
val queryDescription = """
{
"size": 1000,
"query": {
"bool": {
"must": [
{ "match": { "buildNumber": "$buildNumber" } }
]
}
}
}
"""
return getIndex(featureValue).search(queryDescription, listOf("hits.hits._source")).then { responseString ->
val dbResponse = JsonTreeParser.parse(responseString).jsonObject
dbResponse.getObjectOrNull("hits")?.getArrayOrNull("hits")?.let { results ->
results.map {
val element = it as JsonObject
element.getObject("_source").toString()
}
} ?: emptyList()
}
}
// Get benchmarkes names corresponding to needed build number.
fun getBenchmarksList(buildNumber: String, featureValue: String): Promise<List<String>> {
return getBenchmarksReports(buildNumber, featureValue).then { reports ->
reports.map {
val dbResponse = JsonTreeParser.parse(it).jsonObject
parseBenchmarksArray(dbResponse.getArray("benchmarks"))
.map { it.name }
}.flatten()
}
}
// Delete benchmarks from database.
fun deleteBenchmarks(featureValue: String, buildNumber: String? = null): Promise<String> {
// Delete all or for choosen build number.
val matchQuery = buildNumber?.let {
""""match": { "buildNumber": "$it" }"""
} ?: """"match_all": {}"""
val queryDescription = """
{
"query": {
$matchQuery
}
}
""".trimIndent()
return getIndex(featureValue).delete(queryDescription)
}
// Get benchmarks values of needed metric for choosen build number.
fun getSamples(metricName: String, featureValue: String = "", samples: List<String>, buildsCountToShow: Int,
buildNumbers: Iterable<String>? = null,
normalize: Boolean = false): Promise<List<Pair<String, Array<Double?>>>> {
val queryDescription = """
{
"_source": ["buildNumber"],
"size": ${samples.size * buildsCountToShow},
"query": {
"bool": {
"must": [
${buildNumbers.str { builds ->
"""
{ "terms" : { "buildNumber" : [${builds.map { "\"$it\"" }.joinToString()}] } },""" }
}
${featureFilter.str { "${it(featureValue)}," } }
{"nested" : {
"path" : "benchmarks",
"query" : {
"bool": {
"must": [
{ "match": { "benchmarks.metric": "$metricName" } },
{ "terms": { "benchmarks.name": [${samples.map { "\"${it.toLowerCase()}\"" }.joinToString()}] }}
]
}
}, "inner_hits": {
"size": ${samples.size},
"_source": ["benchmarks.name",
"benchmarks.${if (normalize) "normalizedScore" else "score"}"]
}
}
}
]
}
}
}"""
return getIndex(featureValue).search(queryDescription, listOf("hits.hits._source", "hits.hits.inner_hits"))
.then { responseString ->
val dbResponse = JsonTreeParser.parse(responseString).jsonObject
val results = dbResponse.getObjectOrNull("hits")?.getArrayOrNull("hits")
?: error("Wrong response:\n$responseString")
// Get indexes for provided samples.
val indexesMap = samples.mapIndexed { index, it -> it to index }.toMap()
val valuesMap = buildNumbers?.map {
it to arrayOfNulls<Double?>(samples.size)
}?.toMap()?.toMutableMap() ?: mutableMapOf<String, Array<Double?>>()
// Parse and save values in requested order.
results.forEach {
val element = it as JsonObject
val build = element.getObject("_source").getPrimitive("buildNumber").content
buildNumbers?.let { valuesMap.getOrPut(build) { arrayOfNulls<Double?>(samples.size) } }
element
.getObject("inner_hits")
.getObject("benchmarks")
.getObject("hits")
.getArray("hits").forEach {
val source = (it as JsonObject).getObject("_source")
valuesMap[build]!![indexesMap[source.getPrimitive("name").content]!!] =
source.getPrimitive(if (normalize) "normalizedScore" else "score").double
}
}
valuesMap.toList()
}
}
fun insert(data: JsonSerializable, featureValue: String = "") =
getIndex(featureValue).insert(data)
fun delete(data: String, featureValue: String = "") =
getIndex(featureValue).delete(data)
// Get failures number happned during build.
fun getFailuresNumber(featureValue: String = "", buildNumbers: Iterable<String>? = null): Promise<Map<String, Int>> {
val queryDescription = """
{
"_source": false,
${featureFilter.str {
"""
"query": {
"bool": {
"must": [ ${it(featureValue)} ]
}
}, """
} }
${buildNumbers.str { builds ->
"""
"aggs" : {
"builds": {
"filters" : {
"filters": {
${builds.map { "\"$it\": { \"match\" : { \"buildNumber\" : \"$it\" }}" }
.joinToString(",\n")}
}
},"""
} }
"aggs" : {
"metric_build" : {
"nested" : {
"path" : "benchmarks"
},
"aggs" : {
"metric_samples": {
"filters" : {
"filters": { "samples": { "match": { "benchmarks.status": "FAILED" } } }
},
"aggs" : {
"failed_count": {
"value_count": {
"field" : "benchmarks.score"
}
}
}
}
}
${buildNumbers.str {
""" }
}"""
} }
}
}
}
"""
return getIndex(featureValue).search(queryDescription, listOf("aggregations")).then { responseString ->
val dbResponse = JsonTreeParser.parse(responseString).jsonObject
val aggregations = dbResponse.getObjectOrNull("aggregations") ?: error("Wrong response:\n$responseString")
buildNumbers?.let {
// Get failed number for each provided build.
val buckets = aggregations
.getObjectOrNull("builds")
?.getObjectOrNull("buckets")
?: error("Wrong response:\n$responseString")
buildNumbers.map {
it to buckets
.getObject(it)
.getObject("metric_build")
.getObject("metric_samples")
.getObject("buckets")
.getObject("samples")
.getObject("failed_count")
.getPrimitive("value")
.int
}.toMap()
} ?: listOf("golden" to aggregations
.getObject("metric_build")
.getObject("metric_samples")
.getObject("buckets")
.getObject("samples")
.getObject("failed_count")
.getPrimitive("value")
.int
).toMap()
}
}
// Get geometric mean for benchmarks values of needed metric.
fun getGeometricMean(metricName: String, featureValue: String = "",
buildNumbers: Iterable<String>? = null, normalize: Boolean = false,
excludeNames: List<String> = emptyList()): Promise<List<Pair<String, List<Double?>>>> {
// Filter only with metric or also with names.
val filterBenchmarks = if (excludeNames.isEmpty())
"""
"match": { "benchmarks.metric": "$metricName" }
"""
else """
"bool": {
"must": { "match": { "benchmarks.metric": "$metricName" } },
"must_not": { "terms" : { "benchmarks.name" : [${excludeNames.map { "\"$it\"" }.joinToString()}] } }
}
""".trimIndent()
val queryDescription = """
{
"_source": false,
${featureFilter.str {
"""
"query": {
"bool": {
"must": [ ${it(featureValue)} ]
}
}, """
} }
${buildNumbers.str { builds ->
"""
"aggs" : {
"builds": {
"filters" : {
"filters": {
${builds.map { "\"$it\": { \"match\" : { \"buildNumber\" : \"$it\" }}" }
.joinToString(",\n")}
}
},"""
} }
"aggs" : {
"metric_build" : {
"nested" : {
"path" : "benchmarks"
},
"aggs" : {
"metric_samples": {
"filters" : {
"filters": { "samples": { $filterBenchmarks } }
},
"aggs" : {
"sum_log_x": {
"sum": {
"field" : "benchmarks.${if (normalize) "normalizedScore" else "score"}",
"script" : {
"source": "if (_value == 0) { 0.0 } else { Math.log(_value) }"
}
}
},
"geom_mean": {
"bucket_script": {
"buckets_path": {
"sum_log_x": "sum_log_x",
"x_cnt": "_count"
},
"script": "Math.exp(params.sum_log_x/params.x_cnt)"
}
}
}
}
}
${buildNumbers.str {
""" }
}"""
} }
}
}
}
"""
return getIndex(featureValue).search(queryDescription, listOf("aggregations")).then { responseString ->
val dbResponse = JsonTreeParser.parse(responseString).jsonObject
val aggregations = dbResponse.getObjectOrNull("aggregations") ?: error("Wrong response:\n$responseString")
buildNumbers?.let {
val buckets = aggregations
.getObjectOrNull("builds")
?.getObjectOrNull("buckets")
?: error("Wrong response:\n$responseString")
buildNumbers.map {
it to listOf(buckets
.getObject(it)
.getObject("metric_build")
.getObject("metric_samples")
.getObject("buckets")
.getObject("samples")
.getObjectOrNull("geom_mean")
?.getPrimitive("value")
?.double
)
}
} ?: listOf("golden" to listOf(aggregations
.getObject("metric_build")
.getObject("metric_samples")
.getObject("buckets")
.getObject("samples")
.getObjectOrNull("geom_mean")
?.getPrimitive("value")
?.double
)
)
}
}
}
@@ -0,0 +1,149 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.database
import kotlin.js.Promise
import org.jetbrains.elastic.*
import org.jetbrains.utils.*
import org.jetbrains.report.json.*
import org.jetbrains.report.*
// Delete build information from ES index.
internal fun deleteBuildInfo(agentInfo: String, buildInfoIndex: ElasticSearchIndex,
buildNumber: String? = null): Promise<String> {
val queryDescription = """
{
"query": {
"bool": {
"must": [
{ "match": { "agentInfo": "$agentInfo" } }
${buildNumber?.let {
""",
{"match": { "buildNumber": "$it" }}
"""
} ?: ""}
]
}
}
}
""".trimIndent()
return buildInfoIndex.delete(queryDescription)
}
// Get infromation about builds details from database.
internal fun getBuildsDescription(type: String?, branch: String?, agentInfo: String, buildInfoIndex: ElasticSearchIndex,
buildsCountToShow: Int, beforeDate: String?, afterDate: String?,
onlyNumbers: Boolean = false): Promise<JsonArray> {
val queryDescription = """
{ "size": $buildsCountToShow,
${if (onlyNumbers) """"_source": ["buildNumber"],""" else ""}
"sort": {"startTime": "desc" },
"query": {
"bool": {
"must": [
{ "match": { "agentInfo": "$agentInfo" } }
${type?.let {
""",
{ "regexp": { "buildNumber": { "value": "${if (it == "release")
".*eap.*|.*release.*|.*rc.*" else ".*dev.*"}" } }
}
"""
} ?: ""}
${beforeDate?.let {
""",
{ "range": { "startTime": { "lt": "$it" } } }
"""
} ?: ""}
${afterDate?.let {
""",
{ "range": { "startTime": { "gt": "$it" } } }
"""
} ?: ""}
${branch?.let {
""",
{"match": { "branch": "$it" }}
"""
} ?: ""}
]
}
}
}
""".trimIndent()
return buildInfoIndex.search(queryDescription, listOf("hits.hits._source")).then { responseString ->
val dbResponse = JsonTreeParser.parse(responseString).jsonObject
dbResponse.getObjectOrNull("hits")?.getArrayOrNull("hits") ?: error("Wrong response:\n$responseString")
}
}
// Check if current build already exists.
suspend fun buildExists(buildInfo: BuildInfo, buildInfoIndex: ElasticSearchIndex): Boolean {
val queryDescription = """
{ "size": 1,
"_source": ["buildNumber"],
"query": {
"bool": {
"must": [
{ "match": { "buildNumber": "${buildInfo.buildNumber}" } },
{ "match": { "agentInfo": "${buildInfo.agentInfo}" } },
{ "match": { "branch": "${buildInfo.branch}" } }
]
}
}
}
""".trimIndent()
return buildInfoIndex.search(queryDescription, listOf("hits.total.value")).then { responseString ->
val response = JsonTreeParser.parse(responseString).jsonObject
val value = response.getObjectOrNull("hits")?.getObjectOrNull("total")?.getPrimitiveOrNull("value")?.content
?: error("Error response from ElasticSearch:\n$responseString")
value.toInt() > 0
}.await()
}
// Get builds numbers corresponding to machine and branch.
fun getBuildsNumbers(type: String?, branch: String?, agentInfo: String, buildsCountToShow: Int,
buildInfoIndex: ElasticSearchIndex, beforeDate: String? = null, afterDate: String? = null) =
getBuildsDescription(type, branch, agentInfo, buildInfoIndex, buildsCountToShow, beforeDate, afterDate, true)
.then { responseArray ->
responseArray.map { (it as JsonObject).getObject("_source").getPrimitive("buildNumber").content }
}
// Get full builds information corresponding to machine and branch.
fun getBuildsInfo(type: String?, branch: String?, agentInfo: String, buildsCountToShow: Int,
buildInfoIndex: ElasticSearchIndex, beforeDate: String? = null,
afterDate: String? = null) =
getBuildsDescription(type, branch, agentInfo, buildInfoIndex, buildsCountToShow, beforeDate, afterDate).then { responseArray ->
responseArray.map { BuildInfo.create((it as JsonObject).getObject("_source")) }
}
// Get golden results from database.
fun getGoldenResults(goldenResultsIndex: GoldenResultsIndex): Promise<Map<String, List<BenchmarkResult>>> {
return goldenResultsIndex.search("", listOf("hits.hits._source")).then { responseString ->
val dbResponse = JsonTreeParser.parse(responseString).jsonObject
dbResponse.getObjectOrNull("hits")?.getArrayOrNull("hits")?.map {
val reportDescription = (it as JsonObject).getObject("_source")
BenchmarksReport.create(reportDescription).benchmarks
}?.reduce { acc, it -> acc + it } ?: error("Wrong format of response:\n $responseString")
}
}
// Get distinct values for needed field from database.
fun distinctValues(field: String, index: ElasticSearchIndex): Promise<List<String>> {
val queryDescription = """
{
"aggs": {
"unique": {"terms": {"field": "$field", "size": 1000}}
}
}
""".trimIndent()
return index.search(queryDescription, listOf("aggregations.unique.buckets")).then { responseString ->
val dbResponse = JsonTreeParser.parse(responseString).jsonObject
dbResponse.getObjectOrNull("aggregations")?.getObjectOrNull("unique")?.getArrayOrNull("buckets")
?.map { (it as JsonObject).getPrimitiveOrNull("key")?.content }?.filterNotNull()
?: error("Wrong response:\n$responseString")
}
}
@@ -0,0 +1,49 @@
/*
* Copyright 2010-2019 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
external fun require(module: String): dynamic
external val process: dynamic
external val __dirname: dynamic
fun main(args: Array<String>) {
println("Server Starting!")
val express = require("express")
val app = express()
val path = require("path")
val bodyParser = require("body-parser")
val http = require("http")
// Get port from environment and store in Express.
val port = normalizePort(process.env.PORT)
app.use(bodyParser.json())
app.set("port", port)
// View engine setup.
app.set("views", path.join(__dirname, "../ui"))
app.set("view engine", "ejs")
app.use(express.static("ui"))
val server = http.createServer(app)
app.listen(port, {
println("App listening on port " + port + "!")
})
app.use("/", router())
}
fun normalizePort(port: Int) =
if (port >= 0) port else 3000
@@ -0,0 +1,47 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
@file:OptIn(ExperimentalTime::class)
package org.jetbrains.network
import kotlin.js.Promise // TODO - migrate to multiplatform.
import kotlin.time.*
// Response saved in cache.
data class CachedResponse(val cachedResult: Any, val time: TimeMark)
// Dispatcher for work with cachable responses.
object CachableResponseDispatcher {
// Storage of cached responses.
private val cachedResponses = mutableMapOf<String, CachedResponse>()
private val cacheMaxSize = 200
// Get response. If response isn't cached, use provided action to get response.
fun getResponse(request: dynamic, response: dynamic,
action: (success: (result: Any) -> Unit, reject: () -> Unit) -> Unit) {
cachedResponses[request.url]?.let {
// Update cache value if needed. Update only if last result was get later than 2 minutes.
if (it.time.elapsedNow().inMinutes > 2.0) {
println("Cache update for ${request.url}...")
action({ result: Any ->
cachedResponses[request.url] = CachedResponse(result, TimeSource.Monotonic.markNow())
}, { println("Cache update for ${request.url} failed!") })
}
response.json(it.cachedResult)
} ?: run {
action({ result: Any ->
if (cachedResponses.size >= cacheMaxSize) {
cachedResponses[request.url] = CachedResponse(result, TimeSource.Monotonic.markNow())
}
response.json(result)
}, { response.sendStatus(400) })
}
}
fun clear(): Unit {
cachedResponses.clear()
}
}
@@ -0,0 +1,109 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.network
import kotlin.js.Promise // TODO - migrate to multiplatform.
import kotlin.js.json // TODO - migrate to multiplatform.
import kotlin.js.Date
import org.jetbrains.report.json.*
// Placeholder as analog for indexable type in TS.
external interface `T$0` {
@nativeGetter
operator fun get(key: String): String?
@nativeSetter
operator fun set(key: String, value: String)
}
@JsModule("aws-sdk")
@JsNonModule
external object AWSInstance {
// Replace dynamic with some real type
class Endpoint(domain: String)
open class HttpRequest(endpoint: Endpoint, region: String) {
open fun pathname(): String
open var search: String
open var body: String?
open var endpoint: Endpoint
open var headers: `T$0`
open var method: String
open var path: String
}
class HttpClient() {
val handleRequest: dynamic
}
interface Credentials
class SharedIniFileCredentials(options: Map<String, String>): Credentials {
val accessKeyId: String
}
class EnvironmentCredentials(envPrefix: String): Credentials {
val accessKeyId: String
}
class Signers() {
class V4(request: HttpRequest, subsystem: String) {
fun addAuthorization(credentials: Credentials, date: Date)
}
}
}
// Network connector to work with AWS resources.
class AWSNetworkConnector : NetworkConnector() {
val AWSDomain = "vpc-kotlin-perf-service-5e6ldakkdv526ii5hbclzcmpny.eu-west-1.es.amazonaws.com"
val AWSRegion = "eu-west-1"
override fun <T : String?> sendBaseRequest(method: RequestMethod, path: String, user: String?, password: String?,
acceptJsonContentType: Boolean, body: String?,
errorHandler: (url: String, response: dynamic) -> Nothing?): Promise<T> {
val useEnvironmentCredentials = true // For easy test on localhost change to false.
val AWSEndpoint = AWSInstance.Endpoint(AWSDomain)
var request = AWSInstance.HttpRequest(AWSEndpoint, AWSRegion)
request.method = method.toString()
request.path += path
request.body = body
request.headers["host"] = this.AWSDomain
if (acceptJsonContentType) {
request.headers["Content-Type"] = "application/json"
request.headers["Content-Length"] = js("Buffer").byteLength(request.body)
}
val credentials = if (useEnvironmentCredentials) AWSInstance.EnvironmentCredentials("AWS")
else AWSInstance.SharedIniFileCredentials(mapOf<String, String>())
val signer = AWSInstance.Signers.V4(request, "es")
signer.addAuthorization(credentials, Date())
val client = AWSInstance.HttpClient()
return Promise { resolve, reject ->
client.handleRequest(request, null, { response ->
var responseBody = ""
response.on("data") { chunk ->
responseBody += chunk
chunk
}
response.on("end") { _ ->
val dbResponse = JsonTreeParser.parse(responseBody).jsonObject
// Response can fail and return 400 error for ES.
if (dbResponse.getPrimitiveOrNull("status")?.let { it.content != "200" } ?: false) {
println(dbResponse)
val errorMessage = dbResponse.getObject("error").toString()
reject(Throwable(errorMessage))
}
resolve(responseBody as T)
}
}, { error ->
reject(error)
})
}
}
}
@@ -0,0 +1,630 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
import org.w3c.xhr.*
import kotlin.js.json
import kotlin.js.Date
import kotlin.js.Promise
import org.jetbrains.database.*
import org.jetbrains.report.json.*
import org.jetbrains.elastic.*
import org.jetbrains.network.*
import org.jetbrains.buildInfo.Build
import org.jetbrains.analyzer.*
import org.jetbrains.report.*
import org.jetbrains.utils.*
// TODO - create DSL for ES requests?
const val teamCityUrl = "https://buildserver.labs.intellij.net/app/rest"
const val artifactoryUrl = "https://repo.labs.intellij.net/kotlin-native-benchmarks"
operator fun <K, V> Map<K, V>?.get(key: K) = this?.get(key)
fun getArtifactoryHeader(artifactoryApiKey: String) = Pair("X-JFrog-Art-Api", artifactoryApiKey)
external fun decodeURIComponent(url: String): String
// Convert saved old report to expected new format.
internal fun convertToNewFormat(data: JsonObject): List<Any> {
val env = Environment.create(data.getRequiredField("env"))
val benchmarksObj = data.getRequiredField("benchmarks")
val compilerDescription = data.getRequiredField("kotlin")
val compiler = Compiler.create(compilerDescription)
val backend = (compilerDescription as JsonObject).getRequiredField("backend")
val flagsArray = (backend as JsonObject).getOptionalField("flags")
var flags: List<String> = emptyList()
if (flagsArray != null && flagsArray is JsonArray) {
flags = flagsArray.jsonArray.map { (it as JsonLiteral).unquoted() }
}
val benchmarksList = parseBenchmarksArray(benchmarksObj)
return listOf(env, compiler, benchmarksList, flags)
}
// Convert data results to expected format.
internal fun convert(json: String, buildNumber: String, target: String): List<BenchmarksReport> {
val data = JsonTreeParser.parse(json)
val reports = if (data is JsonArray) {
data.map { convertToNewFormat(it as JsonObject) }
} else listOf(convertToNewFormat(data as JsonObject))
// Restored flags for old reports.
val knownFlags = mapOf(
"Cinterop" to listOf("-opt"),
"FrameworkBenchmarksAnalyzer" to listOf("-g"),
"HelloWorld" to if (target == "Mac OS X")
listOf("-Xcache-directory=/Users/teamcity/buildAgent/work/c104dee5223a31c5/test_dist/klib/cache/macos_x64-gSTATIC", "-g")
else listOf("-g"),
"Numerical" to listOf("-opt"),
"ObjCInterop" to listOf("-opt"),
"Ring" to listOf("-opt"),
"Startup" to listOf("-opt"),
"swiftInterop" to listOf("-opt"),
"Videoplayer" to if (target == "Mac OS X")
listOf("-Xcache-directory=/Users/teamcity/buildAgent/work/c104dee5223a31c5/test_dist/klib/cache/macos_x64-gSTATIC", "-g")
else listOf("-g")
)
return reports.map { elements ->
val benchmarks = (elements[2] as List<BenchmarkResult>).groupBy { it.name.substringBefore('.').substringBefore(':') }
val parsedFlags = elements[3] as List<String>
benchmarks.map { (setName, results) ->
val flags = if (parsedFlags.isNotEmpty() && parsedFlags[0] == "-opt") knownFlags[setName]!! else parsedFlags
val savedCompiler = elements[1] as Compiler
val compiler = Compiler(Compiler.Backend(savedCompiler.backend.type, savedCompiler.backend.version, flags),
savedCompiler.kotlinVersion)
val newReport = BenchmarksReport(elements[0] as Environment, results, compiler)
newReport.buildNumber = buildNumber
newReport
}
}.flatten()
}
// Golden result value used to get normalized results.
data class GoldenResult(val benchmarkName: String, val metric: String, val value: Double)
data class GoldenResultsInfo(val goldenResults: Array<GoldenResult>)
// Convert information about golden results to benchmarks report format.
fun GoldenResultsInfo.toBenchmarksReport(): BenchmarksReport {
val benchmarksSamples = goldenResults.map {
BenchmarkResult(it.benchmarkName, BenchmarkResult.Status.PASSED,
it.value, BenchmarkResult.metricFromString(it.metric)!!, it.value, 1, 0)
}
val compiler = Compiler(Compiler.Backend(Compiler.BackendType.NATIVE, "golden", emptyList()), "golden")
val environment = Environment(Environment.Machine("golden", "golden"), Environment.JDKInstance("golden", "golden"))
return BenchmarksReport(environment,
benchmarksSamples, compiler)
}
// Build information provided from request.
data class TCBuildInfo(val buildNumber: String, val branch: String, val startTime: String,
val finishTime: String)
data class BuildRegister(val buildId: String, val teamCityUser: String, val teamCityPassword: String,
val bundleSize: String?, val fileWithResult: String) {
companion object {
fun create(json: String): BuildRegister {
val requestDetails = JSON.parse<BuildRegister>(json)
// Parse method doesn't create real instance with all methods. So create it by hands.
return BuildRegister(requestDetails.buildId, requestDetails.teamCityUser, requestDetails.teamCityPassword,
requestDetails.bundleSize, requestDetails.fileWithResult)
}
}
private val teamCityBuildUrl: String by lazy { "builds/id:$buildId" }
val changesListUrl: String by lazy {
"changes/?locator=build:id:$buildId"
}
val teamCityArtifactsUrl: String by lazy { "builds/id:$buildId/artifacts/content/$fileWithResult" }
fun sendTeamCityRequest(url: String, json: Boolean = false) =
UrlNetworkConnector(teamCityUrl).sendRequest(RequestMethod.GET, url, teamCityUser, teamCityPassword, json)
fun getBranchName(project: String): Promise<String> {
val url = "builds?locator=id:$buildId&fields=build(revisions(revision(vcsBranchName,vcs-root-instance)))"
var branch: String? = null
return sendTeamCityRequest(url, true).then { response ->
val data = JsonTreeParser.parse(response).jsonObject
data.getArray("build").forEach {
(it as JsonObject).getObject("revisions").getArray("revision").forEach {
val currentBranch = (it as JsonObject).getPrimitive("vcsBranchName").content.removePrefix("refs/heads/")
val currentProject = (it as JsonObject).getObject("vcs-root-instance").getPrimitive("name").content
if (project == currentProject) {
branch = currentBranch
}
return@forEach
}
}
branch ?: error("No project $project can be found in build $buildId")
}
}
private fun format(timeValue: Int): String =
if (timeValue < 10) "0$timeValue" else "$timeValue"
fun getBuildInformation(): Promise<TCBuildInfo> {
return Promise.all(arrayOf(sendTeamCityRequest("$teamCityBuildUrl/number"),
getBranchName("Kotlin Native"),
sendTeamCityRequest("$teamCityBuildUrl/startDate"))).then { results ->
val (buildNumber, branch, startTime) = results
val currentTime = Date()
val timeZone = currentTime.getTimezoneOffset() / -60 // Convert to hours.
// Get finish time as current time, because buid on TeamCity isn't finished.
val finishTime = "${format(currentTime.getUTCFullYear())}" +
"${format(currentTime.getUTCMonth() + 1)}" +
"${format(currentTime.getUTCDate())}" +
"T${format(currentTime.getUTCHours())}" +
"${format(currentTime.getUTCMinutes())}" +
"${format(currentTime.getUTCSeconds())}" +
"${if (timeZone > 0) "+" else "-"}${format(timeZone)}${format(0)}"
TCBuildInfo(buildNumber, branch, startTime, finishTime)
}
}
}
// Get builds numbers in right order.
internal fun <T> orderedValues(values: List<T>, buildElement: (T) -> String = { it -> it.toString() },
skipMilestones: Boolean = false) =
values.sortedWith(
compareBy({ buildElement(it).substringBefore(".").toInt() },
{ buildElement(it).substringAfter(".").substringBefore("-").toDouble() },
{
if (skipMilestones) 0
else if (buildElement(it).substringAfter("-").startsWith("M"))
buildElement(it).substringAfter("M").substringBefore("-").toInt()
else
Int.MAX_VALUE
},
{ buildElement(it).substringAfterLast("-").toDouble() }
)
)
// ElasticSearch connector for work with custom instance.
internal val localHostElasticConnector = UrlNetworkConnector("http://localhost", 9200)
// ElasticSearch connector for work with AWS instance.
internal val awsElasticConnector = AWSNetworkConnector()
internal val networkConnector = awsElasticConnector
fun urlParameterToBaseFormat(value: dynamic) =
value.toString().replace("_", " ")
// Routing of requests to current server.
fun router() {
val express = require("express")
val router = express.Router()
val connector = ElasticSearchConnector(networkConnector)
val benchmarksDispatcher = BenchmarksIndexesDispatcher(connector, "env.machine.os",
listOf("Linux", "Mac OS X", "Windows 10")
)
val goldenIndex = GoldenResultsIndex(connector)
val buildInfoIndex = BuildInfoIndex(connector)
router.get("/createMapping") { _, response ->
buildInfoIndex.createMapping().then { _ ->
response.sendStatus(200)
}.catch { _ ->
response.sendStatus(400)
}
}
// Get consistent build information in cases of rerunning the same build.
suspend fun getConsistentBuildInfo(buildInfoInstance: BuildInfo, reports: List<BenchmarksReport>,
rerunNumber: Int = 1): BuildInfo {
var currentBuildInfo = buildInfoInstance
if (buildExists(currentBuildInfo, buildInfoIndex)) {
// Check if benchmarks aren't repeated.
val existingBecnhmarks = benchmarksDispatcher.getBenchmarksList(currentBuildInfo.buildNumber,
currentBuildInfo.agentInfo).await()
val benchmarksToRegister = reports.map { it.benchmarks.keys }.flatten()
if (existingBecnhmarks.toTypedArray().intersect(benchmarksToRegister).isNotEmpty()) {
// Build was rerun.
val buildNumber = "${currentBuildInfo.buildNumber}.$rerunNumber"
currentBuildInfo = BuildInfo(buildNumber, currentBuildInfo.startTime, currentBuildInfo.endTime,
currentBuildInfo.commitsList, currentBuildInfo.branch, currentBuildInfo.agentInfo)
return getConsistentBuildInfo(currentBuildInfo, reports, rerunNumber + 1)
}
}
return currentBuildInfo
}
// Register build on Artifactory.
router.post("/register") { request, response ->
val register = BuildRegister.create(JSON.stringify(request.body))
// Get information from TeamCity.
register.getBuildInformation().then { buildInfo ->
register.sendTeamCityRequest(register.changesListUrl, true).then { changes ->
val commitsList = CommitsList(JsonTreeParser.parse(changes))
// Get artifact.
val content = if(register.fileWithResult.contains("/"))
UrlNetworkConnector(artifactoryUrl).sendRequest(RequestMethod.GET, register.fileWithResult)
else register.sendTeamCityRequest(register.teamCityArtifactsUrl)
content.then { resultsContent ->
launch {
val reportData = JsonTreeParser.parse(resultsContent)
val reports = if (reportData is JsonArray) {
reportData.map { BenchmarksReport.create(it as JsonObject) }
} else listOf(BenchmarksReport.create(reportData as JsonObject))
val goldenResultPromise = getGoldenResults(goldenIndex)
val goldenResults = goldenResultPromise.await()
// Register build information.
var buildInfoInstance = getConsistentBuildInfo(
BuildInfo(buildInfo.buildNumber, buildInfo.startTime, buildInfo.finishTime,
commitsList, buildInfo.branch, reports[0].env.machine.os),
reports
)
if (register.bundleSize != null) {
// Add bundle size.
val bundleSizeBenchmark = BenchmarkResult("KotlinNative",
BenchmarkResult.Status.PASSED, register.bundleSize.toDouble(),
BenchmarkResult.Metric.BUNDLE_SIZE, 0.0, 1, 0)
val bundleSizeReport = BenchmarksReport(reports[0].env,
listOf(bundleSizeBenchmark), reports[0].compiler)
bundleSizeReport.buildNumber = buildInfoInstance.buildNumber
benchmarksDispatcher.insert(bundleSizeReport, reports[0].env.machine.os).then { _ ->
println("[BUNDLE] Success insert ${buildInfoInstance.buildNumber}")
}.catch { errorResponse ->
println("Failed to insert data for build")
println(errorResponse)
}
}
val insertResults = reports.map {
val benchmarksReport = SummaryBenchmarksReport(it).getBenchmarksReport()
.normalizeBenchmarksSet(goldenResults)
benchmarksReport.buildNumber = buildInfoInstance.buildNumber
// Save results in database.
benchmarksDispatcher.insert(benchmarksReport, benchmarksReport.env.machine.os)
}
if (!buildExists(buildInfoInstance, buildInfoIndex)) {
buildInfoIndex.insert(buildInfoInstance).then { _ ->
println("Success insert build information for ${buildInfoInstance.buildNumber}")
}.catch {
response.sendStatus(400)
}
}
Promise.all(insertResults.toTypedArray()).then { _ ->
response.sendStatus(200)
}.catch {
response.sendStatus(400)
}
}
}
}
}
}
// Register golden results to normalize on Artifactory.
router.post("/registerGolden", { request, response ->
val goldenResultsInfo: GoldenResultsInfo = JSON.parse<GoldenResultsInfo>(JSON.stringify(request.body))
val goldenReport = goldenResultsInfo.toBenchmarksReport()
goldenIndex.insert(goldenReport).then { _ ->
response.sendStatus(200)
}.catch {
response.sendStatus(400)
}
})
// Get builds description with additional information.
router.get("/buildsDesc/:target", { request, response ->
CachableResponseDispatcher.getResponse(request, response) { success, reject ->
val target = request.params.target.toString().replace('_', ' ')
var branch: String? = null
var type: String? = null
var buildsCountToShow = 200
var beforeDate: String? = null
var afterDate: String? = null
if (request.query != undefined) {
if (request.query.branch != undefined) {
branch = request.query.branch
}
if (request.query.type != undefined) {
type = request.query.type
}
if (request.query.count != undefined) {
buildsCountToShow = request.query.count.toString().toInt()
}
if (request.query.before != undefined) {
beforeDate = decodeURIComponent(request.query.before)
}
if (request.query.after != undefined) {
afterDate = decodeURIComponent(request.query.after)
}
}
getBuildsInfo(type, branch, target, buildsCountToShow, buildInfoIndex, beforeDate, afterDate)
.then { buildsInfo ->
val buildNumbers = buildsInfo.map { it.buildNumber }
// Get number of failed benchmarks for each build.
benchmarksDispatcher.getFailuresNumber(target, buildNumbers).then { failures ->
success(orderedValues(buildsInfo, { it -> it.buildNumber }, branch == "master").map {
Build(it.buildNumber, it.startTime, it.endTime, it.branch,
it.commitsList.serializeFields(), failures[it.buildNumber] ?: 0)
})
}.catch { errorResponse ->
println("Error during getting failures numbers")
println(errorResponse)
reject()
}
}.catch {
reject()
}
}
})
// Get values of current metric.
router.get("/metricValue/:target/:metric", { request, response ->
CachableResponseDispatcher.getResponse(request, response) { success, reject ->
val metric = request.params.metric
val target = request.params.target.toString().replace('_', ' ')
var samples: List<String> = emptyList()
var aggregation = "geomean"
var normalize = false
var branch: String? = null
var type: String? = null
var excludeNames: List<String> = emptyList()
var buildsCountToShow = 200
var beforeDate: String? = null
var afterDate: String? = null
// Parse parameters from request if it exists.
if (request.query != undefined) {
if (request.query.samples != undefined) {
samples = request.query.samples.toString().split(",").map { it.trim() }
}
if (request.query.agr != undefined) {
aggregation = request.query.agr.toString()
}
if (request.query.normalize != undefined) {
normalize = true
}
if (request.query.branch != undefined) {
branch = request.query.branch
}
if (request.query.type != undefined) {
type = request.query.type
}
if (request.query.exclude != undefined) {
excludeNames = request.query.exclude.toString().split(",").map { it.trim() }
}
if (request.query.count != undefined) {
buildsCountToShow = request.query.count.toString().toInt()
}
if (request.query.before != undefined) {
beforeDate = decodeURIComponent(request.query.before)
}
if (request.query.after != undefined) {
afterDate = decodeURIComponent(request.query.after)
}
}
getBuildsNumbers(type, branch, target, buildsCountToShow, buildInfoIndex, beforeDate, afterDate).then { buildNumbers ->
if (aggregation == "geomean") {
// Get geometric mean for samples.
benchmarksDispatcher.getGeometricMean(metric, target, buildNumbers, normalize,
excludeNames).then { geoMeansValues ->
success(orderedValues(geoMeansValues, { it -> it.first }, branch == "master"))
}.catch { errorResponse ->
println("Error during getting geometric mean")
println(errorResponse)
reject()
}
} else {
benchmarksDispatcher.getSamples(metric, target, samples, buildsCountToShow, buildNumbers, normalize)
.then { geoMeansValues ->
success(orderedValues(geoMeansValues, { it -> it.first }, branch == "master"))
}.catch {
println("Error during getting samples")
reject()
}
}
}.catch {
println("Error during getting builds information")
reject()
}
}
})
// Get branches for [target].
router.get("/branches", { request, response ->
CachableResponseDispatcher.getResponse(request, response) { success, reject ->
distinctValues("branch", buildInfoIndex).then { results ->
success(results)
}.catch { errorMessage ->
error(errorMessage.message ?: "Failed getting branches list.")
reject()
}
}
})
// Get build numbers for [target].
router.get("/buildsNumbers/:target", { request, response ->
CachableResponseDispatcher.getResponse(request, response) { success, reject ->
distinctValues("buildNumber", buildInfoIndex).then { results ->
success(results)
}.catch { errorMessage ->
println(errorMessage.message ?: "Failed getting branches list.")
reject()
}
}
})
// Conert data and migrate it from Artifactory to DB.
router.get("/migrate/:target", { request, response ->
val target = urlParameterToBaseFormat(request.params.target)
val targetPathName = target.replace(" ", "")
var buildNumber: String? = null
if (request.query != undefined) {
if (request.query.buildNumber != undefined) {
buildNumber = request.query.buildNumber
buildNumber = request.query.buildNumber
}
}
getBuildsInfoFromArtifactory(targetPathName).then { buildInfo ->
launch {
val buildsDescription = buildInfo.lines().drop(1)
var shouldConvert = buildNumber?.let { false } ?: true
val goldenResultPromise = getGoldenResults(goldenIndex)
val goldenResults = goldenResultPromise.await()
val buildsSet = mutableSetOf<String>()
buildsDescription.forEach {
if (!it.isEmpty()) {
val currentBuildNumber = it.substringBefore(',')
if (!"\\d+(\\.\\d+)+(-M\\d)?-\\w+-\\d+(\\.\\d+)?".toRegex().matches(currentBuildNumber)) {
error("Build number $currentBuildNumber differs from expected format. File with data for " +
"target $target could be corrupted.")
}
if (!shouldConvert && buildNumber != null && buildNumber == currentBuildNumber) {
shouldConvert = true
}
if (shouldConvert) {
// Save data from Artifactory into database.
val artifactoryUrlConnector = UrlNetworkConnector(artifactoryUrl)
val fileName = "nativeReport.json"
val accessFileUrl = "$targetPathName/$currentBuildNumber/$fileName"
val extrenalFileName = if (target == "Linux") "externalReport.json" else "spaceFrameworkReport.json"
val accessExternalFileUrl = "$targetPathName/$currentBuildNumber/$extrenalFileName"
val infoParts = it.split(", ")
if ((infoParts[3] == "master" || "eap" in currentBuildNumber || "release" in currentBuildNumber) &&
currentBuildNumber !in buildsSet) {
try {
buildsSet.add(currentBuildNumber)
val jsonReport = artifactoryUrlConnector.sendRequest(RequestMethod.GET, accessFileUrl).await()
var reports = convert(jsonReport, currentBuildNumber, target)
val buildInfoRecord = BuildInfo(currentBuildNumber, infoParts[1], infoParts[2],
CommitsList.parse(infoParts[4]), infoParts[3], target)
val externalJsonReport = artifactoryUrlConnector.sendOptionalRequest(RequestMethod.GET, accessExternalFileUrl)
.await()
buildInfoIndex.insert(buildInfoRecord).then { _ ->
println("[BUILD INFO] Success insert build number ${buildInfoRecord.buildNumber}")
externalJsonReport?.let {
var externalReports = convert(externalJsonReport.replace("circlet_iosX64", "SpaceFramework_iosX64"),
currentBuildNumber, target)
externalReports.forEach { externalReport ->
val extrenalAdditionalReport = SummaryBenchmarksReport(externalReport)
.getBenchmarksReport().normalizeBenchmarksSet(goldenResults)
extrenalAdditionalReport.buildNumber = currentBuildNumber
benchmarksDispatcher.insert(extrenalAdditionalReport, target).then { _ ->
println("[External] Success insert ${buildInfoRecord.buildNumber}")
}.catch { errorResponse ->
println("Failed to insert data for build")
println(errorResponse)
}
}
}
val bundleSize = if (infoParts[10] != "-") infoParts[10] else null
if (bundleSize != null) {
// Add bundle size.
val bundleSizeBenchmark = BenchmarkResult("KotlinNative",
BenchmarkResult.Status.PASSED, bundleSize.toDouble(),
BenchmarkResult.Metric.BUNDLE_SIZE, 0.0, 1, 0)
val bundleSizeReport = BenchmarksReport(reports[0].env,
listOf(bundleSizeBenchmark), reports[0].compiler)
bundleSizeReport.buildNumber = currentBuildNumber
benchmarksDispatcher.insert(bundleSizeReport, target).then { _ ->
println("[BUNDLE] Success insert ${buildInfoRecord.buildNumber}")
}.catch { errorResponse ->
println("Failed to insert data for build")
println(errorResponse)
}
}
reports.forEach { report ->
val summaryReport = SummaryBenchmarksReport(report).getBenchmarksReport()
.normalizeBenchmarksSet(goldenResults)
summaryReport.buildNumber = currentBuildNumber
// Save results in database.
benchmarksDispatcher.insert(summaryReport, target).then { _ ->
println("Success insert ${buildInfoRecord.buildNumber}")
}.catch { errorResponse ->
println("Failed to insert data for build")
println(errorResponse.message)
}
}
}.catch { errorResponse ->
println("Failed to insert data for build")
println(errorResponse)
}
} catch (e: Exception) {
println(e)
}
}
}
}
}
}
response.sendStatus(200)
}.catch {
response.sendStatus(400)
}
})
router.get("/delete/:target", { request, response ->
val target = urlParameterToBaseFormat(request.params.target)
var buildNumber: String? = null
if (request.query != undefined) {
if (request.query.buildNumber != undefined) {
buildNumber = request.query.buildNumber
}
}
benchmarksDispatcher.deleteBenchmarks(target, buildNumber).then {
deleteBuildInfo(target, buildInfoIndex, buildNumber).then {
response.sendStatus(200)
}.catch {
response.sendStatus(400)
}
}.catch {
response.sendStatus(400)
}
})
router.get("/report/:target/:buildNumber", { request, response ->
val target = urlParameterToBaseFormat(request.params.target)
val buildNumber = request.params.buildNumber.toString()
benchmarksDispatcher.getBenchmarksReports(buildNumber, target).then { reports ->
response.send(reports.joinToString(", ", "[", "]"))
}.catch {
response.sendStatus(400)
}
})
router.get("/clear", { _, response ->
CachableResponseDispatcher.clear()
response.sendStatus(200)
})
// Main page.
router.get("/", { _, response ->
response.render("index")
})
return router
}
fun getBuildsInfoFromArtifactory(target: String): Promise<String> {
val buildsFileName = "buildsSummary.csv"
val artifactoryBuildsDirectory = "builds"
return UrlNetworkConnector(artifactoryUrl).sendRequest(RequestMethod.GET,
"$artifactoryBuildsDirectory/$target/$buildsFileName")
}
fun BenchmarksReport.normalizeBenchmarksSet(dataForNormalization: Map<String, List<BenchmarkResult>>): BenchmarksReport {
val resultBenchmarksList = benchmarks.map { benchmarksList ->
benchmarksList.value.map {
NormalizedMeanVarianceBenchmark(it.name, it.status, it.score, it.metric,
it.runtimeInUs, it.repeat, it.warmup, (it as MeanVarianceBenchmark).variance,
dataForNormalization[benchmarksList.key]?.get(0)?.score?.let { golden -> it.score / golden } ?: 0.0)
}
}.flatten()
return BenchmarksReport(env, resultBenchmarksList, compiler)
}
@@ -0,0 +1,20 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.utils
import kotlin.coroutines.*
import kotlin.js.Promise
suspend fun <T> Promise<T>.await(): T = suspendCoroutine { cont ->
then({ cont.resume(it) }, { cont.resumeWithException(it) })
}
fun launch(context: CoroutineContext = EmptyCoroutineContext, block: suspend () -> Unit) =
block.startCoroutine(Continuation(context) { result ->
result.onFailure { exception ->
throw exception
}
})
@@ -0,0 +1,77 @@
import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig
buildscript {
ext.rootBuildDirectory = file('../../..')
apply from: "$rootBuildDirectory/gradle/loadRootProperties.gradle"
apply from: "$rootBuildDirectory/gradle/kotlinGradlePlugin.gradle"
repositories {
maven {
url 'https://cache-redirector.jetbrains.com/jcenter'
}
jcenter()
maven {
url kotlinCompilerRepo
}
maven {
url "http://dl.bintray.com/kotlin/kotlin-eap"
}
maven {
url "http://dl.bintray.com/kotlin/kotlin-dev"
}
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
}
}
repositories {
maven {
url 'https://cache-redirector.jetbrains.com/jcenter'
}
jcenter()
maven {
url kotlinCompilerRepo
}
maven {
url buildKotlinCompilerRepo
}
maven {
url "http://dl.bintray.com/kotlin/kotlin-eap"
}
maven {
url "http://dl.bintray.com/kotlin/kotlin-dev"
}
}
apply plugin: 'kotlin-multiplatform'
kotlin {
js {
browser {
binaries.executable()
distribution {
directory = new File("$projectDir/js/")
}
}
}
sourceSets {
commonMain {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-common:$kotlinVersion"
}
kotlin.srcDir '../../benchmarks/shared/src'
}
jsMain {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlinVersion"
}
kotlin.srcDir 'src/main/kotlin'
kotlin.srcDir '../shared/src/main/kotlin'
kotlin.srcDir '../src/main/kotlin-js'
}
}
}
@@ -0,0 +1,106 @@
.chart {
height: 400px;
}
.ct-legend {
position: relative;
z-index: 10;
list-style: none;
text-align: center;
}
.ct-legend li {
position: relative;
padding-left: 23px;
margin-right: 10px;
margin-bottom: 3px;
cursor: pointer;
display: inline-block;
}
.ct-legend li:before {
width: 12px;
height: 12px;
position: absolute;
left: 0;
content: '';
border: 3px solid transparent;
border-radius: 2px;
}
.ct-legend li.inactive:before {
background: transparent;
}
.ct-legend.ct-legend-inside {
position: absolute;
top: 0;
right: 0;
}
.ct-legend.ct-legend-inside li{
display: block;
margin: 0;
}
.ct-legend .ct-series-0:before {
background-color: #d70206;
border-color: #d70206;
}
.ct-legend .ct-series-1:before {
background-color: gold;
border-color: gold;
}
.ct-legend .ct-series-2:before {
background-color: limegreen;
border-color: limegreen;
}
.ct-legend .ct-series-3:before {
background-color: lightsalmon;
border-color: lightsalmon;
}
.ct-legend .ct-series-4:before {
background-color: mediumorchid;
border-color: mediumorchid;
}
.ct-legend .ct-series-5:before {
background-color: navy;
border-color: navy;
}
.ct-legend .ct-series-6:before {
background-color: darkcyan;
border-color: darkcyan;
}
.ct-series-b .ct-line,
.ct-series-b .ct-point {
/* Set the colour of this series line */
stroke: gold;
}
.ct-series-c .ct-line,
.ct-series-c .ct-point {
/* Set the colour of this series line */
stroke: limegreen;
}
.ct-series-d .ct-line,
.ct-series-d .ct-point {
/* Set the colour of this series line */
stroke: lightsalmon;
}
.ct-series-e .ct-line,
.ct-series-e .ct-point {
/* Set the colour of this series line */
stroke: mediumorchid;
}
.ct-series-f .ct-line,
.ct-series-f .ct-point {
/* Set the colour of this series line */
stroke: navy;
}
.ct-series-g .ct-line,
.ct-series-g .ct-point {
/* Set the colour of this series line */
stroke: darkcyan;
}
.tooltip { pointer-events: none; }
@@ -0,0 +1,115 @@
<!DOCTYPE html>
<html>
<head>
<title>
Benchmarks report
</title>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/chartist/0.11.0/chartist.min.css">
<link rel="stylesheet" href="css/style.css">
<script src="https://code.jquery.com/jquery-3.3.1.min.js">
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.6/umd/popper.min.js">
</script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js">
</script>
</head>
<body>
<header class="navbar navbar-expand navbar-dark flex-column flex-md-row bd-navbar" style="background-color:#161616;">
<img src="https://dashboard.snapcraft.io/site_media/appmedia/2018/04/256px-kotlin-logo-svg.png" style="width:60px;height:60px;">
<span class="navbar-brand mb-0 h1">
Benchmarks report
</span>
</header>
<div class="container-fluid">
<p>
<div class="row">
<div class="col">
<div class="input-group mb-3">
<div class="input-group-prepend">
<label class="input-group-text" for="inputGroupTarget">Target</label>
</div>
<select class="custom-select" id="inputGroupTarget">
<option value="Linux">Linux</option>
<option value="Mac_OS_X">Mac OS X</option>
<option value="Windows_10">Windows 10</option>
</select>
</div>
</div>
<div class="col">
<div class="input-group mb-3">
<div class="input-group-prepend">
<label class="input-group-text" for="inputGroupBuildType">Build type</label>
</div>
<select class="custom-select" id="inputGroupBuildType">
<option value="dev">Dev builds</option>
<option value="day">Day</option>
<option value="release">Releases</option>
</select>
</div>
</div>
<div class="col">
<div class="input-group mb-3">
<div class="input-group-prepend">
<label class="input-group-text" for="inputGroupBranch">Branch</label>
</div>
<select class="custom-select" id="inputGroupBranch">
<option value="master">master</option>
<option value="all">All branches</option>
</select>
</div>
</div>
<div class="col">
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text" id="basic-addon1">Highlighted build</span>
</div>
<input id="highligted_build" type="text" class="form-control" placeholder="" aria-label="build" aria-describedby="basic-addon1">
</div>
<button type="button" class="btn btn-primary" style="font-size:25px; font-weight:bold" id="plusBtn">+</button>
<button type="button" class="btn btn-primary" style="font-size:25px; font-weight:bold" id="minusBtn">-</button>
<button type="button" class="btn btn-primary" style="font-size:25px; font-weight:bold" id="prevBtn"><-</button>
<button type="button" class="btn btn-primary" style="font-size:25px; font-weight:bold" id="nextBtn">-></button>
</div>
</div>
</div>
<div class="container-fluid">
<div class="row">
<div class="col">
<h4>Normalized execution time</h4>
<div id="exec_chart" class="chart"></div>
</div>
</div>
<p style="margin-top: 40px">
<div class="row">
<div class="col">
<h4>Compile time</h4>
<div id="compile_chart" class="chart"></div>
</div>
</div>
<p style="margin-top: 40px">
<div class="row">
<div class="col">
<h4>Normalized code size</h4>
<div id="codesize_chart" class="chart"></div>
</div>
</div>
<p style="margin-top: 40px">
<div class="row">
<div class="col">
<h4>Bundle size</h4>
<div id="bundlesize_chart" class="chart"></div>
</div>
</div>
<p style="margin-top: 40px">
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chartist/0.11.0/chartist.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chartist-plugin-legend/0.6.2/chartist-plugin-legend.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.devbridge-autocomplete/1.4.9/jquery.autocomplete.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartist-plugin-axistitle@0.0.4/dist/chartist-plugin-axistitle.min.js"></script>
<script src="js/ui.js"></script>
</body>
</html>
@@ -0,0 +1,502 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
import kotlin.browser.*
import org.w3c.fetch.*
import org.jetbrains.report.json.*
import org.jetbrains.buildInfo.Build
import kotlin.js.*
import kotlin.math.ceil
import org.w3c.dom.*
// API for interop with JS library Chartist.
external class ChartistPlugins {
fun legend(data: dynamic): dynamic
fun ctAxisTitle(data: dynamic): dynamic
}
external object Chartist {
class Svg(form: String, parameters: dynamic, chartArea: String)
val plugins: ChartistPlugins
val Interpolation: dynamic
fun Line(query: String, data: dynamic, options: dynamic): dynamic
}
data class Commit(val revision: String, val developer: String)
fun sendGetRequest(url: String) = window.fetch(url, RequestInit("GET")).then { response ->
if (!response.ok)
error("Error during getting response from $url\n" +
"${response}")
else
response.text()
}.then { text -> text }
// Get data for chart in needed format.
fun getChartData(labels: List<String>, valuesList: Collection<List<*>>,
classNames: Array<String>? = null): dynamic {
val chartData: dynamic = object {}
chartData["labels"] = labels.toTypedArray()
chartData["series"] = valuesList.mapIndexed { index, it ->
val series: dynamic = object {}
series["data"] = it.toTypedArray()
classNames?.let { series["className"] = classNames[index] }
series
}.toTypedArray()
return chartData
}
// Create object with options of chart.
fun getChartOptions(samples: Array<String>, yTitle: String, classNames: Array<String>? = null): dynamic {
val chartOptions: dynamic = object {}
chartOptions["fullWidth"] = true
val paddingObject: dynamic = object {}
paddingObject["right"] = 40
chartOptions["chartPadding"] = paddingObject
val axisXObject: dynamic = object {}
axisXObject["offset"] = 40
axisXObject["labelInterpolationFnc"] = { value, index, labels ->
val labelsCount = 20
val skipNumber = ceil((labels.length as Int).toDouble() / labelsCount).toInt()
if (skipNumber > 1) {
if (index % skipNumber == 0) value else null
} else {
value
}
}
chartOptions["axisX"] = axisXObject
val axisYObject: dynamic = object {}
axisYObject["offset"] = 90
chartOptions["axisY"] = axisYObject
val legendObject: dynamic = object {}
legendObject["legendNames"] = samples
classNames?.let { legendObject["classNames"] = classNames.sliceArray(0 until samples.size) }
val titleObject: dynamic = object {}
val axisYTitle: dynamic = object {}
axisYTitle["axisTitle"] = yTitle
axisYTitle["axisClass"] = "ct-axis-title"
val titleOffset: dynamic = {}
titleOffset["x"] = 15
titleOffset["y"] = 15
axisYTitle["offset"] = titleOffset
axisYTitle["textAnchor"] = "middle"
axisYTitle["flipTitle"] = true
titleObject["axisY"] = axisYTitle
val interpolationObject: dynamic = {}
interpolationObject["fillHoles"] = true
chartOptions["lineSmooth"] = Chartist.Interpolation.simple(interpolationObject)
chartOptions["plugins"] = arrayOf(Chartist.plugins.legend(legendObject), Chartist.plugins.ctAxisTitle(titleObject))
return chartOptions
}
fun redirect(url: String) {
window.location.href = url
}
// Set customizations rules for chart.
fun customizeChart(chart: dynamic, chartContainer: String, jquerySelector: dynamic, builds: List<Build?>,
parameters: Map<String, String>) {
chart.on("draw", { data ->
var element = data.element
if (data.type == "point") {
val pointSize = 12
val currentBuild = builds.get(data.index)
currentBuild?.let { currentBuild ->
// Higlight builds with failures.
if (currentBuild.failuresNumber > 0) {
val svgParameters: dynamic = object {}
svgParameters["d"] = arrayOf("M", data.x, data.y - pointSize,
"L", data.x - pointSize, data.y + pointSize / 2,
"L", data.x + pointSize, data.y + pointSize / 2, "z").joinToString(" ")
svgParameters["style"] = "fill:rgb(255,0,0);stroke-width:0"
val triangle = Chartist.Svg("path", svgParameters, chartContainer)
element = data.element.replace(triangle)
} else if (currentBuild.buildNumber == parameters["build"]) {
// Higlight choosen build.
val svgParameters: dynamic = object {}
svgParameters["x"] = data.x - pointSize / 2
svgParameters["y"] = data.y - pointSize / 2
svgParameters["height"] = pointSize
svgParameters["width"] = pointSize
svgParameters["style"] = "fill:rgb(0,0,255);stroke-width:0"
val rectangle = Chartist.Svg("rect", svgParameters, "ct-point")
element = data.element.replace(rectangle)
}
// Add tooltips.
var shift = 1
var previousBuild: Build? = null
while (previousBuild == null && data.index - shift >= 0) {
previousBuild = builds.get(data.index - shift)
shift++
}
val linkToDetailedInfo = "https://kotlin-native-performance.labs.jb.gg/?report=" +
"${currentBuild.buildNumber}:${parameters["target"]}" +
"${previousBuild?.let {
"&compareTo=${previousBuild.buildNumber}:${parameters["target"]}"
} ?: ""}"
val information = buildString {
append("<a href=\"$linkToDetailedInfo\">${currentBuild.buildNumber}</a><br>")
append("Value: ${data.value.y.toFixed(4)}<br>")
if (currentBuild.failuresNumber > 0) {
append("failures: ${currentBuild.failuresNumber}<br>")
}
append("branch: ${currentBuild.branch}<br>")
append("date: ${currentBuild.date}<br>")
append("time: ${currentBuild.formattedStartTime}-${currentBuild.formattedFinishTime}<br>")
append("Commits:<br>")
val commitsList = (JsonTreeParser.parse("{${currentBuild.commits}}") as JsonObject).getArray("commits").map {
Commit(
(it as JsonObject).getPrimitive("revision").content,
(it as JsonObject).getPrimitive("developer").content
)
}
val commits = if (commitsList.size > 3) commitsList.slice(0..2) else commitsList
commits.forEach {
append("${it.revision.substring(0, 7)} by ${it.developer}<br>")
}
if (commitsList.size > 3) {
append("...")
}
}
element._node.setAttribute("title", information)
element._node.setAttribute("data-chart-tooltip", chartContainer)
element._node.addEventListener("click", {
redirect(linkToDetailedInfo)
})
}
}
})
chart.on("created", {
val currentChart = jquerySelector
val parameters: dynamic = object {}
parameters["selector"] = "[data-chart-tooltip=\"$chartContainer\"]"
parameters["container"] = "#$chartContainer"
parameters["html"] = true
currentChart.tooltip(parameters)
})
}
var buildsNumberToShow: Int = 200
var beforeDate: String? = null
var afterDate: String? = null
external fun decodeURIComponent(url: String): String
external fun encodeURIComponent(url: String): String
fun getDatesComponents() = "${beforeDate?.let {"&before=${encodeURIComponent(it)}"} ?: ""}" +
"${afterDate?.let {"&after=${encodeURIComponent(it)}"} ?: ""}"
fun main(args: Array<String>) {
val serverUrl = "https://kotlin-native-perf-summary.labs.jb.gg"
val zoomRatio = 2
// Get parameters from request.
val url = window.location.href
val parametersPart = url.substringAfter("?").split('&')
val parameters = mutableMapOf("target" to "Linux", "type" to "dev", "build" to "", "branch" to "master")
parametersPart.forEach {
val parsedParameter = it.split("=", limit = 2)
if (parsedParameter.size == 2) {
val (key, value) = parsedParameter
parameters[key] = value
}
}
buildsNumberToShow = parameters["count"]?.toInt() ?: buildsNumberToShow
beforeDate = parameters["before"]?.let{ decodeURIComponent(it)}
afterDate = parameters["after"]?.let{ decodeURIComponent(it)}
// Get branches.
val branchesUrl = "$serverUrl/branches"
sendGetRequest(branchesUrl).then { response ->
val branches: Array<String> = JSON.parse(response)
// Add release branches to selector.
branches.filter { it != "master" }.forEach {
if ("v(\\d|\\.)+(-M\\d)?-fixes".toRegex().matches(it)) {
val option = Option(it, it)
js("$('#inputGroupBranch')").append(js("$(option)"))
}
}
document.querySelector("#inputGroupBranch [value=\"${parameters["branch"]}\"]")?.setAttribute("selected", "true")
}
// Fill autocomplete list with build numbers.
val buildsNumbersUrl = "$serverUrl/buildsNumbers/${parameters["target"]}"
sendGetRequest(buildsNumbersUrl).then { response ->
val buildsNumbers: Array<String> = JSON.parse(response)
val autocompleteParameters: dynamic = object {}
autocompleteParameters["lookup"] = buildsNumbers
autocompleteParameters["onSelect"] = { suggestion ->
if (suggestion.value != parameters["build"]) {
val newLink = "http://${window.location.host}/?target=${parameters["target"]}&type=${parameters["type"]}" +
"${if ((suggestion.value as String).isEmpty()) "" else "&build=${suggestion.value}"}&count=$buildsNumberToShow" +
getDatesComponents()
window.location.href = newLink
}
}
js("$( \"#highligted_build\" )").autocomplete(autocompleteParameters)
js("$('#highligted_build')").change({ value ->
val newValue = js("$(this).val()").toString()
if (newValue.isEmpty() || newValue in buildsNumbers) {
val newLink = "http://${window.location.host}/?target=${parameters["target"]}&type=${parameters["type"]}" +
"${if (newValue.isEmpty()) "" else "&build=$newValue"}&count=$buildsNumberToShow" +
getDatesComponents()
window.location.href = newLink
}
})
}
// Change inputs values connected with parameters and add events listeners.
document.querySelector("#inputGroupTarget [value=\"${parameters["target"]}\"]")?.setAttribute("selected", "true")
document.querySelector("#inputGroupBuildType [value=\"${parameters["type"]}\"]")?.setAttribute("selected", "true")
(document.getElementById("highligted_build") as HTMLInputElement).value = parameters["build"]!!
// Add onChange events for fields.
// Don't use AJAX to have opportunity to share results with simple links.
js("$('#inputGroupTarget')").change({
val newValue = js("$(this).val()")
if (newValue != parameters["target"]) {
val newLink = "http://${window.location.host}/?target=$newValue&type=${parameters["type"]}&branch=${parameters["branch"]}" +
"${if (parameters["build"]!!.isEmpty()) "" else "&build=${parameters["build"]}"}&count=$buildsNumberToShow"
window.location.href = newLink
}
})
js("$('#inputGroupBuildType')").change({
val newValue = js("$(this).val()")
if (newValue != parameters["type"]) {
val newLink = "http://${window.location.host}/?target=${parameters["target"]}&type=$newValue&branch=${parameters["branch"]}" +
"${if (parameters["build"]!!.isEmpty()) "" else "&build=${parameters["build"]}"}&count=$buildsNumberToShow"
window.location.href = newLink
}
})
js("$('#inputGroupBranch')").change({
val newValue = js("$(this).val()")
if (newValue != parameters["branch"]) {
val newLink = "http://${window.location.host}/?target=${parameters["target"]}&type=${parameters["type"]}&branch=$newValue" +
"${if (parameters["build"]!!.isEmpty()) "" else "&build=${parameters["build"]}"}&count=$buildsNumberToShow"
window.location.href = newLink
}
})
val platformSpecificBenchs = if (parameters["target"] == "Mac_OS_X") ",FrameworkBenchmarksAnalyzer,SpaceFramework_iosX64" else
if (parameters["target"] == "Linux") ",kotlinx.coroutines" else ""
// Collect information for charts library.
val valuesToShow = mapOf("EXECUTION_TIME" to listOf(mapOf(
"normalize" to "true"
)),
"COMPILE_TIME" to listOf(mapOf(
"samples" to "HelloWorld,Videoplayer$platformSpecificBenchs",
"agr" to "samples"
)),
"CODE_SIZE" to listOf(mapOf(
"normalize" to "true",
"exclude" to if (parameters["target"] == "Linux")
"kotlinx.coroutines"
else if (parameters["target"] == "Mac_OS_X")
"SpaceFramework_iosX64"
else ""
), if (platformSpecificBenchs.isNotEmpty()) mapOf(
"normalize" to "true",
"agr" to "samples",
"samples" to platformSpecificBenchs.removePrefix(",")
) else null).filterNotNull(),
"BUNDLE_SIZE" to listOf(mapOf("samples" to "KotlinNative",
"agr" to "samples"))
)
var execData = listOf<String>() to listOf<List<Double?>>()
var compileData = listOf<String>() to listOf<List<Double?>>()
var codeSizeData = listOf<String>() to listOf<List<Double?>>()
var bundleSizeData = listOf<String>() to listOf<List<Int?>>()
val sizeClassNames = arrayOf("ct-series-e", "ct-series-f", "ct-series-g")
// Draw charts.
var execChart: dynamic = null
var compileChart: dynamic = null
var codeSizeChart: dynamic = null
var bundleSizeChart: dynamic = null
val descriptionUrl = "$serverUrl/buildsDesc/${parameters["target"]}?type=${parameters["type"]}" +
"${if (parameters["branch"] != "all") "&branch=${parameters["branch"]}" else ""}&count=$buildsNumberToShow" +
getDatesComponents()
val metricUrl = "$serverUrl/metricValue/${parameters["target"]}/"
// Get builds description.
val buildsInfoPromise = sendGetRequest(descriptionUrl).then { response ->
val buildsInfo = response as String
val data = JsonTreeParser.parse(buildsInfo)
if (data !is JsonArray) {
error("Response is expected to be an array.")
}
data.jsonArray.map {
val element = it as JsonElement
if (element.isNull) null else Build.create(element as JsonObject)
}
}
// Send requests to get all needed metric values.
valuesToShow.map { (metric, listOfSettings) ->
val resultValues = listOfSettings.map { settings ->
val getParameters = with(StringBuilder()) {
if (settings.isNotEmpty()) {
append("?")
}
var prefix = ""
settings.forEach { (key, value) ->
if (value.isNotEmpty()) {
append("$prefix$key=$value")
prefix = "&"
}
}
toString()
}
val branchParameter = if (parameters["branch"] != "all")
(if (getParameters.isEmpty()) "?" else "&") + "branch=${parameters["branch"]}"
else ""
val url = "$metricUrl$metric$getParameters$branchParameter${
if (parameters["type"] != "all")
(if (getParameters.isEmpty() && branchParameter.isEmpty()) "?" else "&") + "type=${parameters["type"]}"
else ""
}&count=$buildsNumberToShow${getDatesComponents()}"
sendGetRequest(url)
}.toTypedArray()
// Get metrics values for charts.
Promise.all(resultValues).then { responses ->
val valuesList = responses.map { response ->
val results = (JsonTreeParser.parse(response) as JsonArray).map {
(it as JsonObject).getPrimitive("first").content to
it.getArray("second").map { (it as JsonPrimitive).doubleOrNull }
}
val labels = results.map { it.first }
val values = results[0]?.second?.size?.let { (0..it - 1).map { i -> results.map { it.second[i] } } }
?: emptyList()
labels to values
}
val labels = valuesList[0].first
val values = valuesList.map { it.second }.reduce { acc, valuesPart -> acc + valuesPart }
when (metric) {
// Update chart with gotten data.
"COMPILE_TIME" -> {
compileData = labels to values.map { it.map { it?.let { it / 1000 } } }
compileChart = Chartist.Line("#compile_chart",
getChartData(labels, compileData.second),
getChartOptions(valuesToShow["COMPILE_TIME"]!![0]!!["samples"]!!.split(',').toTypedArray(),
"Time, milliseconds"))
buildsInfoPromise.then { builds ->
customizeChart(compileChart, "compile_chart", js("$(\"#compile_chart\")"), builds, parameters)
compileChart.update(getChartData(compileData.first, compileData.second))
}
}
"EXECUTION_TIME" -> {
execData = labels to values
execChart = Chartist.Line("#exec_chart",
getChartData(labels, execData.second),
getChartOptions(arrayOf("Geometric Mean"), "Normalized time"))
buildsInfoPromise.then { builds ->
customizeChart(execChart, "exec_chart", js("$(\"#exec_chart\")"), builds, parameters)
execChart.update(getChartData(execData.first, execData.second))
}
}
"CODE_SIZE" -> {
codeSizeData = labels to values
codeSizeChart = Chartist.Line("#codesize_chart",
getChartData(labels, codeSizeData.second),
getChartOptions(arrayOf("Geometric Mean") + platformSpecificBenchs.split(',')
.filter { it.isNotEmpty() },
"Normalized size",
arrayOf("ct-series-4", "ct-series-5", "ct-series-6")))
buildsInfoPromise.then { builds ->
customizeChart(codeSizeChart, "codesize_chart", js("$(\"#codesize_chart\")"), builds, parameters)
codeSizeChart.update(getChartData(codeSizeData.first, codeSizeData.second, sizeClassNames))
}
}
"BUNDLE_SIZE" -> {
bundleSizeData = labels to values.map { it.map { it?.let { it.toInt() / 1024 / 1024 } } }
bundleSizeChart = Chartist.Line("#bundlesize_chart",
getChartData(labels,
bundleSizeData.second, sizeClassNames),
getChartOptions(arrayOf("Bundle size"), "Size, MB", arrayOf("ct-series-4")))
buildsInfoPromise.then { builds ->
customizeChart(bundleSizeChart, "bundlesize_chart", js("$(\"#bundlesize_chart\")"), builds, parameters)
bundleSizeChart.update(getChartData(bundleSizeData.first, bundleSizeData.second, sizeClassNames))
}
}
else -> error("No chart for metric $metric")
}
true
}
}
// Update all charts with using same data.
val updateAllCharts: () -> Unit = {
execChart.update(getChartData(execData.first, execData.second))
compileChart.update(getChartData(compileData.first, compileData.second))
codeSizeChart.update(getChartData(codeSizeData.first, codeSizeData.second, sizeClassNames))
bundleSizeChart.update(getChartData(bundleSizeData.first, bundleSizeData.second, sizeClassNames))
}
js("$('#plusBtn')").click({
buildsNumberToShow =
if (buildsNumberToShow / zoomRatio > zoomRatio) {
buildsNumberToShow / zoomRatio
} else {
buildsNumberToShow
}
val newLink = "http://${window.location.host}/?target=${parameters["target"]}&type=${parameters["type"]}&branch=${parameters["branch"]}" +
"${if (parameters["build"]!!.isEmpty()) "" else "&build=${parameters["build"]}"}&count=$buildsNumberToShow" +
getDatesComponents()
window.location.href = newLink
Unit
})
js("$('#minusBtn')").click({
buildsNumberToShow = buildsNumberToShow * zoomRatio
val newLink = "http://${window.location.host}/?target=${parameters["target"]}&type=${parameters["type"]}&branch=${parameters["branch"]}" +
"${if (parameters["build"]!!.isEmpty()) "" else "&build=${parameters["build"]}"}&count=$buildsNumberToShow" +
getDatesComponents()
window.location.href = newLink
Unit
})
js("$('#prevBtn')").click({
buildsInfoPromise.then { builds ->
beforeDate = builds.firstOrNull()?.startTime
afterDate = null
val newLink = "http://${window.location.host}/?target=${parameters["target"]}&type=${parameters["type"]}&branch=${parameters["branch"]}" +
"${if (parameters["build"]!!.isEmpty()) "" else "&build=${parameters["build"]}"}&count=$buildsNumberToShow" +
"${beforeDate?.let {"&before=${encodeURIComponent(it)}"} ?: ""}"
window.location.href = newLink
}
})
js("$('#nextBtn')").click({
buildsInfoPromise.then { builds ->
beforeDate = null
afterDate = builds.lastOrNull()?.startTime
val newLink = "http://${window.location.host}/?target=${parameters["target"]}&type=${parameters["type"]}&branch=${parameters["branch"]}" +
"${if (parameters["build"]!!.isEmpty()) "" else "&build=${parameters["build"]}"}&count=$buildsNumberToShow" +
"${afterDate?.let {"&after=${encodeURIComponent(it)}"} ?: ""}"
window.location.href = newLink
}
})
// Auto reload.
parameters["refresh"]?.let {
// Set event.
window.setInterval({
window.location.reload()
}, it.toInt() * 1000)
}
}
+278
View File
@@ -0,0 +1,278 @@
#! /bin/bash
# This script scans Xcode directores for unknown frameworks
# and tries to add them as Kotlin/Native def files.
#
# Note that some frameworks are unsupported (e.g. swift-only) and marked as .disabled.
# Others may be supported and manual adjustment required (.attention_required).
# Don't consider this script as a fully automatic solution. It tries its best
# to identify unsupported frameworks correctly, but the result still should be validated.
#
# jq is required to run the script. It can be installed via `brew install jq`.
#
# Args:
# 1. Platform name: ios, tvos, osx or watchos
# 2. Path to Kotlin/Native sources.
set -e
NATIVE_SRCDIR=$2
case $1 in
tvos*)
DEV_SDK=$(xcrun --show-sdk-path --sdk appletvos)
SIM_SDK=$(xcrun --show-sdk-path --sdk appletvsimulator)
OS_NAME="tvOS"
DEVICES=("tvos_arm64")
SIMULATORS=("tvos_x64")
;;
ios*)
DEV_SDK=$(xcrun --show-sdk-path --sdk iphoneos)
SIM_SDK=$(xcrun --show-sdk-path --sdk iphonesimulator)
OS_NAME="iOS"
DEVICES=("ios_arm32" "ios_arm64")
SIMULATORS=("ios_x64")
;;
watchos*)
DEV_SDK=$(xcrun --show-sdk-path --sdk watchos)
SIM_SDK=$(xcrun --show-sdk-path --sdk watchsimulator)
OS_NAME="watchOS"
DEVICES=("watchos_arm32" "watchos_arm64")
SIMULATORS=("watchos_i386")
;;
osx*)
DEV_SDK=$(xcrun --show-sdk-path)
SIM_SDK=$DEV_SDK
OS_NAME="macOS"
;;
*)
echo "Expected one of: osx ios watchos tvos. Got: $1"
exit 1
esac
DEFS=$NATIVE_SRCDIR/platformLibs/src/platform/$1
FRAMEWORKS_DEV=$DEV_SDK/System/Library/Frameworks
FRAMEWORKS_SIM=$SIM_SDK/System/Library/Frameworks
DEFS_FILE=$(mktemp)
FRAMEWORKS_DEV_FILE=$(mktemp)
FRAMEWORKS_SIM_FILE=$(mktemp)
FRAMEWORKS_COMMON_FILE=$(mktemp)
FRAMEWORKS_DEV_ONLY_FILE=$(mktemp)
FRAMEWORKS_SIM_ONLY_FILE=$(mktemp)
ls $DEFS | grep .def | cut -d '.' -f 1 > $DEFS_FILE
ls $FRAMEWORKS_DEV | grep .framework | cut -d '.' -f 1 > $FRAMEWORKS_DEV_FILE
ls $FRAMEWORKS_SIM | grep .framework | cut -d '.' -f 1 > $FRAMEWORKS_SIM_FILE
comm -12 $FRAMEWORKS_DEV_FILE $FRAMEWORKS_SIM_FILE > $FRAMEWORKS_COMMON_FILE
comm -13 $FRAMEWORKS_DEV_FILE $FRAMEWORKS_SIM_FILE > $FRAMEWORKS_SIM_ONLY_FILE
comm -23 $FRAMEWORKS_DEV_FILE $FRAMEWORKS_SIM_FILE > $FRAMEWORKS_DEV_ONLY_FILE
ABSENT_COMMON=$(comm -13 $DEFS_FILE $FRAMEWORKS_COMMON_FILE)
ABSENT_SIM=$(comm -13 $DEFS_FILE $FRAMEWORKS_SIM_ONLY_FILE)
ABSENT_DEV=$(comm -13 $DEFS_FILE $FRAMEWORKS_DEV_ONLY_FILE)
rm $DEFS_FILE
rm $FRAMEWORKS_DEV_FILE
rm $FRAMEWORKS_SIM_FILE
rm $FRAMEWORKS_COMMON_FILE
rm $FRAMEWORKS_DEV_ONLY_FILE
rm $FRAMEWORKS_SIM_ONLY_FILE
AVAILABLE=()
AVAILABLE_ON_DEV=()
AVAILABLE_ON_SIM=()
UNAVAILABLE=()
SWIFT_ONLY=()
DRIVER_KIT=()
OS_UNSUPPORTED=()
JSON=$(mktemp)
# Use information about framework on developer.apple.com to put it into
# appropriate bucket.
function classify {
FRAMEWORK_NAME=$1
KIND=$2
URL=https://developer.apple.com/tutorials/data/documentation
# Try to force Objective-C documentation. Swift is used by default.
STATUS=$(curl -s -o $JSON -w "%{http_code}" "$URL/$FRAMEWORK_NAME.json?language=objc")
# Some old frameworks don't have a documentation page.
if [[ $STATUS -ne 200 ]]
then
UNAVAILABLE+=($FRAMEWORK_NAME)
return
fi
# DriverKit is C++. Drop it.
if [[ $(cat $JSON | jq '.metadata.platforms[] | .name' | grep DriverKit) ]]
then
DRIVER_KIT+=($FRAMEWORK_NAME)
return
fi
# Sometimes framework is present in SDK directory, but actually it isn't supported on
# current OS.
if [[ ! $(cat $JSON | jq '.metadata.platforms[] | .name' | grep $OS_NAME) ]]
then
OS_UNSUPPORTED+=($FRAMEWORK_NAME)
return
fi
LANG=$(cat $JSON | jq '.identifier.interfaceLanguage' | grep swift || true)
if [[ $LANG = \"swift\" ]]
then
SWIFT_ONLY+=($FRAMEWORK_NAME)
return
fi
case $KIND in
device*)
AVAILABLE_ON_DEV+=($FRAMEWORK_NAME)
;;
simulator*)
AVAILABLE_ON_SIM+=($FRAMEWORK_NAME)
;;
common*)
AVAILABLE+=($FRAMEWORK_NAME)
;;
esac
}
for framework in $ABSENT_COMMON
do
classify $framework common
done
for framework in $ABSENT_DEV
do
classify $framework device
done
for framework in $ABSENT_SIM
do
classify $framework simulator
done
rm $JSON
PLATFORM_LIBS=$NATIVE_SRCDIR/platformLibs/src/platform/$1
function create_def_content {
FRAMEWORK=$1
DEF_FILE=$2
echo "language = Objective-C" >> $DEF_FILE
echo "package = platform.$FRAMEWORK" >> $DEF_FILE
case $3 in
devices*)
TARGETS=("${DEVICES[@]}")
;;
simulators*)
TARGETS=("${SIMULATORS[@]}")
;;
*)
TARGETS=("")
;;
esac
for target in "${TARGETS[@]}"
do
if [[ -z "$target" ]]
then
SUFFIX=""
else
SUFFIX=".$target"
fi
echo "" >> $DEF_FILE
echo "modules$SUFFIX = $FRAMEWORK" >> $DEF_FILE
echo "compilerOpts$SUFFIX = -framework $FRAMEWORK" >> $DEF_FILE
echo "linkerOpts$SUFFIX = -framework $FRAMEWORK" >> $DEF_FILE
done
}
function create_def {
FRAMEWORK=$1
TARGETS=$2
DEF_FILE=$PLATFORM_LIBS/$FRAMEWORK.def
touch $DEF_FILE
create_def_content $FRAMEWORK $DEF_FILE $TARGETS
echo "Created $DEF_FILE"
}
# Creates def file with additional suffix
# and adds an explanation comment.
function create_disabled {
FRAMEWORK=$1
REASON=$2
EXTENSION=$3
TARGETS=$4
DEF_FILE=$PLATFORM_LIBS/$FRAMEWORK.def.$EXTENSION
touch $DEF_FILE
create_def_content $FRAMEWORK $DEF_FILE $TARGETS
echo "#Disabled: $REASON" >> $DEF_FILE
echo "Created $DEF_FILE"
}
if [ ${#AVAILABLE[@]} -ne 0 ]
then
echo "New frameworks added."
fi
for framework in "${AVAILABLE[@]}"
do
if [[ -d $DEV_SDK/System/Library/Frameworks/$framework.framework/Modules ]]
then
create_def $framework
else
create_disabled $framework "Framework without module" attention_required
fi
done
if [ ${#AVAILABLE_ON_SIM[@]} -ne 0 ]
then
echo "The following frameworks are available only for simulators."
fi
for framework in "${AVAILABLE_ON_SIM[@]}"
do
create_disabled $framework "Check that framework is not available for devices" attention_required simulators
done
if [ ${#AVAILABLE_ON_DEV[@]} -ne 0 ]
then
echo "The following frameworks are available only for devices."
fi
for framework in "${AVAILABLE_ON_DEV[@]}"
do
create_disabled $framework "Check that framework is not available for simulators" attention_required devices
done
if [ ${#UNAVAILABLE[@]} -ne 0 ]
then
echo "Documentation for the following frameworks is not directly accessible."
echo "They may be deprecated or available by different name."
echo "For example, AppClip is accessed as app_clips ¯\_(ツ)_/¯."
fi
for framework in "${UNAVAILABLE[@]}"
do
create_disabled $framework "Unavailable" attention_required
done
if [ ${#SWIFT_ONLY[@]} -ne 0 ]
then
echo "The following frameworks doesn't provide Objective-C API."
fi
for framework in "${SWIFT_ONLY[@]}"
do
create_disabled $framework "Swift-only framework" disabled
done
if [ ${#DRIVER_KIT[@]} -ne 0 ]
then
echo "The following frameworks are from DriverKit."
fi
for framework in "${DRIVER_KIT[@]}"
do
create_disabled $framework "part of DriverKit" disabled
done
if [ ${#OS_UNSUPPORTED[@]} -ne 0 ]
then
echo "The following frameworks are not officially provided for $1."
fi
for framework in "${OS_UNSUPPORTED[@]}"
do
create_disabled $framework "Not officially available for $1" disabled
done
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
set -e
# "brew install coreutils" for grealpath.
KONAN_TOOLCHAIN_VERSION=xcode_12_0
SDKS="macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator"
TARBALL_macosx=target-sysroot-$KONAN_TOOLCHAIN_VERSION-macos_x64
TARBALL_iphoneos=target-sysroot-$KONAN_TOOLCHAIN_VERSION-ios_arm64
TARBALL_iphonesimulator=target-sysroot-$KONAN_TOOLCHAIN_VERSION-ios_x64
TARBALL_appletvos=target-sysroot-$KONAN_TOOLCHAIN_VERSION-tvos_arm64
TARBALL_appletvsimulator=target-sysroot-$KONAN_TOOLCHAIN_VERSION-tvos_x64
TARBALL_watchos=target-sysroot-$KONAN_TOOLCHAIN_VERSION-watchos_arm32
TARBALL_watchsimulator=target-sysroot-$KONAN_TOOLCHAIN_VERSION-watchos_x86
TARBALL_xcode=target-toolchain-$KONAN_TOOLCHAIN_VERSION-macos_x64
TARBALL_xcode_addon=xcode-addon-$KONAN_TOOLCHAIN_VERSION-macos_x64
OUT=`pwd`
for s in $SDKS; do
p=`xcrun --sdk $s --show-sdk-path`
p=`grealpath $p`
tarball_var=TARBALL_${s}
tarball=${!tarball_var}
echo "Packing SDK $s as $OUT/$tarball.tar.gz..."
$SHELL -c "tar czf $OUT/$tarball.tar.gz -C $p -s '/^\./$tarball/HS' ."
done
t=`xcrun -f ld`
t=`dirname $t`
t=`grealpath $t/../..`
tarball=$TARBALL_xcode
echo "Packing toolchain $OUT/$tarball.tar.gz..."
$SHELL -c "tar czf $OUT/$tarball.tar.gz -C $t -s '/^\./$tarball/HS' ."
t=`xcrun -f bitcode-build-tool`
t=`dirname $t`
t=`grealpath $t/..`
tarball=$TARBALL_xcode_addon
echo "Packing additional tools $OUT/$tarball.tar.gz..."
$SHELL -c "tar czf $OUT/$tarball.tar.gz -C $t -s '/^\./$tarball/HS' ."
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
KONAN_TOOLCHAIN_VERSION=1
TARBALL_zephyr_arm=target-sysroot-$KONAN_TOOLCHAIN_VERSION-zephyr-arm
OUT=`pwd`
if [ -z "ZEPHYR_SDK_INSTALL_DIR" ]; then
echo "Using default Zephyr SDK install location"
export ZEPHYR_SDK_INSTALL_DIR="/opt/zephyr-sdk"
fi
sdk=armv5-zephyr-eabi
p=$ZEPHYR_SDK_INSTALL_DIR/sysroots/$sdk/usr
echo "Packing SDK $sdk as $OUT/$TARBALL_zephyr_arm.tar.gz..."
tar -czvf $OUT/$TARBALL_zephyr_arm.tar.gz -C $p .