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