From 108677e961f0e2425083e87fd73feef3f27369f8 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Thu, 10 Sep 2026 16:04:28 +0200 Subject: [PATCH 1/6] fix(windows): ship the native library as `sentry-native` Native crash capture never worked on Windows with the Mono scripting backend. Mono probes the calling assembly's own folder first, so `DllImport("sentry")` from `Sentry.Unity.Native.dll` resolved to the managed `Sentry.dll` sitting beside it in `Managed/` on a case-insensitive file system. That load succeeds, the C entry point is missing, and native support dies with an `EntryPointNotFoundException` that only surfaces when the diagnostic logger is enabled. The desktop library now ships renamed. `native-sdks.targets` writes it out as `sentry-native.dll`, `libsentry-native.so` and `libsentry-native.dylib` while the SDK is built, so the package already carries the new names and the post-build step stays a plain copy. Debug sidecars keep the names their binaries record. Android cannot follow. Its `libsentry.so` comes from the sentry-android-ndk AAR and sentry-java loads it by name from Java, so it gets its own `Sentry.Unity.Native.Android.dll`, built from the same sources with a define the way the console variants already are. Closes #2818 Co-authored-by: Lou Garczynski Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01A1tV2g8KkFQGYdCGLWSay6 --- build/native-sdks.targets | 48 ++++++++---- docs/agent-guides/platform-native.md | 11 ++- .../Sentry.Unity.Native.Android.dll.meta | 78 +++++++++++++++++++ .../Runtime/Sentry.Unity.Native.dll.meta | 4 +- scripts/download-native-sdks.ps1 | 10 +-- .../Sentry.Unity.Android.csproj | 8 +- .../Native/BuildPostProcess.cs | 27 ++++--- src/Sentry.Unity.Native/CFunctions.cs | 6 +- .../Sentry.Unity.Native.csproj | 20 +++++ src/Sentry.Unity.Native/SentryNative.cs | 2 +- src/Sentry.Unity.Native/SentryNativeBridge.cs | 7 +- .../SentryNativeLibrary.cs | 24 ++++++ src/Sentry.Unity/Properties/AssemblyInfo.cs | 1 + .../package-release.zip.snapshot | 28 ++++--- 14 files changed, 213 insertions(+), 61 deletions(-) create mode 100644 package-dev/Runtime/Sentry.Unity.Native.Android.dll.meta create mode 100644 src/Sentry.Unity.Native/SentryNativeLibrary.cs diff --git a/build/native-sdks.targets b/build/native-sdks.targets index 1b6f0cc53..75aa76f75 100644 --- a/build/native-sdks.targets +++ b/build/native-sdks.targets @@ -18,6 +18,16 @@ $(SentryArtifactsDestination)Linux/SentryNative~/ $(SentryArtifactsDestination)Windows/Sentry~/ $(SentryArtifactsDestination)Windows/SentryNative~/ + + + sentry-native.dll + libsentry-native.so + libsentry-native.dylib + @@ -66,17 +77,17 @@ - + - + - + @@ -89,6 +100,7 @@ + @@ -100,16 +112,16 @@ - + - + - + @@ -130,6 +142,7 @@ + @@ -142,19 +155,19 @@ - + - + @@ -166,6 +179,7 @@ + @@ -180,7 +194,6 @@ - @@ -189,8 +202,9 @@ + - + + + @@ -220,19 +236,19 @@ - + - + - + - + + + + $(PackageRuntimePath)/Sentry.Unity.Native.Android.dll + diff --git a/src/Sentry.Unity.Editor/Native/BuildPostProcess.cs b/src/Sentry.Unity.Editor/Native/BuildPostProcess.cs index 3a3629fe2..806dd1b21 100644 --- a/src/Sentry.Unity.Editor/Native/BuildPostProcess.cs +++ b/src/Sentry.Unity.Editor/Native/BuildPostProcess.cs @@ -139,6 +139,13 @@ _ when target.IsSwitch2() => options.SwitchNativeSupportEnabled, _ => false, }; + // The names the package already ships the desktop runtime library under. `native-sdks.targets` + // renames it when the SDK is built. See `SentryNativeLibrary` in Sentry.Unity.Native for why + // binding to plain `sentry` breaks under Mono. Kept here only to clear stale artifacts. + internal const string WindowsLibraryName = "sentry-native.dll"; + internal const string LinuxLibraryName = "libsentry-native.so"; + internal const string MacOSLibraryName = "libsentry-native.dylib"; + private readonly struct NativePluginArtifact(string source, string destination, bool isExecutable = false) { public readonly string Source = source; @@ -165,7 +172,7 @@ private static IEnumerable GetNativePluginArtifact( $"Sentry Windows plugin directory not found: {windowsBackendSourcePath}\n" + $"Run 'dotnet msbuild /t:{buildTarget} src/Sentry.Unity' (or 'dotnet msbuild /t:DownloadNativeSDKs src/Sentry.Unity') to populate it."); } - // Flat copy of every non-PDB file next to the player .exe — sentry.dll and the + // Flat copy of every non-PDB file next to the player .exe. The native library and the // crash handler (crashpad_handler.exe / sentry-crash.exe) all sit at the build root. // PDBs stay in the package and are consumed at symbol-upload time only. foreach (var file in Directory.GetFiles(windowsBackendSourcePath)) @@ -210,8 +217,8 @@ private static IEnumerable GetNativePluginArtifact( $"Sentry Linux plugin directory not found: {linuxBackendSourcePath}\n" + $"Run 'dotnet msbuild /t:{buildTarget} src/Sentry.Unity' (or 'dotnet msbuild /t:DownloadNativeSDKs src/Sentry.Unity') to populate it."); } - // libsentry.so must sit in the player's native plugin dir (_Data/Plugins/x86_64) where the - // Linux player resolves DllImport("sentry"). The crash daemon (sentry-crash, native backend only) + // The native library must sit in the player's native plugin dir (_Data/Plugins/x86_64) + // where the Linux player resolves the P/Invoke. The crash daemon (sentry-crash, native backend only) // sits next to the player executable so sentry-native can spawn it on crash. // The .dbg.so / .dbg debug sidecars stay in the package and are consumed at symbol-upload time only. var linuxPluginDir = GetLinuxPluginDir(buildOutputDir); @@ -252,11 +259,9 @@ private static IEnumerable GetNativePluginArtifact( } } - // On case-insensitive APFS, leftover artifacts from a prior build with - // the *other* macOS backend break DllImport("sentry") resolution - // (Sentry.dylib gets picked over libsentry.dylib, surfacing as - // `sentry_options_new` not found at runtime). Wipe both candidates - // before copying the current backend's files in. + // Wipe both backends' leftovers before copying the current one in, so an iterative build does + // not leave two libraries sitting in PlugIns. `libsentry.dylib` is the pre-rename name and only + // turns up when building over a player made by an older SDK. private static void CleanupStaleMacOSArtifacts(IDiagnosticLogger logger, string executablePath) { var contents = Path.Combine(executablePath, "Contents"); @@ -264,6 +269,7 @@ private static void CleanupStaleMacOSArtifacts(IDiagnosticLogger logger, string { Path.Combine(contents, "PlugIns", "Sentry.dylib"), Path.Combine(contents, "PlugIns", "libsentry.dylib"), + Path.Combine(contents, "PlugIns", MacOSLibraryName), Path.Combine(contents, "MacOS", "sentry-crash"), }) { @@ -287,6 +293,8 @@ private static void CleanupStaleWindowsArtifacts(IDiagnosticLogger logger, strin Path.Combine(buildOutputDir, "crashpad_wer.dll"), Path.Combine(buildOutputDir, "sentry-crash.exe"), Path.Combine(buildOutputDir, "sentry-wer.dll"), + Path.Combine(buildOutputDir, "sentry.dll"), + Path.Combine(buildOutputDir, WindowsLibraryName), }) { if (File.Exists(stale)) @@ -321,6 +329,7 @@ private static void CleanupStaleLinuxArtifacts(IDiagnosticLogger logger, string if (dataDir is not null) { stalePaths.Add(Path.Combine(dataDir, "Plugins", "x86_64", "libsentry.so")); + stalePaths.Add(Path.Combine(dataDir, "Plugins", "x86_64", LinuxLibraryName)); } foreach (var stale in stalePaths) @@ -494,7 +503,7 @@ private static void UploadDebugSymbols(IDiagnosticLogger logger, BuildTarget tar if (options.Experimental.MacosBackend == MacosBackend.Native) { var packageMacOSDir = $"Packages/{SentryPackageInfo.GetName()}/Plugins/macOS/SentryNative~"; - AddPath(paths, Path.GetFullPath($"{packageMacOSDir}/libsentry.dylib.dSYM"), logger); + AddPath(paths, Path.GetFullPath($"{packageMacOSDir}/libsentry-native.dylib.dSYM"), logger); AddPath(paths, Path.GetFullPath($"{packageMacOSDir}/sentry-crash.dSYM"), logger); } else diff --git a/src/Sentry.Unity.Native/CFunctions.cs b/src/Sentry.Unity.Native/CFunctions.cs index e0bef2db9..ec7a27909 100644 --- a/src/Sentry.Unity.Native/CFunctions.cs +++ b/src/Sentry.Unity.Native/CFunctions.cs @@ -8,11 +8,7 @@ namespace Sentry.Unity.Native; internal static class C { -#if SENTRY_NATIVE_SWITCH - private const string SentryLib = "__Internal"; -#else - private const string SentryLib = "sentry"; -#endif + private const string SentryLib = SentryNativeLibrary.Name; internal static void SetValueIfNotNull(sentry_value_t obj, string key, string? value) { diff --git a/src/Sentry.Unity.Native/Sentry.Unity.Native.csproj b/src/Sentry.Unity.Native/Sentry.Unity.Native.csproj index ca7e5386e..d5efb6599 100644 --- a/src/Sentry.Unity.Native/Sentry.Unity.Native.csproj +++ b/src/Sentry.Unity.Native/Sentry.Unity.Native.csproj @@ -65,4 +65,24 @@ /> + + + + + + + diff --git a/src/Sentry.Unity.Native/SentryNative.cs b/src/Sentry.Unity.Native/SentryNative.cs index c255ada10..edbea2569 100644 --- a/src/Sentry.Unity.Native/SentryNative.cs +++ b/src/Sentry.Unity.Native/SentryNative.cs @@ -140,7 +140,7 @@ private static void ReinstallBackend() } catch (EntryPointNotFoundException e) { - Logger?.LogError(e, "Native dependency not found. Did you delete sentry.dll or move files around?"); + Logger?.LogError(e, "Native dependency not found. Did you delete '{0}' or move files around?", SentryNativeLibrary.Name); } } } diff --git a/src/Sentry.Unity.Native/SentryNativeBridge.cs b/src/Sentry.Unity.Native/SentryNativeBridge.cs index a01ddb6e7..216507a8e 100644 --- a/src/Sentry.Unity.Native/SentryNativeBridge.cs +++ b/src/Sentry.Unity.Native/SentryNativeBridge.cs @@ -14,11 +14,7 @@ namespace Sentry.Unity.Native; /// internal static class SentryNativeBridge { -#if SENTRY_NATIVE_SWITCH - private const string SentryLib = "__Internal"; -#else - private const string SentryLib = "sentry"; -#endif + private const string SentryLib = SentryNativeLibrary.Name; private static IDiagnosticLogger? Logger; // This is also the logger we're forwarding native messages to. private static bool UseLibC; @@ -163,7 +159,6 @@ internal static string GetDatabasePath(SentryUnityOptions options, IApplication? internal static void AppHangPause() => sentry_app_hang_pause(); - // libsentry.so [DllImport(SentryLib)] private static extern IntPtr sentry_options_new(); diff --git a/src/Sentry.Unity.Native/SentryNativeLibrary.cs b/src/Sentry.Unity.Native/SentryNativeLibrary.cs new file mode 100644 index 000000000..a848388db --- /dev/null +++ b/src/Sentry.Unity.Native/SentryNativeLibrary.cs @@ -0,0 +1,24 @@ +namespace Sentry.Unity.Native; + +/// +/// The name this build of the assembly binds its P/Invokes to. +/// +/// +/// Desktop cannot bind to plain "sentry". Mono probes the calling assembly's own directory first, +/// and on a case-insensitive file system that resolves to the managed `Sentry.dll` sitting next to +/// us in `Managed/`. It loads, the C entry point is missing, and native support dies with an +/// `EntryPointNotFoundException`. `BuildPostProcess` copies the library into the player under the +/// renamed variant instead. Android and the consoles get theirs from elsewhere - the +/// sentry-android-ndk AAR loads `libsentry.so` by name from Java, and the console plugins ship with +/// the platform SDK - so those builds keep the original name. +/// +internal static class SentryNativeLibrary +{ +#if SENTRY_NATIVE_SWITCH + internal const string Name = "__Internal"; +#elif SENTRY_NATIVE_ANDROID || SENTRY_NATIVE_PLAYSTATION || SENTRY_NATIVE_XBOX + internal const string Name = "sentry"; +#else + internal const string Name = "sentry-native"; +#endif +} diff --git a/src/Sentry.Unity/Properties/AssemblyInfo.cs b/src/Sentry.Unity/Properties/AssemblyInfo.cs index 9e8226ca7..3f9e80ca2 100644 --- a/src/Sentry.Unity/Properties/AssemblyInfo.cs +++ b/src/Sentry.Unity/Properties/AssemblyInfo.cs @@ -4,6 +4,7 @@ [assembly: InternalsVisibleTo("Sentry.Unity.Native.PlayStation")] [assembly: InternalsVisibleTo("Sentry.Unity.Native.Switch")] [assembly: InternalsVisibleTo("Sentry.Unity.Native.Xbox")] +[assembly: InternalsVisibleTo("Sentry.Unity.Native.Android")] [assembly: InternalsVisibleTo("Sentry.Unity.Tests")] [assembly: InternalsVisibleTo("Sentry.Unity.Editor")] [assembly: InternalsVisibleTo("Sentry.Unity.Editor.Tests")] diff --git a/test/Scripts.Tests/package-release.zip.snapshot b/test/Scripts.Tests/package-release.zip.snapshot index 43200dc30..e65f35846 100644 --- a/test/Scripts.Tests/package-release.zip.snapshot +++ b/test/Scripts.Tests/package-release.zip.snapshot @@ -38,9 +38,9 @@ Plugins/Switch.meta Plugins/Windows.meta Plugins/macOS/SentryNativeBridge.m Plugins/macOS/SentryNativeBridge.m.meta -Plugins/macOS/SentryNative~/libsentry.dylib.dSYM/ +Plugins/macOS/SentryNative~/libsentry-native.dylib.dSYM/ Plugins/macOS/SentryNative~/sentry-crash.dSYM/ -Plugins/macOS/SentryNative~/libsentry.dylib +Plugins/macOS/SentryNative~/libsentry-native.dylib Plugins/macOS/SentryNative~/sentry-crash Plugins/macOS/SentryNative~/sentry-crash.dSYM/Contents/Resources/ Plugins/macOS/SentryNative~/sentry-crash.dSYM/Contents/Info.plist @@ -48,12 +48,12 @@ Plugins/macOS/SentryNative~/sentry-crash.dSYM/Contents/Resources/Relocations/ Plugins/macOS/SentryNative~/sentry-crash.dSYM/Contents/Resources/Relocations/aarch64/sentry-crash.yml Plugins/macOS/SentryNative~/sentry-crash.dSYM/Contents/Resources/Relocations/x86_64/sentry-crash.yml Plugins/macOS/SentryNative~/sentry-crash.dSYM/Contents/Resources/DWARF/sentry-crash -Plugins/macOS/SentryNative~/libsentry.dylib.dSYM/Contents/Resources/ -Plugins/macOS/SentryNative~/libsentry.dylib.dSYM/Contents/Info.plist -Plugins/macOS/SentryNative~/libsentry.dylib.dSYM/Contents/Resources/Relocations/ -Plugins/macOS/SentryNative~/libsentry.dylib.dSYM/Contents/Resources/Relocations/aarch64/libsentry.dylib.yml -Plugins/macOS/SentryNative~/libsentry.dylib.dSYM/Contents/Resources/Relocations/x86_64/libsentry.dylib.yml -Plugins/macOS/SentryNative~/libsentry.dylib.dSYM/Contents/Resources/DWARF/libsentry.dylib +Plugins/macOS/SentryNative~/libsentry-native.dylib.dSYM/Contents/Resources/ +Plugins/macOS/SentryNative~/libsentry-native.dylib.dSYM/Contents/Info.plist +Plugins/macOS/SentryNative~/libsentry-native.dylib.dSYM/Contents/Resources/Relocations/ +Plugins/macOS/SentryNative~/libsentry-native.dylib.dSYM/Contents/Resources/Relocations/aarch64/libsentry-native.dylib.yml +Plugins/macOS/SentryNative~/libsentry-native.dylib.dSYM/Contents/Resources/Relocations/x86_64/libsentry-native.dylib.yml +Plugins/macOS/SentryNative~/libsentry-native.dylib.dSYM/Contents/Resources/DWARF/libsentry-native.dylib Plugins/macOS/Sentry~/Sentry.dylib Plugins/macOS/Sentry~/Sentry.dylib.dSYM Plugins/PS5/sentry_utils.c @@ -263,11 +263,11 @@ Plugins/iOS/SentryObjC.xcframework~/ios-arm64/dSYMs/SentryObjC.framework.dSYM/Co Plugins/iOS/SentryObjC.xcframework~/ios-arm64/dSYMs/SentryObjC.framework.dSYM/Contents/Resources/Relocations/aarch64/SentryObjC.yml Plugins/iOS/SentryObjC.xcframework~/ios-arm64/dSYMs/SentryObjC.framework.dSYM/Contents/Resources/DWARF/SentryObjC Plugins/Linux/SentryNative~/libsentry.dbg.so -Plugins/Linux/SentryNative~/libsentry.so +Plugins/Linux/SentryNative~/libsentry-native.so Plugins/Linux/SentryNative~/sentry-crash Plugins/Linux/SentryNative~/sentry-crash.dbg Plugins/Linux/Sentry~/libsentry.dbg.so -Plugins/Linux/Sentry~/libsentry.so +Plugins/Linux/Sentry~/libsentry-native.so Plugins/Android/proguard-sentry-unity.pro Plugins/Android/proguard-sentry-unity.pro.meta Plugins/Android/Sentry~/sentry-android-core-release.aar @@ -280,12 +280,12 @@ Plugins/Windows/SentryNative~/sentry-crash.exe Plugins/Windows/SentryNative~/sentry-crash.pdb Plugins/Windows/SentryNative~/sentry-wer.dll Plugins/Windows/SentryNative~/sentry-wer.pdb -Plugins/Windows/SentryNative~/sentry.dll +Plugins/Windows/SentryNative~/sentry-native.dll Plugins/Windows/SentryNative~/sentry.pdb Plugins/Windows/Sentry~/crashpad_handler.exe Plugins/Windows/Sentry~/crashpad_wer.dll Plugins/Windows/Sentry~/crashpad_wer.pdb -Plugins/Windows/Sentry~/sentry.dll +Plugins/Windows/Sentry~/sentry-native.dll Plugins/Windows/Sentry~/sentry.pdb Prefabs/SentryUserFeedback.prefab Prefabs/SentryUserFeedback.prefab.meta @@ -321,6 +321,10 @@ Runtime/Sentry.Unity.MacOS.dll Runtime/Sentry.Unity.MacOS.dll.meta Runtime/Sentry.Unity.MacOS.pdb Runtime/Sentry.Unity.MacOS.pdb.meta +Runtime/Sentry.Unity.Native.Android.dll +Runtime/Sentry.Unity.Native.Android.dll.meta +Runtime/Sentry.Unity.Native.Android.pdb +Runtime/Sentry.Unity.Native.Android.pdb.meta Runtime/Sentry.Unity.Native.dll Runtime/Sentry.Unity.Native.dll.meta Runtime/Sentry.Unity.Native.pdb From 9b7980867cd7222ab69532452a6dc85f4183676c Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 11 Sep 2026 13:33:30 +0200 Subject: [PATCH 2/6] cleanup --- build/native-sdks.targets | 8 ++----- docs/agent-guides/platform-native.md | 10 ++++---- .../Sentry.Unity.Android.csproj | 4 +--- .../Native/BuildPostProcess.cs | 23 +++++++------------ .../Sentry.Unity.Native.csproj | 3 +-- .../SentryNativeLibrary.cs | 12 ++++------ 6 files changed, 21 insertions(+), 39 deletions(-) diff --git a/build/native-sdks.targets b/build/native-sdks.targets index 75aa76f75..ca6b954aa 100644 --- a/build/native-sdks.targets +++ b/build/native-sdks.targets @@ -19,12 +19,8 @@ $(SentryArtifactsDestination)Windows/Sentry~/ $(SentryArtifactsDestination)Windows/SentryNative~/ - + sentry-native.dll libsentry-native.so libsentry-native.dylib diff --git a/docs/agent-guides/platform-native.md b/docs/agent-guides/platform-native.md index 41419d206..6cfd76150 100644 --- a/docs/agent-guides/platform-native.md +++ b/docs/agent-guides/platform-native.md @@ -7,8 +7,8 @@ - `sentry_get_crashed_last_run` clears native state; SDK caches its result for the process lifetime. Do not make it repeatable. - Native backend reinstalls before first scene after Unity takes crash/signal handlers. - Native logger forwarding to C# exists only under IL2CPP. -- The desktop library is named `sentry-native`, not `sentry`, because Mono probes `Managed/` first and `sentry` resolves to the managed `Sentry.dll`. `build/native-sdks.targets` renames it while building the SDK, so the package already ships it that way. See `SentryNativeLibrary`. -- Android is the exception: its `libsentry.so` comes from the sentry-android-ndk AAR and sentry-java loads it by name, so it keeps `sentry` and gets its own `Sentry.Unity.Native.Android.dll` built from the same sources. +- Desktop library is named `sentry-native`; plain `sentry` resolves to the managed `Sentry.dll` under Mono. Renamed in `build/native-sdks.targets`, so the package already ships it that way. +- Android keeps `sentry` because sentry-java loads its AAR library by name, so it gets its own `Sentry.Unity.Native.Android.dll`. ## Backend Choices @@ -26,14 +26,14 @@ Experimental native modes raise minimum shutdown timeout to 10 seconds. - Windows: runtime files beside player `.exe`; the library lands as `sentry-native.dll`. - Linux: `libsentry-native.so` under `_Data/Plugins/x86_64`; native daemon beside executable. -- macOS: dylib in `.app/Contents/PlugIns` as `libsentry-native.dylib`; handler in `.app/Contents/MacOS`. The Cocoa backend's `Sentry.dylib` keeps its name, it is dlopened rather than P/Invoked. -- Post-build copies file names through unchanged. Stale cleanup still wipes the pre-rename names so builds over a player made by an older SDK do not leave two libraries behind. +- macOS: `libsentry-native.dylib` in `.app/Contents/PlugIns`; handler in `.app/Contents/MacOS`. Cocoa's `Sentry.dylib` keeps its name, it is dlopened not P/Invoked. +- Post-build copies names through unchanged; stale cleanup wipes pre-rename names. ## Console Plugins - PS5/Xbox libraries are user-supplied: `Assets/Plugins/Sentry/{PS5,XSX,XB1}/`. - Switch needs user-supplied static `libsentry.a` and `libzstd.a`; none uses shipped no-op stubs, partial installation is an error. -- Console assemblies, and the Android one, compile separately with platform defines from the same sources. Chained `Csc` targets in `Sentry.Unity.Native.csproj`. +- Console and Android assemblies compile separately with platform defines. Chained `Csc` targets in `Sentry.Unity.Native.csproj`. ## Tests diff --git a/src/Sentry.Unity.Android/Sentry.Unity.Android.csproj b/src/Sentry.Unity.Android/Sentry.Unity.Android.csproj index a9fff2e3f..1953ee328 100644 --- a/src/Sentry.Unity.Android/Sentry.Unity.Android.csproj +++ b/src/Sentry.Unity.Android/Sentry.Unity.Android.csproj @@ -6,9 +6,7 @@ - + $(PackageRuntimePath)/Sentry.Unity.Native.Android.dll diff --git a/src/Sentry.Unity.Editor/Native/BuildPostProcess.cs b/src/Sentry.Unity.Editor/Native/BuildPostProcess.cs index 806dd1b21..fd943e7c8 100644 --- a/src/Sentry.Unity.Editor/Native/BuildPostProcess.cs +++ b/src/Sentry.Unity.Editor/Native/BuildPostProcess.cs @@ -139,9 +139,7 @@ _ when target.IsSwitch2() => options.SwitchNativeSupportEnabled, _ => false, }; - // The names the package already ships the desktop runtime library under. `native-sdks.targets` - // renames it when the SDK is built. See `SentryNativeLibrary` in Sentry.Unity.Native for why - // binding to plain `sentry` breaks under Mono. Kept here only to clear stale artifacts. + // Only needed to clear stale artifacts; the package already ships these names. internal const string WindowsLibraryName = "sentry-native.dll"; internal const string LinuxLibraryName = "libsentry-native.so"; internal const string MacOSLibraryName = "libsentry-native.dylib"; @@ -172,9 +170,8 @@ private static IEnumerable GetNativePluginArtifact( $"Sentry Windows plugin directory not found: {windowsBackendSourcePath}\n" + $"Run 'dotnet msbuild /t:{buildTarget} src/Sentry.Unity' (or 'dotnet msbuild /t:DownloadNativeSDKs src/Sentry.Unity') to populate it."); } - // Flat copy of every non-PDB file next to the player .exe. The native library and the - // crash handler (crashpad_handler.exe / sentry-crash.exe) all sit at the build root. - // PDBs stay in the package and are consumed at symbol-upload time only. + // Windows resolves both the library and the crash handler next to the player .exe. + // PDBs stay in the package for symbol upload. foreach (var file in Directory.GetFiles(windowsBackendSourcePath)) { if (file.EndsWith(".pdb", StringComparison.OrdinalIgnoreCase)) @@ -196,8 +193,7 @@ private static IEnumerable GetNativePluginArtifact( { var name = Path.GetFileName(file); var isDylib = name.EndsWith(".dylib", StringComparison.OrdinalIgnoreCase); - // The .dylibs need to go into the `*.app/Contents/Plugins` dirctory and will be picked - // up by unity. The crash handler (sentry-native) needs to be next to the game's executable + // Unity loads dylibs from PlugIns; the crash handler has to be next to the executable. var desination = Path.Combine(contents, isDylib ? "PlugIns" : "MacOS", name); yield return new NativePluginArtifact( file, @@ -217,10 +213,8 @@ private static IEnumerable GetNativePluginArtifact( $"Sentry Linux plugin directory not found: {linuxBackendSourcePath}\n" + $"Run 'dotnet msbuild /t:{buildTarget} src/Sentry.Unity' (or 'dotnet msbuild /t:DownloadNativeSDKs src/Sentry.Unity') to populate it."); } - // The native library must sit in the player's native plugin dir (_Data/Plugins/x86_64) - // where the Linux player resolves the P/Invoke. The crash daemon (sentry-crash, native backend only) - // sits next to the player executable so sentry-native can spawn it on crash. - // The .dbg.so / .dbg debug sidecars stay in the package and are consumed at symbol-upload time only. + // The Linux player resolves the P/Invoke from the plugin dir, and sentry-native spawns + // the crash daemon from next to the executable. Debug sidecars stay in the package. var linuxPluginDir = GetLinuxPluginDir(buildOutputDir); foreach (var file in Directory.GetFiles(linuxBackendSourcePath)) { @@ -259,9 +253,8 @@ private static IEnumerable GetNativePluginArtifact( } } - // Wipe both backends' leftovers before copying the current one in, so an iterative build does - // not leave two libraries sitting in PlugIns. `libsentry.dylib` is the pre-rename name and only - // turns up when building over a player made by an older SDK. + // A prior build with the other backend, or an older SDK's pre-rename library, would otherwise + // leave a second library behind in PlugIns. private static void CleanupStaleMacOSArtifacts(IDiagnosticLogger logger, string executablePath) { var contents = Path.Combine(executablePath, "Contents"); diff --git a/src/Sentry.Unity.Native/Sentry.Unity.Native.csproj b/src/Sentry.Unity.Native/Sentry.Unity.Native.csproj index d5efb6599..de28b4ec0 100644 --- a/src/Sentry.Unity.Native/Sentry.Unity.Native.csproj +++ b/src/Sentry.Unity.Native/Sentry.Unity.Native.csproj @@ -65,8 +65,7 @@ /> - + diff --git a/src/Sentry.Unity.Native/SentryNativeLibrary.cs b/src/Sentry.Unity.Native/SentryNativeLibrary.cs index a848388db..2d37b61a1 100644 --- a/src/Sentry.Unity.Native/SentryNativeLibrary.cs +++ b/src/Sentry.Unity.Native/SentryNativeLibrary.cs @@ -1,16 +1,12 @@ namespace Sentry.Unity.Native; /// -/// The name this build of the assembly binds its P/Invokes to. +/// The library name this build binds its P/Invokes to. /// /// -/// Desktop cannot bind to plain "sentry". Mono probes the calling assembly's own directory first, -/// and on a case-insensitive file system that resolves to the managed `Sentry.dll` sitting next to -/// us in `Managed/`. It loads, the C entry point is missing, and native support dies with an -/// `EntryPointNotFoundException`. `BuildPostProcess` copies the library into the player under the -/// renamed variant instead. Android and the consoles get theirs from elsewhere - the -/// sentry-android-ndk AAR loads `libsentry.so` by name from Java, and the console plugins ship with -/// the platform SDK - so those builds keep the original name. +/// Desktop cannot use "sentry": Mono probes the calling assembly's own directory first, where it +/// resolves to the managed `Sentry.dll` on a case-insensitive file system. Android and the consoles +/// get their library from elsewhere and keep the original name. /// internal static class SentryNativeLibrary { From 8e13975c37f4b00dc4b363f223c32e56f50f519c Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 11 Sep 2026 15:19:26 +0200 Subject: [PATCH 3/6] updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 266b20bee..b76d49803 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixes +- When targeting Windows using the `Mono` scripting backend the SDK now correctly loads `sentry-native` to capture native crashes. ([#2842](https://github.com/getsentry/sentry-unity/pull/2842)) - Fixed a `NoSuchFieldError` during initialization on Android when setting the `sample rate`. ([#2838](https://github.com/getsentry/sentry-unity/issues/2838)) ### Dependencies From e9a220b2ddb3b845052718045b74085b608ce5c1 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 11 Sep 2026 17:30:19 +0200 Subject: [PATCH 4/6] fix(android): use serializedVersion 2 plugin meta so 2021.3 includes the assembly --- .../Sentry.Unity.Native.Android.dll.meta | 73 ++++++++++++------- 1 file changed, 46 insertions(+), 27 deletions(-) diff --git a/package-dev/Runtime/Sentry.Unity.Native.Android.dll.meta b/package-dev/Runtime/Sentry.Unity.Native.Android.dll.meta index ccb7ad785..89862f8fb 100644 --- a/package-dev/Runtime/Sentry.Unity.Native.Android.dll.meta +++ b/package-dev/Runtime/Sentry.Unity.Native.Android.dll.meta @@ -2,7 +2,7 @@ fileFormatVersion: 2 guid: c5bff87923964bd3827bb7a2c0d7f4e3 PluginImporter: externalObjects: {} - serializedVersion: 3 + serializedVersion: 2 iconMap: {} executionOrder: {} defineConstraints: [] @@ -11,68 +11,87 @@ PluginImporter: isExplicitlyReferenced: 0 validateReferences: 1 platformData: - Android: - enabled: 1 - settings: - AndroidLibraryDependee: UnityLibrary - AndroidSharedLibraryType: Executable - CPU: ARMv7 - Any: + - first: + : Any + second: enabled: 0 settings: Exclude Android: 0 Exclude Editor: 1 Exclude Linux64: 1 + Exclude Lumin: 1 Exclude OSXUniversal: 1 - Exclude PS5: 1 - Exclude Switch: 1 - Exclude Switch2: 1 Exclude WebGL: 1 Exclude Win: 1 Exclude Win64: 1 Exclude iOS: 1 - Editor: + Exclude tvOS: 1 + - first: + Android: Android + second: + enabled: 1 + settings: + CPU: ARMv7 + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: enabled: 0 settings: CPU: AnyCPU DefaultValueInitialized: true OS: AnyOS - Linux64: + - first: + Standalone: Linux64 + second: enabled: 0 settings: CPU: None - OSXUniversal: + - first: + Standalone: OSXUniversal + second: enabled: 0 settings: CPU: None - PS5: - enabled: 0 - settings: {} - Switch: - enabled: 0 - settings: {} - Switch2: - enabled: 0 - settings: {} - Win: + - first: + Standalone: Win + second: enabled: 0 settings: CPU: None - Win64: + - first: + Standalone: Win64 + second: enabled: 0 settings: CPU: None - WindowsStoreApps: + - first: + Windows Store Apps: WindowsStoreApps + second: enabled: 0 settings: CPU: AnyCPU - iOS: + - first: + iPhone: iOS + second: enabled: 0 settings: AddToEmbeddedBinaries: false CPU: AnyCPU CompileFlags: FrameworkDependencies: + - first: + tvOS: tvOS + second: + enabled: 0 + settings: + CPU: AnyCPU + CompileFlags: + FrameworkDependencies: userData: assetBundleName: assetBundleVariant: From 0766d42a5b730ca96c03aabd6dc6ee034ec17374 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 11 Sep 2026 21:15:17 +0200 Subject: [PATCH 5/6] run ci --- .github/workflows/ci.yml | 31 ++++ .github/workflows/test-build-windows-mono.yml | 137 ++++++++++++++++++ .github/workflows/test-run-desktop.yml | 13 +- test/IntegrationTest/Integration.Tests.ps1 | 5 + .../Editor/Builder.cs | 23 ++- test/Scripts.Integration.Test/globals.ps1 | 1 + 6 files changed, 201 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/test-build-windows-mono.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 94ca1885c..87098f321 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -427,6 +427,23 @@ jobs: with: unity-version: ${{ matrix.unity-version }} + test-build-windows-mono: + # The Mono scripting backend resolves P/Invokes differently than IL2CPP, so it needs its own + # player. One version, default (Crashpad) backend only: the IL2CPP matrix already covers the + # version spread and both backends. The version is the first matrix entry rather than a + # literal, because `test-create` only builds what the matrix holds and Unity-bump PRs narrow + # it to the bumped version. The lists in create-unity-matrix.yml are oldest-first. + name: Build Windows Mono ${{ fromJSON(needs.create-unity-matrix.outputs.unity-matrix).unity-version[0] }} Integration Test + if: ${{ !startsWith(github.ref, 'refs/heads/release/') }} + needs: [test-create, create-unity-matrix] + secrets: + UNITY_LICENSE_SERVER_CONFIG: ${{ secrets.UNITY_LICENSE_SERVER_CONFIG }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_TEST_DSN: ${{ secrets.SENTRY_TEST_DSN }} + uses: ./.github/workflows/test-build-windows-mono.yml + with: + unity-version: ${{ fromJSON(needs.create-unity-matrix.outputs.unity-matrix).unity-version[0] }} + test-build-macos: name: Build macOS ${{ matrix.unity-version }} Integration Test if: ${{ !startsWith(github.ref, 'refs/heads/release/') }} @@ -479,6 +496,20 @@ jobs: platform: windows backend: ${{ matrix.backend }} + test-run-windows-mono: + name: Run Windows Mono ${{ fromJSON(needs.create-unity-matrix.outputs.unity-matrix).unity-version[0] }} Integration Test + if: ${{ !startsWith(github.ref, 'refs/heads/release/') }} + needs: [test-build-windows-mono, create-unity-matrix] + secrets: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_TEST_DSN: ${{ secrets.SENTRY_TEST_DSN }} + uses: ./.github/workflows/test-run-desktop.yml + with: + unity-version: ${{ fromJSON(needs.create-unity-matrix.outputs.unity-matrix).unity-version[0] }} + platform: windows + scripting: mono + backend: crashpad + test-run-macos: name: Run macOS ${{ matrix.backend }} ${{ matrix.unity-version }} Integration Test if: ${{ !startsWith(github.ref, 'refs/heads/release/') }} diff --git a/.github/workflows/test-build-windows-mono.yml b/.github/workflows/test-build-windows-mono.yml new file mode 100644 index 000000000..66b7e29ee --- /dev/null +++ b/.github/workflows/test-build-windows-mono.yml @@ -0,0 +1,137 @@ +name: "Test: Build Windows Mono" +on: + workflow_call: + inputs: + unity-version: + required: true + type: string + secrets: + UNITY_LICENSE_SERVER_CONFIG: + required: true + SENTRY_AUTH_TOKEN: + required: true + SENTRY_TEST_DSN: + required: true + +defaults: + run: + shell: pwsh + +jobs: + build: + name: Windows Mono ${{ inputs.unity-version }} + runs-on: windows-latest + env: + UNITY_VERSION: ${{ inputs.unity-version }} + BUILD_PLATFORM: Windows-Mono + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Load env + id: env + run: | + $u = (Get-Content scripts/unity-versions.json -Raw | ConvertFrom-Json).'${{ env.UNITY_VERSION }}' + "unityVersion=$($u.version)" >> $env:GITHUB_OUTPUT + "unityChangeset=$($u.changeset)" >> $env:GITHUB_OUTPUT + + - name: Setup Unity + uses: getsentry/setup-unity@61c0c0944851685b6c1225e940d1b1ab349e3aa3 + with: + unity-version: ${{ steps.env.outputs.unityVersion }} + unity-version-changeset: ${{ steps.env.outputs.unityChangeset }} + unity-modules: windows-il2cpp + + - name: Create Unity license config + run: | + New-Item -Path "C:/ProgramData/Unity/config/" -ItemType Directory -Force + Set-Content -Path "C:/ProgramData/Unity/config/services-config.json" -Value "$env:UNITY_LICENSE_SERVER_CONFIG" + env: + UNITY_LICENSE_SERVER_CONFIG: ${{ secrets.UNITY_LICENSE_SERVER_CONFIG }} + + - name: Download IntegrationTest project + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: test-${{ env.UNITY_VERSION }} + + - name: Extract project archive + run: tar -xvzf test-project.tar.gz + + - name: Restore Unity Library cache + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: samples/IntegrationTest/Library + key: it-library-windows-mono-${{ env.UNITY_VERSION }}-${{ github.run_id }} + restore-keys: | + it-library-windows-mono-${{ env.UNITY_VERSION }}- + + - name: Download UPM package + uses: ./.github/actions/wait-for-artifact + with: + name: package-release + + - name: Extract UPM package + run: ./test/Scripts.Integration.Test/extract-package.ps1 + + - name: Add Sentry to the project + run: ./test/Scripts.Integration.Test/add-sentry.ps1 -UnityPath "$env:UNITY_PATH" -PackagePath "test-package-release" + + - name: Download DependencyConflict package + uses: ./.github/actions/wait-for-artifact + with: + name: dependency-conflict-package + path: dependency-conflict-package + + - name: Add DependencyConflict to the project + run: ./test/Scripts.Integration.Test/add-dependency-conflict.ps1 -PackagePath "dependency-conflict-package" + + - name: Configure Sentry + run: ./test/Scripts.Integration.Test/configure-sentry.ps1 -UnityPath "$env:UNITY_PATH" -Platform Windows + env: + SENTRY_DSN: ${{ secrets.SENTRY_TEST_DSN }} + + # Crashpad backend only. The point of this job is that a Mono player resolves the + # P/Invokes to `sentry-native` instead of the managed `Sentry.dll` at all; both Windows + # backends bind against the same library, and the IL2CPP matrix already covers the + # backend spread. Crashpad is what players ship with by default. + - name: Build with Sentry SDK + run: ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform Windows-Mono -UnityVersion "$env:UNITY_VERSION" + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + + - name: Assert symbols and sources were uploaded + run: ./test/Scripts.Integration.Test/assert-symbol-upload.ps1 -LogPath unity.log + + # We create tar explicitly because upload-artifact is slow for many files. + - name: Create archive + run: | + Remove-Item -Recurse -Force samples/IntegrationTest/Build/*_BackUpThisFolder_ButDontShipItWithYourGame -ErrorAction SilentlyContinue + Copy-Item unity.log samples/IntegrationTest/Build/ -ErrorAction SilentlyContinue + tar -cvzf test-app-desktop.tar.gz samples/IntegrationTest/Build + + - name: Upload test app + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: testapp-desktop-compiled-${{ env.UNITY_VERSION }}-windows-mono-crashpad + if-no-files-found: error + path: test-app-desktop.tar.gz + retention-days: 14 + + - name: Save Unity Library cache + if: github.ref == 'refs/heads/main' + uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: samples/IntegrationTest/Library + key: it-library-windows-mono-${{ env.UNITY_VERSION }}-${{ github.run_id }} + + - name: Upload IntegrationTest project on failure + if: ${{ failure() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: failed-project-desktop-windows-mono-${{ env.UNITY_VERSION }} + path: | + samples/IntegrationTest + unity.log + !samples/IntegrationTest/Build/*_BackUpThisFolder_ButDontShipItWithYourGame + retention-days: 14 diff --git a/.github/workflows/test-run-desktop.yml b/.github/workflows/test-run-desktop.yml index 979ee9a45..381d52da3 100644 --- a/.github/workflows/test-run-desktop.yml +++ b/.github/workflows/test-run-desktop.yml @@ -14,6 +14,11 @@ on: type: string default: "" description: "macOS: native or cocoa. Windows: native or crashpad. Linux: native or breakpad." + scripting: + required: false + type: string + default: "" + description: "Only set for non-default scripting backends, e.g. mono for the Windows Mono player." secrets: SENTRY_AUTH_TOKEN: required: true @@ -26,7 +31,7 @@ defaults: jobs: run: - name: ${{ inputs.platform }}${{ inputs.backend && format(' ({0})', inputs.backend) || '' }} ${{ inputs.unity-version }} + name: ${{ inputs.platform }}${{ inputs.scripting && format(' {0}', inputs.scripting) || '' }}${{ inputs.backend && format(' ({0})', inputs.backend) || '' }} ${{ inputs.unity-version }} runs-on: ${{ inputs.platform == 'linux' && 'ubuntu-latest' || inputs.platform == 'macos' && 'macos-latest' || 'windows-latest' }} env: SENTRY_DSN: ${{ secrets.SENTRY_TEST_DSN }} @@ -43,7 +48,7 @@ jobs: - name: Download test app artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: testapp-desktop-compiled-${{ inputs.unity-version }}-${{ inputs.platform }}${{ inputs.backend && format('-{0}', inputs.backend) || '' }} + name: testapp-desktop-compiled-${{ inputs.unity-version }}-${{ inputs.platform }}${{ inputs.scripting && format('-{0}', inputs.scripting) || '' }}${{ inputs.backend && format('-{0}', inputs.backend) || '' }} - name: Extract test app run: tar -xvzf test-app-desktop.tar.gz @@ -81,6 +86,8 @@ jobs: - name: Run Integration Tests (Windows) if: inputs.platform == 'windows' timeout-minutes: 20 + env: + SENTRY_TEST_SCRIPTING_BACKEND: ${{ inputs.scripting }} run: | $env:SENTRY_TEST_PLATFORM = "Desktop" $env:SENTRY_TEST_APP = "samples/IntegrationTest/Build/test.exe" @@ -90,7 +97,7 @@ jobs: if: ${{ failure() }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: testapp-desktop-logs-${{ inputs.platform }}${{ inputs.backend && format('-{0}', inputs.backend) || '' }}-${{ inputs.unity-version }} + name: testapp-desktop-logs-${{ inputs.platform }}${{ inputs.scripting && format('-{0}', inputs.scripting) || '' }}${{ inputs.backend && format('-{0}', inputs.backend) || '' }}-${{ inputs.unity-version }} path: | test/IntegrationTest/results/ retention-days: 14 diff --git a/test/IntegrationTest/Integration.Tests.ps1 b/test/IntegrationTest/Integration.Tests.ps1 index 5a9b6e7fd..33e6315b0 100644 --- a/test/IntegrationTest/Integration.Tests.ps1 +++ b/test/IntegrationTest/Integration.Tests.ps1 @@ -9,6 +9,7 @@ # # SENTRY_TEST_APP: path to the test app (APK, executable, .app bundle, WebGL build directory, # or Xbox packaged build directory containing a .xvc) +# SENTRY_TEST_SCRIPTING_BACKEND: set to "mono" for a Mono player; empty means IL2CPP # # Platform-specific environment variables: # iOS: SENTRY_IOS_VERSION - iOS simulator version (e.g. "17.0" or "latest") @@ -442,6 +443,10 @@ Describe "Unity $($env:SENTRY_TEST_PLATFORM) Integration Tests" { Set-ItResult -Skipped -Because "Source-line assertions are unsupported on $script:Platform" return } + if ($env:SENTRY_TEST_SCRIPTING_BACKEND -eq "mono") { + Set-ItResult -Skipped -Because "line numbers come from the IL2CPP mappings, which a Mono player does not produce" + return + } $frame = $runEvent.exception.values[0].stacktrace.frames | Where-Object { $_.module -eq "IntegrationTester" -and $_.function -eq "ThrowException" } | diff --git a/test/Scripts.Integration.Test/Editor/Builder.cs b/test/Scripts.Integration.Test/Editor/Builder.cs index 068ee1d07..f4a967045 100644 --- a/test/Scripts.Integration.Test/Editor/Builder.cs +++ b/test/Scripts.Integration.Test/Editor/Builder.cs @@ -9,7 +9,7 @@ public class Builder { public static void BuildIl2CPPPlayer(BuildTarget target, BuildTargetGroup group, BuildOptions buildOptions, - string defaultBuildPath = "./Builds/") + string defaultBuildPath = "./Builds/", ScriptingImplementation scripting = ScriptingImplementation.IL2CPP) { Debug.Log("Builder: Starting to build"); @@ -23,19 +23,22 @@ public static void BuildIl2CPPPlayer(BuildTarget target, BuildTargetGroup group, EditorUserBuildSettings.selectedBuildTargetGroup = group; EditorUserBuildSettings.development = false; EditorUserBuildSettings.allowDebugging = false; - PlayerSettings.SetScriptingBackend(NamedBuildTarget.FromBuildTargetGroup(group), ScriptingImplementation.IL2CPP); + PlayerSettings.SetScriptingBackend(NamedBuildTarget.FromBuildTargetGroup(group), scripting); // Making sure that the app keeps on running in the background. Linux CI is very unhappy with coroutines otherwise. PlayerSettings.runInBackground = true; DisableUnityAudio(); DisableProgressiveLightMapper(); - Debug.Log("Builder: Setting IL2CPP generation to OptimizeSpeed"); + if (scripting == ScriptingImplementation.IL2CPP) + { + Debug.Log("Builder: Setting IL2CPP generation to OptimizeSpeed"); #if UNITY_2022_1_OR_NEWER - PlayerSettings.SetIl2CppCodeGeneration(NamedBuildTarget.FromBuildTargetGroup(group), UnityEditor.Build.Il2CppCodeGeneration.OptimizeSpeed); + PlayerSettings.SetIl2CppCodeGeneration(NamedBuildTarget.FromBuildTargetGroup(group), UnityEditor.Build.Il2CppCodeGeneration.OptimizeSpeed); #elif UNITY_2021_2_OR_NEWER - EditorUserBuildSettings.il2CppCodeGeneration = UnityEditor.Build.Il2CppCodeGeneration.OptimizeSpeed; + EditorUserBuildSettings.il2CppCodeGeneration = UnityEditor.Build.Il2CppCodeGeneration.OptimizeSpeed; #endif + } Debug.Log("Builder: Configuring code stripping level"); #if UNITY_6000_0_OR_NEWER @@ -55,7 +58,7 @@ public static void BuildIl2CPPPlayer(BuildTarget target, BuildTargetGroup group, Debug.Log("Builder: Disabling optimizations to reduce build time"); // TODO Linux fails with `free(): invalid pointer` in the test, after everything seems to have shut down. - if (target != BuildTarget.StandaloneLinux64) + if (scripting == ScriptingImplementation.IL2CPP && target != BuildTarget.StandaloneLinux64) { PlayerSettings.SetIl2CppCompilerConfiguration(NamedBuildTarget.FromBuildTargetGroup(group), Il2CppCompilerConfiguration.Debug); } @@ -123,6 +126,14 @@ public static void BuildWindowsIl2CPPPlayer() defaultBuildPath: "./Builds/Windows/test.exe"); } + [MenuItem("Tools/Builder/Windows Mono")] + public static void BuildWindowsMonoPlayer() + { + Debug.Log("Builder: Building Windows Mono Player"); + BuildIl2CPPPlayer(BuildTarget.StandaloneWindows64, BuildTargetGroup.Standalone, BuildOptions.StrictMode, + defaultBuildPath: "./Builds/Windows/test.exe", scripting: ScriptingImplementation.Mono2x); + } + [MenuItem("Tools/Builder/macOS")] public static void BuildMacIl2CPPPlayer() { diff --git a/test/Scripts.Integration.Test/globals.ps1 b/test/Scripts.Integration.Test/globals.ps1 index 2c1d033f6..805004bff 100644 --- a/test/Scripts.Integration.Test/globals.ps1 +++ b/test/Scripts.Integration.Test/globals.ps1 @@ -148,6 +148,7 @@ function BuildMethodFor([string] $platform) "Android-Export" { return "Builder.BuildAndroidIl2CPPProject" } "MacOS" { return "Builder.BuildMacIl2CPPPlayer" } "Windows" { return "Builder.BuildWindowsIl2CPPPlayer" } + "Windows-Mono" { return "Builder.BuildWindowsMonoPlayer" } "Linux" { return "Builder.BuildLinuxIl2CPPPlayer" } "WebGL" { return "Builder.BuildWebGLPlayer" } "iOS" { return "Builder.BuildIOSProject" } From 71f2f6ece393ab3c9157c296b5c446dc2a306156 Mon Sep 17 00:00:00 2001 From: Stefan Jandl Date: Mon, 14 Sep 2026 11:13:22 +0200 Subject: [PATCH 6/6] chore: pin platform target for all plugins (#2848) --- .github/workflows/ci.yml | 4 + .gitignore | 4 + AGENTS.md | 2 +- CHANGELOG.md | 2 + docs/agent-guides/platform-native.md | 5 +- package-dev/Plugins/PS5/sentry_utils.c.meta | 10 +- .../{ => SentryStub~}/sentry_native_stubs.c | 33 +- .../Plugins/iOS/SentryNativeBridge.m.meta | 2 +- .../Plugins/iOS/SentryNativeBridgeNoOp.m.meta | 2 +- .../Plugins/macOS/SentryNativeBridge.m.meta | 2 +- package-dev/Runtime/Sentry.Unity.iOS.dll.meta | 12 +- .../Runtime/Sentry.Unity.iOS.dll.meta | 73 ++-- .../SwitchNativePluginBuildPreProcess.cs | 92 +---- .../Native/SwitchNativeStub.cs | 230 ++++++++++++ .../package-release.zip.snapshot | 4 +- test/Scripts.Tests/test-plugin-platforms.ps1 | 333 ++++++++++++++++++ .../Native/SwitchNativeStubTests.cs | 40 ++- 17 files changed, 701 insertions(+), 149 deletions(-) rename package-dev/Plugins/Switch/{ => SentryStub~}/sentry_native_stubs.c (89%) rename package-dev/Plugins/Switch/sentry_native_stubs.c.meta => package/Runtime/Sentry.Unity.iOS.dll.meta (59%) create mode 100644 src/Sentry.Unity.Editor/Native/SwitchNativeStub.cs create mode 100644 test/Scripts.Tests/test-plugin-platforms.ps1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 87098f321..6abddf2c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,6 +88,10 @@ jobs: with: name: package-release + - name: Check plugin platform scopes + shell: pwsh + run: ./test/Scripts.Tests/test-plugin-platforms.ps1 + - name: Check snapshot id: snapshot-check shell: pwsh diff --git a/.gitignore b/.gitignore index 8ba14ec97..89dcca713 100644 --- a/.gitignore +++ b/.gitignore @@ -63,6 +63,10 @@ package-dev/Plugins/Windows/SentryNative~/* package-dev/Plugins/Linux/Sentry~/* package-dev/Plugins/Linux/SentryNative~/* +# macOS SDK files +package-dev/Plugins/macOS/Sentry~/* +package-dev/Plugins/macOS/SentryNative~/* + # CLI package-dev/Editor/sentry-cli diff --git a/AGENTS.md b/AGENTS.md index 8a9726921..37984a84d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -50,4 +50,4 @@ Read only guide relevant to task. Do not import all guides at startup. ## Commits - Use direct, capitalized commit subjects without conventional-commit prefixes. -- Include the committing agent's own `Co-Authored-By` attribution when a commit is requested. +- Do not add agent attribution trailers such as `Co-Authored-By`. diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b9a57273..01aa1d6d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ - When targeting Windows using the `Mono` scripting backend the SDK now correctly loads `sentry-native` to capture native crashes. ([#2842](https://github.com/getsentry/sentry-unity/pull/2842)) - Fixed a `NoSuchFieldError` during initialization on Android when setting the `sample rate`. ([#2838](https://github.com/getsentry/sentry-unity/issues/2838)) +- Individual assemblies and plugins are now scoped to the platforms that use them. ([#2848](https://github.com/getsentry/sentry-unity/pull/2848)) +- When targeting Nintendo Switch or Switch 2 the SDK no longer fails to initialize the native layer at runtime. The respective stubs are now written to `Assets/Plugins/Sentry` so the SDK can enable and disable them before the build starts. ([#2849](https://github.com/getsentry/sentry-unity/pull/2849)) ### Dependencies diff --git a/docs/agent-guides/platform-native.md b/docs/agent-guides/platform-native.md index 6cfd76150..a51899c62 100644 --- a/docs/agent-guides/platform-native.md +++ b/docs/agent-guides/platform-native.md @@ -32,7 +32,10 @@ Experimental native modes raise minimum shutdown timeout to 10 seconds. ## Console Plugins - PS5/Xbox libraries are user-supplied: `Assets/Plugins/Sentry/{PS5,XSX,XB1}/`. -- Switch needs user-supplied static `libsentry.a` and `libzstd.a`; none uses shipped no-op stubs, partial installation is an error. +- Switch needs user-supplied static `libsentry.a` and `libzstd.a` per target in `Assets/Plugins/Sentry/{Switch,Switch2}`. It is the only platform that binds `__Internal`, so a missing library is a link error, which is why it alone ships stubs. +- `SwitchNativeStub` copies `Plugins/Switch/SentryStub~/sentry_native_stubs.c` into the user's plugin directory while the libraries are missing and deletes it once they arrive. Copying is limited to the active build target; deletion is not, because a stale stub shadows the libraries beside it. +- The template lives behind a `~` so the AssetDatabase never imports it. Importer settings cannot be written to an immutable package, so it must not become a package asset. +- A copy whose content differs from the template, line endings normalized away, is rewritten from it. That is what refreshes already-copied stubs on upgrade, so editing the template is all a stub change takes. `SwitchNativeStubTests.Stub_ContainsEverySwitchNativeBinding` keeps the template covering every `__Internal` entry point. - Console and Android assemblies compile separately with platform defines. Chained `Csc` targets in `Sentry.Unity.Native.csproj`. ## Tests diff --git a/package-dev/Plugins/PS5/sentry_utils.c.meta b/package-dev/Plugins/PS5/sentry_utils.c.meta index 36d3e4a3b..3e19daff0 100644 --- a/package-dev/Plugins/PS5/sentry_utils.c.meta +++ b/package-dev/Plugins/PS5/sentry_utils.c.meta @@ -18,12 +18,12 @@ PluginImporter: settings: Exclude Android: 1 Exclude Editor: 1 - Exclude Linux64: 0 + Exclude Linux64: 1 Exclude OSXUniversal: 1 Exclude PS5: 0 Exclude WebGL: 1 - Exclude Win: 0 - Exclude Win64: 0 + Exclude Win: 1 + Exclude Win64: 1 Exclude iOS: 1 Exclude tvOS: 1 - first: @@ -95,13 +95,13 @@ PluginImporter: - first: GameCoreScarlett: GameCoreScarlett second: - enabled: 1 + enabled: 0 settings: CPU: AnyCPU - first: GameCoreXboxOne: GameCoreXboxOne second: - enabled: 1 + enabled: 0 settings: CPU: AnyCPU - first: diff --git a/package-dev/Plugins/Switch/sentry_native_stubs.c b/package-dev/Plugins/Switch/SentryStub~/sentry_native_stubs.c similarity index 89% rename from package-dev/Plugins/Switch/sentry_native_stubs.c rename to package-dev/Plugins/Switch/SentryStub~/sentry_native_stubs.c index 57ffbf7d8..2d4554ebb 100644 --- a/package-dev/Plugins/Switch/sentry_native_stubs.c +++ b/package-dev/Plugins/Switch/SentryStub~/sentry_native_stubs.c @@ -1,21 +1,14 @@ /* * Sentry Switch Stubs * - * No-op stub implementations for sentry-native and Switch helper functions. - * These stubs are used when the user has not provided the actual sentry-switch - * native library, allowing the SDK to compile and run without native crash support. + * No-op stubs for the sentry-switch bindings. sentry-switch is distributed under NDA, so without + * them the player fails to link for anyone who does not have it. * - * When the real sentry-switch library is provided by the user at: - * Assets/Plugins/Sentry/Switch/libsentry.a - * Assets/Plugins/Sentry/Switch/SentrySwitchHelpers.cpp + * `SwitchNativeStub` copies this file into the user's project while the real libraries are missing. + * Edit the original here; a copy that differs from it is rewritten from this file. * - * This stub file will be automatically disabled by the build preprocessor, - * and the real library will be linked instead. - * - * All functions here are no-ops that return safe default values. - * The SDK will appear to initialize successfully, but native features - * (crash reporting, native scope sync) will silently do nothing. - * Managed Sentry features continue to work normally. + * `sentry_init` returns failure so `SentryNativeSwitch` reports native support as unavailable + * instead of silently reporting nothing. Managed Sentry features are unaffected. */ #include @@ -49,9 +42,10 @@ int sentry_init(void* options) return -1; } -void sentry_close(void) +int sentry_close(void) { - /* No-op */ + /* Success, matching sentry-native's `int sentry_close(void)` */ + return 0; } /* @@ -251,9 +245,11 @@ sentry_value_t sentry_value_get_by_key(sentry_value_t value, const char* key) return SENTRY_VALUE_NULL; } -void sentry_value_decref(sentry_value_t value) +int sentry_value_decref(sentry_value_t value) { + /* Refcount reached zero, matching sentry-native's `int sentry_value_decref(sentry_value_t)` */ (void)value; + return 0; } /* @@ -352,9 +348,10 @@ int sentry_clear_crashed_last_run(void) return 0; } -void sentry_reinstall_backend(void) +int sentry_reinstall_backend(void) { - /* No-op */ + /* Success, matching sentry-native's `int sentry_reinstall_backend(void)` */ + return 0; } void sentry_app_hang_heartbeat(void) diff --git a/package-dev/Plugins/iOS/SentryNativeBridge.m.meta b/package-dev/Plugins/iOS/SentryNativeBridge.m.meta index 42ac109e5..db6a0427f 100644 --- a/package-dev/Plugins/iOS/SentryNativeBridge.m.meta +++ b/package-dev/Plugins/iOS/SentryNativeBridge.m.meta @@ -79,7 +79,7 @@ PluginImporter: - first: tvOS: tvOS second: - enabled: 1 + enabled: 0 settings: {} userData: assetBundleName: diff --git a/package-dev/Plugins/iOS/SentryNativeBridgeNoOp.m.meta b/package-dev/Plugins/iOS/SentryNativeBridgeNoOp.m.meta index 11c07bd14..33a3bd416 100644 --- a/package-dev/Plugins/iOS/SentryNativeBridgeNoOp.m.meta +++ b/package-dev/Plugins/iOS/SentryNativeBridgeNoOp.m.meta @@ -71,7 +71,7 @@ PluginImporter: - first: tvOS: tvOS second: - enabled: 1 + enabled: 0 settings: {} userData: assetBundleName: diff --git a/package-dev/Plugins/macOS/SentryNativeBridge.m.meta b/package-dev/Plugins/macOS/SentryNativeBridge.m.meta index 43069dbda..197c1277c 100644 --- a/package-dev/Plugins/macOS/SentryNativeBridge.m.meta +++ b/package-dev/Plugins/macOS/SentryNativeBridge.m.meta @@ -79,7 +79,7 @@ PluginImporter: - first: tvOS: tvOS second: - enabled: 1 + enabled: 0 settings: {} userData: assetBundleName: diff --git a/package-dev/Runtime/Sentry.Unity.iOS.dll.meta b/package-dev/Runtime/Sentry.Unity.iOS.dll.meta index a279b2b75..647eb36e8 100644 --- a/package-dev/Runtime/Sentry.Unity.iOS.dll.meta +++ b/package-dev/Runtime/Sentry.Unity.iOS.dll.meta @@ -18,11 +18,11 @@ PluginImporter: settings: Exclude Android: 1 Exclude Editor: 0 - Exclude Linux64: 0 + Exclude Linux64: 1 Exclude OSXUniversal: 0 Exclude WebGL: 1 - Exclude Win: 0 - Exclude Win64: 0 + Exclude Win: 1 + Exclude Win64: 1 Exclude iOS: 0 - first: Android: Android @@ -46,7 +46,7 @@ PluginImporter: - first: Standalone: Linux64 second: - enabled: 1 + enabled: 0 settings: CPU: None - first: @@ -58,13 +58,13 @@ PluginImporter: - first: Standalone: Win second: - enabled: 1 + enabled: 0 settings: CPU: None - first: Standalone: Win64 second: - enabled: 1 + enabled: 0 settings: CPU: None - first: diff --git a/package-dev/Plugins/Switch/sentry_native_stubs.c.meta b/package/Runtime/Sentry.Unity.iOS.dll.meta similarity index 59% rename from package-dev/Plugins/Switch/sentry_native_stubs.c.meta rename to package/Runtime/Sentry.Unity.iOS.dll.meta index c72bd14c2..a14c1e023 100644 --- a/package-dev/Plugins/Switch/sentry_native_stubs.c.meta +++ b/package/Runtime/Sentry.Unity.iOS.dll.meta @@ -1,8 +1,8 @@ fileFormatVersion: 2 -guid: 3e53c83de0e67254dbada2456cd849ba +guid: 7b1845f97d56c487bb20875e78b23bb7 PluginImporter: externalObjects: {} - serializedVersion: 3 + serializedVersion: 2 iconMap: {} executionOrder: {} defineConstraints: [] @@ -11,55 +11,72 @@ PluginImporter: isExplicitlyReferenced: 0 validateReferences: 1 platformData: - Android: - enabled: 0 - settings: - AndroidLibraryDependee: UnityLibrary - AndroidSharedLibraryType: Executable - CPU: ARMv7 - Any: + - first: + : Any + second: enabled: 0 settings: Exclude Android: 1 Exclude Editor: 1 Exclude Linux64: 1 - Exclude OSXUniversal: 1 - Exclude Switch: 0 - Exclude Switch2: 0 + Exclude OSXUniversal: 0 Exclude WebGL: 1 Exclude Win: 1 Exclude Win64: 1 - Exclude iOS: 1 - Editor: + Exclude iOS: 0 + - first: + Android: Android + second: + enabled: 0 + settings: + CPU: ARMv7 + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: enabled: 0 settings: CPU: AnyCPU DefaultValueInitialized: true OS: AnyOS - Linux64: + - first: + Standalone: Linux64 + second: enabled: 0 settings: CPU: None - OSXUniversal: - enabled: 0 - settings: - CPU: None - Switch: - enabled: 1 - settings: {} - Switch2: + - first: + Standalone: OSXUniversal + second: enabled: 1 - settings: {} - Win: + settings: + CPU: AnyCPU + - first: + Standalone: Win + second: enabled: 0 settings: CPU: None - Win64: + - first: + Standalone: Win64 + second: enabled: 0 settings: CPU: None - iOS: + - first: + Windows Store Apps: WindowsStoreApps + second: enabled: 0 + settings: + CPU: AnyCPU + - first: + iPhone: iOS + second: + enabled: 1 settings: AddToEmbeddedBinaries: false CPU: AnyCPU @@ -67,4 +84,4 @@ PluginImporter: FrameworkDependencies: userData: assetBundleName: - assetBundleVariant: \ No newline at end of file + assetBundleVariant: diff --git a/src/Sentry.Unity.Editor/Native/SwitchNativePluginBuildPreProcess.cs b/src/Sentry.Unity.Editor/Native/SwitchNativePluginBuildPreProcess.cs index ed03280e0..8edbc8b9f 100644 --- a/src/Sentry.Unity.Editor/Native/SwitchNativePluginBuildPreProcess.cs +++ b/src/Sentry.Unity.Editor/Native/SwitchNativePluginBuildPreProcess.cs @@ -1,5 +1,3 @@ -using System.IO; -using System.Linq; using Sentry.Extensibility; using UnityEditor; using UnityEditor.Build; @@ -8,32 +6,15 @@ namespace Sentry.Unity.Editor.Native; /// -/// Manages native plugin stubs for Nintendo Switch builds. +/// Defensively fails a Switch build if the no-op stubs are not set up. /// /// -/// For Nintendo Switch, users must compile and provide their own static native Sentry library. -/// This preprocessor detects whether the user has provided the required native files and: -/// -/// If all required files are present: disables the stub (real library will be linked) -/// If files are missing: enables the stub (provides no-op implementations to satisfy linker) -/// If files are partially present: warns the user about misconfiguration -/// +/// should have set this up already. This is the guard if it does not. +/// We cannot repair the setup here. Unity collects native plugins as the build starts, so writing +/// an asset from a build callback may or may not reach the player. /// internal class SwitchNativePluginBuildPreProcess : IPreprocessBuildWithReport { - /// - /// Both platforms share one stub, so the required libraries are what differ between them. The build target's - /// name doubles as the directory name, i.e. `Switch` and `Switch2`. - /// - internal static string[] RequiredFilesFor(BuildTarget target) - { - return - [ - $"Assets/Plugins/Sentry/{target}/libsentry.a", - $"Assets/Plugins/Sentry/{target}/libzstd.a" - ]; - } - public int callbackOrder => -100; public void OnPreprocessBuild(BuildReport report) @@ -45,69 +26,24 @@ public void OnPreprocessBuild(BuildReport report) var options = SentryScriptableObject.LoadOptions(isBuilding: true); var logger = options?.DiagnosticLogger ?? new UnityLogger(new SentryUnityOptions()); + var target = report.summary.platform; - ConfigureStub(logger, options?.SwitchNativeSupportEnabled ?? false, report.summary.platform); - } - - internal static void ConfigureStub(IDiagnosticLogger logger, bool nativeSupportEnabled, BuildTarget target) - { - var requiredFiles = RequiredFilesFor(target); - - logger.LogDebug("{0} native support: checking for required files:\n{1}", - target, string.Join("\n", requiredFiles.Select(f => $" - {f}"))); - - // One stub serves both platforms; the importer tracks compatibility per build target, so - // enabling it for one does not affect the other. - var stubPath = Path.Combine("Packages", SentryPackageInfo.GetName(), "Plugins", "Switch", "sentry_native_stubs.c"); - - var importer = AssetImporter.GetAtPath(stubPath) as PluginImporter; - if (importer == null) + if (SwitchNativeStub.IsInSync(target)) { - logger.LogError("Failed to get PluginImporter for stub at '{0}'. Skipping stub configuration.", stubPath); return; } - var existingFiles = requiredFiles.Where(File.Exists).ToList(); - var missingFiles = requiredFiles.Except(existingFiles).ToList(); + SwitchNativeStub.Sync(logger, target); - var someFilesPresent = existingFiles.Count > 0 && missingFiles.Count > 0; - if (someFilesPresent) - { - logger.LogWarning( - "{0} native support is partially configured. Missing files:\n{1}\n" + - "Please add all required files to enable native support, or remove all files to fall back on no-op stubs.\n" + - "Build sentry-switch and copy the libraries to the expected locations. " + - "See: https://github.com/getsentry/sentry-switch", - target, string.Join("\n", missingFiles.Select(f => $" - {f}")) - ); - return; - } - - var allFilesPresent = missingFiles.Count == 0; - if (allFilesPresent) - { - logger.LogInfo("{0} native libraries found:\n{1}", - target, string.Join("\n", existingFiles.Select(f => $" - {f}"))); - importer.SetCompatibleWithPlatform(target, false); - } - else + if (!SwitchNativeStub.IsInSync(target)) { - if (nativeSupportEnabled) - { - logger.LogWarning( - "{0} native support is enabled but required files are missing:\n{1}\n" + - "Build sentry-switch and copy the libraries to the expected locations. " + - "See: https://github.com/getsentry/sentry-switch", - target, string.Join("\n", missingFiles.Select(f => $" - {f}")) - ); - } - else - { - logger.LogDebug("{0} native support is disabled. Enabling stubs (native calls will be no-op).", target); - } - importer.SetCompatibleWithPlatform(target, true); + throw new BuildFailedException( + $"Sentry failed to update the Switch no-op stub at '{SwitchNativeStub.StubPathFor(target)}'. " + + "See the errors above, resolve it by hand, and trigger the build again."); } - importer.SaveAndReimport(); + throw new BuildFailedException( + "Sentry's Switch no-op stubs in 'Assets/Plugins/Sentry' were out of step with the " + + "installed libraries and have been updated. Please trigger the build again."); } } diff --git a/src/Sentry.Unity.Editor/Native/SwitchNativeStub.cs b/src/Sentry.Unity.Editor/Native/SwitchNativeStub.cs new file mode 100644 index 000000000..50b2b7e4c --- /dev/null +++ b/src/Sentry.Unity.Editor/Native/SwitchNativeStub.cs @@ -0,0 +1,230 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Sentry.Extensibility; +using UnityEditor; + +namespace Sentry.Unity.Editor.Native; + +/// +/// Keeps the no-op native stubs in the user's project up to date with the sentry-switch libraries +/// they have installed. +/// +/// +/// +/// Only the active build target gains a copy, so projects do not carry a file they have no use for. +/// Removal is not gated that way, because a stale stub shadows the libraries next to it. +/// +/// +internal static class SwitchNativeStub +{ + internal const string StubFileName = "sentry_native_stubs.c"; + + /// + /// The sentry-switch libraries the stub stands in for. The watcher below keys off these names. + /// + internal static readonly string[] LibraryFileNames = ["libsentry.a", "libzstd.a"]; + + /// + /// The build target's name doubles as the directory name, i.e. `Switch` and `Switch2`. + /// + internal static string PluginDirectoryFor(BuildTarget target) => $"Assets/Plugins/Sentry/{target}"; + + /// + /// Both platforms share one stub. The difference is the directory name the stub goes in. + /// + internal static string[] RequiredFilesFor(BuildTarget target) + { + var directory = PluginDirectoryFor(target); + return LibraryFileNames.Select(library => $"{directory}/{library}").ToArray(); + } + + internal static string StubPathFor(BuildTarget target) => $"{PluginDirectoryFor(target)}/{StubFileName}"; + + /// + /// Resolved by enumeration because BuildTarget.Switch2 does not exist in Unity 6000.2 or older. + /// + internal static IEnumerable Targets => Enum + .GetValues(typeof(BuildTarget)) + .Cast() + .Where(target => target.IsSwitchFamily()); + + /// + /// Whether the stub is needed and matches the packaged version. + /// + /// + /// Checks the content. An older stub might miss new API. + /// + internal static bool IsInSync(BuildTarget target) + { + var stubPath = StubPathFor(target); + var stubNeeded = RequiredFilesFor(target).Any(file => !File.Exists(file)); + + if (!File.Exists(stubPath)) + { + return !stubNeeded; + } + + return stubNeeded && Matches(File.ReadAllText(stubPath), File.ReadAllText(TemplatePath())); + } + + private static bool Matches(string stub, string template) => + string.Equals(Normalize(stub), Normalize(template), StringComparison.Ordinal); + + private static string Normalize(string content) => + content.Replace("\r\n", "\n").Replace("\r", "\n").TrimEnd(); + + [InitializeOnLoadMethod] + private static void OnDomainReload() => EditorApplication.delayCall += () => Sync(null); + + /// + /// Syncs the SDK with the state of the game. Adds/removes the stub. + /// + internal static void Sync(IDiagnosticLogger? logger) => + Sync(logger, EditorUserBuildSettings.activeBuildTarget); + + internal static void Sync(IDiagnosticLogger? logger, BuildTarget activeTarget) + { + // Running inside a domain reload or an asset change callback. + logger ??= new UnityLogger(new SentryUnityOptions()); + + foreach (var target in Targets) + { + var missingFiles = RequiredFilesFor(target).Where(file => !File.Exists(file)).ToList(); + + if (missingFiles.Count == 0) + { + RemoveStub(logger, target); + } + // No reason to put a file in the user's repository for a target they are not building. + else if (target == activeTarget) + { + WarnAboutPartialInstall(logger, target, missingFiles); + AddStub(logger, target); + } + } + } + + private static void WarnAboutPartialInstall( + IDiagnosticLogger logger, BuildTarget target, List missingFiles) + { + if (missingFiles.Count == LibraryFileNames.Length) + { + return; + } + + logger.LogWarning( + "{0} native support is partially configured. Missing files:\n{1}\n" + + "Sentry's no-op stubs are being used instead. Add all required files to enable native " + + "support, or remove all of them to silence this warning. " + + "Build sentry-switch and copy the libraries to the expected locations. " + + "See: https://github.com/getsentry/sentry-switch", + target, string.Join("\n", missingFiles.Select(file => $" - {file}"))); + } + + private static void AddStub(IDiagnosticLogger logger, BuildTarget target) + { + var stubPath = StubPathFor(target); + var template = File.ReadAllText(TemplatePath()); + + var existing = File.Exists(stubPath); + if (existing && Matches(File.ReadAllText(stubPath), template)) + { + // Manually set the stub's targeted platform. + ConfigureImporter(logger, stubPath, target); + return; + } + + _ = Directory.CreateDirectory(Path.GetDirectoryName(stubPath)!); + File.WriteAllText(stubPath, template); + // Refresh because the plugin directory itself may be new to the AssetDatabase. + AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport); + ConfigureImporter(logger, stubPath, target); + + if (existing) + { + logger.LogInfo("{0} no-op stubs in '{1}' updated to the version this SDK ships.", + target, PluginDirectoryFor(target)); + return; + } + + logger.LogInfo( + "{0} native libraries not found in '{1}'. Added no-op stubs.", + target, PluginDirectoryFor(target)); + } + + private static void RemoveStub(IDiagnosticLogger logger, BuildTarget target) + { + var stubPath = StubPathFor(target); + if (!File.Exists(stubPath)) + { + return; + } + + if (!AssetDatabase.DeleteAsset(stubPath)) + { + logger.LogError( + "Failed to delete '{0}'. Native support might fail at runtime.\n" + + "Delete it by hand and rebuild.", stubPath); + return; + } + + logger.LogInfo("{0} native libraries found in '{1}'. Removed no-op stubs.", + target, PluginDirectoryFor(target)); + } + + /// + /// The stub should only target the platform for the directory's name it is in. `.c` targets every platform + /// by default. + /// + private static void ConfigureImporter(IDiagnosticLogger logger, string stubPath, BuildTarget target) + { + if (AssetImporter.GetAtPath(stubPath) is not PluginImporter importer) + { + logger.LogError("Failed to get the PluginImporter for '{0}'. Skipping stub configuration.", stubPath); + return; + } + + if (!importer.GetCompatibleWithAnyPlatform() && + !importer.GetCompatibleWithEditor() && + Targets.All(candidate => importer.GetCompatibleWithPlatform(candidate) == (candidate == target))) + { + return; + } + + importer.SetCompatibleWithAnyPlatform(false); + importer.SetCompatibleWithEditor(false); + foreach (var candidate in Targets) + { + importer.SetCompatibleWithPlatform(candidate, candidate == target); + } + + importer.SaveAndReimport(); + } + + private static string TemplatePath() => Path.GetFullPath(Path.Combine( + "Packages", SentryPackageInfo.GetName(), "Plugins", "Switch", "SentryStub~", StubFileName)); +} + +/// +/// Monitor the imported Assets so we can replace the stubs when necessary. +/// +internal class SwitchNativeStubWatcher : AssetPostprocessor +{ + private static void OnPostprocessAllAssets( + string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) + { + var librariesChanged = importedAssets + .Concat(deletedAssets) + .Concat(movedAssets) + .Concat(movedFromAssetPaths) + .Any(path => SwitchNativeStub.LibraryFileNames.Any( + library => path.EndsWith(library, StringComparison.Ordinal))); + + if (librariesChanged) + { + EditorApplication.delayCall += () => SwitchNativeStub.Sync(null); + } + } +} diff --git a/test/Scripts.Tests/package-release.zip.snapshot b/test/Scripts.Tests/package-release.zip.snapshot index 564621063..a4471d2f5 100644 --- a/test/Scripts.Tests/package-release.zip.snapshot +++ b/test/Scripts.Tests/package-release.zip.snapshot @@ -28,6 +28,7 @@ Editor/sentry-cli/sentry-cli-Linux-x86_64.meta Editor/sentry-cli/sentry-cli-Windows-x86_64.exe Editor/sentry-cli/sentry-cli-Windows-x86_64.exe.meta Plugins/Linux/ +Plugins/Switch/ Plugins/Windows/ Plugins/Android.meta Plugins/iOS.meta @@ -280,8 +281,7 @@ Plugins/Android/Sentry~/sentry-android-core-release.aar Plugins/Android/Sentry~/sentry-android-ndk-release.aar Plugins/Android/Sentry~/sentry-native-ndk-release.aar Plugins/Android/Sentry~/sentry.jar -Plugins/Switch/sentry_native_stubs.c -Plugins/Switch/sentry_native_stubs.c.meta +Plugins/Switch/SentryStub~/sentry_native_stubs.c Plugins/Windows/SentryNative~/sentry-crash.exe Plugins/Windows/SentryNative~/sentry-crash.pdb Plugins/Windows/SentryNative~/sentry-wer.dll diff --git a/test/Scripts.Tests/test-plugin-platforms.ps1 b/test/Scripts.Tests/test-plugin-platforms.ps1 new file mode 100644 index 000000000..602beaf8c --- /dev/null +++ b/test/Scripts.Tests/test-plugin-platforms.ps1 @@ -0,0 +1,333 @@ +# Pins which platforms every plugin and assembly definition in the package targets. +# +# 1. A hardcoded scope per plugin importer and .asmdef. Any drift fails, so changing a scope takes a +# deliberate edit to the tables below. Guards against a repeat of the platform list inversion, +# where an allowlist became an exclude list and quietly widened what an assembly shipped to. +# +# 2. No managed plugin declaring `DllImport("__Internal")` may target a desktop standalone player. +# `__Internal` binds at link time against a symbol inside the player executable, which desktop +# never has, because a native plugin there is always a separate shared library loaded at runtime. +# Such a build only survives while the UnityLinker strips the unreferenced types. +# +# Runs against `package-release.zip` when it exists, because that is what ships. The `package-dev` +# binaries are whatever was last built there and can describe a different branch entirely. Without +# the zip, the `package-dev` and `package` trees are checked instead, and the `__Internal` rule only +# covers assemblies whose binary is present. + +$ErrorActionPreference = "Stop" + +$projectRoot = (Resolve-Path "$PSScriptRoot/../..").Path +$packageFile = Join-Path $projectRoot "package-release.zip" + +# --------------------------------------------------------------------------------------------------- +# Pinned expectations. Paths are relative to the package root. Platform names are Unity's own, sorted. +# --------------------------------------------------------------------------------------------------- + +# What the released package must contain. "Any" means the plugin is platform agnostic. +$ExpectedPluginScopes = @{ + "Editor/Sentry.Unity.Editor.dll" = "Editor" + "Editor/iOS/Sentry.Unity.Editor.iOS.dll" = "Editor" + # vsnprintf_sentry, imported as __Internal only under SENTRY_NATIVE_PLAYSTATION. The Switch gets + # the same symbol from its own stubs or from sentry-switch, and Xbox goes through msvcrt. + "Plugins/PS5/sentry_utils.c" = "PS5" + "Plugins/iOS/SentryCxaThrowHook.cpp" = "iOS" + # The two bridge sources target nothing on purpose. BuildPostProcess copies whichever one applies + # into the generated Xcode project, so enabling a platform here would collide with that copy. + "Plugins/iOS/SentryNativeBridge.m" = "" + "Plugins/iOS/SentryNativeBridgeNoOp.m" = "" + "Plugins/macOS/SentryNativeBridge.m" = "OSXUniversal" + "Runtime/Sentry.dll" = "Any" + "Runtime/Sentry.Unity.Android.dll" = "Android" + "Runtime/Sentry.Unity.MacOS.dll" = "OSXUniversal" + "Runtime/Sentry.Unity.Native.PlayStation.dll" = "PS5" + "Runtime/Sentry.Unity.Native.Switch.dll" = "Switch, Switch2" + "Runtime/Sentry.Unity.Native.Xbox.dll" = "GameCoreScarlett, GameCoreXboxOne" + # Android binds to "sentry" from the .aar, desktop to "sentry-native", so they are separate + # builds of the same sources. See SentryNativeLibrary.Name. + "Runtime/Sentry.Unity.Native.Android.dll" = "Android" + "Runtime/Sentry.Unity.Native.dll" = "Linux64, OSXUniversal, Win, Win64" + "Runtime/Sentry.Unity.dll" = "Any" + # Holds the Cocoa bridge __Internal declarations, shared by the iOS and macOS integrations. + "Runtime/Sentry.Unity.iOS.dll" = "iOS, OSXUniversal" +} + +# Demo sources rather than SDK plugins, but they ship inside the package, so they are pinned too. +$ExpectedSampleScopes = @{ + "Samples~/unity-of-bugs/Scripts/NativeSupport/CPlugin.c" = "Android, Any, iOS, Linux64, Lumin, OSXUniversal, tvOS, WebGL, Win, Win64" + "Samples~/unity-of-bugs/Scripts/NativeSupport/CppPlugin.cpp" = "Android, Any, iOS, Linux64, Lumin, OSXUniversal, tvOS, WebGL, Win, Win64" + "Samples~/unity-of-bugs/Scripts/NativeSupport/JavaScriptPlugin.jslib" = "WebGL" + "Samples~/unity-of-bugs/Scripts/NativeSupport/KotlinPlugin.kt" = "Android" + "Samples~/unity-of-bugs/Scripts/NativeSupport/ObjectiveCPlugin.m" = "iOS, tvOS" +} + +# The third party assemblies scripts/alias-assemblies.ps1 renames into the `Sentry.` namespace. They +# are managed and platform agnostic, so they carry Unity's folder default. Matched by pattern because +# the set turns over with every sentry-dotnet bump while the scope never does. The patterns cover the +# aliased prefixes only, so a new first party assembly still has to be pinned by name above. +$AliasedDependencyScopes = @( + @{ Pattern = '^Editor/Sentry\.(Microsoft|Mono)\..*\.dll$' ; Scope = "Editor" } + @{ Pattern = '^Runtime/Sentry\.(Microsoft|System)\..*\.dll$'; Scope = "Any" } +) + +# How package-dev deviates. The dev package keeps the test assemblies, which scripts/pack.ps1 excludes +# from the release, and the iOS bridge stays editor-loadable for the editor-only +# Sentry.Unity.iOS.Tests assembly that references it. +$DevOnlyPluginScopes = @{ + "Runtime/Sentry.Unity.iOS.dll" = "Editor, iOS, OSXUniversal" + "Tests/Editor/Sentry.Unity.Editor.Tests.dll" = "Editor" + "Tests/Editor/Sentry.Unity.Editor.iOS.Tests.dll" = "Editor" + "Tests/Runtime/Sentry.Unity.Android.Tests.dll" = "Editor" + "Tests/Runtime/Sentry.Unity.Tests.dll" = "Editor" + "Tests/Runtime/Sentry.Unity.iOS.Tests.dll" = "Editor" +} + +# Files the package directory is allowed to override in the release, each validated against the +# release table above. +$ExpectedReleaseOverrides = @( + "Runtime/Sentry.Unity.iOS.dll" +) + +# Assembly definitions. An empty include list plus an exclude list means "every platform except +# these", so the exclude list is the thing that must not drift unnoticed. +$ExpectedAsmdefs = @{ + "Runtime/io.sentry.unity.runtime.asmdef" = @{ + include = "" + exclude = "CloudRendering, EmbeddedLinux, PS4, tvOS, XboxOne" + } + "Editor/io.sentry.unity.editor.asmdef" = @{ + include = "Editor" + exclude = "" + } +} + +$DevOnlyAsmdefs = @{ + "Runtime/io.sentry.unity.dev.runtime.asmdef" = @{ + include = "" + exclude = "CloudRendering, EmbeddedLinux, PS4, tvOS, XboxOne" + } + "Editor/io.sentry.unity.dev.editor.asmdef" = @{ + include = "Editor" + exclude = "" + } +} + +# Platforms where Unity loads native code exclusively as a separate shared library. +$DynamicOnlyPlatforms = @("Win", "Win64", "Linux64") + +# --------------------------------------------------------------------------------------------------- +# Parsing +# --------------------------------------------------------------------------------------------------- + +# Sorted, comma separated list of the platforms a plugin importer enables. Handles both .meta +# dialects: the legacy "- first:/second:" list and the newer platform-keyed map. +function Get-EnabledPlatforms([string]$metaText) { + $platforms = @() + $current = $null + $expectKey = $false + + foreach ($line in ($metaText -split "`r?`n")) { + if ($line -match '^\s*-\s*first:\s*$') { + $expectKey = $true + continue + } + if ($expectKey) { + # "Standalone: Win64", "iPhone: iOS", "Editor: Editor", ": Any" or "Any:" + if ($line -match '^\s*(.*?):\s*(\S*)\s*$') { + $current = if ($Matches[2]) { $Matches[2] } else { $Matches[1] } + } + $expectKey = $false + continue + } + if ($line -match '^\s*second:\s*$') { continue } + if ($line -match '^\s{4}(\S[^:]*):\s*$') { + $current = $Matches[1] + continue + } + if ($line -match '^\s*enabled:\s*(\d)\s*$' -and $current) { + if ($Matches[1] -eq '1') { $platforms += $current } + $current = $null + } + } + + return (($platforms | Sort-Object -Unique) -join ", ") +} + +# The pinned scope for an aliased third party assembly, or $null when the path is not one. +function Get-AliasedDependencyScope([string]$path) { + foreach ($rule in $script:AliasedDependencyScopes) { + if ($path -match $rule.Pattern) { return $rule.Scope } + } + return $null +} + +function Get-AsmdefPlatforms([string]$asmdefText) { + $json = $asmdefText | ConvertFrom-Json + return @{ + include = ((@($json.includePlatforms) | Where-Object { $_ } | Sort-Object) -join ", ") + exclude = ((@($json.excludePlatforms) | Where-Object { $_ } | Sort-Object) -join ", ") + } +} + +# path -> @{ Text; Bytes } for every .meta, .asmdef and .dll, from the zip or from a directory. +function Get-PackageFiles($source, [bool]$fromZip) { + $files = @{} + + if ($fromZip) { + Add-Type -AssemblyName System.IO.Compression.FileSystem + $zip = [IO.Compression.ZipFile]::OpenRead($source) + try { + foreach ($entry in $zip.Entries) { + if ($entry.FullName -notmatch '\.(meta|asmdef|dll)$') { continue } + $stream = New-Object IO.MemoryStream + $entry.Open().CopyTo($stream) + $bytes = $stream.ToArray() + $stream.Dispose() + $files[$entry.FullName.Replace("\", "/")] = @{ + Text = [Text.Encoding]::UTF8.GetString($bytes) + Bytes = $bytes + } + } + } + finally { + $zip.Dispose() + } + return $files + } + + foreach ($file in Get-ChildItem -Path $source -Recurse -File) { + if ($file.Extension -notin ".meta", ".asmdef", ".dll") { continue } + $bytes = [IO.File]::ReadAllBytes($file.FullName) + $files[$file.FullName.Substring($source.Length + 1).Replace("\", "/")] = @{ + Text = [Text.Encoding]::UTF8.GetString($bytes) + Bytes = $bytes + } + } + + return $files +} + +# --------------------------------------------------------------------------------------------------- +# Checks +# --------------------------------------------------------------------------------------------------- + +$failures = [Collections.ArrayList]::new() +$scopesChecked = 0 +$importsChecked = 0 + +function Test-Tree($label, $files, $expectedScopes, $expectedAsmdefs) { + $seen = @{} + + foreach ($path in ($files.Keys | Sort-Object)) { + $entry = $files[$path] + + if ($path -like "*.meta") { + if ($entry.Text -notmatch "PluginImporter") { continue } + $described = $path -replace '\.meta$', '' + $actual = Get-EnabledPlatforms $entry.Text + $seen[$described] = $true + + $expected = if ($expectedScopes.ContainsKey($described)) { $expectedScopes[$described] } else { Get-AliasedDependencyScope $described } + if ($null -eq $expected) { + [void]$script:failures.Add("$label : '$described' is not in the expected table, it enables '$actual'") + continue + } + + $script:scopesChecked++ + if ($actual -ne $expected) { + [void]$script:failures.Add("$label : '$described' enables '$actual', expected '$expected'") + } + + # The rule that holds no matter what the table says. Needs the binary, which is present in + # the packed artifact and in a built working tree. + $binary = $files[$described] + if ($binary -and $binary.Bytes.Length -gt 0 -and + [Text.Encoding]::ASCII.GetString($binary.Bytes).Contains("__Internal")) { + $script:importsChecked++ + $enabled = $actual -split ",\s*" + foreach ($platform in $script:DynamicOnlyPlatforms) { + if ($enabled -contains $platform) { + [void]$script:failures.Add("$label : '$described' declares __Internal imports and targets '$platform', which cannot link them") + } + } + } + continue + } + + if ($path -like "*.asmdef") { + if (-not $expectedAsmdefs.ContainsKey($path)) { + [void]$script:failures.Add("$label : assembly definition '$path' is not in the expected table") + continue + } + $actual = Get-AsmdefPlatforms $entry.Text + $want = $expectedAsmdefs[$path] + $script:scopesChecked++ + if ($actual.include -ne $want.include) { + [void]$script:failures.Add("$label : '$path' includePlatforms is '$($actual.include)', expected '$($want.include)'") + } + if ($actual.exclude -ne $want.exclude) { + [void]$script:failures.Add("$label : '$path' excludePlatforms is '$($actual.exclude)', expected '$($want.exclude)'") + } + } + } + + foreach ($expected in ($expectedScopes.Keys | Sort-Object)) { + if (-not $seen.ContainsKey($expected)) { + [void]$script:failures.Add("$label : expected plugin '$expected' is missing") + } + } + + foreach ($expected in ($expectedAsmdefs.Keys | Sort-Object)) { + if (-not $files.ContainsKey($expected)) { + [void]$script:failures.Add("$label : expected assembly definition '$expected' is missing") + } + } +} + +if (Test-Path -Path $packageFile) { + Write-Host "Validating $packageFile" + $files = Get-PackageFiles $packageFile $true + if ($files.Count -eq 0) { + Write-Host "No .meta, .asmdef or .dll entries found in the package." -ForegroundColor Yellow + exit 1 + } + $releaseScopes = @{} + foreach ($pair in $ExpectedPluginScopes.GetEnumerator()) { $releaseScopes[$pair.Key] = $pair.Value } + foreach ($pair in $ExpectedSampleScopes.GetEnumerator()) { $releaseScopes[$pair.Key] = $pair.Value } + Test-Tree "release" $files $releaseScopes $ExpectedAsmdefs +} +else { + Write-Host "'$packageFile' not found - validating the package-dev and package trees instead" + + $devRoot = Join-Path $projectRoot "package-dev" + $overrideRoot = Join-Path $projectRoot "package" + foreach ($root in @($devRoot, $overrideRoot)) { + if (-not (Test-Path -Path $root)) { + Write-Host "'$root' not found." -ForegroundColor Yellow + exit 1 + } + } + + # package-dev carries the test assemblies and the editor-loadable iOS bridge. + $devScopes = @{} + foreach ($pair in $ExpectedPluginScopes.GetEnumerator()) { $devScopes[$pair.Key] = $pair.Value } + foreach ($pair in $DevOnlyPluginScopes.GetEnumerator()) { $devScopes[$pair.Key] = $pair.Value } + Test-Tree "package-dev" (Get-PackageFiles $devRoot $false) $devScopes $DevOnlyAsmdefs + + # The package directory overrides the release, so whatever it holds must match the release table. + $overrideScopes = @{} + foreach ($path in $ExpectedReleaseOverrides) { $overrideScopes[$path] = $ExpectedPluginScopes[$path] } + Test-Tree "package" (Get-PackageFiles $overrideRoot $false) $overrideScopes $ExpectedAsmdefs +} + +if ($failures.Count -gt 0) { + Write-Host "Platform scopes do not match the pinned expectations:" -ForegroundColor Yellow + foreach ($failure in $failures) { Write-Host " $failure" -ForegroundColor Red } + Write-Host "" + Write-Host "If the change is intended, update the tables at the top of this script in the same" -ForegroundColor Yellow + Write-Host "commit, so the new scope gets reviewed. A managed plugin declaring __Internal imports" -ForegroundColor Yellow + Write-Host "can never target Win, Win64 or Linux64, whatever the table says." -ForegroundColor Yellow + exit 3 +} + +Write-Host "Pinned $scopesChecked platform scope(s); verified __Internal imports on $importsChecked assembly(ies)." -ForegroundColor Green +exit 0 diff --git a/test/Sentry.Unity.Editor.Tests/Native/SwitchNativeStubTests.cs b/test/Sentry.Unity.Editor.Tests/Native/SwitchNativeStubTests.cs index a9b7371eb..9fd3ed0cd 100644 --- a/test/Sentry.Unity.Editor.Tests/Native/SwitchNativeStubTests.cs +++ b/test/Sentry.Unity.Editor.Tests/Native/SwitchNativeStubTests.cs @@ -12,13 +12,18 @@ namespace Sentry.Unity.Editor.Tests.Native; public class SwitchNativeStubTests { + private static string PackageRoot() => Path.GetFullPath(Path.Combine( + Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "..", "..")); + + private static string StubTemplatePath() => + Path.Combine(PackageRoot(), "Plugins", "Switch", "SentryStub~", SwitchNativeStub.StubFileName); + [Test] public void Stub_ContainsEverySwitchNativeBinding() { - var packageRoot = Path.GetFullPath(Path.Combine( - Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "..", "..")); + var packageRoot = PackageRoot(); var switchAssemblyPath = Path.Combine(packageRoot, "Runtime", "Sentry.Unity.Native.Switch.dll"); - var stubPath = Path.Combine(packageRoot, "Plugins", "Switch", "sentry_native_stubs.c"); + var stubPath = StubTemplatePath(); Assert.That(File.Exists(switchAssemblyPath), Is.True, $"Switch assembly not found at {switchAssemblyPath}"); Assert.That(File.Exists(stubPath), Is.True, $"Switch stubs not found at {stubPath}"); @@ -48,7 +53,7 @@ public void Stub_ContainsEverySwitchNativeBinding() [Test] public void RequiredFilesFor_Switch_ProbesTheSwitchPluginDirectory() { - var requiredFiles = SwitchNativePluginBuildPreProcess.RequiredFilesFor(BuildTarget.Switch); + var requiredFiles = SwitchNativeStub.RequiredFilesFor(BuildTarget.Switch); Assert.That(requiredFiles, Is.EquivalentTo(new[] { @@ -58,8 +63,29 @@ public void RequiredFilesFor_Switch_ProbesTheSwitchPluginDirectory() } /// - /// Switch 2 is resolved by name because BuildTarget.Switch2 does not exist on the Unity versions the - /// SDK still supports, so this parses the member instead of referencing it and skips where it is unavailable. + /// One copy per target, so a project can have real native support on one Switch generation and + /// stubs on the other. + /// + [Test] + public void StubPathFor_SitsBesideTheLibrariesItReplaces() + { + var stubPath = SwitchNativeStub.StubPathFor(BuildTarget.Switch); + + Assert.That(stubPath, Is.EqualTo("Assets/Plugins/Sentry/Switch/sentry_native_stubs.c")); + Assert.That(SwitchNativeStub.RequiredFilesFor(BuildTarget.Switch), + Has.All.StartsWith(SwitchNativeStub.PluginDirectoryFor(BuildTarget.Switch))); + } + + [Test] + public void Targets_CoverTheSwitchFamilyOnly() + { + Assert.That(SwitchNativeStub.Targets, Does.Contain(BuildTarget.Switch)); + Assert.That(SwitchNativeStub.Targets, Has.All.Matches(target => target.IsSwitchFamily())); + } + + /// + /// Resolved by name because BuildTarget.Switch2 does not exist on every Unity version the + /// SDK supports. /// [Test] public void RequiredFilesFor_Switch2_ProbesTheSwitch2PluginDirectory() @@ -69,7 +95,7 @@ public void RequiredFilesFor_Switch2_ProbesTheSwitch2PluginDirectory() Assert.Ignore("This Unity version predates 'BuildTarget.Switch2'."); } - var requiredFiles = SwitchNativePluginBuildPreProcess.RequiredFilesFor(switch2); + var requiredFiles = SwitchNativeStub.RequiredFilesFor(switch2); Assert.That(requiredFiles, Is.EquivalentTo(new[] {