From 1fb9464bfe1ff7e37fb49ff2df024f9886b364d4 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 11 Sep 2026 14:35:54 +0200 Subject: [PATCH 1/9] Scope `Sentry.Unity.iOS` to Apple platforms `Sentry.Unity.iOS` holds the managed side of the Cocoa bridge, declared with `DllImport("__Internal")`. That binds at link time against a symbol inside the player executable, which only exists where Unity compiles the native bridge in: iOS via `Plugins/iOS/SentryNativeBridge.m` and macOS via the macOS equivalent. On Windows and Linux a native plugin is always a separate shared library, so those imports can never resolve. The plugin importer nevertheless enabled Standalone Win, Win64 and Linux64, making it the only platform-specific assembly in the package that was not scoped to its platform. It stayed invisible because nothing in a desktop player references the bridge, so the UnityLinker stripped the types before IL2CPP could emit the imports. Correctness depended on stripping: any consumer that preserves the assembly, for instance another package contributing a `link.xml`, gets 21 unresolved `SentryNativeBridge*` externals and a failed link instead. `package-dev` keeps the Editor entry, because the editor-only `Sentry.Unity.iOS.Tests` assembly references the plugin and cannot load without it. The release excludes `Tests`, so `package/Runtime` now carries an override that drops the Editor too, leaving the shipped plugin scoped to iOS and macOS like `Sentry.Unity.MacOS`, `Sentry.Unity.Native.Switch` and `Sentry.Unity.Native.PlayStation`. `test-plugin-platforms.ps1` guards the invariant: no managed plugin declaring `__Internal` imports may target a desktop standalone player. It runs in the `package-validation` job against the packed artifact, so it checks what ships. --- .github/workflows/ci.yml | 4 + CHANGELOG.md | 1 + package-dev/Runtime/Sentry.Unity.iOS.dll.meta | 12 +- package/Runtime/Sentry.Unity.iOS.dll.meta | 87 +++++++++++ test/Scripts.Tests/test-plugin-platforms.ps1 | 146 ++++++++++++++++++ 5 files changed, 244 insertions(+), 6 deletions(-) create mode 100644 package/Runtime/Sentry.Unity.iOS.dll.meta create mode 100644 test/Scripts.Tests/test-plugin-platforms.ps1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 94ca1885c..f9a4c48cc 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/CHANGELOG.md b/CHANGELOG.md index b76d49803..a665d1e31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - 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)) +- The `Sentry.Unity.iOS` assembly is now scoped to iOS and macOS, matching the other platform-specific assemblies. It carries the `__Internal` P/Invoke declarations for the Cocoa bridge, which can only resolve where Unity compiles the native bridge into the player, so a Windows or Linux build that kept those types instead of stripping them failed to link with unresolved `SentryNativeBridge*` externals. ([#2847](https://github.com/getsentry/sentry-unity/pull/2847)) ### Dependencies 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/Runtime/Sentry.Unity.iOS.dll.meta b/package/Runtime/Sentry.Unity.iOS.dll.meta new file mode 100644 index 000000000..a14c1e023 --- /dev/null +++ b/package/Runtime/Sentry.Unity.iOS.dll.meta @@ -0,0 +1,87 @@ +fileFormatVersion: 2 +guid: 7b1845f97d56c487bb20875e78b23bb7 +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 1 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + : Any + second: + enabled: 0 + settings: + Exclude Android: 1 + Exclude Editor: 1 + Exclude Linux64: 1 + Exclude OSXUniversal: 0 + Exclude WebGL: 1 + Exclude Win: 1 + Exclude Win64: 1 + 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 + - first: + Standalone: Linux64 + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: OSXUniversal + second: + enabled: 1 + settings: + CPU: AnyCPU + - first: + Standalone: Win + second: + enabled: 0 + settings: + CPU: None + - first: + Standalone: Win64 + second: + enabled: 0 + settings: + CPU: None + - first: + Windows Store Apps: WindowsStoreApps + second: + enabled: 0 + settings: + CPU: AnyCPU + - first: + iPhone: iOS + second: + enabled: 1 + settings: + AddToEmbeddedBinaries: false + CPU: AnyCPU + CompileFlags: + FrameworkDependencies: + userData: + assetBundleName: + assetBundleVariant: diff --git a/test/Scripts.Tests/test-plugin-platforms.ps1 b/test/Scripts.Tests/test-plugin-platforms.ps1 new file mode 100644 index 000000000..474a369b4 --- /dev/null +++ b/test/Scripts.Tests/test-plugin-platforms.ps1 @@ -0,0 +1,146 @@ +# Verifies that no managed plugin declaring `DllImport("__Internal")` is marked compatible with a +# desktop standalone player. +# +# `__Internal` tells IL2CPP the native symbol is linked into the player executable. Unity can do that +# on iOS and macOS, where it compiles the Objective-C sources under `Plugins/iOS` and `Plugins/macOS` +# into the player, and on the consoles. On Windows and Linux native plugins are always separate +# shared libraries loaded at runtime, so there is no way for those symbols to exist. The build only +# survives today because the UnityLinker strips the unreferenced bridge types; anything that keeps +# them alive (a `link.xml` from another package, a lower stripping level) turns the mismatch into +# unresolved externals at link time. +# +# Runs against `package-release.zip` when present, so it validates what actually ships, and falls +# back to `package-dev` for a local run before packing. + +$ErrorActionPreference = "Stop" + +$projectRoot = Resolve-Path "$PSScriptRoot/../.." +$packageFile = Join-Path $projectRoot "package-release.zip" + +# Unity platform names that load native code exclusively as a separate shared library. +$dynamicOnlyPlatforms = @("Win", "Win64", "Linux64") + +# Returns a hashtable of Unity platform name -> enabled flag. 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) { + $platforms[$current] = [int]$Matches[1] + $current = $null + } + } + + return $platforms +} + +# name -> @{ Bytes; Meta } for every managed plugin in the package, from the zip or from disk. +function Get-ManagedPlugins { + $plugins = @{} + + if (Test-Path -Path $packageFile) { + Write-Host "Validating $packageFile" + Add-Type -AssemblyName System.IO.Compression.FileSystem + $zip = [IO.Compression.ZipFile]::OpenRead($packageFile) + try { + foreach ($entry in $zip.Entries) { + $path = $entry.FullName.Replace("\", "/") + if ($path -notmatch '\.dll$') { continue } + + $metaEntry = $zip.GetEntry("$($entry.FullName).meta") + if (-not $metaEntry) { continue } + + $dllStream = New-Object IO.MemoryStream + $entry.Open().CopyTo($dllStream) + $metaReader = New-Object IO.StreamReader($metaEntry.Open()) + try { + $plugins[$path] = @{ Bytes = $dllStream.ToArray(); Meta = $metaReader.ReadToEnd() } + } finally { + $metaReader.Dispose() + $dllStream.Dispose() + } + } + } finally { + $zip.Dispose() + } + return $plugins + } + + $packageDev = Join-Path $projectRoot "package-dev" + if (-not (Test-Path -Path $packageDev)) { + Write-Host "Neither '$packageFile' nor '$packageDev' found. Run 'scripts/pack.ps1' first." + exit 1 + } + + Write-Host "'$packageFile' not found - validating $packageDev instead" + foreach ($dll in Get-ChildItem -Path $packageDev -Recurse -Filter "*.dll") { + $meta = "$($dll.FullName).meta" + if (-not (Test-Path -Path $meta)) { continue } + $relative = $dll.FullName.Substring($packageDev.Length + 1).Replace("\", "/") + $plugins[$relative] = @{ + Bytes = [IO.File]::ReadAllBytes($dll.FullName) + Meta = [IO.File]::ReadAllText($meta) + } + } + + return $plugins +} + +$plugins = Get-ManagedPlugins +if ($plugins.Count -eq 0) { + Write-Host "No managed plugins with .meta files found - nothing to validate." -ForegroundColor Yellow + exit 1 +} + +$failures = @() +$checked = 0 + +foreach ($path in $plugins.Keys | Sort-Object) { + $ascii = [Text.Encoding]::ASCII.GetString($plugins[$path].Bytes) + if (-not $ascii.Contains("__Internal")) { continue } + + $checked++ + $enabled = Get-EnabledPlatforms $plugins[$path].Meta + + foreach ($platform in $dynamicOnlyPlatforms) { + if ($enabled[$platform] -eq 1) { + $failures += "$path is enabled for '$platform'" + } + # The legacy `Any` block only governs when `Any` itself is enabled. + if ($enabled["Any"] -eq 1 -and $plugins[$path].Meta -match "Exclude $($platform): 0") { + $failures += "$path is enabled for 'Any' without excluding '$platform'" + } + } +} + +if ($failures) { + Write-Host "Managed plugins declaring __Internal imports must not target desktop standalone players:" -ForegroundColor Yellow + foreach ($failure in $failures) { + Write-Host " $failure" -ForegroundColor Red + } + Write-Host "Disable Standalone Win, Win64 and Linux64 in the plugin's .meta under package-dev." -ForegroundColor Yellow + exit 3 +} + +Write-Host "Checked $checked managed plugin(s) with '__Internal' imports - all correctly scoped." -ForegroundColor Green +exit 0 From a7ace0af042bd5799fd06e9cea5aee4edaa3dd84 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 11 Sep 2026 14:50:04 +0200 Subject: [PATCH 2/9] Pin the platform scope of every plugin and assembly definition Extends the plugin platform test from one rule to a hardcoded expectation for every plugin importer and every `.asmdef` in the package, so a scope cannot change without editing the table in the same commit. It catches a platform gained, a platform lost, a plugin added without a pinned scope, a plugin removed, and an `.asmdef` flipping between an include allowlist and an exclude list, which is the shape of the change that widened the runtime assembly's platform set. The release and `package-dev` expectations are separate, because `pack.ps1` drops the test assemblies and `package/` overrides the iOS bridge meta. The `__Internal` rule still applies on top of the tables and cannot be waived by editing them. Plugin `.meta` files are tracked while the binaries they describe are generated, so scope checks are strict whether the test runs against the packed artifact or the working tree. The `__Internal` rule needs the binary and so covers whatever is present, which in CI is everything. Pins 27 scopes today. Three existing ones are recorded as they are rather than changed, since correcting them is a separate decision: both `Plugins/iOS` Objective-C sources enable tvOS and not iOS, the macOS source enables tvOS alongside OSXUniversal, and `Plugins/PS5/sentry_utils.c` enables both GameCore targets while its own comment describes it as a Windows, Linux and PlayStation wrapper. --- test/Scripts.Tests/test-plugin-platforms.ps1 | 321 ++++++++++++++----- 1 file changed, 239 insertions(+), 82 deletions(-) diff --git a/test/Scripts.Tests/test-plugin-platforms.ps1 b/test/Scripts.Tests/test-plugin-platforms.ps1 index 474a369b4..4bd42b3ad 100644 --- a/test/Scripts.Tests/test-plugin-platforms.ps1 +++ b/test/Scripts.Tests/test-plugin-platforms.ps1 @@ -1,39 +1,119 @@ -# Verifies that no managed plugin declaring `DllImport("__Internal")` is marked compatible with a -# desktop standalone player. +# Pins which platforms every plugin and assembly definition in the package targets. # -# `__Internal` tells IL2CPP the native symbol is linked into the player executable. Unity can do that -# on iOS and macOS, where it compiles the Objective-C sources under `Plugins/iOS` and `Plugins/macOS` -# into the player, and on the consoles. On Windows and Linux native plugins are always separate -# shared libraries loaded at runtime, so there is no way for those symbols to exist. The build only -# survives today because the UnityLinker strips the unreferenced bridge types; anything that keeps -# them alive (a `link.xml` from another package, a lower stripping level) turns the mismatch into -# unresolved externals at link time. +# Two kinds of check: # -# Runs against `package-release.zip` when present, so it validates what actually ships, and falls -# back to `package-dev` for a local run before packing. +# 1. A hardcoded scope for every plugin importer and every .asmdef. Any drift fails, including a +# platform silently gained or lost. Changing a scope means editing the tables below, which makes +# the decision deliberate and reviewable. This is the guard against a repeat of the platform list +# inversion, where an explicit allowlist became an exclude list and quietly widened the set of +# platforms an assembly ships to. +# +# 2. A rule that holds regardless of the tables: no managed plugin declaring `DllImport("__Internal")` +# may target a desktop standalone player. `__Internal` binds at link time against a symbol inside +# the player executable. Unity can only provide that where it compiles native sources into the +# player, which on desktop it never does, 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, so anything that preserves them turns the mismatch into unresolved externals. +# +# Runs against `package-release.zip` when it exists, which is what CI validates and what ships. +# Without it, validates the `package-dev` and `package` trees instead. Plugin .meta files are tracked +# while the binaries they describe are generated, so scope checks are strict in both modes, while the +# `__Internal` rule only covers the assemblies whose binary is actually present. $ErrorActionPreference = "Stop" -$projectRoot = Resolve-Path "$PSScriptRoot/../.." +$projectRoot = (Resolve-Path "$PSScriptRoot/../..").Path $packageFile = Join-Path $projectRoot "package-release.zip" -# Unity platform names that load native code exclusively as a separate shared library. -$dynamicOnlyPlatforms = @("Win", "Win64", "Linux64") +# --------------------------------------------------------------------------------------------------- +# 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" + "Plugins/PS5/sentry_utils.c" = "GameCoreScarlett, GameCoreXboxOne, PS5" + "Plugins/Switch/sentry_native_stubs.c" = "Switch, Switch2" + "Plugins/iOS/SentryCxaThrowHook.cpp" = "iOS" + "Plugins/iOS/SentryNativeBridge.m" = "tvOS" + "Plugins/iOS/SentryNativeBridgeNoOp.m" = "tvOS" + "Plugins/macOS/SentryNativeBridge.m" = "OSXUniversal, tvOS" + "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" + "Runtime/Sentry.Unity.Native.dll" = "Android, 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" +} + +# 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 +# --------------------------------------------------------------------------------------------------- -# Returns a hashtable of Unity platform name -> enabled flag. Handles both .meta dialects: the -# legacy `- first:/second:` list and the newer platform-keyed map. +# 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 = @{} + $platforms = @() $current = $null $expectKey = $false - foreach ($line in $metaText -split "`r?`n") { + 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:` + # "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] } } @@ -46,101 +126,178 @@ function Get-EnabledPlatforms([string]$metaText) { continue } if ($line -match '^\s*enabled:\s*(\d)\s*$' -and $current) { - $platforms[$current] = [int]$Matches[1] + if ($Matches[1] -eq '1') { $platforms += $current } $current = $null } } - return $platforms + return (($platforms | Sort-Object -Unique) -join ", ") +} + +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 ", ") + } } -# name -> @{ Bytes; Meta } for every managed plugin in the package, from the zip or from disk. -function Get-ManagedPlugins { - $plugins = @{} +# path -> @{ Text; Bytes } for every .meta, .asmdef and .dll, from the zip or from a directory. +function Get-PackageFiles($source, [bool]$fromZip) { + $files = @{} - if (Test-Path -Path $packageFile) { - Write-Host "Validating $packageFile" + if ($fromZip) { Add-Type -AssemblyName System.IO.Compression.FileSystem - $zip = [IO.Compression.ZipFile]::OpenRead($packageFile) + $zip = [IO.Compression.ZipFile]::OpenRead($source) try { foreach ($entry in $zip.Entries) { - $path = $entry.FullName.Replace("\", "/") - if ($path -notmatch '\.dll$') { continue } - - $metaEntry = $zip.GetEntry("$($entry.FullName).meta") - if (-not $metaEntry) { continue } - - $dllStream = New-Object IO.MemoryStream - $entry.Open().CopyTo($dllStream) - $metaReader = New-Object IO.StreamReader($metaEntry.Open()) - try { - $plugins[$path] = @{ Bytes = $dllStream.ToArray(); Meta = $metaReader.ReadToEnd() } - } finally { - $metaReader.Dispose() - $dllStream.Dispose() + 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 { + } + finally { $zip.Dispose() } - return $plugins - } - - $packageDev = Join-Path $projectRoot "package-dev" - if (-not (Test-Path -Path $packageDev)) { - Write-Host "Neither '$packageFile' nor '$packageDev' found. Run 'scripts/pack.ps1' first." - exit 1 + return $files } - Write-Host "'$packageFile' not found - validating $packageDev instead" - foreach ($dll in Get-ChildItem -Path $packageDev -Recurse -Filter "*.dll") { - $meta = "$($dll.FullName).meta" - if (-not (Test-Path -Path $meta)) { continue } - $relative = $dll.FullName.Substring($packageDev.Length + 1).Replace("\", "/") - $plugins[$relative] = @{ - Bytes = [IO.File]::ReadAllBytes($dll.FullName) - Meta = [IO.File]::ReadAllText($meta) + 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 $plugins + return $files } -$plugins = Get-ManagedPlugins -if ($plugins.Count -eq 0) { - Write-Host "No managed plugins with .meta files found - nothing to validate." -ForegroundColor Yellow - exit 1 -} +# --------------------------------------------------------------------------------------------------- +# Checks +# --------------------------------------------------------------------------------------------------- + +$failures = [Collections.ArrayList]::new() +$scopesChecked = 0 +$importsChecked = 0 -$failures = @() -$checked = 0 +function Test-Tree($label, $files, $expectedScopes, $expectedAsmdefs) { + $seen = @{} -foreach ($path in $plugins.Keys | Sort-Object) { - $ascii = [Text.Encoding]::ASCII.GetString($plugins[$path].Bytes) - if (-not $ascii.Contains("__Internal")) { continue } + foreach ($path in ($files.Keys | Sort-Object)) { + $entry = $files[$path] - $checked++ - $enabled = Get-EnabledPlatforms $plugins[$path].Meta + if ($path -like "*.meta") { + if ($entry.Text -notmatch "PluginImporter") { continue } + $described = $path -replace '\.meta$', '' + $actual = Get-EnabledPlatforms $entry.Text + $seen[$described] = $true - foreach ($platform in $dynamicOnlyPlatforms) { - if ($enabled[$platform] -eq 1) { - $failures += "$path is enabled for '$platform'" + if (-not $expectedScopes.ContainsKey($described)) { + [void]$script:failures.Add("$label : '$described' is not in the expected table, it enables '$actual'") + continue + } + + $script:scopesChecked++ + if ($actual -ne $expectedScopes[$described]) { + [void]$script:failures.Add("$label : '$described' enables '$actual', expected '$($expectedScopes[$described])'") + } + + # 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") } - # The legacy `Any` block only governs when `Any` itself is enabled. - if ($enabled["Any"] -eq 1 -and $plugins[$path].Meta -match "Exclude $($platform): 0") { - $failures += "$path is enabled for 'Any' without excluding '$platform'" + } + + foreach ($expected in ($expectedAsmdefs.Keys | Sort-Object)) { + if (-not $files.ContainsKey($expected)) { + [void]$script:failures.Add("$label : expected assembly definition '$expected' is missing") } } } -if ($failures) { - Write-Host "Managed plugins declaring __Internal imports must not target desktop standalone players:" -ForegroundColor Yellow - foreach ($failure in $failures) { - Write-Host " $failure" -ForegroundColor Red +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 } - Write-Host "Disable Standalone Win, Win64 and Linux64 in the plugin's .meta under package-dev." -ForegroundColor Yellow + Test-Tree "release" $files $ExpectedPluginScopes $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 "Checked $checked managed plugin(s) with '__Internal' imports - all correctly scoped." -ForegroundColor Green +Write-Host "Pinned $scopesChecked platform scope(s); verified __Internal imports on $importsChecked assembly(ies)." -ForegroundColor Green exit 0 From cb621f78c196e5440d157f1ec25f67b8524933d6 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 11 Sep 2026 14:57:22 +0200 Subject: [PATCH 3/9] Scope the platform-specific native sources to their platforms Three scopes that were wider than the code behind them. `Plugins/PS5/sentry_utils.c` defines `vsnprintf_sentry` and was enabled for PS5 plus both GameCore Xbox targets. `SentryNativeBridge` only declares that import under `SENTRY_NATIVE_PLAYSTATION` or `SENTRY_NATIVE_SWITCH`; the Xbox build takes the `#else` branch and reaches `vsnprintf` through `msvcrt`, and the Switch gets the symbol from its own stubs or from sentry-switch. So the file was compiled into Xbox players that never call it. Now PS5 only. Its legacy `Any` block also left Windows and Linux un-excluded, which was inert while `Any` stayed disabled but is the same trap that made the iOS bridge ship to desktop, so those are excluded now too. `Plugins/macOS/SentryNativeBridge.m` enabled tvOS alongside OSXUniversal, and both `Plugins/iOS` bridge sources enabled tvOS and nothing else. tvOS is in the runtime assembly's exclude list, so the SDK does not run there at all. The iOS sources deliberately target no platform, so Unity never copies them into the generated Xcode project. `BuildPostProcess` copies whichever one applies to `Libraries//SentryNativeBridge.m` and `AddSentryNativeBridge` adds that path to the target, so enabling a platform here would collide with the SDK's own copy. `.gitignore` records the same intent where it un-ignores these metas to control their target platforms. `SentryCxaThrowHook.cpp` keeps iOS, because nothing copies it by hand and it does rely on the importer. The pinned expectations move with the metas, so the table still describes what ships. --- CHANGELOG.md | 1 + package-dev/Plugins/PS5/sentry_utils.c.meta | 10 +++++----- package-dev/Plugins/iOS/SentryNativeBridge.m.meta | 2 +- .../Plugins/iOS/SentryNativeBridgeNoOp.m.meta | 2 +- .../Plugins/macOS/SentryNativeBridge.m.meta | 2 +- test/Scripts.Tests/test-plugin-platforms.ps1 | 15 +++++++++++---- 6 files changed, 20 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a665d1e31..a7202ad33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - 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)) - The `Sentry.Unity.iOS` assembly is now scoped to iOS and macOS, matching the other platform-specific assemblies. It carries the `__Internal` P/Invoke declarations for the Cocoa bridge, which can only resolve where Unity compiles the native bridge into the player, so a Windows or Linux build that kept those types instead of stripping them failed to link with unresolved `SentryNativeBridge*` externals. ([#2847](https://github.com/getsentry/sentry-unity/pull/2847)) +- The platform-specific native sources are now scoped to the platforms that use them. `sentry_utils.c` no longer compiles into Xbox builds, which resolve `vsnprintf` through `msvcrt`, and the iOS and macOS bridge sources no longer compile into tvOS builds. ([#2847](https://github.com/getsentry/sentry-unity/pull/2847)) ### Dependencies 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/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/test/Scripts.Tests/test-plugin-platforms.ps1 b/test/Scripts.Tests/test-plugin-platforms.ps1 index 4bd42b3ad..c3254bcc4 100644 --- a/test/Scripts.Tests/test-plugin-platforms.ps1 +++ b/test/Scripts.Tests/test-plugin-platforms.ps1 @@ -33,12 +33,19 @@ $packageFile = Join-Path $projectRoot "package-release.zip" $ExpectedPluginScopes = @{ "Editor/Sentry.Unity.Editor.dll" = "Editor" "Editor/iOS/Sentry.Unity.Editor.iOS.dll" = "Editor" - "Plugins/PS5/sentry_utils.c" = "GameCoreScarlett, GameCoreXboxOne, PS5" + # 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/Switch/sentry_native_stubs.c" = "Switch, Switch2" "Plugins/iOS/SentryCxaThrowHook.cpp" = "iOS" - "Plugins/iOS/SentryNativeBridge.m" = "tvOS" - "Plugins/iOS/SentryNativeBridgeNoOp.m" = "tvOS" - "Plugins/macOS/SentryNativeBridge.m" = "OSXUniversal, tvOS" + # The two bridge sources deliberately target nothing, so Unity never copies them into the + # generated Xcode project. BuildPostProcess copies whichever one applies to + # Libraries//SentryNativeBridge.m and AddSentryNativeBridge adds that path to the + # target, so enabling a platform here would collide with the SDK's own copy. The same intent is + # recorded in .gitignore, which un-ignores these metas "to control target platforms". + "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" From fc857fad03cc3489b8f6c737293dae65ced7a351 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 11 Sep 2026 15:30:56 +0200 Subject: [PATCH 4/9] Ignore the downloaded macOS SDK binaries Every other platform's native SDK folder is ignored by name, but macOS was missing, so `package-dev/Plugins/macOS/Sentry~` and `SentryNative~` were only partly covered by the extension rules. `libsentry.dylib` matched `*.dylib` while `sentry-crash`, which has no extension, showed up as untracked and was easy to commit by accident when staging a directory. --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) 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 From b043b835dae973b4ab5c6f7a78ed29fc5507a371 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 11 Sep 2026 15:44:47 +0200 Subject: [PATCH 5/9] Pin the Android native assembly and the mechanisms that move scopes Rebased onto `fix/native-library-name-clash`, which splits the desktop and Android builds of `Sentry.Unity.Native`. Desktop binds to `sentry-native` so Mono cannot resolve the name to the managed `Sentry.dll` on a case-insensitive file system, while Android keeps `sentry` from the `.aar`. So `Sentry.Unity.Native.Android.dll` joins the table at Android, and the desktop assembly drops Android from its scope. Also records two things that make a scope or an import table move on their own, so neither reads as a regression: `SwitchNativePluginBuildPreProcess` flips the Switch stub's importer during a build, disabling it once the consumer supplies the real static libraries. That entry is build-time mutable by design, and a local Switch build can leave the meta changed. The binaries under `package-dev` are whatever was last built there, which need not match the checked-out sources. Reading them can therefore describe a different branch entirely, which is why the packed artifact is the authority for the `__Internal` rule. --- test/Scripts.Tests/test-plugin-platforms.ps1 | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/test/Scripts.Tests/test-plugin-platforms.ps1 b/test/Scripts.Tests/test-plugin-platforms.ps1 index c3254bcc4..1e86ed313 100644 --- a/test/Scripts.Tests/test-plugin-platforms.ps1 +++ b/test/Scripts.Tests/test-plugin-platforms.ps1 @@ -19,6 +19,10 @@ # Without it, validates the `package-dev` and `package` trees instead. Plugin .meta files are tracked # while the binaries they describe are generated, so scope checks are strict in both modes, while the # `__Internal` rule only covers the assemblies whose binary is actually present. +# +# Prefer the packed artifact when judging imports. The binaries in `package-dev` are whatever was +# last built there, which need not match the checked-out sources, so their import tables can describe +# a different branch entirely. $ErrorActionPreference = "Stop" @@ -36,6 +40,10 @@ $ExpectedPluginScopes = @{ # 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" + # The shipped default is the stub enabled, so the linker is satisfied even without the native + # libraries. SwitchNativePluginBuildPreProcess flips this importer at build time in the consumer's + # project, disabling the stub once Assets/Plugins/Sentry/ holds the real libsentry.a, so a + # local Switch build in this repo can legitimately leave this meta changed. "Plugins/Switch/sentry_native_stubs.c" = "Switch, Switch2" "Plugins/iOS/SentryCxaThrowHook.cpp" = "iOS" # The two bridge sources deliberately target nothing, so Unity never copies them into the @@ -52,7 +60,10 @@ $ExpectedPluginScopes = @{ "Runtime/Sentry.Unity.Native.PlayStation.dll" = "PS5" "Runtime/Sentry.Unity.Native.Switch.dll" = "Switch, Switch2" "Runtime/Sentry.Unity.Native.Xbox.dll" = "GameCoreScarlett, GameCoreXboxOne" - "Runtime/Sentry.Unity.Native.dll" = "Android, Linux64, OSXUniversal, Win, Win64" + # 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" From 50d4ac9b7b079498e624905b891d1da911e94582 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 11 Sep 2026 16:26:02 +0200 Subject: [PATCH 6/9] Point the changelog entries at the real PR number The entries were written before the pull request existed and guessed 2847 from the highest number then open. It landed as 2848. --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7202ad33..f78b05753 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +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)) -- The `Sentry.Unity.iOS` assembly is now scoped to iOS and macOS, matching the other platform-specific assemblies. It carries the `__Internal` P/Invoke declarations for the Cocoa bridge, which can only resolve where Unity compiles the native bridge into the player, so a Windows or Linux build that kept those types instead of stripping them failed to link with unresolved `SentryNativeBridge*` externals. ([#2847](https://github.com/getsentry/sentry-unity/pull/2847)) -- The platform-specific native sources are now scoped to the platforms that use them. `sentry_utils.c` no longer compiles into Xbox builds, which resolve `vsnprintf` through `msvcrt`, and the iOS and macOS bridge sources no longer compile into tvOS builds. ([#2847](https://github.com/getsentry/sentry-unity/pull/2847)) +- The `Sentry.Unity.iOS` assembly is now scoped to iOS and macOS, matching the other platform-specific assemblies. It carries the `__Internal` P/Invoke declarations for the Cocoa bridge, which can only resolve where Unity compiles the native bridge into the player, so a Windows or Linux build that kept those types instead of stripping them failed to link with unresolved `SentryNativeBridge*` externals. ([#2848](https://github.com/getsentry/sentry-unity/pull/2848)) +- The platform-specific native sources are now scoped to the platforms that use them. `sentry_utils.c` no longer compiles into Xbox builds, which resolve `vsnprintf` through `msvcrt`, and the iOS and macOS bridge sources no longer compile into tvOS builds. ([#2848](https://github.com/getsentry/sentry-unity/pull/2848)) ### Dependencies From 9e99544c31e505d0e47e67e624b4c6667212fc58 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 11 Sep 2026 16:30:59 +0200 Subject: [PATCH 7/9] Drop the agent attribution trailer from the commit convention The convention asked every agent commit to carry a `Co-Authored-By` trailer. It is noise in the history, so the rule now says the opposite, which also stops an agent re-adding the trailer from the file rather than from its own defaults. --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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`. From 5d0f0146657dbb3241ae569443ce4063770aee5a Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 11 Sep 2026 18:08:12 +0200 Subject: [PATCH 8/9] include samples, aliasing, and editor-only --- test/Scripts.Tests/test-plugin-platforms.ps1 | 40 ++++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/test/Scripts.Tests/test-plugin-platforms.ps1 b/test/Scripts.Tests/test-plugin-platforms.ps1 index 1e86ed313..39f55635d 100644 --- a/test/Scripts.Tests/test-plugin-platforms.ps1 +++ b/test/Scripts.Tests/test-plugin-platforms.ps1 @@ -69,6 +69,26 @@ $ExpectedPluginScopes = @{ "Runtime/Sentry.Unity.iOS.dll" = "iOS, OSXUniversal" } +# The samples the release carries under `Samples~`. Demo native sources, not SDK plugins, but they +# ship inside the package, so their scopes are pinned alongside everything else. +$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: editor only under Editor, +# every platform under Runtime. Matched by pattern rather than by name, because the set turns over +# with every sentry-dotnet dependency bump while the scope never does. The patterns cover only the +# aliased prefixes, 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. @@ -152,6 +172,14 @@ function Get-EnabledPlatforms([string]$metaText) { 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 @{ @@ -218,14 +246,15 @@ function Test-Tree($label, $files, $expectedScopes, $expectedAsmdefs) { $actual = Get-EnabledPlatforms $entry.Text $seen[$described] = $true - if (-not $expectedScopes.ContainsKey($described)) { + $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 $expectedScopes[$described]) { - [void]$script:failures.Add("$label : '$described' enables '$actual', expected '$($expectedScopes[$described])'") + 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 @@ -281,7 +310,10 @@ if (Test-Path -Path $packageFile) { Write-Host "No .meta, .asmdef or .dll entries found in the package." -ForegroundColor Yellow exit 1 } - Test-Tree "release" $files $ExpectedPluginScopes $ExpectedAsmdefs + $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" From af3fe08da0a7abcc4e5a9e98a434b3b0ed9b2268 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 11 Sep 2026 18:12:38 +0200 Subject: [PATCH 9/9] updated changelog --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f78b05753..71738665e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,7 @@ - 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)) -- The `Sentry.Unity.iOS` assembly is now scoped to iOS and macOS, matching the other platform-specific assemblies. It carries the `__Internal` P/Invoke declarations for the Cocoa bridge, which can only resolve where Unity compiles the native bridge into the player, so a Windows or Linux build that kept those types instead of stripping them failed to link with unresolved `SentryNativeBridge*` externals. ([#2848](https://github.com/getsentry/sentry-unity/pull/2848)) -- The platform-specific native sources are now scoped to the platforms that use them. `sentry_utils.c` no longer compiles into Xbox builds, which resolve `vsnprintf` through `msvcrt`, and the iOS and macOS bridge sources no longer compile into tvOS builds. ([#2848](https://github.com/getsentry/sentry-unity/pull/2848)) +- Individual assemblies and plugins are now scoped to the platforms that use them. ([#2848](https://github.com/getsentry/sentry-unity/pull/2848)) ### Dependencies