feat(shorebird_cli): add shorebird build aar command (#570)
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
export 'build_aar_command.dart';
|
||||
export 'build_apk_command.dart';
|
||||
export 'build_app_bundle_command.dart';
|
||||
export 'build_command.dart';
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:mason_logger/mason_logger.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:shorebird_cli/src/auth_logger_mixin.dart';
|
||||
import 'package:shorebird_cli/src/command.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_build_mixin.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_config_mixin.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_validation_mixin.dart';
|
||||
|
||||
/// {@template build_aar_command}
|
||||
///
|
||||
/// `shorebird build aar`
|
||||
/// Build an Android aar file from your app.
|
||||
/// {@endtemplate}
|
||||
class BuildAarCommand extends ShorebirdCommand
|
||||
with
|
||||
AuthLoggerMixin,
|
||||
ShorebirdValidationMixin,
|
||||
ShorebirdConfigMixin,
|
||||
ShorebirdBuildMixin {
|
||||
BuildAarCommand({
|
||||
required super.logger,
|
||||
super.auth,
|
||||
}) {
|
||||
// We would have a "target" option here, similar to what [BuildApkCommand]
|
||||
// and [BuildAabCommand] have, but target cannot currently be configured in
|
||||
// `flutter build aar` and is always assumed to be lib/main.dart.
|
||||
argParser
|
||||
..addOption(
|
||||
'flavor',
|
||||
help: 'The product flavor to use when building the app.',
|
||||
)
|
||||
// `flutter build aar` defaults to a build number of 1.0, so we do the
|
||||
// same.
|
||||
..addOption(
|
||||
'build-number',
|
||||
help: 'The build number of the aar',
|
||||
defaultsTo: '1.0',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String get name => 'aar';
|
||||
|
||||
@override
|
||||
String get description => 'Build an Android AAR file from your module.';
|
||||
|
||||
@override
|
||||
Future<int> run() async {
|
||||
if (!auth.isAuthenticated) {
|
||||
printNeedsAuthInstructions();
|
||||
return ExitCode.noUser.code;
|
||||
}
|
||||
|
||||
final pubspec = getPubspecYaml();
|
||||
if (pubspec == null) {
|
||||
logger.err('No pubspec.yaml file found.');
|
||||
return ExitCode.config.code;
|
||||
}
|
||||
|
||||
final module = pubspec.flutter?['module'] as Map?;
|
||||
final androidPackageName = module?['androidPackage'] as String?;
|
||||
if (androidPackageName == null) {
|
||||
logger.err('Could not find androidPackage in pubspec.yaml.');
|
||||
return ExitCode.config.code;
|
||||
}
|
||||
|
||||
final flavor = results['flavor'] as String?;
|
||||
final buildNumber = results['build-number'] as String;
|
||||
final buildProgress = logger.progress('Building aar');
|
||||
try {
|
||||
await buildAar(buildNumber: buildNumber, flavor: flavor);
|
||||
} on ProcessException catch (error) {
|
||||
buildProgress.fail('Failed to build: ${error.message}');
|
||||
return ExitCode.software.code;
|
||||
}
|
||||
|
||||
buildProgress.complete();
|
||||
|
||||
final aarPath = p.joinAll([
|
||||
'build',
|
||||
'host',
|
||||
'outputs',
|
||||
'repo',
|
||||
...androidPackageName.split('.'),
|
||||
'flutter_release',
|
||||
buildNumber,
|
||||
'flutter_release-$buildNumber.aar',
|
||||
]);
|
||||
|
||||
logger.info('''
|
||||
📦 Generated an aar at:
|
||||
${lightCyan.wrap(aarPath)}''');
|
||||
|
||||
return ExitCode.success.code;
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import 'package:shorebird_cli/src/commands/build/build.dart';
|
||||
class BuildCommand extends ShorebirdCommand {
|
||||
/// {@macro build_command}
|
||||
BuildCommand({required super.logger}) {
|
||||
addSubcommand(BuildAarCommand(logger: logger));
|
||||
addSubcommand(BuildApkCommand(logger: logger));
|
||||
addSubcommand(BuildAppBundleCommand(logger: logger));
|
||||
addSubcommand(BuildIpaCommand(logger: logger));
|
||||
|
||||
@@ -93,6 +93,37 @@ mixin ShorebirdBuildMixin on ShorebirdCommand {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> buildAar({
|
||||
required String buildNumber,
|
||||
String? flavor,
|
||||
}) async {
|
||||
const executable = 'flutter';
|
||||
final arguments = [
|
||||
'build',
|
||||
'aar',
|
||||
'--no-debug',
|
||||
'--no-profile',
|
||||
'--build-number=$buildNumber',
|
||||
if (flavor != null) '--flavor=$flavor',
|
||||
...results.rest,
|
||||
];
|
||||
|
||||
final result = await process.run(
|
||||
executable,
|
||||
arguments,
|
||||
runInShell: true,
|
||||
);
|
||||
|
||||
if (result.exitCode != ExitCode.success.code) {
|
||||
throw ProcessException(
|
||||
'flutter',
|
||||
arguments,
|
||||
result.stderr.toString(),
|
||||
result.exitCode,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> buildApk({String? flavor, String? target}) async {
|
||||
const executable = 'flutter';
|
||||
final arguments = [
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:args/args.dart';
|
||||
import 'package:mason_logger/mason_logger.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:shorebird_cli/src/auth/auth.dart';
|
||||
import 'package:shorebird_cli/src/commands/build/build.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_process.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
class _MockArgResults extends Mock implements ArgResults {}
|
||||
|
||||
class _MockAuth extends Mock implements Auth {}
|
||||
|
||||
class _MockLogger extends Mock implements Logger {}
|
||||
|
||||
class _MockProcessResult extends Mock implements ShorebirdProcessResult {}
|
||||
|
||||
class _MockProgress extends Mock implements Progress {}
|
||||
|
||||
class _MockShorebirdProcess extends Mock implements ShorebirdProcess {}
|
||||
|
||||
void main() {
|
||||
group(BuildAarCommand, () {
|
||||
const buildNumber = '1.0';
|
||||
const noModulePubspecYamlContent = '''
|
||||
name: example
|
||||
version: 1.0.0
|
||||
environment:
|
||||
sdk: ">=2.19.0 <3.0.0"
|
||||
|
||||
flutter:
|
||||
assets:
|
||||
- shorebird.yaml''';
|
||||
|
||||
const pubspecYamlContent = '''
|
||||
name: example
|
||||
version: 1.0.0
|
||||
environment:
|
||||
sdk: ">=2.19.0 <3.0.0"
|
||||
|
||||
flutter:
|
||||
module:
|
||||
androidX: true
|
||||
androidPackage: com.example.my_flutter_module
|
||||
iosBundleIdentifier: com.example.myFlutterModule
|
||||
assets:
|
||||
- shorebird.yaml''';
|
||||
|
||||
late ArgResults argResults;
|
||||
late Auth auth;
|
||||
late Logger logger;
|
||||
late Progress progress;
|
||||
late ShorebirdProcess shorebirdProcess;
|
||||
late ShorebirdProcessResult processResult;
|
||||
|
||||
late BuildAarCommand command;
|
||||
|
||||
Directory setUpTempDir({bool includeModule = true}) {
|
||||
final tempDir = Directory.systemTemp.createTempSync();
|
||||
File(
|
||||
p.join(tempDir.path, 'pubspec.yaml'),
|
||||
).writeAsStringSync(
|
||||
includeModule ? pubspecYamlContent : noModulePubspecYamlContent,
|
||||
);
|
||||
return tempDir;
|
||||
}
|
||||
|
||||
setUp(() {
|
||||
argResults = _MockArgResults();
|
||||
auth = _MockAuth();
|
||||
logger = _MockLogger();
|
||||
processResult = _MockProcessResult();
|
||||
progress = _MockProgress();
|
||||
shorebirdProcess = _MockShorebirdProcess();
|
||||
|
||||
command = BuildAarCommand(
|
||||
auth: auth,
|
||||
logger: logger,
|
||||
)
|
||||
..testArgResults = argResults
|
||||
..testProcess = shorebirdProcess
|
||||
..testEngineConfig = const EngineConfig.empty();
|
||||
|
||||
when(() => argResults['build-number']).thenReturn(buildNumber);
|
||||
when(() => argResults.rest).thenReturn([]);
|
||||
when(() => auth.isAuthenticated).thenReturn(true);
|
||||
when(() => logger.progress(any())).thenReturn(progress);
|
||||
|
||||
when(
|
||||
() => shorebirdProcess.run(
|
||||
any(),
|
||||
any(),
|
||||
runInShell: any(named: 'runInShell'),
|
||||
),
|
||||
).thenAnswer((invocation) async {
|
||||
return processResult;
|
||||
});
|
||||
});
|
||||
|
||||
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(any(that: contains('You must be logged in to run'))),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('exits with 78 if no pubspec.yaml exists', () async {
|
||||
final tempDir = Directory.systemTemp.createTempSync();
|
||||
final result = await IOOverrides.runZoned(
|
||||
() async => command.run(),
|
||||
getCurrentDirectory: () => tempDir,
|
||||
);
|
||||
|
||||
expect(result, ExitCode.config.code);
|
||||
});
|
||||
|
||||
test('exits with 78 if no module entry exists in pubspec.yaml', () async {
|
||||
final tempDir = setUpTempDir(includeModule: false);
|
||||
final result = await IOOverrides.runZoned(
|
||||
() async => command.run(),
|
||||
getCurrentDirectory: () => tempDir,
|
||||
);
|
||||
|
||||
expect(result, ExitCode.config.code);
|
||||
});
|
||||
|
||||
test('exits with code 70 when building aar fails', () async {
|
||||
when(() => processResult.exitCode).thenReturn(1);
|
||||
when(() => processResult.stderr).thenReturn('oops');
|
||||
final tempDir = setUpTempDir();
|
||||
|
||||
final result = await IOOverrides.runZoned(
|
||||
() async => command.run(),
|
||||
getCurrentDirectory: () => tempDir,
|
||||
);
|
||||
|
||||
expect(result, equals(ExitCode.software.code));
|
||||
verify(
|
||||
() => shorebirdProcess.run(
|
||||
'flutter',
|
||||
[
|
||||
'build',
|
||||
'aar',
|
||||
'--no-debug',
|
||||
'--no-profile',
|
||||
'--build-number=$buildNumber',
|
||||
],
|
||||
runInShell: any(named: 'runInShell'),
|
||||
),
|
||||
).called(1);
|
||||
verify(() => progress.fail(any(that: contains('Failed to build'))))
|
||||
.called(1);
|
||||
});
|
||||
|
||||
test('exits with code 0 when building aar succeeds', () async {
|
||||
when(() => processResult.exitCode).thenReturn(ExitCode.success.code);
|
||||
final tempDir = setUpTempDir();
|
||||
final result = await IOOverrides.runZoned(
|
||||
() async => command.run(),
|
||||
getCurrentDirectory: () => tempDir,
|
||||
);
|
||||
|
||||
expect(result, equals(ExitCode.success.code));
|
||||
|
||||
verify(
|
||||
() => shorebirdProcess.run(
|
||||
'flutter',
|
||||
[
|
||||
'build',
|
||||
'aar',
|
||||
'--no-debug',
|
||||
'--no-profile',
|
||||
'--build-number=$buildNumber',
|
||||
],
|
||||
runInShell: any(named: 'runInShell'),
|
||||
),
|
||||
).called(1);
|
||||
verify(
|
||||
() => logger.info(
|
||||
'''
|
||||
📦 Generated an aar at:
|
||||
${lightCyan.wrap(
|
||||
p.join(
|
||||
'build',
|
||||
'host',
|
||||
'outputs',
|
||||
'repo',
|
||||
'com',
|
||||
'example',
|
||||
'my_flutter_module',
|
||||
'flutter_release',
|
||||
buildNumber,
|
||||
'flutter_release-$buildNumber.aar',
|
||||
),
|
||||
)}''',
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test(
|
||||
'''exits with code 0 when building aar succeeds with flavor and custom build number''',
|
||||
() async {
|
||||
const flavor = 'development';
|
||||
when(() => argResults['flavor']).thenReturn(flavor);
|
||||
when(() => argResults['build-number']).thenReturn('2.0');
|
||||
when(() => processResult.exitCode).thenReturn(ExitCode.success.code);
|
||||
final tempDir = setUpTempDir();
|
||||
final result = await IOOverrides.runZoned(
|
||||
() async => command.run(),
|
||||
getCurrentDirectory: () => tempDir,
|
||||
);
|
||||
|
||||
expect(result, equals(ExitCode.success.code));
|
||||
|
||||
verify(
|
||||
() => shorebirdProcess.run(
|
||||
'flutter',
|
||||
[
|
||||
'build',
|
||||
'aar',
|
||||
'--no-debug',
|
||||
'--no-profile',
|
||||
'--build-number=2.0',
|
||||
'--flavor=$flavor',
|
||||
],
|
||||
runInShell: any(named: 'runInShell'),
|
||||
),
|
||||
).called(1);
|
||||
verify(
|
||||
() => logger.info(
|
||||
'''
|
||||
📦 Generated an aar at:
|
||||
${lightCyan.wrap(
|
||||
p.join(
|
||||
'build',
|
||||
'host',
|
||||
'outputs',
|
||||
'repo',
|
||||
'com',
|
||||
'example',
|
||||
'my_flutter_module',
|
||||
'flutter_release',
|
||||
'2.0',
|
||||
'flutter_release-2.0.aar',
|
||||
),
|
||||
)}''',
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user