Unverified Commit 36be63ba authored by Jenn Magder's avatar Jenn Magder Committed by GitHub

Embed Flutter and App frameworks for add-to-app on iOS (#102538)

parent d75d54c9
......@@ -59,7 +59,8 @@ Future<void> main() async {
);
});
checkDirectoryExists(path.join(projectDir.path, '.ios', 'Flutter', 'engine', 'Flutter.xcframework'));
// Check the tool is no longer copying to the legacy xcframework location.
checkDirectoryNotExists(path.join(projectDir.path, '.ios', 'Flutter', 'engine', 'Flutter.xcframework'));
final Directory ephemeralIOSHostApp = Directory(path.join(
projectDir.path,
......@@ -79,32 +80,6 @@ Future<void> main() async {
);
}
section('Build ephemeral host app when SDK is on external disk');
// Pretend the SDK was on an external drive with stray "._" files in the xcframework
// and build again.
Directory(path.join(
projectDir.path,
'.ios',
'Flutter',
'engine',
'Flutter.xcframework',
'._ios-arm64_x86_64-simulator',
)).createSync(recursive: true);
await inDirectory(projectDir, () async {
await flutter(
'build',
options: <String>['ios', '--no-codesign', '--simulator', '--debug'],
);
});
section('Clean build');
await inDirectory(projectDir, () async {
await flutter('clean');
});
section('Build ephemeral host app in profile mode without CocoaPods');
await inDirectory(projectDir, () async {
......@@ -114,8 +89,6 @@ Future<void> main() async {
);
});
checkDirectoryExists(path.join(projectDir.path, '.ios', 'Flutter', 'engine', 'Flutter.xcframework'));
if (!exists(ephemeralIOSHostApp)) {
return TaskResult.failure('Failed to build ephemeral host .app');
}
......@@ -211,7 +184,6 @@ dependencies:
options: <String>['ios', '--no-codesign', '-v'],
);
});
checkDirectoryExists(path.join(projectDir.path, '.ios', 'Flutter', 'engine', 'Flutter.xcframework'));
final bool ephemeralHostAppWithCocoaPodsBuilt = exists(ephemeralIOSHostApp);
......@@ -265,8 +237,6 @@ dependencies:
final File objectiveCAnalyticsOutputFile = File(path.join(tempDir.path, 'analytics-objc.log'));
final Directory objectiveCBuildDirectory = Directory(path.join(tempDir.path, 'build-objc'));
final File dummyAppFramework = File(path.join(projectDir.path, '.ios', 'Flutter', 'App.framework', 'App'));
checkFileNotExists(dummyAppFramework.path);
await inDirectory(objectiveCHostApp, () async {
section('Validate iOS Objective-C host app Podfile');
......@@ -305,7 +275,7 @@ end
final File hostPodfileLockFile = File(path.join(objectiveCHostApp.path, 'Podfile.lock'));
final String hostPodfileLockOutput = hostPodfileLockFile.readAsStringSync();
if (!hostPodfileLockOutput.contains(':path: "../hello/.ios/Flutter/engine"')
if (!hostPodfileLockOutput.contains(':path: "../hello/.ios/Flutter"')
|| !hostPodfileLockOutput.contains(':path: "../hello/.ios/Flutter/FlutterPluginRegistrant"')
|| !hostPodfileLockOutput.contains(':path: "../hello/.ios/.symlinks/plugins/url_launcher_ios/ios"')
|| !hostPodfileLockOutput.contains(':path: "../hello/.ios/.symlinks/plugins/google_sign_in/ios"')
......@@ -315,13 +285,9 @@ end
throw TaskResult.failure('Building host app Podfile.lock does not contain expected pods');
}
// Just running "pod install" should create a fake App.framework so CocoaPods recognizes
// it as a framework that needs to be embedded, before Flutter actually creates it.
checkFileExists(dummyAppFramework.path);
final String? version = await minPhoneOSVersion(dummyAppFramework.path);
if (version != '11.0') {
throw TaskResult.failure('Minimum version set to $version, expected 11.0');
}
// Check the tool is no longer copying to the legacy App.framework location.
final File dummyAppFramework = File(path.join(projectDir.path, '.ios', 'Flutter', 'App.framework', 'App'));
checkFileNotExists(dummyAppFramework.path);
section('Build iOS Objective-C host app');
......
......@@ -161,7 +161,8 @@ def flutter_install_ios_engine_pod(ios_application_path = nil)
ios_application_path ||= File.dirname(defined_in_file.realpath) if self.respond_to?(:defined_in_file)
raise 'Could not find iOS application path' unless ios_application_path
copied_podspec_path = File.expand_path('Flutter.podspec', File.join(ios_application_path, 'Flutter'))
podspec_directory = File.join(ios_application_path, 'Flutter')
copied_podspec_path = File.expand_path('Flutter.podspec', podspec_directory)
# Generate a fake podspec to represent the Flutter framework.
# This is only necessary because plugin podspecs contain `s.dependency 'Flutter'`, and if this Podfile
......@@ -190,7 +191,7 @@ def flutter_install_ios_engine_pod(ios_application_path = nil)
}
# Keep pod path relative so it can be checked into Podfile.lock.
pod 'Flutter', :path => 'Flutter'
pod 'Flutter', :path => flutter_relative_path_from_podfile(podspec_directory)
end
# Same as flutter_install_ios_engine_pod for macOS.
......@@ -260,7 +261,9 @@ def flutter_install_plugin_pods(application_path = nil, relative_symlink_dir, pl
File.symlink(plugin_path, symlink)
# Keep pod path relative so it can be checked into Podfile.lock.
pod plugin_name, :path => File.join(relative_symlink_dir, 'plugins', plugin_name, platform)
relative = flutter_relative_path_from_podfile(symlink)
pod plugin_name, :path => File.join(relative, platform)
end
end
end
......@@ -279,3 +282,12 @@ def flutter_parse_plugins_file(file, platform)
return [] unless dependencies_hash['plugins'].has_key?('ios')
dependencies_hash['plugins'][platform] || []
end
def flutter_relative_path_from_podfile(path)
# defined_in_file is set by CocoaPods and is a Pathname to the Podfile.
project_directory_pathname = defined_in_file.dirname
pathname = Pathname.new File.expand_path(path)
relative = pathname.relative_path_from project_directory_pathname
relative.to_s
end
......@@ -321,28 +321,7 @@ class Context {
targetPath = environment['FLUTTER_TARGET']!;
}
String derivedDir = '$sourceRoot/Flutter}';
if (existsDir('$projectPath/.ios')) {
derivedDir = '$projectPath/.ios/Flutter';
}
// Use FLUTTER_BUILD_MODE if it's set, otherwise use the Xcode build configuration name
// This means that if someone wants to use an Xcode build config other than Debug/Profile/Release,
// they _must_ set FLUTTER_BUILD_MODE so we know what type of artifact to build.
final String buildMode = parseFlutterBuildMode();
String artifactVariant = 'unknown';
switch (buildMode) {
case 'release':
artifactVariant = 'ios-release';
break;
case 'profile':
artifactVariant = 'ios-profile';
break;
case 'debug':
artifactVariant = 'ios';
break;
}
// Warn the user if not archiving (ACTION=install) in release mode.
final String? action = environment['ACTION'];
......@@ -353,47 +332,12 @@ class Context {
'--release", then re-run Archive from Xcode.',
);
}
final String frameworkPath = '${environmentEnsure('FLUTTER_ROOT')}/bin/cache/artifacts/engine/$artifactVariant';
String flutterFramework = '$frameworkPath/Flutter.xcframework';
final String? localEngine = environment['LOCAL_ENGINE'];
if (localEngine != null) {
if (!localEngine.toLowerCase().contains(buildMode)) {
echoError('========================================================================');
echoError("ERROR: Requested build with Flutter local engine at '$localEngine'");
echoError("This engine is not compatible with FLUTTER_BUILD_MODE: '$buildMode'.");
echoError('You can fix this by updating the LOCAL_ENGINE environment variable, or');
echoError('by running:');
echoError(' flutter build ios --local-engine=ios_$buildMode');
echoError('or');
echoError(' flutter build ios --local-engine=ios_${buildMode}_unopt');
echoError('========================================================================');
exitApp(-1);
}
flutterFramework = '${environmentEnsure('FLUTTER_ENGINE')}/out/$localEngine/Flutter.xcframework';
}
String bitcodeFlag = '';
if (environment['ENABLE_BITCODE'] == 'YES' && environment['ACTION'] == 'install') {
bitcodeFlag = 'true';
}
// TODO(jmagman): use assemble copied engine in add-to-app.
if (existsDir('$projectPath/.ios')) {
runSync(
'rsync',
<String>[
'-av',
'--delete',
'--filter',
'- .DS_Store',
flutterFramework,
'$derivedDir/engine',
],
verbose: verbose,
);
}
final List<String> flutterArgs = <String>[];
if (verbose) {
......
......@@ -2,7 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'artifacts.dart';
import 'base/error_handling_io.dart';
import 'base/file_system.dart';
import 'base/utils.dart';
......@@ -399,29 +398,6 @@ class IosProject extends XcodeBasedProject {
ephemeralModuleDirectory,
);
}
// Use release mode so host project can link on bitcode variant.
_copyEngineArtifactToProject(BuildMode.release, EnvironmentType.physical);
}
}
void _copyEngineArtifactToProject(BuildMode mode, EnvironmentType environmentType) {
// Copy framework from engine cache. The actual build mode
// doesn't actually matter as it will be overwritten by xcode_backend.sh.
// However, cocoapods will run before that script and requires something
// to be in this location.
final Directory framework = globals.fs.directory(
globals.artifacts?.getArtifactPath(
Artifact.flutterXcframework,
platform: TargetPlatform.ios,
mode: mode,
environmentType: environmentType,
)
);
if (framework.existsSync()) {
copyDirectory(
framework,
engineCopyDirectory.childDirectory('Flutter.xcframework'),
);
}
}
......@@ -467,12 +443,6 @@ class IosProject extends XcodeBasedProject {
return registryDirectory.childFile('GeneratedPluginRegistrant.m');
}
Directory get engineCopyDirectory {
return isModule
? ephemeralModuleDirectory.childDirectory('Flutter').childDirectory('engine')
: hostAppRoot.childDirectory('Flutter');
}
Future<void> _overwriteFromTemplate(String path, Directory target) async {
final Template template = await Template.fromName(
path,
......
#
# NOTE: This podspec is NOT to be published. It is only used as a local source!
#
Pod::Spec.new do |s|
s.name = 'Flutter'
s.version = '1.0.0'
s.summary = 'A UI toolkit for beautiful and fast apps.'
s.description = <<-DESC
Flutter is Google's UI toolkit for building beautiful, fast apps for mobile, web, desktop, and embedded devices from a single codebase.
This pod vends the iOS Flutter engine framework. It is compatible with application frameworks created with this version of the engine and tools.
DESC
s.homepage = 'https://flutter.dev'
s.license = 'BSD'
s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' }
s.source = { :git => 'https://github.com/flutter/engine', :tag => s.version.to_s }
s.documentation_url = 'https://flutter.dev/docs'
s.ios.deployment_target = '11.0'
s.vendored_frameworks = 'Flutter.xcframework'
end
......@@ -31,7 +31,7 @@ POSTINSTALL
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_application_path ||= File.join('..', '..')
install_flutter_engine_pod
install_flutter_engine_pod(flutter_application_path)
install_flutter_plugin_pods(flutter_application_path)
install_flutter_application_pod(flutter_application_path)
end
......@@ -42,30 +42,12 @@ end
# target 'MyApp' do
# install_flutter_engine_pod
# end
def install_flutter_engine_pod
current_directory = File.expand_path('..', __FILE__)
engine_dir = File.expand_path('engine', current_directory)
framework_name = 'Flutter.xcframework'
copied_engine = File.expand_path(framework_name, engine_dir)
if !File.exist?(copied_engine)
# 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.
release_framework_dir = File.join(flutter_root, 'bin', 'cache', 'artifacts', 'engine', 'ios-release')
unless Dir.exist?(release_framework_dir)
# iOS artifacts have not been downloaded.
raise "#{release_framework_dir} must exist. Make sure \"flutter precache --ios\" has been run at least once"
end
FileUtils.cp_r(File.join(release_framework_dir, framework_name), engine_dir)
end
# 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
# defined_in_file is a Pathname to the Podfile set by CocoaPods.
project_directory_pathname = defined_in_file.dirname
relative = engine_pathname.relative_path_from project_directory_pathname
def install_flutter_engine_pod(flutter_application_path = nil)
flutter_application_path ||= File.join('..', '..')
ios_application_path = File.join(flutter_application_path, '.ios')
pod 'Flutter', :path => relative.to_s, :inhibit_warnings => true
# flutter_install_ios_engine_pod is in Flutter root podhelper.rb
flutter_install_ios_engine_pod(ios_application_path)
end
# Install Flutter plugin pods.
......@@ -79,33 +61,13 @@ end
# MyApp/my_flutter/.ios/Flutter/../..
def install_flutter_plugin_pods(flutter_application_path)
flutter_application_path ||= File.join('..', '..')
ios_application_path = File.join(flutter_application_path, '.ios')
# flutter_install_plugin_pods is in Flutter root podhelper.rb
flutter_install_plugin_pods(ios_application_path, '.symlinks', 'ios')
# Keep pod path relative so it can be checked into Podfile.lock.
# Process will be run from project directory.
ios_project_directory_pathname = Pathname.new File.expand_path(File.join('..', '..'), __FILE__)
# defined_in_file is set by CocoaPods and is a Pathname to the Podfile.
project_directory_pathname = defined_in_file.dirname
relative = ios_project_directory_pathname.relative_path_from project_directory_pathname
relative = flutter_relative_path_from_podfile(ios_application_path)
pod 'FlutterPluginRegistrant', :path => File.join(relative, 'Flutter', 'FlutterPluginRegistrant'), :inhibit_warnings => true
symlinks_dir = File.join(relative, '.symlinks', 'plugins')
FileUtils.mkdir_p(symlinks_dir)
plugins_file = File.expand_path('.flutter-plugins-dependencies', flutter_application_path)
# flutter_parse_plugins_file is in Flutter root podhelper.rb
plugin_pods = flutter_parse_plugins_file(plugins_file, 'ios')
plugin_pods.each do |plugin_hash|
plugin_name = plugin_hash['name']
plugin_path = plugin_hash['path']
has_native_build = plugin_hash.fetch('native_build', true)
if (plugin_name && plugin_path && has_native_build)
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
# Install Flutter application pod.
......@@ -118,30 +80,24 @@ 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)
current_directory_pathname = Pathname.new File.expand_path('..', __FILE__)
app_framework_dir = File.expand_path('App.framework', current_directory_pathname.to_path)
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.
# CocoaPods will not embed the framework on pod install (before any build phases can run) if the dylib does not exist.
# Create a dummy dylib.
FileUtils.mkdir_p(app_framework_dir)
sdk_path = `xcrun --sdk iphoneos --show-sdk-path`.strip
`echo "static const int Moo = 88;" | xcrun clang -x c -arch arm64 -dynamiclib -miphoneos-version-min=11.0 -isysroot "#{sdk_path}" -o "#{app_framework_dylib}" -`
end
flutter_application_path ||= File.join('..', '..')
# Keep pod and script phase paths relative so they can be checked into source control.
# Process will be run from project directory.
export_script_directory = File.join(flutter_application_path, '.ios', 'Flutter')
# 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
# Keep script phase paths relative so they can be checked into source control.
relative = flutter_relative_path_from_podfile(export_script_directory)
flutter_export_environment_path = File.join('${SRCROOT}', relative, 'flutter_export_environment.sh');
# Compile App.framework and move it and Flutter.framework to "BUILT_PRODUCTS_DIR"
script_phase :name => 'Run Flutter Build {{projectName}} Script',
:script => "set -e\nset -u\nsource \"#{flutter_export_environment_path}\"\nexport VERBOSE_SCRIPT_LOGGING=1 && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/xcode_backend.sh build",
:execution_position => :before_compile
# Embed App.framework AND Flutter.framework.
script_phase :name => 'Embed Flutter Build {{projectName}} Script',
:script => "set -e\nset -u\nsource \"#{flutter_export_environment_path}\"\nexport VERBOSE_SCRIPT_LOGGING=1 && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/xcode_backend.sh embed_and_thin",
:execution_position => :after_compile
end
def flutter_root
......
Pod::Spec.new do |s|
s.name = '{{projectName}}'
s.version = '0.0.1'
s.summary = 'Flutter module'
s.description = 'Flutter module - {{projectName}}'
s.homepage = 'https://flutter.dev'
s.license = { :type => 'BSD' }
s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' }
s.source = { :path => '.' }
s.ios.deployment_target = '11.0'
s.vendored_frameworks = 'App.framework'
s.dependency 'Flutter'
end
......@@ -186,7 +186,6 @@
"templates/module/ios/host_app_ephemeral/Config.tmpl/Debug.xcconfig",
"templates/module/ios/host_app_ephemeral/Config.tmpl/Flutter.xcconfig",
"templates/module/ios/host_app_ephemeral/Config.tmpl/Release.xcconfig",
"templates/module/ios/host_app_ephemeral/Flutter/engine/Flutter.podspec.tmpl",
"templates/module/ios/host_app_ephemeral/Runner.tmpl/AppDelegate.h",
"templates/module/ios/host_app_ephemeral/Runner.tmpl/AppDelegate.m",
"templates/module/ios/host_app_ephemeral/Runner.tmpl/Assets.xcassets/AppIcon.appiconset/Contents.json",
......@@ -228,7 +227,6 @@
"templates/module/ios/host_app_ephemeral_cocoapods/Runner.tmpl/AppDelegate.m",
"templates/module/ios/library/Flutter.tmpl/AppFrameworkInfo.plist",
"templates/module/ios/library/Flutter.tmpl/podhelper.rb.tmpl",
"templates/module/ios/library/Flutter.tmpl/projectName.podspec.tmpl",
"templates/module/ios/library/Flutter.tmpl/README.md",
"templates/module/README.md",
......
......@@ -222,8 +222,6 @@ void main() {
'.android/app/',
'.gitignore',
'.ios/Flutter',
'.ios/Flutter/flutter_project.podspec',
'.ios/Flutter/engine/Flutter.podspec',
'.metadata',
'analysis_options.yaml',
'lib/main.dart',
......@@ -1163,13 +1161,6 @@ void main() {
// Do not override host app build settings.
expect(buildPhaseScript, isNot(contains('SYMROOT')));
// Generated podspec
final String podspecPath = globals.fs.path.join('.ios', 'Flutter', 'flutter_project.podspec');
expectExists(podspecPath);
final File podspecFile = globals.fs.file(globals.fs.path.join(projectDir.path, podspecPath));
final String podspec = podspecFile.readAsStringSync();
expect(podspec, contains('Flutter module - flutter_project'));
// App identification
final String xcodeProjectPath = globals.fs.path.join('.ios', 'Runner.xcodeproj', 'project.pbxproj');
expectExists(xcodeProjectPath);
......
......@@ -318,7 +318,6 @@ void main() {
final Directory flutter = project.ios.hostAppRoot.childDirectory('Flutter');
expectExists(flutter.childFile('podhelper.rb'));
expectExists(flutter.childFile('flutter_export_environment.sh'));
expectExists(flutter.childFile('${project.manifest.appName}.podspec'));
expectExists(flutter.childFile('Generated.xcconfig'));
final Directory pluginRegistrantClasses = flutter
.childDirectory('FlutterPluginRegistrant')
......
......@@ -26,23 +26,6 @@ const Map<String, String> unknownFlutterBuildMode = <String, String>{
'CONFIGURATION': 'Debug',
};
// Can't use a debug engine build with a release build.
const Map<String, String> localEngineDebugBuildModeRelease = <String, String>{
'SOURCE_ROOT': '../examples/hello_world',
'FLUTTER_ROOT': '../..',
'LOCAL_ENGINE': '/engine/src/out/ios_debug_unopt',
'CONFIGURATION': 'Release',
};
// Can't use a debug build with a profile engine.
const Map<String, String> localEngineProfileBuildModeRelease = <String, String>{
'SOURCE_ROOT': '../examples/hello_world',
'FLUTTER_ROOT': '../..',
'LOCAL_ENGINE': '/engine/src/out/ios_profile',
'CONFIGURATION': 'Debug',
'FLUTTER_BUILD_MODE': 'Debug',
};
void main() {
Future<void> expectXcodeBackendFails(Map<String, String> environment) async {
final ProcessResult result = await Process.run(
......@@ -70,8 +53,6 @@ void main() {
test('Xcode backend fails for on unsupported configuration combinations', () async {
await expectXcodeBackendFails(unknownConfiguration);
await expectXcodeBackendFails(unknownFlutterBuildMode);
await expectXcodeBackendFails(localEngineDebugBuildModeRelease);
await expectXcodeBackendFails(localEngineProfileBuildModeRelease);
}, skip: !io.Platform.isMacOS); // [intended] requires macos toolchain.
test('Xcode backend warns archiving a non-release build.', () async {
......
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