Unverified Commit 93919232 authored by gmackall's avatar gmackall Committed by GitHub

Add an android migrator to upgrade minSdkVersions 16,17,18 to flutter.minSdkVersion (#129729)

This migrator will upgrade the minSdkVersion used in the [module-level build.gradle](https://developer.android.com/build#module-level) file to flutter.minSdkVersion. 

The PR also makes a small refactor to `AndroidProject` to add a getter for the module level build.gradle file, and uses that getter in places where we were getting that file (previously it was being gotten directly via `hostAppGradleRoot.childDirectory('app').childFile('build.gradle')`.

Part of the work for deprecating support for the Jelly Bean android apis.
parent a903f1de
......@@ -35,6 +35,7 @@ import 'gradle_errors.dart';
import 'gradle_utils.dart';
import 'java.dart';
import 'migrations/android_studio_java_gradle_conflict_migration.dart';
import 'migrations/min_sdk_version_migration.dart';
import 'migrations/top_level_gradle_build_file_migration.dart';
import 'multidex.dart';
......@@ -330,8 +331,8 @@ class AndroidGradleBuilder implements AndroidBuilder {
AndroidStudioJavaGradleConflictMigration(_logger,
project: project.android,
androidStudio: _androidStudio,
java: globals.java)
,
java: globals.java),
MinSdkVersionMigration(project.android, _logger),
];
final ProjectMigration migration = ProjectMigration(migrators);
......
......@@ -57,23 +57,28 @@ const String maxKnownAgpVersion = '8.1';
// Parentheticals are use to group which helps with version extraction.
// "...build:gradle:(...)" where group(1) should be the version string.
final RegExp _androidGradlePluginRegExp =
RegExp(r'com\.android\.tools\.build:gradle:(\d+\.\d+\.\d+)');
RegExp(r'com\.android\.tools\.build:gradle:(\d+\.\d+\.\d+)');
// Expected content format (with lines above and below).
// Version can have 2 or 3 numbers.
// 'distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip'
// '^\s*' protects against commented out lines.
final RegExp distributionUrlRegex =
RegExp(r'^\s*distributionUrl\s*=\s*.*\.zip', multiLine: true);
RegExp(r'^\s*distributionUrl\s*=\s*.*\.zip', multiLine: true);
// Modified version of the gradle distribution url match designed to only match
// gradle.org urls so that we can guarantee any modifications to the url
// still points to a hosted zip.
final RegExp gradleOrgVersionMatch =
RegExp(
RegExp(
r'^\s*distributionUrl\s*=\s*https\\://services\.gradle\.org/distributions/gradle-((?:\d|\.)+)-(.*)\.zip',
multiLine: true
);
);
// This matches uncommented minSdkVersion lines in the module-level build.gradle
// file which have minSdkVersion 16,17, or 18 (the Jelly Bean api levels).
final RegExp jellyBeanMinSdkVersionMatch =
RegExp(r'(?<=^\s*)minSdkVersion 1[678](?=\s*(?://|$))', multiLine: true);
// From https://docs.gradle.org/current/userguide/command_line_interface.html#command_line_interface
const String gradleVersionFlag = r'--version';
......
// 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';
import '../../base/file_system.dart';
import '../../base/project_migrator.dart';
import '../../project.dart';
import '../gradle_utils.dart';
/// Replacement value for https://developer.android.com/reference/tools/gradle-api/8.0/com/android/build/api/dsl/BaseFlavor#minSdkVersion(kotlin.Int)
/// that instead of using a value defaults to the version defined by the
/// flutter sdk as the minimum supported by flutter.
@visibleForTesting
const String replacementMinSdkText = 'minSdkVersion flutter.minSdkVersion';
@visibleForTesting
const String appGradleNotFoundWarning = 'Module level build.gradle file not found, skipping minSdkVersion migration.';
class MinSdkVersionMigration extends ProjectMigrator {
MinSdkVersionMigration(
AndroidProject project,
super.logger,
) : _project = project;
final AndroidProject _project;
@override
void migrate() {
// Skip applying migration in modules as the FlutterExtension is not applied.
if (_project.isModule) {
return;
}
try {
processFileLines(_project.appGradleFile);
} on FileSystemException {
// Skip if we cannot find the app level build.gradle file.
logger.printTrace(appGradleNotFoundWarning);
}
}
@override
String migrateFileContents(String fileContents) {
return fileContents.replaceAll(jellyBeanMinSdkVersionMatch, replacementMinSdkText);
}
}
......@@ -509,30 +509,45 @@ class AndroidProject extends FlutterProjectPlatform {
if (plugin.existsSync()) {
return false;
}
final File appGradle = hostAppGradleRoot.childFile(
fileSystem.path.join('app', 'build.gradle'));
if (!appGradle.existsSync()) {
return false;
}
for (final String line in appGradle.readAsLinesSync()) {
final bool fileBasedApply = line.contains(RegExp(r'apply from: .*/flutter.gradle'));
final bool declarativeApply = line.contains('dev.flutter.flutter-gradle-plugin');
final bool managed = line.contains("def flutterPluginVersion = 'managed'");
if (fileBasedApply || declarativeApply || managed) {
return true;
try {
for (final String line in appGradleFile.readAsLinesSync()) {
// This syntax corresponds to applying the Flutter Gradle Plugin with a
// script.
// See https://docs.gradle.org/current/userguide/plugins.html#sec:script_plugins.
final bool fileBasedApply = line.contains(RegExp(r'apply from: .*/flutter.gradle'));
// This syntax corresponds to applying the Flutter Gradle Plugin using
// the declarative "plugins {}" block after including it in the
// pluginManagement block of the settings.gradle file.
// See https://docs.gradle.org/current/userguide/composite_builds.html#included_plugin_builds,
// as well as the settings.gradle and build.gradle templates.
final bool declarativeApply = line.contains('dev.flutter.flutter-gradle-plugin');
// This case allows for flutter run/build to work for modules. It does
// not guarantee the Flutter Gradle Plugin is applied.
final bool managed = line.contains("def flutterPluginVersion = 'managed'");
if (fileBasedApply || declarativeApply || managed) {
return true;
}
}
} on FileSystemException {
return false;
}
return false;
}
/// True, if the app project is using Kotlin.
bool get isKotlin {
final File gradleFile = hostAppGradleRoot.childDirectory('app').childFile('build.gradle');
final bool imperativeMatch = firstMatchInFile(gradleFile, _imperativeKotlinPluginPattern) != null;
final bool declarativeMatch = firstMatchInFile(gradleFile, _declarativeKotlinPluginPattern) != null;
final bool imperativeMatch = firstMatchInFile(appGradleFile, _imperativeKotlinPluginPattern) != null;
final bool declarativeMatch = firstMatchInFile(appGradleFile, _declarativeKotlinPluginPattern) != null;
return imperativeMatch || declarativeMatch;
}
/// Gets the module-level build.gradle file.
/// See https://developer.android.com/build#module-level.
File get appGradleFile => hostAppGradleRoot.childDirectory('app')
.childFile('build.gradle');
File get appManifestFile {
if (isUsingGradle) {
return hostAppGradleRoot
......@@ -627,21 +642,18 @@ $javaGradleCompatUrl
}
String? get applicationId {
final File gradleFile = hostAppGradleRoot.childDirectory('app').childFile('build.gradle');
return firstMatchInFile(gradleFile, _applicationIdPattern)?.group(1);
return firstMatchInFile(appGradleFile, _applicationIdPattern)?.group(1);
}
/// Get the namespace for newer Android projects,
/// which replaces the `package` attribute in the Manifest.xml.
String? get namespace {
final File gradleFile = hostAppGradleRoot.childDirectory('app').childFile('build.gradle');
if (!gradleFile.existsSync()) {
try {
// firstMatchInFile() reads per line but `_androidNamespacePattern` matches a multiline pattern.
return _androidNamespacePattern.firstMatch(appGradleFile.readAsStringSync())?.group(1);
} on FileSystemException {
return null;
}
// firstMatchInFile() reads per line but `_androidNamespacePattern` matches a multiline pattern.
return _androidNamespacePattern.firstMatch(gradleFile.readAsStringSync())?.group(1);
}
String? get group {
......
......@@ -7,6 +7,7 @@ import 'package:file/memory.dart';
import 'package:flutter_tools/src/android/android_studio.dart';
import 'package:flutter_tools/src/android/gradle_utils.dart';
import 'package:flutter_tools/src/android/migrations/android_studio_java_gradle_conflict_migration.dart';
import 'package:flutter_tools/src/android/migrations/min_sdk_version_migration.dart';
import 'package:flutter_tools/src/android/migrations/top_level_gradle_build_file_migration.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/version.dart';
......@@ -41,6 +42,79 @@ zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
''';
String sampleModuleGradleBuildFile(String minSdkVersionString) {
return r'''
plugins {
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
}
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
android {
namespace "com.example.asset_sample"
compileSdkVersion flutter.compileSdkVersion
ndkVersion flutter.ndkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.asset_sample"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
''' + minSdkVersionString + r'''
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug
}
}
}
flutter {
source '../..'
}
dependencies {}
''';
}
final Version androidStudioDolphin = Version(2021, 3, 1);
const Version _javaVersion17 = Version.withText(17, 0, 2, 'openjdk 17.0.2');
......@@ -257,14 +331,128 @@ tasks.register("clean", Delete) {
expect(bufferLogger.traceText, contains(optOutFlagEnabled));
});
});
group('migrate min sdk versions less than 19 to flutter.minSdkVersion '
'when in a FlutterProject that is an app', ()
{
late MemoryFileSystem memoryFileSystem;
late BufferLogger bufferLogger;
late FakeAndroidProject project;
late MinSdkVersionMigration migration;
setUp(() {
memoryFileSystem = MemoryFileSystem.test();
memoryFileSystem.currentDirectory.childDirectory('android').createSync();
bufferLogger = BufferLogger.test();
project = FakeAndroidProject(
root: memoryFileSystem.currentDirectory.childDirectory('android'),
);
project.appGradleFile.parent.createSync(recursive: true);
migration = MinSdkVersionMigration(
project,
bufferLogger
);
});
testWithoutContext('do nothing when files missing', () {
migration.migrate();
expect(bufferLogger.traceText, contains(appGradleNotFoundWarning));
});
testWithoutContext('replace when api 16', () {
const String minSdkVersion16 = 'minSdkVersion 16';
project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(minSdkVersion16));
migration.migrate();
expect(project.appGradleFile.readAsStringSync(), sampleModuleGradleBuildFile(replacementMinSdkText));
});
testWithoutContext('replace when api 17', () {
const String minSdkVersion17 = 'minSdkVersion 17';
project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(minSdkVersion17));
migration.migrate();
expect(project.appGradleFile.readAsStringSync(), sampleModuleGradleBuildFile(replacementMinSdkText));
});
testWithoutContext('replace when api 18', () {
const String minSdkVersion18 = 'minSdkVersion 18';
project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(minSdkVersion18));
migration.migrate();
expect(project.appGradleFile.readAsStringSync(), sampleModuleGradleBuildFile(replacementMinSdkText));
});
testWithoutContext('do nothing when >=api 19', () {
const String minSdkVersion19 = 'minSdkVersion 19';
project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(minSdkVersion19));
migration.migrate();
expect(project.appGradleFile.readAsStringSync(), sampleModuleGradleBuildFile(minSdkVersion19));
});
testWithoutContext('do nothing when already using '
'flutter.minSdkVersion', () {
project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(replacementMinSdkText));
migration.migrate();
expect(project.appGradleFile.readAsStringSync(), sampleModuleGradleBuildFile(replacementMinSdkText));
});
testWithoutContext('avoid rewriting comments', () {
const String code = '// minSdkVersion 16 // old default\n'
' minSdkVersion 23 // new version';
project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(code));
migration.migrate();
expect(project.appGradleFile.readAsStringSync(), sampleModuleGradleBuildFile(code));
});
testWithoutContext('do nothing when project is a module', () {
project = FakeAndroidProject(
root: memoryFileSystem.currentDirectory.childDirectory('android'),
module: true,
);
migration = MinSdkVersionMigration(
project,
bufferLogger
);
const String minSdkVersion16 = 'minSdkVersion 16';
project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(minSdkVersion16));
migration.migrate();
expect(project.appGradleFile.readAsStringSync(), sampleModuleGradleBuildFile(minSdkVersion16));
});
testWithoutContext('do nothing when minSdkVersion is set '
'to a constant', () {
const String minSdkVersionConstant = 'minSdkVersion kMinSdkversion';
project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(minSdkVersionConstant));
migration.migrate();
expect(project.appGradleFile.readAsStringSync(), sampleModuleGradleBuildFile(minSdkVersionConstant));
});
testWithoutContext('do nothing when minSdkVersion is set '
'using = syntax', () {
const String equalsSyntaxMinSdkVersion16 = 'minSdkVersion = 16';
project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(equalsSyntaxMinSdkVersion16));
migration.migrate();
expect(project.appGradleFile.readAsStringSync(), sampleModuleGradleBuildFile(equalsSyntaxMinSdkVersion16));
});
});
});
}
class FakeAndroidProject extends Fake implements AndroidProject {
FakeAndroidProject({required Directory root}) : hostAppGradleRoot = root;
FakeAndroidProject({required Directory root, this.module, this.plugin}) : hostAppGradleRoot = root;
@override
Directory hostAppGradleRoot;
final bool? module;
final bool? plugin;
@override
bool get isPlugin => plugin ?? false;
@override
bool get isModule => module ?? false;
@override
File get appGradleFile => hostAppGradleRoot.childDirectory('app').childFile('build.gradle');
}
class FakeAndroidStudio extends Fake implements AndroidStudio {
......
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