Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Add2App] provide flag for 'build ios-framework' command to build only App.xcframework #114692

Closed
p-mazhnik opened this issue Nov 4, 2022 · 9 comments · Fixed by #129739
Closed
Labels
a: existing-apps Integration with existing apps via the add-to-app flow c: new feature Nothing broken; request for a new capability c: proposal A detailed proposal for a change to Flutter P2 Important issues not at the top of the work list platform-ios iOS applications specifically team-ios Owned by iOS platform team tool Affects the "flutter" command-line tool. See also t: labels. triaged-ios Triaged by iOS platform team

Comments

@p-mazhnik
Copy link
Contributor

p-mazhnik commented Nov 4, 2022

Feature request: provide flag for flutter build ios-framework command to build only App.xcframework
or to prevent building of third-party pod dependencies as frameworks

Use case

I have a Flutter module (let's say FlutterModule) integrated to an iOS app (IOSApp) using frameworks. My setup is similar to Option C from the add-to-app doc for iOS (with an improvement of embedding application and plugin frameworks to XCode automatically using CocoaPods).

  • FlutterModule depends on third-party Flutter plugins, some of them have iOS dependencies, e.g. sentry_flutter which depends on Sentry pod.
  • IOSApp also uses CocoaPods to add third-party iOS dependencies.

The problem appears when I want to add to IOSApp's Podfile a dependency that reference another dependency that is referenced by Flutter plugin. This can be hard to understand, so here is the schema:
IOSApp -> Pod1 -> Pod2
IOSApp -> flutter_plugin.xcframework -> Pod2.xcframework

So my IOSApp and FlutterModule have the same transitive platform dependency.

Because Pod2 is included twice, first through CocoaPods and second directly as a framework, this causes multiple Redefinition of issues in XCode.

Sentry example

Note that behavior will be the same for case
IOSApp -> Pod2
IOSApp -> flutter_plugin.xcframework -> Pod2.xcframework

Currently flutter build ios-framework command produces following outputs for the build mode:

  • App.xcframework (desired, compiled Dart code)
  • Flutter.xcframework (Flutter.podspec in my case, if --cocoapods flag is provided. Desired)
  • FlutterPluginRegistrant.xcframework (desired, registration code for plugins)
  • flutter_plugin.xcframework (desired, each plugin is a separate framework)
  • Pod2.xcframework (If plugin has dependencies in its podspec, frameworks will be built for each of them. Undesired: Pod2 can be also included to iOS app as a dependency, causing issues)

I didn't create this as an issue because obviously I can just remove the duplicated frameworks from the Flutter command output to not include them. UPD: submit an issue #130220 because this workaround is not working for some plugins, like datadog_flutter_plugin

But this solution is not universal. Some downsides are:

  • each time I add new dependency to FlutterModule I have to make sure it doesn't include CocoaPods dependency that conflicts with CocoaPods dependency of IOSApp. (Note that Sentry pod wasn't the only that caused issues)
  • as a result of the previous I can't safely publish my FlutterModule itself as a CocoaPods dependency to be consumed by any iOS apps
  • because flutter build ios-framework produces frameworks that will be removed right after the creation, this has a significant impact on build time. We can speed up this command by preventing the build of undesired frameworks.

Proposal

