From d117e9039bae88d7462db84bc08342929acc362e Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Thu, 22 Oct 2026 12:53:28 +0200 Subject: [PATCH 01/16] Add Linux ARM64 support --- .github/workflows/build.yml | 12 ++++ .github/workflows/ci.yml | 8 +++ .github/workflows/sdk.yml | 11 +++- CHANGELOG.md | 6 ++ build/native-sdks.targets | 32 +++++----- docs/agent-guides/platform-native.md | 2 +- scripts/download-native-sdks.ps1 | 14 ++++- .../Native/BuildPostProcess.cs | 59 ++++++++++++++----- .../package-release.zip.snapshot | 20 +++++-- .../Native/BuildPostProcessTests.cs | 38 ++++++++++++ 10 files changed, 156 insertions(+), 46 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cff3308ec..064159a7a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -107,12 +107,24 @@ jobs: name: Linux-sdk path: package-dev/Plugins/Linux/Sentry~ + - name: Download LinuxArm64 SDK + uses: ./.github/actions/wait-for-artifact + with: + name: LinuxArm64-sdk + path: package-dev/Plugins/Linux/Sentry~ + - name: Download LinuxNative SDK uses: ./.github/actions/wait-for-artifact with: name: LinuxNative-sdk path: package-dev/Plugins/Linux/SentryNative~ + - name: Download LinuxNativeArm64 SDK + uses: ./.github/actions/wait-for-artifact + with: + name: LinuxNativeArm64-sdk + path: package-dev/Plugins/Linux/SentryNative~ + - name: Download Windows SDK uses: ./.github/actions/wait-for-artifact with: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6151395e8..3db421f6a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,6 +43,13 @@ jobs: host: ubuntu-latest - target: LinuxNative host: ubuntu-latest + # 22.04 keeps the glibc floor low for server distros. + - target: Linux + arch: Arm64 + host: ubuntu-22.04-arm + - target: LinuxNative + arch: Arm64 + host: ubuntu-22.04-arm - target: Windows host: windows-latest - target: WindowsNative @@ -54,6 +61,7 @@ jobs: uses: ./.github/workflows/sdk.yml with: target: ${{ matrix.target }} + arch: ${{ matrix.arch }} runsOn: ${{ matrix.host }} build-unity-sdk: diff --git a/.github/workflows/sdk.yml b/.github/workflows/sdk.yml index 93da1db3c..6813c5176 100644 --- a/.github/workflows/sdk.yml +++ b/.github/workflows/sdk.yml @@ -8,11 +8,16 @@ on: target: required: true type: string + arch: + required: false + type: string + default: "" env: DOTNET_CLI_TELEMETRY_OPTOUT: 1 DOTNET_NOLOGO: 1 TARGET: ${{ inputs.target }} + ARTIFACT: ${{ inputs.target }}${{ inputs.arch }} jobs: build: @@ -63,7 +68,7 @@ jobs: # hash of package/package.json for cache busting on release builds (version bump) path: | package-dev/Plugins - key: sdk=${{ env.TARGET }}-${{ hashFiles('submodules-status', 'package/package.json', 'Directory.Build.targets', 'build/native-sdks.targets', 'sdk-static/**', 'scripts/build-cocoa-sdk.ps1', 'scripts/build-native-ndk-local.ps1') }} + key: sdk=${{ env.ARTIFACT }}-${{ hashFiles('submodules-status', 'package/package.json', 'Directory.Build.targets', 'build/native-sdks.targets', 'sdk-static/**', 'scripts/build-cocoa-sdk.ps1', 'scripts/build-native-ndk-local.ps1') }} - name: Installing Linux Dependencies if: ${{ (env.TARGET == 'Linux' || env.TARGET == 'LinuxNative') && steps.cache.outputs.cache-hit != 'true' }} @@ -138,7 +143,7 @@ jobs: - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: ${{ env.TARGET == 'Linux' }} with: - name: ${{ env.TARGET }}-sdk + name: ${{ env.ARTIFACT }}-sdk path: package-dev/Plugins/Linux/Sentry~ # Lower retention period - we only need this to retry CI. retention-days: 14 @@ -146,7 +151,7 @@ jobs: - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: ${{ env.TARGET == 'LinuxNative' }} with: - name: ${{ env.TARGET }}-sdk + name: ${{ env.ARTIFACT }}-sdk path: package-dev/Plugins/Linux/SentryNative~ # Lower retention period - we only need this to retry CI. retention-days: 14 diff --git a/CHANGELOG.md b/CHANGELOG.md index ee836469d..983c5fed2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Features + +- The SDK now supports Linux ARM64 dedicated servers (Unity 6000.3 and newer, IL2CPP). The native SDK for both backends ships for `x86_64` and `arm64`, and the build picks the one matching the player. ([#2466](https://github.com/getsentry/sentry-unity/issues/2466)) + ## 4.11.0 ### Fixes diff --git a/build/native-sdks.targets b/build/native-sdks.targets index c5008ba7b..ec4577c09 100644 --- a/build/native-sdks.targets +++ b/build/native-sdks.targets @@ -14,8 +14,11 @@ $(RepoRoot)modules/sentry-native/ - $(SentryArtifactsDestination)Linux/Sentry~/ - $(SentryArtifactsDestination)Linux/SentryNative~/ + + arm64 + x86_64 + $(SentryArtifactsDestination)Linux/Sentry~/$(SentryLinuxArch)/ + $(SentryArtifactsDestination)Linux/SentryNative~/$(SentryLinuxArch)/ $(SentryArtifactsDestination)Windows/Sentry~/ $(SentryArtifactsDestination)Windows/SentryNative~/ @@ -46,19 +49,17 @@ - + - - - - + @@ -70,7 +71,7 @@ - + - + - - - - - - + @@ -110,7 +106,7 @@ - + diff --git a/docs/agent-guides/platform-native.md b/docs/agent-guides/platform-native.md index ceb8da4e7..d8a006079 100644 --- a/docs/agent-guides/platform-native.md +++ b/docs/agent-guides/platform-native.md @@ -27,7 +27,7 @@ Experimental native modes raise minimum shutdown timeout to 10 seconds. - Windows: runtime files beside player `.exe`; the library lands as `sentry-native.dll`. Both architectures ship, under `x86/` and `x64/` inside each backend folder, because Unity still builds a 32-bit standalone player. `BuildTarget.StandaloneWindows` picks `x86/`. -- Linux: `libsentry-native.so` under `_Data/Plugins/x86_64`; native daemon beside executable. +- Linux: `libsentry-native.so` under `_Data/Plugins/` (`x86_64` or `arm64`, read from the player's ELF header); native daemon beside executable. - 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. - Placement is what sentry-native probes by default. `SentryUnityOptions.NativeHandlerPath` overrides it via `handler_path`, for handlers staged somewhere else; sidecars the handler resolves relative to itself, such as `crashpad_wer.dll`, move with it. diff --git a/scripts/download-native-sdks.ps1 b/scripts/download-native-sdks.ps1 index efccd359d..1aca7f38c 100644 --- a/scripts/download-native-sdks.ps1 +++ b/scripts/download-native-sdks.ps1 @@ -32,12 +32,22 @@ $SDKs = @( @{ Name = "Linux" Destination = Join-Path $ArtifactsDestination "Linux/Sentry~" - CheckFile = "libsentry-native.so" + CheckFile = "x86_64/libsentry-native.so" + }, + @{ + Name = "LinuxArm64" + Destination = Join-Path $ArtifactsDestination "Linux/Sentry~" + CheckFile = "arm64/libsentry-native.so" }, @{ Name = "LinuxNative" Destination = Join-Path $ArtifactsDestination "Linux/SentryNative~" - CheckFile = "libsentry-native.so" + CheckFile = "x86_64/libsentry-native.so" + }, + @{ + Name = "LinuxNativeArm64" + Destination = Join-Path $ArtifactsDestination "Linux/SentryNative~" + CheckFile = "arm64/libsentry-native.so" }, @{ Name = "Android" diff --git a/src/Sentry.Unity.Editor/Native/BuildPostProcess.cs b/src/Sentry.Unity.Editor/Native/BuildPostProcess.cs index b98df038e..d12ff8345 100644 --- a/src/Sentry.Unity.Editor/Native/BuildPostProcess.cs +++ b/src/Sentry.Unity.Editor/Native/BuildPostProcess.cs @@ -108,7 +108,7 @@ _ when targetGroup.IsSwitch2() => Path.GetDirectoryName(executablePath), else if (target == BuildTarget.StandaloneLinux64) { // Since the backend can change between iterative builds we need to clean up after ourselves - CleanupStaleLinuxArtifacts(logger, buildOutputDir); + CleanupStaleLinuxArtifacts(logger, buildOutputDir, GetLinuxArchitecture(executablePath)); } foreach (var artifact in GetNativePluginArtifact(target, options, executablePath, buildOutputDir)) @@ -157,6 +157,37 @@ internal static string GetWindowsBackendDir(BuildTarget target, SentryUnityOptio return Path.GetFullPath($"Packages/{SentryPackageInfo.GetName()}/Plugins/Windows/{backend}/{arch}"); } + // Linux ships x86_64 and arm64, each in a subfolder of the same name. Both build for `StandaloneLinux64`, + // so the architecture is read from the built player's ELF header. + internal static string GetLinuxBackendDir(SentryUnityOptions options, string arch) + { + var backend = options.Experimental.LinuxBackend == LinuxBackend.Native ? "SentryNative~" : "Sentry~"; + return Path.GetFullPath($"Packages/{SentryPackageInfo.GetName()}/Plugins/Linux/{backend}/{arch}"); + } + + private const ushort ElfMachineAarch64 = 0xB7; + + internal static string GetLinuxArchitecture(string executablePath) + { + if (!File.Exists(executablePath)) + { + return "x86_64"; + } + + // `e_machine` is the 16-bit little-endian field at offset 18 of the ELF header. + var header = new byte[20]; + using (var stream = File.OpenRead(executablePath)) + { + if (stream.Read(header, 0, header.Length) != header.Length) + { + return "x86_64"; + } + } + + var machine = header[18] | (header[19] << 8); + return machine == ElfMachineAarch64 ? "arm64" : "x86_64"; + } + private readonly struct NativePluginArtifact(string source, string destination, bool isExecutable = false) { public readonly string Source = source; @@ -214,9 +245,8 @@ private static IEnumerable GetNativePluginArtifact( break; case BuildTarget.StandaloneLinux64: - var linuxBackendSourcePath = options.Experimental.LinuxBackend == LinuxBackend.Native - ? Path.Combine(pluginsPath, "Linux", "SentryNative~") - : Path.Combine(pluginsPath, "Linux", "Sentry~"); + var linuxArch = GetLinuxArchitecture(executablePath); + var linuxBackendSourcePath = GetLinuxBackendDir(options, linuxArch); if (!Directory.Exists(linuxBackendSourcePath)) { var buildTarget = options.Experimental.LinuxBackend == LinuxBackend.Native ? "BuildLinuxNativeSDK" : "BuildLinuxSDK"; @@ -226,7 +256,7 @@ private static IEnumerable GetNativePluginArtifact( } // 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); + var linuxPluginDir = GetLinuxPluginDir(buildOutputDir, linuxArch); foreach (var file in Directory.GetFiles(linuxBackendSourcePath)) { var name = Path.GetFileName(file); @@ -309,9 +339,9 @@ private static void CleanupStaleWindowsArtifacts(IDiagnosticLogger logger, strin } } - // Unity places Linux native plugins under _Data/Plugins/x86_64, which the player adds + // Unity places Linux native plugins under _Data/Plugins/, which the player adds // to its dlopen search path. We resolve the data dir by globbing (the player name isn't known here). - private static string GetLinuxPluginDir(string buildOutputDir) + private static string GetLinuxPluginDir(string buildOutputDir, string arch) { var dataDir = Directory.GetDirectories(buildOutputDir, "*_Data").FirstOrDefault(); if (dataDir is null) @@ -320,20 +350,20 @@ private static string GetLinuxPluginDir(string buildOutputDir) $"Could not locate the player '*_Data' directory under '{buildOutputDir}' to place the Sentry native plugin."); } - return Path.Combine(dataDir, "Plugins", "x86_64"); + return Path.Combine(dataDir, "Plugins", arch); } // Switching Linux backends between iterative builds leaves the other backend's artifacts behind // (a stale sentry-crash next to the player, or the other backend's libsentry.so in the plugin dir). // Wipe them before copying the current backend's files in. - private static void CleanupStaleLinuxArtifacts(IDiagnosticLogger logger, string buildOutputDir) + private static void CleanupStaleLinuxArtifacts(IDiagnosticLogger logger, string buildOutputDir, string arch) { var stalePaths = new List { Path.Combine(buildOutputDir, "sentry-crash") }; var dataDir = Directory.GetDirectories(buildOutputDir, "*_Data").FirstOrDefault(); if (dataDir is not null) { - stalePaths.Add(Path.Combine(dataDir, "Plugins", "x86_64", "libsentry.so")); - stalePaths.Add(Path.Combine(dataDir, "Plugins", "x86_64", LinuxLibraryName)); + stalePaths.Add(Path.Combine(dataDir, "Plugins", arch, "libsentry.so")); + stalePaths.Add(Path.Combine(dataDir, "Plugins", arch, LinuxLibraryName)); } foreach (var stale in stalePaths) @@ -454,11 +484,8 @@ private static void UploadDebugSymbols(IDiagnosticLogger logger, BuildTarget tar // Sentry native SDK symbols from package. // Glob the debug sidecars (libsentry.dbg.so, and sentry-crash.dbg for the native backend) // from whichever backend's source dir is in use. - var linuxBackendDir = options.Experimental.LinuxBackend == LinuxBackend.Native - ? "SentryNative~" - : "Sentry~"; - var linuxSymbolDir = Path.GetFullPath( - $"Packages/{SentryPackageInfo.GetName()}/Plugins/Linux/{linuxBackendDir}"); + var linuxSymbolDir = GetLinuxBackendDir(options, + GetLinuxArchitecture(Path.Combine(buildOutputDir, executableName))); if (Directory.Exists(linuxSymbolDir)) { foreach (var file in Directory.GetFiles(linuxSymbolDir)) diff --git a/test/Scripts.Tests/package-release.zip.snapshot b/test/Scripts.Tests/package-release.zip.snapshot index 5d884886c..2136680dc 100644 --- a/test/Scripts.Tests/package-release.zip.snapshot +++ b/test/Scripts.Tests/package-release.zip.snapshot @@ -267,12 +267,20 @@ Plugins/iOS/SentryObjC.xcframework~/ios-arm64/dSYMs/SentryObjC.framework.dSYM/Co Plugins/iOS/SentryObjC.xcframework~/ios-arm64/dSYMs/SentryObjC.framework.dSYM/Contents/Resources/Relocations/ 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-native.so -Plugins/Linux/SentryNative~/libsentry.dbg.so -Plugins/Linux/SentryNative~/sentry-crash -Plugins/Linux/SentryNative~/sentry-crash.dbg -Plugins/Linux/Sentry~/libsentry-native.so -Plugins/Linux/Sentry~/libsentry.dbg.so +Plugins/Linux/Sentry~/ +Plugins/Linux/SentryNative~/ +Plugins/Linux/SentryNative~/arm64/libsentry-native.so +Plugins/Linux/SentryNative~/arm64/libsentry.dbg.so +Plugins/Linux/SentryNative~/arm64/sentry-crash +Plugins/Linux/SentryNative~/arm64/sentry-crash.dbg +Plugins/Linux/SentryNative~/x86_64/libsentry-native.so +Plugins/Linux/SentryNative~/x86_64/libsentry.dbg.so +Plugins/Linux/SentryNative~/x86_64/sentry-crash +Plugins/Linux/SentryNative~/x86_64/sentry-crash.dbg +Plugins/Linux/Sentry~/arm64/libsentry-native.so +Plugins/Linux/Sentry~/arm64/libsentry.dbg.so +Plugins/Linux/Sentry~/x86_64/libsentry-native.so +Plugins/Linux/Sentry~/x86_64/libsentry.dbg.so Plugins/Android/proguard-sentry-unity.pro Plugins/Android/proguard-sentry-unity.pro.meta Plugins/Android/Sentry~/epitaph.jar diff --git a/test/Sentry.Unity.Editor.Tests/Native/BuildPostProcessTests.cs b/test/Sentry.Unity.Editor.Tests/Native/BuildPostProcessTests.cs index c8817cfbc..eacaeac65 100644 --- a/test/Sentry.Unity.Editor.Tests/Native/BuildPostProcessTests.cs +++ b/test/Sentry.Unity.Editor.Tests/Native/BuildPostProcessTests.cs @@ -97,4 +97,42 @@ public void GetWindowsBackendDir_ResolvesBackendAndArchitecture( Assert.AreEqual(expectedArchDir, Path.GetFileName(actual)); Assert.AreEqual(expectedBackendDir, Path.GetFileName(Path.GetDirectoryName(actual))); } + + [TestCase(LinuxBackend.Breakpad, "Sentry~", "x86_64")] + [TestCase(LinuxBackend.Breakpad, "Sentry~", "arm64")] + [TestCase(LinuxBackend.Native, "SentryNative~", "x86_64")] + [TestCase(LinuxBackend.Native, "SentryNative~", "arm64")] + public void GetLinuxBackendDir_ResolvesBackendAndArchitecture( + LinuxBackend backend, string expectedBackendDir, string arch) + { + var options = new SentryUnityOptions(); + options.Experimental.LinuxBackend = backend; + + var actual = BuildPostProcess.GetLinuxBackendDir(options, arch); + + Assert.AreEqual(arch, Path.GetFileName(actual)); + Assert.AreEqual(expectedBackendDir, Path.GetFileName(Path.GetDirectoryName(actual))); + } + + [TestCase((byte)0x3E, "x86_64")] + [TestCase((byte)0xB7, "arm64")] + public void GetLinuxArchitecture_ReadsElfMachine(byte machine, string expectedArch) + { + var executable = Path.Combine(_testDir, "player"); + var header = new byte[64]; + header[0] = 0x7F; + header[1] = (byte)'E'; + header[2] = (byte)'L'; + header[3] = (byte)'F'; + header[18] = machine; + File.WriteAllBytes(executable, header); + + Assert.AreEqual(expectedArch, BuildPostProcess.GetLinuxArchitecture(executable)); + } + + [Test] + public void GetLinuxArchitecture_MissingExecutable_DefaultsToX86_64() + { + Assert.AreEqual("x86_64", BuildPostProcess.GetLinuxArchitecture(Path.Combine(_testDir, "missing"))); + } } From a9a6d1912c44b76a6358b3b6f796b428032f5c96 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Thu, 22 Oct 2026 12:53:48 +0200 Subject: [PATCH 02/16] Link changelog entry to PR --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 983c5fed2..d1a0d3817 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Features -- The SDK now supports Linux ARM64 dedicated servers (Unity 6000.3 and newer, IL2CPP). The native SDK for both backends ships for `x86_64` and `arm64`, and the build picks the one matching the player. ([#2466](https://github.com/getsentry/sentry-unity/issues/2466)) +- The SDK now supports Linux ARM64 dedicated servers (Unity 6000.3 and newer, IL2CPP). The native SDK for both backends ships for `x86_64` and `arm64`, and the build picks the one matching the player. ([#2894](https://github.com/getsentry/sentry-unity/pull/2894)) ## 4.11.0 From 5bc2ddb4b61a33b20a170e48c9f8bf1ea24ab2ff Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 23 Oct 2026 16:31:27 +0200 Subject: [PATCH 03/16] Skip the va_list copy on Linux arm64 --- src/Sentry.Unity.Native/SentryNativeBridge.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Sentry.Unity.Native/SentryNativeBridge.cs b/src/Sentry.Unity.Native/SentryNativeBridge.cs index df451069e..f09ddc14c 100644 --- a/src/Sentry.Unity.Native/SentryNativeBridge.cs +++ b/src/Sentry.Unity.Native/SentryNativeBridge.cs @@ -27,13 +27,12 @@ public static bool Init(SentryUnityOptions options) // Only x86-64 System V platforms need the `VaListLinux64` copy in `nativeLogImpl`. There // `va_list` is `__va_list_tag[1]` - an array that decays to a pointer - so `vsnprintf` // consumes the caller's list and it cannot be handed over twice. On AArch64 (Android, iOS, - // Switch, Switch 2, Apple silicon) `va_list` is a struct that is copied by value at every - // call, so the pointer can be reused as-is. - UseLibC = Application.platform - is RuntimePlatform.LinuxPlayer or RuntimePlatform.LinuxServer - or RuntimePlatform.PS5; + // Switch, Switch 2, Apple silicon, Linux arm64) `va_list` is a struct that is copied by + // value at every call, so the pointer can be reused as-is. + UseLibC = Application.platform is RuntimePlatform.PS5; if ((Application.platform - is RuntimePlatform.OSXPlayer or RuntimePlatform.OSXServer) + is RuntimePlatform.LinuxPlayer or RuntimePlatform.LinuxServer + or RuntimePlatform.OSXPlayer or RuntimePlatform.OSXServer) && RuntimeInformation.ProcessArchitecture == Architecture.X64) { UseLibC = true; From 7d8982eeb5feaaee574ad92fe95b82b2ae42a27c Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 23 Oct 2026 16:31:27 +0200 Subject: [PATCH 04/16] Build the Linux x86_64 SDK on Ubuntu 22.04 --- .github/workflows/ci.yml | 6 +++--- CHANGELOG.md | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3db421f6a..0d9af56ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,11 +39,11 @@ jobs: include: - target: Android host: ubuntu-latest + # 22.04 keeps the glibc floor low for server distros. - target: Linux - host: ubuntu-latest + host: ubuntu-22.04 - target: LinuxNative - host: ubuntu-latest - # 22.04 keeps the glibc floor low for server distros. + host: ubuntu-22.04 - target: Linux arch: Arm64 host: ubuntu-22.04-arm diff --git a/CHANGELOG.md b/CHANGELOG.md index d1a0d3817..1a3ac3e5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ - The SDK now supports Linux ARM64 dedicated servers (Unity 6000.3 and newer, IL2CPP). The native SDK for both backends ships for `x86_64` and `arm64`, and the build picks the one matching the player. ([#2894](https://github.com/getsentry/sentry-unity/pull/2894)) +### Fixes + +- The Linux native SDK now loads on distros with older glibc versions, such as Ubuntu 22.04 and Debian 12. ([#2894](https://github.com/getsentry/sentry-unity/pull/2894)) + ## 4.11.0 ### Fixes From 6645a377a9fec285cec2a86b61d83ccb204a37ed Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 23 Oct 2026 16:41:37 +0200 Subject: [PATCH 05/16] Explain the Ubuntu 22.04 pin for Linux SDK builds --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0d9af56ee..ba797262e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,7 +39,9 @@ jobs: include: - target: Android host: ubuntu-latest - # 22.04 keeps the glibc floor low for server distros. + # Pinned to 22.04: the Linux libs link against the build host's glibc. From 2.38 on, + # its headers redirect strtol & co. to `__isoc23_*` symbols, so building on newer hosts + # requires glibc 2.38 at runtime and breaks Ubuntu 22.04, Debian 12 and RHEL 9 servers. - target: Linux host: ubuntu-22.04 - target: LinuxNative From 2cf3b95f41c5aaa40c4d26592d5e74dbebfe41a4 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 23 Oct 2026 16:42:18 +0200 Subject: [PATCH 06/16] Shorten the Ubuntu 22.04 pin comment --- .github/workflows/ci.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ba797262e..69f4abd85 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,9 +39,7 @@ jobs: include: - target: Android host: ubuntu-latest - # Pinned to 22.04: the Linux libs link against the build host's glibc. From 2.38 on, - # its headers redirect strtol & co. to `__isoc23_*` symbols, so building on newer hosts - # requires glibc 2.38 at runtime and breaks Ubuntu 22.04, Debian 12 and RHEL 9 servers. + # 22.04 keeps the required glibc version low. - target: Linux host: ubuntu-22.04 - target: LinuxNative From 49bfa7b865bb8ff923ebd2d2145e79a52997b3f9 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 23 Oct 2026 18:05:32 +0200 Subject: [PATCH 07/16] Read the Linux player architecture once so copy, cleanup and symbol upload agree --- .../Native/BuildPostProcess.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Sentry.Unity.Editor/Native/BuildPostProcess.cs b/src/Sentry.Unity.Editor/Native/BuildPostProcess.cs index d12ff8345..68da45c88 100644 --- a/src/Sentry.Unity.Editor/Native/BuildPostProcess.cs +++ b/src/Sentry.Unity.Editor/Native/BuildPostProcess.cs @@ -84,7 +84,9 @@ _ when targetGroup.IsSwitch2() => Path.GetDirectoryName(executablePath), _ => string.Empty }; - UploadDebugSymbols(logger, target, buildOutputDir, executableName, options, cliOptions, isMono); + var linuxArch = target == BuildTarget.StandaloneLinux64 ? GetLinuxArchitecture(executablePath) : string.Empty; + + UploadDebugSymbols(logger, target, buildOutputDir, executableName, linuxArch, options, cliOptions, isMono); if (!IsEnabledForPlatform(target, options)) { @@ -108,10 +110,10 @@ _ when targetGroup.IsSwitch2() => Path.GetDirectoryName(executablePath), else if (target == BuildTarget.StandaloneLinux64) { // Since the backend can change between iterative builds we need to clean up after ourselves - CleanupStaleLinuxArtifacts(logger, buildOutputDir, GetLinuxArchitecture(executablePath)); + CleanupStaleLinuxArtifacts(logger, buildOutputDir, linuxArch); } - foreach (var artifact in GetNativePluginArtifact(target, options, executablePath, buildOutputDir)) + foreach (var artifact in GetNativePluginArtifact(target, options, executablePath, buildOutputDir, linuxArch)) { _ = Directory.CreateDirectory(Path.GetDirectoryName(artifact.Destination)); logger.LogDebug("Copying '{0}' to '{1}'", artifact.Source, artifact.Destination); @@ -196,7 +198,7 @@ private readonly struct NativePluginArtifact(string source, string destination, } private static IEnumerable GetNativePluginArtifact( - BuildTarget target, SentryUnityOptions options, string executablePath, string buildOutputDir) + BuildTarget target, SentryUnityOptions options, string executablePath, string buildOutputDir, string linuxArch) { var pluginsPath = Path.GetFullPath($"Packages/{SentryPackageInfo.GetName()}/Plugins"); @@ -245,7 +247,6 @@ private static IEnumerable GetNativePluginArtifact( break; case BuildTarget.StandaloneLinux64: - var linuxArch = GetLinuxArchitecture(executablePath); var linuxBackendSourcePath = GetLinuxBackendDir(options, linuxArch); if (!Directory.Exists(linuxBackendSourcePath)) { @@ -394,7 +395,7 @@ internal static void AddPath(List paths, string path, IDiagnosticLogger } private static void UploadDebugSymbols(IDiagnosticLogger logger, BuildTarget target, string buildOutputDir, - string executableName, SentryUnityOptions options, SentryCliOptions? cliOptions, bool isMono) + string executableName, string linuxArch, SentryUnityOptions options, SentryCliOptions? cliOptions, bool isMono) { var projectDir = Directory.GetParent(Application.dataPath)?.FullName ?? ""; if (cliOptions?.IsValid(logger, EditorUserBuildSettings.development) is not true) @@ -484,8 +485,7 @@ private static void UploadDebugSymbols(IDiagnosticLogger logger, BuildTarget tar // Sentry native SDK symbols from package. // Glob the debug sidecars (libsentry.dbg.so, and sentry-crash.dbg for the native backend) // from whichever backend's source dir is in use. - var linuxSymbolDir = GetLinuxBackendDir(options, - GetLinuxArchitecture(Path.Combine(buildOutputDir, executableName))); + var linuxSymbolDir = GetLinuxBackendDir(options, linuxArch); if (Directory.Exists(linuxSymbolDir)) { foreach (var file in Directory.GetFiles(linuxSymbolDir)) From 30f5b602c148d5a1d724d89a549774d042fa5126 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 23 Oct 2026 18:05:32 +0200 Subject: [PATCH 08/16] Fall back to the branch run when a downloaded native SDK lacks the expected layout --- scripts/download-native-sdks.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/download-native-sdks.ps1 b/scripts/download-native-sdks.ps1 index 1aca7f38c..bbae49a72 100644 --- a/scripts/download-native-sdks.ps1 +++ b/scripts/download-native-sdks.ps1 @@ -211,11 +211,11 @@ if ($currentBranch -and $currentBranch -ne 'main' -and $currentBranch -ne 'HEAD' $failed = @() foreach ($sdk in $sdksToDownload) { Write-Host "Downloading $($sdk.Name) SDK..." -ForegroundColor Yellow - if (Try-DownloadSDK -Name $sdk.Name -Destination $sdk.Destination -RunId $mainRunId) { + if ((Try-DownloadSDK -Name $sdk.Name -Destination $sdk.Destination -RunId $mainRunId) -and (Test-SDKPresent $sdk)) { Write-Host " Downloaded from main run $mainRunId" -ForegroundColor Green continue } - if ($branchRunId -and (Try-DownloadSDK -Name $sdk.Name -Destination $sdk.Destination -RunId $branchRunId)) { + if ($branchRunId -and (Try-DownloadSDK -Name $sdk.Name -Destination $sdk.Destination -RunId $branchRunId) -and (Test-SDKPresent $sdk)) { Write-Host " Not on main; downloaded from branch '$currentBranch' run $branchRunId" -ForegroundColor Yellow continue } From cc2f4fe438acc02efe6d0959fb14af2f9d3ec356 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 23 Oct 2026 20:08:16 +0200 Subject: [PATCH 09/16] Copy the va_list before each vsnprintf call on Linux arm64 --- src/Sentry.Unity.Native/SentryNativeBridge.cs | 64 +++++++++++++------ 1 file changed, 45 insertions(+), 19 deletions(-) diff --git a/src/Sentry.Unity.Native/SentryNativeBridge.cs b/src/Sentry.Unity.Native/SentryNativeBridge.cs index f09ddc14c..095edad80 100644 --- a/src/Sentry.Unity.Native/SentryNativeBridge.cs +++ b/src/Sentry.Unity.Native/SentryNativeBridge.cs @@ -18,17 +18,17 @@ internal static class SentryNativeBridge private static IDiagnosticLogger? Logger; // This is also the logger we're forwarding native messages to. private static bool UseLibC; + private static bool IsLinuxArm64; private static bool IsWindows; public static bool Init(SentryUnityOptions options) { Logger = options.DiagnosticLogger; - // Only x86-64 System V platforms need the `VaListLinux64` copy in `nativeLogImpl`. There - // `va_list` is `__va_list_tag[1]` - an array that decays to a pointer - so `vsnprintf` - // consumes the caller's list and it cannot be handed over twice. On AArch64 (Android, iOS, - // Switch, Switch 2, Apple silicon, Linux arm64) `va_list` is a struct that is copied by - // value at every call, so the pointer can be reused as-is. + // `nativeLogImpl` hands the `va_list` to `vsnprintf` twice. On x86-64 System V it is + // `__va_list_tag[1]` and on Linux AArch64 a 32-byte struct passed by reference, so `vsnprintf` + // may consume the caller's list and each call needs a fresh copy. On Windows and Apple + // silicon it is a plain pointer, and Switch copies it through `vsnprintf_sentry`. UseLibC = Application.platform is RuntimePlatform.PS5; if ((Application.platform is RuntimePlatform.LinuxPlayer or RuntimePlatform.LinuxServer @@ -38,6 +38,9 @@ is RuntimePlatform.LinuxPlayer or RuntimePlatform.LinuxServer UseLibC = true; } + IsLinuxArm64 = (Application.platform is RuntimePlatform.LinuxPlayer or RuntimePlatform.LinuxServer) + && RuntimeInformation.ProcessArchitecture == Architecture.Arm64; + IsWindows = Application.platform is RuntimePlatform.WindowsPlayer or RuntimePlatform.WindowsServer or RuntimePlatform.GameCoreXboxSeries or RuntimePlatform.GameCoreXboxOne; @@ -278,20 +281,11 @@ private static void nativeLogImpl(int cLevel, IntPtr format, IntPtr args, IntPtr // For Linux and PlayStation, we must make a copy of the VaList to be able to pass it back... if (UseLibC) { - var argsStruct = Marshal.PtrToStructure(args); - var formattedLength = 0; - - WithMarshalledStruct(argsStruct, argsPtr => - { - formattedLength = 1 + vsnprintf(IntPtr.Zero, UIntPtr.Zero, format, argsPtr); - }); - - WithAllocatedPtr(formattedLength, buffer => - WithMarshalledStruct(argsStruct, argsPtr => - { - vsnprintf(buffer, (UIntPtr)formattedLength, format, argsPtr); - message = Marshal.PtrToStringAnsi(buffer); - })); + message = FormatWithCopiedVaList(format, args); + } + else if (IsLinuxArm64) + { + message = FormatWithCopiedVaList(format, args); } else { @@ -353,6 +347,38 @@ private struct VaListLinux64 IntPtr reg_save_area; } + // AAPCS64 `va_list` + [StructLayout(LayoutKind.Sequential)] + private struct VaListAarch64 + { + IntPtr stack; + IntPtr gr_top; + IntPtr vr_top; + int gr_offs; + int vr_offs; + } + + private static string? FormatWithCopiedVaList(IntPtr format, IntPtr args) where T : struct + { + var argsStruct = Marshal.PtrToStructure(args); + var formattedLength = 0; + string? message = null; + + WithMarshalledStruct(argsStruct, argsPtr => + { + formattedLength = 1 + vsnprintf(IntPtr.Zero, UIntPtr.Zero, format, argsPtr); + }); + + WithAllocatedPtr(formattedLength, buffer => + WithMarshalledStruct(argsStruct, argsPtr => + { + vsnprintf(buffer, (UIntPtr)formattedLength, format, argsPtr); + message = Marshal.PtrToStringAnsi(buffer); + })); + + return message; + } + private static void WithAllocatedPtr(int size, Action action) { var ptr = IntPtr.Zero; From 0283c9eb816c9b6766556479e9331d31f30c23e9 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Tue, 27 Oct 2026 17:04:16 +0100 Subject: [PATCH 10/16] Add Linux ARM64 server integration test and use Unity's ARM64 plugin folder --- .github/workflows/ci.yml | 29 ++++ .github/workflows/create-unity-matrix.yml | 7 +- .github/workflows/test-build-linux-arm64.yml | 127 ++++++++++++++++++ .github/workflows/test-run-desktop.yml | 7 +- build/native-sdks.targets | 4 +- docs/agent-guides/platform-native.md | 2 +- scripts/download-native-sdks.ps1 | 4 +- .../Native/BuildPostProcess.cs | 8 +- .../Editor/Builder.cs | 46 ++++++- test/Scripts.Integration.Test/globals.ps1 | 1 + .../package-release.zip.snapshot | 12 +- .../Native/BuildPostProcessTests.cs | 6 +- 12 files changed, 225 insertions(+), 28 deletions(-) create mode 100644 .github/workflows/test-build-linux-arm64.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 69f4abd85..3956c0b91 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -456,6 +456,21 @@ jobs: with: unity-version: ${{ fromJSON(needs.create-unity-matrix.outputs.unity-matrix).unity-version[0] }} + test-build-linux-arm64: + # Unity ships an arm64 Linux player only as an IL2CPP dedicated server, from 6000.3 on. One version, + # Breakpad backend only: the x86_64 matrix already covers the version spread and the native backend. + # The version is the newest matrix entry, so Unity-bump PRs for older versions skip it. + name: Build Linux Server arm64 ${{ needs.create-unity-matrix.outputs.unity-latest }} Integration Test + if: ${{ !startsWith(github.ref, 'refs/heads/release/') && fromJSON(needs.create-unity-matrix.outputs.unity-latest) >= 6000.3 }} + 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-linux-arm64.yml + with: + unity-version: ${{ needs.create-unity-matrix.outputs.unity-latest }} + 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 @@ -525,6 +540,20 @@ jobs: platform: windows backend: ${{ matrix.backend }} + test-run-linux-arm64: + name: Run Linux arm64 breakpad ${{ needs.create-unity-matrix.outputs.unity-latest }} Integration Test + if: ${{ !startsWith(github.ref, 'refs/heads/release/') && fromJSON(needs.create-unity-matrix.outputs.unity-latest) >= 6000.3 }} + needs: [test-build-linux-arm64, 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: ${{ needs.create-unity-matrix.outputs.unity-latest }} + platform: linux + architecture: arm64 + backend: breakpad + test-run-windows-x86: name: Run Windows x86 native ${{ fromJSON(needs.create-unity-matrix.outputs.unity-matrix).unity-version[0] }} Integration Test if: ${{ !startsWith(github.ref, 'refs/heads/release/') }} diff --git a/.github/workflows/create-unity-matrix.yml b/.github/workflows/create-unity-matrix.yml index 2ad5eb93d..d437a84e6 100644 --- a/.github/workflows/create-unity-matrix.yml +++ b/.github/workflows/create-unity-matrix.yml @@ -16,6 +16,9 @@ on: unity-matrix: description: 'Unity version matrix' value: ${{ jobs.create-unity-matrix.outputs.unity-matrix }} + unity-latest: + description: 'Newest Unity version in the matrix' + value: ${{ jobs.create-unity-matrix.outputs.unity-latest }} env: # Unity versions used in PRs @@ -28,6 +31,7 @@ jobs: runs-on: ubuntu-latest outputs: unity-matrix: ${{ steps.set-matrix.outputs.matrix }} + unity-latest: ${{ steps.set-matrix.outputs.latest }} steps: - name: Set Unity version matrices based on event type id: set-matrix @@ -49,4 +53,5 @@ jobs: fi # Unity version matrix - echo "matrix={\"unity-version\":$versions}" >> $GITHUB_OUTPUT \ No newline at end of file + echo "matrix={\"unity-version\":$versions}" >> $GITHUB_OUTPUT + echo "latest=$(jq -r '.[-1]' <<< "$versions")" >> $GITHUB_OUTPUT \ No newline at end of file diff --git a/.github/workflows/test-build-linux-arm64.yml b/.github/workflows/test-build-linux-arm64.yml new file mode 100644 index 000000000..3bebf0952 --- /dev/null +++ b/.github/workflows/test-build-linux-arm64.yml @@ -0,0 +1,127 @@ +name: "Test: Build Linux Server arm64" +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: Linux Server arm64 ${{ inputs.unity-version }} + # The GameCI images are x86_64-only and lack the arm64 server player, so this cross-compiles + # on macOS with the Unity CLI install instead. + runs-on: macos-latest + env: + UNITY_VERSION: ${{ inputs.unity-version }} + BUILD_PLATFORM: Linux-Server-arm64 + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Unity + run: ./scripts/setup-unity.ps1 -UnityVersion "${{ inputs.unity-version }}" -Modules linux-server + 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-linux-arm64-${{ env.UNITY_VERSION }}-${{ github.run_id }} + restore-keys: | + it-library-linux-arm64-${{ 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 Linux + env: + SENTRY_DSN: ${{ secrets.SENTRY_TEST_DSN }} + + # Breakpad backend only. The point of this job is that an arm64 player picks up and loads the + # arm64 sentry-native at all; the x86_64 matrix covers the native backend. + - name: Build with Sentry SDK + run: ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform Linux-Server-arm64 -UnityVersion "$env:UNITY_VERSION" + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + + - name: Assert the player is arm64 + run: | + $info = file samples/IntegrationTest/Build/test + Write-Host $info + if ($info -notmatch 'aarch64') { throw "Expected an arm64 player" } + + - 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 }}-linux-arm64-breakpad + 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-linux-arm64-${{ 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-linux-arm64-${{ 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 4cf4f7e24..7c13cf380 100644 --- a/.github/workflows/test-run-desktop.yml +++ b/.github/workflows/test-run-desktop.yml @@ -18,7 +18,7 @@ on: required: false type: string default: "" - description: "Only set for non-default architectures, e.g. x86 for the 32-bit Windows player." + description: "Only set for non-default architectures, e.g. x86 for the 32-bit Windows player or arm64 for the Linux server." scripting: required: false type: string @@ -37,7 +37,7 @@ defaults: jobs: run: name: ${{ inputs.platform }}${{ inputs.architecture && format(' {0}', inputs.architecture) || '' }}${{ 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' }} + runs-on: ${{ inputs.platform == 'linux' && (inputs.architecture == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest') || inputs.platform == 'macos' && 'macos-latest' || 'windows-latest' }} env: SENTRY_DSN: ${{ secrets.SENTRY_TEST_DSN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} @@ -71,8 +71,9 @@ jobs: - name: Run Integration Tests (Linux) if: inputs.platform == 'linux' timeout-minutes: 20 + # The arm64 player is a headless dedicated server and needs no display. run: | - xvfb-run pwsh -Command ' + ${{ inputs.architecture != 'arm64' && 'xvfb-run' || '' }} pwsh -Command ' $env:SENTRY_TEST_PLATFORM = "Desktop"; $env:SENTRY_TEST_APP = "samples/IntegrationTest/Build/test"; Invoke-Pester -Path test/IntegrationTest/Integration.Tests.ps1 -CI' diff --git a/build/native-sdks.targets b/build/native-sdks.targets index ec4577c09..f15748bd4 100644 --- a/build/native-sdks.targets +++ b/build/native-sdks.targets @@ -15,7 +15,7 @@ $(RepoRoot)modules/sentry-native/ - arm64 + ARM64 x86_64 $(SentryArtifactsDestination)Linux/Sentry~/$(SentryLinuxArch)/ $(SentryArtifactsDestination)Linux/SentryNative~/$(SentryLinuxArch)/ @@ -49,7 +49,7 @@ diff --git a/docs/agent-guides/platform-native.md b/docs/agent-guides/platform-native.md index d8a006079..2bfe42065 100644 --- a/docs/agent-guides/platform-native.md +++ b/docs/agent-guides/platform-native.md @@ -27,7 +27,7 @@ Experimental native modes raise minimum shutdown timeout to 10 seconds. - Windows: runtime files beside player `.exe`; the library lands as `sentry-native.dll`. Both architectures ship, under `x86/` and `x64/` inside each backend folder, because Unity still builds a 32-bit standalone player. `BuildTarget.StandaloneWindows` picks `x86/`. -- Linux: `libsentry-native.so` under `_Data/Plugins/` (`x86_64` or `arm64`, read from the player's ELF header); native daemon beside executable. +- Linux: `libsentry-native.so` under `_Data/Plugins/` (`x86_64` or `ARM64`, read from the player's ELF header); native daemon beside executable. - 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. - Placement is what sentry-native probes by default. `SentryUnityOptions.NativeHandlerPath` overrides it via `handler_path`, for handlers staged somewhere else; sidecars the handler resolves relative to itself, such as `crashpad_wer.dll`, move with it. diff --git a/scripts/download-native-sdks.ps1 b/scripts/download-native-sdks.ps1 index bbae49a72..65b35f864 100644 --- a/scripts/download-native-sdks.ps1 +++ b/scripts/download-native-sdks.ps1 @@ -37,7 +37,7 @@ $SDKs = @( @{ Name = "LinuxArm64" Destination = Join-Path $ArtifactsDestination "Linux/Sentry~" - CheckFile = "arm64/libsentry-native.so" + CheckFile = "ARM64/libsentry-native.so" }, @{ Name = "LinuxNative" @@ -47,7 +47,7 @@ $SDKs = @( @{ Name = "LinuxNativeArm64" Destination = Join-Path $ArtifactsDestination "Linux/SentryNative~" - CheckFile = "arm64/libsentry-native.so" + CheckFile = "ARM64/libsentry-native.so" }, @{ Name = "Android" diff --git a/src/Sentry.Unity.Editor/Native/BuildPostProcess.cs b/src/Sentry.Unity.Editor/Native/BuildPostProcess.cs index 68da45c88..01fb71f1d 100644 --- a/src/Sentry.Unity.Editor/Native/BuildPostProcess.cs +++ b/src/Sentry.Unity.Editor/Native/BuildPostProcess.cs @@ -159,7 +159,7 @@ internal static string GetWindowsBackendDir(BuildTarget target, SentryUnityOptio return Path.GetFullPath($"Packages/{SentryPackageInfo.GetName()}/Plugins/Windows/{backend}/{arch}"); } - // Linux ships x86_64 and arm64, each in a subfolder of the same name. Both build for `StandaloneLinux64`, + // Linux ships x86_64 and ARM64, each in a subfolder named like the player's plugin folder. Both build for `StandaloneLinux64`, // so the architecture is read from the built player's ELF header. internal static string GetLinuxBackendDir(SentryUnityOptions options, string arch) { @@ -187,7 +187,7 @@ internal static string GetLinuxArchitecture(string executablePath) } var machine = header[18] | (header[19] << 8); - return machine == ElfMachineAarch64 ? "arm64" : "x86_64"; + return machine == ElfMachineAarch64 ? "ARM64" : "x86_64"; } private readonly struct NativePluginArtifact(string source, string destination, bool isExecutable = false) @@ -340,8 +340,8 @@ private static void CleanupStaleWindowsArtifacts(IDiagnosticLogger logger, strin } } - // Unity places Linux native plugins under _Data/Plugins/, which the player adds - // to its dlopen search path. We resolve the data dir by globbing (the player name isn't known here). + // Unity places Linux native plugins under _Data/Plugins/, using the plugin importer's + // CPU names (`x86_64`, `ARM64`). We resolve the data dir by globbing (the player name isn't known here). private static string GetLinuxPluginDir(string buildOutputDir, string arch) { var dataDir = Directory.GetDirectories(buildOutputDir, "*_Data").FirstOrDefault(); diff --git a/test/Scripts.Integration.Test/Editor/Builder.cs b/test/Scripts.Integration.Test/Editor/Builder.cs index 410709fbc..f4d960614 100644 --- a/test/Scripts.Integration.Test/Editor/Builder.cs +++ b/test/Scripts.Integration.Test/Editor/Builder.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Reflection; using UnityEditor; using UnityEditor.Build; using UnityEditor.Build.Reporting; @@ -9,7 +10,8 @@ public class Builder { public static void BuildIl2CPPPlayer(BuildTarget target, BuildTargetGroup group, BuildOptions buildOptions, - string defaultBuildPath = "./Builds/", ScriptingImplementation scripting = ScriptingImplementation.IL2CPP) + string defaultBuildPath = "./Builds/", ScriptingImplementation scripting = ScriptingImplementation.IL2CPP, + StandaloneBuildSubtarget subtarget = StandaloneBuildSubtarget.Player) { Debug.Log("Builder: Starting to build"); @@ -21,9 +23,12 @@ public static void BuildIl2CPPPlayer(BuildTarget target, BuildTargetGroup group, // Make sure the configuration is right. EditorUserBuildSettings.selectedBuildTargetGroup = group; + var namedTarget = subtarget == StandaloneBuildSubtarget.Server + ? NamedBuildTarget.Server + : NamedBuildTarget.FromBuildTargetGroup(group); EditorUserBuildSettings.development = false; EditorUserBuildSettings.allowDebugging = false; - PlayerSettings.SetScriptingBackend(NamedBuildTarget.FromBuildTargetGroup(group), scripting); + PlayerSettings.SetScriptingBackend(namedTarget, scripting); // Making sure that the app keeps on running in the background. Linux CI is very unhappy with coroutines otherwise. PlayerSettings.runInBackground = true; @@ -34,7 +39,7 @@ public static void BuildIl2CPPPlayer(BuildTarget target, BuildTargetGroup group, { 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(namedTarget, UnityEditor.Build.Il2CppCodeGeneration.OptimizeSpeed); #elif UNITY_2021_2_OR_NEWER EditorUserBuildSettings.il2CppCodeGeneration = UnityEditor.Build.Il2CppCodeGeneration.OptimizeSpeed; #endif @@ -42,9 +47,9 @@ public static void BuildIl2CPPPlayer(BuildTarget target, BuildTargetGroup group, Debug.Log("Builder: Configuring code stripping level"); #if UNITY_6000_0_OR_NEWER - PlayerSettings.SetManagedStrippingLevel(NamedBuildTarget.FromBuildTargetGroup(group), ManagedStrippingLevel.High); + PlayerSettings.SetManagedStrippingLevel(namedTarget, ManagedStrippingLevel.High); #else - PlayerSettings.SetManagedStrippingLevel(NamedBuildTarget.FromBuildTargetGroup(group), ManagedStrippingLevel.Low); + PlayerSettings.SetManagedStrippingLevel(namedTarget, ManagedStrippingLevel.Low); #endif Debug.Log("Builder: Updating BuildPlayerOptions"); @@ -56,11 +61,16 @@ public static void BuildIl2CPPPlayer(BuildTarget target, BuildTargetGroup group, options = buildOptions }; + if (subtarget == StandaloneBuildSubtarget.Server) + { + buildPlayerOptions.subtarget = (int)subtarget; + } + 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 (scripting == ScriptingImplementation.IL2CPP && target != BuildTarget.StandaloneLinux64) { - PlayerSettings.SetIl2CppCompilerConfiguration(NamedBuildTarget.FromBuildTargetGroup(group), Il2CppCompilerConfiguration.Debug); + PlayerSettings.SetIl2CppCompilerConfiguration(namedTarget, Il2CppCompilerConfiguration.Debug); } if (target == BuildTarget.Android) @@ -171,6 +181,30 @@ public static void BuildLinuxIl2CPPPlayer() defaultBuildPath: "./Builds/Linux/test"); } +#if UNITY_6000_3_OR_NEWER + // Unity only ships an arm64 Linux player as a dedicated server (6000.3+, IL2CPP). The architecture + // setting lives in the Linux module's extension assembly, which only exists with that module installed. + [MenuItem("Tools/Builder/Linux Server arm64")] + public static void BuildLinuxServerArm64Il2CPPPlayer() + { + Debug.Log("Builder: Building Linux Server arm64 IL2CPP Player"); + // `UserBuildSettings.architecture` writes to the settings of the subtarget selected in the build window. + typeof(EditorUserBuildSettings).GetProperty("selectedStandaloneBuildSubtarget", BindingFlags.Static | BindingFlags.NonPublic) + ?.SetValue(null, StandaloneBuildSubtarget.Server); + + var architecture = Type.GetType("UnityEditor.LinuxStandalone.UserBuildSettings, UnityEditor.LinuxStandalone.Extensions") + ?.GetProperty("architecture") ?? throw new Exception("Linux build support is not installed."); + architecture.SetValue(null, OSArchitecture.ARM64); + if (!Equals(architecture.GetValue(null), OSArchitecture.ARM64)) + { + throw new Exception($"Failed to set the Linux architecture to ARM64, got '{architecture.GetValue(null)}'."); + } + + BuildIl2CPPPlayer(BuildTarget.StandaloneLinux64, BuildTargetGroup.Standalone, BuildOptions.StrictMode, + defaultBuildPath: "./Builds/Linux/test", subtarget: StandaloneBuildSubtarget.Server); + } +#endif + [MenuItem("Tools/Builder/Android")] public static void BuildAndroidIl2CPPPlayer() { diff --git a/test/Scripts.Integration.Test/globals.ps1 b/test/Scripts.Integration.Test/globals.ps1 index 275438f5d..d75340b4e 100644 --- a/test/Scripts.Integration.Test/globals.ps1 +++ b/test/Scripts.Integration.Test/globals.ps1 @@ -151,6 +151,7 @@ function BuildMethodFor([string] $platform) "Windows-x86" { return "Builder.BuildWindowsX86Il2CPPPlayer" } "Windows-Mono" { return "Builder.BuildWindowsMonoPlayer" } "Linux" { return "Builder.BuildLinuxIl2CPPPlayer" } + "Linux-Server-arm64" { return "Builder.BuildLinuxServerArm64Il2CPPPlayer" } "WebGL" { return "Builder.BuildWebGLPlayer" } "iOS" { return "Builder.BuildIOSProject" } "Switch" { return "Builder.BuildSwitchIL2CPPPlayer" } diff --git a/test/Scripts.Tests/package-release.zip.snapshot b/test/Scripts.Tests/package-release.zip.snapshot index 2136680dc..c07306eea 100644 --- a/test/Scripts.Tests/package-release.zip.snapshot +++ b/test/Scripts.Tests/package-release.zip.snapshot @@ -269,16 +269,16 @@ Plugins/iOS/SentryObjC.xcframework~/ios-arm64/dSYMs/SentryObjC.framework.dSYM/Co Plugins/iOS/SentryObjC.xcframework~/ios-arm64/dSYMs/SentryObjC.framework.dSYM/Contents/Resources/DWARF/SentryObjC Plugins/Linux/Sentry~/ Plugins/Linux/SentryNative~/ -Plugins/Linux/SentryNative~/arm64/libsentry-native.so -Plugins/Linux/SentryNative~/arm64/libsentry.dbg.so -Plugins/Linux/SentryNative~/arm64/sentry-crash -Plugins/Linux/SentryNative~/arm64/sentry-crash.dbg +Plugins/Linux/SentryNative~/ARM64/libsentry-native.so +Plugins/Linux/SentryNative~/ARM64/libsentry.dbg.so +Plugins/Linux/SentryNative~/ARM64/sentry-crash +Plugins/Linux/SentryNative~/ARM64/sentry-crash.dbg Plugins/Linux/SentryNative~/x86_64/libsentry-native.so Plugins/Linux/SentryNative~/x86_64/libsentry.dbg.so Plugins/Linux/SentryNative~/x86_64/sentry-crash Plugins/Linux/SentryNative~/x86_64/sentry-crash.dbg -Plugins/Linux/Sentry~/arm64/libsentry-native.so -Plugins/Linux/Sentry~/arm64/libsentry.dbg.so +Plugins/Linux/Sentry~/ARM64/libsentry-native.so +Plugins/Linux/Sentry~/ARM64/libsentry.dbg.so Plugins/Linux/Sentry~/x86_64/libsentry-native.so Plugins/Linux/Sentry~/x86_64/libsentry.dbg.so Plugins/Android/proguard-sentry-unity.pro diff --git a/test/Sentry.Unity.Editor.Tests/Native/BuildPostProcessTests.cs b/test/Sentry.Unity.Editor.Tests/Native/BuildPostProcessTests.cs index eacaeac65..19ed20141 100644 --- a/test/Sentry.Unity.Editor.Tests/Native/BuildPostProcessTests.cs +++ b/test/Sentry.Unity.Editor.Tests/Native/BuildPostProcessTests.cs @@ -99,9 +99,9 @@ public void GetWindowsBackendDir_ResolvesBackendAndArchitecture( } [TestCase(LinuxBackend.Breakpad, "Sentry~", "x86_64")] - [TestCase(LinuxBackend.Breakpad, "Sentry~", "arm64")] + [TestCase(LinuxBackend.Breakpad, "Sentry~", "ARM64")] [TestCase(LinuxBackend.Native, "SentryNative~", "x86_64")] - [TestCase(LinuxBackend.Native, "SentryNative~", "arm64")] + [TestCase(LinuxBackend.Native, "SentryNative~", "ARM64")] public void GetLinuxBackendDir_ResolvesBackendAndArchitecture( LinuxBackend backend, string expectedBackendDir, string arch) { @@ -115,7 +115,7 @@ public void GetLinuxBackendDir_ResolvesBackendAndArchitecture( } [TestCase((byte)0x3E, "x86_64")] - [TestCase((byte)0xB7, "arm64")] + [TestCase((byte)0xB7, "ARM64")] public void GetLinuxArchitecture_ReadsElfMachine(byte machine, string expectedArch) { var executable = Path.Combine(_testDir, "player"); From f3e9d14ecd9fed82bb4a5b4efec055d43f02258c Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Tue, 27 Oct 2026 18:12:06 +0100 Subject: [PATCH 11/16] Build the Linux arm64 server test on Ubuntu with the Unity CLI --- .github/workflows/test-build-linux-arm64.yml | 6 +++--- scripts/setup-unity.ps1 | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test-build-linux-arm64.yml b/.github/workflows/test-build-linux-arm64.yml index 3bebf0952..e1067fa6d 100644 --- a/.github/workflows/test-build-linux-arm64.yml +++ b/.github/workflows/test-build-linux-arm64.yml @@ -20,9 +20,9 @@ defaults: jobs: build: name: Linux Server arm64 ${{ inputs.unity-version }} - # The GameCI images are x86_64-only and lack the arm64 server player, so this cross-compiles - # on macOS with the Unity CLI install instead. - runs-on: macos-latest + # Unity has no arm64 Linux editor and the GameCI images lack the arm64 server player, so this + # cross-compiles on x86_64 with the Unity CLI install instead. + runs-on: ubuntu-latest env: UNITY_VERSION: ${{ inputs.unity-version }} BUILD_PLATFORM: Linux-Server-arm64 diff --git a/scripts/setup-unity.ps1 b/scripts/setup-unity.ps1 index 4a875311a..dc1b3b237 100644 --- a/scripts/setup-unity.ps1 +++ b/scripts/setup-unity.ps1 @@ -9,6 +9,7 @@ $PSNativeCommandUseErrorActionPreference = $true $CliVersion = '1.0.0-beta.14' $CliBinaries = @{ 'darwin-arm64' = @('unity-darwin-arm64', '367ea124c61e91dc22409699cb40d2e65d23952f3220e911ccea1f6d8a1ff37f') + 'linux-x64' = @('unity-linux-x64', 'd9a2c1716ce723b970898f7f40adc09cceee04e84ce2f1f1638f07ac3e9898aa') 'win32-x64' = @('unity-windows-x64.exe', '78761a0547cd4a9bc212e27a217c66e6a621bf48c753ba440e4b2327ee7ff8ac') } @@ -20,7 +21,7 @@ $env:UNITY_NON_INTERACTIVE = '1' $env:UNITY_NO_BANNER = '1' $env:UNITY_NO_PAGER = '1' -# Unity CLI, pinned by version and checksum. Linux CI uses the GameCI Docker images instead (scripts/ci-docker.sh). +# Unity CLI, pinned by version and checksum. $os = $IsWindows ? 'win32' : $IsMacOS ? 'darwin' : 'linux' $arch = "$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)".ToLower() $cliFile, $cliSha = $CliBinaries["$os-$arch"] @@ -51,7 +52,7 @@ if ($env:UNITY_LICENSE_SERVER_CONFIG) { New-Item -ItemType Directory -Force -Path 'C:/ProgramData/Unity/config' | Out-Null Set-Content -Path 'C:/ProgramData/Unity/config/services-config.json' -Value $env:UNITY_LICENSE_SERVER_CONFIG } else { - $unityDataDir = '/Library/Application Support/Unity' + $unityDataDir = $IsMacOS ? '/Library/Application Support/Unity' : '/usr/share/unity3d' $configFile = Join-Path $cliDir 'services-config.json' Set-Content -Path $configFile -Value $env:UNITY_LICENSE_SERVER_CONFIG sudo mkdir -p "$unityDataDir/config" From cb3ed1e25c947d43759821ff41f4c01504f14ddf Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Tue, 27 Oct 2026 21:14:13 +0100 Subject: [PATCH 12/16] Pin the Linux arm64 server build to Ubuntu 24.04 --- .github/workflows/test-build-linux-arm64.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-build-linux-arm64.yml b/.github/workflows/test-build-linux-arm64.yml index e1067fa6d..edd6b9593 100644 --- a/.github/workflows/test-build-linux-arm64.yml +++ b/.github/workflows/test-build-linux-arm64.yml @@ -21,8 +21,9 @@ jobs: build: name: Linux Server arm64 ${{ inputs.unity-version }} # Unity has no arm64 Linux editor and the GameCI images lack the arm64 server player, so this - # cross-compiles on x86_64 with the Unity CLI install instead. - runs-on: ubuntu-latest + # cross-compiles on x86_64 with the Unity CLI install instead. Pinned to 24.04: Unity's bundled + # IL2CPP linker needs libxml2.so.2, which 26.04 no longer ships. + runs-on: ubuntu-24.04 env: UNITY_VERSION: ${{ inputs.unity-version }} BUILD_PLATFORM: Linux-Server-arm64 From 5b2898309b2250db9eb849c8189e133c80458743 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Tue, 27 Oct 2026 21:47:59 +0100 Subject: [PATCH 13/16] Run Unity 6 Linux CI jobs with the Unity CLI instead of GameCI --- .github/workflows/ci.yml | 14 +++++++++---- .github/workflows/test-build-android.yml | 26 ++++++++++++++++++++++-- .github/workflows/test-build-ios.yml | 13 ++++++++++-- .github/workflows/test-build-linux.yml | 17 +++++++++++++--- .github/workflows/test-create.yml | 12 ++++++++--- scripts/ci-docker.sh | 4 ++++ 6 files changed, 72 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3956c0b91..18319d51a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -138,12 +138,12 @@ jobs: with: unity-version: ${{ matrix.unity-version }} - # A Linux, docker-based build to prepare a WebGL player. The tests run in `test-run-webgl`. + # A Linux build to prepare a WebGL player. The tests run in `test-run-webgl`. test-build-webgl: name: Build ${{ matrix.platform }} ${{ matrix.unity-version }} Test if: ${{ !startsWith(github.ref, 'refs/heads/release/') }} needs: [test-create, create-unity-matrix] - runs-on: ubuntu-latest + runs-on: ${{ startsWith(matrix.unity-version, '6000') && 'ubuntu-24.04' || 'ubuntu-latest' }} strategy: fail-fast: false matrix: @@ -152,8 +152,6 @@ jobs: include: - platform: WebGL build_platform: WebGL - env: - UNITY_PATH: docker exec unity unity-editor steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -169,6 +167,7 @@ jobs: swap-storage: true - name: Docker Login + if: ${{ !startsWith(matrix.unity-version, '6000') }} uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ghcr.io @@ -176,6 +175,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Start the Unity docker container + if: ${{ !startsWith(matrix.unity-version, '6000') }} run: ./scripts/ci-docker.sh "$UNITY_VERSION" "$PLATFORM_WITH_SUFFIX" "$UNITY_LICENSE_SERVER_CONFIG" shell: bash env: @@ -185,6 +185,12 @@ jobs: PLATFORM_WITH_SUFFIX: ${{ matrix.platform }}${{ matrix.image-suffix }} UNITY_LICENSE_SERVER_CONFIG: ${{ secrets.UNITY_LICENSE_SERVER_CONFIG }} + - name: Setup Unity + if: ${{ startsWith(matrix.unity-version, '6000') }} + run: ./scripts/setup-unity.ps1 -UnityVersion "${{ matrix.unity-version }}" -Modules webgl + env: + UNITY_LICENSE_SERVER_CONFIG: ${{ secrets.UNITY_LICENSE_SERVER_CONFIG }} + - name: Download IntegrationTest project uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: diff --git a/.github/workflows/test-build-android.yml b/.github/workflows/test-build-android.yml index 1524018ed..2794829b3 100644 --- a/.github/workflows/test-build-android.yml +++ b/.github/workflows/test-build-android.yml @@ -20,17 +20,28 @@ defaults: jobs: build: name: ${{ inputs.unity-version }} - runs-on: ubuntu-latest-4-cores + runs-on: ${{ startsWith(inputs.unity-version, '6000') && 'ubuntu-24.04' || 'ubuntu-latest-4-cores' }} env: GITHUB_ACTOR: ${{ github.actor }} - UNITY_PATH: docker exec unity unity-editor UNITY_VERSION: ${{ inputs.unity-version }} steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Free Disk Space + if: ${{ startsWith(inputs.unity-version, '6000') }} + uses: jlumbroso/free-disk-space@f68fdb76e2ea636224182cfb7377ff9a1708f9b8 # v1.3.0 + with: + android: true + dotnet: false + haskell: true + large-packages: false + docker-images: false + swap-storage: true + - name: Docker Login + if: ${{ !startsWith(inputs.unity-version, '6000') }} uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ghcr.io @@ -38,12 +49,19 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Start the Unity docker container + if: ${{ !startsWith(inputs.unity-version, '6000') }} run: ./scripts/ci-docker.sh "$UNITY_VERSION" 'android' "$UNITY_LICENSE_SERVER_CONFIG" shell: bash env: UNITY_LICENSE_SERVER_CONFIG: ${{ secrets.UNITY_LICENSE_SERVER_CONFIG }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + - name: Setup Unity + if: ${{ startsWith(inputs.unity-version, '6000') }} + run: ./scripts/setup-unity.ps1 -UnityVersion "$env:UNITY_VERSION" -Modules android + env: + UNITY_LICENSE_SERVER_CONFIG: ${{ secrets.UNITY_LICENSE_SERVER_CONFIG }} + - name: Download IntegrationTest project uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -105,6 +123,8 @@ jobs: - name: Export APK - Runtime Initialization run: ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform "Android" -UnityVersion "$env:UNITY_VERSION" + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - name: Assert symbols and sources were uploaded (Runtime) run: ./test/Scripts.Integration.Test/assert-symbol-upload.ps1 -LogPath samples/IntegrationTest/Logs/sentry-symbols-upload.log @@ -144,6 +164,8 @@ jobs: - name: Export APK - Build-Time Initialization run: ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform "Android" -UnityVersion "$env:UNITY_VERSION" + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - name: Assert symbols and sources were uploaded (Build-Time) run: ./test/Scripts.Integration.Test/assert-symbol-upload.ps1 -LogPath samples/IntegrationTest/Logs/sentry-symbols-upload.log diff --git a/.github/workflows/test-build-ios.yml b/.github/workflows/test-build-ios.yml index 9dab592ec..c69439471 100644 --- a/.github/workflows/test-build-ios.yml +++ b/.github/workflows/test-build-ios.yml @@ -20,7 +20,7 @@ defaults: jobs: build: name: ${{ inputs.unity-version }} - runs-on: ubuntu-latest + runs-on: ${{ startsWith(inputs.unity-version, '6000') && 'ubuntu-24.04' || 'ubuntu-latest' }} strategy: matrix: include: @@ -29,7 +29,6 @@ jobs: build_platform: iOS env: GITHUB_ACTOR: ${{ github.actor }} - UNITY_PATH: docker exec unity unity-editor UNITY_VERSION: ${{ inputs.unity-version }} steps: @@ -37,6 +36,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Docker Login + if: ${{ !startsWith(inputs.unity-version, '6000') }} uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ghcr.io @@ -44,12 +44,19 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Start the Unity docker container + if: ${{ !startsWith(inputs.unity-version, '6000') }} run: ./scripts/ci-docker.sh "$UNITY_VERSION" "iOS" "$UNITY_LICENSE_SERVER_CONFIG" shell: bash env: UNITY_LICENSE_SERVER_CONFIG: ${{ secrets.UNITY_LICENSE_SERVER_CONFIG }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + - name: Setup Unity + if: ${{ startsWith(inputs.unity-version, '6000') }} + run: ./scripts/setup-unity.ps1 -UnityVersion "$env:UNITY_VERSION" -Modules ios + env: + UNITY_LICENSE_SERVER_CONFIG: ${{ secrets.UNITY_LICENSE_SERVER_CONFIG }} + - name: Download IntegrationTest project uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -127,6 +134,7 @@ jobs: run: ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform "$env:BUILD_PLATFORM" -UnityVersion "$env:UNITY_VERSION" env: BUILD_PLATFORM: ${{ matrix.build_platform }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} # We create tar explicitly because upload-artifact is slow for many files. - name: Create archive (runtime initialization) @@ -155,6 +163,7 @@ jobs: run: ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform "$env:BUILD_PLATFORM" -UnityVersion "$env:UNITY_VERSION" env: BUILD_PLATFORM: ${{ matrix.build_platform }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - name: Create archive (build-time initialization) run: | diff --git a/.github/workflows/test-build-linux.yml b/.github/workflows/test-build-linux.yml index 5535dc050..369570754 100644 --- a/.github/workflows/test-build-linux.yml +++ b/.github/workflows/test-build-linux.yml @@ -20,10 +20,9 @@ defaults: jobs: build: name: Linux ${{ inputs.unity-version }} - runs-on: ubuntu-latest + runs-on: ${{ startsWith(inputs.unity-version, '6000') && 'ubuntu-24.04' || 'ubuntu-latest' }} env: GITHUB_ACTOR: ${{ github.actor }} - UNITY_PATH: docker exec unity unity-editor UNITY_VERSION: ${{ inputs.unity-version }} BUILD_PLATFORM: Linux @@ -42,6 +41,7 @@ jobs: swap-storage: true - name: Log into Docker + if: ${{ !startsWith(inputs.unity-version, '6000') }} uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ghcr.io @@ -49,12 +49,19 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Start the Unity docker container + if: ${{ !startsWith(inputs.unity-version, '6000') }} run: ./scripts/ci-docker.sh "$UNITY_VERSION" "Linux-il2cpp" "$UNITY_LICENSE_SERVER_CONFIG" shell: bash env: UNITY_LICENSE_SERVER_CONFIG: ${{ secrets.UNITY_LICENSE_SERVER_CONFIG }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + - name: Setup Unity + if: ${{ startsWith(inputs.unity-version, '6000') }} + run: ./scripts/setup-unity.ps1 -UnityVersion "$env:UNITY_VERSION" -Modules linux-il2cpp + env: + UNITY_LICENSE_SERVER_CONFIG: ${{ secrets.UNITY_LICENSE_SERVER_CONFIG }} + - name: Download IntegrationTest project uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -116,6 +123,8 @@ jobs: - name: Build with Sentry SDK (Breakpad backend) run: ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform Linux -UnityVersion "$env:UNITY_VERSION" + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - name: Assert symbols and sources were uploaded (Breakpad backend) run: ./test/Scripts.Integration.Test/assert-symbol-upload.ps1 -LogPath unity.log @@ -154,6 +163,8 @@ jobs: - name: Build with Sentry SDK (Native backend) run: ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform Linux -UnityVersion "$env:UNITY_VERSION" + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - name: Assert symbols and sources were uploaded (Native backend) run: ./test/Scripts.Integration.Test/assert-symbol-upload.ps1 -LogPath unity.log @@ -181,7 +192,7 @@ jobs: key: it-library-ubuntu-${{ env.UNITY_VERSION }}-${{ github.run_id }} - name: Docker diagnostics on failure - if: ${{ failure() }} + if: ${{ failure() && !startsWith(inputs.unity-version, '6000') }} run: | Write-Host "=== Docker container status ===" docker ps -a --filter "name=^/unity$" diff --git a/.github/workflows/test-create.yml b/.github/workflows/test-create.yml index 067df280d..a2a2872cd 100644 --- a/.github/workflows/test-create.yml +++ b/.github/workflows/test-create.yml @@ -16,10 +16,9 @@ defaults: jobs: create: name: ${{ inputs.unity-version }} - runs-on: ubuntu-latest + runs-on: ${{ startsWith(inputs.unity-version, '6000') && 'ubuntu-24.04' || 'ubuntu-latest' }} env: GITHUB_ACTOR: ${{ github.actor }} - UNITY_PATH: docker exec unity unity-editor UNITY_VERSION: ${{ inputs.unity-version }} steps: @@ -27,6 +26,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Docker Login + if: ${{ !startsWith(inputs.unity-version, '6000') }} uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ghcr.io @@ -43,12 +43,18 @@ jobs: IntegrationTest-Project-${{ env.UNITY_VERSION }}- - name: Start the Unity docker container - if: steps.cache-project.outputs.cache-hit != 'true' + if: steps.cache-project.outputs.cache-hit != 'true' && !startsWith(inputs.unity-version, '6000') run: ./scripts/ci-docker.sh "$UNITY_VERSION" 'base' "$UNITY_LICENSE_SERVER_CONFIG" shell: bash env: UNITY_LICENSE_SERVER_CONFIG: ${{ secrets.UNITY_LICENSE_SERVER_CONFIG }} + - name: Setup Unity + if: steps.cache-project.outputs.cache-hit != 'true' && startsWith(inputs.unity-version, '6000') + run: ./scripts/setup-unity.ps1 -UnityVersion "$env:UNITY_VERSION" + env: + UNITY_LICENSE_SERVER_CONFIG: ${{ secrets.UNITY_LICENSE_SERVER_CONFIG }} + - name: Create new Project if: steps.cache-project.outputs.cache-hit != 'true' run: ./test/Scripts.Integration.Test/create-project.ps1 -UnityPath "$env:UNITY_PATH" diff --git a/scripts/ci-docker.sh b/scripts/ci-docker.sh index 769647a88..f680b6c20 100755 --- a/scripts/ci-docker.sh +++ b/scripts/ci-docker.sh @@ -64,5 +64,9 @@ $suexec $container chown -R $uid /usr/share/unity3d/config/ # Unity 2021+ tries to write to this directory during asset import... $suexec $container chmod -R 755 /opt/unity/Editor/Data/UnityReferenceAssemblies/ +if [[ -n "${GITHUB_ENV:-}" ]]; then + echo "UNITY_PATH=docker exec $container unity-editor" >> "$GITHUB_ENV" +fi + echo "Container started successfully: " docker ps --filter "name=^/$container$" From fd4fd3e366a380431e101121b06582b64d93ed7b Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Tue, 27 Oct 2026 22:36:08 +0100 Subject: [PATCH 14/16] Gate the Unity CLI jobs on the Unity version number and explain the 24.04 pin --- .github/workflows/ci.yml | 9 +++++---- .github/workflows/test-build-android.yml | 11 ++++++----- .github/workflows/test-build-ios.yml | 9 +++++---- .github/workflows/test-build-linux.yml | 11 ++++++----- .github/workflows/test-create.yml | 9 +++++---- 5 files changed, 27 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 18319d51a..53db20d36 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -143,7 +143,8 @@ jobs: name: Build ${{ matrix.platform }} ${{ matrix.unity-version }} Test if: ${{ !startsWith(github.ref, 'refs/heads/release/') }} needs: [test-create, create-unity-matrix] - runs-on: ${{ startsWith(matrix.unity-version, '6000') && 'ubuntu-24.04' || 'ubuntu-latest' }} + # Unity 6 runs on the Unity CLI, on 24.04: the newest Ubuntu Unity supports (26.04 lacks libxml2.so.2 for IL2CPP). + runs-on: ${{ fromJSON(matrix.unity-version) >= 6000 && 'ubuntu-24.04' || 'ubuntu-latest' }} strategy: fail-fast: false matrix: @@ -167,7 +168,7 @@ jobs: swap-storage: true - name: Docker Login - if: ${{ !startsWith(matrix.unity-version, '6000') }} + if: ${{ fromJSON(matrix.unity-version) < 6000 }} uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ghcr.io @@ -175,7 +176,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Start the Unity docker container - if: ${{ !startsWith(matrix.unity-version, '6000') }} + if: ${{ fromJSON(matrix.unity-version) < 6000 }} run: ./scripts/ci-docker.sh "$UNITY_VERSION" "$PLATFORM_WITH_SUFFIX" "$UNITY_LICENSE_SERVER_CONFIG" shell: bash env: @@ -186,7 +187,7 @@ jobs: UNITY_LICENSE_SERVER_CONFIG: ${{ secrets.UNITY_LICENSE_SERVER_CONFIG }} - name: Setup Unity - if: ${{ startsWith(matrix.unity-version, '6000') }} + if: ${{ fromJSON(matrix.unity-version) >= 6000 }} run: ./scripts/setup-unity.ps1 -UnityVersion "${{ matrix.unity-version }}" -Modules webgl env: UNITY_LICENSE_SERVER_CONFIG: ${{ secrets.UNITY_LICENSE_SERVER_CONFIG }} diff --git a/.github/workflows/test-build-android.yml b/.github/workflows/test-build-android.yml index 2794829b3..a7160736d 100644 --- a/.github/workflows/test-build-android.yml +++ b/.github/workflows/test-build-android.yml @@ -20,7 +20,8 @@ defaults: jobs: build: name: ${{ inputs.unity-version }} - runs-on: ${{ startsWith(inputs.unity-version, '6000') && 'ubuntu-24.04' || 'ubuntu-latest-4-cores' }} + # Unity 6 runs on the Unity CLI, on 24.04: the newest Ubuntu Unity supports (26.04 lacks libxml2.so.2 for IL2CPP). + runs-on: ${{ fromJSON(inputs.unity-version) >= 6000 && 'ubuntu-24.04' || 'ubuntu-latest-4-cores' }} env: GITHUB_ACTOR: ${{ github.actor }} UNITY_VERSION: ${{ inputs.unity-version }} @@ -30,7 +31,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Free Disk Space - if: ${{ startsWith(inputs.unity-version, '6000') }} + if: ${{ fromJSON(inputs.unity-version) >= 6000 }} uses: jlumbroso/free-disk-space@f68fdb76e2ea636224182cfb7377ff9a1708f9b8 # v1.3.0 with: android: true @@ -41,7 +42,7 @@ jobs: swap-storage: true - name: Docker Login - if: ${{ !startsWith(inputs.unity-version, '6000') }} + if: ${{ fromJSON(inputs.unity-version) < 6000 }} uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ghcr.io @@ -49,7 +50,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Start the Unity docker container - if: ${{ !startsWith(inputs.unity-version, '6000') }} + if: ${{ fromJSON(inputs.unity-version) < 6000 }} run: ./scripts/ci-docker.sh "$UNITY_VERSION" 'android' "$UNITY_LICENSE_SERVER_CONFIG" shell: bash env: @@ -57,7 +58,7 @@ jobs: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - name: Setup Unity - if: ${{ startsWith(inputs.unity-version, '6000') }} + if: ${{ fromJSON(inputs.unity-version) >= 6000 }} run: ./scripts/setup-unity.ps1 -UnityVersion "$env:UNITY_VERSION" -Modules android env: UNITY_LICENSE_SERVER_CONFIG: ${{ secrets.UNITY_LICENSE_SERVER_CONFIG }} diff --git a/.github/workflows/test-build-ios.yml b/.github/workflows/test-build-ios.yml index c69439471..673fe0aa8 100644 --- a/.github/workflows/test-build-ios.yml +++ b/.github/workflows/test-build-ios.yml @@ -20,7 +20,8 @@ defaults: jobs: build: name: ${{ inputs.unity-version }} - runs-on: ${{ startsWith(inputs.unity-version, '6000') && 'ubuntu-24.04' || 'ubuntu-latest' }} + # Unity 6 runs on the Unity CLI, on 24.04: the newest Ubuntu Unity supports (26.04 lacks libxml2.so.2 for IL2CPP). + runs-on: ${{ fromJSON(inputs.unity-version) >= 6000 && 'ubuntu-24.04' || 'ubuntu-latest' }} strategy: matrix: include: @@ -36,7 +37,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Docker Login - if: ${{ !startsWith(inputs.unity-version, '6000') }} + if: ${{ fromJSON(inputs.unity-version) < 6000 }} uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ghcr.io @@ -44,7 +45,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Start the Unity docker container - if: ${{ !startsWith(inputs.unity-version, '6000') }} + if: ${{ fromJSON(inputs.unity-version) < 6000 }} run: ./scripts/ci-docker.sh "$UNITY_VERSION" "iOS" "$UNITY_LICENSE_SERVER_CONFIG" shell: bash env: @@ -52,7 +53,7 @@ jobs: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - name: Setup Unity - if: ${{ startsWith(inputs.unity-version, '6000') }} + if: ${{ fromJSON(inputs.unity-version) >= 6000 }} run: ./scripts/setup-unity.ps1 -UnityVersion "$env:UNITY_VERSION" -Modules ios env: UNITY_LICENSE_SERVER_CONFIG: ${{ secrets.UNITY_LICENSE_SERVER_CONFIG }} diff --git a/.github/workflows/test-build-linux.yml b/.github/workflows/test-build-linux.yml index 369570754..fd6f99667 100644 --- a/.github/workflows/test-build-linux.yml +++ b/.github/workflows/test-build-linux.yml @@ -20,7 +20,8 @@ defaults: jobs: build: name: Linux ${{ inputs.unity-version }} - runs-on: ${{ startsWith(inputs.unity-version, '6000') && 'ubuntu-24.04' || 'ubuntu-latest' }} + # Unity 6 runs on the Unity CLI, on 24.04: the newest Ubuntu Unity supports (26.04 lacks libxml2.so.2 for IL2CPP). + runs-on: ${{ fromJSON(inputs.unity-version) >= 6000 && 'ubuntu-24.04' || 'ubuntu-latest' }} env: GITHUB_ACTOR: ${{ github.actor }} UNITY_VERSION: ${{ inputs.unity-version }} @@ -41,7 +42,7 @@ jobs: swap-storage: true - name: Log into Docker - if: ${{ !startsWith(inputs.unity-version, '6000') }} + if: ${{ fromJSON(inputs.unity-version) < 6000 }} uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ghcr.io @@ -49,7 +50,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Start the Unity docker container - if: ${{ !startsWith(inputs.unity-version, '6000') }} + if: ${{ fromJSON(inputs.unity-version) < 6000 }} run: ./scripts/ci-docker.sh "$UNITY_VERSION" "Linux-il2cpp" "$UNITY_LICENSE_SERVER_CONFIG" shell: bash env: @@ -57,7 +58,7 @@ jobs: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - name: Setup Unity - if: ${{ startsWith(inputs.unity-version, '6000') }} + if: ${{ fromJSON(inputs.unity-version) >= 6000 }} run: ./scripts/setup-unity.ps1 -UnityVersion "$env:UNITY_VERSION" -Modules linux-il2cpp env: UNITY_LICENSE_SERVER_CONFIG: ${{ secrets.UNITY_LICENSE_SERVER_CONFIG }} @@ -192,7 +193,7 @@ jobs: key: it-library-ubuntu-${{ env.UNITY_VERSION }}-${{ github.run_id }} - name: Docker diagnostics on failure - if: ${{ failure() && !startsWith(inputs.unity-version, '6000') }} + if: ${{ failure() && fromJSON(inputs.unity-version) < 6000 }} run: | Write-Host "=== Docker container status ===" docker ps -a --filter "name=^/unity$" diff --git a/.github/workflows/test-create.yml b/.github/workflows/test-create.yml index a2a2872cd..9cf2702d6 100644 --- a/.github/workflows/test-create.yml +++ b/.github/workflows/test-create.yml @@ -16,7 +16,8 @@ defaults: jobs: create: name: ${{ inputs.unity-version }} - runs-on: ${{ startsWith(inputs.unity-version, '6000') && 'ubuntu-24.04' || 'ubuntu-latest' }} + # Unity 6 runs on the Unity CLI, on 24.04: the newest Ubuntu Unity supports (26.04 lacks libxml2.so.2 for IL2CPP). + runs-on: ${{ fromJSON(inputs.unity-version) >= 6000 && 'ubuntu-24.04' || 'ubuntu-latest' }} env: GITHUB_ACTOR: ${{ github.actor }} UNITY_VERSION: ${{ inputs.unity-version }} @@ -26,7 +27,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Docker Login - if: ${{ !startsWith(inputs.unity-version, '6000') }} + if: ${{ fromJSON(inputs.unity-version) < 6000 }} uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 with: registry: ghcr.io @@ -43,14 +44,14 @@ jobs: IntegrationTest-Project-${{ env.UNITY_VERSION }}- - name: Start the Unity docker container - if: steps.cache-project.outputs.cache-hit != 'true' && !startsWith(inputs.unity-version, '6000') + if: steps.cache-project.outputs.cache-hit != 'true' && fromJSON(inputs.unity-version) < 6000 run: ./scripts/ci-docker.sh "$UNITY_VERSION" 'base' "$UNITY_LICENSE_SERVER_CONFIG" shell: bash env: UNITY_LICENSE_SERVER_CONFIG: ${{ secrets.UNITY_LICENSE_SERVER_CONFIG }} - name: Setup Unity - if: steps.cache-project.outputs.cache-hit != 'true' && startsWith(inputs.unity-version, '6000') + if: steps.cache-project.outputs.cache-hit != 'true' && fromJSON(inputs.unity-version) >= 6000 run: ./scripts/setup-unity.ps1 -UnityVersion "$env:UNITY_VERSION" env: UNITY_LICENSE_SERVER_CONFIG: ${{ secrets.UNITY_LICENSE_SERVER_CONFIG }} From 0a5768d38e8aada122f6fe47df2302e366955e92 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Wed, 28 Oct 2026 10:07:46 +0100 Subject: [PATCH 15/16] Read the Linux architecture from the build settings and test ARM64 with the native backend --- .github/workflows/ci.yml | 6 ++-- .github/workflows/test-build-linux-arm64.yml | 11 +++++-- docs/agent-guides/platform-native.md | 2 +- .../Native/BuildPostProcess.cs | 32 ++++++------------- .../Native/BuildPostProcessTests.cs | 22 ------------- 5 files changed, 22 insertions(+), 51 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3956c0b91..0c89a1986 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -458,7 +458,7 @@ jobs: test-build-linux-arm64: # Unity ships an arm64 Linux player only as an IL2CPP dedicated server, from 6000.3 on. One version, - # Breakpad backend only: the x86_64 matrix already covers the version spread and the native backend. + # Native backend only: the x86_64 matrix already covers the version spread and Breakpad. # The version is the newest matrix entry, so Unity-bump PRs for older versions skip it. name: Build Linux Server arm64 ${{ needs.create-unity-matrix.outputs.unity-latest }} Integration Test if: ${{ !startsWith(github.ref, 'refs/heads/release/') && fromJSON(needs.create-unity-matrix.outputs.unity-latest) >= 6000.3 }} @@ -541,7 +541,7 @@ jobs: backend: ${{ matrix.backend }} test-run-linux-arm64: - name: Run Linux arm64 breakpad ${{ needs.create-unity-matrix.outputs.unity-latest }} Integration Test + name: Run Linux arm64 native ${{ needs.create-unity-matrix.outputs.unity-latest }} Integration Test if: ${{ !startsWith(github.ref, 'refs/heads/release/') && fromJSON(needs.create-unity-matrix.outputs.unity-latest) >= 6000.3 }} needs: [test-build-linux-arm64, create-unity-matrix] secrets: @@ -552,7 +552,7 @@ jobs: unity-version: ${{ needs.create-unity-matrix.outputs.unity-latest }} platform: linux architecture: arm64 - backend: breakpad + backend: native test-run-windows-x86: name: Run Windows x86 native ${{ fromJSON(needs.create-unity-matrix.outputs.unity-matrix).unity-version[0] }} Integration Test diff --git a/.github/workflows/test-build-linux-arm64.yml b/.github/workflows/test-build-linux-arm64.yml index 3bebf0952..9be9e6948 100644 --- a/.github/workflows/test-build-linux-arm64.yml +++ b/.github/workflows/test-build-linux-arm64.yml @@ -77,8 +77,13 @@ jobs: env: SENTRY_DSN: ${{ secrets.SENTRY_TEST_DSN }} - # Breakpad backend only. The point of this job is that an arm64 player picks up and loads the - # arm64 sentry-native at all; the x86_64 matrix covers the native backend. + - name: Use the Native backend + run: | + $optionsPath = "samples/IntegrationTest/Assets/Scripts/IntegrationOptionsConfiguration.cs" + $content = Get-Content $optionsPath -Raw + $content = $content -replace 'LinuxBackend = LinuxBackend.Breakpad', 'LinuxBackend = LinuxBackend.Native' + Set-Content $optionsPath $content + - name: Build with Sentry SDK run: ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform Linux-Server-arm64 -UnityVersion "$env:UNITY_VERSION" env: @@ -103,7 +108,7 @@ jobs: - name: Upload test app uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: testapp-desktop-compiled-${{ env.UNITY_VERSION }}-linux-arm64-breakpad + name: testapp-desktop-compiled-${{ env.UNITY_VERSION }}-linux-arm64-native if-no-files-found: error path: test-app-desktop.tar.gz retention-days: 14 diff --git a/docs/agent-guides/platform-native.md b/docs/agent-guides/platform-native.md index 2bfe42065..1767d1e76 100644 --- a/docs/agent-guides/platform-native.md +++ b/docs/agent-guides/platform-native.md @@ -27,7 +27,7 @@ Experimental native modes raise minimum shutdown timeout to 10 seconds. - Windows: runtime files beside player `.exe`; the library lands as `sentry-native.dll`. Both architectures ship, under `x86/` and `x64/` inside each backend folder, because Unity still builds a 32-bit standalone player. `BuildTarget.StandaloneWindows` picks `x86/`. -- Linux: `libsentry-native.so` under `_Data/Plugins/` (`x86_64` or `ARM64`, read from the player's ELF header); native daemon beside executable. +- Linux: `libsentry-native.so` under `_Data/Plugins/` (`x86_64` or `ARM64`, from the Linux build settings' architecture); native daemon beside executable. - 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. - Placement is what sentry-native probes by default. `SentryUnityOptions.NativeHandlerPath` overrides it via `handler_path`, for handlers staged somewhere else; sidecars the handler resolves relative to itself, such as `crashpad_wer.dll`, move with it. diff --git a/src/Sentry.Unity.Editor/Native/BuildPostProcess.cs b/src/Sentry.Unity.Editor/Native/BuildPostProcess.cs index 01fb71f1d..5b728242a 100644 --- a/src/Sentry.Unity.Editor/Native/BuildPostProcess.cs +++ b/src/Sentry.Unity.Editor/Native/BuildPostProcess.cs @@ -10,6 +10,7 @@ using UnityEditor.Build.Reporting; using System.Diagnostics; using System.Linq; +using System.Reflection; namespace Sentry.Unity.Editor.Native; @@ -84,7 +85,7 @@ _ when targetGroup.IsSwitch2() => Path.GetDirectoryName(executablePath), _ => string.Empty }; - var linuxArch = target == BuildTarget.StandaloneLinux64 ? GetLinuxArchitecture(executablePath) : string.Empty; + var linuxArch = target == BuildTarget.StandaloneLinux64 ? GetLinuxArchitecture() : string.Empty; UploadDebugSymbols(logger, target, buildOutputDir, executableName, linuxArch, options, cliOptions, isMono); @@ -160,34 +161,21 @@ internal static string GetWindowsBackendDir(BuildTarget target, SentryUnityOptio } // Linux ships x86_64 and ARM64, each in a subfolder named like the player's plugin folder. Both build for `StandaloneLinux64`, - // so the architecture is read from the built player's ELF header. + // so the architecture comes from the Linux build settings. internal static string GetLinuxBackendDir(SentryUnityOptions options, string arch) { var backend = options.Experimental.LinuxBackend == LinuxBackend.Native ? "SentryNative~" : "Sentry~"; return Path.GetFullPath($"Packages/{SentryPackageInfo.GetName()}/Plugins/Linux/{backend}/{arch}"); } - private const ushort ElfMachineAarch64 = 0xB7; - - internal static string GetLinuxArchitecture(string executablePath) + // Unity's Linux build picks the player from `UnityEditor.LinuxStandalone.UserBuildSettings.architecture`. + // It lives in the Linux module's assembly and is missing on versions without ARM64 support. + internal static string GetLinuxArchitecture() { - if (!File.Exists(executablePath)) - { - return "x86_64"; - } - - // `e_machine` is the 16-bit little-endian field at offset 18 of the ELF header. - var header = new byte[20]; - using (var stream = File.OpenRead(executablePath)) - { - if (stream.Read(header, 0, header.Length) != header.Length) - { - return "x86_64"; - } - } - - var machine = header[18] | (header[19] << 8); - return machine == ElfMachineAarch64 ? "ARM64" : "x86_64"; + var architecture = Type.GetType("UnityEditor.LinuxStandalone.UserBuildSettings, UnityEditor.LinuxStandalone.Extensions") + ?.GetProperty("architecture", BindingFlags.Public | BindingFlags.Static) + ?.GetValue(null); + return architecture?.ToString() == "ARM64" ? "ARM64" : "x86_64"; } private readonly struct NativePluginArtifact(string source, string destination, bool isExecutable = false) diff --git a/test/Sentry.Unity.Editor.Tests/Native/BuildPostProcessTests.cs b/test/Sentry.Unity.Editor.Tests/Native/BuildPostProcessTests.cs index 19ed20141..1097a4037 100644 --- a/test/Sentry.Unity.Editor.Tests/Native/BuildPostProcessTests.cs +++ b/test/Sentry.Unity.Editor.Tests/Native/BuildPostProcessTests.cs @@ -113,26 +113,4 @@ public void GetLinuxBackendDir_ResolvesBackendAndArchitecture( Assert.AreEqual(arch, Path.GetFileName(actual)); Assert.AreEqual(expectedBackendDir, Path.GetFileName(Path.GetDirectoryName(actual))); } - - [TestCase((byte)0x3E, "x86_64")] - [TestCase((byte)0xB7, "ARM64")] - public void GetLinuxArchitecture_ReadsElfMachine(byte machine, string expectedArch) - { - var executable = Path.Combine(_testDir, "player"); - var header = new byte[64]; - header[0] = 0x7F; - header[1] = (byte)'E'; - header[2] = (byte)'L'; - header[3] = (byte)'F'; - header[18] = machine; - File.WriteAllBytes(executable, header); - - Assert.AreEqual(expectedArch, BuildPostProcess.GetLinuxArchitecture(executable)); - } - - [Test] - public void GetLinuxArchitecture_MissingExecutable_DefaultsToX86_64() - { - Assert.AreEqual("x86_64", BuildPostProcess.GetLinuxArchitecture(Path.Combine(_testDir, "missing"))); - } } From a906d66dfa8a559f30c6452fa221de7d30ce50be Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Wed, 28 Oct 2026 11:56:06 +0100 Subject: [PATCH 16/16] Use ARM64 in the changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38b071e3f..cc014a7b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ ### Features -- The SDK now supports Linux ARM64 dedicated servers (Unity 6000.3 and newer, IL2CPP). The native SDK for both backends ships for `x86_64` and `arm64`, and the build picks the one matching the player. ([#2894](https://github.com/getsentry/sentry-unity/pull/2894)) +- The SDK now supports Linux ARM64 dedicated servers (Unity 6000.3 and newer, IL2CPP). The native SDK for both backends ships for `x86_64` and `ARM64`, and the build picks the one matching the player. ([#2894](https://github.com/getsentry/sentry-unity/pull/2894)) ### Dependencies