flutter/packages/flutter_tools/test/commands.shard/permeable/build_aar_test.dart
Camille Simon 594ff98a65
[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.
2023-09-20 16:01:04 +00:00

357 lines
12 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/build_info.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/build_aar.dart';
import 'package:flutter_tools/src/features.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/fakes.dart' hide FakeFlutterProjectFactory;
import '../../src/test_flutter_command_runner.dart';
void main() {
Cache.disableLocking();
Future<BuildAarCommand> runCommandIn(String target, { List<String>? arguments }) async {
final BuildAarCommand command = BuildAarCommand(
androidSdk: FakeAndroidSdk(),
fileSystem: globals.fs,
logger: BufferLogger.test(),
verboseHelp: false,
);
final CommandRunner<void> runner = createTestCommandRunner(command);
await runner.run(<String>[
'aar',
'--no-pub',
...?arguments,
target,
]);
return command;
}
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 that project is a module', () async {
final String projectPath = await createProject(tempDir,
arguments: <String>['--no-pub', '--template=module']);
final BuildAarCommand command = await runCommandIn(projectPath);
expect((await command.usageValues).commandBuildAarProjectType, 'module');
}, overrides: <Type, Generator>{
AndroidBuilder: () => FakeAndroidBuilder(),
});
testUsingContext('indicate that project is a plugin', () async {
final String projectPath = await createProject(tempDir,
arguments: <String>['--no-pub', '--template=plugin', '--project-name=aar_test']);
final BuildAarCommand command = await runCommandIn(projectPath);
expect((await command.usageValues).commandBuildAarProjectType, 'plugin');
}, overrides: <Type, Generator>{
AndroidBuilder: () => FakeAndroidBuilder(),
});
testUsingContext('indicate the target platform', () async {
final String projectPath = await createProject(tempDir,
arguments: <String>['--no-pub', '--template=module']);
final BuildAarCommand command = await runCommandIn(projectPath,
arguments: <String>['--target-platform=android-arm']);
expect((await command.usageValues).commandBuildAarTargetPlatform, 'android-arm');
}, overrides: <Type, Generator>{
AndroidBuilder: () => FakeAndroidBuilder(),
});
testUsingContext('logs success', () async {
final String projectPath = await createProject(tempDir,
arguments: <String>['--no-pub', '--template=module']);
await runCommandIn(projectPath,
arguments: <String>['--target-platform=android-arm']);
expect(testUsage.events, contains(
const TestUsageEvent(
'tool-command-result',
'aar',
label: 'success',
),
));
},
overrides: <Type, Generator>{
AndroidBuilder: () => FakeAndroidBuilder(),
Usage: () => testUsage,
});
});
group('flag parsing', () {
late Directory tempDir;
late FakeAndroidBuilder fakeAndroidBuilder;
setUp(() {
fakeAndroidBuilder = FakeAndroidBuilder();
tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_tools_build_aar_test.');
});
tearDown(() {
tryToDelete(tempDir);
});
testUsingContext('defaults', () async {
final String projectPath = await createProject(tempDir,
arguments: <String>['--no-pub']);
await runCommandIn(projectPath);
expect(fakeAndroidBuilder.buildNumber, '1.0');
expect(fakeAndroidBuilder.androidBuildInfo.length, 3);
final List<BuildMode> buildModes = <BuildMode>[];
for (final AndroidBuildInfo androidBuildInfo in fakeAndroidBuilder.androidBuildInfo) {
final BuildInfo buildInfo = androidBuildInfo.buildInfo;
buildModes.add(buildInfo.mode);
if (buildInfo.mode.isPrecompiled) {
expect(buildInfo.treeShakeIcons, isTrue);
expect(buildInfo.trackWidgetCreation, isTrue);
} else {
expect(buildInfo.treeShakeIcons, isFalse);
expect(buildInfo.trackWidgetCreation, isTrue);
}
expect(buildInfo.flavor, isNull);
expect(buildInfo.splitDebugInfoPath, isNull);
expect(buildInfo.dartObfuscation, isFalse);
expect(androidBuildInfo.targetArchs, <AndroidArch>[AndroidArch.armeabi_v7a, AndroidArch.arm64_v8a, AndroidArch.x86_64]);
}
expect(buildModes.length, 3);
expect(buildModes, containsAll(<BuildMode>[BuildMode.debug, BuildMode.profile, BuildMode.release]));
}, overrides: <Type, Generator>{
AndroidBuilder: () => fakeAndroidBuilder,
});
testUsingContext('parses flags', () async {
final String projectPath = await createProject(tempDir,
arguments: <String>['--no-pub']);
await runCommandIn(
projectPath,
arguments: <String>[
'--no-debug',
'--no-profile',
'--target-platform',
'android-x86',
'--tree-shake-icons',
'--flavor',
'free',
'--build-number',
'200',
'--split-debug-info',
'/project-name/v1.2.3/',
'--obfuscate',
'--dart-define=foo=bar',
],
);
expect(fakeAndroidBuilder.buildNumber, '200');
final AndroidBuildInfo androidBuildInfo = fakeAndroidBuilder.androidBuildInfo.single;
expect(androidBuildInfo.targetArchs, <AndroidArch>[AndroidArch.x86]);
final BuildInfo buildInfo = androidBuildInfo.buildInfo;
expect(buildInfo.mode, BuildMode.release);
expect(buildInfo.treeShakeIcons, isTrue);
expect(buildInfo.flavor, 'free');
expect(buildInfo.splitDebugInfoPath, '/project-name/v1.2.3/');
expect(buildInfo.dartObfuscation, isTrue);
expect(buildInfo.dartDefines.contains('foo=bar'), isTrue);
expect(buildInfo.nullSafetyMode, NullSafetyMode.sound);
}, overrides: <Type, Generator>{
AndroidBuilder: () => fakeAndroidBuilder,
});
});
group('Gradle', () {
late Directory tempDir;
late AndroidSdk mockAndroidSdk;
late String gradlew;
late FakeProcessManager processManager;
late String flutterRoot;
setUp(() {
tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_tools_packages_test.');
mockAndroidSdk = FakeAndroidSdk();
gradlew = globals.fs.path.join(tempDir.path, 'flutter_project', '.android',
globals.platform.isWindows ? 'gradlew.bat' : 'gradlew');
processManager = FakeProcessManager.empty();
flutterRoot = getFlutterRoot();
});
tearDown(() {
tryToDelete(tempDir);
});
group('AndroidSdk', () {
testUsingContext('throws throwsToolExit if AndroidSdk is null', () async {
final String projectPath = await createProject(tempDir,
arguments: <String>['--no-pub', '--template=module']);
await expectLater(() async {
await runBuildAarCommand(
projectPath,
null,
arguments: <String>['--no-pub'],
);
}, throwsToolExit(
message: 'No Android SDK found. Try setting the ANDROID_SDK_ROOT environment variable',
));
},
overrides: <Type, Generator>{
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
ProcessManager: () => FakeProcessManager.any(),
});
});
group('throws ToolExit', () {
testUsingContext('main.dart not found', () async {
await expectLater(() async {
await runBuildAarCommand(
'missing_project',
mockAndroidSdk,
arguments: <String>['--no-pub'],
);
}, throwsToolExit(
message: 'main.dart does not exist',
));
});
testUsingContext('flutter project not valid', () async {
await expectLater(() async {
await runCommandIn(
tempDir.path,
arguments: <String>['--no-pub'],
);
}, throwsToolExit(
message: 'is not a valid flutter project',
));
});
});
testUsingContext('support ExtraDartFlagOptions', () async {
final String projectPath = await createProject(tempDir,
arguments: <String>['--no-pub', '--template=module']);
processManager.addCommand(FakeCommand(
command: <String>[
gradlew,
'-I=${globals.fs.path.join(flutterRoot, 'packages', 'flutter_tools', 'gradle','aar_init_script.gradle')}',
'-Pflutter-root=$flutterRoot',
'-Poutput-dir=${globals.fs.path.join(tempDir.path, 'flutter_project', 'build', 'host')}',
'-Pis-plugin=false',
'-PbuildNumber=1.0',
'-q',
'-Ptarget=${globals.fs.path.join('lib', 'main.dart')}',
'-Pdart-obfuscation=false',
'-Pextra-front-end-options=foo,bar',
'-Ptrack-widget-creation=true',
'-Ptree-shake-icons=true',
'-Ptarget-platform=android-arm,android-arm64,android-x64',
'assembleAarRelease',
],
exitCode: 1,
));
await expectLater(() => runBuildAarCommand(projectPath, mockAndroidSdk, arguments: <String>[
'--no-debug',
'--no-profile',
'--extra-front-end-options=foo',
'--extra-front-end-options=bar',
]), throwsToolExit(message: 'Gradle task assembleAarRelease failed with exit code 1'));
expect(processManager, hasNoRemainingExpectations);
},
overrides: <Type, Generator>{
FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
Java: () => null,
ProcessManager: () => processManager,
FeatureFlags: () => TestFeatureFlags(isIOSEnabled: false),
AndroidStudio: () => FakeAndroidStudio(),
});
});
}
Future<BuildAarCommand> runBuildAarCommand(
String target, AndroidSdk? androidSdk, {
List<String>? arguments,
}) async {
final BuildAarCommand command = BuildAarCommand(
androidSdk: androidSdk,
fileSystem: globals.fs,
logger: BufferLogger.test(),
verboseHelp: false,
);
final CommandRunner<void> runner = createTestCommandRunner(command);
await runner.run(<String>[
'aar',
'--no-pub',
...?arguments,
globals.fs.path.join(target, 'lib', 'main.dart'),
]);
return command;
}
class FakeAndroidBuilder extends Fake implements AndroidBuilder {
late FlutterProject project;
late Set<AndroidBuildInfo> androidBuildInfo;
late String target;
String? outputDirectoryPath;
late String buildNumber;
@override
Future<void> buildAar({
required FlutterProject project,
required Set<AndroidBuildInfo> androidBuildInfo,
required String target,
String? outputDirectoryPath,
required String buildNumber,
}) async {
this.project = project;
this.androidBuildInfo = androidBuildInfo;
this.target = target;
this.outputDirectoryPath = outputDirectoryPath;
this.buildNumber = buildNumber;
}
}
class FakeAndroidSdk extends Fake implements AndroidSdk {
}
class FakeAndroidStudio extends Fake implements AndroidStudio {
@override
String get javaPath => 'java';
}