fix(shorebird_cli): surface flutter precache failures instead of swallowing them (#3789)
This commit is contained in:
@@ -12,19 +12,22 @@ import 'package:shorebird_cli/src/platform.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_cli_command_runner.dart';
|
||||
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
|
||||
|
||||
/// Exception thrown when a required file in the Shorebird cache is missing or
|
||||
/// unreadable, indicating a corrupted installation.
|
||||
/// Exception thrown when the Shorebird cache appears to be corrupted.
|
||||
///
|
||||
/// Surfaces a user-actionable message directing the user to run
|
||||
/// `shorebird cache clean` and retry.
|
||||
class CacheCorruptedException implements Exception {
|
||||
/// Creates a [CacheCorruptedException] for the given [filePath].
|
||||
const CacheCorruptedException(this.filePath);
|
||||
/// Creates a [CacheCorruptedException] explaining why the cache is
|
||||
/// considered corrupted via [reason] (a complete sentence).
|
||||
const CacheCorruptedException(this.reason);
|
||||
|
||||
/// The path to the missing or unreadable file.
|
||||
final String filePath;
|
||||
/// Human-readable explanation of why the cache is considered corrupted.
|
||||
final String reason;
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'Could not read $filePath. Your Shorebird installation may be '
|
||||
"corrupted. Try running 'shorebird cache clean' and retrying.";
|
||||
'$reason Your Shorebird installation may be corrupted. '
|
||||
"Try running 'shorebird cache clean' and retrying.";
|
||||
}
|
||||
|
||||
/// A reference to a [ShorebirdEnv] instance.
|
||||
@@ -78,7 +81,7 @@ class ShorebirdEnv {
|
||||
try {
|
||||
return file.readAsStringSync().trim();
|
||||
} on FileSystemException {
|
||||
throw CacheCorruptedException(file.path);
|
||||
throw CacheCorruptedException('Could not read ${file.path}.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +94,7 @@ class ShorebirdEnv {
|
||||
try {
|
||||
return file.readAsStringSync().trim();
|
||||
} on FileSystemException {
|
||||
throw CacheCorruptedException(file.path);
|
||||
throw CacheCorruptedException('Could not read ${file.path}.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,13 @@ class ShorebirdFlutter {
|
||||
}
|
||||
|
||||
/// Install the provided Flutter [revision].
|
||||
///
|
||||
/// Runs `flutter precache` on first install as a convenience so the first
|
||||
/// build is not unexpectedly slow. A precache failure is treated as a
|
||||
/// corrupted install: Flutter's stamp-based cache will otherwise trust a
|
||||
/// partial extraction and surface the missing artifact later as an opaque
|
||||
/// Gradle error (see shorebirdtech/shorebird#3783). The user is directed
|
||||
/// to run `shorebird cache clean` to start over.
|
||||
Future<void> installRevision({required String revision}) async {
|
||||
final targetDirectory = Directory(_workingDirectory(revision: revision));
|
||||
if (targetDirectory.existsSync()) return;
|
||||
@@ -65,9 +72,8 @@ class ShorebirdFlutter {
|
||||
await git.checkout(directory: targetDirectory.path, revision: revision);
|
||||
installProgress.complete();
|
||||
} catch (error) {
|
||||
installProgress.fail(
|
||||
'Failed to install Flutter $version (${shortRevisionString(revision)})',
|
||||
);
|
||||
final short = shortRevisionString(revision);
|
||||
installProgress.fail('Failed to install Flutter $version ($short)');
|
||||
logger.err('$error');
|
||||
rethrow;
|
||||
}
|
||||
@@ -76,18 +82,28 @@ class ShorebirdFlutter {
|
||||
'Running ${lightCyan.wrap('flutter precache')}',
|
||||
);
|
||||
|
||||
final precacheArguments = ['precache', ...precacheArgs];
|
||||
final ShorebirdProcessResult result;
|
||||
try {
|
||||
await process.run(executable, [
|
||||
'precache',
|
||||
...precacheArgs,
|
||||
], workingDirectory: targetDirectory.path);
|
||||
precacheProgress.complete();
|
||||
} on Exception {
|
||||
result = await process.run(
|
||||
executable,
|
||||
precacheArguments,
|
||||
workingDirectory: targetDirectory.path,
|
||||
);
|
||||
} on Exception catch (error) {
|
||||
precacheProgress.fail('Failed to precache Flutter $version');
|
||||
logger.info(
|
||||
'''This is not a critical error, but your next build make take longer than usual.''',
|
||||
throw CacheCorruptedException(
|
||||
'Failed to precache Flutter $version: $error.',
|
||||
);
|
||||
}
|
||||
if (result.exitCode != ExitCode.success.code) {
|
||||
precacheProgress.fail('Failed to precache Flutter $version');
|
||||
final stderr = '${result.stderr}'.trim();
|
||||
throw CacheCorruptedException(
|
||||
'flutter precache exited with code ${result.exitCode}: $stderr.',
|
||||
);
|
||||
}
|
||||
precacheProgress.complete();
|
||||
}
|
||||
|
||||
/// Whether the current revision is unmodified.
|
||||
|
||||
@@ -110,7 +110,10 @@ void main() {
|
||||
workingDirectory: any(named: 'workingDirectory'),
|
||||
),
|
||||
).thenAnswer((_) async => precacheProcessResult);
|
||||
when(() => versionProcessResult.exitCode).thenReturn(0);
|
||||
when(
|
||||
() => precacheProcessResult.exitCode,
|
||||
).thenReturn(ExitCode.success.code);
|
||||
when(() => precacheProcessResult.stderr).thenReturn('');
|
||||
});
|
||||
|
||||
group('precacheArgs', () {
|
||||
@@ -978,7 +981,7 @@ origin/flutter_release/3.10.6''';
|
||||
).called(1);
|
||||
});
|
||||
|
||||
group('when unable to precache', () {
|
||||
group('when precache throws', () {
|
||||
setUp(() {
|
||||
when(
|
||||
() => process.run(
|
||||
@@ -989,33 +992,54 @@ origin/flutter_release/3.10.6''';
|
||||
).thenThrow(Exception('oh no!'));
|
||||
});
|
||||
|
||||
test('logs error and continues', () async {
|
||||
await expectLater(
|
||||
runWithOverrides(
|
||||
() => shorebirdFlutter.installRevision(revision: revision),
|
||||
),
|
||||
completes,
|
||||
);
|
||||
verify(
|
||||
() => process.run(
|
||||
'flutter',
|
||||
[
|
||||
'precache',
|
||||
...runWithOverrides(() => shorebirdFlutter.precacheArgs),
|
||||
],
|
||||
workingDirectory: p.join(flutterDirectory.parent.path, revision),
|
||||
),
|
||||
).called(1);
|
||||
test(
|
||||
'throws CacheCorruptedException directing to shorebird cache clean',
|
||||
() async {
|
||||
await expectLater(
|
||||
runWithOverrides(
|
||||
() => shorebirdFlutter.installRevision(revision: revision),
|
||||
),
|
||||
throwsA(
|
||||
isA<CacheCorruptedException>().having(
|
||||
(e) => e.toString(),
|
||||
'toString',
|
||||
contains('shorebird cache clean'),
|
||||
),
|
||||
),
|
||||
);
|
||||
verify(
|
||||
() => progress.fail('Failed to precache Flutter 3.10.6'),
|
||||
).called(1);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
verify(
|
||||
() => progress.fail('Failed to precache Flutter 3.10.6'),
|
||||
).called(1);
|
||||
verify(
|
||||
() => logger.info(
|
||||
'''This is not a critical error, but your next build make take longer than usual.''',
|
||||
),
|
||||
).called(1);
|
||||
group('when precache exits with a non-zero code', () {
|
||||
setUp(() {
|
||||
when(() => precacheProcessResult.exitCode).thenReturn(1);
|
||||
when(() => precacheProcessResult.stderr).thenReturn('boom');
|
||||
});
|
||||
|
||||
test(
|
||||
'throws CacheCorruptedException directing to shorebird cache clean',
|
||||
() async {
|
||||
await expectLater(
|
||||
runWithOverrides(
|
||||
() => shorebirdFlutter.installRevision(revision: revision),
|
||||
),
|
||||
throwsA(
|
||||
isA<CacheCorruptedException>().having(
|
||||
(e) => e.toString(),
|
||||
'toString',
|
||||
contains('shorebird cache clean'),
|
||||
),
|
||||
),
|
||||
);
|
||||
verify(
|
||||
() => progress.fail('Failed to precache Flutter 3.10.6'),
|
||||
).called(1);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('when clone and checkout succeed', () {
|
||||
|
||||
Reference in New Issue
Block a user