Commit c6a17525 authored by KyleWong's avatar KyleWong Committed by xster

Enable developers to run pod from terminal directly and skip pod install if possible. (#13374)

Avoids running pod install if .flutter-plugins and its flutter framework pod dependency didn't change
parent 70b32e85
...@@ -24,7 +24,7 @@ class InjectPluginsCommand extends FlutterCommand { ...@@ -24,7 +24,7 @@ class InjectPluginsCommand extends FlutterCommand {
@override @override
Future<Null> runCommand() async { Future<Null> runCommand() async {
final bool result = injectPlugins(); final bool result = injectPlugins().hasPlugin;
if (result) { if (result) {
printStatus('GeneratedPluginRegistrants successfully written.'); printStatus('GeneratedPluginRegistrants successfully written.');
} else { } else {
......
...@@ -57,15 +57,17 @@ class CocoaPods { ...@@ -57,15 +57,17 @@ class CocoaPods {
Future<Null> processPods({ Future<Null> processPods({
@required Directory appIosDir, @required Directory appIosDir,
@required String iosEngineDir,
bool isSwift: false, bool isSwift: false,
bool pluginOrFlutterPodChanged: true,
}) async { }) async {
if (await _checkPodCondition()) { if (await _checkPodCondition()) {
if (!fs.file(fs.path.join(appIosDir.path, 'Podfile')).existsSync()) { if (!fs.file(fs.path.join(appIosDir.path, 'Podfile')).existsSync()) {
await _createPodfile(appIosDir, isSwift); await _createPodfile(appIosDir, isSwift);
} // TODO(xster): Add more logic for handling merge conflicts. } // TODO(xster): Add more logic for handling merge conflicts.
if (_checkIfRunPodInstall(appIosDir.path, pluginOrFlutterPodChanged))
await _runPodInstall(appIosDir, iosEngineDir); await _runPodInstall(appIosDir);
} else {
throwToolExit('CocoaPods not available for project using Flutter plugins');
} }
} }
...@@ -108,13 +110,12 @@ class CocoaPods { ...@@ -108,13 +110,12 @@ class CocoaPods {
podfileTemplate.copySync(fs.path.join(bundle.path, 'Podfile')); podfileTemplate.copySync(fs.path.join(bundle.path, 'Podfile'));
} }
Future<Null> _runPodInstall(Directory bundle, String engineDirectory) async { Future<Null> _runPodInstall(Directory bundle) async {
final Status status = logger.startProgress('Running pod install...', expectSlowOperation: true); final Status status = logger.startProgress('Running pod install...', expectSlowOperation: true);
final ProcessResult result = await processManager.run( final ProcessResult result = await processManager.run(
<String>['pod', 'install', '--verbose'], <String>['pod', 'install', '--verbose'],
workingDirectory: bundle.path, workingDirectory: bundle.path,
environment: <String, String>{ environment: <String, String>{
'FLUTTER_FRAMEWORK_DIR': engineDirectory,
// See https://github.com/flutter/flutter/issues/10873. // See https://github.com/flutter/flutter/issues/10873.
// CocoaPods analytics adds a lot of latency. // CocoaPods analytics adds a lot of latency.
'COCOAPODS_DISABLE_STATS': 'true', 'COCOAPODS_DISABLE_STATS': 'true',
...@@ -137,6 +138,28 @@ class CocoaPods { ...@@ -137,6 +138,28 @@ class CocoaPods {
} }
} }
// Check if you need to run pod install.
// The pod install will run if any of below is true.
// 1.Any plugins changed (add/update/delete)
// 2.The flutter.framework has changed (debug/release/profile)
// 3.The podfile.lock doesn't exists
// 4.The Pods/manifest.lock doesn't exists
// 5.The podfile.lock doesn't match Pods/manifest.lock.
bool _checkIfRunPodInstall(String appDir, bool pluginOrFlutterPodChanged) {
if (pluginOrFlutterPodChanged)
return true;
// Check if podfile.lock and Pods/Manifest.lock exists and matches.
final File podfileLockFile = fs.file(fs.path.join(appDir, 'Podfile.lock'));
final File manifestLockFile =
fs.file(fs.path.join(appDir, 'Pods', 'Manifest.lock'));
if (!podfileLockFile.existsSync() ||
!manifestLockFile.existsSync() ||
podfileLockFile.readAsStringSync() !=
manifestLockFile.readAsStringSync())
return true;
return false;
}
void _diagnosePodInstallFailure(ProcessResult result) { void _diagnosePodInstallFailure(ProcessResult result) {
if (result.stdout is String && result.stdout.contains('out-of-date source repos')) { if (result.stdout is String && result.stdout.contains('out-of-date source repos')) {
printError( printError(
......
...@@ -254,14 +254,9 @@ Future<XcodeBuildResult> buildXcodeProject({ ...@@ -254,14 +254,9 @@ Future<XcodeBuildResult> buildXcodeProject({
// 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); final Directory appDirectory = fs.directory(app.appDirectory);
await _addServicesToBundle(appDirectory); await _addServicesToBundle(appDirectory);
final bool hasFlutterPlugins = injectPlugins(); final InjectPluginsResult injectionResult = injectPlugins();
final bool hasFlutterPlugins = injectionResult.hasPlugin;
if (hasFlutterPlugins) final String priorGeneratedXCConfig = readGeneratedXCConfig(app.appDirectory);
await cocoaPods.processPods(
appIosDir: appDirectory,
iosEngineDir: flutterFrameworkDir(buildInfo.mode),
isSwift: app.isSwift,
);
updateXcodeGeneratedProperties( updateXcodeGeneratedProperties(
projectPath: fs.currentDirectory.path, projectPath: fs.currentDirectory.path,
...@@ -271,6 +266,15 @@ Future<XcodeBuildResult> buildXcodeProject({ ...@@ -271,6 +266,15 @@ Future<XcodeBuildResult> buildXcodeProject({
previewDart2: buildInfo.previewDart2, previewDart2: buildInfo.previewDart2,
); );
if (hasFlutterPlugins) {
final String currentGeneratedXCConfig = readGeneratedXCConfig(app.appDirectory);
await cocoaPods.processPods(
appIosDir: appDirectory,
isSwift: app.isSwift,
pluginOrFlutterPodChanged: (injectionResult.hasChanged ||
priorGeneratedXCConfig != currentGeneratedXCConfig));
}
final List<String> commands = <String>[ final List<String> commands = <String>[
'/usr/bin/env', '/usr/bin/env',
'xcrun', 'xcrun',
...@@ -355,7 +359,17 @@ Future<XcodeBuildResult> buildXcodeProject({ ...@@ -355,7 +359,17 @@ Future<XcodeBuildResult> buildXcodeProject({
} }
} }
Future<Null> diagnoseXcodeBuildFailure(XcodeBuildResult result, BuildableIOSApp app) async { String readGeneratedXCConfig(String appPath) {
final String generateXCConfigPath =
fs.path.join(fs.currentDirectory.path, appPath, 'Flutter','Generated.xcconfig');
final File generateXCConfigFile = fs.file(generateXCConfigPath);
if (!generateXCConfigFile.existsSync())
return null;
return generateXCConfigFile.readAsStringSync();
}
Future<Null> diagnoseXcodeBuildFailure(
XcodeBuildResult result, BuildableIOSApp app) async {
if (result.xcodeBuildExecution != null && if (result.xcodeBuildExecution != null &&
result.xcodeBuildExecution.buildForPhysicalDevice && result.xcodeBuildExecution.buildForPhysicalDevice &&
result.stdout?.contains('BCEROR') == true && result.stdout?.contains('BCEROR') == true &&
......
...@@ -77,8 +77,10 @@ List<Plugin> _findPlugins(String directory) { ...@@ -77,8 +77,10 @@ List<Plugin> _findPlugins(String directory) {
return plugins; return plugins;
} }
void _writeFlutterPluginsList(String directory, List<Plugin> plugins) { // Return true if .flutter-plugins has changed, otherwise return false.
bool _writeFlutterPluginsList(String directory, List<Plugin> plugins) {
final File pluginsProperties = fs.file(fs.path.join(directory, '.flutter-plugins')); final File pluginsProperties = fs.file(fs.path.join(directory, '.flutter-plugins'));
final String priorFlutterPlugins = (pluginsProperties.existsSync() ? pluginsProperties.readAsStringSync() : null);
final String pluginManifest = final String pluginManifest =
plugins.map((Plugin p) => '${p.name}=${escapePath(p.path)}').join('\n'); plugins.map((Plugin p) => '${p.name}=${escapePath(p.path)}').join('\n');
if (pluginManifest.isNotEmpty) { if (pluginManifest.isNotEmpty) {
...@@ -88,6 +90,8 @@ void _writeFlutterPluginsList(String directory, List<Plugin> plugins) { ...@@ -88,6 +90,8 @@ void _writeFlutterPluginsList(String directory, List<Plugin> plugins) {
pluginsProperties.deleteSync(); pluginsProperties.deleteSync();
} }
} }
final String currentFlutterPlugins = (pluginsProperties.existsSync() ? pluginsProperties.readAsStringSync() : null);
return currentFlutterPlugins != priorFlutterPlugins;
} }
const String _androidPluginRegistryTemplate = '''package io.flutter.plugins; const String _androidPluginRegistryTemplate = '''package io.flutter.plugins;
...@@ -206,17 +210,25 @@ void _writeIOSPluginRegistry(String directory, List<Plugin> plugins) { ...@@ -206,17 +210,25 @@ void _writeIOSPluginRegistry(String directory, List<Plugin> plugins) {
} }
class InjectPluginsResult{
InjectPluginsResult({this.hasPlugin : false, this.hasChanged : false});
/// True if any flutter plugin exists, otherwise false.
final bool hasPlugin;
/// True if plugins have changed since last build.
final bool hasChanged;
}
/// Finds Flutter plugins in the pubspec.yaml, creates platform injection /// Finds Flutter plugins in the pubspec.yaml, creates platform injection
/// registries classes and add them to the build dependencies. /// registries classes and add them to the build dependencies.
/// ///
/// Returns whether any Flutter plugins are added. /// Returns whether any Flutter plugins are added.
bool injectPlugins({String directory}) { InjectPluginsResult injectPlugins({String directory}) {
directory ??= fs.currentDirectory.path; directory ??= fs.currentDirectory.path;
final List<Plugin> plugins = _findPlugins(directory); final List<Plugin> plugins = _findPlugins(directory);
_writeFlutterPluginsList(directory, plugins); final bool hasPluginsChanged = _writeFlutterPluginsList(directory, plugins);
if (fs.isDirectorySync(fs.path.join(directory, 'android'))) if (fs.isDirectorySync(fs.path.join(directory, 'android')))
_writeAndroidPluginRegistry(directory, plugins); _writeAndroidPluginRegistry(directory, plugins);
if (fs.isDirectorySync(fs.path.join(directory, 'ios'))) if (fs.isDirectorySync(fs.path.join(directory, 'ios')))
_writeIOSPluginRegistry(directory, plugins); _writeIOSPluginRegistry(directory, plugins);
return plugins.isNotEmpty; return new InjectPluginsResult(hasPlugin: plugins.isNotEmpty, hasChanged: hasPluginsChanged);
} }
\ No newline at end of file
# Uncomment this line to define a global platform for your project # Uncomment this line to define a global platform for your project
# platform :ios, '9.0' # platform :ios, '9.0'
if ENV['FLUTTER_FRAMEWORK_DIR'] == nil def parse_KV_file(file,seperator='=')
abort('Please set FLUTTER_FRAMEWORK_DIR to the directory containing Flutter.framework') file_abs_path = File.expand_path(file)
end if !File.exists? file_abs_path
puts "Generated.xcconfig or .flutter-plugins expected but not present. If you're running pod install manually, make sure flutter build or flutter run is executed once first."
target 'Runner' do return [];
# Pods for Runner end
pods_ary = []
# Flutter Pods File.foreach(file_abs_path) { |line|
pod 'Flutter', :path => ENV['FLUTTER_FRAMEWORK_DIR'] plugin = line.split(pattern=seperator)
if File.exists? '../.flutter-plugins'
flutter_root = File.expand_path('..')
File.foreach('../.flutter-plugins') { |line|
plugin = line.split(pattern='=')
if plugin.length == 2 if plugin.length == 2
name = plugin[0].strip() podname = plugin[0].strip()
path = plugin[1].strip() path = plugin[1].strip()
resolved_path = File.expand_path("#{path}/ios", flutter_root) podpath = File.expand_path("#{path}", file_abs_path)
pod name, :path => resolved_path pods_ary.push({:name => podname,:path=>podpath});
else else
puts "Invalid plugin specification: #{line}" puts "Invalid plugin specification: #{line}"
end end
} }
return pods_ary
end
target 'Runner' do
use_frameworks!
# Flutter Pods
generated_xcode_build_settings = parse_KV_file("./Flutter/Generated.xcconfig")
generated_xcode_build_settings.map{ |p|
if p[:name]=='FLUTTER_FRAMEWORK_DIR'
pod 'Flutter', :path => p[:path]
end end
}
# Plugin Pods
plugin_pods = parse_KV_file("../.flutter-plugins")
plugin_pods.map{ |p|
pod p[:name], :path => File.expand_path("ios",p[:path])
}
end end
post_install do |installer| post_install do |installer|
......
# Uncomment this line to define a global platform for your project # Uncomment this line to define a global platform for your project
# platform :ios, '9.0' # platform :ios, '9.0'
if ENV['FLUTTER_FRAMEWORK_DIR'] == nil def parse_KV_file(file,seperator='=')
abort('Please set FLUTTER_FRAMEWORK_DIR to the directory containing Flutter.framework') file_abs_path = File.expand_path(file)
end if !File.exists? file_abs_path
puts "Generated.xcconfig or .flutter-plugins expected but not present. If you're running pod install manually, make sure flutter build or flutter run is executed once first."
target 'Runner' do return [];
use_frameworks! end
pods_ary = []
# Pods for Runner File.foreach(file_abs_path) { |line|
plugin = line.split(pattern=seperator)
# Flutter Pods
pod 'Flutter', :path => ENV['FLUTTER_FRAMEWORK_DIR']
if File.exists? '../.flutter-plugins'
flutter_root = File.expand_path('..')
File.foreach('../.flutter-plugins') { |line|
plugin = line.split(pattern='=')
if plugin.length == 2 if plugin.length == 2
name = plugin[0].strip() podname = plugin[0].strip()
path = plugin[1].strip() path = plugin[1].strip()
resolved_path = File.expand_path("#{path}/ios", flutter_root) podpath = File.expand_path("#{path}", file_abs_path)
pod name, :path => resolved_path pods_ary.push({:name => podname,:path=>podpath});
else else
puts "Invalid plugin specification: #{line}" puts "Invalid plugin specification: #{line}"
end end
} }
return pods_ary
end
target 'Runner' do
use_frameworks!
# Flutter Pods
generated_xcode_build_settings = parse_KV_file("./Flutter/Generated.xcconfig")
generated_xcode_build_settings.map{ |p|
if p[:name]=='FLUTTER_FRAMEWORK_DIR'
pod 'Flutter', :path => p[:path]
end end
}
# Plugin Pods
plugin_pods = parse_KV_file("../.flutter-plugins")
plugin_pods.map{ |p|
pod p[:name], :path => File.expand_path("ios",p[:path])
}
end end
post_install do |installer| post_install do |installer|
......
...@@ -41,7 +41,7 @@ void main() { ...@@ -41,7 +41,7 @@ void main() {
when(mockProcessManager.run( when(mockProcessManager.run(
<String>['pod', 'install', '--verbose'], <String>['pod', 'install', '--verbose'],
workingDirectory: 'project/ios', workingDirectory: 'project/ios',
environment: <String, String>{'FLUTTER_FRAMEWORK_DIR': 'engine/path', 'COCOAPODS_DISABLE_STATS': 'true'}, environment: <String, String>{'COCOAPODS_DISABLE_STATS': 'true'},
)).thenReturn(exitsHappy); )).thenReturn(exitsHappy);
}); });
...@@ -50,13 +50,12 @@ void main() { ...@@ -50,13 +50,12 @@ void main() {
() async { () async {
await cocoaPodsUnderTest.processPods( await cocoaPodsUnderTest.processPods(
appIosDir: projectUnderTest, appIosDir: projectUnderTest,
iosEngineDir: 'engine/path',
); );
expect(fs.file(fs.path.join('project', 'ios', 'Podfile')).readAsStringSync() , 'Objective-C podfile template'); expect(fs.file(fs.path.join('project', 'ios', 'Podfile')).readAsStringSync() , 'Objective-C podfile template');
verify(mockProcessManager.run( verify(mockProcessManager.run(
<String>['pod', 'install', '--verbose'], <String>['pod', 'install', '--verbose'],
workingDirectory: 'project/ios', workingDirectory: 'project/ios',
environment: <String, String>{'FLUTTER_FRAMEWORK_DIR': 'engine/path', 'COCOAPODS_DISABLE_STATS': 'true'}, environment: <String, String>{'COCOAPODS_DISABLE_STATS': 'true'},
)); ));
}, },
overrides: <Type, Generator>{ overrides: <Type, Generator>{
...@@ -70,14 +69,13 @@ void main() { ...@@ -70,14 +69,13 @@ void main() {
() async { () async {
await cocoaPodsUnderTest.processPods( await cocoaPodsUnderTest.processPods(
appIosDir: projectUnderTest, appIosDir: projectUnderTest,
iosEngineDir: 'engine/path',
isSwift: true, isSwift: true,
); );
expect(fs.file(fs.path.join('project', 'ios', 'Podfile')).readAsStringSync() , 'Swift podfile template'); expect(fs.file(fs.path.join('project', 'ios', 'Podfile')).readAsStringSync() , 'Swift podfile template');
verify(mockProcessManager.run( verify(mockProcessManager.run(
<String>['pod', 'install', '--verbose'], <String>['pod', 'install', '--verbose'],
workingDirectory: 'project/ios', workingDirectory: 'project/ios',
environment: <String, String>{'FLUTTER_FRAMEWORK_DIR': 'engine/path', 'COCOAPODS_DISABLE_STATS': 'true'}, environment: <String, String>{'COCOAPODS_DISABLE_STATS': 'true'},
)); ));
}, },
overrides: <Type, Generator>{ overrides: <Type, Generator>{
...@@ -94,12 +92,11 @@ void main() { ...@@ -94,12 +92,11 @@ void main() {
..writeAsString('Existing Podfile'); ..writeAsString('Existing Podfile');
await cocoaPodsUnderTest.processPods( await cocoaPodsUnderTest.processPods(
appIosDir: projectUnderTest, appIosDir: projectUnderTest,
iosEngineDir: 'engine/path',
); expect(fs.file(fs.path.join('project', 'ios', 'Podfile')).readAsStringSync() , 'Existing Podfile'); ); expect(fs.file(fs.path.join('project', 'ios', 'Podfile')).readAsStringSync() , 'Existing Podfile');
verify(mockProcessManager.run( verify(mockProcessManager.run(
<String>['pod', 'install', '--verbose'], <String>['pod', 'install', '--verbose'],
workingDirectory: 'project/ios', workingDirectory: 'project/ios',
environment: <String, String>{'FLUTTER_FRAMEWORK_DIR': 'engine/path', 'COCOAPODS_DISABLE_STATS': 'true'}, environment: <String, String>{'COCOAPODS_DISABLE_STATS': 'true'},
)); ));
}, },
overrides: <Type, Generator>{ overrides: <Type, Generator>{
...@@ -115,14 +112,13 @@ void main() { ...@@ -115,14 +112,13 @@ void main() {
try { try {
await cocoaPodsUnderTest.processPods( await cocoaPodsUnderTest.processPods(
appIosDir: projectUnderTest, appIosDir: projectUnderTest,
iosEngineDir: 'engine/path',
); );
fail('Expected tool error'); fail('Expected tool error');
} catch (ToolExit) { } catch (ToolExit) {
verifyNever(mockProcessManager.run( verifyNever(mockProcessManager.run(
<String>['pod', 'install', '--verbose'], <String>['pod', 'install', '--verbose'],
workingDirectory: 'project/ios', workingDirectory: 'project/ios',
environment: <String, String>{'FLUTTER_FRAMEWORK_DIR': 'engine/path', 'COCOAPODS_DISABLE_STATS': 'true'}, environment: <String, String>{'COCOAPODS_DISABLE_STATS': 'true'},
)); ));
} }
}, },
...@@ -142,7 +138,7 @@ void main() { ...@@ -142,7 +138,7 @@ void main() {
when(mockProcessManager.run( when(mockProcessManager.run(
<String>['pod', 'install', '--verbose'], <String>['pod', 'install', '--verbose'],
workingDirectory: 'project/ios', workingDirectory: 'project/ios',
environment: <String, String>{'FLUTTER_FRAMEWORK_DIR': 'engine/path', 'COCOAPODS_DISABLE_STATS': 'true'}, environment: <String, String>{'COCOAPODS_DISABLE_STATS': 'true'},
)).thenReturn(new ProcessResult( )).thenReturn(new ProcessResult(
1, 1,
1, 1,
...@@ -165,7 +161,6 @@ Note: as of CocoaPods 1.0, `pod repo update` does not happen on `pod install` by ...@@ -165,7 +161,6 @@ Note: as of CocoaPods 1.0, `pod repo update` does not happen on `pod install` by
try { try {
await cocoaPodsUnderTest.processPods( await cocoaPodsUnderTest.processPods(
appIosDir: projectUnderTest, appIosDir: projectUnderTest,
iosEngineDir: 'engine/path',
); expect(fs.file(fs.path.join('project', 'ios', 'Podfile')).readAsStringSync() , 'Existing Podfile'); ); expect(fs.file(fs.path.join('project', 'ios', 'Podfile')).readAsStringSync() , 'Existing Podfile');
fail('Exception expected'); fail('Exception expected');
} catch (ToolExit) { } catch (ToolExit) {
...@@ -177,6 +172,62 @@ Note: as of CocoaPods 1.0, `pod repo update` does not happen on `pod install` by ...@@ -177,6 +172,62 @@ Note: as of CocoaPods 1.0, `pod repo update` does not happen on `pod install` by
ProcessManager: () => mockProcessManager, ProcessManager: () => mockProcessManager,
}, },
); );
testUsingContext(
'Run pod install if plugins or flutter framework have changes.',
() async {
fs.file(fs.path.join('project', 'ios', 'Podfile'))
..createSync()
..writeAsString('Existing Podfile');
fs.file(fs.path.join('project', 'ios', 'Podfile.lock'))
..createSync()
..writeAsString('Existing lock files.');
fs.file(fs.path.join('project', 'ios', 'Pods','Manifest.lock'))
..createSync(recursive: true)
..writeAsString('Existing lock files.');
await cocoaPodsUnderTest.processPods(
appIosDir: projectUnderTest,
pluginOrFlutterPodChanged: true
);
verify(mockProcessManager.run(
<String>['pod', 'install', '--verbose'],
workingDirectory: 'project/ios',
environment: <String, String>{'COCOAPODS_DISABLE_STATS': 'true'},
));
},
overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => mockProcessManager,
},
);
testUsingContext(
'Skip pod install if plugins and flutter framework remain unchanged.',
() async {
fs.file(fs.path.join('project', 'ios', 'Podfile'))
..createSync()
..writeAsString('Existing Podfile');
fs.file(fs.path.join('project', 'ios', 'Podfile.lock'))
..createSync()
..writeAsString('Existing lock files.');
fs.file(fs.path.join('project', 'ios', 'Pods','Manifest.lock'))
..createSync(recursive: true)
..writeAsString('Existing lock files.');
await cocoaPodsUnderTest.processPods(
appIosDir: projectUnderTest,
pluginOrFlutterPodChanged: false
);
verifyNever(mockProcessManager.run(
<String>['pod', 'install', '--verbose'],
workingDirectory: 'project/ios',
environment: <String, String>{'COCOAPODS_DISABLE_STATS': 'true'},
));
},
overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => mockProcessManager,
},
);
} }
class MockProcessManager extends Mock implements ProcessManager {} class MockProcessManager extends Mock implements ProcessManager {}
......
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