From debc6010664ca0255d168bf21703fe2f352bf35b Mon Sep 17 00:00:00 2001 From: Felix Angelov Date: Mon, 6 Mar 2023 16:41:23 -0600 Subject: [PATCH] feat(shorebird_cli): add build command (#26) --- .../shorebird_cli/lib/src/command_runner.dart | 1 + .../lib/src/commands/build_command.dart | 108 ++++++++++++++++++ .../lib/src/commands/commands.dart | 1 + .../test/src/commands/build_command_test.dart | 101 ++++++++++++++++ .../test/src/commands/login_command_test.dart | 2 +- .../src/commands/logout_command_test.dart | 2 +- 6 files changed, 213 insertions(+), 2 deletions(-) create mode 100644 packages/shorebird_cli/lib/src/commands/build_command.dart create mode 100644 packages/shorebird_cli/test/src/commands/build_command_test.dart diff --git a/packages/shorebird_cli/lib/src/command_runner.dart b/packages/shorebird_cli/lib/src/command_runner.dart index 626d6b78..9c4e9bf9 100644 --- a/packages/shorebird_cli/lib/src/command_runner.dart +++ b/packages/shorebird_cli/lib/src/command_runner.dart @@ -50,6 +50,7 @@ class ShorebirdCliCommandRunner extends CompletionCommandRunner { final buildCodePushApiClient = codePushApiClientBuilder ?? ShorebirdCodePushApiClient.new; + addCommand(BuildCommand(auth: authentication, logger: _logger)); addCommand(LoginCommand(auth: authentication, logger: _logger)); addCommand(LogoutCommand(auth: authentication, logger: _logger)); addCommand( diff --git a/packages/shorebird_cli/lib/src/commands/build_command.dart b/packages/shorebird_cli/lib/src/commands/build_command.dart new file mode 100644 index 00000000..353aa522 --- /dev/null +++ b/packages/shorebird_cli/lib/src/commands/build_command.dart @@ -0,0 +1,108 @@ +import 'dart:io'; + +import 'package:args/command_runner.dart'; +import 'package:mason_logger/mason_logger.dart'; +import 'package:path/path.dart' as p; +import 'package:shorebird_cli/src/auth/auth.dart'; + +typedef RunProcess = Future Function( + String executable, + List arguments, { + bool runInShell, +}); + +/// {@template build_command} +/// +/// `shorebird build` +/// Build a new release of your application. +/// {@endtemplate} +class BuildCommand extends Command { + /// {@macro build_command} + BuildCommand({ + required Auth auth, + required Logger logger, + RunProcess? runProcess, + }) : _auth = auth, + _logger = logger, + _runProcess = runProcess ?? Process.run; + + @override + String get description => 'Build a new release of your application.'; + + @override + String get name => 'build'; + + final Auth _auth; + final Logger _logger; + final RunProcess _runProcess; + + @override + Future run() async { + final session = _auth.currentSession; + if (session == null) { + _logger + ..err('You must be logged in to build.') + ..err("Run 'shorebird login' to log in and try again."); + return ExitCode.noUser.code; + } + + final shorebirdEnginePath = p.join( + Directory.current.path, + '.shorebird', + 'engine', + ); + final shorebirdEngineDir = Directory(shorebirdEnginePath); + if (!shorebirdEngineDir.existsSync()) { + _logger.err( + 'Shorebird engine not found. Run `shorebird run` to download it.', + ); + return ExitCode.software.code; + } + + final buildProgress = _logger.progress('Building release '); + try { + await _build(_runProcess, shorebirdEnginePath); + buildProgress.complete(); + } on ProcessException catch (error) { + buildProgress.fail('Failed to build: ${error.message}'); + return ExitCode.software.code; + } + + return ExitCode.success.code; + } + + Future _build(RunProcess runProcess, String shorebirdEnginePath) async { + const executable = 'flutter'; + final arguments = [ + 'build', + // This is temporary because the Shorebird engine currently + // only supports Android. + 'apk', + '--release', + // This is temporary because the Shorebird engine currently + // does not support tree-shaking icons. + '--no-tree-shake-icons', + '--local-engine-src-path', + shorebirdEnginePath, + '--local-engine', + // This is temporary because the Shorebird engine currently + // only supports Android arm64. + 'android_release_arm64', + ]; + + final result = await runProcess( + executable, + arguments, + runInShell: true, + ); + + if (result.exitCode != ExitCode.success.code) { + throw ProcessException( + 'flutter', + arguments, + result.stderr.toString(), + result.exitCode, + ); + } + } +} diff --git a/packages/shorebird_cli/lib/src/commands/commands.dart b/packages/shorebird_cli/lib/src/commands/commands.dart index 005b0f03..0d2ba70c 100644 --- a/packages/shorebird_cli/lib/src/commands/commands.dart +++ b/packages/shorebird_cli/lib/src/commands/commands.dart @@ -1,3 +1,4 @@ +export 'build_command.dart'; export 'login_command.dart'; export 'logout_command.dart'; export 'publish_command.dart'; diff --git a/packages/shorebird_cli/test/src/commands/build_command_test.dart b/packages/shorebird_cli/test/src/commands/build_command_test.dart new file mode 100644 index 00000000..1d2aa144 --- /dev/null +++ b/packages/shorebird_cli/test/src/commands/build_command_test.dart @@ -0,0 +1,101 @@ +import 'dart:io'; + +import 'package:mason_logger/mason_logger.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:shorebird_cli/src/auth/auth.dart'; +import 'package:shorebird_cli/src/auth/session.dart'; +import 'package:shorebird_cli/src/commands/build_command.dart'; +import 'package:test/test.dart'; + +class _MockAuth extends Mock implements Auth {} + +class _MockLogger extends Mock implements Logger {} + +class _MockProgress extends Mock implements Progress {} + +class _MockProcessResult extends Mock implements ProcessResult {} + +void main() { + group('build', () { + const session = Session( + apiKey: 'test-api-key', + projectId: 'test-project-id', + ); + + late Auth auth; + late Logger logger; + late ProcessResult processResult; + late BuildCommand buildCommand; + + setUp(() { + auth = _MockAuth(); + logger = _MockLogger(); + processResult = _MockProcessResult(); + buildCommand = BuildCommand( + auth: auth, + logger: logger, + runProcess: (executable, arguments, {bool runInShell = false}) async { + return processResult; + }, + ); + + when(() => logger.progress(any())).thenReturn(_MockProgress()); + }); + + test('exits with no user when not logged in', () async { + when(() => auth.currentSession).thenReturn(null); + + final result = await buildCommand.run(); + expect(result, equals(ExitCode.noUser.code)); + + verify(() => logger.err('You must be logged in to build.')).called(1); + verify( + () => logger.err("Run 'shorebird login' to log in and try again."), + ).called(1); + }); + + test('exits with code 70 when engine is not found', () async { + when(() => auth.currentSession).thenReturn(session); + + final result = await buildCommand.run(); + expect(result, equals(ExitCode.software.code)); + + verify( + () => logger.err( + 'Shorebird engine not found. Run `shorebird run` to download it.', + ), + ).called(1); + }); + + test('exits with code 70 when building fails', () async { + when(() => processResult.exitCode).thenReturn(1); + when(() => processResult.stderr).thenReturn('oops'); + final tempDir = Directory.systemTemp.createTempSync(); + Directory('${tempDir.path}/.shorebird/engine') + .createSync(recursive: true); + when(() => auth.currentSession).thenReturn(session); + + final result = await IOOverrides.runZoned( + () async => buildCommand.run(), + getCurrentDirectory: () => tempDir, + ); + + expect(result, equals(ExitCode.software.code)); + }); + + test('exits with code 0 when building succeeds', () async { + when(() => processResult.exitCode).thenReturn(ExitCode.success.code); + final tempDir = Directory.systemTemp.createTempSync(); + Directory('${tempDir.path}/.shorebird/engine') + .createSync(recursive: true); + when(() => auth.currentSession).thenReturn(session); + + final result = await IOOverrides.runZoned( + () async => buildCommand.run(), + getCurrentDirectory: () => tempDir, + ); + + expect(result, equals(ExitCode.success.code)); + }); + }); +} diff --git a/packages/shorebird_cli/test/src/commands/login_command_test.dart b/packages/shorebird_cli/test/src/commands/login_command_test.dart index 3cf4bfa7..eb65ee33 100644 --- a/packages/shorebird_cli/test/src/commands/login_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/login_command_test.dart @@ -12,7 +12,7 @@ class _MockLogger extends Mock implements Logger {} class _MockProgress extends Mock implements Progress {} void main() { - group('LoginCommand', () { + group('login', () { const apiKey = 'test-api-key'; const projectId = 'example'; const session = Session(apiKey: apiKey, projectId: projectId); diff --git a/packages/shorebird_cli/test/src/commands/logout_command_test.dart b/packages/shorebird_cli/test/src/commands/logout_command_test.dart index bd9c58d4..213180b8 100644 --- a/packages/shorebird_cli/test/src/commands/logout_command_test.dart +++ b/packages/shorebird_cli/test/src/commands/logout_command_test.dart @@ -12,7 +12,7 @@ class _MockAuth extends Mock implements Auth {} class _MockProgress extends Mock implements Progress {} void main() { - group('LogoutCommand', () { + group('logout', () { late Logger logger; late Auth auth; late LogoutCommand logoutCommand;