project.dart 28.8 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'package:meta/meta.dart';
Dan Field's avatar
Dan Field committed
6
import 'package:xml/xml.dart';
7
import 'package:yaml/yaml.dart';
8

9
import '../src/convert.dart';
10
import 'android/gradle_utils.dart' as gradle;
11
import 'base/common.dart';
12
import 'base/error_handling_io.dart';
13
import 'base/file_system.dart';
14
import 'base/logger.dart';
15
import 'base/utils.dart';
16
import 'bundle.dart' as bundle;
17
import 'cmake_project.dart';
18
import 'features.dart';
19
import 'flutter_manifest.dart';
20
import 'flutter_plugins.dart';
21
import 'globals.dart' as globals;
22
import 'platform_plugins.dart';
23
import 'reporting/reporting.dart';
24
import 'template.dart';
25 26 27 28
import 'xcode_project.dart';

export 'cmake_project.dart';
export 'xcode_project.dart';
29

30 31 32 33 34 35 36 37 38 39 40 41
/// Emum for each officially supported platform.
enum SupportedPlatform {
  android,
  ios,
  linux,
  macos,
  web,
  windows,
  fuchsia,
  root, // Special platform to represent the root project directory
}

42
class FlutterProjectFactory {
43
  FlutterProjectFactory({
44 45
    required Logger logger,
    required FileSystem fileSystem,
46 47 48 49 50
  }) : _logger = logger,
       _fileSystem = fileSystem;

  final Logger _logger;
  final FileSystem _fileSystem;
51

52 53
  @visibleForTesting
  final Map<String, FlutterProject> projects =
54
      <String, FlutterProject>{};
55 56 57 58 59

  /// Returns a [FlutterProject] view of the given directory or a ToolExit error,
  /// if `pubspec.yaml` or `example/pubspec.yaml` is invalid.
  FlutterProject fromDirectory(Directory directory) {
    assert(directory != null);
60
    return projects.putIfAbsent(directory.path, () {
61 62
      final FlutterManifest manifest = FlutterProject._readManifest(
        directory.childFile(bundle.defaultManifestPath).path,
63 64
        logger: _logger,
        fileSystem: _fileSystem,
65 66 67 68 69
      );
      final FlutterManifest exampleManifest = FlutterProject._readManifest(
        FlutterProject._exampleDirectory(directory)
            .childFile(bundle.defaultManifestPath)
            .path,
70 71
        logger: _logger,
        fileSystem: _fileSystem,
72 73 74
      );
      return FlutterProject(directory, manifest, exampleManifest);
    });
75 76 77
  }
}

78
/// Represents the contents of a Flutter project at the specified [directory].
79
///
80 81 82 83 84 85 86
/// [FlutterManifest] information is read from `pubspec.yaml` and
/// `example/pubspec.yaml` files on construction of a [FlutterProject] instance.
/// The constructed instance carries an immutable snapshot representation of the
/// presence and content of those files. Accordingly, [FlutterProject] instances
/// should be discarded upon changes to the `pubspec.yaml` files, but can be
/// used across changes to other files, as no other file-level information is
/// cached.
87
class FlutterProject {
88
  @visibleForTesting
89
  FlutterProject(this.directory, this.manifest, this._exampleManifest)
90 91 92
    : assert(directory != null),
      assert(manifest != null),
      assert(_exampleManifest != null);
93

94 95
  /// Returns a [FlutterProject] view of the given directory or a ToolExit error,
  /// if `pubspec.yaml` or `example/pubspec.yaml` is invalid.
96
  static FlutterProject fromDirectory(Directory directory) => globals.projectFactory.fromDirectory(directory);
97

98 99
  /// Returns a [FlutterProject] view of the current directory or a ToolExit error,
  /// if `pubspec.yaml` or `example/pubspec.yaml` is invalid.
100
  static FlutterProject current() => globals.projectFactory.fromDirectory(globals.fs.currentDirectory);
101

102 103
  /// Create a [FlutterProject] and bypass the project caching.
  @visibleForTesting
104
  static FlutterProject fromDirectoryTest(Directory directory, [Logger? logger]) {
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
    final FileSystem fileSystem = directory.fileSystem;
    logger ??= BufferLogger.test();
    final FlutterManifest manifest = FlutterProject._readManifest(
      directory.childFile(bundle.defaultManifestPath).path,
      logger: logger,
      fileSystem: fileSystem,
    );
    final FlutterManifest exampleManifest = FlutterProject._readManifest(
      FlutterProject._exampleDirectory(directory)
        .childFile(bundle.defaultManifestPath)
        .path,
      logger: logger,
      fileSystem: fileSystem,
    );
    return FlutterProject(directory, manifest, exampleManifest);
  }
121 122 123 124

