flutter_flutter/dev/tools/bin/generate_gradle_lockfiles.dart
Kate Lovett 9d96df2364
Modernize framework lints (#179089)
WIP

Commits separated as follows:
- Update lints in analysis_options files
- Run `dart fix --apply`
- Clean up leftover analysis issues 
- Run `dart format .` in the right places.

Local analysis and testing passes. Checking CI now.

Part of https://github.com/flutter/flutter/issues/178827
- Adoption of flutter_lints in examples/api coming in a separate change
(cc @loic-sharma)

## Pre-launch Checklist

- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] 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].
- [ ] 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].

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- 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
2025-11-26 01:10:39 +00:00

463 lines
15 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 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';
const ignoreFilename = '.ignore-locking.md';
final 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,
)
..addOption(
'ignore-locking',
help: 'Reason to disable gradle dependency locking. A reason must be given.',
)
..addFlag('stop-ignoring', help: 'Delete the ignore lockfile if it exists');
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;
final ignoreLocking = args['ignore-locking'] != null;
final String ignoreReason = (args['ignore-locking'] as String?) ?? '';
// This is an explicit flag that insures the ignore
// lockfile isn't deleted unless specified. This should prevent
// automated scripts from deleting the file when they shouldn't.
final bool stopIgnoring = (args['stop-ignoring'] as bool?) ?? false;
if (ignoreLocking && ignoreReason.isEmpty) {
stderr.writeln('A reason must be provided for --ignore-locking.');
stderr.writeln(usageMessage);
exit(1);
}
if (ignoreLocking && stopIgnoring) {
stderr.writeln(
'Both --ignore-locking and --stop-ignoring cannot be used on the same invocation.',
);
stderr.writeln(usageMessage);
exit(1);
}
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 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}');
final File ignoreFile = androidDirectory.childFile(ignoreFilename);
if (ignoreLocking) {
print('Writing ignore file in ${ignoreFile.path}');
ignoreFile.writeAsStringSync(ignoreReason);
// When ignoring locking, we do not want to actually generate
// the lockfiles
continue;
} else if (stopIgnoring && ignoreFile.existsSync()) {
ignoreFile.deleteSync();
}
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")
def ignoreFile = file("${rootProject.projectDir}/.ignore-locking.md")
if (!ignoreFile.exists() && !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.11.1" apply false
id "org.jetbrains.kotlin.android" version "2.2.20" 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).
// After modification verify formatting with ktlint.
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")
var ignoreFile = file("${rootProject.projectDir}/.ignore-locking.md")
if (!ignoreFile.exists() && !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.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" 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.14-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');
}