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({
@required String target,
BuildInfo buildInfo = BuildInfo.debug
}) async {
if (!project.android.isUsingGradle()) {
if (!project.android.isUsingGradle) {
throwToolExit(
'The build process for Android has changed, and the current project configuration\n'
'is no longer valid. Please consult\n\n'
......
......@@ -17,7 +17,6 @@ import 'build_info.dart';
import 'globals.dart';
import 'ios/ios_workflow.dart';
import 'ios/plist_utils.dart' as plist;
import 'ios/xcodeproj.dart';
import 'project.dart';
import 'tester/flutter_tester.dart';
......@@ -93,7 +92,7 @@ class AndroidApk extends ApplicationPackage {
static Future<AndroidApk> fromAndroidProject(AndroidProject androidProject) async {
File apkFile;
if (androidProject.isUsingGradle()) {
if (androidProject.isUsingGradle) {
apkFile = await getGradleAppOut(androidProject);
if (apkFile.existsSync()) {
// Grab information from the .apk. The gradle build script might alter
......@@ -217,26 +216,10 @@ abstract class IOSApp extends ApplicationPackage {
);
}
factory IOSApp.fromCurrentDirectory() {
factory IOSApp.fromIosProject(IosProject project) {
if (getCurrentHostPlatform() != HostPlatform.darwin_x64)
return null;
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,
);
return new BuildableIOSApp(project);
}
@override
......@@ -248,25 +231,12 @@ abstract class IOSApp extends ApplicationPackage {
}
class BuildableIOSApp extends IOSApp {
static const String kBundleName = 'Runner.app';
BuildableIOSApp({
this.appDirectory,
String projectBundleId,
this.buildSettings,
}) : super(projectBundleId: projectBundleId);
final String appDirectory;
BuildableIOSApp(this.project) : super(projectBundleId: project.productBundleIdentifier);
/// Build settings of the app's Xcode 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;
final IosProject project;
@override
String get name => kBundleName;
String get name => project.hostAppBundleName;
@override
String get simulatorBundlePath => _buildAppPath('iphonesimulator');
......@@ -274,11 +244,8 @@ class BuildableIOSApp extends IOSApp {
@override
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) {
return fs.path.join(getIosBuildDirectory(), type, kBundleName);
return fs.path.join(getIosBuildDirectory(), type, name);
}
}
......@@ -317,7 +284,7 @@ Future<ApplicationPackage> getApplicationPackageForPlatform(
: new AndroidApk.fromApk(applicationBinary);
case TargetPlatform.ios:
return applicationBinary == null
? new IOSApp.fromCurrentDirectory()
? new IOSApp.fromIosProject((await FlutterProject.current()).ios)
: new IOSApp.fromPrebuiltApp(applicationBinary);
case TargetPlatform.tester:
return new FlutterTesterApp.fromCurrentDirectory();
......@@ -346,7 +313,7 @@ class ApplicationPackageStore {
android ??= await AndroidApk.fromAndroidProject((await FlutterProject.current()).android);
return android;
case TargetPlatform.ios:
iOS ??= new IOSApp.fromCurrentDirectory();
iOS ??= IOSApp.fromIosProject((await FlutterProject.current()).ios);
return iOS;
case TargetPlatform.darwin_x64:
case TargetPlatform.linux_x64:
......
......@@ -6,6 +6,7 @@ import 'package:file/file.dart';
import 'package:file/local.dart';
import 'package:file/memory.dart';
import 'package:file/record_replay.dart';
import 'package:meta/meta.dart';
import 'common.dart' show throwToolExit;
import 'context.dart';
......@@ -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
/// path unchanged.
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 {
return fs.file(fs.path.join(getRoot().path, '$artifactName.stamp'));
}
/// Returns `true` if either [file] is older than the tools stamp or if
/// [file] doesn't exist.
bool fileOlderThanToolsStamp(File file) {
if (!file.existsSync()) {
return true;
}
/// Returns `true` if either [entity] is older than the tools stamp or if
/// [entity] doesn't exist.
bool isOlderThanToolsStamp(FileSystemEntity entity) {
final File flutterToolsStamp = getStampFileFor('flutter_tools');
return flutterToolsStamp.existsSync() &&
flutterToolsStamp
.lastModifiedSync()
.isAfter(file.lastModifiedSync());
return isOlderThanReference(entity: entity, referenceFile: flutterToolsStamp);
}
bool isUpToDate() => _artifacts.every((CachedArtifact artifact) => artifact.isUpToDate());
......
......@@ -133,7 +133,7 @@ class CreateCommand extends FlutterCommand {
String organization = argResults['org'];
if (!argResults.wasParsed('org')) {
final FlutterProject project = await FlutterProject.fromDirectory(projectDir);
final Set<String> existingOrganizations = await project.organizationNames();
final Set<String> existingOrganizations = project.organizationNames;
if (existingOrganizations.length == 1) {
organization = existingOrganizations.first;
} else if (1 < existingOrganizations.length) {
......
......@@ -26,6 +26,7 @@ class InjectPluginsCommand extends FlutterCommand {
@override
Future<Null> runCommand() async {
final FlutterProject project = await FlutterProject.current();
refreshPluginsList(project);
await injectPlugins(project);
final bool result = hasPlugins(project);
if (result) {
......
......@@ -140,6 +140,14 @@ class FlutterManifest {
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 {
final List<dynamic> fontList = _flutterDescriptor['fonts'];
return fontList == null
......
......@@ -96,20 +96,21 @@ Future<Map<String, String>> getCodeSigningIdentityDevelopmentTeam({
BuildableIOSApp iosApp,
bool usesTerminalUi = true
}) async{
if (iosApp.buildSettings == null)
final Map<String, String> buildSettings = iosApp.project.buildSettings;
if (buildSettings == null)
return null;
// If the user already has it set in the project build settings itself,
// continue with that.
if (isNotEmpty(iosApp.buildSettings['DEVELOPMENT_TEAM'])) {
if (isNotEmpty(buildSettings['DEVELOPMENT_TEAM'])) {
printStatus(
'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;
}
if (isNotEmpty(iosApp.buildSettings['PROVISIONING_PROFILE']))
if (isNotEmpty(buildSettings['PROVISIONING_PROFILE']))
return null;
// If the user's environment is missing the tools needed to find and read
......
......@@ -189,13 +189,13 @@ Future<XcodeBuildResult> buildXcodeProject({
bool codesign = true,
bool usesTerminalUi = true,
}) async {
if (!await upgradePbxProjWithFlutterAssets(app.name, app.appDirectory))
if (!await upgradePbxProjWithFlutterAssets(app.project))
return new XcodeBuildResult(success: false);
if (!_checkXcodeVersion())
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')) {
printError('The Xcode project does not define target "Runner" which is needed by Flutter tooling.');
printError('Open Xcode to fix the problem:');
......@@ -230,8 +230,7 @@ Future<XcodeBuildResult> buildXcodeProject({
// 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.
final Directory appDirectory = fs.directory(app.appDirectory);
await _addServicesToBundle(appDirectory);
await _addServicesToBundle(app.project.directory);
final FlutterProject project = await FlutterProject.current();
await updateGeneratedXcodeProperties(
......@@ -242,22 +241,21 @@ Future<XcodeBuildResult> buildXcodeProject({
);
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
// last run, pods should be updated.
final Fingerprinter fingerprinter = new Fingerprinter(
fingerprintPath: fs.path.join(getIosBuildDirectory(), 'pod_inputs.fingerprint'),
paths: <String>[
_getPbxProjPath(app.appDirectory),
fs.path.join(iosPath, 'Podfile'),
fs.path.join(iosPath, 'Flutter', 'Generated.xcconfig'),
app.project.xcodeProjectInfoFile.path,
app.project.podfile.path,
app.project.generatedXcodePropertiesFile.path,
],
properties: <String, String>{},
);
final bool didPodInstall = await cocoaPods.processPods(
iosProject: project.ios,
iosEngineDir: flutterFrameworkDir(buildInfo.mode),
isSwift: app.isSwift,
isSwift: project.ios.isSwift,
dependenciesChanged: !await fingerprinter.doesFingerprintMatch()
);
if (didPodInstall)
......@@ -288,7 +286,7 @@ Future<XcodeBuildResult> buildXcodeProject({
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) {
if (fs.path.extension(entity.path) == '.xcworkspace') {
buildCommands.addAll(<String>[
......@@ -353,7 +351,7 @@ Future<XcodeBuildResult> buildXcodeProject({
initialBuildStatus = logger.startProgress('Starting Xcode build...');
final RunResult buildResult = await runAsync(
buildCommands,
workingDirectory: app.appDirectory,
workingDirectory: app.project.directory.path,
allowReentrantFlutter: true
);
buildSubStatus?.stop();
......@@ -381,7 +379,7 @@ Future<XcodeBuildResult> buildXcodeProject({
'-allowProvisioningDeviceRegistration',
].contains(buildCommand);
}).toList(),
workingDirectory: app.appDirectory,
workingDirectory: app.project.directory.path,
));
if (buildResult.exitCode != 0) {
......@@ -400,7 +398,7 @@ Future<XcodeBuildResult> buildXcodeProject({
stderr: buildResult.stderr,
xcodeBuildExecution: new XcodeBuildExecution(
buildCommands: buildCommands,
appDirectory: app.appDirectory,
appDirectory: app.project.directory.path,
buildForPhysicalDevice: buildForDevice,
buildSettings: buildSettings,
),
......@@ -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) {
printTrace("Creating service definitions manifest at '${manifest.path}'");
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
manifest.writeAsStringSync(json.encode(jsonObject), mode: FileMode.write, flush: true);
}
Future<bool> upgradePbxProjWithFlutterAssets(String app, String appPath) async {
final File xcodeProjectFile = fs.file(_getPbxProjPath(appPath));
Future<bool> upgradePbxProjWithFlutterAssets(IosProject project) async {
final File xcodeProjectFile = project.xcodeProjectInfoFile;
assert(await xcodeProjectFile.exists());
final List<String> lines = await xcodeProjectFile.readAsLines();
......@@ -601,7 +596,7 @@ Future<bool> upgradePbxProjWithFlutterAssets(String app, String appPath) async {
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");
if (!lines.contains(l1) || !lines.contains(l3) ||
......
......@@ -241,8 +241,7 @@ Future<void> _writeIOSPluginRegistrant(FlutterProject project, List<Plugin> plug
'name': p.name,
'prefix': p.iosPrefix,
'class': p.pluginClass,
}).
toList();
}).toList();
final Map<String, dynamic> context = <String, dynamic>{
'plugins': iosPlugins,
};
......@@ -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.
///
/// Assumes [refreshPluginsList] has been called since last change to `pubspec.yaml`.
Future<void> injectPlugins(FlutterProject project) async {
final List<Plugin> plugins = findPlugins(project);
final bool changed = _writeFlutterPluginsList(project, plugins);
await _writeAndroidPluginRegistrant(project, plugins);
await _writeIOSPluginRegistrant(project, plugins);
if (project.ios.directory.existsSync()) {
if (!project.isModule && project.ios.directory.existsSync()) {
final CocoaPods cocoaPods = new CocoaPods();
if (plugins.isNotEmpty)
cocoaPods.setupPodfile(project.ios);
if (changed)
cocoaPods.invalidatePodInstallOutput(project.ios);
}
}
/// 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) {
return _readFlutterPluginsList(project) != null;
}
......@@ -3,7 +3,6 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'package:meta/meta.dart';
......@@ -64,17 +63,17 @@ class FlutterProject {
/// 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 @@ class FlutterProject {
/// 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 @@ class FlutterProject {
Future<void> ensureReadyForPlatformSpecificTooling() async {
if (!directory.existsSync() || hasExampleApp)
return;
refreshPluginsList(this);
await android.ensureReadyForPlatformSpecificTooling();
await ios.ensureReadyForPlatformSpecificTooling();
await injectPlugins(this);
......@@ -140,6 +147,7 @@ class FlutterProject {
/// 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 @@ class IosProject {
/// 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 @@ class IosProject {
/// 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 @@ class IosProject {
}
}
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 @@ class IosProject {
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 @@ class AndroidProject {
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 @@ class AndroidProject {
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 @@ class AndroidProject {
}
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 @@ class AndroidProject {
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 @@ 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.
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;
}
......@@ -47,7 +47,8 @@
"type": "object",
"additionalProperties": false,
"properties": {
"androidPackage": { "type": "string" }
"androidPackage": { "type": "string" },
"iosBundleIdentifier": { "type": "string" }
}
},
"plugin": {
......
......@@ -25,9 +25,7 @@ Flutter views.
Written to `.android/` or `android/`.
Mixin for adding Gradle boilerplate to Android projects. The `build.gradle`
file is a template file so that it is created, not copied, on instantiation.
That way, its timestamp reflects template instantiation time.
Mixin for adding Gradle boilerplate to Android projects.
#### host_app_common
......@@ -59,9 +57,31 @@ under app author control) Android host app with a dependency on the
## 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.
#### 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:
uses-material-design: true
module:
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='=')
file_abs_path = File.expand_path(file)
if !File.exists? file_abs_path
......
......@@ -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', () {
final Map<Type, Generator> overrides = <Type, Generator>{
FileSystem: () => new MemoryFileSystem(),
......@@ -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 =
'''N: android=http://schemas.android.com/apk/res/android
E: manifest (line=7)
......
......@@ -340,7 +340,7 @@ void main() {
await _createProject(projectDir, <String>['--no-pub'], <String>[]);
final FlutterProject project = await FlutterProject.fromDirectory(projectDir);
expect(
await project.ios.productBundleIdentifier(),
project.ios.productBundleIdentifier,
'com.bar.foo.flutterProject',
);
}, timeout: allowForCreateFlutterProject);
......@@ -367,7 +367,7 @@ void main() {
);
final FlutterProject project = await FlutterProject.fromDirectory(projectDir);
expect(
await project.example.ios.productBundleIdentifier(),
project.example.ios.productBundleIdentifier,
'com.bar.foo.flutterProjectExample',
);
}, timeout: allowForCreateFlutterProject);
......
......@@ -4,6 +4,7 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter_tools/src/project.dart';
import 'package:mockito/mockito.dart';
import 'package:flutter_tools/src/application_package.dart';
import 'package:flutter_tools/src/base/common.dart';
......@@ -16,39 +17,37 @@ import 'package:process/process.dart';
import '../src/common.dart';
import '../src/context.dart';
import '../src/mocks.dart';
void main() {
group('Auto signing', () {
ProcessManager mockProcessManager;
Config mockConfig;
IosProject mockIosProject;
BuildableIOSApp app;
AnsiTerminal testTerminal;
setUp(() {
mockProcessManager = new MockProcessManager();
mockConfig = new MockConfig();
mockIosProject = new MockIosProject();
when(mockIosProject.buildSettings).thenReturn(<String, String>{
'For our purposes': 'a non-empty build settings map is valid',
});
testTerminal = new TestTerminal();
app = new BuildableIOSApp(
projectBundleId: 'test.app',
buildSettings: <String, String>{
'For our purposes': 'a non-empty build settings map is valid',
},
);
app = new BuildableIOSApp(mockIosProject);
});
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);
expect(signingConfigs, isNull);
});
testUsingContext('No discovery if development team specified in Xcode project', () async {
app = new BuildableIOSApp(
projectBundleId: 'test.app',
buildSettings: <String, String>{
'DEVELOPMENT_TEAM': 'abc',
},
);
when(mockIosProject.buildSettings).thenReturn(<String, String>{
'DEVELOPMENT_TEAM': 'abc',
});
final Map<String, String> signingConfigs = await getCodeSigningIdentityDevelopmentTeam(iosApp: app);
expect(signingConfigs, isNull);
expect(testLogger.statusText, equals(
......
......@@ -17,6 +17,7 @@ import 'package:process/process.dart';
import '../src/common.dart';
import '../src/context.dart';
import '../src/mocks.dart';
class MockIMobileDevice extends Mock implements IMobileDevice {}
class MockProcessManager extends Mock implements ProcessManager {}
......@@ -91,9 +92,11 @@ f577a7903cc54959be2e34bc4f7f80b7009efcf4
});
group('logging', () {
MockIMobileDevice mockIMobileDevice;
MockIosProject mockIosProject;
setUp(() {
mockIMobileDevice = new MockIMobileDevice();
mockIosProject = new MockIosProject();
});
testUsingContext('suppresses non-Flutter lines from output', () async {
......@@ -117,7 +120,7 @@ f577a7903cc54959be2e34bc4f7f80b7009efcf4
final IOSDevice device = new IOSDevice('123456');
final DeviceLogReader logReader = device.getLogReader(
app: new BuildableIOSApp(projectBundleId: 'bundleId'),
app: new BuildableIOSApp(mockIosProject),
);
final List<String> lines = await logReader.logLines.toList();
......@@ -147,7 +150,7 @@ f577a7903cc54959be2e34bc4f7f80b7009efcf4
final IOSDevice device = new IOSDevice('123456');
final DeviceLogReader logReader = device.getLogReader(
app: new BuildableIOSApp(projectBundleId: 'bundleId'),
app: new BuildableIOSApp(mockIosProject),
);
final List<String> lines = await logReader.logLines.toList();
......
......@@ -13,6 +13,7 @@ import 'package:process/process.dart';
import '../src/common.dart';
import '../src/context.dart';
import '../src/mocks.dart';
class MockFile extends Mock implements File {}
class MockIMobileDevice extends Mock implements IMobileDevice {}
......@@ -291,9 +292,11 @@ void main() {
group('log reader', () {
MockProcessManager mockProcessManager;
MockIosProject mockIosProject;
setUp(() {
mockProcessManager = new MockProcessManager();
mockIosProject = new MockIosProject();
});
testUsingContext('simulator can output `)`', () async {
......@@ -316,7 +319,7 @@ void main() {
final IOSSimulator device = new IOSSimulator('123456', category: 'iOS 11.0');
final DeviceLogReader logReader = device.getLogReader(
app: new BuildableIOSApp(projectBundleId: 'bundleId'),
app: new BuildableIOSApp(mockIosProject),
);
final List<String> lines = await logReader.logLines.toList();
......
......@@ -224,50 +224,50 @@ void main() {
group('organization names set', () {
testInMemory('is empty, if project not created', () async {
final FlutterProject project = await someProject();
expect(await project.organizationNames(), isEmpty);
expect(project.organizationNames, isEmpty);
});
testInMemory('is empty, if no platform folders exist', () async {
final FlutterProject project = await someProject();
project.directory.createSync();
expect(await project.organizationNames(), isEmpty);
expect(project.organizationNames, isEmpty);
});
testInMemory('is populated from iOS bundle identifier', () async {
final FlutterProject project = await 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 {
final FlutterProject project = await 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 {
final FlutterProject project = await 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 {
final FlutterProject project = await 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 {
final FlutterProject project = await 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 {
final FlutterProject project = await someProject();
addIosWithBundleId(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 {
final FlutterProject project = await someProject();
addIosWithBundleId(project.directory, 'io.flutter.someProject');
addAndroidWithApplicationId(project.directory, 'io.clutter.someproject');
expect(
await project.organizationNames(),
project.organizationNames,
<String>['io.flutter', 'io.clutter'],
);
});
......
......@@ -16,6 +16,7 @@ import 'package:flutter_tools/src/devfs.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/ios/devices.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:mockito/mockito.dart';
import 'package:process/process.dart';
......@@ -29,10 +30,7 @@ class MockApplicationPackageStore extends ApplicationPackageStore {
file: fs.file('/mock/path/to/android/SkyShell.apk'),
launchActivity: 'io.flutter.android.mock.MockActivity'
),
iOS: new BuildableIOSApp(
appDirectory: '/mock/path/to/iOS/SkyShell.app',
projectBundleId: 'io.flutter.ios.mock'
)
iOS: new BuildableIOSApp(new MockIosProject())
);
}
......@@ -335,6 +333,14 @@ class MockPollingDeviceDiscovery extends PollingDeviceDiscovery {
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 {
@override
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