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 333e1d43..3b970741 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 @@ -39,15 +39,32 @@ class IosFrameworkPatcher extends Patcher { required super.target, }); - String get _patchClassTableLinkInfoFile => - p.join(buildDirectory.path, 'ios', 'shorebird', 'App.ct.link'); + String get _aotOutputPath => + p.join(shorebirdEnv.buildDirectory.path, 'out.aot'); - String get _patchClassTableLinkDebugInfoPath => - p.join(buildDirectory.path, 'ios', 'shorebird', 'App.class_table.json'); + String get _patchClassTableLinkInfoFile => p.join( + shorebirdEnv.buildDirectory.path, + 'ios', + 'shorebird', + 'App.ct.link', + ); - String get _vmcodeOutputPath => p.join(buildDirectory.path, 'out.vmcode'); + String get _patchClassTableLinkDebugInfoPath => p.join( + shorebirdEnv.buildDirectory.path, + 'ios', + 'shorebird', + 'App.class_table.json', + ); - String get _appDillCopyPath => p.join(buildDirectory.path, 'app.dill'); + String get _vmcodeOutputPath => p.join( + shorebirdEnv.buildDirectory.path, + 'out.vmcode', + ); + + String get _appDillCopyPath => p.join( + shorebirdEnv.buildDirectory.path, + 'app.dill', + ); @override String get primaryReleaseArtifactArch => 'xcframework'; @@ -123,11 +140,7 @@ class IosFrameworkPatcher extends Patcher { } await artifactBuilder.buildElfAotSnapshot( appDillPath: buildResult.kernelFile.path, - outFilePath: p.join( - shorebirdEnv.getShorebirdProjectRoot()!.path, - 'build', - 'out.aot', - ), + outFilePath: _aotOutputPath, genSnapshotArtifact: ShorebirdArtifact.genSnapshotIos, additionalArgs: IosPatcher.splitDebugInfoArgs(splitDebugInfoPath), ); @@ -228,16 +241,19 @@ class IosFrameworkPatcher extends Patcher { // Copy the patch's class table link info file to the build directory // so that it can be used to generate a patch. File(_patchClassTableLinkInfoFile).copySync( - p.join(buildDirectory.path, 'out.ct.link'), + p.join(shorebirdEnv.buildDirectory.path, 'out.ct.link'), ); File(_patchClassTableLinkDebugInfoPath).copySync( - p.join(buildDirectory.path, 'out.class_table.json'), + p.join(shorebirdEnv.buildDirectory.path, 'out.class_table.json'), ); } - await _runLinker( - aotSnapshot: aotSnapshotFile, + await apple.runLinker( + kernelFile: File(_appDillCopyPath), releaseArtifact: releaseArtifactFile, + splitDebugInfoArgs: IosPatcher.splitDebugInfoArgs(splitDebugInfoPath), + aotOutputFile: File(_aotOutputPath), + vmCodeFile: File(_vmcodeOutputPath), ); } @@ -302,48 +318,4 @@ class IosFrameworkPatcher extends Patcher { xcodeVersion: await xcodeBuild.version(), ), ); - - Future _runLinker({ - required File aotSnapshot, - required File releaseArtifact, - }) async { - if (!aotSnapshot.existsSync()) { - logger.err('Unable to find patch AOT file at ${aotSnapshot.path}'); - throw ProcessExit(ExitCode.software.code); - } - - final analyzeSnapshot = File( - shorebirdArtifacts.getArtifactPath( - artifact: ShorebirdArtifact.analyzeSnapshotIos, - ), - ); - - if (!analyzeSnapshot.existsSync()) { - logger.err('Unable to find analyze_snapshot at ${analyzeSnapshot.path}'); - throw ProcessExit(ExitCode.software.code); - } - - final genSnapshot = shorebirdArtifacts.getArtifactPath( - artifact: ShorebirdArtifact.genSnapshotIos, - ); - - final linkProgress = logger.progress('Linking AOT files'); - try { - lastBuildLinkPercentage = await aotTools.link( - base: releaseArtifact.path, - patch: aotSnapshot.path, - analyzeSnapshot: analyzeSnapshot.path, - genSnapshot: genSnapshot, - kernel: _appDillCopyPath, - outputPath: _vmcodeOutputPath, - workingDirectory: buildDirectory.path, - additionalArgs: IosPatcher.splitDebugInfoArgs(splitDebugInfoPath), - ); - } on Exception catch (error) { - linkProgress.fail('Failed to link AOT files: $error'); - throw ProcessExit(ExitCode.software.code); - } - - linkProgress.complete(); - } } 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 a2aa7490..164ab1de 100644 --- a/packages/shorebird_cli/lib/src/commands/patch/ios_patcher.dart +++ b/packages/shorebird_cli/lib/src/commands/patch/ios_patcher.dart @@ -31,8 +31,6 @@ import 'package:shorebird_cli/src/third_party/flutter_tools/lib/flutter_tools.da import 'package:shorebird_code_push_client/shorebird_code_push_client.dart'; import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart'; -typedef _LinkResult = ({int exitCode, double? linkPercentage}); - /// {@template ios_patcher} /// Functions to create an iOS patch. /// {@endtemplate} @@ -45,17 +43,28 @@ class IosPatcher extends Patcher { required super.target, }); - String get _patchClassTableLinkInfoPath => - p.join(buildDirectory.path, 'ios', 'shorebird', 'App.ct.link'); + String get _patchClassTableLinkInfoPath => p.join( + shorebirdEnv.buildDirectory.path, + 'ios', + 'shorebird', + 'App.ct.link', + ); - String get _patchClassTableLinkDebugInfoPath => - p.join(buildDirectory.path, 'ios', 'shorebird', 'App.class_table.json'); + String get _patchClassTableLinkDebugInfoPath => p.join( + shorebirdEnv.buildDirectory.path, + 'ios', + 'shorebird', + 'App.class_table.json', + ); - String get _aotOutputPath => p.join(buildDirectory.path, 'out.aot'); + String get _aotOutputPath => + p.join(shorebirdEnv.buildDirectory.path, 'out.aot'); - String get _vmcodeOutputPath => p.join(buildDirectory.path, 'out.vmcode'); + String get _vmcodeOutputPath => + p.join(shorebirdEnv.buildDirectory.path, 'out.vmcode'); - String get _appDillCopyPath => p.join(buildDirectory.path, 'app.dill'); + String get _appDillCopyPath => + p.join(shorebirdEnv.buildDirectory.path, 'app.dill'); /// The name of the split debug info file when the target is iOS. static const splitDebugInfoFileName = 'app.ios-arm64.symbols'; @@ -316,16 +325,19 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''', // Copy the patch's class table link info file to the build directory // so that it can be used to generate a patch. File(_patchClassTableLinkInfoPath).copySync( - p.join(buildDirectory.path, 'out.ct.link'), + p.join(shorebirdEnv.buildDirectory.path, 'out.ct.link'), ); File(_patchClassTableLinkDebugInfoPath).copySync( - p.join(buildDirectory.path, 'out.class_table.json'), + p.join(shorebirdEnv.buildDirectory.path, 'out.class_table.json'), ); } - final (:exitCode, :linkPercentage) = await _runLinker( - releaseArtifact: releaseArtifactFile, + final (:exitCode, :linkPercentage) = await apple.runLinker( kernelFile: File(_appDillCopyPath), + releaseArtifact: releaseArtifactFile, + splitDebugInfoArgs: splitDebugInfoArgs(splitDebugInfoPath), + aotOutputFile: File(_aotOutputPath), + vmCodeFile: File(_vmcodeOutputPath), ); if (exitCode != ExitCode.success.code) throw ProcessExit(exitCode); if (linkPercentage != null && @@ -424,66 +436,4 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''', xcodeVersion: await xcodeBuild.version(), ), ); - - Future<_LinkResult> _runLinker({ - required File releaseArtifact, - required File kernelFile, - }) async { - final patch = File(_aotOutputPath); - - if (!patch.existsSync()) { - logger.err('Unable to find patch AOT file at ${patch.path}'); - return (exitCode: ExitCode.software.code, linkPercentage: null); - } - - final analyzeSnapshot = File( - shorebirdArtifacts.getArtifactPath( - artifact: ShorebirdArtifact.analyzeSnapshotIos, - ), - ); - - if (!analyzeSnapshot.existsSync()) { - logger.err('Unable to find analyze_snapshot at ${analyzeSnapshot.path}'); - return (exitCode: ExitCode.software.code, linkPercentage: null); - } - - final genSnapshot = shorebirdArtifacts.getArtifactPath( - artifact: ShorebirdArtifact.genSnapshotIos, - ); - - final linkProgress = logger.progress('Linking AOT files'); - double? linkPercentage; - final dumpDebugInfoDir = await aotTools.isLinkDebugInfoSupported() - ? Directory.systemTemp.createTempSync() - : null; - - Future dumpDebugInfo() async { - if (dumpDebugInfoDir == null) return; - - final debugInfoZip = await dumpDebugInfoDir.zipToTempFile(); - debugInfoZip.copySync(p.join('build', debugInfoFile.path)); - logger.detail('Link debug info saved to ${debugInfoFile.path}'); - } - - try { - linkPercentage = await aotTools.link( - base: releaseArtifact.path, - patch: patch.path, - analyzeSnapshot: analyzeSnapshot.path, - genSnapshot: genSnapshot, - outputPath: _vmcodeOutputPath, - workingDirectory: buildDirectory.path, - kernel: kernelFile.path, - dumpDebugInfoPath: dumpDebugInfoDir?.path, - additionalArgs: splitDebugInfoArgs(splitDebugInfoPath), - ); - } on Exception catch (error) { - linkProgress.fail('Failed to link AOT files: $error'); - return (exitCode: ExitCode.software.code, linkPercentage: null); - } finally { - await dumpDebugInfo(); - } - linkProgress.complete(); - return (exitCode: ExitCode.success.code, linkPercentage: linkPercentage); - } } diff --git a/packages/shorebird_cli/lib/src/commands/patch/macos_patcher.dart b/packages/shorebird_cli/lib/src/commands/patch/macos_patcher.dart index 3bf9bd7b..f8d96416 100644 --- a/packages/shorebird_cli/lib/src/commands/patch/macos_patcher.dart +++ b/packages/shorebird_cli/lib/src/commands/patch/macos_patcher.dart @@ -43,17 +43,20 @@ class MacosPatcher extends Patcher { // The elf snapshot built for Apple Silicon macs. String get _arm64AotOutputPath => p.join( - buildDirectory.path, + shorebirdEnv.buildDirectory.path, 'out.arm64.aot', ); // The elf snapshot built for Intel macs. String get _x64AotOutputPath => p.join( - buildDirectory.path, + shorebirdEnv.buildDirectory.path, 'out.x64.aot', ); - String get _appDillCopyPath => p.join(buildDirectory.path, 'app.dill'); + String get _appDillCopyPath => p.join( + shorebirdEnv.buildDirectory.path, + 'app.dill', + ); @override ReleaseType get releaseType => ReleaseType.macos; 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 ed5f67c4..f3ef1262 100644 --- a/packages/shorebird_cli/lib/src/commands/patch/patch_command.dart +++ b/packages/shorebird_cli/lib/src/commands/patch/patch_command.dart @@ -552,7 +552,7 @@ Please re-run the release command for this version or create a new release.'''); trackSummary, if (patcher.linkPercentage != null && patcher.linkPercentage! < Patcher.minLinkPercentage) - '''🔍 Debug Info: ${lightCyan.wrap(patcher.debugInfoFile.path)}''', + '''🔍 Debug Info: ${lightCyan.wrap(Patcher.debugInfoFile.path)}''', ]; logger.info( diff --git a/packages/shorebird_cli/lib/src/commands/patch/patcher.dart b/packages/shorebird_cli/lib/src/commands/patch/patcher.dart index 03d5b5ee..07deeb38 100644 --- a/packages/shorebird_cli/lib/src/commands/patch/patcher.dart +++ b/packages/shorebird_cli/lib/src/commands/patch/patcher.dart @@ -140,16 +140,9 @@ More info: ${troubleshootingUrl.toLink()}. ); } - /// The build directory of the respective shorebird project. - Directory get buildDirectory { - return Directory( - p.join(shorebirdEnv.getShorebirdProjectRoot()!.path, 'build'), - ); - } - /// The path to the output file for the debug info. - File get debugInfoFile { - return File(p.join(buildDirectory.path, 'patch-debug.zip')); + static File get debugInfoFile { + return File(p.join(shorebirdEnv.buildDirectory.path, 'patch-debug.zip')); } /// Extracts the --build-name and --build-number from the --release-version diff --git a/packages/shorebird_cli/lib/src/platform/apple.dart b/packages/shorebird_cli/lib/src/platform/apple.dart index c89d0ccd..cda0edaf 100644 --- a/packages/shorebird_cli/lib/src/platform/apple.dart +++ b/packages/shorebird_cli/lib/src/platform/apple.dart @@ -1,9 +1,15 @@ import 'dart:io'; import 'package:collection/collection.dart'; +import 'package:io/io.dart'; import 'package:path/path.dart' as p; import 'package:pub_semver/pub_semver.dart'; import 'package:scoped_deps/scoped_deps.dart'; +import 'package:shorebird_cli/src/archive/directory_archive.dart'; +import 'package:shorebird_cli/src/commands/patch/patcher.dart'; +import 'package:shorebird_cli/src/executables/aot_tools.dart'; +import 'package:shorebird_cli/src/logging/shorebird_logger.dart'; +import 'package:shorebird_cli/src/shorebird_artifacts.dart'; import 'package:shorebird_cli/src/shorebird_env.dart'; import 'package:xml/xml.dart'; @@ -17,6 +23,10 @@ enum ApplePlatform { macos, } +/// A record containing the exit code and optionally link percentage +/// returned by `runLinker`. +typedef LinkResult = ({int exitCode, double? linkPercentage}); + /// {@template missing_xcode_project_exception} /// Thrown when the Flutter project does not have iOS configured as a platform. /// {@endtemplate} @@ -170,6 +180,74 @@ class Apple { .toSet(); } + /// Runs the linking step to minimize differences between patch and release + /// and maximize code that can be executed on the CPU. + Future runLinker({ + required File kernelFile, + required File releaseArtifact, + required List splitDebugInfoArgs, + required File aotOutputFile, + required File vmCodeFile, + }) async { + final patch = aotOutputFile; + final buildDirectory = shorebirdEnv.buildDirectory; + + if (!patch.existsSync()) { + logger.err('Unable to find patch AOT file at ${patch.path}'); + return (exitCode: ExitCode.software.code, linkPercentage: null); + } + + final analyzeSnapshot = File( + shorebirdArtifacts.getArtifactPath( + artifact: ShorebirdArtifact.analyzeSnapshotIos, + ), + ); + + if (!analyzeSnapshot.existsSync()) { + logger.err('Unable to find analyze_snapshot at ${analyzeSnapshot.path}'); + return (exitCode: ExitCode.software.code, linkPercentage: null); + } + + final genSnapshot = shorebirdArtifacts.getArtifactPath( + artifact: ShorebirdArtifact.genSnapshotIos, + ); + + final linkProgress = logger.progress('Linking AOT files'); + double? linkPercentage; + final dumpDebugInfoDir = await aotTools.isLinkDebugInfoSupported() + ? Directory.systemTemp.createTempSync() + : null; + + Future dumpDebugInfo() async { + if (dumpDebugInfoDir == null) return; + + final debugInfoZip = await dumpDebugInfoDir.zipToTempFile(); + debugInfoZip.copySync(p.join('build', Patcher.debugInfoFile.path)); + logger.detail('Link debug info saved to ${Patcher.debugInfoFile.path}'); + } + + try { + linkPercentage = await aotTools.link( + base: releaseArtifact.path, + patch: patch.path, + analyzeSnapshot: analyzeSnapshot.path, + genSnapshot: genSnapshot, + outputPath: vmCodeFile.path, + workingDirectory: buildDirectory.path, + kernel: kernelFile.path, + dumpDebugInfoPath: dumpDebugInfoDir?.path, + additionalArgs: splitDebugInfoArgs, + ); + } on Exception catch (error) { + linkProgress.fail('Failed to link AOT files: $error'); + return (exitCode: ExitCode.software.code, linkPercentage: null); + } finally { + await dumpDebugInfo(); + } + linkProgress.complete(); + return (exitCode: ExitCode.success.code, linkPercentage: linkPercentage); + } + /// Parses the .xcscheme file to determine if it was created for an app /// extension. We don't want to include these schemes as app flavors. /// diff --git a/packages/shorebird_cli/lib/src/shorebird_env.dart b/packages/shorebird_cli/lib/src/shorebird_env.dart index 3f1b8684..eb75a55d 100644 --- a/packages/shorebird_cli/lib/src/shorebird_env.dart +++ b/packages/shorebird_cli/lib/src/shorebird_env.dart @@ -114,6 +114,11 @@ class ShorebirdEnv { return File(p.join(getFlutterProjectRoot()!.path, 'macos', 'Podfile.lock')); } + /// The build directory of the current shorebird project. + Directory get buildDirectory { + return Directory(p.join(getFlutterProjectRoot()!.path, 'build')); + } + /// The `shorebird.yaml` file for this project. File getShorebirdYamlFile({required Directory cwd}) { return File(p.join(cwd.path, 'shorebird.yaml')); diff --git a/packages/shorebird_cli/test/src/archive_analysis/file_set_diff_test.dart b/packages/shorebird_cli/test/src/archive_analysis/file_set_diff_test.dart index cbf075c8..54c034aa 100644 --- a/packages/shorebird_cli/test/src/archive_analysis/file_set_diff_test.dart +++ b/packages/shorebird_cli/test/src/archive_analysis/file_set_diff_test.dart @@ -76,5 +76,32 @@ void main() { expect(fileSetDiff.isEmpty, isFalse); expect(fileSetDiff.isNotEmpty, isTrue); }); + + test('supports value based equality comparisons', () { + // For the sake of testing equality comparisons avoid using const. + // ignore: prefer_const_constructors + final fileSetDiffA = FileSetDiff( + addedPaths: const {'a'}, + changedPaths: const {'b'}, + removedPaths: const {'c'}, + ); + // For the sake of testing equality comparisons avoid using const. + // ignore: prefer_const_constructors + final fileSetDiffB = FileSetDiff( + addedPaths: const {'a'}, + changedPaths: const {'b'}, + removedPaths: const {'c'}, + ); + // For the sake of testing equality comparisons avoid using const. + // ignore: prefer_const_constructors + final fileSetDiffC = FileSetDiff( + addedPaths: const {'c'}, + changedPaths: const {'b'}, + removedPaths: const {'a'}, + ); + expect(fileSetDiffA, equals(fileSetDiffB)); + expect(fileSetDiffA, isNot(equals(fileSetDiffC))); + expect(fileSetDiffB, isNot(equals(fileSetDiffC))); + }); }); } 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 0715eaf8..64535d5b 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 Apple apple; late ArgParser argParser; late ArgResults argResults; late ArtifactBuilder artifactBuilder; @@ -69,6 +70,7 @@ void main() { body, values: { aotToolsRef.overrideWith(() => aotTools), + appleRef.overrideWith(() => apple), artifactBuilderRef.overrideWith(() => artifactBuilder), artifactManagerRef.overrideWith(() => artifactManager), codePushClientWrapperRef.overrideWith(() => codePushClientWrapper), @@ -97,6 +99,7 @@ void main() { }); setUp(() { + apple = MockApple(); aotTools = MockAotTools(); argParser = MockArgParser(); argResults = MockArgResults(); @@ -126,6 +129,9 @@ void main() { when(() => logger.progress(any())).thenReturn(progress); + when(() => shorebirdEnv.buildDirectory).thenReturn( + Directory(p.join(projectRoot.path, 'build')), + ); when( () => shorebirdEnv.getShorebirdProjectRoot(), ).thenReturn(projectRoot); @@ -634,17 +640,19 @@ void main() { )..createSync(recursive: true); when( - () => aotTools.link( - base: any(named: 'base'), - patch: any(named: 'patch'), - analyzeSnapshot: any(named: 'analyzeSnapshot'), - genSnapshot: any(named: 'genSnapshot'), - kernel: any(named: 'kernel'), - outputPath: any(named: 'outputPath'), - workingDirectory: any(named: 'workingDirectory'), - additionalArgs: any(named: 'additionalArgs'), + () => apple.runLinker( + kernelFile: any(named: 'kernelFile'), + aotOutputFile: any(named: 'aotOutputFile'), + releaseArtifact: any(named: 'releaseArtifact'), + splitDebugInfoArgs: any(named: 'splitDebugInfoArgs'), + vmCodeFile: any(named: 'vmCodeFile'), ), - ).thenAnswer((_) async => linkPercentage); + ).thenAnswer( + (_) async => ( + exitCode: ExitCode.success.code, + linkPercentage: linkPercentage + ), + ); when( aotTools.isGeneratePatchDiffBaseSupported, ).thenAnswer((_) async => false); @@ -663,144 +671,6 @@ void main() { ).thenReturn(genSnapshotFile.path); }); - group('when linking fails', () { - group('when aot snapshot does not exist', () { - test('logs error and exits with code 70', () async { - await expectLater( - () => runWithOverrides( - () => patcher.createPatchArtifacts( - appId: appId, - releaseId: releaseId, - releaseArtifact: releaseArtifactFile, - ), - ), - exitsWithCode(ExitCode.software), - ); - - verify( - () => logger.err( - any(that: startsWith('Unable to find patch AOT file at')), - ), - ).called(1); - }); - }); - - group('when analyzeSnapshot binary does not exist', () { - setUp(() { - when( - () => shorebirdArtifacts.getArtifactPath( - artifact: ShorebirdArtifact.analyzeSnapshotIos, - ), - ).thenReturn(''); - setUpProjectRootArtifacts(); - }); - - test('logs error and exits with code 70', () async { - await expectLater( - () => runWithOverrides( - () => patcher.createPatchArtifacts( - appId: appId, - releaseId: releaseId, - releaseArtifact: releaseArtifactFile, - ), - ), - exitsWithCode(ExitCode.software), - ); - - verify( - () => logger.err('Unable to find analyze_snapshot at '), - ).called(1); - }); - }); - - 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, - ), - ); - } on Exception { - // ignore - } - 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( - () => aotTools.link( - base: any(named: 'base'), - patch: any(named: 'patch'), - analyzeSnapshot: any(named: 'analyzeSnapshot'), - genSnapshot: any(named: 'genSnapshot'), - kernel: any(named: 'kernel'), - outputPath: any(named: 'outputPath'), - workingDirectory: any(named: 'workingDirectory'), - ), - ).thenThrow(Exception('oops')); - - setUpProjectRootArtifacts(); - }); - - test('logs error and exits with code 70', () async { - await expectLater( - () => runWithOverrides( - () => patcher.createPatchArtifacts( - appId: appId, - releaseId: releaseId, - releaseArtifact: releaseArtifactFile, - ), - ), - exitsWithCode(ExitCode.software), - ); - - verify( - () => progress.fail( - 'Failed to link AOT files: Exception: oops', - ), - ).called(1); - }); - }); - }); - group('when generate patch diff base is supported', () { setUp(() { when( @@ -855,6 +725,56 @@ void main() { setUpProjectRootArtifacts(); }); + test('calls runLinker with correct arguments', () async { + await runWithOverrides( + () => patcher.createPatchArtifacts( + appId: appId, + releaseId: releaseId, + releaseArtifact: releaseArtifactFile, + ), + ); + + verify( + () => apple.runLinker( + kernelFile: any( + named: 'kernelFile', + that: isA().having( + (f) => f.path, + 'path', + p.join(projectRoot.path, 'build', 'app.dill'), + ), + ), + aotOutputFile: any( + named: 'aotOutputFile', + that: isA().having( + (f) => f.path, + 'path', + p.join(projectRoot.path, 'build', 'out.aot'), + ), + ), + releaseArtifact: any( + named: 'releaseArtifact', + that: isA().having( + (f) => f.path, + 'path', + endsWith( + p.join('ios-arm64', 'App.framework', 'App'), + ), + ), + ), + vmCodeFile: any( + named: 'vmCodeFile', + that: isA().having( + (f) => f.path, + 'path', + p.join(projectRoot.path, 'build', 'out.vmcode'), + ), + ), + splitDebugInfoArgs: [], + ), + ).called(1); + }); + test('returns linked patch artifact in patch bundle', () async { final patchBundle = await runWithOverrides( () => patcher.createPatchArtifacts( 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 4b62a532..23745de2 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 @@ -142,6 +142,9 @@ void main() { when( () => shorebirdEnv.getShorebirdProjectRoot(), ).thenReturn(projectRoot); + when(() => shorebirdEnv.buildDirectory).thenReturn( + Directory(p.join(projectRoot.path, 'build')), + ); when(aotTools.isLinkDebugInfoSupported).thenAnswer((_) async => false); @@ -1001,18 +1004,19 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''', )..createSync(recursive: true); when( - () => aotTools.link( - base: any(named: 'base'), - patch: any(named: 'patch'), - analyzeSnapshot: any(named: 'analyzeSnapshot'), - genSnapshot: any(named: 'genSnapshot'), - kernel: any(named: 'kernel'), - outputPath: any(named: 'outputPath'), - workingDirectory: any(named: 'workingDirectory'), - dumpDebugInfoPath: any(named: 'dumpDebugInfoPath'), - additionalArgs: any(named: 'additionalArgs'), + () => apple.runLinker( + kernelFile: any(named: 'kernelFile'), + aotOutputFile: any(named: 'aotOutputFile'), + releaseArtifact: any(named: 'releaseArtifact'), + splitDebugInfoArgs: any(named: 'splitDebugInfoArgs'), + vmCodeFile: any(named: 'vmCodeFile'), ), - ).thenAnswer((_) async => linkPercentage); + ).thenAnswer( + (_) async => ( + exitCode: ExitCode.success.code, + linkPercentage: linkPercentage + ), + ); when( aotTools.isGeneratePatchDiffBaseSupported, ).thenAnswer((_) async => false); @@ -1095,144 +1099,6 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''', ).called(1); }); }); - - group('when aot snapshot does not exist', () { - test('logs error and exits with code 70', () async { - await expectLater( - () => runWithOverrides( - () => patcher.createPatchArtifacts( - appId: appId, - releaseId: releaseId, - releaseArtifact: releaseArtifactFile, - ), - ), - exitsWithCode(ExitCode.software), - ); - - verify( - () => logger.err( - any(that: startsWith('Unable to find patch AOT file at')), - ), - ).called(1); - }); - }); - - group('when analyzeSnapshot binary does not exist', () { - setUp(() { - when( - () => shorebirdArtifacts.getArtifactPath( - artifact: ShorebirdArtifact.analyzeSnapshotIos, - ), - ).thenReturn(''); - setUpProjectRootArtifacts(); - }); - - test('logs error and exits with code 70', () async { - await expectLater( - () => runWithOverrides( - () => patcher.createPatchArtifacts( - appId: appId, - releaseId: releaseId, - releaseArtifact: releaseArtifactFile, - ), - ), - exitsWithCode(ExitCode.software), - ); - - verify( - () => logger.err('Unable to find analyze_snapshot at '), - ).called(1); - }); - }); - - 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, - ), - ); - } on Exception { - // ignore - } - 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( - () => aotTools.link( - base: any(named: 'base'), - patch: any(named: 'patch'), - analyzeSnapshot: any(named: 'analyzeSnapshot'), - genSnapshot: any(named: 'genSnapshot'), - kernel: any(named: 'kernel'), - outputPath: any(named: 'outputPath'), - workingDirectory: any(named: 'workingDirectory'), - dumpDebugInfoPath: any(named: 'dumpDebugInfoPath'), - additionalArgs: any(named: 'additionalArgs'), - ), - ).thenThrow(Exception('oops')); - - setUpProjectRootArtifacts(); - }); - - test('logs error and exits with code 70', () async { - await expectLater( - () => runWithOverrides( - () => patcher.createPatchArtifacts( - appId: appId, - releaseId: releaseId, - releaseArtifact: releaseArtifactFile, - ), - ), - exitsWithCode(ExitCode.software), - ); - - verify( - () => progress.fail( - 'Failed to link AOT files: Exception: oops', - ), - ).called(1); - }); - }); }); group('when generate patch diff base is supported', () { @@ -1418,119 +1284,6 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''', }); }); - group('when isLinkDebugInfoSupported is true', () { - setUp(() { - when( - aotTools.isLinkDebugInfoSupported, - ).thenAnswer((_) async => true); - }); - - test('dumps debug info', () async { - await runWithOverrides( - () => patcher.createPatchArtifacts( - appId: appId, - releaseId: releaseId, - releaseArtifact: releaseArtifactFile, - ), - ); - verify( - () => aotTools.link( - base: any(named: 'base'), - patch: any(named: 'patch'), - analyzeSnapshot: any(named: 'analyzeSnapshot'), - genSnapshot: any(named: 'genSnapshot'), - kernel: any(named: 'kernel'), - outputPath: any(named: 'outputPath'), - workingDirectory: any(named: 'workingDirectory'), - dumpDebugInfoPath: any( - named: 'dumpDebugInfoPath', - that: isNotNull, - ), - ), - ).called(1); - verify( - () => logger.detail( - any( - that: contains( - 'Link debug info saved to', - ), - ), - ), - ).called(1); - }); - - group('when aot_tools link fails', () { - setUp(() { - when( - () => aotTools.link( - base: any(named: 'base'), - patch: any(named: 'patch'), - analyzeSnapshot: any(named: 'analyzeSnapshot'), - genSnapshot: any(named: 'genSnapshot'), - kernel: any(named: 'kernel'), - outputPath: any(named: 'outputPath'), - workingDirectory: any(named: 'workingDirectory'), - dumpDebugInfoPath: any( - named: 'dumpDebugInfoPath', - that: isNotNull, - ), - ), - ).thenThrow(Exception('oops')); - }); - - test('dumps debug info and logs', () async { - await expectLater( - () => runWithOverrides( - () => patcher.createPatchArtifacts( - appId: appId, - releaseId: releaseId, - releaseArtifact: releaseArtifactFile, - ), - ), - exitsWithCode(ExitCode.software), - ); - verify( - () => logger.detail( - any( - that: contains( - 'Link debug info saved to', - ), - ), - ), - ).called(1); - }); - }); - }); - - group('when isLinkDebugInfoSupported is false', () { - setUp(() { - when(aotTools.isLinkDebugInfoSupported) - .thenAnswer((_) async => false); - }); - - test('does not pass dumpDebugInfoPath to aotTools.link', - () async { - await runWithOverrides( - () => patcher.createPatchArtifacts( - appId: appId, - releaseId: releaseId, - releaseArtifact: releaseArtifactFile, - ), - ); - verify( - () => aotTools.link( - base: any(named: 'base'), - patch: any(named: 'patch'), - analyzeSnapshot: any(named: 'analyzeSnapshot'), - genSnapshot: any(named: 'genSnapshot'), - kernel: any(named: 'kernel'), - outputPath: any(named: 'outputPath'), - workingDirectory: any(named: 'workingDirectory'), - ), - ).called(1); - }); - }); - group('when code signing the patch', () { setUp(() { final privateKey = File( diff --git a/packages/shorebird_cli/test/src/commands/patch/macos_patcher_test.dart b/packages/shorebird_cli/test/src/commands/patch/macos_patcher_test.dart index 08b05995..7fcb6349 100644 --- a/packages/shorebird_cli/test/src/commands/patch/macos_patcher_test.dart +++ b/packages/shorebird_cli/test/src/commands/patch/macos_patcher_test.dart @@ -150,6 +150,9 @@ void main() { when( () => shorebirdEnv.getShorebirdProjectRoot(), ).thenReturn(projectRoot); + when( + () => shorebirdEnv.buildDirectory, + ).thenReturn(Directory(p.join(projectRoot.path, 'build'))); when( () => shorebirdEnv.flutterRevision, ).thenReturn('5c1dcc19ebcee3565c65262dd95970186e4d81cc'); 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 692dd525..3bb3900d 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 @@ -5,6 +5,7 @@ import 'package:collection/collection.dart'; import 'package:equatable/equatable.dart'; import 'package:mason_logger/mason_logger.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:path/path.dart' as p; import 'package:scoped_deps/scoped_deps.dart'; import 'package:shorebird_cli/src/archive_analysis/archive_analysis.dart'; import 'package:shorebird_cli/src/artifact_builder.dart'; @@ -710,20 +711,24 @@ void main() { group('when has link percentage', () { const linkPercentage = 42.1337; - final debugInfoFile = File('debug-info.txt'); + late Directory buildDirectory; setUp(() { + buildDirectory = Directory.systemTemp.createTempSync(); + when(() => shorebirdEnv.buildDirectory).thenReturn(buildDirectory); when(() => patcher.linkPercentage).thenReturn(linkPercentage); - when(() => patcher.debugInfoFile).thenReturn(debugInfoFile); }); test('logs correct summary', () async { + final debugInfoFile = File( + p.join(buildDirectory.path, 'patch-debug.zip'), + ); final expectedSummary = [ '''📱 App: ${lightCyan.wrap(appDisplayName)} ${lightCyan.wrap('($appId)')}''', 'đŸ“Ļ Release Version: ${lightCyan.wrap(releaseVersion)}', '''đŸ•šī¸ Platform: ${lightCyan.wrap(patcher.releaseType.releasePlatform.displayName)} ${lightCyan.wrap('[arm32 (42 B)]')}''', 'đŸŸĸ Track: ${lightCyan.wrap('Stable')}', - '''🔍 Debug Info: ${lightCyan.wrap(patcher.debugInfoFile.path)}''', + '''🔍 Debug Info: ${lightCyan.wrap(debugInfoFile.path)}''', ]; await expectLater( runWithOverrides( diff --git a/packages/shorebird_cli/test/src/platform/apple_test.dart b/packages/shorebird_cli/test/src/platform/apple_test.dart index f2d83ece..8c798fe0 100644 --- a/packages/shorebird_cli/test/src/platform/apple_test.dart +++ b/packages/shorebird_cli/test/src/platform/apple_test.dart @@ -1,9 +1,14 @@ import 'dart:io'; +import 'package:io/io.dart'; +import 'package:mason_logger/mason_logger.dart'; import 'package:mocktail/mocktail.dart'; import 'package:path/path.dart' as p; import 'package:scoped_deps/scoped_deps.dart'; +import 'package:shorebird_cli/src/executables/executables.dart'; +import 'package:shorebird_cli/src/logging/logging.dart'; import 'package:shorebird_cli/src/platform/platform.dart'; +import 'package:shorebird_cli/src/shorebird_artifacts.dart'; import 'package:shorebird_cli/src/shorebird_env.dart'; import 'package:test/test.dart'; @@ -18,21 +23,34 @@ void main() { }); group(Apple, () { - late ShorebirdEnv shorebirdEnv; + late AotTools aotTools; late Apple apple; + late Progress progress; + late ShorebirdArtifacts shorebirdArtifacts; + late ShorebirdLogger logger; + late ShorebirdEnv shorebirdEnv; R runWithOverrides(R Function() body) { return runScoped( body, values: { + aotToolsRef.overrideWith(() => aotTools), + loggerRef.overrideWith(() => logger), + shorebirdArtifactsRef.overrideWith(() => shorebirdArtifacts), shorebirdEnvRef.overrideWith(() => shorebirdEnv), }, ); } setUp(() { - shorebirdEnv = MockShorebirdEnv(); + aotTools = MockAotTools(); apple = Apple(); + progress = MockProgress(); + logger = MockShorebirdLogger(); + shorebirdArtifacts = MockShorebirdArtifacts(); + shorebirdEnv = MockShorebirdEnv(); + + when(() => logger.progress(any())).thenReturn(progress); }); group(MissingXcodeProjectException, () { @@ -280,5 +298,238 @@ To add macOS, run "flutter create . --platforms macos"''', }); }); }); + + group('runLinker', () { + const linkPercentage = 42.0; + + late Directory buildDirectory; + late File aotOutputFile; + late File analyzeSnapshotFile; + late File genSnapshotFile; + + setUp(() { + buildDirectory = Directory.systemTemp.createTempSync(); + aotOutputFile = File(p.join(buildDirectory.path, 'out.aot')) + ..createSync(); + analyzeSnapshotFile = File( + p.join(buildDirectory.path, 'analyze_snapshot'), + )..createSync(); + genSnapshotFile = File( + p.join(buildDirectory.path, 'gen_snapshot'), + )..createSync(); + + when( + () => shorebirdArtifacts.getArtifactPath( + artifact: ShorebirdArtifact.analyzeSnapshotIos, + ), + ).thenReturn(analyzeSnapshotFile.path); + when( + () => shorebirdArtifacts.getArtifactPath( + artifact: ShorebirdArtifact.genSnapshotIos, + ), + ).thenReturn(genSnapshotFile.path); + + when(() => shorebirdEnv.buildDirectory).thenReturn(buildDirectory); + + when( + () => aotTools.link( + base: any(named: 'base'), + patch: any(named: 'patch'), + analyzeSnapshot: any(named: 'analyzeSnapshot'), + genSnapshot: any(named: 'genSnapshot'), + kernel: any(named: 'kernel'), + outputPath: any(named: 'outputPath'), + workingDirectory: any(named: 'workingDirectory'), + additionalArgs: any(named: 'additionalArgs'), + dumpDebugInfoPath: any(named: 'dumpDebugInfoPath'), + ), + ).thenAnswer((_) async => linkPercentage); + when( + () => aotTools.isLinkDebugInfoSupported(), + ).thenAnswer((_) async => false); + }); + + group('when aot snapshot does not exist', () { + test('logs error and exits with code 70', () async { + final result = await runWithOverrides( + () => apple.runLinker( + aotOutputFile: File('missing'), + kernelFile: File('missing'), + releaseArtifact: File('missing'), + vmCodeFile: File('missing'), + splitDebugInfoArgs: [], + ), + ); + expect(result.exitCode, equals(ExitCode.software.code)); + expect(result.linkPercentage, isNull); + + verify( + () => logger.err( + any(that: startsWith('Unable to find patch AOT file at')), + ), + ).called(1); + }); + }); + + group('when analyzeSnapshot binary does not exist', () { + setUp(() { + when( + () => shorebirdArtifacts.getArtifactPath( + artifact: ShorebirdArtifact.analyzeSnapshotIos, + ), + ).thenReturn(''); + }); + + test('logs error and exits with code 70', () async { + final result = await runWithOverrides( + () => apple.runLinker( + aotOutputFile: aotOutputFile, + kernelFile: File('missing'), + releaseArtifact: File('missing'), + vmCodeFile: File('missing'), + splitDebugInfoArgs: [], + ), + ); + expect(result.exitCode, equals(ExitCode.software.code)); + expect(result.linkPercentage, isNull); + + verify( + () => logger.err('Unable to find analyze_snapshot at '), + ).called(1); + }); + }); + + 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'), + ); + final splitDebugInfoArgs = [ + '--dwarf-stack-traces', + '--resolve-dwarf-paths', + '--save-debugging-info=${splitDebugInfoFile.path}', + ]; + + test('forwards correct args to linker', () async { + try { + await runWithOverrides( + () => apple.runLinker( + aotOutputFile: aotOutputFile, + kernelFile: File('missing'), + releaseArtifact: File('missing'), + vmCodeFile: File('missing'), + splitDebugInfoArgs: splitDebugInfoArgs, + ), + ); + } on Exception { + // ignore + } + 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: splitDebugInfoArgs, + ), + ).called(1); + }); + }); + + group('when isLinkDebugInfoSupported is true', () { + setUp(() { + when( + aotTools.isLinkDebugInfoSupported, + ).thenAnswer((_) async => true); + }); + + test('dumps debug info', () async { + await runWithOverrides( + () => apple.runLinker( + aotOutputFile: aotOutputFile, + kernelFile: File('missing'), + releaseArtifact: File('missing'), + vmCodeFile: File('missing'), + splitDebugInfoArgs: [], + ), + ); + verify( + () => aotTools.link( + base: any(named: 'base'), + patch: any(named: 'patch'), + analyzeSnapshot: any(named: 'analyzeSnapshot'), + genSnapshot: any(named: 'genSnapshot'), + kernel: any(named: 'kernel'), + outputPath: any(named: 'outputPath'), + workingDirectory: any(named: 'workingDirectory'), + dumpDebugInfoPath: any( + named: 'dumpDebugInfoPath', + that: isNotNull, + ), + ), + ).called(1); + verify( + () => logger.detail( + any(that: contains('Link debug info saved to')), + ), + ).called(1); + }); + }); + + group('when call to aotTools.link fails', () { + setUp(() { + when( + () => aotTools.link( + base: any(named: 'base'), + patch: any(named: 'patch'), + analyzeSnapshot: any(named: 'analyzeSnapshot'), + genSnapshot: any(named: 'genSnapshot'), + kernel: any(named: 'kernel'), + outputPath: any(named: 'outputPath'), + workingDirectory: any(named: 'workingDirectory'), + ), + ).thenThrow(Exception('oops')); + }); + + test('logs error and exits with code 70', () async { + await runWithOverrides( + () => apple.runLinker( + aotOutputFile: aotOutputFile, + kernelFile: File('missing'), + releaseArtifact: File('missing'), + vmCodeFile: File('missing'), + splitDebugInfoArgs: [], + ), + ); + + verify( + () => progress.fail( + 'Failed to link AOT files: Exception: oops', + ), + ).called(1); + }); + }); + + group('when call to aotTools.link succeeds', () { + test('completes and exits with code 0', () async { + final result = await runWithOverrides( + () => apple.runLinker( + aotOutputFile: aotOutputFile, + kernelFile: File('missing'), + releaseArtifact: File('missing'), + vmCodeFile: File('missing'), + splitDebugInfoArgs: [], + ), + ); + expect(result.exitCode, equals(ExitCode.success.code)); + expect(result.linkPercentage, equals(linkPercentage)); + }); + }); + }); }); } diff --git a/packages/shorebird_cli/test/src/shorebird_env_test.dart b/packages/shorebird_cli/test/src/shorebird_env_test.dart index e92e48ae..3fd9b04a 100644 --- a/packages/shorebird_cli/test/src/shorebird_env_test.dart +++ b/packages/shorebird_cli/test/src/shorebird_env_test.dart @@ -216,6 +216,24 @@ void main() { }); }); + group('buildDirectory', () { + test('returns correct path', () { + final tempDir = Directory.systemTemp.createTempSync(); + File(p.join(tempDir.path, 'pubspec.yaml')).createSync(recursive: true); + Directory(p.join(tempDir.path, 'build')).createSync(recursive: true); + final buildDirectory = IOOverrides.runZoned( + () => runWithOverrides( + () => shorebirdEnv.buildDirectory, + ), + getCurrentDirectory: () => tempDir, + ); + expect( + buildDirectory.path, + equals(p.join(tempDir.path, 'build')), + ); + }); + }); + group('macosPodfileLockFile', () { test('returns correct path', () { final tempDir = Directory.systemTemp.createTempSync();