feat: adding gradlew version to the doctor output (#2255)

This commit is contained in:
Erick
2024-06-18 15:49:28 -03:00
committed by GitHub
parent 0f8e8fec46
commit 3e08a4dc2c
4 changed files with 402 additions and 214 deletions
@@ -73,6 +73,12 @@ Engine • revision ${shorebirdEnv.shorebirdEngineRevision}''',
.join('${Platform.lineTerminator} ');
}
}
String? gradlewVersion;
if (gradlew.exists(Directory.current.path)) {
gradlewVersion = await gradlew.version(Directory.current.path);
}
output.writeln('''
Logs: ${shorebirdEnv.logsDirectory.path}
@@ -82,7 +88,8 @@ Android Toolchain
• ADB: ${androidSdk.adbPath ?? notDetected}
• JAVA_HOME: ${java.home ?? notDetected}
• JAVA_EXECUTABLE: ${javaExe ?? notDetected}
• JAVA_VERSION: $javaVersion''');
• JAVA_VERSION: $javaVersion
• Gradle: ${gradlewVersion ?? notDetected}''');
}
logger.info(output.toString());
@@ -16,15 +16,15 @@ import 'package:shorebird_cli/src/shorebird_process.dart';
/// {@endtemplate}
class MissingAndroidProjectException implements Exception {
/// {@macro missing_android_project_exception}
const MissingAndroidProjectException(this.projectPath);
const MissingAndroidProjectException(this.projectRoot);
/// Expected path for the Android project.
final String projectPath;
final String projectRoot;
@override
String toString() {
return '''
Could not find an android project in $projectPath.
Could not find an android project in $projectRoot.
To add android, run "flutter create . --platforms android"''';
}
}
@@ -58,14 +58,12 @@ Gradlew get gradlew => read(gradlewRef);
class Gradlew {
String get executable => platform.isWindows ? 'gradlew.bat' : 'gradlew';
/// Return the set of product flavors configured for the app at [projectPath].
/// Returns an empty set for apps that do not use product flavors.
Future<Set<String>> productFlavors(String projectPath) async {
Future<ShorebirdProcessResult> _run(List<String> args, String projectRoot) {
final javaHome = java.home;
final androidRoot = Directory(p.join(projectPath, 'android'));
final androidRoot = Directory(p.join(projectRoot, 'android'));
if (!androidRoot.existsSync()) {
throw MissingAndroidProjectException(projectPath);
throw MissingAndroidProjectException(projectRoot);
}
final executableFile = File(p.join(androidRoot.path, executable));
@@ -75,15 +73,39 @@ class Gradlew {
}
final executablePath = executableFile.path;
final result = await process.run(
return process.run(
executablePath,
['app:tasks', '--all', '--console=auto'],
args,
runInShell: true,
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();
/// Return the version of the gradle wrapper at [projectRoot].
Future<String> version(String projectRoot) async {
final result = await _run(['--version'], projectRoot);
// Tries to match version string in the output (e.g. "Gradle 7.6.3")
final versionPattern = RegExp(r'Gradle (\d+\.\d+\.\d+)');
final match = versionPattern.firstMatch(result.stdout.toString());
return match?.group(1) ?? 'unknown';
}
/// 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 {
final result = await _run(
['app:tasks', '--all', '--console=auto'],
projectRoot,
);
if (result.exitCode != 0) {
throw Exception('${result.stdout}\n${result.stderr}');
@@ -28,6 +28,7 @@ void main() {
late AndroidSdk androidSdk;
late Directory logsDirectory;
late Doctor doctor;
late Gradlew gradlew;
late Java java;
late ShorebirdLogger logger;
late ShorebirdEnv shorebirdEnv;
@@ -42,6 +43,7 @@ void main() {
androidStudioRef.overrideWith(() => androidStudio),
androidSdkRef.overrideWith(() => androidSdk),
doctorRef.overrideWith(() => doctor),
gradlewRef.overrideWith(() => gradlew),
javaRef.overrideWith(() => java),
loggerRef.overrideWith(() => logger),
shorebirdEnvRef.overrideWith(() => shorebirdEnv),
@@ -55,6 +57,7 @@ void main() {
androidStudio = MockAndroidStudio();
androidSdk = MockAndroidSdk();
doctor = MockDoctor();
gradlew = MockGradlew();
logsDirectory = Directory.systemTemp.createTempSync('shorebird_logs');
java = MockJava();
logger = MockShorebirdLogger();
@@ -67,6 +70,7 @@ void main() {
when(() => androidStudio.path).thenReturn(null);
when(() => androidSdk.path).thenReturn(null);
when(() => androidSdk.adbPath).thenReturn(null);
when(() => gradlew.exists(any())).thenReturn(false);
when(() => java.home).thenReturn(null);
when(
() => shorebirdEnv.shorebirdEngineRevision,
@@ -142,6 +146,7 @@ Android Toolchain
• JAVA_HOME: $notDetectedText
• JAVA_EXECUTABLE: $notDetectedText
• JAVA_VERSION: $notDetectedText
• Gradle: $notDetectedText
'''),
);
});
@@ -166,6 +171,7 @@ OpenJDK 64-Bit Server VM (build 17.0.9+0-17.0.9b1087.7-11185874, mixed mode)'''
final msg =
verify(() => logger.info(captureAny())).captured.first as String;
final notDetectedText = red.wrap('not detected');
expect(
msg.replaceAll(
Platform.lineTerminator,
@@ -187,10 +193,65 @@ Android Toolchain
• JAVA_VERSION: openjdk version "17.0.9" 2023-10-17
OpenJDK Runtime Environment (build 17.0.9+0-17.0.9b1087.7-11185874)
OpenJDK 64-Bit Server VM (build 17.0.9+0-17.0.9b1087.7-11185874, mixed mode)
• Gradle: $notDetectedText
''',
),
);
});
group('when a gradlew executable exists', () {
setUp(() {
when(() => gradlew.exists(any())).thenReturn(true);
when(() => gradlew.version(any())).thenAnswer((_) async => '7.6.3');
when(() => argResults['verbose']).thenReturn(true);
when(() => androidStudio.path).thenReturn('test-studio-path');
when(() => androidSdk.path).thenReturn('test-sdk-path');
when(() => androidSdk.adbPath).thenReturn('test-adb-path');
when(() => java.home).thenReturn('test-java-home');
when(() => java.executable).thenReturn('test-java-executable');
when(() => java.version).thenReturn(
'''
openjdk version "17.0.9" 2023-10-17
OpenJDK Runtime Environment (build 17.0.9+0-17.0.9b1087.7-11185874)
OpenJDK 64-Bit Server VM (build 17.0.9+0-17.0.9b1087.7-11185874, mixed mode)'''
.replaceAll('\n', Platform.lineTerminator),
);
});
test('prints the gradle version', () async {
await runWithOverrides(command.run);
final msg =
verify(() => logger.info(captureAny())).captured.first as String;
expect(
msg.replaceAll(
Platform.lineTerminator,
'\n',
),
equals(
'''
Shorebird $packageVersion • git@github.com:shorebirdtech/shorebird.git
Flutter • revision ${shorebirdEnv.flutterRevision}
Engine • revision $shorebirdEngineRevision
Logs: ${logsDirectory.path}
Android Toolchain
• Android Studio: test-studio-path
• Android SDK: test-sdk-path
• ADB: test-adb-path
• JAVA_HOME: test-java-home
• JAVA_EXECUTABLE: test-java-executable
• JAVA_VERSION: openjdk version "17.0.9" 2023-10-17
OpenJDK Runtime Environment (build 17.0.9+0-17.0.9b1087.7-11185874)
OpenJDK 64-Bit Server VM (build 17.0.9+0-17.0.9b1087.7-11185874, mixed mode)
• Gradle: 7.6.3
''',
),
);
});
});
});
test('runs validators without applying fixes if no fix flag exists',
@@ -3,10 +3,8 @@ import 'dart:io' hide Platform;
import 'package:mason_logger/mason_logger.dart';
import 'package:mocktail/mocktail.dart';
import 'package:path/path.dart' as p;
import 'package:platform/platform.dart';
import 'package:scoped_deps/scoped_deps.dart';
import 'package:shorebird_cli/src/executables/executables.dart';
import 'package:shorebird_cli/src/platform.dart';
import 'package:shorebird_cli/src/shorebird_process.dart';
import 'package:test/test.dart';
@@ -17,7 +15,6 @@ void main() {
const javaHome = 'test_java_home';
late Java java;
late Platform platform;
late ShorebirdProcess process;
late ShorebirdProcessResult result;
late Gradlew gradlew;
@@ -27,7 +24,6 @@ void main() {
body,
values: {
javaRef.overrideWith(() => java),
platformRef.overrideWith(() => platform),
processRef.overrideWith(() => process),
},
);
@@ -35,7 +31,6 @@ void main() {
setUp(() {
java = MockJava();
platform = MockPlatform();
process = MockShorebirdProcess();
result = MockProcessResult();
gradlew = runWithOverrides(Gradlew.new);
@@ -76,206 +71,168 @@ Make sure you have run "flutter build apk" at least once.''',
});
});
Directory setUpAppTempDir() {
final tempDir = Directory.systemTemp.createTempSync();
Directory(p.join(tempDir.path, 'android')).createSync(recursive: true);
return tempDir;
}
group('productFlavors', () {
Directory setUpAppTempDir() {
final tempDir = Directory.systemTemp.createTempSync();
Directory(p.join(tempDir.path, 'android')).createSync(recursive: true);
return tempDir;
}
test(
'throws MissingAndroidProjectException '
'when android root does not exist', () async {
when(() => platform.isLinux).thenReturn(true);
when(() => platform.isMacOS).thenReturn(false);
when(() => platform.isWindows).thenReturn(false);
final tempDir = Directory.systemTemp.createTempSync();
await expectLater(
runWithOverrides(() => gradlew.productFlavors(tempDir.path)),
throwsA(isA<MissingAndroidProjectException>()),
);
verifyNever(
() => process.run(
p.join(tempDir.path, 'android', 'gradlew'),
['app:tasks', '--all', '--console=auto'],
runInShell: true,
workingDirectory: p.join(tempDir.path, 'android'),
environment: {'JAVA_HOME': javaHome},
),
);
});
test(
'throws MissingGradleWrapperException '
'when gradlew does not exist', () async {
when(() => platform.isLinux).thenReturn(true);
when(() => platform.isMacOS).thenReturn(false);
when(() => platform.isWindows).thenReturn(false);
final tempDir = setUpAppTempDir();
await expectLater(
runWithOverrides(() => gradlew.productFlavors(tempDir.path)),
throwsA(isA<MissingGradleWrapperException>()),
);
verifyNever(
() => process.run(
p.join(tempDir.path, 'android', 'gradlew'),
['app:tasks', '--all', '--console=auto'],
runInShell: true,
workingDirectory: p.join(tempDir.path, 'android'),
environment: {'JAVA_HOME': javaHome},
),
);
});
test('uses existing JAVA_HOME when set', () async {
when(() => platform.isLinux).thenReturn(true);
when(() => platform.isMacOS).thenReturn(false);
when(() => platform.isWindows).thenReturn(false);
final tempDir = setUpAppTempDir();
File(
p.join(tempDir.path, 'android', 'gradlew'),
).createSync(recursive: true);
await expectLater(
runWithOverrides(() => gradlew.productFlavors(tempDir.path)),
completes,
);
verify(
() => process.run(
p.join(tempDir.path, 'android', 'gradlew'),
['app:tasks', '--all', '--console=auto'],
runInShell: true,
workingDirectory: p.join(tempDir.path, 'android'),
environment: {'JAVA_HOME': javaHome},
),
).called(1);
});
test(
'throws Exception '
'when process exits with non-zero code', () async {
when(() => platform.isLinux).thenReturn(true);
when(() => platform.isMacOS).thenReturn(false);
when(() => platform.isWindows).thenReturn(false);
final tempDir = setUpAppTempDir();
File(
p.join(tempDir.path, 'android', 'gradlew'),
).createSync(recursive: true);
when(() => result.exitCode).thenReturn(1);
when(() => result.stderr).thenReturn('test error');
await expectLater(
runWithOverrides(() => gradlew.productFlavors(tempDir.path)),
throwsA(
isA<Exception>().having(
(e) => '$e',
'message',
contains('test error'),
'throws MissingAndroidProjectException '
'when android root does not exist',
() async {
final tempDir = Directory.systemTemp.createTempSync();
await expectLater(
runWithOverrides(() => gradlew.productFlavors(tempDir.path)),
throwsA(isA<MissingAndroidProjectException>()),
);
verifyNever(
() => process.run(
p.join(tempDir.path, 'android', 'gradlew'),
['app:tasks', '--all', '--console=auto'],
runInShell: true,
workingDirectory: p.join(tempDir.path, 'android'),
environment: {'JAVA_HOME': javaHome},
),
),
);
verify(
() => process.run(
p.join(tempDir.path, 'android', 'gradlew'),
['app:tasks', '--all', '--console=auto'],
runInShell: true,
workingDirectory: p.join(tempDir.path, 'android'),
environment: {'JAVA_HOME': javaHome},
),
).called(1);
});
);
},
testOn: 'linux',
);
test('extracts flavors', () async {
when(() => platform.isLinux).thenReturn(true);
when(() => platform.isMacOS).thenReturn(false);
when(() => platform.isWindows).thenReturn(false);
final tempDir = setUpAppTempDir();
File(
p.join(tempDir.path, 'android', 'gradlew'),
).createSync(recursive: true);
const javaHome = 'test_java_home';
when(() => platform.environment).thenReturn({'JAVA_HOME': javaHome});
when(() => result.stdout).thenReturn(
test(
'throws MissingGradleWrapperException '
'when gradlew does not exist',
() async {
final tempDir = setUpAppTempDir();
await expectLater(
runWithOverrides(() => gradlew.productFlavors(tempDir.path)),
throwsA(isA<MissingGradleWrapperException>()),
);
verifyNever(
() => process.run(
p.join(tempDir.path, 'android', 'gradlew'),
['app:tasks', '--all', '--console=auto'],
runInShell: true,
workingDirectory: p.join(tempDir.path, 'android'),
environment: {'JAVA_HOME': javaHome},
),
);
},
testOn: 'linux',
);
test(
'uses existing JAVA_HOME when set',
() async {
final tempDir = setUpAppTempDir();
File(
p.join('test', 'fixtures', 'gradle', 'gradle_app_tasks.txt'),
).readAsStringSync(),
);
await expectLater(
runWithOverrides(() => gradlew.productFlavors(tempDir.path)),
completion(
equals({
'development',
'developmentInternal',
'staging',
'stagingInternal',
'production',
'productionInternal',
}),
),
);
verify(
() => process.run(
p.join(tempDir.path, 'android', 'gradlew'),
['app:tasks', '--all', '--console=auto'],
runInShell: true,
workingDirectory: p.join(tempDir.path, 'android'),
environment: {'JAVA_HOME': javaHome},
),
).called(1);
});
).createSync(recursive: true);
await expectLater(
runWithOverrides(() => gradlew.productFlavors(tempDir.path)),
completes,
);
verify(
() => process.run(
p.join(tempDir.path, 'android', 'gradlew'),
['app:tasks', '--all', '--console=auto'],
runInShell: true,
workingDirectory: p.join(tempDir.path, 'android'),
environment: {'JAVA_HOME': javaHome},
),
).called(1);
},
testOn: 'linux',
);
group('when flavors are all upper case', () {
test('extracts flavors', () async {
when(() => platform.isLinux).thenReturn(true);
when(() => platform.isMacOS).thenReturn(false);
when(() => platform.isWindows).thenReturn(false);
test(
'throws Exception '
'when process exits with non-zero code',
() async {
final tempDir = setUpAppTempDir();
File(
p.join(tempDir.path, 'android', 'gradlew'),
).createSync(recursive: true);
when(() => result.exitCode).thenReturn(1);
when(() => result.stderr).thenReturn('test error');
await expectLater(
runWithOverrides(() => gradlew.productFlavors(tempDir.path)),
throwsA(
isA<Exception>().having(
(e) => '$e',
'message',
contains('test error'),
),
),
);
verify(
() => process.run(
p.join(tempDir.path, 'android', 'gradlew'),
['app:tasks', '--all', '--console=auto'],
runInShell: true,
workingDirectory: p.join(tempDir.path, 'android'),
environment: {'JAVA_HOME': javaHome},
),
).called(1);
},
testOn: 'linux',
);
test(
'extracts flavors',
() async {
final tempDir = setUpAppTempDir();
File(
p.join(tempDir.path, 'android', 'gradlew'),
).createSync(recursive: true);
const javaHome = 'test_java_home';
when(() => platform.environment).thenReturn({'JAVA_HOME': javaHome});
when(() => result.stdout).thenReturn(
File(
p.join(
'test',
'fixtures',
'gradle',
'gradle_app_tasks_upper_case_flavors.txt',
),
p.join('test', 'fixtures', 'gradle', 'gradle_app_tasks.txt'),
).readAsStringSync(),
);
await expectLater(
runWithOverrides(() => gradlew.productFlavors(tempDir.path)),
completion(
equals({
'SP',
'RJ',
'development',
'developmentInternal',
'staging',
'stagingInternal',
'production',
'productionInternal',
}),
),
);
});
});
verify(
() => process.run(
p.join(tempDir.path, 'android', 'gradlew'),
['app:tasks', '--all', '--console=auto'],
runInShell: true,
workingDirectory: p.join(tempDir.path, 'android'),
environment: {'JAVA_HOME': javaHome},
),
).called(1);
},
testOn: 'linux',
);
group(
'when flavors are mixed, starting with upper case, finishin with camel',
() {
test('extracts flavors', () async {
when(() => platform.isLinux).thenReturn(true);
when(() => platform.isMacOS).thenReturn(false);
when(() => platform.isWindows).thenReturn(false);
group('when flavors are all upper case', () {
test(
'extracts flavors',
() async {
final tempDir = setUpAppTempDir();
File(
p.join(tempDir.path, 'android', 'gradlew'),
).createSync(recursive: true);
const javaHome = 'test_java_home';
when(() => platform.environment)
.thenReturn({'JAVA_HOME': javaHome});
when(() => result.stdout).thenReturn(
File(
p.join(
'test',
'fixtures',
'gradle',
'gradle_app_tasks_mixed_case_flavors.txt',
'gradle_app_tasks_upper_case_flavors.txt',
),
).readAsStringSync(),
);
@@ -283,51 +240,192 @@ Make sure you have run "flutter build apk" at least once.''',
runWithOverrides(() => gradlew.productFlavors(tempDir.path)),
completion(
equals({
'SPaulo',
'RJaneiro',
'SP',
'RJ',
}),
),
);
});
},
testOn: 'linux',
);
});
group(
'when flavors are mixed, starting with upper case, finishin with camel',
() {
test(
'extracts flavors',
() async {
final tempDir = setUpAppTempDir();
File(
p.join(tempDir.path, 'android', 'gradlew'),
).createSync(recursive: true);
when(() => result.stdout).thenReturn(
File(
p.join(
'test',
'fixtures',
'gradle',
'gradle_app_tasks_mixed_case_flavors.txt',
),
).readAsStringSync(),
);
await expectLater(
runWithOverrides(() => gradlew.productFlavors(tempDir.path)),
completion(
equals({
'SPaulo',
'RJaneiro',
}),
),
);
},
testOn: 'linux',
);
},
);
group(
'when flavors starts with upper case, finishing with numbers',
() {
test('extracts flavors', () async {
when(() => platform.isLinux).thenReturn(true);
when(() => platform.isMacOS).thenReturn(false);
when(() => platform.isWindows).thenReturn(false);
final tempDir = setUpAppTempDir();
File(
p.join(tempDir.path, 'android', 'gradlew'),
).createSync(recursive: true);
const javaHome = 'test_java_home';
when(() => platform.environment)
.thenReturn({'JAVA_HOME': javaHome});
when(() => result.stdout).thenReturn(
test(
'extracts flavors',
() async {
final tempDir = setUpAppTempDir();
File(
p.join(
'test',
'fixtures',
'gradle',
'gradle_app_tasks_numbers_upper_case_flavors.txt',
p.join(tempDir.path, 'android', 'gradlew'),
).createSync(recursive: true);
when(() => result.stdout).thenReturn(
File(
p.join(
'test',
'fixtures',
'gradle',
'gradle_app_tasks_numbers_upper_case_flavors.txt',
),
).readAsStringSync(),
);
await expectLater(
runWithOverrides(() => gradlew.productFlavors(tempDir.path)),
completion(
equals({
'CB500',
'NX700',
}),
),
).readAsStringSync(),
);
await expectLater(
runWithOverrides(() => gradlew.productFlavors(tempDir.path)),
completion(
equals({
'CB500',
'NX700',
}),
),
);
});
);
},
testOn: 'linux',
);
},
);
});
group('exists', () {
late Directory tempDir;
setUp(() {
tempDir = setUpAppTempDir();
});
group('when gradlew does not exist', () {
test('returns false', () {
expect(gradlew.exists(tempDir.path), isFalse);
});
});
group('when gradlew exists', () {
group(
'when on unix based OSs',
() {
setUp(() {
File(
p.join(tempDir.path, 'android', 'gradlew'),
).createSync(recursive: true);
});
test(
'returns true',
() {
expect(gradlew.exists(tempDir.path), isTrue);
},
testOn: 'linux || mac-os',
);
},
);
group(
'when on windows',
() {
setUp(() {
File(
p.join(tempDir.path, 'android', 'gradlew.bat'),
).createSync(recursive: true);
});
test(
'returns true',
() {
expect(gradlew.exists(tempDir.path), isTrue);
},
testOn: 'windows',
);
},
);
});
});
group('version', () {
late Directory tempDir;
setUp(() {
tempDir = setUpAppTempDir();
File(
p.join(tempDir.path, 'android', 'gradlew'),
).createSync(recursive: true);
when(() => result.stdout).thenReturn('''
------------------------------------------------------------
Gradle 7.6.3
------------------------------------------------------------
Build time: 2023-10-04 15:59:47 UTC
Revision: 1694251d59e0d4752d547e1fd5b5020b798a7e71
Kotlin: 1.7.10
Groovy: 3.0.13
Ant: Apache Ant(TM) version 1.10.11 compiled on July 10 2021
JVM: 11.0.23 (Azul Systems, Inc. 11.0.23+9-LTS)
OS: Mac OS X 14.4.1 aarch64
''');
});
test(
'returns the correct version',
() async {
final version = await runWithOverrides(
() => gradlew.version(tempDir.path),
);
expect(version, '7.6.3');
},
testOn: 'linux || mac-os',
);
group('when the output cannot be parsed', () {
setUp(() {
when(() => result.stdout).thenReturn('not a real version');
});
test(
'returns unknown',
() async {
final version = await runWithOverrides(
() => gradlew.version(tempDir.path),
);
expect(version, 'unknown');
},
testOn: 'linux || mac-os',
);
});
});
});
}