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