Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions buildSrc/src/main/kotlin/io/opentelemetry/gradle/WeaverTasks.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package io.opentelemetry.gradle

import java.io.IOException
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
import org.gradle.process.ExecOperations
import org.gradle.work.DisableCachingByDefault
import javax.inject.Inject

@DisableCachingByDefault(because = "Docker run is external and side-effectful")
abstract class WeaverTasks @Inject constructor(
private val execOps: ExecOperations
) : DefaultTask() {

companion object {
private const val WEAVER_MODEL_PATH = "/home/weaver/model"
private const val WEAVER_TEMPLATES_PATH = "/home/weaver/templates"
private const val WEAVER_TARGET_PATH = "/home/weaver/target"
}

@get:Input
abstract val dockerExecutable: Property<String>
@get:Input
abstract val platform: Property<String>
@get:Input
abstract val image: Property<String>

@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val modelDir: DirectoryProperty

@get:InputFiles
@get:Optional
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val templatesDir: DirectoryProperty

// Choose ONE of these per task
@get:OutputDirectory
@get:Optional
abstract val outputDir: DirectoryProperty
@get:OutputFile
@get:Optional
abstract val outputFile: RegularFileProperty

// e.g., ["registry","check","--registry=/home/weaver/model"]
@get:Input
abstract val toolArgs: ListProperty<String>

@TaskAction
fun runWeaver() {
validateDockerAvailable()

val mounts = mutableListOf(
"--mount", "type=bind,source=${modelDir.get().asFile.absolutePath},target=$WEAVER_MODEL_PATH,readonly"
)

val templates = templatesDir.orNull
if (templates != null) {
when {
templates.asFile.isDirectory -> {
mounts += listOf("--mount", "type=bind,source=${templates.asFile.absolutePath},target=$WEAVER_TEMPLATES_PATH,readonly")
}
templates.asFile.exists() -> {
logger.warn("templatesDir exists but is not a directory: ${templates.asFile.absolutePath}. Skipping templates mount.")
}
}
}

val targetMount = when {
outputDir.isPresent -> {
outputDir.get().asFile.mkdirs()
listOf("--mount", "type=bind,source=${outputDir.get().asFile.absolutePath},target=$WEAVER_TARGET_PATH")
}

outputFile.isPresent -> {
// Mount parent directory and ensure weaver writes to the correct filename
val outputFileObj = outputFile.get().asFile
val parent = outputFileObj.parentFile.also { it.mkdirs() }
logger.info("Mounting ${parent.absolutePath} for output file: ${outputFileObj.name}")
listOf("--mount", "type=bind,source=${parent.absolutePath},target=$WEAVER_TARGET_PATH")
}

else -> error("Either outputDir or outputFile must be set")
}
mounts += targetMount

val base = mutableListOf("run", "--rm", "--platform=${platform.get()}")
val os = System.getProperty("os.name").lowercase()
if (os.contains("linux")) {
try {
val uid = ProcessBuilder("id", "-u").start().inputStream.bufferedReader().readText().trim()
val gid = ProcessBuilder("id", "-g").start().inputStream.bufferedReader().readText().trim()
base += listOf("-u", "$uid:$gid")
} catch (e: IOException) {
logger.warn("Could not determine uid/gid: ${e.message}. Generated files may be owned by root")
}
}

execOps.exec {
executable = dockerExecutable.get()
args = base + mounts + listOf(image.get()) + toolArgs.get()
standardOutput = System.out
errorOutput = System.err
isIgnoreExitValue = false
}
}

private fun validateDockerAvailable() {
try {
val process = ProcessBuilder(dockerExecutable.get(), "--version")
.redirectErrorStream(true)
.start()
val exitCode = process.waitFor()
if (exitCode != 0) {
throw GradleException("Docker is not available or not functioning correctly. Please ensure Docker is installed and running.")
}
} catch (e: IOException) {
throw GradleException("Docker is required but could not be executed. Please install and start Docker. Error: ${e.message}", e)
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ publishing {
connection.set("scm:git:[email protected]:open-telemetry/opentelemetry-java-contrib.git")
developerConnection.set("scm:git:[email protected]:open-telemetry/opentelemetry-java-contrib.git")
tag.set(tagVersion)
url.set("https://github.com/open-telemetry/opentelemetry-java-contrib/tree/${tagVersion}")
url.set("https://github.com/open-telemetry/opentelemetry-java-contrib/tree/$tagVersion")
}

issueManagement {
Expand Down
188 changes: 188 additions & 0 deletions buildSrc/src/main/kotlin/otel.weaver-conventions.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import io.opentelemetry.gradle.WeaverTasks
import org.gradle.api.GradleException
import org.gradle.api.provider.Property

// Weaver code generation convention plugin for OpenTelemetry
// Apply this plugin to modules that have a model/ directory with weaver model files
// It will generate Java code, documentation, and YAML configs using the OpenTelemetry Weaver tool

val weaverContainer =
"otel/weaver:v0.18.0@sha256:5425ade81dc22ddd840902b0638b4b6a9186fb654c5b50c1d1ccd31299437390"

// Auto-detect platform for Docker, with fallback to x86_64
val dockerPlatform = System.getProperty("os.arch").let { arch ->
when {
arch == "aarch64" || arch == "arm64" -> "linux/arm64"
else -> "linux/x86_64"
}
}

interface OtelWeaverExtension {
/**
* REQUIRED: The Java package path where generated code will be placed. Path should use forward
* slashes (e.g., "io/opentelemetry/ibm/mq/metrics").
*
* Example configuration in build.gradle.kts:
* ```kotlin
* otelWeaver {
* javaOutputPackage.set("io/opentelemetry/ibm/mq/metrics")
* }
* ```
*/
val javaOutputPackage: Property<String>
}

val weaverExtension = extensions.create("otelWeaver", OtelWeaverExtension::class.java)

val projectModelDir = layout.projectDirectory.dir("model")
val hasWeaverModel = projectModelDir.asFile.exists() && projectModelDir.asFile.isDirectory

if (hasWeaverModel) {
val projectTemplatesDir = layout.projectDirectory.dir("templates")
val projectDocsDir = layout.projectDirectory.dir("docs")

logger.lifecycle("Weaver model found in ${project.name}")
logger.lifecycle(" Model directory: ${projectModelDir.asFile.absolutePath}")
logger.lifecycle(" Templates directory: ${projectTemplatesDir.asFile.absolutePath}")
logger.lifecycle(" Container: $weaverContainer")

tasks.register<WeaverTasks>("weaverCheck") {
group = "weaver"
description = "Check the weaver model for errors"

dockerExecutable.set("docker")
platform.set(dockerPlatform)
image.set(weaverContainer)

modelDir.set(projectModelDir)
templatesDir.set(projectTemplatesDir)
outputDir.set(layout.buildDirectory.dir("weaver-check"))

toolArgs.set(listOf("registry", "check", "--registry=/home/weaver/model"))

// Always run check task to ensure model validity, even if inputs haven't changed.
// This is intentional as validation should always run when explicitly requested.
outputs.upToDateWhen { false }
}

tasks.register<WeaverTasks>("weaverGenerateDocs") {
group = "weaver"
description = "Generate markdown documentation from weaver model"

dockerExecutable.set("docker")
platform.set(dockerPlatform)
image.set(weaverContainer)

modelDir.set(projectModelDir)
templatesDir.set(projectTemplatesDir)
outputDir.set(projectDocsDir)

toolArgs.set(
listOf(
"registry",
"generate",
"--registry=/home/weaver/model",
"markdown",
"--future",
"/home/weaver/target"
)
)
}

val weaverGenerateJavaTask =
tasks.register<WeaverTasks>("weaverGenerateJava") {
group = "weaver"
description = "Generate Java code from weaver model"

dockerExecutable.set("docker")
platform.set(dockerPlatform)
image.set(weaverContainer)

modelDir.set(projectModelDir)
templatesDir.set(projectTemplatesDir)

// Map the javaOutputPackage to the output directory
// Finalize the value to ensure it's set at configuration time and avoid capturing the extension
val javaPackage = weaverExtension.javaOutputPackage
javaPackage.finalizeValueOnRead()
outputDir.set(javaPackage.map { layout.projectDirectory.dir("src/main/java/$it") })

toolArgs.set(
listOf(
"registry",
"generate",
"--registry=/home/weaver/model",
"java",
"--future",
"/home/weaver/target"
)
)

doFirst { logger.lifecycle(" Java output: ${outputDir.get().asFile.absolutePath}") }
}

// Validate the required configuration at configuration time (not execution time)
afterEvaluate {
if (weaverExtension.javaOutputPackage.orNull == null) {
throw GradleException(
"""
otelWeaver.javaOutputPackage must be configured in project '${project.name}'.

Add this to your build.gradle.kts:
otelWeaver {
javaOutputPackage.set("io/opentelemetry/your/package")
}
""".trimIndent()
)
}
}

// Make spotless tasks always run after the generate task
tasks
.matching {
it.name == "spotlessJava" || it.name == "spotlessJavaApply" || it.name == "spotlessApply"
}
.configureEach { mustRunAfter(weaverGenerateJavaTask) }

// Make weaverGenerateJava automatically format generated code
weaverGenerateJavaTask.configure { finalizedBy("spotlessJavaApply") }

tasks.register<WeaverTasks>("weaverGenerateYaml") {
group = "weaver"
description = "Generate YAML configuration from weaver model"

dockerExecutable.set("docker")
platform.set(dockerPlatform)
image.set(weaverContainer)

modelDir.set(projectModelDir)
templatesDir.set(projectTemplatesDir)
outputFile.set(layout.projectDirectory.file("config.yml"))

toolArgs.set(
listOf(
"registry",
"generate",
"--registry=/home/weaver/model",
"yaml",
"--future",
"/home/weaver/target"
)
)
}

tasks.register("weaverGenerate") {
description = "Generate all outputs (Java, docs, YAML) from weaver model"
group = "weaver"
dependsOn("weaverGenerateJava", "weaverGenerateDocs", "weaverGenerateYaml")
}

// Ensure proper task ordering without forcing automatic execution
// Use mustRunAfter so weaver generation only runs when explicitly invoked
tasks.named("compileJava") { mustRunAfter(weaverGenerateJavaTask) }
tasks.named("sourcesJar") { mustRunAfter(weaverGenerateJavaTask) }
} else {
logger.debug(
"No weaver model directory found in ${project.name}, skipping weaver task registration"
)
}
Loading