  /// The location of this project.
  final Directory directory;

125
  /// The manifest of this project.
126 127
  final FlutterManifest manifest;

128
  /// The manifest of the example sub-project of this project.
129
  final FlutterManifest _exampleManifest;
130

131
  /// The set of organization names found in this project as
132 133
  /// part of iOS product bundle identifier, Android application ID, or
  /// Gradle group ID.
134
  Future<Set<String>> get organizationNames async {
135 136 137
    final List<String> candidates = <String>[];

    if (ios.existsSync()) {
138 139 140
      // Don't require iOS build info, this method is only
      // used during create as best-effort, use the
      // default target bundle identifier.
141
      final String? bundleIdentifier = await ios.productBundleIdentifier(null);
142 143 144 145 146
      if (bundleIdentifier != null) {
        candidates.add(bundleIdentifier);
      }
    }
    if (android.existsSync()) {
147 148
      final String? applicationId = android.applicationId;
      final String? group = android.group;
149 150 151 152 153 154 155 156
      candidates.addAll(<String>[
        if (applicationId != null)
          applicationId,
        if (group != null)
          group,
      ]);
    }
    if (example.android.existsSync()) {
157
      final String? applicationId = example.android.applicationId;
158 159 160 161 162
      if (applicationId != null) {
        candidates.add(applicationId);
      }
    }
    if (example.ios.existsSync()) {
163
      final String? bundleIdentifier = await example.ios.productBundleIdentifier(null);
164 165 166 167
      if (bundleIdentifier != null) {
        candidates.add(bundleIdentifier);
      }
    }
168
    return Set<String>.of(candidates.map<String?>(_organizationNameFromPackageName).whereType<String>());
169 170
  }

171
  String? _organizationNameFromPackageName(String packageName) {
172
    if (packageName != null && 0 <= packageName.lastIndexOf('.')) {
173
      return packageName.substring(0, packageName.lastIndexOf('.'));
174 175
    }
    return null;
176 177 178
  }

  /// The iOS sub project of this project.
179
  late final IosProject ios = IosProject.fromFlutter(this);
180 181

  /// The Android sub project of this project.
182
  late final AndroidProject android = AndroidProject._(this);
183

184
  /// The web sub project of this project.
185
  late final WebProject web = WebProject._(this);
186

187
  /// The MacOS sub project of this project.
188
  late final MacOSProject macos = MacOSProject.fromFlutter(this);
189

190
  /// The Linux sub project of this project.
191
  late final LinuxProject linux = LinuxProject.fromFlutter(this);
192

193
  /// The Windows sub project of this project.
194
  late final WindowsProject windows = WindowsProject.fromFlutter(this);
195 196

  /// The Fuchsia sub project of this project.
197
  late final FuchsiaProject fuchsia = FuchsiaProject._(this);
198

199 200 201 202 203 204
  /// The `pubspec.yaml` file of this project.
  File get pubspecFile => directory.childFile('pubspec.yaml');

