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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- Android builds with Minify (R8) enabled failed to launch the SDK with `ClassNotFoundException: com.bugsee.unity.UnityWrapper`: Unity does not read the package's `Plugins/Android/bugsee-unity-wrapper.proguard`, so R8 stripped the wrapper classes C# reaches over JNI. A new post-generate step (`BugseeAndroidProguardRules`) appends the rules, between markers and idempotently, to the generated `unityLibrary/proguard-unity.txt` (the library's consumer rules), so no Player Settings change is needed. The rules now also keep the `UnityManagedException` constructor (built by name from C#) and the SDK interfaces implemented with `AndroidJavaProxy`; tests fail when a class C# names over JNI, a wrapper method it calls, or a proxied interface has no rule.
- Android builds on Unity 6 failed in Gradle (`DependencyHandler.module(java.lang.Object)` while evaluating `:launcher`): the package wrote its own `Assets/Plugins/Android/baseProjectTemplate.gradle` and `launcherTemplate.gradle` pinning Android Gradle plugin 7.4.2, which cannot run on Unity 6's Gradle 9. The package no longer creates Gradle templates. It patches the Gradle project Unity generates instead (so nothing in Assets goes stale on Unity upgrades): the Bugsee Gradle plugin is added when the generated project pins AGP 8.6+ (previously: any Unity 6), and on 2021.3 the release build type gets `debugSymbolLevel 'FULL'`. The exact templates earlier package versions wrote are removed on an Android build when Unity's default AGP is another major; any other template pinning a different AGP major is left alone with a warning naming the file and the fix. The sample no longer ships custom Gradle templates. Verified with real IL2CPP arm64 builds on 2021.3.45f1 and 6000.0.84f1; Bugsee Gradle plugin 4.0.8 works with Unity 6's AGP 9.0.0 / Gradle 9.3.1.
- Android native symbols on Unity 6: release builds no longer force `debugSymbolLevel 'FULL'` in the launcher; the level follows Player Settings (Create symbols.zip), which Unity 6 defaults to Disabled. Public gives symbol tables only (no file:line); Debugging is about ten times larger. An Android build with a Bugsee app token set now warns when the level is not Debugging, naming what is lost and the size trade-off. On Unity 6 the setting is `UnityEditor.Android.UserBuildSettings.DebugSymbols` (level None/SymbolTable/Full plus a Zip format; the 2021.3 `androidCreateSymbols` is obsolete there), and the opt-in also enables the Zip format. Opt in with `Bugsee > Android > Use Full Native Symbols (Debugging) For Symbol Upload` (or `BUGSEE_ANDROID_FULL_SYMBOLS=1` in CI): that build uses Debugging and the setting is restored afterwards (the saved state lives in `Library/Bugsee`, survives an editor exit after an interrupted build, and is restored only while the settings still hold what the opt-in set; a non-Android build never touches it).
- Android Gradle cleanup never deletes a template you edited: a file is removed only when it is exactly what an earlier package version generated (or an unedited Unity 2021.3 default base copy), after copying it (and its `.meta`) to `Library/Bugsee/legacy-templates/<timestamp>/`. A marker-bearing launcher with your own changes is kept with a warning. A launcher that is left alone and still uses `lintOptions`/`aaptOptions` is reported, and an unreadable AGP version (version catalog, Kotlin DSL) now warns that the Bugsee Gradle plugin was skipped.
Expand Down
3 changes: 2 additions & 1 deletion Documentation~/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ Failure policy: **never fail the Unity build**. Missing maps/UUIDs/CLI → warn
2. Create symbols.zip / native debug symbols enabled
3. For primary LNM apply: native debug symbol level **FULL** (DWARF lines), not SYMBOL_TABLE-only
4. No custom Gradle templates needed: Bugsee patches the generated Gradle project (the Bugsee Gradle plugin with `ndk { enabled = true }` when it pins AGP 8.6+, i.e. Unity 6; on older AGPs, see below); a template of your own that pins another AGP major than your Unity's default fails the build and is reported
5. **Minify** (R8) may be enabled for Release and Debug with no extra setup: the package appends its keep rules (`Plugins/Android/bugsee-unity-wrapper.proguard`) to the generated `unityLibrary/proguard-unity.txt` on every Android build, because Unity does not read that file from a package. The rules keep the wrapper classes C# looks up by name over JNI (`UnityWrapper`, `UnityManagedException`) and the SDK interfaces implemented from C#. They sit between `# bugsee:proguard begin` / `end` markers and are replaced, not duplicated, on the next build. If you add your own JNI code, keep its classes in your Custom Proguard File

### Unity 2021.3 and 2022.3 (no Bugsee Gradle plugin)

Expand Down Expand Up @@ -190,7 +191,7 @@ Verified with real IL2CPP arm64 builds (staging token set, `bugsee-cli` 0.8.1, u

Not verified: AGP 8.0 to 8.5 (no block is added there by design), Unity 2021.3 releases with AGP below 7.4, AAB output, build with Gradle configuration cache (the block avoids `Task.project` at execution time but was run without it), and Windows/Linux Editors.

Releases built without R8 (Minify off, the Unity default) have no mapping and no Java names to restore. If you do minify, add keep rules for the Unity wrapper classes: the package's `bugsee-unity-wrapper.proguard` is not applied by Unity unless you enable Player Settings > Publishing Settings > Custom Proguard File and copy the rules into `Assets/Plugins/Android/proguard-user.txt`, and that file does not yet cover `com.bugsee.unity.UnityManagedException` or `UnityWrapperIdentity` (R8 removes them: `ClassNotFoundException` at launch or on the first managed exception).
Releases built without R8 (Minify off, the Unity default) have no mapping and no Java names to restore. Minify may be turned on with no extra setup: the package appends its keep rules to the generated `unityLibrary/proguard-unity.txt` on every build (see the Player Settings checklist above), so `UnityWrapper` and `UnityManagedException` survive R8 and the SDK launches.

**iOS**

Expand Down
63 changes: 63 additions & 0 deletions Editor/BugseeAndroidProguardRules.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using System;
using System.IO;
using Bugsee.WrapperPolicy;
using UnityEditor.Android;
using UnityEngine;

namespace Bugsee.Editor
{
/// <summary>
/// Makes R8 keep the wrapper classes C# reaches over JNI.
///
/// Unity does not read a loose proguard file from a package's <c>Plugins/Android</c> (the Custom Proguard File in
/// Player Settings is user-level), so with Minify enabled R8 stripped <c>UnityWrapper</c> and
/// <c>UnityManagedException</c> and the SDK failed with ClassNotFoundException. Every Unity version generates
/// <c>unityLibrary/proguard-unity.txt</c> and lists it as the library's <c>consumerProguardFiles</c>, so the rules
/// (<c>Plugins/Android/bugsee-unity-wrapper.proguard</c>) are appended there: no project setup, and it applies
/// to the app's R8 run.
///
/// Like <see cref="BugseeAndroidWrapperManifest"/> this is not behind a UNITY_ANDROID guard: the callback only
/// fires for Android builds.
/// </summary>
sealed class BugseeAndroidProguardRules : IPostGenerateGradleAndroidProject
{
public int callbackOrder => 21;

public void OnPostGenerateGradleAndroidProject(string unityLibraryPath)
{
try
{
var template = ResolveRulesPath();
if (template == null)
{
Debug.LogWarning(
"[Bugsee] Could not find Plugins/Android/bugsee-unity-wrapper.proguard in com.bugsee.unity; " +
"with Minify enabled R8 will strip the wrapper classes and the SDK will not launch.");
return;
}

var target = Path.Combine(unityLibraryPath, "proguard-unity.txt");
var existing = File.Exists(target) ? File.ReadAllText(target) : "";
var updated = ProguardRules.Apply(existing, File.ReadAllText(template));
if (updated != existing)
{
File.WriteAllText(target, updated);
Debug.Log("[Bugsee] Added the wrapper keep rules to " + target);
}
}
catch (Exception ex)
{
// Never fail the build, but be loud: a missing rule is a launch failure in minified builds.
Debug.LogWarning("[Bugsee] Failed to add the wrapper keep rules: " + ex);
}
}

static string ResolveRulesPath()
{
var info = UnityEditor.PackageManager.PackageInfo.FindForAssembly(typeof(BugseeAndroidProguardRules).Assembly);
if (info == null) return null;
var path = Path.Combine(info.resolvedPath, "Plugins", "Android", "bugsee-unity-wrapper.proguard");
return File.Exists(path) ? path : null;
}
}
}
11 changes: 11 additions & 0 deletions Editor/BugseeAndroidProguardRules.cs.meta

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

6 changes: 3 additions & 3 deletions Editor/BugseeAndroidWrapperManifest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public void OnPostGenerateGradleAndroidProject(string unityLibraryPath)
var template = ResolveTemplatePath();
if (template == null)
{
Debug.LogError(
Debug.LogWarning(
"[Bugsee] Could not find Plugins/Android/AndroidManifest.xml in com.bugsee.unity; " +
"the wrapper provider was not registered, so crashes recovered on relaunch will lack wrapper data.");
return;
Expand All @@ -46,7 +46,7 @@ public void OnPostGenerateGradleAndroidProject(string unityLibraryPath)
var target = Path.Combine(unityLibraryPath, "src", "main", "AndroidManifest.xml");
if (!File.Exists(target))
{
Debug.LogError("[Bugsee] unityLibrary manifest not found at " + target + "; wrapper provider not registered.");
Debug.LogWarning("[Bugsee] unityLibrary manifest not found at " + target + "; wrapper provider not registered.");
return;
}

Expand All @@ -57,7 +57,7 @@ public void OnPostGenerateGradleAndroidProject(string unityLibraryPath)
catch (Exception ex)
{
// Never fail the build, but be loud: a missing provider is a silent data-loss bug.
Debug.LogError("[Bugsee] Failed to register the wrapper provider: " + ex);
Debug.LogWarning("[Bugsee] Failed to register the wrapper provider: " + ex);
}
}

Expand Down
19 changes: 18 additions & 1 deletion Plugins/Android/bugsee-unity-wrapper.proguard
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# C# calls com.bugsee.unity.UnityWrapper.refineContext via JNI; R8 cannot trace it.
# Applied to the app's R8 run by Editor/BugseeAndroidProguardRules (appended to unityLibrary/proguard-unity.txt);
# Unity does not read this file from Plugins/Android itself. UnityWrapper and UnityManagedException are looked up by
# name from C# over JNI (AndroidJavaClass / AndroidJavaObject), which R8 cannot trace.
-keep class com.bugsee.unity.UnityWrapper {
static void refineContext(java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String);
static void channelLog(java.lang.String, int, int);
Expand All @@ -14,3 +16,18 @@
static void testCrash();
}
-keepnames class com.bugsee.unity.UnityWrapper$*
# C# builds this Throwable by name (new AndroidJavaObject("com.bugsee.unity.UnityManagedException", json)); the worker
# routes managed exceptions on its class name, so it must keep both the constructor and its name.
-keep class com.bugsee.unity.UnityManagedException {
<init>(java.lang.String);
}
# Not reached by name (its constants are inlined at compile time); kept so the identity file survives intact.
-keep class com.bugsee.unity.UnityWrapperIdentity { *; }
# C# implements these SDK interfaces with AndroidJavaProxy, which dispatches by method name. The SDK's own consumer
# rules keep them today; stating it here keeps the wrapper working if those rules narrow.
-keep interface com.bugsee.library.contracts.exchange.EventFilter { *; }
-keep interface com.bugsee.library.contracts.reporting.ReportHandler { *; }
-keep interface com.bugsee.library.contracts.reporting.ReportCreationListener { *; }
-keep interface com.bugsee.library.contracts.lifecycle.LifecycleEventListener { *; }
-keep interface com.bugsee.library.contracts.common.Callback1 { *; }
-keep interface com.bugsee.library.contracts.feedback.FeedbackListener { *; }
48 changes: 48 additions & 0 deletions Runtime/WrapperPolicy/ProguardRules.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System;
using System.Text;

namespace Bugsee.WrapperPolicy
{
/// <summary>
/// Puts the wrapper's keep rules into the generated project's consumer rules file, between markers, so that
/// applying them twice (or after the rules changed) leaves exactly one current copy.
/// </summary>
public static class ProguardRules
{
public const string BeginMarker = "# bugsee:proguard begin (managed by com.bugsee.unity; do not edit)";
public const string EndMarker = "# bugsee:proguard end";

public static string Apply(string existing, string rules)
{
if (existing == null) throw new ArgumentNullException(nameof(existing));
if (rules == null) throw new ArgumentNullException(nameof(rules));

string newline = existing.Contains("\r\n") ? "\r\n" : "\n";
string block = BeginMarker + newline
+ rules.Replace("\r\n", "\n").Trim('\n').Replace("\n", newline) + newline
+ EndMarker + newline;

int begin = existing.IndexOf(BeginMarker, StringComparison.Ordinal);
if (begin >= 0)
{
int end = existing.IndexOf(EndMarker, begin, StringComparison.Ordinal);
if (end >= 0)
{
int after = end + EndMarker.Length;
if (existing.Substring(after).StartsWith(newline, StringComparison.Ordinal)) after += newline.Length;
return existing.Substring(0, begin) + block + existing.Substring(after);
}

// A begin marker whose end marker is gone (a hand edit): what follows it may be someone else's
// rules, so only the dangling marker line is dropped and a complete block is appended.
int lineEnd = existing.IndexOf('\n', begin);
existing = existing.Substring(0, begin) + (lineEnd < 0 ? "" : existing.Substring(lineEnd + 1));
}

var sb = new StringBuilder(existing);
if (existing.Length > 0 && !existing.EndsWith("\n", StringComparison.Ordinal)) sb.Append(newline);
sb.Append(block);
return sb.ToString();
}
}
}
11 changes: 11 additions & 0 deletions Runtime/WrapperPolicy/ProguardRules.cs.meta

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,60 @@
package com.bugsee.unity;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.junit.Test;

/** Every member the keep rules name must exist, or the rule silently protects nothing. */
public class KeepRulesTests {
static Class<?> typeOf(String name) throws Exception {
switch (name) {
case "int": return int.class;
case "void": return void.class;
case "int[]": return int[].class;
default: return Class.forName(name);
}
}

static String rules() throws Exception {
return new String(
Files.readAllBytes(AndroidWrapperTests.repoFile("Plugins/Android/bugsee-unity-wrapper.proguard")),
StandardCharsets.UTF_8);
}

@Test
public void everyStaticMethodTheRulesKeepExistsOnUnityWrapper() throws Exception {
Matcher block = Pattern.compile("-keep class com\\.bugsee\\.unity\\.UnityWrapper \\{([^}]*)\\}").matcher(rules());
int checked = 0;
while (block.find()) {
Matcher member = Pattern.compile("static\\s+(\\S+)\\s+(\\w+)\\(([^)]*)\\);").matcher(block.group(1));
while (member.find()) {
List<Class<?>> params = new ArrayList<Class<?>>();
for (String p : member.group(3).split(",")) {
if (!p.trim().isEmpty()) params.add(typeOf(p.trim()));
}
Method m = UnityWrapper.class.getDeclaredMethod(member.group(2), params.toArray(new Class<?>[0]));
assertNotNull(m);
assertFalse(member.group(2) + " is not static", !Modifier.isStatic(m.getModifiers()));
assertFalse(member.group(2) + " returns " + m.getReturnType() + ", rule says " + member.group(1),
!m.getReturnType().equals(typeOf(member.group(1))));
checked++;
}
}
assertFalse("no static members found in the rules", checked == 0);
}

@Test
public void managedExceptionHasTheConstructorTheRulesKeep() throws Exception {
assertFalse(!rules().contains("<init>(java.lang.String);"));
assertNotNull(UnityManagedException.class.getConstructor(String.class));
}
}
Loading
Loading