Remove use of modulemaps in flutter/packages when using SwiftPM
vashworth opened this issue · 3 comments
Use case
To support both CocoaPods and Swift Package Manager, dynamic and static linking, two different modulemaps are needed.
CocoaPods modulemap
CocoaPods seems to require the modulemap be a framework
module and doesn't use relative paths.
For example,
framework module plugin_name_ios {
umbrella header "plugin_name_ios-umbrella.h"
export *
module * { export * }
explicit module Test {
header "PublicHeader_Test.h"
header "OtherPublicHeader_Test.h"
}
}
SwiftPM modulemap
SwiftPM requires the modulemap to be named module.modulemap
and be located within the include
directory. It also doesn't seem to work if defined as a framework
module and uses relative paths.
For example,
- framework module plugin_name_ios {
+ module plugin_name_ios {
explicit module Test {
- header "PublicHeader_Test.h"
+ header "plugin_name_ios/OtherPublicHeader_Test.h"
The problem
The problem is that when you have two modulemaps (most likely also because one is named module.modulemap
, which is the implicit modulemap file), this can cause issues:
Proposal
It's been pretty difficult to figure out a way to support both CocoaPods and SwiftPM modulemaps. As a result, we're leaning towards eliminating modulemaps in SwiftPM.
Instead of using a modulemap for the SwiftPM integration of plugins, we create a new target in the Package.swift that must be imported directly into the app (for the RunnerTests target).
Using camera_avfoundation plugin as an example:
Package.swift
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.
// Copyright 2013 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.
import PackageDescription
let package = Package(
name: "camera_avfoundation",
platforms: [
.iOS("12.0")
],
products: [
.library(name: "camera-avfoundation", targets: ["camera_avfoundation"]),
.library(name: "camera-avfoundation-test", targets: ["camera_avfoundation_test"])
],
dependencies: [],
targets: [
.target(
name: "camera_avfoundation",
dependencies: [],
exclude: ["CameraPlugin.modulemap", "camera_avfoundation-umbrella.h"],
resources: [
.process("PrivacyInfo.xcprivacy")
],
cSettings: [
.headerSearchPath("include/camera_avfoundation")
]
),
.target(
name: "camera_avfoundation_test",
dependencies: ["camera_avfoundation"],
cSettings: [
.headerSearchPath("include/camera_avfoundation_test")
]
)
]
)
Then create the following files:
camera_avfoundation/Sources/camera_avfoundation_test/include/camera_avfoundation_test/CameraAVFoundation_Test.h
#import <camera_avfoundation/CameraPlugin_Test.h>
#import <camera_avfoundation/CameraPermissionUtils.h>
#import <camera_avfoundation/CameraProperties.h>
#import <camera_avfoundation/FLTCam.h>
#import <camera_avfoundation/FLTCam_Test.h>
#import <camera_avfoundation/FLTSavePhotoDelegate_Test.h>
#import <camera_avfoundation/FLTThreadSafeEventChannel.h>
#import <camera_avfoundation/QueueUtils.h>
These are all the headers that would have been declared in the camera_avfoundation.Test
submodule.
camera_avfoundation/Sources/camera_avfoundation_test/CameraAVFoundation_Test.m
#import "CameraAVFoundation_Test.h"
camera_avfoundation_test
will need to added as a dependency to RunnerTests via Xcode
Then in test files, replace camera_avfoundation.Test
with camera_avfoundation_test
- @import camera_avfoundation.Test;
+ @import camera_avfoundation_test;
This eliminates the modulemap. It unfortunately does not resolve having more headers public than we'd like, but they are already public so it's not a regression.
Prototype found here: flutter/packages@main...vashworth:packages:camera_avfoundation_remove_swiftpm_modulemap
@loic-sharma and I like this approach and are planning to move forward with it. If users want to use modulemaps with SwiftPM, we plan to point them to SwiftPM's documentation about it: https://github.com/apple/swift-package-manager/blob/main/Documentation/Usage.md#creating-c-language-targets
If users want to use modulemaps with SwiftPM, we plan to point them to SwiftPM's documentation about it: https://github.com/apple/swift-package-manager/blob/main/Documentation/Usage.md#creating-c-language-targets
I created an experiment that allows for custom module maps with SwiftPM: flutter/packages@main...loic-sharma:flutter-packages:spm_camera_separate_public_file_symlinks
This works by creating separate header directories for CocoaPods (/cocoapods_headers
) and Swift Package Manager (/include
). To minimize duplication, the CocoaPods header files are symlinks back to the header files in the Swift Package Manager header directory. I verified this works on Xcode 14.2+. Some drawbacks to this approach:
- Pigeon generates implementation files that use relative
#import "..."
paths. This is incompatible with this experiment as the relative path to the header files changes depending on whether you're using CocoaPods or Swift Package Manager. Instead, the Pigeon implementation file should use non-relative imports like#import <my_plugin/MyHeader.h>
. Today, this requires manually editing the code generated by Pigeon. - You need to remember to update symlinks if you add/move/delete header files. If we were to use this approach in the
flutter/packages
repo, we'd likely want tooling/tests to verify CocoaPods/SwiftPM header files are in sync.
Due to these drawbacks we prefer to avoid module maps in SwiftPM if possible.
In the tests:
@import camera_avfoundation;
@import camera_avfoundation_test;
If we were only in SwiftPM world, could we normally #import
the test headers directly? Or was there some problem with the hmap?