Skip to content

Commit

Permalink
Add Kotlin scripts to build data capturing samples
Browse files Browse the repository at this point in the history
  • Loading branch information
alextu committed Dec 30, 2022
1 parent 1e97e14 commit 96394a0
Show file tree
Hide file tree
Showing 11 changed files with 753 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ jobs:
working-directory: common-gradle-enterprise-gradle-configuration-kotlin
run: |
# apply sample file
echo "apply(from = \"../build-data-capturing-gradle-samples/${{matrix.sample-file}}\")" >> build.gradle.kts
echo "apply(from = \"../build-data-capturing-gradle-samples/${{matrix.sample-file}}.kts\")" >> build.gradle.kts
- name: Run Gradle build using Kotlin DSL
working-directory: common-gradle-enterprise-gradle-configuration-kotlin
run: ./gradlew tasks -Dgradle.enterprise.url=https://ge.solutions-team.gradle.com
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import com.gradle.enterprise.gradleplugin.GradleEnterpriseExtension

/**
* This Kotlin script captures the Gradle Enterprise Gradle plugin version as a custom value.
*/

project.extensions.configure<GradleEnterpriseExtension>() {
buildScan {
val url = GradleEnterpriseExtension::class.java.classLoader.getResource("com.gradle.scan.plugin.internal.meta.buildAgentVersion.txt")
val buildAgentVersion = url.readText()
value("GE Gradle plugin version", buildAgentVersion)
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import com.gradle.enterprise.gradleplugin.GradleEnterpriseExtension
import com.gradle.scan.plugin.BuildScanExtension
import groovy.json.JsonBuilder
import groovy.json.JsonSlurper

import java.nio.charset.Charset
import java.util.Base64
import java.util.concurrent.TimeUnit

/**
* This Gradle script captures the <i>Git diff</i> in a GitHub gist,
* and references the gist via a custom link.
*
* In order for the gist to be created successfully, you must specify the Gradle property `gistToken` where
* the value is a Github access token that has gist permission on the given Github repo.
*
* See https://docs.github.com/en/rest/gists/gists#create-a-gist for reference.
*/

project.extensions.configure<GradleEnterpriseExtension>() {

val capture = Capture(gradle.startParameter.isOffline,
gradle.startParameter.isContinuous,
gradle.rootProject.logger,
getProviders().gradleProperty("gistToken"),
rootProject.name)

buildScan {
background {
if(capture.isEnabled()) {
capture.captureGitDiffInGist(buildScan)
}
}
}
}

class Capture(val offline: Boolean,
val continuous: Boolean,
val logger: Logger,
val gistTokenProvider: Provider<String>,
val projectName: String) {

fun isEnabled(): Boolean {
val isCapturingScan = !offline && !continuous
if (!isCapturingScan) {
logger.warn("Build is offline or continuous. Will not publish gist.")
return false
}
return true
}

fun captureGitDiffInGist(api: BuildScanExtension): Unit {
val hasCredentials = gistTokenProvider.isPresent
if (!hasCredentials) {
logger.warn("User has not set 'gistToken'. Cannot publish gist.")
return
}

val diff = execAndGetStdout("git", "diff")
if (!diff.isNullOrEmpty()) {
try {
val baseUrl = java.net.URL("https://api.github.com/gists")
val credentials = Base64.getEncoder().encodeToString(gistTokenProvider.get().toByteArray())
val basicAuth = "Basic ${credentials}"

val connection = baseUrl.openConnection() as java.net.HttpURLConnection
connection.apply {
// request
setRequestProperty("Authorization", basicAuth)
setRequestProperty("Accept", "application/vnd.github+json")
requestMethod = "POST"
doOutput = true
outputStream.bufferedWriter().use { writer ->
jsonRequest(writer, diff)
}

// response
val url = JsonSlurper().parse(content as java.io.InputStream).withGroovyBuilder { getProperty("html_url") }
api.link("Git diff", url as String)
}
logger.info("Successfully published gist.")
} catch (ex: Exception) {
logger.warn("Unable to publish gist", ex)
}
}
}

// this method must be static, otherwise Gradle will interpret `files` as Project.files() and this won't work
private fun jsonRequest(writer: java.io.Writer, diff: String): Unit {
val json = groovy.json.JsonOutput.toJson(mapOf(
"description" to "Git diff for ${projectName}",
"public" to false,
"files" to mapOf("${projectName}.diff" to mapOf("content" to diff))))
writer.write(json)
}

private fun execAndGetStdout(vararg args: String): String {
val process = ProcessBuilder(*args)
.redirectOutput(ProcessBuilder.Redirect.PIPE)
.redirectError(ProcessBuilder.Redirect.PIPE)
.start()
try {
val finished = process.waitFor(10, TimeUnit.SECONDS)
val standardText = process.inputStream.bufferedReader().readText()
val ignore = process.errorStream.bufferedReader().readText()

return if (finished && process.exitValue() == 0) trimAtEnd(standardText) else return ""
} finally {
process.destroyForcibly()
}
}

private fun trimAtEnd(str: String): String {
return ("x" + str).trim().substring(1)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import com.gradle.enterprise.gradleplugin.GradleEnterpriseExtension
import com.gradle.scan.plugin.BuildScanExtension
import java.nio.charset.Charset
import java.util.concurrent.TimeUnit

/**
* This Gradle script captures the OS processes as reported by the OS 'ps' command,
* and adds these as a custom value.
*/

project.extensions.configure<GradleEnterpriseExtension>() {
buildScan {
background {
Capture.captureOsProcesses(buildScan)
}
}
}

class Capture {
companion object {
fun captureOsProcesses(api: BuildScanExtension): Unit {
val psOutput = execAndGetStdout("ps", "-o pid,ppid,time,command")
api.value("OS processes", psOutput)
}

private fun execAndGetStdout(vararg args: String): String {
val process = ProcessBuilder(*args)
.redirectOutput(ProcessBuilder.Redirect.PIPE)
.redirectError(ProcessBuilder.Redirect.PIPE)
.start()
try {
val finished = process.waitFor(10, TimeUnit.SECONDS)
val standardText = process.inputStream.bufferedReader().readText()
val ignore = process.errorStream.bufferedReader().readText()

return if (finished && process.exitValue() == 0) trimAtEnd(standardText) else return ""
} finally {
process.destroyForcibly()
}
}

private fun trimAtEnd(str: String): String {
return ("x" + str).trim().substring(1)
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import com.gradle.enterprise.gradleplugin.GradleEnterpriseExtension
import com.gradle.scan.plugin.BuildScanExtension
import java.nio.charset.Charset
import java.util.concurrent.TimeUnit

/**
* This Gradle script captures the processor architecture
* and adds these as a custom value.
*/

project.extensions.configure<GradleEnterpriseExtension>() {
buildScan {
background {
Capture.captureProcessorArch(buildScan)
}
}
}

class Capture {
companion object {
fun captureProcessorArch(api: BuildScanExtension): Unit {
val osName = System.getProperty("os.name")
api.value("os.name", osName)

val osArch = System.getProperty("os.arch")
api.value("os.arch", osArch)

if (isDarwin(osName)) {
if (isTranslatedByRosetta()) {
api.tag("M1-translated")
} else if (isM1()) {
api.tag("M1")
}
}
}

private fun isDarwin(osName: String): Boolean {
return osName.contains("OS X") || osName.startsWith("Darwin")
}

private fun isM1(): Boolean {
return execAndGetStdout("uname", "-p") == "arm"
}

// On Apple silicon, a universal binary may run either natively or as a translated binary
// https://developer.apple.com/documentation/apple-silicon/about-the-rosetta-translation-environment#Determine-Whether-Your-App-Is-Running-as-a-Translated-Binary
private fun isTranslatedByRosetta(): Boolean {
return execAndGetStdout("sysctl", "sysctl.proc_translated") == "sysctl.proc_translated: 1"
}

private fun execAndGetStdout(vararg args: String): String {
val process = ProcessBuilder(*args)
.redirectOutput(ProcessBuilder.Redirect.PIPE)
.redirectError(ProcessBuilder.Redirect.PIPE)
.start()
try {
val finished = process.waitFor(10, TimeUnit.SECONDS)
val standardText = process.inputStream.bufferedReader().readText()
val ignore = process.errorStream.bufferedReader().readText()

return if (finished && process.exitValue() == 0) trimAtEnd(standardText) else return ""
} finally {
process.destroyForcibly()
}
}

private fun trimAtEnd(str: String): String {
return ("x" + str).trim().substring(1)
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import com.gradle.enterprise.gradleplugin.GradleEnterpriseExtension
import com.gradle.scan.plugin.BuildScanExtension
import groovy.xml.XmlSlurper
import groovy.xml.slurpersupport.NodeChildren
import groovy.xml.slurpersupport.NodeChild
import groovy.xml.slurpersupport.GPathResult

/**
* This Gradle script captures issues found by reporting tasks,
* and adds these as custom values.
*/

project.extensions.configure<GradleEnterpriseExtension>() {
buildScan {
gradle.taskGraph.beforeTask {
if (ReportingTask.isSupported(this) && this is Reporting<*>) {
val reportTask = this as Reporting<ReportContainer<*>>
reportTask.reports.withGroovyBuilder { "xml" { setProperty("enabled", true) } }
} else if (ReportingTask.SPOTBUGS.isTask(this)) {
this.withGroovyBuilder {
val reports = getProperty("reports")
val xml = (reports as NamedDomainObjectContainer<*>).create("xml") {
enabled = true
}
}
}
}

gradle.taskGraph.afterTask {
if (ReportingTask.isSupported(this)) {
val reportFile = reportFromTask(this)
if (reportFile.exists()) {
val report = XmlSlurper().parse(reportFile)
var valueName = ""
var errors = mutableListOf<String>()

if (ReportingTask.CHECKSTYLE.isTask(this)) {
valueName = "Verification Checkstyle"
val files = report.getProperty("file") as NodeChildren
files.forEach { f ->
val file = f as NodeChild
val filePath = project.rootProject.relativePath(file.attributes()["name"])
val checkErrors = file.getProperty("error") as NodeChildren
checkErrors.forEach { e ->
val error = e as NodeChild
errors.add("${filePath}:${error.attributes()["line"]}:${error.attributes()["column"]} \u2192 ${error.attributes()["message"]}")
}
}
} else if (ReportingTask.CODENARC.isTask(this)) {
valueName = "Verification CodeNarc"
val packages = report.getProperty("Package") as NodeChildren
packages.forEach { p ->
val proj = report.getProperty("Project") as NodeChildren
val sourceDirectoryNode = proj.getProperty("SourceDirectory") as NodeChildren
val sourceDirectory = appendIfMissing(sourceDirectoryNode.text() as String, "/")
val files = (p as NodeChild).getProperty("File") as NodeChildren
files.forEach { f ->
val file = f as NodeChild
val filePath =
project.rootProject.relativePath(sourceDirectory + file.attributes()["name"])
(file.getProperty("Violation") as NodeChildren).forEach { v ->
val violation = v as NodeChild
errors.add("${filePath}:${violation.attributes()["lineNumber"]} \u2192 ${violation.attributes()["ruleName"]}")
}
}
}
} else if (ReportingTask.SPOTBUGS.isTask(this)) {
valueName = "Verification SpotBugs"
val bugs = report.getProperty("BugInstance") as NodeChildren
bugs.forEach { b ->
val bug = b as NodeChild
val type = bug.attributes()["type"]
val error = bug.breadthFirst().asSequence().filter { l ->
val line = l as NodeChild
l.name() == "SourceLine"
}.sortedBy { l ->
(l as NodeChild).parent().name()
}.first() as NodeChild
val startLine = error.attributes()["start"]
val endLine = error.attributes()["end"]
val lineNumber = if (endLine == startLine) startLine else "${startLine}-${endLine}"
val className = error.attributes()["classname"]
errors.add("${className}:${lineNumber} \u2192 ${type}")
}
}
errors.forEach { e -> buildScan.value(valueName, e) }
}
}
}
}
}

fun reportFromTask(task: Task): File {
if (task is Reporting<*>) {
val task = task as Reporting<ReportContainer<*>>
return (task.reports.withGroovyBuilder { "xml" { getProperty("destination") } } as Report).getOutputLocation().get().asFile as File
} else if (ReportingTask.SPOTBUGS.isTask(task)) {
val reports = task.withGroovyBuilder { getProperty("reports") as NamedDomainObjectContainer<*> }
val report = (reports.named("XML") as NamedDomainObjectProvider).get() as SingleFileReport
return report.getOutputLocation().get().asFile
} else {
throw IllegalStateException("Unsupported report task: " + task)
}
}

fun appendIfMissing(str: String, suffix: String): String {
return if (str.endsWith(suffix)) str else str + suffix
}

enum class SpotBugsParent {
BugInstance, Method, Class
}

enum class ReportingTask(val className: String) {

CHECKSTYLE("org.gradle.api.plugins.quality.Checkstyle"),
CODENARC("org.gradle.api.plugins.quality.CodeNarc"),
SPOTBUGS("com.github.spotbugs.snom.SpotBugsTask");

fun isTask(task: Task): Boolean {
return task::class.java.name.contains(className)
}

companion object {
fun isSupported(task: Task): Boolean {
return values().any { it.isTask(task) }
}
}
}
Loading

0 comments on commit 96394a0

Please sign in to comment.