mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00

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.
468 lines
17 KiB
Dart
468 lines
17 KiB
Dart
// 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: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';
|
|
import 'package:flutter_tools/src/cache.dart';
|
|
import 'package:flutter_tools/src/commands/build_apk.dart';
|
|
import 'package:flutter_tools/src/globals.dart' as globals;
|
|
import 'package:flutter_tools/src/project.dart';
|
|
import 'package:flutter_tools/src/reporting/reporting.dart';
|
|
import 'package:test/fake.dart';
|
|
|
|
import '../../src/android_common.dart';
|
|
import '../../src/common.dart';
|
|
import '../../src/context.dart';
|
|
import '../../src/fake_process_manager.dart';
|
|
import '../../src/test_flutter_command_runner.dart';
|
|
|
|
void main() {
|
|
Cache.disableLocking();
|
|
|
|
group('Usage', () {
|
|
late Directory tempDir;
|
|
late TestUsage testUsage;
|
|
|
|
setUp(() {
|
|
testUsage = TestUsage();
|
|
tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_tools_packages_test.');
|
|
});
|
|
|
|
tearDown(() {
|
|
tryToDelete(tempDir);
|
|
});
|
|
|
|
testUsingContext('indicate the default target platforms', () async {
|
|
final String projectPath = await createProject(tempDir,
|
|
arguments: <String>['--no-pub', '--template=app']);
|
|
final BuildApkCommand command = await runBuildApkCommand(projectPath);
|
|
|
|
expect((await command.usageValues).commandBuildApkTargetPlatform, 'android-arm,android-arm64,android-x64');
|
|
|
|
}, overrides: <Type, Generator>{
|
|
AndroidBuilder: () => FakeAndroidBuilder(),
|
|
});
|
|
|
|
testUsingContext('split per abi', () async {
|
|
final String projectPath = await createProject(tempDir,
|
|
arguments: <String>['--no-pub', '--template=app']);
|
|
|
|
final BuildApkCommand commandWithFlag = await runBuildApkCommand(projectPath,
|
|
arguments: <String>['--split-per-abi']);
|
|
expect((await commandWithFlag.usageValues).commandBuildApkSplitPerAbi, true);
|
|
|
|
final BuildApkCommand commandWithoutFlag = await runBuildApkCommand(projectPath);
|
|
expect((await commandWithoutFlag.usageValues).commandBuildApkSplitPerAbi, false);
|
|
|
|
}, overrides: <Type, Generator>{
|
|
AndroidBuilder: () => FakeAndroidBuilder(),
|
|
});
|
|
|
|
testUsingContext('build type', () async {
|
|
final String projectPath = await createProject(tempDir,
|
|
arguments: <String>['--no-pub', '--template=app']);
|
|
|
|
final BuildApkCommand commandDefault = await runBuildApkCommand(projectPath);
|
|
expect((await commandDefault.usageValues).commandBuildApkBuildMode, 'release');
|
|
|
|
final BuildApkCommand commandInRelease = await runBuildApkCommand(projectPath,
|
|
arguments: <String>['--release']);
|
|
expect((await commandInRelease.usageValues).commandBuildApkBuildMode, 'release');
|
|
|
|
final BuildApkCommand commandInDebug = await runBuildApkCommand(projectPath,
|
|
arguments: <String>['--debug']);
|
|
expect((await commandInDebug.usageValues).commandBuildApkBuildMode, 'debug');
|
|
|
|
final BuildApkCommand commandInProfile = await runBuildApkCommand(projectPath,
|
|
arguments: <String>['--profile']);
|
|
expect((await commandInProfile.usageValues).commandBuildApkBuildMode, 'profile');
|
|
|
|
}, overrides: <Type, Generator>{
|
|
AndroidBuilder: () => FakeAndroidBuilder(),
|
|
});
|
|
|
|
testUsingContext('logs success', () async {
|
|
final String projectPath = await createProject(tempDir,
|
|
arguments: <String>['--no-pub', '--template=app']);
|
|
|
|
await runBuildApkCommand(projectPath);
|
|
|
|
expect(testUsage.events, contains(
|
|
const TestUsageEvent(
|
|
'tool-command-result',
|
|
'apk',
|
|
label: 'success',
|
|
),
|
|
));
|
|
},
|
|
overrides: <Type, Generator>{
|
|
AndroidBuilder: () => FakeAndroidBuilder(),
|
|
Usage: () => testUsage,
|
|
});
|
|
});
|
|
|
|
group('Gradle', () {
|
|
late Directory tempDir;
|
|
late FakeProcessManager processManager;
|
|
late String gradlew;
|
|
late AndroidSdk mockAndroidSdk;
|
|
late TestUsage testUsage;
|
|
|
|
setUp(() {
|
|
testUsage = TestUsage();
|
|
tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_tools_packages_test.');
|
|
gradlew = globals.fs.path.join(tempDir.path, 'flutter_project', 'android',
|
|
globals.platform.isWindows ? 'gradlew.bat' : 'gradlew');
|
|
processManager = FakeProcessManager.empty();
|
|
mockAndroidSdk = FakeAndroidSdk(globals.fs.directory('irrelevant'));
|
|
});
|
|
|
|
tearDown(() {
|
|
tryToDelete(tempDir);
|
|
});
|
|
|
|
group('AndroidSdk', () {
|
|
testUsingContext('throws throwsToolExit if AndroidSdk is null', () async {
|
|
final String projectPath = await createProject(tempDir, arguments: <String>['--no-pub', '--template=app', '--platform=android']);
|
|
|
|
await expectLater(
|
|
() => runBuildApkCommand(
|
|
projectPath,
|
|
arguments: <String>['--no-pub'],
|
|
),
|
|
throwsToolExit(
|
|
message: 'No Android SDK found. Try setting the ANDROID_SDK_ROOT environment variable',
|
|
),
|
|
);
|
|
},
|
|
overrides: <Type, Generator>{
|
|
AndroidSdk: () => null,
|
|
Java: () => null,
|
|
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
|
|
ProcessManager: () => processManager,
|
|
AndroidStudio: () => FakeAndroidStudio(),
|
|
});
|
|
});
|
|
|
|
testUsingContext('shrinking is enabled by default on release mode', () async {
|
|
final String projectPath = await createProject(tempDir, arguments: <String>['--no-pub', '--template=app', '--platform=android']);
|
|
processManager.addCommand(FakeCommand(
|
|
command: <String>[
|
|
gradlew,
|
|
'-q',
|
|
'-Ptarget-platform=android-arm,android-arm64,android-x64',
|
|
'-Ptarget=${globals.fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}',
|
|
'-Pbase-application-name=android.app.Application',
|
|
'-Pdart-obfuscation=false',
|
|
'-Ptrack-widget-creation=true',
|
|
'-Ptree-shake-icons=true',
|
|
'assembleRelease',
|
|
],
|
|
exitCode: 1,
|
|
));
|
|
|
|
await expectLater(
|
|
() => runBuildApkCommand(projectPath),
|
|
throwsToolExit(message: 'Gradle task assembleRelease failed with exit code 1'),
|
|
);
|
|
expect(processManager, hasNoRemainingExpectations);
|
|
},
|
|
overrides: <Type, Generator>{
|
|
AndroidSdk: () => mockAndroidSdk,
|
|
Java: () => null,
|
|
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
|
|
ProcessManager: () => processManager,
|
|
AndroidStudio: () => FakeAndroidStudio(),
|
|
});
|
|
|
|
testUsingContext('--split-debug-info is enabled when an output directory is provided', () async {
|
|
final String projectPath = await createProject(tempDir, arguments: <String>['--no-pub', '--template=app', '--platform=android']);
|
|
processManager.addCommand(FakeCommand(
|
|
command: <String>[
|
|
gradlew,
|
|
'-q',
|
|
'-Ptarget-platform=android-arm,android-arm64,android-x64',
|
|
'-Ptarget=${globals.fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}',
|
|
'-Pbase-application-name=android.app.Application',
|
|
'-Pdart-obfuscation=false',
|
|
'-Psplit-debug-info=${tempDir.path}',
|
|
'-Ptrack-widget-creation=true',
|
|
'-Ptree-shake-icons=true',
|
|
'assembleRelease',
|
|
],
|
|
exitCode: 1,
|
|
));
|
|
|
|
await expectLater(
|
|
() => runBuildApkCommand(projectPath, arguments: <String>['--split-debug-info=${tempDir.path}']),
|
|
throwsToolExit(message: 'Gradle task assembleRelease failed with exit code 1'),
|
|
);
|
|
expect(processManager, hasNoRemainingExpectations);
|
|
},
|
|
overrides: <Type, Generator>{
|
|
AndroidSdk: () => mockAndroidSdk,
|
|
Java: () => null,
|
|
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
|
|
ProcessManager: () => processManager,
|
|
AndroidStudio: () => FakeAndroidStudio(),
|
|
});
|
|
|
|
testUsingContext('--extra-front-end-options are provided to gradle project', () async {
|
|
final String projectPath = await createProject(tempDir, arguments: <String>['--no-pub', '--template=app', '--platform=android']);
|
|
processManager.addCommand(FakeCommand(
|
|
command: <String>[
|
|
gradlew,
|
|
'-q',
|
|
'-Ptarget-platform=android-arm,android-arm64,android-x64',
|
|
'-Ptarget=${globals.fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}',
|
|
'-Pbase-application-name=android.app.Application',
|
|
'-Pdart-obfuscation=false',
|
|
'-Pextra-front-end-options=foo,bar',
|
|
'-Ptrack-widget-creation=true',
|
|
'-Ptree-shake-icons=true',
|
|
'assembleRelease',
|
|
],
|
|
exitCode: 1,
|
|
));
|
|
|
|
await expectLater(() => runBuildApkCommand(projectPath, arguments: <String>[
|
|
'--extra-front-end-options=foo',
|
|
'--extra-front-end-options=bar',
|
|
]), throwsToolExit(message: 'Gradle task assembleRelease failed with exit code 1'));
|
|
expect(processManager, hasNoRemainingExpectations);
|
|
},
|
|
overrides: <Type, Generator>{
|
|
AndroidSdk: () => mockAndroidSdk,
|
|
Java: () => null,
|
|
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
|
|
ProcessManager: () => processManager,
|
|
AndroidStudio: () => FakeAndroidStudio(),
|
|
});
|
|
|
|
testUsingContext('shrinking is disabled when --no-shrink is passed', () async {
|
|
final String projectPath = await createProject(tempDir, arguments: <String>['--no-pub', '--template=app', '--platform=android']);
|
|
processManager.addCommand(FakeCommand(
|
|
command: <String>[
|
|
gradlew,
|
|
'-q',
|
|
'-Ptarget-platform=android-arm,android-arm64,android-x64',
|
|
'-Ptarget=${globals.fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}',
|
|
'-Pbase-application-name=android.app.Application',
|
|
'-Pdart-obfuscation=false',
|
|
'-Ptrack-widget-creation=true',
|
|
'-Ptree-shake-icons=true',
|
|
'assembleRelease',
|
|
],
|
|
exitCode: 1,
|
|
));
|
|
|
|
await expectLater(
|
|
() => runBuildApkCommand(
|
|
projectPath,
|
|
arguments: <String>['--no-shrink'],
|
|
),
|
|
throwsToolExit(message: 'Gradle task assembleRelease failed with exit code 1'),
|
|
);
|
|
expect(processManager, hasNoRemainingExpectations);
|
|
},
|
|
overrides: <Type, Generator>{
|
|
AndroidSdk: () => mockAndroidSdk,
|
|
Java: () => null,
|
|
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
|
|
ProcessManager: () => processManager,
|
|
AndroidStudio: () => FakeAndroidStudio(),
|
|
});
|
|
|
|
testUsingContext('guides the user when the shrinker fails', () async {
|
|
final String projectPath = await createProject(tempDir, arguments: <String>['--no-pub', '--template=app', '--platform=android']);
|
|
const String r8StdoutWarning =
|
|
"Execution failed for task ':app:transformClassesAndResourcesWithR8ForStageInternal'.\n"
|
|
'> com.android.tools.r8.CompilationFailedException: Compilation failed to complete';
|
|
processManager.addCommand(FakeCommand(
|
|
command: <String>[
|
|
gradlew,
|
|
'-q',
|
|
'-Ptarget-platform=android-arm,android-arm64,android-x64',
|
|
'-Ptarget=${globals.fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}',
|
|
'-Pbase-application-name=android.app.Application',
|
|
'-Pdart-obfuscation=false',
|
|
'-Ptrack-widget-creation=true',
|
|
'-Ptree-shake-icons=true',
|
|
'assembleRelease',
|
|
],
|
|
exitCode: 1,
|
|
stdout: r8StdoutWarning,
|
|
));
|
|
|
|
await expectLater(
|
|
() => runBuildApkCommand(
|
|
projectPath,
|
|
),
|
|
throwsToolExit(message: 'Gradle task assembleRelease failed with exit code 1'),
|
|
);
|
|
expect(
|
|
testLogger.statusText, allOf(
|
|
containsIgnoringWhitespace('The shrinker may have failed to optimize the Java bytecode.'),
|
|
containsIgnoringWhitespace('To disable the shrinker, pass the `--no-shrink` flag to this command.'),
|
|
containsIgnoringWhitespace('To learn more, see: https://developer.android.com/studio/build/shrink-code'),
|
|
)
|
|
);
|
|
expect(testUsage.events, contains(
|
|
const TestUsageEvent(
|
|
'build',
|
|
'gradle',
|
|
label: 'gradle-r8-failure',
|
|
parameters: CustomDimensions(),
|
|
),
|
|
));
|
|
expect(processManager, hasNoRemainingExpectations);
|
|
},
|
|
overrides: <Type, Generator>{
|
|
AndroidSdk: () => mockAndroidSdk,
|
|
Java: () => null,
|
|
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
|
|
ProcessManager: () => processManager,
|
|
Usage: () => testUsage,
|
|
AndroidStudio: () => FakeAndroidStudio(),
|
|
});
|
|
|
|
testUsingContext("reports when the app isn't using AndroidX", () async {
|
|
final String projectPath = await createProject(tempDir, arguments: <String>['--no-pub', '--template=app', '--platform=android']);
|
|
// Simulate a non-androidx project.
|
|
tempDir
|
|
.childDirectory('flutter_project')
|
|
.childDirectory('android')
|
|
.childFile('gradle.properties')
|
|
.writeAsStringSync('android.useAndroidX=false');
|
|
processManager.addCommand(FakeCommand(
|
|
command: <String>[
|
|
gradlew,
|
|
'-q',
|
|
'-Ptarget-platform=android-arm,android-arm64,android-x64',
|
|
'-Ptarget=${globals.fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}',
|
|
'-Pbase-application-name=android.app.Application',
|
|
'-Pdart-obfuscation=false',
|
|
'-Ptrack-widget-creation=true',
|
|
'-Ptree-shake-icons=true',
|
|
'assembleRelease',
|
|
],
|
|
));
|
|
|
|
// The command throws a [ToolExit] because it expects an APK in the file system.
|
|
await expectLater(() => runBuildApkCommand(projectPath), throwsToolExit());
|
|
|
|
expect(
|
|
testLogger.statusText,
|
|
allOf(
|
|
containsIgnoringWhitespace("Your app isn't using AndroidX"),
|
|
containsIgnoringWhitespace(
|
|
'To avoid potential build failures, you can quickly migrate your app by '
|
|
'following the steps on https://goo.gl/CP92wY'
|
|
),
|
|
),
|
|
);
|
|
expect(testUsage.events, contains(
|
|
const TestUsageEvent(
|
|
'build',
|
|
'gradle',
|
|
label: 'app-not-using-android-x',
|
|
parameters: CustomDimensions(),
|
|
),
|
|
));
|
|
expect(processManager, hasNoRemainingExpectations);
|
|
},
|
|
overrides: <Type, Generator>{
|
|
AndroidSdk: () => mockAndroidSdk,
|
|
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
|
|
Java: () => null,
|
|
ProcessManager: () => processManager,
|
|
Usage: () => testUsage,
|
|
AndroidStudio: () => FakeAndroidStudio(),
|
|
});
|
|
|
|
testUsingContext('reports when the app is using AndroidX', () async {
|
|
final String projectPath = await createProject(tempDir, arguments: <String>['--no-pub', '--template=app', '--platform=android']);
|
|
processManager.addCommand(FakeCommand(
|
|
command: <String>[
|
|
gradlew,
|
|
'-q',
|
|
'-Ptarget-platform=android-arm,android-arm64,android-x64',
|
|
'-Ptarget=${globals.fs.path.join(tempDir.path, 'flutter_project', 'lib', 'main.dart')}',
|
|
'-Pbase-application-name=android.app.Application',
|
|
'-Pdart-obfuscation=false',
|
|
'-Ptrack-widget-creation=true',
|
|
'-Ptree-shake-icons=true',
|
|
'assembleRelease',
|
|
],
|
|
));
|
|
|
|
// The command throws a [ToolExit] because it expects an APK in the file system.
|
|
await expectLater(() => runBuildApkCommand(projectPath), throwsToolExit());
|
|
|
|
expect(
|
|
testLogger.statusText, allOf(
|
|
isNot(contains("[!] Your app isn't using AndroidX")),
|
|
isNot(contains(
|
|
'To avoid potential build failures, you can quickly migrate your app by '
|
|
'following the steps on https://goo.gl/CP92wY'
|
|
))
|
|
),
|
|
);
|
|
expect(testUsage.events, contains(
|
|
const TestUsageEvent(
|
|
'build',
|
|
'gradle',
|
|
label: 'app-using-android-x',
|
|
parameters: CustomDimensions(),
|
|
),
|
|
));
|
|
expect(processManager, hasNoRemainingExpectations);
|
|
},
|
|
overrides: <Type, Generator>{
|
|
AndroidSdk: () => mockAndroidSdk,
|
|
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
|
|
Java: () => null,
|
|
ProcessManager: () => processManager,
|
|
Usage: () => testUsage,
|
|
AndroidStudio: () => FakeAndroidStudio(),
|
|
});
|
|
});
|
|
}
|
|
|
|
Future<BuildApkCommand> runBuildApkCommand(
|
|
String target, {
|
|
List<String>? arguments,
|
|
}) async {
|
|
final BuildApkCommand command = BuildApkCommand(logger: BufferLogger.test());
|
|
final CommandRunner<void> runner = createTestCommandRunner(command);
|
|
await runner.run(<String>[
|
|
'apk',
|
|
...?arguments,
|
|
'--no-pub',
|
|
globals.fs.path.join(target, 'lib', 'main.dart'),
|
|
]);
|
|
return command;
|
|
}
|
|
|
|
class FakeAndroidSdk extends Fake implements AndroidSdk {
|
|
FakeAndroidSdk(this.directory);
|
|
|
|
@override
|
|
final Directory directory;
|
|
}
|
|
|
|
class FakeAndroidStudio extends Fake implements AndroidStudio {
|
|
@override
|
|
String get javaPath => 'java';
|
|
|
|
@override
|
|
Version get version => Version(2021, 3, 1);
|
|
}
|