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

Provided by KDAB

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