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 'bundle.dart' as bundle;
19import 'cmake_project.dart';
20import 'features.dart';
21import 'flutter_manifest.dart';
22import 'flutter_plugins.dart';
23import 'globals.dart' as globals;
24import 'macos/xcode.dart';
25import 'platform_plugins.dart';
26import 'project_validator_result.dart';
27import 'template.dart';
28import 'xcode_project.dart';
29
30export 'cmake_project.dart';
31export 'xcode_project.dart';
32
33/// Enum for each officially supported platform.
34enum 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
51class 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.
95class 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.
426abstract 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.
439class 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 $hostAppGradleRoot is 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 = '''
688Incompatible Gradle/AGP versions. \n
689Gradle Version: $gradleVersion, AGP Version: $agpVersion
690$gradleDescription\n
691See 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}
699Incompatible Java/Gradle versions.
700Java Version: $javaVersion, Gradle Version: $gradleVersion\n
701See 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 $appManifestFile even 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 $appManifestFile even 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.
920enum 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.
930class 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.
941enum 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.
951class 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.
994class 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
1013class 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].
1020String? versionToParsableString(Version? version) {
1021 if (version == null) {
1022 return null;
1023 }
1024
1025 return '${version.major}.${version.minor}.${version.patch}';
1026}
1027

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com