feat(shorebird_cli): add build command (#26)
This commit is contained in:
@@ -50,6 +50,7 @@ class ShorebirdCliCommandRunner extends CompletionCommandRunner<int> {
|
||||
final buildCodePushApiClient =
|
||||
codePushApiClientBuilder ?? ShorebirdCodePushApiClient.new;
|
||||
|
||||
addCommand(BuildCommand(auth: authentication, logger: _logger));
|
||||
addCommand(LoginCommand(auth: authentication, logger: _logger));
|
||||
addCommand(LogoutCommand(auth: authentication, logger: _logger));
|
||||
addCommand(
|
||||
|
||||
@@ -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<ProcessResult> Function(
|
||||
String executable,
|
||||
List<String> arguments, {
|
||||
bool runInShell,
|
||||
});
|
||||
|
||||
/// {@template build_command}
|
||||
///
|
||||
/// `shorebird build`
|
||||
/// Build a new release of your application.
|
||||
/// {@endtemplate}
|
||||
class BuildCommand extends Command<int> {
|
||||
/// {@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<int> 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<void> _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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export 'build_command.dart';
|
||||
export 'login_command.dart';
|
||||
export 'logout_command.dart';
|
||||
export 'publish_command.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));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user