feat(shorebird_cli): support for shorebird build apk (#311)
This commit is contained in:
+1
-1
@@ -342,7 +342,7 @@ variable inside Dart as you might have done with `flutter build` directly.
|
||||
Success should look like this:
|
||||
|
||||
```
|
||||
% shorebird build
|
||||
% shorebird build appbundle
|
||||
✓ Building shorebird engine (8.0s)
|
||||
✓ Building release (6.4s)
|
||||
```
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# Files and directories created by pub
|
||||
.dart_tool/
|
||||
.packages
|
||||
build/
|
||||
/build/
|
||||
pubspec.lock
|
||||
|
||||
# Files generated during tests
|
||||
|
||||
@@ -274,7 +274,11 @@ Would you like to continue? (y/N) Yes
|
||||
Build a new release of your application using the `shorebird build` command:
|
||||
|
||||
```bash
|
||||
shorebird build
|
||||
# Build an AppBundle
|
||||
shorebird build appbundle
|
||||
|
||||
# Build an APK
|
||||
shorebird build apk
|
||||
```
|
||||
|
||||
### List Channels
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export 'build_apk_command.dart';
|
||||
export 'build_app_bundle_command.dart.dart';
|
||||
export 'build_command.dart';
|
||||
+10
-11
@@ -6,27 +6,26 @@ import 'package:shorebird_cli/src/flutter_validation_mixin.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_build_mixin.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_config_mixin.dart';
|
||||
|
||||
/// {@template build_command}
|
||||
/// {@template build_apk_command}
|
||||
///
|
||||
/// `shorebird build`
|
||||
/// Build a new release of your application.
|
||||
/// `shorebird build apk`
|
||||
/// Build an Android APK file from your app.
|
||||
/// {@endtemplate}
|
||||
class BuildCommand extends ShorebirdCommand
|
||||
class BuildApkCommand extends ShorebirdCommand
|
||||
with ShorebirdValidationMixin, ShorebirdConfigMixin, ShorebirdBuildMixin {
|
||||
/// {@macro build_command}
|
||||
BuildCommand({
|
||||
/// {@macro build_apk_command}
|
||||
BuildApkCommand({
|
||||
required super.logger,
|
||||
super.auth,
|
||||
super.buildCodePushClient,
|
||||
super.runProcess,
|
||||
super.validators,
|
||||
});
|
||||
|
||||
@override
|
||||
String get description => 'Build a new release of your application.';
|
||||
String get description => 'Build an Android APK file from your app.';
|
||||
|
||||
@override
|
||||
String get name => 'build';
|
||||
String get name => 'apk';
|
||||
|
||||
@override
|
||||
Future<int> run() async {
|
||||
@@ -39,9 +38,9 @@ class BuildCommand extends ShorebirdCommand
|
||||
|
||||
await logValidationIssues();
|
||||
|
||||
final buildProgress = logger.progress('Building release ');
|
||||
final buildProgress = logger.progress('Building apk');
|
||||
try {
|
||||
await buildRelease();
|
||||
await buildApk();
|
||||
buildProgress.complete();
|
||||
} on ProcessException catch (error) {
|
||||
buildProgress.fail('Failed to build: ${error.message}');
|
||||
@@ -0,0 +1,52 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:mason_logger/mason_logger.dart';
|
||||
import 'package:shorebird_cli/src/command.dart';
|
||||
import 'package:shorebird_cli/src/flutter_validation_mixin.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_build_mixin.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_config_mixin.dart';
|
||||
|
||||
/// {@template build_app_bundle_command}
|
||||
///
|
||||
/// `shorebird build appbundle`
|
||||
/// Build an Android App Bundle file from your app.
|
||||
/// {@endtemplate}
|
||||
class BuildAppBundleCommand extends ShorebirdCommand
|
||||
with ShorebirdValidationMixin, ShorebirdConfigMixin, ShorebirdBuildMixin {
|
||||
/// {@macro build_app_bundle_command}
|
||||
BuildAppBundleCommand({
|
||||
required super.logger,
|
||||
super.auth,
|
||||
super.runProcess,
|
||||
super.validators,
|
||||
});
|
||||
|
||||
@override
|
||||
String get description => 'Build an Android App Bundle file from your app.';
|
||||
|
||||
@override
|
||||
String get name => 'appbundle';
|
||||
|
||||
@override
|
||||
Future<int> run() async {
|
||||
if (!auth.isAuthenticated) {
|
||||
logger
|
||||
..err('You must be logged in to build.')
|
||||
..err("Run 'shorebird login' to log in and try again.");
|
||||
return ExitCode.noUser.code;
|
||||
}
|
||||
|
||||
await logValidationIssues();
|
||||
|
||||
final buildProgress = logger.progress('Building appbundle');
|
||||
try {
|
||||
await buildAppBundle();
|
||||
buildProgress.complete();
|
||||
} on ProcessException catch (error) {
|
||||
buildProgress.fail('Failed to build: ${error.message}');
|
||||
return ExitCode.software.code;
|
||||
}
|
||||
|
||||
return ExitCode.success.code;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import 'package:shorebird_cli/src/command.dart';
|
||||
import 'package:shorebird_cli/src/commands/build/build.dart';
|
||||
import 'package:shorebird_cli/src/flutter_validation_mixin.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_build_mixin.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_config_mixin.dart';
|
||||
|
||||
/// {@template build_command}
|
||||
///
|
||||
/// `shorebird build`
|
||||
/// Build a new release of your application.
|
||||
/// {@endtemplate}
|
||||
class BuildCommand extends ShorebirdCommand
|
||||
with ShorebirdValidationMixin, ShorebirdConfigMixin, ShorebirdBuildMixin {
|
||||
/// {@macro build_command}
|
||||
BuildCommand({required super.logger}) {
|
||||
addSubcommand(
|
||||
BuildApkCommand(
|
||||
auth: auth,
|
||||
logger: logger,
|
||||
runProcess: runProcess,
|
||||
validators: validators,
|
||||
),
|
||||
);
|
||||
addSubcommand(
|
||||
BuildAppBundleCommand(
|
||||
auth: auth,
|
||||
logger: logger,
|
||||
runProcess: runProcess,
|
||||
validators: validators,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String get description => 'Build a new release of your application.';
|
||||
|
||||
@override
|
||||
String get name => 'build';
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
export 'account_command.dart';
|
||||
export 'apps/apps.dart';
|
||||
export 'build_command.dart';
|
||||
export 'build/build_command.dart';
|
||||
export 'cache/cache.dart';
|
||||
export 'channels/channels.dart';
|
||||
export 'doctor_command.dart';
|
||||
|
||||
@@ -125,7 +125,7 @@ class PatchCommand extends ShorebirdCommand
|
||||
|
||||
final buildProgress = logger.progress('Building patch');
|
||||
try {
|
||||
await buildRelease();
|
||||
await buildAppBundle();
|
||||
buildProgress.complete();
|
||||
} on ProcessException catch (error) {
|
||||
buildProgress.fail('Failed to build: ${error.message}');
|
||||
|
||||
@@ -74,7 +74,7 @@ make smaller updates to your app.
|
||||
|
||||
final buildProgress = logger.progress('Building release');
|
||||
try {
|
||||
await buildRelease();
|
||||
await buildAppBundle();
|
||||
buildProgress.complete();
|
||||
} on ProcessException catch (error) {
|
||||
buildProgress.fail('Failed to build: ${error.message}');
|
||||
|
||||
@@ -33,12 +33,10 @@ mixin ShorebirdBuildMixin on ShorebirdCommand {
|
||||
),
|
||||
};
|
||||
|
||||
Future<void> buildRelease() async {
|
||||
Future<void> buildAppBundle() async {
|
||||
const executable = 'flutter';
|
||||
final arguments = [
|
||||
'build',
|
||||
// This is temporary because the Shorebird engine currently
|
||||
// only supports Android.
|
||||
'appbundle',
|
||||
'--release',
|
||||
...results.rest,
|
||||
@@ -59,4 +57,29 @@ mixin ShorebirdBuildMixin on ShorebirdCommand {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> buildApk() async {
|
||||
const executable = 'flutter';
|
||||
final arguments = [
|
||||
'build',
|
||||
'apk',
|
||||
'--release',
|
||||
...results.rest,
|
||||
];
|
||||
|
||||
final result = await runProcess(
|
||||
executable,
|
||||
arguments,
|
||||
runInShell: true,
|
||||
);
|
||||
|
||||
if (result.exitCode != ExitCode.success.code) {
|
||||
throw ProcessException(
|
||||
'flutter',
|
||||
arguments,
|
||||
result.stderr.toString(),
|
||||
result.exitCode,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+25
-21
@@ -5,9 +5,8 @@ import 'package:http/http.dart' as http;
|
||||
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/commands/build_command.dart';
|
||||
import 'package:shorebird_cli/src/commands/build/build.dart';
|
||||
import 'package:shorebird_cli/src/validators/validators.dart';
|
||||
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
class _MockArgResults extends Mock implements ArgResults {}
|
||||
@@ -22,38 +21,33 @@ class _MockProgress extends Mock implements Progress {}
|
||||
|
||||
class _MockProcessResult extends Mock implements ProcessResult {}
|
||||
|
||||
class _MockCodePushClient extends Mock implements CodePushClient {}
|
||||
|
||||
class _MockShorebirdFlutterValidator extends Mock
|
||||
implements ShorebirdFlutterValidator {}
|
||||
|
||||
void main() {
|
||||
group('build', () {
|
||||
group('build apk', () {
|
||||
late ArgResults argResults;
|
||||
late http.Client httpClient;
|
||||
late Auth auth;
|
||||
late CodePushClient codePushClient;
|
||||
late Logger logger;
|
||||
late ProcessResult processResult;
|
||||
late BuildCommand buildCommand;
|
||||
late BuildApkCommand command;
|
||||
late ShorebirdFlutterValidator flutterValidator;
|
||||
|
||||
String? processExecutable;
|
||||
List<String>? processArguments;
|
||||
|
||||
setUp(() {
|
||||
argResults = _MockArgResults();
|
||||
httpClient = _MockHttpClient();
|
||||
auth = _MockAuth();
|
||||
codePushClient = _MockCodePushClient();
|
||||
logger = _MockLogger();
|
||||
processResult = _MockProcessResult();
|
||||
flutterValidator = _MockShorebirdFlutterValidator();
|
||||
buildCommand = BuildCommand(
|
||||
processExecutable = null;
|
||||
processArguments = null;
|
||||
command = BuildApkCommand(
|
||||
auth: auth,
|
||||
buildCodePushClient: ({
|
||||
required http.Client httpClient,
|
||||
Uri? hostedUri,
|
||||
}) {
|
||||
return codePushClient;
|
||||
},
|
||||
logger: logger,
|
||||
runProcess: (
|
||||
executable,
|
||||
@@ -63,6 +57,8 @@ void main() {
|
||||
String? workingDirectory,
|
||||
bool useVendedFlutter = true,
|
||||
}) async {
|
||||
processExecutable = executable;
|
||||
processArguments = arguments;
|
||||
return processResult;
|
||||
},
|
||||
validators: [flutterValidator],
|
||||
@@ -76,10 +72,14 @@ void main() {
|
||||
when(() => flutterValidator.validate()).thenAnswer((_) async => []);
|
||||
});
|
||||
|
||||
test('has correct description', () {
|
||||
expect(command.description, isNotEmpty);
|
||||
});
|
||||
|
||||
test('exits with no user when not logged in', () async {
|
||||
when(() => auth.isAuthenticated).thenReturn(false);
|
||||
|
||||
final result = await buildCommand.run();
|
||||
final result = await command.run();
|
||||
expect(result, equals(ExitCode.noUser.code));
|
||||
|
||||
verify(() => logger.err('You must be logged in to build.')).called(1);
|
||||
@@ -88,28 +88,32 @@ void main() {
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('exits with code 70 when building fails', () async {
|
||||
test('exits with code 70 when building apk fails', () async {
|
||||
when(() => processResult.exitCode).thenReturn(1);
|
||||
when(() => processResult.stderr).thenReturn('oops');
|
||||
final tempDir = Directory.systemTemp.createTempSync();
|
||||
|
||||
final result = await IOOverrides.runZoned(
|
||||
() async => buildCommand.run(),
|
||||
() async => command.run(),
|
||||
getCurrentDirectory: () => tempDir,
|
||||
);
|
||||
|
||||
expect(result, equals(ExitCode.software.code));
|
||||
expect(processExecutable, equals('flutter'));
|
||||
expect(processArguments, equals(['build', 'apk', '--release']));
|
||||
});
|
||||
|
||||
test('exits with code 0 when building succeeds', () async {
|
||||
test('exits with code 0 when building apk succeeds', () async {
|
||||
when(() => processResult.exitCode).thenReturn(ExitCode.success.code);
|
||||
final tempDir = Directory.systemTemp.createTempSync();
|
||||
final result = await IOOverrides.runZoned(
|
||||
() async => buildCommand.run(),
|
||||
() async => command.run(),
|
||||
getCurrentDirectory: () => tempDir,
|
||||
);
|
||||
|
||||
expect(result, equals(ExitCode.success.code));
|
||||
expect(processExecutable, equals('flutter'));
|
||||
expect(processArguments, equals(['build', 'apk', '--release']));
|
||||
});
|
||||
|
||||
test('prints flutter validation warnings', () async {
|
||||
@@ -127,7 +131,7 @@ void main() {
|
||||
);
|
||||
when(() => processResult.exitCode).thenReturn(ExitCode.success.code);
|
||||
|
||||
final result = await buildCommand.run();
|
||||
final result = await command.run();
|
||||
|
||||
expect(result, equals(ExitCode.success.code));
|
||||
verify(
|
||||
@@ -0,0 +1,145 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:args/args.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
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/commands/build/build.dart';
|
||||
import 'package:shorebird_cli/src/validators/validators.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
class _MockArgResults extends Mock implements ArgResults {}
|
||||
|
||||
class _MockHttpClient extends Mock implements http.Client {}
|
||||
|
||||
class _MockAuth extends Mock implements Auth {}
|
||||
|
||||
class _MockLogger extends Mock implements Logger {}
|
||||
|
||||
class _MockProgress extends Mock implements Progress {}
|
||||
|
||||
class _MockProcessResult extends Mock implements ProcessResult {}
|
||||
|
||||
class _MockShorebirdFlutterValidator extends Mock
|
||||
implements ShorebirdFlutterValidator {}
|
||||
|
||||
void main() {
|
||||
group('build appbundle', () {
|
||||
late ArgResults argResults;
|
||||
late http.Client httpClient;
|
||||
late Auth auth;
|
||||
late Logger logger;
|
||||
late ProcessResult processResult;
|
||||
late BuildAppBundleCommand command;
|
||||
late ShorebirdFlutterValidator flutterValidator;
|
||||
|
||||
String? processExecutable;
|
||||
List<String>? processArguments;
|
||||
|
||||
setUp(() {
|
||||
argResults = _MockArgResults();
|
||||
httpClient = _MockHttpClient();
|
||||
auth = _MockAuth();
|
||||
logger = _MockLogger();
|
||||
processResult = _MockProcessResult();
|
||||
flutterValidator = _MockShorebirdFlutterValidator();
|
||||
processExecutable = null;
|
||||
processArguments = null;
|
||||
command = BuildAppBundleCommand(
|
||||
auth: auth,
|
||||
logger: logger,
|
||||
runProcess: (
|
||||
executable,
|
||||
arguments, {
|
||||
bool runInShell = false,
|
||||
Map<String, String>? environment,
|
||||
String? workingDirectory,
|
||||
bool useVendedFlutter = true,
|
||||
}) async {
|
||||
processExecutable = executable;
|
||||
processArguments = arguments;
|
||||
return processResult;
|
||||
},
|
||||
validators: [flutterValidator],
|
||||
)..testArgResults = argResults;
|
||||
|
||||
when(() => argResults.rest).thenReturn([]);
|
||||
when(() => auth.isAuthenticated).thenReturn(true);
|
||||
when(() => auth.client).thenReturn(httpClient);
|
||||
when(() => logger.progress(any())).thenReturn(_MockProgress());
|
||||
when(() => logger.info(any())).thenReturn(null);
|
||||
when(() => flutterValidator.validate()).thenAnswer((_) async => []);
|
||||
});
|
||||
|
||||
test('has correct description', () {
|
||||
expect(command.description, isNotEmpty);
|
||||
});
|
||||
|
||||
test('exits with no user when not logged in', () async {
|
||||
when(() => auth.isAuthenticated).thenReturn(false);
|
||||
|
||||
final result = await command.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 building appbundle fails', () async {
|
||||
when(() => processResult.exitCode).thenReturn(1);
|
||||
when(() => processResult.stderr).thenReturn('oops');
|
||||
final tempDir = Directory.systemTemp.createTempSync();
|
||||
|
||||
final result = await IOOverrides.runZoned(
|
||||
() async => command.run(),
|
||||
getCurrentDirectory: () => tempDir,
|
||||
);
|
||||
|
||||
expect(result, equals(ExitCode.software.code));
|
||||
expect(processExecutable, equals('flutter'));
|
||||
expect(processArguments, equals(['build', 'appbundle', '--release']));
|
||||
});
|
||||
|
||||
test('exits with code 0 when building appbundle succeeds', () async {
|
||||
when(() => processResult.exitCode).thenReturn(ExitCode.success.code);
|
||||
final tempDir = Directory.systemTemp.createTempSync();
|
||||
final result = await IOOverrides.runZoned(
|
||||
() async => command.run(),
|
||||
getCurrentDirectory: () => tempDir,
|
||||
);
|
||||
|
||||
expect(result, equals(ExitCode.success.code));
|
||||
expect(processExecutable, equals('flutter'));
|
||||
expect(processArguments, equals(['build', 'appbundle', '--release']));
|
||||
});
|
||||
|
||||
test('prints flutter validation warnings', () async {
|
||||
when(() => flutterValidator.validate()).thenAnswer(
|
||||
(_) async => [
|
||||
const ValidationIssue(
|
||||
severity: ValidationIssueSeverity.warning,
|
||||
message: 'Flutter issue 1',
|
||||
),
|
||||
const ValidationIssue(
|
||||
severity: ValidationIssueSeverity.warning,
|
||||
message: 'Flutter issue 2',
|
||||
),
|
||||
],
|
||||
);
|
||||
when(() => processResult.exitCode).thenReturn(ExitCode.success.code);
|
||||
|
||||
final result = await command.run();
|
||||
|
||||
expect(result, equals(ExitCode.success.code));
|
||||
verify(
|
||||
() => logger.info(any(that: contains('Flutter issue 1'))),
|
||||
).called(1);
|
||||
verify(
|
||||
() => logger.info(any(that: contains('Flutter issue 2'))),
|
||||
).called(1);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import 'package:mason_logger/mason_logger.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:shorebird_cli/src/commands/build/build.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
class _MockLogger extends Mock implements Logger {}
|
||||
|
||||
void main() {
|
||||
group('build', () {
|
||||
late Logger logger;
|
||||
late BuildCommand command;
|
||||
|
||||
setUp(() {
|
||||
logger = _MockLogger();
|
||||
command = BuildCommand(logger: logger);
|
||||
});
|
||||
|
||||
test('has a description', () async {
|
||||
expect(command.description, isNotEmpty);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user