Unverified Commit 22832d36 authored by Mikkel Nygaard Ravn's avatar Mikkel Nygaard Ravn Committed by GitHub

Support for flutter run/build module on iOS (#21216)

parent 19c96282
...@@ -19,7 +19,7 @@ Future<Null> buildApk({ ...@@ -19,7 +19,7 @@ Future<Null> buildApk({
@required String target, @required String target,
BuildInfo buildInfo = BuildInfo.debug BuildInfo buildInfo = BuildInfo.debug
}) async { }) async {
if (!project.android.isUsingGradle()) { if (!project.android.isUsingGradle) {
throwToolExit( throwToolExit(
'The build process for Android has changed, and the current project configuration\n' 'The build process for Android has changed, and the current project configuration\n'
'is no longer valid. Please consult\n\n' 'is no longer valid. Please consult\n\n'
......
...@@ -17,7 +17,6 @@ import 'build_info.dart'; ...@@ -17,7 +17,6 @@ import 'build_info.dart';
import 'globals.dart'; import 'globals.dart';
import 'ios/ios_workflow.dart'; import 'ios/ios_workflow.dart';
import 'ios/plist_utils.dart' as plist; import 'ios/plist_utils.dart' as plist;
import 'ios/xcodeproj.dart';
import 'project.dart'; import 'project.dart';
import 'tester/flutter_tester.dart'; import 'tester/flutter_tester.dart';
...@@ -93,7 +92,7 @@ class AndroidApk extends ApplicationPackage { ...@@ -93,7 +92,7 @@ class AndroidApk extends ApplicationPackage {
static Future<AndroidApk> fromAndroidProject(AndroidProject androidProject) async { static Future<AndroidApk> fromAndroidProject(AndroidProject androidProject) async {
File apkFile; File apkFile;
if (androidProject.isUsingGradle()) { if (androidProject.isUsingGradle) {
apkFile = await getGradleAppOut(androidProject); apkFile = await getGradleAppOut(androidProject);
if (apkFile.existsSync()) { if (apkFile.existsSync()) {
// Grab information from the .apk. The gradle build script might alter // Grab information from the .apk. The gradle build script might alter
...@@ -217,26 +216,10 @@ abstract class IOSApp extends ApplicationPackage { ...@@ -217,26 +216,10 @@ abstract class IOSApp extends ApplicationPackage {
); );
} }
factory IOSApp.fromCurrentDirectory() { factory IOSApp.fromIosProject(IosProject project) {
if (getCurrentHostPlatform() != HostPlatform.darwin_x64) if (getCurrentHostPlatform() != HostPlatform.darwin_x64)
return null; return null;
return new BuildableIOSApp(project);
final String plistPath = fs.path.join('ios', 'Runner', 'Info.plist');
String id = iosWorkflow.getPlistValueFromFile(
plistPath,
plist.kCFBundleIdentifierKey,
);
if (id == null || !xcodeProjectInterpreter.isInstalled)
return null;
final String projectPath = fs.path.join('ios', 'Runner.xcodeproj');
final Map<String, String> buildSettings = xcodeProjectInterpreter.getBuildSettings(projectPath, 'Runner');
id = substituteXcodeVariables(id, buildSettings);
return new BuildableIOSApp(
appDirectory: 'ios',
projectBundleId: id,
buildSettings: buildSettings,
);
} }
@override @override
...@@ -248,25 +231,12 @@ abstract class IOSApp extends ApplicationPackage { ...@@ -248,25 +231,12 @@ abstract class IOSApp extends ApplicationPackage {
} }
class BuildableIOSApp extends IOSApp { class BuildableIOSApp extends IOSApp {
static const String kBundleName = 'Runner.app'; BuildableIOSApp(this.project) : super(projectBundleId: project.productBundleIdentifier);
BuildableIOSApp({
this.appDirectory,
String projectBundleId,
this.buildSettings,
}) : super(projectBundleId: projectBundleId);
final String appDirectory;
/// Build settings of the app's Xcode project. final IosProject project;
///
/// These are the build settings as specified in the Xcode project files.
///
/// Build settings may change depending on the parameters passed while building.
final Map<String, String> buildSettings;
@override @override
String get name => kBundleName; String get name => project.hostAppBundleName;
@override @override
String get simulatorBundlePath => _buildAppPath('iphonesimulator'); String get simulatorBundlePath => _buildAppPath('iphonesimulator');
...@@ -274,11 +244,8 @@ class BuildableIOSApp extends IOSApp { ...@@ -274,11 +244,8 @@ class BuildableIOSApp extends IOSApp {
@override @override
String get deviceBundlePath => _buildAppPath('iphoneos'); String get deviceBundlePath => _buildAppPath('iphoneos');
/// True if the app is built from a Swift project. Null if unknown.
bool get isSwift => buildSettings?.containsKey('SWIFT_VERSION');
String _buildAppPath(String type) { String _buildAppPath(String type) {
return fs.path.join(getIosBuildDirectory(), type, kBundleName); return fs.path.join(getIosBuildDirectory(), type, name);
} }
} }
...@@ -317,7 +284,7 @@ Future<ApplicationPackage> getApplicationPackageForPlatform( ...@@ -317,7 +284,7 @@ Future<ApplicationPackage> getApplicationPackageForPlatform(
: new AndroidApk.fromApk(applicationBinary); : new AndroidApk.fromApk(applicationBinary);
case TargetPlatform.ios: case TargetPlatform.ios:
return applicationBinary == null return applicationBinary == null
? new IOSApp.fromCurrentDirectory() ? new IOSApp.fromIosProject((await FlutterProject.current()).ios)
: new IOSApp.fromPrebuiltApp(applicationBinary); : new IOSApp.fromPrebuiltApp(applicationBinary);
case TargetPlatform.tester: case TargetPlatform.tester:
return new FlutterTesterApp.fromCurrentDirectory(); return new FlutterTesterApp.fromCurrentDirectory();
...@@ -346,7 +313,7 @@ class ApplicationPackageStore { ...@@ -346,7 +313,7 @@ class ApplicationPackageStore {
android ??= await AndroidApk.fromAndroidProject((await FlutterProject.current()).android); android ??= await AndroidApk.fromAndroidProject((await FlutterProject.current()).android);
return android; return android;
case TargetPlatform.ios: case TargetPlatform.ios:
iOS ??= new IOSApp.fromCurrentDirectory(); iOS ??= IOSApp.fromIosProject((await FlutterProject.current()).ios);
return iOS; return iOS;
case TargetPlatform.darwin_x64: case TargetPlatform.darwin_x64:
case TargetPlatform.linux_x64: case TargetPlatform.linux_x64:
......
...@@ -6,6 +6,7 @@ import 'package:file/file.dart'; ...@@ -6,6 +6,7 @@ import 'package:file/file.dart';
import 'package:file/local.dart'; import 'package:file/local.dart';
import 'package:file/memory.dart'; import 'package:file/memory.dart';
import 'package:file/record_replay.dart'; import 'package:file/record_replay.dart';
import 'package:meta/meta.dart';
import 'common.dart' show throwToolExit; import 'common.dart' show throwToolExit;
import 'context.dart'; import 'context.dart';
...@@ -145,3 +146,16 @@ String canonicalizePath(String path) => fs.path.normalize(fs.path.absolute(path) ...@@ -145,3 +146,16 @@ String canonicalizePath(String path) => fs.path.normalize(fs.path.absolute(path)
/// On Windows it replaces all '\' with '\\'. On other platforms, it returns the /// On Windows it replaces all '\' with '\\'. On other platforms, it returns the
/// path unchanged. /// path unchanged.
String escapePath(String path) => platform.isWindows ? path.replaceAll('\\', '\\\\') : path; String escapePath(String path) => platform.isWindows ? path.replaceAll('\\', '\\\\') : path;
/// Returns true if the file system [entity] has not been modified since the
/// latest modification to [referenceFile].
///
/// Returns true, if [entity] does not exist.
///
/// Returns false, if [entity] exists, but [referenceFile] does not.
bool isOlderThanReference({@required FileSystemEntity entity, @required File referenceFile}) {
if (!entity.existsSync())
return true;
return referenceFile.existsSync()
&& referenceFile.lastModifiedSync().isAfter(entity.statSync().modified);
}
...@@ -169,17 +169,11 @@ class Cache { ...@@ -169,17 +169,11 @@ class Cache {
return fs.file(fs.path.join(getRoot().path, '$artifactName.stamp')); return fs.file(fs.path.join(getRoot().path, '$artifactName.stamp'));
} }
/// Returns `true` if either [file] is older than the tools stamp or if /// Returns `true` if either [entity] is older than the tools stamp or if
/// [file] doesn't exist. /// [entity] doesn't exist.
bool fileOlderThanToolsStamp(File file) { bool isOlderThanToolsStamp(FileSystemEntity entity) {
if (!file.existsSync()) {
return true;
}
final File flutterToolsStamp = getStampFileFor('flutter_tools'); final File flutterToolsStamp = getStampFileFor('flutter_tools');
return flutterToolsStamp.existsSync() && return isOlderThanReference(entity: entity, referenceFile: flutterToolsStamp);
flutterToolsStamp
.lastModifiedSync()
.isAfter(file.lastModifiedSync());
} }
bool isUpToDate() => _artifacts.every((CachedArtifact artifact) => artifact.isUpToDate()); bool isUpToDate() => _artifacts.every((CachedArtifact artifact) => artifact.isUpToDate());
......
...@@ -133,7 +133,7 @@ class CreateCommand extends FlutterCommand { ...@@ -133,7 +133,7 @@ class CreateCommand extends FlutterCommand {
String organization = argResults['org']; String organization = argResults['org'];
if (!argResults.wasParsed('org')) { if (!argResults.wasParsed('org')) {
final FlutterProject project = await FlutterProject.fromDirectory(projectDir); final FlutterProject project = await FlutterProject.fromDirectory(projectDir);
final Set<String> existingOrganizations = await project.organizationNames(); final Set<String> existingOrganizations = project.organizationNames;
if (existingOrganizations.length == 1) { if (existingOrganizations.length == 1) {
organization = existingOrganizations.first; organization = existingOrganizations.first;
} else if (1 < existingOrganizations.length) { } else if (1 < existingOrganizations.length) {
......
...@@ -26,6 +26,7 @@ class InjectPluginsCommand extends FlutterCommand { ...@@ -26,6 +26,7 @@ class InjectPluginsCommand extends FlutterCommand {
@override @override
Future<Null> runCommand() async { Future<Null> runCommand() async {
final FlutterProject project = await FlutterProject.current(); final FlutterProject project = await FlutterProject.current();
refreshPluginsList(project);
await injectPlugins(project); await injectPlugins(project);
final bool result = hasPlugins(project); final bool result = hasPlugins(project);
if (result) { if (result) {
......
...@@ -140,6 +140,14 @@ class FlutterManifest { ...@@ -140,6 +140,14 @@ class FlutterManifest {
return null; return null;
} }
/// Returns the iOS bundle identifier declared by this manifest in its
/// module descriptor. Returns null, if there is no such declaration.
String get iosBundleIdentifier {
if (isModule)
return _flutterDescriptor['module']['iosBundleIdentifier'];
return null;
}
List<Map<String, dynamic>> get fontsDescriptor { List<Map<String, dynamic>> get fontsDescriptor {
final List<dynamic> fontList = _flutterDescriptor['fonts']; final List<dynamic> fontList = _flutterDescriptor['fonts'];
return fontList == null return fontList == null
......
...@@ -96,20 +96,21 @@ Future<Map<String, String>> getCodeSigningIdentityDevelopmentTeam({ ...@@ -96,20 +96,21 @@ Future<Map<String, String>> getCodeSigningIdentityDevelopmentTeam({
BuildableIOSApp iosApp, BuildableIOSApp iosApp,
bool usesTerminalUi = true bool usesTerminalUi = true
}) async{ }) async{
if (iosApp.buildSettings == null) final Map<String, String> buildSettings = iosApp.project.buildSettings;
if (buildSettings == null)
return null; return null;
// If the user already has it set in the project build settings itself, // If the user already has it set in the project build settings itself,
// continue with that. // continue with that.
if (isNotEmpty(iosApp.buildSettings['DEVELOPMENT_TEAM'])) { if (isNotEmpty(buildSettings['DEVELOPMENT_TEAM'])) {
printStatus( printStatus(
'Automatically signing iOS for device deployment using specified development ' 'Automatically signing iOS for device deployment using specified development '
'team in Xcode project: ${iosApp.buildSettings['DEVELOPMENT_TEAM']}' 'team in Xcode project: ${buildSettings['DEVELOPMENT_TEAM']}'
); );
return null; return null;
} }
if (isNotEmpty(iosApp.buildSettings['PROVISIONING_PROFILE'])) if (isNotEmpty(buildSettings['PROVISIONING_PROFILE']))
return null; return null;
// If the user's environment is missing the tools needed to find and read // If the user's environment is missing the tools needed to find and read
......
...@@ -189,13 +189,13 @@ Future<XcodeBuildResult> buildXcodeProject({ ...@@ -189,13 +189,13 @@ Future<XcodeBuildResult> buildXcodeProject({
bool codesign = true, bool codesign = true,
bool usesTerminalUi = true, bool usesTerminalUi = true,
}) async { }) async {
if (!await upgradePbxProjWithFlutterAssets(app.name, app.appDirectory)) if (!await upgradePbxProjWithFlutterAssets(app.project))
return new XcodeBuildResult(success: false); return new XcodeBuildResult(success: false);
if (!_checkXcodeVersion()) if (!_checkXcodeVersion())
return new XcodeBuildResult(success: false); return new XcodeBuildResult(success: false);
final XcodeProjectInfo projectInfo = xcodeProjectInterpreter.getInfo(app.appDirectory); final XcodeProjectInfo projectInfo = xcodeProjectInterpreter.getInfo(app.project.directory.path);
if (!projectInfo.targets.contains('Runner')) { if (!projectInfo.targets.contains('Runner')) {
printError('The Xcode project does not define target "Runner" which is needed by Flutter tooling.'); printError('The Xcode project does not define target "Runner" which is needed by Flutter tooling.');
printError('Open Xcode to fix the problem:'); printError('Open Xcode to fix the problem:');
...@@ -230,8 +230,7 @@ Future<XcodeBuildResult> buildXcodeProject({ ...@@ -230,8 +230,7 @@ Future<XcodeBuildResult> buildXcodeProject({
// Before the build, all service definitions must be updated and the dylibs // Before the build, all service definitions must be updated and the dylibs
// copied over to a location that is suitable for Xcodebuild to find them. // copied over to a location that is suitable for Xcodebuild to find them.
final Directory appDirectory = fs.directory(app.appDirectory); await _addServicesToBundle(app.project.directory);
await _addServicesToBundle(appDirectory);
final FlutterProject project = await FlutterProject.current(); final FlutterProject project = await FlutterProject.current();
await updateGeneratedXcodeProperties( await updateGeneratedXcodeProperties(
...@@ -242,22 +241,21 @@ Future<XcodeBuildResult> buildXcodeProject({ ...@@ -242,22 +241,21 @@ Future<XcodeBuildResult> buildXcodeProject({
); );
if (hasPlugins(project)) { if (hasPlugins(project)) {
final String iosPath = fs.path.join(fs.currentDirectory.path, app.appDirectory);
// If the Xcode project, Podfile, or Generated.xcconfig have changed since // If the Xcode project, Podfile, or Generated.xcconfig have changed since
// last run, pods should be updated. // last run, pods should be updated.
final Fingerprinter fingerprinter = new Fingerprinter( final Fingerprinter fingerprinter = new Fingerprinter(
fingerprintPath: fs.path.join(getIosBuildDirectory(), 'pod_inputs.fingerprint'), fingerprintPath: fs.path.join(getIosBuildDirectory(), 'pod_inputs.fingerprint'),
paths: <String>[ paths: <String>[
_getPbxProjPath(app.appDirectory), app.project.xcodeProjectInfoFile.path,
fs.path.join(iosPath, 'Podfile'), app.project.podfile.path,
fs.path.join(iosPath, 'Flutter', 'Generated.xcconfig'), app.project.generatedXcodePropertiesFile.path,
], ],
properties: <String, String>{}, properties: <String, String>{},
); );
final bool didPodInstall = await cocoaPods.processPods( final bool didPodInstall = await cocoaPods.processPods(
iosProject: project.ios, iosProject: project.ios,
iosEngineDir: flutterFrameworkDir(buildInfo.mode), iosEngineDir: flutterFrameworkDir(buildInfo.mode),
isSwift: app.isSwift, isSwift: project.ios.isSwift,
dependenciesChanged: !await fingerprinter.doesFingerprintMatch() dependenciesChanged: !await fingerprinter.doesFingerprintMatch()
); );
if (didPodInstall) if (didPodInstall)
...@@ -288,7 +286,7 @@ Future<XcodeBuildResult> buildXcodeProject({ ...@@ -288,7 +286,7 @@ Future<XcodeBuildResult> buildXcodeProject({
buildCommands.add('-allowProvisioningDeviceRegistration'); buildCommands.add('-allowProvisioningDeviceRegistration');
} }
final List<FileSystemEntity> contents = fs.directory(app.appDirectory).listSync(); final List<FileSystemEntity> contents = app.project.directory.listSync();
for (FileSystemEntity entity in contents) { for (FileSystemEntity entity in contents) {
if (fs.path.extension(entity.path) == '.xcworkspace') { if (fs.path.extension(entity.path) == '.xcworkspace') {
buildCommands.addAll(<String>[ buildCommands.addAll(<String>[
...@@ -353,7 +351,7 @@ Future<XcodeBuildResult> buildXcodeProject({ ...@@ -353,7 +351,7 @@ Future<XcodeBuildResult> buildXcodeProject({
initialBuildStatus = logger.startProgress('Starting Xcode build...'); initialBuildStatus = logger.startProgress('Starting Xcode build...');
final RunResult buildResult = await runAsync( final RunResult buildResult = await runAsync(
buildCommands, buildCommands,
workingDirectory: app.appDirectory, workingDirectory: app.project.directory.path,
allowReentrantFlutter: true allowReentrantFlutter: true
); );
buildSubStatus?.stop(); buildSubStatus?.stop();
...@@ -381,7 +379,7 @@ Future<XcodeBuildResult> buildXcodeProject({ ...@@ -381,7 +379,7 @@ Future<XcodeBuildResult> buildXcodeProject({
'-allowProvisioningDeviceRegistration', '-allowProvisioningDeviceRegistration',
].contains(buildCommand); ].contains(buildCommand);
}).toList(), }).toList(),
workingDirectory: app.appDirectory, workingDirectory: app.project.directory.path,
)); ));
if (buildResult.exitCode != 0) { if (buildResult.exitCode != 0) {
...@@ -400,7 +398,7 @@ Future<XcodeBuildResult> buildXcodeProject({ ...@@ -400,7 +398,7 @@ Future<XcodeBuildResult> buildXcodeProject({
stderr: buildResult.stderr, stderr: buildResult.stderr,
xcodeBuildExecution: new XcodeBuildExecution( xcodeBuildExecution: new XcodeBuildExecution(
buildCommands: buildCommands, buildCommands: buildCommands,
appDirectory: app.appDirectory, appDirectory: app.project.directory.path,
buildForPhysicalDevice: buildForDevice, buildForPhysicalDevice: buildForDevice,
buildSettings: buildSettings, buildSettings: buildSettings,
), ),
...@@ -568,9 +566,6 @@ Future<Null> _copyServiceFrameworks(List<Map<String, String>> services, Director ...@@ -568,9 +566,6 @@ Future<Null> _copyServiceFrameworks(List<Map<String, String>> services, Director
} }
} }
/// The path of the Xcode project file.
String _getPbxProjPath(String appPath) => fs.path.join(fs.currentDirectory.path, appPath, 'Runner.xcodeproj', 'project.pbxproj');
void _copyServiceDefinitionsManifest(List<Map<String, String>> services, File manifest) { void _copyServiceDefinitionsManifest(List<Map<String, String>> services, File manifest) {
printTrace("Creating service definitions manifest at '${manifest.path}'"); printTrace("Creating service definitions manifest at '${manifest.path}'");
final List<Map<String, String>> jsonServices = services.map((Map<String, String> service) => <String, String>{ final List<Map<String, String>> jsonServices = services.map((Map<String, String> service) => <String, String>{
...@@ -583,8 +578,8 @@ void _copyServiceDefinitionsManifest(List<Map<String, String>> services, File ma ...@@ -583,8 +578,8 @@ void _copyServiceDefinitionsManifest(List<Map<String, String>> services, File ma
manifest.writeAsStringSync(json.encode(jsonObject), mode: FileMode.write, flush: true); manifest.writeAsStringSync(json.encode(jsonObject), mode: FileMode.write, flush: true);
} }
Future<bool> upgradePbxProjWithFlutterAssets(String app, String appPath) async { Future<bool> upgradePbxProjWithFlutterAssets(IosProject project) async {
final File xcodeProjectFile = fs.file(_getPbxProjPath(appPath)); final File xcodeProjectFile = project.xcodeProjectInfoFile;
assert(await xcodeProjectFile.exists()); assert(await xcodeProjectFile.exists());
final List<String> lines = await xcodeProjectFile.readAsLines(); final List<String> lines = await xcodeProjectFile.readAsLines();
...@@ -601,7 +596,7 @@ Future<bool> upgradePbxProjWithFlutterAssets(String app, String appPath) async { ...@@ -601,7 +596,7 @@ Future<bool> upgradePbxProjWithFlutterAssets(String app, String appPath) async {
const String l8 = ' 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */,'; const String l8 = ' 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */,';
printStatus("Upgrading project.pbxproj of $app' to include the " printStatus("Upgrading project.pbxproj of ${project.hostAppBundleName}' to include the "
"'flutter_assets' directory"); "'flutter_assets' directory");
if (!lines.contains(l1) || !lines.contains(l3) || if (!lines.contains(l1) || !lines.contains(l3) ||
......
...@@ -241,8 +241,7 @@ Future<void> _writeIOSPluginRegistrant(FlutterProject project, List<Plugin> plug ...@@ -241,8 +241,7 @@ Future<void> _writeIOSPluginRegistrant(FlutterProject project, List<Plugin> plug
'name': p.name, 'name': p.name,
'prefix': p.iosPrefix, 'prefix': p.iosPrefix,
'class': p.pluginClass, 'class': p.pluginClass,
}). }).toList();
toList();
final Map<String, dynamic> context = <String, dynamic>{ final Map<String, dynamic> context = <String, dynamic>{
'plugins': iosPlugins, 'plugins': iosPlugins,
}; };
...@@ -279,23 +278,34 @@ Future<void> _writeIOSPluginRegistrant(FlutterProject project, List<Plugin> plug ...@@ -279,23 +278,34 @@ Future<void> _writeIOSPluginRegistrant(FlutterProject project, List<Plugin> plug
} }
} }
/// Rewrites the `.flutter-plugins` file of [project] based on the plugin
/// dependencies declared in `pubspec.yaml`.
///
/// Assumes `pub get` has been executed since last change to `pubspec.yaml`.
void refreshPluginsList(FlutterProject project) {
final List<Plugin> plugins = findPlugins(project);
final bool changed = _writeFlutterPluginsList(project, plugins);
if (changed)
cocoaPods.invalidatePodInstallOutput(project.ios);
}
/// Injects plugins found in `pubspec.yaml` into the platform-specific projects. /// Injects plugins found in `pubspec.yaml` into the platform-specific projects.
///
/// Assumes [refreshPluginsList] has been called since last change to `pubspec.yaml`.
Future<void> injectPlugins(FlutterProject project) async { Future<void> injectPlugins(FlutterProject project) async {
final List<Plugin> plugins = findPlugins(project); final List<Plugin> plugins = findPlugins(project);
final bool changed = _writeFlutterPluginsList(project, plugins);
await _writeAndroidPluginRegistrant(project, plugins); await _writeAndroidPluginRegistrant(project, plugins);
await _writeIOSPluginRegistrant(project, plugins); await _writeIOSPluginRegistrant(project, plugins);
if (!project.isModule && project.ios.directory.existsSync()) {
if (project.ios.directory.existsSync()) {
final CocoaPods cocoaPods = new CocoaPods(); final CocoaPods cocoaPods = new CocoaPods();
if (plugins.isNotEmpty) if (plugins.isNotEmpty)
cocoaPods.setupPodfile(project.ios); cocoaPods.setupPodfile(project.ios);
if (changed)
cocoaPods.invalidatePodInstallOutput(project.ios);
} }
} }
/// Returns whether the specified Flutter [project] has any plugin dependencies. /// Returns whether the specified Flutter [project] has any plugin dependencies.
///
/// Assumes [refreshPluginsList] has been called since last change to `pubspec.yaml`.
bool hasPlugins(FlutterProject project) { bool hasPlugins(FlutterProject project) {
return _readFlutterPluginsList(project) != null; return _readFlutterPluginsList(project) != null;
} }
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
...@@ -64,17 +63,17 @@ class FlutterProject { ...@@ -64,17 +63,17 @@ class FlutterProject {
/// The manifest of the example sub-project of this project. /// The manifest of the example sub-project of this project.
final FlutterManifest _exampleManifest; 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 /// part of iOS product bundle identifier, Android application ID, or
/// Gradle group ID. /// Gradle group ID.
Future<Set<String>> organizationNames() async { Set<String> get organizationNames {
final List<String> candidates = await Future.wait(<Future<String>>[ final List<String> candidates = <String>[
ios.productBundleIdentifier(), ios.productBundleIdentifier,
android.applicationId(), android.applicationId,
android.group(), android.group,
example.android.applicationId(), example.android.applicationId,
example.ios.productBundleIdentifier(), example.ios.productBundleIdentifier,
]); ];
return new Set<String>.from(candidates return new Set<String>.from(candidates
.map(_organizationNameFromPackageName) .map(_organizationNameFromPackageName)
.where((String name) => name != null)); .where((String name) => name != null));
...@@ -93,6 +92,13 @@ class FlutterProject { ...@@ -93,6 +92,13 @@ class FlutterProject {
/// The Android sub project of this project. /// The Android sub project of this project.
AndroidProject get android => new AndroidProject._(this); 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'); File get flutterPluginsFile => directory.childFile('.flutter-plugins');
/// The example sub-project of this project. /// The example sub-project of this project.
...@@ -128,6 +134,7 @@ class FlutterProject { ...@@ -128,6 +134,7 @@ class FlutterProject {
Future<void> ensureReadyForPlatformSpecificTooling() async { Future<void> ensureReadyForPlatformSpecificTooling() async {
if (!directory.existsSync() || hasExampleApp) if (!directory.existsSync() || hasExampleApp)
return; return;
refreshPluginsList(this);
await android.ensureReadyForPlatformSpecificTooling(); await android.ensureReadyForPlatformSpecificTooling();
await ios.ensureReadyForPlatformSpecificTooling(); await ios.ensureReadyForPlatformSpecificTooling();
await injectPlugins(this); await injectPlugins(this);
...@@ -140,6 +147,7 @@ class FlutterProject { ...@@ -140,6 +147,7 @@ class FlutterProject {
/// Flutter applications and the `.ios/` sub-folder of Flutter modules. /// Flutter applications and the `.ios/` sub-folder of Flutter modules.
class IosProject { class IosProject {
static final RegExp _productBundleIdPattern = new RegExp(r'^\s*PRODUCT_BUNDLE_IDENTIFIER\s*=\s*(.*);\s*$'); static final RegExp _productBundleIdPattern = new RegExp(r'^\s*PRODUCT_BUNDLE_IDENTIFIER\s*=\s*(.*);\s*$');
static const String _hostAppBundleName = 'Runner';
IosProject._(this.parent); IosProject._(this.parent);
...@@ -149,6 +157,8 @@ class IosProject { ...@@ -149,6 +157,8 @@ class IosProject {
/// The directory of this project. /// The directory of this project.
Directory get directory => parent.directory.childDirectory(isModule ? '.ios' : 'ios'); Directory get directory => parent.directory.childDirectory(isModule ? '.ios' : 'ios');
String get hostAppBundleName => '$_hostAppBundleName.app';
/// True, if the parent Flutter project is a module. /// True, if the parent Flutter project is a module.
bool get isModule => parent.isModule; bool get isModule => parent.isModule;
...@@ -164,19 +174,30 @@ class IosProject { ...@@ -164,19 +174,30 @@ class IosProject {
/// The 'Manifest.lock'. /// The 'Manifest.lock'.
File get podManifestLock => directory.childDirectory('Pods').childFile('Manifest.lock'); File get podManifestLock => directory.childDirectory('Pods').childFile('Manifest.lock');
Future<String> productBundleIdentifier() { /// '.xcodeproj' folder of the host app.
final File projectFile = directory.childDirectory('Runner.xcodeproj').childFile('project.pbxproj'); Directory get xcodeProject => directory.childDirectory('$_hostAppBundleName.xcodeproj');
return _firstMatchInFile(projectFile, _productBundleIdPattern).then((Match match) => match?.group(1));
/// 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);
} }
Future<void> ensureReadyForPlatformSpecificTooling() async { /// True, if the host app project is using Swift.
if (isModule && _shouldRegenerateFromTemplate()) { bool get isSwift => buildSettings?.containsKey('SWIFT_VERSION');
final Template template = new Template.fromName(fs.path.join('module', 'ios'));
template.render(directory, <String, dynamic>{}, printStatusWhenWriting: false); /// 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 {
_regenerateFromTemplateIfNeeded();
if (!directory.existsSync()) if (!directory.existsSync())
return; return;
if (Cache.instance.fileOlderThanToolsStamp(generatedXcodePropertiesFile)) { if (Cache.instance.isOlderThanToolsStamp(generatedXcodePropertiesFile)) {
await xcode.updateGeneratedXcodeProperties( await xcode.updateGeneratedXcodeProperties(
project: parent, project: parent,
buildInfo: BuildInfo.debug, buildInfo: BuildInfo.debug,
...@@ -186,12 +207,23 @@ class IosProject { ...@@ -186,12 +207,23 @@ class IosProject {
} }
} }
Future<void> materialize() async { void _regenerateFromTemplateIfNeeded() {
throwToolExit('flutter materialize has not yet been implemented for iOS'); 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() { Future<void> materialize() async {
return Cache.instance.fileOlderThanToolsStamp(directory.childFile('podhelper.rb')); throwToolExit('flutter materialize has not yet been implemented for iOS');
} }
File get generatedXcodePropertiesFile => directory.childDirectory('Flutter').childFile('Generated.xcconfig'); File get generatedXcodePropertiesFile => directory.childDirectory('Flutter').childFile('Generated.xcconfig');
...@@ -199,7 +231,20 @@ class IosProject { ...@@ -199,7 +231,20 @@ class IosProject {
Directory get pluginRegistrantHost { Directory get pluginRegistrantHost {
return isModule return isModule
? directory.childDirectory('Flutter').childDirectory('FlutterPluginRegistrant') ? 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 @@ class AndroidProject { ...@@ -237,7 +282,7 @@ class AndroidProject {
bool get isModule => parent.isModule; bool get isModule => parent.isModule;
File get appManifestFile { File get appManifestFile {
return isUsingGradle() return isUsingGradle
? fs.file(fs.path.join(hostAppGradleRoot.path, 'app', 'src', 'main', 'AndroidManifest.xml')) ? fs.file(fs.path.join(hostAppGradleRoot.path, 'app', 'src', 'main', 'AndroidManifest.xml'))
: hostAppGradleRoot.childFile('AndroidManifest.xml'); : hostAppGradleRoot.childFile('AndroidManifest.xml');
} }
...@@ -248,18 +293,18 @@ class AndroidProject { ...@@ -248,18 +293,18 @@ class AndroidProject {
return fs.directory(fs.path.join(hostAppGradleRoot.path, 'app', 'build', 'outputs', 'apk')); return fs.directory(fs.path.join(hostAppGradleRoot.path, 'app', 'build', 'outputs', 'apk'));
} }
bool isUsingGradle() { bool get isUsingGradle {
return hostAppGradleRoot.childFile('build.gradle').existsSync(); return hostAppGradleRoot.childFile('build.gradle').existsSync();
} }
Future<String> 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).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'); 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 { Future<void> ensureReadyForPlatformSpecificTooling() async {
...@@ -278,7 +323,8 @@ class AndroidProject { ...@@ -278,7 +323,8 @@ class AndroidProject {
} }
bool _shouldRegenerateFromTemplate() { 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 { Future<void> materialize() async {
...@@ -305,11 +351,6 @@ class AndroidProject { ...@@ -305,11 +351,6 @@ class AndroidProject {
gradle.injectGradleWrapper(_ephemeralDirectory); gradle.injectGradleWrapper(_ephemeralDirectory);
} }
void _deleteIfExistsSync(Directory directory) {
if (directory.existsSync())
directory.deleteSync(recursive: true);
}
void _overwriteFromTemplate(String path, Directory target) { void _overwriteFromTemplate(String path, Directory target) {
final Template template = new Template.fromName(path); final Template template = new Template.fromName(path);
template.render( template.render(
...@@ -324,17 +365,25 @@ class AndroidProject { ...@@ -324,17 +365,25 @@ class AndroidProject {
} }
} }
/// 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. /// Assumes UTF8 encoding.
Future<Match> _firstMatchInFile(File file, RegExp regExp) async { Match _firstMatchInFile(File file, RegExp regExp) {
if (!await file.exists()) { if (!file.existsSync()) {
return null; return null;
} }
return file for (String line in file.readAsLinesSync()) {
.openRead() final Match match = regExp.firstMatch(line);
.transform(utf8.decoder) if (match != null) {
.transform(const LineSplitter()) return match;
.map(regExp.firstMatch) }
.firstWhere((Match match) => match != null, orElse: () => null); }
return null;
} }
...@@ -47,7 +47,8 @@ ...@@ -47,7 +47,8 @@
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"androidPackage": { "type": "string" } "androidPackage": { "type": "string" },
"iosBundleIdentifier": { "type": "string" }
} }
}, },
"plugin": { "plugin": {
......
...@@ -25,9 +25,7 @@ Flutter views. ...@@ -25,9 +25,7 @@ Flutter views.
Written to `.android/` or `android/`. Written to `.android/` or `android/`.
Mixin for adding Gradle boilerplate to Android projects. The `build.gradle` Mixin for adding Gradle boilerplate to Android projects.
file is a template file so that it is created, not copied, on instantiation.
That way, its timestamp reflects template instantiation time.
#### host_app_common #### host_app_common
...@@ -59,9 +57,31 @@ under app author control) Android host app with a dependency on the ...@@ -59,9 +57,31 @@ under app author control) Android host app with a dependency on the
## ios ## ios
Written to the `.ios/` hidden folder. #### library
Written to the `.ios/Flutter` hidden folder.
Contents wraps Flutter/Dart code as a CocoaPods pod. Contents wraps Flutter/Dart code for consumption by an Xcode project.
iOS host apps can set up a dependency to this project to consume iOS host apps can set up a dependency to this contents to consume
Flutter views. Flutter views.
#### host_app_ephemeral
Written to `.ios/` outside the `Flutter/` sub-folder.
Combined contents define an *ephemeral* (hidden, auto-generated,
under Flutter tooling control) iOS host app with a dependency on the
`.ios/Flutter` folder contents.
The host app does not make use of CocoaPods, and is therefore
suitable only when the Flutter part declares no plugin dependencies.
#### host_app_ephemeral_cocoapods
Written to `.ios/` on top of `host_app_ephemeral`.
Adds CocoaPods support.
Combined contents define an ephemeral host app suitable for when the
Flutter part declares plugin dependencies.
...@@ -17,3 +17,4 @@ flutter: ...@@ -17,3 +17,4 @@ flutter:
uses-material-design: true uses-material-design: true
module: module:
androidPackage: {{androidIdentifier}} androidPackage: {{androidIdentifier}}
iosBundleIdentifier: {{iosIdentifier}}
#import <Flutter/Flutter.h>
#import <UIKit/UIKit.h>
@interface AppDelegate : FlutterAppDelegate
@end
#include "AppDelegate.h"
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
@end
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>{{projectName}}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>
#import <Flutter/Flutter.h>
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char* argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0910"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>
#include "Flutter.xcconfig"
#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Flutter.xcconfig"
#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
FLUTTER_BUILD_MODE=release
platform :ios, '8.0'
target 'Runner' do
flutter_application_path = '../'
eval(File.read(File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')))
end
#include "AppDelegate.h"
#import "FlutterPluginRegistrant/GeneratedPluginRegistrant.h"
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[GeneratedPluginRegistrant registerWithRegistry:self];
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
@end
# This file should be used from the target section of the host-app's Podfile like this:
# ```
# target 'host' do
# flutter_application_path = /"(.*)\/.ios\/Flutter\/Generated.xcconfig"/.match(File.read("./Flutter/FlutterConfig.xcconfig"))[1]
# eval(File.read(File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')))
# end
# ```
def parse_KV_file(file, separator='=') def parse_KV_file(file, separator='=')
file_abs_path = File.expand_path(file) file_abs_path = File.expand_path(file)
if !File.exists? file_abs_path if !File.exists? file_abs_path
......
...@@ -41,17 +41,6 @@ void main() { ...@@ -41,17 +41,6 @@ void main() {
}); });
}); });
group('BuildableIOSApp', () {
testUsingContext('check isSwift', () {
final BuildableIOSApp buildableIOSApp = new BuildableIOSApp(
projectBundleId: 'blah',
appDirectory: 'not/important',
buildSettings: _swiftBuildSettings,
);
expect(buildableIOSApp.isSwift, true);
});
});
group('PrebuiltIOSApp', () { group('PrebuiltIOSApp', () {
final Map<Type, Generator> overrides = <Type, Generator>{ final Map<Type, Generator> overrides = <Type, Generator>{
FileSystem: () => new MemoryFileSystem(), FileSystem: () => new MemoryFileSystem(),
...@@ -165,19 +154,6 @@ void main() { ...@@ -165,19 +154,6 @@ void main() {
}); });
} }
final Map<String, String> _swiftBuildSettings = <String, String>{
'ARCHS': 'arm64',
'ASSETCATALOG_COMPILER_APPICON_NAME': 'AppIcon',
'CLANG_ENABLE_MODULES': 'YES',
'ENABLE_BITCODE': 'NO',
'INFOPLIST_FILE': 'Runner/Info.plist',
'PRODUCT_BUNDLE_IDENTIFIER': 'com.example.test',
'PRODUCT_NAME': 'blah',
'SWIFT_OBJC_BRIDGING_HEADER': 'Runner/Runner-Bridging-Header.h',
'SWIFT_OPTIMIZATION_LEVEL': '-Onone',
'SWIFT_VERSION': '3.0',
};
const String _aaptDataWithExplicitEnabledActivity = const String _aaptDataWithExplicitEnabledActivity =
'''N: android=http://schemas.android.com/apk/res/android '''N: android=http://schemas.android.com/apk/res/android
E: manifest (line=7) E: manifest (line=7)
......
...@@ -340,7 +340,7 @@ void main() { ...@@ -340,7 +340,7 @@ void main() {
await _createProject(projectDir, <String>['--no-pub'], <String>[]); await _createProject(projectDir, <String>['--no-pub'], <String>[]);
final FlutterProject project = await FlutterProject.fromDirectory(projectDir); final FlutterProject project = await FlutterProject.fromDirectory(projectDir);
expect( expect(
await project.ios.productBundleIdentifier(), project.ios.productBundleIdentifier,
'com.bar.foo.flutterProject', 'com.bar.foo.flutterProject',
); );
}, timeout: allowForCreateFlutterProject); }, timeout: allowForCreateFlutterProject);
...@@ -367,7 +367,7 @@ void main() { ...@@ -367,7 +367,7 @@ void main() {
); );
final FlutterProject project = await FlutterProject.fromDirectory(projectDir); final FlutterProject project = await FlutterProject.fromDirectory(projectDir);
expect( expect(
await project.example.ios.productBundleIdentifier(), project.example.ios.productBundleIdentifier,
'com.bar.foo.flutterProjectExample', 'com.bar.foo.flutterProjectExample',
); );
}, timeout: allowForCreateFlutterProject); }, timeout: allowForCreateFlutterProject);
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:flutter_tools/src/project.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import 'package:flutter_tools/src/application_package.dart'; import 'package:flutter_tools/src/application_package.dart';
import 'package:flutter_tools/src/base/common.dart'; import 'package:flutter_tools/src/base/common.dart';
...@@ -16,39 +17,37 @@ import 'package:process/process.dart'; ...@@ -16,39 +17,37 @@ import 'package:process/process.dart';
import '../src/common.dart'; import '../src/common.dart';
import '../src/context.dart'; import '../src/context.dart';
import '../src/mocks.dart';
void main() { void main() {
group('Auto signing', () { group('Auto signing', () {
ProcessManager mockProcessManager; ProcessManager mockProcessManager;
Config mockConfig; Config mockConfig;
IosProject mockIosProject;
BuildableIOSApp app; BuildableIOSApp app;
AnsiTerminal testTerminal; AnsiTerminal testTerminal;
setUp(() { setUp(() {
mockProcessManager = new MockProcessManager(); mockProcessManager = new MockProcessManager();
mockConfig = new MockConfig(); mockConfig = new MockConfig();
testTerminal = new TestTerminal(); mockIosProject = new MockIosProject();
app = new BuildableIOSApp( when(mockIosProject.buildSettings).thenReturn(<String, String>{
projectBundleId: 'test.app',
buildSettings: <String, String>{
'For our purposes': 'a non-empty build settings map is valid', 'For our purposes': 'a non-empty build settings map is valid',
}, });
); testTerminal = new TestTerminal();
app = new BuildableIOSApp(mockIosProject);
}); });
testUsingContext('No auto-sign if Xcode project settings are not available', () async { testUsingContext('No auto-sign if Xcode project settings are not available', () async {
app = new BuildableIOSApp(projectBundleId: 'test.app'); when(mockIosProject.buildSettings).thenReturn(null);
final Map<String, String> signingConfigs = await getCodeSigningIdentityDevelopmentTeam(iosApp: app); final Map<String, String> signingConfigs = await getCodeSigningIdentityDevelopmentTeam(iosApp: app);
expect(signingConfigs, isNull); expect(signingConfigs, isNull);
}); });
testUsingContext('No discovery if development team specified in Xcode project', () async { testUsingContext('No discovery if development team specified in Xcode project', () async {
app = new BuildableIOSApp( when(mockIosProject.buildSettings).thenReturn(<String, String>{
projectBundleId: 'test.app',
buildSettings: <String, String>{
'DEVELOPMENT_TEAM': 'abc', 'DEVELOPMENT_TEAM': 'abc',
}, });
);
final Map<String, String> signingConfigs = await getCodeSigningIdentityDevelopmentTeam(iosApp: app); final Map<String, String> signingConfigs = await getCodeSigningIdentityDevelopmentTeam(iosApp: app);
expect(signingConfigs, isNull); expect(signingConfigs, isNull);
expect(testLogger.statusText, equals( expect(testLogger.statusText, equals(
......
...@@ -17,6 +17,7 @@ import 'package:process/process.dart'; ...@@ -17,6 +17,7 @@ import 'package:process/process.dart';
import '../src/common.dart'; import '../src/common.dart';
import '../src/context.dart'; import '../src/context.dart';
import '../src/mocks.dart';
class MockIMobileDevice extends Mock implements IMobileDevice {} class MockIMobileDevice extends Mock implements IMobileDevice {}
class MockProcessManager extends Mock implements ProcessManager {} class MockProcessManager extends Mock implements ProcessManager {}
...@@ -91,9 +92,11 @@ f577a7903cc54959be2e34bc4f7f80b7009efcf4 ...@@ -91,9 +92,11 @@ f577a7903cc54959be2e34bc4f7f80b7009efcf4
}); });
group('logging', () { group('logging', () {
MockIMobileDevice mockIMobileDevice; MockIMobileDevice mockIMobileDevice;
MockIosProject mockIosProject;
setUp(() { setUp(() {
mockIMobileDevice = new MockIMobileDevice(); mockIMobileDevice = new MockIMobileDevice();
mockIosProject = new MockIosProject();
}); });
testUsingContext('suppresses non-Flutter lines from output', () async { testUsingContext('suppresses non-Flutter lines from output', () async {
...@@ -117,7 +120,7 @@ f577a7903cc54959be2e34bc4f7f80b7009efcf4 ...@@ -117,7 +120,7 @@ f577a7903cc54959be2e34bc4f7f80b7009efcf4
final IOSDevice device = new IOSDevice('123456'); final IOSDevice device = new IOSDevice('123456');
final DeviceLogReader logReader = device.getLogReader( final DeviceLogReader logReader = device.getLogReader(
app: new BuildableIOSApp(projectBundleId: 'bundleId'), app: new BuildableIOSApp(mockIosProject),
); );
final List<String> lines = await logReader.logLines.toList(); final List<String> lines = await logReader.logLines.toList();
...@@ -147,7 +150,7 @@ f577a7903cc54959be2e34bc4f7f80b7009efcf4 ...@@ -147,7 +150,7 @@ f577a7903cc54959be2e34bc4f7f80b7009efcf4
final IOSDevice device = new IOSDevice('123456'); final IOSDevice device = new IOSDevice('123456');
final DeviceLogReader logReader = device.getLogReader( final DeviceLogReader logReader = device.getLogReader(
app: new BuildableIOSApp(projectBundleId: 'bundleId'), app: new BuildableIOSApp(mockIosProject),
); );
final List<String> lines = await logReader.logLines.toList(); final List<String> lines = await logReader.logLines.toList();
......
...@@ -13,6 +13,7 @@ import 'package:process/process.dart'; ...@@ -13,6 +13,7 @@ import 'package:process/process.dart';
import '../src/common.dart'; import '../src/common.dart';
import '../src/context.dart'; import '../src/context.dart';
import '../src/mocks.dart';
class MockFile extends Mock implements File {} class MockFile extends Mock implements File {}
class MockIMobileDevice extends Mock implements IMobileDevice {} class MockIMobileDevice extends Mock implements IMobileDevice {}
...@@ -291,9 +292,11 @@ void main() { ...@@ -291,9 +292,11 @@ void main() {
group('log reader', () { group('log reader', () {
MockProcessManager mockProcessManager; MockProcessManager mockProcessManager;
MockIosProject mockIosProject;
setUp(() { setUp(() {
mockProcessManager = new MockProcessManager(); mockProcessManager = new MockProcessManager();
mockIosProject = new MockIosProject();
}); });
testUsingContext('simulator can output `)`', () async { testUsingContext('simulator can output `)`', () async {
...@@ -316,7 +319,7 @@ void main() { ...@@ -316,7 +319,7 @@ void main() {
final IOSSimulator device = new IOSSimulator('123456', category: 'iOS 11.0'); final IOSSimulator device = new IOSSimulator('123456', category: 'iOS 11.0');
final DeviceLogReader logReader = device.getLogReader( final DeviceLogReader logReader = device.getLogReader(
app: new BuildableIOSApp(projectBundleId: 'bundleId'), app: new BuildableIOSApp(mockIosProject),
); );
final List<String> lines = await logReader.logLines.toList(); final List<String> lines = await logReader.logLines.toList();
......
...@@ -224,50 +224,50 @@ void main() { ...@@ -224,50 +224,50 @@ void main() {
group('organization names set', () { group('organization names set', () {
testInMemory('is empty, if project not created', () async { testInMemory('is empty, if project not created', () async {
final FlutterProject project = await someProject(); final FlutterProject project = await someProject();
expect(await project.organizationNames(), isEmpty); expect(project.organizationNames, isEmpty);
}); });
testInMemory('is empty, if no platform folders exist', () async { testInMemory('is empty, if no platform folders exist', () async {
final FlutterProject project = await someProject(); final FlutterProject project = await someProject();
project.directory.createSync(); project.directory.createSync();
expect(await project.organizationNames(), isEmpty); expect(project.organizationNames, isEmpty);
}); });
testInMemory('is populated from iOS bundle identifier', () async { testInMemory('is populated from iOS bundle identifier', () async {
final FlutterProject project = await someProject(); final FlutterProject project = await someProject();
addIosWithBundleId(project.directory, 'io.flutter.someProject'); addIosWithBundleId(project.directory, 'io.flutter.someProject');
expect(await project.organizationNames(), <String>['io.flutter']); expect(project.organizationNames, <String>['io.flutter']);
}); });
testInMemory('is populated from Android application ID', () async { testInMemory('is populated from Android application ID', () async {
final FlutterProject project = await someProject(); final FlutterProject project = await someProject();
addAndroidWithApplicationId(project.directory, 'io.flutter.someproject'); addAndroidWithApplicationId(project.directory, 'io.flutter.someproject');
expect(await project.organizationNames(), <String>['io.flutter']); expect(project.organizationNames, <String>['io.flutter']);
}); });
testInMemory('is populated from iOS bundle identifier in plugin example', () async { testInMemory('is populated from iOS bundle identifier in plugin example', () async {
final FlutterProject project = await someProject(); final FlutterProject project = await someProject();
addIosWithBundleId(project.example.directory, 'io.flutter.someProject'); addIosWithBundleId(project.example.directory, 'io.flutter.someProject');
expect(await project.organizationNames(), <String>['io.flutter']); expect(project.organizationNames, <String>['io.flutter']);
}); });
testInMemory('is populated from Android application ID in plugin example', () async { testInMemory('is populated from Android application ID in plugin example', () async {
final FlutterProject project = await someProject(); final FlutterProject project = await someProject();
addAndroidWithApplicationId(project.example.directory, 'io.flutter.someproject'); addAndroidWithApplicationId(project.example.directory, 'io.flutter.someproject');
expect(await project.organizationNames(), <String>['io.flutter']); expect(project.organizationNames, <String>['io.flutter']);
}); });
testInMemory('is populated from Android group in plugin', () async { testInMemory('is populated from Android group in plugin', () async {
final FlutterProject project = await someProject(); final FlutterProject project = await someProject();
addAndroidWithGroup(project.directory, 'io.flutter.someproject'); addAndroidWithGroup(project.directory, 'io.flutter.someproject');
expect(await project.organizationNames(), <String>['io.flutter']); expect(project.organizationNames, <String>['io.flutter']);
}); });
testInMemory('is singleton, if sources agree', () async { testInMemory('is singleton, if sources agree', () async {
final FlutterProject project = await someProject(); final FlutterProject project = await someProject();
addIosWithBundleId(project.directory, 'io.flutter.someProject'); addIosWithBundleId(project.directory, 'io.flutter.someProject');
addAndroidWithApplicationId(project.directory, 'io.flutter.someproject'); addAndroidWithApplicationId(project.directory, 'io.flutter.someproject');
expect(await project.organizationNames(), <String>['io.flutter']); expect(project.organizationNames, <String>['io.flutter']);
}); });
testInMemory('is non-singleton, if sources disagree', () async { testInMemory('is non-singleton, if sources disagree', () async {
final FlutterProject project = await someProject(); final FlutterProject project = await someProject();
addIosWithBundleId(project.directory, 'io.flutter.someProject'); addIosWithBundleId(project.directory, 'io.flutter.someProject');
addAndroidWithApplicationId(project.directory, 'io.clutter.someproject'); addAndroidWithApplicationId(project.directory, 'io.clutter.someproject');
expect( expect(
await project.organizationNames(), project.organizationNames,
<String>['io.flutter', 'io.clutter'], <String>['io.flutter', 'io.clutter'],
); );
}); });
......
...@@ -16,6 +16,7 @@ import 'package:flutter_tools/src/devfs.dart'; ...@@ -16,6 +16,7 @@ import 'package:flutter_tools/src/devfs.dart';
import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/ios/devices.dart'; import 'package:flutter_tools/src/ios/devices.dart';
import 'package:flutter_tools/src/ios/simulators.dart'; import 'package:flutter_tools/src/ios/simulators.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/runner/flutter_command.dart'; import 'package:flutter_tools/src/runner/flutter_command.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import 'package:process/process.dart'; import 'package:process/process.dart';
...@@ -29,10 +30,7 @@ class MockApplicationPackageStore extends ApplicationPackageStore { ...@@ -29,10 +30,7 @@ class MockApplicationPackageStore extends ApplicationPackageStore {
file: fs.file('/mock/path/to/android/SkyShell.apk'), file: fs.file('/mock/path/to/android/SkyShell.apk'),
launchActivity: 'io.flutter.android.mock.MockActivity' launchActivity: 'io.flutter.android.mock.MockActivity'
), ),
iOS: new BuildableIOSApp( iOS: new BuildableIOSApp(new MockIosProject())
appDirectory: '/mock/path/to/iOS/SkyShell.app',
projectBundleId: 'io.flutter.ios.mock'
)
); );
} }
...@@ -335,6 +333,14 @@ class MockPollingDeviceDiscovery extends PollingDeviceDiscovery { ...@@ -335,6 +333,14 @@ class MockPollingDeviceDiscovery extends PollingDeviceDiscovery {
Stream<Device> get onRemoved => _onRemovedController.stream; Stream<Device> get onRemoved => _onRemovedController.stream;
} }
class MockIosProject extends Mock implements IosProject {
@override
String get productBundleIdentifier => 'com.example.test';
@override
String get hostAppBundleName => 'Runner.app';
}
class MockAndroidDevice extends Mock implements AndroidDevice { class MockAndroidDevice extends Mock implements AndroidDevice {
@override @override
Future<TargetPlatform> get targetPlatform async => TargetPlatform.android_arm; Future<TargetPlatform> get targetPlatform async => TargetPlatform.android_arm;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment