1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
---|---|
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | import 'package:meta/meta.dart'; |
6 | import 'package:xml/xml.dart'; |
7 | import 'package:yaml/yaml.dart'; |
8 | |
9 | import '../src/convert.dart'; |
10 | import 'android/android_builder.dart'; |
11 | import 'android/gradle_utils.dart' as gradle; |
12 | import 'base/common.dart'; |
13 | import 'base/error_handling_io.dart'; |
14 | import 'base/file_system.dart'; |
15 | import 'base/logger.dart'; |
16 | import 'base/utils.dart'; |
17 | import 'base/version.dart'; |
18 | import 'base/yaml.dart'; |
19 | import 'bundle.dart' as bundle; |
20 | import 'cmake_project.dart'; |
21 | import 'dart/package_map.dart'; |
22 | import 'features.dart'; |
23 | import 'flutter_manifest.dart'; |
24 | import 'flutter_plugins.dart'; |
25 | import 'globals.dart' as globals; |
26 | import 'platform_plugins.dart'; |
27 | import 'project_validator_result.dart'; |
28 | import 'template.dart'; |
29 | import 'xcode_project.dart'; |
30 | |
31 | export 'cmake_project.dart'; |
32 | export 'xcode_project.dart'; |
33 | |
34 | /// Enum for each officially supported platform. |
35 | enum SupportedPlatform { |
36 | android(name: 'android'), |
37 | ios(name: 'ios'), |
38 | linux(name: 'linux'), |
39 | macos(name: 'macos'), |
40 | web(name: 'web'), |
41 | windows(name: 'windows'), |
42 | fuchsia(name: 'fuchsia'), |
43 | root(name: 'root'); // Special platform to represent the root project directory |
44 | |
45 | const SupportedPlatform({required this.name}); |
46 | |
47 | final String name; |
48 | } |
49 | |
50 | class FlutterProjectFactory { |
51 | FlutterProjectFactory({required Logger logger, required FileSystem fileSystem}) |
52 | : _logger = logger, |
53 | _fileSystem = fileSystem; |
54 | |
55 | final Logger _logger; |
56 | final FileSystem _fileSystem; |
57 | |
58 | @visibleForTesting |
59 | final Map<String, FlutterProject> projects = <String, FlutterProject>{}; |
60 | |
61 | /// Returns a [FlutterProject] view of the given directory or a ToolExit error, |
62 | /// if `pubspec.yaml` or `example/pubspec.yaml` is invalid. |
63 | FlutterProject fromDirectory(Directory directory) { |
64 | return projects.putIfAbsent(directory.path, () { |
65 | final FlutterManifest manifest = FlutterProject._readManifest( |
66 | directory.childFile(bundle.defaultManifestPath).path, |
67 | logger: _logger, |
68 | fileSystem: _fileSystem, |
69 | ); |
70 | final FlutterManifest exampleManifest = FlutterProject._readManifest( |
71 | FlutterProject._exampleDirectory(directory).childFile(bundle.defaultManifestPath).path, |
72 | logger: _logger, |
73 | fileSystem: _fileSystem, |
74 | ); |
75 | return FlutterProject(directory, manifest, exampleManifest); |
76 | }); |
77 | } |
78 | } |
79 | |
80 | /// Represents the contents of a Flutter project at the specified [directory]. |
81 | /// |
82 | /// [FlutterManifest] information is read from `pubspec.yaml` and |
83 | /// `example/pubspec.yaml` files on construction of a [FlutterProject] instance. |
84 | /// The constructed instance carries an immutable snapshot representation of the |
85 | /// presence and content of those files. Accordingly, [FlutterProject] instances |
86 | /// should be discarded upon changes to the `pubspec.yaml` files, but can be |
87 | /// used across changes to other files, as no other file-level information is |
88 | /// cached. |
89 | class FlutterProject { |
90 | @visibleForTesting |
91 | FlutterProject(this.directory, this._manifest, this._exampleManifest); |
92 | |
93 | /// Returns a [FlutterProject] view of the given directory or a ToolExit error, |
94 | /// if `pubspec.yaml` or `example/pubspec.yaml` is invalid. |
95 | static FlutterProject fromDirectory(Directory directory) => |
96 | globals.projectFactory.fromDirectory(directory); |
97 | |
98 | /// Returns a [FlutterProject] view of the current directory or a ToolExit error, |
99 | /// if `pubspec.yaml` or `example/pubspec.yaml` is invalid. |
100 | static FlutterProject current() => |
101 | globals.projectFactory.fromDirectory(globals.fs.currentDirectory); |
102 | |
103 | /// Create a [FlutterProject] and bypass the project caching. |
104 | @visibleForTesting |
105 | static FlutterProject fromDirectoryTest(Directory directory, [Logger? logger]) { |
106 | final FileSystem fileSystem = directory.fileSystem; |
107 | logger ??= BufferLogger.test(); |
108 | final FlutterManifest manifest = FlutterProject._readManifest( |
109 | directory.childFile(bundle.defaultManifestPath).path, |
110 | logger: logger, |
111 | fileSystem: fileSystem, |
112 | ); |
113 | final FlutterManifest exampleManifest = FlutterProject._readManifest( |
114 | FlutterProject._exampleDirectory(directory).childFile(bundle.defaultManifestPath).path, |
115 | logger: logger, |
116 | fileSystem: fileSystem, |
117 | ); |
118 | return FlutterProject(directory, manifest, exampleManifest); |
119 | } |
120 | |
121 | /// The location of this project. |
122 | final Directory directory; |
123 | |
124 | /// The location of the build folder. |
125 | Directory get buildDirectory => directory.childDirectory('build'); |
126 | |
127 | /// The manifest of this project. |
128 | FlutterManifest get manifest => _manifest; |
129 | late FlutterManifest _manifest; |
130 | |
131 | /// The manifest of the example sub-project of this project. |
132 | final FlutterManifest _exampleManifest; |
133 | |
134 | /// List of [FlutterProject]s corresponding to the workspace entries. |
135 | List<FlutterProject> get workspaceProjects => |
136 | manifest.workspace |
137 | .map( |
138 | (String entry) => FlutterProject.fromDirectory( |
139 | directory.childDirectory(directory.fileSystem.path.normalize(entry)), |
140 | ), |
141 | ) |
142 | .toList(); |
143 | |
144 | /// The set of organization names found in this project as |
145 | /// part of iOS product bundle identifier, Android application ID, or |
146 | /// Gradle group ID. |
147 | Future<Set<String>> get organizationNames async { |
148 | final List<String> candidates = <String>[]; |
149 | |
150 | if (ios.existsSync()) { |
151 | // Don't require iOS build info, this method is only |
152 | // used during create as best-effort, use the |
153 | // default target bundle identifier. |
154 | try { |
155 | final String? bundleIdentifier = await ios.productBundleIdentifier(null); |
156 | if (bundleIdentifier != null) { |
157 | candidates.add(bundleIdentifier); |
158 | } |
159 | } on ToolExit { |
160 | // It's possible that while parsing the build info for the ios project |
161 | // that the bundleIdentifier can't be resolve. However, we would like |
162 | // skip parsing that id in favor of searching in other place. We can |
163 | // consider a tool exit in this case to be non fatal for the program. |
164 | } |
165 | } |
166 | if (android.existsSync()) { |
167 | final String? applicationId = android.applicationId; |
168 | final String? group = android.group; |
169 | candidates.addAll(<String>[ |
170 | if (applicationId != null) applicationId, |
171 | if (group != null) group, |
172 | ]); |
173 | } |
174 | if (example.android.existsSync()) { |
175 | final String? applicationId = example.android.applicationId; |
176 | if (applicationId != null) { |
177 | candidates.add(applicationId); |
178 | } |
179 | } |
180 | if (example.ios.existsSync()) { |
181 | final String? bundleIdentifier = await example.ios.productBundleIdentifier(null); |
182 | if (bundleIdentifier != null) { |
183 | candidates.add(bundleIdentifier); |
184 | } |
185 | } |
186 | return Set<String>.of( |
187 | candidates.map<String?>(_organizationNameFromPackageName).whereType<String>(), |
188 | ); |
189 | } |
190 | |
191 | String? _organizationNameFromPackageName(String packageName) { |
192 | if (0 <= packageName.lastIndexOf('.')) { |
193 | return packageName.substring(0, packageName.lastIndexOf('.')); |
194 | } |
195 | return null; |
196 | } |
197 | |
198 | /// The iOS sub project of this project. |
199 | late final IosProject ios = IosProject.fromFlutter(this); |
200 | |
201 | /// The Android sub project of this project. |
202 | late final AndroidProject android = AndroidProject._(this); |
203 | |
204 | /// The web sub project of this project. |
205 | late final WebProject web = WebProject._(this); |
206 | |
207 | /// The MacOS sub project of this project. |
208 | late final MacOSProject macos = MacOSProject.fromFlutter(this); |
209 | |
210 | /// The Linux sub project of this project. |
211 | late final LinuxProject linux = LinuxProject.fromFlutter(this); |
212 | |
213 | /// The Windows sub project of this project. |
214 | late final WindowsProject windows = WindowsProject.fromFlutter(this); |
215 | |
216 | /// The Fuchsia sub project of this project. |
217 | late final FuchsiaProject fuchsia = FuchsiaProject._(this); |
218 | |
219 | /// The `pubspec.yaml` file of this project. |
220 | File get pubspecFile => directory.childFile('pubspec.yaml'); |
221 | |
222 | /// The `.metadata` file of this project. |
223 | File get metadataFile => directory.childFile('.metadata'); |
224 | |
225 | /// The `.flutter-plugins` file of this project. |
226 | File get flutterPluginsFile => directory.childFile('.flutter-plugins'); |
227 | |
228 | /// The `.flutter-plugins-dependencies` file of this project, |
229 | /// which contains the dependencies each plugin depends on. |
230 | File get flutterPluginsDependenciesFile => directory.childFile('.flutter-plugins-dependencies'); |
231 | |
232 | /// The `.gitignore` file of this project. |
233 | File get gitignoreFile => directory.childFile('.gitignore'); |
234 | |
235 | File get packageConfig => findPackageConfigFileOrDefault(directory); |
236 | |
237 | /// The `.dart-tool` directory of this project. |
238 | Directory get dartTool => directory.childDirectory('.dart_tool'); |
239 | |
240 | /// The location of the generated scaffolding project for hosting widget |
241 | /// previews from this project. |
242 | Directory get widgetPreviewScaffold => dartTool.childDirectory('widget_preview_scaffold'); |
243 | |
244 | /// The directory containing the generated code for this project. |
245 | Directory get generated => directory.absolute |
246 | .childDirectory('.dart_tool') |
247 | .childDirectory('build') |
248 | .childDirectory('generated') |
249 | .childDirectory(manifest.appName); |
250 | |
251 | /// The generated Dart plugin registrant for non-web platforms. |
252 | File get dartPluginRegistrant => |
253 | dartTool.childDirectory('flutter_build').childFile( 'dart_plugin_registrant.dart'); |
254 | |
255 | /// The example sub-project of this project. |
256 | FlutterProject get example => FlutterProject( |
257 | _exampleDirectory(directory), |
258 | _exampleManifest, |
259 | FlutterManifest.empty(logger: globals.logger), |
260 | ); |
261 | |
262 | /// The generated scaffolding project for hosting widget previews from this |
263 | /// project. |
264 | late final FlutterProject widgetPreviewScaffoldProject = FlutterProject.fromDirectory( |
265 | widgetPreviewScaffold, |
266 | ); |
267 | |
268 | /// True if this project is a Flutter module project. |
269 | bool get isModule => manifest.isModule; |
270 | |
271 | /// True if this project is a Flutter plugin project. |
272 | bool get isPlugin => manifest.isPlugin; |
273 | |
274 | /// True if the Flutter project is using the AndroidX support library. |
275 | bool get usesAndroidX => manifest.usesAndroidX; |
276 | |
277 | /// True if this project has an example application. |
278 | bool get hasExampleApp => _exampleDirectory(directory).existsSync(); |
279 | |
280 | /// Returns a list of platform names that are supported by the project. |
281 | List<SupportedPlatform> getSupportedPlatforms({bool includeRoot = false}) { |
282 | return <SupportedPlatform>[ |
283 | if (includeRoot) SupportedPlatform.root, |
284 | if (android.existsSync()) SupportedPlatform.android, |
285 | if (ios.exists) SupportedPlatform.ios, |
286 | if (web.existsSync()) SupportedPlatform.web, |
287 | if (macos.existsSync()) SupportedPlatform.macos, |
288 | if (linux.existsSync()) SupportedPlatform.linux, |
289 | if (windows.existsSync()) SupportedPlatform.windows, |
290 | if (fuchsia.existsSync()) SupportedPlatform.fuchsia, |
291 | ]; |
292 | } |
293 | |
294 | /// The directory that will contain the example if an example exists. |
295 | static Directory _exampleDirectory(Directory directory) => directory.childDirectory('example'); |
296 | |
297 | /// Reads and validates the `pubspec.yaml` file at [path], asynchronously |
298 | /// returning a [FlutterManifest] representation of the contents. |
299 | /// |
300 | /// Completes with an empty [FlutterManifest], if the file does not exist. |
301 | /// Completes with a ToolExit on validation error. |
302 | static FlutterManifest _readManifest( |
303 | String path, { |
304 | required Logger logger, |
305 | required FileSystem fileSystem, |
306 | }) { |
307 | FlutterManifest? manifest; |
308 | try { |
309 | manifest = FlutterManifest.createFromPath(path, logger: logger, fileSystem: fileSystem); |
310 | } on YamlException catch (e) { |
311 | logger.printStatus('Error detected in pubspec.yaml:', emphasis: true); |
312 | logger.printError('$e '); |
313 | } on FormatException catch (e) { |
314 | logger.printError('Error detected while parsing pubspec.yaml:', emphasis: true); |
315 | logger.printError('$e '); |
316 | } on FileSystemException catch (e) { |
317 | logger.printError('Error detected while reading pubspec.yaml:', emphasis: true); |
318 | logger.printError('$e '); |
319 | } |
320 | if (manifest == null) { |
321 | throwToolExit('Please correct the pubspec.yaml file at$path '); |
322 | } |
323 | return manifest; |
324 | } |
325 | |
326 | /// Replaces the content of [pubspecFile] with the contents of [updated] and |
327 | /// sets [manifest] to the [updated] manifest. |
328 | void replacePubspec(FlutterManifest updated) { |
329 | final YamlMap updatedPubspecContents = updated.toYaml(); |
330 | pubspecFile.writeAsStringSync(encodeYamlAsString(updatedPubspecContents)); |
331 | _manifest = updated; |
332 | } |
333 | |
334 | /// Reapplies template files and regenerates project files and plugin |
335 | /// registrants for app and module projects only. |
336 | /// |
337 | /// Will not create project platform directories if they do not already exist. |
338 | /// |
339 | /// If [releaseMode] is `true`, platform-specific tooling and metadata generated |
340 | /// may apply optimizations or changes that are only specific to release builds, |
341 | /// such as not including dev-only dependencies. |
342 | Future<void> regeneratePlatformSpecificTooling({ |
343 | DeprecationBehavior deprecationBehavior = DeprecationBehavior.none, |
344 | required bool releaseMode, |
345 | }) async { |
346 | return ensureReadyForPlatformSpecificTooling( |
347 | androidPlatform: android.existsSync(), |
348 | iosPlatform: ios.existsSync(), |
349 | // TODO(stuartmorgan): Revisit the conditions here once the plans for handling |
350 | // desktop in existing projects are in place. |
351 | linuxPlatform: featureFlags.isLinuxEnabled && linux.existsSync(), |
352 | macOSPlatform: featureFlags.isMacOSEnabled && macos.existsSync(), |
353 | windowsPlatform: featureFlags.isWindowsEnabled && windows.existsSync(), |
354 | webPlatform: featureFlags.isWebEnabled && web.existsSync(), |
355 | deprecationBehavior: deprecationBehavior, |
356 | releaseMode: releaseMode, |
357 | ); |
358 | } |
359 | |
360 | /// Applies template files and generates project files and plugin |
361 | /// registrants for app and module projects only for the specified platforms. |
362 | /// |
363 | /// If [releaseMode] is `true`, platform-specific tooling and metadata generated |
364 | /// may apply optimizations or changes that are only specific to release builds, |
365 | /// such as not including dev-only dependencies. |
366 | Future<void> ensureReadyForPlatformSpecificTooling({ |
367 | required bool releaseMode, |
368 | bool androidPlatform = false, |
369 | bool iosPlatform = false, |
370 | bool linuxPlatform = false, |
371 | bool macOSPlatform = false, |
372 | bool windowsPlatform = false, |
373 | bool webPlatform = false, |
374 | DeprecationBehavior deprecationBehavior = DeprecationBehavior.none, |
375 | }) async { |
376 | if (!directory.existsSync() || isPlugin) { |
377 | return; |
378 | } |
379 | await refreshPluginsList(this, iosPlatform: iosPlatform, macOSPlatform: macOSPlatform); |
380 | if (androidPlatform) { |
381 | await android.ensureReadyForPlatformSpecificTooling(deprecationBehavior: deprecationBehavior); |
382 | } |
383 | if (iosPlatform) { |
384 | await ios.ensureReadyForPlatformSpecificTooling(); |
385 | } |
386 | if (linuxPlatform) { |
387 | await linux.ensureReadyForPlatformSpecificTooling(); |
388 | } |
389 | if (macOSPlatform) { |
390 | await macos.ensureReadyForPlatformSpecificTooling(); |
391 | } |
392 | if (windowsPlatform) { |
393 | await windows.ensureReadyForPlatformSpecificTooling(); |
394 | } |
395 | if (webPlatform) { |
396 | await web.ensureReadyForPlatformSpecificTooling(); |
397 | } |
398 | await injectPlugins( |
399 | this, |
400 | androidPlatform: androidPlatform, |
401 | iosPlatform: iosPlatform, |
402 | linuxPlatform: linuxPlatform, |
403 | macOSPlatform: macOSPlatform, |
404 | windowsPlatform: windowsPlatform, |
405 | releaseMode: releaseMode, |
406 | ); |
407 | } |
408 | |
409 | void checkForDeprecation({DeprecationBehavior deprecationBehavior = DeprecationBehavior.none}) { |
410 | if (android.existsSync() && pubspecFile.existsSync()) { |
411 | android.checkForDeprecation(deprecationBehavior: deprecationBehavior); |
412 | } |
413 | } |
414 | |
415 | /// Returns a json encoded string containing the [appName], [version], and [buildNumber] that is used to generate version.json |
416 | String getVersionInfo() { |
417 | final String? buildName = manifest.buildName; |
418 | final String? buildNumber = manifest.buildNumber; |
419 | final Map<String, String> versionFileJson = <String, String>{ |
420 | 'app_name': manifest.appName, |
421 | if (buildName != null) 'version': buildName, |
422 | if (buildNumber != null) 'build_number': buildNumber, |
423 | 'package_name': manifest.appName, |
424 | }; |
425 | return jsonEncode(versionFileJson); |
426 | } |
427 | } |
428 | |
429 | /// Base class for projects per platform. |
430 | abstract class FlutterProjectPlatform { |
431 | /// Plugin's platform config key, e.g., "macos", "ios". |
432 | String get pluginConfigKey; |
433 | |
434 | /// Whether the platform exists in the project. |
435 | bool existsSync(); |
436 | } |
437 | |
438 | /// Represents the Android sub-project of a Flutter project. |
439 | /// |
440 | /// Instances will reflect the contents of the `android/` sub-folder of |
441 | /// Flutter applications and the `.android/` sub-folder of Flutter module projects. |
442 | class AndroidProject extends FlutterProjectPlatform { |
443 | AndroidProject._(this.parent); |
444 | |
445 | // User facing string when java/gradle/agp versions are compatible. |
446 | @visibleForTesting |
447 | static const String validJavaGradleAgpString = 'compatible java/gradle/agp'; |
448 | |
449 | // User facing link that describes compatibility between gradle and |
450 | // android gradle plugin. |
451 | static const String gradleAgpCompatUrl = |
452 | 'https://developer.android.com/studio/releases/gradle-plugin#updating-gradle'; |
453 | |
454 | // User facing link that describes compatibility between java and the first |
455 | // version of gradle to support it. |
456 | static const String javaGradleCompatUrl = |
457 | 'https://docs.gradle.org/current/userguide/compatibility.html#java'; |
458 | |
459 | // User facing link that describes instructions for downloading |
460 | // the latest version of Android Studio. |
461 | static const String installAndroidStudioUrl ='https://developer.android.com/studio/install'; |
462 | |
463 | /// The parent of this project. |
464 | final FlutterProject parent; |
465 | |
466 | @override |
467 | String get pluginConfigKey => AndroidPlugin.kConfigKey; |
468 | |
469 | static final RegExp _androidNamespacePattern = RegExp( |
470 | 'android {[\\S\\s]+namespace\\s*=?\\s*[\'"](.+)[\'"]', |
471 | ); |
472 | static final RegExp _applicationIdPattern = RegExp( |
473 | '^\\s*applicationId\\s*=?\\s*[\'"](.*)[\'"]\\s*\$', |
474 | ); |
475 | static final RegExp _imperativeKotlinPluginPattern = RegExp( |
476 | '^\\s*apply plugin\\:\\s+[\'"]kotlin-android[\'"]\\s*\$', |
477 | ); |
478 | |
479 | /// Examples of strings that this regex matches: |
480 | /// - `id "kotlin-android"` |
481 | /// - `id("kotlin-android")` |
482 | /// - `id ( "kotlin-android" ) ` |
483 | /// - `id "org.jetbrains.kotlin.android"` |
484 | /// - `id("org.jetbrains.kotlin.android")` |
485 | /// - `id ( "org.jetbrains.kotlin.android" )` |
486 | static final List<RegExp> _declarativeKotlinPluginPatterns = <RegExp>[ |
487 | RegExp('^\\s*id\\s*\\(?\\s*[\'"]kotlin-android[\'"]\\s*\\)?\\s*\$'), |
488 | RegExp('^\\s*id\\s*\\(?\\s*[\'"]org.jetbrains.kotlin.android[\'"]\\s*\\)?\\s*\$'), |
489 | ]; |
490 | |
491 | /// Pattern used to find the assignment of the "group" property in Gradle. |
492 | /// Expected example: `group "dev.flutter.plugin"` |
493 | /// Regex is used in both Groovy and Kotlin Gradle files. |
494 | static final RegExp _groupPattern = RegExp('^\\s*group\\s*=?\\s*[\'"](.*)[\'"]\\s*\$'); |
495 | |
496 | /// The Gradle root directory of the Android host app. This is the directory |
497 | /// containing the `app/` subdirectory and the `settings.gradle` file that |
498 | /// includes it in the overall Gradle project. |
499 | Directory get hostAppGradleRoot { |
500 | if (!isModule || _editableHostAppDirectory.existsSync()) { |
501 | return _editableHostAppDirectory; |
502 | } |
503 | return ephemeralDirectory; |
504 | } |
505 | |
506 | /// The Gradle root directory of the Android wrapping of Flutter and plugins. |
507 | /// This is the same as [hostAppGradleRoot] except when the project is |
508 | /// a Flutter module with an editable host app. |
509 | Directory get _flutterLibGradleRoot => isModule ? ephemeralDirectory : _editableHostAppDirectory; |
510 | |
511 | Directory get ephemeralDirectory => parent.directory.childDirectory('.android'); |
512 | Directory get _editableHostAppDirectory => parent.directory.childDirectory('android'); |
513 | |
514 | /// True if the parent Flutter project is a module. |
515 | bool get isModule => parent.isModule; |
516 | |
517 | /// True if the parent Flutter project is a plugin. |
518 | bool get isPlugin => parent.isPlugin; |
519 | |
520 | /// True if the Flutter project is using the AndroidX support library. |
521 | bool get usesAndroidX => parent.usesAndroidX; |
522 | |
523 | /// Returns true if the current version of the Gradle plugin is supported. |
524 | late final bool isSupportedVersion = _computeSupportedVersion(); |
525 | |
526 | /// Gets all build variants of this project. |
527 | Future<List<String>> getBuildVariants() async { |
528 | if (!existsSync() || androidBuilder == null) { |
529 | return const <String>[]; |
530 | } |
531 | return androidBuilder!.getBuildVariants(project: parent); |
532 | } |
533 | |
534 | /// Outputs app link related settings into a json file. |
535 | /// |
536 | /// The return future resolves to the path of the json file. |
537 | /// |
538 | /// The future resolves to null if it fails to retrieve app link settings. |
539 | Future<String> outputsAppLinkSettings({required String variant}) async { |
540 | if (!existsSync() || androidBuilder == null) { |
541 | throwToolExit('Target directory$hostAppGradleRootis not an Android project'); |
542 | } |
543 | return androidBuilder!.outputsAppLinkSettings(variant, project: parent); |
544 | } |
545 | |
546 | bool _computeSupportedVersion() { |
547 | final FileSystem fileSystem = hostAppGradleRoot.fileSystem; |
548 | final File plugin = hostAppGradleRoot.childFile( |
549 | fileSystem.path.join('buildSrc','src','main','groovy','FlutterPlugin.groovy'), |
550 | ); |
551 | if (plugin.existsSync()) { |
552 | return false; |
553 | } |
554 | try { |
555 | for (final String line in appGradleFile.readAsLinesSync()) { |
556 | // This syntax corresponds to applying the Flutter Gradle Plugin with a |
557 | // script. |
558 | // See https://docs.gradle.org/current/userguide/plugins.html#sec:script_plugins. |
559 | final bool fileBasedApply = line.contains(RegExp(r'apply from: .*/flutter.gradle')); |
560 | |
561 | // This syntax corresponds to applying the Flutter Gradle Plugin using |
562 | // the declarative "plugins {}" block after including it in the |
563 | // pluginManagement block of the settings.gradle file. |
564 | // See https://docs.gradle.org/current/userguide/composite_builds.html#included_plugin_builds, |
565 | // as well as the settings.gradle and build.gradle templates. |
566 | final bool declarativeApply = line.contains( |
567 | RegExp(r'dev\.flutter\.(?:(?:flutter-gradle-plugin)|(?:`flutter-gradle-plugin`))'), |
568 | ); |
569 | |
570 | // This case allows for flutter run/build to work for modules. It does |
571 | // not guarantee the Flutter Gradle Plugin is applied. |
572 | final bool managed = line.contains(RegExp('def flutterPluginVersion = [\'"]managed[\'"]')); |
573 | if (fileBasedApply || declarativeApply || managed) { |
574 | return true; |
575 | } |
576 | } |
577 | } on FileSystemException { |
578 | return false; |
579 | } |
580 | return false; |
581 | } |
582 | |
583 | /// True, if the app project is using Kotlin. |
584 | bool get isKotlin { |
585 | final bool imperativeMatch = |
586 | firstMatchInFile(appGradleFile, _imperativeKotlinPluginPattern) != null; |
587 | final bool declarativeMatch = _declarativeKotlinPluginPatterns.any((RegExp pattern) { |
588 | return (firstMatchInFile(appGradleFile, pattern) != null); |
589 | }); |
590 | return imperativeMatch || declarativeMatch; |
591 | } |
592 | |
593 | /// Gets top-level Gradle build file. |
594 | /// See https://developer.android.com/build#top-level. |
595 | /// |
596 | /// The file must exist and it must be written in either Groovy (build.gradle) |
597 | /// or Kotlin (build.gradle.kts). |
598 | File get hostAppGradleFile { |
599 | return getGroovyOrKotlin(hostAppGradleRoot,'build.gradle'); |
600 | } |
601 | |
602 | /// Gets the project root level Gradle settings file. |
603 | /// |
604 | /// The file must exist and it must be written in either Groovy (build.gradle) |
605 | /// or Kotlin (build.gradle.kts). |
606 | File get settingsGradleFile { |
607 | return getGroovyOrKotlin(hostAppGradleRoot,'settings.gradle'); |
608 | } |
609 | |
610 | File getGroovyOrKotlin(Directory directory, String baseFilename) { |
611 | final File groovyFile = directory.childFile(baseFilename); |
612 | final File kotlinFile = directory.childFile('$baseFilename.kts'); |
613 | |
614 | if (groovyFile.existsSync()) { |
615 | // We mimic Gradle's behavior of preferring Groovy over Kotlin when both files exist. |
616 | return groovyFile; |
617 | } |
618 | if (kotlinFile.existsSync()) { |
619 | return kotlinFile; |
620 | } |
621 | |
622 | // TODO(bartekpacia): An exception should be thrown when neither |
623 | // the Groovy or Kotlin file exists, instead of falling back to the |
624 | // Groovy file. See #141180. |
625 | return groovyFile; |
626 | } |
627 | |
628 | /// Gets the module-level build.gradle file. |
629 | /// See https://developer.android.com/build#module-level. |
630 | /// |
631 | /// The file must exist and it must be written in either Groovy (build.gradle) |
632 | /// or Kotlin (build.gradle.kts). |
633 | File get appGradleFile { |
634 | final Directory appDir = hostAppGradleRoot.childDirectory('app'); |
635 | return getGroovyOrKotlin(appDir,'build.gradle'); |
636 | } |
637 | |
638 | File get appManifestFile { |
639 | if (isUsingGradle) { |
640 | return hostAppGradleRoot |
641 | .childDirectory('app') |
642 | .childDirectory('src') |
643 | .childDirectory('main') |
644 | .childFile('AndroidManifest.xml'); |
645 | } |
646 | |
647 | return hostAppGradleRoot.childFile('AndroidManifest.xml'); |
648 | } |
649 | |
650 | File get generatedPluginRegistrantFile { |
651 | return hostAppGradleRoot |
652 | .childDirectory('app') |
653 | .childDirectory('src') |
654 | .childDirectory('main') |
655 | .childDirectory('java') |
656 | .childDirectory('io') |
657 | .childDirectory('flutter') |
658 | .childDirectory('plugins') |
659 | .childFile('GeneratedPluginRegistrant.java'); |
660 | } |
661 | |
662 | File get gradleAppOutV1File => gradleAppOutV1Directory.childFile('app-debug.apk'); |
663 | |
664 | Directory get gradleAppOutV1Directory { |
665 | return globals.fs.directory( |
666 | globals.fs.path.join(hostAppGradleRoot.path,'app','build','outputs','apk'), |
667 | ); |
668 | } |
669 | |
670 | /// Whether the current flutter project has an Android sub-project. |
671 | @override |
672 | bool existsSync() { |
673 | return parent.isModule || _editableHostAppDirectory.existsSync(); |
674 | } |
675 | |
676 | /// Check if the versions of Java, Gradle and AGP are compatible. |
677 | /// |
678 | /// This is expected to be called from |
679 | /// flutter_tools/lib/src/project_validator.dart. |
680 | Future<ProjectValidatorResult> validateJavaAndGradleAgpVersions() async { |
681 | // Constructing ProjectValidatorResult happens here and not in |
682 | // flutter_tools/lib/src/project_validator.dart because of the additional |
683 | // Complexity of variable status values and error string formatting. |
684 | const String visibleName ='Java/Gradle/Android Gradle Plugin'; |
685 | final CompatibilityResult validJavaGradleAgpVersions = await hasValidJavaGradleAgpVersions(); |
686 | |
687 | return ProjectValidatorResult( |
688 | name: visibleName, |
689 | value: validJavaGradleAgpVersions.description, |
690 | status: |
691 | validJavaGradleAgpVersions.success |
692 | ? StatusProjectValidator.success |
693 | : StatusProjectValidator.error, |
694 | ); |
695 | } |
696 | |
697 | /// Ensures Java SDK is compatible with the project's Gradle version and |
698 | /// the project's Gradle version is compatible with the AGP version used |
699 | /// in build.gradle. |
700 | Future<CompatibilityResult> hasValidJavaGradleAgpVersions() async { |
701 | final String? gradleVersion = await gradle.getGradleVersion( |
702 | hostAppGradleRoot, |
703 | globals.logger, |
704 | globals.processManager, |
705 | ); |
706 | final String? agpVersion = gradle.getAgpVersion(hostAppGradleRoot, globals.logger); |
707 | final String? javaVersion = versionToParsableString(globals.java?.version); |
708 | |
709 | // Assume valid configuration. |
710 | String description = validJavaGradleAgpString; |
711 | |
712 | final bool compatibleGradleAgp = gradle.validateGradleAndAgp( |
713 | globals.logger, |
714 | gradleV: gradleVersion, |
715 | agpV: agpVersion, |
716 | ); |
717 | |
718 | final bool compatibleJavaGradle = gradle.validateJavaAndGradle( |
719 | globals.logger, |
720 | javaV: javaVersion, |
721 | gradleV: gradleVersion, |
722 | ); |
723 | |
724 | // Begin description formatting. |
725 | if (!compatibleGradleAgp) { |
726 | final String gradleDescription = |
727 | agpVersion != null |
728 | ?'Update Gradle to at least "${gradle.getGradleVersionFor(agpVersion)}".' |
729 | :''; |
730 | description =''' |
731 | Incompatible Gradle/AGP versions. \n |
732 | Gradle Version:$gradleVersion, AGP Version:$agpVersion |
733 | $gradleDescription\n |
734 | See the link below for more information: |
735 | $gradleAgpCompatUrl |
736 | '''; |
737 | } |
738 | if (!compatibleJavaGradle) { |
739 | // Should contain the agp error (if present) but not the valid String. |
740 | description =''' |
741 | ${compatibleGradleAgp ?'': description} |
742 | Incompatible Java/Gradle versions. |
743 | Java Version:$javaVersion, Gradle Version:$gradleVersion\n |
744 | See the link below for more information: |
745 | $javaGradleCompatUrl |
746 | '''; |
747 | } |
748 | return CompatibilityResult(compatibleJavaGradle && compatibleGradleAgp, description); |
749 | } |
750 | |
751 | bool get isUsingGradle { |
752 | return hostAppGradleFile.existsSync(); |
753 | } |
754 | |
755 | String? get applicationId { |
756 | return firstMatchInFile(appGradleFile, _applicationIdPattern)?.group(1); |
757 | } |
758 | |
759 | /// Get the namespace for newer Android projects, |
760 | /// which replaces the `package` attribute in the Manifest.xml. |
761 | String? get namespace { |
762 | try { |
763 | // firstMatchInFile() reads per line but `_androidNamespacePattern` matches a multiline pattern. |
764 | return _androidNamespacePattern.firstMatch(appGradleFile.readAsStringSync())?.group(1); |
765 | } on FileSystemException { |
766 | return null; |
767 | } |
768 | } |
769 | |
770 | String? get group { |
771 | return firstMatchInFile(hostAppGradleFile, _groupPattern)?.group(1); |
772 | } |
773 | |
774 | /// The build directory where the Android artifacts are placed. |
775 | Directory get buildDirectory { |
776 | return parent.buildDirectory; |
777 | } |
778 | |
779 | Future<void> ensureReadyForPlatformSpecificTooling({ |
780 | DeprecationBehavior deprecationBehavior = DeprecationBehavior.none, |
781 | }) async { |
782 | if (isModule && _shouldRegenerateFromTemplate()) { |
783 | await _regenerateLibrary(); |
784 | // Add ephemeral host app, if an editable host app does not already exist. |
785 | if (!_editableHostAppDirectory.existsSync()) { |
786 | await _overwriteFromTemplate( |
787 | globals.fs.path.join('module','android','host_app_common'), |
788 | ephemeralDirectory, |
789 | ); |
790 | await _overwriteFromTemplate( |
791 | globals.fs.path.join('module','android','host_app_ephemeral'), |
792 | ephemeralDirectory, |
793 | ); |
794 | } |
795 | } |
796 | if (!hostAppGradleRoot.existsSync()) { |
797 | return; |
798 | } |
799 | gradle.updateLocalProperties(project: parent, requireAndroidSdk: false); |
800 | } |
801 | |
802 | bool _shouldRegenerateFromTemplate() { |
803 | return globals.fsUtils.isOlderThanReference( |
804 | entity: ephemeralDirectory, |
805 | referenceFile: parent.pubspecFile, |
806 | ) || |
807 | globals.cache.isOlderThanToolsStamp(ephemeralDirectory); |
808 | } |
809 | |
810 | File get localPropertiesFile => _flutterLibGradleRoot.childFile('local.properties'); |
811 | |
812 | Directory get pluginRegistrantHost => |
813 | _flutterLibGradleRoot.childDirectory(isModule ?'Flutter':'app'); |
814 | |
815 | Future<void> _regenerateLibrary() async { |
816 | ErrorHandlingFileSystem.deleteIfExists(ephemeralDirectory, recursive: true); |
817 | await _overwriteFromTemplate( |
818 | globals.fs.path.join('module','android','library_new_embedding'), |
819 | ephemeralDirectory, |
820 | ); |
821 | await _overwriteFromTemplate( |
822 | globals.fs.path.join('module','android','gradle'), |
823 | ephemeralDirectory, |
824 | ); |
825 | globals.gradleUtils?.injectGradleWrapperIfNeeded(ephemeralDirectory); |
826 | } |
827 | |
828 | Future<void> _overwriteFromTemplate(String path, Directory target) async { |
829 | final Template template = await Template.fromName( |
830 | path, |
831 | fileSystem: globals.fs, |
832 | templateManifest: null, |
833 | logger: globals.logger, |
834 | templateRenderer: globals.templateRenderer, |
835 | ); |
836 | final String androidIdentifier = |
837 | parent.manifest.androidPackage ??'com.example.${parent.manifest.appName}'; |
838 | template.render(target, <String, Object>{ |
839 | 'android': true, |
840 | 'projectName': parent.manifest.appName, |
841 | 'androidIdentifier': androidIdentifier, |
842 | 'androidX': usesAndroidX, |
843 | 'agpVersion': gradle.templateAndroidGradlePluginVersion, |
844 | 'agpVersionForModule': gradle.templateAndroidGradlePluginVersionForModule, |
845 | 'kotlinVersion': gradle.templateKotlinGradlePluginVersion, |
846 | 'gradleVersion': gradle.templateDefaultGradleVersion, |
847 | 'compileSdkVersion': gradle.compileSdkVersion, |
848 | 'minSdkVersion': gradle.minSdkVersion, |
849 | 'ndkVersion': gradle.ndkVersion, |
850 | 'targetSdkVersion': gradle.targetSdkVersion, |
851 | }, printStatusWhenWriting: false); |
852 | } |
853 | |
854 | void checkForDeprecation({DeprecationBehavior deprecationBehavior = DeprecationBehavior.none}) { |
855 | if (deprecationBehavior == DeprecationBehavior.none) { |
856 | return; |
857 | } |
858 | final AndroidEmbeddingVersionResult result = computeEmbeddingVersion(); |
859 | if (result.version != AndroidEmbeddingVersion.v1) { |
860 | return; |
861 | } |
862 | // The v1 android embedding has been deleted. |
863 | throwToolExit('Build failed due to use of deleted Android v1 embedding.', exitCode: 1); |
864 | } |
865 | |
866 | AndroidEmbeddingVersion getEmbeddingVersion() { |
867 | final AndroidEmbeddingVersion androidEmbeddingVersion = computeEmbeddingVersion().version; |
868 | if (androidEmbeddingVersion == AndroidEmbeddingVersion.v1) { |
869 | throwToolExit('Build failed due to use of deleted Android v1 embedding.', exitCode: 1); |
870 | } |
871 | |
872 | return androidEmbeddingVersion; |
873 | } |
874 | |
875 | AndroidEmbeddingVersionResult computeEmbeddingVersion() { |
876 | if (isModule) { |
877 | // A module type's Android project is used in add-to-app scenarios and |
878 | // only supports the V2 embedding. |
879 | return AndroidEmbeddingVersionResult(AndroidEmbeddingVersion.v2,'Is add-to-app module'); |
880 | } |
881 | if (isPlugin) { |
882 | // Plugins do not use an appManifest, so we stop here. |
883 | // |
884 | // TODO(garyq): This method does not currently check for code references to |
885 | // the v1 embedding, we should check for this once removal is further along. |
886 | return AndroidEmbeddingVersionResult(AndroidEmbeddingVersion.v2,'Is plugin'); |
887 | } |
888 | if (!appManifestFile.existsSync()) { |
889 | return AndroidEmbeddingVersionResult( |
890 | AndroidEmbeddingVersion.v1, |
891 | 'No `${appManifestFile.absolute.path}` file', |
892 | ); |
893 | } |
894 | XmlDocument document; |
895 | try { |
896 | document = XmlDocument.parse(appManifestFile.readAsStringSync()); |
897 | } on XmlException { |
898 | throwToolExit( |
899 | 'Error parsing$appManifestFile' |
900 | 'Please ensure that the android manifest is a valid XML document and try again.', |
901 | ); |
902 | } on FileSystemException { |
903 | throwToolExit( |
904 | 'Error reading$appManifestFileeven though it exists. ' |
905 | 'Please ensure that you have read permission to this file and try again.', |
906 | ); |
907 | } |
908 | for (final XmlElement application in document.findAllElements('application')) { |
909 | final String? applicationName = application.getAttribute('android:name'); |
910 | if (applicationName =='io.flutter.app.FlutterApplication') { |
911 | return AndroidEmbeddingVersionResult( |
912 | AndroidEmbeddingVersion.v1, |
913 | '${appManifestFile.absolute.path}uses `android:name="io.flutter.app.FlutterApplication"`', |
914 | ); |
915 | } |
916 | } |
917 | for (final XmlElement metaData in document.findAllElements('meta-data')) { |
918 | final String? name = metaData.getAttribute('android:name'); |
919 | // External code checks for this string to identify flutter android apps. |
920 | // See cl/667760684 as an example. |
921 | if (name =='flutterEmbedding') { |
922 | final String? embeddingVersionString = metaData.getAttribute('android:value'); |
923 | if (embeddingVersionString =='1') { |
924 | return AndroidEmbeddingVersionResult( |
925 | AndroidEmbeddingVersion.v1, |
926 | '${appManifestFile.absolute.path}`<meta-data android:name="flutterEmbedding"` has value 1', |
927 | ); |
928 | } |
929 | if (embeddingVersionString =='2') { |
930 | return AndroidEmbeddingVersionResult( |
931 | AndroidEmbeddingVersion.v2, |
932 | '${appManifestFile.absolute.path}`<meta-data android:name="flutterEmbedding"` has value 2', |
933 | ); |
934 | } |
935 | } |
936 | } |
937 | return AndroidEmbeddingVersionResult( |
938 | AndroidEmbeddingVersion.v1, |
939 | 'No `<meta-data android:name="flutterEmbedding" android:value="2"/>` in${appManifestFile.absolute.path}', |
940 | ); |
941 | } |
942 | |
943 | static const bool _impellerEnabledByDefault = true; |
944 | |
945 | /// Returns the `io.flutter.embedding.android.EnableImpeller` manifest value. |
946 | /// |
947 | /// If there is no manifest file, or the key is not present, returns `false`. |
948 | bool computeImpellerEnabled() { |
949 | if (!appManifestFile.existsSync()) { |
950 | return _impellerEnabledByDefault; |
951 | } |
952 | final XmlDocument document; |
953 | try { |
954 | document = XmlDocument.parse(appManifestFile.readAsStringSync()); |
955 | } on XmlException { |
956 | throwToolExit( |
957 | 'Error parsing$appManifestFile' |
958 | 'Please ensure that the android manifest is a valid XML document and try again.', |
959 | ); |
960 | } on FileSystemException { |
961 | throwToolExit( |
962 | 'Error reading$appManifestFileeven though it exists. ' |
963 | 'Please ensure that you have read permission to this file and try again.', |
964 | ); |
965 | } |
966 | for (final XmlElement metaData in document.findAllElements('meta-data')) { |
967 | final String? name = metaData.getAttribute('android:name'); |
968 | if (name =='io.flutter.embedding.android.EnableImpeller') { |
969 | final String? value = metaData.getAttribute('android:value'); |
970 | if (value =='true') { |
971 | return true; |
972 | } |
973 | if (value =='false') { |
974 | return false; |
975 | } |
976 | } |
977 | } |
978 | return _impellerEnabledByDefault; |
979 | } |
980 | } |
981 | |
982 | /// Iteration of the embedding Java API in the engine used by the Android project. |
983 | enum AndroidEmbeddingVersion { |
984 | /// V1 APIs based on io.flutter.app.FlutterActivity. |
985 | v1, |
986 | |
987 | /// V2 APIs based on io.flutter.embedding.android.FlutterActivity. |
988 | v2, |
989 | } |
990 | |
991 | /// Data class that holds the results of checking for embedding version. |
992 | /// |
993 | /// This class includes the reason why a particular embedding was selected. |
994 | class AndroidEmbeddingVersionResult { |
995 | AndroidEmbeddingVersionResult(this.version, this.reason); |
996 | |
997 | /// The embedding version. |
998 | AndroidEmbeddingVersion version; |
999 | |
1000 | /// The reason why the embedding version was selected. |
1001 | String reason; |
1002 | } |
1003 | |
1004 | // What the tool should do when encountering deprecated API in applications. |
1005 | enum DeprecationBehavior { |
1006 | // The command being run does not care about deprecation status. |
1007 | none, |
1008 | // The command should continue and ignore the deprecation warning. |
1009 | ignore, |
1010 | // The command should exit the tool. |
1011 | exit, |
1012 | } |
1013 | |
1014 | /// Represents the web sub-project of a Flutter project. |
1015 | class WebProject extends FlutterProjectPlatform { |
1016 | WebProject._(this.parent); |
1017 | |
1018 | final FlutterProject parent; |
1019 | |
1020 | @override |
1021 | String get pluginConfigKey => WebPlugin.kConfigKey; |
1022 | |
1023 | /// Whether this flutter project has a web sub-project. |
1024 | @override |
1025 | bool existsSync() { |
1026 | return parent.directory.childDirectory('web').existsSync() && indexFile.existsSync(); |
1027 | } |
1028 | |
1029 | /// The 'lib' directory for the application. |
1030 | Directory get libDirectory => parent.directory.childDirectory('lib'); |
1031 | |
1032 | /// The directory containing additional files for the application. |
1033 | Directory get directory => parent.directory.childDirectory('web'); |
1034 | |
1035 | /// The html file used to host the flutter web application. |
1036 | File get indexFile => parent.directory.childDirectory('web').childFile('index.html'); |
1037 | |
1038 | /// The .dart_tool/dartpad directory |
1039 | Directory get dartpadToolDirectory => |
1040 | parent.directory.childDirectory('.dart_tool').childDirectory('dartpad'); |
1041 | |
1042 | Future<void> ensureReadyForPlatformSpecificTooling() async { |
1043 | /// Create .dart_tool/dartpad/web_plugin_registrant.dart. |
1044 | /// See: https://github.com/dart-lang/dart-services/pull/874 |
1045 | await injectBuildTimePluginFilesForWebPlatform(parent, destination: dartpadToolDirectory); |
1046 | } |
1047 | } |
1048 | |
1049 | /// The Fuchsia sub project. |
1050 | class FuchsiaProject { |
1051 | FuchsiaProject._(this.project); |
1052 | |
1053 | final FlutterProject project; |
1054 | |
1055 | Directory? _editableHostAppDirectory; |
1056 | Directory get editableHostAppDirectory => |
1057 | _editableHostAppDirectory ??= project.directory.childDirectory('fuchsia'); |
1058 | |
1059 | bool existsSync() => editableHostAppDirectory.existsSync(); |
1060 | |
1061 | Directory? _meta; |
1062 | Directory get meta => _meta ??= editableHostAppDirectory.childDirectory('meta'); |
1063 | } |
1064 | |
1065 | // Combines success and a description into one object that can be returned |
1066 | // together. |
1067 | @visibleForTesting |
1068 | class CompatibilityResult { |
1069 | CompatibilityResult(this.success, this.description); |
1070 | final bool success; |
1071 | final String description; |
1072 | } |
1073 | |
1074 | /// Converts a [Version] to a string that can be parsed by [Version.parse]. |
1075 | String? versionToParsableString(Version? version) { |
1076 | if (version == null) { |
1077 | return null; |
1078 | } |
1079 | |
1080 | return'${version.major}.${version.minor}.${version.patch}'; |
1081 | } |
1082 |
Definitions
- SupportedPlatform
- SupportedPlatform
- FlutterProjectFactory
- FlutterProjectFactory
- fromDirectory
- FlutterProject
- FlutterProject
- fromDirectory
- current
- fromDirectoryTest
- buildDirectory
- manifest
- workspaceProjects
- organizationNames
- _organizationNameFromPackageName
- pubspecFile
- metadataFile
- flutterPluginsFile
- flutterPluginsDependenciesFile
- gitignoreFile
- packageConfig
- dartTool
- widgetPreviewScaffold
- generated
- dartPluginRegistrant
- example
- isModule
- isPlugin
- usesAndroidX
- hasExampleApp
- getSupportedPlatforms
- _exampleDirectory
- _readManifest
- replacePubspec
- regeneratePlatformSpecificTooling
- ensureReadyForPlatformSpecificTooling
- checkForDeprecation
- getVersionInfo
- FlutterProjectPlatform
- pluginConfigKey
- existsSync
- AndroidProject
- _
- pluginConfigKey
- hostAppGradleRoot
- _flutterLibGradleRoot
- ephemeralDirectory
- _editableHostAppDirectory
- isModule
- isPlugin
- usesAndroidX
- getBuildVariants
- outputsAppLinkSettings
- _computeSupportedVersion
- isKotlin
- hostAppGradleFile
- settingsGradleFile
- getGroovyOrKotlin
- appGradleFile
- appManifestFile
- generatedPluginRegistrantFile
- gradleAppOutV1File
- gradleAppOutV1Directory
- existsSync
- validateJavaAndGradleAgpVersions
- hasValidJavaGradleAgpVersions
- isUsingGradle
- applicationId
- namespace
- group
- buildDirectory
- ensureReadyForPlatformSpecificTooling
- _shouldRegenerateFromTemplate
- localPropertiesFile
- pluginRegistrantHost
- _regenerateLibrary
- _overwriteFromTemplate
- checkForDeprecation
- getEmbeddingVersion
- computeEmbeddingVersion
- computeImpellerEnabled
- AndroidEmbeddingVersion
- AndroidEmbeddingVersionResult
- AndroidEmbeddingVersionResult
- DeprecationBehavior
- WebProject
- _
- pluginConfigKey
- existsSync
- libDirectory
- directory
- indexFile
- dartpadToolDirectory
- ensureReadyForPlatformSpecificTooling
- FuchsiaProject
- _
- editableHostAppDirectory
- existsSync
- meta
- CompatibilityResult
- CompatibilityResult
Learn more about Flutter for embedded and desktop on industrialflutter.com