diff --git a/dev/devicelab/bin/tasks/module_test_ios.dart b/dev/devicelab/bin/tasks/module_test_ios.dart index f20fff9208a..e16a0685307 100644 --- a/dev/devicelab/bin/tasks/module_test_ios.dart +++ b/dev/devicelab/bin/tasks/module_test_ios.dart @@ -14,6 +14,7 @@ import 'package:path/path.dart' as path; /// adding Flutter to an existing iOS app. Future main() async { await task(() async { + String simulatorDeviceId; section('Create Flutter module project'); final Directory tempDir = Directory.systemTemp.createTempSync('flutter_module_test.'); @@ -52,7 +53,7 @@ Future main() async { ); }); - final Directory ephemeralReleaseHostApp = Directory(path.join( + final Directory ephemeralIOSHostApp = Directory(path.join( projectDir.path, 'build', 'ios', @@ -60,13 +61,13 @@ Future main() async { 'Runner.app', )); - if (!exists(ephemeralReleaseHostApp)) { + if (!exists(ephemeralIOSHostApp)) { return TaskResult.failure('Failed to build ephemeral host .app'); } - if (!await _isAppAotBuild(ephemeralReleaseHostApp)) { + if (!await _isAppAotBuild(ephemeralIOSHostApp)) { 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 main() async { ); }); - final Directory ephemeralProfileHostApp = Directory(path.join( - projectDir.path, - 'build', - 'ios', - 'iphoneos', - 'Runner.app', - )); - - if (!exists(ephemeralProfileHostApp)) { + if (!exists(ephemeralIOSHostApp)) { return TaskResult.failure('Failed to build ephemeral host .app'); } - if (!await _isAppAotBuild(ephemeralProfileHostApp)) { + if (!await _isAppAotBuild(ephemeralIOSHostApp)) { 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 main() async { ); }); - final Directory ephemeralDebugHostApp = Directory(path.join( + final Directory ephemeralSimulatorHostApp = Directory(path.join( projectDir.path, 'build', 'ios', @@ -126,19 +119,19 @@ Future main() async { 'Runner.app', )); - if (!exists(ephemeralDebugHostApp)) { + if (!exists(ephemeralSimulatorHostApp)) { return TaskResult.failure('Failed to build ephemeral host .app'); } if (!exists(File(path.join( - ephemeralDebugHostApp.path, + ephemeralSimulatorHostApp.path, 'Frameworks', 'App.framework', 'flutter_assets', 'isolate_snapshot_data', )))) { 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 main() async { String content = await pubspec.readAsString(); content = content.replaceFirst( '\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 inDirectory(projectDir, () async { @@ -173,13 +167,7 @@ Future main() async { ); }); - final bool ephemeralHostAppWithCocoaPodsBuilt = exists(Directory(path.join( - projectDir.path, - 'build', - 'ios', - 'iphoneos', - 'Runner.app', - ))); + final bool ephemeralHostAppWithCocoaPodsBuilt = exists(ephemeralIOSHostApp); if (!ephemeralHostAppWithCocoaPodsBuilt) { return TaskResult.failure('Failed to build ephemeral host .app with CocoaPods'); @@ -190,10 +178,19 @@ Future main() async { if (!podfileLockOutput.contains(':path: Flutter/engine') || !podfileLockOutput.contains(':path: Flutter/FlutterPluginRegistrant') || !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'); } + 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'); final Directory objectiveCHostApp = Directory(path.join(tempDir.path, 'hello_host_app')); @@ -255,8 +252,9 @@ Future main() async { } section('Run platform unit tests'); - await testWithNewiOSSimulator('TestAdd2AppSim', (String deviceId) => - inDirectory(objectiveCHostApp, () => + await testWithNewIOSSimulator('TestAdd2AppSim', (String deviceId) { + simulatorDeviceId = deviceId; + return inDirectory(objectiveCHostApp, () => exec( 'xcodebuild', [ @@ -275,8 +273,8 @@ Future main() async { 'EXPANDED_CODE_SIGN_IDENTITY=-', 'COMPILER_INDEX_STORE_ENABLE=NO', ], - ) - ) + )); + } ); section('Fail building existing Objective-C iOS app if flutter script fails'); @@ -371,6 +369,7 @@ Future main() async { } catch (e) { return TaskResult.failure(e.toString()); } finally { + removeIOSimulator(simulatorDeviceId); rmTree(tempDir); } }); diff --git a/dev/devicelab/lib/framework/ios.dart b/dev/devicelab/lib/framework/ios.dart index 80aa03c3993..5ea618b3344 100644 --- a/dev/devicelab/lib/framework/ios.dart +++ b/dev/devicelab/lib/framework/ios.dart @@ -103,8 +103,10 @@ Future containsBitcode(String pathToBinary) async { } /// Creates and boots a new simulator, passes the new simulator's identifier to -/// `testFunction`, then shuts down and deletes simulator. -Future testWithNewiOSSimulator( +/// `testFunction`. +/// +/// Remember to call removeIOSimulator in the test teardown. +Future testWithNewIOSSimulator( String deviceName, SimulatorFunction testFunction, { String deviceTypeId = 'com.apple.CoreSimulator.SimDeviceType.iPhone-11', @@ -160,7 +162,10 @@ Future testWithNewiOSSimulator( ); await testFunction(deviceId); +} +/// Shuts down and deletes simulator with deviceId. +Future removeIOSimulator(String deviceId) async { if (deviceId != null && deviceId != '') { await eval( 'xcrun', diff --git a/packages/flutter_tools/templates/module/ios/library/Flutter.tmpl/podhelper.rb.tmpl b/packages/flutter_tools/templates/module/ios/library/Flutter.tmpl/podhelper.rb.tmpl index 5ab99f161f2..4d7715a23af 100644 --- a/packages/flutter_tools/templates/module/ios/library/Flutter.tmpl/podhelper.rb.tmpl +++ b/packages/flutter_tools/templates/module/ios/library/Flutter.tmpl/podhelper.rb.tmpl @@ -1,3 +1,9 @@ +# 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 # from the host application Podfile. # @@ -22,7 +28,8 @@ end # install_flutter_engine_pod # end 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) # 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. @@ -35,7 +42,8 @@ def install_flutter_engine_pod # Keep pod path relative so it can be checked into Podfile.lock. # Process will be run from project directory. 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 pod 'Flutter', :path => relative.to_s, :inhibit_warnings => true @@ -55,19 +63,26 @@ def install_flutter_plugin_pods(flutter_application_path) # Keep pod path relative so it can be checked into Podfile.lock. # Process will be run from project directory. - current_directory_pathname = Pathname.new __dir__ - project_directory_pathname = Pathname.new Dir.pwd + current_directory_pathname = Pathname.new File.expand_path('..', __FILE__) + # 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 pod 'FlutterPluginRegistrant', :path => File.join(relative, 'FlutterPluginRegistrant'), :inhibit_warnings => true symlinks_dir = File.join(relative, '.symlinks') FileUtils.mkdir_p(symlinks_dir) - plugin_pods = parse_KV_file(File.join(flutter_application_path, '.flutter-plugins')) - plugin_pods.map do |r| - symlink = File.join(symlinks_dir, r[:name]) - FileUtils.rm_f(symlink) - File.symlink(r[:path], symlink) - pod r[:name], :path => File.join(symlink, 'ios'), :inhibit_warnings => true + + plugins_file = File.expand_path('.flutter-plugins-dependencies', flutter_application_path) + 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) + File.symlink(plugin_path, symlink) + pod plugin_name, :path => File.join(symlink, 'ios'), :inhibit_warnings => true + end end end @@ -81,7 +96,7 @@ end # Optional, defaults to two levels up from the directory of this script. # MyApp/my_flutter/.ios/Flutter/../.. 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') 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. @@ -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. # Process will be run from project directory. - current_directory_pathname = Pathname.new __dir__ - project_directory_pathname = Pathname.new Dir.pwd + current_directory_pathname = Pathname.new File.expand_path('..', __FILE__) + # 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 pod '{{projectName}}', :path => relative.to_s, :inhibit_warnings => true @@ -110,37 +126,31 @@ def install_flutter_application_pod(flutter_application_path) :execution_position => :before_compile end -def parse_KV_file(file, separator='=') - file_abs_path = File.expand_path(file) - if !File.exists? file_abs_path - return []; - end - pods_array = [] - skip_line_start_symbols = ["#", "/"] - File.foreach(file_abs_path) { |line| - next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } - plugin = line.split(pattern=separator) - if plugin.length == 2 - podname = plugin[0].strip() - path = plugin[1].strip() - 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 +# .flutter-plugins-dependencies format documented at +# https://flutter.dev/go/plugins-list-migration +def flutter_parse_dependencies_file_for_ios_plugin(file) + file_path = File.expand_path(file) + return [] unless File.exists? file_path + + dependencies_file = File.read(file) + dependencies_hash = JSON.parse(dependencies_file) + + # dependencies_hash.dig('plugins', 'ios') not available until Ruby 2.3 + return [] unless dependencies_hash.has_key?('plugins') + return [] unless dependencies_hash['plugins'].has_key?('ios') + dependencies_hash['plugins']['ios'] || [] end def flutter_root - generated_xcode_build_settings = parse_KV_file(File.join(__dir__, 'Generated.xcconfig')) - if generated_xcode_build_settings.empty? - puts "Generated.xcconfig must exist. Make sure `flutter pub get` is executed in the Flutter module." - exit + generated_xcode_build_settings_path = File.expand_path(File.join('..', '..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" end - generated_xcode_build_settings.map { |p| - if p[:name] == 'FLUTTER_ROOT' - return p[:path] - end - } + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + 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