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
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
<!-- Copyright 2026, Datadog, Inc. -->

<!-- CLAUDE BOOTSTRAP INSTRUCTIONS
If you are Claude and reading this file, check if CLAUDE.md exists in the same directory.
If CLAUDE.md does not exist or only contains the redirect + Compact Instructions below, that
Expand Down Expand Up @@ -126,6 +128,8 @@ debug symbols via `buildDebug`, memory errors via ASan, suppressions in
Custom Kotlin-based native build plugin (`build-logic/`) instead of Gradle's
`cpp-library`/`cpp-application` plugins; it invokes `clang++`/`g++` directly. Build
configurations are discovered dynamically from `ConfigurationPresets.kt`, never hardcoded.
`buildNativeLibs` passes the resolved C++ compiler to Make as `CXX` and tracks it as an
input. Helper Makefiles retain their own flags; allocator probes remain unsanitized.
Two-JDK pattern: `JAVA_HOME` (build, JDK 17+, Gradle runs on 21 LTS) runs Gradle;
`JAVA_TEST_HOME` runs tests against the target JDK; target bytecode is Java 8 via
`--release 8`.
Expand Down
7 changes: 7 additions & 0 deletions build-logic/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
<!-- Copyright 2026, Datadog, Inc. -->

# Native Build Plugins

This directory contains a Gradle composite build that provides plugins for building C++ libraries and tests:
Expand Down Expand Up @@ -315,6 +317,11 @@ GtestPlugin consumes configurations from NativeBuildPlugin:
2. **Compiler detection**: Uses `PlatformUtils.findCompiler()` with `-Pnative.forceCompiler` support
3. **Consistent flags**: Inherits compiler/linker args from build configurations

Linux support helpers built by `buildNativeLibs` receive the resolved compiler as Make's
`CXX` variable. Changing the compiler invalidates the helper task. Their Makefiles own
the flags; configuration sanitizer flags are not forwarded, so allocator probes remain
unsanitized. Makefiles that hardcode a compiler instead of using `CXX` are unchanged.

## Example Output

