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

feat(spm): Support plugins as Swift packages #1515

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from

Conversation

dpogue
Copy link
Member

@dpogue dpogue commented Dec 17, 2024

Platforms affected

iOS

Motivation and Context

Ref GH-1089.

Swift package manager is the built-in way to handle 3rd party dependencies for Xcode and Swift projects, and Cocoapods is no longer in active development. We want to provide a way for plugins to express and manage their dependencies as Swift packages.

The best way to do that seems to be for the plugins themselves to (optionally) become Swift packages.

Description

When a plugin declares in its plugin.xml file that it supports <platform name="ios" package="swift"> and contains a Package.swift file in the plugin root, we will treat it as a Swift package and add it as a dependency of the app project (technically a dependency of a stub dependency, because that's easier than monkeying further with Xcodeproj files).

Why This is Complicated

Each Swift package is treated as its own self-contained library, with access only to the dependencies that it declares. This means that plugins cannot access CordovaLib headers unless the declare a dependency on CordovaLib in their own Package.swift file. That would end up tying them to a very specific version of Cordova and potentially resulting in conflicts due to multiple plugins requesting different versions that don't match the app's version.

It feels like the only workaround is for the plugin install process to edit the Package.swift of each plugin and adjust the CordovaLib dependency to point to the local app one, but then we need to be able to parse and modify arbitrary Package.swift files.

Even with the plan to rewrite, it's not clear (for our own Apache plugins) if we can point them at the master branch of cordova-ios and still pass a release vote, because they would be pointing to a dependency that has not had a release vote. This could result in needing to churn the version dependency in every plugin every time we release a new cordova-ios version (even if we don't need to publish those changes).

Approach Taken

Rather than editing Package.swift in the top-level plugins folder (which we hope one day to get rid of), we've added a packages directory to the platforms/ios folder and will copy plugins there if they are using Swift Package Manager. The added advantage here is that we don't have files in the platform project pointing to things in node_modules, so the project is more self-contained.

Since we now have a copy of the plugin inside the platform folder, we can edit its Package.swift to update any references to CordovaLib to point to the same module as the rest of the app so there are no issues with version mismatches.

Remaining Work

  • Add unit tests around the CordovaLib path fixup, for both linked and non-linked CordovaLib
  • Figure out what we should do about CocoaPods
    (By this, I mean, what we should do when a plugin wants to consume a library through CocoaPods for compatibility with older releases but wants to consume the same library through SwiftPM going forward)
  • Write some documentation for plugin authors about how to set up their plugins to use SwiftPM
  • Figure out how we want to (or are allowed to) reference CordovaLib for our own Apache plugins, if we want to move them to using SwiftPM at all

Testing

Tested locally against this branch of cordova-plugin-device: https://github.com/dpogue/cordova-plugin-device/tree/spm

Checklist

  • I've run the tests to see all new and existing tests pass
  • I added automated test coverage as appropriate for this change
  • If this Pull Request resolves an issue, I linked to the issue in the text above (and used the correct keyword to close issues using keywords)
  • I've updated the documentation if necessary

@dpogue dpogue added this to the 8.0.0 milestone Dec 17, 2024
@codecov-commenter
Copy link

codecov-commenter commented Dec 17, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 82.03%. Comparing base (5cf4d94) to head (5abb309).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1515      +/-   ##
==========================================
+ Coverage   81.41%   82.03%   +0.61%     
==========================================
  Files          16       17       +1     
  Lines        1862     1926      +64     
==========================================
+ Hits         1516     1580      +64     
  Misses        346      346              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@breautek
Copy link
Contributor

Even with the plan to rewrite, it's not clear (for our own Apache plugins) if we can point them at the master branch of cordova-ios and still pass a release vote, because they would be pointing to a dependency that has not had a release vote. This could result in needing to churn the version dependency in every plugin every time we release a new cordova-ios version (even if we don't need to publish those changes).

I would think it's not acceptable to point it to master branch as that would effectively be using the dev build. At minimum it would need to be using the rel/<version> tag. But we could potentially have a "Local" SPM package that is just simply shipped with the cordova-ios tarball. The app template could reference it locally via file path. Not 100% sure how that will work with plugins however.

Each Swift package is treated as its own self-contained library, with access only to the dependencies that it declares. This means that plugins cannot access CordovaLib headers unless the declare a dependency on CordovaLib in their own Package.swift file.

I think this is more of a side issue because our plugins are not proper modules. They are just loose source code that eventually makes their way part of the App project. And I assume SPM packages are importing these packages as their own build targets, which means they have their own build configurations (and thus needs a reference to the CordovaLib framework).

This in itself shouldn't be a blocker though, as long as CordovaLib framework is consumed as a dynamic library, instead of a static library. Plugins should be able to declare the dependency on CordovaLib and compile against it without providing the CordovaLib framework itself. The application project will be the one responsible of importing the CordovaLib framework, and making sure that the dynamic library is available during runtime (by packaging it with it's product). Because most plugins are not pre-compiled, if they are any incompatibilities, it should appear during build time. Plugin authors pre-building their plugins and distributing a prebuilt binary however would need to take extra care to not call on an API that might not exist without properly guarding so, as it would cause runtime crashes.

I think this is more of a side issue because our plugins are not proper modules.

