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