Modify build ios-framework command to support this use case. I thought about two options:

  • Option 1: flag to prevent third-party pod dependencies to be built as frameworks.
    Current solution is to build every target from .ios/Pods/Pods.xcodeproj (see _producePlugins function). Targets include both Flutter plugins and their pod dependencies. We can try to change this, but I am not sure if it is possible to build only selected targets without building their dependencies.
    Another question here is how to then easily include all of the remaining pods dependencies to app. Probably we can combine this with --cocoapods flag and generate another podspec with all third-party pods listed.
  • Option 2: flag to build only App.xcframework
    All we need to do is to prevent _producePlugins function from running. Inclusion of the pods dependencies to the app will be developer's responsibility and not the Flutter's (unlike in Option 1, it's easy to do)

I personally implemented Option 2 in my FlutterModule project because it was easier to include pods from Flutter plugins. I had to create a custom build command based on the BuildIOSFrameworkCommand class to prevent the _producePlugins function from running. As a result, only App.xcframework was generated. In order to include Flutter plugins, their dependencies and FlutterPluginRegistrant, I combined this solution with Option A from the add-to-app doc. Basically, for each flutter_plugin included a podspec from .ios/.symlinks/plugins/<flutter_plugin>/ios/<flutter_plugin>.podspec and for the FlutterPluginRegistrant a podspec from .ios/Flutter/FlutterPluginRegistrant/FlutterPluginRegistrant.podspec.


So the main goal for me personally is to remove the custom build command (also BuildIOSFrameworkCommand class has a lot of private methods that I had to copy-paste to the subclass) and use a solution provided by Flutter. And I think it can be a good improvement in general. It also could help writing iOS version of flutter/website#7775 because it currently seems impossible to easily share the same transitive platform dependency between Flutter module and iOS app

Please let me know if I misunderstood something and my use case can be achieved in some existing way

@darshankawar darshankawar added in triage Presently being triaged by the triage team c: new feature Nothing broken; request for a new capability platform-ios iOS applications specifically tool Affects the "flutter" command-line tool. See also t: labels. a: existing-apps Integration with existing apps via the add-to-app flow c: proposal A detailed proposal for a change to Flutter and removed in triage Presently being triaged by the triage team labels Nov 7, 2022
@jmagman
Copy link
Member

jmagman commented Nov 10, 2022

Thanks for so much detail!

  • because flutter build ios-framework produces frameworks that will be removed right after the creation, this has a significant impact on build time. We can speed up this command by preventing the build of undesired frameworks.

We need to build the third-party pod dependencies in order to build the flutter plugins that depend on them, so even if we don't include the xcframework in the output directory, I don't think you'll be able to improve build time much.

Either before or after building, it's tricky to know which are Flutter plugins and which are their dependencies to avoid making the xcframework for only the former. As you already discovered in _producePlugins we build -alltargets.

Basically, for each flutter_plugin included a podspec from .ios/.symlinks/plugins/<flutter_plugin>/ios/<flutter_plugin>.podspec and for the FlutterPluginRegistrant a podspec from .ios/Flutter/FlutterPluginRegistrant/FlutterPluginRegistrant.podspec.

I'm curious, did you just copy those podspecs to a known directory and then pod them by path into your Podfile? Did that work right out of the box? Or did you have to generate podspecs based on parsing the symlinked ones? How do you avoid changing your Podfile when you add new dependencies to FlutterModule, or did you figure out how to do this generically? Would be interested to see your scripts and Podfile.

@p-mazhnik
Copy link
Contributor Author

Hi @jmagman !

We need to build the third-party pod dependencies in order to build the flutter plugins that depend on them, so even if we don't include the xcframework in the output directory, I don't think you'll be able to improve build time much.

Yeah, so it makes Option 1 impossible to implement. But we don't need to build the third-party pod dependencies for the App.framework, so we can consider Option 2

did you just copy those podspecs to a known directory and then pod them by path into your Podfile?

This is exactly what I did :)

I have a script, _copyPlugins (which is actually running instead of _producePlugins in my custom command). This script is inspired by .ios/Flutter/podhelper.rb and flutter_tools/bin/podhelper.rb files and actually rewrites ruby functions in dart. So this script

  • reads data from _project.flutterPluginsDependenciesFile (.flutter-plugins-dependencies file)
List<dynamic> flutterParseDependenciesFileForIosPlugin(file.File flutterPluginsDependenciesFile) {
  final dependenciesFile = flutterPluginsDependenciesFile.readAsStringSync();
  final dependenciesHash = json.decode(dependenciesFile);
  return dependenciesHash['plugins']['ios'];
}

This is actually a flutter_parse_plugins_file function from podhelper.rb.

