flutter/dev/tools/bin/generate_gradle_lockfiles.dart
Gray Mackall 052be9c41a
Update generate_gradle_lockfiles.dart to handle batch updating kotlin Gradle files (#162628)
Also removes the only entry from the exclusion script, as this fixes the
reason for its exclusion.

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md

---------

Co-authored-by: Gray Mackall <mackall@google.com>
2025-02-04 02:07:16 +00:00

416 lines
14 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.
// For `android` directory in the repo, this script generates:
// 1. The top-level build.gradle (android/build.gradle).
// 2. The top level settings.gradle (android/settings.gradle).
// 3. The gradle wrapper file (android/gradle/wrapper/gradle-wrapper.properties).
// Then it generate the lockfiles for each Gradle project.
// To regenerate these files, run `dart dev/tools/bin/generate_gradle_lockfiles.dart`.
import 'dart:collection';
import 'dart:io';
import 'package:args/args.dart';
import 'package:file/file.dart';
import 'package:file/local.dart';
import 'package:yaml/yaml.dart';
void main(List<String> arguments) {
const String usageMessage =
"If you don't wish to re-generate the "
'settings.gradle, build.gradle, and gradle-wrapper.properties files,\n'
'add the flag `--no-gradle-generation`.\n'
'This tool automatically excludes a set of android subdirectories, '
'defined at dev/tools/bin/config/lockfile_exclusion.yaml.\n'
'To disable this behavior, run with `--no-exclusion`.\n';
final ArgParser argParser =
ArgParser()
..addFlag(
'gradle-generation',
help: 'Re-generate gradle files in each processed directory.',
defaultsTo: true,
)
..addFlag(
'exclusion',
help:
'Run the script using the config file at ./configs/lockfile_exclusion.yaml to skip the specified subdirectories.',
defaultsTo: true,
);
ArgResults args;
try {
args = argParser.parse(arguments);
} on FormatException catch (error) {
stderr.writeln('${error.message}\n');
stderr.writeln(usageMessage);
exit(1);
}
print(usageMessage);
/// Re-generate gradle files in each processed directory.
final bool gradleGeneration = (args['gradle-generation'] as bool?) ?? true;
// Skip android subdirectories specified in the ./config/lockfile_exclusion.yaml file.
final bool useExclusion = (args['exclusion'] as bool?) ?? true;
const FileSystem fileSystem = LocalFileSystem();
final Directory repoRoot =
(() {
final String repoRootPath =
exec('git', const <String>['rev-parse', '--show-toplevel']).trim();
final Directory repoRoot = fileSystem.directory(repoRootPath);
if (!repoRoot.existsSync()) {
throw StateError("Expected $repoRoot to exist but it didn't!");
}
return repoRoot;
})();
final Iterable<Directory> androidDirectories = discoverAndroidDirectories(repoRoot);
final File exclusionFile = repoRoot
.childDirectory('dev')
.childDirectory('tools')
.childDirectory('bin')
.childDirectory('config')
.childFile('lockfile_exclusion.yaml');
// Load the exclusion set, or make an empty exclusion set.
final Set<String> exclusionSet;
if (useExclusion) {
exclusionSet = HashSet<String>.from(
((loadYaml(exclusionFile.readAsStringSync()) ?? YamlList()) as YamlList)
.toList()
.cast<String>()
.map((String s) => '${repoRoot.path}/$s'),
);
print('Loaded exclusion file from ${exclusionFile.path}.');
} else {
exclusionSet = <String>{};
print('Running without exclusion.');
}
for (final Directory androidDirectory in androidDirectories) {
if (!androidDirectory.existsSync()) {
throw '$androidDirectory does not exist';
}
if (exclusionSet.contains(androidDirectory.path)) {
print(
'${androidDirectory.path} is included in the exclusion config file at ${exclusionFile.path} - skipping',
);
continue;
}
late File rootBuildGradle;
if (androidDirectory.childFile('build.gradle').existsSync()) {
rootBuildGradle = androidDirectory.childFile('build.gradle');
} else if (androidDirectory.childFile('build.gradle.kts').existsSync()) {
rootBuildGradle = androidDirectory.childFile('build.gradle.kts');
} else {
print('${androidDirectory.childFile('build.gradle').path}(.kts) does not exist - skipping');
continue;
}
late File settingsGradle;
if (androidDirectory.childFile('settings.gradle').existsSync()) {
settingsGradle = androidDirectory.childFile('settings.gradle');
} else if (androidDirectory.childFile('settings.gradle.kts').existsSync()) {
settingsGradle = androidDirectory.childFile('settings.gradle.kts');
} else {
print(
'${androidDirectory.childFile('settings.gradle').path}(.kts) does not exist - skipping',
);
continue;
}
final File wrapperGradle = androidDirectory
.childDirectory('gradle')
.childDirectory('wrapper')
.childFile('gradle-wrapper.properties');
if (!wrapperGradle.existsSync()) {
print('${wrapperGradle.path} does not exist - skipping');
continue;
}
if (settingsGradle.readAsStringSync().contains('include_flutter.groovy')) {
print('${settingsGradle.path} add to app - skipping');
continue;
}
if (!androidDirectory.childDirectory('app').existsSync()) {
print('${rootBuildGradle.path} is not an app - skipping');
continue;
}
if (!androidDirectory.parent.childFile('pubspec.yaml').existsSync()) {
print('${rootBuildGradle.path} no pubspec.yaml in parent directory - skipping');
continue;
}
if (androidDirectory.parent
.childFile('pubspec.yaml')
.readAsStringSync()
.contains('deferred-components')) {
print('${rootBuildGradle.path} uses deferred components - skipping');
continue;
}
if (!androidDirectory.parent.childDirectory('lib').childFile('main.dart').existsSync()) {
print('${rootBuildGradle.path} no main.dart under lib - skipping');
continue;
}
print('Processing ${androidDirectory.path}');
try {
androidDirectory.childFile('buildscript-gradle.lockfile').deleteSync();
} on FileSystemException {
// noop
}
if (gradleGeneration) {
// Write file content corresponding to original file language.
if (rootBuildGradle.basename.endsWith('.kts')) {
rootBuildGradle.writeAsStringSync(rootGradleKtsFileContent);
} else {
rootBuildGradle.writeAsStringSync(rootGradleFileContent);
}
if (settingsGradle.basename.endsWith('.kts')) {
settingsGradle.writeAsStringSync(settingsGradleKtsFileContent);
} else {
settingsGradle.writeAsStringSync(settingGradleFileContent);
}
wrapperGradle.writeAsStringSync(wrapperGradleFileContent);
}
final String appDirectory = androidDirectory.parent.absolute.path;
// Fetch pub dependencies.
final String flutterPath = repoRoot.childDirectory('bin').childFile('flutter').path;
exec(flutterPath, <String>['pub', 'get'], workingDirectory: appDirectory);
// Verify that the Gradlew wrapper exists.
final File gradleWrapper = androidDirectory.childFile('gradlew');
// Generate Gradle wrapper if it doesn't exist.
if (!gradleWrapper.existsSync()) {
exec(flutterPath, <String>['build', 'apk', '--config-only'], workingDirectory: appDirectory);
}
// Generate lock files.
exec(gradleWrapper.absolute.path, <String>[
':generateLockfiles',
], workingDirectory: androidDirectory.absolute.path);
print('Processed');
}
}
String exec(String cmd, List<String> args, {String? workingDirectory}) {
final ProcessResult result = Process.runSync(cmd, args, workingDirectory: workingDirectory);
if (result.exitCode != 0) {
throw ProcessException(cmd, args, '${result.stdout}${result.stderr}', result.exitCode);
}
return result.stdout as String;
}
const String rootGradleFileContent = r'''
// 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 build.gradle files in the Flutter repo,
// See dev/tools/bin/generate_gradle_lockfiles.dart.
allprojects {
repositories {
google()
mavenCentral()
}
}
rootProject.layout.buildDirectory.value(rootProject.layout.buildDirectory.dir("../../build").get())
subprojects {
project.layout.buildDirectory.value(rootProject.layout.buildDirectory.dir(project.name).get())
}
subprojects {
project.evaluationDependsOn(':app')
dependencyLocking {
ignoredDependencies.add('io.flutter:*')
lockFile = file("${rootProject.projectDir}/project-${project.name}.lockfile")
if (!project.hasProperty('local-engine-repo')) {
lockAllConfigurations()
}
}
}
tasks.register("clean", Delete) {
delete rootProject.layout.buildDirectory
}
''';
const String settingGradleFileContent = r'''
// 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()
includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
buildscript {
dependencyLocking {
lockFile = file("${rootProject.projectDir}/buildscript-gradle.lockfile")
lockAllConfigurations()
}
}
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.7.0" apply false
id "org.jetbrains.kotlin.android" version "1.8.10" apply false
}
include ":app"
''';
// Consider updating this file to reflect the latest updates to app templates
// when performing batch updates (this file is modeled after
// root_app/android/build.gradle.kts).
const String rootGradleKtsFileContent = r'''
// 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.
allprojects {
repositories {
google()
mavenCentral()
}
}
rootProject.layout.buildDirectory.value(rootProject.layout.buildDirectory.dir("../../build").get())
subprojects {
project.layout.buildDirectory.value(rootProject.layout.buildDirectory.dir(project.name).get())
}
subprojects {
project.evaluationDependsOn(":app")
dependencyLocking {
ignoredDependencies.add("io.flutter:*")
lockFile = file("${rootProject.projectDir}/project-${project.name}.lockfile")
if (!project.hasProperty("local-engine-repo")) {
lockAllConfigurations()
}
}
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
''';
// Consider updating this file to reflect the latest updates to app templates
// when performing batch updates (this file is modeled after
// root_app/android/settings.gradle.kts).
const String settingsGradleKtsFileContent = r'''
// 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 {
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()
}
}
buildscript {
dependencyLocking {
lockFile = file("${rootProject.projectDir}/buildscript-gradle.lockfile")
lockAllConfigurations()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.7.0" apply false
id("org.jetbrains.kotlin.android") version "1.8.22" apply false
}
include(":app")
''';
const String wrapperGradleFileContent = r'''
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
''';
Iterable<Directory> discoverAndroidDirectories(Directory repoRoot) {
return repoRoot
.listSync()
.whereType<Directory>()
// Exclude the top-level "engine/" directory, which is not covered by the the tool.
.where((Directory directory) => directory.basename != 'engine')
// ... and then recurse into every directory (other than the excluded directory).
.expand((Directory directory) => directory.listSync(recursive: true))
.whereType<Directory>()
// These directories are build artifacts which are not part of source control.
.where(
(Directory directory) =>
!directory.path.contains('/build/') && !directory.path.contains('.symlinks'),
)
// ... where the directory ultimately is named "android".
.where((FileSystemEntity entity) => entity.basename == 'android');
}