diff --git a/build-logic/conventions/src/main/kotlin/com/datadoghq/native/NativeBuildPlugin.kt b/build-logic/conventions/src/main/kotlin/com/datadoghq/native/NativeBuildPlugin.kt index 33628c1f3..c2acf83cc 100644 --- a/build-logic/conventions/src/main/kotlin/com/datadoghq/native/NativeBuildPlugin.kt +++ b/build-logic/conventions/src/main/kotlin/com/datadoghq/native/NativeBuildPlugin.kt @@ -149,7 +149,7 @@ class NativeBuildPlugin : Plugin { project.logger.debug("Created tasks for configuration: ${config.name}") } - private fun findCompiler(project: Project): String = PlatformUtils.findCompiler(project) + private fun findCompiler(project: Project): String = PlatformUtils.findCompiler(project).executable private fun createAggregationTasks( project: Project, diff --git a/build-logic/conventions/src/main/kotlin/com/datadoghq/native/SimpleNativeLibPlugin.kt b/build-logic/conventions/src/main/kotlin/com/datadoghq/native/SimpleNativeLibPlugin.kt index f39087c02..1189a3c5e 100644 --- a/build-logic/conventions/src/main/kotlin/com/datadoghq/native/SimpleNativeLibPlugin.kt +++ b/build-logic/conventions/src/main/kotlin/com/datadoghq/native/SimpleNativeLibPlugin.kt @@ -46,7 +46,7 @@ class SimpleNativeLibPlugin : Plugin { return@afterEvaluate } - val compiler = extension.compiler.getOrElse(PlatformUtils.findCompiler(project)) + val compiler = extension.compiler.getOrElse(PlatformUtils.findCompiler(project).executable) val linker = extension.linker.getOrElse(compiler) val libraryName = extension.libraryName.get() val objectDir = extension.objectDir.get().asFile diff --git a/build-logic/conventions/src/main/kotlin/com/datadoghq/native/config/ConfigurationPresets.kt b/build-logic/conventions/src/main/kotlin/com/datadoghq/native/config/ConfigurationPresets.kt index b2be2b586..cea7c8d58 100644 --- a/build-logic/conventions/src/main/kotlin/com/datadoghq/native/config/ConfigurationPresets.kt +++ b/build-logic/conventions/src/main/kotlin/com/datadoghq/native/config/ConfigurationPresets.kt @@ -4,8 +4,10 @@ package com.datadoghq.native.config import com.datadoghq.native.model.Architecture import com.datadoghq.native.model.BuildConfiguration import com.datadoghq.native.model.Platform +import com.datadoghq.native.util.CompilerInfo import com.datadoghq.native.util.PlatformUtils import java.io.File +import org.gradle.api.logging.Logger /** * Provides factory methods for creating standard build configurations @@ -35,23 +37,25 @@ object ConfigurationPresets { val compiler = PlatformUtils.findCompiler(project) project.logger.lifecycle("Setting up standard build configurations for $currentPlatform-$currentArch") - project.logger.lifecycle("Using compiler: $compiler") + project.logger.lifecycle("Using compiler: ${compiler.executable} (version ${compiler.majorVersion})") + + val commonCompilerArgs = commonCompilerArgs(version, compiler, currentArch, currentPlatform, project.logger) extension.buildConfigurations.apply { register("release") { - configureRelease(this, currentPlatform, currentArch, version) + configureRelease(this, currentPlatform, currentArch, commonCompilerArgs) } register("debug") { - configureDebug(this, currentPlatform, currentArch, version) + configureDebug(this, currentPlatform, currentArch, commonCompilerArgs) } register("asan") { - configureAsan(this, currentPlatform, currentArch, version, rootDir, compiler) + configureAsan(this, currentPlatform, currentArch, rootDir, compiler, commonCompilerArgs) } register("tsan") { - configureTsan(this, currentPlatform, currentArch, version, rootDir, compiler) + configureTsan(this, currentPlatform, currentArch, rootDir, compiler, commonCompilerArgs) } register("fuzzer") { - configureFuzzer(this, currentPlatform, currentArch, version, rootDir) + configureFuzzer(this, currentPlatform, currentArch, rootDir, commonCompilerArgs) } } @@ -59,7 +63,13 @@ object ConfigurationPresets { project.logger.lifecycle("Active configurations: ${activeConfigs.map { it.name }.joinToString(", ")}") } - private fun commonLinuxCompilerArgs(version: String): List { + private fun commonCompilerArgs( + version: String, + compiler: CompilerInfo, + architecture: Architecture, + platform: Platform, + logger: Logger + ): List { val args = mutableListOf( "-fPIC", "-fno-omit-frame-pointer", @@ -75,6 +85,26 @@ object ConfigurationPresets { if (PlatformUtils.isMusl()) { args.add("-D__musl__") } + + // Use TLS descriptors (GNU2 dialect) for thread-local storage on x86_64 Linux, + // required by thread context (https://github.com/open-telemetry/opentelemetry-specification/pull/4947). + // The `-mtls-dialect=gnu2` spelling is x86-specific, so gate on platform/arch. + if (platform == Platform.LINUX && architecture == Architecture.X64) { + if (compiler.supportsTlsDialectGnu2()) { + args.add("-mtls-dialect=gnu2") + } else { + // Only clang older than 19 reaches here (gcc/others are always supported). + logger.lifecycle( + "GNU2 TLS dialect disabled: -mtls-dialect=gnu2 requires clang >= 19, " + + "but detected clang ${compiler.majorVersion} for ${compiler.executable}." + ) + } + } + + if (platform == Platform.MACOS) { + args += listOf("-D_XOPEN_SOURCE", "-D_DARWIN_C_SOURCE") + } + return args } @@ -88,14 +118,11 @@ object ConfigurationPresets { "-Wl,--build-id" ) - private fun commonMacosCompilerArgs(version: String): List = - commonLinuxCompilerArgs(version) + listOf("-D_XOPEN_SOURCE", "-D_DARWIN_C_SOURCE") - fun configureRelease( config: BuildConfiguration, platform: Platform, architecture: Architecture, - version: String + commonCompilerArgs: List ) { config.platform.set(platform) config.architecture.set(architecture) @@ -104,7 +131,7 @@ object ConfigurationPresets { when (platform) { Platform.LINUX -> { config.compilerArgs.set( - listOf("-O3", "-DNDEBUG", "-g") + commonLinuxCompilerArgs(version) + listOf("-O3", "-DNDEBUG", "-g") + commonCompilerArgs ) config.linkerArgs.set( commonLinuxLinkerArgs() + listOf( @@ -118,7 +145,7 @@ object ConfigurationPresets { } Platform.MACOS -> { config.compilerArgs.set( - commonMacosCompilerArgs(version) + listOf("-O3", "-DNDEBUG", "-g") + commonCompilerArgs + listOf("-O3", "-DNDEBUG", "-g") ) config.linkerArgs.set(emptyList()) } @@ -129,7 +156,7 @@ object ConfigurationPresets { config: BuildConfiguration, platform: Platform, architecture: Architecture, - version: String + commonCompilerArgs: List ) { config.platform.set(platform) config.architecture.set(architecture) @@ -138,13 +165,13 @@ object ConfigurationPresets { when (platform) { Platform.LINUX -> { config.compilerArgs.set( - listOf("-O0", "-g", "-DDEBUG") + commonLinuxCompilerArgs(version) + listOf("-O0", "-g", "-DDEBUG") + commonCompilerArgs ) config.linkerArgs.set(commonLinuxLinkerArgs()) } Platform.MACOS -> { config.compilerArgs.set( - commonMacosCompilerArgs(version) + listOf("-O0", "-g", "-DDEBUG") + commonCompilerArgs + listOf("-O0", "-g", "-DDEBUG") ) config.linkerArgs.set(emptyList()) } @@ -155,13 +182,13 @@ object ConfigurationPresets { config: BuildConfiguration, platform: Platform, architecture: Architecture, - version: String, rootDir: File, - compiler: String = "gcc" + compiler: CompilerInfo, + commonCompilerArgs: List ) { config.platform.set(platform) config.architecture.set(architecture) - config.active.set(PlatformUtils.hasAsan(compiler)) + config.active.set(PlatformUtils.hasAsan(compiler.executable)) val asanCompilerArgs = listOf( "-g", @@ -184,9 +211,9 @@ object ConfigurationPresets { when (platform) { Platform.LINUX -> { - config.compilerArgs.set(asanCompilerArgs + commonLinuxCompilerArgs(version)) + config.compilerArgs.set(asanCompilerArgs + commonCompilerArgs) - val libasan = PlatformUtils.locateLibasan(compiler) + val libasan = PlatformUtils.locateLibasan(compiler.executable) // Link against the sanitizer runtime that matches the compiler: // - clang: locateLibasan returns libclang_rt.asan-.so, which // includes UBSan symbols; -lclang_rt.asan- satisfies -z defs @@ -228,13 +255,13 @@ object ConfigurationPresets { config: BuildConfiguration, platform: Platform, architecture: Architecture, - version: String, rootDir: File, - compiler: String = "gcc" + compiler: CompilerInfo, + commonCompilerArgs: List ) { config.platform.set(platform) config.architecture.set(architecture) - config.active.set(PlatformUtils.hasTsan(compiler)) + config.active.set(PlatformUtils.hasTsan(compiler.executable)) val tsanCompilerArgs = listOf( "-g", @@ -247,9 +274,9 @@ object ConfigurationPresets { when (platform) { Platform.LINUX -> { - config.compilerArgs.set(tsanCompilerArgs + commonLinuxCompilerArgs(version)) + config.compilerArgs.set(tsanCompilerArgs + commonCompilerArgs) - val libtsan = PlatformUtils.locateLibtsan(compiler) + val libtsan = PlatformUtils.locateLibtsan(compiler.executable) // Use the library name from the resolved path so that clang's own // libclang_rt.tsan-.so is linked by name (not as -ltsan). val tsanLinkerArgs = if (libtsan != null) { @@ -295,8 +322,8 @@ object ConfigurationPresets { config: BuildConfiguration, platform: Platform, architecture: Architecture, - version: String, - rootDir: File + rootDir: File, + commonCompilerArgs: List ) { config.platform.set(platform) config.architecture.set(architecture) @@ -321,7 +348,7 @@ object ConfigurationPresets { when (platform) { Platform.LINUX -> { - config.compilerArgs.set(fuzzerCompilerArgs + commonLinuxCompilerArgs(version)) + config.compilerArgs.set(fuzzerCompilerArgs + commonCompilerArgs) config.linkerArgs.set(commonLinuxLinkerArgs() + fuzzerLinkerArgs) config.testEnvironment.apply { @@ -330,7 +357,7 @@ object ConfigurationPresets { } } Platform.MACOS -> { - config.compilerArgs.set(fuzzerCompilerArgs + commonMacosCompilerArgs(version)) + config.compilerArgs.set(fuzzerCompilerArgs + commonCompilerArgs) config.linkerArgs.set(fuzzerLinkerArgs) config.testEnvironment.apply { diff --git a/build-logic/conventions/src/main/kotlin/com/datadoghq/native/gtest/GtestPlugin.kt b/build-logic/conventions/src/main/kotlin/com/datadoghq/native/gtest/GtestPlugin.kt index d2196cf8d..eafe356ac 100644 --- a/build-logic/conventions/src/main/kotlin/com/datadoghq/native/gtest/GtestPlugin.kt +++ b/build-logic/conventions/src/main/kotlin/com/datadoghq/native/gtest/GtestPlugin.kt @@ -253,7 +253,7 @@ class GtestPlugin : Plugin { } } - private fun findCompiler(project: Project): String = PlatformUtils.findCompiler(project) + private fun findCompiler(project: Project): String = PlatformUtils.findCompiler(project).executable private fun getGtestIncludes(extension: GtestExtension): List { return when (PlatformUtils.currentPlatform) { diff --git a/build-logic/conventions/src/main/kotlin/com/datadoghq/native/util/PlatformUtils.kt b/build-logic/conventions/src/main/kotlin/com/datadoghq/native/util/PlatformUtils.kt index c6fb32129..2b7c2dad1 100644 --- a/build-logic/conventions/src/main/kotlin/com/datadoghq/native/util/PlatformUtils.kt +++ b/build-logic/conventions/src/main/kotlin/com/datadoghq/native/util/PlatformUtils.kt @@ -5,12 +5,27 @@ import com.datadoghq.native.model.Architecture import com.datadoghq.native.model.Platform import org.gradle.api.GradleException import org.gradle.api.Project +import org.gradle.api.logging.Logger import java.io.File import kotlin.io.path.createTempFile import kotlin.io.path.deleteIfExists import kotlin.io.path.writeText import java.util.concurrent.TimeUnit +/** + * A resolved C++ compiler: the executable plus its clang-family flag and major + * version, which are parsed from the `--version` banner (not the executable name). + */ +data class CompilerInfo(val executable: String, val isClang: Boolean, val majorVersion: Int) { + init { + require(executable.isNotBlank()) { "compiler executable must not be blank" } + require(majorVersion > 0) { "compiler majorVersion must be positive, was $majorVersion" } + } + + // gcc has supported GNU2 TLS for x86_64 for a long time, clang only since 19. + fun supportsTlsDialectGnu2(): Boolean = !isClang || majorVersion >= 19 +} + object PlatformUtils { val currentPlatform: Platform by lazy { Platform.current() } val currentArchitecture: Architecture by lazy { Architecture.current() } @@ -199,6 +214,51 @@ object PlatformUtils { } } + /** + * Detect a compiler's family and major version from ` --version`, + * or null if neither can be parsed. The family comes from the matching banner + * shape (not the executable name): "clang version " -> clang; + * ") ." -> non-clang. Failures are logged via [logger]. + */ + fun detectCompiler(compiler: String, logger: Logger): CompilerInfo? { + val output = try { + val process = ProcessBuilder(compiler, "--version") + .redirectErrorStream(true) + .start() + val text = process.inputStream.bufferedReader().readText() + if (!process.waitFor(5, TimeUnit.SECONDS)) { + process.destroyForcibly() + process.waitFor(5, TimeUnit.SECONDS) + logger.warn("Timed out after 5s running '$compiler --version'.") + return null + } + if (process.exitValue() != 0) { + logger.warn("'$compiler --version' exited with ${process.exitValue()}: ${text.trim().take(500)}") + return null + } + text + } catch (e: InterruptedException) { + Thread.currentThread().interrupt() + return null + } catch (e: Exception) { + logger.warn("Failed to run '$compiler --version'", e) + return null + } + + Regex("""clang version (\d+)""").find(output)?.let { match -> + match.groupValues[1].toIntOrNull()?.let { major -> + return CompilerInfo(compiler, isClang = true, majorVersion = major) + } + } + Regex("""\)\s+(\d+)\.\d+""").find(output)?.let { match -> + match.groupValues[1].toIntOrNull()?.let { major -> + return CompilerInfo(compiler, isClang = false, majorVersion = major) + } + } + logger.warn("Could not parse a compiler version from '$compiler --version' output: ${output.trim().take(500)}") + return null + } + fun hasAsan(compiler: String = "gcc"): Boolean { return !isMusl() && locateLibasan(compiler) != null } @@ -294,7 +354,7 @@ object PlatformUtils { return "$homebrewLLVM/bin/clang++" } } - return findCompiler(project) + return findCompiler(project).executable } /** @@ -319,9 +379,18 @@ object PlatformUtils { /** * Find a C++ compiler, respecting -Pnative.forceCompiler property. - * Auto-detects clang++ or g++ if not specified. + * Auto-detects clang++ or g++ if not specified. Throws if the version cannot + * be determined ([detectCompiler] logs the specific reason). */ - fun findCompiler(project: Project): String { + fun findCompiler(project: Project): CompilerInfo { + val executable = resolveCompilerExecutable(project) + return detectCompiler(executable, project.logger) + ?: throw GradleException( + "Unable to determine the version of compiler '$executable'." + ) + } + + private fun resolveCompilerExecutable(project: Project): String { // Check for forced compiler override val forcedCompiler = project.findProperty("native.forceCompiler") as? String if (forcedCompiler != null) { diff --git a/ddprof-lib/benchmarks/build.gradle.kts b/ddprof-lib/benchmarks/build.gradle.kts index d3f0d2f3a..bc4295c2a 100644 --- a/ddprof-lib/benchmarks/build.gradle.kts +++ b/ddprof-lib/benchmarks/build.gradle.kts @@ -22,7 +22,7 @@ val shouldBuild = PlatformUtils.currentPlatform == Platform.MACOS || PlatformUtils.currentPlatform == Platform.LINUX if (shouldBuild) { - val compiler = PlatformUtils.findCompiler(project) + val compiler = PlatformUtils.findCompiler(project).executable // Compile task val compileTask = tasks.register("compileBenchmark") {