// 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 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 ['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 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 exclusionSet; if (useExclusion) { exclusionSet = HashSet.from( ((loadYaml(exclusionFile.readAsStringSync()) ?? YamlList()) as YamlList) .toList() .cast() .map((String s) => '${repoRoot.path}/$s'), ); print('Loaded exclusion file from ${exclusionFile.path}.'); } else { exclusionSet = {}; 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, ['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, ['build', 'apk', '--config-only'], workingDirectory: appDirectory); } // Generate lock files. exec(gradleWrapper.absolute.path, [ ':generateLockfiles', ], workingDirectory: androidDirectory.absolute.path); print('Processed'); } } String exec(String cmd, List 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("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 discoverAndroidDirectories(Directory repoRoot) { return repoRoot .listSync() .whereType() // 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() // 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'); }