feat(shorebird_cli): improve gradlew logs during shorebird init (#2991)
This commit is contained in:
@@ -107,6 +107,28 @@ Please make sure you are running "shorebird init" from within your Flutter proje
|
||||
Set<String>? macosFlavors;
|
||||
var productFlavors = <String>{};
|
||||
final projectRoot = shorebirdEnv.getFlutterProjectRoot()!;
|
||||
final initializeGradleProgress = logger.progress('Initializing gradlew');
|
||||
final bool shouldStartGradleDaemon;
|
||||
try {
|
||||
shouldStartGradleDaemon = await _shouldStartGradleDaemon(
|
||||
projectRoot.path,
|
||||
);
|
||||
} on Exception {
|
||||
initializeGradleProgress.fail();
|
||||
logger.err('Unable to initialize gradlew.');
|
||||
return ExitCode.software.code;
|
||||
}
|
||||
initializeGradleProgress.complete();
|
||||
|
||||
if (shouldStartGradleDaemon) {
|
||||
try {
|
||||
await gradlew.startDaemon(projectRoot.path);
|
||||
} on Exception {
|
||||
logger.err('Unable to start gradle daemon.');
|
||||
return ExitCode.software.code;
|
||||
}
|
||||
}
|
||||
|
||||
final detectFlavorsProgress = logger.progress('Detecting product flavors');
|
||||
try {
|
||||
androidFlavors = await _maybeGetAndroidFlavors(projectRoot.path);
|
||||
@@ -287,6 +309,15 @@ For more information about Shorebird, visit ${link(uri: Uri.parse('https://shore
|
||||
return ExitCode.success.code;
|
||||
}
|
||||
|
||||
Future<bool> _shouldStartGradleDaemon(String projectPath) async {
|
||||
try {
|
||||
final isAvailable = await gradlew.isDaemonAvailable(projectPath);
|
||||
return !isAvailable;
|
||||
} on MissingAndroidProjectException {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Set<String>?> _maybeGetAndroidFlavors(String projectPath) async {
|
||||
try {
|
||||
return await gradlew.productFlavors(projectPath);
|
||||
|
||||
@@ -120,6 +120,29 @@ class Gradlew {
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<int> _stream(List<String> args, String projectRoot) async {
|
||||
final javaHome = java.home;
|
||||
final androidRoot = Directory(p.join(projectRoot, 'android'));
|
||||
|
||||
if (!androidRoot.existsSync()) {
|
||||
throw MissingAndroidProjectException(projectRoot);
|
||||
}
|
||||
|
||||
final executableFile = File(p.join(androidRoot.path, executable));
|
||||
|
||||
if (!executableFile.existsSync()) {
|
||||
throw MissingGradleWrapperException(p.relative(executableFile.path));
|
||||
}
|
||||
|
||||
final executablePath = executableFile.path;
|
||||
return process.stream(
|
||||
executablePath,
|
||||
args,
|
||||
workingDirectory: p.dirname(executablePath),
|
||||
environment: {if (!javaHome.isNullOrEmpty) 'JAVA_HOME': javaHome!},
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns whether the gradle wrapper exists at [projectRoot].
|
||||
bool exists(String projectRoot) =>
|
||||
File(p.join(projectRoot, 'android', executable)).existsSync();
|
||||
@@ -136,6 +159,34 @@ class Gradlew {
|
||||
return match?.group(1) ?? 'unknown';
|
||||
}
|
||||
|
||||
/// Whether the gradle daemon is available at [projectRoot].
|
||||
/// Command: `./gradlew --status`
|
||||
Future<bool> isDaemonAvailable(String projectRoot) async {
|
||||
// Sample output:
|
||||
// PID STATUS INFO
|
||||
// 30047 IDLE 8.11.1
|
||||
// 26397 STOPPED (after the daemon registry became unreadable)
|
||||
// 23432 STOPPED (by user or operating system)
|
||||
final status = await _run(['--status'], projectRoot);
|
||||
if (status.exitCode != 0) {
|
||||
throw Exception('Unable to determine gradle daemon status');
|
||||
}
|
||||
|
||||
// If we have a daemon that is either IDLE or BUSY then subsequent
|
||||
// gradle commands will be faster.
|
||||
return status.stdout.toString().contains('IDLE') ||
|
||||
status.stdout.toString().contains('BUSY');
|
||||
}
|
||||
|
||||
/// Starts the daemon if not running at [projectRoot].
|
||||
/// Command: `./gradlew --daemon`
|
||||
Future<void> startDaemon(String projectRoot) async {
|
||||
final exitCode = await _stream(['--daemon'], projectRoot);
|
||||
if (exitCode != 0) {
|
||||
throw Exception('Unable to start gradle daemon');
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the set of product flavors configured for the app at [projectRoot].
|
||||
/// Returns an empty set for apps that do not use product flavors.
|
||||
Future<Set<String>> productFlavors(String projectRoot) async {
|
||||
|
||||
@@ -112,6 +112,9 @@ environment:
|
||||
() => doctor.runValidators(any(), applyFixes: any(named: 'applyFixes')),
|
||||
).thenAnswer((_) async => {});
|
||||
when(() => doctor.generalValidators).thenReturn([]);
|
||||
when(
|
||||
() => gradlew.isDaemonAvailable(any()),
|
||||
).thenAnswer((_) async => true);
|
||||
when(
|
||||
() => shorebirdEnv.getShorebirdYamlFile(cwd: any(named: 'cwd')),
|
||||
).thenReturn(shorebirdYamlFile);
|
||||
@@ -251,6 +254,45 @@ Please make sure you are running "shorebird init" from within your Flutter proje
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('gracefully handles missing android project exceptions', () async {
|
||||
when(
|
||||
() => gradlew.isDaemonAvailable(any()),
|
||||
).thenThrow(const MissingAndroidProjectException(''));
|
||||
final exitCode = await runWithOverrides(command.run);
|
||||
expect(exitCode, ExitCode.success.code);
|
||||
verifyNever(() => gradlew.startDaemon(any()));
|
||||
});
|
||||
|
||||
test('throws when unable to initialize gradle wrapper', () async {
|
||||
when(() => gradlew.isDaemonAvailable(any())).thenThrow(Exception('oops'));
|
||||
final exitCode = await runWithOverrides(command.run);
|
||||
expect(exitCode, ExitCode.software.code);
|
||||
verifyNever(() => gradlew.startDaemon(any()));
|
||||
verify(() => logger.err('Unable to initialize gradlew.')).called(1);
|
||||
});
|
||||
|
||||
test('starts gradle daemon if needed and throws on error', () async {
|
||||
when(
|
||||
() => gradlew.isDaemonAvailable(any()),
|
||||
).thenAnswer((_) async => false);
|
||||
when(() => gradlew.startDaemon(any())).thenThrow(Exception('oops'));
|
||||
final exitCode = await runWithOverrides(command.run);
|
||||
expect(exitCode, ExitCode.software.code);
|
||||
verify(() => gradlew.startDaemon(projectRoot.path)).called(1);
|
||||
verify(() => logger.err('Unable to start gradle daemon.')).called(1);
|
||||
});
|
||||
|
||||
test('starts gradle daemon if needed and streams logs', () async {
|
||||
when(
|
||||
() => gradlew.isDaemonAvailable(any()),
|
||||
).thenAnswer((_) async => false);
|
||||
when(() => gradlew.startDaemon(any())).thenAnswer((_) async {});
|
||||
final exitCode = await runWithOverrides(command.run);
|
||||
expect(exitCode, ExitCode.success.code);
|
||||
verify(() => gradlew.startDaemon(projectRoot.path)).called(1);
|
||||
verify(() => gradlew.isDaemonAvailable(projectRoot.path)).called(1);
|
||||
});
|
||||
|
||||
test('fails when an error occurs while extracting flavors', () async {
|
||||
final exception = Exception('oops');
|
||||
when(() => gradlew.productFlavors(any())).thenThrow(exception);
|
||||
|
||||
@@ -429,5 +429,162 @@ OS: Mac OS X 14.4.1 aarch64
|
||||
}, testOn: 'linux || mac-os');
|
||||
});
|
||||
});
|
||||
|
||||
group('isDaemonAvailable', () {
|
||||
late Directory projectRoot;
|
||||
|
||||
setUp(() {
|
||||
projectRoot = setUpAppTempDir();
|
||||
File(
|
||||
p.join(projectRoot.path, 'android', 'gradlew'),
|
||||
).createSync(recursive: true);
|
||||
});
|
||||
|
||||
test('returns true when status is IDLE', () async {
|
||||
when(() => result.stdout).thenReturn('''
|
||||
PID STATUS INFO
|
||||
30047 IDLE 8.11.1
|
||||
''');
|
||||
expect(
|
||||
await runWithOverrides(
|
||||
() => gradlew.isDaemonAvailable(projectRoot.path),
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('returns true when status is BUSY', () async {
|
||||
when(() => result.stdout).thenReturn('''
|
||||
PID STATUS INFO
|
||||
30047 BUSY 8.11.1
|
||||
''');
|
||||
expect(
|
||||
await runWithOverrides(
|
||||
() => gradlew.isDaemonAvailable(projectRoot.path),
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('returns false when status is STOPPED', () async {
|
||||
when(() => result.stdout).thenReturn('''
|
||||
PID STATUS INFO
|
||||
26397 STOPPED (after the daemon registry became unreadable)
|
||||
''');
|
||||
expect(
|
||||
await runWithOverrides(
|
||||
() => gradlew.isDaemonAvailable(projectRoot.path),
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
group('when there are no daemons', () {
|
||||
test('returns false when no daemons are running', () async {
|
||||
when(() => result.stdout).thenReturn('''
|
||||
No daemons are running.
|
||||
''');
|
||||
expect(
|
||||
await runWithOverrides(
|
||||
() => gradlew.isDaemonAvailable(projectRoot.path),
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('throws when the process exits with non-zero exit code', () async {
|
||||
when(() => result.exitCode).thenReturn(1);
|
||||
await expectLater(
|
||||
runWithOverrides(() => gradlew.isDaemonAvailable(projectRoot.path)),
|
||||
throwsA(isA<Exception>()),
|
||||
);
|
||||
});
|
||||
}, testOn: 'linux || mac-os');
|
||||
|
||||
group('startDaemon', () {
|
||||
late Directory projectRoot;
|
||||
|
||||
setUp(() {
|
||||
projectRoot = setUpAppTempDir();
|
||||
File(
|
||||
p.join(projectRoot.path, 'android', 'gradlew'),
|
||||
).createSync(recursive: true);
|
||||
});
|
||||
|
||||
test('throws MissingAndroidProjectException '
|
||||
'when android root does not exist', () async {
|
||||
final tempDir = Directory.systemTemp.createTempSync();
|
||||
await expectLater(
|
||||
runWithOverrides(() => gradlew.startDaemon(tempDir.path)),
|
||||
throwsA(isA<MissingAndroidProjectException>()),
|
||||
);
|
||||
verifyNever(
|
||||
() => process.stream(
|
||||
any(),
|
||||
any(),
|
||||
workingDirectory: any(named: 'workingDirectory'),
|
||||
environment: any(named: 'environment'),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('throws MissingGradleWrapperException '
|
||||
'when gradlew does not exist', () async {
|
||||
final tempDir = setUpAppTempDir();
|
||||
await expectLater(
|
||||
runWithOverrides(() => gradlew.startDaemon(tempDir.path)),
|
||||
throwsA(isA<MissingGradleWrapperException>()),
|
||||
);
|
||||
verifyNever(
|
||||
() => process.stream(
|
||||
any(),
|
||||
any(),
|
||||
workingDirectory: any(named: 'workingDirectory'),
|
||||
environment: any(named: 'environment'),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('throws when the process exits with non-zero exit code', () async {
|
||||
final exitCode = ExitCode.software.code;
|
||||
when(
|
||||
() => process.stream(
|
||||
any(),
|
||||
any(),
|
||||
workingDirectory: any(named: 'workingDirectory'),
|
||||
environment: any(named: 'environment'),
|
||||
),
|
||||
).thenAnswer((_) async => exitCode);
|
||||
await expectLater(
|
||||
runWithOverrides(() => gradlew.startDaemon(projectRoot.path)),
|
||||
throwsA(isA<Exception>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('calls process.stream with correct args', () async {
|
||||
final exitCode = ExitCode.success.code;
|
||||
when(
|
||||
() => process.stream(
|
||||
any(),
|
||||
any(),
|
||||
workingDirectory: any(named: 'workingDirectory'),
|
||||
environment: any(named: 'environment'),
|
||||
),
|
||||
).thenAnswer((_) async => exitCode);
|
||||
await expectLater(
|
||||
runWithOverrides(() => gradlew.startDaemon(projectRoot.path)),
|
||||
completes,
|
||||
);
|
||||
verify(
|
||||
() => process.stream(
|
||||
p.join(projectRoot.path, 'android', 'gradlew'),
|
||||
['--daemon'],
|
||||
workingDirectory: p.join(projectRoot.path, 'android'),
|
||||
environment: {'JAVA_HOME': javaHome},
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
}, testOn: 'linux || mac-os');
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user