Unverified Commit 758711c8 authored by Sigurd Meldgaard's avatar Sigurd Meldgaard Committed by GitHub

Allow `--use-application-binary` using app-bundles on ios (#17691)

This makes it easier to run ios add2app apps with Flutter run.
parent b7fd1d8b
...@@ -10,7 +10,8 @@ import '../base/platform.dart'; ...@@ -10,7 +10,8 @@ import '../base/platform.dart';
import '../base/process_manager.dart'; import '../base/process_manager.dart';
import '../base/version.dart'; import '../base/version.dart';
import '../globals.dart'; import '../globals.dart';
import '../ios/plist_utils.dart'; import '../ios/ios_workflow.dart';
import '../ios/plist_utils.dart' as plist;
AndroidStudio get androidStudio => context[AndroidStudio]; AndroidStudio get androidStudio => context[AndroidStudio];
...@@ -46,8 +47,11 @@ class AndroidStudio implements Comparable<AndroidStudio> { ...@@ -46,8 +47,11 @@ class AndroidStudio implements Comparable<AndroidStudio> {
factory AndroidStudio.fromMacOSBundle(String bundlePath) { factory AndroidStudio.fromMacOSBundle(String bundlePath) {
final String studioPath = fs.path.join(bundlePath, 'Contents'); final String studioPath = fs.path.join(bundlePath, 'Contents');
final String plistFile = fs.path.join(studioPath, 'Info.plist'); final String plistFile = fs.path.join(studioPath, 'Info.plist');
final String versionString = final String versionString = iosWorkflow.getPlistValueFromFile(
getValueFromFile(plistFile, kCFBundleShortVersionStringKey); plistFile,
plist.kCFBundleShortVersionStringKey,
);
Version version; Version version;
if (versionString != null) if (versionString != null)
version = new Version.parse(versionString); version = new Version.parse(versionString);
......
...@@ -15,6 +15,7 @@ import 'base/os.dart' show os; ...@@ -15,6 +15,7 @@ import 'base/os.dart' show os;
import 'base/process.dart'; import 'base/process.dart';
import 'build_info.dart'; import 'build_info.dart';
import 'globals.dart'; import 'globals.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 'ios/xcodeproj.dart';
import 'tester/flutter_tester.dart'; import 'tester/flutter_tester.dart';
...@@ -145,29 +146,61 @@ bool _isBundleDirectory(FileSystemEntity entity) => ...@@ -145,29 +146,61 @@ bool _isBundleDirectory(FileSystemEntity entity) =>
abstract class IOSApp extends ApplicationPackage { abstract class IOSApp extends ApplicationPackage {
IOSApp({@required String projectBundleId}) : super(id: projectBundleId); IOSApp({@required String projectBundleId}) : super(id: projectBundleId);
/// Creates a new IOSApp from an existing IPA. /// Creates a new IOSApp from an existing app bundle or IPA.
factory IOSApp.fromIpa(String applicationBinary) { factory IOSApp.fromPrebuiltApp(String applicationBinary) {
final FileSystemEntityType entityType = fs.typeSync(applicationBinary);
if (entityType == FileSystemEntityType.notFound) {
printError(
'File "$applicationBinary" does not exist. Use an app bundle or an ipa.');
return null;
}
Directory bundleDir; Directory bundleDir;
try { if (entityType == FileSystemEntityType.directory) {
final Directory tempDir = fs.systemTempDirectory.createTempSync('flutter_app_'); final Directory directory = fs.directory(applicationBinary);
if (!_isBundleDirectory(directory)) {
printError('Folder "$applicationBinary" is not an app bundle.');
return null;
}
bundleDir = fs.directory(applicationBinary);
} else {
// Try to unpack as an ipa.
final Directory tempDir = fs.systemTempDirectory.createTempSync(
'flutter_app_');
addShutdownHook(() async { addShutdownHook(() async {
await tempDir.delete(recursive: true); await tempDir.delete(recursive: true);
}, ShutdownStage.STILL_RECORDING); }, ShutdownStage.STILL_RECORDING);
os.unzip(fs.file(applicationBinary), tempDir); os.unzip(fs.file(applicationBinary), tempDir);
final Directory payloadDir = fs.directory(fs.path.join(tempDir.path, 'Payload')); final Directory payloadDir = fs.directory(
fs.path.join(tempDir.path, 'Payload'),
);
if (!payloadDir.existsSync()) {
printError(
'Invalid prebuilt iOS ipa. Does not contain a "Payload" directory.');
return null;
}
try {
bundleDir = payloadDir.listSync().singleWhere(_isBundleDirectory); bundleDir = payloadDir.listSync().singleWhere(_isBundleDirectory);
} on StateError catch (e, stackTrace) { } on StateError {
printError('Invalid prebuilt iOS binary: ${e.toString()}', stackTrace: stackTrace); printError(
'Invalid prebuilt iOS ipa. Does not contain a single app bundle.');
return null; return null;
} }
}
final String plistPath = fs.path.join(bundleDir.path, 'Info.plist'); final String plistPath = fs.path.join(bundleDir.path, 'Info.plist');
final String id = plist.getValueFromFile(plistPath, plist.kCFBundleIdentifierKey); if (!fs.file(plistPath).existsSync()) {
if (id == null) printError('Invalid prebuilt iOS app. Does not contain Info.plist.');
return null;
}
final String id = iosWorkflow.getPlistValueFromFile(
plistPath,
plist.kCFBundleIdentifierKey,
);
if (id == null) {
printError('Invalid prebuilt iOS app. Info.plist does not contain bundle identifier');
return null; return null;
}
return new PrebuiltIOSApp( return new PrebuiltIOSApp(
ipaPath: applicationBinary,
bundleDir: bundleDir, bundleDir: bundleDir,
bundleName: fs.path.basename(bundleDir.path), bundleName: fs.path.basename(bundleDir.path),
projectBundleId: id, projectBundleId: id,
...@@ -179,7 +212,10 @@ abstract class IOSApp extends ApplicationPackage { ...@@ -179,7 +212,10 @@ abstract class IOSApp extends ApplicationPackage {
return null; return null;
final String plistPath = fs.path.join('ios', 'Runner', 'Info.plist'); final String plistPath = fs.path.join('ios', 'Runner', 'Info.plist');
String id = plist.getValueFromFile(plistPath, plist.kCFBundleIdentifierKey); String id = iosWorkflow.getPlistValueFromFile(
plistPath,
plist.kCFBundleIdentifierKey,
);
if (id == null || !xcodeProjectInterpreter.isInstalled) if (id == null || !xcodeProjectInterpreter.isInstalled)
return null; return null;
final String projectPath = fs.path.join('ios', 'Runner.xcodeproj'); final String projectPath = fs.path.join('ios', 'Runner.xcodeproj');
...@@ -237,12 +273,10 @@ class BuildableIOSApp extends IOSApp { ...@@ -237,12 +273,10 @@ class BuildableIOSApp extends IOSApp {
} }
class PrebuiltIOSApp extends IOSApp { class PrebuiltIOSApp extends IOSApp {
final String ipaPath;
final Directory bundleDir; final Directory bundleDir;
final String bundleName; final String bundleName;
PrebuiltIOSApp({ PrebuiltIOSApp({
this.ipaPath,
this.bundleDir, this.bundleDir,
this.bundleName, this.bundleName,
@required String projectBundleId, @required String projectBundleId,
...@@ -274,7 +308,7 @@ Future<ApplicationPackage> getApplicationPackageForPlatform(TargetPlatform platf ...@@ -274,7 +308,7 @@ Future<ApplicationPackage> getApplicationPackageForPlatform(TargetPlatform platf
case TargetPlatform.ios: case TargetPlatform.ios:
return applicationBinary == null return applicationBinary == null
? new IOSApp.fromCurrentDirectory() ? new IOSApp.fromCurrentDirectory()
: new IOSApp.fromIpa(applicationBinary); : new IOSApp.fromPrebuiltApp(applicationBinary);
case TargetPlatform.tester: case TargetPlatform.tester:
return new FlutterTesterApp.fromCurrentDirectory(); return new FlutterTesterApp.fromCurrentDirectory();
case TargetPlatform.darwin_x64: case TargetPlatform.darwin_x64:
......
...@@ -507,7 +507,10 @@ class IntelliJValidatorOnMac extends IntelliJValidator { ...@@ -507,7 +507,10 @@ class IntelliJValidatorOnMac extends IntelliJValidator {
String get version { String get version {
if (_version == null) { if (_version == null) {
final String plistFile = fs.path.join(installPath, 'Contents', 'Info.plist'); final String plistFile = fs.path.join(installPath, 'Contents', 'Info.plist');
_version = getValueFromFile(plistFile, kCFBundleShortVersionStringKey) ?? 'unknown'; _version = iosWorkflow.getPlistValueFromFile(
plistFile,
kCFBundleShortVersionStringKey,
) ?? 'unknown';
} }
return _version; return _version;
} }
......
...@@ -12,6 +12,7 @@ import '../base/version.dart'; ...@@ -12,6 +12,7 @@ import '../base/version.dart';
import '../doctor.dart'; import '../doctor.dart';
import 'cocoapods.dart'; import 'cocoapods.dart';
import 'mac.dart'; import 'mac.dart';
import 'plist_utils.dart' as plist;
IOSWorkflow get iosWorkflow => context[IOSWorkflow]; IOSWorkflow get iosWorkflow => context[IOSWorkflow];
...@@ -33,6 +34,10 @@ class IOSWorkflow extends DoctorValidator implements Workflow { ...@@ -33,6 +34,10 @@ class IOSWorkflow extends DoctorValidator implements Workflow {
@override @override
bool get canListEmulators => false; bool get canListEmulators => false;
String getPlistValueFromFile(String path, String key) {
return plist.getValueFromFile(path, key);
}
Future<bool> get hasIDeviceInstaller => exitsHappyAsync(<String>['ideviceinstaller', '-h']); Future<bool> get hasIDeviceInstaller => exitsHappyAsync(<String>['ideviceinstaller', '-h']);
Future<bool> get hasIosDeploy => exitsHappyAsync(<String>['ios-deploy', '--version']); Future<bool> get hasIosDeploy => exitsHappyAsync(<String>['ios-deploy', '--version']);
......
...@@ -8,6 +8,7 @@ import '../base/process.dart'; ...@@ -8,6 +8,7 @@ import '../base/process.dart';
const String kCFBundleIdentifierKey = 'CFBundleIdentifier'; const String kCFBundleIdentifierKey = 'CFBundleIdentifier';
const String kCFBundleShortVersionStringKey = 'CFBundleShortVersionString'; const String kCFBundleShortVersionStringKey = 'CFBundleShortVersionString';
// Prefer using [iosWorkflow.getPlistValueFromFile] to enable mocking.
String getValueFromFile(String plistFilePath, String key) { String getValueFromFile(String plistFilePath, String key) {
// TODO(chinmaygarde): For now, we only need to read from plist files on a mac // TODO(chinmaygarde): For now, we only need to read from plist files on a mac
// host. If this changes, we will need our own Dart plist reader. // host. If this changes, we will need our own Dart plist reader.
......
...@@ -2,15 +2,25 @@ ...@@ -2,15 +2,25 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:convert';
import 'package:flutter_tools/src/application_package.dart'; import 'package:flutter_tools/src/application_package.dart';
import 'package:flutter_tools/src/base/context.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/os.dart';
import 'package:flutter_tools/src/ios/ios_workflow.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:mockito/mockito.dart';
import 'src/context.dart'; import 'src/context.dart';
void main() { void main() {
group('ApkManifestData', () { group('ApkManifestData', () {
testUsingContext('parse sdk', () { testUsingContext('parse sdk', () {
final ApkManifestData data = ApkManifestData.parseFromAaptBadging(_aaptData); final ApkManifestData data =
ApkManifestData.parseFromAaptBadging(_aaptData);
expect(data, isNotNull); expect(data, isNotNull);
expect(data.packageName, 'io.flutter.gallery'); expect(data.packageName, 'io.flutter.gallery');
expect(data.launchableActivityName, 'io.flutter.app.FlutterActivity'); expect(data.launchableActivityName, 'io.flutter.app.FlutterActivity');
...@@ -28,6 +38,118 @@ void main() { ...@@ -28,6 +38,118 @@ void main() {
expect(buildableIOSApp.isSwift, true); expect(buildableIOSApp.isSwift, true);
}); });
}); });
group('PrebuiltIOSApp', () {
final Map<Type, Generator> overrides = <Type, Generator>{
FileSystem: () => new MemoryFileSystem(),
IOSWorkflow: () => new MockIosWorkFlow()
};
testUsingContext('Error on non-existing file', () {
final PrebuiltIOSApp iosApp =
new IOSApp.fromPrebuiltApp('not_existing.ipa');
expect(iosApp, isNull);
final BufferLogger logger = context[Logger];
expect(
logger.errorText,
'File "not_existing.ipa" does not exist. Use an app bundle or an ipa.\n',
);
}, overrides: overrides);
testUsingContext('Error on non-app-bundle folder', () {
fs.directory('regular_folder').createSync();
final PrebuiltIOSApp iosApp =
new IOSApp.fromPrebuiltApp('regular_folder');
expect(iosApp, isNull);
final BufferLogger logger = context[Logger];
expect(
logger.errorText, 'Folder "regular_folder" is not an app bundle.\n');
}, overrides: overrides);
testUsingContext('Error on no info.plist', () {
fs.directory('bundle.app').createSync();
final PrebuiltIOSApp iosApp = new IOSApp.fromPrebuiltApp('bundle.app');
expect(iosApp, isNull);
final BufferLogger logger = context[Logger];
expect(
logger.errorText,
'Invalid prebuilt iOS app. Does not contain Info.plist.\n',
);
}, overrides: overrides);
testUsingContext('Error on bad info.plist', () {
fs.directory('bundle.app').createSync();
fs.file('bundle.app/Info.plist').writeAsStringSync(badPlistData);
final PrebuiltIOSApp iosApp = new IOSApp.fromPrebuiltApp('bundle.app');
expect(iosApp, isNull);
final BufferLogger logger = context[Logger];
expect(
logger.errorText,
contains(
'Invalid prebuilt iOS app. Info.plist does not contain bundle identifier\n'),
);
}, overrides: overrides);
testUsingContext('Success with app bundle', () {
fs.directory('bundle.app').createSync();
fs.file('bundle.app/Info.plist').writeAsStringSync(plistData);
final PrebuiltIOSApp iosApp = new IOSApp.fromPrebuiltApp('bundle.app');
final BufferLogger logger = context[Logger];
expect(logger.errorText, isEmpty);
expect(iosApp.bundleDir.path, 'bundle.app');
expect(iosApp.id, 'fooBundleId');
expect(iosApp.bundleName, 'bundle.app');
}, overrides: overrides);
testUsingContext('Bad ipa zip-file, no payload dir', () {
fs.file('app.ipa').createSync();
when(os.unzip(fs.file('app.ipa'), any)).thenAnswer((Invocation _) {});
final PrebuiltIOSApp iosApp = new IOSApp.fromPrebuiltApp('app.ipa');
expect(iosApp, isNull);
final BufferLogger logger = context[Logger];
expect(
logger.errorText,
'Invalid prebuilt iOS ipa. Does not contain a "Payload" directory.\n',
);
}, overrides: overrides);
testUsingContext('Bad ipa zip-file, two app bundles', () {
fs.file('app.ipa').createSync();
when(os.unzip(any, any)).thenAnswer((Invocation invocation) {
final File zipFile = invocation.positionalArguments[0];
if (zipFile.path != 'app.ipa') {
return null;
}
final Directory targetDirectory = invocation.positionalArguments[1];
final String bundlePath1 =
fs.path.join(targetDirectory.path, 'Payload', 'bundle1.app');
final String bundlePath2 =
fs.path.join(targetDirectory.path, 'Payload', 'bundle2.app');
fs.directory(bundlePath1).createSync(recursive: true);
fs.directory(bundlePath2).createSync(recursive: true);
});
final PrebuiltIOSApp iosApp = new IOSApp.fromPrebuiltApp('app.ipa');
expect(iosApp, isNull);
final BufferLogger logger = context[Logger];
expect(logger.errorText,
'Invalid prebuilt iOS ipa. Does not contain a single app bundle.\n');
}, overrides: overrides);
testUsingContext('Success with ipa', () {
fs.file('app.ipa').createSync();
when(os.unzip(any, any)).thenAnswer((Invocation invocation) {
final File zipFile = invocation.positionalArguments[0];
if (zipFile.path != 'app.ipa') {
return null;
}
final Directory targetDirectory = invocation.positionalArguments[1];
final Directory bundleAppDir = fs.directory(
fs.path.join(targetDirectory.path, 'Payload', 'bundle.app'));
bundleAppDir.createSync(recursive: true);
fs
.file(fs.path.join(bundleAppDir.path, 'Info.plist'))
.writeAsStringSync(plistData);
});
final PrebuiltIOSApp iosApp = new IOSApp.fromPrebuiltApp('app.ipa');
final BufferLogger logger = context[Logger];
expect(logger.errorText, isEmpty);
expect(iosApp.bundleDir.path, endsWith('bundle.app'));
expect(iosApp.id, 'fooBundleId');
expect(iosApp.bundleName, 'bundle.app');
}, overrides: overrides);
});
} }
const String _aaptData = ''' const String _aaptData = '''
...@@ -69,3 +191,23 @@ final Map<String, String> _swiftBuildSettings = <String, String>{ ...@@ -69,3 +191,23 @@ final Map<String, String> _swiftBuildSettings = <String, String>{
'SWIFT_OPTIMIZATION_LEVEL': '-Onone', 'SWIFT_OPTIMIZATION_LEVEL': '-Onone',
'SWIFT_VERSION': '3.0', 'SWIFT_VERSION': '3.0',
}; };
class MockIosWorkFlow extends Mock implements IOSWorkflow {
@override
String getPlistValueFromFile(String path, String key) {
final File file = fs.file(path);
if (!file.existsSync()) {
return null;
}
return json.decode(file.readAsStringSync())[key];
}
}
// Contains no bundle identifier.
const String badPlistData = '''
{}
''';
const String plistData = '''
{"CFBundleIdentifier": "fooBundleId"}
''';
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