diff --git a/CHANGELOG.md b/CHANGELOG.md index a816c409e..73a732c2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- Automated symbols upload for iOS builds when bitcode is disabled ([#443](https://github.com/getsentry/sentry-unity/pull/443)) + ### Fixes - Sentry no longer requires Xcode projects to be exported on macOS ([#442](https://github.com/getsentry/sentry-unity/pull/442)) diff --git a/src/Sentry.Unity.Editor.iOS/BuildPostProcess.cs b/src/Sentry.Unity.Editor.iOS/BuildPostProcess.cs index 6556da88e..942d03d87 100644 --- a/src/Sentry.Unity.Editor.iOS/BuildPostProcess.cs +++ b/src/Sentry.Unity.Editor.iOS/BuildPostProcess.cs @@ -17,6 +17,7 @@ public static void OnPostProcessBuild(BuildTarget target, string pathToProject) } var options = ScriptableSentryUnityOptions.LoadSentryUnityOptions(BuildPipeline.isBuildingPlayer); + var logger = options?.DiagnosticLogger ?? new UnityLogger(new SentryUnityOptions()); try { @@ -27,22 +28,48 @@ public static void OnPostProcessBuild(BuildTarget target, string pathToProject) if (options?.Validate() != true) { - new UnityLogger(new SentryOptions()).LogWarning("Failed to validate Sentry Options. Native support disabled."); + logger.LogWarning("Failed to validate Sentry Options. Native support disabled."); return; } if (!options.IosNativeSupportEnabled) { - options.DiagnosticLogger?.LogDebug("iOS Native support disabled through the options."); + logger.LogDebug("iOS Native support disabled through the options."); return; } sentryXcodeProject.AddNativeOptions(options); sentryXcodeProject.AddSentryToMain(options); + + var sentryCliOptions = SentryCliOptions.LoadCliOptions(); + if (!sentryCliOptions.UploadSymbols) + { + logger.LogDebug("Automated symbols upload has been disabled."); + return; + } + + if (EditorUserBuildSettings.development && !sentryCliOptions.UploadDevelopmentSymbols) + { + logger.LogDebug("Automated symbols upload for development builds has been disabled."); + return; + } + + if (!sentryCliOptions.Validate(logger)) + { + logger.LogWarning("sentry-cli validation failed. Symbols will not be uploaded." + + "\nYou can disable this warning by disabling the automated symbols upload under " + + "Tools -> Sentry -> Editor"); + return; + } + + SentryCli.CreateSentryProperties(pathToProject, sentryCliOptions); + SentryCli.AddExecutableToXcodeProject(pathToProject); + + sentryXcodeProject.AddBuildPhaseSymbolUpload(logger); } catch (Exception e) { - options?.DiagnosticLogger?.LogError("Failed to add the Sentry framework to the generated Xcode project", e); + logger.LogError("Failed to add the Sentry framework to the generated Xcode project", e); } } diff --git a/src/Sentry.Unity.Editor.iOS/Sentry.Unity.Editor.iOS.csproj b/src/Sentry.Unity.Editor.iOS/Sentry.Unity.Editor.iOS.csproj index ad79c341d..e2b679a3d 100644 --- a/src/Sentry.Unity.Editor.iOS/Sentry.Unity.Editor.iOS.csproj +++ b/src/Sentry.Unity.Editor.iOS/Sentry.Unity.Editor.iOS.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/Sentry.Unity.Editor.iOS/SentryXcodeProject.cs b/src/Sentry.Unity.Editor.iOS/SentryXcodeProject.cs index 7f3d656e6..b4c62d2f6 100644 --- a/src/Sentry.Unity.Editor.iOS/SentryXcodeProject.cs +++ b/src/Sentry.Unity.Editor.iOS/SentryXcodeProject.cs @@ -1,5 +1,7 @@ using System; using System.IO; +using System.Linq; +using Sentry.Extensibility; using UnityEditor.iOS.Xcode; using UnityEditor.iOS.Xcode.Extensions; @@ -8,6 +10,7 @@ namespace Sentry.Unity.Editor.iOS internal class SentryXcodeProject : IDisposable { private const string FrameworkName = "Sentry.framework"; + internal const string SymbolUploadPhaseName = "SymbolUpload"; private readonly string _mainPath = Path.Combine("MainApp", "main.mm"); private readonly string _optionsPath = Path.Combine("MainApp", "SentryOptions.m"); @@ -71,9 +74,37 @@ public void AddSentryFramework() _project.SetBuildProperty(unityFrameworkTargetGuid, "FRAMEWORK_SEARCH_PATHS", "$(inherited)"); _project.AddBuildProperty(unityFrameworkTargetGuid, "FRAMEWORK_SEARCH_PATHS", "$(PROJECT_DIR)/Frameworks/"); + _project.SetBuildProperty(mainTargetGuid, "DEBUG_INFORMATION_FORMAT", "dwarf-with-dsym"); + _project.SetBuildProperty(unityFrameworkTargetGuid, "DEBUG_INFORMATION_FORMAT", "dwarf-with-dsym"); + _project.AddBuildProperty(mainTargetGuid, "OTHER_LDFLAGS", "-ObjC"); } + public void AddBuildPhaseSymbolUpload(IDiagnosticLogger? logger) + { + if (MainTargetContainsSymbolUploadBuildPhase()) + { + logger?.LogDebug("Build phase '{0}' already added.", SymbolUploadPhaseName); + return; + } + + var mainTargetGuid = _project.GetUnityMainTargetGuid(); + _project.AddShellScriptBuildPhase(mainTargetGuid, + SymbolUploadPhaseName, + "/bin/sh", + $@"export SENTRY_PROPERTIES=sentry.properties +if [ ""$ENABLE_BITCODE"" = ""NO"" ] ; then + echo ""Bitcode is disabled - Uploading symbols"" + ERROR=$(./{SentryCli.SentryCliMacOS} upload-dif $BUILT_PRODUCTS_DIR > ./sentry-symbols-upload.log 2>&1 &) + if [ ! $? -eq 0 ] ; then + echo ""warning: sentry-cli - $ERROR"" + fi +else + echo ""Bitcode is enabled - Skipping symbols upload"" +fi" + ); + } + public void AddNativeOptions(SentryUnityOptions options) { _nativeOptions.CreateFile(Path.Combine(_projectRoot, _optionsPath), options); @@ -83,6 +114,12 @@ public void AddNativeOptions(SentryUnityOptions options) public void AddSentryToMain(SentryUnityOptions options) => _nativeMain.AddSentry(Path.Combine(_projectRoot, _mainPath), options.DiagnosticLogger); + internal bool MainTargetContainsSymbolUploadBuildPhase() + { + var allBuildPhases = _project.GetAllBuildPhasesForTarget(_project.GetUnityMainTargetGuid()); + return allBuildPhases.Any(buildPhase => _project.GetBuildPhaseName(buildPhase) == SymbolUploadPhaseName); + } + internal string ProjectToString() => _project.WriteToString(); public void Dispose() => _project.WriteToFile(_projectPath); diff --git a/src/Sentry.Unity.Editor/Android/AndroidManifestConfiguration.cs b/src/Sentry.Unity.Editor/Android/AndroidManifestConfiguration.cs index da33357e6..72978445c 100644 --- a/src/Sentry.Unity.Editor/Android/AndroidManifestConfiguration.cs +++ b/src/Sentry.Unity.Editor/Android/AndroidManifestConfiguration.cs @@ -149,7 +149,9 @@ internal void SetupSymbolsUpload(string basePath) if (!sentryCliOptions.Validate(logger)) { - logger.LogWarning("Loading sentry-cli configuration failed. Symbols will not be uploaded"); + logger.LogWarning("sentry-cli validation failed. Symbols will not be uploaded." + + "\nYou can disable this warning by disabling the automated symbols upload under " + + "Tools -> Sentry -> Editor"); return; } diff --git a/src/Sentry.Unity.Editor/SentryCli.cs b/src/Sentry.Unity.Editor/SentryCli.cs index 1905057e0..8cf8034d8 100644 --- a/src/Sentry.Unity.Editor/SentryCli.cs +++ b/src/Sentry.Unity.Editor/SentryCli.cs @@ -8,6 +8,10 @@ namespace Sentry.Unity.Editor { internal static class SentryCli { + internal const string SentryCliWindows = "sentry-cli-Windows-x86_64.exe"; + internal const string SentryCliMacOS = "sentry-cli-Darwin-universal"; + internal const string SentryCliLinux = "sentry-cli-Linux-x86_64"; + [DllImport("libc", SetLastError = true)] private static extern int chmod(string pathname, int mode); @@ -34,9 +38,9 @@ internal static string GetSentryCliPlatformName(IApplication? application = null return application.Platform switch { - RuntimePlatform.WindowsEditor => "sentry-cli-Windows-x86_64.exe ", - RuntimePlatform.OSXEditor => "sentry-cli-Darwin-universal", - RuntimePlatform.LinuxEditor => "sentry-cli-Linux-x86_64 ", + RuntimePlatform.WindowsEditor => SentryCliWindows, + RuntimePlatform.OSXEditor => SentryCliMacOS, + RuntimePlatform.LinuxEditor => SentryCliLinux, _ => throw new InvalidOperationException( $"Cannot get sentry-cli for the current platform: {Application.platform}") }; @@ -69,5 +73,19 @@ internal static void SetExecutePermission(string? filePath = null, IApplication? throw new UnauthorizedAccessException($"Failed to set permission to {filePath}"); } } + + internal static void AddExecutableToXcodeProject(string projectPath) + { + var executableSource = GetSentryCliPath(SentryCliMacOS); + var executableDestination = Path.Combine(projectPath, SentryCliMacOS); + + if (!Directory.Exists(projectPath)) + { + throw new DirectoryNotFoundException($"Xcode project directory not found at {executableDestination}"); + } + + File.Copy(executableSource, executableDestination); + SetExecutePermission(executableDestination); + } } } diff --git a/src/Sentry.Unity/Properties/AssemblyInfo.cs b/src/Sentry.Unity/Properties/AssemblyInfo.cs index 22e4895d5..256d67944 100644 --- a/src/Sentry.Unity/Properties/AssemblyInfo.cs +++ b/src/Sentry.Unity/Properties/AssemblyInfo.cs @@ -2,8 +2,9 @@ [assembly: InternalsVisibleTo("Sentry.Unity.Tests")] [assembly: InternalsVisibleTo("Sentry.Unity.Editor")] -[assembly: InternalsVisibleTo("Sentry.Unity.Editor.iOS")] [assembly: InternalsVisibleTo("Sentry.Unity.Editor.Tests")] +[assembly: InternalsVisibleTo("Sentry.Unity.Editor.iOS")] +[assembly: InternalsVisibleTo("Sentry.Unity.Editor.iOS.Tests")] [assembly: InternalsVisibleTo("Sentry.Unity.iOS")] [assembly: InternalsVisibleTo("Sentry.Unity.iOS.Tests")] [assembly: InternalsVisibleTo("Sentry.Unity.Android")] diff --git a/test/Sentry.Unity.Editor.Tests/SentryCliTests.cs b/test/Sentry.Unity.Editor.Tests/SentryCliTests.cs index f027593ed..cc4209382 100644 --- a/test/Sentry.Unity.Editor.Tests/SentryCliTests.cs +++ b/test/Sentry.Unity.Editor.Tests/SentryCliTests.cs @@ -17,9 +17,9 @@ public void GetSentryCliPlatformName_UnrecognizedPlatform_ThrowsInvalidOperation } [Test] - [TestCase(RuntimePlatform.WindowsEditor, "sentry-cli-Windows-x86_64.exe ")] - [TestCase(RuntimePlatform.OSXEditor, "sentry-cli-Darwin-universal")] - [TestCase(RuntimePlatform.LinuxEditor, "sentry-cli-Linux-x86_64 ")] + [TestCase(RuntimePlatform.WindowsEditor, SentryCli.SentryCliWindows)] + [TestCase(RuntimePlatform.OSXEditor, SentryCli.SentryCliMacOS)] + [TestCase(RuntimePlatform.LinuxEditor, SentryCli.SentryCliLinux)] public void GetSentryPlatformName_RecognizedPlatform_SetsSentryCliName(RuntimePlatform platform, string expectedName) { var application = new TestApplication(platform: platform); @@ -80,5 +80,24 @@ public void CreateSentryProperties_PropertyFileCreatedAndContainsSentryCliOption Directory.Delete(propertiesDirectory, true); } + + [Test] + public void AddExecutableToXcodeProject_ProjectPathDoesNotExist_ThrowsDirectoryNotFoundException() + { + Assert.Throws(() => SentryCli.AddExecutableToXcodeProject("non-existent-path")); + } + + [Test] + public void AddExecutableToXcodeProject_ProjectPathExists_CopiesSentryCliForMacOS() + { + var fakeXcodeProjectDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(fakeXcodeProjectDirectory); + + SentryCli.AddExecutableToXcodeProject(fakeXcodeProjectDirectory); + + Assert.IsTrue(File.Exists(Path.Combine(fakeXcodeProjectDirectory, SentryCli.SentryCliMacOS))); + + Directory.Delete(fakeXcodeProjectDirectory, true); + } } } diff --git a/test/Sentry.Unity.Editor.iOS.Tests/Sentry.Unity.Editor.iOS.Tests.csproj b/test/Sentry.Unity.Editor.iOS.Tests/Sentry.Unity.Editor.iOS.Tests.csproj index 6731f98d0..8d917b393 100644 --- a/test/Sentry.Unity.Editor.iOS.Tests/Sentry.Unity.Editor.iOS.Tests.csproj +++ b/test/Sentry.Unity.Editor.iOS.Tests/Sentry.Unity.Editor.iOS.Tests.csproj @@ -15,4 +15,11 @@ + + + + %(RecursiveDir)/%(Filename)%(Extension) + + + diff --git a/test/Sentry.Unity.Editor.iOS.Tests/SentryXcodeProjectTests.cs b/test/Sentry.Unity.Editor.iOS.Tests/SentryXcodeProjectTests.cs index d70cbbae0..00cdf9d13 100644 --- a/test/Sentry.Unity.Editor.iOS.Tests/SentryXcodeProjectTests.cs +++ b/test/Sentry.Unity.Editor.iOS.Tests/SentryXcodeProjectTests.cs @@ -1,7 +1,9 @@ using System.IO; using System.Reflection; +using System.Text.RegularExpressions; using NUnit.Framework; using Sentry.Extensibility; +using Sentry.Unity.Tests.SharedClasses; namespace Sentry.Unity.Editor.iOS.Tests { @@ -21,10 +23,22 @@ private class Fixture { public string ProjectRoot { get; set; } = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "TestFiles", "2019_4"); - public SentryUnityOptions Options { get; set; } = new(); + public SentryUnityOptions Options { get; set; } + public TestLogger TestLogger { get; set; } public INativeMain NativeMain { get; set; } = new NativeMainTest(); public INativeOptions NativeOptions { get; set; } = new NativeOptionsTest(); + public Fixture() + { + TestLogger = new TestLogger(); + Options = new SentryUnityOptions + { + Debug = true, + DiagnosticLevel = SentryLevel.Debug, + DiagnosticLogger = TestLogger + }; + } + public SentryXcodeProject GetSut() => new(ProjectRoot, NativeMain, NativeOptions); } @@ -83,5 +97,36 @@ public void CreateNativeOptions_CleanXcodeProject_NativeOptionsAdded() StringAssert.Contains("SentryOptions.m", xcodeProject.ProjectToString()); } + + [Test] + public void AddBuildPhaseSymbolUpload_CleanXcodeProject_BuildPhaseSymbolUploadAdded() + { + var xcodeProject = _fixture.GetSut(); + xcodeProject.ReadFromProjectFile(); + + var didContainUploadPhase = xcodeProject.MainTargetContainsSymbolUploadBuildPhase(); + xcodeProject.AddBuildPhaseSymbolUpload(_fixture.Options.DiagnosticLogger); + var doesContainUploadPhase = xcodeProject.MainTargetContainsSymbolUploadBuildPhase(); + + Assert.IsFalse(didContainUploadPhase); + Assert.IsTrue(doesContainUploadPhase); + } + + [Test] + public void AddBuildPhaseSymbolUpload_PhaseAlreadyAdded_LogsAndDoesNotAddAgain() + { + const int expectedBuildPhaseOccurence = 1; + var xcodeProject = _fixture.GetSut(); + xcodeProject.ReadFromProjectFile(); + + xcodeProject.AddBuildPhaseSymbolUpload(_fixture.Options.DiagnosticLogger); + xcodeProject.AddBuildPhaseSymbolUpload(_fixture.Options.DiagnosticLogger); + + var actualBuildPhaseOccurence = Regex.Matches(xcodeProject.ProjectToString(), + Regex.Escape(SentryXcodeProject.SymbolUploadPhaseName)).Count; + + Assert.AreEqual(1, _fixture.TestLogger.Logs.Count); + Assert.AreEqual(expectedBuildPhaseOccurence, actualBuildPhaseOccurence); + } } } diff --git a/test/Sentry.Unity.Tests/Json/SafeSerializerTests.cs b/test/Sentry.Unity.Tests/Json/SafeSerializerTests.cs index 4fc8073f7..d01650b52 100644 --- a/test/Sentry.Unity.Tests/Json/SafeSerializerTests.cs +++ b/test/Sentry.Unity.Tests/Json/SafeSerializerTests.cs @@ -2,6 +2,7 @@ using System.Linq; using NUnit.Framework; using Sentry.Unity.Json; +using Sentry.Unity.Tests.SharedClasses; namespace Sentry.Unity.Tests.Json { diff --git a/test/Sentry.Unity.Tests/Sentry.Unity.Tests.csproj b/test/Sentry.Unity.Tests/Sentry.Unity.Tests.csproj index f5f703e74..fa1f81c72 100644 --- a/test/Sentry.Unity.Tests/Sentry.Unity.Tests.csproj +++ b/test/Sentry.Unity.Tests/Sentry.Unity.Tests.csproj @@ -11,4 +11,9 @@ + + + %(RecursiveDir)/%(Filename)%(Extension) + + diff --git a/test/Sentry.Unity.Tests/UnityEventProcessorTests.cs b/test/Sentry.Unity.Tests/UnityEventProcessorTests.cs index 9a6c90aab..f2c476c7f 100644 --- a/test/Sentry.Unity.Tests/UnityEventProcessorTests.cs +++ b/test/Sentry.Unity.Tests/UnityEventProcessorTests.cs @@ -1,13 +1,11 @@ using System; using System.Collections; -using System.Collections.Concurrent; -using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Threading.Tasks; using NUnit.Framework; -using Sentry.Extensibility; +using Sentry.Unity.Tests.SharedClasses; using Sentry.Unity.Tests.Stubs; using UnityEngine; using UnityEngine.TestTools; @@ -545,19 +543,6 @@ public IEnumerator Process_GpuProtocolGraphicsShaderLevelMinusOne_Ignored() } } - internal sealed class TestLogger : IDiagnosticLogger - { - internal readonly ConcurrentBag<(SentryLevel logLevel, string message, Exception? exception)> Logs = new(); - - public bool IsEnabled(SentryLevel level) => true; - - public void Log(SentryLevel logLevel, string message, Exception? exception = null, params object?[] args) - { - var log = (logLevel, string.Format(message, args), exception); - Logs.Add(log); - } - } - internal sealed class TestSentrySystemInfo : ISentrySystemInfo { public int? MainThreadId { get; set; } = 1; diff --git a/test/SharedClasses/README.md b/test/SharedClasses/README.md new file mode 100644 index 000000000..885be6749 --- /dev/null +++ b/test/SharedClasses/README.md @@ -0,0 +1,2 @@ +Here we place classes that are used in multiple projects. Unity re-imports all DLLs and complains about duplications so +the regular approach of adding a "Helpers" project does not work. diff --git a/test/SharedClasses/TestLogger.cs b/test/SharedClasses/TestLogger.cs new file mode 100644 index 000000000..3ec40b6b2 --- /dev/null +++ b/test/SharedClasses/TestLogger.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Concurrent; +using Sentry.Extensibility; + +namespace Sentry.Unity.Tests.SharedClasses +{ + internal sealed class TestLogger : IDiagnosticLogger + { + internal readonly ConcurrentBag<(SentryLevel logLevel, string message, Exception? exception)> Logs = new(); + + public bool IsEnabled(SentryLevel level) => true; + + public void Log(SentryLevel logLevel, string message, Exception? exception = null, params object?[] args) + { + var log = (logLevel, string.Format(message, args), exception); + Logs.Add(log); + } + } +}