def flutter_parse_plugins_file(file, platform)

  • For each plugin I copy its ios files and License to the build output (build/ios/framework/plugins/$pluginName)
    final Directory pluginsDirectory = outputDirectory.childDirectory('plugins');
    if (pluginsDirectory.existsSync()) {
      pluginsDirectory.deleteSync(recursive: true);
    }
    
    final pluginPods = flutterParseDependenciesFileForIosPlugin(_project.flutterPluginsDependenciesFile);
    
    final pluginsOutputPath = pluginsDirectory.path;
    
    for (final pluginHash in pluginPods) {
      final pluginName = pluginHash['name'] as String?;
      final pluginPath = pluginHash['path'] as String?;
      final hasNativeBuild = (pluginHash['native_build'] as bool?) == true;
      if (pluginName != null && pluginPath != null && hasNativeBuild) {
        final iosDirPath = p.join(pluginPath, 'ios');
        // copy '$pluginName/ios' to 'build/ios/framework/plugins/$pluginName/ios'
        final licensePath = p.join(pluginPath, 'LICENSE');
        // copy License to 'build/ios/framework/plugins/$pluginName'
      }
    }

This logic is similar to flutter_install_plugin_pods

def flutter_install_plugin_pods(application_path = nil, relative_symlink_dir, platform)

  • Script also copies FlutterPluginRegistrant and .flutter-plugins-dependencies to the build/ios/frameworks/plugins.
    Note that I don't actually need to copy flutter-plugins-dependencies file, this is just for convenience, I'll explain below.

So this is how my build/ios directory looks like:

.
├── framework
│   ├── Debug
│   │   ├── App.xcframework
│   │   └── Flutter.podspec
│   ├── Release
│   │   ├── App.xcframework
│   │   └── Flutter.podspec
│   └── plugins
│       ├── FlutterPluginRegistrant
│       ├── device_info
│       │   ├── LICENSE
│       │   └── ios
│       │       ├── Classes
│       │       └── device_info.podspec
│       ├── flutter-plugins-dependencies
│       └── url_launcher_ios
│           ├── LICENSE
│           └── ios
│               ├── Classes
│               └── url_launcher_ios.podspec

How do you avoid changing your Podfile when you add new dependencies to FlutterModule

This is similar to Option A from the add-to-app doc.
I didn't like the fact that it requires developers to install flutter locally, because I didn't want my iOS team to deal with the installation of flutter. But there are some really useful ruby scripts in the Option A approach that I reused.

So I have the following requirement for the host apps:

FlutterModule will add iOS dependencies via Cocoapods. Add the following lines to your Podfile:

# import FlutterModule functions
require_relative 'path_to_flutter_module'

target 'YourApp' do
  # reference FlutterModule pods
  use_flutter_modules!

  post_install do |installer|
    # FlutterModule postinstall script
    flutter_post_install(installer)
  end
end

--
Here use_flutter_modules ruby function links plugins pods, Flutter, FlutterPluginRegistrant and App.xcframework.
Plugins are linked the similar way as flutter_install_plugin_pods from podhelper.rb does:

def custom_flutter_install_plugin_pods(relative_root_dir)
  plugins_dir = File.join(relative_root_dir, 'build', 'ios', 'framework', 'plugins')
  
  pod 'FlutterPluginRegistrant', :path => File.join(plugins_dir, 'FlutterPluginRegistrant'), :inhibit_warnings => true

  # Link Flutter plugins
  plugins_file = File.expand_path('flutter-plugins-dependencies', plugins_dir)
  plugin_pods = flutter_parse_dependencies_file_for_ios_plugin(plugins_file)
  plugin_pods.each do |plugin_hash|
    # todo: just link everything from `plugins_dir` instead of parsing 'flutter-plugins-dependencies' again
    plugin_name = plugin_hash['name']
    has_native_build = plugin_hash.fetch('native_build', true)
    if (plugin_name && has_native_build)
      plugin_path = File.join(plugins_dir, plugin_name, 'ios')
      pod plugin_name, :path => plugin_path, :inhibit_warnings => true
    end
  end
end

post install script is also similar to what we already have in podhelper.rb.

You can see in the script above that it parses flutter-plugins-dependencies again. But we can just add everything from plugins directory, so we don't need this (script will look different and I was too lazy to rewrite it)

--
As a result, when a new plugin is added to Flutter, my custom build function adds it to the build output. In the host application, the pod install command will call the ruby script and add the plugin as a dependency. So no additional changes are required for the host app.

@4rthurmonteiro
Copy link

Hi @p-mazhnik ... we are having the same problem with DataDog SDK for iOS. Do you have some update about your solution?

@p-mazhnik
Copy link
Contributor Author

p-mazhnik commented Jun 26, 2023

