Repository: flutter
Issue: Allow Swift Package Manager to be used with Add to App
Labels: tool,P2,team-ios,triaged-ios

Build solution for add to app to be able to use Swift Package Manager for plugins.

## Design / Proposal

Unfortunately, SPM doesn't really have a way to convert a package into an xcframework like CocoaPods does. It seems at some point there may have been a [hacky way to do it](https://forums.swift.org/t/how-to-build-swift-package-as-xcframework/41414/28), but I was unable to get it to work.

As an alternative, we can create a `FlutterPluginRegistrant` Swift Package that then has dependencies on all the Swift Package plugins and CocoaPod xcframeworks. The tricky part, though, is adding the dependency of the Flutter framework. If a plugin does not have a direct dependency on the Flutter framework, it's not guaranteed to be processed before the plugin compiles, which can cause errors about Flutter header not being found.

In order to get around this issue, we could do one of the following:

### Option 1 - Use Build Phases
In https://github.com/flutter/flutter/pull/146256, we get around this issue by using a pre-build script that copies the Flutter framework into a spot (`BUILT_PRODUCTS_DIR`) Swift Package Manager automatically uses as a framework search path. 

We could do the same for add to app.
<details>
  <summary>prepare_framework.sh</summary>
  
```bash
simulator=false
if [[ $SDKROOT == *"iphone"* ]]; then
  if [[ $SDKROOT == *"simulator"* ]]; then
    simulator=true
  fi
else
    echo "No iOS" 1>&2
    exit -1
fi

directory="path/to/build/ios/framework/${CONFIGURATION}/FlutterFrameworks/Flutter.xcframework"
flutter_framework_directory=""
for file in "$directory"/*; do
  if [[ "$file" == *"ios-"* ]]; then
    is_simulator_directory=false
    if [[ "$file" == *"-simulator" ]]; then
        is_simulator_directory=true
    fi

    if $simulator; then
        if $is_simulator_directory; then
            flutter_framework_directory="$file"
        fi
    else
        if ! $is_simulator_directory; then
            flutter_framework_directory="$file"
        fi
    fi
  fi
done

mkdir -p "$BUILT_PRODUCTS_DIR"

rsync -av --delete --filter "- .DS_Store" "$flutter_framework_directory/Flutter.framework" "$BUILT_PRODUCTS_DIR"
```

</details>

It would require also adding a post compile/link/embed phase, that copies the Flutter.framework from the `$BUILT_PRODUCTS_DIR` to the `${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}` directory and then codesigning it with `$EXPANDED_CODE_SIGN_IDENTITY`.

<details>
  <summary>copy_and_codesign_framework.sh</summary>
  
```bash
xcode_frameworks_dir="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}"

mkdir -p -- $xcode_frameworks_dir

frameworkPath="${BUILT_PRODUCTS_DIR}/Flutter.framework"

# Thin Framework to only needed archs
lipo_info=$(lipo -info "${frameworkPath}/Flutter")

lipo "${frameworkPath}/Flutter" -verify_arch "${ARCHS}"
lipo_verification_result=$?
if [ $lipo_verification_result != 0 ]; then
    echo "Binary ${frameworkPath}/Flutter does not contain ${ARCHS}" 1>&2
    exit -1
fi

if [[ $lipo_info != "Non-fat file:"* ]]; then
    extract_command=(
        "lipo"
        "-output"
        "${frameworkPath}/Flutter"
    )
    for arch in "${ARCHS}"
        do
            extract_command+=("-extract")
            extract_command+=("${arch}")
        done
    extract_command+=("${frameworkPath}/Flutter")
    eval "${extract_command[*]}"
fi

# Copy Flutter framework from BUILT_PRODUCTS_DIR to TARGET_BUILD_DIR.
rsync -av --delete --filter "- .DS_Store" "${frameworkPath}" "${xcode_frameworks_dir}/"

# Sign the binaries we moved.
if [[ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" ]]; then
  codesign --force --verbose --sign "${EXPANDED_CODE_SIGN_IDENTITY}" -- "${xcode_frameworks_dir}/Flutter.framework/Flutter"
fi
```

</details>

This would required `ENABLE_USER_SCRIPT_SANDBOXING` to be set to NO in the project's settings.

### Option 2 - Parse and alter Package.swift for each plugin

My current plan is instead to inject a dependency at the bottom of each plugin's Package.swift like so:
```swift
package.dependencies += [
    .package(path: "/path/to/flutter_framework_swift_package")
]
let result = package.targets.filter({ $0.name == "plugin_name" })
// or maybe: 
// let result = package.targets.filter({ $0.type == PackageDescription.Target.TargetType.regular })
if let target = result.first {
    target.dependencies.append(
        .product(name: "Flutter", package: "FlutterFrameworkPackage")
    )
}
```
We also will need to ensure the supported platforms for the plugin are higher than or equal to that of the flutter framework ([otherwise Swift Package Manager may give an error](https://developer.apple.com/documentation/packagedescription/supportedplatform#:~:text=Swift%20Package%20Manager%20emits%20an%20error%20if%20a%20dependency%20isn%E2%80%99t%20compatible%20with%20the%20top%2Dlevel%20package%E2%80%99s%20deployment%20version.%20The%20deployment%20target%20of%20a%20package%E2%80%99s%20dependencies%20must%20be%20lower%20than%20or%20equal%20to%20the%20top%2Dlevel%20package%E2%80%99s%20deployment%20target%20version%20for%20a%20particular%20platform.)).

To do this we can use `swift package dump-package` to convert the Package.swift to JSON to check if the version needs to be updated and then inject some swift code at the bottom of the Package.swift to alter the supported platforms, for example:
```swift
if package.platforms != nil {
    package.platforms = package.platforms?.filter({ !String(describing: $0).contains("ios") })
    package.platforms?.append(.iOS("12.0"))
} else {
    package.platforms = [
        .iOS("12.0")
    ]
}
```