├── .gitattributes ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── api └── multiplatform-swiftpackage.api ├── build.gradle.kts ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── src ├── main ├── kotlin │ └── com │ │ └── chromaticnoise │ │ └── multiplatformswiftpackage │ │ ├── MultiplatformSwiftPackagePlugin.kt │ │ ├── SwiftPackageExtension.kt │ │ ├── domain │ │ ├── AppleFramework.kt │ │ ├── AppleTarget.kt │ │ ├── BuildConfiguration.kt │ │ ├── DistributionMode.kt │ │ ├── DistributionURL.kt │ │ ├── Either.kt │ │ ├── OutputDirectory.kt │ │ ├── PackageName.kt │ │ ├── PlatformVersion.kt │ │ ├── PluginConfiguration.kt │ │ ├── Project+PluginConfiguration.kt │ │ ├── SwiftPackageConfiguration.kt │ │ ├── SwiftToolVersion.kt │ │ ├── TargetName.kt │ │ ├── TargetPlatform.kt │ │ ├── ZipFileName.kt │ │ ├── extensions.kt │ │ └── functions.kt │ │ ├── dsl │ │ ├── BuildConfigurationDSL.kt │ │ ├── DistributionModeDSL.kt │ │ └── TargetPlatformDsl.kt │ │ └── task │ │ ├── CreateSwiftPackageTask.kt │ │ ├── CreateXCFrameworkTask.kt │ │ ├── CreateZipFileTask.kt │ │ └── zip-functions.kt └── resources │ ├── META-INF │ └── gradle-plugins │ │ └── com.chromaticnoise.multiplatform-swiftpackage.properties │ └── templates │ └── Package.swift.template └── test └── kotlin └── com └── chromaticnoise └── multiplatformswiftpackage ├── domain ├── DistributionURLTest.kt ├── PackageNameTest.kt ├── PlatformVersionTest.kt ├── PluginConfigurationTest.kt ├── SwiftPackageConfigurationTest.kt ├── SwiftToolVersionTest.kt └── ZipFileNameTest.kt └── dsl └── TargetPlatformDslTest.kt /.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # These are explicitly windows files and should use crlf 5 | *.bat text eol=crlf 6 | 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/gradle,intellij+all,macos,windows,linux,kotlin 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=gradle,intellij+all,macos,windows,linux,kotlin 4 | 5 | ### Intellij+all ### 6 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 7 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 8 | 9 | # User-specific stuff 10 | .idea/**/workspace.xml 11 | .idea/**/tasks.xml 12 | .idea/**/usage.statistics.xml 13 | .idea/**/dictionaries 14 | .idea/**/shelf 15 | 16 | # Generated files 17 | .idea/**/contentModel.xml 18 | 19 | # Sensitive or high-churn files 20 | .idea/**/dataSources/ 21 | .idea/**/dataSources.ids 22 | .idea/**/dataSources.local.xml 23 | .idea/**/sqlDataSources.xml 24 | .idea/**/dynamic.xml 25 | .idea/**/uiDesigner.xml 26 | .idea/**/dbnavigator.xml 27 | 28 | # Gradle 29 | .idea/**/gradle.xml 30 | .idea/**/libraries 31 | 32 | # Gradle and Maven with auto-import 33 | # When using Gradle or Maven with auto-import, you should exclude module files, 34 | # since they will be recreated, and may cause churn. Uncomment if using 35 | # auto-import. 36 | # .idea/artifacts 37 | # .idea/compiler.xml 38 | # .idea/jarRepositories.xml 39 | # .idea/modules.xml 40 | # .idea/*.iml 41 | # .idea/modules 42 | # *.iml 43 | # *.ipr 44 | 45 | # CMake 46 | cmake-build-*/ 47 | 48 | # Mongo Explorer plugin 49 | .idea/**/mongoSettings.xml 50 | 51 | # File-based project format 52 | *.iws 53 | 54 | # IntelliJ 55 | out/ 56 | 57 | # mpeltonen/sbt-idea plugin 58 | .idea_modules/ 59 | 60 | # JIRA plugin 61 | atlassian-ide-plugin.xml 62 | 63 | # Cursive Clojure plugin 64 | .idea/replstate.xml 65 | 66 | # Crashlytics plugin (for Android Studio and IntelliJ) 67 | com_crashlytics_export_strings.xml 68 | crashlytics.properties 69 | crashlytics-build.properties 70 | fabric.properties 71 | 72 | # Editor-based Rest Client 73 | .idea/httpRequests 74 | 75 | # Android studio 3.1+ serialized cache file 76 | .idea/caches/build_file_checksums.ser 77 | 78 | ### Intellij+all Patch ### 79 | # Ignores the whole .idea folder and all .iml files 80 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 81 | 82 | .idea/ 83 | 84 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 85 | 86 | *.iml 87 | modules.xml 88 | .idea/misc.xml 89 | *.ipr 90 | 91 | # Sonarlint plugin 92 | .idea/sonarlint 93 | 94 | ### Kotlin ### 95 | # Compiled class file 96 | *.class 97 | 98 | # Log file 99 | *.log 100 | 101 | # BlueJ files 102 | *.ctxt 103 | 104 | # Mobile Tools for Java (J2ME) 105 | .mtj.tmp/ 106 | 107 | # Package Files # 108 | *.jar 109 | *.war 110 | *.nar 111 | *.ear 112 | *.zip 113 | *.tar.gz 114 | *.rar 115 | 116 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 117 | hs_err_pid* 118 | 119 | ### Linux ### 120 | *~ 121 | 122 | # temporary files which can be created if a process still has a handle open of a deleted file 123 | .fuse_hidden* 124 | 125 | # KDE directory preferences 126 | .directory 127 | 128 | # Linux trash folder which might appear on any partition or disk 129 | .Trash-* 130 | 131 | # .nfs files are created when an open file is removed but is still being accessed 132 | .nfs* 133 | 134 | ### macOS ### 135 | # General 136 | .DS_Store 137 | .AppleDouble 138 | .LSOverride 139 | 140 | # Icon must end with two \r 141 | Icon 142 | 143 | # Thumbnails 144 | ._* 145 | 146 | # Files that might appear in the root of a volume 147 | .DocumentRevisions-V100 148 | .fseventsd 149 | .Spotlight-V100 150 | .TemporaryItems 151 | .Trashes 152 | .VolumeIcon.icns 153 | .com.apple.timemachine.donotpresent 154 | 155 | # Directories potentially created on remote AFP share 156 | .AppleDB 157 | .AppleDesktop 158 | Network Trash Folder 159 | Temporary Items 160 | .apdisk 161 | 162 | ### Windows ### 163 | # Windows thumbnail cache files 164 | Thumbs.db 165 | Thumbs.db:encryptable 166 | ehthumbs.db 167 | ehthumbs_vista.db 168 | 169 | # Dump file 170 | *.stackdump 171 | 172 | # Folder config file 173 | [Dd]esktop.ini 174 | 175 | # Recycle Bin used on file shares 176 | $RECYCLE.BIN/ 177 | 178 | # Windows Installer files 179 | *.cab 180 | *.msi 181 | *.msix 182 | *.msm 183 | *.msp 184 | 185 | # Windows shortcuts 186 | *.lnk 187 | 188 | ### Gradle ### 189 | .gradle 190 | build/ 191 | 192 | # Ignore Gradle GUI config 193 | gradle-app.setting 194 | 195 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 196 | !gradle-wrapper.jar 197 | 198 | # Cache of project 199 | .gradletasknamecache 200 | 201 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 202 | # gradle/wrapper/gradle-wrapper.properties 203 | 204 | ### Gradle Patch ### 205 | **/build/ 206 | 207 | # End of https://www.toptal.com/developers/gitignore/api/gradle,intellij+all,macos,windows,linux,kotlin 208 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | install: skip 3 | 4 | os: linux 5 | dist: xenial 6 | jdk: openjdk8 7 | 8 | jobs: 9 | include: 10 | - stage: test 11 | script: ./gradlew check --info --stacktrace --console=plain --max-workers=1 --no-daemon --build-cache --scan -s -Dkotlin.colors.enabled=false 12 | 13 | before_cache: 14 | - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock 15 | - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ 16 | 17 | cache: 18 | directories: 19 | - $HOME/.gradle/caches/ 20 | - $HOME/.gradle/wrapper/ 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | tbd 10 | 11 | ## [2.0.3] 12 | ### Added 13 | - The plugin DSL is now compatible with Groovy 14 | [#17](https://github.com/ge-org/multiplatform-swiftpackage/issues/17) 15 | - The name of the generated ZIP file can now be customized 16 | [#22](https://github.com/ge-org/multiplatform-swiftpackage/issues/22) 17 | 18 | ## [2.0.2] 19 | ### Fixed 20 | - Fix a bug where the plugin was not able to detect the frameworks for some project configurations 21 | [#8](https://github.com/ge-org/multiplatform-swiftpackage/issues/8) 22 | 23 | ## [2.0.1] 24 | ### Changed 25 | - The class `MultiplatformSwiftPackagePlugin` is now public [#7](https://github.com/ge-org/multiplatform-swiftpackage/pull/7). 26 | 27 | Thanks [@netroy](https://github.com/netroy) 28 | 29 | ## [2.0.0] 30 | ### Added 31 | - You can now declare the name of the Swift package [#5](https://github.com/ge-org/multiplatform-swiftpackage/pull/5). 32 | Thanks for the collaboration [@JUSTINMKAUFMAN](https://github.com/JUSTINMKAUFMAN) 33 | ```kotlin 34 | packageName("MyAwesomeKit") 35 | ``` 36 | 37 | ### Changed 38 | - BREAKING: By default the name of the Swift package is now the base name of the framework instead of the name of the project. 39 | 40 | ## [1.0.2] 41 | ### Fixed 42 | - Fix a bug where target names were not always resolved correctly 43 | [#1](https://github.com/ge-org/multiplatform-swiftpackage/issues/1) 44 | 45 | - Fix a bug where the creation of the XCFramework failed if dSYM files did not exist 46 | 47 | ## [1.0.1] 48 | ### Added 49 | - Add all architectures of a platform at once 50 | ```kotlin 51 | targetPlatforms { 52 | iOS { v("13") } 53 | macOS { v("10.0") } 54 | } 55 | ``` 56 | - Better error messages if the plugin is not configured correctly 57 | - Better validation of plugin configuration 58 | 59 | ## [1.0.0] - 2020-10-12 60 | The first release :partying_face: 61 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /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 {yyyy} {name of copyright owner} 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Multiplatform Swift Package 2 | 3 | [![Build Status](https://travis-ci.com/ge-org/multiplatform-swiftpackage.svg?branch=master)](https://travis-ci.com/ge-org/multiplatform-swiftpackage) 4 | 5 | This is a Gradle plugin for Kotlin Multiplatform projects that generates an XCFramework for your native Apple targets 6 | and creates a matching Package.swift file to distribute it as a binary target. 7 | 8 | To distribute the framework a ZIP file containing it will be created and referenced from the Package. 9 | You can upload the ZIP file to your package registry so that SPM can download it. 10 | 11 | ## Prerequisites 12 | * XCode version 12.0+ 13 | * Gradle version 6.0+ 14 | 15 | ## Installing 16 | The plugin is published on Maven central. Add it to the `plugins` block in the Gradle build file. 17 | 18 | ```kotlin 19 | plugins { 20 | id("com.chromaticnoise.multiplatform-swiftpackage") version "2.0.3" 21 | } 22 | ``` 23 | 24 | ## Execution 25 | The plugin adds two Gradle tasks to your project. 26 | 27 | * ```./gradlew createSwiftPackage``` 28 | 29 | Creates the XCFramework, ZIP file and Package.swift 30 | 31 | 32 | * ```./gradlew createXCFramework``` 33 | 34 | Creates only the XCFramework 35 | 36 | ## Configuration 37 | Generation of the Package and XCFramework can be configured using the plugin's DSL. 38 | Add the configuration to the Gradle build file. 39 | 40 | This is a complete example of the required configuration. All available options will be explained in the following sections. 41 | 42 | ```kotlin 43 | multiplatformSwiftPackage { 44 | swiftToolsVersion("5.3") 45 | targetPlatforms { 46 | iOS { v("13") } 47 | } 48 | } 49 | ``` 50 | 51 | ### Package Name 52 | By default, the name of the Swift package will be the base name of the first framework found in the project. 53 | However, you can declare a different name for the package. 54 | This might be useful if your frameworks have different base names, and you want your package to have a common name. 55 | 56 | ```kotlin 57 | packageName("MyAwesomeKit") 58 | ``` 59 | 60 | Hint: 61 | If the cocoapods plugin is applied the name of the package will default to the value assigned to the `frameworkName` property. 62 | Otherwise, the value of the `baseName` property of the framework configuration will be used. 63 | 64 | ### Output Directory 65 | By default, the plugin will write all files into the _swiftpackage_ folder in the project directory. 66 | You can configure the output folder by providing a File object pointing to it. 67 | 68 | ```kotlin 69 | outputDirectory(File(projectDir, "swiftpackage")) 70 | ``` 71 | 72 | ### Swift Tools Version 73 | The first line of every Package.swift file is a header declaring the Swift tools version required by the package. 74 | To set the version use the following configuration and provide the version your XCode project supports. 75 | 76 | ```kotlin 77 | swiftToolsVersion("5.3") 78 | ``` 79 | 80 | ### Distribution Mode 81 | Swift packages can distribute binary targets either via the local file system or via a remote ZIP file. 82 | Depending on your requirements (e.g. local development or CI) use one of the following configurations. 83 | 84 | #### Local distribution 85 | ```kotlin 86 | distributionMode { local() } 87 | ``` 88 | 89 | #### Remote distribution 90 | Provide a URL where the ZIP file containing the XCFramework is located. 91 | This should point to the root directory and not to the ZIP file itself. 92 | ```kotlin 93 | // correct 94 | distributionMode { remote("https://example.com") } 95 | 96 | // wrong 97 | distributionMode { remote("https://example.com/MyLib.zip") } 98 | ``` 99 | 100 | ### ZIP File Name 101 | By default, the name of the generated ZIP file consists of the package name concatenated with the project's version. 102 | You can configure the name by setting a custom value. 103 | The `.zip` file extension will be added during the build and should be omitted here. 104 | ```kotlin 105 | zipFileName("MyAwesomeKit") 106 | ``` 107 | 108 | ### Build Configuration 109 | Apple frameworks can be built with different configurations. By default, these are _release_ and _debug_. 110 | However, you can also create your own configurations. 111 | 112 | #### Default 113 | ```kotlin 114 | buildConfiguration { release() } 115 | // or 116 | buildConfiguration { debug() } 117 | ``` 118 | 119 | #### Custom 120 | ```kotlin 121 | buildConfiguration { named("staging") } 122 | ``` 123 | 124 | ### Target Platforms 125 | The main feature of an XCFramework is packaging multiple architectures for the same platform. 126 | This is great since it allows distributing e.g. iOS builds for both the physical device, and the simulator in one package. 127 | 128 | Swift packages require declaring the minimum supported version for each platform. 129 | Therefore, you need to configure both the architectures for each platform and the version. 130 | 131 | You can either declare all target architectures specifically or add all architectures of a platform at once. 132 | 133 | ```kotlin 134 | targetPlatforms { 135 | // all iOS targets (== device and simulator) with minimum version 13 136 | iOS { v("13") } 137 | 138 | // macOS with minimum version 10.0 139 | targets("macosX64") { v("10.0") } 140 | } 141 | ``` 142 | 143 | __Note:__ 144 | If you are using Groovy for the build script the target names must be passed as a list. 145 | ```groovy 146 | targetPlatforms { 147 | // the catch-all DSL works the same in Groovy and Kotlin 148 | iOS { v("13") } 149 | 150 | // however, Groovy requires a list when the targets() DSL is used 151 | targets(['macosX64']) { v('10.0') } 152 | } 153 | ``` 154 | 155 | Available platform shortcuts are: 156 | - `iOS { v("xxx") }` 157 | - `tvOS { v("xxx") }` 158 | - `macOS { v("xxx") }` 159 | - `watchOS { v("xxx") }` 160 | 161 | ## Further Reading 162 | To learn more about the Swift Package Manager I recommend reading the following resources. 163 | 164 | * https://swift.org/package-manager/ 165 | * https://github.com/apple/swift-package-manager 166 | * https://docs.swift.org/package-manager/PackageDescription/PackageDescription.html 167 | 168 | ## License 169 | ```text 170 | Copyright 2020 Georg Dresler 171 | 172 | Licensed under the Apache License, Version 2.0 (the "License"); 173 | you may not use this file except in compliance with the License. 174 | You may obtain a copy of the License at 175 | 176 | http://www.apache.org/licenses/LICENSE-2.0 177 | 178 | Unless required by applicable law or agreed to in writing, software 179 | distributed under the License is distributed on an "AS IS" BASIS, 180 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 181 | See the License for the specific language governing permissions and 182 | limitations under the License. 183 | ``` 184 | -------------------------------------------------------------------------------- /api/multiplatform-swiftpackage.api: -------------------------------------------------------------------------------- 1 | public final class com/chromaticnoise/multiplatformswiftpackage/MultiplatformSwiftPackagePlugin : org/gradle/api/Plugin { 2 | public static final field Companion Lcom/chromaticnoise/multiplatformswiftpackage/MultiplatformSwiftPackagePlugin$Companion; 3 | public fun ()V 4 | public synthetic fun apply (Ljava/lang/Object;)V 5 | public fun apply (Lorg/gradle/api/Project;)V 6 | } 7 | 8 | public class com/chromaticnoise/multiplatformswiftpackage/SwiftPackageExtension { 9 | public fun (Lorg/gradle/api/Project;)V 10 | public final fun buildConfiguration (Lgroovy/lang/Closure;)V 11 | public final fun buildConfiguration (Lkotlin/jvm/functions/Function1;)V 12 | public final fun distributionMode (Lgroovy/lang/Closure;)V 13 | public final fun distributionMode (Lkotlin/jvm/functions/Function1;)V 14 | public final fun outputDirectory (Ljava/io/File;)V 15 | public final fun packageName (Ljava/lang/String;)V 16 | public final fun swiftToolsVersion (Ljava/lang/String;)V 17 | public final fun targetPlatforms (Lgroovy/lang/Closure;)V 18 | public final fun targetPlatforms (Lkotlin/jvm/functions/Function1;)V 19 | public final fun zipFileName (Ljava/lang/String;)V 20 | } 21 | 22 | public final class com/chromaticnoise/multiplatformswiftpackage/dsl/BuildConfigurationDSL { 23 | public fun ()V 24 | public final fun debug ()V 25 | public final fun named (Ljava/lang/String;)V 26 | public final fun release ()V 27 | } 28 | 29 | public final class com/chromaticnoise/multiplatformswiftpackage/dsl/DistributionModeDSL { 30 | public fun ()V 31 | public final fun local ()V 32 | public final fun remote (Ljava/lang/String;)V 33 | } 34 | 35 | public final class com/chromaticnoise/multiplatformswiftpackage/dsl/TargetPlatformDsl { 36 | public fun ()V 37 | public final fun iOS (Lgroovy/lang/Closure;)V 38 | public final fun iOS (Lkotlin/jvm/functions/Function1;)V 39 | public final fun macOS (Lgroovy/lang/Closure;)V 40 | public final fun macOS (Lkotlin/jvm/functions/Function1;)V 41 | public final fun targets (Ljava/util/Collection;Lgroovy/lang/Closure;)V 42 | public final fun targets ([Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V 43 | public final fun tvOS (Lgroovy/lang/Closure;)V 44 | public final fun tvOS (Lkotlin/jvm/functions/Function1;)V 45 | public final fun watchOS (Lgroovy/lang/Closure;)V 46 | public final fun watchOS (Lkotlin/jvm/functions/Function1;)V 47 | } 48 | 49 | public final class com/chromaticnoise/multiplatformswiftpackage/dsl/TargetPlatformDsl$PlatformVersionDsl { 50 | public fun ()V 51 | public final fun v (Ljava/lang/String;)V 52 | } 53 | 54 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | jcenter() 5 | } 6 | dependencies { 7 | classpath("org.jetbrains.kotlinx:binary-compatibility-validator:0.2.4") 8 | } 9 | } 10 | 11 | apply(plugin = "binary-compatibility-validator") 12 | 13 | plugins { 14 | `kotlin-dsl` 15 | `java-gradle-plugin` 16 | `maven-publish` 17 | signing 18 | } 19 | 20 | version = "2.0.3" 21 | 22 | repositories { 23 | jcenter() 24 | } 25 | 26 | dependencies { 27 | compileOnly(kotlin("gradle-plugin")) 28 | testImplementation("io.kotest:kotest-runner-junit5:4.3.0") 29 | testImplementation("io.kotest:kotest-assertions-core:4.3.0") 30 | testImplementation("io.kotest:kotest-property:4.3.0") 31 | testImplementation("io.mockk:mockk:1.10.0") 32 | testImplementation(kotlin("gradle-plugin")) 33 | } 34 | 35 | java { 36 | sourceCompatibility = JavaVersion.VERSION_1_8 37 | targetCompatibility = JavaVersion.VERSION_1_8 38 | 39 | withJavadocJar() 40 | withSourcesJar() 41 | } 42 | 43 | tasks.withType { 44 | useJUnitPlatform() 45 | } 46 | 47 | extensions.findByName("buildScan")?.withGroovyBuilder { 48 | setProperty("termsOfServiceUrl", "https://gradle.com/terms-of-service") 49 | setProperty("termsOfServiceAgree", "yes") 50 | } 51 | 52 | gradlePlugin { 53 | plugins { 54 | create("pluginMaven") { 55 | id = "com.chromaticnoise.multiplatform-swiftpackage" 56 | implementationClass = "com.chromaticnoise.multiplatformswiftpackage.MultiplatformSwiftPackagePlugin" 57 | } 58 | } 59 | } 60 | 61 | publishing { 62 | publications { 63 | create("pluginMaven") { 64 | pom { 65 | groupId = "com.chromaticnoise.multiplatform-swiftpackage" 66 | artifactId = "com.chromaticnoise.multiplatform-swiftpackage.gradle.plugin" 67 | 68 | name.set("Multiplatform Swift Package") 69 | description.set("Gradle plugin to generate a Swift.package file and XCFramework to distribute a Kotlin Multiplatform iOS library") 70 | url.set("https://github.com/ge-org/multiplatform-swiftpackage") 71 | 72 | licenses { 73 | license { 74 | name.set("Apache License, Version 2.0") 75 | url.set("https://www.apache.org/licenses/LICENSE-2.0.txt") 76 | } 77 | } 78 | developers { 79 | developer { 80 | name.set("Georg Dresler") 81 | } 82 | } 83 | scm { 84 | connection.set("scm:git:https://github.com/ge-org/multiplatform-swiftpackage.git") 85 | developerConnection.set("scm:git:ssh://git@github.com/ge-org/multiplatform-swiftpackage.git") 86 | url.set("https://github.com/ge-org/multiplatform-swiftpackage") 87 | } 88 | } 89 | } 90 | } 91 | 92 | repositories { 93 | maven { 94 | val releasesUrl = "https://oss.sonatype.org/service/local/staging/deploy/maven2/" 95 | val snapshotsUrl = "https://oss.sonatype.org/content/repositories/snapshots/" 96 | name = "mavencentral" 97 | url = uri(if (version.toString().endsWith("SNAPSHOT")) snapshotsUrl else releasesUrl) 98 | credentials { 99 | username = System.getenv("SONATYPE_NEXUS_USERNAME") 100 | password = System.getenv("SONATYPE_NEXUS_PASSWORD") 101 | } 102 | } 103 | } 104 | } 105 | 106 | signing { 107 | sign(publishing.publications["pluginMaven"]) 108 | } 109 | 110 | tasks.javadoc { 111 | if (JavaVersion.current().isJava9Compatible) { 112 | (options as StandardJavadocDocletOptions).addBooleanOption("html5", true) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ge-org/multiplatform-swiftpackage/8d7c25ecc19841d4af515db885ae3426f7f32274/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "multiplatform-swiftpackage" 2 | -------------------------------------------------------------------------------- /src/main/kotlin/com/chromaticnoise/multiplatformswiftpackage/MultiplatformSwiftPackagePlugin.kt: -------------------------------------------------------------------------------- 1 | package com.chromaticnoise.multiplatformswiftpackage 2 | 3 | import com.chromaticnoise.multiplatformswiftpackage.domain.AppleTarget 4 | import com.chromaticnoise.multiplatformswiftpackage.domain.platforms 5 | import com.chromaticnoise.multiplatformswiftpackage.task.registerCreateSwiftPackageTask 6 | import com.chromaticnoise.multiplatformswiftpackage.task.registerCreateXCFrameworkTask 7 | import com.chromaticnoise.multiplatformswiftpackage.task.registerCreateZipFileTask 8 | import org.gradle.api.Plugin 9 | import org.gradle.api.Project 10 | import org.gradle.kotlin.dsl.create 11 | import org.gradle.kotlin.dsl.findByType 12 | import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension 13 | 14 | /** 15 | * Plugin to generate XCFramework and Package.swift file for Apple platform targets. 16 | */ 17 | public class MultiplatformSwiftPackagePlugin : Plugin { 18 | 19 | override fun apply(project: Project) { 20 | val extension = project.extensions.create(EXTENSION_NAME, project) 21 | 22 | project.afterEvaluate { 23 | project.extensions.findByType()?.let { kmpExtension -> 24 | extension.appleTargets = AppleTarget.allOf( 25 | nativeTargets = kmpExtension.targets.toList(), 26 | platforms = extension.targetPlatforms.platforms 27 | ) 28 | project.registerCreateXCFrameworkTask() 29 | project.registerCreateZipFileTask() 30 | project.registerCreateSwiftPackageTask() 31 | } 32 | } 33 | } 34 | 35 | internal companion object { 36 | internal const val EXTENSION_NAME = "multiplatformSwiftPackage" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/kotlin/com/chromaticnoise/multiplatformswiftpackage/SwiftPackageExtension.kt: -------------------------------------------------------------------------------- 1 | package com.chromaticnoise.multiplatformswiftpackage 2 | 3 | import com.chromaticnoise.multiplatformswiftpackage.domain.* 4 | import com.chromaticnoise.multiplatformswiftpackage.domain.PluginConfiguration.PluginConfigurationError 5 | import com.chromaticnoise.multiplatformswiftpackage.dsl.BuildConfigurationDSL 6 | import com.chromaticnoise.multiplatformswiftpackage.dsl.DistributionModeDSL 7 | import com.chromaticnoise.multiplatformswiftpackage.dsl.TargetPlatformDsl 8 | import groovy.lang.Closure 9 | import org.gradle.api.Project 10 | import org.gradle.util.ConfigureUtil 11 | import java.io.File 12 | 13 | public open class SwiftPackageExtension(internal val project: Project) { 14 | 15 | internal var buildConfiguration: BuildConfiguration = BuildConfiguration.Release 16 | internal var packageName: Either? = null 17 | internal var outputDirectory: OutputDirectory = OutputDirectory(File(project.projectDir, "swiftpackage")) 18 | internal var swiftToolsVersion: SwiftToolVersion? = null 19 | internal var distributionMode: DistributionMode = DistributionMode.Local 20 | internal var targetPlatforms: Collection, TargetPlatform>> = emptyList() 21 | internal var appleTargets: Collection = emptyList() 22 | internal var zipFileName: Either? = null 23 | 24 | /** 25 | * Sets the name of the Swift package. 26 | * Defaults to the base name of the first framework found in the project. 27 | * 28 | * @param name of the Swift package. 29 | */ 30 | public fun packageName(name: String) { 31 | packageName = PackageName.of(name) 32 | } 33 | 34 | /** 35 | * Sets the directory where files like the Package.swift and XCFramework will be created. 36 | * Defaults to $projectDir/swiftpackage 37 | * 38 | * @param directory where the files will be created. 39 | */ 40 | public fun outputDirectory(directory: File) { 41 | outputDirectory = OutputDirectory((directory)) 42 | } 43 | 44 | /** 45 | * Version of the Swift tools. That's the version added to the Package.swift header. 46 | * E.g. 5.3 47 | */ 48 | public fun swiftToolsVersion(name: String) { 49 | swiftToolsVersion = SwiftToolVersion.of(name) 50 | } 51 | 52 | /** 53 | * Builder for the [BuildConfiguration]. 54 | */ 55 | public fun buildConfiguration(configure: BuildConfigurationDSL.() -> Unit) { 56 | BuildConfigurationDSL().also { dsl -> 57 | dsl.configure() 58 | buildConfiguration = dsl.buildConfiguration 59 | } 60 | } 61 | 62 | public fun buildConfiguration(configure: Closure) { 63 | buildConfiguration { ConfigureUtil.configure(configure, this) } 64 | } 65 | 66 | /** 67 | * Builder for the [DistributionMode]. 68 | */ 69 | public fun distributionMode(configure: DistributionModeDSL.() -> Unit) { 70 | DistributionModeDSL().also { dsl -> 71 | dsl.configure() 72 | distributionMode = dsl.distributionMode 73 | } 74 | } 75 | 76 | public fun distributionMode(configure: Closure) { 77 | distributionMode { ConfigureUtil.configure(configure, this) } 78 | } 79 | 80 | /** 81 | * Builder for instances of [TargetPlatform]. 82 | */ 83 | public fun targetPlatforms(configure: TargetPlatformDsl.() -> Unit) { 84 | TargetPlatformDsl().also { dsl -> 85 | dsl.configure() 86 | targetPlatforms = dsl.targetPlatforms 87 | } 88 | } 89 | 90 | public fun targetPlatforms(configure: Closure) { 91 | targetPlatforms { ConfigureUtil.configure(configure, this) } 92 | } 93 | 94 | /** 95 | * Sets the name of the ZIP file. 96 | * Do not append the `.zip` file extension since it will be added during the build. 97 | * 98 | * Defaults to the [packageName] concatenated with the project version. E.g. 99 | * MyAwesomeKit-2.3.42-SNAPSHOT 100 | */ 101 | public fun zipFileName(name: String) { 102 | zipFileName = ZipFileName.of(name) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/main/kotlin/com/chromaticnoise/multiplatformswiftpackage/domain/AppleFramework.kt: -------------------------------------------------------------------------------- 1 | package com.chromaticnoise.multiplatformswiftpackage.domain 2 | 3 | import org.jetbrains.kotlin.gradle.plugin.mpp.NativeBinary 4 | import java.io.File 5 | 6 | internal class AppleFramework( 7 | val outputFile: AppleFrameworkOutputFile, 8 | val name: AppleFrameworkName, 9 | val linkTask: AppleFrameworkLinkTask 10 | ) { 11 | 12 | val dsymFile: File get() = File(outputFile.parent, "${name.value}.framework.dSYM") 13 | 14 | internal companion object 15 | } 16 | 17 | internal fun AppleFramework.Companion.of(binary: NativeBinary?): AppleFramework? = binary?.let { 18 | AppleFramework( 19 | AppleFrameworkOutputFile(it.outputFile), 20 | AppleFrameworkName(it.baseName), 21 | AppleFrameworkLinkTask(it.linkTaskName) 22 | ) 23 | } 24 | 25 | internal data class AppleFrameworkOutputFile(private val file: File) { 26 | val path: String get() = file.path 27 | 28 | val parent: File get() = file.parentFile 29 | } 30 | 31 | internal data class AppleFrameworkName(val value: String) 32 | 33 | internal data class AppleFrameworkLinkTask(val name: String) 34 | -------------------------------------------------------------------------------- /src/main/kotlin/com/chromaticnoise/multiplatformswiftpackage/domain/AppleTarget.kt: -------------------------------------------------------------------------------- 1 | package com.chromaticnoise.multiplatformswiftpackage.domain 2 | 3 | import org.jetbrains.kotlin.gradle.plugin.KotlinTarget 4 | import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget 5 | import org.jetbrains.kotlin.gradle.plugin.mpp.NativeOutputKind 6 | 7 | internal class AppleTarget private constructor(val nativeTarget: KotlinNativeTarget) { 8 | 9 | internal fun getFramework(buildConfiguration: BuildConfiguration): AppleFramework? = 10 | try { 11 | val nativeBinary = nativeTarget.binaries.find { binary -> 12 | binary.buildType.getName().equals(buildConfiguration.name, ignoreCase = true) && 13 | binary.outputKind == NativeOutputKind.FRAMEWORK 14 | } 15 | AppleFramework.of(nativeBinary) 16 | } catch (_: Exception) { null } 17 | 18 | internal companion object { 19 | fun allOf( 20 | nativeTargets: Collection, 21 | platforms: Collection 22 | ): Collection = nativeTargets 23 | .filterIsInstance() 24 | .filter { it.konanTarget.family.isAppleFamily } 25 | .filter { target -> 26 | platforms 27 | .flatMap { platform -> platform.targets.map { it.konanTarget } } 28 | .firstOrNull { it == target.konanTarget } != null 29 | } 30 | .map { AppleTarget(it) } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/kotlin/com/chromaticnoise/multiplatformswiftpackage/domain/BuildConfiguration.kt: -------------------------------------------------------------------------------- 1 | package com.chromaticnoise.multiplatformswiftpackage.domain 2 | 3 | import org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType 4 | 5 | internal sealed class BuildConfiguration { 6 | internal object Release : BuildConfiguration() 7 | internal object Debug : BuildConfiguration() 8 | internal data class Custom(val configurationName: String) : BuildConfiguration() 9 | 10 | val name get() = when(this) { 11 | Release -> NativeBuildType.RELEASE.getName() 12 | Debug -> NativeBuildType.DEBUG.getName() 13 | is Custom -> configurationName 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/kotlin/com/chromaticnoise/multiplatformswiftpackage/domain/DistributionMode.kt: -------------------------------------------------------------------------------- 1 | package com.chromaticnoise.multiplatformswiftpackage.domain 2 | 3 | internal sealed class DistributionMode { 4 | internal object Local : DistributionMode() 5 | internal data class Remote(val url: DistributionURL) : DistributionMode() 6 | } 7 | -------------------------------------------------------------------------------- /src/main/kotlin/com/chromaticnoise/multiplatformswiftpackage/domain/DistributionURL.kt: -------------------------------------------------------------------------------- 1 | package com.chromaticnoise.multiplatformswiftpackage.domain 2 | 3 | internal data class DistributionURL(val value: String) { 4 | private val slashTerminatedValue: String get() = 5 | value.takeIf { it.endsWith("/") } ?: 6 | "$value/" 7 | 8 | fun appendPath(path: String) = DistributionURL("$slashTerminatedValue$path") 9 | } 10 | -------------------------------------------------------------------------------- /src/main/kotlin/com/chromaticnoise/multiplatformswiftpackage/domain/Either.kt: -------------------------------------------------------------------------------- 1 | package com.chromaticnoise.multiplatformswiftpackage.domain 2 | 3 | internal sealed class Either { 4 | data class Left(val value: L) : Either() 5 | data class Right(val value: R) : Either() 6 | 7 | val leftValueOrNull: L? get() = (this as? Left)?.value 8 | 9 | val orNull: R? get() = (this as? Right)?.value 10 | 11 | fun fold(l: (L) -> T, r: (R) -> T): T = when (this) { 12 | is Left -> l(value) 13 | is Right -> r(value) 14 | } 15 | 16 | internal companion object { 17 | fun ofNullable(right: R?, left: L): Either = when (right) { 18 | null -> Left(left) 19 | else -> Right(right) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/kotlin/com/chromaticnoise/multiplatformswiftpackage/domain/OutputDirectory.kt: -------------------------------------------------------------------------------- 1 | package com.chromaticnoise.multiplatformswiftpackage.domain 2 | 3 | import java.io.File 4 | 5 | internal data class OutputDirectory(val value: File) 6 | -------------------------------------------------------------------------------- /src/main/kotlin/com/chromaticnoise/multiplatformswiftpackage/domain/PackageName.kt: -------------------------------------------------------------------------------- 1 | package com.chromaticnoise.multiplatformswiftpackage.domain 2 | 3 | import com.chromaticnoise.multiplatformswiftpackage.domain.PluginConfiguration.PluginConfigurationError 4 | import com.chromaticnoise.multiplatformswiftpackage.domain.PluginConfiguration.PluginConfigurationError.BlankPackageName 5 | 6 | internal class PackageName private constructor(val value: String) { 7 | 8 | internal companion object { 9 | fun of(name: String?): Either = 10 | name?.ifNotBlank { Either.Right(PackageName(it)) } 11 | ?: Either.Left(BlankPackageName) 12 | } 13 | 14 | override fun equals(other: Any?): Boolean = value == (other as? PackageName)?.value 15 | 16 | override fun hashCode(): Int = value.hashCode() 17 | 18 | override fun toString(): String = "PackageName(value='$value')" 19 | } 20 | -------------------------------------------------------------------------------- /src/main/kotlin/com/chromaticnoise/multiplatformswiftpackage/domain/PlatformVersion.kt: -------------------------------------------------------------------------------- 1 | package com.chromaticnoise.multiplatformswiftpackage.domain 2 | 3 | internal class PlatformVersion private constructor(private val value: String) { 4 | val name: String get() = value 5 | 6 | internal companion object { 7 | fun of(name: String) = name.ifNotBlank { PlatformVersion(it) } 8 | } 9 | 10 | override fun equals(other: Any?): Boolean = value == (other as? PlatformVersion)?.value 11 | 12 | override fun hashCode(): Int = value.hashCode() 13 | 14 | override fun toString() = "PlatformVersion(name='$name')" 15 | } 16 | -------------------------------------------------------------------------------- /src/main/kotlin/com/chromaticnoise/multiplatformswiftpackage/domain/PluginConfiguration.kt: -------------------------------------------------------------------------------- 1 | package com.chromaticnoise.multiplatformswiftpackage.domain 2 | 3 | import com.chromaticnoise.multiplatformswiftpackage.SwiftPackageExtension 4 | import com.chromaticnoise.multiplatformswiftpackage.domain.PluginConfiguration.PluginConfigurationError.* 5 | import org.gradle.api.Project 6 | 7 | internal class PluginConfiguration private constructor( 8 | val buildConfiguration: BuildConfiguration, 9 | val packageName: PackageName, 10 | val outputDirectory: OutputDirectory, 11 | val swiftToolsVersion: SwiftToolVersion, 12 | val distributionMode: DistributionMode, 13 | val targetPlatforms: Collection, 14 | val appleTargets: Collection, 15 | val zipFileName: ZipFileName 16 | ) { 17 | internal companion object { 18 | fun of(extension: SwiftPackageExtension): Either, PluginConfiguration> { 19 | val targetPlatforms = extension.targetPlatforms.platforms 20 | val packageName = extension.getPackageName() 21 | 22 | val errors = mutableListOf().apply { 23 | if (extension.swiftToolsVersion == null) { 24 | add(MissingSwiftToolsVersion) 25 | } 26 | 27 | val targetPlatformErrors = extension.targetPlatforms.errors 28 | if (targetPlatformErrors.isNotEmpty()) { 29 | addAll(targetPlatformErrors) 30 | } 31 | 32 | if (targetPlatformErrors.isEmpty() && targetPlatforms.isEmpty()) { 33 | add(MissingTargetPlatforms) 34 | } 35 | 36 | if (extension.appleTargets.isEmpty() && targetPlatforms.isNotEmpty()) { 37 | add(MissingAppleTargets) 38 | } 39 | 40 | packageName.leftValueOrNull?.let { error -> add(error) } 41 | 42 | extension.zipFileName?.leftValueOrNull?.let { error -> add(error) } 43 | } 44 | 45 | return if (errors.isEmpty()) { 46 | Either.Right( 47 | PluginConfiguration( 48 | extension.buildConfiguration, 49 | packageName.orNull!!, 50 | extension.outputDirectory, 51 | extension.swiftToolsVersion!!, 52 | extension.distributionMode, 53 | targetPlatforms, 54 | extension.appleTargets, 55 | extension.zipFileName?.orNull ?: defaultZipFileName(packageName.orNull!!, extension.project) 56 | ) 57 | ) 58 | } else { 59 | Either.Left(errors) 60 | } 61 | } 62 | 63 | private fun SwiftPackageExtension.getPackageName(): Either = packageName 64 | ?: appleTargets.map { it.getFramework(buildConfiguration) }.firstOrNull()?.let { framework -> 65 | PackageName.of(framework.name.value) 66 | } ?: Either.Left(BlankPackageName) 67 | 68 | private fun defaultZipFileName(packageName: PackageName, project: Project) = 69 | ZipFileName.of("${packageName.value}-${project.version}").orNull!! 70 | } 71 | 72 | internal sealed class PluginConfigurationError { 73 | object MissingSwiftToolsVersion : PluginConfigurationError() 74 | data class InvalidTargetName(val name: String) : PluginConfigurationError() 75 | object MissingTargetPlatforms : PluginConfigurationError() 76 | object MissingAppleTargets : PluginConfigurationError() 77 | object BlankPackageName : PluginConfigurationError() 78 | object BlankZipFileName : PluginConfigurationError() 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/kotlin/com/chromaticnoise/multiplatformswiftpackage/domain/Project+PluginConfiguration.kt: -------------------------------------------------------------------------------- 1 | package com.chromaticnoise.multiplatformswiftpackage.domain 2 | 3 | import com.chromaticnoise.multiplatformswiftpackage.MultiplatformSwiftPackagePlugin 4 | import com.chromaticnoise.multiplatformswiftpackage.SwiftPackageExtension 5 | import com.chromaticnoise.multiplatformswiftpackage.domain.PluginConfiguration.PluginConfigurationError 6 | import org.gradle.api.InvalidUserDataException 7 | import org.gradle.api.Project 8 | 9 | @Throws(InvalidUserDataException::class) 10 | internal fun Project.getConfigurationOrThrow() = PluginConfiguration.of( 11 | extensions.findByName(MultiplatformSwiftPackagePlugin.EXTENSION_NAME) as SwiftPackageExtension 12 | ).fold({ 13 | throw InvalidUserDataException( 14 | "${it.toErrorMessage()}\nFind more information on https://github.com/ge-org/multiplatform-swiftpackage" 15 | ) 16 | }, { it }) 17 | 18 | private fun List.toErrorMessage() = joinToString("\n\n") { error -> 19 | when (error) { 20 | PluginConfigurationError.MissingSwiftToolsVersion -> """ 21 | * Swift tools version is missing. 22 | Declare it by adding your Swift version to the plugin configuration block. 23 | """.trimIndent() 24 | PluginConfigurationError.MissingTargetPlatforms -> """ 25 | * Target platforms are missing. 26 | Declare at least one platform by adding it to the plugin configuration block. 27 | """.trimIndent() 28 | PluginConfigurationError.MissingAppleTargets -> """ 29 | * No Apple targets declared. 30 | It appears your multiplatform project does not contain any Apple target. 31 | """.trimIndent() 32 | is PluginConfigurationError.InvalidTargetName -> """ 33 | * Target name is invalid: ${error.name} 34 | Only the following target names are valid: ${TargetName.values().joinToString { it.identifier }} 35 | """.trimIndent() 36 | PluginConfigurationError.BlankPackageName -> """ 37 | * Package name must not be blank 38 | Either declare the base name of your frameworks or use a non-empty package name. 39 | """.trimIndent() 40 | PluginConfigurationError.BlankZipFileName -> """ 41 | * ZIP file name must not be blank 42 | """.trimIndent() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/kotlin/com/chromaticnoise/multiplatformswiftpackage/domain/SwiftPackageConfiguration.kt: -------------------------------------------------------------------------------- 1 | package com.chromaticnoise.multiplatformswiftpackage.domain 2 | 3 | import com.chromaticnoise.multiplatformswiftpackage.MultiplatformSwiftPackagePlugin 4 | import org.gradle.api.Project 5 | 6 | internal data class SwiftPackageConfiguration( 7 | private val project: Project, 8 | private val packageName: PackageName, 9 | private val toolVersion: SwiftToolVersion, 10 | private val platforms: String, 11 | private val distributionMode: DistributionMode, 12 | private val zipChecksum: String, 13 | private val zipFileName: ZipFileName 14 | ) { 15 | 16 | private val distributionUrl = when (distributionMode) { 17 | DistributionMode.Local -> null 18 | is DistributionMode.Remote -> distributionMode.url.appendPath(zipFileName.nameWithExtension) 19 | } 20 | 21 | internal val templateProperties = mapOf( 22 | "toolsVersion" to toolVersion.name, 23 | "name" to packageName.value, 24 | "platforms" to platforms, 25 | "isLocal" to (distributionMode == DistributionMode.Local), 26 | "url" to distributionUrl?.value, 27 | "checksum" to zipChecksum.trim() 28 | ) 29 | 30 | internal companion object { 31 | internal const val FILE_NAME = "Package.swift" 32 | 33 | internal val templateFile = 34 | MultiplatformSwiftPackagePlugin::class.java.getResource("/templates/Package.swift.template") 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/kotlin/com/chromaticnoise/multiplatformswiftpackage/domain/SwiftToolVersion.kt: -------------------------------------------------------------------------------- 1 | package com.chromaticnoise.multiplatformswiftpackage.domain 2 | 3 | internal class SwiftToolVersion private constructor(private val value: String) { 4 | 5 | val name get() = value 6 | 7 | companion object { 8 | fun of(name: String) = name.ifNotBlank { SwiftToolVersion(it) } 9 | } 10 | 11 | override fun equals(other: Any?): Boolean = value == (other as? SwiftToolVersion)?.value 12 | 13 | override fun hashCode(): Int = value.hashCode() 14 | } 15 | -------------------------------------------------------------------------------- /src/main/kotlin/com/chromaticnoise/multiplatformswiftpackage/domain/TargetName.kt: -------------------------------------------------------------------------------- 1 | package com.chromaticnoise.multiplatformswiftpackage.domain 2 | 3 | internal enum class TargetName(val identifier: String) { 4 | IOSarm64("iosArm64"), 5 | IOSx64("iosX64"), 6 | WatchOSarm32("watchosArm32"), 7 | WatchOSarm64("watchosArm64"), 8 | WatchOSx86("watchosX86"), 9 | WatchOSx64("watchosX64"), 10 | TvOSarm64("tvosArm64"), 11 | TvOSx64("tvosX64"), 12 | MacOSx64("macosX64"); 13 | 14 | internal companion object { 15 | private val map = values().associateBy(TargetName::identifier) 16 | fun of(id: String): TargetName? = map[id] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/kotlin/com/chromaticnoise/multiplatformswiftpackage/domain/TargetPlatform.kt: -------------------------------------------------------------------------------- 1 | package com.chromaticnoise.multiplatformswiftpackage.domain 2 | 3 | internal data class TargetPlatform( 4 | val version: PlatformVersion, 5 | val targets: Collection 6 | ) 7 | -------------------------------------------------------------------------------- /src/main/kotlin/com/chromaticnoise/multiplatformswiftpackage/domain/ZipFileName.kt: -------------------------------------------------------------------------------- 1 | package com.chromaticnoise.multiplatformswiftpackage.domain 2 | 3 | import com.chromaticnoise.multiplatformswiftpackage.domain.PluginConfiguration.PluginConfigurationError 4 | 5 | internal class ZipFileName private constructor(private val value: String) { 6 | 7 | internal val nameWithExtension: String get() = "$value.zip" 8 | 9 | internal companion object { 10 | fun of(name: String?): Either = 11 | name?.ifNotBlank { Either.Right(ZipFileName(it)) } 12 | ?: Either.Left(PluginConfigurationError.BlankZipFileName) 13 | } 14 | 15 | override fun equals(other: Any?): Boolean = value == (other as? ZipFileName)?.value 16 | 17 | override fun hashCode(): Int = value.hashCode() 18 | 19 | override fun toString(): String = "ZipFileName(value='$value')" 20 | } 21 | -------------------------------------------------------------------------------- /src/main/kotlin/com/chromaticnoise/multiplatformswiftpackage/domain/extensions.kt: -------------------------------------------------------------------------------- 1 | package com.chromaticnoise.multiplatformswiftpackage.domain 2 | 3 | import org.jetbrains.kotlin.konan.target.Family 4 | import org.jetbrains.kotlin.konan.target.KonanTarget 5 | 6 | internal val Family.swiftPackagePlatformName get() = when (this) { 7 | Family.OSX -> "macOS" 8 | Family.IOS -> "iOS" 9 | Family.TVOS -> "tvOS" 10 | Family.WATCHOS -> "watchOS" 11 | else -> null 12 | } 13 | 14 | internal val Collection, TargetPlatform>>.platforms get() = 15 | filterIsInstance, TargetPlatform>>().map { it.value } 16 | 17 | internal val Collection, TargetPlatform>>.errors get() = 18 | filterIsInstance, TargetPlatform>>().map { it.value }.flatten() 19 | 20 | internal val TargetName.konanTarget: KonanTarget get() = when (this) { 21 | TargetName.IOSarm64 -> KonanTarget.IOS_ARM64 22 | TargetName.IOSx64 -> KonanTarget.IOS_X64 23 | TargetName.WatchOSarm32 -> KonanTarget.WATCHOS_ARM32 24 | TargetName.WatchOSarm64 -> KonanTarget.WATCHOS_ARM64 25 | TargetName.WatchOSx86 -> KonanTarget.WATCHOS_X86 26 | TargetName.WatchOSx64 -> KonanTarget.WATCHOS_X64 27 | TargetName.TvOSarm64 -> KonanTarget.TVOS_ARM64 28 | TargetName.TvOSx64 -> KonanTarget.TVOS_X64 29 | TargetName.MacOSx64 -> KonanTarget.MACOS_X64 30 | } 31 | -------------------------------------------------------------------------------- /src/main/kotlin/com/chromaticnoise/multiplatformswiftpackage/domain/functions.kt: -------------------------------------------------------------------------------- 1 | package com.chromaticnoise.multiplatformswiftpackage.domain 2 | 3 | internal fun String.ifNotBlank(f: (String) -> T?): T? = takeIf { it.isNotBlank() }?.let { f(it) } 4 | -------------------------------------------------------------------------------- /src/main/kotlin/com/chromaticnoise/multiplatformswiftpackage/dsl/BuildConfigurationDSL.kt: -------------------------------------------------------------------------------- 1 | package com.chromaticnoise.multiplatformswiftpackage.dsl 2 | 3 | import com.chromaticnoise.multiplatformswiftpackage.domain.BuildConfiguration 4 | 5 | /** 6 | * DSL to create instance of a [BuildConfiguration]. 7 | */ 8 | public class BuildConfigurationDSL { 9 | internal var buildConfiguration: BuildConfiguration = BuildConfiguration.Release 10 | 11 | /** 12 | * XCode release configuration. 13 | */ 14 | public fun release() { buildConfiguration = BuildConfiguration.Release } 15 | 16 | /** 17 | * XCode debug configuration. 18 | */ 19 | public fun debug() { buildConfiguration = BuildConfiguration.Debug } 20 | 21 | /** 22 | * Custom configuration. 23 | * 24 | * @param name of the custom configuration. 25 | */ 26 | public fun named(name: String) { buildConfiguration = BuildConfiguration.Custom(name) } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/kotlin/com/chromaticnoise/multiplatformswiftpackage/dsl/DistributionModeDSL.kt: -------------------------------------------------------------------------------- 1 | package com.chromaticnoise.multiplatformswiftpackage.dsl 2 | 3 | import com.chromaticnoise.multiplatformswiftpackage.domain.DistributionMode 4 | import com.chromaticnoise.multiplatformswiftpackage.domain.DistributionURL 5 | 6 | /** 7 | * DSL to create instance of a [DistributionModeDSL]. 8 | */ 9 | public class DistributionModeDSL { 10 | internal var distributionMode: DistributionMode = DistributionMode.Local 11 | 12 | /** 13 | * The XCFramework will be distributed via the local file system. 14 | */ 15 | public fun local() { 16 | distributionMode = DistributionMode.Local 17 | } 18 | 19 | /** 20 | * The XCFramework will be distributed via a ZIP file that can be downloaded from the [url]. 21 | * 22 | * @param url where the ZIP file can be downloaded from. E.g. https://example.com/packages/ 23 | */ 24 | public fun remote(url: String) { 25 | distributionMode = DistributionMode.Remote(DistributionURL(url)) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/kotlin/com/chromaticnoise/multiplatformswiftpackage/dsl/TargetPlatformDsl.kt: -------------------------------------------------------------------------------- 1 | package com.chromaticnoise.multiplatformswiftpackage.dsl 2 | 3 | import com.chromaticnoise.multiplatformswiftpackage.domain.Either 4 | import com.chromaticnoise.multiplatformswiftpackage.domain.PlatformVersion 5 | import com.chromaticnoise.multiplatformswiftpackage.domain.PluginConfiguration.PluginConfigurationError 6 | import com.chromaticnoise.multiplatformswiftpackage.domain.PluginConfiguration.PluginConfigurationError.InvalidTargetName 7 | import com.chromaticnoise.multiplatformswiftpackage.domain.TargetName 8 | import com.chromaticnoise.multiplatformswiftpackage.domain.TargetPlatform 9 | import groovy.lang.Closure 10 | import org.gradle.util.ConfigureUtil 11 | 12 | /** 13 | * DSL to create instances of [TargetPlatform]. 14 | */ 15 | public class TargetPlatformDsl { 16 | internal var targetPlatforms = mutableListOf, TargetPlatform>>() 17 | 18 | /** 19 | * Adds all iOS targets as a [TargetPlatform] using the provided [version]. 20 | * 21 | * @param version builder for an instance of [PlatformVersion] 22 | */ 23 | public fun iOS(version: PlatformVersionDsl.() -> Unit) { 24 | targetsInternal(listOf(Either.Right(TargetName.IOSarm64), Either.Right(TargetName.IOSx64)), version) 25 | } 26 | 27 | public fun iOS(version: Closure) { 28 | iOS { ConfigureUtil.configure(version, this) } 29 | } 30 | 31 | /** 32 | * Adds all watchOS targets as a [TargetPlatform] using the provided [version]. 33 | * 34 | * @param version builder for an instance of [PlatformVersion] 35 | */ 36 | public fun watchOS(version: PlatformVersionDsl.() -> Unit) { 37 | targetsInternal(listOf( 38 | Either.Right(TargetName.WatchOSarm32), 39 | Either.Right(TargetName.WatchOSarm64), 40 | Either.Right(TargetName.WatchOSx86), 41 | Either.Right(TargetName.WatchOSx64)), 42 | version 43 | ) 44 | } 45 | 46 | public fun watchOS(version: Closure) { 47 | watchOS { ConfigureUtil.configure(version, this) } 48 | } 49 | 50 | /** 51 | * Adds all tvOS targets as a [TargetPlatform] using the provided [version]. 52 | * 53 | * @param version builder for an instance of [PlatformVersion] 54 | */ 55 | public fun tvOS(version: PlatformVersionDsl.() -> Unit) { 56 | targetsInternal(listOf(Either.Right(TargetName.TvOSarm64), Either.Right(TargetName.TvOSx64)), version) 57 | } 58 | 59 | public fun tvOS(version: Closure) { 60 | tvOS { ConfigureUtil.configure(version, this) } 61 | } 62 | 63 | /** 64 | * Adds all macOS targets as a [TargetPlatform] using the provided [version]. 65 | * 66 | * @param version builder for an instance of [PlatformVersion] 67 | */ 68 | public fun macOS(version: PlatformVersionDsl.() -> Unit) { 69 | targetsInternal(listOf(Either.Right(TargetName.MacOSx64)), version) 70 | } 71 | 72 | public fun macOS(version: Closure) { 73 | macOS { ConfigureUtil.configure(version, this) } 74 | } 75 | 76 | /** 77 | * Adds a [TargetPlatform] with targets for the given [names] and the provided [version]. 78 | * The [names] corresponds to the Kotlin multiplatform target names. 79 | * 80 | * @param names of the targets. E.g. iosArm64, iosX64 81 | * @param version builder for an instance of [PlatformVersion] 82 | */ 83 | public fun targets(vararg names: String, version: PlatformVersionDsl.() -> Unit) { 84 | targetsInternal(names.asList().toTargetNames(), version) 85 | } 86 | 87 | public fun targets(names: Collection, version: Closure) { 88 | targetsInternal(names.toTargetNames()) { ConfigureUtil.configure(version, this) } 89 | } 90 | 91 | private fun targetsInternal(names: Collection>, configure: PlatformVersionDsl.() -> Unit) { 92 | if (names.isEmpty()) return 93 | val platformVersion = PlatformVersionDsl().apply(configure).version ?: return 94 | 95 | val errors = names.filterIsInstance>().map { it.value } 96 | val targetNames = names.filterIsInstance>().map { it.value } 97 | val platform: Either, TargetPlatform> = when { 98 | errors.isNotEmpty() -> Either.Left(errors) 99 | else -> Either.Right(TargetPlatform(version = platformVersion, targets = targetNames)) 100 | } 101 | targetPlatforms.add(platform) 102 | } 103 | 104 | private fun Collection.toTargetNames() = map { Either.ofNullable(TargetName.of(it), InvalidTargetName(it)) } 105 | 106 | /** 107 | * DSL to create instances of [PlatformVersion]. 108 | */ 109 | public class PlatformVersionDsl { 110 | internal var version: PlatformVersion? = null 111 | 112 | /** 113 | * Creates a [PlatformVersion] for the given [versionName]. 114 | * The [versionName] corresponds to the SupportedPlatform section of a Package.swift file 115 | * 116 | * @param versionName of the platform version. E.g. 13, 11.0.1 117 | * 118 | * @see "https://docs.swift.org/package-manager/PackageDescription/PackageDescription.html#supportedplatform" 119 | */ 120 | public fun v(versionName: String) { 121 | PlatformVersion.of(versionName)?.let { 122 | this.version = it 123 | } 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/main/kotlin/com/chromaticnoise/multiplatformswiftpackage/task/CreateSwiftPackageTask.kt: -------------------------------------------------------------------------------- 1 | package com.chromaticnoise.multiplatformswiftpackage.task 2 | 3 | import com.chromaticnoise.multiplatformswiftpackage.domain.* 4 | import groovy.text.SimpleTemplateEngine 5 | import org.gradle.api.Project 6 | import java.io.File 7 | 8 | internal fun Project.registerCreateSwiftPackageTask() { 9 | tasks.register("createSwiftPackage") { 10 | group = "multiplatform-swift-package" 11 | description = "Creates a Swift package to distribute an XCFramework" 12 | 13 | dependsOn("createXCFramework") 14 | dependsOn("createZipFile") 15 | 16 | doLast { 17 | val configuration = getConfigurationOrThrow() 18 | val packageFile = File(configuration.outputDirectory.value, SwiftPackageConfiguration.FILE_NAME).apply { 19 | parentFile.mkdirs() 20 | createNewFile() 21 | } 22 | 23 | val packageConfiguration = SwiftPackageConfiguration( 24 | project = project, 25 | packageName = configuration.packageName, 26 | toolVersion = configuration.swiftToolsVersion, 27 | platforms = platforms(configuration), 28 | distributionMode = configuration.distributionMode, 29 | zipChecksum = zipFileChecksum(project, configuration.outputDirectory, configuration.zipFileName), 30 | zipFileName = configuration.zipFileName 31 | ) 32 | 33 | SimpleTemplateEngine() 34 | .createTemplate(SwiftPackageConfiguration.templateFile) 35 | .make(packageConfiguration.templateProperties) 36 | .writeTo(packageFile.writer()) 37 | } 38 | } 39 | } 40 | 41 | private fun platforms(configuration: PluginConfiguration): String = configuration.targetPlatforms.flatMap { platform -> 42 | configuration.appleTargets 43 | .filter { appleTarget -> platform.targets.firstOrNull { it.konanTarget == appleTarget.nativeTarget.konanTarget } != null } 44 | .mapNotNull { target -> target.nativeTarget.konanTarget.family.swiftPackagePlatformName } 45 | .distinct() 46 | .map { platformName -> ".$platformName(.v${platform.version.name})" } 47 | }.joinToString(",\n") 48 | -------------------------------------------------------------------------------- /src/main/kotlin/com/chromaticnoise/multiplatformswiftpackage/task/CreateXCFrameworkTask.kt: -------------------------------------------------------------------------------- 1 | package com.chromaticnoise.multiplatformswiftpackage.task 2 | 3 | import com.chromaticnoise.multiplatformswiftpackage.domain.getConfigurationOrThrow 4 | import org.gradle.api.Project 5 | import org.gradle.api.tasks.Exec 6 | import java.io.File 7 | 8 | internal fun Project.registerCreateXCFrameworkTask() = tasks.register("createXCFramework", Exec::class.java) { 9 | group = "multiplatform-swift-package" 10 | description = "Creates an XCFramework for all declared Apple targets" 11 | 12 | val configuration = getConfigurationOrThrow() 13 | val xcFrameworkDestination = File(configuration.outputDirectory.value, "${configuration.packageName.value}.xcframework") 14 | val frameworks = configuration.appleTargets.mapNotNull { it.getFramework(configuration.buildConfiguration) } 15 | 16 | dependsOn(frameworks.map { it.linkTask.name }) 17 | 18 | executable = "xcodebuild" 19 | args(mutableListOf().apply { 20 | add("-create-xcframework") 21 | add("-output") 22 | add(xcFrameworkDestination.path) 23 | frameworks.forEach { framework -> 24 | add("-framework") 25 | add(framework.outputFile.path) 26 | 27 | framework.dsymFile.takeIf { it.exists() }?.let { dsymFile -> 28 | add("-debug-symbols") 29 | add(dsymFile.path) 30 | } 31 | } 32 | }) 33 | 34 | doFirst { 35 | xcFrameworkDestination.deleteRecursively() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/kotlin/com/chromaticnoise/multiplatformswiftpackage/task/CreateZipFileTask.kt: -------------------------------------------------------------------------------- 1 | package com.chromaticnoise.multiplatformswiftpackage.task 2 | 3 | import com.chromaticnoise.multiplatformswiftpackage.domain.getConfigurationOrThrow 4 | import org.gradle.api.Project 5 | import org.gradle.api.tasks.bundling.Zip 6 | 7 | internal fun Project.registerCreateZipFileTask() { 8 | tasks.register("createZipFile", Zip::class.java) { 9 | setGroup(null) // hide the task from the task list 10 | description = "Creates a ZIP file containing the XCFramework" 11 | 12 | dependsOn("createXCFramework") 13 | 14 | val configuration = getConfigurationOrThrow() 15 | val outputDirectory = configuration.outputDirectory.value 16 | archiveFileName.set(configuration.zipFileName.nameWithExtension) 17 | destinationDirectory.set(outputDirectory) 18 | from(outputDirectory) { 19 | include("**/*.xcframework/") 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/kotlin/com/chromaticnoise/multiplatformswiftpackage/task/zip-functions.kt: -------------------------------------------------------------------------------- 1 | package com.chromaticnoise.multiplatformswiftpackage.task 2 | 3 | import com.chromaticnoise.multiplatformswiftpackage.domain.OutputDirectory 4 | import com.chromaticnoise.multiplatformswiftpackage.domain.ZipFileName 5 | import org.gradle.api.Project 6 | import java.io.ByteArrayOutputStream 7 | import java.io.File 8 | 9 | internal fun zipFileChecksum(project: Project, outputDirectory: OutputDirectory, zipFileName: ZipFileName): String { 10 | val outputPath = outputDirectory.value 11 | return File(outputPath, zipFileName.nameWithExtension) 12 | .takeIf { it.exists() } 13 | ?.let { zipFile -> 14 | ByteArrayOutputStream().use { os -> 15 | project.exec { 16 | workingDir = outputPath 17 | executable = "swift" 18 | args = listOf("package", "compute-checksum", zipFile.name) 19 | standardOutput = os 20 | } 21 | os.toString() 22 | } 23 | } ?: "" 24 | } 25 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/gradle-plugins/com.chromaticnoise.multiplatform-swiftpackage.properties: -------------------------------------------------------------------------------- 1 | implementationClass=com.chromaticnoise.multiplatformswiftpackage.MultiplatformSwiftPackagePlugin 2 | -------------------------------------------------------------------------------- /src/main/resources/templates/Package.swift.template: -------------------------------------------------------------------------------- 1 | // swift-tools-version:$toolsVersion 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "$name", 6 | platforms: [ 7 | $platforms 8 | ], 9 | products: [ 10 | .library( 11 | name: "$name", 12 | targets: ["$name"] 13 | ), 14 | ], 15 | targets: [ 16 | <% if (isLocal) print """ .binaryTarget( 17 | name: "$name", 18 | path: "./${name}.xcframework" 19 | ),""" else print """ .binaryTarget( 20 | name: "$name", 21 | url: "$url", 22 | checksum: "$checksum" 23 | ),""" %> 24 | ] 25 | ) 26 | -------------------------------------------------------------------------------- /src/test/kotlin/com/chromaticnoise/multiplatformswiftpackage/domain/DistributionURLTest.kt: -------------------------------------------------------------------------------- 1 | package com.chromaticnoise.multiplatformswiftpackage.domain 2 | 3 | import io.kotest.core.spec.style.StringSpec 4 | import io.kotest.matchers.shouldBe 5 | 6 | class DistributionURLTest : StringSpec({ 7 | 8 | "when appending a path it should be separated by a / from the URL" { 9 | val url = DistributionURL("my-url").appendPath("my-path") 10 | 11 | url.value.shouldBe("my-url/my-path") 12 | } 13 | }) 14 | -------------------------------------------------------------------------------- /src/test/kotlin/com/chromaticnoise/multiplatformswiftpackage/domain/PackageNameTest.kt: -------------------------------------------------------------------------------- 1 | package com.chromaticnoise.multiplatformswiftpackage.domain 2 | 3 | import com.chromaticnoise.multiplatformswiftpackage.domain.PluginConfiguration.PluginConfigurationError.BlankPackageName 4 | import io.kotest.core.spec.style.StringSpec 5 | import io.kotest.matchers.booleans.shouldBeFalse 6 | import io.kotest.matchers.booleans.shouldBeTrue 7 | import io.kotest.matchers.types.shouldBeInstanceOf 8 | import io.kotest.property.Arb 9 | import io.kotest.property.arbitrary.filter 10 | import io.kotest.property.arbitrary.string 11 | import io.kotest.property.forAll 12 | 13 | class PackageNameTest : StringSpec({ 14 | 15 | "empty name should not be valid" { 16 | PackageName.of("").leftValueOrNull!!.shouldBeInstanceOf() 17 | } 18 | 19 | "blank names should not be valid" { 20 | forAll(Arb.string().filter { it.isBlank() }) { name -> 21 | PackageName.of(name).leftValueOrNull is BlankPackageName 22 | } 23 | } 24 | 25 | "two instances should be equal if their names are equal" { 26 | (PackageName.of("equal name") == PackageName.of("equal name")) 27 | .shouldBeTrue() 28 | } 29 | 30 | "two instances should not be equal if their names differ" { 31 | (PackageName.of("some name") == PackageName.of("different name")) 32 | .shouldBeFalse() 33 | } 34 | }) 35 | -------------------------------------------------------------------------------- /src/test/kotlin/com/chromaticnoise/multiplatformswiftpackage/domain/PlatformVersionTest.kt: -------------------------------------------------------------------------------- 1 | package com.chromaticnoise.multiplatformswiftpackage.domain 2 | 3 | import io.kotest.core.spec.style.StringSpec 4 | import io.kotest.matchers.booleans.shouldBeFalse 5 | import io.kotest.matchers.booleans.shouldBeTrue 6 | import io.kotest.matchers.nulls.shouldBeNull 7 | import io.kotest.property.Arb 8 | import io.kotest.property.arbitrary.filter 9 | import io.kotest.property.arbitrary.string 10 | import io.kotest.property.forAll 11 | 12 | class PlatformVersionTest : StringSpec({ 13 | 14 | "empty name should not be valid" { 15 | PlatformVersion.of("").shouldBeNull() 16 | } 17 | 18 | "blank names should not be valid" { 19 | forAll(Arb.string().filter { it.isBlank() }) { name -> 20 | PlatformVersion.of(name) == null 21 | } 22 | } 23 | 24 | "two instances should be equal if their names are equal" { 25 | (PlatformVersion.of("equal name") == PlatformVersion.of("equal name")) 26 | .shouldBeTrue() 27 | } 28 | 29 | "two instances should not be equal if their names differ" { 30 | (PlatformVersion.of("some name") == PlatformVersion.of("different name")) 31 | .shouldBeFalse() 32 | } 33 | }) 34 | -------------------------------------------------------------------------------- /src/test/kotlin/com/chromaticnoise/multiplatformswiftpackage/domain/PluginConfigurationTest.kt: -------------------------------------------------------------------------------- 1 | package com.chromaticnoise.multiplatformswiftpackage.domain 2 | 3 | import com.chromaticnoise.multiplatformswiftpackage.SwiftPackageExtension 4 | import com.chromaticnoise.multiplatformswiftpackage.domain.PluginConfiguration.PluginConfigurationError.* 5 | import io.kotest.core.spec.style.BehaviorSpec 6 | import io.kotest.core.test.TestCase 7 | import io.kotest.matchers.shouldBe 8 | import io.mockk.every 9 | import io.mockk.mockk 10 | import org.gradle.api.Project 11 | import java.io.File 12 | 13 | class PluginConfigurationTest : BehaviorSpec() { 14 | 15 | init { 16 | Given("an extension") { 17 | val extension = SwiftPackageExtension(project) 18 | 19 | When("the swift tool version is null") { 20 | extension.swiftToolsVersion = null 21 | 22 | Then("an error should be returned") { 23 | (PluginConfiguration.of(extension) as Either.Left).value.contains(MissingSwiftToolsVersion) 24 | } 25 | } 26 | 27 | When("the target platforms produced errors") { 28 | val expectedErrors = listOf(InvalidTargetName("invalid name"), InvalidTargetName("whatever")) 29 | extension.targetPlatforms = listOf(Either.Left(expectedErrors)) 30 | 31 | Then("the errors should be returned") { 32 | (PluginConfiguration.of(extension) as Either.Left).value.containsAll(expectedErrors) 33 | } 34 | } 35 | 36 | When("the target platforms are empty") { 37 | extension.targetPlatforms = emptyList() 38 | 39 | Then("an error should be returned") { 40 | (PluginConfiguration.of(extension) as Either.Left).value.contains(MissingTargetPlatforms) 41 | } 42 | } 43 | 44 | When("the apple platforms are empty but target platforms are not") { 45 | extension.targetPlatforms = listOf(Either.Right(TargetPlatform(PlatformVersion.of("13")!!, listOf(TargetName.IOSarm64)))) 46 | extension.appleTargets = emptyList() 47 | 48 | Then("an error should be returned") { 49 | (PluginConfiguration.of(extension) as Either.Left).value.contains(MissingAppleTargets) 50 | } 51 | } 52 | 53 | When("the package name produced errors") { 54 | val expectedError = BlankPackageName 55 | extension.packageName = Either.Left(expectedError) 56 | 57 | Then("an error should be returned") { 58 | (PluginConfiguration.of(extension) as Either.Left).value.contains(expectedError) 59 | } 60 | } 61 | 62 | When("the package name is null and no apple framework exists") { 63 | extension.packageName = null 64 | extension.appleTargets = emptyList() 65 | 66 | Then("an error should be returned") { 67 | (PluginConfiguration.of(extension) as Either.Left).value.contains(BlankPackageName) 68 | } 69 | } 70 | 71 | When("the package name is null and apple frameworks exist") { 72 | val expectedName = "expected name" 73 | val framework = AppleFramework(AppleFrameworkOutputFile(mockk()), AppleFrameworkName(expectedName), AppleFrameworkLinkTask("")) 74 | extension.swiftToolsVersion = SwiftToolVersion.of("42") 75 | extension.packageName = null 76 | extension.appleTargets = listOf( 77 | mockk { every { getFramework(any()) } returns framework } 78 | ) 79 | 80 | Then("the base name of the first framework should be used") { 81 | PluginConfiguration.of(extension).orNull!!.packageName.value shouldBe expectedName 82 | } 83 | } 84 | 85 | When("the ZIP file name produced errors") { 86 | val expectedError = BlankZipFileName 87 | extension.zipFileName = Either.Left(expectedError) 88 | 89 | Then("an error should be returned") { 90 | (PluginConfiguration.of(extension) as Either.Left).value.contains(expectedError) 91 | } 92 | } 93 | } 94 | } 95 | 96 | private lateinit var project: Project 97 | 98 | override fun beforeTest(testCase: TestCase) { 99 | project = mockk(relaxed = true) { 100 | every { projectDir } returns File("") 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/test/kotlin/com/chromaticnoise/multiplatformswiftpackage/domain/SwiftPackageConfigurationTest.kt: -------------------------------------------------------------------------------- 1 | package com.chromaticnoise.multiplatformswiftpackage.domain 2 | 3 | import io.kotest.core.spec.style.StringSpec 4 | import io.kotest.matchers.nulls.shouldBeNull 5 | import io.kotest.matchers.shouldBe 6 | import io.kotest.matchers.string.shouldEndWith 7 | import io.kotest.matchers.string.shouldStartWith 8 | import io.mockk.mockk 9 | 10 | class SwiftPackageConfigurationTest : StringSpec() { 11 | 12 | init { 13 | "tools version property should match the tools version name" { 14 | configuration() 15 | .copy(toolVersion = SwiftToolVersion.of("42")!!) 16 | .templateProperties["toolsVersion"].shouldBe("42") 17 | } 18 | 19 | "name property should match the configured package name" { 20 | configuration() 21 | .copy(packageName = PackageName.of("expected name").orNull!!) 22 | .templateProperties["name"].shouldBe("expected name") 23 | } 24 | 25 | "platforms property should match the given platforms" { 26 | configuration() 27 | .copy(platforms = "my platforms") 28 | .templateProperties["platforms"].shouldBe("my platforms") 29 | } 30 | 31 | "is-local property should be true if distribution mode is local" { 32 | configuration() 33 | .copy(distributionMode = DistributionMode.Local) 34 | .templateProperties["isLocal"].shouldBe(true) 35 | } 36 | 37 | "is-local property should be false if distribution mode is remote" { 38 | configuration() 39 | .copy(distributionMode = DistributionMode.Remote(DistributionURL(""))) 40 | .templateProperties["isLocal"].shouldBe(false) 41 | } 42 | 43 | "url property should be null if distribution mode is local" { 44 | configuration() 45 | .copy(distributionMode = DistributionMode.Local) 46 | .templateProperties["url"].shouldBeNull() 47 | } 48 | 49 | "url property should match the value of the remote distribution url" { 50 | (configuration() 51 | .copy(distributionMode = DistributionMode.Remote(DistributionURL("my url"))) 52 | .templateProperties["url"] as String).shouldStartWith("my url") 53 | } 54 | 55 | "url property should end with the zip file name and the .zip file extension" { 56 | (configuration() 57 | .copy( 58 | distributionMode = DistributionMode.Remote(DistributionURL("url")), 59 | zipFileName = ZipFileName.of("zip file name").orNull!! 60 | ) 61 | .templateProperties["url"] as String).shouldEndWith("zip file name.zip") 62 | } 63 | 64 | "checksum property should match the value of zip checksum" { 65 | configuration() 66 | .copy(zipChecksum = "the checksum") 67 | .templateProperties["checksum"].shouldBe("the checksum") 68 | } 69 | } 70 | 71 | private fun configuration() = SwiftPackageConfiguration( 72 | project = mockk(relaxed = true), 73 | packageName = PackageName.of("package name").orNull!!, 74 | toolVersion = SwiftToolVersion.of("42")!!, 75 | platforms = "", 76 | distributionMode = DistributionMode.Local, 77 | zipChecksum = "", 78 | zipFileName = ZipFileName.of("file name").orNull!! 79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /src/test/kotlin/com/chromaticnoise/multiplatformswiftpackage/domain/SwiftToolVersionTest.kt: -------------------------------------------------------------------------------- 1 | package com.chromaticnoise.multiplatformswiftpackage.domain 2 | 3 | import io.kotest.core.spec.style.StringSpec 4 | import io.kotest.matchers.booleans.shouldBeFalse 5 | import io.kotest.matchers.booleans.shouldBeTrue 6 | import io.kotest.matchers.nulls.shouldBeNull 7 | import io.kotest.property.Arb 8 | import io.kotest.property.arbitrary.filter 9 | import io.kotest.property.arbitrary.string 10 | import io.kotest.property.forAll 11 | 12 | class SwiftToolVersionTest : StringSpec({ 13 | 14 | "empty name should not be valid" { 15 | SwiftToolVersion.of("").shouldBeNull() 16 | } 17 | 18 | "blank names should not be valid" { 19 | forAll(Arb.string().filter { it.isBlank() }) { name -> 20 | SwiftToolVersion.of(name) == null 21 | } 22 | } 23 | 24 | "two instances should be equal if their names are equal" { 25 | (SwiftToolVersion.of("equal name") == SwiftToolVersion.of("equal name")) 26 | .shouldBeTrue() 27 | } 28 | 29 | "two instances should not be equal if their names differ" { 30 | (SwiftToolVersion.of("some name") == SwiftToolVersion.of("different name")) 31 | .shouldBeFalse() 32 | } 33 | }) 34 | -------------------------------------------------------------------------------- /src/test/kotlin/com/chromaticnoise/multiplatformswiftpackage/domain/ZipFileNameTest.kt: -------------------------------------------------------------------------------- 1 | package com.chromaticnoise.multiplatformswiftpackage.domain 2 | 3 | import com.chromaticnoise.multiplatformswiftpackage.domain.PluginConfiguration.PluginConfigurationError.BlankZipFileName 4 | import io.kotest.core.spec.style.StringSpec 5 | import io.kotest.matchers.booleans.shouldBeFalse 6 | import io.kotest.matchers.booleans.shouldBeTrue 7 | import io.kotest.matchers.types.shouldBeInstanceOf 8 | import io.kotest.property.Arb 9 | import io.kotest.property.arbitrary.filter 10 | import io.kotest.property.arbitrary.string 11 | import io.kotest.property.forAll 12 | 13 | class ZipFileNameTest : StringSpec({ 14 | 15 | "empty name should not be valid" { 16 | ZipFileName.of("").leftValueOrNull!!.shouldBeInstanceOf() 17 | } 18 | 19 | "blank names should not be valid" { 20 | forAll(Arb.string().filter { it.isBlank() }) { name -> 21 | ZipFileName.of(name).leftValueOrNull is BlankZipFileName 22 | } 23 | } 24 | 25 | "two instances should be equal if their names are equal" { 26 | (ZipFileName.of("equal name") == ZipFileName.of("equal name")) 27 | .shouldBeTrue() 28 | } 29 | 30 | "two instances should not be equal if their names differ" { 31 | (ZipFileName.of("some name") == ZipFileName.of("different name")) 32 | .shouldBeFalse() 33 | } 34 | 35 | "file extension should be .zip" { 36 | forAll(Arb.string().filter { it.isNotBlank() }) { name -> 37 | ZipFileName.of(name).orNull!!.nameWithExtension.endsWith(".zip") 38 | } 39 | } 40 | }) 41 | -------------------------------------------------------------------------------- /src/test/kotlin/com/chromaticnoise/multiplatformswiftpackage/dsl/TargetPlatformDslTest.kt: -------------------------------------------------------------------------------- 1 | package com.chromaticnoise.multiplatformswiftpackage.dsl 2 | 3 | import com.chromaticnoise.multiplatformswiftpackage.domain.* 4 | import com.chromaticnoise.multiplatformswiftpackage.domain.PluginConfiguration.PluginConfigurationError 5 | import com.chromaticnoise.multiplatformswiftpackage.domain.PluginConfiguration.PluginConfigurationError.InvalidTargetName 6 | import com.chromaticnoise.multiplatformswiftpackage.dsl.TargetPlatformDsl.PlatformVersionDsl 7 | import io.kotest.core.spec.style.StringSpec 8 | import io.kotest.matchers.collections.shouldBeEmpty 9 | import io.kotest.matchers.nulls.shouldNotBeNull 10 | import io.kotest.property.Arb 11 | import io.kotest.property.arbitrary.filter 12 | import io.kotest.property.arbitrary.string 13 | import io.kotest.property.forAll 14 | 15 | class TargetPlatformDslTest : StringSpec() { 16 | 17 | init { 18 | "adding ios targets should add arm 64 target" { 19 | TargetPlatformDsl().apply { iOS(someVersion) }.targetPlatforms 20 | .shouldHaveTarget("iosArm64") 21 | } 22 | 23 | "adding ios targets should add x64 target" { 24 | TargetPlatformDsl().apply { iOS(someVersion) }.targetPlatforms 25 | .shouldHaveTarget("iosX64") 26 | } 27 | 28 | "adding watchOS targets should add arm 32 target" { 29 | TargetPlatformDsl().apply { watchOS(someVersion) }.targetPlatforms 30 | .shouldHaveTarget("watchosArm32") 31 | } 32 | 33 | "adding watchOS targets should add arm 64 target" { 34 | TargetPlatformDsl().apply { watchOS(someVersion) }.targetPlatforms 35 | .shouldHaveTarget("watchosArm64") 36 | } 37 | 38 | "adding watchOS targets should add x86 target" { 39 | TargetPlatformDsl().apply { watchOS(someVersion) }.targetPlatforms 40 | .shouldHaveTarget("watchosX86") 41 | } 42 | 43 | "adding tvOS targets should add arm 64 target" { 44 | TargetPlatformDsl().apply { tvOS(someVersion) }.targetPlatforms 45 | .shouldHaveTarget("tvosArm64") 46 | } 47 | 48 | "adding tvOS targets should add x64 target" { 49 | TargetPlatformDsl().apply { tvOS(someVersion) }.targetPlatforms 50 | .shouldHaveTarget("tvosX64") 51 | } 52 | 53 | "adding macOS targets should add x64 target" { 54 | TargetPlatformDsl().apply { macOS(someVersion) }.targetPlatforms 55 | .shouldHaveTarget("macosX64") 56 | } 57 | 58 | "adding target without names should not add a platform target" { 59 | TargetPlatformDsl().apply { targets(version = someVersion) }.targetPlatforms 60 | .shouldBeEmpty() 61 | } 62 | 63 | "adding target with empty name should add an invalid-name error" { 64 | TargetPlatformDsl().apply { targets("", version = someVersion) }.targetPlatforms 65 | .shouldHaveError(InvalidTargetName("")) 66 | } 67 | 68 | "adding target with blank name should add an invalid-name error" { 69 | forAll(Arb.string().filter { it.isBlank() }) { name -> 70 | TargetPlatformDsl().apply { targets(name, version = someVersion) }.targetPlatforms.errors.firstOrNull { 71 | it == InvalidTargetName(name) 72 | } != null 73 | } 74 | } 75 | 76 | "adding target with unknown name should add an invalid-name error" { 77 | forAll(Arb.string().filter { TargetName.of(it) == null }) { name -> 78 | TargetPlatformDsl().apply { targets(name, version = someVersion) }.targetPlatforms.errors.firstOrNull { 79 | it == InvalidTargetName(name) 80 | } != null 81 | } 82 | } 83 | 84 | "adding target with invalid version should not add a platform target" { 85 | TargetPlatformDsl().apply { targets("target", version = invalidVersion) }.targetPlatforms 86 | .shouldBeEmpty() 87 | } 88 | } 89 | 90 | private val someVersion: (PlatformVersionDsl) -> Unit = { it.v("13") } 91 | private val invalidVersion: (PlatformVersionDsl) -> Unit = { it.v("") } 92 | 93 | private fun Collection, TargetPlatform>>.shouldHaveTarget(name: String) = 94 | platforms.firstOrNull { 95 | it.targets.contains(TargetName.of(name)!!) 96 | }.shouldNotBeNull() 97 | 98 | private fun Collection, TargetPlatform>>.shouldHaveError(expectedError: PluginConfigurationError) = 99 | errors.firstOrNull { 100 | it == expectedError 101 | }.shouldNotBeNull() 102 | } 103 | --------------------------------------------------------------------------------