diff --git a/packages/shorebird_cli/bin/shorebird.dart b/packages/shorebird_cli/bin/shorebird.dart index 91882649..0eeaa0fb 100644 --- a/packages/shorebird_cli/bin/shorebird.dart +++ b/packages/shorebird_cli/bin/shorebird.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:scoped/scoped.dart'; import 'package:shorebird_cli/src/android_sdk.dart'; import 'package:shorebird_cli/src/android_studio.dart'; +import 'package:shorebird_cli/src/artifact_builder.dart'; import 'package:shorebird_cli/src/artifact_manager.dart'; import 'package:shorebird_cli/src/auth/auth.dart'; import 'package:shorebird_cli/src/cache.dart'; @@ -35,6 +36,7 @@ Future main(List args) async { androidSdkRef, androidStudioRef, aotToolsRef, + artifactBuilderRef, artifactManagerRef, authRef, bundletoolRef, diff --git a/packages/shorebird_cli/lib/src/artifact_builder.dart b/packages/shorebird_cli/lib/src/artifact_builder.dart new file mode 100644 index 00000000..6c9a1b83 --- /dev/null +++ b/packages/shorebird_cli/lib/src/artifact_builder.dart @@ -0,0 +1,135 @@ +import 'dart:io'; + +import 'package:mason_logger/mason_logger.dart'; +import 'package:scoped/scoped.dart'; +import 'package:shorebird_cli/src/logger.dart'; +import 'package:shorebird_cli/src/os/operating_system_interface.dart'; +import 'package:shorebird_cli/src/platform/platform.dart'; +import 'package:shorebird_cli/src/shorebird_android_artifacts.dart'; +import 'package:shorebird_cli/src/shorebird_env.dart'; +import 'package:shorebird_cli/src/shorebird_process.dart'; + +/// Used to wrap code that invokes `flutter build` with Shorebird's fork of +/// Flutter. +typedef ShorebirdBuildCommand = Future Function(); + +/// {@template artifact_build_exception} +/// Thrown when a build fails. +/// {@endtemplate} +class ArtifactBuildException implements Exception { + /// {@macro artifact_build_exception} + ArtifactBuildException(this.message); + + /// Information about the build failure. + final String message; +} + +/// A reference to a [ArtifactBuilder] instance. +final artifactBuilderRef = create(ArtifactBuilder.new); + +/// The [ArtifactBuilder] instance available in the current zone. +ArtifactBuilder get artifactBuilder => read(artifactBuilderRef); + +/// @{template artifact_builder} +/// Builds aabs, ipas, and other artifacts produced by `flutter build`. +/// @{endtemplate} +class ArtifactBuilder { + /// Builds an aab using `flutter build appbundle`. Runs `flutter pub get` with + /// the system installation of Flutter to reset + /// `.dart_tool/package_config.json` after the build completes or fails. + Future buildAppBundle({ + String? flavor, + String? target, + Iterable? targetPlatforms, + List argResultsRest = const [], + }) async { + await _runShorebirdBuildCommand(() async { + const executable = 'flutter'; + final targetPlatformArgs = targetPlatforms?.targetPlatformArg; + final arguments = [ + 'build', + 'appbundle', + '--release', + if (flavor != null) '--flavor=$flavor', + if (target != null) '--target=$target', + if (targetPlatformArgs != null) '--target-platform=$targetPlatformArgs', + ...argResultsRest, + ]; + + final result = await process.run( + executable, + arguments, + runInShell: true, + ); + + if (result.exitCode != ExitCode.success.code) { + throw ArtifactBuildException( + 'Failed to build: ${result.stderr}', + ); + } + }); + + final projectRoot = shorebirdEnv.getShorebirdProjectRoot()!; + try { + return shorebirdAndroidArtifacts.findAab( + project: projectRoot, + flavor: flavor, + ); + } on MultipleArtifactsFoundException catch (error) { + throw ArtifactBuildException( + 'Build succeeded, but it generated multiple AABs in the ' + 'build directory. ${error.foundArtifacts.map((e) => e.path)}', + ); + } on ArtifactNotFoundException catch (error) { + throw ArtifactBuildException( + 'Build succeeded, but could not find the AAB in the build directory. ' + 'Expected to find ${error.artifactName}', + ); + } + } + + /// A wrapper around [command] (which runs a `flutter build` command with + /// Shorebird's fork of Flutter) with a try/finally that runs + /// `flutter pub get` with the system installation of Flutter to reset + /// `.dart_tool/package_config.json` to the system Flutter. + Future _runShorebirdBuildCommand(ShorebirdBuildCommand command) async { + try { + await command(); + } finally { + await _systemFlutterPubGet(); + } + } + + /// This is a hack to reset `.dart_tool/package_config.json` to point to the + /// Flutter SDK on the user's PATH. This is necessary because Flutter commands + /// run by shorebird update the package_config.json file to point to + /// shorebird's version of Flutter, which confuses VS Code. See + /// https://github.com/shorebirdtech/shorebird/issues/1101 for more info. + Future _systemFlutterPubGet() async { + const executable = 'flutter'; + if (osInterface.which(executable) == null) { + // If the user doesn't have Flutter on their PATH, then we can't run + // `flutter pub get` with the system Flutter. + return; + } + + final arguments = ['--no-version-check', 'pub', 'get', '--offline']; + + final result = await process.run( + executable, + arguments, + runInShell: true, + useVendedFlutter: false, + ); + + if (result.exitCode != ExitCode.success.code) { + logger.warn( + ''' +Build was successful, but `flutter pub get` failed to run after the build completed. You may see unexpected behavior in VS Code. + +Either run `flutter pub get` manually, or follow the steps in ${link(uri: Uri.parse('https://docs.shorebird.dev/troubleshooting#i-installed-shorebird-and-now-i-cant-run-my-app-in-vs-code'))}. +''', + ); + } + } +} diff --git a/packages/shorebird_cli/lib/src/commands/build/build_app_bundle_command.dart b/packages/shorebird_cli/lib/src/commands/build/build_app_bundle_command.dart index e1c4c749..35d02246 100644 --- a/packages/shorebird_cli/lib/src/commands/build/build_app_bundle_command.dart +++ b/packages/shorebird_cli/lib/src/commands/build/build_app_bundle_command.dart @@ -1,9 +1,9 @@ import 'package:mason_logger/mason_logger.dart'; import 'package:path/path.dart' as p; +import 'package:shorebird_cli/src/artifact_builder.dart'; import 'package:shorebird_cli/src/command.dart'; import 'package:shorebird_cli/src/doctor.dart'; import 'package:shorebird_cli/src/logger.dart'; -import 'package:shorebird_cli/src/shorebird_build_mixin.dart'; import 'package:shorebird_cli/src/shorebird_validator.dart'; /// {@template build_app_bundle_command} @@ -11,7 +11,7 @@ import 'package:shorebird_cli/src/shorebird_validator.dart'; /// `shorebird build appbundle` /// Build an Android App Bundle file from your app. /// {@endtemplate} -class BuildAppBundleCommand extends ShorebirdCommand with ShorebirdBuildMixin { +class BuildAppBundleCommand extends ShorebirdCommand { /// {@macro build_app_bundle_command} BuildAppBundleCommand() { argParser @@ -48,8 +48,8 @@ class BuildAppBundleCommand extends ShorebirdCommand with ShorebirdBuildMixin { final target = results['target'] as String?; final buildProgress = logger.progress('Building appbundle'); try { - await buildAppBundle(flavor: flavor, target: target); - } on BuildException catch (error) { + await artifactBuilder.buildAppBundle(flavor: flavor, target: target); + } on ArtifactBuildException catch (error) { buildProgress.fail(error.message); return ExitCode.software.code; } diff --git a/packages/shorebird_cli/test/src/artifact_builder_test.dart b/packages/shorebird_cli/test/src/artifact_builder_test.dart new file mode 100644 index 00000000..62061438 --- /dev/null +++ b/packages/shorebird_cli/test/src/artifact_builder_test.dart @@ -0,0 +1,314 @@ +import 'dart:io'; + +import 'package:mason_logger/mason_logger.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:scoped/scoped.dart'; +import 'package:shorebird_cli/src/artifact_builder.dart'; +import 'package:shorebird_cli/src/logger.dart'; +import 'package:shorebird_cli/src/os/operating_system_interface.dart'; +import 'package:shorebird_cli/src/platform/platform.dart'; +import 'package:shorebird_cli/src/shorebird_android_artifacts.dart'; +import 'package:shorebird_cli/src/shorebird_env.dart'; +import 'package:shorebird_cli/src/shorebird_process.dart'; +import 'package:test/test.dart'; + +import 'fakes.dart'; +import 'mocks.dart'; + +void main() { + group(ArtifactBuilder, () { + late Logger logger; + late OperatingSystemInterface operatingSystemInterface; + late ShorebirdAndroidArtifacts shorebirdAndroidArtifacts; + late ShorebirdEnv shorebirdEnv; + late ShorebirdProcess shorebirdProcess; + late ShorebirdProcessResult buildProcessResult; + late ShorebirdProcessResult pubGetProcessResult; + late ArtifactBuilder builder; + + R runWithOverrides(R Function() body) { + return runScoped( + body, + values: { + loggerRef.overrideWith(() => logger), + osInterfaceRef.overrideWith(() => operatingSystemInterface), + processRef.overrideWith(() => shorebirdProcess), + shorebirdEnvRef.overrideWith(() => shorebirdEnv), + shorebirdAndroidArtifactsRef + .overrideWith(() => shorebirdAndroidArtifacts), + }, + ); + } + + setUpAll(() { + registerFallbackValue(FakeShorebirdProcess()); + registerFallbackValue(Directory('')); + }); + + setUp(() { + buildProcessResult = MockProcessResult(); + logger = MockLogger(); + operatingSystemInterface = MockOperatingSystemInterface(); + pubGetProcessResult = MockProcessResult(); + shorebirdAndroidArtifacts = MockShorebirdAndroidArtifacts(); + shorebirdEnv = MockShorebirdEnv(); + shorebirdProcess = MockShorebirdProcess(); + + when( + () => shorebirdProcess.run( + 'flutter', + ['--no-version-check', 'pub', 'get', '--offline'], + runInShell: any(named: 'runInShell'), + useVendedFlutter: false, + ), + ).thenAnswer((_) async => pubGetProcessResult); + when(() => pubGetProcessResult.exitCode) + .thenReturn(ExitCode.success.code); + when( + () => shorebirdProcess.run( + any(), + any(), + runInShell: any(named: 'runInShell'), + ), + ).thenAnswer((_) async => buildProcessResult); + when(() => buildProcessResult.exitCode).thenReturn(ExitCode.success.code); + when(() => logger.progress(any())).thenReturn(MockProgress()); + when(() => logger.info(any())).thenReturn(null); + when(() => operatingSystemInterface.which('flutter')) + .thenReturn('/path/to/flutter'); + when(() => shorebirdEnv.flutterRevision).thenReturn('1234'); + when(shorebirdEnv.getShorebirdProjectRoot).thenReturn(Directory('')); + when( + () => shorebirdAndroidArtifacts.findAab( + project: any(named: 'project'), + flavor: any(named: 'flavor'), + ), + ).thenReturn(File('app-release.aab')); + + builder = ArtifactBuilder(); + }); + + group('buildAppBundle', () { + test('invokes the correct flutter build command', () async { + await runWithOverrides(() => builder.buildAppBundle()); + + verify( + () => shorebirdProcess.run( + 'flutter', + ['build', 'appbundle', '--release'], + runInShell: any(named: 'runInShell'), + environment: any(named: 'environment'), + ), + ).called(1); + }); + + test('forward arguments to flutter build', () async { + await runWithOverrides( + () => builder.buildAppBundle( + flavor: 'flavor', + target: 'target', + targetPlatforms: [Arch.arm64], + argResultsRest: ['--foo', 'bar'], + ), + ); + + verify( + () => shorebirdProcess.run( + 'flutter', + [ + 'build', + 'appbundle', + '--release', + '--flavor=flavor', + '--target=target', + '--target-platform=android-arm64', + '--foo', + 'bar', + ], + runInShell: any(named: 'runInShell'), + ), + ).called(1); + }); + + group('when multiple artifacts are found', () { + setUp(() { + when( + () => shorebirdAndroidArtifacts.findAab( + project: any(named: 'project'), + flavor: any(named: 'flavor'), + ), + ).thenThrow( + MultipleArtifactsFoundException( + foundArtifacts: [File('a'), File('b')], + buildDir: 'buildDir', + ), + ); + }); + + test('throws BuildException', () async { + expect( + () async => runWithOverrides(() => builder.buildAppBundle()), + throwsA( + isA().having( + (e) => e.message, + 'message', + '''Build succeeded, but it generated multiple AABs in the build directory. (a, b)''', + ), + ), + ); + }); + }); + + group('when no artifacts are found', () { + setUp(() { + when( + () => shorebirdAndroidArtifacts.findAab( + project: any(named: 'project'), + flavor: any(named: 'flavor'), + ), + ).thenThrow( + ArtifactNotFoundException( + artifactName: 'app-release.aab', + buildDir: 'buildDir', + ), + ); + }); + + test('throws BuildException', () async { + expect( + () async => runWithOverrides(() => builder.buildAppBundle()), + throwsA( + isA().having( + (e) => e.message, + 'message', + '''Build succeeded, but could not find the AAB in the build directory. Expected to find app-release.aab''', + ), + ), + ); + }); + }); + + group('after a build', () { + group('when the build is successful', () { + setUp(() { + when(() => buildProcessResult.exitCode) + .thenReturn(ExitCode.success.code); + }); + + group('when flutter is installed', () { + setUp(() { + when(() => operatingSystemInterface.which('flutter')) + .thenReturn('/path/to/flutter'); + }); + + test('runs flutter pub get with system flutter', () async { + await runWithOverrides(() => builder.buildAppBundle()); + + verify( + () => shorebirdProcess.run( + 'flutter', + ['--no-version-check', 'pub', 'get', '--offline'], + runInShell: any(named: 'runInShell'), + useVendedFlutter: false, + ), + ).called(1); + }); + }); + + group('when flutter is not installed', () { + setUp(() { + when(() => operatingSystemInterface.which('flutter')) + .thenReturn(null); + }); + + test('does not attempt to run flutter pub get', () async { + await runWithOverrides(() => builder.buildAppBundle()); + + verifyNever( + () => shorebirdProcess.run( + 'flutter', + ['--no-version-check', 'pub', 'get', '--offline'], + runInShell: any(named: 'runInShell'), + useVendedFlutter: false, + ), + ); + }); + }); + }); + + group('when the build fails', () { + setUp(() { + when(() => buildProcessResult.exitCode) + .thenReturn(ExitCode.software.code); + }); + + group('when flutter is installed', () { + setUp(() { + when(() => operatingSystemInterface.which('flutter')) + .thenReturn('/path/to/flutter'); + }); + + test('runs flutter pub get with system flutter', () async { + await expectLater( + () async => runWithOverrides(() => builder.buildAppBundle()), + throwsA(isA()), + ); + + verify( + () => shorebirdProcess.run( + 'flutter', + ['--no-version-check', 'pub', 'get', '--offline'], + runInShell: any(named: 'runInShell'), + useVendedFlutter: false, + ), + ).called(1); + }); + + test('prints error message if system flutter pub get fails', + () async { + when(() => pubGetProcessResult.exitCode).thenReturn(1); + + await expectLater( + () async => runWithOverrides(() => builder.buildAppBundle()), + throwsA(isA()), + ); + + verify( + () => logger.warn( + ''' +Build was successful, but `flutter pub get` failed to run after the build completed. You may see unexpected behavior in VS Code. + +Either run `flutter pub get` manually, or follow the steps in ${link(uri: Uri.parse('https://docs.shorebird.dev/troubleshooting#i-installed-shorebird-and-now-i-cant-run-my-app-in-vs-code'))}. +''', + ), + ).called(1); + }); + }); + + group('when flutter is not installed', () { + setUp(() { + when(() => operatingSystemInterface.which('flutter')) + .thenReturn(null); + }); + + test('does not attempt to run flutter pub get', () async { + await expectLater( + () async => runWithOverrides(() => builder.buildAppBundle()), + throwsA(isA()), + ); + + verifyNever( + () => shorebirdProcess.run( + 'flutter', + ['--no-version-check', 'pub', 'get', '--offline'], + runInShell: any(named: 'runInShell'), + useVendedFlutter: false, + ), + ); + }); + }); + }); + }); + }); + }); +} diff --git a/packages/shorebird_cli/test/src/commands/build/build_app_bundle_command_test.dart b/packages/shorebird_cli/test/src/commands/build/build_app_bundle_command_test.dart index 2d55217e..27c038e6 100644 --- a/packages/shorebird_cli/test/src/commands/build/build_app_bundle_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/build/build_app_bundle_command_test.dart @@ -5,15 +5,12 @@ import 'package:mason_logger/mason_logger.dart'; import 'package:mocktail/mocktail.dart'; import 'package:path/path.dart' as p; import 'package:scoped/scoped.dart'; +import 'package:shorebird_cli/src/artifact_builder.dart'; import 'package:shorebird_cli/src/commands/build/build.dart'; import 'package:shorebird_cli/src/doctor.dart'; import 'package:shorebird_cli/src/engine_config.dart'; import 'package:shorebird_cli/src/logger.dart'; -import 'package:shorebird_cli/src/os/operating_system_interface.dart'; import 'package:shorebird_cli/src/platform/platform.dart'; -import 'package:shorebird_cli/src/shorebird_android_artifacts.dart'; -import 'package:shorebird_cli/src/shorebird_env.dart'; -import 'package:shorebird_cli/src/shorebird_process.dart'; import 'package:shorebird_cli/src/shorebird_validator.dart'; import 'package:shorebird_cli/src/validators/validators.dart'; import 'package:test/test.dart'; @@ -24,31 +21,22 @@ import '../../mocks.dart'; void main() { group(BuildAppBundleCommand, () { late ArgResults argResults; + late ArtifactBuilder artifactBuilder; late Doctor doctor; late Logger logger; - late OperatingSystemInterface operatingSystemInterface; - late ShorebirdProcessResult flutterPubGetProcessResult; - late ShorebirdProcessResult buildProcessResult; late BuildAppBundleCommand command; - late ShorebirdEnv shorebirdEnv; late ShorebirdFlutterValidator flutterValidator; - late ShorebirdProcess shorebirdProcess; late ShorebirdValidator shorebirdValidator; - late ShorebirdAndroidArtifacts shorebirdAndroidArtifacts; R runWithOverrides(R Function() body) { return runScoped( body, values: { + artifactBuilderRef.overrideWith(() => artifactBuilder), doctorRef.overrideWith(() => doctor), engineConfigRef.overrideWith(() => const EngineConfig.empty()), loggerRef.overrideWith(() => logger), - osInterfaceRef.overrideWith(() => operatingSystemInterface), - processRef.overrideWith(() => shorebirdProcess), - shorebirdEnvRef.overrideWith(() => shorebirdEnv), shorebirdValidatorRef.overrideWith(() => shorebirdValidator), - shorebirdAndroidArtifactsRef - .overrideWith(() => shorebirdAndroidArtifacts), }, ); } @@ -60,44 +48,18 @@ void main() { setUp(() { argResults = MockArgResults(); + artifactBuilder = MockArtifactBuilder(); doctor = MockDoctor(); logger = MockLogger(); - operatingSystemInterface = MockOperatingSystemInterface(); - buildProcessResult = MockProcessResult(); - flutterPubGetProcessResult = MockProcessResult(); flutterValidator = MockShorebirdFlutterValidator(); - shorebirdEnv = MockShorebirdEnv(); - shorebirdProcess = MockShorebirdProcess(); shorebirdValidator = MockShorebirdValidator(); - shorebirdAndroidArtifacts = MockShorebirdAndroidArtifacts(); - when( - () => shorebirdProcess.run( - 'flutter', - ['--no-version-check', 'pub', 'get', '--offline'], - runInShell: any(named: 'runInShell'), - useVendedFlutter: false, - ), - ).thenAnswer((_) async => flutterPubGetProcessResult); - when(() => flutterPubGetProcessResult.exitCode) - .thenReturn(ExitCode.success.code); - when( - () => shorebirdProcess.run( - any(), - any(), - runInShell: any(named: 'runInShell'), - ), - ).thenAnswer((_) async => buildProcessResult); when(() => argResults.rest).thenReturn([]); when(() => logger.progress(any())).thenReturn(MockProgress()); when(() => logger.info(any())).thenReturn(null); - when(() => operatingSystemInterface.which('flutter')) - .thenReturn('/path/to/flutter'); when( () => doctor.androidCommandValidators, ).thenReturn([flutterValidator]); - when(() => shorebirdEnv.flutterRevision).thenReturn('1234'); - when(shorebirdEnv.getShorebirdProjectRoot).thenReturn(Directory('')); when( () => shorebirdValidator.validatePreconditions( checkUserIsAuthenticated: any(named: 'checkUserIsAuthenticated'), @@ -106,11 +68,13 @@ void main() { ), ).thenAnswer((_) async {}); when( - () => shorebirdAndroidArtifacts.findAab( - project: any(named: 'project'), + () => artifactBuilder.buildAppBundle( flavor: any(named: 'flavor'), + target: any(named: 'target'), ), - ).thenReturn(File('app-release.aab')); + ).thenAnswer( + (_) async => File(''), + ); command = runWithOverrides(BuildAppBundleCommand.new) ..testArgResults = argResults; @@ -143,33 +107,21 @@ void main() { }); test('exits with code 70 when building appbundle fails', () async { - when(() => buildProcessResult.exitCode).thenReturn(1); - when(() => buildProcessResult.stderr).thenReturn('oops'); + when(() => artifactBuilder.buildAppBundle()).thenThrow( + ArtifactBuildException('Failed to build: oops'), + ); final exitCode = await runWithOverrides(command.run); expect(exitCode, equals(ExitCode.software.code)); - verify( - () => shorebirdProcess.run( - 'flutter', - ['build', 'appbundle', '--release'], - runInShell: any(named: 'runInShell'), - ), - ).called(1); + verify(() => artifactBuilder.buildAppBundle()).called(1); }); test('exits with code 0 when building appbundle succeeds', () async { - when(() => buildProcessResult.exitCode).thenReturn(ExitCode.success.code); final exitCode = await runWithOverrides(command.run); expect(exitCode, equals(ExitCode.success.code)); - verify( - () => shorebirdProcess.run( - 'flutter', - ['build', 'appbundle', '--release'], - runInShell: true, - ), - ).called(1); + verify(() => artifactBuilder.buildAppBundle()).called(1); verify( () => logger.info( @@ -187,21 +139,13 @@ ${lightCyan.wrap(p.join('build', 'app', 'outputs', 'bundle', 'release', 'app-rel final target = p.join('lib', 'main_development.dart'); when(() => argResults['flavor']).thenReturn(flavor); when(() => argResults['target']).thenReturn(target); - when(() => buildProcessResult.exitCode).thenReturn(ExitCode.success.code); final exitCode = await runWithOverrides(command.run); expect(exitCode, equals(ExitCode.success.code)); verify( - () => shorebirdProcess.run( - 'flutter', - [ - 'build', - 'appbundle', - '--release', - '--flavor=$flavor', - '--target=$target', - ], - runInShell: true, + () => artifactBuilder.buildAppBundle( + flavor: flavor, + target: target, ), ).called(1); @@ -253,21 +197,5 @@ ${lightCyan.wrap(p.join('build', 'app', 'outputs', 'bundle', '${flavor}Release', throwsException, ); }); - - test('runs flutter pub get with system flutter after successful build', - () async { - when(() => buildProcessResult.exitCode).thenReturn(ExitCode.success.code); - - await runWithOverrides(command.run); - - verify( - () => shorebirdProcess.run( - 'flutter', - ['--no-version-check', 'pub', 'get', '--offline'], - runInShell: any(named: 'runInShell'), - useVendedFlutter: false, - ), - ).called(1); - }); }); } diff --git a/packages/shorebird_cli/test/src/mocks.dart b/packages/shorebird_cli/test/src/mocks.dart index b583f5f8..75709702 100644 --- a/packages/shorebird_cli/test/src/mocks.dart +++ b/packages/shorebird_cli/test/src/mocks.dart @@ -11,6 +11,7 @@ import 'package:shorebird_cli/src/android_sdk.dart'; import 'package:shorebird_cli/src/android_studio.dart'; import 'package:shorebird_cli/src/archive_analysis/archive_analysis.dart'; import 'package:shorebird_cli/src/archive_analysis/archive_differ.dart'; +import 'package:shorebird_cli/src/artifact_builder.dart'; import 'package:shorebird_cli/src/artifact_manager.dart'; import 'package:shorebird_cli/src/auth/auth.dart'; import 'package:shorebird_cli/src/cache.dart' show Cache; @@ -54,6 +55,8 @@ class MockArchiveDiffer extends Mock implements ArchiveDiffer {} class MockArgResults extends Mock implements ArgResults {} +class MockArtifactBuilder extends Mock implements ArtifactBuilder {} + class MockArtifactManager extends Mock implements ArtifactManager {} class MockAuth extends Mock implements Auth {}