Unverified Commit 594ff98a authored by Camille Simon's avatar Camille Simon Committed by GitHub

[Android] Add Java/AGP/Gradle incompatibility warning to `flutter create` (#131444)

Adds warning to `flutter create` command that checks if detected Java version is compatible with the template AGP and template Gradle versions. If a developer is building for Android and their Java version is incompatible with either the AGP or Gradle versions that Flutter currently supports by default for new Flutter projects, then

- a warning will show noting the incompatibility and
- steps will be shown to fix the issue, the recommended option being to configure a new compatible Java version given that Flutter knows we can support the template Gradle/AGP versions and updating them manually may be risky (feedback on this approach would be greatly appreciated!)

Given that the template AGP and Gradle versions are compatible, this PR assumes that the detected Java version may only conflict with one of the template AGP or Gradle versions because:
 - the minimum Java version for a given AGP version is less than the maximum Java version compatible for the minimum Gradle version required for that AGP version (too low a Java version will fail AGP compatibility test, but not Gradle compatibility).
- the maximum Java version compatible with minimum Gradle version for a given AGP version is higher than minimum Java version required for that AGP version (too high a Java version will fail Gradle compatibility test, but not AGP compatibility test).

Fixes https://github.com/flutter/flutter/issues/130515 in the sense that `flutter create foo`; `cd foo`; `flutter run` should always be successful.
parent 3a78e5c5
# Flutter Tools for Android
This section of the Flutter repository contains the command line developer tools
for building Flutter applications on Android. What follows are some notes about
updating this part of the tool.
## Updating Android dependencies
The Android dependencies that Flutter uses to run on Android
include the Android NDK and SDK versions, Gradle, the Kotlin Gradle Plugin,
and the Android Gradle Plugin (AGP). The template versions of these
dependencies can be found in [gradle_utils.dart](gradle_utils.dart).
Follow the guides below when*...
### Updating the template version of...
#### The Android SDK & NDK
All of the Android SDK/NDK versions noted in `gradle_utils.dart`
(`compileSdkVersion`, `minSdkVersion`, `targetSdkVersion`, `ndkVersion`)
versions should match the values in Flutter Gradle Plugin (`FlutterExtension`),
so updating any of these versions also requires an update in
[flutter.groovy](../../../gradle/src/main/groovy/flutter.groovy).
When updating the Android `compileSdkVersion`, `minSdkVersion`, or
`targetSdkVersion`, make sure that:
- Framework integration & benchmark tests are running with at least that SDK
version.
- Flutter tools tests that perform String checks with the current template
SDK verisons are updated (you should see these fail if you do not fix them
preemptively).
#### Gradle
When updating the Gradle version used in project templates
(`templateDefaultGradleVersion`), make sure that:
- Framework integration & benchmark tests are running with at least this Gradle
version.
- Flutter tools tests that perform String checks with the current template
Gradle version are updated (you should see these fail if you do not fix them
preemptively).
#### The Kotlin Gradle Plugin
When updating the Kotlin Gradle Plugin (KGP) version used in project templates
(`templateKotlinGradlePluginVersion`), make sure that the framework integration
& benchmark tests are running with at least this KGP version.
For information aboout the latest version, check https://kotlinlang.org/docs/releases.html#release-details.
#### The Android Gradle Plugin (AGP)
When updating the Android Gradle Plugin (AGP) versions used in project templates
(`templateAndroidGradlePluginVersion`, `templateAndroidGradlePluginVersionForModule`),
make sure that:
- Framework integration & benchmark tests are running with at least this AGP
version.
- Flutter tools tests that perform String checks with the current template
AGP verisons are updated (you should see these fail if you do not fix them
preemptively).
### A new version becomes available for...
#### Gradle
When new versions of Gradle become available, make sure to:
- Check if the maximum version of Gradle that we support
(`maxKnownAndSupportedGradleVersion`) can be updated, and if so, take the
necessary steps to ensure we are testing this version in CI.
- Check that the Java version that is one higher than we currently support
(`oneMajorVersionHigherJavaVersion`) based on current maximum supported
Gradle version is up-to-date.
- Update the `_javaGradleCompatList` that contains the Java/Gradle
compatibility information known to the tool.
- Update the test cases in [gradle_utils_test.dart](../../..test/general.shard/android/gradle_utils_test.dart) that test compatibility between Java and Gradle versions
(relevant tests should fail if you do not fix them preemptively, but should also
be marked inline).
- Update the test cases in [create_test.dart](../../../test/commands.shard/permeable/create_test.dart) that test for a warning for Java/Gradle incompatibilities as needed
(relevant tests should fail if you do not fix them preemptively).
For more information about the latest version, check https://gradle.org/releases/.
#### The Android Gradle Plugin (AGP)
When new versions of the Android Gradle Plugin become available, make sure to:
- Update the maximum version of AGP that we know of (`maxKnownAgpVersion`).
- Check if the maximum version of AGP that we support
(`maxKnownAndSupportedAgpVersion`) can be updated, and if so, take the necessary
steps to ensure that we are testing this version in CI.
- Update the `_javaAgpCompatList` that contains the Java/AGP compatibility
information known to the tool.
- Update the test cases in [gradle_utils_test.dart](../../..test/general.shard/android/gradle_utils_test.dart) that test compatibility between Java and AGP versions
(relevant tests should fail if you do not fix them preemptively, but should also
be marked inline).
- Update the test cases in [create_test.dart](../../../test/commands.shard/permeable/create_test.dart) that test for a warning for Java/AGP incompatibilities as needed
(relevant tests should fail if you do not fix them preemptively).
For information about the latest version, check https://developer.android.com/studio/releases/gradle-plugin#updating-gradle.
\* There is an ongoing effort to reduce these steps; see https://github.com/flutter/flutter/issues/134780.
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:meta/meta.dart' show immutable;
/// Data class that represents a range of versions in their String
/// representation.
///
/// Both the [versionMin] and [versionMax] are inclusive versions, and undefined
/// values represent an unknown minimum/maximum version.
@immutable
class VersionRange{
const VersionRange(
this.versionMin,
this.versionMax,
);
final String? versionMin;
final String? versionMax;
@override
bool operator ==(Object other) =>
other is VersionRange &&
other.versionMin == versionMin &&
other.versionMax == versionMax;
@override
int get hashCode => Object.hash(versionMin, versionMax);
}
......@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:meta/meta.dart';
import '../android/gradle_utils.dart' as gradle;
import '../base/common.dart';
import '../base/context.dart';
......@@ -9,6 +11,8 @@ import '../base/file_system.dart';
import '../base/net.dart';
import '../base/terminal.dart';
import '../base/utils.dart';
import '../base/version.dart';
import '../base/version_range.dart';
import '../convert.dart';
import '../dart/pub.dart';
import '../features.dart';
......@@ -501,6 +505,19 @@ Your $application code is in $relativeAppMain.
}
}
// Show warning for Java/AGP or Java/Gradle incompatibility if building for
// Android and Java version has been detected.
if (includeAndroid && globals.java?.version != null) {
_printIncompatibleJavaAgpGradleVersionsWarning(
javaVersion: versionToParsableString(globals.java?.version)!,
templateGradleVersion: templateContext['gradleVersion']! as String,
templateAgpVersion: templateContext['agpVersion']! as String,
templateAgpVersionForModule: templateContext['agpVersionForModule']! as String,
projectType: template,
projectDirPath: projectDirPath,
);
}
return FlutterCommandResult.success();
}
......@@ -853,3 +870,165 @@ For more details, see: https://flutter.dev/docs/get-started/web
''');
}
}
// Prints a warning if the specified Java version conflicts with either the
// template Gradle or AGP version.
//
// Assumes the specified templateGradleVersion and templateAgpVersion are
// compatible, meaning that the Java version may only conflict with one of the
// template Gradle or AGP versions.
void _printIncompatibleJavaAgpGradleVersionsWarning({
required String javaVersion,
required String templateGradleVersion,
required String templateAgpVersion,
required String templateAgpVersionForModule,
required FlutterProjectType projectType,
required String projectDirPath}) {
// Determine if the Java version specified conflicts with the template Gradle or AGP version.
final bool javaGradleVersionsCompatible = gradle.validateJavaAndGradle(globals.logger, javaV: javaVersion, gradleV: templateGradleVersion);
bool javaAgpVersionsCompatible = gradle.validateJavaAndAgp(globals.logger, javaV: javaVersion, agpV: templateAgpVersion);
String relevantTemplateAgpVersion = templateAgpVersion;
if (projectType == FlutterProjectType.module && Version.parse(templateAgpVersion)! < Version.parse(templateAgpVersionForModule)!) {
// If a module is being created, make sure to check for Java/AGP compatibility between the highest used version of AGP in the module template.
javaAgpVersionsCompatible = gradle.validateJavaAndAgp(globals.logger, javaV: javaVersion, agpV: templateAgpVersionForModule);
relevantTemplateAgpVersion = templateAgpVersionForModule;
}
if (javaGradleVersionsCompatible && javaAgpVersionsCompatible) {
return;
}
// Determine header of warning with recommended fix of re-configuring Java version.
final String incompatibleVersionsAndRecommendedOptionMessage = getIncompatibleJavaGradleAgpMessageHeader(javaGradleVersionsCompatible, templateGradleVersion, relevantTemplateAgpVersion, projectType.cliName);
if (!javaGradleVersionsCompatible) {
if (projectType == FlutterProjectType.plugin || projectType == FlutterProjectType.pluginFfi) {
// Only impacted files could be in sample code.
return;
}
// Gradle template version incompatible with Java version.
final gradle.JavaGradleCompat? validCompatibleGradleVersionRange = gradle.getValidGradleVersionRangeForJavaVersion(globals.logger, javaV: javaVersion);
final String compatibleGradleVersionMessage = validCompatibleGradleVersionRange == null ? '' : ' (compatible Gradle version range: ${validCompatibleGradleVersionRange.gradleMin} - ${validCompatibleGradleVersionRange.gradleMax})';
globals.printWarning('''
$incompatibleVersionsAndRecommendedOptionMessage
Alternatively, to continue using your configured Java version, update the Gradle
version specified in the following file to a compatible Gradle version$compatibleGradleVersionMessage:
${_getGradleWrapperPropertiesFilePath(projectType, projectDirPath)}
You may also update the Gradle version used by running
`./gradlew wrapper --gradle-version=<COMPATIBLE_GRADLE_VERSION>`.
See
https://docs.gradle.org/current/userguide/compatibility.html#java for details
on compatible Java/Gradle versions, and see
https://docs.gradle.org/current/userguide/gradle_wrapper.html#sec:upgrading_wrapper
for more details on using the Gradle Wrapper command to update the Gradle version
used.
''',
emphasis: true
);
return;
}
// AGP template version incompatible with Java version.
final gradle.JavaAgpCompat? minimumCompatibleAgpVersion = gradle.getMinimumAgpVersionForJavaVersion(globals.logger, javaV: javaVersion);
final String compatibleAgpVersionMessage = minimumCompatibleAgpVersion == null ? '' : ' (minimum compatible AGP version: ${minimumCompatibleAgpVersion.agpMin})';
final String gradleBuildFilePaths = ' - ${_getBuildGradleConfigurationFilePaths(projectType, projectDirPath)!.join('\n - ')}';
globals.printWarning('''
$incompatibleVersionsAndRecommendedOptionMessage
Alternatively, to continue using your configured Java version, update the AGP
version specified in the following files to a compatible AGP
version$compatibleAgpVersionMessage as necessary:
$gradleBuildFilePaths
See
https://developer.android.com/build/releases/gradle-plugin for details on
compatible Java/AGP versions.
''',
emphasis: true
);
}
// Returns incompatible Java/template Gradle/template AGP message header based
// on incompatibility and project type.
@visibleForTesting
String getIncompatibleJavaGradleAgpMessageHeader(
bool javaGradleVersionsCompatible,
String templateGradleVersion,
String templateAgpVersion,
String projectType) {
final String incompatibleDependency = javaGradleVersionsCompatible ? 'Android Gradle Plugin (AGP)' :'Gradle' ;
final String incompatibleDependencyVersion = javaGradleVersionsCompatible ? 'AGP version $templateAgpVersion' : 'Gradle version $templateGradleVersion';
final VersionRange validJavaRange = gradle.getJavaVersionFor(gradleV: templateGradleVersion, agpV: templateAgpVersion);
// validJavaRange should have non-null verisonMin and versionMax since it based on our template AGP and Gradle versions.
final String validJavaRangeMessage = '(minimum compatible version: ${validJavaRange.versionMin!}, maximum compatible version: ${validJavaRange.versionMax!})';
return '''
The configured version of Java detected may conflict with the $incompatibleDependency version in your new Flutter $projectType.
[RECOMMENDED] If so, to keep the default $incompatibleDependencyVersion, make
sure to download a compatible Java version
$validJavaRangeMessage.
You may configure this compatible Java version by running:
`flutter config --jdk-dir=<JDK_DIRECTORY>`
Note that this is a global configuration for Flutter.
''';
}
// Returns path of the gradle-wrapper.properties file for the specified
// generated project type.
String? _getGradleWrapperPropertiesFilePath(FlutterProjectType projectType, String projectDirPath) {
String gradleWrapperPropertiesFilePath = '';
switch (projectType) {
case FlutterProjectType.app:
case FlutterProjectType.skeleton:
gradleWrapperPropertiesFilePath = globals.fs.path.join(projectDirPath, 'android/gradle/wrapper/gradle-wrapper.properties');
case FlutterProjectType.module:
gradleWrapperPropertiesFilePath = globals.fs.path.join(projectDirPath, '.android/gradle/wrapper/gradle-wrapper.properties');
case FlutterProjectType.plugin:
case FlutterProjectType.pluginFfi:
case FlutterProjectType.package:
case FlutterProjectType.packageFfi:
// TODO(camsim99): Add relevant file path for packageFfi when Android is supported.
// No gradle-wrapper.properties files not part of sample code that
// can be determined.
return null;
}
return gradleWrapperPropertiesFilePath;
}
// Returns the path(s) of the build.gradle file(s) for the specified generated
// project type.
List<String>? _getBuildGradleConfigurationFilePaths(FlutterProjectType projectType, String projectDirPath) {
final List<String> buildGradleConfigurationFilePaths = <String>[];
switch (projectType) {
case FlutterProjectType.app:
case FlutterProjectType.skeleton:
case FlutterProjectType.pluginFfi:
buildGradleConfigurationFilePaths.add(globals.fs.path.join(projectDirPath, 'android/build.gradle'));
case FlutterProjectType.module:
const String moduleBuildGradleFilePath = '.android/build.gradle';
const String moduleAppBuildGradleFlePath = '.android/app/build.gradle';
const String moduleFlutterBuildGradleFilePath = '.android/Flutter/build.gradle';
buildGradleConfigurationFilePaths.addAll(<String>[
globals.fs.path.join(projectDirPath, moduleBuildGradleFilePath),
globals.fs.path.join(projectDirPath, moduleAppBuildGradleFlePath),
globals.fs.path.join(projectDirPath, moduleFlutterBuildGradleFilePath),
]);
case FlutterProjectType.plugin:
buildGradleConfigurationFilePaths.add(globals.fs.path.join(projectDirPath, 'android/app/build.gradle'));
case FlutterProjectType.package:
case FlutterProjectType.packageFfi:
// TODO(camsim99): Add any relevant file paths for packageFfi when Android is supported.
// No build.gradle file because there is no platform-specific implementation.
return null;
}
return buildGradleConfigurationFilePaths;
}
......@@ -422,9 +422,9 @@ abstract class CreateBase extends FlutterCommand {
'dartSdkVersionBounds': dartSdkVersionBounds,
'implementationTests': implementationTests,
'agpVersion': agpVersion,
'agpVersionForModule': gradle.templateAndroidGradlePluginVersionForModule,
'kotlinVersion': kotlinVersion,
'gradleVersion': gradleVersion,
'gradleVersionForModule': gradle.templateDefaultGradleVersionForModule,
'compileSdkVersion': gradle.compileSdkVersion,
'minSdkVersion': gradle.minSdkVersion,
'ndkVersion': gradle.ndkVersion,
......
......@@ -573,7 +573,7 @@ class AndroidProject extends FlutterProjectPlatform {
///
/// This is expected to be called from
/// flutter_tools/lib/src/project_validator.dart.
Future<ProjectValidatorResult> validateJavaGradleAgpVersions() async {
Future<ProjectValidatorResult> validateJavaAndGradleAgpVersions() async {
// Constructing ProjectValidatorResult happens here and not in
// flutter_tools/lib/src/project_validator.dart because of the additional
// Complexity of variable status values and error string formatting.
......@@ -599,7 +599,7 @@ class AndroidProject extends FlutterProjectPlatform {
hostAppGradleRoot, globals.logger, globals.processManager);
final String? agpVersion =
gradle.getAgpVersion(hostAppGradleRoot, globals.logger);
final String? javaVersion = _versionToParsableString(globals.java?.version);
final String? javaVersion = versionToParsableString(globals.java?.version);
// Assume valid configuration.
String description = validJavaGradleAgpString;
......@@ -607,7 +607,7 @@ class AndroidProject extends FlutterProjectPlatform {
final bool compatibleGradleAgp = gradle.validateGradleAndAgp(globals.logger,
gradleV: gradleVersion, agpV: agpVersion);
final bool compatibleJavaGradle = gradle.validateJavaGradle(globals.logger,
final bool compatibleJavaGradle = gradle.validateJavaAndGradle(globals.logger,
javaV: javaVersion, gradleV: gradleVersion);
// Begin description formatting.
......@@ -722,9 +722,9 @@ $javaGradleCompatUrl
'androidIdentifier': androidIdentifier,
'androidX': usesAndroidX,
'agpVersion': gradle.templateAndroidGradlePluginVersion,
'agpVersionForModule': gradle.templateAndroidGradlePluginVersionForModule,
'kotlinVersion': gradle.templateKotlinGradlePluginVersion,
'gradleVersion': gradle.templateDefaultGradleVersion,
'gradleVersionForModule': gradle.templateDefaultGradleVersionForModule,
'compileSdkVersion': gradle.compileSdkVersion,
'minSdkVersion': gradle.minSdkVersion,
'ndkVersion': gradle.ndkVersion,
......@@ -923,7 +923,7 @@ class CompatibilityResult {
}
/// Converts a [Version] to a string that can be parsed by [Version.parse].
String? _versionToParsableString(Version? version) {
String? versionToParsableString(Version? version) {
if (version == null) {
return null;
}
......
......@@ -227,7 +227,7 @@ class GeneralInfoProjectValidator extends ProjectValidator{
result.add(_materialDesignResult(flutterManifest));
result.add(_pluginValidatorResult(flutterManifest));
}
result.add(await project.android.validateJavaGradleAgpVersions());
result.add(await project.android.validateJavaAndGradleAgpVersions());
return result;
}
......
......@@ -8,7 +8,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:{{gradleVersionForModule}}'
classpath 'com.android.tools.build:gradle:{{agpVersionForModule}}'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
......
......@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'package:args/command_runner.dart';
import 'package:flutter_tools/src/android/java.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/create.dart';
......@@ -141,7 +142,10 @@ void main() {
await runner.run(<String>['create', '--no-pub', '--template=package_ffi', 'testy6']);
expect((await command.usageValues).commandCreateProjectType, 'package_ffi');
}));
}),
overrides: <Type, Generator>{
Java: () => FakeJava(),
});
testUsingContext('set iOS host language type as usage value', () => testbed.run(() async {
final CreateCommand command = CreateCommand();
......@@ -160,8 +164,10 @@ void main() {
'testy',
]);
expect((await command.usageValues).commandCreateIosLanguage, 'objc');
}));
}),
overrides: <Type, Generator>{
Java: () => FakeJava(),
});
testUsingContext('set Android host language type as usage value', () => testbed.run(() async {
final CreateCommand command = CreateCommand();
......@@ -178,7 +184,9 @@ void main() {
'testy',
]);
expect((await command.usageValues).commandCreateAndroidLanguage, 'java');
}));
}), overrides: <Type, Generator>{
Java: () => FakeJava(),
});
testUsingContext('create --offline', () => testbed.run(() async {
final CreateCommand command = CreateCommand();
......@@ -189,6 +197,7 @@ void main() {
expect(command.argParser.options.containsKey('offline'), true);
expect(command.shouldUpdateCache, true);
}, overrides: <Type, Generator>{
Java: () => null,
Pub: () => fakePub,
}));
......
......@@ -6,6 +6,7 @@ import 'package:args/command_runner.dart';
import 'package:flutter_tools/src/android/android_builder.dart';
import 'package:flutter_tools/src/android/android_sdk.dart';
import 'package:flutter_tools/src/android/android_studio.dart';
import 'package:flutter_tools/src/android/java.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/build_info.dart';
......@@ -295,6 +296,7 @@ void main() {
},
overrides: <Type, Generator>{
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
Java: () => null,
ProcessManager: () => processManager,
FeatureFlags: () => TestFeatureFlags(isIOSEnabled: false),
AndroidStudio: () => FakeAndroidStudio(),
......
......@@ -6,6 +6,7 @@ import 'package:args/command_runner.dart';
import 'package:flutter_tools/src/android/android_builder.dart';
import 'package:flutter_tools/src/android/android_sdk.dart';
import 'package:flutter_tools/src/android/android_studio.dart';
import 'package:flutter_tools/src/android/java.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/version.dart';
......@@ -143,6 +144,7 @@ void main() {
},
overrides: <Type, Generator>{
AndroidSdk: () => null,
Java: () => null,
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
ProcessManager: () => processManager,
AndroidStudio: () => FakeAndroidStudio(),
......@@ -174,6 +176,7 @@ void main() {
},
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
Java: () => null,
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
ProcessManager: () => processManager,
AndroidStudio: () => FakeAndroidStudio(),
......@@ -205,6 +208,7 @@ void main() {
},
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
Java: () => null,
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
ProcessManager: () => processManager,
AndroidStudio: () => FakeAndroidStudio(),
......@@ -236,6 +240,7 @@ void main() {
},
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
Java: () => null,
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
ProcessManager: () => processManager,
AndroidStudio: () => FakeAndroidStudio(),
......@@ -269,6 +274,7 @@ void main() {
},
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
Java: () => null,
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
ProcessManager: () => processManager,
AndroidStudio: () => FakeAndroidStudio(),
......@@ -320,6 +326,7 @@ void main() {
},
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
Java: () => null,
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
ProcessManager: () => processManager,
Usage: () => testUsage,
......@@ -374,6 +381,7 @@ void main() {
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
Java: () => null,
ProcessManager: () => processManager,
Usage: () => testUsage,
AndroidStudio: () => FakeAndroidStudio(),
......@@ -420,6 +428,7 @@ void main() {
overrides: <Type, Generator>{
AndroidSdk: () => mockAndroidSdk,
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
Java: () => null,
ProcessManager: () => processManager,
Usage: () => testUsage,
AndroidStudio: () => FakeAndroidStudio(),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment