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

import 'dart:async';
6

7
import 'package:meta/meta.dart';
8
import 'package:xml/xml.dart' as xml;
9
import 'package:yaml/yaml.dart';
10

11
import 'android/gradle_utils.dart' as gradle;
12
import 'artifacts.dart';
13
import 'base/common.dart';
14
import 'base/context.dart';
15
import 'base/file_system.dart';
16
import 'build_info.dart';
17
import 'bundle.dart' as bundle;
18
import 'cache.dart';
19
import 'features.dart';
20
import 'flutter_manifest.dart';
21
import 'globals.dart';
22
import 'ios/plist_parser.dart';
23
import 'ios/xcodeproj.dart' as xcode;
24
import 'plugins.dart';
25
import 'template.dart';
26

27
FlutterProjectFactory get projectFactory => context.get<FlutterProjectFactory>() ?? FlutterProjectFactory();
28 29

class FlutterProjectFactory {
30 31
  FlutterProjectFactory();

32 33
  @visibleForTesting
  final Map<String, FlutterProject> projects =
34
      <String, FlutterProject>{};
35 36 37 38 39

  /// 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);
40
    return projects.putIfAbsent(directory.path, /* ifAbsent */ () {
41 42 43 44 45 46 47 48 49 50
      final FlutterManifest manifest = FlutterProject._readManifest(
        directory.childFile(bundle.defaultManifestPath).path,
      );
      final FlutterManifest exampleManifest = FlutterProject._readManifest(
        FlutterProject._exampleDirectory(directory)
            .childFile(bundle.defaultManifestPath)
            .path,
      );
      return FlutterProject(directory, manifest, exampleManifest);
    });
51 52 53
  }
}

54
/// Represents the contents of a Flutter project at the specified [directory].
55
///
56 57 58 59 60 61 62
/// [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.
63
class FlutterProject {
64
  @visibleForTesting
65
  FlutterProject(this.directory, this.manifest, this._exampleManifest)
66 67 68
    : assert(directory != null),
      assert(manifest != null),
      assert(_exampleManifest != null);
69

70 71 72
  /// Returns a [FlutterProject] view of the given directory or a ToolExit error,
  /// if `pubspec.yaml` or `example/pubspec.yaml` is invalid.
  static FlutterProject fromDirectory(Directory directory) => projectFactory.fromDirectory(directory);
73

74 75
  /// Returns a [FlutterProject] view of the current directory or a ToolExit error,
  /// if `pubspec.yaml` or `example/pubspec.yaml` is invalid.
76
  static FlutterProject current() => fromDirectory(fs.currentDirectory);
77

78 79
  /// Returns a [FlutterProject] view of the given directory or a ToolExit error,
  /// if `pubspec.yaml` or `example/pubspec.yaml` is invalid.
80
  static FlutterProject fromPath(String path) => fromDirectory(fs.directory(path));
81 82 83 84

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

85
  /// The manifest of this project.
86 87
  final FlutterManifest manifest;

88
  /// The manifest of the example sub-project of this project.
89
  final FlutterManifest _exampleManifest;
90

91
  /// The set of organization names found in this project as
92 93
  /// part of iOS product bundle identifier, Android application ID, or
  /// Gradle group ID.
94
  Future<Set<String>> get organizationNames async {
95
    final List<String> candidates = <String>[
96
      await ios.productBundleIdentifier,
97 98 99
      android.applicationId,
      android.group,
      example.android.applicationId,
100
      await example.ios.productBundleIdentifier,
101
    ];
102
    return Set<String>.from(candidates
103
        .map<String>(_organizationNameFromPackageName)
104
        .where((String name) => name != null));
105 106 107
  }

  String _organizationNameFromPackageName(String packageName) {
108
    if (packageName != null && 0 <= packageName.lastIndexOf('.')) {
109
      return packageName.substring(0, packageName.lastIndexOf('.'));
110 111
    }
    return null;
112 113 114
  }

  /// The iOS sub project of this project.
115 116
  IosProject _ios;
  IosProject get ios => _ios ??= IosProject.fromFlutter(this);
117 118

  /// The Android sub project of this project.
119 120
  AndroidProject _android;
  AndroidProject get android => _android ??= AndroidProject._(this);
121

122
  /// The web sub project of this project.
123 124
  WebProject _web;
  WebProject get web => _web ??= WebProject._(this);
125

126 127 128
  /// The MacOS sub project of this project.
  MacOSProject _macos;
  MacOSProject get macos => _macos ??= MacOSProject._(this);
129

130 131 132
  /// The Linux sub project of this project.
  LinuxProject _linux;
  LinuxProject get linux => _linux ??= LinuxProject._(this);
133

134 135 136 137 138 139 140
  /// The Windows sub project of this project.
  WindowsProject _windows;
  WindowsProject get windows => _windows ??= WindowsProject._(this);

  /// The Fuchsia sub project of this project.
  FuchsiaProject _fuchsia;
  FuchsiaProject get fuchsia => _fuchsia ??= FuchsiaProject._(this);
141

142 143 144 145 146 147 148
  /// 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');

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

151 152 153 154
  /// The `.flutter-plugins-dependencies` file of this project,
  /// which contains the dependencies each plugin depends on.
  File get flutterPluginsDependenciesFile => directory.childFile('.flutter-plugins-dependencies');

155 156 157
  /// The `.dart-tool` directory of this project.
  Directory get dartTool => directory.childDirectory('.dart_tool');

158 159
  /// The directory containing the generated code for this project.
  Directory get generated => directory
160
    .absolute
161 162 163 164 165
    .childDirectory('.dart_tool')
    .childDirectory('build')
    .childDirectory('generated')
    .childDirectory(manifest.appName);

166
  /// The example sub-project of this project.
167
  FlutterProject get example => FlutterProject(
168 169 170 171 172
    _exampleDirectory(directory),
    _exampleManifest,
    FlutterManifest.empty(),
  );

173 174
  /// True if this project is a Flutter module project.
  bool get isModule => manifest.isModule;
175

176 177 178
  /// True if the Flutter project is using the AndroidX support library
  bool get usesAndroidX => manifest.usesAndroidX;

179
  /// True if this project has an example application.
180
  bool get hasExampleApp => _exampleDirectory(directory).existsSync();
181 182

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

185 186 187 188 189
  /// 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.
190
  static FlutterManifest _readManifest(String path) {
191 192 193 194 195 196 197 198
    FlutterManifest manifest;
    try {
      manifest = FlutterManifest.createFromPath(path);
    } on YamlException catch (e) {
      printStatus('Error detected in pubspec.yaml:', emphasis: true);
      printError('$e');
    }
    if (manifest == null) {
199
      throwToolExit('Please correct the pubspec.yaml file at $path');
200
    }
201 202 203
    return manifest;
  }

204
  /// Generates project files necessary to make Gradle builds work on Android
205
  /// and CocoaPods+Xcode work on iOS, for app and module projects only.
206 207
  Future<void> ensureReadyForPlatformSpecificTooling({bool checkProjects = false}) async {
    if (!directory.existsSync() || hasExampleApp) {
208
      return;
209
    }
210
    refreshPluginsList(this);
211 212 213 214 215 216
    if ((android.existsSync() && checkProjects) || !checkProjects) {
      await android.ensureReadyForPlatformSpecificTooling();
    }
    if ((ios.existsSync() && checkProjects) || !checkProjects) {
      await ios.ensureReadyForPlatformSpecificTooling();
    }
217 218 219 220 221 222
    // TODO(stuartmorgan): Revisit conditions once there is a plan for handling
    // non-default platform projects. For now, always treat checkProjects as
    // true for desktop.
    if (featureFlags.isLinuxEnabled && linux.existsSync()) {
      await linux.ensureReadyForPlatformSpecificTooling();
    }
223
    if (featureFlags.isMacOSEnabled && macos.existsSync()) {
224 225
      await macos.ensureReadyForPlatformSpecificTooling();
    }
226 227 228
    if (featureFlags.isWindowsEnabled && windows.existsSync()) {
      await windows.ensureReadyForPlatformSpecificTooling();
    }
229
    if (featureFlags.isWebEnabled && web.existsSync()) {
230 231
      await web.ensureReadyForPlatformSpecificTooling();
    }
232
    await injectPlugins(this, checkProjects: checkProjects);
233
  }
234 235

  /// Return the set of builders used by this package.
236 237 238 239
  YamlMap get builders {
    if (!pubspecFile.existsSync()) {
      return null;
    }
240
    final YamlMap pubspec = loadYaml(pubspecFile.readAsStringSync()) as YamlMap;
241 242 243 244
    // If the pubspec file is empty, this will be null.
    if (pubspec == null) {
      return null;
    }
245
    return pubspec['builders'] as YamlMap;
246
  }
247 248

  /// Whether there are any builders used by this package.
249 250
  bool get hasBuilders {
    final YamlMap result = builders;
251 252
    return result != null && result.isNotEmpty;
  }
253 254
}

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
/// Represents an Xcode-based sub-project.
///
/// This defines interfaces common to iOS and macOS projects.
abstract class XcodeBasedProject {
  /// The parent of this project.
  FlutterProject get parent;

  /// Whether the subproject (either iOS or macOS) exists in the Flutter project.
  bool existsSync();

  /// The Xcode project (.xcodeproj directory) of the host app.
  Directory get xcodeProject;

  /// The 'project.pbxproj' file of [xcodeProject].
  File get xcodeProjectInfoFile;

  /// The Xcode workspace (.xcworkspace directory) of the host app.
  Directory get xcodeWorkspace;

  /// Contains definitions for FLUTTER_ROOT, LOCAL_ENGINE, and more flags for
  /// the Xcode build.
  File get generatedXcodePropertiesFile;

  /// The Flutter-managed Xcode config file for [mode].
  File xcodeConfigFor(String mode);

281 282 283 284 285 286
  /// The script that exports environment variables needed for Flutter tools.
  /// Can be run first in a Xcode Script build phase to make FLUTTER_ROOT,
  /// LOCAL_ENGINE, and other Flutter variables available to any flutter
  /// tooling (`flutter build`, etc) to convert into flags.
  File get generatedEnvironmentVariableExportScript;

287 288 289 290 291 292 293 294 295
  /// The CocoaPods 'Podfile'.
  File get podfile;

  /// The CocoaPods 'Podfile.lock'.
  File get podfileLock;

  /// The CocoaPods 'Manifest.lock'.
  File get podManifestLock;

296 297
  /// Directory containing symlinks to pub cache plugins source generated on `pod install`.
  Directory get symlinks;
298 299
}

300 301 302
/// Represents the iOS sub-project of a Flutter project.
///
/// Instances will reflect the contents of the `ios/` sub-folder of
303
/// Flutter applications and the `.ios/` sub-folder of Flutter module projects.
304
class IosProject implements XcodeBasedProject {
305
  IosProject.fromFlutter(this.parent);
306

307
  @override
308 309
  final FlutterProject parent;

310
  static final RegExp _productBundleIdPattern = RegExp(r'''^\s*PRODUCT_BUNDLE_IDENTIFIER\s*=\s*(["']?)(.*?)\1;\s*$''');
311
  static const String _productBundleIdVariable = r'$(PRODUCT_BUNDLE_IDENTIFIER)';
312
  static const String _hostAppBundleName = 'Runner';
313

314
  Directory get ephemeralDirectory => parent.directory.childDirectory('.ios');
315 316 317 318
  Directory get _editableDirectory => parent.directory.childDirectory('ios');

  /// This parent folder of `Runner.xcodeproj`.
  Directory get hostAppRoot {
319
    if (!isModule || _editableDirectory.existsSync()) {
320
      return _editableDirectory;
321
    }
322
    return ephemeralDirectory;
323 324 325 326 327 328 329
  }

  /// The root directory of the iOS wrapping of Flutter and plugins. This is the
  /// parent of the `Flutter/` folder into which Flutter artifacts are written
  /// during build.
  ///
  /// This is the same as [hostAppRoot] except when the project is
330
  /// a Flutter module with an editable host app.
331
  Directory get _flutterLibRoot => isModule ? ephemeralDirectory : _editableDirectory;
332

333 334 335
  /// The bundle name of the host app, `Runner.app`.
  String get hostAppBundleName => '$_hostAppBundleName.app';

336 337
  /// True, if the parent Flutter project is a module project.
  bool get isModule => parent.isModule;
338

339 340 341
  /// Whether the flutter application has an iOS project.
  bool get exists => hostAppRoot.existsSync();

342
  @override
343
  File xcodeConfigFor(String mode) => _flutterLibRoot.childDirectory('Flutter').childFile('$mode.xcconfig');
344

345 346 347
  @override
  File get generatedEnvironmentVariableExportScript => _flutterLibRoot.childDirectory('Flutter').childFile('flutter_export_environment.sh');

348
  @override
349
  File get podfile => hostAppRoot.childFile('Podfile');
350

351
  @override
352
  File get podfileLock => hostAppRoot.childFile('Podfile.lock');
353

354
  @override
355
  File get podManifestLock => hostAppRoot.childDirectory('Pods').childFile('Manifest.lock');
356

357
  /// The 'Info.plist' file of the host app.
358
  File get hostInfoPlist => hostAppRoot.childDirectory(_hostAppBundleName).childFile('Info.plist');
359

360 361 362
  @override
  Directory get symlinks => _flutterLibRoot.childDirectory('.symlinks');

363
  @override
364
  Directory get xcodeProject => hostAppRoot.childDirectory('$_hostAppBundleName.xcodeproj');
365

366
  @override
367 368
  File get xcodeProjectInfoFile => xcodeProject.childFile('project.pbxproj');

369
  @override
370
  Directory get xcodeWorkspace => hostAppRoot.childDirectory('$_hostAppBundleName.xcworkspace');
371 372 373 374 375 376 377

  /// Xcode workspace shared data directory for the host app.
  Directory get xcodeWorkspaceSharedData => xcodeWorkspace.childDirectory('xcshareddata');

  /// Xcode workspace shared workspace settings file for the host app.
  File get xcodeWorkspaceSharedSettings => xcodeWorkspaceSharedData.childFile('WorkspaceSettings.xcsettings');

378
  @override
379 380 381 382
  bool existsSync()  {
    return parent.isModule || _editableDirectory.existsSync();
  }

383 384
  /// The product bundle identifier of the host app, or null if not set or if
  /// iOS tooling needed to read it is not installed.
385
  Future<String> get productBundleIdentifier async {
386 387 388 389 390 391 392 393 394
    String fromPlist;
    try {
      fromPlist = PlistParser.instance.getValueFromFile(
        hostInfoPlist.path,
        PlistParser.kCFBundleIdentifierKey,
      );
    } on FileNotFoundException {
      // iOS tooling not found; likely not running OSX; let [fromPlist] be null
    }
395 396 397 398
    if (fromPlist != null && !fromPlist.contains('\$')) {
      // Info.plist has no build variables in product bundle ID.
      return fromPlist;
    }
399
    final String fromPbxproj = _firstMatchInFile(xcodeProjectInfoFile, _productBundleIdPattern)?.group(2);
400 401 402 403 404 405
    if (fromPbxproj != null && (fromPlist == null || fromPlist == _productBundleIdVariable)) {
      // Common case. Avoids parsing build settings.
      return fromPbxproj;
    }
    if (fromPlist != null && xcode.xcodeProjectInterpreter.isInstalled) {
      // General case: perform variable substitution using build settings.
406
      return xcode.substituteXcodeVariables(fromPlist, await buildSettings);
407 408
    }
    return null;
409 410 411
  }

  /// The build settings for the host app of this project, as a detached map.
412 413
  ///
  /// Returns null, if iOS tooling is unavailable.
414
  Future<Map<String, String>> get buildSettings async {
415
    if (!xcode.xcodeProjectInterpreter.isInstalled) {
416
      return null;
417
    }
418
    _buildSettings ??= await xcode.xcodeProjectInterpreter.getBuildSettings(
419
      xcodeProject.path,
420
      _hostAppBundleName,
421
    );
422
    return _buildSettings;
423
  }
424

425 426
  Map<String, String> _buildSettings;

427
  Future<void> ensureReadyForPlatformSpecificTooling() async {
428
    _regenerateFromTemplateIfNeeded();
429
    if (!_flutterLibRoot.existsSync()) {
430
      return;
431
    }
432 433 434 435
    await _updateGeneratedXcodeConfigIfNeeded();
  }

  Future<void> _updateGeneratedXcodeConfigIfNeeded() async {
436
    if (Cache.instance.isOlderThanToolsStamp(generatedXcodePropertiesFile)) {
437 438 439 440 441 442
      await xcode.updateGeneratedXcodeProperties(
        project: parent,
        buildInfo: BuildInfo.debug,
        targetOverride: bundle.defaultMainPath,
      );
    }
443 444
  }

445
  void _regenerateFromTemplateIfNeeded() {
446
    if (!isModule) {
447
      return;
448
    }
449 450
    final bool pubspecChanged = isOlderThanReference(entity: ephemeralDirectory, referenceFile: parent.pubspecFile);
    final bool toolingChanged = Cache.instance.isOlderThanToolsStamp(ephemeralDirectory);
451
    if (!pubspecChanged && !toolingChanged) {
452
      return;
453
    }
454 455 456 457 458

    final Directory engineDest = ephemeralDirectory
      .childDirectory('Flutter')
      .childDirectory('engine');

459 460
    _deleteIfExistsSync(ephemeralDirectory);
    _overwriteFromTemplate(fs.path.join('module', 'ios', 'library'), ephemeralDirectory);
461 462
    // Add ephemeral host app, if a editable host app does not already exist.
    if (!_editableDirectory.existsSync()) {
463
      _overwriteFromTemplate(fs.path.join('module', 'ios', 'host_app_ephemeral'), ephemeralDirectory);
464
      if (hasPlugins(parent)) {
465
        _overwriteFromTemplate(fs.path.join('module', 'ios', 'host_app_ephemeral_cocoapods'), ephemeralDirectory);
466
      }
467 468 469 470 471 472 473 474 475 476 477
      // Copy podspec and framework from engine cache. The actual build mode
      // doesn't actually matter as it will be overwritten by xcode_backend.sh.
      // However, cocoapods will run before that script and requires something
      // to be in this location.
      final Directory framework = fs.directory(artifacts.getArtifactPath(Artifact.flutterFramework,
        platform: TargetPlatform.ios, mode: BuildMode.debug));
      if (framework.existsSync()) {
        final File podspec = framework.parent.childFile('Flutter.podspec');
        copyDirectorySync(framework, engineDest.childDirectory('Flutter.framework'));
        podspec.copySync(engineDest.childFile('Flutter.podspec').path);
      }
478
    }
479 480
  }

481
  Future<void> makeHostAppEditable() async {
482
    assert(isModule);
483
    if (_editableDirectory.existsSync()) {
484
      throwToolExit('iOS host app is already editable. To start fresh, delete the ios/ folder.');
485
    }
486 487
    _deleteIfExistsSync(ephemeralDirectory);
    _overwriteFromTemplate(fs.path.join('module', 'ios', 'library'), ephemeralDirectory);
488 489 490
    _overwriteFromTemplate(fs.path.join('module', 'ios', 'host_app_ephemeral'), _editableDirectory);
    _overwriteFromTemplate(fs.path.join('module', 'ios', 'host_app_ephemeral_cocoapods'), _editableDirectory);
    _overwriteFromTemplate(fs.path.join('module', 'ios', 'host_app_editable_cocoapods'), _editableDirectory);
491 492
    await _updateGeneratedXcodeConfigIfNeeded();
    await injectPlugins(parent);
493
  }
494

495
  @override
496
  File get generatedXcodePropertiesFile => _flutterLibRoot.childDirectory('Flutter').childFile('Generated.xcconfig');
497 498

  Directory get pluginRegistrantHost {
499
    return isModule
500
        ? _flutterLibRoot.childDirectory('Flutter').childDirectory('FlutterPluginRegistrant')
501
        : hostAppRoot.childDirectory(_hostAppBundleName);
502 503 504
  }

  void _overwriteFromTemplate(String path, Directory target) {
505
    final Template template = Template.fromName(path);
506 507 508 509
    template.render(
      target,
      <String, dynamic>{
        'projectName': parent.manifest.appName,
510
        'iosIdentifier': parent.manifest.iosBundleIdentifier,
511 512 513 514
      },
      printStatusWhenWriting: false,
      overwriteExisting: true,
    );
515
  }
516 517
}

518 519 520
/// Represents the Android sub-project of a Flutter project.
///
/// Instances will reflect the contents of the `android/` sub-folder of
521
/// Flutter applications and the `.android/` sub-folder of Flutter module projects.
522
class AndroidProject {
523 524 525 526 527
  AndroidProject._(this.parent);

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

528
  static final RegExp _applicationIdPattern = RegExp('^\\s*applicationId\\s+[\'\"](.*)[\'\"]\\s*\$');
529
  static final RegExp _kotlinPluginPattern = RegExp('^\\s*apply plugin\:\\s+[\'\"]kotlin-android[\'\"]\\s*\$');
530 531
  static final RegExp _groupPattern = RegExp('^\\s*group\\s+[\'\"](.*)[\'\"]\\s*\$');

532 533 534 535
  /// 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 {
536
    if (!isModule || _editableHostAppDirectory.existsSync()) {
537
      return _editableHostAppDirectory;
538
    }
539
    return ephemeralDirectory;
540 541 542 543
  }

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

547
  Directory get ephemeralDirectory => parent.directory.childDirectory('.android');
548
  Directory get _editableHostAppDirectory => parent.directory.childDirectory('android');
549

550 551
  /// True if the parent Flutter project is a module.
  bool get isModule => parent.isModule;
552

553 554 555
  /// True if the Flutter project is using the AndroidX support library
  bool get usesAndroidX => parent.usesAndroidX;

556 557 558 559 560 561
  /// True, if the app project is using Kotlin.
  bool get isKotlin {
    final File gradleFile = hostAppGradleRoot.childDirectory('app').childFile('build.gradle');
    return _firstMatchInFile(gradleFile, _kotlinPluginPattern) != null;
  }

562
  File get appManifestFile {
563
    return isUsingGradle
564 565
        ? fs.file(fs.path.join(hostAppGradleRoot.path, 'app', 'src', 'main', 'AndroidManifest.xml'))
        : hostAppGradleRoot.childFile('AndroidManifest.xml');
566 567
  }

568
  File get gradleAppOutV1File => gradleAppOutV1Directory.childFile('app-debug.apk');
