├── .github └── workflows │ ├── codecov.yml │ ├── swift-linux.yml │ └── swift-macos.yml ├── .gitignore ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── PluginManager │ ├── Documentation.docc │ │ └── Documentation.md │ ├── FilePathDirectoryView.swift │ ├── PluginManager.swift │ └── PluginManagerError.swift ├── TestPluginExample │ ├── PluginExample.swift │ └── PluginExampleLoader.swift ├── TestPluginExampleAPI │ ├── PluginExampleAPI.swift │ └── PluginExampleAPIFactory.swift ├── TestPluginExampleActor │ ├── PluginExampleActor.swift │ └── PluginExampleActorLoader.swift ├── TestPluginExampleActorAPI │ ├── PluginExampleActorAPI.swift │ └── PluginExampleActorAPIFactory.swift ├── TestPluginManagerExampleAPI │ └── PluginManagerExampleAPI.swift ├── TestPluginManagerExampleActorAPI │ └── PluginManagerExampleActorAPI.swift └── TestPluginManagerTestsInvalidPlugin │ └── PluginManagerTestsInvalidPlugin.swift └── Tests └── PluginManagerTests ├── PluginManagerExampleAPIProvider.swift ├── PluginManagerExampleActorAPIProvider.swift └── PluginManagerTests.swift /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | name: Code coverage 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build-test-and-upload-test-coverage: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Run tests 17 | run: swift test --enable-code-coverage 18 | - name: Set path 19 | run: echo "/usr/lib/llvm-9/bin" >> $GITHUB_PATH 20 | - name: Install LLVM and Clang 21 | uses: KyleMayes/install-llvm-action@v1 22 | with: 23 | version: "13" 24 | - name: Export code coverage 25 | run: llvm-cov export -format="lcov" .build/debug/swift-plugin-managerPackageTests.xctest -instr-profile .build/debug/codecov/default.profdata > info.lcov 26 | - name: Upload codecov 27 | uses: codecov/codecov-action@v2 28 | with: 29 | files: info.lcov 30 | -------------------------------------------------------------------------------- /.github/workflows/swift-linux.yml: -------------------------------------------------------------------------------- 1 | name: Linux 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Build 17 | run: swift build -v 18 | - name: Run tests 19 | run: swift test -v 20 | -------------------------------------------------------------------------------- /.github/workflows/swift-macos.yml: -------------------------------------------------------------------------------- 1 | name: macOS 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: macos-12 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Build 17 | run: swift build -v 18 | - name: Run tests 19 | run: swift test -v 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## SPM internals 6 | .swiftpm 7 | 8 | ## User settings 9 | xcuserdata/ 10 | 11 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 12 | *.xcscmblueprint 13 | *.xccheckout 14 | 15 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 16 | build/ 17 | DerivedData/ 18 | *.moved-aside 19 | *.pbxuser 20 | !default.pbxuser 21 | *.mode1v3 22 | !default.mode1v3 23 | *.mode2v3 24 | !default.mode2v3 25 | *.perspectivev3 26 | !default.perspectivev3 27 | 28 | ## Obj-C/Swift specific 29 | *.hmap 30 | 31 | ## App packaging 32 | *.ipa 33 | *.dSYM.zip 34 | *.dSYM 35 | 36 | ## Playgrounds 37 | timeline.xctimeline 38 | playground.xcworkspace 39 | 40 | # Swift Package Manager 41 | # 42 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 43 | # Packages/ 44 | # Package.pins 45 | # Package.resolved 46 | # *.xcodeproj 47 | # 48 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 49 | # hence it is not needed unless you have added a package configuration file to your project 50 | # .swiftpm 51 | 52 | .build/ 53 | 54 | # CocoaPods 55 | # 56 | # We recommend against adding the Pods directory to your .gitignore. However 57 | # you should judge for yourself, the pros and cons are mentioned at: 58 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 59 | # 60 | # Pods/ 61 | # 62 | # Add this line if you want to avoid checking in source code from the Xcode workspace 63 | # *.xcworkspace 64 | 65 | # Carthage 66 | # 67 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 68 | # Carthage/Checkouts 69 | 70 | Carthage/Build/ 71 | 72 | # Accio dependency management 73 | Dependencies/ 74 | .accio/ 75 | 76 | # fastlane 77 | # 78 | # It is recommended to not store the screenshots in the git repo. 79 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 80 | # For more information about the recommended setup visit: 81 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 82 | 83 | fastlane/report.xml 84 | fastlane/Preview.html 85 | fastlane/screenshots/**/*.png 86 | fastlane/test_output 87 | 88 | # Code Injection 89 | # 90 | # After new code Injection tools there's a generated folder /iOSInjectionProject 91 | # https://github.com/johnno1962/injectionforxcode 92 | 93 | iOSInjectionProject/ 94 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2021 Joakim Hassila 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "swift-plugin-manager", 8 | platforms: [ 9 | .macOS(.v12) 10 | ], 11 | products: [ 12 | // Products define the executables and libraries a package produces, and make them visible to other packages. 13 | .library( 14 | name: "PluginManager", 15 | targets: ["PluginManager"]), 16 | .library( 17 | name: "TestPluginExample", 18 | type: .dynamic, 19 | targets: ["TestPluginExample"]), 20 | .library( 21 | name: "TestPluginExampleActor", 22 | type: .dynamic, 23 | targets: ["TestPluginExampleActor"]), 24 | .library( 25 | name: "TestPluginManagerTestsInvalidPlugin", 26 | type: .dynamic, 27 | targets: ["TestPluginManagerTestsInvalidPlugin"]), 28 | ], 29 | dependencies: [ 30 | .package(url: "https://github.com/apple/swift-system", from: "1.0.0"), 31 | .package(url: "https://github.com/apple/swift-log", from: "1.0.0"), 32 | .package(url: "https://github.com/hassila/swift-plugin", from: "0.1.0"), 33 | // .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0") 34 | ], 35 | targets: [ 36 | .target( 37 | name: "PluginManager", 38 | dependencies: [ 39 | .product(name: "SystemPackage", package: "swift-system"), 40 | .product(name: "Logging", package: "swift-log"), 41 | .product(name: "Plugin", package: "swift-plugin"), 42 | ], 43 | swiftSettings: [ 44 | .unsafeFlags(["-Xfrontend", "-validate-tbd-against-ir=none"]) // due to https://bugs.swift.org/browse/SR-15629 45 | ] 46 | ), 47 | .target( 48 | name: "TestPluginExample", 49 | dependencies: ["TestPluginManagerExampleAPI", 50 | "TestPluginExampleAPI", 51 | .product(name: "Plugin", package: "swift-plugin"), 52 | ]), 53 | .target( 54 | name: "TestPluginExampleActor", 55 | dependencies: ["TestPluginManagerExampleActorAPI", 56 | "TestPluginExampleActorAPI", 57 | .product(name: "Plugin", package: "swift-plugin"), 58 | ]), 59 | .target( 60 | name: "TestPluginManagerTestsInvalidPlugin", 61 | dependencies: []), 62 | .target( 63 | name: "TestPluginManagerExampleAPI", 64 | dependencies: []), 65 | .target( 66 | name: "TestPluginManagerExampleActorAPI", 67 | dependencies: []), 68 | .target( 69 | name: "TestPluginExampleAPI", 70 | dependencies: [ 71 | .product(name: "Plugin", package: "swift-plugin"), 72 | "TestPluginManagerExampleAPI" 73 | ]), 74 | .target( 75 | name: "TestPluginExampleActorAPI", 76 | dependencies: [ 77 | .product(name: "Plugin", package: "swift-plugin"), 78 | "TestPluginManagerExampleActorAPI" 79 | ]), 80 | .testTarget( 81 | name: "PluginManagerTests", 82 | dependencies: ["PluginManager", 83 | "TestPluginExampleAPI", 84 | "TestPluginExampleActorAPI", 85 | "TestPluginManagerExampleAPI", 86 | "TestPluginManagerExampleActorAPI", 87 | ]), 88 | ] 89 | ) 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Swift version](https://img.shields.io/badge/Swift-5.5-orange?style=flat-square)](https://img.shields.io/badge/Swift-5.5-orange?style=flat-square) 2 | [![Ubuntu](https://github.com/hassila/swift-plugin-manager/actions/workflows/swift-linux.yml/badge.svg?branch=main)](https://github.com/hassila/swift-plugin-manager/actions/workflows/swift-linux.yml) 3 | [![macOS](https://github.com/hassila/swift-plugin-manager/actions/workflows/swift-macos.yml/badge.svg)](https://github.com/hassila/swift-plugin-manager/actions/workflows/swift-macos.yml) 4 | [![codecov](https://codecov.io/gh/hassila/swift-plugin-manager/branch/main/graph/badge.svg)](https://codecov.io/gh/hassila/swift-plugin-manager) 5 | 6 | # PluginManager 7 | 8 | Support for dynamic loading and management of plugins to extend hosting application functionality. 9 | 10 | ## Overview 11 | 12 | A multi-platform server-side Swift Infrastructure to support a plugin architecture that allows for dynamically extended functionality of hosting applications. 13 | 14 | The PluginManager is implemented as an actor type which takes a plugin type factory as a generic parameter allowing for multiple simultaneous PluginManagers each supporting a specific plugin type. 15 | 16 | A plugin type is defined in terms of the protocol it implements, a sample is available in [swift-plugin-example-api](https://github.com/hassila/swift-plugin-example-api). 17 | 18 | A concrete implementation of a Plugin of that plugin type (there can be several concrete implementations for a given type) is available as a sample in [swift-plugin-example](https://github.com/hassila/swift-plugin-example). 19 | 20 | A hosting application can provide an API that allows the plugin to access functionality from the hosting application, an example of such an API is defined as a protocol in [swift-plugin-manager-example-api](https://github.com/hassila/swift-plugin-manager-example-api). 21 | 22 | A given hosting application can load plugins and should implement the API that plugins can use, a sample hosting application is available as [swift-plugin-manager-example](https://github.com/hassila/swift-plugin-manager-example) which also can load the sample plugin. 23 | 24 | The fundamental plugin protocol is available as a plugin package dependency [swift-plugin](https://github.com/hassila/swift-plugin). 25 | 26 | ## Sample usage 27 | 28 | Load all plugins from a given directory, create the factory for each plugin type to create an instance and call a function that uses the host application API: 29 | ```swift 30 | let pluginManager = try await PluginManager(withPath: validPath) 31 | 32 | for (_, plugin) in await pluginManager.plugins { 33 | var myInstance = plugin.factory.create() 34 | myInstance.setPluginManagerExampleAPI(PluginManagerExampleAPIProvider()) 35 | print(myInstance.callFunctionThatUsesHostingApplicationAPI()) 36 | } 37 | ``` 38 | 39 | ## Supported Platforms 40 | 41 | PluginManager currently supports macOS and Linux with a Swift toolchain versin of at least 5.5 (as the PluginManager is implemented as an actor) 42 | 43 | PluginManager uses and supports swift-log. 44 | 45 | ## Getting Started 46 | 47 | You just need to do a few things to add plugin capabilities to your application: 48 | 1. Create an API protocol for the plugin (usually as a separate package, as multiple concrete plugins will depend on that) 49 | 2. Create a concrete plugin implementation that implements that API and that includes a trivial factory class to create instances 50 | 3. Add the Plugin Manager dependency to your hosting application and add code to load instances 51 | 4. (optionally) Add an API protocol for the hostsing application so the plugin can use specific features there. 52 | 53 | For point 1 and 2, see the sample projects linked above. 54 | 55 | For point 3, to add PluginManager as dependency in your own project to add plugin capabilities, it's as simple as adding a dependencies clause to your Package.swift: 56 | ``` 57 | dependencies: [ 58 | .package(url: "https://github.com/hassila/swift-plugin-manager.git") 59 | ] 60 | ``` 61 | 62 | and then add the dependency to your target: 63 | ``` 64 | .executableTarget( 65 | name: "PluginManagerExample", 66 | dependencies: [ 67 | .product(name: "PluginManager", package: "swift-plugin-manager") 68 | ]), 69 | ``` 70 | 71 | The easiest approach to learn is probably to play with the samples published above that are minimal in scope, so download and run the example: 72 | 73 | ``` 74 | mkdir plugin-test 75 | cd plugin-test 76 | git clone https://github.com/hassila/swift-plugin-manager-example 77 | git clone https://github.com/hassila/swift-plugin-example 78 | cd swift-plugin-example 79 | swift build 80 | cd ../swift-plugin-manager-example 81 | swift run 82 | ``` 83 | 84 | ## Runtime warnings 85 | A runtime warning will be issued when a plugin is loaded as the factory class will be implemented both in the hosting application and in the plugin that is loaded - the trivial transport class should be identical in both and the warning can be disregarded. 86 | 87 | ``` 88 | objc[21884]: Class _TtC16PluginExampleAPI23PluginExampleAPIFactory is implemented in both /Users/jocke/Library/Developer/Xcode/DerivedData/swift-plugin-manager-example-gpipkszbaeyszjgfyfslngejclgt/Build/Products/Debug/PluginManagerExample (0x100060e90) and /Users/jocke/GitHub/swift-plugin-example/.build/arm64-apple-macosx/debug/libPluginExample.dylib (0x1007cc108). One of the two will be used. Which one is undefined. 89 | ``` 90 | 91 | ## Related projects and usage notes 92 | 93 | Loading of plugins is fundamentally unsafe from a security perspective as it allows for arbitrary code execution and no sandboxing is performed. 94 | This makes use of this plugin infrastructur suitable for environments and use cases where the user/operator installing plugins have full control of what's loaded. 95 | 96 | This package was primarily put together with server-swide Swift usage in mind. Similar functionality is available in [Foundation in Bundle](https://developer.apple.com/documentation/foundation/bundle) with some caveats - it [seems to require using Objective-C bridging headers](https://blog.pendowski.com/plugin-architecture-in-swift-ish/) for a "pure Swift" version and for e.g. Linux [some functionality like principalClass isn't yet implemented](https://github.com/apple/swift-corelibs-foundation/blob/main/Docs/Status.md). That being said, if you are building something only on Apple platforms and can accept a dependency on Foundation, it is a very reasonable alternative solution to consider and has a lot of additional features like co-packaging of resources. 97 | 98 | This package does not depend on neither Foundation nor Objective-C facilities and works on both macOS and Linux. 99 | 100 | Documentation is supplied in docc format, easiest is to open the package in xcode and build documentation, alternatively run docc from command line. 101 | 102 | ### Future directions 103 | 104 | Add a proper github pipeline with autogenerating of html for GH pages in the future to make docs available without downloading package. 105 | 106 | Autogeneration of plugin and hosting API:s semvers when doinga a release using Swift Package Managers diagnose-api-breaking-changes (previously named experimental-api-diff) feature. Then we can expose the semver as another known entry point in the module and check that it is compatible during loading. 107 | 108 | Feedback and PR:s are welcome. 109 | 110 | -------------------------------------------------------------------------------- /Sources/PluginManager/Documentation.docc/Documentation.md: -------------------------------------------------------------------------------- 1 | # ``PluginManager`` 2 | 3 | Support for dynamic loading and management of plugins to extend hosting application functionality. 4 | 5 | ## Overview 6 | 7 | A multi-platform server-side Swift Infrastructure to support a plugin architecture that allows for dynamically extend functionality of hosting applications. 8 | 9 | #### Concepts 10 | 11 | Term | Description 12 | --- | --- 13 | Hosting application | an executable that dynamically loads plugins at runtime 14 | Plugin | a dynamic library that conforms to relevant protocols and conventions to support dynamic loading 15 | Plugin type | specifies which Plugin API protocol the plugin implements, a hosting application can support multiple types of plugins 16 | Plugin factory | a simple class that needs to be provided by a given plugin type and that can instantiate structs/classes/actors for the hosting applications use 17 | Plugin API | the protocol that a given plugin type must implement 18 | Plugin Manager API | to provide access to supporting functionality in the hosting application, the plugin is commonly provided one or more API surfaces to allow the plugin to use that functionality. 19 | 20 | The ``PluginManager`` is implemented as an actor type which takes a plugin type factory as a generic parameter allowing for multiple simultaneous PluginManagers each supporting a specific plugin type. 21 | 22 | A plugin type is defined in terms of the protocol it implements, a sample is available in [swift-plugin-example-api](https://github.com/hassila/swift-plugin-example-api). 23 | 24 | A concrete implementation of a Plugin of that plugin type (there can be several concrete implementations for a given type) is available as a sample in [swift-plugin-example](https://github.com/hassila/swift-plugin-example). 25 | 26 | A hosting application can provide an API that allows the plugin to access functionality from the hosting application, an example of such an API is defined as a protocol in [swift-plugin-manager-example-api](https://github.com/hassila/swift-plugin-manager-example-api). 27 | 28 | A given hosting application can load plugins and should implement the API that plugins can use, a sample hosting application is available as [swift-plugin-manager-example](https://github.com/hassila/swift-plugin-manager-example) which also can load the sample plugin. 29 | 30 | The fundamental plugin protocol is available as a plugin package dependency [swift-plugin](https://github.com/hassila/swift-plugin). 31 | 32 | ### Related projects and usage notes 33 | 34 | Loading of plugins is fundamentally unsafe by design as it allows for arbitrary code execution and no sandboxing is performed. This is only suitable for environments and use cases where the user/operator installing plugins have full control for obvious reasons. 35 | 36 | This package was primarily put together with server-swide Swift usage in mind. Similar functionality is available in [Foundation in Bundle](https://developer.apple.com/documentation/foundation/bundle) with some caveats - it [seems to require using Objective-C bridging headers](https://blog.pendowski.com/plugin-architecture-in-swift-ish/) for a "pure Swift" version and for e.g. Linux [some functionality like principalClass isn't yet implemented](https://github.com/apple/swift-corelibs-foundation/blob/main/Docs/Status.md). That being said, if you are building something only on Apple platforms it is a very reasonable alternative solution to consider and has a lot of additional features like co-packaging of resources. 37 | 38 | This package does not depend on neither Foundation nor Objective-C facilities and works on both macOS and Linux. 39 | 40 | ## Topics 41 | 42 | ### Group 43 | 44 | // - ``PluginManager`` 45 | -------------------------------------------------------------------------------- /Sources/PluginManager/FilePathDirectoryView.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the PluginManager open source project 4 | // 5 | // Copyright (c) 2021 Joakim Hassila and the PluginManager project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import SystemPackage 15 | 16 | #if canImport(Darwin) 17 | import Darwin 18 | typealias DirectoryStreamPointer = UnsafeMutablePointer? 19 | #elseif canImport(Glibc) 20 | import Glibc 21 | typealias DirectoryStreamPointer = OpaquePointer? 22 | #else 23 | #error("Unsupported Platform") 24 | #endif 25 | 26 | /// Extends FilePath with basic directory iteration capabilities 27 | extension FilePath { 28 | 29 | /// `DirectoryView` provides an iteratable sequence of the contents of a directory referenced by a `FilePath` 30 | public struct DirectoryView { 31 | internal var _directoryStreamPointer: DirectoryStreamPointer = nil 32 | internal var _path: FilePath 33 | 34 | /// Initializer 35 | /// - Parameter path: The file system path to provide directory entries for, should reference a directory 36 | internal init(_ path: FilePath) { 37 | self._path = path 38 | self._path.withPlatformString { 39 | _directoryStreamPointer = opendir($0) 40 | } 41 | } 42 | } 43 | 44 | public var directoryEntries: DirectoryView { 45 | get { DirectoryView(self) } 46 | } 47 | } 48 | 49 | extension FilePath.DirectoryView: IteratorProtocol, Sequence { 50 | 51 | mutating public func next() -> FilePath? { 52 | guard let directoryStreamPointer = self._directoryStreamPointer else { 53 | return nil 54 | } 55 | 56 | guard let directoryEntry = readdir(directoryStreamPointer) else 57 | { 58 | closedir(directoryStreamPointer) 59 | _directoryStreamPointer = nil 60 | return nil 61 | } 62 | 63 | let fileName = withUnsafePointer(to: &directoryEntry.pointee.d_name) { (pointer) -> FilePath.Component in 64 | pointer.withMemoryRebound(to: CChar.self, capacity: MemoryLayout.size(ofValue: directoryEntry.pointee.d_name)) { 65 | guard let fileName = FilePath.Component.init(platformString: $0) else { 66 | fatalError("Could not initialize FilePath.Component from platformString \(String(cString:$0))") 67 | } 68 | return fileName 69 | } 70 | } 71 | return self._path.appending(fileName) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Sources/PluginManager/PluginManager.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the PluginManager open source project 4 | // 5 | // Copyright (c) 2021 Joakim Hassila and the PluginManager project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import SystemPackage 15 | import Logging 16 | import Plugin 17 | 18 | #if canImport(Darwin) 19 | import Darwin 20 | private let defaultPluginExtension = "dylib" 21 | #elseif canImport(Glibc) 22 | import Glibc 23 | private let defaultPluginExtension = "so" 24 | #else 25 | #error("Unsupported Platform") 26 | #endif 27 | 28 | /// The coordinator that dynamically loads shared library plugins 29 | /// 30 | public actor PluginManager { 31 | 32 | /// A successfully loaded plugin instance using the plugin dynamic library path as the key. 33 | /// 34 | public struct Plugin { 35 | /// The path to the plugin dynamic library. 36 | /// 37 | public var path : FilePath 38 | /// An instance of the factory class for this ``PluginManager`` that can create plugin instances. 39 | /// 40 | ///```swift 41 | /// var pluginInstance = plugin.factory.create() 42 | /// print(pluginInstance.name()) 43 | ///``` 44 | /// 45 | public var factory : T 46 | fileprivate var _dlHandle : UnsafeMutableRawPointer? 47 | } 48 | 49 | /// The currently successfully loaded plugins which can be used to create instances. 50 | /// 51 | /// 52 | /// ```swift 53 | /// let pluginManager = try await PluginManager(withPath: validPath) 54 | /// let pluginCount = await pluginManager.plugins.count 55 | /// 56 | /// for (_, plugin) in await pluginManager.plugins { 57 | /// var pluginInstance = plugin.factory.create() 58 | /// pluginInstance.setPluginManagerExampleAPI(PluginManagerExampleAPIProvider()) 59 | /// print(pluginInstance.name()) 60 | /// } 61 | /// ``` 62 | /// 63 | public var plugins : [FilePath:Plugin] = [:] 64 | 65 | private var pluginDirectory : FilePath = "/tmp/plugins" 66 | private let logger = Logger(label: "PluginManager") 67 | private let pluginSuffix : String 68 | 69 | /// Initialize the ``PluginManager`` and dynamically load plugin instances. 70 | /// By default all plugin shared libraries will be loaded from the given path. 71 | /// 72 | /// - Parameters: 73 | /// - directoryPath: The filesystem directory path from where plugins will be loaded 74 | /// - pluginExtension: Specifies a custom plugin extension to use, otherwise use the platform default (.dylib/.so/.dll) 75 | /// - loadPlugins: Whether to load all the plugins from the specified directory as part of initialization, by default they will be loaded. 76 | public init(withPath directoryPath: FilePath, 77 | pluginExtension: String? = nil, 78 | loadPlugins: Bool = true) async throws { 79 | 80 | self.pluginDirectory = directoryPath 81 | 82 | if let pluginSuffix = pluginExtension { 83 | self.pluginSuffix = pluginSuffix 84 | } else { 85 | self.pluginSuffix = defaultPluginExtension 86 | } 87 | 88 | if loadPlugins { 89 | try self.loadPlugins() 90 | } 91 | } 92 | } 93 | 94 | extension PluginManager { 95 | typealias PluginFactoryFunctionPointer = @convention(c) () -> UnsafeMutableRawPointer 96 | 97 | private func loadPlugins() throws { 98 | 99 | logger.debug("Loading plugins from [\(self.pluginDirectory)] of type \(String.init(describing: T.self)).") 100 | for file in self.pluginDirectory.directoryEntries { 101 | if file.extension == self.pluginSuffix { 102 | do { 103 | try self.load (plugin: file) 104 | } catch PluginManagerError.missingPluginModuleEntrypoint(let path, let reason) { 105 | logger.debug("loadPlugin missing plugin module entry point for [\(path)], failed with reason [\(reason)]") 106 | } catch PluginManagerError.incompatibleFactoryClass(let path) { 107 | logger.debug("loadPlugin failed due to an incompatible factory class for [\(path)]") 108 | } 109 | } 110 | } 111 | logger.debug("Loaded \(self.plugins.count) plugins.") 112 | } 113 | 114 | fileprivate func _resolveFactoryFor(_ dlHandle: UnsafeMutableRawPointer?, _ symbol: String) -> T? { 115 | let pluginFactorySymbolReference = dlsym(dlHandle, symbol) 116 | 117 | guard pluginFactorySymbolReference != nil else { 118 | return nil 119 | } 120 | 121 | let pluginFactoryCreator: PluginFactoryFunctionPointer = unsafeBitCast(pluginFactorySymbolReference, to: PluginFactoryFunctionPointer.self) 122 | let pluginFactory = Unmanaged.fromOpaque(pluginFactoryCreator()).takeRetainedValue() as T 123 | 124 | return pluginFactory 125 | } 126 | 127 | /// Try to load a specific plugin from the given path 128 | /// - Parameter path: The full path to the specific plugin to load including suffix, e.g. `/usr/local/plugins/myplugin.dylib` 129 | /// - Throws: ``PluginManagerError`` 130 | /// 131 | /// ```swift 132 | /// let pluginManager = try await PluginManager(withPath: "", loadPlugins: false) 133 | /// 134 | /// try await pluginManager.load(plugin: "/usr/local/plugins/myplugin.dylib"). 135 | /// ``` 136 | public func load(plugin path: FilePath) throws { 137 | 138 | if plugins[path] != nil { 139 | logger.debug("loadPlugin called for \(path) which was already loaded") 140 | throw PluginManagerError.duplicatePlugin(path) 141 | } 142 | 143 | logger.debug("Loading plugin [\(path)]") 144 | 145 | try path.withPlatformString { 146 | // TODO: Check RTLD_ flags to use again 147 | guard let dlHandle = dlopen($0, RTLD_NOW|RTLD_LOCAL|RTLD_NODELETE) else { 148 | throw PluginManagerError.failedToLoadPlugin(path: path, errorMessage: String.init(cString: dlerror())) 149 | } 150 | 151 | guard let pluginFactory = _resolveFactoryFor(dlHandle, "_pluginFactory") else { 152 | throw PluginManagerError.missingPluginModuleEntrypoint(path: path, errorMessage: String.init(cString: dlerror())) 153 | } 154 | 155 | guard pluginFactory.compatible(withType: T.self) else { 156 | throw PluginManagerError.incompatibleFactoryClass(path) 157 | } 158 | 159 | plugins[path] = Plugin(path: path, factory: pluginFactory, _dlHandle: dlHandle) 160 | logger.debug("Loaded plugin [\(path)] factory [\(String.init(describing: pluginFactory.self))]") 161 | } 162 | } 163 | 164 | /// Reload the plugin executable image to allow for on-the-fly new versions. Old instances will continue to run the original code. 165 | /// - Parameter path: The full path to the specific plugin to reload including suffix, e.g. `/usr/local/plugins/myplugin.dylib` 166 | public func reload(plugin path: FilePath) throws { 167 | logger.debug("Reloading plugin \(path)") 168 | try self.unload(plugin: path) 169 | try self.load(plugin: path) 170 | } 171 | 172 | private func _unloadPlugin(_ plugin: Plugin) { 173 | logger.debug("Unloading plugin \(plugin.path)") 174 | 175 | guard let dlHandle = plugin._dlHandle else { 176 | logger.debug("_unloadPlugin called for plugin._dlHandle that was nil for [\(plugin.path)]") 177 | return 178 | } 179 | 180 | if dlclose(dlHandle) == -1 { 181 | logger.debug("dlclose failed with \(String.init(cString: dlerror()))") // We don't throw on a failed dlclose, but lets warn about it 182 | } 183 | } 184 | 185 | /// Unload the plugin executable image and remove it from the internal ``plugins`` dictionary. Existing instances will continue to run the original code loaded. 186 | /// - Parameter path: The full path to the specific plugin to unload including suffix, e.g. `/usr/local/plugins/myplugin.dylib` 187 | public func unload(plugin path: FilePath) throws { 188 | guard let plugin = plugins[path] else { 189 | logger.debug("unloadPlugin failed as \(path) was not a previously loaded plugin.") 190 | throw PluginManagerError.unknownPlugin(path) 191 | } 192 | self._unloadPlugin(plugin) 193 | plugins[path] = nil 194 | } 195 | 196 | /// Unload all plugin executable images and remove them from the internal ``plugins`` dictionary. Existing plugin instances will continue to run the original code loaded. 197 | public func unloadAll() { 198 | logger.debug("Unloading all plugins:") 199 | 200 | for (_, value) in plugins { 201 | self._unloadPlugin(value) 202 | } 203 | plugins.removeAll() 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /Sources/PluginManager/PluginManagerError.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the PluginManager open source project 4 | // 5 | // Copyright (c) 2021 Joakim Hassila and the PluginManager project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE for license information 9 | // 10 | // SPDX-License-Identifier: Apache-2.0 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | import SystemPackage 15 | 16 | /// Throwable errors from ``PluginManager`` operations 17 | public enum PluginManagerError : Error { 18 | /// The loading of the plugin shared library (.dylib or .so) at the given path failed. 19 | case failedToLoadPlugin(path: FilePath, errorMessage: String) 20 | /// The plugin is missing the required function entry point 21 | /// 22 | /// The plugin shared library must contain a `@_cdecl`'d function named `_pluginFactory` that returns the factory class to be used. 23 | /// 24 | ///```swift 25 | /// @_cdecl("_pluginFactory") 26 | /// public func _pluginFactory() -> UnsafeMutableRawPointer { 27 | /// return Unmanaged.passRetained(PluginExampleAPIFactory(PluginExample.self)).toOpaque() 28 | /// } 29 | ///``` 30 | case missingPluginModuleEntrypoint(path: FilePath, errorMessage: String) 31 | /// The factory class was of a different type than the generic factory type for this ``PluginManager`` instance, so loading failed. 32 | case incompatibleFactoryClass(FilePath) 33 | /// Attempt to explcitily load a plugin that already was loaded by the ``PluginManager`` 34 | case duplicatePlugin(FilePath) 35 | /// Attempt to unload or reload a plugin that was unknown to the ``PluginManager`` 36 | case unknownPlugin(FilePath) 37 | } 38 | -------------------------------------------------------------------------------- /Sources/TestPluginExample/PluginExample.swift: -------------------------------------------------------------------------------- 1 | import TestPluginExampleAPI 2 | import TestPluginManagerExampleAPI 3 | 4 | public struct PluginExample : PluginExampleAPI { 5 | var api : PluginManagerExampleAPI? = nil 6 | 7 | public init() { 8 | } 9 | } 10 | 11 | extension PluginExample { 12 | public mutating func setPluginManagerExampleAPI(_ pluginAPI: PluginManagerExampleAPI) 13 | { 14 | api = pluginAPI 15 | } 16 | 17 | public func name() -> String { 18 | // return "I was reloaded" 19 | if let a = api { 20 | return a.name() 21 | } 22 | return "failed to use api" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/TestPluginExample/PluginExampleLoader.swift: -------------------------------------------------------------------------------- 1 | import TestPluginExampleAPI 2 | 3 | @_cdecl("_pluginFactory") 4 | public func _pluginFactory() -> UnsafeMutableRawPointer { 5 | return Unmanaged.passRetained(PluginExampleAPIFactory(PluginExample.self)).toOpaque() 6 | } 7 | -------------------------------------------------------------------------------- /Sources/TestPluginExampleAPI/PluginExampleAPI.swift: -------------------------------------------------------------------------------- 1 | import TestPluginManagerExampleAPI 2 | 3 | public protocol PluginExampleAPI { 4 | init() 5 | func name() -> String 6 | mutating func setPluginManagerExampleAPI(_ pluginAPI: PluginManagerExampleAPI) 7 | } 8 | -------------------------------------------------------------------------------- /Sources/TestPluginExampleAPI/PluginExampleAPIFactory.swift: -------------------------------------------------------------------------------- 1 | import Plugin 2 | 3 | public final class PluginExampleAPIFactory : PluginFactory { // rename the class after the API 4 | public typealias FactoryType = PluginExampleAPI // update this to the specific API implemented 5 | 6 | fileprivate let _pluginType: FactoryType.Type 7 | 8 | public init(_ pluginType: FactoryType.Type) { 9 | self._pluginType = pluginType 10 | } 11 | 12 | public func create() -> FactoryType { 13 | return _pluginType.init() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/TestPluginExampleActor/PluginExampleActor.swift: -------------------------------------------------------------------------------- 1 | import TestPluginExampleActorAPI 2 | import TestPluginManagerExampleActorAPI 3 | 4 | public actor PluginExampleActor : PluginExampleActorAPI { 5 | var api : PluginManagerExampleActorAPI? = nil 6 | 7 | public init() { 8 | } 9 | 10 | public func setPluginManagerExampleActorAPI(_ pluginAPI: PluginManagerExampleActorAPI) 11 | { 12 | api = pluginAPI 13 | } 14 | } 15 | 16 | extension PluginExampleActor { 17 | public func name() -> String { 18 | // return "I was reloaded" 19 | if let a = api { 20 | return a.name() 21 | } 22 | return "failed to use api" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/TestPluginExampleActor/PluginExampleActorLoader.swift: -------------------------------------------------------------------------------- 1 | import TestPluginExampleActorAPI 2 | 3 | @_cdecl("_pluginFactory") 4 | public func _pluginFactory() -> UnsafeMutableRawPointer { 5 | return Unmanaged.passRetained(PluginExampleActorAPIFactory(PluginExampleActor.self)).toOpaque() 6 | } 7 | -------------------------------------------------------------------------------- /Sources/TestPluginExampleActorAPI/PluginExampleActorAPI.swift: -------------------------------------------------------------------------------- 1 | import Plugin 2 | import TestPluginManagerExampleActorAPI 3 | 4 | public protocol PluginExampleActorAPI : Actor { 5 | init() 6 | func name() -> String 7 | func setPluginManagerExampleActorAPI(_ pluginAPI: PluginManagerExampleActorAPI) 8 | } 9 | -------------------------------------------------------------------------------- /Sources/TestPluginExampleActorAPI/PluginExampleActorAPIFactory.swift: -------------------------------------------------------------------------------- 1 | import Plugin 2 | 3 | public final class PluginExampleActorAPIFactory : PluginFactory { // rename the class after the API 4 | public typealias FactoryType = PluginExampleActorAPI // update this to the specific API implemented 5 | 6 | fileprivate let _pluginType: FactoryType.Type 7 | 8 | public init(_ pluginType: FactoryType.Type) { 9 | self._pluginType = pluginType 10 | } 11 | 12 | public func create() -> FactoryType { 13 | return _pluginType.init() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/TestPluginManagerExampleAPI/PluginManagerExampleAPI.swift: -------------------------------------------------------------------------------- 1 | public protocol PluginManagerExampleAPI { 2 | func name() -> String 3 | } 4 | -------------------------------------------------------------------------------- /Sources/TestPluginManagerExampleActorAPI/PluginManagerExampleActorAPI.swift: -------------------------------------------------------------------------------- 1 | public protocol PluginManagerExampleActorAPI { 2 | func name() -> String 3 | } 4 | -------------------------------------------------------------------------------- /Sources/TestPluginManagerTestsInvalidPlugin/PluginManagerTestsInvalidPlugin.swift: -------------------------------------------------------------------------------- 1 | @_cdecl("_invalidPluginFactory") 2 | public func _invalidPluginFactory() { 3 | return 4 | } 5 | -------------------------------------------------------------------------------- /Tests/PluginManagerTests/PluginManagerExampleAPIProvider.swift: -------------------------------------------------------------------------------- 1 | import TestPluginManagerExampleAPI 2 | 3 | struct PluginManagerExampleAPIProvider : PluginManagerExampleAPI { 4 | public func name() -> String 5 | { 6 | return "Awesome PluginManagerExampleAPIProvider callback" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Tests/PluginManagerTests/PluginManagerExampleActorAPIProvider.swift: -------------------------------------------------------------------------------- 1 | import TestPluginManagerExampleActorAPI 2 | 3 | public actor PluginManagerExampleActorAPIProvider : PluginManagerExampleActorAPI { 4 | public nonisolated func name() -> String 5 | { 6 | return "Awesome PluginManagerExampleActorAPIProvider callback" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Tests/PluginManagerTests/PluginManagerTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import SystemPackage 3 | import TestPluginManagerExampleAPI 4 | import TestPluginExampleAPI 5 | import TestPluginExampleActorAPI 6 | 7 | @testable import PluginManager 8 | 9 | final class PluginManagerTests: XCTestCase { 10 | 11 | fileprivate var validPath : FilePath = "" 12 | fileprivate var invalidPath : FilePath = "" 13 | 14 | // find build directory for debug test 15 | private func _getPluginPath() -> FilePath { 16 | var path : FilePath = #file 17 | path.components.removeLast(3) 18 | path.components.append(".build") 19 | path.components.append("debug") 20 | return path 21 | } 22 | 23 | private func _getInvalidPluginPath() -> FilePath { 24 | return "/invalid/path/to/plugin" 25 | } 26 | 27 | override func setUp() { 28 | super.setUp() 29 | validPath = _getPluginPath() 30 | invalidPath = _getInvalidPluginPath() 31 | } 32 | 33 | func testThatLoadAndRunPluginReturnsCorrectValueForStruct() async throws { 34 | do { 35 | let pluginManager = try await PluginManager(withPath: validPath) 36 | let pluginCount = await pluginManager.plugins.count 37 | 38 | XCTAssertGreaterThan(pluginCount, 0) 39 | 40 | for (_, p) in await pluginManager.plugins { 41 | var x = p.factory.create() 42 | x.setPluginManagerExampleAPI(PluginManagerExampleAPIProvider()) 43 | let name = x.name() 44 | XCTAssertEqual(name, "Awesome PluginManagerExampleAPIProvider callback") 45 | } 46 | } 47 | } 48 | 49 | func testThatLoadAndRunPluginReturnsCorrectValueForActor() async throws { 50 | do { 51 | let pluginManager = try await PluginManager(withPath: validPath) 52 | let pluginCount = await pluginManager.plugins.count 53 | 54 | XCTAssertGreaterThan(pluginCount, 0) 55 | 56 | for (_, p) in await pluginManager.plugins { 57 | let x = p.factory.create() 58 | await x.setPluginManagerExampleActorAPI(PluginManagerExampleActorAPIProvider()) 59 | let name = await x.name() 60 | XCTAssertEqual(name, "Awesome PluginManagerExampleActorAPIProvider callback") 61 | } 62 | } 63 | } 64 | 65 | func testThatLoadAndUnloadResultsInEmptyPluginList() async throws { 66 | do { 67 | let pluginManager = try await PluginManager(withPath: validPath) 68 | 69 | var pluginCount = await pluginManager.plugins.count 70 | 71 | XCTAssertGreaterThan(pluginCount, 0) 72 | 73 | await pluginManager.unloadAll() 74 | 75 | pluginCount = await pluginManager.plugins.count 76 | 77 | XCTAssertEqual(pluginCount, 0) 78 | } 79 | } 80 | 81 | func testThatPluginLoadForInvalidPathThrows() async throws { 82 | let pluginManager = try await PluginManager(withPath: "", loadPlugins: false) 83 | let pluginCount = await pluginManager.plugins.count 84 | 85 | XCTAssertEqual(pluginCount, 0) 86 | do { 87 | try await pluginManager.load(plugin: invalidPath) 88 | } catch PluginManagerError.failedToLoadPlugin { 89 | return 90 | } 91 | XCTFail("PluginManager should have thrown a PluginManagerError.failedToLoadPlugin exception") 92 | } 93 | 94 | func testThatDuplicateLoadingOfPluginThrows() async throws { 95 | do { 96 | let pluginManager = try await PluginManager(withPath: validPath) 97 | let pluginCount = await pluginManager.plugins.count 98 | 99 | XCTAssertGreaterThan(pluginCount, 0) 100 | 101 | for (_, p) in await pluginManager.plugins { 102 | try await pluginManager.load(plugin: p.path) 103 | } 104 | } catch PluginManagerError.duplicatePlugin { 105 | return 106 | } 107 | XCTFail("PluginManager should have thrown a PluginManagerError.duplicatePlugin exception") 108 | } 109 | 110 | func testThatReloadOfUnknownPluginThrows() async throws { 111 | do { 112 | let pluginManager = try await PluginManager(withPath: "", loadPlugins: false) 113 | 114 | try await pluginManager.reload(plugin: invalidPath) 115 | 116 | } catch PluginManagerError.unknownPlugin { 117 | return 118 | } 119 | XCTFail("PluginManager should have thrown a PluginManagerError.unknownPlugin exception") 120 | } 121 | 122 | func testThatLoadOfPluginWithMissingEntrypointThrows() async throws { 123 | // Test for missing entry point in plugin module 124 | // This is actually tested but not propagated from loadPlugins, so need to sort out path for a manual load of 125 | // PluginManagerTestsInvalidPlugin.dylib 126 | } 127 | 128 | func testThatReloadOfPluginReturnsCorrectValue() async throws { 129 | // TODO: implement test for reload of module will give new updated value - need additional plugin which we copy over existing dylib 130 | // and check for return of new values 131 | } 132 | } 133 | --------------------------------------------------------------------------------