  /// The `.packages` file of this project.
  File get packagesFile => directory.childFile('.packages');

205 206 207 208 209 210
  /// The `package_config.json` file of the project.
  ///
  /// This is the replacement for .packages which contains language
  /// version information.
  File get packageConfigFile => directory.childDirectory('.dart_tool').childFile('package_config.json');

211 212 213
  /// The `.metadata` file of this project.
  File get metadataFile => directory.childFile('.metadata');

214
  /// The `.flutter-plugins` file of this project.
215 216
  File get flutterPluginsFile => directory.childFile('.flutter-plugins');

217 218 219 220
  /// The `.flutter-plugins-dependencies` file of this project,
  /// which contains the dependencies each plugin depends on.
  File get flutterPluginsDependenciesFile => directory.childFile('.flutter-plugins-dependencies');

221 222 223
  /// The `.dart-tool` directory of this project.
  Directory get dartTool => directory.childDirectory('.dart_tool');

224 225
  /// The directory containing the generated code for this project.
  Directory get generated => directory
226
    .absolute
227 228 229 230 231
    .childDirectory('.dart_tool')
    .childDirectory('build')
    .childDirectory('generated')
    .childDirectory(manifest.appName);

232 233 234
  /// The generated Dart plugin registrant for non-web platforms.
  File get dartPluginRegistrant => dartTool
    .childDirectory('flutter_build')
235
    .childFile('dart_plugin_registrant.dart');
236

237
  /// The example sub-project of this project.
238
  FlutterProject get example => FlutterProject(
239 240
    _exampleDirectory(directory),
    _exampleManifest,
241
    FlutterManifest.empty(logger: globals.logger),
242 243
  );

244 245
  /// True if this project is a Flutter module project.
  bool get isModule => manifest.isModule;
246

247 248 249
  /// True if this project is a Flutter plugin project.
  bool get isPlugin => manifest.isPlugin;

250
  /// True if the Flutter project is using the AndroidX support library.
251 252
  bool get usesAndroidX => manifest.usesAndroidX;

253
  /// True if this project has an example application.
254
  bool get hasExampleApp => _exampleDirectory(directory).existsSync();
255

256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282
  /// Returns a list of platform names that are supported by the project.
  List<SupportedPlatform> getSupportedPlatforms({bool includeRoot = false}) {
    final List<SupportedPlatform> platforms = includeRoot ? <SupportedPlatform>[SupportedPlatform.root] : <SupportedPlatform>[];
    if (android.existsSync()) {
      platforms.add(SupportedPlatform.android);
    }
    if (ios.exists) {
      platforms.add(SupportedPlatform.ios);
    }
    if (web.existsSync()) {
      platforms.add(SupportedPlatform.web);
    }
    if (macos.existsSync()) {
      platforms.add(SupportedPlatform.macos);
    }
    if (linux.existsSync()) {
      platforms.add(SupportedPlatform.linux);
    }
    if (windows.existsSync()) {
      platforms.add(SupportedPlatform.windows);
    }
    if (fuchsia.existsSync()) {
      platforms.add(SupportedPlatform.fuchsia);
    }
    return platforms;
  }

283
  /// The directory that will contain the example if an example exists.
284
  static Directory _exampleDirectory(Directory directory) => directory.childDirectory('example');
285

286 287 288 289 290
  /// Reads and validates the `pubspec.yaml` file at [path], asynchronously
  /// returning a [FlutterManifest] representation of the contents.
  ///
  /// Completes with an empty [FlutterManifest], if the file does not exist.
  /// Completes with a ToolExit on validation error.
291
  static FlutterManifest _readManifest(String path, {
292 293
    required Logger logger,
    required FileSystem fileSystem,
294
  }) {
295
    FlutterManifest? manifest;
296
    try {
297 298 299 300 301
      manifest = FlutterManifest.createFromPath(
        path,
        logger: logger,
        fileSystem: fileSystem,
      );
302
    } on YamlException catch (e) {
303 304
      logger.printStatus('Error detected in pubspec.yaml:', emphasis: true);
      logger.printError('$e');
305 306 307 308 309 310
    } on FormatException catch (e) {
      logger.printError('Error detected while parsing pubspec.yaml:', emphasis: true);
      logger.printError('$e');
    } on FileSystemException catch (e) {
      logger.printError('Error detected while reading pubspec.yaml:', emphasis: true);
      logger.printError('$e');
311 312
    }
    if (manifest == null) {
313
      throwToolExit('Please correct the pubspec.yaml file at $path');
314
    }
315 316 317
    return manifest;
  }

318 319 320 321
  /// Reapplies template files and regenerates project files and plugin
  /// registrants for app and module projects only.
  ///
  /// Will not create project platform directories if they do not already exist.
322
  Future<void> regeneratePlatformSpecificTooling({DeprecationBehavior deprecationBehavior = DeprecationBehavior.none}) async {
323 324 325 326 327 328 329 330 331
    return ensureReadyForPlatformSpecificTooling(
      androidPlatform: android.existsSync(),
      iosPlatform: ios.existsSync(),
      // TODO(stuartmorgan): Revisit the conditions here once the plans for handling
      // desktop in existing projects are in place.
      linuxPlatform: featureFlags.isLinuxEnabled && linux.existsSync(),
      macOSPlatform: featureFlags.isMacOSEnabled && macos.existsSync(),
      windowsPlatform: featureFlags.isWindowsEnabled && windows.existsSync(),
      webPlatform: featureFlags.isWebEnabled && web.existsSync(),
332
      deprecationBehavior: deprecationBehavior,
333 334 335 336 337 338 339 340 341 342 343 344
    );
  }

  /// Applies template files and generates project files and plugin
  /// registrants for app and module projects only for the specified platforms.
  Future<void> ensureReadyForPlatformSpecificTooling({
    bool androidPlatform = false,
    bool iosPlatform = false,
    bool linuxPlatform = false,
    bool macOSPlatform = false,
    bool windowsPlatform = false,
    bool webPlatform = false,
345
    DeprecationBehavior deprecationBehavior = DeprecationBehavior.none,
346
  }) async {
347
    if (!directory.existsSync() || isPlugin) {
348
      return;
349
    }
350 351
    await refreshPluginsList(this, iosPlatform: iosPlatform, macOSPlatform: macOSPlatform);
    if (androidPlatform) {
352
      await android.ensureReadyForPlatformSpecificTooling(deprecationBehavior: deprecationBehavior);
353
    }
354
    if (iosPlatform) {
355 356
      await ios.ensureReadyForPlatformSpecificTooling();
    }
357
    if (linuxPlatform) {
358 359
      await linux.ensureReadyForPlatformSpecificTooling();
    }
360
    if (macOSPlatform) {
361 362
      await macos.ensureReadyForPlatformSpecificTooling();
    }
363
    if (windowsPlatform) {
364 365
      await windows.ensureReadyForPlatformSpecificTooling();
    }
366
    if (webPlatform) {
367 368
      await web.ensureReadyForPlatformSpecificTooling();
    }
369 370 371 372 373 374 375 376
    await injectPlugins(
      this,
      androidPlatform: androidPlatform,
      iosPlatform: iosPlatform,
      linuxPlatform: linuxPlatform,
      macOSPlatform: macOSPlatform,
      windowsPlatform: windowsPlatform,
    );
377
  }
378

379
  void checkForDeprecation({DeprecationBehavior deprecationBehavior = DeprecationBehavior.none}) {
380
    if (android.existsSync() && pubspecFile.existsSync()) {
381 382 383 384
      android.checkForDeprecation(deprecationBehavior: deprecationBehavior);
    }
  }

385 386
  /// Returns a json encoded string containing the [appName], [version], and [buildNumber] that is used to generate version.json
  String getVersionInfo()  {
387 388
    final String? buildName = manifest.buildName;
    final String? buildNumber = manifest.buildNumber;
389 390
    final Map<String, String> versionFileJson = <String, String>{
      'app_name': manifest.appName,
391 392 393 394
      if (buildName != null)
        'version': buildName,
      if (buildNumber != null)
        'build_number': buildNumber,
395
      'package_name': manifest.appName,
396 397 398
    };
    return jsonEncode(versionFileJson);
  }
399 400
}

401 402 403 404 405 406 407 408 409 410
/// Base class for projects per platform.
abstract class FlutterProjectPlatform {

  /// Plugin's platform config key, e.g., "macos", "ios".
  String get pluginConfigKey;

  /// Whether the platform exists in the project.
  bool existsSync();
}

411 412 413
/// Represents the Android sub-project of a Flutter project.
///
/// Instances will reflect the contents of the `android/` sub-folder of
414
/// Flutter applications and the `.android/` sub-folder of Flutter module projects.
415
class AndroidProject extends FlutterProjectPlatform {
416 417 418 419 420
  AndroidProject._(this.parent);

  /// The parent of this project.
  final FlutterProject parent;

421 422 423
  @override
  String get pluginConfigKey => AndroidPlugin.kConfigKey;

424
  static final RegExp _applicationIdPattern = RegExp('^\\s*applicationId\\s+[\'"](.*)[\'"]\\s*\$');
425
  static final RegExp _kotlinPluginPattern = RegExp('^\\s*apply plugin\\:\\s+[\'"]kotlin-android[\'"]\\s*\$');
426
  static final RegExp _groupPattern = RegExp('^\\s*group\\s+[\'"](.*)[\'"]\\s*\$');
427

428 429 430 431
  /// The Gradle root directory of the Android host app. This is the directory
  /// containing the `app/` subdirectory and the `settings.gradle` file that
  /// includes it in the overall Gradle project.
  Directory get hostAppGradleRoot {
432
    if (!isModule || _editableHostAppDirectory.existsSync()) {
433
      return _editableHostAppDirectory;
434
    }
435
    return ephemeralDirectory;
436 437 438 439
  }

  /// The Gradle root directory of the Android wrapping of Flutter and plugins.
  /// This is the same as [hostAppGradleRoot] except when the project is
440
  /// a Flutter module with an editable host app.
441
  Directory get _flutterLibGradleRoot => isModule ? ephemeralDirectory : _editableHostAppDirectory;
442

443
  Directory get ephemeralDirectory => parent.directory.childDirectory('.android');
444
  Directory get _editableHostAppDirectory => parent.directory.childDirectory('android');
445

446 447
  /// True if the parent Flutter project is a module.
  bool get isModule => parent.isModule;
448

449 450 451
  /// True if the parent Flutter project is a plugin.
  bool get isPlugin => parent.isPlugin;

452
  /// True if the Flutter project is using the AndroidX support library.
453 454
  bool get usesAndroidX => parent.usesAndroidX;

455
  /// Returns true if the current version of the Gradle plugin is supported.
456
  late final bool isSupportedVersion = _computeSupportedVersion();
457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478

  bool _computeSupportedVersion() {
    final FileSystem fileSystem = hostAppGradleRoot.fileSystem;
    final File plugin = hostAppGradleRoot.childFile(
        fileSystem.path.join('buildSrc', 'src', 'main', 'groovy', 'FlutterPlugin.groovy'));
    if (plugin.existsSync()) {
      return false;
    }
    final File appGradle = hostAppGradleRoot.childFile(
        fileSystem.path.join('app', 'build.gradle'));
    if (!appGradle.existsSync()) {
      return false;
    }
    for (final String line in appGradle.readAsLinesSync()) {
      if (line.contains(RegExp(r'apply from: .*/flutter.gradle')) ||
          line.contains("def flutterPluginVersion = 'managed'")) {
        return true;
      }
    }
    return false;
  }

479 480 481
  /// True, if the app project is using Kotlin.
  bool get isKotlin {
    final File gradleFile = hostAppGradleRoot.childDirectory('app').childFile('build.gradle');
482
    return firstMatchInFile(gradleFile, _kotlinPluginPattern) != null;
483 484
  }

485
  File get appManifestFile {
486
    return isUsingGradle
487
        ? globals.fs.file(globals.fs.path.join(hostAppGradleRoot.path, 'app', 'src', 'main', 'AndroidManifest.xml'))
488
        : hostAppGradleRoot.childFile('AndroidManifest.xml');
489 490
  }

491
  File get gradleAppOutV1File => gradleAppOutV1Directory.childFile('app-debug.apk');
492 493

  Directory get gradleAppOutV1Directory {
494
    return globals.fs.directory(globals.fs.path.join(hostAppGradleRoot.path, 'app', 'build', 'outputs', 'apk'));
495 496
  }

497
  /// Whether the current flutter project has an Android sub-project.
498
  @override
499 500 501 502
  bool existsSync() {
    return parent.isModule || _editableHostAppDirectory.existsSync();
  }

503
  bool get isUsingGradle {
504
    return hostAppGradleRoot.childFile('build.gradle').existsSync();
505
  }
506

507
  String? get applicationId {
508
    final File gradleFile = hostAppGradleRoot.childDirectory('app').childFile('build.gradle');
509
    return firstMatchInFile(gradleFile, _applicationIdPattern)?.group(1);
510 511
  }

512
  String? get group {
513
    final File gradleFile = hostAppGradleRoot.childFile('build.gradle');
514
    return firstMatchInFile(gradleFile, _groupPattern)?.group(1);
515
  }
516

517 518 519 520 521
  /// The build directory where the Android artifacts are placed.
  Directory get buildDirectory {
    return parent.directory.childDirectory('build');
  }

522
  Future<void> ensureReadyForPlatformSpecificTooling({DeprecationBehavior deprecationBehavior = DeprecationBehavior.none}) async {
523
    if (isModule && _shouldRegenerateFromTemplate()) {
524
      await _regenerateLibrary();
525 526
      // Add ephemeral host app, if an editable host app does not already exist.
      if (!_editableHostAppDirectory.existsSync()) {
527 528
        await _overwriteFromTemplate(globals.fs.path.join('module', 'android', 'host_app_common'), ephemeralDirectory);
        await _overwriteFromTemplate(globals.fs.path.join('module', 'android', 'host_app_ephemeral'), ephemeralDirectory);
529
      }
530
    }
531
    if (!hostAppGradleRoot.existsSync()) {
532
      return;
533 534
    }
    gradle.updateLocalProperties(project: parent, requireAndroidSdk: false);
535 536
  }

537
  bool _shouldRegenerateFromTemplate() {
538
    return globals.fsUtils.isOlderThanReference(
539 540 541
      entity: ephemeralDirectory,
      referenceFile: parent.pubspecFile,
    ) || globals.cache.isOlderThanToolsStamp(ephemeralDirectory);
542
  }
543

544 545
  File get localPropertiesFile => _flutterLibGradleRoot.childFile('local.properties');

546
  Directory get pluginRegistrantHost => _flutterLibGradleRoot.childDirectory(isModule ? 'Flutter' : 'app');
547

548
  Future<void> _regenerateLibrary() async {
549
    ErrorHandlingFileSystem.deleteIfExists(ephemeralDirectory, recursive: true);
550
    await _overwriteFromTemplate(globals.fs.path.join(
551 552
      'module',
      'android',
553
      'library_new_embedding',
554
    ), ephemeralDirectory);
555
    await _overwriteFromTemplate(globals.fs.path.join('module', 'android', 'gradle'), ephemeralDirectory);
556
    globals.gradleUtils?.injectGradleWrapperIfNeeded(ephemeralDirectory);
557
  }
558

559
  Future<void> _overwriteFromTemplate(String path, Directory target) async {
560 561 562 563 564 565 566
    final Template template = await Template.fromName(
      path,
      fileSystem: globals.fs,
      templateManifest: null,
      logger: globals.logger,
      templateRenderer: globals.templateRenderer,
    );
567
    final String androidIdentifier = parent.manifest.androidPackage ?? 'com.example.${parent.manifest.appName}';
568 569
    template.render(
      target,
570
      <String, Object>{
571
        'android': true,
572
        'projectName': parent.manifest.appName,
573
        'androidIdentifier': androidIdentifier,
574
        'androidX': usesAndroidX,
575 576 577
        'agpVersion': gradle.templateAndroidGradlePluginVersion,
        'kotlinVersion': gradle.templateKotlinGradlePluginVersion,
        'gradleVersion': gradle.templateDefaultGradleVersion,
578 579 580 581 582
        'gradleVersionForModule': gradle.templateDefaultGradleVersionForModule,
        'compileSdkVersion': gradle.compileSdkVersion,
        'minSdkVersion': gradle.minSdkVersion,
        'ndkVersion': gradle.ndkVersion,
        'targetSdkVersion': gradle.targetSdkVersion,
583 584 585 586
      },
      printStatusWhenWriting: false,
    );
  }
587

588
  void checkForDeprecation({DeprecationBehavior deprecationBehavior = DeprecationBehavior.none}) {
589 590 591
    if (deprecationBehavior == DeprecationBehavior.none) {
      return;
    }
592
    final AndroidEmbeddingVersionResult result = computeEmbeddingVersion();
593 594 595 596
    if (result.version != AndroidEmbeddingVersion.v1) {
      return;
    }
    globals.printStatus(
597 598 599 600 601
'''
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Warning
──────────────────────────────────────────────────────────────────────────────
Your Flutter application is created using an older version of the Android
602 603
embedding. It is being deprecated in favor of Android embedding v2. To migrate
your project, follow the steps at:
604

605
https://github.com/flutter/flutter/wiki/Upgrading-pre-1.12-Android-projects
606 607

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
608 609 610 611
The detected reason was:

  ${result.reason}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
612 613 614 615
''');
    if (deprecationBehavior == DeprecationBehavior.ignore) {
      BuildEvent('deprecated-v1-android-embedding-ignored', type: 'gradle', flutterUsage: globals.flutterUsage).send();
    } else { // DeprecationBehavior.exit
616 617 618 619 620
      BuildEvent('deprecated-v1-android-embedding-failed', type: 'gradle', flutterUsage: globals.flutterUsage).send();
      throwToolExit(
        'Build failed due to use of deprecated Android v1 embedding.',
        exitCode: 1,
      );
621 622 623
    }
  }

624
  AndroidEmbeddingVersion getEmbeddingVersion() {
625 626 627 628
    return computeEmbeddingVersion().version;
  }

  AndroidEmbeddingVersionResult computeEmbeddingVersion() {
629 630 631
    if (isModule) {
      // A module type's Android project is used in add-to-app scenarios and
      // only supports the V2 embedding.
632
      return AndroidEmbeddingVersionResult(AndroidEmbeddingVersion.v2, 'Is add-to-app module');
633
    }
634 635 636 637 638 639 640
    if (isPlugin) {
      // Plugins do not use an appManifest, so we stop here.
      //
      // TODO(garyq): This method does not currently check for code references to
      // the v1 embedding, we should check for this once removal is further along.
      return AndroidEmbeddingVersionResult(AndroidEmbeddingVersion.v2, 'Is plugin');
    }
641
    if (appManifestFile == null || !appManifestFile.existsSync()) {
642
      return AndroidEmbeddingVersionResult(AndroidEmbeddingVersion.v1, 'No `${appManifestFile.absolute.path}` file');
643
    }
Dan Field's avatar
Dan Field committed
644
    XmlDocument document;
645
    try {
Dan Field's avatar
Dan Field committed
646
      document = XmlDocument.parse(appManifestFile.readAsStringSync());
647
    } on XmlException {
648 649 650 651 652 653
      throwToolExit('Error parsing $appManifestFile '
                    'Please ensure that the android manifest is a valid XML document and try again.');
    } on FileSystemException {
      throwToolExit('Error reading $appManifestFile even though it exists. '
                    'Please ensure that you have read permission to this file and try again.');
    }
654 655 656
    for (final XmlElement application in document.findAllElements('application')) {
      final String? applicationName = application.getAttribute('android:name');
      if (applicationName == 'io.flutter.app.FlutterApplication') {
657
        return AndroidEmbeddingVersionResult(AndroidEmbeddingVersion.v1, '${appManifestFile.absolute.path} uses `android:name="io.flutter.app.FlutterApplication"`');
658 659
      }
    }
Dan Field's avatar
Dan Field committed
660
    for (final XmlElement metaData in document.findAllElements('meta-data')) {
661
      final String? name = metaData.getAttribute('android:name');
662
      if (name == 'flutterEmbedding') {
663
        final String? embeddingVersionString = metaData.getAttribute('android:value');
664
        if (embeddingVersionString == '1') {
665
          return AndroidEmbeddingVersionResult(AndroidEmbeddingVersion.v1, '${appManifestFile.absolute.path} `<meta-data android:name="flutterEmbedding"` has value 1');
666 667
        }
        if (embeddingVersionString == '2') {
668
          return AndroidEmbeddingVersionResult(AndroidEmbeddingVersion.v2, '${appManifestFile.absolute.path} `<meta-data android:name="flutterEmbedding"` has value 2');
669 670 671
        }
      }
    }
672
    return AndroidEmbeddingVersionResult(AndroidEmbeddingVersion.v1, 'No `<meta-data android:name="flutterEmbedding" android:value="2"/>` in ${appManifestFile.absolute.path}');
673 674 675 676 677 678 679 680 681
  }
}

