diff --git a/packages/flutter_tools/lib/src/base/utils.dart b/packages/flutter_tools/lib/src/base/utils.dart index 81b16fa4cc7..6e5d8d0f940 100644 --- a/packages/flutter_tools/lib/src/base/utils.dart +++ b/packages/flutter_tools/lib/src/base/utils.dart @@ -443,3 +443,19 @@ String interpolateString(String toInterpolate, Map replacementVa List interpolateStringList(List toInterpolate, Map replacementValues) { return toInterpolate.map((String s) => interpolateString(s, replacementValues)).toList(); } + +/// Returns the first line-based match for [regExp] in [file]. +/// +/// Assumes UTF8 encoding. +Match? firstMatchInFile(File file, RegExp regExp) { + if (!file.existsSync()) { + return null; + } + for (final String line in file.readAsLinesSync()) { + final Match? match = regExp.firstMatch(line); + if (match != null) { + return match; + } + } + return null; +} diff --git a/packages/flutter_tools/lib/src/cmake.dart b/packages/flutter_tools/lib/src/cmake.dart index 1ea1846030c..19589d46835 100644 --- a/packages/flutter_tools/lib/src/cmake.dart +++ b/packages/flutter_tools/lib/src/cmake.dart @@ -3,7 +3,7 @@ // found in the LICENSE file. import 'base/file_system.dart'; -import 'project.dart'; +import 'cmake_project.dart'; /// Extracts the `BINARY_NAME` from a project's CMake file. /// diff --git a/packages/flutter_tools/lib/src/cmake_project.dart b/packages/flutter_tools/lib/src/cmake_project.dart new file mode 100644 index 00000000000..dbd3fff1f53 --- /dev/null +++ b/packages/flutter_tools/lib/src/cmake_project.dart @@ -0,0 +1,175 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:meta/meta.dart'; +import 'package:xml/xml.dart'; + +import 'base/common.dart'; +import 'base/file_system.dart'; +import 'base/utils.dart'; +import 'cmake.dart'; +import 'platform_plugins.dart'; +import 'project.dart'; + +/// Represents a CMake-based sub-project. +/// +/// This defines interfaces common to Windows and Linux projects. +abstract class CmakeBasedProject { + /// The parent of this project. + FlutterProject get parent; + + /// Whether the subproject (either Windows or Linux) exists in the Flutter project. + bool existsSync(); + + /// The native project CMake specification. + File get cmakeFile; + + /// Contains definitions for the Flutter library and the tool. + File get managedCmakeFile; + + /// Contains definitions for FLUTTER_ROOT, LOCAL_ENGINE, and more flags for + /// the build. + File get generatedCmakeConfigFile; + + /// Included CMake with rules and variables for plugin builds. + File get generatedPluginCmakeFile; + + /// The directory to write plugin symlinks. + Directory get pluginSymlinkDirectory; +} + +/// The Windows sub project. +class WindowsProject extends FlutterProjectPlatform implements CmakeBasedProject { + WindowsProject.fromFlutter(this.parent); + + @override + final FlutterProject parent; + + @override + String get pluginConfigKey => WindowsPlugin.kConfigKey; + + String get _childDirectory => 'windows'; + + @override + bool existsSync() => _editableDirectory.existsSync() && cmakeFile.existsSync(); + + @override + File get cmakeFile => _editableDirectory.childFile('CMakeLists.txt'); + + @override + File get managedCmakeFile => managedDirectory.childFile('CMakeLists.txt'); + + @override + File get generatedCmakeConfigFile => ephemeralDirectory.childFile('generated_config.cmake'); + + @override + File get generatedPluginCmakeFile => managedDirectory.childFile('generated_plugins.cmake'); + + @override + Directory get pluginSymlinkDirectory => ephemeralDirectory.childDirectory('.plugin_symlinks'); + + Directory get _editableDirectory => parent.directory.childDirectory(_childDirectory); + + /// 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'); + + Future ensureReadyForPlatformSpecificTooling() async {} +} + +/// The Windows UWP version of the Windows project. +class WindowsUwpProject extends WindowsProject { + WindowsUwpProject.fromFlutter(FlutterProject parent) : super.fromFlutter(parent); + + @override + String get _childDirectory => 'winuwp'; + + File get runnerCmakeFile => _editableDirectory.childDirectory('runner_uwp').childFile('CMakeLists.txt'); + + /// Eventually this will be used to check if the user's unstable project needs to be regenerated. + int? get projectVersion => int.tryParse(_editableDirectory.childFile('project_version').readAsStringSync()); + + /// Retrieve the GUID of the UWP package. + String? get packageGuid => _packageGuid ??= getCmakePackageGuid(runnerCmakeFile); + String? _packageGuid; + + File get appManifest => _editableDirectory.childDirectory('runner_uwp').childFile('appxmanifest.in'); + + String? get packageVersion => _packageVersion ??= parseAppVersion(this); + String? _packageVersion; +} + +@visibleForTesting +String? parseAppVersion(WindowsUwpProject project) { + final File appManifestFile = project.appManifest; + if (!appManifestFile.existsSync()) { + return null; + } + + XmlDocument document; + try { + document = XmlDocument.parse(appManifestFile.readAsStringSync()); + } on XmlParserException { + throwToolExit('Error parsing $appManifestFile. Please ensure that the appx manifest is a valid XML document and try again.'); + } + for (final XmlElement metaData in document.findAllElements('Identity')) { + return metaData.getAttribute('Version'); + } + return null; +} + +/// The Linux sub project. +class LinuxProject extends FlutterProjectPlatform implements CmakeBasedProject { + LinuxProject.fromFlutter(this.parent); + + @override + final FlutterProject parent; + + @override + String get pluginConfigKey => LinuxPlugin.kConfigKey; + + static final RegExp _applicationIdPattern = RegExp(r'''^\s*set\s*\(\s*APPLICATION_ID\s*"(.*)"\s*\)\s*$'''); + + Directory get _editableDirectory => parent.directory.childDirectory('linux'); + + /// 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'); + + @override + bool existsSync() => _editableDirectory.existsSync(); + + @override + File get cmakeFile => _editableDirectory.childFile('CMakeLists.txt'); + + @override + File get managedCmakeFile => managedDirectory.childFile('CMakeLists.txt'); + + @override + File get generatedCmakeConfigFile => ephemeralDirectory.childFile('generated_config.cmake'); + + @override + File get generatedPluginCmakeFile => managedDirectory.childFile('generated_plugins.cmake'); + + @override + Directory get pluginSymlinkDirectory => ephemeralDirectory.childDirectory('.plugin_symlinks'); + + Future ensureReadyForPlatformSpecificTooling() async {} + + String? get applicationId { + return firstMatchInFile(cmakeFile, _applicationIdPattern)?.group(1); + } +} diff --git a/packages/flutter_tools/lib/src/ios/application_package.dart b/packages/flutter_tools/lib/src/ios/application_package.dart index 0d018a338d5..ce97f97ef45 100644 --- a/packages/flutter_tools/lib/src/ios/application_package.dart +++ b/packages/flutter_tools/lib/src/ios/application_package.dart @@ -6,7 +6,7 @@ import '../application_package.dart'; import '../base/file_system.dart'; import '../build_info.dart'; import '../globals_null_migrated.dart' as globals; -import '../project.dart'; +import '../xcode_project.dart'; import 'plist_parser.dart'; /// Tests whether a [Directory] is an iOS bundle directory. diff --git a/packages/flutter_tools/lib/src/ios/migrations/deployment_target_migration.dart b/packages/flutter_tools/lib/src/ios/migrations/deployment_target_migration.dart index 082e515b417..58a37977f06 100644 --- a/packages/flutter_tools/lib/src/ios/migrations/deployment_target_migration.dart +++ b/packages/flutter_tools/lib/src/ios/migrations/deployment_target_migration.dart @@ -5,7 +5,7 @@ import '../../base/file_system.dart'; import '../../base/logger.dart'; import '../../base/project_migrator.dart'; -import '../../project.dart'; +import '../../xcode_project.dart'; /// Update the minimum iOS deployment version to the minimum allowed by Xcode without causing a warning. class DeploymentTargetMigration extends ProjectMigrator { diff --git a/packages/flutter_tools/lib/src/ios/migrations/project_base_configuration_migration.dart b/packages/flutter_tools/lib/src/ios/migrations/project_base_configuration_migration.dart index 329dd9d99fc..eff8099149a 100644 --- a/packages/flutter_tools/lib/src/ios/migrations/project_base_configuration_migration.dart +++ b/packages/flutter_tools/lib/src/ios/migrations/project_base_configuration_migration.dart @@ -5,7 +5,7 @@ import '../../base/file_system.dart'; import '../../base/logger.dart'; import '../../base/project_migrator.dart'; -import '../../project.dart'; +import '../../xcode_project.dart'; // The Runner target should inherit its build configuration from Generated.xcconfig. // However the top-level Runner project should not inherit any build configuration so diff --git a/packages/flutter_tools/lib/src/ios/migrations/project_build_location_migration.dart b/packages/flutter_tools/lib/src/ios/migrations/project_build_location_migration.dart index e0bed32027b..118f5a696d5 100644 --- a/packages/flutter_tools/lib/src/ios/migrations/project_build_location_migration.dart +++ b/packages/flutter_tools/lib/src/ios/migrations/project_build_location_migration.dart @@ -5,7 +5,7 @@ import '../../base/file_system.dart'; import '../../base/logger.dart'; import '../../base/project_migrator.dart'; -import '../../project.dart'; +import '../../xcode_project.dart'; // Update the xcodeproj build location. Legacy build location does not work with Swift Packages. class ProjectBuildLocationMigration extends ProjectMigrator { diff --git a/packages/flutter_tools/lib/src/ios/migrations/remove_framework_link_and_embedding_migration.dart b/packages/flutter_tools/lib/src/ios/migrations/remove_framework_link_and_embedding_migration.dart index 3ad65dfd55f..317672647d9 100644 --- a/packages/flutter_tools/lib/src/ios/migrations/remove_framework_link_and_embedding_migration.dart +++ b/packages/flutter_tools/lib/src/ios/migrations/remove_framework_link_and_embedding_migration.dart @@ -6,8 +6,8 @@ import '../../base/common.dart'; import '../../base/file_system.dart'; import '../../base/logger.dart'; import '../../base/project_migrator.dart'; -import '../../project.dart'; import '../../reporting/reporting.dart'; +import '../../xcode_project.dart'; // Xcode 11.4 requires linked and embedded frameworks to contain all targeted architectures before build phases are run. // This caused issues switching between a real device and simulator due to architecture mismatch. diff --git a/packages/flutter_tools/lib/src/ios/migrations/xcode_build_system_migration.dart b/packages/flutter_tools/lib/src/ios/migrations/xcode_build_system_migration.dart index 4472e2acf25..49024a39ef8 100644 --- a/packages/flutter_tools/lib/src/ios/migrations/xcode_build_system_migration.dart +++ b/packages/flutter_tools/lib/src/ios/migrations/xcode_build_system_migration.dart @@ -5,7 +5,7 @@ import '../../base/file_system.dart'; import '../../base/logger.dart'; import '../../base/project_migrator.dart'; -import '../../project.dart'; +import '../../xcode_project.dart'; // Xcode legacy build system no longer supported by Xcode. // Set in https://github.com/flutter/flutter/pull/21901/. diff --git a/packages/flutter_tools/lib/src/linux/application_package.dart b/packages/flutter_tools/lib/src/linux/application_package.dart index 07e8c397357..0f99299244b 100644 --- a/packages/flutter_tools/lib/src/linux/application_package.dart +++ b/packages/flutter_tools/lib/src/linux/application_package.dart @@ -6,8 +6,8 @@ import '../application_package.dart'; import '../base/file_system.dart'; import '../build_info.dart'; import '../cmake.dart'; +import '../cmake_project.dart'; import '../globals_null_migrated.dart' as globals; -import '../project.dart'; abstract class LinuxApp extends ApplicationPackage { LinuxApp({required String projectBundleId}) : super(id: projectBundleId); diff --git a/packages/flutter_tools/lib/src/linux/build_linux.dart b/packages/flutter_tools/lib/src/linux/build_linux.dart index 40045670f3b..2c0e1c7bb29 100644 --- a/packages/flutter_tools/lib/src/linux/build_linux.dart +++ b/packages/flutter_tools/lib/src/linux/build_linux.dart @@ -12,11 +12,11 @@ import '../base/utils.dart'; import '../build_info.dart'; import '../cache.dart'; import '../cmake.dart'; +import '../cmake_project.dart'; import '../convert.dart'; import '../flutter_plugins.dart'; import '../globals_null_migrated.dart' as globals; import '../migrations/cmake_custom_command_migration.dart'; -import '../project.dart'; // Matches the following error and warning patterns: // - ::: (fatal) error: diff --git a/packages/flutter_tools/lib/src/macos/application_package.dart b/packages/flutter_tools/lib/src/macos/application_package.dart index baeba59868b..ed629ce05af 100644 --- a/packages/flutter_tools/lib/src/macos/application_package.dart +++ b/packages/flutter_tools/lib/src/macos/application_package.dart @@ -13,7 +13,7 @@ import '../base/utils.dart'; import '../build_info.dart'; import '../globals_null_migrated.dart' as globals; import '../ios/plist_parser.dart'; -import '../project.dart'; +import '../xcode_project.dart'; /// Tests whether a [FileSystemEntity] is an macOS bundle directory. bool _isBundleDirectory(FileSystemEntity entity) => diff --git a/packages/flutter_tools/lib/src/macos/cocoapods.dart b/packages/flutter_tools/lib/src/macos/cocoapods.dart index 771935aabef..acf9d210161 100644 --- a/packages/flutter_tools/lib/src/macos/cocoapods.dart +++ b/packages/flutter_tools/lib/src/macos/cocoapods.dart @@ -17,8 +17,8 @@ import '../base/version.dart'; import '../build_info.dart'; import '../cache.dart'; import '../ios/xcodeproj.dart'; -import '../project.dart'; import '../reporting/reporting.dart'; +import '../xcode_project.dart'; const String noCocoaPodsConsequence = ''' CocoaPods is used to retrieve the iOS and macOS platform side's plugin code that responds to your plugin usage on the Dart side. diff --git a/packages/flutter_tools/lib/src/macos/migrations/remove_macos_framework_link_and_embedding_migration.dart b/packages/flutter_tools/lib/src/macos/migrations/remove_macos_framework_link_and_embedding_migration.dart index eecca696bc1..d41f76a1f12 100644 --- a/packages/flutter_tools/lib/src/macos/migrations/remove_macos_framework_link_and_embedding_migration.dart +++ b/packages/flutter_tools/lib/src/macos/migrations/remove_macos_framework_link_and_embedding_migration.dart @@ -6,8 +6,8 @@ import '../../base/common.dart'; import '../../base/file_system.dart'; import '../../base/logger.dart'; import '../../base/project_migrator.dart'; -import '../../project.dart'; import '../../reporting/reporting.dart'; +import '../../xcode_project.dart'; // Remove the linking and embedding logic from the Xcode project to give the tool more control over these. class RemoveMacOSFrameworkLinkAndEmbeddingMigration extends ProjectMigrator { diff --git a/packages/flutter_tools/lib/src/migrations/cmake_custom_command_migration.dart b/packages/flutter_tools/lib/src/migrations/cmake_custom_command_migration.dart index fd09cd2366c..6a537f00d79 100644 --- a/packages/flutter_tools/lib/src/migrations/cmake_custom_command_migration.dart +++ b/packages/flutter_tools/lib/src/migrations/cmake_custom_command_migration.dart @@ -5,7 +5,7 @@ import '../base/file_system.dart'; import '../base/logger.dart'; import '../base/project_migrator.dart'; -import '../project.dart'; +import '../cmake_project.dart'; // CMake's add_custom_command() should use VERBATIM to handle escaping of spaces // and special characters correctly. diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart index 59b9f66bf7f..1454d8f05fa 100644 --- a/packages/flutter_tools/lib/src/project.dart +++ b/packages/flutter_tools/lib/src/project.dart @@ -8,22 +8,23 @@ import 'package:yaml/yaml.dart'; import '../src/convert.dart'; import 'android/gradle_utils.dart' as gradle; -import 'artifacts.dart'; import 'base/common.dart'; +import 'base/error_handling_io.dart'; import 'base/file_system.dart'; import 'base/logger.dart'; -import 'build_info.dart'; +import 'base/utils.dart'; import 'bundle.dart' as bundle; -import 'cmake.dart'; +import 'cmake_project.dart'; import 'features.dart'; import 'flutter_manifest.dart'; import 'flutter_plugins.dart'; import 'globals_null_migrated.dart' as globals; -import 'ios/plist_parser.dart'; -import 'ios/xcode_build_settings.dart' as xcode; -import 'ios/xcodeproj.dart'; import 'platform_plugins.dart'; import 'template.dart'; +import 'xcode_project.dart'; + +export 'cmake_project.dart'; +export 'xcode_project.dart'; class FlutterProjectFactory { FlutterProjectFactory({ @@ -175,19 +176,19 @@ class FlutterProject { /// The MacOS sub project of this project. MacOSProject? _macos; - MacOSProject get macos => _macos ??= MacOSProject._(this); + MacOSProject get macos => _macos ??= MacOSProject.fromFlutter(this); /// The Linux sub project of this project. LinuxProject? _linux; - LinuxProject get linux => _linux ??= LinuxProject._(this); + LinuxProject get linux => _linux ??= LinuxProject.fromFlutter(this); /// The Windows sub project of this project. WindowsProject? _windows; - WindowsProject get windows => _windows ??= WindowsProject._(this); + WindowsProject get windows => _windows ??= WindowsProject.fromFlutter(this); /// The Windows UWP sub project of this project. WindowsUwpProject? _windowUwp; - WindowsUwpProject get windowsUwp => _windowUwp ??= WindowsUwpProject._(this); + WindowsUwpProject get windowsUwp => _windowUwp ??= WindowsUwpProject.fromFlutter(this); /// The Fuchsia sub project of this project. FuchsiaProject? _fuchsia; @@ -371,484 +372,6 @@ abstract class FlutterProjectPlatform { bool existsSync(); } -/// 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); - - /// 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; - - /// The CocoaPods 'Podfile'. - File get podfile; - - /// The CocoaPods 'Podfile.lock'. - File get podfileLock; - - /// The CocoaPods 'Manifest.lock'. - File get podManifestLock; -} - -/// Represents a CMake-based sub-project. -/// -/// This defines interfaces common to Windows and Linux projects. -abstract class CmakeBasedProject { - /// The parent of this project. - FlutterProject get parent; - - /// Whether the subproject (either Windows or Linux) exists in the Flutter project. - bool existsSync(); - - /// The native project CMake specification. - File get cmakeFile; - - /// Contains definitions for the Flutter library and the tool. - File get managedCmakeFile; - - /// Contains definitions for FLUTTER_ROOT, LOCAL_ENGINE, and more flags for - /// the build. - File get generatedCmakeConfigFile; - - /// Included CMake with rules and variables for plugin builds. - File get generatedPluginCmakeFile; - - /// The directory to write plugin symlinks. - Directory get pluginSymlinkDirectory; -} - -/// Represents the iOS sub-project of a Flutter project. -/// -/// Instances will reflect the contents of the `ios/` sub-folder of -/// Flutter applications and the `.ios/` sub-folder of Flutter module projects. -class IosProject extends FlutterProjectPlatform implements XcodeBasedProject { - IosProject.fromFlutter(this.parent); - - @override - final FlutterProject parent; - - @override - String get pluginConfigKey => IOSPlugin.kConfigKey; - - static final RegExp _productBundleIdPattern = RegExp(r'''^\s*PRODUCT_BUNDLE_IDENTIFIER\s*=\s*(["']?)(.*?)\1;\s*$'''); - static const String _productBundleIdVariable = r'$(PRODUCT_BUNDLE_IDENTIFIER)'; - static const String _hostAppProjectName = 'Runner'; - - Directory get ephemeralModuleDirectory => parent.directory.childDirectory('.ios'); - Directory get _editableDirectory => parent.directory.childDirectory('ios'); - - /// This parent folder of `Runner.xcodeproj`. - Directory get hostAppRoot { - if (!isModule || _editableDirectory.existsSync()) { - return _editableDirectory; - } - return ephemeralModuleDirectory; - } - - /// 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 - /// a Flutter module with an editable host app. - Directory get _flutterLibRoot => isModule ? ephemeralModuleDirectory : _editableDirectory; - - /// True, if the parent Flutter project is a module project. - bool get isModule => parent.isModule; - - /// Whether the flutter application has an iOS project. - bool get exists => hostAppRoot.existsSync(); - - /// Put generated files here. - Directory get ephemeralDirectory => _flutterLibRoot.childDirectory('Flutter').childDirectory('ephemeral'); - - @override - File xcodeConfigFor(String mode) => _flutterLibRoot.childDirectory('Flutter').childFile('$mode.xcconfig'); - - @override - File get generatedEnvironmentVariableExportScript => _flutterLibRoot.childDirectory('Flutter').childFile('flutter_export_environment.sh'); - - @override - File get podfile => hostAppRoot.childFile('Podfile'); - - @override - File get podfileLock => hostAppRoot.childFile('Podfile.lock'); - - @override - File get podManifestLock => hostAppRoot.childDirectory('Pods').childFile('Manifest.lock'); - - /// The default 'Info.plist' file of the host app. The developer can change this location in Xcode. - File get defaultHostInfoPlist => hostAppRoot.childDirectory(_hostAppProjectName).childFile('Info.plist'); - - File get appFrameworkInfoPlist => _flutterLibRoot.childDirectory('Flutter').childFile('AppFrameworkInfo.plist'); - - Directory get symlinks => _flutterLibRoot.childDirectory('.symlinks'); - - @override - Directory get xcodeProject => hostAppRoot.childDirectory('$_hostAppProjectName.xcodeproj'); - - @override - File get xcodeProjectInfoFile => xcodeProject.childFile('project.pbxproj'); - - File get xcodeProjectWorkspaceData => - xcodeProject - .childDirectory('project.xcworkspace') - .childFile('contents.xcworkspacedata'); - - @override - Directory get xcodeWorkspace => hostAppRoot.childDirectory('$_hostAppProjectName.xcworkspace'); - - /// 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'); - - @override - bool existsSync() { - return parent.isModule || _editableDirectory.existsSync(); - } - - /// The product bundle identifier of the host app, or null if not set or if - /// iOS tooling needed to read it is not installed. - Future productBundleIdentifier(BuildInfo? buildInfo) async { - if (!existsSync()) { - return null; - } - return _productBundleIdentifier ??= await _parseProductBundleIdentifier(buildInfo); - } - String? _productBundleIdentifier; - - Future _parseProductBundleIdentifier(BuildInfo? buildInfo) async { - String? fromPlist; - final File defaultInfoPlist = defaultHostInfoPlist; - // Users can change the location of the Info.plist. - // Try parsing the default, first. - if (defaultInfoPlist.existsSync()) { - try { - fromPlist = globals.plistParser.getValueFromFile( - defaultHostInfoPlist.path, - PlistParser.kCFBundleIdentifierKey, - ); - } on FileNotFoundException { - // iOS tooling not found; likely not running OSX; let [fromPlist] be null - } - if (fromPlist != null && !fromPlist.contains(r'$')) { - // Info.plist has no build variables in product bundle ID. - return fromPlist; - } - } - final Map? allBuildSettings = await buildSettingsForBuildInfo(buildInfo); - if (allBuildSettings != null) { - if (fromPlist != null) { - // Perform variable substitution using build settings. - return substituteXcodeVariables(fromPlist, allBuildSettings); - } - return allBuildSettings['PRODUCT_BUNDLE_IDENTIFIER']; - } - - // On non-macOS platforms, parse the first PRODUCT_BUNDLE_IDENTIFIER from - // the project file. This can return the wrong bundle identifier if additional - // bundles have been added to the project and are found first, like frameworks - // or companion watchOS projects. However, on non-macOS platforms this is - // only used for display purposes and to regenerate organization names, so - // best-effort is probably fine. - final String? fromPbxproj = _firstMatchInFile(xcodeProjectInfoFile, _productBundleIdPattern)?.group(2); - if (fromPbxproj != null && (fromPlist == null || fromPlist == _productBundleIdVariable)) { - return fromPbxproj; - } - - return null; - } - - /// The bundle name of the host app, `My App.app`. - Future hostAppBundleName(BuildInfo buildInfo) async { - if (!existsSync()) { - return null; - } - return _hostAppBundleName ??= await _parseHostAppBundleName(buildInfo); - } - String? _hostAppBundleName; - - Future _parseHostAppBundleName(BuildInfo buildInfo) async { - // The product name and bundle name are derived from the display name, which the user - // is instructed to change in Xcode as part of deploying to the App Store. - // https://flutter.dev/docs/deployment/ios#review-xcode-project-settings - // The only source of truth for the name is Xcode's interpretation of the build settings. - String? productName; - if (globals.xcodeProjectInterpreter?.isInstalled == true) { - final Map? xcodeBuildSettings = await buildSettingsForBuildInfo(buildInfo); - if (xcodeBuildSettings != null) { - productName = xcodeBuildSettings['FULL_PRODUCT_NAME']; - } - } - if (productName == null) { - globals.printTrace('FULL_PRODUCT_NAME not present, defaulting to $_hostAppProjectName'); - } - return productName ?? '$_hostAppProjectName.app'; - } - - /// The build settings for the host app of this project, as a detached map. - /// - /// Returns null, if iOS tooling is unavailable. - Future?> buildSettingsForBuildInfo(BuildInfo? buildInfo, { EnvironmentType environmentType = EnvironmentType.physical }) async { - if (!existsSync()) { - return null; - } - final XcodeProjectInfo? info = await projectInfo(); - if (info == null) { - return null; - } - - final String? scheme = info.schemeFor(buildInfo); - if (scheme == null) { - info.reportFlavorNotFoundAndExit(); - } - - final String? configuration = (await projectInfo())?.buildConfigurationFor( - buildInfo, - scheme, - ); - final XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(environmentType: environmentType, scheme: scheme, configuration: configuration); - final Map? currentBuildSettings = _buildSettingsByBuildContext[buildContext]; - if (currentBuildSettings == null) { - final Map? calculatedBuildSettings = await _xcodeProjectBuildSettings(buildContext); - if (calculatedBuildSettings != null) { - _buildSettingsByBuildContext[buildContext] = calculatedBuildSettings; - } - } - return _buildSettingsByBuildContext[buildContext]; - } - - final Map> _buildSettingsByBuildContext = >{}; - - Future projectInfo() async { - final XcodeProjectInterpreter? xcodeProjectInterpreter = globals.xcodeProjectInterpreter; - if (!xcodeProject.existsSync() || xcodeProjectInterpreter == null || !xcodeProjectInterpreter.isInstalled) { - return null; - } - return _projectInfo ??= await xcodeProjectInterpreter.getInfo(hostAppRoot.path); - } - XcodeProjectInfo? _projectInfo; - - Future?> _xcodeProjectBuildSettings(XcodeProjectBuildContext buildContext) async { - final XcodeProjectInterpreter? xcodeProjectInterpreter = globals.xcodeProjectInterpreter; - if (xcodeProjectInterpreter == null || !xcodeProjectInterpreter.isInstalled) { - return null; - } - - final Map buildSettings = await xcodeProjectInterpreter.getBuildSettings( - xcodeProject.path, - buildContext: buildContext, - ); - if (buildSettings != null && buildSettings.isNotEmpty) { - // No timeouts, flakes, or errors. - return buildSettings; - } - return null; - } - - Future ensureReadyForPlatformSpecificTooling() async { - await _regenerateFromTemplateIfNeeded(); - if (!_flutterLibRoot.existsSync()) { - return; - } - await _updateGeneratedXcodeConfigIfNeeded(); - } - - /// Check if one the [targets] of the project is a watchOS companion app target. - Future containsWatchCompanion(List targets, BuildInfo buildInfo) async { - final String? bundleIdentifier = await productBundleIdentifier(buildInfo); - // A bundle identifier is required for a companion app. - if (bundleIdentifier == null) { - return false; - } - for (final String target in targets) { - // Create Info.plist file of the target. - final File infoFile = hostAppRoot.childDirectory(target).childFile('Info.plist'); - // The Info.plist file of a target contains the key WKCompanionAppBundleIdentifier, - // if it is a watchOS companion app. - if (infoFile.existsSync()) { - final String? fromPlist = globals.plistParser.getValueFromFile(infoFile.path, 'WKCompanionAppBundleIdentifier'); - if (bundleIdentifier == fromPlist) { - return true; - } - - // The key WKCompanionAppBundleIdentifier might contain an xcode variable - // that needs to be substituted before comparing it with bundle id - if (fromPlist != null && fromPlist.contains(r'$')) { - final Map? allBuildSettings = await buildSettingsForBuildInfo(buildInfo); - if (allBuildSettings != null) { - final String substituedVariable = substituteXcodeVariables(fromPlist, allBuildSettings); - if (substituedVariable == bundleIdentifier) { - return true; - } - } - } - } - } - return false; - } - - Future _updateGeneratedXcodeConfigIfNeeded() async { - if (globals.cache.isOlderThanToolsStamp(generatedXcodePropertiesFile)) { - await xcode.updateGeneratedXcodeProperties( - project: parent, - buildInfo: BuildInfo.debug, - targetOverride: bundle.defaultMainPath, - ); - } - } - - Future _regenerateFromTemplateIfNeeded() async { - if (!isModule) { - return; - } - final bool pubspecChanged = globals.fsUtils.isOlderThanReference( - entity: ephemeralModuleDirectory, - referenceFile: parent.pubspecFile, - ); - final bool toolingChanged = globals.cache.isOlderThanToolsStamp(ephemeralModuleDirectory); - if (!pubspecChanged && !toolingChanged) { - return; - } - - _deleteIfExistsSync(ephemeralModuleDirectory); - await _overwriteFromTemplate( - globals.fs.path.join('module', 'ios', 'library'), - ephemeralModuleDirectory, - ); - // Add ephemeral host app, if a editable host app does not already exist. - if (!_editableDirectory.existsSync()) { - await _overwriteFromTemplate( - globals.fs.path.join('module', 'ios', 'host_app_ephemeral'), - ephemeralModuleDirectory, - ); - if (hasPlugins(parent)) { - await _overwriteFromTemplate( - globals.fs.path.join('module', 'ios', 'host_app_ephemeral_cocoapods'), - ephemeralModuleDirectory, - ); - } - // Use release mode so host project can link on bitcode variant. - _copyEngineArtifactToProject(BuildMode.release, EnvironmentType.physical); - } - } - - void _copyEngineArtifactToProject(BuildMode mode, EnvironmentType environmentType) { - // Copy 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 = globals.fs.directory( - globals.artifacts?.getArtifactPath( - Artifact.flutterXcframework, - platform: TargetPlatform.ios, - mode: mode, - environmentType: environmentType, - ) - ); - if (framework.existsSync()) { - copyDirectory( - framework, - engineCopyDirectory.childDirectory('Flutter.xcframework'), - ); - } - } - - @override - File get generatedXcodePropertiesFile => _flutterLibRoot - .childDirectory('Flutter') - .childFile('Generated.xcconfig'); - - /// No longer compiled to this location. - /// - /// Used only for "flutter clean" to remove old references. - Directory get deprecatedCompiledDartFramework => _flutterLibRoot - .childDirectory('Flutter') - .childDirectory('App.framework'); - - /// No longer copied to this location. - /// - /// Used only for "flutter clean" to remove old references. - Directory get deprecatedProjectFlutterFramework => _flutterLibRoot - .childDirectory('Flutter') - .childDirectory('Flutter.framework'); - - /// Used only for "flutter clean" to remove old references. - File get flutterPodspec => _flutterLibRoot - .childDirectory('Flutter') - .childFile('Flutter.podspec'); - - Directory get pluginRegistrantHost { - return isModule - ? _flutterLibRoot - .childDirectory('Flutter') - .childDirectory('FlutterPluginRegistrant') - : hostAppRoot.childDirectory(_hostAppProjectName); - } - - File get pluginRegistrantHeader { - final Directory registryDirectory = isModule ? pluginRegistrantHost.childDirectory('Classes') : pluginRegistrantHost; - return registryDirectory.childFile('GeneratedPluginRegistrant.h'); - } - - File get pluginRegistrantImplementation { - final Directory registryDirectory = isModule ? pluginRegistrantHost.childDirectory('Classes') : pluginRegistrantHost; - return registryDirectory.childFile('GeneratedPluginRegistrant.m'); - } - - Directory get engineCopyDirectory { - return isModule - ? ephemeralModuleDirectory.childDirectory('Flutter').childDirectory('engine') - : hostAppRoot.childDirectory('Flutter'); - } - - Future _overwriteFromTemplate(String path, Directory target) async { - final Template template = await Template.fromName( - path, - fileSystem: globals.fs, - templateManifest: null, - logger: globals.logger, - templateRenderer: globals.templateRenderer, - ); - final String iosBundleIdentifier = parent.manifest.iosBundleIdentifier ?? 'com.example.${parent.manifest.appName}'; - template.render( - target, - { - 'ios': true, - 'projectName': parent.manifest.appName, - 'iosIdentifier': iosBundleIdentifier, - }, - printStatusWhenWriting: false, - overwriteExisting: true, - ); - } -} - /// Represents the Android sub-project of a Flutter project. /// /// Instances will reflect the contents of the `android/` sub-folder of @@ -918,7 +441,7 @@ class AndroidProject extends FlutterProjectPlatform { /// 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; + return firstMatchInFile(gradleFile, _kotlinPluginPattern) != null; } File get appManifestFile { @@ -945,12 +468,12 @@ class AndroidProject extends FlutterProjectPlatform { String? get applicationId { final File gradleFile = hostAppGradleRoot.childDirectory('app').childFile('build.gradle'); - return _firstMatchInFile(gradleFile, _applicationIdPattern)?.group(1); + return firstMatchInFile(gradleFile, _applicationIdPattern)?.group(1); } String? get group { final File gradleFile = hostAppGradleRoot.childFile('build.gradle'); - return _firstMatchInFile(gradleFile, _groupPattern)?.group(1); + return firstMatchInFile(gradleFile, _groupPattern)?.group(1); } /// The build directory where the Android artifacts are placed. @@ -1002,7 +525,7 @@ to migrate your project. Directory get pluginRegistrantHost => _flutterLibGradleRoot.childDirectory(isModule ? 'Flutter' : 'app'); Future _regenerateLibrary() async { - _deleteIfExistsSync(ephemeralDirectory); + ErrorHandlingFileSystem.deleteIfExists(ephemeralDirectory, recursive: true); await _overwriteFromTemplate(globals.fs.path.join( 'module', 'android', @@ -1107,249 +630,6 @@ class WebProject extends FlutterProjectPlatform { Future ensureReadyForPlatformSpecificTooling() async {} } -/// Deletes [directory] with all content. -void _deleteIfExistsSync(Directory directory) { - if (directory.existsSync()) { - directory.deleteSync(recursive: true); - } -} - - -/// Returns the first line-based match for [regExp] in [file]. -/// -/// Assumes UTF8 encoding. -Match? _firstMatchInFile(File file, RegExp regExp) { - if (!file.existsSync()) { - return null; - } - for (final String line in file.readAsLinesSync()) { - final Match? match = regExp.firstMatch(line); - if (match != null) { - return match; - } - } - return null; -} - -/// The macOS sub project. -class MacOSProject extends FlutterProjectPlatform implements XcodeBasedProject { - MacOSProject._(this.parent); - - @override - final FlutterProject parent; - - @override - String get pluginConfigKey => MacOSPlugin.kConfigKey; - - static const String _hostAppProjectName = 'Runner'; - - @override - bool existsSync() => _macOSDirectory.existsSync(); - - Directory get _macOSDirectory => parent.directory.childDirectory('macos'); - - /// 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'); - - /// 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'); - - @override - File get generatedXcodePropertiesFile => ephemeralDirectory.childFile('Flutter-Generated.xcconfig'); - - @override - File xcodeConfigFor(String mode) => managedDirectory.childFile('Flutter-$mode.xcconfig'); - - @override - File get generatedEnvironmentVariableExportScript => ephemeralDirectory.childFile('flutter_export_environment.sh'); - - @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 - Directory get xcodeProject => _macOSDirectory.childDirectory('$_hostAppProjectName.xcodeproj'); - - @override - File get xcodeProjectInfoFile => xcodeProject.childFile('project.pbxproj'); - - @override - Directory get xcodeWorkspace => _macOSDirectory.childDirectory('$_hostAppProjectName.xcworkspace'); - - /// The file where the Xcode build will write the name of the built app. - /// - /// Ideally this will be replaced in the future with inspection of the Runner - /// scheme's target. - File get nameFile => ephemeralDirectory.childFile('.app_filename'); - - Future ensureReadyForPlatformSpecificTooling() async { - // TODO(stuartmorgan): Add create-from-template logic here. - await _updateGeneratedXcodeConfigIfNeeded(); - } - - Future _updateGeneratedXcodeConfigIfNeeded() async { - if (globals.cache.isOlderThanToolsStamp(generatedXcodePropertiesFile)) { - await xcode.updateGeneratedXcodeProperties( - project: parent, - buildInfo: BuildInfo.debug, - useMacOSConfig: true, - ); - } - } -} - -/// The Windows sub project. -class WindowsProject extends FlutterProjectPlatform implements CmakeBasedProject { - WindowsProject._(this.parent); - - @override - final FlutterProject parent; - - @override - String get pluginConfigKey => WindowsPlugin.kConfigKey; - - String get _childDirectory => 'windows'; - - @override - bool existsSync() => _editableDirectory.existsSync() && cmakeFile.existsSync(); - - @override - File get cmakeFile => _editableDirectory.childFile('CMakeLists.txt'); - - @override - File get managedCmakeFile => managedDirectory.childFile('CMakeLists.txt'); - - @override - File get generatedCmakeConfigFile => ephemeralDirectory.childFile('generated_config.cmake'); - - @override - File get generatedPluginCmakeFile => managedDirectory.childFile('generated_plugins.cmake'); - - @override - Directory get pluginSymlinkDirectory => ephemeralDirectory.childDirectory('.plugin_symlinks'); - - Directory get _editableDirectory => parent.directory.childDirectory(_childDirectory); - - /// 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'); - - Future ensureReadyForPlatformSpecificTooling() async {} -} - -/// The Windows UWP version of the Windows project. -class WindowsUwpProject extends WindowsProject { - WindowsUwpProject._(FlutterProject parent) : super._(parent); - - @override - String get _childDirectory => 'winuwp'; - - File get runnerCmakeFile => _editableDirectory.childDirectory('runner_uwp').childFile('CMakeLists.txt'); - - /// Eventually this will be used to check if the user's unstable project needs to be regenerated. - int? get projectVersion => int.tryParse(_editableDirectory.childFile('project_version').readAsStringSync()); - - /// Retrieve the GUID of the UWP package. - String? get packageGuid => _packageGuid ??= getCmakePackageGuid(runnerCmakeFile); - String? _packageGuid; - - File get appManifest => _editableDirectory.childDirectory('runner_uwp').childFile('appxmanifest.in'); - - String? get packageVersion => _packageVersion ??= parseAppVersion(this); - String? _packageVersion; -} - -@visibleForTesting -String? parseAppVersion(WindowsUwpProject project) { - final File appManifestFile = project.appManifest; - if (!appManifestFile.existsSync()) { - return null; - } - - XmlDocument document; - try { - document = XmlDocument.parse(appManifestFile.readAsStringSync()); - } on XmlParserException { - throwToolExit('Error parsing $appManifestFile. Please ensure that the appx manifest is a valid XML document and try again.'); - } - for (final XmlElement metaData in document.findAllElements('Identity')) { - return metaData.getAttribute('Version'); - } - return null; -} - -/// The Linux sub project. -class LinuxProject extends FlutterProjectPlatform implements CmakeBasedProject { - LinuxProject._(this.parent); - - @override - final FlutterProject parent; - - @override - String get pluginConfigKey => LinuxPlugin.kConfigKey; - - static final RegExp _applicationIdPattern = RegExp(r'''^\s*set\s*\(\s*APPLICATION_ID\s*"(.*)"\s*\)\s*$'''); - - Directory get _editableDirectory => parent.directory.childDirectory('linux'); - - /// 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'); - - @override - bool existsSync() => _editableDirectory.existsSync(); - - @override - File get cmakeFile => _editableDirectory.childFile('CMakeLists.txt'); - - @override - File get managedCmakeFile => managedDirectory.childFile('CMakeLists.txt'); - - @override - File get generatedCmakeConfigFile => ephemeralDirectory.childFile('generated_config.cmake'); - - @override - File get generatedPluginCmakeFile => managedDirectory.childFile('generated_plugins.cmake'); - - @override - Directory get pluginSymlinkDirectory => ephemeralDirectory.childDirectory('.plugin_symlinks'); - - Future ensureReadyForPlatformSpecificTooling() async {} - - String? get applicationId { - return _firstMatchInFile(cmakeFile, _applicationIdPattern)?.group(1); - } -} - /// The Fuchsia sub project. class FuchsiaProject { FuchsiaProject._(this.project); diff --git a/packages/flutter_tools/lib/src/windows/application_package.dart b/packages/flutter_tools/lib/src/windows/application_package.dart index de5e2129edd..c569fffebdf 100644 --- a/packages/flutter_tools/lib/src/windows/application_package.dart +++ b/packages/flutter_tools/lib/src/windows/application_package.dart @@ -11,8 +11,8 @@ import '../base/file_system.dart'; import '../base/utils.dart'; import '../build_info.dart'; import '../cmake.dart'; +import '../cmake_project.dart'; import '../globals_null_migrated.dart' as globals; -import '../project.dart'; abstract class WindowsApp extends ApplicationPackage { WindowsApp({@required String projectBundleId}) : super(id: projectBundleId); diff --git a/packages/flutter_tools/lib/src/windows/build_windows.dart b/packages/flutter_tools/lib/src/windows/build_windows.dart index 3c4cf739b6d..5a87af23f7d 100644 --- a/packages/flutter_tools/lib/src/windows/build_windows.dart +++ b/packages/flutter_tools/lib/src/windows/build_windows.dart @@ -14,11 +14,11 @@ import '../base/utils.dart'; import '../build_info.dart'; import '../cache.dart'; import '../cmake.dart'; +import '../cmake_project.dart'; import '../convert.dart'; import '../flutter_plugins.dart'; import '../globals_null_migrated.dart' as globals; import '../migrations/cmake_custom_command_migration.dart'; -import '../project.dart'; import 'install_manifest.dart'; import 'visual_studio.dart'; diff --git a/packages/flutter_tools/lib/src/windows/install_manifest.dart b/packages/flutter_tools/lib/src/windows/install_manifest.dart index 9d6e9a8e09e..4de691150de 100644 --- a/packages/flutter_tools/lib/src/windows/install_manifest.dart +++ b/packages/flutter_tools/lib/src/windows/install_manifest.dart @@ -12,7 +12,7 @@ import '../base/file_system.dart'; import '../base/logger.dart'; import '../base/platform.dart'; import '../build_info.dart'; -import '../project.dart'; +import '../cmake_project.dart'; /// Generate an install manifest that is required for CMAKE on UWP projects. Future createManifest({ diff --git a/packages/flutter_tools/lib/src/xcode_project.dart b/packages/flutter_tools/lib/src/xcode_project.dart new file mode 100644 index 00000000000..beb5f8a40ba --- /dev/null +++ b/packages/flutter_tools/lib/src/xcode_project.dart @@ -0,0 +1,553 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'artifacts.dart'; +import 'base/error_handling_io.dart'; +import 'base/file_system.dart'; +import 'base/utils.dart'; +import 'build_info.dart'; +import 'bundle.dart' as bundle; +import 'flutter_plugins.dart'; +import 'globals_null_migrated.dart' as globals; +import 'ios/plist_parser.dart'; +import 'ios/xcode_build_settings.dart' as xcode; +import 'ios/xcodeproj.dart'; +import 'platform_plugins.dart'; +import 'project.dart'; +import 'template.dart'; + +/// 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); + + /// 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; + + /// The CocoaPods 'Podfile'. + File get podfile; + + /// The CocoaPods 'Podfile.lock'. + File get podfileLock; + + /// The CocoaPods 'Manifest.lock'. + File get podManifestLock; +} + +/// Represents the iOS sub-project of a Flutter project. +/// +/// Instances will reflect the contents of the `ios/` sub-folder of +/// Flutter applications and the `.ios/` sub-folder of Flutter module projects. +class IosProject extends FlutterProjectPlatform implements XcodeBasedProject { + IosProject.fromFlutter(this.parent); + + @override + final FlutterProject parent; + + @override + String get pluginConfigKey => IOSPlugin.kConfigKey; + + static final RegExp _productBundleIdPattern = RegExp(r'''^\s*PRODUCT_BUNDLE_IDENTIFIER\s*=\s*(["']?)(.*?)\1;\s*$'''); + static const String _productBundleIdVariable = r'$(PRODUCT_BUNDLE_IDENTIFIER)'; + static const String _hostAppProjectName = 'Runner'; + + Directory get ephemeralModuleDirectory => parent.directory.childDirectory('.ios'); + Directory get _editableDirectory => parent.directory.childDirectory('ios'); + + /// This parent folder of `Runner.xcodeproj`. + Directory get hostAppRoot { + if (!isModule || _editableDirectory.existsSync()) { + return _editableDirectory; + } + return ephemeralModuleDirectory; + } + + /// 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 + /// a Flutter module with an editable host app. + Directory get _flutterLibRoot => isModule ? ephemeralModuleDirectory : _editableDirectory; + + /// True, if the parent Flutter project is a module project. + bool get isModule => parent.isModule; + + /// Whether the flutter application has an iOS project. + bool get exists => hostAppRoot.existsSync(); + + /// Put generated files here. + Directory get ephemeralDirectory => _flutterLibRoot.childDirectory('Flutter').childDirectory('ephemeral'); + + @override + File xcodeConfigFor(String mode) => _flutterLibRoot.childDirectory('Flutter').childFile('$mode.xcconfig'); + + @override + File get generatedEnvironmentVariableExportScript => _flutterLibRoot.childDirectory('Flutter').childFile('flutter_export_environment.sh'); + + @override + File get podfile => hostAppRoot.childFile('Podfile'); + + @override + File get podfileLock => hostAppRoot.childFile('Podfile.lock'); + + @override + File get podManifestLock => hostAppRoot.childDirectory('Pods').childFile('Manifest.lock'); + + /// The default 'Info.plist' file of the host app. The developer can change this location in Xcode. + File get defaultHostInfoPlist => hostAppRoot.childDirectory(_hostAppProjectName).childFile('Info.plist'); + + File get appFrameworkInfoPlist => _flutterLibRoot.childDirectory('Flutter').childFile('AppFrameworkInfo.plist'); + + Directory get symlinks => _flutterLibRoot.childDirectory('.symlinks'); + + @override + Directory get xcodeProject => hostAppRoot.childDirectory('$_hostAppProjectName.xcodeproj'); + + @override + File get xcodeProjectInfoFile => xcodeProject.childFile('project.pbxproj'); + + File get xcodeProjectWorkspaceData => + xcodeProject + .childDirectory('project.xcworkspace') + .childFile('contents.xcworkspacedata'); + + @override + Directory get xcodeWorkspace => hostAppRoot.childDirectory('$_hostAppProjectName.xcworkspace'); + + /// 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'); + + @override + bool existsSync() { + return parent.isModule || _editableDirectory.existsSync(); + } + + /// The product bundle identifier of the host app, or null if not set or if + /// iOS tooling needed to read it is not installed. + Future productBundleIdentifier(BuildInfo? buildInfo) async { + if (!existsSync()) { + return null; + } + return _productBundleIdentifier ??= await _parseProductBundleIdentifier(buildInfo); + } + String? _productBundleIdentifier; + + Future _parseProductBundleIdentifier(BuildInfo? buildInfo) async { + String? fromPlist; + final File defaultInfoPlist = defaultHostInfoPlist; + // Users can change the location of the Info.plist. + // Try parsing the default, first. + if (defaultInfoPlist.existsSync()) { + try { + fromPlist = globals.plistParser.getValueFromFile( + defaultHostInfoPlist.path, + PlistParser.kCFBundleIdentifierKey, + ); + } on FileNotFoundException { + // iOS tooling not found; likely not running OSX; let [fromPlist] be null + } + if (fromPlist != null && !fromPlist.contains(r'$')) { + // Info.plist has no build variables in product bundle ID. + return fromPlist; + } + } + final Map? allBuildSettings = await buildSettingsForBuildInfo(buildInfo); + if (allBuildSettings != null) { + if (fromPlist != null) { + // Perform variable substitution using build settings. + return substituteXcodeVariables(fromPlist, allBuildSettings); + } + return allBuildSettings['PRODUCT_BUNDLE_IDENTIFIER']; + } + + // On non-macOS platforms, parse the first PRODUCT_BUNDLE_IDENTIFIER from + // the project file. This can return the wrong bundle identifier if additional + // bundles have been added to the project and are found first, like frameworks + // or companion watchOS projects. However, on non-macOS platforms this is + // only used for display purposes and to regenerate organization names, so + // best-effort is probably fine. + final String? fromPbxproj = firstMatchInFile(xcodeProjectInfoFile, _productBundleIdPattern)?.group(2); + if (fromPbxproj != null && (fromPlist == null || fromPlist == _productBundleIdVariable)) { + return fromPbxproj; + } + + return null; + } + + /// The bundle name of the host app, `My App.app`. + Future hostAppBundleName(BuildInfo buildInfo) async { + if (!existsSync()) { + return null; + } + return _hostAppBundleName ??= await _parseHostAppBundleName(buildInfo); + } + String? _hostAppBundleName; + + Future _parseHostAppBundleName(BuildInfo buildInfo) async { + // The product name and bundle name are derived from the display name, which the user + // is instructed to change in Xcode as part of deploying to the App Store. + // https://flutter.dev/docs/deployment/ios#review-xcode-project-settings + // The only source of truth for the name is Xcode's interpretation of the build settings. + String? productName; + if (globals.xcodeProjectInterpreter?.isInstalled == true) { + final Map? xcodeBuildSettings = await buildSettingsForBuildInfo(buildInfo); + if (xcodeBuildSettings != null) { + productName = xcodeBuildSettings['FULL_PRODUCT_NAME']; + } + } + if (productName == null) { + globals.printTrace('FULL_PRODUCT_NAME not present, defaulting to $_hostAppProjectName'); + } + return productName ?? '$_hostAppProjectName.app'; + } + + /// The build settings for the host app of this project, as a detached map. + /// + /// Returns null, if iOS tooling is unavailable. + Future?> buildSettingsForBuildInfo(BuildInfo? buildInfo, { EnvironmentType environmentType = EnvironmentType.physical }) async { + if (!existsSync()) { + return null; + } + final XcodeProjectInfo? info = await projectInfo(); + if (info == null) { + return null; + } + + final String? scheme = info.schemeFor(buildInfo); + if (scheme == null) { + info.reportFlavorNotFoundAndExit(); + } + + final String? configuration = (await projectInfo())?.buildConfigurationFor( + buildInfo, + scheme, + ); + final XcodeProjectBuildContext buildContext = XcodeProjectBuildContext(environmentType: environmentType, scheme: scheme, configuration: configuration); + final Map? currentBuildSettings = _buildSettingsByBuildContext[buildContext]; + if (currentBuildSettings == null) { + final Map? calculatedBuildSettings = await _xcodeProjectBuildSettings(buildContext); + if (calculatedBuildSettings != null) { + _buildSettingsByBuildContext[buildContext] = calculatedBuildSettings; + } + } + return _buildSettingsByBuildContext[buildContext]; + } + + final Map> _buildSettingsByBuildContext = >{}; + + Future projectInfo() async { + final XcodeProjectInterpreter? xcodeProjectInterpreter = globals.xcodeProjectInterpreter; + if (!xcodeProject.existsSync() || xcodeProjectInterpreter == null || !xcodeProjectInterpreter.isInstalled) { + return null; + } + return _projectInfo ??= await xcodeProjectInterpreter.getInfo(hostAppRoot.path); + } + XcodeProjectInfo? _projectInfo; + + Future?> _xcodeProjectBuildSettings(XcodeProjectBuildContext buildContext) async { + final XcodeProjectInterpreter? xcodeProjectInterpreter = globals.xcodeProjectInterpreter; + if (xcodeProjectInterpreter == null || !xcodeProjectInterpreter.isInstalled) { + return null; + } + + final Map buildSettings = await xcodeProjectInterpreter.getBuildSettings( + xcodeProject.path, + buildContext: buildContext, + ); + if (buildSettings != null && buildSettings.isNotEmpty) { + // No timeouts, flakes, or errors. + return buildSettings; + } + return null; + } + + Future ensureReadyForPlatformSpecificTooling() async { + await _regenerateFromTemplateIfNeeded(); + if (!_flutterLibRoot.existsSync()) { + return; + } + await _updateGeneratedXcodeConfigIfNeeded(); + } + + /// Check if one the [targets] of the project is a watchOS companion app target. + Future containsWatchCompanion(List targets, BuildInfo buildInfo) async { + final String? bundleIdentifier = await productBundleIdentifier(buildInfo); + // A bundle identifier is required for a companion app. + if (bundleIdentifier == null) { + return false; + } + for (final String target in targets) { + // Create Info.plist file of the target. + final File infoFile = hostAppRoot.childDirectory(target).childFile('Info.plist'); + // The Info.plist file of a target contains the key WKCompanionAppBundleIdentifier, + // if it is a watchOS companion app. + if (infoFile.existsSync()) { + final String? fromPlist = globals.plistParser.getValueFromFile(infoFile.path, 'WKCompanionAppBundleIdentifier'); + if (bundleIdentifier == fromPlist) { + return true; + } + + // The key WKCompanionAppBundleIdentifier might contain an xcode variable + // that needs to be substituted before comparing it with bundle id + if (fromPlist != null && fromPlist.contains(r'$')) { + final Map? allBuildSettings = await buildSettingsForBuildInfo(buildInfo); + if (allBuildSettings != null) { + final String substituedVariable = substituteXcodeVariables(fromPlist, allBuildSettings); + if (substituedVariable == bundleIdentifier) { + return true; + } + } + } + } + } + return false; + } + + Future _updateGeneratedXcodeConfigIfNeeded() async { + if (globals.cache.isOlderThanToolsStamp(generatedXcodePropertiesFile)) { + await xcode.updateGeneratedXcodeProperties( + project: parent, + buildInfo: BuildInfo.debug, + targetOverride: bundle.defaultMainPath, + ); + } + } + + Future _regenerateFromTemplateIfNeeded() async { + if (!isModule) { + return; + } + final bool pubspecChanged = globals.fsUtils.isOlderThanReference( + entity: ephemeralModuleDirectory, + referenceFile: parent.pubspecFile, + ); + final bool toolingChanged = globals.cache.isOlderThanToolsStamp(ephemeralModuleDirectory); + if (!pubspecChanged && !toolingChanged) { + return; + } + + ErrorHandlingFileSystem.deleteIfExists(ephemeralModuleDirectory, recursive: true); + await _overwriteFromTemplate( + globals.fs.path.join('module', 'ios', 'library'), + ephemeralModuleDirectory, + ); + // Add ephemeral host app, if a editable host app does not already exist. + if (!_editableDirectory.existsSync()) { + await _overwriteFromTemplate( + globals.fs.path.join('module', 'ios', 'host_app_ephemeral'), + ephemeralModuleDirectory, + ); + if (hasPlugins(parent)) { + await _overwriteFromTemplate( + globals.fs.path.join('module', 'ios', 'host_app_ephemeral_cocoapods'), + ephemeralModuleDirectory, + ); + } + // Use release mode so host project can link on bitcode variant. + _copyEngineArtifactToProject(BuildMode.release, EnvironmentType.physical); + } + } + + void _copyEngineArtifactToProject(BuildMode mode, EnvironmentType environmentType) { + // Copy 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 = globals.fs.directory( + globals.artifacts?.getArtifactPath( + Artifact.flutterXcframework, + platform: TargetPlatform.ios, + mode: mode, + environmentType: environmentType, + ) + ); + if (framework.existsSync()) { + copyDirectory( + framework, + engineCopyDirectory.childDirectory('Flutter.xcframework'), + ); + } + } + + @override + File get generatedXcodePropertiesFile => _flutterLibRoot + .childDirectory('Flutter') + .childFile('Generated.xcconfig'); + + /// No longer compiled to this location. + /// + /// Used only for "flutter clean" to remove old references. + Directory get deprecatedCompiledDartFramework => _flutterLibRoot + .childDirectory('Flutter') + .childDirectory('App.framework'); + + /// No longer copied to this location. + /// + /// Used only for "flutter clean" to remove old references. + Directory get deprecatedProjectFlutterFramework => _flutterLibRoot + .childDirectory('Flutter') + .childDirectory('Flutter.framework'); + + /// Used only for "flutter clean" to remove old references. + File get flutterPodspec => _flutterLibRoot + .childDirectory('Flutter') + .childFile('Flutter.podspec'); + + Directory get pluginRegistrantHost { + return isModule + ? _flutterLibRoot + .childDirectory('Flutter') + .childDirectory('FlutterPluginRegistrant') + : hostAppRoot.childDirectory(_hostAppProjectName); + } + + File get pluginRegistrantHeader { + final Directory registryDirectory = isModule ? pluginRegistrantHost.childDirectory('Classes') : pluginRegistrantHost; + return registryDirectory.childFile('GeneratedPluginRegistrant.h'); + } + + File get pluginRegistrantImplementation { + final Directory registryDirectory = isModule ? pluginRegistrantHost.childDirectory('Classes') : pluginRegistrantHost; + return registryDirectory.childFile('GeneratedPluginRegistrant.m'); + } + + Directory get engineCopyDirectory { + return isModule + ? ephemeralModuleDirectory.childDirectory('Flutter').childDirectory('engine') + : hostAppRoot.childDirectory('Flutter'); + } + + Future _overwriteFromTemplate(String path, Directory target) async { + final Template template = await Template.fromName( + path, + fileSystem: globals.fs, + templateManifest: null, + logger: globals.logger, + templateRenderer: globals.templateRenderer, + ); + final String iosBundleIdentifier = parent.manifest.iosBundleIdentifier ?? 'com.example.${parent.manifest.appName}'; + template.render( + target, + { + 'ios': true, + 'projectName': parent.manifest.appName, + 'iosIdentifier': iosBundleIdentifier, + }, + printStatusWhenWriting: false, + overwriteExisting: true, + ); + } +} + +/// The macOS sub project. +class MacOSProject extends FlutterProjectPlatform implements XcodeBasedProject { + MacOSProject.fromFlutter(this.parent); + + @override + final FlutterProject parent; + + @override + String get pluginConfigKey => MacOSPlugin.kConfigKey; + + static const String _hostAppProjectName = 'Runner'; + + @override + bool existsSync() => _macOSDirectory.existsSync(); + + Directory get _macOSDirectory => parent.directory.childDirectory('macos'); + + /// 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'); + + /// 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'); + + @override + File get generatedXcodePropertiesFile => ephemeralDirectory.childFile('Flutter-Generated.xcconfig'); + + @override + File xcodeConfigFor(String mode) => managedDirectory.childFile('Flutter-$mode.xcconfig'); + + @override + File get generatedEnvironmentVariableExportScript => ephemeralDirectory.childFile('flutter_export_environment.sh'); + + @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 + Directory get xcodeProject => _macOSDirectory.childDirectory('$_hostAppProjectName.xcodeproj'); + + @override + File get xcodeProjectInfoFile => xcodeProject.childFile('project.pbxproj'); + + @override + Directory get xcodeWorkspace => _macOSDirectory.childDirectory('$_hostAppProjectName.xcworkspace'); + + /// The file where the Xcode build will write the name of the built app. + /// + /// Ideally this will be replaced in the future with inspection of the Runner + /// scheme's target. + File get nameFile => ephemeralDirectory.childFile('.app_filename'); + + Future ensureReadyForPlatformSpecificTooling() async { + // TODO(stuartmorgan): Add create-from-template logic here. + await _updateGeneratedXcodeConfigIfNeeded(); + } + + Future _updateGeneratedXcodeConfigIfNeeded() async { + if (globals.cache.isOlderThanToolsStamp(generatedXcodePropertiesFile)) { + await xcode.updateGeneratedXcodeProperties( + project: parent, + buildInfo: BuildInfo.debug, + useMacOSConfig: true, + ); + } + } +} diff --git a/packages/flutter_tools/test/general.shard/ios/ios_project_migration_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_project_migration_test.dart index 0a609331577..c270c041001 100644 --- a/packages/flutter_tools/test/general.shard/ios/ios_project_migration_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/ios_project_migration_test.dart @@ -11,8 +11,8 @@ import 'package:flutter_tools/src/ios/migrations/project_base_configuration_migr import 'package:flutter_tools/src/ios/migrations/project_build_location_migration.dart'; import 'package:flutter_tools/src/ios/migrations/remove_framework_link_and_embedding_migration.dart'; import 'package:flutter_tools/src/ios/migrations/xcode_build_system_migration.dart'; -import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/reporting/reporting.dart'; +import 'package:flutter_tools/src/xcode_project.dart'; import 'package:test/fake.dart'; import '../../src/common.dart'; diff --git a/packages/flutter_tools/test/general.shard/macos/macos_project_migration_test.dart b/packages/flutter_tools/test/general.shard/macos/macos_project_migration_test.dart index eef5b65f1c7..175c864756b 100644 --- a/packages/flutter_tools/test/general.shard/macos/macos_project_migration_test.dart +++ b/packages/flutter_tools/test/general.shard/macos/macos_project_migration_test.dart @@ -7,8 +7,8 @@ import 'package:file/memory.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/project_migrator.dart'; import 'package:flutter_tools/src/macos/migrations/remove_macos_framework_link_and_embedding_migration.dart'; -import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/reporting/reporting.dart'; +import 'package:flutter_tools/src/xcode_project.dart'; import 'package:test/fake.dart'; import '../../src/common.dart'; diff --git a/packages/flutter_tools/test/general.shard/migrations/cmake_project_migration_test.dart b/packages/flutter_tools/test/general.shard/migrations/cmake_project_migration_test.dart index 78307d4013e..88f148f1218 100644 --- a/packages/flutter_tools/test/general.shard/migrations/cmake_project_migration_test.dart +++ b/packages/flutter_tools/test/general.shard/migrations/cmake_project_migration_test.dart @@ -7,8 +7,8 @@ import 'package:file/memory.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/project_migrator.dart'; import 'package:flutter_tools/src/base/terminal.dart'; +import 'package:flutter_tools/src/cmake_project.dart'; import 'package:flutter_tools/src/migrations/cmake_custom_command_migration.dart'; -import 'package:flutter_tools/src/project.dart'; import 'package:test/fake.dart'; import '../../src/common.dart';