569 570

  Directory get gradleAppOutV1Directory {
571
    return fs.directory(fs.path.join(hostAppGradleRoot.path, 'app', 'build', 'outputs', 'apk'));
572 573
  }

574 575 576 577 578
  /// Whether the current flutter project has an Android sub-project.
  bool existsSync() {
    return parent.isModule || _editableHostAppDirectory.existsSync();
  }

579
  bool get isUsingGradle {
580
    return hostAppGradleRoot.childFile('build.gradle').existsSync();
581
  }
582

583
  String get applicationId {
584
    final File gradleFile = hostAppGradleRoot.childDirectory('app').childFile('build.gradle');
585
    return _firstMatchInFile(gradleFile, _applicationIdPattern)?.group(1);
586 587
  }

588
  String get group {
589
    final File gradleFile = hostAppGradleRoot.childFile('build.gradle');
590
    return _firstMatchInFile(gradleFile, _groupPattern)?.group(1);
591
  }
592

593 594 595 596 597
  /// The build directory where the Android artifacts are placed.
  Directory get buildDirectory {
    return parent.directory.childDirectory('build');
  }

598
  Future<void> ensureReadyForPlatformSpecificTooling() async {
599
    if (isModule && _shouldRegenerateFromTemplate()) {
600
      _regenerateLibrary();
601 602
      // Add ephemeral host app, if an editable host app does not already exist.
      if (!_editableHostAppDirectory.existsSync()) {
603 604
        _overwriteFromTemplate(fs.path.join('module', 'android', 'host_app_common'), ephemeralDirectory);
        _overwriteFromTemplate(fs.path.join('module', 'android', 'host_app_ephemeral'), ephemeralDirectory);
605
      }
606
    }
607
    if (!hostAppGradleRoot.existsSync()) {
608
      return;
609 610
    }
    gradle.updateLocalProperties(project: parent, requireAndroidSdk: false);
611 612
  }

613
  bool _shouldRegenerateFromTemplate() {
614 615
    return isOlderThanReference(entity: ephemeralDirectory, referenceFile: parent.pubspecFile)
        || Cache.instance.isOlderThanToolsStamp(ephemeralDirectory);
616
  }
617

618
  Future<void> makeHostAppEditable() async {
619
    assert(isModule);
620
    if (_editableHostAppDirectory.existsSync()) {
621
      throwToolExit('Android host app is already editable. To start fresh, delete the android/ folder.');
622
    }
623
    _regenerateLibrary();
624 625 626
    _overwriteFromTemplate(fs.path.join('module', 'android', 'host_app_common'), _editableHostAppDirectory);
    _overwriteFromTemplate(fs.path.join('module', 'android', 'host_app_editable'), _editableHostAppDirectory);
    _overwriteFromTemplate(fs.path.join('module', 'android', 'gradle'), _editableHostAppDirectory);
627
    gradle.gradleUtils.injectGradleWrapperIfNeeded(_editableHostAppDirectory);
628
    gradle.writeLocalProperties(_editableHostAppDirectory.childFile('local.properties'));
629 630 631 632 633
    await injectPlugins(parent);
  }

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

634
  Directory get pluginRegistrantHost => _flutterLibGradleRoot.childDirectory(isModule ? 'Flutter' : 'app');
635 636

  void _regenerateLibrary() {
637
    _deleteIfExistsSync(ephemeralDirectory);
638 639 640
    _overwriteFromTemplate(fs.path.join(
      'module',
      'android',
641
      featureFlags.isAndroidEmbeddingV2Enabled ? 'library_new_embedding' : 'library',
642
    ), ephemeralDirectory);
643
    _overwriteFromTemplate(fs.path.join('module', 'android', 'gradle'), ephemeralDirectory);
644
    gradle.gradleUtils.injectGradleWrapperIfNeeded(ephemeralDirectory);
645
  }
646

647
  void _overwriteFromTemplate(String path, Directory target) {
648
    final Template template = Template.fromName(path);
649 650 651 652 653
    template.render(
      target,
      <String, dynamic>{
        'projectName': parent.manifest.appName,
        'androidIdentifier': parent.manifest.androidPackage,
654
        'androidX': usesAndroidX,
655
        'useAndroidEmbeddingV2': featureFlags.isAndroidEmbeddingV2Enabled,
656 657 658 659 660
      },
      printStatusWhenWriting: false,
      overwriteExisting: true,
    );
  }
