· 13 min read Posted by Kevin Galligan

KMMBridge Without GitHub

KMMBridge is designed to work in any environment, but the out-of-the-box experience for GitHub is much easier than others. This post will walk through the decisions and setup necessary to use KMMBridge with environments other than GitHub.

Overview

KMMBridge is ultimately a component that needs to exist in a larger build ecosystem. Production build environments tend to be fairly complex and custom, so attempting to provide specific instructions for a range of environments would be a lot of work, and generally fall short of the mark.

Many teams trying KMP want to do a quick POC, but that involves a lot of up front work. We added a “batteries-included” flow for GitHub, which will allow you to create and publish shared Kotlin code to your native mobile teams. Using that flow requires that you’re on GitHub.

It may seem like using KMMBridge in any other environment is significantly more difficult, but that is because we are able to pull a lot of info from GitHub while running, and have made opinionated defaults. You will need to supply that info and make those decisions yourself if you are using KMMBridge in a different environment.

This post will not explain how to do that in your environment. Build environments and needs vary considerably. This post will be a rough map down the road of what you will need to configure, and why.

Touchlab is a services business focused on KMP. We work with teams who want to accelerate their KMP success. Development environment setup and configuration is a major part of that success. Please reach out if you’d like to get started on the right foot.
Javascript disabled?
Our site requires Javascript for some sections. Please check that Javascript is enabled.

Basic Parts

There are a few minimum steps required to publish binaries with KMMBridge. Beyond those, you’ll need to make some decisions about your development workflow, versioning, and dependency manager integrations (SPM and CocoaPods). We’ll mostly follow what we did with the KMMBridge Quick Start, but explain the thought process behind those decisions and point out where you can divert from those in your own environment.

What does KMMBridge Do?

For a more detailed walkthrough of what KMMBridge actually does, and why, see What Are We Doing? from the KMMBridge docs.

KMMBridge itself only cares about Xcode Frameworks built from Kotlin. How to package them, publish them, and have users consume them. KMMBridge is not involved in your Android (or other) targets, does not impose requirements on your dev workflow, and beyond a few uses of version, does not care about how you version your builds.

When you apply KMMBridge to a Kotlin module, it does the following:

  • Find all Kotlin/Native targets with a Framework output.
  • Converts those Framework instances to XCFramework.
  • Creates a zip with all of those XCFramework instances.
  • Pushes that zip to the storage location of your choice.
  • For SPM, it will write a Package.swift file in the repo root, with info about that zip.
  • For CocoaPods, it will create a podspec, and attempt to publish it to the git Podspec repo of your choice.

That is (essentially) it. Everything else that our KMMBridge Quick Start does is an opinionated decision on our part about how you would develop shared Kotlin code in a small environment.

Framework Configuration

KMMBridge should be applied on the Kotlin module that produces an Xcode Framework. KMMBridge will automatically detect that configuration, and wrap it with XCFramework from the Kotlin Gradle plugin (reference).

KMMBridge will then add its tasks and create dependencies on these XCFramework assemblies. By default, KMMBridge will build RELEASE versions, but you can override this.

Be aware that if you are using the Kotlin CocoaPods integration, and also explicitly setting up your framework config, you can run into issues. Also, if you are manually setting up XCFramework config in your build file, KMMBridge will have issues. If you would like to have the ability to let KMMBridge bypass that part of the config, please reach out

Generally speaking, you don’t need to decide much for Framework config. Just be aware of what KMMBridge is doing.

Artifact Publishing

Your XCFramework zip file needs to live somewhere. “Where” is not critical, as long as you can push to it, and Xcode (& friends) can access it.

Pushing is more flexible. KMMBridge only comes with 2 implementations of ArtifactManager out of the box. That means if you have a custom location, it probably won’t be compatible with the stock implementations. However, writing your own is simple.

  • Maven: KMMBridge ships with an ArtifactManager that can publish to a Maven repo. We use this for GitHub Packages, but you can use it for other Maven repos.

  • AWS S3: KMMBridge also ships with an implementation for AWS S3. See docs. You cannot pull private S3 files with basic auth, so using S3 for private binaries is not simple. However, you can mark your files public, and use S3 to distribute public libraries.

Pulling binaries into Xcode is less simple, mostly because there are not many options. If the URL is public, well, no problem. That’ll work, as long as it’s https and not some less-common protocol (s3: comes to mind). For private urls, if you can do basic auth, you should be good. If you need something more exotic, that is beyond the scope of this post. Generally speaking, when this is the case, you’ll need to configure your dev machine to be able to access these private files directly. Xcode’s config only handles basic auth. You’ll generally need some kind of VPN config. Many larger orgs do have private network access to things like S3 or shared drives. That config is outside the scope of KMMBridge or Xcode config.

Dependency Manager(s)

While you can directly drag Xcode Frameworks into a project, pretty much everybody uses a dependency manager for obvious reasons. There are a few, but the only two that really matter are Swift Package Manager (SPM) and CocoaPods (sorry Carthage :().

If you have an internal team, it is likely that you’ll only need one of these. If this was 2020/2021, I’d say probably CocoaPods. In 2023, probably SPM.

KMMBridge directly supports SPM and CocoaPods. You can configure KMMBridge to publish to either SPM, CocoaPods, or both. Generally speaking, for internal teams, you’ll be using one or the other, but KMMBridge can publish to both (and, while a little complicated, you can use both in a single Xcode project).

If you needed a different one, in theory you could write a new one to the given interface, but since SPM and CocoaPods are wildly different, and yet, the only two implementations that we’ve thought through, a different dependency manager would likely need to stretch the interface in different ways. I would be curious to hear about how that went, but we’d probably be reluctant to change KMMBridge to accommodate anything that required significant changes and/or is very obscure. Please reach out before sending PRs!

SPM

Most teams now seem to be moving to SPM. To setup and configure a publishing flow with SPM, you should really understand the basics of how SPM works. From our experience, SPM functions well and is pretty smooth once set up, but that is because SPM is rather strict on how you set it up.

You can read the overview in the KMMBridge Docs.

In summary:

  • An SPM library lives in a git repo.
  • The Package.swift file for that library lives at the root of that repo.
  • Versioning of the library is strict semantic versioning, ex 1.2.3.
  • Versions are marked by git tags.

KMMBridge writes the Package.swift file pointing at your remote artifact, and our GitHub flow deals with git version tags. When writing your own CI, be sure to pay attention to the git tag details, as specifying versions and committing tags is done by the CI process.

Just FYI. The Package.swift file really needs to live in the repo root. We have tried a number of different configurations, but ultimately ended up fighting SPM. It is best to do things the way SPM expects. KMMBridge assumes a rather simple SPM configuration. If you want/need something more custom, we would like to hear about the use cases. The best place for this is probably Discussions in the KMMBridge repo.

CocoaPods

CocoaPods uses a separate git repo to hold package config info, in the form of podspec files. CocoaPods has a CLI tool to publish these, which we use. For that to work, you need to have a separate repo, and access to that repo needs to be configured correctly. CocoaPods publishing has a number of points at which it can break as a result. Be prepared to dig into this for a while.

The rules around versioning are a little less strict, but CocoaPods is a little less strict about everything in general.

Be sure to refer to the KMMBridge CocoaPods Docs for more info.

Publishing Workflow

You will need to make some decisions about publishing workflow. In fact, whether you’re using GitHub or another CI, if your intended use case differs from our GitHub Flow, you will need to make some customizations.

A lot of the extra “stuff” in our GitHub Flow is focused around incremental version numbering. This allows a dev build process that can have frequent publications under a major/minor semver version. If you are handling versions in a different way, you’ll need to customize the flow.

GitHub Flow, Step By Step

Our main GitHub Actions workflow is here.

You would call that workflow from another repo (see example, passing in arguments for various features.

Let’s walk through the steps in the flow to discuss what it does.

Checkout the repo with tags

- name: Checkout the repo with tags
  uses: actions/checkout@v4
  with:
  fetch-depth: 0
  fetch-tags: true

Nothing complex here. The only really important part to point out is you’ll want to make sure you have tags for versioning, assuming you’re using SPM.

Version Base Property

- uses: touchlab/read-property@0.1
  id: versionBasePropertyValue
  with:
    file: ./gradle.properties
    property: ${{ inputs.versionBaseProperty }}

- name: Print versionBasePropertyValue
  id: output
  run: echo "${{ steps.versionBasePropertyValue.outputs.propVal }}"

This is a property defined in gradle.properties that is specific to our flow. It is the “base version”. For development, we supply the major/minor part of a semver version, and automatically increment the patch version on each publication.

So, in our sample template, we have the following:

LIBRARY_VERSION=0.1

This will likely be custom to your workflow. Grabbing and incrementing the version is tied to GitHub actions. As far as KMMBridge is concerned, it just needs a regular Gradle version defined.

Touchlab Sample Sanity Check

- name: Touchlab Sample Sanity Check (Ignore this for your CI)
  uses: touchlab/sample-group-sanity-check@0.1

You can just ignore this. We added this to make sure somebody using our template didn’t read instructions and attempted to publish with the same group value. GitHub Packages can get weird with namespaces. Your CI should have no use for this step.

AutoVersion-NextVersion

- uses: touchlab/autoversion-nextversion@main
  id: autoversion
  with:
      versionBase: ${{ steps.versionBasePropertyValue.outputs.propVal }}

- name: Print Next Version
  id: outputversion
  run: echo "${{ steps.autoversion.outputs.nextVersion }}"

Autoversion-nextversion is a GitHub Action, written in Typescript. The logic is relatively simple. It runs through the git tags to find the highest version for the provided base version (major/minor semver).

For example, if your base version is 2.1, then autoversion-nextversion will scan your tags for that base version. If the matching tags were as follows:

2.1.0
2.1.2
2.1.3
2.1.4

autoversion-nextversion would calculate the next version as 2.1.5.

There is one special note to point out. Because publishing can fail, and by that point an artifact may have been pushed already, a build will mark a version with a temp prefix. The prefix is autoversion-tmp-publishing-. So, if the list of versions is actually the following:

2.1.0
2.1.2
2.1.3
2.1.4
autoversion-tmp-publishing-2.1.5

The next calculated version would be 2.1.6.

In our GitHub flow, publishing to CocoaPods is far more likely to fail than SPM. If you are only publishing to SPM, you can probably skip this temporary tag complication altogether. If you are publishing CocoaPods, you may be fine without it, but at least with out publishing, if a publication failed, it would be blocked from that point on until the version was manually bumped.

AutoVersion-TagMarker

- uses: touchlab/autoversion-tagmarker@main
  id: autoversion-tagmarker
  with:
    nextVersion: ${{ steps.autoversion.outputs.nextVersion }}

This is another GitHub Action that is part of the AutoVersion family. This action actually runs twice: once at the start, and once when the publication is successful, to clean up. When a publication is starting, this action will add the marker tag mentioned above.

AutoVersion-BuildBranch

- uses: touchlab/autoversion-buildbranch@main
  id: autoversion-buildbranch
  with:
      buildBranch: "build-${{ steps.autoversion.outputs.nextVersion }}"

This is yet another GitHub Action. This one is very simple. It just creates a branch for the build. The available actions didn’t quite do what we wanted, so we created this one.

Apply SSH Key

- name: Apply SSH Key
  if: ${{ env.PODSPEC_SSH_KEY_EXISTS == 'true' }}
  uses: webfactory/ssh-agent@v0.5.4
  with:
      ssh-private-key: ${{ secrets.PODSPEC_SSH_KEY }}

This is only necessary for CocoaPods publication, assuming you’re using SSH to handle auth for publishing. See Publishing Podspecs to GitHub for details.

Apply netrc

- uses: extractions/netrc@v1
  with:
    machine: ${{ inputs.netrcMachine }}
    username: ${{ secrets.netrcUsername != '' && secrets.netrcUsername || 'cirunner' }}
    password: ${{ secrets.netrcPassword != '' && secrets.netrcPassword || secrets.GITHUB_TOKEN  }}

This is for CocoaPods publishing and private URLs. CocoaPods publication involves actually building a test app with the publishing framework, to confirm that everything is set up correctly. To access binaries from private repos, or even public GitHub repos, you need to set up authentication with netrc. This step pulls values from GitHub in the default GitHub workflow, but you can specify values if needed.

JVM/KMP Build Setup

- uses: actions/setup-java@v2
  with:
      distribution: "adopt"
      java-version: ${{ inputs.jvmVersion }}

- name: Validate Gradle Wrapper
  uses: gradle/wrapper-validation-action@v1

- name: Cache build tooling
  uses: actions/cache@v3
  with:
      path: |
          ~/.gradle/caches
          ~/.konan
      key: ${{ runner.os }}-v4-${{ hashFiles('*.gradle.kts') }}      

These tasks simply prepare the build setup for KMP.

Build and Publish

- name: Build Main
  run: ./gradlew ${{ env.MODULE }}${{ inputs.publishTask }} -PAUTO_VERSION=${{ steps.autoversion.outputs.nextVersion }} -PENABLE_PUBLISHING=true -PGITHUB_PUBLISH_TOKEN=${{ secrets.GITHUB_TOKEN }} -PGITHUB_REPO=${{ github.repository }} ${{ secrets.gradle_params }} --no-daemon --stacktrace
  env:
    GRADLE_OPTS: -Dkotlin.incremental=false -Dorg.gradle.jvmargs="-Xmx3g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:MaxMetaspaceSize=512m"

This task publishes the KMP library/libraries. For our GitHub Workflow, you can pass in an alternative task, or tasks, but the default task is kmmBridgePublish. That is a KMMBridge-specific task that publishing the Apple Xcode Framework. You will need some task to actually publish your binaries (obviously).

Our template project, KMMBridge With SKIE Template, supports two basic flows.

  1. Publish Xcode Frameworks only
  2. Publish Android aars and Xcode Frameworks

Read the template docs to see how that works.

Finish Release

- uses: touchlab/autoversion-finishrelease@main
  id: autoversion-finishrelease
  with:
    commitMessage: "KMM SPM package release for ${{ steps.autoversion.outputs.nextVersion }}"
    tagMessage: "KMM release version ${{ steps.autoversion.outputs.nextVersion }}"
    tagVersion: ${{ steps.autoversion.outputs.nextVersion }}
    branchName: "build-${{ steps.autoversion.outputs.nextVersion }}"

This GitHub Action applies the actual release tag to your build. A tag is technically only needed for SPM, but obviously tagging the builds has some value regardless.

AutoVersion-TagMarker (Cleanup)

- uses: touchlab/autoversion-tagmarker@main
  id: autoversion-tagmarker-cleanup
  with:
    nextVersion: ${{ steps.autoversion.outputs.nextVersion }}
    cleanupMarkers: 'true'

This is the same action as AutoVersion-TagMarker above. However, cleanupMarkers: 'true' tells it to delete all tags with the marker prefix (autoversion-tmp-publishing-). Presumably, if we got here, the publication was successful, so we can clean up the markers.

Delete branch

- name: Delete branch
  if: (!inputs.retainBuildBranch) && (!cancelled())
  uses: touchlab/action-delete-branch@v3
  with:
    github_token: ${{ secrets.GITHUB_TOKEN }}
    branches: "build-${{ steps.autoversion.outputs.nextVersion }}"

We create a build branch when publishing. There are multiple reasons for this.

First, when building SPM, KMMBridge will modify the Package.swift file to point at our newly published binary. This may conflict with a local-dev config, so we don’t want that in the main branch.

Second, adding commits to the main branch will clutter up the history.

You can leave the build branch, but by default, our GitHub workflow deletes it. When you view the tag in GitHub, you will get a warning that the tag points at a commit that is in no branch, but the commit still exists, and if needed, you can create a new branch from it. The headless commit does not impact SPM’s versioning capability.

Summary

Our GitHub Workflow and template project together form an opinionated approach to building and publishing shared Kotlin libraries. Adapting this to your CI and workflow will require performing similar steps. While the workflow does seem to have a lot of steps, none of them are really all that complex.

If you would like to invest in your team’s KMP success, please reach out!