diff --git a/packages/shorebird_cli/lib/src/artifact_builder.dart b/packages/shorebird_cli/lib/src/artifact_builder.dart index 9c0ebe5b..4c88cb15 100644 --- a/packages/shorebird_cli/lib/src/artifact_builder.dart +++ b/packages/shorebird_cli/lib/src/artifact_builder.dart @@ -406,11 +406,13 @@ Either run `flutter pub get` manually, or follow the steps in ${cannotRunInVSCod Future buildElfAotSnapshot({ required String appDillPath, required String outFilePath, + List additionalArgs = const [], }) async { final arguments = [ '--deterministic', '--snapshot-kind=app-aot-elf', '--elf=$outFilePath', + ...additionalArgs, appDillPath, ]; diff --git a/packages/shorebird_cli/lib/src/commands/patch/aar_patcher.dart b/packages/shorebird_cli/lib/src/commands/patch/aar_patcher.dart index 2dddede1..723bdaf3 100644 --- a/packages/shorebird_cli/lib/src/commands/patch/aar_patcher.dart +++ b/packages/shorebird_cli/lib/src/commands/patch/aar_patcher.dart @@ -29,6 +29,7 @@ class AarPatcher extends Patcher { /// {@macro aar_patcher} AarPatcher({ required super.argResults, + required super.argParser, required super.flavor, required super.target, }); diff --git a/packages/shorebird_cli/lib/src/commands/patch/android_patcher.dart b/packages/shorebird_cli/lib/src/commands/patch/android_patcher.dart index f712a3f2..2291a2d3 100644 --- a/packages/shorebird_cli/lib/src/commands/patch/android_patcher.dart +++ b/packages/shorebird_cli/lib/src/commands/patch/android_patcher.dart @@ -29,6 +29,7 @@ class AndroidPatcher extends Patcher { /// {@macro android_patcher} AndroidPatcher({ required super.argResults, + required super.argParser, required super.flavor, required super.target, }); diff --git a/packages/shorebird_cli/lib/src/commands/patch/ios_framework_patcher.dart b/packages/shorebird_cli/lib/src/commands/patch/ios_framework_patcher.dart index 53646a99..a1d5ad22 100644 --- a/packages/shorebird_cli/lib/src/commands/patch/ios_framework_patcher.dart +++ b/packages/shorebird_cli/lib/src/commands/patch/ios_framework_patcher.dart @@ -34,6 +34,7 @@ class IosFrameworkPatcher extends Patcher { /// {@macro ios_framework_patcher} IosFrameworkPatcher({ required super.argResults, + required super.argParser, required super.flavor, required super.target, }); @@ -108,6 +109,9 @@ class IosFrameworkPatcher extends Patcher { throw ProcessExit(ExitCode.software.code); } try { + if (splitDebugInfoPath != null) { + Directory(splitDebugInfoPath!).createSync(recursive: true); + } await artifactBuilder.buildElfAotSnapshot( appDillPath: buildResult.kernelFile.path, outFilePath: p.join( @@ -115,6 +119,7 @@ class IosFrameworkPatcher extends Patcher { 'build', 'out.aot', ), + additionalArgs: IosPatcher.splitDebugInfoArgs(splitDebugInfoPath), ); } catch (error) { buildProgress.fail('$error'); @@ -271,6 +276,7 @@ class IosFrameworkPatcher extends Patcher { kernel: _appDillCopyPath, outputPath: _vmcodeOutputPath, workingDirectory: buildDirectory.path, + additionalArgs: IosPatcher.splitDebugInfoArgs(splitDebugInfoPath), ); } catch (error) { linkProgress.fail('Failed to link AOT files: $error'); diff --git a/packages/shorebird_cli/lib/src/commands/patch/ios_patcher.dart b/packages/shorebird_cli/lib/src/commands/patch/ios_patcher.dart index 6614d105..50c35fa4 100644 --- a/packages/shorebird_cli/lib/src/commands/patch/ios_patcher.dart +++ b/packages/shorebird_cli/lib/src/commands/patch/ios_patcher.dart @@ -42,6 +42,7 @@ class IosPatcher extends Patcher { /// {@macro ios_patcher} IosPatcher({ required super.argResults, + required super.argParser, required super.flavor, required super.target, }); @@ -52,6 +53,26 @@ class IosPatcher extends Patcher { String get _appDillCopyPath => p.join(buildDirectory.path, 'app.dill'); + /// The name of the split debug info file when the target is iOS. + static const splitDebugInfoFileName = 'app.ios-arm64.symbols'; + + /// The additional gen_snapshot arguments to use when building the patch with + /// `--split-debug-info`. + static List splitDebugInfoArgs(String? splitDebugInfoPath) { + return splitDebugInfoPath != null + ? [ + '--dwarf-stack-traces', + '--resolve-dwarf-paths', + '''--save-debugging-info=${saveDebuggingInfoPath(splitDebugInfoPath)}''', + ] + : []; + } + + /// The path to save the split debug info file. + static String saveDebuggingInfoPath(String directory) { + return p.join(p.absolute(directory), splitDebugInfoFileName); + } + @visibleForTesting double? lastBuildLinkPercentage; @@ -186,9 +207,13 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''', } try { + if (splitDebugInfoPath != null) { + Directory(splitDebugInfoPath!).createSync(recursive: true); + } await artifactBuilder.buildElfAotSnapshot( appDillPath: ipaBuildResult.kernelFile.path, outFilePath: _aotOutputPath, + additionalArgs: splitDebugInfoArgs(splitDebugInfoPath), ); } catch (error) { buildProgress.fail('$error'); @@ -398,6 +423,7 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''', workingDirectory: buildDirectory.path, kernel: kernelFile.path, dumpDebugInfoPath: dumpDebugInfoDir?.path, + additionalArgs: splitDebugInfoArgs(splitDebugInfoPath), ); } catch (error) { linkProgress.fail('Failed to link AOT files: $error'); diff --git a/packages/shorebird_cli/lib/src/commands/patch/patch_command.dart b/packages/shorebird_cli/lib/src/commands/patch/patch_command.dart index cea9075f..52d26bac 100644 --- a/packages/shorebird_cli/lib/src/commands/patch/patch_command.dart +++ b/packages/shorebird_cli/lib/src/commands/patch/patch_command.dart @@ -123,6 +123,10 @@ of the iOS app that is using this module.''', ..addOption( CommonArguments.publicKeyArg.name, help: CommonArguments.publicKeyArg.description, + ) + ..addOption( + CommonArguments.splitDebugInfoArg.name, + help: CommonArguments.splitDebugInfoArg.description, ); } @@ -178,24 +182,28 @@ NOTE: this is ${styleBold.wrap('not')} recommended. Asset changes cannot be incl case ReleaseType.android: return AndroidPatcher( argResults: results, + argParser: argParser, flavor: flavor, target: target, ); case ReleaseType.ios: return IosPatcher( argResults: results, + argParser: argParser, flavor: flavor, target: target, ); case ReleaseType.iosFramework: return IosFrameworkPatcher( argResults: results, + argParser: argParser, flavor: flavor, target: target, ); case ReleaseType.aar: return AarPatcher( argResults: results, + argParser: argParser, flavor: flavor, target: target, ); diff --git a/packages/shorebird_cli/lib/src/commands/patch/patcher.dart b/packages/shorebird_cli/lib/src/commands/patch/patcher.dart index ecf469a9..7611985f 100644 --- a/packages/shorebird_cli/lib/src/commands/patch/patcher.dart +++ b/packages/shorebird_cli/lib/src/commands/patch/patcher.dart @@ -8,6 +8,7 @@ import 'package:path/path.dart' as p; import 'package:shorebird_cli/src/code_push_client_wrapper.dart'; import 'package:shorebird_cli/src/common_arguments.dart'; import 'package:shorebird_cli/src/deployment_track.dart'; +import 'package:shorebird_cli/src/extensions/arg_results.dart'; import 'package:shorebird_cli/src/extensions/iterable.dart'; import 'package:shorebird_cli/src/metadata/metadata.dart'; import 'package:shorebird_cli/src/patch_diff_checker.dart'; @@ -24,6 +25,7 @@ import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart'; abstract class Patcher { /// {@macro patcher} Patcher({ + required this.argParser, required this.argResults, required this.flavor, required this.target, @@ -41,6 +43,9 @@ ${iOSLinkPercentageUrl.toLink()} '''; } + /// The parser for the arguments passed to the command. + final ArgParser argParser; + /// The arguments passed to the command. final ArgResults argResults; @@ -124,6 +129,14 @@ ${iOSLinkPercentageUrl.toLink()} /// step has not yet been run. double? get linkPercentage => null; + /// The value of `--split-debug-info-path` if specified. + String? get splitDebugInfoPath { + return argResults.findOption( + CommonArguments.splitDebugInfoArg.name, + argParser: argParser, + ); + } + /// The build directory of the respective shorebird project. Directory get buildDirectory { return Directory( diff --git a/packages/shorebird_cli/lib/src/commands/release/release_command.dart b/packages/shorebird_cli/lib/src/commands/release/release_command.dart index 2fa8c6a9..8effe0bd 100644 --- a/packages/shorebird_cli/lib/src/commands/release/release_command.dart +++ b/packages/shorebird_cli/lib/src/commands/release/release_command.dart @@ -132,6 +132,10 @@ of the iOS app that is using this module. (aar and ios-framework only)''', ..addOption( CommonArguments.publicKeyArg.name, help: CommonArguments.publicKeyArg.description, + ) + ..addOption( + CommonArguments.splitDebugInfoArg.name, + help: CommonArguments.splitDebugInfoArg.description, ); } diff --git a/packages/shorebird_cli/lib/src/common_arguments.dart b/packages/shorebird_cli/lib/src/common_arguments.dart index abfc50bf..f7c583df 100644 --- a/packages/shorebird_cli/lib/src/common_arguments.dart +++ b/packages/shorebird_cli/lib/src/common_arguments.dart @@ -99,6 +99,18 @@ The path for a public key .pem file that will be used to validate patch signatur name: 'private-key-path', description: ''' The path for a private key .pem file that will be used to sign the patch artifact. +''', + ); + + /// An argument that allows the user to specify a directory where program + /// symbols are stored. + static const splitDebugInfoArg = ArgumentDescriber( + name: 'split-debug-info', + description: ''' +In a release build, this flag reduces application size by storing Dart program symbols in a separate file on the host rather than +in the application. The value of the flag should be a directory where program symbol files can be stored for later use. These +symbol files contain the information needed to symbolize Dart stack traces. For an app built with this flag, the "flutter +symbolize" command with the right program symbol file is required to obtain a human readable stack trace. ''', ); } diff --git a/packages/shorebird_cli/lib/src/executables/aot_tools.dart b/packages/shorebird_cli/lib/src/executables/aot_tools.dart index 312d9cc3..231d915f 100644 --- a/packages/shorebird_cli/lib/src/executables/aot_tools.dart +++ b/packages/shorebird_cli/lib/src/executables/aot_tools.dart @@ -243,6 +243,7 @@ class AotTools { required String outputPath, String? workingDirectory, String? dumpDebugInfoPath, + List additionalArgs = const [], }) async { // We use the json lines format. https://jsonlines.org const linkJson = 'link.jsonl'; @@ -263,6 +264,10 @@ class AotTools { '--redirect-to=${p.join(outputDir, linkJson)}', ], if (dumpDebugInfoPath != null) '--dump-debug-info=$dumpDebugInfoPath', + if (additionalArgs.isNotEmpty) ...[ + '--', + ...additionalArgs, + ] ], workingDirectory: workingDirectory, ); diff --git a/packages/shorebird_cli/lib/src/extensions/arg_results.dart b/packages/shorebird_cli/lib/src/extensions/arg_results.dart index 30373e85..bafe962a 100644 --- a/packages/shorebird_cli/lib/src/extensions/arg_results.dart +++ b/packages/shorebird_cli/lib/src/extensions/arg_results.dart @@ -137,6 +137,7 @@ extension ForwardedArgs on ArgResults { ..._argsNamed(CommonArguments.dartDefineFromFileArg.name), ..._argsNamed(CommonArguments.buildNameArg.name), ..._argsNamed(CommonArguments.buildNumberArg.name), + ..._argsNamed(CommonArguments.splitDebugInfoArg.name), ], ); diff --git a/packages/shorebird_cli/test/src/artifact_builder_test.dart b/packages/shorebird_cli/test/src/artifact_builder_test.dart index e98cbfba..f7f276df 100644 --- a/packages/shorebird_cli/test/src/artifact_builder_test.dart +++ b/packages/shorebird_cli/test/src/artifact_builder_test.dart @@ -999,10 +999,35 @@ Please file a bug at https://github.com/shorebirdtech/shorebird/issues/new with ).thenReturn('gen_snapshot'); }); + test('passes additional args to gen_snapshot', () async { + await runWithOverrides( + () => builder.buildElfAotSnapshot( + appDillPath: '/app/dill/path', + outFilePath: '/path/to/out', + additionalArgs: ['--foo', 'bar'], + ), + ); + + verify( + () => shorebirdProcess.run( + 'gen_snapshot', + [ + '--deterministic', + '--snapshot-kind=app-aot-elf', + '--elf=/path/to/out', + '--foo', + 'bar', + '/app/dill/path', + ], + ), + ).called(1); + }); + group('when build fails', () { setUp(() { - when(() => buildProcessResult.exitCode) - .thenReturn(ExitCode.software.code); + when( + () => buildProcessResult.exitCode, + ).thenReturn(ExitCode.software.code); }); test('throws ArtifactBuildException', () { diff --git a/packages/shorebird_cli/test/src/artifact_manager_test.dart b/packages/shorebird_cli/test/src/artifact_manager_test.dart index 74e4efe2..50941fdb 100644 --- a/packages/shorebird_cli/test/src/artifact_manager_test.dart +++ b/packages/shorebird_cli/test/src/artifact_manager_test.dart @@ -284,6 +284,20 @@ void main() { expect(download.progress, emitsInOrder([1 / 3, 2 / 3, 3 / 3])); }); + + test('uses outputPath when specified', () async { + final tempDir = Directory.systemTemp.createTempSync(); + final outputPath = p.join(tempDir.path, 'output-file.txt'); + final download = await runWithOverrides( + () => artifactManager.startFileDownload( + Uri.parse('https://example.com'), + outputPath: outputPath, + ), + ); + final file = await download.file; + expect(file.path, equals(outputPath)); + expect(file.lengthSync(), equals(3)); + }); }); }); }); diff --git a/packages/shorebird_cli/test/src/commands/patch/aar_patcher_test.dart b/packages/shorebird_cli/test/src/commands/patch/aar_patcher_test.dart index 697e646c..41976756 100644 --- a/packages/shorebird_cli/test/src/commands/patch/aar_patcher_test.dart +++ b/packages/shorebird_cli/test/src/commands/patch/aar_patcher_test.dart @@ -35,6 +35,7 @@ void main() { const packageName = 'com.example.my_flutter_module'; const buildNumber = '1.0'; + late ArgParser argParser; late ArgResults argResults; late ArtifactBuilder artifactBuilder; late ArtifactManager artifactManager; @@ -93,6 +94,7 @@ void main() { }); setUp(() { + argParser = MockArgParser(); argResults = MockArgResults(); artifactBuilder = MockArtifactBuilder(); artifactManager = MockArtifactManager(); @@ -117,7 +119,12 @@ void main() { () => shorebirdEnv.getShorebirdProjectRoot(), ).thenReturn(projectRoot); - patcher = AarPatcher(argResults: argResults, flavor: null, target: null); + patcher = AarPatcher( + argParser: argParser, + argResults: argResults, + flavor: null, + target: null, + ); }); group('buildNumber', () { diff --git a/packages/shorebird_cli/test/src/commands/patch/android_patcher_test.dart b/packages/shorebird_cli/test/src/commands/patch/android_patcher_test.dart index b920771f..6721417b 100644 --- a/packages/shorebird_cli/test/src/commands/patch/android_patcher_test.dart +++ b/packages/shorebird_cli/test/src/commands/patch/android_patcher_test.dart @@ -37,6 +37,7 @@ import '../../mocks.dart'; void main() { group(AndroidPatcher, () { + late ArgParser argParser; late ArgResults argResults; late ArtifactBuilder artifactBuilder; late ArtifactManager artifactManager; @@ -116,6 +117,7 @@ void main() { }); setUp(() { + argParser = MockArgParser(); argResults = MockArgResults(); artifactBuilder = MockArtifactBuilder(); artifactManager = MockArtifactManager(); @@ -144,6 +146,7 @@ void main() { ).thenReturn(projectRoot); patcher = AndroidPatcher( + argParser: argParser, argResults: argResults, flavor: null, target: null, diff --git a/packages/shorebird_cli/test/src/commands/patch/ios_framework_patcher_test.dart b/packages/shorebird_cli/test/src/commands/patch/ios_framework_patcher_test.dart index 10fca40f..0fd81cea 100644 --- a/packages/shorebird_cli/test/src/commands/patch/ios_framework_patcher_test.dart +++ b/packages/shorebird_cli/test/src/commands/patch/ios_framework_patcher_test.dart @@ -42,6 +42,7 @@ void main() { IosFrameworkPatcher, () { late AotTools aotTools; + late ArgParser argParser; late ArgResults argResults; late ArtifactBuilder artifactBuilder; late ArtifactManager artifactManager; @@ -96,6 +97,7 @@ void main() { setUp(() { aotTools = MockAotTools(); + argParser = MockArgParser(); argResults = MockArgResults(); artifactBuilder = MockArtifactBuilder(); artifactManager = MockArtifactManager(); @@ -115,13 +117,11 @@ void main() { shorebirdValidator = MockShorebirdValidator(); xcodeBuild = MockXcodeBuild(); + when(() => argParser.options).thenReturn({}); + when(() => argResults['build-number']).thenReturn('1.0'); when(() => argResults.rest).thenReturn([]); when(() => argResults.wasParsed(any())).thenReturn(false); - when(() => argResults.wasParsed(CommonArguments.privateKeyArg.name)) - .thenReturn(false); - when(() => argResults.wasParsed(CommonArguments.publicKeyArg.name)) - .thenReturn(false); when(() => logger.progress(any())).thenReturn(progress); @@ -130,6 +130,7 @@ void main() { ).thenReturn(projectRoot); patcher = IosFrameworkPatcher( + argParser: argParser, argResults: argResults, flavor: null, target: null, @@ -356,6 +357,7 @@ void main() { () => artifactBuilder.buildElfAotSnapshot( appDillPath: any(named: 'appDillPath'), outFilePath: any(named: 'outFilePath'), + additionalArgs: any(named: 'additionalArgs'), ), ).thenThrow(const FileSystemException('error')); }); @@ -390,6 +392,7 @@ void main() { () => artifactBuilder.buildElfAotSnapshot( appDillPath: any(named: 'appDillPath'), outFilePath: any(named: 'outFilePath'), + additionalArgs: any(named: 'additionalArgs'), ), ).thenAnswer( (invocation) async => @@ -400,8 +403,44 @@ void main() { Directory( p.join(projectRoot.path, ArtifactManager.appXcframeworkName), ).createSync(recursive: true); - when(() => artifactManager.getAppXcframeworkDirectory()) - .thenReturn(projectRoot); + when( + () => artifactManager.getAppXcframeworkDirectory(), + ).thenReturn(projectRoot); + }); + + group('when --split-debug-info is provided', () { + final tempDir = Directory.systemTemp.createTempSync(); + final splitDebugInfoPath = p.join(tempDir.path, 'symbols'); + final splitDebugInfoFile = File( + p.join(splitDebugInfoPath, 'app.ios-arm64.symbols'), + ); + setUp(() { + when( + () => argResults.wasParsed( + CommonArguments.splitDebugInfoArg.name, + ), + ).thenReturn(true); + when( + () => argResults['split-debug-info'], + ).thenReturn(splitDebugInfoPath); + }); + + test('forwards --split-debug-info to builder', () async { + try { + await runWithOverrides(patcher.buildPatchArtifact); + } catch (_) {} + verify( + () => artifactBuilder.buildElfAotSnapshot( + appDillPath: any(named: 'appDillPath'), + outFilePath: any(named: 'outFilePath'), + additionalArgs: [ + '--dwarf-stack-traces', + '--resolve-dwarf-paths', + '--save-debugging-info=${splitDebugInfoFile.path}', + ], + ), + ).called(1); + }); }); group('when platform was specified via arg results rest', () { @@ -562,10 +601,12 @@ void main() { kernel: any(named: 'kernel'), outputPath: any(named: 'outputPath'), workingDirectory: any(named: 'workingDirectory'), + additionalArgs: any(named: 'additionalArgs'), ), ).thenAnswer((_) async => linkPercentage); - when(() => shorebirdEnv.flutterRevision) - .thenReturn(postLinkerFlutterRevision); + when( + () => shorebirdEnv.flutterRevision, + ).thenReturn(postLinkerFlutterRevision); when( () => shorebirdArtifacts.getArtifactPath( artifact: ShorebirdArtifact.analyzeSnapshot, @@ -628,6 +669,54 @@ void main() { }); }); + group('when --split-debug-info is provided', () { + final tempDirectory = Directory.systemTemp.createTempSync(); + final splitDebugInfoPath = p.join(tempDirectory.path, 'symbols'); + final splitDebugInfoFile = File( + p.join(splitDebugInfoPath, 'app.ios-arm64.symbols'), + ); + setUp(() { + when( + () => argResults.wasParsed( + CommonArguments.splitDebugInfoArg.name, + ), + ).thenReturn(true); + when( + () => argResults[CommonArguments.splitDebugInfoArg.name], + ).thenReturn(splitDebugInfoPath); + setUpProjectRootArtifacts(); + }); + + test('forwards correct args to linker', () async { + try { + await runWithOverrides( + () => patcher.createPatchArtifacts( + appId: appId, + releaseId: releaseId, + releaseArtifact: releaseArtifactFile, + ), + ); + } catch (_) {} + verify( + () => aotTools.link( + base: any(named: 'base'), + patch: any(named: 'patch'), + analyzeSnapshot: analyzeSnapshotFile.path, + genSnapshot: genSnapshotFile.path, + kernel: any(named: 'kernel'), + outputPath: any(named: 'outputPath'), + workingDirectory: any(named: 'workingDirectory'), + dumpDebugInfoPath: any(named: 'dumpDebugInfoPath'), + additionalArgs: [ + '--dwarf-stack-traces', + '--resolve-dwarf-paths', + '--save-debugging-info=${splitDebugInfoFile.path}', + ], + ), + ).called(1); + }); + }); + group('when call to aotTools.link fails', () { setUp(() { when( diff --git a/packages/shorebird_cli/test/src/commands/patch/ios_patcher_test.dart b/packages/shorebird_cli/test/src/commands/patch/ios_patcher_test.dart index 5d3e1e36..e1fae2dd 100644 --- a/packages/shorebird_cli/test/src/commands/patch/ios_patcher_test.dart +++ b/packages/shorebird_cli/test/src/commands/patch/ios_patcher_test.dart @@ -48,6 +48,7 @@ void main() { IosPatcher, () { late AotTools aotTools; + late ArgParser argParser; late ArgResults argResults; late ArtifactBuilder artifactBuilder; late ArtifactManager artifactManager; @@ -107,6 +108,7 @@ void main() { setUp(() { aotTools = MockAotTools(); + argParser = MockArgParser(); argResults = MockArgResults(); artifactBuilder = MockArtifactBuilder(); artifactManager = MockArtifactManager(); @@ -128,6 +130,8 @@ void main() { shorebirdValidator = MockShorebirdValidator(); xcodeBuild = MockXcodeBuild(); + when(() => argParser.options).thenReturn({}); + when(() => argResults.options).thenReturn([]); when(() => argResults.rest).thenReturn([]); when(() => argResults.wasParsed(any())).thenReturn(false); @@ -143,6 +147,7 @@ void main() { when(aotTools.isLinkDebugInfoSupported).thenAnswer((_) async => false); patcher = IosPatcher( + argParser: argParser, argResults: argResults, flavor: null, target: null, @@ -665,6 +670,7 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''', () => artifactBuilder.buildElfAotSnapshot( appDillPath: any(named: 'appDillPath'), outFilePath: any(named: 'outFilePath'), + additionalArgs: any(named: 'additionalArgs'), ), ).thenAnswer( (invocation) async => @@ -673,6 +679,41 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''', ); }); + group('when --split-debug-info is provided', () { + final tempDir = Directory.systemTemp.createTempSync(); + final splitDebugInfoPath = p.join(tempDir.path, 'symbols'); + final splitDebugInfoFile = File( + p.join(splitDebugInfoPath, 'app.ios-arm64.symbols'), + ); + setUp(() { + when( + () => argResults.wasParsed( + CommonArguments.splitDebugInfoArg.name, + ), + ).thenReturn(true); + when( + () => argResults[CommonArguments.splitDebugInfoArg.name], + ).thenReturn(splitDebugInfoPath); + }); + + test('forwards --split-debug-info to builder', () async { + try { + await runWithOverrides(patcher.buildPatchArtifact); + } catch (_) {} + verify( + () => artifactBuilder.buildElfAotSnapshot( + appDillPath: any(named: 'appDillPath'), + outFilePath: any(named: 'outFilePath'), + additionalArgs: [ + '--dwarf-stack-traces', + '--resolve-dwarf-paths', + '--save-debugging-info=${splitDebugInfoFile.path}', + ], + ), + ).called(1); + }); + }); + group('when releaseVersion is provided', () { test('forwards --build-name and --build-number to builder', () async { @@ -718,8 +759,9 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''', group('when the key pair is provided', () { setUp(() { - when(() => codeSigner.base64PublicKey(any())) - .thenReturn('public_key_encoded'); + when( + () => codeSigner.base64PublicKey(any()), + ).thenReturn('public_key_encoded'); }); test('calls the buildIpa passing the key', () async { @@ -730,13 +772,13 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''', final key = createTempFile('public.pem') ..writeAsStringSync('public_key'); - when(() => argResults[CommonArguments.publicKeyArg.name]) - .thenReturn(key.path); - when(() => argResults[CommonArguments.publicKeyArg.name]) - .thenReturn(key.path); - await runWithOverrides( - patcher.buildPatchArtifact, - ); + when( + () => argResults[CommonArguments.publicKeyArg.name], + ).thenReturn(key.path); + when( + () => argResults[CommonArguments.publicKeyArg.name], + ).thenReturn(key.path); + await runWithOverrides(patcher.buildPatchArtifact); verify( () => artifactBuilder.buildIpa( @@ -950,6 +992,7 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''', outputPath: any(named: 'outputPath'), workingDirectory: any(named: 'workingDirectory'), dumpDebugInfoPath: any(named: 'dumpDebugInfoPath'), + additionalArgs: any(named: 'additionalArgs'), ), ).thenAnswer((_) async => linkPercentage); when( @@ -983,8 +1026,9 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''', ), ), ); - when(() => shorebirdEnv.flutterRevision) - .thenReturn(postLinkerFlutterRevision); + when( + () => shorebirdEnv.flutterRevision, + ).thenReturn(postLinkerFlutterRevision); when( () => shorebirdArtifacts.getArtifactPath( artifact: ShorebirdArtifact.analyzeSnapshot, @@ -1080,6 +1124,54 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''', }); }); + group('when --split-debug-info is provided', () { + final tempDirectory = Directory.systemTemp.createTempSync(); + final splitDebugInfoPath = p.join(tempDirectory.path, 'symbols'); + final splitDebugInfoFile = File( + p.join(splitDebugInfoPath, 'app.ios-arm64.symbols'), + ); + setUp(() { + when( + () => argResults.wasParsed( + CommonArguments.splitDebugInfoArg.name, + ), + ).thenReturn(true); + when( + () => argResults[CommonArguments.splitDebugInfoArg.name], + ).thenReturn(splitDebugInfoPath); + setUpProjectRootArtifacts(); + }); + + test('forwards correct args to linker', () async { + try { + await runWithOverrides( + () => patcher.createPatchArtifacts( + appId: appId, + releaseId: releaseId, + releaseArtifact: releaseArtifactFile, + ), + ); + } catch (_) {} + verify( + () => aotTools.link( + base: any(named: 'base'), + patch: any(named: 'patch'), + analyzeSnapshot: analyzeSnapshotFile.path, + genSnapshot: genSnapshotFile.path, + kernel: any(named: 'kernel'), + outputPath: any(named: 'outputPath'), + workingDirectory: any(named: 'workingDirectory'), + dumpDebugInfoPath: any(named: 'dumpDebugInfoPath'), + additionalArgs: [ + '--dwarf-stack-traces', + '--resolve-dwarf-paths', + '--save-debugging-info=${splitDebugInfoFile.path}', + ], + ), + ).called(1); + }); + }); + group('when call to aotTools.link fails', () { setUp(() { when( @@ -1092,6 +1184,7 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''', outputPath: any(named: 'outputPath'), workingDirectory: any(named: 'workingDirectory'), dumpDebugInfoPath: any(named: 'dumpDebugInfoPath'), + additionalArgs: any(named: 'additionalArgs'), ), ).thenThrow(Exception('oops')); diff --git a/packages/shorebird_cli/test/src/commands/patch/patch_command_test.dart b/packages/shorebird_cli/test/src/commands/patch/patch_command_test.dart index 6a14c2cf..e4926521 100644 --- a/packages/shorebird_cli/test/src/commands/patch/patch_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/patch/patch_command_test.dart @@ -319,8 +319,9 @@ void main() { test('validates successfully', () async { await runWithOverrides(() => command.createPatch(patcher)); - verify(() => shorebirdValidator.validateFlavors(flavorArg: null)) - .called(1); + verify( + () => shorebirdValidator.validateFlavors(flavorArg: null), + ).called(1); }); }); @@ -333,8 +334,9 @@ void main() { test('validates successfully', () async { await runWithOverrides(() => command.createPatch(patcher)); - verify(() => shorebirdValidator.validateFlavors(flavorArg: flavor)) - .called(1); + verify( + () => shorebirdValidator.validateFlavors(flavorArg: flavor), + ).called(1); }); }); }); @@ -382,8 +384,9 @@ void main() { when( () => argResults.wasParsed(CommonArguments.publicKeyArg.name), ).thenReturn(false); - when(() => argResults[CommonArguments.privateKeyArg.name]) - .thenReturn(createTempFile('private.pem').path); + when( + () => argResults[CommonArguments.privateKeyArg.name], + ).thenReturn(createTempFile('private.pem').path); await expectLater( runWithOverrides(() => command.createPatch(patcher)), @@ -408,8 +411,9 @@ void main() { when( () => argResults.wasParsed(CommonArguments.publicKeyArg.name), ).thenReturn(true); - when(() => argResults[CommonArguments.publicKeyArg.name]) - .thenReturn(createTempFile('public.pem').path); + when( + () => argResults[CommonArguments.publicKeyArg.name], + ).thenReturn(createTempFile('public.pem').path); await expectLater( runWithOverrides(() => command.createPatch(patcher)), diff --git a/packages/shorebird_cli/test/src/commands/patch/patcher_test.dart b/packages/shorebird_cli/test/src/commands/patch/patcher_test.dart index f96fb156..187e458a 100644 --- a/packages/shorebird_cli/test/src/commands/patch/patcher_test.dart +++ b/packages/shorebird_cli/test/src/commands/patch/patcher_test.dart @@ -26,6 +26,7 @@ void main() { test('defaults to null', () { expect( _TestPatcher( + argParser: MockArgParser(), argResults: MockArgResults(), flavor: null, target: null, @@ -39,6 +40,7 @@ void main() { test('has no validations by default', () { expect( _TestPatcher( + argParser: MockArgParser(), argResults: MockArgResults(), flavor: null, target: null, @@ -59,6 +61,7 @@ void main() { test('returns an empty list', () { expect( _TestPatcher( + argParser: MockArgParser(), argResults: MockArgResults(), flavor: null, target: null, @@ -72,6 +75,7 @@ void main() { test('returns an empty list', () { expect( _TestPatcher( + argParser: MockArgParser(), argResults: argResults, flavor: null, target: null, @@ -90,6 +94,7 @@ void main() { test('returns an empty list', () { expect( _TestPatcher( + argParser: MockArgParser(), argResults: argResults, flavor: null, target: null, @@ -107,6 +112,7 @@ void main() { test('returns an empty list', () { expect( _TestPatcher( + argParser: MockArgParser(), argResults: argResults, flavor: null, target: null, @@ -122,6 +128,7 @@ void main() { expect( _TestPatcher( + argParser: MockArgParser(), argResults: argResults, flavor: null, target: null, @@ -150,6 +157,7 @@ void main() { test('returns an empty list', () { expect( _TestPatcher( + argParser: MockArgParser(), argResults: argResults, flavor: null, target: null, @@ -167,6 +175,7 @@ void main() { 'with correct args', () async { final args = MockArgResults(); final patcher = _TestPatcher( + argParser: MockArgParser(), argResults: args, flavor: null, target: null, @@ -219,6 +228,7 @@ void main() { class _TestPatcher extends Patcher { _TestPatcher({ + required super.argParser, required super.argResults, required super.flavor, required super.target, diff --git a/packages/shorebird_cli/test/src/executables/aot_tools_test.dart b/packages/shorebird_cli/test/src/executables/aot_tools_test.dart index d49bfe0c..9b1615a7 100644 --- a/packages/shorebird_cli/test/src/executables/aot_tools_test.dart +++ b/packages/shorebird_cli/test/src/executables/aot_tools_test.dart @@ -205,6 +205,74 @@ stderr: error''', ).called(1); }); + test('passes additional args to underlying process', () async { + when( + () => process.run( + aotToolsPath, + any(), + workingDirectory: any(named: 'workingDirectory'), + ), + ).thenAnswer((_) async { + return const ShorebirdProcessResult( + exitCode: 0, + stdout: '', + stderr: '', + ); + }); + when( + () => process.start( + aotToolsPath, + any(), + workingDirectory: any(named: 'workingDirectory'), + ), + ).thenAnswer( + (_) async { + final mockProcess = MockProcess(); + when(() => mockProcess.exitCode).thenAnswer((_) async => 0); + when( + () => mockProcess.stdout, + ).thenAnswer((_) => const Stream.empty()); + when( + () => mockProcess.stderr, + ).thenAnswer((_) => const Stream.empty()); + + return mockProcess; + }, + ); + await expectLater( + runWithOverrides( + () => aotTools.link( + base: base, + patch: patch, + analyzeSnapshot: analyzeSnapshot, + genSnapshot: genSnapshot, + kernel: kernel, + workingDirectory: workingDirectory.path, + outputPath: outputPath, + additionalArgs: ['--foo', 'bar'], + ), + ), + completes, + ); + verify( + () => process.start( + aotToolsPath, + [ + 'link', + '--base=$base', + '--patch=$patch', + '--analyze-snapshot=$analyzeSnapshot', + '--output=$outputPath', + '--verbose', + '--', + '--foo', + 'bar', + ], + workingDirectory: any(named: 'workingDirectory'), + ), + ).called(1); + }); + test('forwards stdout from aot_tools link to the logger', () async { when( () => process.start( diff --git a/packages/shorebird_cli/test/src/extensions/arg_results_test.dart b/packages/shorebird_cli/test/src/extensions/arg_results_test.dart index 6d46f37a..f9be9918 100644 --- a/packages/shorebird_cli/test/src/extensions/arg_results_test.dart +++ b/packages/shorebird_cli/test/src/extensions/arg_results_test.dart @@ -107,6 +107,10 @@ void main() { CommonArguments.buildNumberArg.name, help: CommonArguments.buildNumberArg.description, ) + ..addOption( + CommonArguments.splitDebugInfoArg.name, + help: CommonArguments.splitDebugInfoArg.description, + ) ..addMultiOption( 'platforms', allowed: ReleaseType.values.map((e) => e.cliName), @@ -230,5 +234,36 @@ void main() { ); }); }); + + group('when split-debug-info is provided before the --', () { + test('forwards it', () { + final args = [ + '--verbose', + '--split-debug-info=build/symbols', + ]; + final result = parser.parse(args); + expect(result.forwardedArgs, hasLength(1)); + expect( + result.forwardedArgs, + contains('--split-debug-info=build/symbols'), + ); + }); + }); + + group('when split-debug-info is provided after the --', () { + test('forwards it', () { + final args = [ + '--verbose', + '--', + '--split-debug-info=build/symbols', + ]; + final result = parser.parse(args); + expect(result.forwardedArgs, hasLength(1)); + expect( + result.forwardedArgs, + contains('--split-debug-info=build/symbols'), + ); + }); + }); }); } diff --git a/packages/shorebird_cli/test/src/mocks.dart b/packages/shorebird_cli/test/src/mocks.dart index 0732903c..9d8bcb8c 100644 --- a/packages/shorebird_cli/test/src/mocks.dart +++ b/packages/shorebird_cli/test/src/mocks.dart @@ -59,6 +59,8 @@ class MockAppleDevice extends Mock implements AppleDevice {} class MockArchiveDiffer extends Mock implements ArchiveDiffer {} +class MockArgParser extends Mock implements ArgParser {} + class MockArgResults extends Mock implements ArgResults {} class MockArtifactBuilder extends Mock implements ArtifactBuilder {} diff --git a/scripts/patch_e2e.sh b/scripts/patch_e2e.sh index 3ee06cb7..6a49a58b 100755 --- a/scripts/patch_e2e.sh +++ b/scripts/patch_e2e.sh @@ -37,7 +37,7 @@ echo "base_url: https://api-dev.shorebird.dev" >> shorebird.yaml APP_ID=$(cat shorebird.yaml | grep 'app_id:' | awk '{print $2}') # Create a new release on Android -shorebird release android --flutter-version=$FLUTTER_VERSION -v +shorebird release android --flutter-version=$FLUTTER_VERSION --split-debug-info=./build/symbols -v # Run the app on Android and ensure that the print statement is printed. while IFS= read line; do @@ -55,7 +55,7 @@ echo "lib/main.dart is now:" cat lib/main.dart # Create a patch -shorebird patch android -v +shorebird patch android --split-debug-info=./build/symbols -v # Run the app on Android and ensure that the original print statement is printed. while IFS= read line; do