/// Iteration of the embedding Java API in the engine used by the Android project.
enum AndroidEmbeddingVersion {
  /// V1 APIs based on io.flutter.app.FlutterActivity.
  v1,
  /// V2 APIs based on io.flutter.embedding.android.FlutterActivity.
  v2,
682 683
}

684 685 686 687 688 689 690 691 692 693 694 695 696
/// Data class that holds the results of checking for embedding version.
///
/// This class includes the reason why a particular embedding was selected.
class AndroidEmbeddingVersionResult {
  AndroidEmbeddingVersionResult(this.version, this.reason);

  /// The embedding version.
  AndroidEmbeddingVersion version;

  /// The reason why the embedding version was selected.
  String reason;
}

697 698 699 700 701 702 703 704 705 706
// What the tool should do when encountering deprecated API in applications.
enum DeprecationBehavior {
  // The command being run does not care about deprecation status.
  none,
  // The command should continue and ignore the deprecation warning.
  ignore,
  // The command should exit the tool.
  exit,
}

707
/// Represents the web sub-project of a Flutter project.
708
class WebProject extends FlutterProjectPlatform {
709 710 711 712
  WebProject._(this.parent);

  final FlutterProject parent;

713 714 715
  @override
  String get pluginConfigKey => WebPlugin.kConfigKey;

716
  /// Whether this flutter project has a web sub-project.
717
  @override
718
  bool existsSync() {
719 720
    return parent.directory.childDirectory('web').existsSync()
      && indexFile.existsSync();
721
  }
722

723 724 725
  /// The 'lib' directory for the application.
  Directory get libDirectory => parent.directory.childDirectory('lib');

726 727 728
  /// The directory containing additional files for the application.
  Directory get directory => parent.directory.childDirectory('web');

729
  /// The html file used to host the flutter web application.
730 731 732
  File get indexFile => parent.directory
      .childDirectory('web')
      .childFile('index.html');
733

734 735 736 737 738 739 740 741 742 743 744 745 746 747
  /// The .dart_tool/dartpad directory
  Directory get dartpadToolDirectory => parent.directory
      .childDirectory('.dart_tool')
      .childDirectory('dartpad');

  Future<void> ensureReadyForPlatformSpecificTooling() async {
    /// Create .dart_tool/dartpad/web_plugin_registrant.dart.
    /// See: https://github.com/dart-lang/dart-services/pull/874
    await injectBuildTimePluginFiles(
      parent,
      destination: dartpadToolDirectory,
      webPlatform: true,
    );
  }
748 749
}

750
/// The Fuchsia sub project.
751 752 753 754 755
class FuchsiaProject {
  FuchsiaProject._(this.project);

  final FlutterProject project;

756
  Directory? _editableHostAppDirectory;
757 758 759 760 761
  Directory get editableHostAppDirectory =>
      _editableHostAppDirectory ??= project.directory.childDirectory('fuchsia');

  bool existsSync() => editableHostAppDirectory.existsSync();

762
  Directory? _meta;
763 764 765
  Directory get meta =>
      _meta ??= editableHostAppDirectory.childDirectory('meta');
}