```
Expand Down
6 changes: 6 additions & 0 deletions build-logic/conventions/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/*
* Copyright 2026, Datadog, Inc.
* SPDX-License-Identifier: Apache-2.0
*/

plugins {
`kotlin-dsl`
}
Expand All @@ -15,6 +20,7 @@ dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib")
implementation("com.diffplug.spotless:spotless-plugin-gradle:8.10.4")

testImplementation(gradleTestKit())
testImplementation("org.junit.jupiter:junit-jupiter-api:6.1.3")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:6.1.3")
testRuntimeOnly("org.junit.platform:junit-platform-launcher:6.1.3")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ class GtestPlugin : Plugin<Project> {
}

private fun createBuildNativeLibsTask(project: Project, extension: GtestExtension, hasGtest: Boolean) {
val compiler = findCompiler(project)
project.tasks.register("buildNativeLibs") {
group = "build"
description = "Build the native libs for the Google Tests"
Expand Down Expand Up @@ -132,13 +133,13 @@ class GtestPlugin : Plugin<Project> {
val libDir = File("$targetDir/$libName")
val libSrcDir = File("$srcDir/$libName")

// Use ProcessBuilder directly (Gradle 9 removed project.exec in task actions)
val process = ProcessBuilder("sh", "-c", """
echo "Processing library: $libName @ $libSrcDir"
mkdir -p $libDir
cd $libSrcDir
make TARGET_DIR=$libDir
""".trimIndent())
project.logger.lifecycle("Processing library: $libName @ $libSrcDir")
if (!libDir.isDirectory && !libDir.mkdirs()) {
throw org.gradle.api.GradleException("Failed to create native lib output directory: $libDir")
}
// Pass only the compiler, not sanitizer flags: allocator probes need the real allocator.
val process = ProcessBuilder("make", "TARGET_DIR=$libDir", "CXX=$compiler")
.directory(libSrcDir)
.inheritIO()
.start()
val exitCode = process.waitFor()
Expand All @@ -148,6 +149,7 @@ class GtestPlugin : Plugin<Project> {
}
}

inputs.property("compiler", compiler)
inputs.files(project.fileTree(srcDir))
outputs.dir(targetDir)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright 2026, Datadog, Inc.
* SPDX-License-Identifier: Apache-2.0
*/

package com.datadoghq.native.gtest

import org.gradle.testkit.runner.GradleRunner
import org.gradle.testkit.runner.TaskOutcome
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.condition.EnabledOnOs
import org.junit.jupiter.api.condition.OS
import org.junit.jupiter.api.io.TempDir
import java.io.File

/** Exercises the Linux helper task through real Gradle and Make invocations. */
@EnabledOnOs(OS.LINUX)
class GtestPluginTest {
@TempDir
lateinit var projectDir: File

@Test
fun `helpers use the selected compiler without sanitizer flags and rebuild when it changes`() {
projectDir.resolve("settings.gradle").writeText("rootProject.name = 'helper-compiler-test'\n")
projectDir.resolve("build.gradle.kts").writeText("""
import com.datadoghq.native.NativeBuildExtension
import com.datadoghq.native.gtest.GtestExtension
import com.datadoghq.native.model.Platform
import com.datadoghq.native.util.PlatformUtils

plugins { id("com.datadoghq.gtest") }

val nativeBuild = extensions.create(
"nativeBuild", NativeBuildExtension::class.java, project, objects
)
nativeBuild.buildConfigurations.create("asan") {
platform.set(Platform.LINUX)
architecture.set(PlatformUtils.currentArchitecture)
compilerArgs.set(listOf("-fsanitize=address"))
linkerArgs.set(listOf("-fsanitize=address"))
}
extensions.configure<GtestExtension>("gtest") {
testSourceDir.set(layout.projectDirectory.dir("src/test/cpp"))
mainSourceDir.set(layout.projectDirectory.dir("src/main/cpp"))
nativeLibsSourceDir.set(layout.projectDirectory.dir("native-libs"))
nativeLibsOutputDir.set(layout.buildDirectory.dir("native-libs"))
}
// The fixture only records Make arguments; it does not link Google Test.
afterEvaluate {
tasks.named("buildNativeLibs") { setOnlyIf { true } }
}
""".trimIndent())
val helper = projectDir.resolve("native-libs/probe")
assertTrue(helper.mkdirs())
helper.resolve("Makefile").writeText(
".PHONY: all\nall:\n" +
"\t@printf '%s\\n' '$(CXX)' > '$(TARGET_DIR)/compiler.txt'\n" +
"\t@printf '%s\\n' '$(CXXFLAGS)$(LDFLAGS)' > '$(TARGET_DIR)/flags.txt'\n"
)
val firstCompiler = compiler("first-c++")
val secondCompiler = compiler("second-c++")
val output = projectDir.resolve("build/native-libs/probe")

assertEquals(TaskOutcome.SUCCESS, build(firstCompiler))
assertEquals(firstCompiler.absolutePath, output.resolve("compiler.txt").readText().trim())
assertEquals("", output.resolve("flags.txt").readText().trim())
assertEquals(TaskOutcome.UP_TO_DATE, build(firstCompiler))

assertEquals(TaskOutcome.SUCCESS, build(secondCompiler))
assertEquals(secondCompiler.absolutePath, output.resolve("compiler.txt").readText().trim())
assertEquals("", output.resolve("flags.txt").readText().trim())
}

/** A version-only compiler fixture makes the selected executable distinguishable from Make's default. */
private fun compiler(name: String): File = projectDir.resolve(name).apply {
writeText("#!/bin/sh\nprintf '%s\\n' 'compiler fixture'\n")
assertTrue(setExecutable(true))
}

/** Runs the real helper task without unrelated flags inherited from the test runner. */
private fun build(compiler: File): TaskOutcome? = GradleRunner.create()
.withProjectDir(projectDir)
.withPluginClasspath()
.withEnvironment(System.getenv() + mapOf("CXXFLAGS" to "", "LDFLAGS" to ""))
.withArguments("buildNativeLibs", "-Pnative.forceCompiler=${compiler.absolutePath}", "--offline")
.build()
.task(":buildNativeLibs")?.outcome
}
12 changes: 12 additions & 0 deletions ddprof-lib/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,18 @@ gtest {
failFast.set(true)
}

// The isolated probe includes headers outside the native-libs resource tree.
// Track them explicitly so header-only edits cannot leave an old probe executable.
val mallocFootprintChecks = layout.projectDirectory.file("src/test/cpp/mallocFootprintChecks.h")
tasks.matching { it.name == "buildNativeLibs" }.configureEach {
inputs.files("src/main/cpp/mallocFootprint.h", mallocFootprintChecks)
}
tasks.matching {
it.name.startsWith("compileGtest") && it.name.endsWith("_mallocFootprint_ut")
}.configureEach {
inputs.file(mallocFootprintChecks)
}

// referenceChainJfrRoundtrip_ut writes a .jfr that :ddprof-test's ReferenceChainJfrParserTest
// reads back. Both sides get the path from this env var (set here and in ddprof-test's
// build.gradle.kts). The path is scoped per build configuration (derived from the gtest task
Expand Down
23 changes: 23 additions & 0 deletions ddprof-lib/src/test/cpp/mallocFootprintChecks.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright 2026, Datadog, Inc.
* SPDX-License-Identifier: Apache-2.0
*/
#ifndef _MALLOCFOOTPRINTCHECKS_H
#define _MALLOCFOOTPRINTCHECKS_H

#include <gtest/gtest.h>

// Shared with the isolated probe executable so these exact assertions run with
// both a discovered header and an inconclusive (zero) header estimate.
inline void checkOverheadFractions(double small_pct, double large_pct) {
ASSERT_GE(small_pct, 0);
ASSERT_GE(large_pct, 0);
// A zero header estimate still exposes usable-size rounding: a 96-byte
// request with 104 usable bytes has 8.33% overhead. Compare size classes
// without requiring double-digit overhead on every small allocation.
EXPECT_LT(large_pct, 1.0)
<< "a 512 KB chunk must not be charged a double-digit percentage";
EXPECT_GT(small_pct, large_pct * 10);
}

#endif // _MALLOCFOOTPRINTCHECKS_H
Loading
Loading