Unverified Commit 0a1af8a1 authored by Bartek Pacia's avatar Bartek Pacia Committed by GitHub

Add support for Gradle Kotlin DSL (#140744)

This PR resolves #140548. It's based on my work in #118067.
parent 5887d6c7
// 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.
// This file is auto generated.
// To update all the settings.gradle files in the Flutter repo,
// See dev/tools/bin/generate_gradle_lockfiles.dart.
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}
settings.ext.flutterSdkPath = flutterSdkPath()
// Flutter Gradle Plugin ships together with the Flutter SDK
includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.4.2" apply false
}
include ':app'
// 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.
// Contents of this file automatically generated by dev/tools/bin/generate_gradle_lockfiles.dart.
// Do not merge changes to this file. See #140115.
pluginManagement {
val flutterSdkPath = run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "7.4.2" apply false
}
include(":app")
......@@ -29,7 +29,8 @@ import org.gradle.api.tasks.bundling.Jar
import org.gradle.internal.os.OperatingSystem
/**
* For apps only. Provides the flutter extension used in app/build.gradle.
* For apps only. Provides the flutter extension used in the app-level Gradle
* build file (app/build.gradle or app/build.gradle.kts).
*
* The versions specified here should match the values in
* packages/flutter_tools/lib/src/android/gradle_utils.dart, so when bumping,
......@@ -62,7 +63,7 @@ class FlutterExtension {
/**
* Specifies the relative directory to the Flutter project directory.
* In an app project, this is ../.. since the app's build.gradle is under android/app.
* In an app project, this is ../.. since the app's Gradle build file is under android/app.
*/
String source = "../.."
......@@ -90,7 +91,8 @@ buildscript {
/**
* Some apps don't set default compile options.
* Apps can change these values in android/app/build.gradle.
* Apps can change these values in the app-level Gradle build file
* (android/app/build.gradle or android/app/build.gradle.kts).
* This just ensures that default values are set.
*/
android {
......@@ -423,13 +425,12 @@ class FlutterPlugin implements Plugin<Project> {
* just using the `plugins.android` list.
*/
private configureLegacyPluginEachProjects(Project project) {
File settingsGradle = new File(project.projectDir.parentFile, "settings.gradle")
try {
if (!settingsGradle.text.contains("'.flutter-plugins'")) {
if (!settingsGradleFile(project).text.contains("'.flutter-plugins'")) {
return
}
} catch (FileNotFoundException ignored) {
throw new GradleException("settings.gradle does not exist: ${settingsGradle.absolutePath}")
throw new GradleException("settings.gradle/settings.gradle.kts does not exist: ${settingsGradleFile(project).absolutePath}")
}
List<Map<String, Object>> deps = getPluginDependencies(project)
List<String> plugins = getPluginList(project).collect { it.name as String }
......@@ -438,7 +439,7 @@ class FlutterPlugin implements Plugin<Project> {
Project pluginProject = project.rootProject.findProject(":${it.name}")
if (pluginProject == null) {
// Plugin was not included in `settings.gradle`, but is listed in `.flutter-plugins`.
project.logger.error("Plugin project :${it.name} listed, but not found. Please fix your settings.gradle.")
project.logger.error("Plugin project :${it.name} listed, but not found. Please fix your settings.gradle/settings.gradle.kts.")
} else if (doesSupportAndroidPlatform(pluginProject.projectDir.parentFile.path as String)) {
// Plugin has a functioning `android` folder and is included successfully, although it's not supported.
// It must be configured nonetheless, to not throw an "Unresolved reference" exception.
......@@ -451,11 +452,38 @@ class FlutterPlugin implements Plugin<Project> {
// TODO(54566): Can remove this function and its call sites once resolved.
/**
* Returns `true` if the given path contains an `android/build.gradle` file.
* Returns `true` if the given path contains an `android` directory
* containing a `build.gradle` or `build.gradle.kts` file.
*/
private Boolean doesSupportAndroidPlatform(String path) {
File buildGradle = new File(path, 'android' + File.separator + 'build.gradle')
File buildGradleKts = new File(path, 'android' + File.separator + 'build.gradle.kts')
if (buildGradle.exists() && buildGradleKts.exists()) {
logger.error(
"Both build.gradle and build.gradle.kts exist, so " +
"build.gradle.kts is ignored. This is likely a mistake."
)
}
return buildGradle.exists() || buildGradleKts.exists()
}
/**
* Returns the Gradle settings script for the build. When both Groovy and
* Kotlin variants exist, then Groovy (settings.gradle) is preferred over
* Kotlin (settings.gradle.kts). This is the same behavior as Gradle 8.5.
*/
private static Boolean doesSupportAndroidPlatform(String path) {
File editableAndroidProject = new File(path, "android" + File.separator + "build.gradle")
return editableAndroidProject.exists()
private File settingsGradleFile(Project project) {
File settingsGradle = new File(project.projectDir.parentFile, "settings.gradle")
File settingsGradleKts = new File(project.projectDir.parentFile, "settings.gradle.kts")
if (settingsGradle.exists() && settingsGradleKts.exists()) {
logger.error(
"Both settings.gradle and settings.gradle.kts exist, so " +
"settings.gradle.kts is ignored. This is likely a mistake."
)
}
return settingsGradle.exists() ? settingsGradle : settingsGradleKts
}
/** Adds the plugin project dependency to the app project. */
......
......@@ -225,9 +225,15 @@ class _DeferredComponentAndroidFiles {
Directory get componentDir => androidDir.childDirectory(name);
File get androidManifestFile => componentDir.childDirectory('src').childDirectory('main').childFile('AndroidManifest.xml');
File get buildGradleFile => componentDir.childFile('build.gradle');
File get buildGradleFile {
if (componentDir.childFile('build.gradle').existsSync()) {
return componentDir.childFile('build.gradle');
}
return componentDir.childFile('build.gradle.kts');
}
// True when AndroidManifest.xml and build.gradle exist for the android dynamic feature.
// True when AndroidManifest.xml and build.gradle/build.gradle.kts exist for
// the android dynamic feature.
bool verifyFilesExist() {
return androidManifestFile.existsSync() && buildGradleFile.existsSync();
}
......
......@@ -204,20 +204,26 @@ distributionUrl=https\\://services.gradle.org/distributions/gradle-$gradleVersio
/// The Android plugin version is specified in the [build.gradle] file within
/// the project's Android directory.
String getGradleVersionForAndroidPlugin(Directory directory, Logger logger) {
final File buildFile = directory.childFile('build.gradle');
const String buildFileName = 'build.gradle/build.gradle.kts';
File buildFile = directory.childFile('build.gradle');
if (!buildFile.existsSync()) {
buildFile = directory.childFile('build.gradle.kts');
}
if (!buildFile.existsSync()) {
logger.printTrace(
"$buildFile doesn't exist, assuming Gradle version: $templateDefaultGradleVersion");
"$buildFileName doesn't exist, assuming Gradle version: $templateDefaultGradleVersion");
return templateDefaultGradleVersion;
}
final String buildFileContent = buildFile.readAsStringSync();
final Iterable<Match> pluginMatches = _buildAndroidGradlePluginRegExp.allMatches(buildFileContent);
if (pluginMatches.isEmpty) {
logger.printTrace("$buildFile doesn't provide an AGP version, assuming Gradle version: $templateDefaultGradleVersion");
logger.printTrace("$buildFileName doesn't provide an AGP version, assuming Gradle version: $templateDefaultGradleVersion");
return templateDefaultGradleVersion;
}
final String? androidPluginVersion = pluginMatches.first.group(1);
logger.printTrace('$buildFile provides AGP version: $androidPluginVersion');
logger.printTrace('$buildFileName provides AGP version: $androidPluginVersion');
return getGradleVersionFor(androidPluginVersion ?? 'unknown');
}
......@@ -325,9 +331,13 @@ OS: Mac OS X 13.2.1 aarch64
/// [settings.gradle] file within the project's
/// Android directory ([androidDirectory]).
String? getAgpVersion(Directory androidDirectory, Logger logger) {
final File buildFile = androidDirectory.childFile('build.gradle');
File buildFile = androidDirectory.childFile('build.gradle');
if (!buildFile.existsSync()) {
buildFile = androidDirectory.childFile('build.gradle.kts');
}
if (!buildFile.existsSync()) {
logger.printTrace('Can not find build.gradle in $androidDirectory');
logger.printTrace('Can not find build.gradle/build.gradle.kts in $androidDirectory');
return null;
}
final String buildFileContent = buildFile.readAsStringSync();
......
......@@ -459,6 +459,10 @@ class AndroidProject extends FlutterProjectPlatform {
static final RegExp _applicationIdPattern = RegExp('^\\s*applicationId\\s+[\'"](.*)[\'"]\\s*\$');
static final RegExp _imperativeKotlinPluginPattern = RegExp('^\\s*apply plugin\\:\\s+[\'"]kotlin-android[\'"]\\s*\$');
static final RegExp _declarativeKotlinPluginPattern = RegExp('^\\s*id\\s+[\'"]kotlin-android[\'"]\\s*\$');
/// Pattern used to find the assignment of the "group" property in Gradle.
/// Expected example: `group "dev.flutter.plugin"`
/// Regex is used in both Groovy and Kotlin Gradle files.
static final RegExp _groupPattern = RegExp('^\\s*group\\s+[\'"](.*)[\'"]\\s*\$');
/// The Gradle root directory of the Android host app. This is the directory
......@@ -552,10 +556,54 @@ class AndroidProject extends FlutterProjectPlatform {
return imperativeMatch || declarativeMatch;
}
/// Gets top-level Gradle build file.
/// See https://developer.android.com/build#top-level.
///
/// The file must exist and it must be written in either Groovy (build.gradle)
/// or Kotlin (build.gradle.kts).
File get hostAppGradleFile {
final File buildGroovy = hostAppGradleRoot.childFile('build.gradle');
final File buildKotlin = hostAppGradleRoot.childFile('build.gradle.kts');
if (buildGroovy.existsSync() && buildKotlin.existsSync()) {
// We mimic Gradle's behavior of preferring Groovy over Kotlin when both files exist.
return buildGroovy;
}
if (buildKotlin.existsSync()) {
return buildKotlin;
}
// TODO(bartekpacia): An exception should be thrown when neither
// build.gradle nor build.gradle.kts exist, instead of falling back to the
// Groovy file. See #141180.
return buildGroovy;
}
/// Gets the module-level build.gradle file.
/// See https://developer.android.com/build#module-level.
File get appGradleFile => hostAppGradleRoot.childDirectory('app')
.childFile('build.gradle');
///
/// The file must exist and it must be written in either Groovy (build.gradle)
/// or Kotlin (build.gradle.kts).
File get appGradleFile {
final Directory appDir = hostAppGradleRoot.childDirectory('app');
final File buildGroovy = appDir.childFile('build.gradle');
final File buildKotlin = appDir.childFile('build.gradle.kts');
if (buildGroovy.existsSync() && buildKotlin.existsSync()) {
// We mimic Gradle's behavior of preferring Groovy over Kotlin when both files exist.
return buildGroovy;
}
if (buildKotlin.existsSync()) {
return buildKotlin;
}
// TODO(bartekpacia): An exception should be thrown when neither
// build.gradle nor build.gradle.kts exist, instead of falling back to the
// Groovy file. See #141180.
return buildGroovy;
}
File get appManifestFile {
if (isUsingGradle) {
......@@ -652,7 +700,7 @@ $javaGradleCompatUrl
}
bool get isUsingGradle {
return hostAppGradleRoot.childFile('build.gradle').existsSync();
return hostAppGradleFile.existsSync();
}
String? get applicationId {
......@@ -671,8 +719,7 @@ $javaGradleCompatUrl
}
String? get group {
final File gradleFile = hostAppGradleRoot.childFile('build.gradle');
return firstMatchInFile(gradleFile, _groupPattern)?.group(1);
return firstMatchInFile(hostAppGradleFile, _groupPattern)?.group(1);
}
/// The build directory where the Android artifacts are placed.
......
......@@ -10,13 +10,14 @@ import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/version_range.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/project.dart';
import '../../src/common.dart';
import '../../src/fake_process_manager.dart';
import '../../src/fakes.dart';
void main() {
group('injectGradleWrapperIfNeeded', () {
late MemoryFileSystem fileSystem;
late FileSystem fileSystem;
late Directory gradleWrapperDirectory;
late GradleUtils gradleUtils;
......@@ -386,7 +387,7 @@ OS: Mac OS X 13.2.1 aarch64
);
});
testWithoutContext('returns the AGP version when set', () async {
testWithoutContext('returns the AGP version when set in Groovy', () async {
const String expectedVersion = '7.3.0';
final Directory androidDirectory = fileSystem.directory('/android')
..createSync();
......@@ -415,6 +416,90 @@ allprojects {
expectedVersion,
);
});
testWithoutContext('returns the AGP version when set in Kotlin', () async {
const String expectedVersion = '7.3.0';
final Directory androidDirectory = fileSystem.directory('/android')
..createSync();
androidDirectory.childFile('build.gradle.kts').writeAsStringSync('''
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:$expectedVersion")
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
''');
expect(
getAgpVersion(androidDirectory, BufferLogger.test()),
expectedVersion,
);
});
testWithoutContext('prefers the AGP version when set in Groovy, ignores Kotlin', () async {
const String versionInGroovy = '7.3.0';
const String versionInKotlin = '7.4.2';
final Directory androidDirectory = fileSystem.directory('/android')
..createSync();
androidDirectory.childFile('build.gradle').writeAsStringSync('''
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:$versionInGroovy'
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
''');
androidDirectory.childFile('build.gradle.kts').writeAsStringSync('''
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:$versionInKotlin")
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
''');
expect(
getAgpVersion(androidDirectory, BufferLogger.test()),
versionInGroovy,
);
});
testWithoutContext('returns null when AGP version not set', () async {
final Directory androidDirectory = fileSystem.directory('/android')
..createSync();
......@@ -703,6 +788,68 @@ include ":app"
});
});
group('getGradleVersionForAndroidPlugin', () {
late FileSystem fileSystem;
late Logger testLogger;
setUp(() {
fileSystem = MemoryFileSystem.test();
testLogger = BufferLogger.test();
});
testWithoutContext('prefers build.gradle over build.gradle.kts', () async {
const String versionInGroovy = '4.0.0';
const String versionInKotlin = '7.4.2';
final Directory androidDirectory = fileSystem.directory('/android')..createSync();
androidDirectory.childFile('build.gradle').writeAsStringSync('''
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:$versionInGroovy'
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
''');
androidDirectory.childFile('build.gradle.kts').writeAsStringSync('''
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:$versionInKotlin")
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
''');
expect(
getGradleVersionForAndroidPlugin(androidDirectory, testLogger),
'6.7', // as per compatibility matrix in gradle_utils.dart
);
});
});
group('validates java/AGP versions', () {
final List<JavaAgpTestData> testData = <JavaAgpTestData>[
// Strictly too old Java versions for known AGP versions.
......
......@@ -738,6 +738,60 @@ apply plugin: 'kotlin-android'
XcodeProjectInterpreter: () => xcodeProjectInterpreter,
FlutterProjectFactory: () => flutterProjectFactory,
});
testUsingContext('kotlin host app language with Gradle Kotlin DSL', () async {
final FlutterProject project = await someProject();
addAndroidGradleFile(project.directory,
kotlinDsl: true,
gradleFileContent: () {
return '''
plugins {
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
}
''';
});
expect(project.android.isKotlin, isTrue);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
XcodeProjectInterpreter: () => xcodeProjectInterpreter,
FlutterProjectFactory: () => flutterProjectFactory,
});
testUsingContext('Gradle Groovy files are preferred to Gradle Kotlin files', () async {
final FlutterProject project = await someProject();
addAndroidGradleFile(project.directory,
gradleFileContent: () {
return '''
plugins {
id "com.android.application"
id "dev.flutter.flutter-gradle-plugin"
}
''';
});
addAndroidGradleFile(project.directory,
kotlinDsl: true,
gradleFileContent: () {
return '''
plugins {
id("com.android.application")
id("kotlin-android")
id("dev.flutter.flutter-gradle-plugin")
}
''';
});
expect(project.android.isKotlin, isFalse);
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
XcodeProjectInterpreter: () => xcodeProjectInterpreter,
FlutterProjectFactory: () => flutterProjectFactory,
});
});
group('With mocked context', () {
......@@ -1568,11 +1622,18 @@ void addIosProjectFile(Directory directory, {required String Function() projectF
..writeAsStringSync(projectFileContent());
}
void addAndroidGradleFile(Directory directory, { required String Function() gradleFileContent }) {
/// Adds app-level Gradle Groovy build file (build.gradle) to [directory].
///
/// If [kotlinDsl] is true, then build.gradle.kts is created instead of
/// build.gradle. It's the caller's responsibility to make sure that
/// [gradleFileContent] is consistent with the value of the [kotlinDsl] flag.
void addAndroidGradleFile(Directory directory, {
required String Function() gradleFileContent, bool kotlinDsl = false,
}) {
directory
.childDirectory('android')
.childDirectory('app')
.childFile('build.gradle')
.childFile(kotlinDsl ? 'build.gradle.kts' : 'build.gradle')
..createSync(recursive: true)
..writeAsStringSync(gradleFileContent());
}
......@@ -1608,8 +1669,8 @@ FileSystem getFileSystemForPlatform() {
);
}
void addAndroidWithGroup(Directory directory, String id) {
directory.childDirectory('android').childFile('build.gradle')
void addAndroidWithGroup(Directory directory, String id, {bool kotlinDsl = false}) {
directory.childDirectory('android').childFile(kotlinDsl ? 'build.gradle.kts' : 'build.gradle')
..createSync(recursive: true)
..writeAsStringSync(gradleFileWithGroupId(id));
}
......
......@@ -147,7 +147,7 @@ void main() {
final ProcessResult buildApkResult = await testUnsupportedPlugin(
project: project, createAndroidPluginFolder: true);
expect(buildApkResult.stderr.toString(),
isNot(contains('Please fix your settings.gradle.')));
isNot(contains('Please fix your settings.gradle')));
expect(buildApkResult, const ProcessResultMatcher());
});
......@@ -167,7 +167,7 @@ void main() {
expect(
buildApkResult,
const ProcessResultMatcher(
stderrPattern: 'Please fix your settings.gradle.'),
stderrPattern: 'Please fix your settings.gradle/settings.gradle.kts'),
);
});
}
......
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