1This patch introduces an intermediate Gradle build step to alter the behavior
2of flutter_tools' Gradle project, specifically moving the creation of `build`
3and `.gradle` directories from within the Nix Store to somewhere in `$HOME/.cache/flutter/nix-flutter-tools-gradle/$engineShortRev`.
4
5Without this patch, flutter_tools' Gradle project tries to generate `build` and `.gradle`
6directories within the Nix Store. Resulting in read-only errors when trying to build a
7Flutter Android app at runtime.
8
9This patch takes advantage of the fact settings.gradle takes priority over settings.gradle.kts to build the intermediate Gradle project
10when a Flutter app runs `includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")`
11
12`rootProject.buildFileName = "/dev/null"` so that the intermediate project doesn't use `build.gradle.kts` that's in the same directory.
13
14The intermediate project makes a `settings.gradle` file in `$HOME/.cache/flutter/nix-flutter-tools-gradle/<short engine rev>/` and `includeBuild`s it.
15This Gradle project will build the actual `packages/flutter_tools/gradle` project by setting
16`rootProject.projectDir = new File("$settingsDir")` and `apply from: new File("$settingsDir/settings.gradle.kts")`.
17
18To move `build` to `$HOME/.cache/flutter/nix-flutter-tools-gradle/<short engine rev>/`, we need to set `buildDirectory`.
19To move `.gradle` as well, the `--project-cache-dir` argument must be passed to the Gradle wrapper.
20Changing the `GradleUtils.getExecutable` function signature is a delibarate choice, to ensure that no new unpatched usages slip in.
21--- /dev/null
22+++ b/packages/flutter_tools/gradle/settings.gradle
23@@ -0,0 +1,19 @@
24+rootProject.buildFileName = "/dev/null"
25+
26+def engineShortRev = (new File("$settingsDir/../../../bin/internal/engine.version")).text.take(10)
27+def dir = new File("$System.env.HOME/.cache/flutter/nix-flutter-tools-gradle/$engineShortRev")
28+dir.mkdirs()
29+def file = new File(dir, "settings.gradle")
30+
31+file.text = """
32+rootProject.projectDir = new File("$settingsDir")
33+apply from: new File("$settingsDir/settings.gradle.kts")
34+
35+gradle.allprojects { project ->
36+ project.beforeEvaluate {
37+ project.layout.buildDirectory = new File("$dir/build")
38+ }
39+}
40+"""
41+
42+includeBuild(dir)
43--- a/packages/flutter_tools/gradle/build.gradle.kts
44+++ b/packages/flutter_tools/gradle/build.gradle.kts
45@@ -4,6 +4,11 @@
46
47 import org.jetbrains.kotlin.gradle.dsl.JvmTarget
48
49+// While flutter_tools runs Gradle with a --project-cache-dir, this startParameter
50+// is not passed correctly to the Kotlin Gradle plugin for some reason, and so
51+// must be set here as well.
52+gradle.startParameter.projectCacheDir = layout.buildDirectory.dir("cache").get().asFile
53+
54 plugins {
55 `java-gradle-plugin`
56 groovy
57--- a/packages/flutter_tools/lib/src/android/gradle.dart
58+++ b/packages/flutter_tools/lib/src/android/gradle.dart
59@@ -474,9 +474,9 @@ class AndroidGradleBuilder implements AndroidBuilder {
60 // from the local.properties file.
61 updateLocalProperties(project: project, buildInfo: androidBuildInfo.buildInfo);
62
63- final options = <String>[];
64-
65- final String gradleExecutablePath = _gradleUtils.getExecutable(project);
66+ final [String gradleExecutablePath, ...List<String> options] = _gradleUtils.getExecutable(
67+ project,
68+ );
69
70 // All automatically created files should exist.
71 if (configOnly) {
72@@ -797,7 +797,7 @@ class AndroidGradleBuilder implements AndroidBuilder {
73 'aar_init_script.gradle',
74 );
75 final command = <String>[
76- _gradleUtils.getExecutable(project),
77+ ..._gradleUtils.getExecutable(project),
78 '-I=$initScript',
79 '-Pflutter-root=$flutterRoot',
80 '-Poutput-dir=${outputDirectory.path}',
81@@ -912,6 +912,10 @@ class AndroidGradleBuilder implements AndroidBuilder {
82 final results = <String>[];
83
84 try {
85+ final [String gradleExecutablePath, ...List<String> options] = _gradleUtils.getExecutable(
86+ project,
87+ );
88+
89 exitCode = await _runGradleTask(
90 _kBuildVariantTaskName,
91 preRunTask: () {
92@@ -927,10 +931,10 @@ class AndroidGradleBuilder implements AndroidBuilder {
93 ),
94 );
95 },
96- options: const <String>['-q'],
97+ options: <String>[...options, '-q'],
98 project: project,
99 localGradleErrors: gradleErrors,
100- gradleExecutablePath: _gradleUtils.getExecutable(project),
101+ gradleExecutablePath: gradleExecutablePath,
102 outputParser: (String line) {
103 if (_kBuildVariantRegex.firstMatch(line) case final RegExpMatch match) {
104 results.add(match.namedGroup(_kBuildVariantRegexGroupName)!);
105@@ -964,6 +968,10 @@ class AndroidGradleBuilder implements AndroidBuilder {
106 late Stopwatch sw;
107 var exitCode = 1;
108 try {
109+ final [String gradleExecutablePath, ...List<String> options] = _gradleUtils.getExecutable(
110+ project,
111+ );
112+
113 exitCode = await _runGradleTask(
114 taskName,
115 preRunTask: () {
116@@ -979,10 +987,10 @@ class AndroidGradleBuilder implements AndroidBuilder {
117 ),
118 );
119 },
120- options: <String>['-q', '-PoutputPath=$outputPath'],
121+ options: <String>[...options, '-q', '-PoutputPath=$outputPath'],
122 project: project,
123 localGradleErrors: gradleErrors,
124- gradleExecutablePath: _gradleUtils.getExecutable(project),
125+ gradleExecutablePath: gradleExecutablePath,
126 );
127 } on Error catch (error) {
128 _logger.printError(error.toString());
129--- a/packages/flutter_tools/lib/src/android/gradle_errors.dart
130+++ b/packages/flutter_tools/lib/src/android/gradle_errors.dart
131@@ -228,7 +228,12 @@ final flavorUndefinedHandler = GradleHandledError(
132 },
133 handler: ({required String line, required FlutterProject project, required bool usesAndroidX}) async {
134 final RunResult tasksRunResult = await globals.processUtils.run(
135- <String>[globals.gradleUtils!.getExecutable(project), 'app:tasks', '--all', '--console=auto'],
136+ <String>[
137+ ...globals.gradleUtils!.getExecutable(project),
138+ 'app:tasks',
139+ '--all',
140+ '--console=auto',
141+ ],
142 throwOnError: true,
143 workingDirectory: project.android.hostAppGradleRoot.path,
144 environment: globals.java?.environment,
145--- a/packages/flutter_tools/lib/src/android/gradle_utils.dart
146+++ b/packages/flutter_tools/lib/src/android/gradle_utils.dart
147@@ -3,6 +3,7 @@
148 // found in the LICENSE file.
149
150 import 'package:meta/meta.dart';
151+import 'package:path/path.dart';
152 import 'package:process/process.dart';
153 import 'package:unified_analytics/unified_analytics.dart';
154
155@@ -197,9 +198,29 @@ class GradleUtils {
156 final Logger _logger;
157 final OperatingSystemUtils _operatingSystemUtils;
158
159+ List<String> get _requiredArguments {
160+ final String cacheDir = join(
161+ switch (globals.platform.environment['XDG_CACHE_HOME']) {
162+ final String cacheHome => cacheHome,
163+ _ => join(
164+ globals.fsUtils.homeDirPath ?? throwToolExit('No cache directory has been specified.'),
165+ '.cache',
166+ ),
167+ },
168+ 'flutter',
169+ 'nix-flutter-tools-gradle',
170+ globals.flutterVersion.engineRevision.substring(0, 10),
171+ );
172+
173+ return <String>[
174+ '--project-cache-dir=${join(cacheDir, 'cache')}',
175+ '-Pkotlin.project.persistent.dir=${join(cacheDir, 'kotlin')}',
176+ ];
177+ }
178+
179 /// Gets the Gradle executable path and prepares the Gradle project.
180 /// This is the `gradlew` or `gradlew.bat` script in the `android/` directory.
181- String getExecutable(FlutterProject project) {
182+ List<String> getExecutable(FlutterProject project) {
183 final Directory androidDir = project.android.hostAppGradleRoot;
184 injectGradleWrapperIfNeeded(androidDir);
185
186@@ -210,7 +231,7 @@ class GradleUtils {
187 // If the Gradle executable doesn't have execute permission,
188 // then attempt to set it.
189 _operatingSystemUtils.makeExecutable(gradle);
190- return gradle.absolute.path;
191+ return <String>[gradle.absolute.path, ..._requiredArguments];
192 }
193 throwToolExit(
194 'Unable to locate gradlew script. Please check that ${gradle.path} '
195--- a/packages/flutter_tools/test/general.shard/android/android_gradle_builder_test.dart
196+++ b/packages/flutter_tools/test/general.shard/android/android_gradle_builder_test.dart
197@@ -2606,8 +2606,8 @@ Gradle Crashed
198
199 class FakeGradleUtils extends Fake implements GradleUtils {
200 @override
201- String getExecutable(FlutterProject project) {
202- return 'gradlew';
203+ List<String> getExecutable(FlutterProject project) {
204+ return const <String>['gradlew'];
205 }
206 }
207
208--- a/packages/flutter_tools/test/general.shard/android/gradle_errors_test.dart
209+++ b/packages/flutter_tools/test/general.shard/android/gradle_errors_test.dart
210@@ -1633,8 +1633,8 @@ Platform fakePlatform(String name) {
211
212 class FakeGradleUtils extends Fake implements GradleUtils {
213 @override
214- String getExecutable(FlutterProject project) {
215- return 'gradlew';
216+ List<String> getExecutable(FlutterProject project) {
217+ return const <String>['gradlew'];
218 }
219 }
220