project.dart 29.7 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

Lioness100's avatar
Lioness100 committed
30
/// Enum for each officially supported platform.
31 32 33 34 35 36 37 38 39 40 41
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

  /// 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) {
59
    return projects.putIfAbsent(directory.path, () {
60 61
      final FlutterManifest manifest = FlutterProject._readManifest(
        directory.childFile(bundle.defaultManifestPath).path,
62 63
        logger: _logger,
        fileSystem: _fileSystem,
64 65 66 67 68
      );
      final FlutterManifest exampleManifest = FlutterProject._readManifest(
        FlutterProject._exampleDirectory(directory)
            .childFile(bundle.defaultManifestPath)
            .path,
69 70
        logger: _logger,
        fileSystem: _fileSystem,
71 72 73
      );
      return FlutterProject(directory, manifest, exampleManifest);
    });
74 75 76
  }
}

77
/// Represents the contents of a Flutter project at the specified [directory].
78
///
79 80 81 82 83 84 85
/// [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.
86
class FlutterProject {
87
  @visibleForTesting
88
  FlutterProject(this.directory, this.manifest, this._exampleManifest);
89

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

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

98 99
  /// Create a [FlutterProject] and bypass the project caching.
  @visibleForTesting
100
  static FlutterProject fromDirectoryTest(Directory directory, [Logger? logger]) {
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
    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);
  }
117 118 119 120

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

121
  /// The manifest of this project.
122 123
  final FlutterManifest manifest;

124
  /// The manifest of the example sub-project of this project.
125
  final FlutterManifest _exampleManifest;
126

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

    if (ios.existsSync()) {
134 135 136
      // Don't require iOS build info, this method is only
      // used during create as best-effort, use the
      // default target bundle identifier.
137 138 139 140 141 142 143 144 145 146
      try {
        final String? bundleIdentifier = await ios.productBundleIdentifier(null);
        if (bundleIdentifier != null) {
          candidates.add(bundleIdentifier);
        }
      } on ToolExit {
        // It's possible that while parsing the build info for the ios project
        // that the bundleIdentifier can't be resolve. However, we would like
        // skip parsing that id in favor of searching in other place. We can
        // consider a tool exit in this case to be non fatal for the program.
147 148 149
      }
    }
    if (android.existsSync()) {
150 151
      final String? applicationId = android.applicationId;
      final String? group = android.group;
152 153 154 155 156 157 158 159
      candidates.addAll(<String>[
        if (applicationId != null)
          applicationId,
        if (group != null)
          group,
      ]);
    }
    if (example.android.existsSync()) {
160
      final String? applicationId = example.android.applicationId;
161 162 163 164 165
      if (applicationId != null) {
        candidates.add(applicationId);
      }
    }
    if (example.ios.existsSync()) {
166
      final String? bundleIdentifier = await example.ios.productBundleIdentifier(null);
167 168 169 170
      if (bundleIdentifier != null) {
        candidates.add(bundleIdentifier);
      }
    }
171
    return Set<String>.of(candidates.map<String?>(_organizationNameFromPackageName).whereType<String>());
172 173
  }

174
  String? _organizationNameFromPackageName(String packageName) {
175
    if (0 <= packageName.lastIndexOf('.')) {
176
      return packageName.substring(0, packageName.lastIndexOf('.'));
177 178
    }
    return null;
179 180 181
  }

  /// The iOS sub project of this project.
182
  late final IosProject ios = IosProject.fromFlutter(this);
183 184

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

187
  /// The web sub project of this project.
188
  late final WebProject web = WebProject._(this);
189

190
  /// The MacOS sub project of this project.
191
  late final MacOSProject macos = MacOSProject.fromFlutter(this);
192

193
  /// The Linux sub project of this project.
194
  late final LinuxProject linux = LinuxProject.fromFlutter(this);
195

196
  /// The Windows sub project of this project.
197
  late final WindowsProject windows = WindowsProject.fromFlutter(this);
198 199

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

202 203 204 205 206 207
  /// 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');

208 209 210 211 212 213
  /// 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');

214 215 216
  /// The `.metadata` file of this project.
  File get metadataFile => directory.childFile('.metadata');

217
  /// The `.flutter-plugins` file of this project.
218 219
  File get flutterPluginsFile => directory.childFile('.flutter-plugins');

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

224 225 226
  /// The `.dart-tool` directory of this project.
  Directory get dartTool => directory.childDirectory('.dart_tool');

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

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

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

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

250 251 252
  /// True if this project is a Flutter plugin project.
  bool get isPlugin => manifest.isPlugin;

253
  /// True if the Flutter project is using the AndroidX support library.
254 255
  bool get usesAndroidX => manifest.usesAndroidX;

256
  /// True if this project has an example application.
257
  bool get hasExampleApp => _exampleDirectory(directory).existsSync();
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 283 284 285
  /// 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;
  }

286
  /// The directory that will contain the example if an example exists.
287
  static Directory _exampleDirectory(Directory directory) => directory.childDirectory('example');
288

289 290 291 292 293
  /// 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.
294
  static FlutterManifest _readManifest(String path, {
295 296
    required Logger logger,
    required FileSystem fileSystem,
297
  }) {
298
    FlutterManifest? manifest;
299
    try {
300 301 302 303 304
      manifest = FlutterManifest.createFromPath(
        path,
        logger: logger,
        fileSystem: fileSystem,
      );
305
    } on YamlException catch (e) {
306 307
      logger.printStatus('Error detected in pubspec.yaml:', emphasis: true);
      logger.printError('$e');
308 309 310 311 312 313
    } 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');
314 315
    }
    if (manifest == null) {
316
      throwToolExit('Please correct the pubspec.yaml file at $path');
317
    }
318 319 320
    return manifest;
  }

321 322 323 324
  /// 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.
325
  Future<void> regeneratePlatformSpecificTooling({DeprecationBehavior deprecationBehavior = DeprecationBehavior.none}) async {
326 327 328 329 330 331 332 333 334
    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(),
335
      deprecationBehavior: deprecationBehavior,
336 337 338 339 340 341 342 343 344 345 346 347
    );
  }

  /// 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,
348
    DeprecationBehavior deprecationBehavior = DeprecationBehavior.none,
349
  }) async {
350
    if (!directory.existsSync() || isPlugin) {
351
      return;
352
    }
353 354
    await refreshPluginsList(this, iosPlatform: iosPlatform, macOSPlatform: macOSPlatform);
    if (androidPlatform) {
355
      await android.ensureReadyForPlatformSpecificTooling(deprecationBehavior: deprecationBehavior);
356
    }
357
    if (iosPlatform) {
358 359
      await ios.ensureReadyForPlatformSpecificTooling();
    }
360
    if (linuxPlatform) {
361 362
      await linux.ensureReadyForPlatformSpecificTooling();
    }
363
    if (macOSPlatform) {
364 365
      await macos.ensureReadyForPlatformSpecificTooling();
    }
366
    if (windowsPlatform) {
367 368
      await windows.ensureReadyForPlatformSpecificTooling();
    }
369
    if (webPlatform) {
370 371
      await web.ensureReadyForPlatformSpecificTooling();
    }
372 373 374 375 376 377 378 379
    await injectPlugins(
      this,
      androidPlatform: androidPlatform,
      iosPlatform: iosPlatform,
      linuxPlatform: linuxPlatform,
      macOSPlatform: macOSPlatform,
      windowsPlatform: windowsPlatform,
    );
380
  }
381

382
  void checkForDeprecation({DeprecationBehavior deprecationBehavior = DeprecationBehavior.none}) {
383
    if (android.existsSync() && pubspecFile.existsSync()) {
384 385 386 387
      android.checkForDeprecation(deprecationBehavior: deprecationBehavior);
    }
  }

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

404 405 406 407 408 409 410 411 412 413
/// 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();
}

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

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

424 425 426
  @override
  String get pluginConfigKey => AndroidPlugin.kConfigKey;

427
  static final RegExp _androidNamespacePattern = RegExp('android {[\\S\\s]+namespace[\\s]+[\'"](.+)[\'"]');
428
  static final RegExp _applicationIdPattern = RegExp('^\\s*applicationId\\s+[\'"](.*)[\'"]\\s*\$');
429
  static final RegExp _kotlinPluginPattern = RegExp('^\\s*apply plugin\\:\\s+[\'"]kotlin-android[\'"]\\s*\$');
430
  static final RegExp _groupPattern = RegExp('^\\s*group\\s+[\'"](.*)[\'"]\\s*\$');
431

432 433 434 435
  /// 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 {
436
    if (!isModule || _editableHostAppDirectory.existsSync()) {
437
      return _editableHostAppDirectory;
438
    }
439
    return ephemeralDirectory;
440 441 442 443
  }

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

447
  Directory get ephemeralDirectory => parent.directory.childDirectory('.android');
448
  Directory get _editableHostAppDirectory => parent.directory.childDirectory('android');
449

450 451
  /// True if the parent Flutter project is a module.
  bool get isModule => parent.isModule;
452

453 454 455
  /// True if the parent Flutter project is a plugin.
  bool get isPlugin => parent.isPlugin;

456
  /// True if the Flutter project is using the AndroidX support library.
457 458
  bool get usesAndroidX => parent.usesAndroidX;

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

  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;
  }

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

489
  File get appManifestFile {
490 491 492 493 494 495 496 497 498
    if(isUsingGradle) {
      return hostAppGradleRoot
        .childDirectory('app')
        .childDirectory('src')
        .childDirectory('main')
        .childFile('AndroidManifest.xml');
    }

    return hostAppGradleRoot.childFile('AndroidManifest.xml');
499 500
  }

501
  File get gradleAppOutV1File => gradleAppOutV1Directory.childFile('app-debug.apk');
502 503

  Directory get gradleAppOutV1Directory {
504
    return globals.fs.directory(globals.fs.path.join(hostAppGradleRoot.path, 'app', 'build', 'outputs', 'apk'));
505 506
  }

507
  /// Whether the current flutter project has an Android sub-project.
508
  @override
509 510 511 512
  bool existsSync() {
    return parent.isModule || _editableHostAppDirectory.existsSync();
  }

513
  bool get isUsingGradle {
514
    return hostAppGradleRoot.childFile('build.gradle').existsSync();
515
  }
516

517
  String? get applicationId {
518
    final File gradleFile = hostAppGradleRoot.childDirectory('app').childFile('build.gradle');
519
    return firstMatchInFile(gradleFile, _applicationIdPattern)?.group(1);
520 521
  }

522 523 524 525 526 527 528 529 530 531 532 533 534
  /// Get the namespace for newer Android projects,
  /// which replaces the `package` attribute in the Manifest.xml.
  String? get namespace {
    final File gradleFile = hostAppGradleRoot.childDirectory('app').childFile('build.gradle');

    if (!gradleFile.existsSync()) {
      return null;
    }

    // firstMatchInFile() reads per line but `_androidNamespacePattern` matches a multiline pattern.
    return _androidNamespacePattern.firstMatch(gradleFile.readAsStringSync())?.group(1);
  }

535
  String? get group {
536
    final File gradleFile = hostAppGradleRoot.childFile('build.gradle');
537
    return firstMatchInFile(gradleFile, _groupPattern)?.group(1);
538
  }
539

540 541 542 543 544
  /// The build directory where the Android artifacts are placed.
  Directory get buildDirectory {
    return parent.directory.childDirectory('build');
  }

545
  Future<void> ensureReadyForPlatformSpecificTooling({DeprecationBehavior deprecationBehavior = DeprecationBehavior.none}) async {
546
    if (isModule && _shouldRegenerateFromTemplate()) {
547
      await _regenerateLibrary();
548 549
      // Add ephemeral host app, if an editable host app does not already exist.
      if (!_editableHostAppDirectory.existsSync()) {
550 551
        await _overwriteFromTemplate(globals.fs.path.join('module', 'android', 'host_app_common'), ephemeralDirectory);
        await _overwriteFromTemplate(globals.fs.path.join('module', 'android', 'host_app_ephemeral'), ephemeralDirectory);
552
      }
553
    }
554
    if (!hostAppGradleRoot.existsSync()) {
555
      return;
556 557
    }
    gradle.updateLocalProperties(project: parent, requireAndroidSdk: false);
558 559
  }

560
  bool _shouldRegenerateFromTemplate() {
561
    return globals.fsUtils.isOlderThanReference(
562 563 564
      entity: ephemeralDirectory,
      referenceFile: parent.pubspecFile,
    ) || globals.cache.isOlderThanToolsStamp(ephemeralDirectory);
565
  }
566

567 568
  File get localPropertiesFile => _flutterLibGradleRoot.childFile('local.properties');

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

571
  Future<void> _regenerateLibrary() async {
572
    ErrorHandlingFileSystem.deleteIfExists(ephemeralDirectory, recursive: true);
573
    await _overwriteFromTemplate(globals.fs.path.join(
574 575
      'module',
      'android',
576
      'library_new_embedding',
577
    ), ephemeralDirectory);
578
    await _overwriteFromTemplate(globals.fs.path.join('module', 'android', 'gradle'), ephemeralDirectory);
579
    globals.gradleUtils?.injectGradleWrapperIfNeeded(ephemeralDirectory);
580
  }
581

582
  Future<void> _overwriteFromTemplate(String path, Directory target) async {
583 584 585 586 587 588 589
    final Template template = await Template.fromName(
      path,
      fileSystem: globals.fs,
      templateManifest: null,
      logger: globals.logger,
      templateRenderer: globals.templateRenderer,
    );
590
    final String androidIdentifier = parent.manifest.androidPackage ?? 'com.example.${parent.manifest.appName}';
591 592
    template.render(
      target,
593
      <String, Object>{
594
        'android': true,
595
        'projectName': parent.manifest.appName,
596
        'androidIdentifier': androidIdentifier,
597
        'androidX': usesAndroidX,
598 599 600
        'agpVersion': gradle.templateAndroidGradlePluginVersion,
        'kotlinVersion': gradle.templateKotlinGradlePluginVersion,
        'gradleVersion': gradle.templateDefaultGradleVersion,
601 602 603 604 605
        'gradleVersionForModule': gradle.templateDefaultGradleVersionForModule,
        'compileSdkVersion': gradle.compileSdkVersion,
        'minSdkVersion': gradle.minSdkVersion,
        'ndkVersion': gradle.ndkVersion,
        'targetSdkVersion': gradle.targetSdkVersion,
606 607 608 609
      },
      printStatusWhenWriting: false,
    );
  }
610

611
  void checkForDeprecation({DeprecationBehavior deprecationBehavior = DeprecationBehavior.none}) {
612 613 614
    if (deprecationBehavior == DeprecationBehavior.none) {
      return;
    }
615
    final AndroidEmbeddingVersionResult result = computeEmbeddingVersion();
616 617 618 619
    if (result.version != AndroidEmbeddingVersion.v1) {
      return;
    }
    globals.printStatus(
620 621 622 623 624
'''
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Warning
──────────────────────────────────────────────────────────────────────────────
Your Flutter application is created using an older version of the Android
625 626
embedding. It is being deprecated in favor of Android embedding v2. To migrate
your project, follow the steps at:
627

628
https://github.com/flutter/flutter/wiki/Upgrading-pre-1.12-Android-projects
629 630

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
631 632 633 634
The detected reason was:

  ${result.reason}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
635 636 637 638
''');
    if (deprecationBehavior == DeprecationBehavior.ignore) {
      BuildEvent('deprecated-v1-android-embedding-ignored', type: 'gradle', flutterUsage: globals.flutterUsage).send();
    } else { // DeprecationBehavior.exit
639 640 641 642 643
      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,
      );
644 645 646
    }
  }

647
  AndroidEmbeddingVersion getEmbeddingVersion() {
648 649 650 651
    return computeEmbeddingVersion().version;
  }

  AndroidEmbeddingVersionResult computeEmbeddingVersion() {
652 653 654
    if (isModule) {
      // A module type's Android project is used in add-to-app scenarios and
      // only supports the V2 embedding.
655
      return AndroidEmbeddingVersionResult(AndroidEmbeddingVersion.v2, 'Is add-to-app module');
656
    }
657 658 659 660 661 662 663
    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');
    }
664
    if (!appManifestFile.existsSync()) {
665
      return AndroidEmbeddingVersionResult(AndroidEmbeddingVersion.v1, 'No `${appManifestFile.absolute.path}` file');
666
    }
Dan Field's avatar
Dan Field committed
667
    XmlDocument document;
668
    try {
Dan Field's avatar
Dan Field committed
669
      document = XmlDocument.parse(appManifestFile.readAsStringSync());
670
    } on XmlException {
671 672 673 674 675 676
      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.');
    }
677 678 679
    for (final XmlElement application in document.findAllElements('application')) {
      final String? applicationName = application.getAttribute('android:name');
      if (applicationName == 'io.flutter.app.FlutterApplication') {
680
        return AndroidEmbeddingVersionResult(AndroidEmbeddingVersion.v1, '${appManifestFile.absolute.path} uses `android:name="io.flutter.app.FlutterApplication"`');
681 682
      }
    }
Dan Field's avatar
Dan Field committed
683
    for (final XmlElement metaData in document.findAllElements('meta-data')) {
684
      final String? name = metaData.getAttribute('android:name');
685
      if (name == 'flutterEmbedding') {
686
        final String? embeddingVersionString = metaData.getAttribute('android:value');
687
        if (embeddingVersionString == '1') {
688
          return AndroidEmbeddingVersionResult(AndroidEmbeddingVersion.v1, '${appManifestFile.absolute.path} `<meta-data android:name="flutterEmbedding"` has value 1');
689 690
        }
        if (embeddingVersionString == '2') {
691
          return AndroidEmbeddingVersionResult(AndroidEmbeddingVersion.v2, '${appManifestFile.absolute.path} `<meta-data android:name="flutterEmbedding"` has value 2');
692 693 694
        }
      }
    }
695
    return AndroidEmbeddingVersionResult(AndroidEmbeddingVersion.v1, 'No `<meta-data android:name="flutterEmbedding" android:value="2"/>` in ${appManifestFile.absolute.path}');
696 697 698 699 700 701 702 703 704
  }
}

/// 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,
705 706
}

707 708 709 710 711 712 713 714 715 716 717 718 719
/// 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;
}

720 721 722 723 724 725 726 727 728 729
// 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,
}

730
/// Represents the web sub-project of a Flutter project.
731
class WebProject extends FlutterProjectPlatform {
732 733 734 735
  WebProject._(this.parent);

  final FlutterProject parent;

736 737 738
  @override
  String get pluginConfigKey => WebPlugin.kConfigKey;

739
  /// Whether this flutter project has a web sub-project.
740
  @override
741
  bool existsSync() {
742 743
    return parent.directory.childDirectory('web').existsSync()
      && indexFile.existsSync();
744
  }
745

746 747 748
  /// The 'lib' directory for the application.
  Directory get libDirectory => parent.directory.childDirectory('lib');

749 750 751
  /// The directory containing additional files for the application.
  Directory get directory => parent.directory.childDirectory('web');

752
  /// The html file used to host the flutter web application.
753 754 755
  File get indexFile => parent.directory
      .childDirectory('web')
      .childFile('index.html');
756

757 758 759 760 761 762 763 764 765 766 767 768 769 770
  /// 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,
    );
  }
771 772
}

773
/// The Fuchsia sub project.
774 775 776 777 778
class FuchsiaProject {
  FuchsiaProject._(this.project);

  final FlutterProject project;

779
  Directory? _editableHostAppDirectory;
780 781 782 783 784
  Directory get editableHostAppDirectory =>
      _editableHostAppDirectory ??= project.directory.childDirectory('fuchsia');

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

785
  Directory? _meta;
786 787 788
  Directory get meta =>
      _meta ??= editableHostAppDirectory.childDirectory('meta');
}