Skip to content
Merged
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 packages/Datadog.Unity/Editor/DatadogGradlePostProcessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2025-Present Datadog, Inc.

using System;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using UnityEditor.Android;
using UnityEngine;

namespace Datadog.Unity.Editor
{
/// <summary>
/// Modifies a Unity project's build.gradle file to ensure compatibility with certain transitive dependencies
/// included by dd-sdk-android.
/// </summary>
public class DatadogGradlePostProcessor : IPostGenerateGradleAndroidProject
{
// These comments mark the start and end of the section in the `dependencies` block where EDM4U writes gradle
// dependencies
private const string EdmHeaderText = "// Android Resolver Dependencies Start";
private const string EdmFooterText = "// Android Resolver Dependencies End";

public int callbackOrder => 999; // Run after EDM4U

public void OnPostGenerateGradleAndroidProject(string path)
{
// Early-out if we're building in an environment that requires no fixes
if (!RequiresAndroidxMetricsCompatibilityFix())
{
return;
}

// Modify the generated build.gradle file; abort silently if it doesn't exist
string gradlePath = Path.Combine(path, "build.gradle");
if (!File.Exists(gradlePath))
{
return;
}

// Read the existing contents of build.gradle, normalizing line endings
string[] lines = File.ReadAllLines(gradlePath);

// Ensure that our dependency on dd-sdk-android-rum is declared in such a way that all transitive
// dependencies are resolved to versions that are compatible with this version of Unity
lines = ApplyAndroidxMetricsCompatibilityFix(lines);

// Write our modifications to the file
File.WriteAllLines(gradlePath, lines);
}

/// <summary>
/// Evaluates whether we need to apply a compatibility fix to build.gradle in order to prevent build errors due
/// to the inclusion of `androidx.metrics:metrics-performance:1.0.0-beta02`, which requires AGP 8.6.0+. Unity
/// 2022 and 2021 use AGP 7.x, so they require the fix.
/// </summary>
/// <returns>True if the detected Unity version predates Unity 6.</returns>
private bool RequiresAndroidxMetricsCompatibilityFix()
{
string version = Application.unityVersion;
string[] parts = version.Split('.');
if (int.TryParse(parts[0], out int majorVersion))
{
return majorVersion < 6000;
}
return false;
}

/// <summary>
/// Modifies the contents of a build.gradle file to apply the compatibility fix for
/// androidx.metrics:metrics-performance, downgrading it from 1.0.0-beta02 to 1.0.0-beta01.
/// </summary>
/// <param name="lines">The complete set of lines parsed from a build.gradle file.</param>
/// <returns>The same set of lines with androidx.metrics:metrics-performance downgraded to beta01.</returns>
public static string[] ApplyAndroidxMetricsCompatibilityFix(string[] lines)
{
// Find the start and end of the EDM4U dependencies, and abort silently if there's no such section
int edmHeaderIndex = Array.IndexOf(lines, EdmHeaderText);
int edmFooterIndex = Array.IndexOf(lines, EdmFooterText, edmHeaderIndex + 1);
if (edmHeaderIndex == -1 || edmFooterIndex == -1)
{
return lines;
}

// Find the first `implementation` directive that declares dd-sdk-android-rum as a dependency, using a regex
// that will capture the relevant details of that declaration, and ensuring that it's located within the
// EDM4U-managed section of the file
var regex = new Regex(@"^(\s+)implementation([ \(])(['""]com\.datadoghq:dd-sdk-android-rum:.*['""])(\)\s*{)?(?:\s*(\/\/.*))?");
var found = lines
.Select((line, index) => (Match: regex.Match(line), Index: index))
.FirstOrDefault(t => t.Match.Success);
if (found.Match == null || !found.Match.Success || found.Index <= edmHeaderIndex ||
found.Index >= edmFooterIndex)
{
return lines;
}

// Parse the dependency declaration so we can examine whether it's a single-line statement as written by
// EDM4U, e.g.:
// implementation 'com.datadoghq:dd-sdk-android-rum:2.20.0' // DatadogDependencies.xml:12
// ...or else a multi-line declaration that we've already modified, e.g.:
// implementation('com.datadoghq:dd-sdk-android-rum:2.20.0') { // DatadogDependencies.xml:12
string indentStr = found.Match.Groups[1].Value; // Whitespace chars for a single-level indent
string openStr = found.Match.Groups[2].Value; // Either space or '('
string packageSpecLiteral = found.Match.Groups[3].Value; // Full package specifier, as quoted string literal
string closeStr = found.Match.Groups[4].Value; // Either nothing or ') {'
string comment = found.Match.Groups[5].Value; // Any comment appearing at the end of the line, incl. '//'

// If the declaration already has a body, we'll assume that the necessary edit has already been made by a
// previous invocation of our callback
if (openStr.Contains("(") || closeStr.Contains(") {"))
{
return lines;
}

// Otherwise, the line at found.Index is a single-line 'implementation' declaration: replace it with an
// expanded dependency specifier that explicitly excludes the problematic version of
// `androidx.metrics:metrics-performance`, then follow it with another declaration that pulls in the version
// of that library that's compatible with AGP 7.x
string[] newDeclarationLines =
{
indentStr + "implementation(" + packageSpecLiteral + ") {" + (comment.Length > 0 ? $" {comment}" : string.Empty),
indentStr + indentStr + "// DatadogGradlePostProcessor: exclude the dependency on androidx.metrics:metrics-performance:1.0.0-beta02",
indentStr + indentStr + "// Version beta02 requires Android Gradle plugin 8.6.0+, which is not supported on Unity 2022 and older",
indentStr + indentStr + "exclude group: 'androidx.metrics', module: 'metrics-performance'",
indentStr + "}",
indentStr + "// DatadogGradlePostProcessor: Explicitly require version beta01 of the same dependency, as it works with AGP 7",
indentStr + "implementation 'androidx.metrics:metrics-performance:1.0.0-beta01'",
};
return lines.Take(found.Index).Concat(newDeclarationLines).Concat(lines.Skip(found.Index + 1)).ToArray();
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2025-Present Datadog, Inc.

using NUnit.Framework;

namespace Datadog.Unity.Editor.Tests
{
public class DatadogGradlePostProcessorTests
{
[Test]
public void ModifiesAndroidxMetricsDependencyIfRumDependencyIsDeclared()
{
string[] lines = GradleFileAsWrittenByEdm.Split("\n");
string[] gotLines = DatadogGradlePostProcessor.ApplyAndroidxMetricsCompatibilityFix(lines);
string got = string.Join("\n", gotLines);
Assert.AreEqual(GradleFileAsModifiedByDatadogGradlePostProcessor, got);
}

[Test]
public void HasNoEffectWhenRunAgain()
{
string[] lines = GradleFileAsModifiedByDatadogGradlePostProcessor.Split("\n");
string[] gotLines = DatadogGradlePostProcessor.ApplyAndroidxMetricsCompatibilityFix(lines);
Assert.AreEqual(lines, gotLines);
}

[Test]
public void HasNoEffectIfEdmSectionDoesNotExist()
{
string[] lines = GradleFileWithoutEdmDependencies.Split("\n");
string[] gotLines = DatadogGradlePostProcessor.ApplyAndroidxMetricsCompatibilityFix(lines);
Assert.AreEqual(lines, gotLines);
}

private const string GradleFileAsWrittenByEdm = @"apply plugin: 'com.android.library'


dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
// Android Resolver Dependencies Start
implementation 'com.datadoghq:dd-sdk-android-logs:2+' // Packages/com.datadoghq.unity/Editor/DatadogDependencies.xml:10
implementation 'com.datadoghq:dd-sdk-android-ndk:2+' // Packages/com.datadoghq.unity/Editor/DatadogDependencies.xml:12
implementation 'com.datadoghq:dd-sdk-android-rum:2+' // Packages/com.datadoghq.unity/Editor/DatadogDependencies.xml:14
implementation 'com.example:some-other-dependency:4.13.0' // Packages/com.datadoghq.unity/Editor/DatadogDependencies.xml:16
// Android Resolver Dependencies End

constraints {
implementation(""org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0"") {
because(""kotlin-stdlib-jdk8 is now a part of kotlin-stdlib"")
}
}

}

// Android Resolver Exclusions Start
android {
namespace ""com.unity3d.player""
packagingOptions {
exclude ('/lib/armeabi/*' + '*')
exclude ('/lib/mips/*' + '*')
exclude ('/lib/mips64/*' + '*')
exclude ('/lib/x86/*' + '*')
exclude ('/lib/x86_64/*' + '*')
}
}
// Android Resolver Exclusions End
android {
ndkPath ""/Applications/Unity/Hub/Editor/2022.3.55f1/PlaybackEngines/AndroidPlayer/NDK""

compileSdkVersion 35
buildToolsVersion '34.0.0'

compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}

defaultConfig {
minSdkVersion 24
targetSdkVersion 35
ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a'
}
versionCode 1
versionName '1.0'
consumerProguardFiles 'proguard-unity.txt'
}

lintOptions {
abortOnError false
}

aaptOptions {
noCompress = ['.unity3d', '.ress', '.resource', '.obb', '.bundle', '.unityexp'] + unityStreamingAssets.tokenize(', ')
ignoreAssetsPattern = ""!.svn:!.git:!.ds_store:!*.scc:!CVS:!thumbs.db:!picasa.ini:!*~""
}

packagingOptions {
doNotStrip '*/armeabi-v7a/*.so'
doNotStrip '*/arm64-v8a/*.so'
jniLibs {
useLegacyPackaging true
}
}
}
";

private const string GradleFileAsModifiedByDatadogGradlePostProcessor = @"apply plugin: 'com.android.library'


dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
// Android Resolver Dependencies Start
implementation 'com.datadoghq:dd-sdk-android-logs:2+' // Packages/com.datadoghq.unity/Editor/DatadogDependencies.xml:10
implementation 'com.datadoghq:dd-sdk-android-ndk:2+' // Packages/com.datadoghq.unity/Editor/DatadogDependencies.xml:12
implementation('com.datadoghq:dd-sdk-android-rum:2+') { // Packages/com.datadoghq.unity/Editor/DatadogDependencies.xml:14
// DatadogGradlePostProcessor: exclude the dependency on androidx.metrics:metrics-performance:1.0.0-beta02
// Version beta02 requires Android Gradle plugin 8.6.0+, which is not supported on Unity 2022 and older
exclude group: 'androidx.metrics', module: 'metrics-performance'
}
// DatadogGradlePostProcessor: Explicitly require version beta01 of the same dependency, as it works with AGP 7
implementation 'androidx.metrics:metrics-performance:1.0.0-beta01'
implementation 'com.example:some-other-dependency:4.13.0' // Packages/com.datadoghq.unity/Editor/DatadogDependencies.xml:16
// Android Resolver Dependencies End

constraints {
implementation(""org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0"") {
because(""kotlin-stdlib-jdk8 is now a part of kotlin-stdlib"")
}
}

}

// Android Resolver Exclusions Start
android {
namespace ""com.unity3d.player""
packagingOptions {
exclude ('/lib/armeabi/*' + '*')
exclude ('/lib/mips/*' + '*')
exclude ('/lib/mips64/*' + '*')
exclude ('/lib/x86/*' + '*')
exclude ('/lib/x86_64/*' + '*')
}
}
// Android Resolver Exclusions End
android {
ndkPath ""/Applications/Unity/Hub/Editor/2022.3.55f1/PlaybackEngines/AndroidPlayer/NDK""

compileSdkVersion 35
buildToolsVersion '34.0.0'

compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}

defaultConfig {
minSdkVersion 24
targetSdkVersion 35
ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a'
}
versionCode 1
versionName '1.0'
consumerProguardFiles 'proguard-unity.txt'
}

lintOptions {
abortOnError false
}

aaptOptions {
noCompress = ['.unity3d', '.ress', '.resource', '.obb', '.bundle', '.unityexp'] + unityStreamingAssets.tokenize(', ')
ignoreAssetsPattern = ""!.svn:!.git:!.ds_store:!*.scc:!CVS:!thumbs.db:!picasa.ini:!*~""
}

packagingOptions {
doNotStrip '*/armeabi-v7a/*.so'
doNotStrip '*/arm64-v8a/*.so'
jniLibs {
useLegacyPackaging true
}
}
}
";

private const string GradleFileWithoutEdmDependencies = @"apply plugin: 'com.android.library'

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.example:some-other-dependency:4.13.0'

constraints {
implementation(""org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0"") {
because(""kotlin-stdlib-jdk8 is now a part of kotlin-stdlib"")
}
}

}

android {
ndkPath ""/Applications/Unity/Hub/Editor/2022.3.55f1/PlaybackEngines/AndroidPlayer/NDK""

compileSdkVersion 35
buildToolsVersion '34.0.0'

compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
}
";
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.