661 662

  AndroidEmbeddingVersion getEmbeddingVersion() {
663 664 665 666 667
    if (isModule) {
      // A module type's Android project is used in add-to-app scenarios and
      // only supports the V2 embedding.
      return AndroidEmbeddingVersion.v2;
    }
668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702
    if (appManifestFile == null || !appManifestFile.existsSync()) {
      return AndroidEmbeddingVersion.v1;
    }
    xml.XmlDocument document;
    try {
      document = xml.parse(appManifestFile.readAsStringSync());
    } on xml.XmlParserException {
      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.');
    }
    for (xml.XmlElement metaData in document.findAllElements('meta-data')) {
      final String name = metaData.getAttribute('android:name');
      if (name == 'flutterEmbedding') {
        final String embeddingVersionString = metaData.getAttribute('android:value');
        if (embeddingVersionString == '1') {
          return AndroidEmbeddingVersion.v1;
        }
        if (embeddingVersionString == '2') {
          return AndroidEmbeddingVersion.v2;
        }
      }
    }
    return AndroidEmbeddingVersion.v1;
  }
}

/// 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,
703 704
}

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

  final FlutterProject parent;

711 712
  /// Whether this flutter project has a web sub-project.
  bool existsSync() {
713 714
    return parent.directory.childDirectory('web').existsSync()
      && indexFile.existsSync();
715
  }
716

717 718 719
  /// The 'lib' directory for the application.
  Directory get libDirectory => parent.directory.childDirectory('lib');

720 721 722
  /// The directory containing additional files for the application.
  Directory get directory => parent.directory.childDirectory('web');

723
  /// The html file used to host the flutter web application.
724 725 726
  File get indexFile => parent.directory
      .childDirectory('web')
      .childFile('index.html');
727

728
  Future<void> ensureReadyForPlatformSpecificTooling() async {}
729 730
}

731 732
/// Deletes [directory] with all content.
void _deleteIfExistsSync(Directory directory) {
733
  if (directory.existsSync()) {
734
    directory.deleteSync(recursive: true);
735
  }
736 737 738 739
}


/// Returns the first line-based match for [regExp] in [file].
740 741
///
/// Assumes UTF8 encoding.
742 743
Match _firstMatchInFile(File file, RegExp regExp) {
  if (!file.existsSync()) {
744 745
    return null;
  }
746 747 748 749 750 751 752
  for (String line in file.readAsLinesSync()) {
    final Match match = regExp.firstMatch(line);
    if (match != null) {
      return match;
    }
  }
  return null;
753
}
754 755

/// The macOS sub project.
756 757
class MacOSProject implements XcodeBasedProject {
  MacOSProject._(this.parent);
758

759 760
  @override
  final FlutterProject parent;
761

762
  static const String _hostAppBundleName = 'Runner';
763

764
  @override
765
  bool existsSync() => _macOSDirectory.existsSync();
766

767
  Directory get _macOSDirectory => parent.directory.childDirectory('macos');
768

769 770 771 772 773 774 775 776 777
  /// The directory in the project that is managed by Flutter. As much as
  /// possible, files that are edited by Flutter tooling after initial project
  /// creation should live here.
  Directory get managedDirectory => _macOSDirectory.childDirectory('Flutter');

  /// The subdirectory of [managedDirectory] that contains files that are
  /// generated on the fly. All generated files that are not intended to be
  /// checked in should live here.
  Directory get ephemeralDirectory => managedDirectory.childDirectory('ephemeral');
778

779 780 781 782 783 784 785 786
  /// The xcfilelist used to track the inputs for the Flutter script phase in
  /// the Xcode build.
  File get inputFileList => ephemeralDirectory.childFile('FlutterInputs.xcfilelist');

  /// The xcfilelist used to track the outputs for the Flutter script phase in
  /// the Xcode build.
  File get outputFileList => ephemeralDirectory.childFile('FlutterOutputs.xcfilelist');

787
  @override
788 789
  File get generatedXcodePropertiesFile => ephemeralDirectory.childFile('Flutter-Generated.xcconfig');

790
  @override
791
  File xcodeConfigFor(String mode) => managedDirectory.childFile('Flutter-$mode.xcconfig');
792

793
  @override
794
  File get generatedEnvironmentVariableExportScript => ephemeralDirectory.childFile('flutter_export_environment.sh');
795

796 797 798 799 800 801 802 803 804 805
  @override
  File get podfile => _macOSDirectory.childFile('Podfile');

  @override
  File get podfileLock => _macOSDirectory.childFile('Podfile.lock');

  @override
  File get podManifestLock => _macOSDirectory.childDirectory('Pods').childFile('Manifest.lock');

  @override
806
  Directory get xcodeProject => _macOSDirectory.childDirectory('$_hostAppBundleName.xcodeproj');
807

808 809 810 811
  @override
  File get xcodeProjectInfoFile => xcodeProject.childFile('project.pbxproj');