Extending on this, a year or two ago I was experimenting with an alternate approach in authoring plugins, so that plugins can have their own native projects and can compile (and potentially run) independently of a Cordova project. Would be very useful to have for using native unit test features. This involved having plugins be in their own projects and having their own build targets, so each plugin would produce their own .framework which gets imported by the Cordova application. This mostly worked but I did run into runtime errors where the cordova framework failed to look up any symbol that came from a dynamic library, wihch the cordova framework does when processing native bridge calls here if my memory serves me right.

Just some thoughts. I've been experimenting with SPM with my own framework, and will be sharing what worked for me once I have something but it is a very different architecture than Cordova.

@dpogue
Copy link
Member Author

dpogue commented Dec 18, 2024

This in itself shouldn't be a blocker though, as long as CordovaLib framework is consumed as a dynamic library, instead of a static library. Plugins should be able to declare the dependency on CordovaLib and compile against it without providing the CordovaLib framework itself.

This actually works if CordovaLib is a static library for the app target, but not when it's consumed as a Swift package itself (which is how we're using it in 8.0.0)

I assume SPM packages are importing these packages as their own build targets, which means they have their own build configurations (and thus needs a reference to the CordovaLib framework)

This is correct, when a plugin is a Swift package, it becomes its own framework target that is linked to the app project, rather than copying plugin source files into the project directly.

@breautek
Copy link
Contributor

This actually works if CordovaLib is a static library for the app target, but not when it's consumed as a Swift package itself (which is how we're using it in 8.0.0)

I would have thought that this would create duplicate symbols in the app binary.

If we have 3 targets:

  1. App (Produces Executable)
  2. CordovaLib (Produces Static Library)
  3. Plugin (Produces Dynamic Library)

CordovaLib itself is standalone.
Plugin would link against CordovaLib and produces an executable object (the shared library). If CordovaLib is static, it's symbols should be embedded into this shared library.
App would link against CordovaLib & Plugin and here during linking I would expect duplicate symbol issues, because CordovaLib symbols will be embedded into both Plugin binary, and the App executable binary.

This configuration would work, but only if you have no plugins, as a plugin that is linking with a static CordovaLib would cause duplicate symbols assuming that the app is also linking against the same static library. Doesn't this happen?

@dpogue
Copy link
Member Author

dpogue commented Dec 18, 2024

In the case where it worked with the static CordovaLib, the plugin did not specify a dependency on CordovaLib as part of its Package.swift file

When using Package.swift, even if the app asks to consume CordovaLib at the top level as a static library, plugins cannot find Cordova headers unless they also include a dependency on CordovaLib

@dpogue dpogue force-pushed the spm-plugins branch 3 times, most recently from 328dd50 to b92ceaa Compare January 18, 2025 07:09
This helps keep the top level project directory a bit cleaner by hiding
our CordovaPlugins SPM accumulator package inside the packages
directory.
@dpogue
Copy link
Member Author

dpogue commented Jan 19, 2025

Okay, I've updated this into something that I hope might be workable. There are essentially 2 main changes:

  1. All plugins are copied into a packages directory within the platforms/ios folder. The primary reason for this is so that the platform folder remains self-contained and can be zipped up and moved to another machine without references to things outside the platform folder itself.

  2. Any package dependency on cordova-ios within a plugin will (hopefully) be rewritten when the plugin is copied, to that it points to the local path to CordovaLib (which is also now copied into the packages folder). This ensures that every plugin and the app itself are using the same version of CordovaLib.

This seems to work in my (fairly basic) testing with the cordova-plugin-device branch linked in the PR description.

TODO

  • Manage the path to cordova-ios when it is using --link (because we don't copy it to packages in that case)

@dpogue dpogue changed the title [Broken] feat(spm): Support plugins as Swift packages feat(spm): Support plugins as Swift packages Jan 19, 2025
@erisu
Copy link
Member

erisu commented Jan 19, 2025

The primary reason for this is so that the platform folder remains self-contained and can be zipped up and moved to another machine without references to things outside the platform folder itself.

I want to add a note that this might be partially correct. There might be a case where the zipped up content is not complete and can not be built on another machine.

If the host machine preparing the platforms is either Windows or Linux and the plugin contains pods, the process might not install and prepare the plugin correctly. I believe during plugin add, when prepare is triggered, the pod information is added to the Podfile and then pod install is executed. Since neither Linux nor Windows has the pod tooling, this step would fail.

Additionally, on a successful pod install, the next step would be to update the swift version information on a file in Pods/Pods.xcodeproj. I believe the entire Pods directory is created by pod install. If we suggested to the app developers to run it manually, they would probably need to figure out and set the Swift version as well.

If the host machine is macOS then this should not be a problem.
If the app does not use any plugins with pods, then this should not be an issue as well.

This would need testing for confirmation. If my assumptions are correct, we should document this behavior so it would be made aware.

@msmtamburro
Copy link
Contributor

This is great work. On the topic of plugin consumption, considering the "tale of two audiences:" I would imagine that native devs with "platform centered approach" projects would prefer to include the plugins as swift packages directly. This begs the question of how the other bits in that native project (e.g., the post-processed config.xml) would get updated, other than manually.

@dpogue
Copy link
Member Author

dpogue commented Jan 31, 2025

I would imagine that native devs with "platform centered approach" projects would prefer to include the plugins as swift packages directly.

The complication here (if managing plugins manually in a project's Package.swift) becomes making sure that all those plugins refer to the same version of cordova-ios for their CordovaLib dependency. That's why we're needing to rewrite the Package.swift when we install them in this PR.

SwiftPM doesn't have particularly good support for overriding library versions from sub-dependencies or a concept like npm's peerDependencies 😞

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants