Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Update tooling to enforce java 17 compile options and kotlinOptions
  • Loading branch information
reidbaker committed Oct 9, 2025
commit 071f1d5b3158f286bf85e0e652b248d1711113e4
48 changes: 40 additions & 8 deletions script/tool/lib/src/gradle_check_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 'dart:math' as math;

import 'package:collection/collection.dart';
import 'package:file/file.dart';
import 'package:meta/meta.dart';
Expand Down Expand Up @@ -354,33 +356,34 @@ build.gradle "namespace" must match the "package" attribute in AndroidManifest.x
/// than using whatever the client's local toolchaing defaults to (which can
/// lead to compile errors that show up for clients, but not in CI).
bool _validateCompatibilityVersions(List<String> gradleLines) {
const String requiredJavaVersion = '17';
final bool hasLanguageVersion = gradleLines.any((String line) =>
line.contains('languageVersion') && !_isCommented(line));
final bool hasCompabilityVersions = gradleLines.any((String line) =>
line.contains('sourceCompatibility') && !_isCommented(line)) &&
line.contains('sourceCompatibility = JavaVersion.VERSION_$requiredJavaVersion') && !_isCommented(line)) &&
// Newer toolchains default targetCompatibility to the same value as
// sourceCompatibility, but older toolchains require it to be set
// explicitly. The exact version cutoff (and of which piece of the
// toolchain; likely AGP) is unknown; for context see
// https://github.com/flutter/flutter/issues/125482
gradleLines.any((String line) =>
line.contains('targetCompatibility') && !_isCommented(line));
line.contains('targetCompatibility = JavaVersion.VERSION_$requiredJavaVersion') && !_isCommented(line));

This comment was marked as off-topic.

if (!hasLanguageVersion && !hasCompabilityVersions) {
const String errorMessage = '''
build.gradle must set an explicit Java compatibility version.
const String javaErrorMessage = '''
build.gradle(.kts) must set an explicit Java compatibility version.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should say "must set an explicit Java compatibility version of $requiredJavaVersion."

With the updated check, someone who adds an incorrect compatibility version will get an error message that just says that they need to set an explicit version, which they did, so it will be confusing.


This can be done either via "sourceCompatibility"/"targetCompatibility":
android {
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
sourceCompatibility = JavaVersion.VERSION_$requiredJavaVersion
targetCompatibility = JavaVersion.VERSION_$requiredJavaVersion
}
}

or "toolchain":
java {
toolchain {
languageVersion = JavaLanguageVersion.of(11)
languageVersion = JavaLanguageVersion.of($requiredJavaVersion)
}
}

Expand All @@ -389,9 +392,38 @@ https://docs.gradle.org/current/userguide/java_plugin.html#toolchain_and_compati
for more details.''';

printError(
'$indentation${errorMessage.split('\n').join('\n$indentation')}');
'$indentation${javaErrorMessage.split('\n').join('\n$indentation')}');
return false;
}
bool isKotlinOptions(String line) => line.contains('kotlinOptions') && !_isCommented(line);
final bool hasKotlinOptions = gradleLines.any(isKotlinOptions);
final bool kotlinOptionsUsesJavaVersion = gradleLines.any((String line) =>
line.contains('jvmTarget = JavaVersion.VERSION_$requiredJavaVersion') &&
!_isCommented(line));
Comment on lines +405 to +407

This comment was marked as off-topic.

// Either does not set kotlinOptions or does and uses non-string based syntax.

This comment was marked as off-topic.

if (hasKotlinOptions && !kotlinOptionsUsesJavaVersion) {
// Bad lines contains the first 4 lines including the kotlinOptions section.
String badLines = '';
final int startIndex = gradleLines.indexOf(gradleLines.firstWhere(isKotlinOptions));
for(int i = startIndex; i < math.min(startIndex + 4, gradleLines.length); i++) {
badLines += '${gradleLines[i]}\n';
}
final String kotlinErrorMessage = '''
If build.gradle(.kts) sets jvmTarget then it must use JavaVersion syntax.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, if I'm reading this right, setting, say jvmTarget = JavaVersion.VERSION_21.toString() would lead to an error message saying that they are using the wrong syntax, when the actual problem in that the version is wrong.

I think we should either detect those cases separately and have different messages, or just make the message more specific (e.g., "it must set version $requiredJavaVersion using JavaVersion syntax.")

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stuartmorgan-g and I talked offline and I will make this change in my next pr. Tooling is easier to land and less likely to have a conflict and this corner case is unlikely to hit anyone in the short term.

Good:
android {
kotlinOptions {
jvmTarget = JavaVersion.VERSION_$requiredJavaVersion.toString()
}
}
BAD:
$badLines
''';
printError(
'$indentation${kotlinErrorMessage.split('\n').join('\n$indentation')}');
return false;
}

return true;
}

Expand Down
120 changes: 110 additions & 10 deletions script/tool/test/gradle_check_command_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ const String _defaultFakeNamespace = 'dev.flutter.foo';
void main() {
late CommandRunner<void> runner;
late Directory packagesDir;
const String javaIncompatabilityIndicator =
'build.gradle(.kts) must set an explicit Java compatibility version.';

setUp(() {
final GitDir gitDir;
Expand Down Expand Up @@ -46,6 +48,9 @@ void main() {
bool useDeprecatedCompileSdkVersion = false,
bool usePropertyAssignment = true,
String compileSdk = '36',
bool includeKotlinOptions = true,
bool commentKotlinOptions = false,
bool useDeprecatedJvmTarget = false,
}) {
final File buildGradle = package
.platformDirectory(FlutterPlatform.android)
Expand All @@ -69,11 +74,17 @@ java {

''';
final String sourceCompat =
'${commentSourceLanguage ? '// ' : ''}sourceCompatibility = JavaVersion.VERSION_11';
'${commentSourceLanguage ? '// ' : ''}sourceCompatibility = JavaVersion.VERSION_17';
final String targetCompat =
'${commentSourceLanguage ? '// ' : ''}targetCompatibility = JavaVersion.VERSION_11';
'${commentSourceLanguage ? '// ' : ''}targetCompatibility = JavaVersion.VERSION_17';
final String namespace =
" ${commentNamespace ? '// ' : ''}namespace = '$_defaultFakeNamespace'";
final String jvmTarget =
useDeprecatedJvmTarget ? '17' : 'JavaVersion.VERSION_17.toString()';
final String kotlinConfig = '''
${commentKotlinOptions ? '//' : ''}kotlinOptions {
${commentKotlinOptions ? '//' : ''}jvmTarget = $jvmTarget
${commentKotlinOptions ? '//' : ''}}''';

buildGradle.writeAsStringSync('''
group 'dev.flutter.plugins.fake'
Expand Down Expand Up @@ -101,6 +112,7 @@ ${warningsConfigured ? warningConfig : ''}
${includeSourceCompat ? sourceCompat : ''}
${includeTargetCompat ? targetCompat : ''}
}
${includeKotlinOptions ? kotlinConfig : ''}
testOptions {
unitTests.includeAndroidResources = true
}
Expand Down Expand Up @@ -351,8 +363,7 @@ dependencies {
expect(
output,
containsAllInOrder(<Matcher>[
contains(
'build.gradle must set an explicit Java compatibility version.'),
contains(javaIncompatabilityIndicator),
]),
);
});
Expand All @@ -375,8 +386,7 @@ dependencies {
expect(
output,
containsAllInOrder(<Matcher>[
contains(
'build.gradle must set an explicit Java compatibility version.'),
contains(javaIncompatabilityIndicator),
]),
);
});
Expand Down Expand Up @@ -457,8 +467,7 @@ dependencies {
expect(
output,
containsAllInOrder(<Matcher>[
contains(
'build.gradle must set an explicit Java compatibility version.'),
contains(javaIncompatabilityIndicator),
]),
);
});
Expand All @@ -480,8 +489,7 @@ dependencies {
expect(
output,
containsAllInOrder(<Matcher>[
contains(
'build.gradle must set an explicit Java compatibility version.'),
contains(javaIncompatabilityIndicator),
]),
);
});
Expand Down Expand Up @@ -1162,4 +1170,96 @@ dependencies {
);
});
});

group('kotlinOptions check', () {
test('passes when kotlin options are specified', () async {
final RepositoryPackage package =
createFakePlugin('a_plugin', packagesDir, examples: <String>[]);
writeFakePluginBuildGradle(
package,
includeLanguageVersion: true,
// ignore: avoid_redundant_argument_values ensure codepath is tested if defaults change.
includeKotlinOptions: true,
);
writeFakeManifest(package);

final List<String> output =
await runCapturingPrint(runner, <String>['gradle-check']);

expect(
output,
containsAllInOrder(<Matcher>[
contains('Validating android/build.gradle'),
]),
);
});

test('passes when kotlin options are not specified', () async {
final RepositoryPackage package =
createFakePlugin('a_plugin', packagesDir, examples: <String>[]);
writeFakePluginBuildGradle(
package,
includeLanguageVersion: true,
includeKotlinOptions: false,
);
writeFakeManifest(package);

final List<String> output =
await runCapturingPrint(runner, <String>['gradle-check']);

expect(
output,
containsAllInOrder(<Matcher>[
contains('Validating android/build.gradle'),
]),
);
});

test('passes when kotlin options commented out', () async {
final RepositoryPackage package =
createFakePlugin('a_plugin', packagesDir, examples: <String>[]);
writeFakePluginBuildGradle(
package,
includeLanguageVersion: true,
commentKotlinOptions: true,
);
writeFakeManifest(package);

final List<String> output =
await runCapturingPrint(runner, <String>['gradle-check']);

expect(
output,
containsAllInOrder(<Matcher>[
contains('Validating android/build.gradle'),
]),
);
});

test('fails when kotlin options uses string jvm version', () async {
final RepositoryPackage package =
createFakePlugin('a_plugin', packagesDir, examples: <String>[]);
writeFakePluginBuildGradle(
package,
includeLanguageVersion: true,
useDeprecatedJvmTarget: true,
);
writeFakeManifest(package);

Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['gradle-check'], errorHandler: (Error e) {
commandError = e;
});

expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains(
'build.gradle(.kts) sets jvmTarget then it must use JavaVersion syntax'),
]),
);
});
});
}