Skip to content

Commit a7e2d30

Browse files
authored
SONARGO-450 Extract Gradle configuration for Docker-based Go build into cloud-native-gradle-modules (#34)
1 parent cc7fbe8 commit a7e2d30

4 files changed

Lines changed: 274 additions & 1 deletion

File tree

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,20 @@ git config submodule.recurse true
3434
```
3535

3636
Optionally, run this command with `--global` to apply this configuration globally.
37+
38+
# Common use cases
39+
40+
## Publishing a sonar plugin
41+
42+
* Apply `artifactory-configuration` to the root project to set some defaults
43+
* Apply `sonar-plugin` to the subproject that contains the plugin
44+
* If necessary, re-apply `artifactory-configuration` to the subproject to override the defaults
45+
46+
## Building a Go executable
47+
48+
* Apply `go-binary-builder` script to the subproject that contains the Go code
49+
* It expects `make.sh` and `make.bat` scripts to be present in the same directory as `build.gradle[.kts]`
50+
* Configure the `goBuild` extension
51+
* Go-related tasks are automatically linked to `test`, `assemble`, `check`, and `build` tasks
52+
* A configuration `goBinaries` is created, and it can be used to depend on the Go binaries like
53+
`implementation(projects(":go-subprojcet", "goBinaries"))`

gradle-modules/src/main/kotlin/org.sonarsource.cloud-native.go-binary-builder.gradle.kts

Lines changed: 184 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,195 @@
1515
* along with this program; if not, see https://sonarsource.com/license/ssal/
1616
*/
1717
import org.gradle.kotlin.dsl.registering
18+
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
19+
import org.sonarsource.cloudnative.gradle.GO_BINARY_OUTPUT_DIR
20+
import org.sonarsource.cloudnative.gradle.GoBuild
21+
import org.sonarsource.cloudnative.gradle.allGoSourcesAndMakeScripts
22+
import org.sonarsource.cloudnative.gradle.callMake
23+
import org.sonarsource.cloudnative.gradle.getArchitecture
24+
import org.sonarsource.cloudnative.gradle.getPlatform
25+
import org.sonarsource.cloudnative.gradle.goSources
1826

1927
val goBinaries: Configuration by configurations.creating
2028
val goBinariesJar by tasks.registering(Jar::class) {
2129
group = "build"
2230
dependsOn("compileGo")
2331
archiveClassifier.set("binaries")
24-
from("build/executable")
32+
from(GO_BINARY_OUTPUT_DIR)
2533
}
2634
artifacts.add(goBinaries.name, goBinariesJar)
35+
36+
val goVersion = providers.environmentVariable("GO_VERSION")
37+
.orElse(providers.gradleProperty("goVersion"))
38+
.orNull ?: error("Either `GO_VERSION` env variable or `goVersion` Gradle property must be set")
39+
val isCrossCompile = providers.environmentVariable("GO_CROSS_COMPILE").orElse("0")
40+
val isCi: Boolean = System.getenv("CI")?.equals("true") == true
41+
val goBuildExtension = extensions.create("goBuild", GoBuild::class)
42+
goBuildExtension.dockerWorkDir.convention("/home/sonarsource/${project.name}")
43+
goBuildExtension.additionalOutputFiles.convention(emptySet())
44+
45+
if (isCi) {
46+
val cleanGoCode by tasks.registering(Exec::class) {
47+
description = "Clean all compiled version of the go code."
48+
group = "build"
49+
50+
callMake("clean")
51+
}
52+
53+
val compileGo by tasks.registering(Exec::class) {
54+
description = "Compile the go code for the local system."
55+
group = "build"
56+
57+
inputs.property("GO_CROSS_COMPILE", isCrossCompile)
58+
inputs.files(allGoSourcesAndMakeScripts())
59+
60+
outputs.dir(GO_BINARY_OUTPUT_DIR)
61+
outputs.files(goBuildExtension.additionalOutputFiles)
62+
outputs.cacheIf { true }
63+
64+
callMake("build")
65+
}
66+
67+
val goLangCiLint by tasks.registering(Exec::class) {
68+
description = "Run an external Go linter."
69+
group = "verification"
70+
71+
val reportPath = layout.buildDirectory.file("reports/golangci-lint-report.xml")
72+
inputs.files(goSources())
73+
inputs.property("goVersion", goVersion)
74+
75+
outputs.files(reportPath)
76+
outputs.cacheIf { true }
77+
78+
commandLine(
79+
"golangci-lint",
80+
"run",
81+
"--go=${inputs.properties["goVersion"]}",
82+
"--out-format=checkstyle:${reportPath.get().asFile}"
83+
)
84+
// golangci-lint returns non-zero exit code if there are issues, we don't want to fail the build in this case.
85+
// A report with issues will be later ingested by SonarQube.
86+
isIgnoreExitValue = true
87+
}
88+
89+
val testGoCode by tasks.registering(Exec::class) {
90+
description = "Test the executable produced by the compile go code step."
91+
group = "verification"
92+
93+
dependsOn(compileGo)
94+
callMake("test")
95+
}
96+
97+
tasks.named("clean") {
98+
dependsOn(cleanGoCode)
99+
}
100+
101+
tasks.named("assemble") {
102+
dependsOn(compileGo)
103+
}
104+
105+
tasks.named("test") {
106+
dependsOn(testGoCode)
107+
}
108+
109+
tasks.named("check") {
110+
dependsOn(goLangCiLint)
111+
}
112+
113+
rootProject.tasks.named("sonar") {
114+
// As the Go linter produces a report to be ingested by SonarQube, we need to add an explicit dependency to it.
115+
// See https://docs.sonarsource.com/sonarqube-server/latest/analyzing-source-code/scanners/sonarscanner-for-gradle/#task-dependencies
116+
dependsOn(goLangCiLint)
117+
}
118+
} else {
119+
val buildDockerImage by tasks.registering(Exec::class) {
120+
description = "Build the docker image to build the Go code."
121+
group = "build"
122+
123+
inputs.file(goBuildExtension.dockerfile)
124+
inputs.file("$projectDir/go.mod")
125+
inputs.file("$projectDir/go.sum")
126+
// Task outputs are not set, because it is too difficult to check if image is built;
127+
// We can ignore Gradle caches here, because Docker takes care of its own caches anyway.
128+
errorOutput = System.out
129+
130+
val uidProvider = objects.property<Long>()
131+
val os = DefaultNativePlatform.getCurrentOperatingSystem()
132+
if (os.isLinux || os.isMacOsX) {
133+
// UID of the user inside the container should match this of the host user, otherwise files from the host will be not accessible by the container.
134+
val uid = com.sun.security.auth.module.UnixSystem().uid
135+
uidProvider.set(uid)
136+
}
137+
138+
val noTrafficInspection = "false" == System.getProperty("trafficInspection")
139+
140+
val arguments = buildList {
141+
add("docker")
142+
add("buildx")
143+
add("build")
144+
add("--file")
145+
add(goBuildExtension.dockerfile.asFile.get().absolutePath)
146+
if (noTrafficInspection) {
147+
add("--build-arg")
148+
add("BUILD_ENV=dev")
149+
} else {
150+
add("--network=host")
151+
add("--build-arg")
152+
add("BUILD_ENV=dev_custom_cert")
153+
}
154+
if (uidProvider.isPresent) {
155+
add("--build-arg")
156+
add("UID=${uidProvider.get()}")
157+
}
158+
add("--build-arg")
159+
add("GO_VERSION=$goVersion")
160+
add("--platform")
161+
add("linux/amd64")
162+
add("-t")
163+
add("${project.name}-builder")
164+
add("--progress")
165+
add("plain")
166+
add("${project.projectDir}")
167+
}
168+
169+
commandLine(arguments)
170+
}
171+
172+
val compileGo by tasks.registering(Exec::class) {
173+
description = "Build the Go executable inside a Docker container."
174+
group = "build"
175+
dependsOn(buildDockerImage)
176+
errorOutput = System.out
177+
178+
inputs.files(allGoSourcesAndMakeScripts())
179+
inputs.property("goCrossCompile", isCrossCompile)
180+
outputs.files(goBuildExtension.additionalOutputFiles)
181+
outputs.dir(GO_BINARY_OUTPUT_DIR)
182+
outputs.cacheIf { true }
183+
184+
val platform = getPlatform()
185+
val arch = getArchitecture()
186+
187+
val workDir = goBuildExtension.dockerWorkDir.get()
188+
commandLine(
189+
"docker",
190+
"run",
191+
"--rm",
192+
"--network=host",
193+
"--platform",
194+
"linux/amd64",
195+
"--mount",
196+
"type=bind,source=${project.projectDir},target=$workDir",
197+
"--env",
198+
"GO_CROSS_COMPILE=${inputs.properties["goCrossCompile"]}",
199+
"${project.name}-builder",
200+
"bash",
201+
"-c",
202+
"cd $workDir && ./make.sh clean && ./make.sh build $platform $arch && ./make.sh test"
203+
)
204+
}
205+
206+
tasks.named("assemble") {
207+
dependsOn(compileGo)
208+
}
209+
}

gradle-modules/src/main/kotlin/org/sonarsource/cloudnative/gradle/BuildUtils.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package org.sonarsource.cloudnative.gradle
1818

1919
import java.io.File
2020
import java.util.HashSet
21+
import java.util.Locale
2122
import java.util.jar.JarInputStream
2223
import org.gradle.api.GradleException
2324
import org.gradle.api.Project
@@ -104,3 +105,20 @@ fun Project.commitHashProvider(ref: String = "HEAD") =
104105
providers.exec {
105106
commandLine("git", "rev-parse", ref)
106107
}.standardOutput.asText
108+
109+
fun getPlatform(): String {
110+
val os = System.getProperty("os.name").lowercase(Locale.getDefault())
111+
return when {
112+
os.contains("mac") -> "darwin"
113+
os.contains("win") -> "windows"
114+
else -> "linux"
115+
}
116+
}
117+
118+
fun getArchitecture(): String {
119+
val arch = System.getProperty("os.arch").lowercase(Locale.getDefault())
120+
return when {
121+
arch.contains("aarch64") -> "arm64"
122+
else -> "amd64"
123+
}
124+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* SonarSource Cloud Native Gradle Modules
3+
* Copyright (C) 2024-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonarsource.cloudnative.gradle
18+
19+
import org.gradle.api.Project
20+
import org.gradle.api.file.FileTree
21+
import org.gradle.api.file.RegularFile
22+
import org.gradle.api.file.RegularFileProperty
23+
import org.gradle.api.provider.Property
24+
import org.gradle.api.provider.SetProperty
25+
26+
const val GO_BINARY_OUTPUT_DIR = "build/executable"
27+
28+
interface GoBuild {
29+
val dockerfile: RegularFileProperty
30+
31+
/**
32+
* Working directory inside the container, e.g. `/home/sonarsource/sonar-go-to-slang`.
33+
*/
34+
val dockerWorkDir: Property<String>
35+
36+
val additionalOutputFiles: SetProperty<RegularFile>
37+
}
38+
39+
fun Project.allGoSourcesAndMakeScripts(): FileTree =
40+
fileTree(projectDir).matching {
41+
include(
42+
"**/*.go",
43+
"**/go.mod",
44+
"**/go.sum",
45+
"make.bat",
46+
"make.sh"
47+
)
48+
exclude("build/**")
49+
}
50+
51+
fun Project.goSources(): FileTree =
52+
fileTree(projectDir).matching {
53+
include("**/*.go")
54+
exclude("build/**", "**/*_generated.go")
55+
}

0 commit comments

Comments
 (0)