My current proposal is:

  1. Update flutter add-to-app documentation to indicate that Options B & Option C have limitation in case flutter plugin and native iOS app have a shared dependency.
  2. Introduce new flag --[no-]-plugins for the build ios-framework command that excludes plugins from build (_producePlugins function is not called).
  3. Introduce Option D that suggests developers to:
    • build frameworks using flutter build ios-framework --no-plugins
    • link & embed App.xcframework using option B
    • link & embed Flutter.xcframework using option B or C
    • link plugins to project by adding them to Podfile directly (e.g. by using install_flutter_plugin_pods, similar to Option A). We can also mention another advantage -- we don't need to determine whether plugin produce static or dynamic framework (like in option B when embed plugins frameworks manually)
      • If using install_flutter_plugin_pods we will run into a limitation:
        it requires Flutter and module dependencies to be installed locally. Workaround is to copy plugins somewhere and add them to project manually or using script similar to flutter_install_plugin_pods. I am not sure if we should provide such scripts to the developers.
      • we may also need a slightly different version of flutter_post_install script

@4rthurmonteiro
Copy link

4rthurmonteiro commented Jul 6, 2023

My current proposal is:

  1. Update flutter add-to-app documentation to indicate that Options B & Option C have limitation in case flutter plugin and native iOS app have a shared dependency.

  2. Introduce new flag --[no-]-plugins for the build ios-framework command that excludes plugins from build (_producePlugins function is not called).

  3. Introduce Option D that suggests developers to:

    • build frameworks using flutter build ios-framework --no-plugins

    • link & embed App.xcframework using option B

    • link & embed Flutter.xcframework using option B or C

    • link plugins to project by adding them to Podfile directly (e.g. by using install_flutter_plugin_pods, similar to Option A). We can also mention another advantage -- we don't need to determine whether plugin produce static or dynamic framework (like in option B when embed plugins frameworks manually)

      • If using install_flutter_plugin_pods we will run into a limitation:
        it requires Flutter and module dependencies to be installed locally. Workaround is to copy plugins somewhere and add them to project manually or using script similar to flutter_install_plugin_pods. I am not sure if we should provide such scripts to the developers.
      • we may also need a slightly different version of flutter_post_install script

@p-mazhnik i followed these steps.

In a real device worked fine. But in the iphone simulator e received the following errors:

[VERBOSE-2:engine.cc(210)] Engine run configuration was invalid.
[VERBOSE-2:shell.cc(614)] Could not launch engine with configuration.

So our Flutter Screens are all blanks.

Some tip?

@p-mazhnik
Copy link
Contributor Author

@4rthurmonteiro my guess will be that Release App.xcframework is used for the simulator, instead of the Debug version

@p-mazhnik
Copy link
Contributor Author

I've decided to create a separate issue for the root cause #130220, and left this one as a feature request, because I want to validate and discuss my proposal with the Flutter team and make it easier for developers to find the existing issue and workarounds.

@hyiso
Copy link
Contributor

hyiso commented Aug 19, 2023

We also need this.
Currently we have one Flutter module to be used in two iOS App with only a small difference in --dart-define passed to flutter build ios-framework,
We only need a new App.xcframework at the second build, so a flag like --no-plugins will decrease the build time a lot.

@stuartmorgan stuartmorgan added the P2 Important issues not at the top of the work list label Aug 28, 2023
auto-submit bot pushed a commit that referenced this issue Dec 4, 2023
…orks from the build (#129739)

A lot of details are written in the feature request: #114692.

tl;dr: Options B & C from the add2app iOS guide have a limitation (build error) in case the Flutter plugin and native iOS app have a shared dependency. We can use a workaround to avoid the issue, but in this case we don't need to build frameworks for plugins.

Closes #114692
Part of #130220
Copy link

This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Dec 18, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
a: existing-apps Integration with existing apps via the add-to-app flow c: new feature Nothing broken; request for a new capability c: proposal A detailed proposal for a change to Flutter P2 Important issues not at the top of the work list platform-ios iOS applications specifically team-ios Owned by iOS platform team tool Affects the "flutter" command-line tool. See also t: labels. triaged-ios Triaged by iOS platform team
Projects
None yet
6 participants