Skip to content

Commit 6994b90

Browse files
Gradle plugin for generating weaver files (#2413)
Co-authored-by: otelbot <197425009+otelbot@users.noreply.github.com>
1 parent bfdfc9f commit 6994b90

8 files changed

Lines changed: 508 additions & 90 deletions

File tree

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package io.opentelemetry.gradle
2+
3+
import java.io.IOException
4+
import org.gradle.api.DefaultTask
5+
import org.gradle.api.GradleException
6+
import org.gradle.api.file.DirectoryProperty
7+
import org.gradle.api.file.RegularFileProperty
8+
import org.gradle.api.provider.ListProperty
9+
import org.gradle.api.provider.Property
10+
import org.gradle.api.tasks.Input
11+
import org.gradle.api.tasks.InputFiles
12+
import org.gradle.api.tasks.Optional
13+
import org.gradle.api.tasks.OutputDirectory
14+
import org.gradle.api.tasks.OutputFile
15+
import org.gradle.api.tasks.PathSensitive
16+
import org.gradle.api.tasks.PathSensitivity
17+
import org.gradle.api.tasks.TaskAction
18+
import org.gradle.process.ExecOperations
19+
import org.gradle.work.DisableCachingByDefault
20+
import javax.inject.Inject
21+
22+
@DisableCachingByDefault(because = "Docker run is external and side-effectful")
23+
abstract class WeaverTasks @Inject constructor(
24+
private val execOps: ExecOperations
25+
) : DefaultTask() {
26+
27+
companion object {
28+
private const val WEAVER_MODEL_PATH = "/home/weaver/model"
29+
private const val WEAVER_TEMPLATES_PATH = "/home/weaver/templates"
30+
private const val WEAVER_TARGET_PATH = "/home/weaver/target"
31+
}
32+
33+
@get:Input
34+
abstract val dockerExecutable: Property<String>
35+
@get:Input
36+
abstract val platform: Property<String>
37+
@get:Input
38+
abstract val image: Property<String>
39+
40+
@get:InputFiles
41+
@get:PathSensitive(PathSensitivity.RELATIVE)
42+
abstract val modelDir: DirectoryProperty
43+
44+
@get:InputFiles
45+
@get:Optional
46+
@get:PathSensitive(PathSensitivity.RELATIVE)
47+
abstract val templatesDir: DirectoryProperty
48+
49+
// Choose ONE of these per task
50+
@get:OutputDirectory
51+
@get:Optional
52+
abstract val outputDir: DirectoryProperty
53+
@get:OutputFile
54+
@get:Optional
55+
abstract val outputFile: RegularFileProperty
56+
57+
// e.g., ["registry","check","--registry=/home/weaver/model"]
58+
@get:Input
59+
abstract val toolArgs: ListProperty<String>
60+
61+
@TaskAction
62+
fun runWeaver() {
63+
validateDockerAvailable()
64+
65+
val mounts = mutableListOf(
66+
"--mount", "type=bind,source=${modelDir.get().asFile.absolutePath},target=$WEAVER_MODEL_PATH,readonly"
67+
)
68+
69+
val templates = templatesDir.orNull
70+
if (templates != null) {
71+
when {
72+
templates.asFile.isDirectory -> {
73+
mounts += listOf("--mount", "type=bind,source=${templates.asFile.absolutePath},target=$WEAVER_TEMPLATES_PATH,readonly")
74+
}
75+
templates.asFile.exists() -> {
76+
logger.warn("templatesDir exists but is not a directory: ${templates.asFile.absolutePath}. Skipping templates mount.")
77+
}
78+
}
79+
}
80+
81+
val targetMount = when {
82+
outputDir.isPresent -> {
83+
outputDir.get().asFile.mkdirs()
84+
listOf("--mount", "type=bind,source=${outputDir.get().asFile.absolutePath},target=$WEAVER_TARGET_PATH")
85+
}
86+
87+
outputFile.isPresent -> {
88+
// Mount parent directory and ensure weaver writes to the correct filename
89+
val outputFileObj = outputFile.get().asFile
90+
val parent = outputFileObj.parentFile.also { it.mkdirs() }
91+
logger.info("Mounting ${parent.absolutePath} for output file: ${outputFileObj.name}")
92+
listOf("--mount", "type=bind,source=${parent.absolutePath},target=$WEAVER_TARGET_PATH")
93+
}
94+
95+
else -> error("Either outputDir or outputFile must be set")
96+
}
97+
mounts += targetMount
98+
99+
val base = mutableListOf("run", "--rm", "--platform=${platform.get()}")
100+
val os = System.getProperty("os.name").lowercase()
101+
if (os.contains("linux")) {
102+
try {
103+
val uid = ProcessBuilder("id", "-u").start().inputStream.bufferedReader().readText().trim()
104+
val gid = ProcessBuilder("id", "-g").start().inputStream.bufferedReader().readText().trim()
105+
base += listOf("-u", "$uid:$gid")
106+
} catch (e: IOException) {
107+
logger.warn("Could not determine uid/gid: ${e.message}. Generated files may be owned by root")
108+
}
109+
}
110+
111+
execOps.exec {
112+
executable = dockerExecutable.get()
113+
args = base + mounts + listOf(image.get()) + toolArgs.get()
114+
standardOutput = System.out
115+
errorOutput = System.err
116+
isIgnoreExitValue = false
117+
}
118+
}
119+
120+
private fun validateDockerAvailable() {
121+
try {
122+
val process = ProcessBuilder(dockerExecutable.get(), "--version")
123+
.redirectErrorStream(true)
124+
.start()
125+
val exitCode = process.waitFor()
126+
if (exitCode != 0) {
127+
throw GradleException("Docker is not available or not functioning correctly. Please ensure Docker is installed and running.")
128+
}
129+
} catch (e: IOException) {
130+
throw GradleException("Docker is required but could not be executed. Please install and start Docker. Error: ${e.message}", e)
131+
}
132+
}
133+
}
134+

buildSrc/src/main/kotlin/otel.publish-conventions.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ publishing {
5858
connection.set("scm:git:git@github.com:open-telemetry/opentelemetry-java-contrib.git")
5959
developerConnection.set("scm:git:git@github.com:open-telemetry/opentelemetry-java-contrib.git")
6060
tag.set(tagVersion)
61-
url.set("https://github.com/open-telemetry/opentelemetry-java-contrib/tree/${tagVersion}")
61+
url.set("https://github.com/open-telemetry/opentelemetry-java-contrib/tree/$tagVersion")
6262
}
6363

6464
issueManagement {
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import io.opentelemetry.gradle.WeaverTasks
2+
import org.gradle.api.GradleException
3+
import org.gradle.api.provider.Property
4+
5+
// Weaver code generation convention plugin for OpenTelemetry
6+
// Apply this plugin to modules that have a model/ directory with weaver model files
7+
// It will generate Java code, documentation, and YAML configs using the OpenTelemetry Weaver tool
8+
9+
val weaverContainer =
10+
"otel/weaver:v0.18.0@sha256:5425ade81dc22ddd840902b0638b4b6a9186fb654c5b50c1d1ccd31299437390"
11+
12+
// Auto-detect platform for Docker, with fallback to x86_64
13+
val dockerPlatform = System.getProperty("os.arch").let { arch ->
14+
when {
15+
arch == "aarch64" || arch == "arm64" -> "linux/arm64"
16+
else -> "linux/x86_64"
17+
}
18+
}
19+
20+
// Docker executable path: use env var DOCKER_EXECUTABLE or usual platform defaults
21+
val dockerExecutablePath = System.getenv("DOCKER_EXECUTABLE") ?: run {
22+
val os = System.getProperty("os.name").lowercase()
23+
when {
24+
os.contains("mac") || os.contains("darwin") -> "/usr/local/bin/docker"
25+
os.contains("windows") -> "docker"
26+
else -> "docker" // Linux typically has docker in PATH
27+
}
28+
}
29+
30+
interface OtelWeaverExtension {
31+
/**
32+
* REQUIRED: The Java package path where generated code will be placed. Path should use forward
33+
* slashes (e.g., "io/opentelemetry/ibm/mq/metrics").
34+
*
35+
* Example configuration in build.gradle.kts:
36+
* ```kotlin
37+
* otelWeaver {
38+
* javaOutputPackage.set("io/opentelemetry/ibm/mq/metrics")
39+
* }
40+
* ```
41+
*/
42+
val javaOutputPackage: Property<String>
43+
}
44+
45+
val weaverExtension = extensions.create("otelWeaver", OtelWeaverExtension::class.java)
46+
47+
val projectModelDir = layout.projectDirectory.dir("model")
48+
val hasWeaverModel = projectModelDir.asFile.exists() && projectModelDir.asFile.isDirectory
49+
50+
if (hasWeaverModel) {
51+
val projectTemplatesDir = layout.projectDirectory.dir("templates")
52+
val projectDocsDir = layout.projectDirectory.dir("docs")
53+
54+
logger.lifecycle("Weaver model found in ${project.name}")
55+
logger.lifecycle(" Model directory: ${projectModelDir.asFile.absolutePath}")
56+
logger.lifecycle(" Templates directory: ${projectTemplatesDir.asFile.absolutePath}")
57+
logger.lifecycle(" Container: $weaverContainer")
58+
59+
tasks.register<WeaverTasks>("weaverCheck") {
60+
group = "weaver"
61+
description = "Check the weaver model for errors"
62+
63+
dockerExecutable.set(dockerExecutablePath)
64+
platform.set(dockerPlatform)
65+
image.set(weaverContainer)
66+
67+
modelDir.set(projectModelDir)
68+
templatesDir.set(projectTemplatesDir)
69+
outputDir.set(layout.buildDirectory.dir("weaver-check"))
70+
71+
toolArgs.set(listOf("registry", "check", "--registry=/home/weaver/model"))
72+
73+
// Always run check task to ensure model validity, even if inputs haven't changed.
74+
// This is intentional as validation should always run when explicitly requested.
75+
outputs.upToDateWhen { false }
76+
}
77+
78+
tasks.register<WeaverTasks>("weaverGenerateDocs") {
79+
group = "weaver"
80+
description = "Generate markdown documentation from weaver model"
81+
82+
dockerExecutable.set(dockerExecutablePath)
83+
platform.set(dockerPlatform)
84+
image.set(weaverContainer)
85+
86+
modelDir.set(projectModelDir)
87+
templatesDir.set(projectTemplatesDir)
88+
outputDir.set(projectDocsDir)
89+
90+
toolArgs.set(
91+
listOf(
92+
"registry",
93+
"generate",
94+
"--registry=/home/weaver/model",
95+
"markdown",
96+
"--future",
97+
"/home/weaver/target"
98+
)
99+
)
100+
}
101+
102+
val weaverGenerateJavaTask =
103+
tasks.register<WeaverTasks>("weaverGenerateJava") {
104+
group = "weaver"
105+
description = "Generate Java code from weaver model"
106+
107+
dockerExecutable.set(dockerExecutablePath)
108+
platform.set(dockerPlatform)
109+
image.set(weaverContainer)
110+
111+
modelDir.set(projectModelDir)
112+
templatesDir.set(projectTemplatesDir)
113+
114+
// Map the javaOutputPackage to the output directory
115+
// Finalize the value to ensure it's set at configuration time and avoid capturing the extension
116+
val javaPackage = weaverExtension.javaOutputPackage
117+
javaPackage.finalizeValueOnRead()
118+
outputDir.set(javaPackage.map { layout.projectDirectory.dir("src/main/java/$it") })
119+
120+
toolArgs.set(
121+
listOf(
122+
"registry",
123+
"generate",
124+
"--registry=/home/weaver/model",
125+
"java",
126+
"--future",
127+
"/home/weaver/target"
128+
)
129+
)
130+
131+
doFirst { logger.lifecycle(" Java output: ${outputDir.get().asFile.absolutePath}") }
132+
}
133+
134+
// Validate the required configuration at configuration time (not execution time)
135+
afterEvaluate {
136+
if (weaverExtension.javaOutputPackage.orNull == null) {
137+
throw GradleException(
138+
"""
139+
otelWeaver.javaOutputPackage must be configured in project '${project.name}'.
140+
141+
Add this to your build.gradle.kts:
142+
otelWeaver {
143+
javaOutputPackage.set("io/opentelemetry/your/package")
144+
}
145+
""".trimIndent()
146+
)
147+
}
148+
}
149+
150+
// Make spotless tasks always run after the generate task
151+
tasks
152+
.matching {
153+
it.name == "spotlessJava" || it.name == "spotlessJavaApply" || it.name == "spotlessApply"
154+
}
155+
.configureEach { mustRunAfter(weaverGenerateJavaTask) }
156+
157+
// Make weaverGenerateJava automatically format generated code
158+
weaverGenerateJavaTask.configure { finalizedBy("spotlessJavaApply") }
159+
160+
tasks.register<WeaverTasks>("weaverGenerateYaml") {
161+
group = "weaver"
162+
description = "Generate YAML configuration from weaver model"
163+
164+
dockerExecutable.set(dockerExecutablePath)
165+
platform.set(dockerPlatform)
166+
image.set(weaverContainer)
167+
168+
modelDir.set(projectModelDir)
169+
templatesDir.set(projectTemplatesDir)
170+
outputFile.set(layout.projectDirectory.file("config.yml"))
171+
172+
toolArgs.set(
173+
listOf(
174+
"registry",
175+
"generate",
176+
"--registry=/home/weaver/model",
177+
"yaml",
178+
"--future",
179+
"/home/weaver/target"
180+
)
181+
)
182+
}
183+
184+
tasks.register("weaverGenerate") {
185+
description = "Generate all outputs (Java, docs, YAML) from weaver model"
186+
group = "weaver"
187+
dependsOn("weaverGenerateJava", "weaverGenerateDocs", "weaverGenerateYaml")
188+
}
189+
190+
// Ensure proper task ordering without forcing automatic execution
191+
// Use mustRunAfter so weaver generation only runs when explicitly invoked
192+
tasks.named("compileJava") { mustRunAfter(weaverGenerateJavaTask) }
193+
tasks.named("sourcesJar") { mustRunAfter(weaverGenerateJavaTask) }
194+
} else {
195+
logger.debug(
196+
"No weaver model directory found in ${project.name}, skipping weaver task registration"
197+
)
198+
}

0 commit comments

Comments
 (0)