Split project.dart into CMake and Xcode projects (#85359)

This commit is contained in:
Jenn Magder 2021-06-28 11:31:04 -07:00 committed by GitHub
parent 610ee89b17
commit e028d0f046
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 778 additions and 754 deletions

View File

@ -443,3 +443,19 @@ String interpolateString(String toInterpolate, Map<String, String> replacementVa
List<String> interpolateStringList(List<String> toInterpolate, Map<String, String> replacementValues) { List<String> interpolateStringList(List<String> toInterpolate, Map<String, String> replacementValues) {
return toInterpolate.map((String s) => interpolateString(s, replacementValues)).toList(); 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;
}

View File

@ -3,7 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'base/file_system.dart'; import 'base/file_system.dart';
import 'project.dart'; import 'cmake_project.dart';
/// Extracts the `BINARY_NAME` from a project's CMake file. /// Extracts the `BINARY_NAME` from a project's CMake file.
/// ///

View File

@ -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<void> 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<void> ensureReadyForPlatformSpecificTooling() async {}
String? get applicationId {
return firstMatchInFile(cmakeFile, _applicationIdPattern)?.group(1);
}
}

View File

@ -6,7 +6,7 @@ import '../application_package.dart';
import '../base/file_system.dart'; import '../base/file_system.dart';
import '../build_info.dart'; import '../build_info.dart';
import '../globals_null_migrated.dart' as globals; import '../globals_null_migrated.dart' as globals;
import '../project.dart'; import '../xcode_project.dart';
import 'plist_parser.dart'; import 'plist_parser.dart';
/// Tests whether a [Directory] is an iOS bundle directory. /// Tests whether a [Directory] is an iOS bundle directory.

View File

@ -5,7 +5,7 @@
import '../../base/file_system.dart'; import '../../base/file_system.dart';
import '../../base/logger.dart'; import '../../base/logger.dart';
import '../../base/project_migrator.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. /// Update the minimum iOS deployment version to the minimum allowed by Xcode without causing a warning.
class DeploymentTargetMigration extends ProjectMigrator { class DeploymentTargetMigration extends ProjectMigrator {

View File

@ -5,7 +5,7 @@
import '../../base/file_system.dart'; import '../../base/file_system.dart';
import '../../base/logger.dart'; import '../../base/logger.dart';
import '../../base/project_migrator.dart'; import '../../base/project_migrator.dart';
import '../../project.dart'; import '../../xcode_project.dart';
// The Runner target should inherit its build configuration from Generated.xcconfig. // The Runner target should inherit its build configuration from Generated.xcconfig.
// However the top-level Runner project should not inherit any build configuration so // However the top-level Runner project should not inherit any build configuration so

View File

@ -5,7 +5,7 @@
import '../../base/file_system.dart'; import '../../base/file_system.dart';
import '../../base/logger.dart'; import '../../base/logger.dart';
import '../../base/project_migrator.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. // Update the xcodeproj build location. Legacy build location does not work with Swift Packages.
class ProjectBuildLocationMigration extends ProjectMigrator { class ProjectBuildLocationMigration extends ProjectMigrator {

View File

@ -6,8 +6,8 @@ import '../../base/common.dart';
import '../../base/file_system.dart'; import '../../base/file_system.dart';
import '../../base/logger.dart'; import '../../base/logger.dart';
import '../../base/project_migrator.dart'; import '../../base/project_migrator.dart';
import '../../project.dart';
import '../../reporting/reporting.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. // 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. // This caused issues switching between a real device and simulator due to architecture mismatch.

View File

@ -5,7 +5,7 @@
import '../../base/file_system.dart'; import '../../base/file_system.dart';
import '../../base/logger.dart'; import '../../base/logger.dart';
import '../../base/project_migrator.dart'; import '../../base/project_migrator.dart';
import '../../project.dart'; import '../../xcode_project.dart';
// Xcode legacy build system no longer supported by Xcode. // Xcode legacy build system no longer supported by Xcode.
// Set in https://github.com/flutter/flutter/pull/21901/. // Set in https://github.com/flutter/flutter/pull/21901/.

View File

@ -6,8 +6,8 @@ import '../application_package.dart';
import '../base/file_system.dart'; import '../base/file_system.dart';
import '../build_info.dart'; import '../build_info.dart';
import '../cmake.dart'; import '../cmake.dart';
import '../cmake_project.dart';
import '../globals_null_migrated.dart' as globals; import '../globals_null_migrated.dart' as globals;
import '../project.dart';
abstract class LinuxApp extends ApplicationPackage { abstract class LinuxApp extends ApplicationPackage {
LinuxApp({required String projectBundleId}) : super(id: projectBundleId); LinuxApp({required String projectBundleId}) : super(id: projectBundleId);

View File

@ -12,11 +12,11 @@ import '../base/utils.dart';
import '../build_info.dart'; import '../build_info.dart';
import '../cache.dart'; import '../cache.dart';
import '../cmake.dart'; import '../cmake.dart';
import '../cmake_project.dart';
import '../convert.dart'; import '../convert.dart';
import '../flutter_plugins.dart'; import '../flutter_plugins.dart';
import '../globals_null_migrated.dart' as globals; import '../globals_null_migrated.dart' as globals;
import '../migrations/cmake_custom_command_migration.dart'; import '../migrations/cmake_custom_command_migration.dart';
import '../project.dart';
// Matches the following error and warning patterns: // Matches the following error and warning patterns:
// - <file path>:<line>:<column>: (fatal) error: <error...> // - <file path>:<line>:<column>: (fatal) error: <error...>

View File

@ -13,7 +13,7 @@ import '../base/utils.dart';
import '../build_info.dart'; import '../build_info.dart';
import '../globals_null_migrated.dart' as globals; import '../globals_null_migrated.dart' as globals;
import '../ios/plist_parser.dart'; import '../ios/plist_parser.dart';
import '../project.dart'; import '../xcode_project.dart';
/// Tests whether a [FileSystemEntity] is an macOS bundle directory. /// Tests whether a [FileSystemEntity] is an macOS bundle directory.
bool _isBundleDirectory(FileSystemEntity entity) => bool _isBundleDirectory(FileSystemEntity entity) =>

View File

@ -17,8 +17,8 @@ import '../base/version.dart';
import '../build_info.dart'; import '../build_info.dart';
import '../cache.dart'; import '../cache.dart';
import '../ios/xcodeproj.dart'; import '../ios/xcodeproj.dart';
import '../project.dart';
import '../reporting/reporting.dart'; import '../reporting/reporting.dart';
import '../xcode_project.dart';
const String noCocoaPodsConsequence = ''' 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. CocoaPods is used to retrieve the iOS and macOS platform side's plugin code that responds to your plugin usage on the Dart side.

View File

@ -6,8 +6,8 @@ import '../../base/common.dart';
import '../../base/file_system.dart'; import '../../base/file_system.dart';
import '../../base/logger.dart'; import '../../base/logger.dart';
import '../../base/project_migrator.dart'; import '../../base/project_migrator.dart';
import '../../project.dart';
import '../../reporting/reporting.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. // Remove the linking and embedding logic from the Xcode project to give the tool more control over these.
class RemoveMacOSFrameworkLinkAndEmbeddingMigration extends ProjectMigrator { class RemoveMacOSFrameworkLinkAndEmbeddingMigration extends ProjectMigrator {

View File

@ -5,7 +5,7 @@
import '../base/file_system.dart'; import '../base/file_system.dart';
import '../base/logger.dart'; import '../base/logger.dart';
import '../base/project_migrator.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 // CMake's add_custom_command() should use VERBATIM to handle escaping of spaces
// and special characters correctly. // and special characters correctly.

View File

@ -8,22 +8,23 @@ import 'package:yaml/yaml.dart';
import '../src/convert.dart'; import '../src/convert.dart';
import 'android/gradle_utils.dart' as gradle; import 'android/gradle_utils.dart' as gradle;
import 'artifacts.dart';
import 'base/common.dart'; import 'base/common.dart';
import 'base/error_handling_io.dart';
import 'base/file_system.dart'; import 'base/file_system.dart';
import 'base/logger.dart'; import 'base/logger.dart';
import 'build_info.dart'; import 'base/utils.dart';
import 'bundle.dart' as bundle; import 'bundle.dart' as bundle;
import 'cmake.dart'; import 'cmake_project.dart';
import 'features.dart'; import 'features.dart';
import 'flutter_manifest.dart'; import 'flutter_manifest.dart';
import 'flutter_plugins.dart'; import 'flutter_plugins.dart';
import 'globals_null_migrated.dart' as globals; 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 'platform_plugins.dart';
import 'template.dart'; import 'template.dart';
import 'xcode_project.dart';
export 'cmake_project.dart';
export 'xcode_project.dart';
class FlutterProjectFactory { class FlutterProjectFactory {
FlutterProjectFactory({ FlutterProjectFactory({
@ -175,19 +176,19 @@ class FlutterProject {
/// The MacOS sub project of this project. /// The MacOS sub project of this project.
MacOSProject? _macos; MacOSProject? _macos;
MacOSProject get macos => _macos ??= MacOSProject._(this); MacOSProject get macos => _macos ??= MacOSProject.fromFlutter(this);
/// The Linux sub project of this project. /// The Linux sub project of this project.
LinuxProject? _linux; LinuxProject? _linux;
LinuxProject get linux => _linux ??= LinuxProject._(this); LinuxProject get linux => _linux ??= LinuxProject.fromFlutter(this);
/// The Windows sub project of this project. /// The Windows sub project of this project.
WindowsProject? _windows; WindowsProject? _windows;
WindowsProject get windows => _windows ??= WindowsProject._(this); WindowsProject get windows => _windows ??= WindowsProject.fromFlutter(this);
/// The Windows UWP sub project of this project. /// The Windows UWP sub project of this project.
WindowsUwpProject? _windowUwp; WindowsUwpProject? _windowUwp;
WindowsUwpProject get windowsUwp => _windowUwp ??= WindowsUwpProject._(this); WindowsUwpProject get windowsUwp => _windowUwp ??= WindowsUwpProject.fromFlutter(this);
/// The Fuchsia sub project of this project. /// The Fuchsia sub project of this project.
FuchsiaProject? _fuchsia; FuchsiaProject? _fuchsia;
@ -371,484 +372,6 @@ abstract class FlutterProjectPlatform {
bool existsSync(); 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<String?> productBundleIdentifier(BuildInfo? buildInfo) async {
if (!existsSync()) {
return null;
}
return _productBundleIdentifier ??= await _parseProductBundleIdentifier(buildInfo);
}
String? _productBundleIdentifier;
Future<String?> _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<String, String>? 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<String?> hostAppBundleName(BuildInfo buildInfo) async {
if (!existsSync()) {
return null;
}
return _hostAppBundleName ??= await _parseHostAppBundleName(buildInfo);
}
String? _hostAppBundleName;
Future<String> _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<String, String>? 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<Map<String, String>?> 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<String, String>? currentBuildSettings = _buildSettingsByBuildContext[buildContext];
if (currentBuildSettings == null) {
final Map<String, String>? calculatedBuildSettings = await _xcodeProjectBuildSettings(buildContext);
if (calculatedBuildSettings != null) {
_buildSettingsByBuildContext[buildContext] = calculatedBuildSettings;
}
}
return _buildSettingsByBuildContext[buildContext];
}
final Map<XcodeProjectBuildContext, Map<String, String>> _buildSettingsByBuildContext = <XcodeProjectBuildContext, Map<String, String>>{};
Future<XcodeProjectInfo?> 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<Map<String, String>?> _xcodeProjectBuildSettings(XcodeProjectBuildContext buildContext) async {
final XcodeProjectInterpreter? xcodeProjectInterpreter = globals.xcodeProjectInterpreter;
if (xcodeProjectInterpreter == null || !xcodeProjectInterpreter.isInstalled) {
return null;
}
final Map<String, String> buildSettings = await xcodeProjectInterpreter.getBuildSettings(
xcodeProject.path,
buildContext: buildContext,
);
if (buildSettings != null && buildSettings.isNotEmpty) {
// No timeouts, flakes, or errors.
return buildSettings;
}
return null;
}
Future<void> 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<bool> containsWatchCompanion(List<String> 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<String, String>? allBuildSettings = await buildSettingsForBuildInfo(buildInfo);
if (allBuildSettings != null) {
final String substituedVariable = substituteXcodeVariables(fromPlist, allBuildSettings);
if (substituedVariable == bundleIdentifier) {
return true;
}
}
}
}
}
return false;
}
Future<void> _updateGeneratedXcodeConfigIfNeeded() async {
if (globals.cache.isOlderThanToolsStamp(generatedXcodePropertiesFile)) {
await xcode.updateGeneratedXcodeProperties(
project: parent,
buildInfo: BuildInfo.debug,
targetOverride: bundle.defaultMainPath,
);
}
}
Future<void> _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<void> _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,
<String, Object>{
'ios': true,
'projectName': parent.manifest.appName,
'iosIdentifier': iosBundleIdentifier,
},
printStatusWhenWriting: false,
overwriteExisting: true,
);
}
}
/// Represents the Android sub-project of a Flutter project. /// Represents the Android sub-project of a Flutter project.
/// ///
/// Instances will reflect the contents of the `android/` sub-folder of /// 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. /// True, if the app project is using Kotlin.
bool get isKotlin { bool get isKotlin {
final File gradleFile = hostAppGradleRoot.childDirectory('app').childFile('build.gradle'); final File gradleFile = hostAppGradleRoot.childDirectory('app').childFile('build.gradle');
return _firstMatchInFile(gradleFile, _kotlinPluginPattern) != null; return firstMatchInFile(gradleFile, _kotlinPluginPattern) != null;
} }
File get appManifestFile { File get appManifestFile {
@ -945,12 +468,12 @@ class AndroidProject extends FlutterProjectPlatform {
String? get applicationId { String? get applicationId {
final File gradleFile = hostAppGradleRoot.childDirectory('app').childFile('build.gradle'); final File gradleFile = hostAppGradleRoot.childDirectory('app').childFile('build.gradle');
return _firstMatchInFile(gradleFile, _applicationIdPattern)?.group(1); return firstMatchInFile(gradleFile, _applicationIdPattern)?.group(1);
} }
String? get group { String? get group {
final File gradleFile = hostAppGradleRoot.childFile('build.gradle'); 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. /// 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'); Directory get pluginRegistrantHost => _flutterLibGradleRoot.childDirectory(isModule ? 'Flutter' : 'app');
Future<void> _regenerateLibrary() async { Future<void> _regenerateLibrary() async {
_deleteIfExistsSync(ephemeralDirectory); ErrorHandlingFileSystem.deleteIfExists(ephemeralDirectory, recursive: true);
await _overwriteFromTemplate(globals.fs.path.join( await _overwriteFromTemplate(globals.fs.path.join(
'module', 'module',
'android', 'android',
@ -1107,249 +630,6 @@ class WebProject extends FlutterProjectPlatform {
Future<void> ensureReadyForPlatformSpecificTooling() async {} Future<void> 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<void> ensureReadyForPlatformSpecificTooling() async {
// TODO(stuartmorgan): Add create-from-template logic here.
await _updateGeneratedXcodeConfigIfNeeded();
}
Future<void> _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<void> 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<void> ensureReadyForPlatformSpecificTooling() async {}
String? get applicationId {
return _firstMatchInFile(cmakeFile, _applicationIdPattern)?.group(1);
}
}
/// The Fuchsia sub project. /// The Fuchsia sub project.
class FuchsiaProject { class FuchsiaProject {
FuchsiaProject._(this.project); FuchsiaProject._(this.project);

View File

@ -11,8 +11,8 @@ import '../base/file_system.dart';
import '../base/utils.dart'; import '../base/utils.dart';
import '../build_info.dart'; import '../build_info.dart';
import '../cmake.dart'; import '../cmake.dart';
import '../cmake_project.dart';
import '../globals_null_migrated.dart' as globals; import '../globals_null_migrated.dart' as globals;
import '../project.dart';
abstract class WindowsApp extends ApplicationPackage { abstract class WindowsApp extends ApplicationPackage {
WindowsApp({@required String projectBundleId}) : super(id: projectBundleId); WindowsApp({@required String projectBundleId}) : super(id: projectBundleId);

View File

@ -14,11 +14,11 @@ import '../base/utils.dart';
import '../build_info.dart'; import '../build_info.dart';
import '../cache.dart'; import '../cache.dart';
import '../cmake.dart'; import '../cmake.dart';
import '../cmake_project.dart';
import '../convert.dart'; import '../convert.dart';
import '../flutter_plugins.dart'; import '../flutter_plugins.dart';
import '../globals_null_migrated.dart' as globals; import '../globals_null_migrated.dart' as globals;
import '../migrations/cmake_custom_command_migration.dart'; import '../migrations/cmake_custom_command_migration.dart';
import '../project.dart';
import 'install_manifest.dart'; import 'install_manifest.dart';
import 'visual_studio.dart'; import 'visual_studio.dart';

View File

@ -12,7 +12,7 @@ import '../base/file_system.dart';
import '../base/logger.dart'; import '../base/logger.dart';
import '../base/platform.dart'; import '../base/platform.dart';
import '../build_info.dart'; import '../build_info.dart';
import '../project.dart'; import '../cmake_project.dart';
/// Generate an install manifest that is required for CMAKE on UWP projects. /// Generate an install manifest that is required for CMAKE on UWP projects.
Future<void> createManifest({ Future<void> createManifest({

View File

@ -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<String?> productBundleIdentifier(BuildInfo? buildInfo) async {
if (!existsSync()) {
return null;
}
return _productBundleIdentifier ??= await _parseProductBundleIdentifier(buildInfo);
}
String? _productBundleIdentifier;
Future<String?> _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<String, String>? 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<String?> hostAppBundleName(BuildInfo buildInfo) async {
if (!existsSync()) {
return null;
}
return _hostAppBundleName ??= await _parseHostAppBundleName(buildInfo);
}
String? _hostAppBundleName;
Future<String> _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<String, String>? 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<Map<String, String>?> 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<String, String>? currentBuildSettings = _buildSettingsByBuildContext[buildContext];
if (currentBuildSettings == null) {
final Map<String, String>? calculatedBuildSettings = await _xcodeProjectBuildSettings(buildContext);
if (calculatedBuildSettings != null) {
_buildSettingsByBuildContext[buildContext] = calculatedBuildSettings;
}
}
return _buildSettingsByBuildContext[buildContext];
}
final Map<XcodeProjectBuildContext, Map<String, String>> _buildSettingsByBuildContext = <XcodeProjectBuildContext, Map<String, String>>{};
Future<XcodeProjectInfo?> 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<Map<String, String>?> _xcodeProjectBuildSettings(XcodeProjectBuildContext buildContext) async {
final XcodeProjectInterpreter? xcodeProjectInterpreter = globals.xcodeProjectInterpreter;
if (xcodeProjectInterpreter == null || !xcodeProjectInterpreter.isInstalled) {
return null;
}
final Map<String, String> buildSettings = await xcodeProjectInterpreter.getBuildSettings(
xcodeProject.path,
buildContext: buildContext,
);
if (buildSettings != null && buildSettings.isNotEmpty) {
// No timeouts, flakes, or errors.
return buildSettings;
}
return null;
}
Future<void> 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<bool> containsWatchCompanion(List<String> 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<String, String>? allBuildSettings = await buildSettingsForBuildInfo(buildInfo);
if (allBuildSettings != null) {
final String substituedVariable = substituteXcodeVariables(fromPlist, allBuildSettings);
if (substituedVariable == bundleIdentifier) {
return true;
}
}
}
}
}
return false;
}
Future<void> _updateGeneratedXcodeConfigIfNeeded() async {
if (globals.cache.isOlderThanToolsStamp(generatedXcodePropertiesFile)) {
await xcode.updateGeneratedXcodeProperties(
project: parent,
buildInfo: BuildInfo.debug,
targetOverride: bundle.defaultMainPath,
);
}
}
Future<void> _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<void> _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,
<String, Object>{
'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<void> ensureReadyForPlatformSpecificTooling() async {
// TODO(stuartmorgan): Add create-from-template logic here.
await _updateGeneratedXcodeConfigIfNeeded();
}
Future<void> _updateGeneratedXcodeConfigIfNeeded() async {
if (globals.cache.isOlderThanToolsStamp(generatedXcodePropertiesFile)) {
await xcode.updateGeneratedXcodeProperties(
project: parent,
buildInfo: BuildInfo.debug,
useMacOSConfig: true,
);
}
}
}

View File

@ -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/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/remove_framework_link_and_embedding_migration.dart';
import 'package:flutter_tools/src/ios/migrations/xcode_build_system_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/reporting/reporting.dart';
import 'package:flutter_tools/src/xcode_project.dart';
import 'package:test/fake.dart'; import 'package:test/fake.dart';
import '../../src/common.dart'; import '../../src/common.dart';

View File

@ -7,8 +7,8 @@ import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/project_migrator.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/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/reporting/reporting.dart';
import 'package:flutter_tools/src/xcode_project.dart';
import 'package:test/fake.dart'; import 'package:test/fake.dart';
import '../../src/common.dart'; import '../../src/common.dart';

View File

@ -7,8 +7,8 @@ import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/project_migrator.dart'; import 'package:flutter_tools/src/base/project_migrator.dart';
import 'package:flutter_tools/src/base/terminal.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/migrations/cmake_custom_command_migration.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:test/fake.dart'; import 'package:test/fake.dart';
import '../../src/common.dart'; import '../../src/common.dart';