From bfd77cdd4add4fe27d0de10da1a88b03237bca30 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Fri, 8 Aug 2025 17:22:35 +0200 Subject: [PATCH 1/5] Initial impl --- lib/sentry_dart_plugin.dart | 68 +++-------- lib/src/utils/flutter_debug_files.dart | 159 +++++++++++++++++++++++++ test/flutter_debug_files_test.dart | 159 +++++++++++++++++++++++++ 3 files changed, 336 insertions(+), 50 deletions(-) create mode 100644 lib/src/utils/flutter_debug_files.dart create mode 100644 test/flutter_debug_files_test.dart diff --git a/lib/sentry_dart_plugin.dart b/lib/sentry_dart_plugin.dart index f8c0ff5a..915fc22d 100644 --- a/lib/sentry_dart_plugin.dart +++ b/lib/sentry_dart_plugin.dart @@ -5,6 +5,7 @@ import 'package:process/process.dart'; import 'package:sentry_dart_plugin/src/utils/extensions.dart'; import 'src/configuration.dart'; +import 'src/utils/flutter_debug_files.dart'; import 'src/utils/injector.dart'; import 'src/utils/log.dart'; @@ -97,61 +98,28 @@ class SentryDartPlugin { await _executeAndLog('Failed to upload symbols', [...params, path]); } - Log.taskCompleted(taskName); - } - - Stream _enumerateDebugSymbolPaths(FileSystem fs) async* { - final buildDir = _configuration.buildFilesFolder; - final projectRoot = fs.currentDirectory.path; - - // Android (apk, appbundle) - yield '$buildDir/app/outputs'; - yield '$buildDir/app/intermediates'; - - // Windows - for (final subdir in ['', '/x64', '/arm64']) { - yield '$buildDir/windows$subdir/runner/Release'; + final all = await _findFlutterRelevantDebugFilePaths(); + for (final path in all) { + print('Found path: $path'); } - // TODO we should delete this once we have windows symbols collected automatically. - // Related to https://github.com/getsentry/sentry-dart-plugin/issues/173 - yield 'windows/flutter/ephemeral/flutter_windows.dll.pdb'; - // Linux - for (final subdir in ['/x64', '/arm64']) { - yield '$buildDir/linux$subdir/release/bundle'; - } - - // macOS - yield '$buildDir/macos/Build/Products/Release'; - - // macOS (macOS-framework) - yield '$buildDir/macos/framework/Release'; - - // iOS - yield '$buildDir/ios/iphoneos/Runner.app'; - if (await fs.directory('$buildDir/ios').exists()) { - final regexp = RegExp(r'^Release(-.*)?-iphoneos$'); - yield* fs - .directory('$buildDir/ios') - .list() - .where((v) => regexp.hasMatch(v.basename)) - .map((e) => e.path); - } - - // iOS (ipa) - yield '$buildDir/ios/archive'; - - // iOS (ios-framework) - yield '$buildDir/ios/framework/Release'; + Log.taskCompleted(taskName); + } - // iOS in Fastlane - if (projectRoot == '/') { - yield 'ios/build'; - } else { - yield '$projectRoot/ios/build'; - } + /// Internal helper to discover Flutter-relevant debug files without altering + /// the current upload behavior. Intended for future use. + // ignore: unused_element + Future> _findFlutterRelevantDebugFilePaths() async { + final fs = injector.get(); + return await findFlutterRelevantDebugFilePaths( + fs: fs, + config: _configuration, + ); } + Stream _enumerateDebugSymbolPaths(FileSystem fs) => + enumerateDebugSearchRoots(fs: fs, config: _configuration); + Future> _enumerateSymbolFiles() async { final result = {}; final fs = injector.get(); diff --git a/lib/src/utils/flutter_debug_files.dart b/lib/src/utils/flutter_debug_files.dart new file mode 100644 index 00000000..f649effa --- /dev/null +++ b/lib/src/utils/flutter_debug_files.dart @@ -0,0 +1,159 @@ +import 'package:file/file.dart'; + +import '../configuration.dart'; + +/// Finds Flutter-relevant debug file paths for Android and Apple (iOS/macOS). +/// +/// Task 1: Provide the public API surface only. Discovery logic will be added +/// in subsequent tasks to enumerate Android `.symbols` files and Apple Mach-O +/// files within `.dSYM` bundles. The function is expected to return absolute, +/// de-duplicated paths. +Future> findFlutterRelevantDebugFilePaths({ + required FileSystem fs, + required Configuration config, +}) async { + final Set foundPaths = {}; + + Future collectAndroidSymbolsUnder(String rootPath) async { + if (rootPath.isEmpty) return; + + final directory = fs.directory(rootPath); + if (await directory.exists()) { + await for (final entity + in directory.list(recursive: true, followLinks: false)) { + if (entity is! File) continue; + final String basename = fs.path.basename(entity.path); + if (basename.startsWith('app') && + basename.endsWith('.symbols') && + !basename.contains('darwin')) { + foundPaths.add(fs.file(entity.path).absolute.path); + } + } + return; + } + + final file = fs.file(rootPath); + if (await file.exists()) { + final String basename = fs.path.basename(file.path); + if (basename.startsWith('app') && + basename.endsWith('.symbols') && + !basename.contains('darwin')) { + foundPaths.add(file.absolute.path); + } + } + } + + // First, scan the configured symbols folder (if any) + if (config.symbolsFolder.isNotEmpty) { + await collectAndroidSymbolsUnder(config.symbolsFolder); + } + + // Backward compatibility: also scan build folder if different + if (config.buildFilesFolder != config.symbolsFolder) { + await collectAndroidSymbolsUnder(config.buildFilesFolder); + } + + // Then, scan all current search roots used by the plugin + await for (final root in enumerateDebugSearchRoots(fs: fs, config: config)) { + await collectAndroidSymbolsUnder(root); + } + + Future collectAppleMachOUnder(String rootPath) async { + if (rootPath.isEmpty) return; + final dir = fs.directory(rootPath); + if (!await dir.exists()) return; + + await for (final entity in dir.list(recursive: true, followLinks: false)) { + if (entity is! Directory) continue; + final String basename = fs.path.basename(entity.path); + if (basename == 'App.framework.dSYM') { + final String machOPath = fs.path.join( + entity.path, + 'Contents', + 'Resources', + 'DWARF', + 'App', + ); + final File machOFile = fs.file(machOPath); + if (await machOFile.exists()) { + foundPaths.add(machOFile.absolute.path); + } + } + } + } + + // Search under the build directory directly to catch common iOS layouts + await collectAppleMachOUnder(config.buildFilesFolder); + + // Search all known roots (includes Fastlane ios/build) + await for (final root in enumerateDebugSearchRoots(fs: fs, config: config)) { + await collectAppleMachOUnder(root); + } + + return foundPaths; +} + +/// Enumerates the search roots used to discover native debug files, matching +/// the existing behavior used by the plugin when uploading debug files. +/// +/// This preserves current directories and files probed for: +/// - Android (apk, appbundle) +/// - Windows +/// - Linux +/// - macOS (app and framework) +/// - iOS (Runner.app, Release-*-iphoneos folders, archive, framework) +/// - iOS in Fastlane (ios/build) +Stream enumerateDebugSearchRoots({ + required FileSystem fs, + required Configuration config, +}) async* { + final String buildDir = config.buildFilesFolder; + final String projectRoot = fs.currentDirectory.path; + + // Android (apk, appbundle) + yield '$buildDir/app/outputs'; + yield '$buildDir/app/intermediates'; + + // Windows + for (final subdir in ['', '/x64', '/arm64']) { + yield '$buildDir/windows$subdir/runner/Release'; + } + // TODO: Consider removing once Windows symbols are collected automatically. + // Related to https://github.com/getsentry/sentry-dart-plugin/issues/173 + yield 'windows/flutter/ephemeral/flutter_windows.dll.pdb'; + + // Linux + for (final subdir in ['/x64', '/arm64']) { + yield '$buildDir/linux$subdir/release/bundle'; + } + + // macOS + yield '$buildDir/macos/Build/Products/Release'; + + // macOS (macOS-framework) + yield '$buildDir/macos/framework/Release'; + + // iOS + yield '$buildDir/ios/iphoneos/Runner.app'; + final iosDir = fs.directory('$buildDir/ios'); + if (await iosDir.exists()) { + final regexp = RegExp(r'^Release(-.*)?-iphoneos$'); + yield* iosDir + .list() + .where((entity) => regexp.hasMatch(fs.path.basename(entity.path))) + .map((entity) => entity.path); + } + + // iOS (ipa) + yield '$buildDir/ios/archive'; + + // iOS (ios-framework) + yield '$buildDir/ios/framework/Release'; + + // iOS in Fastlane + if (projectRoot == '/') { + yield 'ios/build'; + } else { + yield '$projectRoot/ios/build'; + } +} diff --git a/test/flutter_debug_files_test.dart b/test/flutter_debug_files_test.dart new file mode 100644 index 00000000..3e0fbe96 --- /dev/null +++ b/test/flutter_debug_files_test.dart @@ -0,0 +1,159 @@ +import 'package:file/memory.dart'; +import 'package:test/test.dart'; + +import 'package:sentry_dart_plugin/src/utils/flutter_debug_files.dart'; +import 'package:sentry_dart_plugin/src/configuration.dart'; + +void main() { + group('findFlutterRelevantDebugFilePaths', () { + test('returns Android .symbols only and Apple App.framework.dSYM Mach-O', + () async { + final fs = MemoryFileSystem(style: FileSystemStyle.posix); + final projectRootDir = fs.directory('/work')..createSync(recursive: true); + fs.currentDirectory = projectRootDir; + + final buildDir = '/work/build'; + final symbolsDir = '/work/symbols'; + + // Android .symbols files + fs + .file('$symbolsDir/app.android-arm.symbols') + .createSync(recursive: true); + fs + .file('$symbolsDir/app.android-arm64.symbols') + .createSync(recursive: true); + fs + .file('$symbolsDir/app.android-x64.symbols') + .createSync(recursive: true); + + // Apple App.framework.dSYM Mach-O + final appDsymMachO = + '$buildDir/ios/iphoneos/App.framework.dSYM/Contents/Resources/DWARF/App'; + fs.file(appDsymMachO).createSync(recursive: true); + + // Noise: other .dSYM bundles should be ignored + fs + .file( + '$buildDir/ios/iphoneos/Runner.app.dSYM/Contents/Resources/DWARF/Runner') + .createSync(recursive: true); + fs + .file( + '$buildDir/macos/Build/Products/Release/FlutterMacOS.framework.dSYM/Contents/Resources/DWARF/FlutterMacOS') + .createSync(recursive: true); + + final config = Configuration() + ..buildFilesFolder = buildDir + ..symbolsFolder = symbolsDir; + + final result = await findFlutterRelevantDebugFilePaths( + fs: fs, + config: config, + ); + + expect( + result, + containsAll([ + fs.path.normalize('/work/symbols/app.android-arm.symbols'), + fs.path.normalize('/work/symbols/app.android-arm64.symbols'), + fs.path.normalize('/work/symbols/app.android-x64.symbols'), + fs.path.normalize(appDsymMachO), + ])); + + // Ensure we did not include non-App.framework dSYMs + expect(result.any((p) => p.endsWith('/Runner')), isFalse); + expect(result.any((p) => p.endsWith('/FlutterMacOS')), isFalse); + + // Ensure deduplication and absoluteness + expect(result.length, 4); + for (final p in result) { + expect(p.startsWith('/'), isTrue, + reason: 'path should be absolute: $p'); + } + }); + + test('finds App.framework.dSYM under Fastlane ios/build path', () async { + final fs = MemoryFileSystem(style: FileSystemStyle.posix); + final projectRootDir = fs.directory('/project') + ..createSync(recursive: true); + fs.currentDirectory = projectRootDir; + + final buildDir = '/project/build'; + final symbolsDir = '/project/symbols'; + + // Fastlane path + final machO = + '/project/ios/build/App.framework.dSYM/Contents/Resources/DWARF/App'; + fs.file(machO).createSync(recursive: true); + + final config = Configuration() + ..buildFilesFolder = buildDir + ..symbolsFolder = symbolsDir; + + final result = await findFlutterRelevantDebugFilePaths( + fs: fs, + config: config, + ); + + expect(result, contains(fs.path.normalize(machO))); + }); + + test('finds App.framework.dSYM in macOS build products', () async { + final fs = MemoryFileSystem(style: FileSystemStyle.posix); + final projectRootDir = fs.directory('/macosproj') + ..createSync(recursive: true); + fs.currentDirectory = projectRootDir; + + final buildDir = '/macosproj/build'; + final symbolsDir = '/macosproj/symbols'; + + // macOS Products Release path + final macMachO = + '$buildDir/macos/Build/Products/Release/App.framework.dSYM/Contents/Resources/DWARF/App'; + fs.file(macMachO).createSync(recursive: true); + + // Noise: other dSYMs should be ignored + fs + .file( + '$buildDir/macos/Build/Products/Release/Runner.app.dSYM/Contents/Resources/DWARF/Runner') + .createSync(recursive: true); + fs + .file( + '$buildDir/macos/framework/Release/FlutterMacOS.framework.dSYM/Contents/Resources/DWARF/FlutterMacOS') + .createSync(recursive: true); + + final config = Configuration() + ..buildFilesFolder = buildDir + ..symbolsFolder = symbolsDir; + + final result = await findFlutterRelevantDebugFilePaths( + fs: fs, + config: config, + ); + + expect(result, contains(fs.path.normalize(macMachO))); + expect(result.any((p) => p.endsWith('/Runner')), isFalse); + expect(result.any((p) => p.endsWith('/FlutterMacOS')), isFalse); + }); + + test('returns empty set when no roots or symbols exist', () async { + final fs = MemoryFileSystem(style: FileSystemStyle.posix); + final projectRootDir = fs.directory('/empty') + ..createSync(recursive: true); + fs.currentDirectory = projectRootDir; + + final buildDir = '/empty/build'; + final symbolsDir = '/empty/symbols'; + + final config = Configuration() + ..buildFilesFolder = buildDir + ..symbolsFolder = symbolsDir; + + final result = await findFlutterRelevantDebugFilePaths( + fs: fs, + config: config, + ); + + expect(result, isEmpty); + }); + }); +} From 99dee07c4f599463f660ce6c0a81a5398267efa4 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Fri, 8 Aug 2025 17:24:42 +0200 Subject: [PATCH 2/5] Initial impl --- lib/sentry_dart_plugin.dart | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/sentry_dart_plugin.dart b/lib/sentry_dart_plugin.dart index 915fc22d..c53a1b2c 100644 --- a/lib/sentry_dart_plugin.dart +++ b/lib/sentry_dart_plugin.dart @@ -98,10 +98,8 @@ class SentryDartPlugin { await _executeAndLog('Failed to upload symbols', [...params, path]); } - final all = await _findFlutterRelevantDebugFilePaths(); - for (final path in all) { - print('Found path: $path'); - } + final _ = await _findFlutterRelevantDebugFilePaths(); + // TODO(buenaflor): upload these files with the mapping file Log.taskCompleted(taskName); } From 4449ef8dad6ece2f57de718311a6586782c6a91a Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Mon, 11 Aug 2025 12:23:02 +0200 Subject: [PATCH 3/5] Update --- lib/sentry_dart_plugin.dart | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/lib/sentry_dart_plugin.dart b/lib/sentry_dart_plugin.dart index c53a1b2c..6ac92eb8 100644 --- a/lib/sentry_dart_plugin.dart +++ b/lib/sentry_dart_plugin.dart @@ -87,7 +87,8 @@ class SentryDartPlugin { _addWait(params); final fs = injector.get(); - final debugSymbolPaths = _enumerateDebugSymbolPaths(fs); + final debugSymbolPaths = + enumerateDebugSearchRoots(fs: fs, config: _configuration); await for (final path in debugSymbolPaths) { if (await fs.directory(path).exists() || await fs.file(path).exists()) { await _executeAndLog('Failed to upload symbols', [...params, path]); @@ -98,25 +99,14 @@ class SentryDartPlugin { await _executeAndLog('Failed to upload symbols', [...params, path]); } - final _ = await _findFlutterRelevantDebugFilePaths(); - // TODO(buenaflor): upload these files with the mapping file - - Log.taskCompleted(taskName); - } - - /// Internal helper to discover Flutter-relevant debug files without altering - /// the current upload behavior. Intended for future use. - // ignore: unused_element - Future> _findFlutterRelevantDebugFilePaths() async { - final fs = injector.get(); - return await findFlutterRelevantDebugFilePaths( + final _ = await findFlutterRelevantDebugFilePaths( fs: fs, config: _configuration, ); - } + // TODO(buenaflor): in the follow up PR use these files with the dart symbol mapping file - Stream _enumerateDebugSymbolPaths(FileSystem fs) => - enumerateDebugSearchRoots(fs: fs, config: _configuration); + Log.taskCompleted(taskName); + } Future> _enumerateSymbolFiles() async { final result = {}; From 7fed2aa27a23233a1ffb6bc8ff55b0140d0abffb Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Mon, 11 Aug 2025 13:05:48 +0200 Subject: [PATCH 4/5] Update --- lib/src/utils/flutter_debug_files.dart | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/src/utils/flutter_debug_files.dart b/lib/src/utils/flutter_debug_files.dart index f649effa..7c821954 100644 --- a/lib/src/utils/flutter_debug_files.dart +++ b/lib/src/utils/flutter_debug_files.dart @@ -3,11 +3,7 @@ import 'package:file/file.dart'; import '../configuration.dart'; /// Finds Flutter-relevant debug file paths for Android and Apple (iOS/macOS). -/// -/// Task 1: Provide the public API surface only. Discovery logic will be added -/// in subsequent tasks to enumerate Android `.symbols` files and Apple Mach-O -/// files within `.dSYM` bundles. The function is expected to return absolute, -/// de-duplicated paths. +/// TODO(buenaflor): in the follow-up PR this should be coupled together with the dart symbol map Future> findFlutterRelevantDebugFilePaths({ required FileSystem fs, required Configuration config, From 46729fd2d415d16dfc0057464b7d225f3db5546a Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Mon, 11 Aug 2025 13:11:37 +0200 Subject: [PATCH 5/5] Fix --- .github/workflows/integration-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 43c33b6a..9a96a28c 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -47,7 +47,7 @@ jobs: distribution: 'temurin' java-version: '17' - - run: sudo apt-get install ninja-build libgtk-3-dev + - run: sudo apt-get update && sudo apt-get install -y ninja-build libgtk-3-dev if: runner.os == 'Linux' - run: (flutter --version)[0] | Out-File flutter.version