Support for flutter run/build module on iOS (#21216)
diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart index d54d8e9..b1fc04d 100644 --- a/packages/flutter_tools/lib/src/project.dart +++ b/packages/flutter_tools/lib/src/project.dart
@@ -3,7 +3,6 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:convert'; import 'package:meta/meta.dart'; @@ -64,17 +63,17 @@ /// The manifest of the example sub-project of this project. final FlutterManifest _exampleManifest; - /// Asynchronously returns the organization names found in this project as + /// The set of organization names found in this project as /// part of iOS product bundle identifier, Android application ID, or /// Gradle group ID. - Future<Set<String>> organizationNames() async { - final List<String> candidates = await Future.wait(<Future<String>>[ - ios.productBundleIdentifier(), - android.applicationId(), - android.group(), - example.android.applicationId(), - example.ios.productBundleIdentifier(), - ]); + Set<String> get organizationNames { + final List<String> candidates = <String>[ + ios.productBundleIdentifier, + android.applicationId, + android.group, + example.android.applicationId, + example.ios.productBundleIdentifier, + ]; return new Set<String>.from(candidates .map(_organizationNameFromPackageName) .where((String name) => name != null)); @@ -93,6 +92,13 @@ /// The Android sub project of this project. AndroidProject get android => new AndroidProject._(this); + /// The `pubspec.yaml` file of this project. + File get pubspecFile => directory.childFile('pubspec.yaml'); + + /// The `.packages` file of this project. + File get packagesFile => directory.childFile('.packages'); + + /// The `.flutter-plugins` file of this project. File get flutterPluginsFile => directory.childFile('.flutter-plugins'); /// The example sub-project of this project. @@ -128,6 +134,7 @@ Future<void> ensureReadyForPlatformSpecificTooling() async { if (!directory.existsSync() || hasExampleApp) return; + refreshPluginsList(this); await android.ensureReadyForPlatformSpecificTooling(); await ios.ensureReadyForPlatformSpecificTooling(); await injectPlugins(this); @@ -140,6 +147,7 @@ /// Flutter applications and the `.ios/` sub-folder of Flutter modules. class IosProject { static final RegExp _productBundleIdPattern = new RegExp(r'^\s*PRODUCT_BUNDLE_IDENTIFIER\s*=\s*(.*);\s*$'); + static const String _hostAppBundleName = 'Runner'; IosProject._(this.parent); @@ -149,6 +157,8 @@ /// The directory of this project. Directory get directory => parent.directory.childDirectory(isModule ? '.ios' : 'ios'); + String get hostAppBundleName => '$_hostAppBundleName.app'; + /// True, if the parent Flutter project is a module. bool get isModule => parent.isModule; @@ -164,19 +174,30 @@ /// The 'Manifest.lock'. File get podManifestLock => directory.childDirectory('Pods').childFile('Manifest.lock'); - Future<String> productBundleIdentifier() { - final File projectFile = directory.childDirectory('Runner.xcodeproj').childFile('project.pbxproj'); - return _firstMatchInFile(projectFile, _productBundleIdPattern).then((Match match) => match?.group(1)); + /// '.xcodeproj' folder of the host app. + Directory get xcodeProject => directory.childDirectory('$_hostAppBundleName.xcodeproj'); + + /// The '.pbxproj' file of the host app. + File get xcodeProjectInfoFile => xcodeProject.childFile('project.pbxproj'); + + /// The product bundle identifier of the host app. + String get productBundleIdentifier { + return _firstMatchInFile(xcodeProjectInfoFile, _productBundleIdPattern)?.group(1); + } + + /// True, if the host app project is using Swift. + bool get isSwift => buildSettings?.containsKey('SWIFT_VERSION'); + + /// The build settings for the host app of this project, as a detached map. + Map<String, String> get buildSettings { + return xcode.xcodeProjectInterpreter.getBuildSettings(xcodeProject.path, _hostAppBundleName); } Future<void> ensureReadyForPlatformSpecificTooling() async { - if (isModule && _shouldRegenerateFromTemplate()) { - final Template template = new Template.fromName(fs.path.join('module', 'ios')); - template.render(directory, <String, dynamic>{}, printStatusWhenWriting: false); - } + _regenerateFromTemplateIfNeeded(); if (!directory.existsSync()) return; - if (Cache.instance.fileOlderThanToolsStamp(generatedXcodePropertiesFile)) { + if (Cache.instance.isOlderThanToolsStamp(generatedXcodePropertiesFile)) { await xcode.updateGeneratedXcodeProperties( project: parent, buildInfo: BuildInfo.debug, @@ -186,12 +207,23 @@ } } - Future<void> materialize() async { - throwToolExit('flutter materialize has not yet been implemented for iOS'); + void _regenerateFromTemplateIfNeeded() { + if (!isModule) + return; + final bool pubspecChanged = isOlderThanReference(entity: directory, referenceFile: parent.pubspecFile); + final bool toolingChanged = Cache.instance.isOlderThanToolsStamp(directory); + if (!pubspecChanged && !toolingChanged) + return; + _deleteIfExistsSync(directory); + _overwriteFromTemplate(fs.path.join('module', 'ios', 'library'), directory); + _overwriteFromTemplate(fs.path.join('module', 'ios', 'host_app_ephemeral'), directory); + if (hasPlugins(parent)) { + _overwriteFromTemplate(fs.path.join('module', 'ios', 'host_app_ephemeral_cocoapods'), directory); + } } - bool _shouldRegenerateFromTemplate() { - return Cache.instance.fileOlderThanToolsStamp(directory.childFile('podhelper.rb')); + Future<void> materialize() async { + throwToolExit('flutter materialize has not yet been implemented for iOS'); } File get generatedXcodePropertiesFile => directory.childDirectory('Flutter').childFile('Generated.xcconfig'); @@ -199,7 +231,20 @@ Directory get pluginRegistrantHost { return isModule ? directory.childDirectory('Flutter').childDirectory('FlutterPluginRegistrant') - : directory.childDirectory('Runner'); + : directory.childDirectory(_hostAppBundleName); + } + + void _overwriteFromTemplate(String path, Directory target) { + final Template template = new Template.fromName(path); + template.render( + target, + <String, dynamic>{ + 'projectName': parent.manifest.appName, + 'iosIdentifier': parent.manifest.iosBundleIdentifier + }, + printStatusWhenWriting: false, + overwriteExisting: true, + ); } } @@ -237,7 +282,7 @@ bool get isModule => parent.isModule; File get appManifestFile { - return isUsingGradle() + return isUsingGradle ? fs.file(fs.path.join(hostAppGradleRoot.path, 'app', 'src', 'main', 'AndroidManifest.xml')) : hostAppGradleRoot.childFile('AndroidManifest.xml'); } @@ -248,18 +293,18 @@ return fs.directory(fs.path.join(hostAppGradleRoot.path, 'app', 'build', 'outputs', 'apk')); } - bool isUsingGradle() { + bool get isUsingGradle { return hostAppGradleRoot.childFile('build.gradle').existsSync(); } - Future<String> applicationId() { + String get applicationId { final File gradleFile = hostAppGradleRoot.childDirectory('app').childFile('build.gradle'); - return _firstMatchInFile(gradleFile, _applicationIdPattern).then((Match match) => match?.group(1)); + return _firstMatchInFile(gradleFile, _applicationIdPattern)?.group(1); } - Future<String> group() { + String get group { final File gradleFile = hostAppGradleRoot.childFile('build.gradle'); - return _firstMatchInFile(gradleFile, _groupPattern).then((Match match) => match?.group(1)); + return _firstMatchInFile(gradleFile, _groupPattern)?.group(1); } Future<void> ensureReadyForPlatformSpecificTooling() async { @@ -278,7 +323,8 @@ } bool _shouldRegenerateFromTemplate() { - return Cache.instance.fileOlderThanToolsStamp(_ephemeralDirectory.childFile('build.gradle')); + return isOlderThanReference(entity: _ephemeralDirectory, referenceFile: parent.pubspecFile) + || Cache.instance.isOlderThanToolsStamp(_ephemeralDirectory); } Future<void> materialize() async { @@ -305,11 +351,6 @@ gradle.injectGradleWrapper(_ephemeralDirectory); } - void _deleteIfExistsSync(Directory directory) { - if (directory.existsSync()) - directory.deleteSync(recursive: true); - } - void _overwriteFromTemplate(String path, Directory target) { final Template template = new Template.fromName(path); template.render( @@ -324,17 +365,25 @@ } } -/// Asynchronously returns the first line-based match for [regExp] in [file]. +/// 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. -Future<Match> _firstMatchInFile(File file, RegExp regExp) async { - if (!await file.exists()) { +Match _firstMatchInFile(File file, RegExp regExp) { + if (!file.existsSync()) { return null; } - return file - .openRead() - .transform(utf8.decoder) - .transform(const LineSplitter()) - .map(regExp.firstMatch) - .firstWhere((Match match) => match != null, orElse: () => null); + for (String line in file.readAsLinesSync()) { + final Match match = regExp.firstMatch(line); + if (match != null) { + return match; + } + } + return null; }