feat: add gradle steps to appbundle build progress (#2579)

This commit is contained in:
Bryan Oltman
2024-10-25 17:41:10 -04:00
committed by GitHub
parent d8b1b00763
commit 08f5441f24
11 changed files with 344 additions and 18 deletions
+1
View File
@@ -99,6 +99,7 @@ words:
- udid # Unique Device Identifier
- unawaited
- unmockable
- unsets
- upvote
- usbmuxd
- vmcode
@@ -1,5 +1,6 @@
// cspell:words endtemplate aabs ipas appbundle bryanoltman codesign xcarchive
// cspell:words xcframework
import 'dart:convert';
import 'dart:io';
import 'package:mason_logger/mason_logger.dart';
@@ -86,6 +87,7 @@ class ArtifactBuilder {
Iterable<Arch>? targetPlatforms,
List<String> args = const [],
String? base64PublicKey,
DetailProgress? buildProgress,
}) async {
await _runShorebirdBuildCommand(() async {
const executable = 'flutter';
@@ -100,20 +102,46 @@ class ArtifactBuilder {
...args,
];
final result = await process.run(
final buildProcess = await process.start(
executable,
arguments,
runInShell: true,
environment: base64PublicKey?.toPublicKeyEnv(),
);
if (result.exitCode != ExitCode.success.code) {
throw ArtifactBuildException(
'Failed to build: ${result.stderr}',
);
// Android builds are a series of gradle tasks that are all logged in
// this format. We can use the 'Task :' line to get the current task
// being run.
final gradleTaskRegex = RegExp(r'^\[.*\] \> (Task :.*)$');
buildProcess.stdout
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen((line) {
if (buildProgress == null) {
return;
}
final captured = gradleTaskRegex.firstMatch(line)?.group(1);
if (captured != null) {
buildProgress.updateDetailMessage(captured);
}
});
final stderrLines = await buildProcess.stderr
.transform(utf8.decoder)
.transform(const LineSplitter())
.toList();
final stdErr = stderrLines.join('\n');
final exitCode = await buildProcess.exitCode;
if (exitCode != ExitCode.success.code) {
throw ArtifactBuildException('Failed to build: $stdErr');
}
});
// If we've been updating the progress with gradle tasks, reset it to the
// original base message so as not to leave the user with a confusing
// message.
buildProgress?.updateDetailMessage(null);
final projectRoot = shorebirdEnv.getShorebirdProjectRoot()!;
try {
return shorebirdAndroidArtifacts.findAab(
@@ -71,8 +71,8 @@ class AndroidPatcher extends Patcher {
Future<File> buildPatchArtifact({String? releaseVersion}) async {
final File aabFile;
final flutterVersionString = await shorebirdFlutter.getVersionAndRevision();
final buildProgress =
logger.progress('Building patch with Flutter $flutterVersionString');
final buildProgress = logger
.detailProgress('Building patch with Flutter $flutterVersionString');
try {
aabFile = await artifactBuilder.buildAppBundle(
@@ -81,6 +81,7 @@ class AndroidPatcher extends Patcher {
args: argResults.forwardedArgs +
buildNameAndNumberArgsFromReleaseVersion(releaseVersion),
base64PublicKey: argResults.encodedPublicKey,
buildProgress: buildProgress,
);
buildProgress.complete();
} on ArtifactBuildException catch (error) {
@@ -100,8 +100,9 @@ Please comment and upvote ${link(uri: Uri.parse('https://github.com/shorebirdtec
final flutterVersionString = await shorebirdFlutter.getVersionAndRevision();
final buildAppBundleProgress = logger
.progress('Building app bundle with Flutter $flutterVersionString');
final buildAppBundleProgress = logger.detailProgress(
'Building app bundle with Flutter $flutterVersionString',
);
final File aab;
@@ -114,6 +115,7 @@ Please comment and upvote ${link(uri: Uri.parse('https://github.com/shorebirdtec
targetPlatforms: architectures,
args: argResults.forwardedArgs,
base64PublicKey: base64PublicKey,
buildProgress: buildAppBundleProgress,
);
} on ArtifactBuildException catch (e) {
buildAppBundleProgress.fail(e.message);
@@ -0,0 +1,77 @@
import 'package:mason_logger/mason_logger.dart';
/// {@template detail_progress}
/// A [Progress] wrapper that allows for maintaining a base message (e.g., a
/// task title) while updating the message with more specific information,
/// rendered in dark gray.
/// {@endtemplate}
class DetailProgress implements Progress {
/// {@macro detail_progress}
DetailProgress._({
required Progress progress,
required String primaryMessage,
}) : _progress = progress,
_primaryMessage = primaryMessage;
String _primaryMessage;
String? _detailMessage;
final Progress _progress;
/// Updates the main message of the progress with the given [message]. This is
/// roughly equivalent to calling [Progress.update], except that this will
/// preserve the detail message if one exists.
void updatePrimaryMessage(String message) {
_primaryMessage = message;
_updateImpl();
}
/// Updates the detail message of the progress with the given [message].
void updateDetailMessage(String? message) {
_detailMessage = message;
_updateImpl();
}
@override
void update(String update) {
_primaryMessage = update;
_detailMessage = null;
_updateImpl();
}
void _updateImpl() {
final detailMessage = _detailMessage;
if (detailMessage != null) {
_progress.update('$_primaryMessage ${darkGray.wrap(detailMessage)}');
} else {
_progress.update(_primaryMessage);
}
}
@override
void cancel() {
_progress.cancel();
}
@override
void complete([String? update]) {
_progress.complete(update ?? _primaryMessage);
}
@override
void fail([String? update]) {
_progress.fail(update ?? _primaryMessage);
}
}
/// {@template detail_progress_logger}
/// Adds a method to [Logger] to create an [DetailProgress] instance.
/// {@endtemplate}
extension DetailProgressLogger on Logger {
/// {@macro detail_progress_logger}
DetailProgress detailProgress(String primaryMessage) {
return DetailProgress._(
progress: progress(primaryMessage),
primaryMessage: primaryMessage,
);
}
}
@@ -1,2 +1,3 @@
export 'detail_progress.dart';
export 'logging_stdout.dart';
export 'shorebird_logger.dart';
@@ -1,3 +1,4 @@
import 'dart:convert';
import 'dart:io';
import 'package:mason_logger/mason_logger.dart';
@@ -39,6 +40,7 @@ void main() {
late ShorebirdProcessResult buildProcessResult;
late ShorebirdProcessResult pubGetProcessResult;
late ArtifactBuilder builder;
late Process buildProcess;
R runWithOverrides<R>(R Function() body) {
return runScoped(
@@ -71,6 +73,7 @@ void main() {
shorebirdArtifacts = MockShorebirdArtifacts();
shorebirdEnv = MockShorebirdEnv();
shorebirdProcess = MockShorebirdProcess();
buildProcess = MockProcess();
when(
() => shorebirdProcess.run(
@@ -89,6 +92,13 @@ void main() {
runInShell: any(named: 'runInShell'),
),
).thenAnswer((_) async => buildProcessResult);
when(
() => shorebirdProcess.start(
any(),
any(),
runInShell: any(named: 'runInShell'),
),
).thenAnswer((_) async => buildProcess);
when(() => buildProcessResult.exitCode).thenReturn(ExitCode.success.code);
when(() => buildProcessResult.stdout).thenReturn(
'''
@@ -175,13 +185,28 @@ Either run `flutter pub get` manually, or follow the steps in ${cannotRunInVSCod
flavor: any(named: 'flavor'),
),
).thenReturn(File('app-release.aab'));
when(() => buildProcess.stdout).thenAnswer(
(_) => Stream.fromIterable(
[
'Some build output',
].map(utf8.encode),
),
);
when(() => buildProcess.stderr).thenAnswer(
(_) => Stream.fromIterable(
[
'Some build output',
].map(utf8.encode),
),
);
when(() => buildProcess.exitCode).thenAnswer((_) async => 0);
});
test('invokes the correct flutter build command', () async {
await runWithOverrides(() => builder.buildAppBundle());
verify(
() => shorebirdProcess.run(
() => shorebirdProcess.start(
'flutter',
['build', 'appbundle', '--release'],
runInShell: any(named: 'runInShell'),
@@ -201,7 +226,7 @@ Either run `flutter pub get` manually, or follow the steps in ${cannotRunInVSCod
);
verify(
() => shorebirdProcess.run(
() => shorebirdProcess.start(
'flutter',
[
'build',
@@ -223,7 +248,7 @@ Either run `flutter pub get` manually, or follow the steps in ${cannotRunInVSCod
setUp(() {
when(
() => shorebirdProcess.run(
() => shorebirdProcess.start(
'flutter',
[
'build',
@@ -238,7 +263,7 @@ Either run `flutter pub get` manually, or follow the steps in ${cannotRunInVSCod
'SHOREBIRD_PUBLIC_KEY': base64PublicKey,
},
),
).thenAnswer((_) async => buildProcessResult);
).thenAnswer((_) async => buildProcess);
});
test('adds the SHOREBIRD_PUBLIC_KEY to the environment', () async {
@@ -252,7 +277,7 @@ Either run `flutter pub get` manually, or follow the steps in ${cannotRunInVSCod
);
verify(
() => shorebirdProcess.run(
() => shorebirdProcess.start(
'flutter',
[
'build',
@@ -329,11 +354,63 @@ Either run `flutter pub get` manually, or follow the steps in ${cannotRunInVSCod
});
});
group('when output contains gradle task names', () {
late DetailProgress progress;
setUp(() {
progress = MockDetailProgress();
when(() => buildProcess.stdout).thenAnswer(
(_) => Stream.fromIterable(
[
'Some build output',
'[ ] > Task :app:bundleRelease',
'More build output',
'[ ] > Task :app:someOtherTask',
'Even more build output',
]
.map((line) => '$line${Platform.lineTerminator}')
.map(utf8.encode),
),
);
when(() => buildProcess.stderr).thenAnswer(
(_) => Stream.fromIterable(
['Some build output'].map(utf8.encode),
),
);
});
test('updates progress with gradle task names', () async {
await expectLater(
runWithOverrides(
() => builder.buildAppBundle(
buildProgress: progress,
),
),
completes,
);
// Required to trigger stdout stream events
await pumpEventQueue();
// Ensure we update the progress in the correct order and with the
// correct messages, and reset to the base message after the build
// completes.
verifyInOrder(
[
() => progress.updateDetailMessage('Task :app:bundleRelease'),
() => progress.updateDetailMessage('Task :app:someOtherTask'),
() => progress.updateDetailMessage(null),
],
);
});
});
group('after a build', () {
group('when the build is successful', () {
setUp(() {
when(() => buildProcessResult.exitCode)
.thenReturn(ExitCode.success.code);
when(() => buildProcess.exitCode)
.thenAnswer((_) async => ExitCode.success.code);
});
verifyCorrectFlutterPubGet(
@@ -342,8 +419,8 @@ Either run `flutter pub get` manually, or follow the steps in ${cannotRunInVSCod
group('when the build fails', () {
setUp(() {
when(() => buildProcessResult.exitCode)
.thenReturn(ExitCode.software.code);
when(() => buildProcess.exitCode)
.thenAnswer((_) async => ExitCode.software.code);
});
verifyCorrectFlutterPubGet(
@@ -281,6 +281,7 @@ void main() {
targetPlatforms: any(named: 'targetPlatforms'),
args: any(named: 'args'),
base64PublicKey: any(named: 'base64PublicKey'),
buildProgress: any(named: 'buildProgress'),
),
).thenAnswer((_) async => aabFile);
});
@@ -293,7 +294,10 @@ void main() {
() => artifactBuilder.buildAppBundle(
flavor: any(named: 'flavor'),
target: any(named: 'target'),
targetPlatforms: any(named: 'targetPlatforms'),
args: any(named: 'args'),
base64PublicKey: any(named: 'base64PublicKey'),
buildProgress: any(named: 'buildProgress'),
),
).thenThrow(exception);
when(() => logger.progress(any())).thenReturn(progress);
@@ -350,6 +354,7 @@ Looked in:
named: 'args',
that: containsAll(['--build-name=1.2.3', '--build-number=4']),
),
buildProgress: any(named: 'buildProgress'),
),
).called(1);
});
@@ -369,6 +374,7 @@ Looked in:
verify(
() => artifactBuilder.buildAppBundle(
args: ['--verbose'],
buildProgress: any(named: 'buildProgress'),
),
).called(1);
});
@@ -401,6 +407,7 @@ Looked in:
flavor: any(named: 'flavor'),
target: any(named: 'target'),
base64PublicKey: 'public_key_encoded',
buildProgress: any(named: 'buildProgress'),
),
).called(1);
});
@@ -289,6 +289,7 @@ To change the version of this release, change your app's version in your pubspec
target: any(named: 'target'),
targetPlatforms: any(named: 'targetPlatforms'),
args: any(named: 'args'),
buildProgress: any(named: 'buildProgress'),
),
).thenAnswer((_) async => aabFile);
when(
@@ -320,6 +321,7 @@ To change the version of this release, change your app's version in your pubspec
target: any(named: 'target'),
targetPlatforms: any(named: 'targetPlatforms'),
args: any(named: 'args'),
buildProgress: any(named: 'buildProgress'),
),
).thenThrow(ArtifactBuildException('Uh oh'));
});
@@ -375,6 +377,7 @@ To change the version of this release, change your app's version in your pubspec
() => artifactBuilder.buildAppBundle(
targetPlatforms: Arch.values,
args: ['--verbose'],
buildProgress: any(named: 'buildProgress'),
),
).called(1);
});
@@ -389,6 +392,7 @@ To change the version of this release, change your app's version in your pubspec
() => artifactBuilder.buildAppBundle(
targetPlatforms: Arch.values,
args: [],
buildProgress: any(named: 'buildProgress'),
),
).called(1);
});
@@ -429,6 +433,7 @@ To change the version of this release, change your app's version in your pubspec
target: target,
targetPlatforms: Arch.values,
args: [],
buildProgress: any(named: 'buildProgress'),
),
).called(1);
verify(
@@ -462,6 +467,7 @@ To change the version of this release, change your app's version in your pubspec
targetPlatforms: any(named: 'targetPlatforms'),
args: any(named: 'args'),
base64PublicKey: any(named: 'base64PublicKey'),
buildProgress: any(named: 'buildProgress'),
),
).thenAnswer((_) async => aabFile);
when(
@@ -492,6 +498,7 @@ To change the version of this release, change your app's version in your pubspec
targetPlatforms: any(named: 'targetPlatforms'),
args: any(named: 'args'),
base64PublicKey: base64PublicKey,
buildProgress: any(named: 'buildProgress'),
),
).called(1);
},
@@ -516,6 +523,7 @@ To change the version of this release, change your app's version in your pubspec
targetPlatforms: any(named: 'targetPlatforms'),
args: any(named: 'args'),
base64PublicKey: base64PublicKey,
buildProgress: any(named: 'buildProgress'),
),
).called(1);
@@ -0,0 +1,122 @@
import 'package:mason_logger/mason_logger.dart';
import 'package:mocktail/mocktail.dart';
import 'package:scoped_deps/scoped_deps.dart';
import 'package:shorebird_cli/src/logging/logging.dart';
import 'package:test/test.dart';
import '../mocks.dart';
void main() {
group(DetailProgress, () {
late ShorebirdLogger logger;
late Progress progress;
late DetailProgress detailProgress;
R runWithOverrides<R>(R Function() body) {
return runScoped(
body,
values: {
loggerRef.overrideWith(() => logger),
},
);
}
setUp(() {
logger = MockShorebirdLogger();
progress = MockProgress();
when(() => logger.progress(any())).thenReturn(progress);
detailProgress = runWithOverrides(() => logger.detailProgress('title'));
});
group('updatePrimaryMessage', () {
group('when no detail message is set', () {
test('updates the primary message', () {
detailProgress.updatePrimaryMessage('new title');
verify(() => progress.update('new title')).called(1);
});
});
group('when a detail message is present', () {
setUp(() {
detailProgress.updateDetailMessage('detail');
});
test('updates the primary message and detail message', () {
detailProgress.updatePrimaryMessage('new title');
verify(
() => progress.update('new title ${darkGray.wrap('detail')}'),
).called(1);
});
});
});
group('updateDetailMessage', () {
test('updates the detail message', () {
detailProgress.updateDetailMessage('new detail');
verify(
() => progress.update('title ${darkGray.wrap('new detail')}'),
).called(1);
});
test('can unset the detail message', () {
detailProgress.updateDetailMessage(null);
verify(() => progress.update('title')).called(1);
});
});
group('update', () {
test('updates the primary message', () {
detailProgress.update('new title');
verify(() => progress.update('new title')).called(1);
});
group('when a detail message is set', () {
setUp(() {
detailProgress.updateDetailMessage('detail');
});
test('unsets the detail message', () {
detailProgress.update('new title');
verify(() => progress.update('new title')).called(1);
});
});
});
group('cancel', () {
test('cancels the progress', () {
detailProgress.cancel();
verify(() => progress.cancel()).called(1);
});
});
group('complete', () {
test('completes the progress', () {
detailProgress.complete();
verify(() => progress.complete('title')).called(1);
});
test('completes the progress with an update', () {
detailProgress.complete('update');
verify(() => progress.complete('update')).called(1);
});
});
group('fail', () {
test('fails the progress', () {
detailProgress.fail();
verify(() => progress.fail('title')).called(1);
});
test('fails the progress with an update', () {
detailProgress.fail('update');
verify(() => progress.fail('update')).called(1);
});
});
test('implements Progress', () {
expect(detailProgress, isA<Progress>());
});
});
}
@@ -81,6 +81,8 @@ class MockCodePushClientWrapper extends Mock implements CodePushClientWrapper {}
class MockCodeSigner extends Mock implements CodeSigner {}
class MockDetailProgress extends Mock implements DetailProgress {}
class MockDevicectl extends Mock implements Devicectl {}
class MockDirectory extends Mock implements Directory {}