Unverified Commit e8e5de44 authored by Jenn Magder's avatar Jenn Magder Committed by GitHub

Parse .flutter-plugins-dependencies for add-to-app iOS modules (#61269)

parent 734782f5
...@@ -14,6 +14,7 @@ import 'package:path/path.dart' as path; ...@@ -14,6 +14,7 @@ import 'package:path/path.dart' as path;
/// adding Flutter to an existing iOS app. /// adding Flutter to an existing iOS app.
Future<void> main() async { Future<void> main() async {
await task(() async { await task(() async {
String simulatorDeviceId;
section('Create Flutter module project'); section('Create Flutter module project');
final Directory tempDir = Directory.systemTemp.createTempSync('flutter_module_test.'); final Directory tempDir = Directory.systemTemp.createTempSync('flutter_module_test.');
...@@ -52,7 +53,7 @@ Future<void> main() async { ...@@ -52,7 +53,7 @@ Future<void> main() async {
); );
}); });
final Directory ephemeralReleaseHostApp = Directory(path.join( final Directory ephemeralIOSHostApp = Directory(path.join(
projectDir.path, projectDir.path,
'build', 'build',
'ios', 'ios',
...@@ -60,13 +61,13 @@ Future<void> main() async { ...@@ -60,13 +61,13 @@ Future<void> main() async {
'Runner.app', 'Runner.app',
)); ));
if (!exists(ephemeralReleaseHostApp)) { if (!exists(ephemeralIOSHostApp)) {
return TaskResult.failure('Failed to build ephemeral host .app'); return TaskResult.failure('Failed to build ephemeral host .app');
} }
if (!await _isAppAotBuild(ephemeralReleaseHostApp)) { if (!await _isAppAotBuild(ephemeralIOSHostApp)) {
return TaskResult.failure( return TaskResult.failure(
'Ephemeral host app ${ephemeralReleaseHostApp.path} was not a release build as expected' 'Ephemeral host app ${ephemeralIOSHostApp.path} was not a release build as expected'
); );
} }
...@@ -85,21 +86,13 @@ Future<void> main() async { ...@@ -85,21 +86,13 @@ Future<void> main() async {
); );
}); });
final Directory ephemeralProfileHostApp = Directory(path.join( if (!exists(ephemeralIOSHostApp)) {
projectDir.path,
'build',
'ios',
'iphoneos',
'Runner.app',
));
if (!exists(ephemeralProfileHostApp)) {
return TaskResult.failure('Failed to build ephemeral host .app'); return TaskResult.failure('Failed to build ephemeral host .app');
} }
if (!await _isAppAotBuild(ephemeralProfileHostApp)) { if (!await _isAppAotBuild(ephemeralIOSHostApp)) {
return TaskResult.failure( return TaskResult.failure(
'Ephemeral host app ${ephemeralProfileHostApp.path} was not a profile build as expected' 'Ephemeral host app ${ephemeralIOSHostApp.path} was not a profile build as expected'
); );
} }
...@@ -118,7 +111,7 @@ Future<void> main() async { ...@@ -118,7 +111,7 @@ Future<void> main() async {
); );
}); });
final Directory ephemeralDebugHostApp = Directory(path.join( final Directory ephemeralSimulatorHostApp = Directory(path.join(
projectDir.path, projectDir.path,
'build', 'build',
'ios', 'ios',
...@@ -126,19 +119,19 @@ Future<void> main() async { ...@@ -126,19 +119,19 @@ Future<void> main() async {
'Runner.app', 'Runner.app',
)); ));
if (!exists(ephemeralDebugHostApp)) { if (!exists(ephemeralSimulatorHostApp)) {
return TaskResult.failure('Failed to build ephemeral host .app'); return TaskResult.failure('Failed to build ephemeral host .app');
} }
if (!exists(File(path.join( if (!exists(File(path.join(
ephemeralDebugHostApp.path, ephemeralSimulatorHostApp.path,
'Frameworks', 'Frameworks',
'App.framework', 'App.framework',
'flutter_assets', 'flutter_assets',
'isolate_snapshot_data', 'isolate_snapshot_data',
)))) { )))) {
return TaskResult.failure( return TaskResult.failure(
'Ephemeral host app ${ephemeralDebugHostApp.path} was not a debug build as expected' 'Ephemeral host app ${ephemeralSimulatorHostApp.path} was not a debug build as expected'
); );
} }
...@@ -154,7 +147,8 @@ Future<void> main() async { ...@@ -154,7 +147,8 @@ Future<void> main() async {
String content = await pubspec.readAsString(); String content = await pubspec.readAsString();
content = content.replaceFirst( content = content.replaceFirst(
'\ndependencies:\n', '\ndependencies:\n',
'\ndependencies:\n device_info:\n google_maps_flutter:\n', // One dynamic and one static framework. // One dynamic framework, one static framework, and one that does not support iOS.
'\ndependencies:\n device_info:\n google_maps_flutter:\n android_alarm_manager:\n',
); );
await pubspec.writeAsString(content, flush: true); await pubspec.writeAsString(content, flush: true);
await inDirectory(projectDir, () async { await inDirectory(projectDir, () async {
...@@ -173,13 +167,7 @@ Future<void> main() async { ...@@ -173,13 +167,7 @@ Future<void> main() async {
); );
}); });
final bool ephemeralHostAppWithCocoaPodsBuilt = exists(Directory(path.join( final bool ephemeralHostAppWithCocoaPodsBuilt = exists(ephemeralIOSHostApp);
projectDir.path,
'build',
'ios',
'iphoneos',
'Runner.app',
)));
if (!ephemeralHostAppWithCocoaPodsBuilt) { if (!ephemeralHostAppWithCocoaPodsBuilt) {
return TaskResult.failure('Failed to build ephemeral host .app with CocoaPods'); return TaskResult.failure('Failed to build ephemeral host .app with CocoaPods');
...@@ -190,10 +178,19 @@ Future<void> main() async { ...@@ -190,10 +178,19 @@ Future<void> main() async {
if (!podfileLockOutput.contains(':path: Flutter/engine') if (!podfileLockOutput.contains(':path: Flutter/engine')
|| !podfileLockOutput.contains(':path: Flutter/FlutterPluginRegistrant') || !podfileLockOutput.contains(':path: Flutter/FlutterPluginRegistrant')
|| !podfileLockOutput.contains(':path: Flutter/.symlinks/device_info/ios') || !podfileLockOutput.contains(':path: Flutter/.symlinks/device_info/ios')
|| !podfileLockOutput.contains(':path: Flutter/.symlinks/google_maps_flutter/ios')) { || !podfileLockOutput.contains(':path: Flutter/.symlinks/google_maps_flutter/ios')
|| podfileLockOutput.contains('android_alarm_manager')) {
return TaskResult.failure('Building ephemeral host app Podfile.lock does not contain expected pods'); return TaskResult.failure('Building ephemeral host app Podfile.lock does not contain expected pods');
} }
checkFileExists(path.join(ephemeralIOSHostApp.path, 'Frameworks', 'device_info.framework', 'device_info'));
// Static, no embedded framework.
checkDirectoryNotExists(path.join(ephemeralIOSHostApp.path, 'Frameworks', 'google_maps_flutter.framework'));
// Android-only, no embedded framework.
checkDirectoryNotExists(path.join(ephemeralIOSHostApp.path, 'Frameworks', 'android_alarm_manager.framework'));
section('Add to existing iOS Objective-C app'); section('Add to existing iOS Objective-C app');
final Directory objectiveCHostApp = Directory(path.join(tempDir.path, 'hello_host_app')); final Directory objectiveCHostApp = Directory(path.join(tempDir.path, 'hello_host_app'));
...@@ -255,8 +252,9 @@ Future<void> main() async { ...@@ -255,8 +252,9 @@ Future<void> main() async {
} }
section('Run platform unit tests'); section('Run platform unit tests');
await testWithNewiOSSimulator('TestAdd2AppSim', (String deviceId) => await testWithNewIOSSimulator('TestAdd2AppSim', (String deviceId) {
inDirectory(objectiveCHostApp, () => simulatorDeviceId = deviceId;
return inDirectory(objectiveCHostApp, () =>
exec( exec(
'xcodebuild', 'xcodebuild',
<String>[ <String>[
...@@ -275,8 +273,8 @@ Future<void> main() async { ...@@ -275,8 +273,8 @@ Future<void> main() async {
'EXPANDED_CODE_SIGN_IDENTITY=-', 'EXPANDED_CODE_SIGN_IDENTITY=-',
'COMPILER_INDEX_STORE_ENABLE=NO', 'COMPILER_INDEX_STORE_ENABLE=NO',
], ],
) ));
) }
); );
section('Fail building existing Objective-C iOS app if flutter script fails'); section('Fail building existing Objective-C iOS app if flutter script fails');
...@@ -371,6 +369,7 @@ Future<void> main() async { ...@@ -371,6 +369,7 @@ Future<void> main() async {
} catch (e) { } catch (e) {
return TaskResult.failure(e.toString()); return TaskResult.failure(e.toString());
} finally { } finally {
removeIOSimulator(simulatorDeviceId);
rmTree(tempDir); rmTree(tempDir);
} }
}); });
......
...@@ -103,8 +103,10 @@ Future<bool> containsBitcode(String pathToBinary) async { ...@@ -103,8 +103,10 @@ Future<bool> containsBitcode(String pathToBinary) async {
} }
/// Creates and boots a new simulator, passes the new simulator's identifier to /// Creates and boots a new simulator, passes the new simulator's identifier to
/// `testFunction`, then shuts down and deletes simulator. /// `testFunction`.
Future<void> testWithNewiOSSimulator( ///
/// Remember to call removeIOSimulator in the test teardown.
Future<void> testWithNewIOSSimulator(
String deviceName, String deviceName,
SimulatorFunction testFunction, { SimulatorFunction testFunction, {
String deviceTypeId = 'com.apple.CoreSimulator.SimDeviceType.iPhone-11', String deviceTypeId = 'com.apple.CoreSimulator.SimDeviceType.iPhone-11',
...@@ -160,7 +162,10 @@ Future<void> testWithNewiOSSimulator( ...@@ -160,7 +162,10 @@ Future<void> testWithNewiOSSimulator(
); );
await testFunction(deviceId); await testFunction(deviceId);
}
/// Shuts down and deletes simulator with deviceId.
Future<void> removeIOSimulator(String deviceId) async {
if (deviceId != null && deviceId != '') { if (deviceId != null && deviceId != '') {
await eval( await eval(
'xcrun', 'xcrun',
......
# Copyright 2014 The Flutter Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
require 'json'
# Install pods needed to embed Flutter application, Flutter engine, and plugins # Install pods needed to embed Flutter application, Flutter engine, and plugins
# from the host application Podfile. # from the host application Podfile.
# #
...@@ -22,7 +28,8 @@ end ...@@ -22,7 +28,8 @@ end
# install_flutter_engine_pod # install_flutter_engine_pod
# end # end
def install_flutter_engine_pod def install_flutter_engine_pod
engine_dir = File.join(__dir__, 'engine') current_directory = File.expand_path('..', __FILE__)
engine_dir = File.expand_path('engine', current_directory)
if !File.exist?(engine_dir) if !File.exist?(engine_dir)
# Copy the debug engine to have something to link against if the xcode backend script has not run yet. # Copy the debug engine to have something to link against if the xcode backend script has not run yet.
# CocoaPods will not embed the framework on pod install (before any build phases can generate) if the dylib does not exist. # CocoaPods will not embed the framework on pod install (before any build phases can generate) if the dylib does not exist.
...@@ -35,7 +42,8 @@ def install_flutter_engine_pod ...@@ -35,7 +42,8 @@ def install_flutter_engine_pod
# Keep pod path relative so it can be checked into Podfile.lock. # Keep pod path relative so it can be checked into Podfile.lock.
# Process will be run from project directory. # Process will be run from project directory.
engine_pathname = Pathname.new engine_dir engine_pathname = Pathname.new engine_dir
project_directory_pathname = Pathname.new Dir.pwd # defined_in_file is set by CocoaPods and is a Pathname to the Podfile.
project_directory_pathname = defined_in_file.dirname
relative = engine_pathname.relative_path_from project_directory_pathname relative = engine_pathname.relative_path_from project_directory_pathname
pod 'Flutter', :path => relative.to_s, :inhibit_warnings => true pod 'Flutter', :path => relative.to_s, :inhibit_warnings => true
...@@ -55,19 +63,26 @@ def install_flutter_plugin_pods(flutter_application_path) ...@@ -55,19 +63,26 @@ def install_flutter_plugin_pods(flutter_application_path)
# Keep pod path relative so it can be checked into Podfile.lock. # Keep pod path relative so it can be checked into Podfile.lock.
# Process will be run from project directory. # Process will be run from project directory.
current_directory_pathname = Pathname.new __dir__ current_directory_pathname = Pathname.new File.expand_path('..', __FILE__)
project_directory_pathname = Pathname.new Dir.pwd # defined_in_file is set by CocoaPods and is a Pathname to the Podfile.
project_directory_pathname = defined_in_file.dirname
relative = current_directory_pathname.relative_path_from project_directory_pathname relative = current_directory_pathname.relative_path_from project_directory_pathname
pod 'FlutterPluginRegistrant', :path => File.join(relative, 'FlutterPluginRegistrant'), :inhibit_warnings => true pod 'FlutterPluginRegistrant', :path => File.join(relative, 'FlutterPluginRegistrant'), :inhibit_warnings => true
symlinks_dir = File.join(relative, '.symlinks') symlinks_dir = File.join(relative, '.symlinks')
FileUtils.mkdir_p(symlinks_dir) FileUtils.mkdir_p(symlinks_dir)
plugin_pods = parse_KV_file(File.join(flutter_application_path, '.flutter-plugins'))
plugin_pods.map do |r| plugins_file = File.expand_path('.flutter-plugins-dependencies', flutter_application_path)
symlink = File.join(symlinks_dir, r[:name]) plugin_pods = flutter_parse_dependencies_file_for_ios_plugin(plugins_file)
plugin_pods.each do |plugin_hash|
plugin_name = plugin_hash['name']
plugin_path = plugin_hash['path']
if (plugin_name && plugin_path)
symlink = File.join(symlinks_dir, plugin_name)
FileUtils.rm_f(symlink) FileUtils.rm_f(symlink)
File.symlink(r[:path], symlink) File.symlink(plugin_path, symlink)
pod r[:name], :path => File.join(symlink, 'ios'), :inhibit_warnings => true pod plugin_name, :path => File.join(symlink, 'ios'), :inhibit_warnings => true
end
end end
end end
...@@ -81,7 +96,7 @@ end ...@@ -81,7 +96,7 @@ end
# Optional, defaults to two levels up from the directory of this script. # Optional, defaults to two levels up from the directory of this script.
# MyApp/my_flutter/.ios/Flutter/../.. # MyApp/my_flutter/.ios/Flutter/../..
def install_flutter_application_pod(flutter_application_path) def install_flutter_application_pod(flutter_application_path)
app_framework_dir = File.join(__dir__, 'App.framework') app_framework_dir = File.expand_path('App.framework', File.join('..', __FILE__))
app_framework_dylib = File.join(app_framework_dir, 'App') app_framework_dylib = File.join(app_framework_dir, 'App')
if !File.exist?(app_framework_dylib) if !File.exist?(app_framework_dylib)
# Fake an App.framework to have something to link against if the xcode backend script has not run yet. # Fake an App.framework to have something to link against if the xcode backend script has not run yet.
...@@ -93,8 +108,9 @@ def install_flutter_application_pod(flutter_application_path) ...@@ -93,8 +108,9 @@ def install_flutter_application_pod(flutter_application_path)
# Keep pod and script phase paths relative so they can be checked into source control. # Keep pod and script phase paths relative so they can be checked into source control.
# Process will be run from project directory. # Process will be run from project directory.
current_directory_pathname = Pathname.new __dir__ current_directory_pathname = Pathname.new File.expand_path('..', __FILE__)
project_directory_pathname = Pathname.new Dir.pwd # defined_in_file is set by CocoaPods and is a Pathname to the Podfile.
project_directory_pathname = defined_in_file.dirname
relative = current_directory_pathname.relative_path_from project_directory_pathname relative = current_directory_pathname.relative_path_from project_directory_pathname
pod '{{projectName}}', :path => relative.to_s, :inhibit_warnings => true pod '{{projectName}}', :path => relative.to_s, :inhibit_warnings => true
...@@ -110,37 +126,31 @@ def install_flutter_application_pod(flutter_application_path) ...@@ -110,37 +126,31 @@ def install_flutter_application_pod(flutter_application_path)
:execution_position => :before_compile :execution_position => :before_compile
end end
def parse_KV_file(file, separator='=') # .flutter-plugins-dependencies format documented at
file_abs_path = File.expand_path(file) # https://flutter.dev/go/plugins-list-migration
if !File.exists? file_abs_path def flutter_parse_dependencies_file_for_ios_plugin(file)
return []; file_path = File.expand_path(file)
end return [] unless File.exists? file_path
pods_array = []
skip_line_start_symbols = ["#", "/"] dependencies_file = File.read(file)
File.foreach(file_abs_path) { |line| dependencies_hash = JSON.parse(dependencies_file)
next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ }
plugin = line.split(pattern=separator) # dependencies_hash.dig('plugins', 'ios') not available until Ruby 2.3
if plugin.length == 2 return [] unless dependencies_hash.has_key?('plugins')
podname = plugin[0].strip() return [] unless dependencies_hash['plugins'].has_key?('ios')
path = plugin[1].strip() dependencies_hash['plugins']['ios'] || []
podpath = File.expand_path("#{path}", file_abs_path)
pods_array.push({:name => podname, :path => podpath});
else
puts "Invalid plugin specification: #{line}"
end
}
return pods_array
end end
def flutter_root def flutter_root
generated_xcode_build_settings = parse_KV_file(File.join(__dir__, 'Generated.xcconfig')) generated_xcode_build_settings_path = File.expand_path(File.join('..', '..', 'Flutter', 'Generated.xcconfig'), __FILE__)
if generated_xcode_build_settings.empty? unless File.exist?(generated_xcode_build_settings_path)
puts "Generated.xcconfig must exist. Make sure `flutter pub get` is executed in the Flutter module." raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
exit
end end
generated_xcode_build_settings.map { |p|
if p[:name] == 'FLUTTER_ROOT' File.foreach(generated_xcode_build_settings_path) do |line|
return p[:path] matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end end
} # This should never happen...
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end end
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