Skip to content

Commit

Permalink
feat: add gradle steps to appbundle build progress (#2579)
Browse files Browse the repository at this point in the history
  • Loading branch information
bryanoltman authored Oct 25, 2024
1 parent d8b1b00 commit 08f5441
Show file tree
Hide file tree
Showing 11 changed files with 344 additions and 18 deletions.
1 change: 1 addition & 0 deletions cspell.config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ words:
- udid # Unique Device Identifier
- unawaited
- unmockable
- unsets
- upvote
- usbmuxd
- vmcode
Expand Down
38 changes: 33 additions & 5 deletions packages/shorebird_cli/lib/src/artifact_builder.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -86,6 +87,7 @@ class ArtifactBuilder {
Iterable<Arch>? targetPlatforms,
List<String> args = const [],
String? base64PublicKey,
DetailProgress? buildProgress,
}) async {
await _runShorebirdBuildCommand(() async {
const executable = 'flutter';
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -81,6 +81,7 @@ class AndroidPatcher extends Patcher {
args: argResults.forwardedArgs +
buildNameAndNumberArgsFromReleaseVersion(releaseVersion),
base64PublicKey: argResults.encodedPublicKey,
buildProgress: buildProgress,
);
buildProgress.complete();
} on ArtifactBuildException catch (error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);
Expand Down
77 changes: 77 additions & 0 deletions packages/shorebird_cli/lib/src/logging/detail_progress.dart
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions packages/shorebird_cli/lib/src/logging/logging.dart
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export 'detail_progress.dart';
export 'logging_stdout.dart';
export 'shorebird_logger.dart';
95 changes: 86 additions & 9 deletions packages/shorebird_cli/test/src/artifact_builder_test.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'dart:convert';
import 'dart:io';

import 'package:mason_logger/mason_logger.dart';
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -71,6 +73,7 @@ void main() {
shorebirdArtifacts = MockShorebirdArtifacts();
shorebirdEnv = MockShorebirdEnv();
shorebirdProcess = MockShorebirdProcess();
buildProcess = MockProcess();

when(
() => shorebirdProcess.run(
Expand All @@ -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(
'''
Expand Down Expand Up @@ -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'),
Expand All @@ -201,7 +226,7 @@ Either run `flutter pub get` manually, or follow the steps in ${cannotRunInVSCod
);

verify(
() => shorebirdProcess.run(
() => shorebirdProcess.start(
'flutter',
[
'build',
Expand All @@ -223,7 +248,7 @@ Either run `flutter pub get` manually, or follow the steps in ${cannotRunInVSCod

setUp(() {
when(
() => shorebirdProcess.run(
() => shorebirdProcess.start(
'flutter',
[
'build',
Expand All @@ -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 {
Expand All @@ -252,7 +277,7 @@ Either run `flutter pub get` manually, or follow the steps in ${cannotRunInVSCod
);

verify(
() => shorebirdProcess.run(
() => shorebirdProcess.start(
'flutter',
[
'build',
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand Down
Loading

0 comments on commit 08f5441

Please sign in to comment.