Skip to content

[SwiftPM] ☂ Allow Swift Package Manager to be used with Add to App #146957

@vashworth

Description

@vashworth

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, 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 #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.

prepare_framework.sh
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"

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.

copy_and_codesign_framework.sh
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

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:

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).

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:

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")
    ]
}

Sub-issues

Metadata

Metadata

Assignees

Labels

P2Important issues not at the top of the work lista: existing-appsIntegration with existing apps via the add-to-app flowteam-iosOwned by iOS platform teamtoolAffects the "flutter" command-line tool. See also t: labels.triaged-iosTriaged by iOS platform team

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions