├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── build_mac.yml │ ├── deploy.yml │ └── release.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── NOTES.md ├── README.md ├── RELEASING.md ├── TESTING.md ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── kmmbridge-announcement.png ├── kmmbridge-github ├── build.gradle.kts └── src │ └── main │ └── kotlin │ ├── Extensions.kt │ └── co │ └── touchlab │ └── kmmbridge │ └── github │ ├── GithubReleaseArtifactManager.kt │ ├── KMMBridgeGitHubPlugin.kt │ └── internal │ ├── GithubApi.kt │ ├── GithubCalls.kt │ └── ProcessHelper.kt ├── kmmbridge-gitlab ├── build.gradle.kts └── src │ └── main │ └── kotlin │ ├── Extensions.kt │ └── co │ └── touchlab │ └── kmmbridge │ └── gitlab │ ├── KMMBridgeGitLabPlugin.kt │ └── internal │ └── GitLabApi.kt ├── kmmbridge-test ├── build.gradle.kts └── src │ └── main │ └── kotlin │ ├── Extensions.kt │ └── co │ └── touchlab │ └── kmmbridge │ └── test │ ├── KMMBridgeTestPlugin.kt │ ├── TestArtifactManager.kt │ └── TestUploadArtifactManager.kt ├── kmmbridge ├── build.gradle.kts └── src │ ├── main │ └── kotlin │ │ └── co │ │ └── touchlab │ │ └── kmmbridge │ │ ├── Constants.kt │ │ ├── KMMBridge.kt │ │ ├── KmmBridgeExtension.kt │ │ ├── ProjectExtensions.kt │ │ ├── artifactmanager │ │ ├── ArtifactManager.kt │ │ ├── AwsS3PublicArtifactManager.kt │ │ └── MavenPublishArtifactManager.kt │ │ ├── dependencymanager │ │ ├── CocoapodsDependencyManager.kt │ │ ├── DependencyManager.kt │ │ └── SpmDependencyManager.kt │ │ ├── dsl │ │ └── TargetPlatformDsl.kt │ │ └── internal │ │ ├── ProjectExtensionsInternal.kt │ │ ├── StringExtensions.kt │ │ └── domain │ │ ├── PlatformVersion.kt │ │ ├── SwiftToolVersion.kt │ │ ├── TargetName.kt │ │ ├── TargetPlatform.kt │ │ └── extensions.kt │ └── test │ └── kotlin │ └── co │ └── touchlab │ └── kmmbridge │ ├── ArtifactManagerTest.kt │ ├── BasePluginTest.kt │ ├── KmmBridgeExtensionTest.kt │ ├── NonKmmBridgeTasksTest.kt │ ├── ProcessHelper.kt │ ├── SpmLocalDevTest.kt │ └── dependencyManager │ └── PackageFileUpdateTest.kt ├── settings.gradle.kts └── test-projects └── basic ├── .gitignore ├── LICENSE.txt ├── Package.swift ├── README.md ├── allshared ├── build.gradle.kts └── src │ └── iosMain │ └── kotlin │ └── co.touchlab │ └── kmmbridgetest │ └── StartSDK.kt ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Summary 2 | 3 | 4 | 5 | ## Details 6 | 7 | 8 | 9 | ## Reproduction 10 | 11 | 12 | 13 | ## Expected result 14 | 15 | 16 | 17 | ## Current state 18 | 19 | 20 | 21 | ## Possible Fix 22 | 23 | 24 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Issue: https://github.com/touchlab/KMMBridge/issues/[issue number] 5 | 6 | ## Summary 7 | 8 | 9 | ## Fix 10 | 11 | 12 | ## Testing 13 | 14 | - `./gradlew test` 15 | - `./gradlew build` 16 | - manual testing 17 | -------------------------------------------------------------------------------- /.github/workflows/build_mac.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | on: 3 | pull_request: 4 | push: 5 | workflow_dispatch: 6 | 7 | jobs: 8 | build: 9 | runs-on: macos-latest 10 | steps: 11 | - name: Checkout the repo 12 | uses: actions/checkout@v4 13 | 14 | - uses: actions/setup-java@v4 15 | with: 16 | distribution: "adopt" 17 | java-version: "17" 18 | 19 | - name: Setup Gradle 20 | uses: gradle/actions/setup-gradle@v4 21 | 22 | - name: Write Faktory Server Code 23 | run: echo ${{ secrets.TOUCHLAB_TEST_ARTIFACT_CODE }} > kmmbridge/TOUCHLAB_TEST_ARTIFACT_CODE 24 | 25 | - name: Read Faktory Server Code 26 | run: cat kmmbridge/TOUCHLAB_TEST_ARTIFACT_CODE 27 | 28 | - name: Local Publish For Tests 29 | run: ./gradlew publishToMavenLocal --no-daemon --stacktrace --build-cache -PRELEASE_SIGNING_ENABLED=false -PVERSION_NAME=9.9.9 30 | 31 | - name: Build 32 | run: ./gradlew build --no-daemon --stacktrace --build-cache 33 | 34 | env: 35 | GRADLE_OPTS: -Dkotlin.incremental=false -Dorg.gradle.jvmargs="-Xmx4g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:MaxMetaspaceSize=512m" -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: deploy 2 | on: workflow_dispatch 3 | 4 | jobs: 5 | deploy: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout the repo 9 | uses: actions/checkout@v4 10 | 11 | - name: Setup Gradle 12 | uses: gradle/actions/setup-gradle@v4 13 | 14 | - uses: touchlab/read-property@0.1 15 | id: version-name 16 | with: 17 | file: ./gradle.properties 18 | property: VERSION_NAME 19 | 20 | - name: Echo Version 21 | run: echo "${{ steps.version-name.outputs.propVal }}" 22 | 23 | - name: Publish Plugin 24 | run: ./gradlew publish --no-daemon --stacktrace --no-build-cache 25 | env: 26 | ORG_GRADLE_PROJECT_SONATYPE_NEXUS_USERNAME: ${{ secrets.SONATYPE_NEXUS_USERNAME }} 27 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} 28 | ORG_GRADLE_PROJECT_SONATYPE_NEXUS_PASSWORD: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} 29 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} 30 | ORG_GRADLE_PROJECT_SIGNING_KEY: ${{ secrets.SIGNING_KEY }} 31 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_KEY }} 32 | 33 | # - name: Publish Gradle Plugin 34 | # if: ${{ contains(steps.read_version.outputs.value, 'SNAPSHOT') == false }} 35 | # run: ./gradlew publishPlugins --no-daemon --stacktrace --no-build-cache 36 | # env: 37 | # GRADLE_PUBLISH_KEY: ${{ secrets.GRADLE_PUBLISH_KEY }} 38 | # GRADLE_PUBLISH_SECRET: ${{ secrets.GRADLE_PUBLISH_SECRET }} 39 | # ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_KEY }} 40 | 41 | env: 42 | GRADLE_OPTS: -Dkotlin.incremental=false -Dorg.gradle.jvmargs="-Xmx3g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:MaxMetaspaceSize=512m" 43 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: workflow_dispatch 3 | 4 | jobs: 5 | release: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout the repo 9 | uses: actions/checkout@v4 10 | 11 | - uses: actions/setup-java@v4 12 | with: 13 | distribution: "adopt" 14 | java-version: "17" 15 | 16 | - name: Setup Gradle 17 | uses: gradle/actions/setup-gradle@v4 18 | 19 | - uses: touchlab/read-property@0.1 20 | id: version-name 21 | with: 22 | file: ./gradle.properties 23 | property: VERSION_NAME 24 | 25 | - name: Echo Version 26 | run: echo "${{ steps.version-name.outputs.propVal }}" 27 | 28 | - name: Finish Maven Central Release 29 | run: ./gradlew releaseRepository --no-daemon --stacktrace --no-build-cache 30 | env: 31 | ORG_GRADLE_PROJECT_SONATYPE_NEXUS_USERNAME: ${{ secrets.SONATYPE_NEXUS_USERNAME }} 32 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} 33 | ORG_GRADLE_PROJECT_SONATYPE_NEXUS_PASSWORD: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} 34 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} 35 | ORG_GRADLE_PROJECT_SIGNING_KEY: ${{ secrets.SIGNING_KEY }} 36 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_KEY }} 37 | 38 | - name: Create Release 39 | if: ${{ contains(steps.version-match.outputs.group1, 'SNAPSHOT') == false }} 40 | uses: touchlab/release-action@v1.10.0 41 | with: 42 | tag: ${{ steps.version-name.outputs.propVal }} 43 | 44 | env: 45 | GRADLE_OPTS: -Dkotlin.incremental=false -Dorg.gradle.jvmargs="-Xmx3g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:MaxMetaspaceSize=512m" 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | build 7 | /buildSrc/build 8 | /captures 9 | .externalNativeBuild 10 | .cxx 11 | *.xcuserstate 12 | *.xcbkptlist 13 | !/.idea/codeStyles/* 14 | !/.idea/inspectionProfiles/* 15 | .kotlin 16 | TOUCHLAB_TEST_ARTIFACT_CODE -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, caste, color, religion, or sexual 11 | identity and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the overall 27 | community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or advances of 32 | any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email address, 36 | without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at [codeofconduct@touchlab.co](mailto:codeofconduct@touchlab.co). 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series of 86 | actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or permanent 93 | ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within the 113 | community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.1, available at 119 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 126 | [https://www.contributor-covenant.org/translations][translations]. 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 130 | [Mozilla CoC]: https://github.com/mozilla/diversity 131 | [FAQ]: https://www.contributor-covenant.org/faq 132 | [translations]: https://www.contributor-covenant.org/translations 133 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | In the first place, thank you for thinking about contributing to KMMBridge! 4 | Here you can find a set of guidelines for pitching in. 5 | 6 | ## Questions 7 | 8 | If you have any questions, please, contact us in the Kotlin [Community Slack](https://kotlinlang.slack.com/) in the [touchlab-support](https://kotlinlang.slack.com/archives/CTJB58X7X) channel. To join the Kotlin Community Slack, [request access here](http://slack.kotlinlang.org/). 9 | 10 | For direct assistance, please [reach out to Touchlab](https://touchlab.co/contact-us/) to discuss support options. 11 | 12 | ## Set up environment 13 | 14 | For instructions on how to set up your environment and run the project, refer to the [documentation website](https://touchlab.github.io/KMMBridge/) and [quickstart with KMM tutorial](https://touchlab.co/quick-start-with-kmmbridge-1-hour-tutorial/). 15 | 16 | ## Create an issue 17 | 18 | If you have stumbled across a bug or have a good feature suggestion / enhancement, you can create an [issue](https://github.com/touchlab/KMMBridge/issues), but please don't mistake it for the general KMM helpline. You can get answers for general questions in Slack. Please, fill in carefully all the info the **issue template** suggests. It will save us time when investigating the problem. There might be a bit of a delay until we get to your ticket, so we ask for your patience. 19 | 20 | ## Submit a merge request 21 | 22 | If you wish to participate in submitting code changes, to start with, you can look for issues tagged with **good first issue** (if there is none look for an issue that looks adequate to your skills). 23 | In case you feel like making significant changes or adding features, please discuss with the team first before you start working on it, to ensure we are on the same page. 24 | When your fix / feature is ready, create a merge request using the **pull request template** and fill in as much information as possible. 25 | All merge requests need to pass a code review from our team member, and subsequently they are approved or rejected with a reason. It might take some time before we get to your merge request, but don't worry, it didn't get lost. 26 | 27 | ## Lint 28 | 29 | We use Lint and KtLint in our projects to ensure the Kotlin code guidelines are met. Please, check that your suggested changes follow these guidelines as well by running `./gradlew :ktlintCheck` or you can use [KtLint plugin](https://plugins.jetbrains.com/plugin/15057-ktlint-unofficial-) for IntelliJ IDEA or Android Studio. Some existing files may not conform with this standard, please be careful about refactoring them as part of your merge request, with many refactored lines the actual changes may get lost when reviewing. 30 | 31 | ## Testing 32 | 33 | Before creating a merge request, make sure all the tests pass by `./gradlew test` to run the tests locally. 34 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2021 Touchlab 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. -------------------------------------------------------------------------------- /NOTES.md: -------------------------------------------------------------------------------- 1 | # Local publish test 2 | 3 | To test publishing from your local machine, use the following command: 4 | 5 | ```shell 6 | ./gradlew kmmBridgePublish -PENABLE_PUBLISHING=true -PGITHUB_PUBLISH_TOKEN=[GitHub PAT] -PGITHUB_REPO=[org]/[repo] --no-daemon --stacktrace 7 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![GitHub release (latest by date)](https://img.shields.io/github/v/release/touchlab/KMMBridge) 2 | ![GitHub](https://img.shields.io/github/license/touchlab/KMMBridge) 3 | [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](CODE_OF_CONDUCT.md) 4 | 5 | # KMMBridge for Teams 6 | 7 | ![KMMBridge for Teams](kmmbridge-announcement.png) 8 | 9 | KMMBridge is a set of Gradle tooling that facilitates publishing and consuming pre-built KMP (Kotlin Multiplatform) Xcode Framework binaries. 10 | 11 | ## Documentation Website 12 | 13 | See [https://touchlab.co/kmmbridge/](https://touchlab.co/kmmbridge/) for setup and detailed documentation. 14 | 15 | To provide feedback about your experience with KMMBridge join the conversation in the Kotlinlang [#touchlab-tools](https://kotlinlang.slack.com/archives/CTJB58X7X) Slack channel. 16 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | 4 | -------------------------------------------------------------------------------- /TESTING.md: -------------------------------------------------------------------------------- 1 | # KMMBridge Project Tests 2 | 3 | Gradle guidance suggests using include builds to test Gradle plugins. However, after a significant effort to avoid JVM classpath and other related issues, we decided to run tests by starting external Gradle builds. 4 | 5 | To do that, we need to locally publish KMMBridge, then point tests projects at that new version. For each test: 6 | 7 | * A temp folder is constructed 8 | * A sample app project is copied to that folder 9 | * A command line process is run, generally running Gradle to perform whatever task we intend to test 10 | 11 | ## Publishing KMMBridge locally 12 | 13 | We want to publish KMMBridge as version `9.9.9`. This is a fake local version, just for testing. To do that, run the following on the root folder of KMMBridge: 14 | 15 | ```shell 16 | ./gradlew publishToMavenLocal -PVERSION_NAME=9.9.9 17 | ``` 18 | 19 | ## Editing the test project 20 | 21 | Our simple test project lives at `test-projects/basic`. It points at KMMBridge version `9.9.9`. You should be able to open it directly with IJ or AS. 22 | 23 | ## Tests 24 | 25 | See class `co.touchlab.kmmbridge.SimplePluginTest`. `fun setup()` copies the test project an initializes the test project. 26 | 27 | Here is a sample test: 28 | 29 | ```kotlin 30 | @Test 31 | fun runSpmDevBuild() { 32 | val result = ProcessHelper.runSh("./gradlew spmDevBuild --stacktrace", workingDir = testProjectDir) 33 | logExecResult(result) 34 | assertEquals(0, result.status) 35 | } 36 | ``` -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Touchlab. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 4 | * in compliance with the License. You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software distributed under the License 9 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 10 | * or implied. See the License for the specific language governing permissions and limitations under 11 | * the License. 12 | */ 13 | plugins { 14 | alias(libs.plugins.kotlin) apply false 15 | id("org.jetbrains.kotlin.plugin.allopen") version "1.9.0" apply false 16 | alias(libs.plugins.maven.publish) apply false 17 | } 18 | 19 | subprojects { 20 | repositories { 21 | gradlePluginPortal() 22 | mavenCentral() 23 | } 24 | 25 | extensions.findByType()?.apply { 26 | jvmToolchain(17) 27 | } 28 | 29 | val GROUP: String by project 30 | val VERSION_NAME: String by project 31 | 32 | group = GROUP 33 | version = VERSION_NAME 34 | 35 | afterEvaluate { 36 | tasks.getByName("test") { 37 | useJUnitPlatform() 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2020-2024 Touchlab. 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 4 | # in compliance with the License. You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software distributed under the License 9 | # is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 10 | # or implied. See the License for the specific language governing permissions and limitations under 11 | # the License. 12 | # 13 | kotlin.code.style=official 14 | android.useAndroidX=true 15 | org.gradle.jvmargs=-Xmx4g 16 | 17 | SONATYPE_HOST=DEFAULT 18 | RELEASE_SIGNING_ENABLED=true 19 | 20 | GROUP=co.touchlab.kmmbridge 21 | 22 | VERSION_NAME=1.2.1 23 | 24 | POM_URL=https://github.com/touchlab/KMMBridge 25 | POM_DESCRIPTION=KMP Xcode XCFramework Packaging and tooling 26 | POM_NAME=KMMBridge 27 | POM_SCM_URL=https://github.com/touchlab/KMMBridge 28 | POM_SCM_CONNECTION=scm:git:git://github.com/touchlab/KMMBridge.git 29 | POM_SCM_DEV_CONNECTION=scm:git:git://github.com/touchlab/KMMBridge.git 30 | 31 | POM_LICENCE_NAME=The Apache Software License, Version 2.0 32 | POM_LICENCE_URL=https://www.apache.org/licenses/LICENSE-2.0.txt 33 | POM_LICENCE_DIST=repo 34 | 35 | POM_DEVELOPER_ID=kpgalligan 36 | POM_DEVELOPER_NAME=Kevin Galligan 37 | POM_DEVELOPER_ORG=Touchlab 38 | POM_DEVELOPER_URL=https://touchlab.co/ 39 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | kotlin = "2.0.20" 3 | mavenPublish = "0.30.0" 4 | 5 | [libraries] 6 | aws = { module = "software.amazon.awssdk:s3", version = "2.23.8" } 7 | okhttp = { module = "com.squareup.okhttp3:okhttp", version = "4.12.0" } 8 | gson = { module = "com.google.code.gson:gson", version = "2.10.1" } 9 | kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin-api", version.ref = "kotlin" } 10 | 11 | [plugins] 12 | kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } 13 | maven-publish = { id = "com.vanniktech.maven.publish.base", version.ref = "mavenPublish" } 14 | 15 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/touchlab/KMMBridge/d146084e29515c07921c7fa369568977e9026869/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-8.11.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original 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 POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Stop when "xargs" is not available. 209 | if ! command -v xargs >/dev/null 2>&1 210 | then 211 | die "xargs is not available" 212 | fi 213 | 214 | # Use "xargs" to parse quoted args. 215 | # 216 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 217 | # 218 | # In Bash we could simply go: 219 | # 220 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 221 | # set -- "${ARGS[@]}" "$@" 222 | # 223 | # but POSIX shell has neither arrays nor command substitution, so instead we 224 | # post-process each arg (as a line of input to sed) to backslash-escape any 225 | # character that might be a shell metacharacter, then use eval to reverse 226 | # that process (while maintaining the separation between arguments), and wrap 227 | # the whole thing up as a single "set" statement. 228 | # 229 | # This will of course break if any of these variables contains a newline or 230 | # an unmatched quote. 231 | # 232 | 233 | eval "set -- $( 234 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 235 | xargs -n1 | 236 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 237 | tr '\n' ' ' 238 | )" '"$@"' 239 | 240 | exec "$JAVACMD" "$@" 241 | -------------------------------------------------------------------------------- /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% equ 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% equ 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 | set EXIT_CODE=%ERRORLEVEL% 84 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 85 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 86 | exit /b %EXIT_CODE% 87 | 88 | :mainEnd 89 | if "%OS%"=="Windows_NT" endlocal 90 | 91 | :omega 92 | -------------------------------------------------------------------------------- /kmmbridge-announcement.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/touchlab/KMMBridge/d146084e29515c07921c7fa369568977e9026869/kmmbridge-announcement.png -------------------------------------------------------------------------------- /kmmbridge-github/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("PropertyName") 2 | 3 | /* 4 | * Copyright (c) 2024 Touchlab. 5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 6 | * in compliance with the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | */ 15 | 16 | plugins { 17 | `kotlin-dsl` 18 | alias(libs.plugins.kotlin) 19 | id("org.jetbrains.kotlin.plugin.allopen") 20 | id("java-gradle-plugin") 21 | alias(libs.plugins.maven.publish) 22 | id("com.gradle.plugin-publish") version "1.0.0" 23 | } 24 | 25 | @Suppress("UnstableApiUsage") 26 | gradlePlugin { 27 | website = "https://github.com/touchlab/KMMBridge" 28 | vcsUrl = "https://github.com/touchlab/KMMBridge.git" 29 | description = 30 | "KMMBridge is a set of Gradle tooling that facilitates publishing and consuming pre-built KMM (Kotlin Multiplatform Mobile) Xcode Framework binaries." 31 | plugins { 32 | register("kmmbridge-github-plugin") { 33 | id = "co.touchlab.kmmbridge.github" 34 | implementationClass = "co.touchlab.kmmbridge.github.KMMBridgeGitHubPlugin" 35 | displayName = "KMMBridge/GitHub" 36 | tags = listOf( 37 | "kmm", 38 | "kotlin", 39 | "multiplatform", 40 | "mobile", 41 | "ios", 42 | "xcode", 43 | "framework", 44 | "binary", 45 | "publish", 46 | "consume" 47 | ) 48 | } 49 | } 50 | } 51 | 52 | dependencies { 53 | implementation(kotlin("stdlib")) 54 | implementation(libs.okhttp) 55 | implementation(libs.gson) 56 | api(project(":kmmbridge")) 57 | 58 | testImplementation(kotlin("test")) 59 | } 60 | 61 | mavenPublishing { 62 | publishToMavenCentral() 63 | val releaseSigningEnabled = 64 | project.properties["RELEASE_SIGNING_ENABLED"]?.toString()?.equals("false", ignoreCase = true) != true 65 | if (releaseSigningEnabled) signAllPublications() 66 | @Suppress("UnstableApiUsage") 67 | pomFromGradleProperties() 68 | @Suppress("UnstableApiUsage") 69 | configureBasedOnAppliedPlugins() 70 | } -------------------------------------------------------------------------------- /kmmbridge-github/src/main/kotlin/Extensions.kt: -------------------------------------------------------------------------------- 1 | import co.touchlab.kmmbridge.KmmBridgeExtension 2 | import co.touchlab.kmmbridge.github.GithubReleaseArtifactManager 3 | import co.touchlab.kmmbridge.github.internal.githubPublishTokenOrNull 4 | import co.touchlab.kmmbridge.github.internal.githubPublishUser 5 | import co.touchlab.kmmbridge.github.internal.githubRepoOrNull 6 | import co.touchlab.kmmbridge.github.kmmBridgeExtension 7 | import co.touchlab.kmmbridge.publishingExtension 8 | import org.gradle.api.Project 9 | import java.net.URI 10 | 11 | @Suppress("unused") 12 | fun Project.gitHubReleaseArtifacts( 13 | repository: String? = null, 14 | releasString: String? = null, 15 | useExistingRelease: Boolean = false 16 | ) { 17 | kmmBridgeExtension.setupGitHubReleaseArtifacts( 18 | GithubReleaseArtifactManager( 19 | repository, 20 | releasString, 21 | useExistingRelease 22 | ) 23 | ) 24 | } 25 | 26 | private fun KmmBridgeExtension.setupGitHubReleaseArtifacts( 27 | githubReleaseArtifactManager: GithubReleaseArtifactManager 28 | ) { 29 | artifactManager.setAndFinalize(githubReleaseArtifactManager) 30 | } 31 | 32 | /** 33 | * Helper function to support GitHub Packages publishing. Use with https://github.com/touchlab/KMMBridgeGithubWorkflow 34 | * or pass in a valid GitHub token with GITHUB_PUBLISH_TOKEN. Defaults user to "cirunner", which can be overridden with 35 | * GITHUB_PUBLISH_USER. 36 | * 37 | * Generally, just add the following in the Gradle build file. 38 | * 39 | * addGithubPackagesRepository() 40 | */ 41 | @Suppress("unused") 42 | fun Project.addGithubPackagesRepository() { 43 | publishingExtension.apply { 44 | val githubPublishUser = project.githubPublishUser ?: "cirunner" 45 | val githubRepo = project.githubRepoOrNull ?: return 46 | val githubPublishToken = project.githubPublishTokenOrNull ?: return 47 | repositories.maven { 48 | name = "GitHubPackages" 49 | url = URI.create("https://maven.pkg.github.com/$githubRepo") 50 | credentials { 51 | username = githubPublishUser 52 | password = githubPublishToken 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /kmmbridge-github/src/main/kotlin/co/touchlab/kmmbridge/github/GithubReleaseArtifactManager.kt: -------------------------------------------------------------------------------- 1 | package co.touchlab.kmmbridge.github 2 | 3 | import co.touchlab.kmmbridge.KmmBridgeExtension 4 | import co.touchlab.kmmbridge.artifactmanager.ArtifactManager 5 | import co.touchlab.kmmbridge.github.internal.GithubCalls 6 | import co.touchlab.kmmbridge.github.internal.githubArtifactIdentifierName 7 | import co.touchlab.kmmbridge.github.internal.githubArtifactReleaseId 8 | import co.touchlab.kmmbridge.github.internal.githubPublishToken 9 | import co.touchlab.kmmbridge.github.internal.githubRepo 10 | import org.gradle.api.GradleException 11 | import org.gradle.api.Project 12 | import org.gradle.api.Task 13 | import org.gradle.api.tasks.Input 14 | import org.gradle.api.tasks.TaskProvider 15 | import org.gradle.kotlin.dsl.getByType 16 | import java.io.File 17 | import kotlin.properties.Delegates 18 | 19 | open class GithubReleaseArtifactManager( 20 | private val repository: String?, 21 | private val releaseString: String?, 22 | @Deprecated("Releases should be created externally. This parameter controls the flow for releases created " + 23 | "by this class, which will eventually be unsupported.") 24 | private val useExistingRelease: Boolean 25 | ) : ArtifactManager { 26 | 27 | @get:Input 28 | lateinit var releaseVersion: String 29 | 30 | @get:Input 31 | lateinit var repoName: String 32 | 33 | @get:Input 34 | lateinit var frameworkName: String 35 | 36 | @get:Input 37 | lateinit var artifactIdentifierName: String 38 | 39 | @get:Input 40 | var artifactReleaseId by Delegates.notNull() 41 | 42 | // TODO: This value is stored in the config cache. It is encrypted, but this still feels insecure. Review alternatives. 43 | // https://docs.gradle.org/current/userguide/configuration_cache.html#config_cache:secrets 44 | lateinit var githubPublishToken: String 45 | 46 | override fun configure( 47 | project: Project, version: String, uploadTask: TaskProvider, kmmPublishTask: TaskProvider 48 | ) { 49 | this.releaseVersion = releaseString ?: project.version.toString() 50 | this.repoName = this.repository ?: project.githubRepo 51 | this.githubPublishToken = project.githubPublishToken 52 | this.frameworkName = project.kmmBridgeExtension.frameworkName.get() 53 | artifactReleaseId = project.githubArtifactReleaseId?.toInt() ?: 0 54 | this.artifactIdentifierName = project.githubArtifactIdentifierName ?: "" 55 | } 56 | 57 | override fun deployArtifact(task: Task, zipFilePath: File, version: String): String { 58 | val uploadReleaseId = if (artifactReleaseId == 0) { 59 | val existingReleaseId = GithubCalls.findReleaseId( 60 | githubPublishToken, repoName, releaseVersion 61 | ) 62 | 63 | task.logger.info("existingReleaseId: $existingReleaseId") 64 | 65 | if (existingReleaseId != null && !useExistingRelease) { 66 | throw GradleException("Release for '$releaseVersion' exists. Set 'useExistingRelease = true' to update existing releases.") 67 | } 68 | 69 | val idReply: Int = existingReleaseId ?: GithubCalls.createRelease( 70 | githubPublishToken, repoName, releaseVersion, null 71 | ) 72 | 73 | task.logger.info("GitHub Release created with id: $idReply") 74 | 75 | idReply 76 | } else { 77 | artifactReleaseId 78 | } 79 | 80 | val fileName = artifactName(version, useExistingRelease) 81 | 82 | val uploadUrl = GithubCalls.uploadZipFile(githubPublishToken, zipFilePath, repoName, uploadReleaseId, fileName) 83 | return "${uploadUrl}.zip" 84 | } 85 | 86 | private fun artifactName(versionString: String, useExistingRelease: Boolean): String { 87 | return if (useExistingRelease) { 88 | "$frameworkName-${versionString}-${(System.currentTimeMillis() / 1000)}.xcframework.zip" 89 | } else { 90 | uploadZipFileName(versionString) 91 | } 92 | } 93 | 94 | open fun uploadZipFileName(versionString: String) = if (artifactIdentifierName.isNotEmpty()) { 95 | "$frameworkName-${artifactIdentifierName}.xcframework.zip" 96 | } else { 97 | "$frameworkName.xcframework.zip" 98 | } 99 | } 100 | 101 | internal val Project.kmmBridgeExtension get() = extensions.getByType() -------------------------------------------------------------------------------- /kmmbridge-github/src/main/kotlin/co/touchlab/kmmbridge/github/KMMBridgeGitHubPlugin.kt: -------------------------------------------------------------------------------- 1 | package co.touchlab.kmmbridge.github 2 | 3 | import co.touchlab.kmmbridge.BaseKMMBridgePlugin 4 | import co.touchlab.kmmbridge.TASK_GROUP_NAME 5 | import co.touchlab.kmmbridge.findStringProperty 6 | import co.touchlab.kmmbridge.github.internal.procRunFailLog 7 | import org.gradle.api.Action 8 | import org.gradle.api.Project 9 | import org.gradle.api.Task 10 | import java.io.File 11 | import java.nio.file.Files 12 | 13 | @Suppress("unused") 14 | class KMMBridgeGitHubPlugin : BaseKMMBridgePlugin() { 15 | override fun apply(project: Project) { 16 | super.apply(project) 17 | val githubDeploySourceRepo = project.findStringProperty("githubDeploySourceRepo") 18 | val githubDeployTargetRepo = project.findStringProperty("githubDeployTargetRepo") 19 | if (githubDeploySourceRepo != null && githubDeployTargetRepo != null) { 20 | project.tasks.register("setupDeployKeys") { 21 | group = TASK_GROUP_NAME 22 | description = "Helper task to setup GitHub deploy keys. Creates an ssh public/private key pair and adds them to the target and source repos." 23 | outputs.upToDateWhen { false } // This should always run 24 | 25 | @Suppress("ObjectLiteralToLambda") 26 | doLast(object : Action { 27 | override fun execute(t: Task) { 28 | val githubDeployKeyPrefix = project.findStringProperty("githubDeployKeyPrefix") ?: "KMMBridge" 29 | 30 | val keyname = "$githubDeployKeyPrefix Key" 31 | 32 | val tempDir = Files.createTempDirectory("kmmbridge") 33 | println("Generated temp dir: $tempDir") 34 | val deployKeyName = "deploykey" 35 | val deployKeyPrivateFilePath = File(tempDir.toFile(), deployKeyName) 36 | val deployKeyPublicFilePath = File(tempDir.toFile(), "${deployKeyName}.pub") 37 | 38 | try { 39 | project.procRunFailLog( 40 | "ssh-keygen", 41 | "-t", 42 | "ed25519", 43 | "-f", 44 | deployKeyPrivateFilePath.toString(), 45 | "-C", 46 | "git@github.com:$githubDeployTargetRepo", 47 | "-P", 48 | "" 49 | ) 50 | 51 | project.procRunFailLog( 52 | "gh", 53 | "repo", 54 | "deploy-key", 55 | "add", 56 | deployKeyPublicFilePath.toString(), 57 | "-w", 58 | "-R", 59 | githubDeployTargetRepo, 60 | "-t", 61 | keyname 62 | ) 63 | 64 | project.procRunFailLog( 65 | "gh", 66 | "secret", 67 | "set", 68 | "${githubDeployKeyPrefix}_SSH_KEY", 69 | "--body", 70 | deployKeyPrivateFilePath.readText(), 71 | "-R", 72 | githubDeploySourceRepo 73 | ) 74 | } finally { 75 | deployKeyPrivateFilePath.delete() 76 | deployKeyPublicFilePath.delete() 77 | Files.deleteIfExists(tempDir) 78 | } 79 | } 80 | }) 81 | } 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /kmmbridge-github/src/main/kotlin/co/touchlab/kmmbridge/github/internal/GithubApi.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Touchlab. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 4 | * in compliance with the License. You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software distributed under the License 9 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 10 | * or implied. See the License for the specific language governing permissions and limitations under 11 | * the License. 12 | */ 13 | 14 | package co.touchlab.kmmbridge.github.internal 15 | 16 | import co.touchlab.kmmbridge.findStringProperty 17 | import org.gradle.api.Project 18 | 19 | internal val Project.githubPublishTokenOrNull: String? 20 | get() = project.property("GITHUB_PUBLISH_TOKEN") as String? 21 | 22 | internal val Project.githubPublishUser: String? 23 | get() = project.findStringProperty("GITHUB_PUBLISH_USER") 24 | 25 | internal val Project.githubRepoOrNull: String? 26 | get() { 27 | val repo = project.findStringProperty("GITHUB_REPO") ?: return null 28 | val repoWithoutGitSuffix = repo.removeSuffix(".git") 29 | val regex = Regex("((.*)[/:])?(?[^:/]+)/(?[^/]+)") 30 | val matchResult = regex.matchEntire(repoWithoutGitSuffix) 31 | if (matchResult != null) { 32 | return (matchResult.groups["owner"]!!.value + "/" + matchResult.groups["repo"]!!.value) 33 | } else { 34 | throw IllegalArgumentException("Incorrect Github repository path, should be \"Owner/Repo\"") 35 | } 36 | } 37 | 38 | internal val Project.githubPublishToken 39 | get() = (project.property("GITHUB_PUBLISH_TOKEN") 40 | ?: throw IllegalArgumentException("KMMBridge Github operations need property GITHUB_PUBLISH_TOKEN")) as String 41 | 42 | internal val Project.githubArtifactReleaseId 43 | get() = project.findStringProperty("GITHUB_ARTIFACT_RELEASE_ID") 44 | 45 | internal val Project.githubArtifactIdentifierName 46 | get() = project.findStringProperty("GITHUB_ARTIFACT_IDENTIFIER_NAME") 47 | 48 | internal val Project.githubRepo: String 49 | get() = githubRepoOrNull 50 | ?: throw IllegalArgumentException("KMMBridge Github operations need a repo param or property GITHUB_REPO") 51 | -------------------------------------------------------------------------------- /kmmbridge-github/src/main/kotlin/co/touchlab/kmmbridge/github/internal/GithubCalls.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Touchlab. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 4 | * in compliance with the License. You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software distributed under the License 9 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 10 | * or implied. See the License for the specific language governing permissions and limitations under 11 | * the License. 12 | */ 13 | 14 | package co.touchlab.kmmbridge.github.internal 15 | 16 | import com.google.gson.Gson 17 | import okhttp3.MediaType.Companion.toMediaTypeOrNull 18 | import okhttp3.OkHttpClient 19 | import okhttp3.Request 20 | import okhttp3.RequestBody 21 | import okhttp3.RequestBody.Companion.asRequestBody 22 | import okhttp3.RequestBody.Companion.toRequestBody 23 | import org.gradle.api.GradleException 24 | import java.io.File 25 | import java.net.URLEncoder 26 | import java.time.Duration 27 | 28 | object GithubCalls { 29 | private val okHttpClient = 30 | OkHttpClient.Builder().callTimeout(Duration.ofMinutes(5)).connectTimeout(Duration.ofMinutes(2)) 31 | .writeTimeout(Duration.ofMinutes(5)).readTimeout(Duration.ofMinutes(2)).build() 32 | 33 | fun createRelease(githubPublishToken: String, repo: String, tag: String, commitId: String?): Int { 34 | val gson = Gson() 35 | val createReleaseBody = if (commitId == null) { 36 | CreateReleaseBody(tag) 37 | } else { 38 | CreateReleaseWithCommitBody(tag, commitId) 39 | } 40 | val createRequest = Request.Builder().url("https://api.github.com/repos/${repo}/releases") 41 | .post(gson.toJson(createReleaseBody).toRequestBody("application/json".toMediaTypeOrNull())) 42 | .addHeader("Accept", "application/vnd.github+json").addHeader("Authorization", "Bearer $githubPublishToken") 43 | .build() 44 | 45 | val response = okHttpClient.newCall(createRequest).execute() 46 | if (!response.isSuccessful) { 47 | if (response.code == 403) { 48 | throw GradleException("Failed to create GitHub Release. Check Workflow permissions. Write permissions required.") 49 | } else { 50 | throw GradleException("Failed to create GitHub Release. Response code ${response.code}") 51 | } 52 | } 53 | 54 | return gson.fromJson(response.body!!.string(), IdReply::class.java).id 55 | } 56 | 57 | fun uploadZipFile( 58 | githubPublishToken: String, 59 | zipFilePath: File, 60 | repo: String, 61 | releaseId: Int, 62 | fileName: String 63 | ): String { 64 | val gson = Gson() 65 | val body: RequestBody = zipFilePath.asRequestBody("application/zip".toMediaTypeOrNull()) 66 | 67 | val uploadRequest = Request.Builder().url( 68 | "https://uploads.github.com/repos/${repo}/releases/${releaseId}/assets?name=${ 69 | URLEncoder.encode( 70 | fileName, "UTF-8" 71 | ) 72 | }" 73 | ).post(body).addHeader("Accept", "application/vnd.github+json").addHeader( 74 | "Authorization", 75 | "Bearer $githubPublishToken" 76 | ) 77 | .addHeader("Content-Type", "application/zip").build() 78 | 79 | val response = okHttpClient.newCall(uploadRequest).execute() 80 | if (response.code != 201) { 81 | throw GithubReleaseException("Upload call failed ${response.code}, ${response.message}") 82 | } 83 | val uploadResponseString = response.body!!.string() 84 | return gson.fromJson(uploadResponseString, UploadReply::class.java).url 85 | } 86 | 87 | fun findReleaseId( 88 | githubPublishToken: String, 89 | repoName: String, 90 | artifactReleaseTag: String 91 | ): Int? { 92 | val request: Request = 93 | Request.Builder().url("https://api.github.com/repos/${repoName}/releases/tags/${artifactReleaseTag}").get() 94 | .addHeader("Accept", "application/vnd.github+json").addHeader( 95 | "Authorization", 96 | "Bearer $githubPublishToken" 97 | ).build() 98 | 99 | val responseString = okHttpClient.newCall(request).execute().body!!.string() 100 | return if (!responseString.contains("Not Found")) { 101 | val id = Gson().fromJson(responseString, IdReply::class.java).id 102 | id.takeIf { it != 0 } 103 | } else { 104 | null 105 | } 106 | } 107 | } 108 | 109 | data class CreateReleaseWithCommitBody(val tag_name: String, val target_commitish: String) 110 | 111 | data class CreateReleaseBody(val tag_name: String) 112 | 113 | data class IdReply(var id: Int) 114 | 115 | data class UploadReply(var url: String) 116 | 117 | class GithubReleaseException(message: String, cause: Throwable? = null) : Exception(message, cause) -------------------------------------------------------------------------------- /kmmbridge-github/src/main/kotlin/co/touchlab/kmmbridge/github/internal/ProcessHelper.kt: -------------------------------------------------------------------------------- 1 | package co.touchlab.kmmbridge.github.internal 2 | 3 | import org.gradle.api.GradleException 4 | import org.gradle.api.Project 5 | import java.io.BufferedReader 6 | import java.io.File 7 | import java.io.InputStreamReader 8 | 9 | internal fun procRun(vararg params: String, dir: File?, processLines: (String, Int) -> Unit) { 10 | val processBuilder = ProcessBuilder(*params) 11 | .redirectErrorStream(true) 12 | if (dir != null) { 13 | println("*** Running proc in ${dir.path}") 14 | processBuilder.directory(dir) 15 | } 16 | 17 | val process = processBuilder 18 | .start() 19 | 20 | val streamReader = InputStreamReader(process.inputStream) 21 | val bufferedReader = BufferedReader(streamReader) 22 | var lineCount = 1 23 | 24 | bufferedReader.forEachLine { line -> 25 | processLines(line, lineCount) 26 | lineCount++ 27 | } 28 | 29 | bufferedReader.close() 30 | val returnValue = process.waitFor() 31 | if (returnValue != 0) 32 | throw GradleException("Process failed: ${params.joinToString(" ")}") 33 | } 34 | 35 | internal fun Project.procRunFailLog(vararg params: String, dir: File? = null): List { 36 | val output = mutableListOf() 37 | try { 38 | logger.info("Project.procRunFailLog: ${params.joinToString(" ")}") 39 | procRun(*params, dir = dir) { line, _ -> output.add(line) } 40 | } catch (e: Exception) { 41 | output.forEach { logger.error("error: $it") } 42 | throw e 43 | } 44 | return output 45 | } -------------------------------------------------------------------------------- /kmmbridge-gitlab/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("PropertyName") 2 | 3 | /* 4 | * Copyright (c) 2024 Touchlab. 5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 6 | * in compliance with the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | */ 15 | 16 | plugins { 17 | `kotlin-dsl` 18 | alias(libs.plugins.kotlin) 19 | id("org.jetbrains.kotlin.plugin.allopen") 20 | id("java-gradle-plugin") 21 | alias(libs.plugins.maven.publish) 22 | id("com.gradle.plugin-publish") version "1.0.0" 23 | } 24 | 25 | @Suppress("UnstableApiUsage") 26 | gradlePlugin { 27 | website = "https://github.com/touchlab/KMMBridge" 28 | vcsUrl = "https://github.com/touchlab/KMMBridge.git" 29 | description = 30 | "KMMBridge is a set of Gradle tooling that facilitates publishing and consuming pre-built KMM (Kotlin Multiplatform Mobile) Xcode Framework binaries." 31 | plugins { 32 | register("kmmbridge-gitlab-plugin") { 33 | id = "co.touchlab.kmmbridge.gitlab" 34 | implementationClass = "co.touchlab.kmmbridge.gitlab.KMMBridgeGitLabPlugin" 35 | displayName = "KMMBridge/GitLab" 36 | tags = listOf( 37 | "kmm", 38 | "kotlin", 39 | "multiplatform", 40 | "mobile", 41 | "ios", 42 | "xcode", 43 | "framework", 44 | "binary", 45 | "publish", 46 | "consume" 47 | ) 48 | } 49 | } 50 | } 51 | 52 | dependencies { 53 | implementation(kotlin("stdlib")) 54 | implementation(libs.okhttp) 55 | implementation(libs.gson) 56 | api(project(":kmmbridge")) 57 | 58 | testImplementation(kotlin("test")) 59 | } 60 | 61 | mavenPublishing { 62 | publishToMavenCentral() 63 | val releaseSigningEnabled = 64 | project.properties["RELEASE_SIGNING_ENABLED"]?.toString()?.equals("false", ignoreCase = true) != true 65 | if (releaseSigningEnabled) signAllPublications() 66 | @Suppress("UnstableApiUsage") 67 | pomFromGradleProperties() 68 | @Suppress("UnstableApiUsage") 69 | configureBasedOnAppliedPlugins() 70 | } -------------------------------------------------------------------------------- /kmmbridge-gitlab/src/main/kotlin/Extensions.kt: -------------------------------------------------------------------------------- 1 | import co.touchlab.kmmbridge.gitlab.internal.gitLabDomain 2 | import co.touchlab.kmmbridge.gitlab.internal.gitLabPublishTokenOrNull 3 | import co.touchlab.kmmbridge.gitlab.internal.gitLabPublishUser 4 | import co.touchlab.kmmbridge.gitlab.internal.gitLabRepoOrNull 5 | import co.touchlab.kmmbridge.publishingExtension 6 | import org.gradle.api.Project 7 | import org.gradle.api.credentials.HttpHeaderCredentials 8 | import org.gradle.authentication.http.HttpHeaderAuthentication 9 | 10 | 11 | /** 12 | * Helper function to support GitLab Packages publishing. 13 | * Pass in a valid GitLab token type with GITLAB_PUBLISH_USER. Options include; "Private-Token", "Deploy-Token" & "Job-Token" (default). 14 | * Pass in a valid GitLab token for the specified type with GITLAB_PUBLISH_TOKEN. Defaults to CI_JOB_TOKEN environment variable. 15 | * Pass in a custom GitLab domain. Useful for self-managed instances. Defaults to "gitlab.com". 16 | * 17 | * Generally, just add the following in the Gradle build file. 18 | * 19 | * addGitlabPackagesRepository() 20 | */ 21 | @Suppress("unused") 22 | fun Project.addGitlabPackagesRepository() { 23 | publishingExtension.apply { 24 | try { 25 | val gitLabPublishUser = project.gitLabPublishUser ?: "Job-Token" 26 | val gitLabPublishToken = project.gitLabPublishTokenOrNull ?: System.getenv("CI_JOB_TOKEN") ?: return 27 | val gitLabRepo = project.gitLabRepoOrNull ?: return 28 | val gitLabDomain = project.gitLabDomain ?: "gitlab.com" 29 | repositories.maven { 30 | name = "GitLabPackages" 31 | url = uri("https://$gitLabDomain/api/v4/projects/$gitLabRepo/packages/maven") 32 | credentials(HttpHeaderCredentials::class.java) { 33 | name = gitLabPublishUser 34 | value = gitLabPublishToken 35 | } 36 | authentication { 37 | create("header", HttpHeaderAuthentication::class.java) 38 | } 39 | } 40 | } catch (e: Exception) { 41 | logger.warn("Could not configure GitLabPackagesRepository! - $e") 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /kmmbridge-gitlab/src/main/kotlin/co/touchlab/kmmbridge/gitlab/KMMBridgeGitLabPlugin.kt: -------------------------------------------------------------------------------- 1 | package co.touchlab.kmmbridge.gitlab 2 | 3 | import co.touchlab.kmmbridge.BaseKMMBridgePlugin 4 | 5 | @Suppress("unused") 6 | class KMMBridgeGitLabPlugin : BaseKMMBridgePlugin() { 7 | } -------------------------------------------------------------------------------- /kmmbridge-gitlab/src/main/kotlin/co/touchlab/kmmbridge/gitlab/internal/GitLabApi.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Touchlab. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 4 | * in compliance with the License. You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software distributed under the License 9 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 10 | * or implied. See the License for the specific language governing permissions and limitations under 11 | * the License. 12 | */ 13 | 14 | package co.touchlab.kmmbridge.gitlab.internal 15 | 16 | import co.touchlab.kmmbridge.findStringProperty 17 | import org.gradle.api.Project 18 | import java.net.URLEncoder 19 | 20 | internal val Project.gitLabPublishTokenOrNull: String? 21 | get() = project.property("GITLAB_PUBLISH_TOKEN") as String? 22 | 23 | internal val Project.gitLabPublishUser: String? 24 | get() = project.findStringProperty("GITLAB_PUBLISH_USER") 25 | 26 | internal val Project.gitLabRepoOrNull: String? 27 | get() { 28 | val repo = 29 | (project.findStringProperty("GITLAB_REPO") ?: project.findStringProperty("GITHUB_REPO")) ?: return null 30 | // The GitLab API accepts repo id or url-encoded path 31 | val repoId = repo.toIntOrNull() 32 | return repoId?.toString() ?: URLEncoder.encode(repo, "UTF-8") 33 | } 34 | 35 | internal val Project.gitLabDomain: String? 36 | get() = project.findStringProperty("GITLAB_DOMAIN") -------------------------------------------------------------------------------- /kmmbridge-test/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("PropertyName") 2 | 3 | /* 4 | * Copyright (c) 2024 Touchlab. 5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 6 | * in compliance with the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | */ 15 | plugins { 16 | `kotlin-dsl` 17 | alias(libs.plugins.kotlin) 18 | id("org.jetbrains.kotlin.plugin.allopen") 19 | id("java-gradle-plugin") 20 | alias(libs.plugins.maven.publish) 21 | id("com.gradle.plugin-publish") version "1.0.0" 22 | } 23 | 24 | @Suppress("UnstableApiUsage") 25 | gradlePlugin { 26 | website = "https://github.com/touchlab/KMMBridge" 27 | vcsUrl = "https://github.com/touchlab/KMMBridge.git" 28 | description = 29 | "KMMBridge is a set of Gradle tooling that facilitates publishing and consuming pre-built KMM (Kotlin Multiplatform Mobile) Xcode Framework binaries." 30 | plugins { 31 | register("kmmbridge-test-plugin") { 32 | id = "co.touchlab.kmmbridge.test" 33 | implementationClass = "co.touchlab.kmmbridge.test.KMMBridgeTestPlugin" 34 | displayName = "KMMBridge/Test" 35 | tags = listOf( 36 | "kmm", 37 | "kotlin", 38 | "multiplatform", 39 | "mobile", 40 | "ios", 41 | "xcode", 42 | "framework", 43 | "binary", 44 | "publish", 45 | "consume" 46 | ) 47 | } 48 | } 49 | } 50 | 51 | dependencies { 52 | implementation(kotlin("stdlib")) 53 | implementation(libs.okhttp) 54 | implementation(libs.gson) 55 | api(project(":kmmbridge")) 56 | 57 | testImplementation(kotlin("test")) 58 | } 59 | 60 | mavenPublishing { 61 | publishToMavenCentral() 62 | val releaseSigningEnabled = 63 | project.properties["RELEASE_SIGNING_ENABLED"]?.toString()?.equals("false", ignoreCase = true) != true 64 | if (releaseSigningEnabled) signAllPublications() 65 | @Suppress("UnstableApiUsage") 66 | pomFromGradleProperties() 67 | @Suppress("UnstableApiUsage") 68 | configureBasedOnAppliedPlugins() 69 | } -------------------------------------------------------------------------------- /kmmbridge-test/src/main/kotlin/Extensions.kt: -------------------------------------------------------------------------------- 1 | import co.touchlab.kmmbridge.findStringProperty 2 | import co.touchlab.kmmbridge.test.TestArtifactManager 3 | import co.touchlab.kmmbridge.test.TestUploadArtifactManager 4 | import co.touchlab.kmmbridge.test.kmmBridgeExtension 5 | import org.gradle.api.GradleException 6 | import org.gradle.api.Project 7 | 8 | @Suppress("unused") 9 | fun Project.testArtifacts() { 10 | val artifactManager = kmmBridgeExtension.artifactManager 11 | artifactManager.set(TestArtifactManager()) 12 | artifactManager.finalizeValue() 13 | } 14 | 15 | /** 16 | * This is for Touchlab use. See the code for more details. 17 | */ 18 | @Suppress("unused") 19 | fun Project.testUploadArtifacts() { 20 | val server = findStringProperty("TOUCHLAB_TEST_ARTIFACT_SERVER") 21 | val code = findStringProperty("TOUCHLAB_TEST_ARTIFACT_CODE") 22 | 23 | if (server == null || code == null) { 24 | throw GradleException("TODO: Figure out a way for forks to not fail builds. But not today...") 25 | } 26 | 27 | val am = TestUploadArtifactManager(server, code) 28 | 29 | val artifactManager = kmmBridgeExtension.artifactManager 30 | artifactManager.set(am) 31 | artifactManager.finalizeValue() 32 | } -------------------------------------------------------------------------------- /kmmbridge-test/src/main/kotlin/co/touchlab/kmmbridge/test/KMMBridgeTestPlugin.kt: -------------------------------------------------------------------------------- 1 | package co.touchlab.kmmbridge.test 2 | 3 | import co.touchlab.kmmbridge.BaseKMMBridgePlugin 4 | 5 | @Suppress("unused") 6 | class KMMBridgeTestPlugin : BaseKMMBridgePlugin() { 7 | } -------------------------------------------------------------------------------- /kmmbridge-test/src/main/kotlin/co/touchlab/kmmbridge/test/TestArtifactManager.kt: -------------------------------------------------------------------------------- 1 | package co.touchlab.kmmbridge.test 2 | 3 | import co.touchlab.kmmbridge.KmmBridgeExtension 4 | import co.touchlab.kmmbridge.artifactmanager.ArtifactManager 5 | import org.gradle.api.Project 6 | import org.gradle.api.Task 7 | import org.gradle.api.tasks.TaskProvider 8 | import org.gradle.kotlin.dsl.getByType 9 | import java.io.File 10 | 11 | class TestArtifactManager : ArtifactManager { 12 | private lateinit var url: String 13 | 14 | override fun configure( 15 | project: Project, 16 | version: String, 17 | uploadTask: TaskProvider, 18 | kmmPublishTask: TaskProvider 19 | ) { 20 | url = "test://$version" 21 | } 22 | 23 | override fun deployArtifact(task: Task, zipFilePath: File, version: String): String = url 24 | } 25 | 26 | internal val Project.kmmBridgeExtension get() = extensions.getByType() -------------------------------------------------------------------------------- /kmmbridge-test/src/main/kotlin/co/touchlab/kmmbridge/test/TestUploadArtifactManager.kt: -------------------------------------------------------------------------------- 1 | package co.touchlab.kmmbridge.test 2 | 3 | import co.touchlab.kmmbridge.artifactmanager.ArtifactManager 4 | import com.google.gson.Gson 5 | import okhttp3.MediaType.Companion.toMediaTypeOrNull 6 | import okhttp3.OkHttpClient 7 | import okhttp3.Request 8 | import okhttp3.RequestBody 9 | import okhttp3.RequestBody.Companion.asRequestBody 10 | import org.gradle.api.GradleException 11 | import org.gradle.api.Task 12 | import java.io.File 13 | import java.time.Duration 14 | 15 | /** 16 | * Simple artifact manager that posts files to our server (Touchlab). This isn't designed for general usage. Just for 17 | * our tests. If there is more general demand for this functionality, reach out and we'll discuss ways of making it work. 18 | */ 19 | internal class TestUploadArtifactManager(private val server: String, private val code: String) : ArtifactManager { 20 | override fun deployArtifact(task: Task, zipFilePath: File, version: String): String { 21 | val body: RequestBody = zipFilePath.asRequestBody("application/octet-stream".toMediaTypeOrNull()) 22 | val uploadRequest = Request.Builder().url( 23 | "https://$server/infoadmin/storeTestZip" 24 | ).post(body).addHeader("code", code) 25 | .addHeader("Content-Type", "application/octet-stream").build() 26 | 27 | val okHttpClient = 28 | OkHttpClient.Builder().callTimeout(Duration.ofMinutes(5)).connectTimeout(Duration.ofMinutes(2)) 29 | .writeTimeout(Duration.ofMinutes(5)).readTimeout(Duration.ofMinutes(2)).build() 30 | 31 | val response = okHttpClient.newCall(uploadRequest).execute() 32 | if (response.code >= 400) { 33 | throw GradleException("Test zip upload failed. Code: ${response.code}, message: ${response.message}") 34 | } 35 | 36 | val uploadResponseString = response.body!!.string() 37 | val url = Gson().fromJson(uploadResponseString, UploadReply::class.java).url 38 | 39 | return "https://$server$url" 40 | } 41 | 42 | data class UploadReply(var url: String) 43 | } -------------------------------------------------------------------------------- /kmmbridge/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("PropertyName") 2 | 3 | /* 4 | * Copyright (c) 2024 Touchlab. 5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 6 | * in compliance with the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License 11 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 | * or implied. See the License for the specific language governing permissions and limitations under 13 | * the License. 14 | */ 15 | 16 | plugins { 17 | `kotlin-dsl` 18 | alias(libs.plugins.kotlin) 19 | id("org.jetbrains.kotlin.plugin.allopen") 20 | id("java-gradle-plugin") 21 | alias(libs.plugins.maven.publish) 22 | id("com.gradle.plugin-publish") version "1.0.0" 23 | } 24 | 25 | @Suppress("UnstableApiUsage") 26 | gradlePlugin { 27 | website = "https://github.com/touchlab/KMMBridge" 28 | vcsUrl = "https://github.com/touchlab/KMMBridge.git" 29 | description = 30 | "KMMBridge is a set of Gradle tooling that facilitates publishing and consuming pre-built KMM (Kotlin Multiplatform Mobile) Xcode Framework binaries." 31 | plugins { 32 | register("kmmbridge-plugin") { 33 | id = "co.touchlab.kmmbridge" 34 | implementationClass = "co.touchlab.kmmbridge.KMMBridgePlugin" 35 | displayName = "KMMBridge for Teams" 36 | tags = listOf( 37 | "kmm", 38 | "kotlin", 39 | "multiplatform", 40 | "mobile", 41 | "ios", 42 | "xcode", 43 | "framework", 44 | "binary", 45 | "publish", 46 | "consume" 47 | ) 48 | } 49 | } 50 | } 51 | 52 | dependencies { 53 | implementation(kotlin("stdlib")) 54 | compileOnly(kotlin("gradle-plugin")) 55 | implementation(libs.aws) 56 | implementation(libs.okhttp) 57 | implementation(libs.gson) 58 | 59 | testImplementation(kotlin("test")) 60 | testImplementation(libs.kotlin.gradle.plugin) 61 | testImplementation(gradleTestKit()) 62 | testImplementation("commons-io:commons-io:2.18.0") 63 | } 64 | 65 | mavenPublishing { 66 | publishToMavenCentral() 67 | val releaseSigningEnabled = 68 | project.properties["RELEASE_SIGNING_ENABLED"]?.toString()?.equals("false", ignoreCase = true) != true 69 | if (releaseSigningEnabled) signAllPublications() 70 | @Suppress("UnstableApiUsage") 71 | pomFromGradleProperties() 72 | @Suppress("UnstableApiUsage") 73 | configureBasedOnAppliedPlugins() 74 | } -------------------------------------------------------------------------------- /kmmbridge/src/main/kotlin/co/touchlab/kmmbridge/Constants.kt: -------------------------------------------------------------------------------- 1 | package co.touchlab.kmmbridge 2 | 3 | public const val TASK_GROUP_NAME = "kmmbridge" 4 | public const val EXTENSION_NAME = "kmmbridge" -------------------------------------------------------------------------------- /kmmbridge/src/main/kotlin/co/touchlab/kmmbridge/KMMBridge.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Touchlab. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 4 | * in compliance with the License. You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software distributed under the License 9 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 10 | * or implied. See the License for the specific language governing permissions and limitations under 11 | * the License. 12 | */ 13 | 14 | package co.touchlab.kmmbridge 15 | 16 | import co.touchlab.kmmbridge.artifactmanager.ArtifactManager 17 | import co.touchlab.kmmbridge.dependencymanager.SpmDependencyManager 18 | import co.touchlab.kmmbridge.internal.enablePublishing 19 | import co.touchlab.kmmbridge.internal.findXCFrameworkAssembleTask 20 | import co.touchlab.kmmbridge.internal.kotlin 21 | import co.touchlab.kmmbridge.internal.layoutBuildDir 22 | import co.touchlab.kmmbridge.internal.spmBuildTargets 23 | import co.touchlab.kmmbridge.internal.urlFile 24 | import co.touchlab.kmmbridge.internal.zipFilePath 25 | import org.gradle.api.Action 26 | import org.gradle.api.Plugin 27 | import org.gradle.api.Project 28 | import org.gradle.api.Task 29 | import org.gradle.api.tasks.TaskProvider 30 | import org.gradle.api.tasks.bundling.Zip 31 | import org.gradle.kotlin.dsl.create 32 | import org.gradle.kotlin.dsl.getByType 33 | import org.gradle.kotlin.dsl.register 34 | import org.gradle.kotlin.dsl.withType 35 | import org.jetbrains.kotlin.gradle.plugin.mpp.Framework 36 | import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget 37 | import org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType 38 | import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework 39 | import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFrameworkConfig 40 | import java.io.File 41 | import kotlin.collections.filter 42 | import kotlin.collections.flatMap 43 | import kotlin.collections.forEach 44 | 45 | @Suppress("unused") 46 | open class KMMBridgePlugin : BaseKMMBridgePlugin() { 47 | } 48 | 49 | abstract class BaseKMMBridgePlugin : Plugin { 50 | 51 | override fun apply(project: Project): Unit = with(project) { 52 | val extension = extensions.create(EXTENSION_NAME) 53 | 54 | extension.dependencyManagers.convention(emptyList()) 55 | 56 | val defaultNativeBuildType = if (project.findStringProperty("NATIVE_BUILD_TYPE") == "DEBUG") { 57 | NativeBuildType.DEBUG 58 | } else { 59 | NativeBuildType.RELEASE 60 | } 61 | 62 | extension.buildType.convention(defaultNativeBuildType) 63 | 64 | afterEvaluate { 65 | val kmmBridgeExtension = extensions.getByType() 66 | 67 | configureXcFramework(kmmBridgeExtension) 68 | configureLocalDev(kmmBridgeExtension) 69 | if (enablePublishing) { 70 | configureArtifactManagerAndDeploy(kmmBridgeExtension) 71 | } 72 | } 73 | } 74 | 75 | // Collect all declared frameworks in project and combine into xcframework 76 | private fun Project.configureXcFramework(kmmBridgeExtension: KmmBridgeExtension) { 77 | var xcFrameworkConfig: XCFrameworkConfig? = null 78 | 79 | val spmBuildTargets: Set = 80 | project.spmBuildTargets?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }?.toSet() ?: emptySet() 81 | 82 | kotlin.targets 83 | .withType() 84 | .filter { it.konanTarget.family.isAppleFamily } 85 | .flatMap { it.binaries.filterIsInstance() } 86 | .forEach { framework -> 87 | val theName = framework.baseName 88 | val currentName = kmmBridgeExtension.frameworkName.orNull 89 | if (currentName == null) { 90 | kmmBridgeExtension.frameworkName.set(theName) 91 | } else { 92 | if (currentName != theName) { 93 | throw IllegalStateException("Only one framework name currently allowed. Found $currentName and $theName") 94 | } 95 | } 96 | val shouldAddTarget = 97 | spmBuildTargets.isEmpty() || spmBuildTargets.contains(framework.target.konanTarget.name) 98 | if (shouldAddTarget) { 99 | if (xcFrameworkConfig == null) { 100 | xcFrameworkConfig = XCFramework(theName) 101 | } 102 | xcFrameworkConfig!!.add(framework) 103 | } 104 | } 105 | } 106 | 107 | private fun Project.configureLocalDev(kmmBridgeExtension: KmmBridgeExtension) { 108 | (kmmBridgeExtension.dependencyManagers.get() 109 | .find { it is SpmDependencyManager } as? SpmDependencyManager)?.configureLocalDev( 110 | this 111 | ) 112 | } 113 | 114 | private fun Project.configureArtifactManagerAndDeploy(kmmBridgeExtension: KmmBridgeExtension) { 115 | // Early-out with a warning if user hasn't added required config yet, to ensure project still syncs 116 | val artifactManager = kmmBridgeExtension.artifactManager.orNull ?: run { 117 | project.logger.warn("You must apply an artifact manager! Call `artifactManager.set(...)` or a configuration function like `mavenPublishArtifacts()` in your `kmmbridge` block.") 118 | return 119 | } 120 | 121 | val (zipTask, zipFile) = configureZipTask(kmmBridgeExtension, project.layoutBuildDir) 122 | 123 | // Zip task depends on the XCFramework assemble task 124 | zipTask.configure { 125 | dependsOn(findXCFrameworkAssembleTask()) 126 | } 127 | 128 | // Upload task depends on the zip task 129 | val uploadTask = configureUploadTask(zipTask, zipFile, artifactManager) 130 | 131 | val dependencyManagers = kmmBridgeExtension.dependencyManagers.get() 132 | 133 | // Publish task depends on the upload task 134 | val publishRemoteTask = tasks.register("kmmBridgePublish") { 135 | description = "Publishes your framework. Uses your KMMBridge block configured in the build gradle to determine details." 136 | group = TASK_GROUP_NAME 137 | dependsOn(uploadTask) 138 | } 139 | 140 | // MavenPublishArtifactManager is somewhat complex because we have to hook into maven publishing 141 | // If you are exploring the task dependencies, be aware of that code 142 | artifactManager.configure(this, version.toString(), uploadTask, publishRemoteTask) 143 | 144 | for (dependencyManager in dependencyManagers) { 145 | dependencyManager.configure(providers, this, version.toString(), uploadTask, publishRemoteTask) 146 | } 147 | } 148 | 149 | private fun Project.configureUploadTask( 150 | zipTask: TaskProvider, 151 | zipFile: File, 152 | artifactManager: ArtifactManager 153 | ) = tasks.register("uploadXCFramework") { 154 | group = TASK_GROUP_NAME 155 | 156 | dependsOn(zipTask) 157 | inputs.file(zipFile) 158 | outputs.files(urlFile) 159 | outputs.upToDateWhen { false } // We want to always upload when this task is called 160 | val versionLocal = version 161 | val urlFileLocal = urlFile 162 | 163 | @Suppress("ObjectLiteralToLambda") 164 | doLast(object : Action { 165 | override fun execute(t: Task) { 166 | logger.info("Uploading XCFramework version $versionLocal") 167 | val deployUrl = artifactManager.deployArtifact(this@register, zipFile, versionLocal.toString()) 168 | urlFileLocal.writeText(deployUrl) 169 | } 170 | }) 171 | } 172 | 173 | private fun Project.configureZipTask( 174 | kmmBridgeExtension: KmmBridgeExtension, 175 | buildDir: File 176 | ): Pair, File> { 177 | val zipFile = zipFilePath() 178 | val zipTask = tasks.register("zipXCFramework") { 179 | group = TASK_GROUP_NAME 180 | from("$buildDir/XCFrameworks/${kmmBridgeExtension.buildType.get().getName()}") 181 | destinationDirectory.set(zipFile.parentFile) 182 | archiveFileName.set(zipFile.name) 183 | } 184 | 185 | return Pair(zipTask, zipFile) 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /kmmbridge/src/main/kotlin/co/touchlab/kmmbridge/KmmBridgeExtension.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Touchlab. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 4 | * in compliance with the License. You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software distributed under the License 9 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 10 | * or implied. See the License for the specific language governing permissions and limitations under 11 | * the License. 12 | */ 13 | 14 | package co.touchlab.kmmbridge 15 | 16 | import co.touchlab.kmmbridge.artifactmanager.ArtifactManager 17 | import co.touchlab.kmmbridge.artifactmanager.AwsS3PublicArtifactManager 18 | import co.touchlab.kmmbridge.artifactmanager.MavenPublishArtifactManager 19 | import co.touchlab.kmmbridge.dependencymanager.CocoapodsDependencyManager 20 | import co.touchlab.kmmbridge.dependencymanager.DependencyManager 21 | import co.touchlab.kmmbridge.dependencymanager.SpecRepo 22 | import co.touchlab.kmmbridge.dependencymanager.SpmDependencyManager 23 | import co.touchlab.kmmbridge.dsl.TargetPlatformDsl 24 | import co.touchlab.kmmbridge.internal.cocoapods 25 | import co.touchlab.kmmbridge.internal.domain.SwiftToolVersion 26 | import co.touchlab.kmmbridge.internal.kotlin 27 | import org.gradle.api.Project 28 | import org.gradle.api.provider.ListProperty 29 | import org.gradle.api.provider.Property 30 | import org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType 31 | 32 | interface KmmBridgeExtension { 33 | /** 34 | * The name of the kotlin framework, which will be wrapped into a cocoapod. The name may be the same or different from podName. 35 | * This should be the same as 36 | */ 37 | val frameworkName: Property 38 | 39 | val dependencyManagers: ListProperty 40 | 41 | val artifactManager: Property 42 | 43 | val buildType: Property 44 | 45 | @Suppress("unused") 46 | fun Project.s3PublicArtifacts( 47 | region: String, 48 | bucket: String, 49 | accessKeyId: String, 50 | secretAccessKey: String, 51 | makeArtifactsPublic: Boolean = true, 52 | altBaseUrl: String? = null, 53 | ) { 54 | artifactManager.setAndFinalize( 55 | AwsS3PublicArtifactManager( 56 | region, 57 | bucket, 58 | accessKeyId, 59 | secretAccessKey, 60 | makeArtifactsPublic, 61 | altBaseUrl 62 | ) 63 | ) 64 | } 65 | 66 | /** 67 | * If using multiple repos, you can specify which one the `Package.swift` and/or podspec point to, bypassing 68 | * the name in here. 69 | */ 70 | @Suppress("unused") 71 | fun Project.mavenPublishArtifacts( 72 | repository: String? = null, 73 | publication: String? = null, 74 | artifactSuffix: String? = null, 75 | isMavenCentral: Boolean = false 76 | ) { 77 | artifactManager.setAndFinalize( 78 | MavenPublishArtifactManager( 79 | publication, 80 | artifactSuffix, 81 | repository, 82 | isMavenCentral 83 | ) 84 | ) 85 | } 86 | 87 | /** 88 | * Enable Swift Package Manager publication 89 | * 90 | * @param spmDirectory Folder where the Package.swift file lives 91 | * @param useCustomPackageFile Allow to use custom Package.swift file 92 | * @param perModuleVariablesBlock Allow the same Package.swift file to host multiple kotlin frameworks 93 | * @param swiftToolVersion Specifies swift-tools-version in Package.swift. Default: [SwiftToolVersion.Default] 94 | */ 95 | @Suppress("unused") 96 | fun Project.spm( 97 | spmDirectory: String? = null, 98 | useCustomPackageFile: Boolean = false, 99 | perModuleVariablesBlock: Boolean = false, 100 | swiftToolVersion: String = SwiftToolVersion.Default, 101 | targetPlatforms: TargetPlatformDsl.() -> Unit = { iOS { v("13") } }, 102 | ) { 103 | val dependencyManager = SpmDependencyManager( 104 | spmDirectory, 105 | useCustomPackageFile, 106 | perModuleVariablesBlock, 107 | swiftToolVersion, 108 | targetPlatforms 109 | ) 110 | dependencyManagers.set(dependencyManagers.getOrElse(emptyList()) + dependencyManager) 111 | } 112 | 113 | /** 114 | * Enable CocoaPods publication 115 | * 116 | * @param specRepoUrl Url to repo that holds specs. 117 | * @param allowWarnings Allow publishing with warnings. Defaults to true. 118 | * @param verboseErrors Output extra error info. Generally used if publishing fails. Defaults to false. 119 | */ 120 | @Suppress("unused") 121 | fun Project.cocoapods( 122 | specRepoUrl: String, 123 | allowWarnings: Boolean = true, 124 | verboseErrors: Boolean = false, 125 | ) { 126 | kotlin.cocoapods // This will throw error if we didn't apply cocoapods plugin 127 | 128 | val dependencyManager = CocoapodsDependencyManager({ 129 | SpecRepo.Private(specRepoUrl) 130 | }, allowWarnings, verboseErrors) 131 | 132 | dependencyManagers.set(dependencyManagers.getOrElse(emptyList()) + dependencyManager) 133 | } 134 | 135 | /** 136 | * Enable CocoaPods publication using the [Trunk](https://github.com/CocoaPods/Specs) as a spec repo 137 | * 138 | * @param allowWarnings Allow publishing with warnings. Defaults to true. 139 | * @param verboseErrors Output extra error info. Generally used if publishing fails. Defaults to false. 140 | */ 141 | @Suppress("unused") 142 | fun Project.cocoapodsTrunk( 143 | allowWarnings: Boolean = true, 144 | verboseErrors: Boolean = false, 145 | ) { 146 | kotlin.cocoapods // This will throw error if we didn't apply cocoapods plugin 147 | 148 | val dependencyManager = CocoapodsDependencyManager({ SpecRepo.Trunk }, allowWarnings, verboseErrors) 149 | 150 | dependencyManagers.set(dependencyManagers.getOrElse(emptyList()) + dependencyManager) 151 | } 152 | 153 | fun Property.setAndFinalize(value: T) { 154 | this.set(value) 155 | this.finalizeValue() 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /kmmbridge/src/main/kotlin/co/touchlab/kmmbridge/ProjectExtensions.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Touchlab. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 4 | * in compliance with the License. You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software distributed under the License 9 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 10 | * or implied. See the License for the specific language governing permissions and limitations under 11 | * the License. 12 | */ 13 | 14 | package co.touchlab.kmmbridge 15 | 16 | import org.gradle.api.Project 17 | import org.gradle.api.plugins.ExtraPropertiesExtension 18 | import org.gradle.api.publish.PublishingExtension 19 | import org.gradle.kotlin.dsl.getByType 20 | 21 | 22 | val Project.publishingExtension get() = extensions.getByType() 23 | 24 | fun Project.findStringProperty(name: String): String? { 25 | rootProject.extensions.getByType(ExtraPropertiesExtension::class.java).run { 26 | if (has(name)) 27 | return get(name).toString() 28 | } 29 | return null 30 | } 31 | -------------------------------------------------------------------------------- /kmmbridge/src/main/kotlin/co/touchlab/kmmbridge/artifactmanager/ArtifactManager.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Touchlab. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 4 | * in compliance with the License. You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software distributed under the License 9 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 10 | * or implied. See the License for the specific language governing permissions and limitations under 11 | * the License. 12 | */ 13 | 14 | package co.touchlab.kmmbridge.artifactmanager 15 | 16 | import org.gradle.api.Project 17 | import org.gradle.api.Task 18 | import org.gradle.api.tasks.TaskProvider 19 | import java.io.File 20 | 21 | /** 22 | * Publishes XCFramework zip files to where they are hosted in the cloud. 23 | */ 24 | interface ArtifactManager { 25 | 26 | /** 27 | * Run during Gradle project config. 28 | */ 29 | fun configure( 30 | project: Project, 31 | version: String, 32 | uploadTask: TaskProvider, 33 | kmmPublishTask: TaskProvider 34 | ) { 35 | } 36 | 37 | /** 38 | * Run during task execution. With custom implementations, be careful not to break Gradle config cache rules. 39 | */ 40 | fun deployArtifact(task: Task, zipFilePath: File, version: String): String 41 | } -------------------------------------------------------------------------------- /kmmbridge/src/main/kotlin/co/touchlab/kmmbridge/artifactmanager/AwsS3PublicArtifactManager.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Touchlab. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 4 | * in compliance with the License. You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software distributed under the License 9 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 10 | * or implied. See the License for the specific language governing permissions and limitations under 11 | * the License. 12 | */ 13 | 14 | package co.touchlab.kmmbridge.artifactmanager 15 | 16 | import co.touchlab.kmmbridge.internal.kmmBridgeExtension 17 | import org.gradle.api.Project 18 | import org.gradle.api.Task 19 | import org.gradle.api.tasks.TaskProvider 20 | import software.amazon.awssdk.auth.credentials.AwsBasicCredentials 21 | import software.amazon.awssdk.core.sync.RequestBody 22 | import software.amazon.awssdk.regions.Region 23 | import software.amazon.awssdk.services.s3.S3Client 24 | import software.amazon.awssdk.services.s3.S3Configuration 25 | import software.amazon.awssdk.services.s3.model.HeadObjectRequest 26 | import software.amazon.awssdk.services.s3.model.PutObjectRequest 27 | import java.io.File 28 | import java.util.* 29 | 30 | internal class AwsS3PublicArtifactManager( 31 | private val s3Region: String, 32 | private val s3Bucket: String, 33 | private val s3AccessKeyId: String, 34 | private val s3SecretAccessKey: String, 35 | private val makeArtifactsPublic: Boolean, 36 | private val altBaseUrl: String?, 37 | ) : ArtifactManager { 38 | 39 | lateinit var frameworkName: String 40 | 41 | override fun configure( 42 | project: Project, 43 | version: String, 44 | uploadTask: TaskProvider, 45 | kmmPublishTask: TaskProvider 46 | ) { 47 | frameworkName = project.kmmBridgeExtension.frameworkName.get() 48 | } 49 | 50 | override fun deployArtifact(task: Task, zipFilePath: File, version: String): String { 51 | val fileName = obscureFileName(frameworkName, version) 52 | uploadArtifact(zipFilePath, fileName) 53 | return deployUrl(fileName) 54 | } 55 | 56 | /** 57 | * Compute the fully qualified URL for the artifact we just uploaded 58 | * 59 | * @see uploadArtifact 60 | */ 61 | private fun deployUrl(zipFileName: String): String { 62 | val baseUrl = altBaseUrl ?: "https://${s3Bucket}.s3.${s3Region}.amazonaws.com" 63 | return "${baseUrl}/$zipFileName" 64 | } 65 | 66 | /** 67 | * If the artifact doesn't already exist in remote storage, upload it. Note: if there 68 | * is a problem determining if it exists it's assumed not to be there and will be 69 | * uploaded. 70 | */ 71 | @Suppress("NAME_SHADOWING") 72 | private fun uploadArtifact(zipFilePath: File, fileName: String) { 73 | val s3Client = S3Client.builder() 74 | .serviceConfiguration(S3Configuration.builder().pathStyleAccessEnabled(true).build()) 75 | .region(Region.of(s3Region)) 76 | .credentialsProvider { 77 | AwsBasicCredentials.create( 78 | s3AccessKeyId, 79 | s3SecretAccessKey 80 | ) 81 | } 82 | .build() 83 | 84 | s3Client.use { s3Client -> 85 | 86 | val headObjectRequest = HeadObjectRequest.builder() 87 | .bucket(s3Bucket) 88 | .key(fileName) 89 | .build() 90 | 91 | val exists = try { 92 | s3Client.headObject(headObjectRequest).sdkHttpResponse().isSuccessful 93 | } catch (e: Exception) { 94 | false 95 | } 96 | 97 | if (!exists) { 98 | val builder = PutObjectRequest.builder() 99 | .bucket(s3Bucket) 100 | .key(fileName) 101 | 102 | if (makeArtifactsPublic) 103 | builder.acl("public-read") 104 | 105 | val putObjectRequest = builder.build() 106 | 107 | val requestBody = RequestBody.fromFile(zipFilePath) 108 | s3Client.putObject(putObjectRequest, requestBody) 109 | } 110 | } 111 | } 112 | } 113 | 114 | /** 115 | * Generate a file name that isn't guessable. Some artifact managers don't have auth guarding the urls. 116 | */ 117 | private fun obscureFileName(frameworkName: String, versionString: String): String { 118 | val randomId = UUID.randomUUID().toString() 119 | return "${frameworkName}-${versionString}-${randomId}.xcframework.zip" 120 | } 121 | -------------------------------------------------------------------------------- /kmmbridge/src/main/kotlin/co/touchlab/kmmbridge/artifactmanager/MavenPublishArtifactManager.kt: -------------------------------------------------------------------------------- 1 | package co.touchlab.kmmbridge.artifactmanager 2 | 3 | import co.touchlab.kmmbridge.internal.capitalized 4 | import co.touchlab.kmmbridge.publishingExtension 5 | import org.gradle.api.GradleException 6 | import org.gradle.api.Project 7 | import org.gradle.api.Task 8 | import org.gradle.api.UnknownTaskException 9 | import org.gradle.api.artifacts.repositories.MavenArtifactRepository 10 | import org.gradle.api.publish.PublishingExtension 11 | import org.gradle.api.publish.maven.MavenPublication 12 | import org.gradle.api.tasks.TaskProvider 13 | import org.gradle.api.tasks.bundling.Zip 14 | import org.gradle.kotlin.dsl.getByType 15 | import java.io.File 16 | 17 | private const val FRAMEWORK_PUBLICATION_NAME = "KMMBridgeFramework" 18 | private const val KMMBRIDGE_ARTIFACT_SUFFIX = "kmmbridge" 19 | 20 | internal class MavenPublishArtifactManager( 21 | private val publicationName: String?, 22 | private val artifactSuffix: String?, 23 | private val repositoryName: String?, 24 | private val isMavenCentral: Boolean = false, 25 | ) : ArtifactManager { 26 | lateinit var group: String 27 | lateinit var kmmbridgeArtifactId: String 28 | lateinit var mavenArtifactRepositoryUrl: String 29 | 30 | override fun configure( 31 | project: Project, 32 | version: String, 33 | uploadTask: TaskProvider, 34 | kmmPublishTask: TaskProvider 35 | ) { 36 | this.group = project.group.toString().replace(".", "/") 37 | this.kmmbridgeArtifactId = "${project.name}-${artifactSuffix ?: KMMBRIDGE_ARTIFACT_SUFFIX}" 38 | this.mavenArtifactRepositoryUrl = project.evaluateRepoUrl() 39 | 40 | project.publishingExtension.publications.create( 41 | publicationName ?: FRAMEWORK_PUBLICATION_NAME, 42 | MavenPublication::class.java 43 | ) { 44 | this.version = version 45 | val archiveProvider = project.tasks.named("zipXCFramework", Zip::class.java).flatMap { 46 | it.archiveFile 47 | } 48 | artifact(archiveProvider) { 49 | extension = "zip" 50 | } 51 | artifactId = kmmbridgeArtifactId 52 | } 53 | 54 | publishingTasks(project).forEach { 55 | uploadTask.configure { 56 | dependsOn(it) 57 | } 58 | } 59 | try { 60 | project.tasks.named("publish").also { task -> 61 | task.configure { 62 | dependsOn(kmmPublishTask) 63 | } 64 | } 65 | } catch (_: UnknownTaskException) { 66 | project.logger.warn("Gradle publish task not found") 67 | } 68 | } 69 | 70 | /** 71 | * The GradlePublishArtifactManager relies on the gradle publishing plugin to manage uploading 72 | * to maven repositories, and its presence as the artifact manager is a marker for the main 73 | * KMMBridge plugin to configure task level dependencies. Since the gradle publishing plugin 74 | * doesn't tell you anything about the remote URLs that it's creating, it's inferred based on 75 | * maven's well known conventions. 76 | */ 77 | override fun deployArtifact(task: Task, zipFilePath: File, version: String): String { 78 | return artifactPath(mavenArtifactRepositoryUrl, version) 79 | } 80 | 81 | private fun Project.evaluateRepoUrl(): String { 82 | val publishingExtension = project.extensions.getByType() 83 | 84 | // There may be more than one repo, but it's also possible we get none. This will allow us to continue and trying 85 | // to use the dependency should fail. 86 | // If there are multiple repos, the repo name needs to be specified. 87 | return if (!isMavenCentral) { 88 | findArtifactRepository(publishingExtension).url.toString() 89 | } else { 90 | "https://repo.maven.apache.org/maven2" 91 | } 92 | } 93 | 94 | private fun publishingTasks(project: Project): List> { 95 | val publishingExtension = project.extensions.getByType() 96 | 97 | // Either the user has supplied a correct name, or we use the default. If neither is found, fail. 98 | val publicationNameCap = 99 | publishingExtension.publications.getByName(publicationName ?: FRAMEWORK_PUBLICATION_NAME).name.capitalized() 100 | 101 | return publishingExtension.repositories.filterIsInstance().map { repo -> 102 | val repositoryName = repo.name.capitalized() 103 | val publishTaskName = "publish${publicationNameCap}PublicationTo${repositoryName}Repository" 104 | // Verify that the "publish" task exists before collecting 105 | project.tasks.named(publishTaskName) 106 | } 107 | } 108 | 109 | private fun findArtifactRepository(publishingExtension: PublishingExtension): MavenArtifactRepository = 110 | repositoryName?.let { 111 | publishingExtension.repositories.findByName(it) as MavenArtifactRepository 112 | } ?: publishingExtension.repositories.filterIsInstance().firstOrNull() 113 | ?: throw GradleException( 114 | "Artifact repository not found, please, specify maven repository\n" + 115 | "publishing {\n" + 116 | " repositories {\n" + 117 | " maven {\n" + 118 | " url = uri(\"https://someservice/path/to/repo\")\n" + 119 | " credentials {\n" + 120 | " username = publishUsername\n" + 121 | " password = publishPassword\n" + 122 | " }\n" + 123 | " }\n" + 124 | " }\n" + 125 | "}" 126 | ) 127 | 128 | private fun artifactPath(url: String, version: String) = 129 | "$url/$group/$kmmbridgeArtifactId/$version/$kmmbridgeArtifactId-$version.zip" 130 | } 131 | -------------------------------------------------------------------------------- /kmmbridge/src/main/kotlin/co/touchlab/kmmbridge/dependencymanager/CocoapodsDependencyManager.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Touchlab. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 4 | * in compliance with the License. You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software distributed under the License 9 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 10 | * or implied. See the License for the specific language governing permissions and limitations under 11 | * the License. 12 | */ 13 | 14 | package co.touchlab.kmmbridge.dependencymanager 15 | 16 | import co.touchlab.kmmbridge.TASK_GROUP_NAME 17 | import co.touchlab.kmmbridge.internal.cocoapods 18 | import co.touchlab.kmmbridge.internal.kmmBridgeExtension 19 | import co.touchlab.kmmbridge.internal.kotlin 20 | import co.touchlab.kmmbridge.internal.layoutBuildDir 21 | import co.touchlab.kmmbridge.internal.urlFile 22 | import org.gradle.api.Action 23 | import org.gradle.api.Project 24 | import org.gradle.api.Task 25 | import org.gradle.api.provider.Provider 26 | import org.gradle.api.provider.ProviderFactory 27 | import org.gradle.api.provider.ValueSource 28 | import org.gradle.api.provider.ValueSourceParameters 29 | import org.gradle.api.tasks.TaskProvider 30 | import org.gradle.kotlin.dsl.of 31 | import org.gradle.process.ExecOperations 32 | import org.gradle.process.ExecSpec 33 | import org.gradle.process.internal.ExecException 34 | import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension.PodspecPlatformSettings 35 | import org.jetbrains.kotlin.gradle.plugin.cocoapods.KotlinCocoapodsPlugin 36 | import org.jetbrains.kotlin.gradle.plugin.mpp.Framework 37 | import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget 38 | import java.io.ByteArrayOutputStream 39 | import java.io.File 40 | import java.nio.charset.Charset 41 | import javax.inject.Inject 42 | 43 | internal sealed class SpecRepo { 44 | object Trunk : SpecRepo() 45 | class Private(val url: String) : SpecRepo() 46 | } 47 | 48 | /** 49 | * State storage to help avoid Gradle configuration cache issues 50 | */ 51 | internal data class SafeCocoapodsData( 52 | val ios: PodspecPlatformSettings, 53 | val osx: PodspecPlatformSettings, 54 | val tvos: PodspecPlatformSettings, 55 | val watchos: PodspecPlatformSettings, 56 | val extraSpecAttributes: MutableMap, 57 | val version: String?, 58 | val name: String, 59 | val homepage: String?, 60 | val license: String?, 61 | val authors: String?, 62 | val summary: String?, 63 | val podsDependencies: String 64 | ) 65 | 66 | private abstract class PodPushValueSource : ValueSource { 67 | 68 | @get:Inject 69 | abstract val execOperations: ExecOperations 70 | 71 | abstract fun ExecSpec.command() 72 | 73 | override fun obtain(): String { 74 | val output = ByteArrayOutputStream() 75 | 76 | val result = execOperations.exec { 77 | command() 78 | standardOutput = output 79 | // Exit Value handled below 80 | isIgnoreExitValue = true 81 | } 82 | val outputString = String(output.toByteArray(), Charset.defaultCharset()) 83 | if (result.exitValue != 0) { 84 | // Handling the exception ourselves 85 | throw ExecException(outputString) 86 | } 87 | return outputString 88 | } 89 | } 90 | 91 | private abstract class PodPrivatePushValueSource : PodPushValueSource() { 92 | 93 | interface Params : ValueSourceParameters { 94 | var specUrl: String 95 | var podSpecFile: File 96 | var extras: Array 97 | } 98 | 99 | override fun ExecSpec.command() { 100 | commandLine( 101 | "pod", 102 | "repo", 103 | "push", 104 | parameters.specUrl, 105 | parameters.podSpecFile, 106 | *parameters.extras 107 | ) 108 | } 109 | } 110 | 111 | private abstract class PodTrunkPushValueSource : PodPushValueSource() { 112 | 113 | interface Params : ValueSourceParameters { 114 | var podSpecFile: File 115 | var extras: Array 116 | } 117 | 118 | override fun ExecSpec.command() { 119 | commandLine( 120 | "pod", "trunk", "push", parameters.podSpecFile, *parameters.extras 121 | ) 122 | } 123 | } 124 | 125 | 126 | internal class CocoapodsDependencyManager( 127 | private val specRepoDeferred: () -> SpecRepo, 128 | private val allowWarnings: Boolean, 129 | private val verboseErrors: Boolean 130 | ) : DependencyManager { 131 | 132 | override fun configure( 133 | providers: ProviderFactory, 134 | project: Project, 135 | version: String, 136 | uploadTask: TaskProvider, 137 | publishRemoteTask: TaskProvider 138 | ) { 139 | 140 | val podSpecFile = 141 | project.file("${project.layoutBuildDir}/kmmbridge/podspec/${project.kmmBridgeExtension.buildType.get().name.lowercase()}/${project.kotlin.cocoapods.name}.podspec") 142 | 143 | val generatePodspecTask = project.tasks.register("generateReleasePodspec") { 144 | inputs.files(project.urlFile) 145 | outputs.file(podSpecFile) 146 | dependsOn(uploadTask) 147 | 148 | val cocoapodsExtension = project.kotlin.cocoapods 149 | 150 | val safeCocoapodsData = SafeCocoapodsData( 151 | cocoapodsExtension.ios, 152 | cocoapodsExtension.osx, 153 | cocoapodsExtension.tvos, 154 | cocoapodsExtension.watchos, 155 | cocoapodsExtension.extraSpecAttributes, 156 | cocoapodsExtension.version, 157 | cocoapodsExtension.name, 158 | cocoapodsExtension.homepage, 159 | cocoapodsExtension.license, 160 | cocoapodsExtension.authors, 161 | cocoapodsExtension.summary, 162 | cocoapodsExtension.pods.joinToString(separator = "\n") { pod -> 163 | val versionSuffix = if (pod.version != null) ", '${pod.version}'" else "" 164 | "| spec.dependency '${pod.name}'$versionSuffix" 165 | } 166 | ) 167 | 168 | val urlFileLocal = project.urlFile 169 | val frameworkName = findFrameworkName(project) 170 | 171 | @Suppress("ObjectLiteralToLambda") 172 | doLast(object : Action { 173 | override fun execute(t: Task) { 174 | generatePodspec( 175 | safeCocoapodsData, urlFileLocal, version, podSpecFile, frameworkName 176 | ) 177 | } 178 | }) 179 | } 180 | 181 | val pushRemotePodspecTask = project.tasks.register("pushRemotePodspec") { 182 | group = TASK_GROUP_NAME 183 | inputs.files(podSpecFile) 184 | dependsOn(generatePodspecTask) 185 | outputs.upToDateWhen { false } // We want to always upload when this task is called 186 | 187 | val allowWarningsLocal = allowWarnings 188 | val verboseErrorsLocal = verboseErrors 189 | val specRepo = specRepoDeferred() 190 | 191 | @Suppress("ObjectLiteralToLambda") 192 | doLast(object : Action { 193 | override fun execute(t: Task) { 194 | val extras = mutableListOf() 195 | 196 | if (allowWarningsLocal) { 197 | extras.add("--allow-warnings") 198 | } 199 | 200 | if (verboseErrorsLocal) { 201 | extras.add("--verbose") 202 | } 203 | 204 | when (specRepo) { 205 | is SpecRepo.Trunk -> { 206 | val podPushProvider = providers.of(PodTrunkPushValueSource::class) { 207 | parameters { 208 | this.podSpecFile = podSpecFile 209 | this.extras = extras.toTypedArray() 210 | } 211 | } 212 | t.logger.info(podPushProvider.get()) 213 | } 214 | 215 | is SpecRepo.Private -> { 216 | val podPushProvider = providers.of(PodPrivatePushValueSource::class) { 217 | parameters { 218 | this.specUrl = specRepo.url 219 | this.podSpecFile = podSpecFile 220 | this.extras = extras.toTypedArray() 221 | } 222 | } 223 | t.logger.info(podPushProvider.get()) 224 | } 225 | } 226 | } 227 | }) 228 | } 229 | 230 | publishRemoteTask.configure { 231 | dependsOn(pushRemotePodspecTask) 232 | } 233 | } 234 | 235 | override val needsGitTags: Boolean = false 236 | } 237 | 238 | private fun findFrameworkName(project: Project): Provider { 239 | val anyPodFramework = project.provider { 240 | val anyTarget = project.kotlin.targets 241 | .withType(KotlinNativeTarget::class.java) 242 | .matching { it.konanTarget.family.isAppleFamily }.first() 243 | val anyFramework = anyTarget.binaries 244 | .matching { it.name.startsWith(KotlinCocoapodsPlugin.POD_FRAMEWORK_PREFIX) } 245 | .withType(Framework::class.java) 246 | .first() 247 | anyFramework 248 | } 249 | return anyPodFramework.map { it.baseName } 250 | } 251 | 252 | // Adapted from spec generation logic in the kotlin.cocoapods plugin, but we skip script phases and some other details, 253 | // and we read straight from the project and cocoapods extension rather than task properties. We also ignore source and 254 | // insert our deploy URL instead, and include our own version logic. 255 | // TODO it might be nice to migrate this back to using the kotlin.cocoapods podspec task directly, but not worth the 256 | // effort to wire it up right now. 257 | private fun generatePodspec( 258 | safeCocoapodsData: SafeCocoapodsData, 259 | urlFile: File, 260 | projectVersion: String, 261 | outputFile: File, 262 | frameworkName: Provider 263 | ) = with(safeCocoapodsData) { 264 | val deploymentTargets = run { 265 | listOf(ios, osx, tvos, watchos).filter { it.deploymentTarget != null }.joinToString("\n") { 266 | if (extraSpecAttributes.containsKey("${it.name}.deployment_target")) "" else "| spec.${it.name}.deployment_target = '${it.deploymentTarget}'" 267 | } 268 | } 269 | 270 | val dependencies = podsDependencies 271 | 272 | val vendoredFramework = "${frameworkName.get()}.xcframework" 273 | val vendoredFrameworks = 274 | if (extraSpecAttributes.containsKey("vendored_frameworks")) "" else "| spec.vendored_frameworks = '$vendoredFramework'" 275 | 276 | val libraries = 277 | if (extraSpecAttributes.containsKey("libraries")) "" else "| spec.libraries = 'c++'" 278 | 279 | val customSpec = 280 | extraSpecAttributes.map { "| spec.${it.key} = ${it.value}" }.joinToString("\n") 281 | 282 | val url = urlFile.readText() 283 | val version = version ?: projectVersion 284 | 285 | // 'Accept: application/octet-stream' needed for GitHub release file downloads 286 | outputFile.writeText( 287 | """ 288 | |Pod::Spec.new do |spec| 289 | | spec.name = '$name' 290 | | spec.version = '$version' 291 | | spec.homepage = ${ 292 | homepage.orEmpty().surroundWithSingleQuotesIfNeeded() 293 | } 294 | | spec.source = { 295 | | :http => '${url}', 296 | | :type => 'zip', 297 | | :headers => ["Accept: application/octet-stream"] 298 | | } 299 | | spec.authors = ${ 300 | authors.orEmpty().surroundWithSingleQuotesIfNeeded() 301 | } 302 | | spec.license = ${ 303 | license.orEmpty().surroundWithSingleQuotesIfNeeded() 304 | } 305 | | spec.summary = '${summary.orEmpty()}' 306 | $vendoredFrameworks 307 | $libraries 308 | $deploymentTargets 309 | $dependencies 310 | $customSpec 311 | |end 312 | """.trimMargin() 313 | ) 314 | } 315 | 316 | private fun String.surroundWithSingleQuotesIfNeeded(): String = 317 | if (startsWith("{") || startsWith("<<-") || startsWith("'")) this else "'$this'" 318 | -------------------------------------------------------------------------------- /kmmbridge/src/main/kotlin/co/touchlab/kmmbridge/dependencymanager/DependencyManager.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Touchlab. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 4 | * in compliance with the License. You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software distributed under the License 9 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 10 | * or implied. See the License for the specific language governing permissions and limitations under 11 | * the License. 12 | */ 13 | 14 | package co.touchlab.kmmbridge.dependencymanager 15 | 16 | import org.gradle.api.Project 17 | import org.gradle.api.Task 18 | import org.gradle.api.provider.ProviderFactory 19 | import org.gradle.api.tasks.TaskProvider 20 | 21 | interface DependencyManager { 22 | /** 23 | * Do configuration specific to this `DependencyManager`. Generally this involves creating tasks that depend on 24 | * [uploadTask] and are dependencies of [publishRemoteTask]. 25 | */ 26 | fun configure( 27 | providers: ProviderFactory, 28 | project: Project, 29 | version: String, 30 | uploadTask: TaskProvider, 31 | publishRemoteTask: TaskProvider 32 | ) { 33 | } 34 | 35 | /** 36 | * True if this type of dependency needs git tags to function properly (currently SPM true, Cocoapods false) 37 | */ 38 | val needsGitTags: Boolean 39 | } -------------------------------------------------------------------------------- /kmmbridge/src/main/kotlin/co/touchlab/kmmbridge/dependencymanager/SpmDependencyManager.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Touchlab. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 4 | * in compliance with the License. You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software distributed under the License 9 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 10 | * or implied. See the License for the specific language governing permissions and limitations under 11 | * the License. 12 | */ 13 | 14 | package co.touchlab.kmmbridge.dependencymanager 15 | 16 | import co.touchlab.kmmbridge.TASK_GROUP_NAME 17 | import co.touchlab.kmmbridge.dsl.TargetPlatformDsl 18 | import co.touchlab.kmmbridge.internal.domain.SwiftToolVersion 19 | import co.touchlab.kmmbridge.internal.domain.TargetPlatform 20 | import co.touchlab.kmmbridge.internal.domain.konanTarget 21 | import co.touchlab.kmmbridge.internal.domain.swiftPackagePlatformName 22 | import co.touchlab.kmmbridge.internal.findXCFrameworkAssembleTask 23 | import co.touchlab.kmmbridge.internal.kmmBridgeExtension 24 | import co.touchlab.kmmbridge.internal.kotlin 25 | import co.touchlab.kmmbridge.internal.layoutBuildDir 26 | import co.touchlab.kmmbridge.internal.urlFile 27 | import co.touchlab.kmmbridge.internal.zipFilePath 28 | import org.gradle.api.Action 29 | import org.gradle.api.Project 30 | import org.gradle.api.Task 31 | import org.gradle.api.provider.ProviderFactory 32 | import org.gradle.api.provider.ValueSource 33 | import org.gradle.api.provider.ValueSourceParameters 34 | import org.gradle.api.tasks.TaskProvider 35 | import org.gradle.kotlin.dsl.withType 36 | import org.gradle.process.ExecOperations 37 | import org.gradle.process.internal.ExecException 38 | import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget 39 | import org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType 40 | import java.io.ByteArrayOutputStream 41 | import java.io.File 42 | import java.nio.charset.Charset 43 | import java.util.* 44 | import javax.inject.Inject 45 | 46 | internal class SpmDependencyManager( 47 | /** 48 | * Folder where the Package.swift file lives 49 | */ 50 | private val _swiftPackageFolder: String?, 51 | private val useCustomPackageFile: Boolean, 52 | private val perModuleVariablesBlock: Boolean, 53 | private val _swiftToolVersion: String, 54 | private val _targetPlatforms: TargetPlatformDsl.() -> Unit, 55 | ) : DependencyManager { 56 | /** 57 | * For new projects that aren't in git repos, it's *probably* OK to just return the current folder 58 | * until this is resolved, or let the user enter it manually. 59 | */ 60 | private fun Project.findRepoRoot(projectDir: File): String { 61 | val result = providers.of(GitRevParseValue::class.java) {}.get() 62 | val repoRootFile = if (result == "") { 63 | projectDir 64 | } else { 65 | File(result) 66 | } 67 | return repoRootFile.toString() 68 | } 69 | 70 | private fun Project.swiftPackageFile(projectDir: File): File { 71 | val swiftPackageFolderPath = _swiftPackageFolder ?: this.findRepoRoot(projectDir) 72 | return project.file("${stripEndSlash(swiftPackageFolderPath)}/Package.swift") 73 | } 74 | 75 | override fun configure( 76 | providers: ProviderFactory, 77 | project: Project, 78 | version: String, 79 | uploadTask: TaskProvider, 80 | publishRemoteTask: TaskProvider 81 | ) { 82 | val extension = project.kmmBridgeExtension 83 | val projectDir = project.projectDir 84 | val swiftToolVersion = SwiftToolVersion.of(_swiftToolVersion) 85 | ?: throw IllegalArgumentException("Parameter swiftToolVersion should be not blank!") 86 | val platforms = swiftTargetPlatforms(project) 87 | 88 | val swiftPackageFile = project.swiftPackageFile(project.rootDir) 89 | val packageName = extension.frameworkName.get() 90 | if (useCustomPackageFile && !hasKmmbridgeVariablesSection(swiftPackageFile, packageName)) { 91 | project.logger.error(buildPackageFileErrorMessage(packageName, perModuleVariablesBlock)) 92 | } 93 | 94 | val updatePackageSwiftTask = project.tasks.register("updatePackageSwift") { 95 | group = TASK_GROUP_NAME 96 | val zipFile = project.zipFilePath() 97 | inputs.files(zipFile, project.urlFile) 98 | outputs.files(swiftPackageFile) 99 | 100 | val urlFile = project.urlFile 101 | 102 | @Suppress("ObjectLiteralToLambda") 103 | doLast(object : Action { 104 | override fun execute(t: Task) { 105 | val checksum = providers.findSpmChecksum(zipFile, projectDir) 106 | val url = urlFile.readText() 107 | if (useCustomPackageFile && hasKmmbridgeVariablesSection( 108 | swiftPackageFile, 109 | packageName 110 | ) 111 | ) { 112 | modifyPackageFileVariables(swiftPackageFile, packageName, url, checksum) 113 | } else if (useCustomPackageFile) { 114 | // We warned you earlier, but you didn't fix it, so now we interrupt the publish process because it's 115 | // probably not going to do what you want 116 | error(buildPackageFileErrorMessage(packageName, perModuleVariablesBlock)) 117 | } else { 118 | writePackageFile( 119 | swiftPackageFile, 120 | extension.frameworkName.get(), 121 | url, 122 | checksum, 123 | swiftToolVersion, 124 | platforms 125 | ) 126 | } 127 | } 128 | }) 129 | } 130 | 131 | updatePackageSwiftTask.configure { dependsOn(uploadTask) } 132 | publishRemoteTask.configure { dependsOn(updatePackageSwiftTask) } 133 | } 134 | 135 | private fun hasKmmbridgeVariablesSection(swiftPackageFile: File, packageName: String): Boolean { 136 | val (startTag) = kmmBridgeVariablesForPackage(packageName, perModuleVariablesBlock) 137 | return swiftPackageFile.readText().contains(startTag) 138 | } 139 | 140 | private fun modifyPackageFileVariables( 141 | swiftPackageFile: File, 142 | packageName: String, 143 | url: String, 144 | checksum: String, 145 | ) { 146 | swiftPackageFile.writeText( 147 | getModifiedPackageFileText( 148 | swiftPackageFile.readText(), 149 | packageName, 150 | perModuleVariablesBlock, 151 | url, 152 | checksum 153 | ) 154 | ) 155 | } 156 | 157 | private fun writePackageFile( 158 | swiftPackageFile: File, 159 | packageName: String, 160 | url: String, 161 | checksum: String, 162 | swiftToolVersion: SwiftToolVersion, 163 | platforms: String 164 | ) { 165 | 166 | val packageText = 167 | makePackageFileText( 168 | packageName, 169 | url, 170 | checksum, 171 | perModuleVariablesBlock, 172 | swiftToolVersion, 173 | platforms 174 | ) 175 | swiftPackageFile.parentFile.mkdirs() 176 | swiftPackageFile.writeText(packageText) 177 | } 178 | 179 | private fun ProviderFactory.findSpmChecksum(zipFilePath: File, swiftPackageFile: File): String { 180 | // checksum requires a package file, but doesn't require it to be real (and we might only have cocoapods) 181 | val hadPackageSwift = swiftPackageFile.exists() 182 | 183 | if (!hadPackageSwift) { 184 | swiftPackageFile.writeText("") 185 | } 186 | 187 | val checksumOut = exec { 188 | commandLine( 189 | "swift", 190 | "package", 191 | "compute-checksum", 192 | zipFilePath.path 193 | ) 194 | }.standardOutput.asText.get() 195 | 196 | if (!hadPackageSwift) { 197 | swiftPackageFile.delete() 198 | } 199 | 200 | return checksumOut.trim() 201 | } 202 | 203 | override val needsGitTags: Boolean = true 204 | fun configureLocalDev(project: Project) { 205 | if (useCustomPackageFile) return // No local dev when using a custom package file 206 | 207 | val extension = project.kmmBridgeExtension 208 | val swiftToolVersion = SwiftToolVersion.of(_swiftToolVersion) 209 | ?: throw IllegalArgumentException("Parameter swiftToolVersion should be not blank!") 210 | val platforms = swiftTargetPlatforms(project) 211 | 212 | project.tasks.register("spmDevBuild") { 213 | description = 214 | "When using SPM, builds a debug version of the XCFramework and writes a local dev path to your Package.swift." 215 | group = TASK_GROUP_NAME 216 | dependsOn(project.findXCFrameworkAssembleTask(NativeBuildType.DEBUG)) 217 | 218 | val swiftPackageFile = project.swiftPackageFile(project.rootDir) 219 | val layoutBuildDir = project.layoutBuildDir 220 | 221 | @Suppress("ObjectLiteralToLambda") 222 | doLast(object : Action { 223 | override fun execute(t: Task) { 224 | swiftPackageFile.writeText( 225 | makeLocalDevPackageFileText( 226 | swiftPackageFile, 227 | layoutBuildDir, 228 | extension.frameworkName.get(), 229 | swiftToolVersion, 230 | platforms 231 | ) 232 | ) 233 | } 234 | }) 235 | } 236 | } 237 | 238 | private fun swiftTargetPlatforms(project: Project): String { 239 | val targetPlatforms = TargetPlatformDsl().apply(_targetPlatforms) 240 | .targetPlatforms 241 | .ifEmpty { 242 | throw IllegalArgumentException("At least one target platform should be specified!") 243 | } 244 | 245 | val platforms = platforms(project, targetPlatforms) 246 | return platforms 247 | } 248 | 249 | private fun platforms(project: Project, targetPlatforms: List): String = 250 | targetPlatforms.flatMap { platform -> 251 | project.kotlin.targets 252 | .withType() 253 | .asSequence() 254 | .filter { it.konanTarget.family.isAppleFamily } 255 | .filter { appleTarget -> platform.targets.firstOrNull { it.konanTarget == appleTarget.konanTarget } != null } 256 | .mapNotNull { it.konanTarget.family.swiftPackagePlatformName } 257 | .distinct() 258 | .map { platformName -> ".$platformName(.v${platform.version.name})" } 259 | .toList() 260 | }.joinToString(separator = ",\n") 261 | } 262 | 263 | /** 264 | * Runs a git command to grab the root of the git repo. If there is no git repo, return an empty string. 265 | */ 266 | abstract class GitRevParseValue : ValueSource { 267 | @get:Inject 268 | abstract val execOperations: ExecOperations 269 | 270 | override fun obtain(): String { 271 | val output = ByteArrayOutputStream() 272 | val error = ByteArrayOutputStream() 273 | return try { 274 | execOperations.exec { 275 | try { 276 | commandLine("git", "rev-parse", "--show-toplevel") 277 | standardOutput = output 278 | errorOutput = error 279 | } catch (e: Exception) { 280 | } 281 | } 282 | String(output.toByteArray(), Charset.defaultCharset()).lines().first() 283 | } catch (e: ExecException) { 284 | "" 285 | } 286 | } 287 | } 288 | 289 | internal fun stripEndSlash(path: String): String { 290 | return if (path.endsWith("/")) { 291 | path.substring(0, path.length - 1) 292 | } else { 293 | path 294 | } 295 | } 296 | 297 | private fun makeLocalDevPackageFileText( 298 | swiftPackageFile: File, 299 | layoutBuildDir: File, 300 | frameworkName: String, 301 | swiftToolVersion: SwiftToolVersion, 302 | platforms: String 303 | ): String { 304 | val swiftFolderPath = swiftPackageFile.parentFile.toPath() 305 | val projectBuildFolderPath = layoutBuildDir.toPath() 306 | val xcFrameworkPath = 307 | "${swiftFolderPath.relativize(projectBuildFolderPath)}/XCFrameworks/${NativeBuildType.DEBUG.getName()}" 308 | val packageFileString = """ 309 | // swift-tools-version:${swiftToolVersion.name} 310 | import PackageDescription 311 | 312 | let packageName = "$frameworkName" 313 | 314 | let package = Package( 315 | name: packageName, 316 | platforms: [ 317 | $platforms 318 | ], 319 | products: [ 320 | .library( 321 | name: packageName, 322 | targets: [packageName] 323 | ), 324 | ], 325 | targets: [ 326 | .binaryTarget( 327 | name: packageName, 328 | path: "./${xcFrameworkPath}/\(packageName).xcframework" 329 | ) 330 | , 331 | ] 332 | ) 333 | """.trimIndent() 334 | return packageFileString 335 | } 336 | 337 | internal fun getModifiedPackageFileText( 338 | oldPackageFile: String, 339 | packageName: String, 340 | perModuleVariablesBlock: Boolean, 341 | url: String, 342 | checksum: String, 343 | ): String = buildString { 344 | var editingManagedBlock = false 345 | val (startTag, endTag) = kmmBridgeVariablesForPackage(packageName, perModuleVariablesBlock) 346 | 347 | oldPackageFile.lines().forEach { line -> 348 | when { 349 | line.trim() == endTag -> { 350 | editingManagedBlock = false 351 | } 352 | 353 | editingManagedBlock -> { 354 | // Ignore old lines in our managed blocks because we've already edited them 355 | } 356 | 357 | line.trim() == startTag -> { 358 | editingManagedBlock = true 359 | val indent = line.split(startTag).first() 360 | 361 | appendLine( 362 | makePackageDetailsText(packageName, url, checksum, perModuleVariablesBlock) 363 | .prependIndent(indent) 364 | ) 365 | } 366 | 367 | else -> { 368 | appendLine(line) 369 | } 370 | } 371 | } 372 | }.removeSuffix("\n") 373 | 374 | private fun kmmBridgeVariablesForPackage( 375 | packageName: String, 376 | perModuleVariablesBlock: Boolean, 377 | ): Pair { 378 | if (!perModuleVariablesBlock) { 379 | return "// BEGIN KMMBRIDGE VARIABLES BLOCK (do not edit)" to "// END KMMBRIDGE BLOCK" 380 | } 381 | 382 | return "// BEGIN KMMBRIDGE VARIABLES BLOCK FOR '$packageName' (do not edit)" to "// END KMMBRIDGE BLOCK FOR '$packageName'" 383 | } 384 | 385 | private fun makePackageFileText( 386 | packageName: String, 387 | url: String, 388 | checksum: String, 389 | perModuleVariablesBlock: Boolean, 390 | swiftToolVersion: SwiftToolVersion, 391 | platforms: String 392 | ): String = """ 393 | // swift-tools-version:${swiftToolVersion.name} 394 | import PackageDescription 395 | 396 | ${makePackageDetailsText(packageName, url, checksum, perModuleVariablesBlock)} 397 | 398 | let package = Package( 399 | name: ${packageNameVariableName(packageName, perModuleVariablesBlock)}, 400 | platforms: [ 401 | $platforms 402 | ], 403 | products: [ 404 | .library( 405 | name: ${packageNameVariableName(packageName, perModuleVariablesBlock)}, 406 | targets: [${packageNameVariableName(packageName, perModuleVariablesBlock)}] 407 | ), 408 | ], 409 | targets: [ 410 | .binaryTarget( 411 | name: ${packageNameVariableName(packageName, perModuleVariablesBlock)}, 412 | url: ${urlVariableName(packageName, perModuleVariablesBlock)}, 413 | checksum: ${checksumVariableName(packageName, perModuleVariablesBlock)} 414 | ) 415 | , 416 | ] 417 | ) 418 | """.trimIndent() 419 | 420 | private fun makePackageDetailsText( 421 | packageName: String, 422 | url: String, 423 | checksum: String, 424 | perModuleVariablesBlock: Boolean, 425 | ): String { 426 | val (startTag, endTag) = kmmBridgeVariablesForPackage(packageName, perModuleVariablesBlock) 427 | 428 | val remoteUrlVarName = urlVariableName(packageName, perModuleVariablesBlock) 429 | val remoteChecksumVarName = checksumVariableName(packageName, perModuleVariablesBlock) 430 | val remotePackageName = packageNameVariableName(packageName, perModuleVariablesBlock) 431 | 432 | return """ 433 | $startTag 434 | let $remoteUrlVarName = "$url" 435 | let $remoteChecksumVarName = "$checksum" 436 | let $remotePackageName = "$packageName" 437 | $endTag 438 | """.trimIndent() 439 | } 440 | 441 | private fun urlVariableName(packageName: String, perModuleVariablesBlock: Boolean): String = 442 | if (perModuleVariablesBlock) { 443 | "remote${packageName}Url" 444 | } else { 445 | "remoteKotlinUrl" 446 | } 447 | 448 | private fun checksumVariableName(packageName: String, perModuleVariablesBlock: Boolean): String = 449 | if (perModuleVariablesBlock) { 450 | "remote${packageName}Checksum" 451 | } else { 452 | "remoteKotlinChecksum" 453 | } 454 | 455 | private fun packageNameVariableName(packageName: String, perModuleVariablesBlock: Boolean): String = 456 | if (perModuleVariablesBlock) { 457 | "${packageName.replaceFirstChar { it.lowercase(Locale.US) }}PackageName" 458 | } else { 459 | "packageName" 460 | } 461 | 462 | private fun buildPackageFileErrorMessage( 463 | packageName: String, 464 | perModuleVariablesBlock: Boolean, 465 | ): String { 466 | val (beginTag, endTag) = kmmBridgeVariablesForPackage(packageName, perModuleVariablesBlock) 467 | 468 | return """ 469 | KMMBridge: SPM configured with useCustomPackageFile=true, but no custom variable block detected! Add the following lines to your package file to generate variables for binaryTarget() declaration: 470 | $beginTag 471 | $endTag 472 | """.trimIndent() 473 | } -------------------------------------------------------------------------------- /kmmbridge/src/main/kotlin/co/touchlab/kmmbridge/dsl/TargetPlatformDsl.kt: -------------------------------------------------------------------------------- 1 | package co.touchlab.kmmbridge.dsl 2 | 3 | import co.touchlab.kmmbridge.internal.domain.PlatformVersion 4 | import co.touchlab.kmmbridge.internal.domain.TargetName 5 | import co.touchlab.kmmbridge.internal.domain.TargetPlatform 6 | import groovy.lang.Closure 7 | import org.gradle.util.internal.ConfigureUtil 8 | 9 | /** 10 | * DSL to create instances of [TargetPlatform] 11 | */ 12 | public class TargetPlatformDsl { 13 | internal var targetPlatforms: MutableList = mutableListOf() 14 | 15 | /** 16 | * Adds all iOS targets as a [TargetPlatform] using the provided [version] 17 | * 18 | * @param version builder for an instance of [PlatformVersion] 19 | */ 20 | public fun iOS(version: PlatformVersionDsl.() -> Unit) { 21 | targetsInternal( 22 | listOf( 23 | TargetName.IOSarm64, 24 | TargetName.IOSx64, 25 | TargetName.IOSSimulatorArm64, 26 | ), 27 | version 28 | ) 29 | } 30 | 31 | public fun iOS(version: Closure) { 32 | iOS { ConfigureUtil.configure(version, this) } 33 | } 34 | 35 | /** 36 | * Adds all macOS targets as a [TargetPlatform] using the provided [version] 37 | * 38 | * @param version builder for an instance of [PlatformVersion] 39 | */ 40 | public fun macOS(version: PlatformVersionDsl.() -> Unit) { 41 | targetsInternal( 42 | listOf( 43 | TargetName.macosArm64, 44 | TargetName.macosX64, 45 | ), 46 | version 47 | ) 48 | } 49 | 50 | public fun macOS(version: Closure) { 51 | macOS { ConfigureUtil.configure(version, this) } 52 | } 53 | 54 | /** 55 | * Adds all tvOS targets as a [TargetPlatform] using the provided [version] 56 | * 57 | * @param version builder for an instance of [PlatformVersion] 58 | */ 59 | public fun tvOS(version: PlatformVersionDsl.() -> Unit) { 60 | targetsInternal( 61 | listOf( 62 | TargetName.tvosX64, 63 | TargetName.tvosArm64, 64 | TargetName.tvosSimulatorArm64, 65 | ), 66 | version 67 | ) 68 | } 69 | 70 | public fun tvOS(version: Closure) { 71 | tvOS { ConfigureUtil.configure(version, this) } 72 | } 73 | 74 | /** 75 | * Adds all watchOS targets as a [TargetPlatform] using the provided [version] 76 | * 77 | * @param version builder for an instance of [PlatformVersion] 78 | */ 79 | public fun watchOS(version: PlatformVersionDsl.() -> Unit) { 80 | targetsInternal( 81 | listOf( 82 | TargetName.watchosX64, 83 | TargetName.watchosArm32, 84 | TargetName.watchosArm64, 85 | TargetName.watchosDeviceArm64, 86 | TargetName.watchosSimulatorArm64, 87 | ), 88 | version 89 | ) 90 | } 91 | 92 | public fun watchOS(version: Closure) { 93 | watchOS { ConfigureUtil.configure(version, this) } 94 | } 95 | 96 | private fun targetsInternal(names: List, configure: PlatformVersionDsl.() -> Unit) { 97 | val platformVersion: PlatformVersion = PlatformVersionDsl().apply(configure).version ?: return 98 | 99 | targetPlatforms.add(TargetPlatform(version = platformVersion, targets = names)) 100 | } 101 | 102 | public class PlatformVersionDsl { 103 | internal var version: PlatformVersion? = null 104 | 105 | public fun v(versionName: String) { 106 | PlatformVersion.of(versionName)?.let { this.version = it } 107 | } 108 | } 109 | 110 | } -------------------------------------------------------------------------------- /kmmbridge/src/main/kotlin/co/touchlab/kmmbridge/internal/ProjectExtensionsInternal.kt: -------------------------------------------------------------------------------- 1 | package co.touchlab.kmmbridge.internal 2 | 3 | import co.touchlab.kmmbridge.KmmBridgeExtension 4 | import co.touchlab.kmmbridge.findStringProperty 5 | import org.gradle.api.Project 6 | import org.gradle.api.Task 7 | import org.gradle.api.UnknownTaskException 8 | import org.gradle.api.plugins.ExtensionAware 9 | import org.gradle.api.tasks.TaskProvider 10 | import org.gradle.kotlin.dsl.findByType 11 | import org.gradle.kotlin.dsl.getByType 12 | import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension 13 | import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension 14 | import org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType 15 | import java.io.File 16 | 17 | internal val Project.layoutBuildDir get() = layout.buildDirectory.get().asFile 18 | 19 | internal val Project.kotlin: KotlinMultiplatformExtension get() = extensions.getByType() 20 | internal val Project.kmmBridgeExtension get() = extensions.getByType() 21 | 22 | internal val Project.urlFile get() = file("$layoutBuildDir/kmmbridge/url") 23 | 24 | // Cocoapods is an extension of KMP extension, so you can't just do project.extensions.getByType() 25 | internal val KotlinMultiplatformExtension.cocoapodsOrNull get() = (this as ExtensionAware).extensions.findByType() 26 | internal val KotlinMultiplatformExtension.cocoapods 27 | get() = cocoapodsOrNull 28 | ?: error("You must apply the org.jetbrains.kotlin.native.cocoapods plugin to use cocoapods() configuration") 29 | 30 | // This previously defaulted to 'false', but now you can disable it if needed, but otherwise ignore 31 | internal val Project.enablePublishing: Boolean 32 | get() = project.findStringProperty("ENABLE_PUBLISHING")?.toBoolean() ?: false 33 | 34 | internal val Project.spmBuildTargets: String? 35 | get() = project.findStringProperty("spmBuildTargets") 36 | 37 | @Suppress("SpellCheckingInspection") 38 | internal fun Project.zipFilePath(): File { 39 | val tempDir = file("$layoutBuildDir/kmmbridge/zip") 40 | val artifactName = "frameworkarchive.zip" 41 | return file("$tempDir/$artifactName") 42 | } 43 | 44 | internal fun Project.findXCFrameworkAssembleTask(buildType: NativeBuildType? = null): TaskProvider { 45 | val extension = extensions.getByType() 46 | val name = extension.frameworkName.get() 47 | val buildTypeString = (buildType ?: extension.buildType.get()).getName().capitalized() 48 | val taskWithoutName = "assemble${buildTypeString}XCFramework" 49 | val taskWithName = "assemble${name.capitalized()}${buildTypeString}XCFramework" 50 | return runCatching { 51 | tasks.named(taskWithName) 52 | }.recoverCatching { 53 | tasks.named(taskWithoutName) 54 | }.getOrElse { 55 | throw UnknownTaskException( 56 | "Cannot find XCFramework assemble task. Tried $taskWithName and ${taskWithoutName}." 57 | ) 58 | } 59 | } -------------------------------------------------------------------------------- /kmmbridge/src/main/kotlin/co/touchlab/kmmbridge/internal/StringExtensions.kt: -------------------------------------------------------------------------------- 1 | package co.touchlab.kmmbridge.internal 2 | 3 | import java.util.* 4 | 5 | internal fun String.capitalized(): String { 6 | return this.replaceFirstChar { 7 | if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() 8 | } 9 | } -------------------------------------------------------------------------------- /kmmbridge/src/main/kotlin/co/touchlab/kmmbridge/internal/domain/PlatformVersion.kt: -------------------------------------------------------------------------------- 1 | package co.touchlab.kmmbridge.internal.domain 2 | 3 | internal class PlatformVersion private constructor(private val value: String) { 4 | 5 | val name get() = value 6 | 7 | internal companion object { 8 | internal fun of(version: String): PlatformVersion? = 9 | version.takeIf(String::isNotBlank)?.let(::PlatformVersion) 10 | } 11 | 12 | override fun equals(other: Any?): Boolean = value == (other as? PlatformVersion)?.value 13 | 14 | override fun hashCode(): Int = value.hashCode() 15 | 16 | override fun toString() = "PlatformVersion(name='$name')" 17 | 18 | } -------------------------------------------------------------------------------- /kmmbridge/src/main/kotlin/co/touchlab/kmmbridge/internal/domain/SwiftToolVersion.kt: -------------------------------------------------------------------------------- 1 | package co.touchlab.kmmbridge.internal.domain 2 | 3 | internal class SwiftToolVersion private constructor(private val value: String) { 4 | 5 | val name get() = value 6 | 7 | internal companion object { 8 | internal const val Default = "5.3" 9 | 10 | internal fun of(version: String): SwiftToolVersion? = 11 | version.takeIf(String::isNotBlank)?.let(::SwiftToolVersion) 12 | } 13 | 14 | override fun equals(other: Any?): Boolean = value == (other as? SwiftToolVersion)?.value 15 | 16 | override fun hashCode(): Int = value.hashCode() 17 | 18 | } -------------------------------------------------------------------------------- /kmmbridge/src/main/kotlin/co/touchlab/kmmbridge/internal/domain/TargetName.kt: -------------------------------------------------------------------------------- 1 | package co.touchlab.kmmbridge.internal.domain 2 | 3 | internal enum class TargetName(val identifier: String) { 4 | IOSarm64("iosArm64"), 5 | IOSx64("iosX64"), 6 | IOSSimulatorArm64("iosSimulatorArm64"), 7 | macosArm64("macosArm64"), 8 | macosX64("macosX64"), 9 | tvosX64("tvosX64"), 10 | tvosArm64("tvosArm64"), 11 | tvosSimulatorArm64("tvosSimulatorArm64"), 12 | watchosX64("watchosX64"), 13 | watchosArm32("watchosArm32"), 14 | watchosArm64("watchosArm64"), 15 | watchosDeviceArm64("watchosDeviceArm64"), 16 | watchosSimulatorArm64("watchosSimulatorArm64") 17 | ; 18 | } -------------------------------------------------------------------------------- /kmmbridge/src/main/kotlin/co/touchlab/kmmbridge/internal/domain/TargetPlatform.kt: -------------------------------------------------------------------------------- 1 | package co.touchlab.kmmbridge.internal.domain 2 | 3 | internal data class TargetPlatform( 4 | val version: PlatformVersion, 5 | val targets: List 6 | ) -------------------------------------------------------------------------------- /kmmbridge/src/main/kotlin/co/touchlab/kmmbridge/internal/domain/extensions.kt: -------------------------------------------------------------------------------- 1 | package co.touchlab.kmmbridge.internal.domain 2 | 3 | import org.jetbrains.kotlin.konan.target.Family 4 | import org.jetbrains.kotlin.konan.target.KonanTarget 5 | 6 | internal val Family.swiftPackagePlatformName 7 | get() = when (this) { 8 | Family.OSX -> "macOS" 9 | Family.IOS -> "iOS" 10 | Family.TVOS -> "tvOS" 11 | Family.WATCHOS -> "watchOS" 12 | else -> null 13 | } 14 | 15 | internal val TargetName.konanTarget: KonanTarget 16 | get() = when (this) { 17 | TargetName.IOSarm64 -> KonanTarget.IOS_ARM64 18 | TargetName.IOSx64 -> KonanTarget.IOS_X64 19 | TargetName.IOSSimulatorArm64 -> KonanTarget.IOS_SIMULATOR_ARM64 20 | TargetName.watchosArm32 -> KonanTarget.WATCHOS_ARM32 21 | TargetName.watchosArm64 -> KonanTarget.WATCHOS_ARM64 22 | TargetName.watchosX64 -> KonanTarget.WATCHOS_X64 23 | TargetName.watchosSimulatorArm64 -> KonanTarget.WATCHOS_SIMULATOR_ARM64 24 | TargetName.tvosArm64 -> KonanTarget.TVOS_ARM64 25 | TargetName.tvosX64 -> KonanTarget.TVOS_X64 26 | TargetName.tvosSimulatorArm64 -> KonanTarget.TVOS_SIMULATOR_ARM64 27 | TargetName.macosX64 -> KonanTarget.MACOS_X64 28 | TargetName.macosArm64 -> KonanTarget.MACOS_ARM64 29 | TargetName.watchosDeviceArm64 -> KonanTarget.WATCHOS_DEVICE_ARM64 30 | } -------------------------------------------------------------------------------- /kmmbridge/src/test/kotlin/co/touchlab/kmmbridge/ArtifactManagerTest.kt: -------------------------------------------------------------------------------- 1 | package co.touchlab.kmmbridge 2 | 3 | import org.junit.jupiter.api.Test 4 | import java.io.File 5 | import kotlin.test.assertEquals 6 | import kotlin.test.assertFalse 7 | import kotlin.test.assertTrue 8 | 9 | class ArtifactManagerTest : BasePluginTest() { 10 | override fun testProjectPath(): String = "test-projects/basic" 11 | 12 | @Test 13 | fun runKmmBridgePublishNoPublishingEnabled() { 14 | val result = ProcessHelper.runSh( 15 | "./gradlew kmmBridgePublish " + 16 | "-PTOUCHLAB_TEST_ARTIFACT_SERVER=api.touchlab.dev " + 17 | "-PTOUCHLAB_TEST_ARTIFACT_CODE=${TOUCHLAB_TEST_ARTIFACT_CODE} " + 18 | "--stacktrace", workingDir = testProjectDir 19 | ) 20 | logExecResult(result) 21 | assertEquals(1, result.status) 22 | } 23 | 24 | @Test 25 | fun runKmmBridgePublish() { 26 | val urlFile = File(testProjectDir, "allshared/build/kmmbridge/url") 27 | assertFalse(urlFile.exists()) 28 | val result = ProcessHelper.runSh( 29 | "./gradlew clean kmmBridgePublish " + 30 | "-PENABLE_PUBLISHING=true " + 31 | "-PTOUCHLAB_TEST_ARTIFACT_SERVER=api.touchlab.dev " + 32 | "-PTOUCHLAB_TEST_ARTIFACT_CODE=${TOUCHLAB_TEST_ARTIFACT_CODE} " + 33 | "--stacktrace", 34 | workingDir = testProjectDir 35 | ) 36 | logExecResult(result) 37 | 38 | assertTrue(urlFile.exists()) 39 | val urlValue = urlFile.readText() 40 | assertTrue(urlValue.startsWith("https://api.touchlab.dev/infoadmin/streamTestZip")) 41 | assertEquals(0, result.status) 42 | } 43 | } -------------------------------------------------------------------------------- /kmmbridge/src/test/kotlin/co/touchlab/kmmbridge/BasePluginTest.kt: -------------------------------------------------------------------------------- 1 | package co.touchlab.kmmbridge 2 | 3 | import org.apache.commons.io.FileUtils 4 | import org.junit.jupiter.api.BeforeEach 5 | import org.junit.jupiter.api.io.TempDir 6 | import java.io.File 7 | import java.io.FileInputStream 8 | import java.util.* 9 | 10 | abstract class BasePluginTest { 11 | @TempDir 12 | lateinit var testProjectDir: File 13 | internal val assumedRootProjectDir = File(File("..").absolutePath) 14 | private val testProjectSource = File(assumedRootProjectDir, testProjectPath()) 15 | 16 | internal lateinit var settingsFile: File 17 | internal lateinit var buildFile: File 18 | internal lateinit var TOUCHLAB_TEST_ARTIFACT_CODE: String 19 | 20 | abstract fun testProjectPath(): String 21 | 22 | @BeforeEach 23 | fun setup() { 24 | TOUCHLAB_TEST_ARTIFACT_CODE = File("TOUCHLAB_TEST_ARTIFACT_CODE").readText().lines().first() 25 | FileUtils.copyDirectory(testProjectSource, testProjectDir) 26 | ProcessHelper.runSh("git init;git add .;git commit -m 'arst'", workingDir = testProjectDir) 27 | settingsFile = File(testProjectDir, "settings.gradle.kts") 28 | buildFile = File(testProjectDir, "build.gradle.kts") 29 | } 30 | 31 | internal fun loadTestGradleProperties(): Properties { 32 | val properties = Properties() 33 | FileInputStream(File(testProjectDir, "gradle.properties")).use { stream -> 34 | properties.load(stream) 35 | } 36 | return properties 37 | } 38 | 39 | internal fun logExecResult(result: ExecutionResult) { 40 | println("***********START***********") 41 | println("Params: ${result.params.joinToString(" ")}") 42 | println("Working dir: ${result.workingDir.absolutePath}") 43 | 44 | if (result.output.isNotEmpty()) 45 | println(result.output) 46 | if (result.error.isNotEmpty()) 47 | System.err.println(result.error) 48 | println("***********END***********") 49 | } 50 | } -------------------------------------------------------------------------------- /kmmbridge/src/test/kotlin/co/touchlab/kmmbridge/KmmBridgeExtensionTest.kt: -------------------------------------------------------------------------------- 1 | package co.touchlab.kmmbridge 2 | 3 | import co.touchlab.kmmbridge.artifactmanager.ArtifactManager 4 | import co.touchlab.kmmbridge.artifactmanager.AwsS3PublicArtifactManager 5 | import co.touchlab.kmmbridge.artifactmanager.MavenPublishArtifactManager 6 | import co.touchlab.kmmbridge.dependencymanager.CocoapodsDependencyManager 7 | import co.touchlab.kmmbridge.dependencymanager.DependencyManager 8 | import co.touchlab.kmmbridge.dependencymanager.SpmDependencyManager 9 | import org.gradle.api.Project 10 | import org.gradle.api.provider.Property 11 | import org.gradle.testfixtures.ProjectBuilder 12 | import org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType 13 | import kotlin.test.BeforeTest 14 | import kotlin.test.Ignore 15 | import kotlin.test.Test 16 | import kotlin.test.assertEquals 17 | import kotlin.test.assertTrue 18 | 19 | class KmmBridgeExtensionTest { 20 | private lateinit var project: Project 21 | private lateinit var extension: TestKmmBridgeExtension 22 | 23 | @BeforeTest 24 | fun setup() { 25 | project = ProjectBuilder.builder().build() 26 | extension = TestKmmBridgeExtension(project) 27 | } 28 | 29 | @Test 30 | fun `test s3 artifact configuration`() { 31 | extension.apply { 32 | project.s3PublicArtifacts( 33 | region = "us-east-1", 34 | bucket = "test-bucket", 35 | accessKeyId = "test-key", 36 | secretAccessKey = "test-secret" 37 | ) 38 | } 39 | 40 | val artifactManager = extension.artifactManager.get() 41 | assertTrue(artifactManager is AwsS3PublicArtifactManager) 42 | } 43 | 44 | @Test 45 | fun `test maven publish configuration`() { 46 | extension.apply { 47 | project.mavenPublishArtifacts( 48 | repository = "test-repo", 49 | publication = "test-pub", 50 | isMavenCentral = true 51 | ) 52 | } 53 | 54 | val artifactManager = extension.artifactManager.get() 55 | assertTrue(artifactManager is MavenPublishArtifactManager) 56 | } 57 | 58 | @Test 59 | fun `test spm configuration`() { 60 | extension.apply { 61 | project.spm( 62 | spmDirectory = "test-dir", 63 | useCustomPackageFile = true 64 | ) 65 | } 66 | 67 | val dependencyManager = extension.dependencyManagers.get().first() 68 | assertTrue(dependencyManager is SpmDependencyManager) 69 | } 70 | 71 | @Test 72 | @Ignore("CocoaPods plugin not loaded in test environment. Trunk specifically isn't important.") 73 | fun `test cocoapods trunk configuration`() { 74 | extension.apply { 75 | project.cocoapodsTrunk( 76 | allowWarnings = true, 77 | verboseErrors = true 78 | ) 79 | } 80 | 81 | val dependencyManager = extension.dependencyManagers.get().first() 82 | assertTrue(dependencyManager is CocoapodsDependencyManager) 83 | } 84 | 85 | @Test 86 | fun `test property finalization`() { 87 | val testValue = "test-framework" 88 | extension.frameworkName.set(testValue) 89 | extension.frameworkName.finalizeValue() 90 | 91 | assertEquals(testValue, extension.frameworkName.get()) 92 | assertTrue(extension.frameworkName.isPresent) 93 | } 94 | } 95 | 96 | private class TestKmmBridgeExtension(private val project: Project) : KmmBridgeExtension { 97 | override val frameworkName: Property = project.objects.property(String::class.java) 98 | override val dependencyManagers = project.objects.listProperty(DependencyManager::class.java) 99 | override val artifactManager = project.objects.property(ArtifactManager::class.java) 100 | override val buildType: Property = project.objects.property(NativeBuildType::class.java) 101 | } 102 | -------------------------------------------------------------------------------- /kmmbridge/src/test/kotlin/co/touchlab/kmmbridge/NonKmmBridgeTasksTest.kt: -------------------------------------------------------------------------------- 1 | package co.touchlab.kmmbridge 2 | 3 | import org.junit.jupiter.api.Test 4 | import kotlin.test.assertEquals 5 | 6 | /** 7 | * Tests to ensure KMMBridge doesn't impact non-KMMBridge Gradle operation in any significant way. 8 | */ 9 | class NonKmmBridgeTasksTest : BasePluginTest() { 10 | override fun testProjectPath(): String = "test-projects/basic" 11 | 12 | @Test 13 | fun runBasicBuild() { 14 | val result = ProcessHelper.runSh( 15 | "./gradlew linkDebugFrameworkIosSimulatorArm64 " + 16 | "-PTOUCHLAB_TEST_ARTIFACT_SERVER=api.touchlab.dev " + 17 | "-PTOUCHLAB_TEST_ARTIFACT_CODE=${TOUCHLAB_TEST_ARTIFACT_CODE}", workingDir = testProjectDir 18 | ) 19 | logExecResult(result) 20 | assertEquals(0, result.status) 21 | } 22 | } -------------------------------------------------------------------------------- /kmmbridge/src/test/kotlin/co/touchlab/kmmbridge/ProcessHelper.kt: -------------------------------------------------------------------------------- 1 | package co.touchlab.kmmbridge 2 | 3 | import java.io.BufferedReader 4 | import java.io.File 5 | import java.io.InputStream 6 | import java.io.InputStreamReader 7 | import java.util.concurrent.atomic.AtomicReference 8 | 9 | object ProcessHelper { 10 | fun runSh( 11 | command: String, 12 | envVars: Map = emptyMap(), 13 | workingDir: File = File(".") 14 | ): ExecutionResult { 15 | return runParams("/bin/sh", "-c", command, envVars = envVars, workingDir = workingDir) 16 | } 17 | 18 | fun runParams( 19 | vararg params: String, 20 | envVars: Map = emptyMap(), 21 | workingDir: File = File(".") 22 | ): ExecutionResult { 23 | val processBuilder = ProcessBuilder(*params) 24 | processBuilder.environment().putAll(envVars) 25 | processBuilder.directory(workingDir) 26 | val process = processBuilder 27 | .start() 28 | 29 | val stdOut = readProcStream(process.inputStream) 30 | val errOut = readProcStream(process.errorStream) 31 | 32 | val returnValue = process.waitFor() 33 | 34 | while (!stdOut.isDone && !errOut.isDone) { 35 | Thread.sleep(1000) 36 | } 37 | 38 | return ExecutionResult( 39 | params = params.toList(), 40 | workingDir = workingDir, 41 | status = returnValue, 42 | output = stdOut.result, 43 | error = errOut.result 44 | ) 45 | } 46 | 47 | private fun readProcStream(iStream: InputStream): StreamCatcher { 48 | val atom = AtomicReference("") 49 | val t = Thread { 50 | val bufferedReader = BufferedReader(InputStreamReader(iStream)) 51 | val allOut = bufferedReader.readText() 52 | 53 | bufferedReader.close() 54 | atom.set(allOut) 55 | } 56 | 57 | t.start() 58 | 59 | return StreamCatcher(t, atom) 60 | } 61 | 62 | private class StreamCatcher(val t: Thread, val atom: AtomicReference) { 63 | val isDone: Boolean 64 | get() = !t.isAlive 65 | val result: String 66 | get() = atom.get() 67 | } 68 | } 69 | 70 | data class ExecutionResult( 71 | val params: List, 72 | val workingDir: File, 73 | val status: Int, 74 | val output: String, 75 | val error: String 76 | ) -------------------------------------------------------------------------------- /kmmbridge/src/test/kotlin/co/touchlab/kmmbridge/SpmLocalDevTest.kt: -------------------------------------------------------------------------------- 1 | package co.touchlab.kmmbridge 2 | 3 | import org.junit.jupiter.api.Test 4 | import kotlin.test.assertEquals 5 | 6 | class SpmLocalDevTest : BasePluginTest() { 7 | override fun testProjectPath(): String = "test-projects/basic" 8 | 9 | @Test 10 | fun runSpmDevBuild() { 11 | val result = ProcessHelper.runSh( 12 | "./gradlew spmDevBuild --stacktrace " + 13 | "-PTOUCHLAB_TEST_ARTIFACT_SERVER=api.touchlab.dev " + 14 | "-PTOUCHLAB_TEST_ARTIFACT_CODE=${TOUCHLAB_TEST_ARTIFACT_CODE}", workingDir = testProjectDir 15 | ) 16 | logExecResult(result) 17 | assertEquals(0, result.status) 18 | } 19 | 20 | /** 21 | * Ensure that SPM local dev can load and run when there is no git repo set up. 22 | */ 23 | @Test 24 | fun runSpmDevBuildNoGit() { 25 | ProcessHelper.runSh("rm -rdf .git", workingDir = testProjectDir) 26 | val result = ProcessHelper.runSh( 27 | "./gradlew spmDevBuild --stacktrace " + 28 | "-PTOUCHLAB_TEST_ARTIFACT_SERVER=api.touchlab.dev " + 29 | "-PTOUCHLAB_TEST_ARTIFACT_CODE=${TOUCHLAB_TEST_ARTIFACT_CODE}", workingDir = testProjectDir 30 | ) 31 | logExecResult(result) 32 | assertEquals(0, result.status) 33 | } 34 | } -------------------------------------------------------------------------------- /kmmbridge/src/test/kotlin/co/touchlab/kmmbridge/dependencyManager/PackageFileUpdateTest.kt: -------------------------------------------------------------------------------- 1 | package co.touchlab.kmmbridge.dependencyManager 2 | 3 | import co.touchlab.kmmbridge.dependencymanager.getModifiedPackageFileText 4 | import kotlin.test.Test 5 | import kotlin.test.assertEquals 6 | 7 | class PackageFileUpdateTest { 8 | @Test 9 | fun realisticUrlUpdate() { 10 | val oldFile = """ 11 | // swift-tools-version:5.3 12 | import PackageDescription 13 | 14 | // BEGIN KMMBRIDGE VARIABLES BLOCK (do not edit) 15 | let remoteKotlinUrl = "https://www.example.com/" 16 | let remoteKotlinChecksum = "01234567890abcdef" 17 | let packageName = "TestPackage" 18 | // END KMMBRIDGE BLOCK 19 | 20 | let package = Package( 21 | name: packageName, 22 | platforms: [ 23 | .iOS(.v13) 24 | ], 25 | products: [ 26 | .library( 27 | name: packageName, 28 | targets: [packageName] 29 | ), 30 | ], 31 | targets: [ 32 | .binaryTarget( 33 | name: packageName, 34 | url: remoteKotlinUrl, 35 | checksum: remoteKotlinChecksum 36 | ) 37 | , 38 | ] 39 | ) 40 | """.trimIndent() 41 | 42 | val expectedNewFile = """ 43 | // swift-tools-version:5.3 44 | import PackageDescription 45 | 46 | // BEGIN KMMBRIDGE VARIABLES BLOCK (do not edit) 47 | let remoteKotlinUrl = "https://www.example.com/" 48 | let remoteKotlinChecksum = "fedcba9876543210" 49 | let packageName = "TestPackage" 50 | // END KMMBRIDGE BLOCK 51 | 52 | let package = Package( 53 | name: packageName, 54 | platforms: [ 55 | .iOS(.v13) 56 | ], 57 | products: [ 58 | .library( 59 | name: packageName, 60 | targets: [packageName] 61 | ), 62 | ], 63 | targets: [ 64 | .binaryTarget( 65 | name: packageName, 66 | url: remoteKotlinUrl, 67 | checksum: remoteKotlinChecksum 68 | ) 69 | , 70 | ] 71 | ) 72 | """.trimIndent() 73 | 74 | val newFile = getModifiedPackageFileText( 75 | oldFile, 76 | "TestPackage", 77 | false, 78 | "https://www.example.com/", 79 | "fedcba9876543210" 80 | ) 81 | assertEquals(expectedNewFile, newFile) 82 | } 83 | 84 | @Test 85 | fun indentedVariables() { 86 | val oldFile = """ 87 | // For some reason my variables are indented in a strange way 88 | // BEGIN KMMBRIDGE VARIABLES BLOCK (do not edit) 89 | let remoteKotlinUrl = "https://www.example.com/" 90 | let remoteKotlinChecksum = "01234567890abcdef" 91 | let packageName = "TestPackage" 92 | // END KMMBRIDGE BLOCK 93 | // Fin 94 | """.trimIndent() 95 | 96 | val expectedNewFile = """ 97 | // For some reason my variables are indented in a strange way 98 | // BEGIN KMMBRIDGE VARIABLES BLOCK (do not edit) 99 | let remoteKotlinUrl = "https://www.example.com/" 100 | let remoteKotlinChecksum = "fedcba9876543210" 101 | let packageName = "TestPackage" 102 | // END KMMBRIDGE BLOCK 103 | // Fin 104 | """.trimIndent() 105 | 106 | val newFile = getModifiedPackageFileText( 107 | oldFile, 108 | "TestPackage", 109 | false, 110 | "https://www.example.com/", 111 | "fedcba9876543210" 112 | ) 113 | assertEquals(expectedNewFile, newFile) 114 | } 115 | 116 | @Test 117 | fun withMultipleModules() { 118 | val oldFile = """ 119 | // swift-tools-version:5.3 120 | import PackageDescription 121 | 122 | // BEGIN KMMBRIDGE VARIABLES BLOCK FOR 'TestPackage' (do not edit) 123 | let remoteTestPackageUrl = "https://www.example.com/" 124 | let remoteTestPackageChecksum = "01234567890abcdef" 125 | let testPackagePackageName = "TestPackage" 126 | // END KMMBRIDGE BLOCK FOR 'TestPackage' 127 | 128 | // BEGIN KMMBRIDGE VARIABLES BLOCK FOR 'TestPackage2' (do not edit) 129 | let remoteTestPackageUrl = "https://www.example.com/" 130 | let remoteTestPackageChecksum = "01234567890abcdeg" 131 | let testPackagePackageName = "TestPackage2" 132 | // END KMMBRIDGE BLOCK FOR 'TestPackage2' 133 | 134 | let package = Package( 135 | name: packageName, 136 | platforms: [ 137 | .iOS(.v13) 138 | ], 139 | products: [ 140 | .library( 141 | name: testPackagePackageName, 142 | targets: [testPackagePackageName] 143 | ), 144 | .library( 145 | name: testPackage2PackageName, 146 | targets: [testPackage2PackageName] 147 | ), 148 | ], 149 | targets: [ 150 | .binaryTarget( 151 | name: testPackagePackageName, 152 | url: remoteTestPackageUrl, 153 | checksum: remoteTestPackageChecksum 154 | ), 155 | .binaryTarget( 156 | name: testPackage2PackageName, 157 | url: remoteTest2PackageUrl, 158 | checksum: remoteTestPackage2Checksum 159 | ) 160 | ] 161 | ) 162 | """.trimIndent() 163 | 164 | val expectedNewFile = """ 165 | // swift-tools-version:5.3 166 | import PackageDescription 167 | 168 | // BEGIN KMMBRIDGE VARIABLES BLOCK FOR 'TestPackage' (do not edit) 169 | let remoteTestPackageUrl = "https://www.example.com/" 170 | let remoteTestPackageChecksum = "fedcba9876543210" 171 | let testPackagePackageName = "TestPackage" 172 | // END KMMBRIDGE BLOCK FOR 'TestPackage' 173 | 174 | // BEGIN KMMBRIDGE VARIABLES BLOCK FOR 'TestPackage2' (do not edit) 175 | let remoteTestPackageUrl = "https://www.example.com/" 176 | let remoteTestPackageChecksum = "01234567890abcdeg" 177 | let testPackagePackageName = "TestPackage2" 178 | // END KMMBRIDGE BLOCK FOR 'TestPackage2' 179 | 180 | let package = Package( 181 | name: packageName, 182 | platforms: [ 183 | .iOS(.v13) 184 | ], 185 | products: [ 186 | .library( 187 | name: testPackagePackageName, 188 | targets: [testPackagePackageName] 189 | ), 190 | .library( 191 | name: testPackage2PackageName, 192 | targets: [testPackage2PackageName] 193 | ), 194 | ], 195 | targets: [ 196 | .binaryTarget( 197 | name: testPackagePackageName, 198 | url: remoteTestPackageUrl, 199 | checksum: remoteTestPackageChecksum 200 | ), 201 | .binaryTarget( 202 | name: testPackage2PackageName, 203 | url: remoteTest2PackageUrl, 204 | checksum: remoteTestPackage2Checksum 205 | ) 206 | ] 207 | ) 208 | """.trimIndent() 209 | 210 | val newFile = getModifiedPackageFileText( 211 | oldFile, 212 | "TestPackage", 213 | true, 214 | "https://www.example.com/", 215 | "fedcba9876543210" 216 | ) 217 | assertEquals(expectedNewFile, newFile) 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Touchlab. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 4 | * in compliance with the License. You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software distributed under the License 9 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 10 | * or implied. See the License for the specific language governing permissions and limitations under 11 | * the License. 12 | */ 13 | 14 | pluginManagement { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | gradlePluginPortal() 19 | } 20 | } 21 | plugins { 22 | id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" 23 | } 24 | 25 | dependencyResolutionManagement { 26 | @Suppress("UnstableApiUsage") 27 | repositories { 28 | mavenLocal() 29 | mavenCentral() 30 | google() 31 | } 32 | } 33 | 34 | rootProject.name = "KMMBridge" 35 | include(":kmmbridge") 36 | include(":kmmbridge-github") 37 | include(":kmmbridge-gitlab") 38 | include(":kmmbridge-test") 39 | -------------------------------------------------------------------------------- /test-projects/basic/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | 3 | *.iml 4 | .gradle 5 | /local.properties 6 | /.idea 7 | .DS_Store 8 | build 9 | .build 10 | /captures 11 | .externalNativeBuild 12 | .cxx 13 | *.xcuserstate 14 | *.xcbkptlist 15 | !/.idea/codeStyles/* 16 | !/.idea/inspectionProfiles/* 17 | .kotlin 18 | 19 | .swiftpm -------------------------------------------------------------------------------- /test-projects/basic/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2021 Touchlab 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. -------------------------------------------------------------------------------- /test-projects/basic/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.8 2 | import PackageDescription 3 | 4 | let packageName = "allshared" 5 | 6 | let package = Package( 7 | name: packageName, 8 | platforms: [ 9 | .iOS(.v14) 10 | ], 11 | products: [ 12 | .library( 13 | name: packageName, 14 | targets: [packageName] 15 | ), 16 | ], 17 | targets: [ 18 | .binaryTarget( 19 | name: packageName, 20 | path: "./allshared/build/XCFrameworks/debug/\(packageName).xcframework" 21 | ) 22 | , 23 | ] 24 | ) -------------------------------------------------------------------------------- /test-projects/basic/README.md: -------------------------------------------------------------------------------- 1 | # KMMBridge v1 SPM Template 2 | 3 | This is a template project for Kotlin Multiplatform using KMMBridge to publish Xcode Framework binaries. 4 | 5 | ## Links 6 | 7 | [KMMBridge SPM Quick Start](https://touchlab.co/kmmbridge/spmquickstart) (For this template) 8 | [KMMBridge v1 Blog Post](https://touchlab.co/kmmbridge-v1) 9 | [KMMBridge Docs](https://touchlab.co/kmmbridge/) 10 | -------------------------------------------------------------------------------- /test-projects/basic/allshared/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import co.touchlab.kmmbridge.test.TestArtifactManager 2 | 3 | plugins { 4 | alias(libs.plugins.kotlin.multiplatform) 5 | alias(libs.plugins.kmmbridge) 6 | } 7 | 8 | kotlin { 9 | listOf( 10 | iosX64(), 11 | iosArm64(), 12 | iosSimulatorArm64() 13 | ).forEach { 14 | it.binaries.framework { 15 | isStatic = true 16 | } 17 | } 18 | } 19 | 20 | kmmbridge { 21 | testUploadArtifacts() 22 | spm(swiftToolVersion = "5.8") { 23 | iOS { v("14") } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test-projects/basic/allshared/src/iosMain/kotlin/co.touchlab/kmmbridgetest/StartSDK.kt: -------------------------------------------------------------------------------- 1 | package co.touchlab.kmmbridgetest 2 | 3 | fun sayHello() = "Hello from Kotlin!" -------------------------------------------------------------------------------- /test-projects/basic/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.kotlin.multiplatform) apply false 3 | alias(libs.plugins.kmmbridge) apply false 4 | } 5 | 6 | subprojects { 7 | val GROUP: String by project 8 | val LIBRARY_VERSION: String by project 9 | group = GROUP 10 | version = LIBRARY_VERSION 11 | } 12 | 13 | tasks.register("clean") { 14 | delete(rootProject.layout.buildDirectory) 15 | } 16 | -------------------------------------------------------------------------------- /test-projects/basic/gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | android.useAndroidX=true 3 | org.gradle.jvmargs=-Xmx4g 4 | 5 | LIBRARY_VERSION=0.1.8 6 | GROUP=co.touchlab.kmmbridgespmquickstart 7 | org.gradle.configuration-cache=true -------------------------------------------------------------------------------- /test-projects/basic/gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | kotlin = "2.0.10" 3 | kmmBridge = "9.9.9" 4 | 5 | [plugins] 6 | kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 7 | kmmbridge = { id = "co.touchlab.kmmbridge.test", version.ref = "kmmBridge" } -------------------------------------------------------------------------------- /test-projects/basic/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/touchlab/KMMBridge/d146084e29515c07921c7fa369568977e9026869/test-projects/basic/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /test-projects/basic/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /test-projects/basic/gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original 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 POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 87 | 88 | # Use the maximum available, or set MAX_FD != -1 to use that value. 89 | MAX_FD=maximum 90 | 91 | warn () { 92 | echo "$*" 93 | } >&2 94 | 95 | die () { 96 | echo 97 | echo "$*" 98 | echo 99 | exit 1 100 | } >&2 101 | 102 | # OS specific support (must be 'true' or 'false'). 103 | cygwin=false 104 | msys=false 105 | darwin=false 106 | nonstop=false 107 | case "$( uname )" in #( 108 | CYGWIN* ) cygwin=true ;; #( 109 | Darwin* ) darwin=true ;; #( 110 | MSYS* | MINGW* ) msys=true ;; #( 111 | NONSTOP* ) nonstop=true ;; 112 | esac 113 | 114 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 115 | 116 | 117 | # Determine the Java command to use to start the JVM. 118 | if [ -n "$JAVA_HOME" ] ; then 119 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 120 | # IBM's JDK on AIX uses strange locations for the executables 121 | JAVACMD=$JAVA_HOME/jre/sh/java 122 | else 123 | JAVACMD=$JAVA_HOME/bin/java 124 | fi 125 | if [ ! -x "$JAVACMD" ] ; then 126 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 127 | 128 | Please set the JAVA_HOME variable in your environment to match the 129 | location of your Java installation." 130 | fi 131 | else 132 | JAVACMD=java 133 | if ! command -v java >/dev/null 2>&1 134 | then 135 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 136 | 137 | Please set the JAVA_HOME variable in your environment to match the 138 | location of your Java installation." 139 | fi 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 147 | # shellcheck disable=SC3045 148 | MAX_FD=$( ulimit -H -n ) || 149 | warn "Could not query maximum file descriptor limit" 150 | esac 151 | case $MAX_FD in #( 152 | '' | soft) :;; #( 153 | *) 154 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 155 | # shellcheck disable=SC3045 156 | ulimit -n "$MAX_FD" || 157 | warn "Could not set maximum file descriptor limit to $MAX_FD" 158 | esac 159 | fi 160 | 161 | # Collect all arguments for the java command, stacking in reverse order: 162 | # * args from the command line 163 | # * the main class name 164 | # * -classpath 165 | # * -D...appname settings 166 | # * --module-path (only if needed) 167 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 168 | 169 | # For Cygwin or MSYS, switch paths to Windows format before running java 170 | if "$cygwin" || "$msys" ; then 171 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 172 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 173 | 174 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 175 | 176 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 177 | for arg do 178 | if 179 | case $arg in #( 180 | -*) false ;; # don't mess with options #( 181 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 182 | [ -e "$t" ] ;; #( 183 | *) false ;; 184 | esac 185 | then 186 | arg=$( cygpath --path --ignore --mixed "$arg" ) 187 | fi 188 | # Roll the args list around exactly as many times as the number of 189 | # args, so each arg winds up back in the position where it started, but 190 | # possibly modified. 191 | # 192 | # NB: a `for` loop captures its iteration list before it begins, so 193 | # changing the positional parameters here affects neither the number of 194 | # iterations, nor the values presented in `arg`. 195 | shift # remove old arg 196 | set -- "$@" "$arg" # push replacement arg 197 | done 198 | fi 199 | 200 | 201 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 202 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 203 | 204 | # Collect all arguments for the java command; 205 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 206 | # shell script including quotes and variable substitutions, so put them in 207 | # double quotes to make sure that they get re-expanded; and 208 | # * put everything else in single quotes, so that it's not re-expanded. 209 | 210 | set -- \ 211 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 212 | -classpath "$CLASSPATH" \ 213 | org.gradle.wrapper.GradleWrapperMain \ 214 | "$@" 215 | 216 | # Stop when "xargs" is not available. 217 | if ! command -v xargs >/dev/null 2>&1 218 | then 219 | die "xargs is not available" 220 | fi 221 | 222 | # Use "xargs" to parse quoted args. 223 | # 224 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 225 | # 226 | # In Bash we could simply go: 227 | # 228 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 229 | # set -- "${ARGS[@]}" "$@" 230 | # 231 | # but POSIX shell has neither arrays nor command substitution, so instead we 232 | # post-process each arg (as a line of input to sed) to backslash-escape any 233 | # character that might be a shell metacharacter, then use eval to reverse 234 | # that process (while maintaining the separation between arguments), and wrap 235 | # the whole thing up as a single "set" statement. 236 | # 237 | # This will of course break if any of these variables contains a newline or 238 | # an unmatched quote. 239 | # 240 | 241 | eval "set -- $( 242 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 243 | xargs -n1 | 244 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 245 | tr '\n' ' ' 246 | )" '"$@"' 247 | 248 | exec "$JAVACMD" "$@" 249 | -------------------------------------------------------------------------------- /test-projects/basic/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 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /test-projects/basic/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | // REPLACE_WITH_INCLUDE_BUILD 3 | repositories { 4 | mavenLocal() 5 | google() 6 | gradlePluginPortal() 7 | mavenCentral() 8 | } 9 | } 10 | 11 | dependencyResolutionManagement { 12 | @Suppress("UnstableApiUsage") 13 | repositories { 14 | mavenLocal() 15 | google() 16 | mavenCentral() 17 | } 18 | } 19 | 20 | include("allshared") 21 | --------------------------------------------------------------------------------