• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2018 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
6
7import 'package:meta/meta.dart';
8import 'package:yaml/yaml.dart';
9
10import 'android/gradle.dart' as gradle;
11import 'base/common.dart';
12import 'base/context.dart';
13import 'base/file_system.dart';
14import 'build_info.dart';
15import 'bundle.dart' as bundle;
16import 'cache.dart';
17import 'features.dart';
18import 'flutter_manifest.dart';
19import 'globals.dart';
20import 'ios/plist_parser.dart';
21import 'ios/xcodeproj.dart' as xcode;
22import 'plugins.dart';
23import 'template.dart';
24
25FlutterProjectFactory get projectFactory => context.get<FlutterProjectFactory>() ?? const FlutterProjectFactory();
26
27class FlutterProjectFactory {
28  const FlutterProjectFactory();
29
30  /// Returns a [FlutterProject] view of the given directory or a ToolExit error,
31  /// if `pubspec.yaml` or `example/pubspec.yaml` is invalid.
32  FlutterProject fromDirectory(Directory directory) {
33    assert(directory != null);
34    final FlutterManifest manifest = FlutterProject._readManifest(
35      directory.childFile(bundle.defaultManifestPath).path,
36    );
37    final FlutterManifest exampleManifest = FlutterProject._readManifest(
38      FlutterProject._exampleDirectory(directory)
39          .childFile(bundle.defaultManifestPath)
40          .path,
41    );
42    return FlutterProject(directory, manifest, exampleManifest);
43  }
44}
45
46/// Represents the contents of a Flutter project at the specified [directory].
47///
48/// [FlutterManifest] information is read from `pubspec.yaml` and
49/// `example/pubspec.yaml` files on construction of a [FlutterProject] instance.
50/// The constructed instance carries an immutable snapshot representation of the
51/// presence and content of those files. Accordingly, [FlutterProject] instances
52/// should be discarded upon changes to the `pubspec.yaml` files, but can be
53/// used across changes to other files, as no other file-level information is
54/// cached.
55class FlutterProject {
56  @visibleForTesting
57  FlutterProject(this.directory, this.manifest, this._exampleManifest)
58    : assert(directory != null),
59      assert(manifest != null),
60      assert(_exampleManifest != null);
61
62  /// Returns a [FlutterProject] view of the given directory or a ToolExit error,
63  /// if `pubspec.yaml` or `example/pubspec.yaml` is invalid.
64  static FlutterProject fromDirectory(Directory directory) => projectFactory.fromDirectory(directory);
65
66  /// Returns a [FlutterProject] view of the current directory or a ToolExit error,
67  /// if `pubspec.yaml` or `example/pubspec.yaml` is invalid.
68  static FlutterProject current() => fromDirectory(fs.currentDirectory);
69
70  /// Returns a [FlutterProject] view of the given directory or a ToolExit error,
71  /// if `pubspec.yaml` or `example/pubspec.yaml` is invalid.
72  static FlutterProject fromPath(String path) => fromDirectory(fs.directory(path));
73
74  /// The location of this project.
75  final Directory directory;
76
77  /// The manifest of this project.
78  final FlutterManifest manifest;
79
80  /// The manifest of the example sub-project of this project.
81  final FlutterManifest _exampleManifest;
82
83  /// The set of organization names found in this project as
84  /// part of iOS product bundle identifier, Android application ID, or
85  /// Gradle group ID.
86  Set<String> get organizationNames {
87    final List<String> candidates = <String>[
88      ios.productBundleIdentifier,
89      android.applicationId,
90      android.group,
91      example.android.applicationId,
92      example.ios.productBundleIdentifier,
93    ];
94    return Set<String>.from(candidates
95        .map<String>(_organizationNameFromPackageName)
96        .where((String name) => name != null));
97  }
98
99  String _organizationNameFromPackageName(String packageName) {
100    if (packageName != null && 0 <= packageName.lastIndexOf('.'))
101      return packageName.substring(0, packageName.lastIndexOf('.'));
102    else
103      return null;
104  }
105
106  /// The iOS sub project of this project.
107  IosProject _ios;
108  IosProject get ios => _ios ??= IosProject.fromFlutter(this);
109
110  /// The Android sub project of this project.
111  AndroidProject _android;
112  AndroidProject get android => _android ??= AndroidProject._(this);
113
114  /// The web sub project of this project.
115  WebProject _web;
116  WebProject get web => _web ??= WebProject._(this);
117
118  /// The MacOS sub project of this project.
119  MacOSProject _macos;
120  MacOSProject get macos => _macos ??= MacOSProject._(this);
121
122  /// The Linux sub project of this project.
123  LinuxProject _linux;
124  LinuxProject get linux => _linux ??= LinuxProject._(this);
125
126  /// The Windows sub project of this project.
127  WindowsProject _windows;
128  WindowsProject get windows => _windows ??= WindowsProject._(this);
129
130  /// The Fuchsia sub project of this project.
131  FuchsiaProject _fuchsia;
132  FuchsiaProject get fuchsia => _fuchsia ??= FuchsiaProject._(this);
133
134  /// The `pubspec.yaml` file of this project.
135  File get pubspecFile => directory.childFile('pubspec.yaml');
136
137  /// The `.packages` file of this project.
138  File get packagesFile => directory.childFile('.packages');
139
140  /// The `.flutter-plugins` file of this project.
141  File get flutterPluginsFile => directory.childFile('.flutter-plugins');
142
143  /// The `.dart-tool` directory of this project.
144  Directory get dartTool => directory.childDirectory('.dart_tool');
145
146  /// The directory containing the generated code for this project.
147  Directory get generated => directory
148    .absolute
149    .childDirectory('.dart_tool')
150    .childDirectory('build')
151    .childDirectory('generated')
152    .childDirectory(manifest.appName);
153
154  /// The example sub-project of this project.
155  FlutterProject get example => FlutterProject(
156    _exampleDirectory(directory),
157    _exampleManifest,
158    FlutterManifest.empty(),
159  );
160
161  /// True if this project is a Flutter module project.
162  bool get isModule => manifest.isModule;
163
164  /// True if the Flutter project is using the AndroidX support library
165  bool get usesAndroidX => manifest.usesAndroidX;
166
167  /// True if this project has an example application.
168  bool get hasExampleApp => _exampleDirectory(directory).existsSync();
169
170  /// The directory that will contain the example if an example exists.
171  static Directory _exampleDirectory(Directory directory) => directory.childDirectory('example');
172
173  /// Reads and validates the `pubspec.yaml` file at [path], asynchronously
174  /// returning a [FlutterManifest] representation of the contents.
175  ///
176  /// Completes with an empty [FlutterManifest], if the file does not exist.
177  /// Completes with a ToolExit on validation error.
178  static FlutterManifest _readManifest(String path) {
179    FlutterManifest manifest;
180    try {
181      manifest = FlutterManifest.createFromPath(path);
182    } on YamlException catch (e) {
183      printStatus('Error detected in pubspec.yaml:', emphasis: true);
184      printError('$e');
185    }
186    if (manifest == null) {
187      throwToolExit('Please correct the pubspec.yaml file at $path');
188    }
189    return manifest;
190  }
191
192  /// Generates project files necessary to make Gradle builds work on Android
193  /// and CocoaPods+Xcode work on iOS, for app and module projects only.
194  Future<void> ensureReadyForPlatformSpecificTooling({bool checkProjects = false}) async {
195    if (!directory.existsSync() || hasExampleApp) {
196      return;
197    }
198    refreshPluginsList(this);
199    if ((android.existsSync() && checkProjects) || !checkProjects) {
200      await android.ensureReadyForPlatformSpecificTooling();
201    }
202    if ((ios.existsSync() && checkProjects) || !checkProjects) {
203      await ios.ensureReadyForPlatformSpecificTooling();
204    }
205    // TODO(stuartmorgan): Add checkProjects logic once a create workflow exists
206    // for macOS. For now, always treat checkProjects as true for macOS.
207    if (featureFlags.isMacOSEnabled && macos.existsSync()) {
208      await macos.ensureReadyForPlatformSpecificTooling();
209    }
210    if (featureFlags.isWebEnabled && web.existsSync()) {
211      await web.ensureReadyForPlatformSpecificTooling();
212    }
213    await injectPlugins(this, checkProjects: checkProjects);
214  }
215
216  /// Return the set of builders used by this package.
217  YamlMap get builders {
218    if (!pubspecFile.existsSync()) {
219      return null;
220    }
221    final YamlMap pubspec = loadYaml(pubspecFile.readAsStringSync());
222    // If the pubspec file is empty, this will be null.
223    if (pubspec == null) {
224      return null;
225    }
226    return pubspec['builders'];
227  }
228
229  /// Whether there are any builders used by this package.
230  bool get hasBuilders {
231    final YamlMap result = builders;
232    return result != null && result.isNotEmpty;
233  }
234}
235
236/// Represents an Xcode-based sub-project.
237///
238/// This defines interfaces common to iOS and macOS projects.
239abstract class XcodeBasedProject {
240  /// The parent of this project.
241  FlutterProject get parent;
242
243  /// Whether the subproject (either iOS or macOS) exists in the Flutter project.
244  bool existsSync();
245
246  /// The Xcode project (.xcodeproj directory) of the host app.
247  Directory get xcodeProject;
248
249  /// The 'project.pbxproj' file of [xcodeProject].
250  File get xcodeProjectInfoFile;
251
252  /// The Xcode workspace (.xcworkspace directory) of the host app.
253  Directory get xcodeWorkspace;
254
255  /// Contains definitions for FLUTTER_ROOT, LOCAL_ENGINE, and more flags for
256  /// the Xcode build.
257  File get generatedXcodePropertiesFile;
258
259  /// The Flutter-managed Xcode config file for [mode].
260  File xcodeConfigFor(String mode);
261
262  /// The script that exports environment variables needed for Flutter tools.
263  /// Can be run first in a Xcode Script build phase to make FLUTTER_ROOT,
264  /// LOCAL_ENGINE, and other Flutter variables available to any flutter
265  /// tooling (`flutter build`, etc) to convert into flags.
266  File get generatedEnvironmentVariableExportScript;
267
268  /// The CocoaPods 'Podfile'.
269  File get podfile;
270
271  /// The CocoaPods 'Podfile.lock'.
272  File get podfileLock;
273
274  /// The CocoaPods 'Manifest.lock'.
275  File get podManifestLock;
276
277  /// True if the host app project is using Swift.
278  bool get isSwift;
279}
280
281/// Represents the iOS sub-project of a Flutter project.
282///
283/// Instances will reflect the contents of the `ios/` sub-folder of
284/// Flutter applications and the `.ios/` sub-folder of Flutter module projects.
285class IosProject implements XcodeBasedProject {
286  IosProject.fromFlutter(this.parent);
287
288  @override
289  final FlutterProject parent;
290
291  static final RegExp _productBundleIdPattern = RegExp(r'''^\s*PRODUCT_BUNDLE_IDENTIFIER\s*=\s*(["']?)(.*?)\1;\s*$''');
292  static const String _productBundleIdVariable = r'$(PRODUCT_BUNDLE_IDENTIFIER)';
293  static const String _hostAppBundleName = 'Runner';
294
295  Directory get ephemeralDirectory => parent.directory.childDirectory('.ios');
296  Directory get _editableDirectory => parent.directory.childDirectory('ios');
297
298  /// This parent folder of `Runner.xcodeproj`.
299  Directory get hostAppRoot {
300    if (!isModule || _editableDirectory.existsSync())
301      return _editableDirectory;
302    return ephemeralDirectory;
303  }
304
305  /// The root directory of the iOS wrapping of Flutter and plugins. This is the
306  /// parent of the `Flutter/` folder into which Flutter artifacts are written
307  /// during build.
308  ///
309  /// This is the same as [hostAppRoot] except when the project is
310  /// a Flutter module with an editable host app.
311  Directory get _flutterLibRoot => isModule ? ephemeralDirectory : _editableDirectory;
312
313  /// The bundle name of the host app, `Runner.app`.
314  String get hostAppBundleName => '$_hostAppBundleName.app';
315
316  /// True, if the parent Flutter project is a module project.
317  bool get isModule => parent.isModule;
318
319  /// Whether the flutter application has an iOS project.
320  bool get exists => hostAppRoot.existsSync();
321
322  @override
323  File xcodeConfigFor(String mode) => _flutterLibRoot.childDirectory('Flutter').childFile('$mode.xcconfig');
324
325  @override
326  File get generatedEnvironmentVariableExportScript => _flutterLibRoot.childDirectory('Flutter').childFile('flutter_export_environment.sh');
327
328  @override
329  File get podfile => hostAppRoot.childFile('Podfile');
330
331  @override
332  File get podfileLock => hostAppRoot.childFile('Podfile.lock');
333
334  @override
335  File get podManifestLock => hostAppRoot.childDirectory('Pods').childFile('Manifest.lock');
336
337  /// The 'Info.plist' file of the host app.
338  File get hostInfoPlist => hostAppRoot.childDirectory(_hostAppBundleName).childFile('Info.plist');
339
340  @override
341  Directory get xcodeProject => hostAppRoot.childDirectory('$_hostAppBundleName.xcodeproj');
342
343  @override
344  File get xcodeProjectInfoFile => xcodeProject.childFile('project.pbxproj');
345
346  @override
347  Directory get xcodeWorkspace => hostAppRoot.childDirectory('$_hostAppBundleName.xcworkspace');
348
349  /// Xcode workspace shared data directory for the host app.
350  Directory get xcodeWorkspaceSharedData => xcodeWorkspace.childDirectory('xcshareddata');
351
352  /// Xcode workspace shared workspace settings file for the host app.
353  File get xcodeWorkspaceSharedSettings => xcodeWorkspaceSharedData.childFile('WorkspaceSettings.xcsettings');
354
355  @override
356  bool existsSync()  {
357    return parent.isModule || _editableDirectory.existsSync();
358  }
359
360  /// The product bundle identifier of the host app, or null if not set or if
361  /// iOS tooling needed to read it is not installed.
362  String get productBundleIdentifier {
363    String fromPlist;
364    try {
365      fromPlist = PlistParser.instance.getValueFromFile(
366        hostInfoPlist.path,
367        PlistParser.kCFBundleIdentifierKey,
368      );
369    } on FileNotFoundException {
370      // iOS tooling not found; likely not running OSX; let [fromPlist] be null
371    }
372    if (fromPlist != null && !fromPlist.contains('\$')) {
373      // Info.plist has no build variables in product bundle ID.
374      return fromPlist;
375    }
376    final String fromPbxproj = _firstMatchInFile(xcodeProjectInfoFile, _productBundleIdPattern)?.group(2);
377    if (fromPbxproj != null && (fromPlist == null || fromPlist == _productBundleIdVariable)) {
378      // Common case. Avoids parsing build settings.
379      return fromPbxproj;
380    }
381    if (fromPlist != null && xcode.xcodeProjectInterpreter.isInstalled) {
382      // General case: perform variable substitution using build settings.
383      return xcode.substituteXcodeVariables(fromPlist, buildSettings);
384    }
385    return null;
386  }
387
388  @override
389  bool get isSwift => buildSettings?.containsKey('SWIFT_VERSION') ?? false;
390
391  /// The build settings for the host app of this project, as a detached map.
392  ///
393  /// Returns null, if iOS tooling is unavailable.
394  Map<String, String> get buildSettings {
395    if (!xcode.xcodeProjectInterpreter.isInstalled)
396      return null;
397    return xcode.xcodeProjectInterpreter.getBuildSettings(xcodeProject.path, _hostAppBundleName);
398  }
399
400  Future<void> ensureReadyForPlatformSpecificTooling() async {
401    _regenerateFromTemplateIfNeeded();
402    if (!_flutterLibRoot.existsSync())
403      return;
404    await _updateGeneratedXcodeConfigIfNeeded();
405  }
406
407  Future<void> _updateGeneratedXcodeConfigIfNeeded() async {
408    if (Cache.instance.isOlderThanToolsStamp(generatedXcodePropertiesFile)) {
409      await xcode.updateGeneratedXcodeProperties(
410        project: parent,
411        buildInfo: BuildInfo.debug,
412        targetOverride: bundle.defaultMainPath,
413      );
414    }
415  }
416
417  void _regenerateFromTemplateIfNeeded() {
418    if (!isModule)
419      return;
420    final bool pubspecChanged = isOlderThanReference(entity: ephemeralDirectory, referenceFile: parent.pubspecFile);
421    final bool toolingChanged = Cache.instance.isOlderThanToolsStamp(ephemeralDirectory);
422    if (!pubspecChanged && !toolingChanged)
423      return;
424    _deleteIfExistsSync(ephemeralDirectory);
425    _overwriteFromTemplate(fs.path.join('module', 'ios', 'library'), ephemeralDirectory);
426    // Add ephemeral host app, if a editable host app does not already exist.
427    if (!_editableDirectory.existsSync()) {
428      _overwriteFromTemplate(fs.path.join('module', 'ios', 'host_app_ephemeral'), ephemeralDirectory);
429      if (hasPlugins(parent)) {
430        _overwriteFromTemplate(fs.path.join('module', 'ios', 'host_app_ephemeral_cocoapods'), ephemeralDirectory);
431      }
432    }
433  }
434
435  Future<void> makeHostAppEditable() async {
436    assert(isModule);
437    if (_editableDirectory.existsSync())
438      throwToolExit('iOS host app is already editable. To start fresh, delete the ios/ folder.');
439    _deleteIfExistsSync(ephemeralDirectory);
440    _overwriteFromTemplate(fs.path.join('module', 'ios', 'library'), ephemeralDirectory);
441    _overwriteFromTemplate(fs.path.join('module', 'ios', 'host_app_ephemeral'), _editableDirectory);
442    _overwriteFromTemplate(fs.path.join('module', 'ios', 'host_app_ephemeral_cocoapods'), _editableDirectory);
443    _overwriteFromTemplate(fs.path.join('module', 'ios', 'host_app_editable_cocoapods'), _editableDirectory);
444    await _updateGeneratedXcodeConfigIfNeeded();
445    await injectPlugins(parent);
446  }
447
448  @override
449  File get generatedXcodePropertiesFile => _flutterLibRoot.childDirectory('Flutter').childFile('Generated.xcconfig');
450
451  Directory get pluginRegistrantHost {
452    return isModule
453        ? _flutterLibRoot.childDirectory('Flutter').childDirectory('FlutterPluginRegistrant')
454        : hostAppRoot.childDirectory(_hostAppBundleName);
455  }
456
457  void _overwriteFromTemplate(String path, Directory target) {
458    final Template template = Template.fromName(path);
459    template.render(
460      target,
461      <String, dynamic>{
462        'projectName': parent.manifest.appName,
463        'iosIdentifier': parent.manifest.iosBundleIdentifier,
464      },
465      printStatusWhenWriting: false,
466      overwriteExisting: true,
467    );
468  }
469}
470
471/// Represents the Android sub-project of a Flutter project.
472///
473/// Instances will reflect the contents of the `android/` sub-folder of
474/// Flutter applications and the `.android/` sub-folder of Flutter module projects.
475class AndroidProject {
476  AndroidProject._(this.parent);
477
478  /// The parent of this project.
479  final FlutterProject parent;
480
481  static final RegExp _applicationIdPattern = RegExp('^\\s*applicationId\\s+[\'\"](.*)[\'\"]\\s*\$');
482  static final RegExp _kotlinPluginPattern = RegExp('^\\s*apply plugin\:\\s+[\'\"]kotlin-android[\'\"]\\s*\$');
483  static final RegExp _groupPattern = RegExp('^\\s*group\\s+[\'\"](.*)[\'\"]\\s*\$');
484
485  /// The Gradle root directory of the Android host app. This is the directory
486  /// containing the `app/` subdirectory and the `settings.gradle` file that
487  /// includes it in the overall Gradle project.
488  Directory get hostAppGradleRoot {
489    if (!isModule || _editableHostAppDirectory.existsSync())
490      return _editableHostAppDirectory;
491    return ephemeralDirectory;
492  }
493
494  /// The Gradle root directory of the Android wrapping of Flutter and plugins.
495  /// This is the same as [hostAppGradleRoot] except when the project is
496  /// a Flutter module with an editable host app.
497  Directory get _flutterLibGradleRoot => isModule ? ephemeralDirectory : _editableHostAppDirectory;
498
499  Directory get ephemeralDirectory => parent.directory.childDirectory('.android');
500  Directory get _editableHostAppDirectory => parent.directory.childDirectory('android');
501
502  /// True if the parent Flutter project is a module.
503  bool get isModule => parent.isModule;
504
505  /// True if the Flutter project is using the AndroidX support library
506  bool get usesAndroidX => parent.usesAndroidX;
507
508  /// True, if the app project is using Kotlin.
509  bool get isKotlin {
510    final File gradleFile = hostAppGradleRoot.childDirectory('app').childFile('build.gradle');
511    return _firstMatchInFile(gradleFile, _kotlinPluginPattern) != null;
512  }
513
514  File get appManifestFile {
515    return isUsingGradle
516        ? fs.file(fs.path.join(hostAppGradleRoot.path, 'app', 'src', 'main', 'AndroidManifest.xml'))
517        : hostAppGradleRoot.childFile('AndroidManifest.xml');
518  }
519
520  File get gradleAppOutV1File => gradleAppOutV1Directory.childFile('app-debug.apk');
521
522  Directory get gradleAppOutV1Directory {
523    return fs.directory(fs.path.join(hostAppGradleRoot.path, 'app', 'build', 'outputs', 'apk'));
524  }
525
526  /// Whether the current flutter project has an Android sub-project.
527  bool existsSync() {
528    return parent.isModule || _editableHostAppDirectory.existsSync();
529  }
530
531  bool get isUsingGradle {
532    return hostAppGradleRoot.childFile('build.gradle').existsSync();
533  }
534
535  String get applicationId {
536    final File gradleFile = hostAppGradleRoot.childDirectory('app').childFile('build.gradle');
537    return _firstMatchInFile(gradleFile, _applicationIdPattern)?.group(1);
538  }
539
540  String get group {
541    final File gradleFile = hostAppGradleRoot.childFile('build.gradle');
542    return _firstMatchInFile(gradleFile, _groupPattern)?.group(1);
543  }
544
545  Future<void> ensureReadyForPlatformSpecificTooling() async {
546    if (isModule && _shouldRegenerateFromTemplate()) {
547      _regenerateLibrary();
548      // Add ephemeral host app, if an editable host app does not already exist.
549      if (!_editableHostAppDirectory.existsSync()) {
550        _overwriteFromTemplate(fs.path.join('module', 'android', 'host_app_common'), ephemeralDirectory);
551        _overwriteFromTemplate(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 isOlderThanReference(entity: ephemeralDirectory, referenceFile: parent.pubspecFile)
562        || Cache.instance.isOlderThanToolsStamp(ephemeralDirectory);
563  }
564
565  Future<void> makeHostAppEditable() async {
566    assert(isModule);
567    if (_editableHostAppDirectory.existsSync())
568      throwToolExit('Android host app is already editable. To start fresh, delete the android/ folder.');
569    _regenerateLibrary();
570    _overwriteFromTemplate(fs.path.join('module', 'android', 'host_app_common'), _editableHostAppDirectory);
571    _overwriteFromTemplate(fs.path.join('module', 'android', 'host_app_editable'), _editableHostAppDirectory);
572    _overwriteFromTemplate(fs.path.join('module', 'android', 'gradle'), _editableHostAppDirectory);
573    gradle.injectGradleWrapper(_editableHostAppDirectory);
574    gradle.writeLocalProperties(_editableHostAppDirectory.childFile('local.properties'));
575    await injectPlugins(parent);
576  }
577
578  File get localPropertiesFile => _flutterLibGradleRoot.childFile('local.properties');
579
580  Directory get pluginRegistrantHost => _flutterLibGradleRoot.childDirectory(isModule ? 'Flutter' : 'app');
581
582  void _regenerateLibrary() {
583    _deleteIfExistsSync(ephemeralDirectory);
584    _overwriteFromTemplate(fs.path.join('module', 'android', 'library'), ephemeralDirectory);
585    _overwriteFromTemplate(fs.path.join('module', 'android', 'gradle'), ephemeralDirectory);
586    gradle.injectGradleWrapper(ephemeralDirectory);
587  }
588
589  void _overwriteFromTemplate(String path, Directory target) {
590    final Template template = Template.fromName(path);
591    template.render(
592      target,
593      <String, dynamic>{
594        'projectName': parent.manifest.appName,
595        'androidIdentifier': parent.manifest.androidPackage,
596        'androidX': usesAndroidX,
597      },
598      printStatusWhenWriting: false,
599      overwriteExisting: true,
600    );
601  }
602}
603
604/// Represents the web sub-project of a Flutter project.
605class WebProject {
606  WebProject._(this.parent);
607
608  final FlutterProject parent;
609
610  /// Whether this flutter project has a web sub-project.
611  bool existsSync() {
612    return parent.directory.childDirectory('web').existsSync()
613      && indexFile.existsSync();
614  }
615
616  /// The html file used to host the flutter web application.
617  File get indexFile => parent.directory
618      .childDirectory('web')
619      .childFile('index.html');
620
621  Future<void> ensureReadyForPlatformSpecificTooling() async {}
622}
623
624/// Deletes [directory] with all content.
625void _deleteIfExistsSync(Directory directory) {
626  if (directory.existsSync())
627    directory.deleteSync(recursive: true);
628}
629
630
631/// Returns the first line-based match for [regExp] in [file].
632///
633/// Assumes UTF8 encoding.
634Match _firstMatchInFile(File file, RegExp regExp) {
635  if (!file.existsSync()) {
636    return null;
637  }
638  for (String line in file.readAsLinesSync()) {
639    final Match match = regExp.firstMatch(line);
640    if (match != null) {
641      return match;
642    }
643  }
644  return null;
645}
646
647/// The macOS sub project.
648class MacOSProject implements XcodeBasedProject {
649  MacOSProject._(this.parent);
650
651  @override
652  final FlutterProject parent;
653
654  static const String _hostAppBundleName = 'Runner';
655
656  @override
657  bool existsSync() => _macOSDirectory.existsSync();
658
659  Directory get _macOSDirectory => parent.directory.childDirectory('macos');
660
661  /// The directory in the project that is managed by Flutter. As much as
662  /// possible, files that are edited by Flutter tooling after initial project
663  /// creation should live here.
664  Directory get managedDirectory => _macOSDirectory.childDirectory('Flutter');
665
666  /// The subdirectory of [managedDirectory] that contains files that are
667  /// generated on the fly. All generated files that are not intended to be
668  /// checked in should live here.
669  Directory get ephemeralDirectory => managedDirectory.childDirectory('ephemeral');
670
671  /// The xcfilelist used to track the inputs for the Flutter script phase in
672  /// the Xcode build.
673  File get inputFileList => ephemeralDirectory.childFile('FlutterInputs.xcfilelist');
674
675  /// The xcfilelist used to track the outputs for the Flutter script phase in
676  /// the Xcode build.
677  File get outputFileList => ephemeralDirectory.childFile('FlutterOutputs.xcfilelist');
678
679  @override
680  File get generatedXcodePropertiesFile => ephemeralDirectory.childFile('Flutter-Generated.xcconfig');
681
682  @override
683  File xcodeConfigFor(String mode) => managedDirectory.childFile('Flutter-$mode.xcconfig');
684
685  @override
686  File get generatedEnvironmentVariableExportScript => ephemeralDirectory.childFile('flutter_export_environment.sh');
687
688  @override
689  File get podfile => _macOSDirectory.childFile('Podfile');
690
691  @override
692  File get podfileLock => _macOSDirectory.childFile('Podfile.lock');
693
694  @override
695  File get podManifestLock => _macOSDirectory.childDirectory('Pods').childFile('Manifest.lock');
696
697  @override
698  Directory get xcodeProject => _macOSDirectory.childDirectory('$_hostAppBundleName.xcodeproj');
699
700  @override
701  File get xcodeProjectInfoFile => xcodeProject.childFile('project.pbxproj');
702
703  @override
704  Directory get xcodeWorkspace => _macOSDirectory.childDirectory('$_hostAppBundleName.xcworkspace');
705
706  @override
707  bool get isSwift => true;
708
709  /// The file where the Xcode build will write the name of the built app.
710  ///
711  /// Ideally this will be replaced in the future with inspection of the Runner
712  /// scheme's target.
713  File get nameFile => ephemeralDirectory.childFile('.app_filename');
714
715  Future<void> ensureReadyForPlatformSpecificTooling() async {
716    // TODO(stuartmorgan): Add create-from-template logic here.
717    await _updateGeneratedXcodeConfigIfNeeded();
718  }
719
720  Future<void> _updateGeneratedXcodeConfigIfNeeded() async {
721    if (Cache.instance.isOlderThanToolsStamp(generatedXcodePropertiesFile)) {
722      await xcode.updateGeneratedXcodeProperties(
723        project: parent,
724        buildInfo: BuildInfo.debug,
725        useMacOSConfig: true,
726        setSymroot: false,
727      );
728    }
729  }
730}
731
732/// The Windows sub project
733class WindowsProject {
734  WindowsProject._(this.project);
735
736  final FlutterProject project;
737
738  bool existsSync() => _editableDirectory.existsSync();
739
740  Directory get _editableDirectory => project.directory.childDirectory('windows');
741
742  Directory get _cacheDirectory => _editableDirectory.childDirectory('flutter');
743
744  /// Contains definitions for FLUTTER_ROOT, LOCAL_ENGINE, and more flags for
745  /// the build.
746  File get generatedPropertySheetFile => _cacheDirectory.childFile('Generated.props');
747
748  // The MSBuild project file.
749  File get vcprojFile => _editableDirectory.childFile('Runner.vcxproj');
750
751  // The MSBuild solution file.
752  File get solutionFile => _editableDirectory.childFile('Runner.sln');
753
754  /// The file where the VS build will write the name of the built app.
755  ///
756  /// Ideally this will be replaced in the future with inspection of the project.
757  File get nameFile => _cacheDirectory.childFile('exe_filename');
758}
759
760/// The Linux sub project.
761class LinuxProject {
762  LinuxProject._(this.project);
763
764  final FlutterProject project;
765
766  Directory get editableHostAppDirectory => project.directory.childDirectory('linux');
767
768  Directory get cacheDirectory => editableHostAppDirectory.childDirectory('flutter');
769
770  bool existsSync() => editableHostAppDirectory.existsSync();
771
772  /// The Linux project makefile.
773  File get makeFile => editableHostAppDirectory.childFile('Makefile');
774}
775
776/// The Fuchisa sub project
777class FuchsiaProject {
778  FuchsiaProject._(this.project);
779
780  final FlutterProject project;
781
782  Directory _editableHostAppDirectory;
783  Directory get editableHostAppDirectory =>
784      _editableHostAppDirectory ??= project.directory.childDirectory('fuchsia');
785
786  bool existsSync() => editableHostAppDirectory.existsSync();
787
788  Directory _meta;
789  Directory get meta =>
790      _meta ??= editableHostAppDirectory.childDirectory('meta');
791}
792