  @override
812
  Directory get xcodeWorkspace => _macOSDirectory.childDirectory('$_hostAppBundleName.xcworkspace');
813

814 815 816
  @override
  Directory get symlinks => ephemeralDirectory.childDirectory('.symlinks');

817 818
  /// The file where the Xcode build will write the name of the built app.
  ///
Chris Bracken's avatar
Chris Bracken committed
819
  /// Ideally this will be replaced in the future with inspection of the Runner
820
  /// scheme's target.
821
  File get nameFile => ephemeralDirectory.childFile('.app_filename');
822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837

  Future<void> ensureReadyForPlatformSpecificTooling() async {
    // TODO(stuartmorgan): Add create-from-template logic here.
    await _updateGeneratedXcodeConfigIfNeeded();
  }

  Future<void> _updateGeneratedXcodeConfigIfNeeded() async {
    if (Cache.instance.isOlderThanToolsStamp(generatedXcodePropertiesFile)) {
      await xcode.updateGeneratedXcodeProperties(
        project: parent,
        buildInfo: BuildInfo.debug,
        useMacOSConfig: true,
        setSymroot: false,
      );
    }
  }
838 839 840 841 842 843 844 845
}

/// The Windows sub project
class WindowsProject {
  WindowsProject._(this.project);

  final FlutterProject project;

846
  bool existsSync() => _editableDirectory.existsSync();
847

848 849
  Directory get _editableDirectory => project.directory.childDirectory('windows');

850 851 852 853 854 855 856 857 858
  /// The directory in the project that is managed by Flutter. As much as
  /// possible, files that are edited by Flutter tooling after initial project
  /// creation should live here.
  Directory get managedDirectory => _editableDirectory.childDirectory('flutter');

  /// The subdirectory of [managedDirectory] that contains files that are
  /// generated on the fly. All generated files that are not intended to be
  /// checked in should live here.
  Directory get ephemeralDirectory => managedDirectory.childDirectory('ephemeral');
859

860 861
  /// Contains definitions for FLUTTER_ROOT, LOCAL_ENGINE, and more flags for
  /// the build.
862
  File get generatedPropertySheetFile => ephemeralDirectory.childFile('Generated.props');
863 864 865

  // The MSBuild project file.
  File get vcprojFile => _editableDirectory.childFile('Runner.vcxproj');
866

867 868 869
  // The MSBuild solution file.
  File get solutionFile => _editableDirectory.childFile('Runner.sln');

870 871 872
  /// The file where the VS build will write the name of the built app.
  ///
  /// Ideally this will be replaced in the future with inspection of the project.
873
  File get nameFile => ephemeralDirectory.childFile('exe_filename');
874 875

  Future<void> ensureReadyForPlatformSpecificTooling() async {}
876 877 878 879 880 881 882 883
}

/// The Linux sub project.
class LinuxProject {
  LinuxProject._(this.project);

  final FlutterProject project;

884
  Directory get _editableDirectory => project.directory.childDirectory('linux');
885

886 887 888 889
  /// The directory in the project that is managed by Flutter. As much as
  /// possible, files that are edited by Flutter tooling after initial project
  /// creation should live here.
  Directory get managedDirectory => _editableDirectory.childDirectory('flutter');
890

891 892 893 894 895 896
  /// The subdirectory of [managedDirectory] that contains files that are
  /// generated on the fly. All generated files that are not intended to be
  /// checked in should live here.
  Directory get ephemeralDirectory => managedDirectory.childDirectory('ephemeral');

  bool existsSync() => _editableDirectory.existsSync();
897

898
  /// The Linux project makefile.
899 900 901 902 903
  File get makeFile => _editableDirectory.childFile('Makefile');

  /// Contains definitions for FLUTTER_ROOT, LOCAL_ENGINE, and more flags for
  /// the build.
  File get generatedMakeConfigFile => ephemeralDirectory.childFile('generated_config.mk');
904 905

  Future<void> ensureReadyForPlatformSpecificTooling() async {}
906
}
907

908
/// The Fuchsia sub project
909 910 911 912 913 914 915 916 917 918 919 920 921 922 923
class FuchsiaProject {
  FuchsiaProject._(this.project);

  final FlutterProject project;

  Directory _editableHostAppDirectory;
  Directory get editableHostAppDirectory =>
      _editableHostAppDirectory ??= project.directory.childDirectory('fuchsia');

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

  Directory _meta;
  Directory get meta =>
      _meta ??= editableHostAppDirectory.childDirectory('meta');
}