diff --git a/flutter/linux/CMakeLists.txt b/flutter/linux/CMakeLists.txt index 2c886a675b..a2a17eaa1b 100644 --- a/flutter/linux/CMakeLists.txt +++ b/flutter/linux/CMakeLists.txt @@ -3,6 +3,8 @@ # the plugin to fail to compile for some customers of the plugin. cmake_minimum_required(VERSION 3.10) +set(SENTRY_BACKEND "crashpad" CACHE STRING "The sentry backend responsible for reporting crashes" FORCE) + include("${CMAKE_CURRENT_SOURCE_DIR}/../sentry-native/sentry-native.cmake") # Even though sentry_flutter doesn't actually provide a useful plugin, we need to accommodate the Flutter tooling. diff --git a/flutter/sentry-native/sentry-native.cmake b/flutter/sentry-native/sentry-native.cmake index 8e16cb9900..ce61d083e3 100644 --- a/flutter/sentry-native/sentry-native.cmake +++ b/flutter/sentry-native/sentry-native.cmake @@ -2,9 +2,16 @@ load_cache("${CMAKE_CURRENT_LIST_DIR}" READ_WITH_PREFIX SENTRY_NATIVE_ repo vers message(STATUS "Fetching Sentry native version: ${SENTRY_NATIVE_version} from ${SENTRY_NATIVE_repo}") set(SENTRY_SDK_NAME "sentry.native.flutter" CACHE STRING "The SDK name to report when sending events." FORCE) -set(SENTRY_BACKEND "crashpad" CACHE STRING "The sentry backend responsible for reporting crashes" FORCE) set(SENTRY_BUILD_SHARED_LIBS ON CACHE BOOL "Build shared libraries (.dll/.so) instead of static ones (.lib/.a)" FORCE) +# Note: the backend is also set in linux/CMakeLists.txt and windows/CMakeLists.txt. This overwrites those if user sets an env var. +if("$ENV{SENTRY_NATIVE_BACKEND}" STREQUAL "") + # Until sentry-dart v9, we disable native backend by default. + set(SENTRY_BACKEND "none" CACHE STRING "The sentry backend responsible for reporting crashes" FORCE) +else() + set(SENTRY_BACKEND $ENV{SENTRY_NATIVE_BACKEND} CACHE STRING "The sentry backend responsible for reporting crashes" FORCE) +endif() + include(FetchContent) FetchContent_Declare( sentry-native @@ -17,15 +24,19 @@ FetchContent_MakeAvailable(sentry-native) # List of absolute paths to libraries that should be bundled with the plugin. # This list could contain prebuilt libraries, or libraries created by an external build triggered from this build file. -if(WIN32) - set(sentry_flutter_bundled_libraries - $ - $ - PARENT_SCOPE) +if(SENTRY_BACKEND STREQUAL "crashpad") + if(WIN32) + set(sentry_flutter_bundled_libraries + $ + $ + PARENT_SCOPE) + else() + set(sentry_flutter_bundled_libraries + $ + PARENT_SCOPE) + endif() else() - set(sentry_flutter_bundled_libraries - $ - PARENT_SCOPE) + set(sentry_flutter_bundled_libraries "" PARENT_SCOPE) endif() # `*_plugin` is the name of the plugin library as expected by flutter. diff --git a/flutter/test/sentry_native/sentry_native_test_ffi.dart b/flutter/test/sentry_native/sentry_native_test_ffi.dart index 9a57e07325..a0b5a3aa2b 100644 --- a/flutter/test/sentry_native/sentry_native_test_ffi.dart +++ b/flutter/test/sentry_native/sentry_native_test_ffi.dart @@ -12,254 +12,305 @@ import 'package:sentry_flutter/src/native/factory.dart'; import '../mocks.dart'; import '../mocks.mocks.dart'; -late final String repoRootDir; -late final List expectedDistFiles; +enum NativeBackend { default_, crashpad, breakpad, inproc, none } + +extension on NativeBackend { + // TODO change default to crashpad in v9 + String get actualValue => this == NativeBackend.default_ ? 'none' : name; +} // NOTE: Don't run/debug this main(), it likely won't work. // You can use main() in `sentry_native_test.dart`. void main() { - repoRootDir = Directory.current.path.endsWith('/test') + final repoRootDir = Directory.current.path.endsWith('/test') ? Directory.current.parent.path : Directory.current.path; - expectedDistFiles = platform.instance.isWindows - ? ['sentry.dll', 'crashpad_handler.exe', 'crashpad_wer.dll'] - : ['libsentry.so', 'crashpad_handler']; - - setUpAll(() async { - Directory.current = - await _buildSentryNative('$repoRootDir/temp/native-test'); - SentryNative.dynamicLibraryDirectory = '${Directory.current.path}/'; - SentryNative.crashpadPath = - '${Directory.current.path}/${expectedDistFiles.firstWhere((f) => f.contains('crashpad_handler'))}'; - }); - - late SentryNative sut; - late SentryFlutterOptions options; - - setUp(() { - options = SentryFlutterOptions(dsn: fakeDsn) - // ignore: invalid_use_of_internal_member - ..automatedTestMode = true - ..debug = true; - sut = createBinding(options) as SentryNative; - }); - - test('expected output files', () { - for (var name in expectedDistFiles) { - if (!File(name).existsSync()) { - fail('Native distribution file $name does not exist'); - } - } - }); - - test('options', () { - options - ..debug = true - ..environment = 'foo' - ..release = 'foo@bar+1' - ..enableAutoSessionTracking = true - ..dist = 'distfoo' - ..maxBreadcrumbs = 42; - - final cOptions = sut.createOptions(options); - try { - expect( - SentryNative.native - .options_get_dsn(cOptions) - .cast() - .toDartString(), - fakeDsn); - expect( - SentryNative.native - .options_get_environment(cOptions) - .cast() - .toDartString(), - 'foo'); - expect( - SentryNative.native - .options_get_release(cOptions) - .cast() - .toDartString(), - 'foo@bar+1'); - expect( - SentryNative.native.options_get_auto_session_tracking(cOptions), 1); - expect(SentryNative.native.options_get_max_breadcrumbs(cOptions), 42); - } finally { - SentryNative.native.options_free(cOptions); - } - }); - - test('SDK version', () { - expect(_configuredSentryNativeVersion.length, greaterThanOrEqualTo(5)); - expect(SentryNative.native.sdk_version().cast().toDartString(), - _configuredSentryNativeVersion); - }); - - test('SDK name', () { - expect(SentryNative.native.sdk_name().cast().toDartString(), - 'sentry.native.flutter'); - }); - - test('init', () async { - addTearDown(sut.close); - await sut.init(MockHub()); - }); - - test('app start', () { - expect(sut.fetchNativeAppStart(), null); - }); - - test('frames tracking', () { - sut.beginNativeFrames(); - expect(sut.endNativeFrames(SentryId.newId()), null); - }); - - test('hang tracking', () { - sut.pauseAppHangTracking(); - sut.resumeAppHangTracking(); - }); - - test('setUser', () async { - final user = SentryUser( - id: "fixture-id", - username: 'username', - email: 'mail@domain.tld', - ipAddress: '1.2.3.4', - name: 'User Name', - data: { - 'str': 'foo-bar', - 'double': 1.0, - 'int': 1, - 'int64': 0x7FFFFFFF + 1, - 'boo': true, - 'inner-map': {'str': 'inner'}, - 'unsupported': Object() - }, - ); - - await sut.setUser(user); - }); - - test('addBreadcrumb', () async { - final breadcrumb = Breadcrumb( - type: 'type', - message: 'message', - category: 'category', - ); - await sut.addBreadcrumb(breadcrumb); - }); - - test('clearBreadcrumbs', () async { - await sut.clearBreadcrumbs(); - }); - - test('displayRefreshRate', () async { - expect(sut.displayRefreshRate(), isNull); - }); - - test('setContexts', () async { - final value = {'object': Object()}; - await sut.setContexts('fixture-key', value); - }); - - test('removeContexts', () async { - await sut.removeContexts('fixture-key'); - }); - - test('setExtra', () async { - final value = {'object': Object()}; - await sut.setExtra('fixture-key', value); - }); - - test('removeExtra', () async { - await sut.removeExtra('fixture-key'); - }); - - test('setTag', () async { - await sut.setTag('fixture-key', 'fixture-value'); - }); - - test('removeTag', () async { - await sut.removeTag('fixture-key'); - }); - - test('startProfiler', () { - expect(() => sut.startProfiler(SentryId.newId()), throwsUnsupportedError); - }); - - test('discardProfiler', () async { - expect(() => sut.discardProfiler(SentryId.newId()), throwsUnsupportedError); - }); - - test('collectProfile', () async { - final traceId = SentryId.newId(); - const startTime = 42; - const endTime = 50; - expect(() => sut.collectProfile(traceId, startTime, endTime), - throwsUnsupportedError); - }); - - test('captureEnvelope', () async { - final data = Uint8List.fromList([1, 2, 3]); - expect(() => sut.captureEnvelope(data, false), throwsUnsupportedError); - }); - - test('loadContexts', () async { - expect(await sut.loadContexts(), isNull); - }); - - test('loadDebugImages', () async { - final list = await sut.loadDebugImages(SentryStackTrace(frames: [])); - expect(list, isNotEmpty); - expect(list![0].type, platform.instance.isWindows ? 'pe' : 'elf'); - expect(list[0].debugId!.length, greaterThan(30)); - expect( - list[0].debugFile, platform.instance.isWindows ? isNotEmpty : isNull); - expect(list[0].imageSize, greaterThan(0)); - expect(list[0].imageAddr, startsWith('0x')); - expect(list[0].imageAddr?.length, greaterThan(2)); - expect(list[0].codeId!.length, greaterThan(10)); - expect(list[0].codeFile, isNotEmpty); - expect( - File(list[0].codeFile!), - (File file) => file.existsSync(), - ); - }); + // assert(NativeBackend.values.length == 4); + for (final backend in NativeBackend.values) { + group(backend.name, () { + late final NativeTestHelper helper; + setUpAll(() async { + late final List expectedDistFiles; + if (backend.actualValue == 'crashpad') { + expectedDistFiles = platform.instance.isWindows + ? ['sentry.dll', 'crashpad_handler.exe', 'crashpad_wer.dll'] + : ['libsentry.so', 'crashpad_handler']; + } else { + expectedDistFiles = + platform.instance.isWindows ? ['sentry.dll'] : ['libsentry.so']; + } + + helper = NativeTestHelper( + repoRootDir, + backend, + expectedDistFiles, + '$repoRootDir/temp/native-test-${backend.name}', + ); + + Directory.current = await helper._buildSentryNative(); + SentryNative.dynamicLibraryDirectory = '${Directory.current.path}/'; + if (backend.actualValue == 'crashpad') { + SentryNative.crashpadPath = + '${Directory.current.path}/${expectedDistFiles.firstWhere((f) => f.contains('crashpad_handler'))}'; + } + }); + + late SentryNative sut; + late SentryFlutterOptions options; + + setUp(() { + options = SentryFlutterOptions(dsn: fakeDsn) + // ignore: invalid_use_of_internal_member + ..automatedTestMode = true + ..debug = true; + sut = createBinding(options) as SentryNative; + }); + + test('native CMake was configured with configured backend', () async { + final cmakeCacheTxt = + await File('${helper.cmakeBuildDir}/CMakeCache.txt').readAsLines(); + expect(cmakeCacheTxt, + contains('SENTRY_BACKEND:STRING=${backend.actualValue}')); + }); + + test('expected output files', () { + for (var name in helper.expectedDistFiles) { + if (!File(name).existsSync()) { + fail('Native distribution file $name does not exist'); + } + } + }); + + test('options', () { + options + ..debug = true + ..environment = 'foo' + ..release = 'foo@bar+1' + ..enableAutoSessionTracking = true + ..dist = 'distfoo' + ..maxBreadcrumbs = 42; + + final cOptions = sut.createOptions(options); + try { + expect( + SentryNative.native + .options_get_dsn(cOptions) + .cast() + .toDartString(), + fakeDsn); + expect( + SentryNative.native + .options_get_environment(cOptions) + .cast() + .toDartString(), + 'foo'); + expect( + SentryNative.native + .options_get_release(cOptions) + .cast() + .toDartString(), + 'foo@bar+1'); + expect( + SentryNative.native.options_get_auto_session_tracking(cOptions), + 1); + expect(SentryNative.native.options_get_max_breadcrumbs(cOptions), 42); + } finally { + SentryNative.native.options_free(cOptions); + } + }); + + test('SDK version', () { + expect(helper.configuredSentryNativeVersion.length, + greaterThanOrEqualTo(5)); + expect(SentryNative.native.sdk_version().cast().toDartString(), + helper.configuredSentryNativeVersion); + }); + + test('SDK name', () { + expect(SentryNative.native.sdk_name().cast().toDartString(), + 'sentry.native.flutter'); + }); + + test('init', () async { + addTearDown(sut.close); + await sut.init(MockHub()); + }); + + test('app start', () { + expect(sut.fetchNativeAppStart(), null); + }); + + test('frames tracking', () { + sut.beginNativeFrames(); + expect(sut.endNativeFrames(SentryId.newId()), null); + }); + + test('hang tracking', () { + sut.pauseAppHangTracking(); + sut.resumeAppHangTracking(); + }); + + test('setUser', () async { + final user = SentryUser( + id: "fixture-id", + username: 'username', + email: 'mail@domain.tld', + ipAddress: '1.2.3.4', + name: 'User Name', + data: { + 'str': 'foo-bar', + 'double': 1.0, + 'int': 1, + 'int64': 0x7FFFFFFF + 1, + 'boo': true, + 'inner-map': {'str': 'inner'}, + 'unsupported': Object() + }, + ); + + await sut.setUser(user); + }); + + test('addBreadcrumb', () async { + final breadcrumb = Breadcrumb( + type: 'type', + message: 'message', + category: 'category', + ); + await sut.addBreadcrumb(breadcrumb); + }); + + test('clearBreadcrumbs', () async { + await sut.clearBreadcrumbs(); + }); + + test('displayRefreshRate', () async { + expect(sut.displayRefreshRate(), isNull); + }); + + test('setContexts', () async { + final value = {'object': Object()}; + await sut.setContexts('fixture-key', value); + }); + + test('removeContexts', () async { + await sut.removeContexts('fixture-key'); + }); + + test('setExtra', () async { + final value = {'object': Object()}; + await sut.setExtra('fixture-key', value); + }); + + test('removeExtra', () async { + await sut.removeExtra('fixture-key'); + }); + + test('setTag', () async { + await sut.setTag('fixture-key', 'fixture-value'); + }); + + test('removeTag', () async { + await sut.removeTag('fixture-key'); + }); + + test('startProfiler', () { + expect( + () => sut.startProfiler(SentryId.newId()), throwsUnsupportedError); + }); + + test('discardProfiler', () async { + expect(() => sut.discardProfiler(SentryId.newId()), + throwsUnsupportedError); + }); + + test('collectProfile', () async { + final traceId = SentryId.newId(); + const startTime = 42; + const endTime = 50; + expect(() => sut.collectProfile(traceId, startTime, endTime), + throwsUnsupportedError); + }); + + test('captureEnvelope', () async { + final data = Uint8List.fromList([1, 2, 3]); + expect(() => sut.captureEnvelope(data, false), throwsUnsupportedError); + }); + + test('loadContexts', () async { + expect(await sut.loadContexts(), isNull); + }); + + test('loadDebugImages', () async { + final list = await sut.loadDebugImages(SentryStackTrace(frames: [])); + expect(list, isNotEmpty); + expect(list![0].type, platform.instance.isWindows ? 'pe' : 'elf'); + expect(list[0].debugId!.length, greaterThan(30)); + expect(list[0].debugFile, + platform.instance.isWindows ? isNotEmpty : isNull); + expect(list[0].imageSize, greaterThan(0)); + expect(list[0].imageAddr, startsWith('0x')); + expect(list[0].imageAddr?.length, greaterThan(2)); + expect(list[0].codeId!.length, greaterThan(10)); + expect(list[0].codeFile, isNotEmpty); + expect( + File(list[0].codeFile!), + (File file) => file.existsSync(), + ); + }); + }); + } } -/// Runs [command] with command's stdout and stderr being forwrarded to -/// test runner's respective streams. It buffers stdout and returns it. -/// -/// Returns [_CommandResult] with exitCode and stdout as a single sting -Future _exec(String executable, List arguments) async { - final process = await Process.start(executable, arguments); - - // forward standard streams - unawaited(stderr.addStream(process.stderr)); - unawaited(stdout.addStream(process.stdout)); - - int exitCode = await process.exitCode; - if (exitCode != 0) { - throw Exception( - "$executable ${arguments.join(' ')} failed with exit code $exitCode"); +class NativeTestHelper { + final String repoRootDir; + final NativeBackend nativeBackend; + final List expectedDistFiles; + final String nativeTestRoot; + late final cmakeBuildDir = '$nativeTestRoot/build'; + late final cmakeConfDir = '$nativeTestRoot/conf'; + late final buildOutputDir = '$nativeTestRoot/dist/'; + + NativeTestHelper(this.repoRootDir, this.nativeBackend, this.expectedDistFiles, + this.nativeTestRoot); + + /// Runs [command] with command's stdout and stderr being forwrarded to + /// test runner's respective streams. It buffers stdout and returns it. + /// + /// Returns [_CommandResult] with exitCode and stdout as a single sting + Future _exec(String executable, List arguments) async { + final env = Map.of(Platform.environment); + if (nativeBackend != NativeBackend.default_) { + env['SENTRY_NATIVE_BACKEND'] = nativeBackend.name; + } else { + env.remove('SENTRY_NATIVE_BACKEND'); + } + + final process = await Process.start(executable, arguments, + environment: env, includeParentEnvironment: false); + + // forward standard streams + unawaited(stderr.addStream(process.stderr)); + unawaited(stdout.addStream(process.stdout)); + + int exitCode = await process.exitCode; + if (exitCode != 0) { + throw Exception( + "$executable ${arguments.join(' ')} failed with exit code $exitCode"); + } } -} -/// Compile sentry-native using CMake, as if it was part of a Flutter app. -/// Returns the directory containing built libraries -Future _buildSentryNative(String nativeTestRoot) async { - final cmakeBuildDir = '$nativeTestRoot/build'; - final cmakeConfDir = '$nativeTestRoot/conf'; - final buildOutputDir = '$nativeTestRoot/dist/'; - - if (!_builtVersionIsExpected(cmakeBuildDir, buildOutputDir)) { - Directory(cmakeConfDir).createSync(recursive: true); - Directory(buildOutputDir).createSync(recursive: true); - File('$cmakeConfDir/main.c').writeAsStringSync(''' + /// Compile sentry-native using CMake, as if it was part of a Flutter app. + /// Returns the directory containing built libraries + Future _buildSentryNative() async { + if (!_builtVersionIsExpected()) { + Directory(cmakeConfDir).createSync(recursive: true); + Directory(buildOutputDir).createSync(recursive: true); + File('$cmakeConfDir/main.c').writeAsStringSync(''' int main(int argc, char *argv[]) { return 0; } '''); - File('$cmakeConfDir/CMakeLists.txt').writeAsStringSync(''' + File('$cmakeConfDir/CMakeLists.txt').writeAsStringSync(''' cmake_minimum_required(VERSION 3.14) project(sentry-native-flutter-test) add_subdirectory(../../../${platform.instance.operatingSystem} plugin) @@ -272,39 +323,40 @@ list(APPEND PLUGIN_BUNDLED_LIBRARIES \${sentry_flutter_bundled_libraries}) install(FILES "\${PLUGIN_BUNDLED_LIBRARIES}" DESTINATION "${buildOutputDir.replaceAll('\\', '/')}" COMPONENT Runtime) set(CMAKE_INSTALL_PREFIX "${buildOutputDir.replaceAll('\\', '/')}") '''); - await _exec('cmake', ['-B', cmakeBuildDir, cmakeConfDir]); - await _exec('cmake', - ['--build', cmakeBuildDir, '--config', 'Release', '--parallel']); - await _exec('cmake', [ - '--install', - cmakeBuildDir, - '--config', - 'Release', - ]); - if (platform.instance.isLinux) { - await _exec('chmod', ['+x', '$buildOutputDir/crashpad_handler']); + await _exec('cmake', ['-B', cmakeBuildDir, cmakeConfDir]); + await _exec('cmake', + ['--build', cmakeBuildDir, '--config', 'Release', '--parallel']); + await _exec('cmake', [ + '--install', + cmakeBuildDir, + '--config', + 'Release', + ]); + if (platform.instance.isLinux) { + await _exec('chmod', ['+x', '$buildOutputDir/crashpad_handler']); + } } + return buildOutputDir; } - return buildOutputDir; -} -bool _builtVersionIsExpected(String cmakeBuildDir, String buildOutputDir) { - final buildCmake = File( - '$cmakeBuildDir/_deps/sentry-native-build/sentry-config-version.cmake'); - if (!buildCmake.existsSync()) return false; + bool _builtVersionIsExpected() { + final buildCmake = File( + '$cmakeBuildDir/_deps/sentry-native-build/sentry-config-version.cmake'); + if (!buildCmake.existsSync()) return false; + + if (!buildCmake + .readAsStringSync() + .contains('set(PACKAGE_VERSION "$configuredSentryNativeVersion")')) { + return false; + } - if (!buildCmake - .readAsStringSync() - .contains('set(PACKAGE_VERSION "$_configuredSentryNativeVersion")')) { - return false; + return !expectedDistFiles + .any((name) => !File('$buildOutputDir/$name').existsSync()); } - return !expectedDistFiles - .any((name) => !File('$buildOutputDir/$name').existsSync()); + late final configuredSentryNativeVersion = + File('$repoRootDir/sentry-native/CMakeCache.txt') + .readAsLinesSync() + .map((line) => line.startsWith('version=') ? line.substring(8) : null) + .firstWhere((line) => line != null)!; } - -final _configuredSentryNativeVersion = - File('$repoRootDir/sentry-native/CMakeCache.txt') - .readAsLinesSync() - .map((line) => line.startsWith('version=') ? line.substring(8) : null) - .firstWhere((line) => line != null)!; diff --git a/flutter/windows/CMakeLists.txt b/flutter/windows/CMakeLists.txt index 7926cf0571..9b1fcbcc5e 100644 --- a/flutter/windows/CMakeLists.txt +++ b/flutter/windows/CMakeLists.txt @@ -4,6 +4,14 @@ # customers of the plugin. cmake_minimum_required(VERSION 3.14) +if(FLUTTER_TARGET_PLATFORM EQUAL "windows-arm64") + set(native_backend "breakpad") +else() + set(native_backend "crashpad") +endif() + +set(SENTRY_BACKEND ${native_backend} CACHE STRING "The sentry backend responsible for reporting crashes" FORCE) + include("${CMAKE_CURRENT_SOURCE_DIR}/../sentry-native/sentry-native.cmake") # Even though sentry_flutter doesn't actually provide a useful plugin, we need to accommodate the Flutter tooling.