:loudspeaker: Debug Kotlin in Xcode from SPM build...
# ios
k
📢 Debug Kotlin in Xcode from SPM builds! 📢 Previously, you'd have to build Kotlin locally to support debugging. Now you can just use SPM. Read more here: https://bsky.app/profile/kpgalligan.bsky.social/post/3ld7iy2xr4s2o (direct link: https://touchlab.co/spm-kotlin-debugging) Slack Conversation
m
Great news. I just tried using it and I was expecting Kotlin code to be bundled in XCFramework itself so it can be mapped during debugging process. However, I still had to copy/paste Kotlin code into SPM module to be able to use breakpoints. Did I miss something?
k
SPM should grab the source from the repo which defines the SPM dependency. The Kotlin code should be in the folder there. What does your git repo and publication setup look like?
m
Copy code
skie {
    analytics {
        enabled.set(false)
    }
    features {
        group {
            FunctionInterop.FileScopeConversion.Enabled(false)
            DefaultArgumentInterop.Enabled(false)
        }
    }
    build {
        produceDistributableFramework()
        enableRelativeSourcePathsInDebugSymbols.set(true)
    }
}

... 

kotlin {
    XCFramework(iosLibraryName).apply {
        listOf(
            iosX64(),
            iosArm64(),
            iosSimulatorArm64()
        ).forEach { target ->
            target.binaries.framework {
                sharedData.forEach(::export)
                baseName = iosLibraryName
                binaryOption("bundleId", iosLibraryName)
                isStatic = true
                add(this)
            }
        }
    }

    sourceSets {
        commonMain {
            ...
        }
    }
}
It is basically Android project with
shared
module. I build (debug) XCFramework using this command:
:shared:assembleMultiplatformLibDebugXCFramework
which is then published to S3 bucket. I saw in articles that you suggest using KMMBridge. Perhaps XCFramework export is done differently through it. 🤔
k
I saw in articles that you suggest using KMMBridge
It's easier for most to set up publishing with KMMBridge, generally speaking, but it's not doing anything specific that would impact debugging (IIRC). Where do you host the
Package.swift
file? Is it the same Android repo, or do you "publish" SPM to another repo?
To clarify, the xcode-kotlin plugin and SKIE aren't moving Kotlin source files anywhere or embedding them. The Kotlin source is pulled by SPM/Xcode from the repo used as an SPM dependency. If you push the
Package.swift
file to a different repo without the rest of the source code, SPM/Xcode won't have it when the repo is cloned.
m
Package.swift is another repo, not in same place as KMP source. That explains is then. Thank you very much! 🙂
k
When you push
Package.swift
to the "other" repo, you can just push the code as well. That'll let you debug. Pushing code around seems a little ugly, but doing it that way makes sure the code you're looking at is the correct version.
This is a work in progress, but made it public early. It's a little ugly, but...
Debug build GitHub Actions workflow: https://github.com/touchlab/KMMBridgeAndroidDevFlow/blob/main/.github/workflows/KMMBridge-Debug.yml. Publishes to an external repo, copies code, and pushes to the same branch name that the build is run on with:
Copy code
- uses: touchlab/ga-push-remote-swift-package@v1.1
        id: push-remote-swift-package
        with:
          commitMessage: "KMP SPM package debug build for ${{ github.ref }}"
          remoteRepo: touchlab/KMMBridgeAndroidDevFlowPublish
          remoteBranch: ${{ github.ref }}
The dev/debug flow doesn't use tag versions. The idea is you can publish a feature change from Android to a branch, and point SPM to a branch rather than a version. "Why?" is a bit longer of an explanation.
👏 1
Still to be completed is remote publishing with version tag and source. But, you can see the basic concepts here.
m
When you push
Package.swift
to the "other" repo, you can just push the code as well. That'll let you debug. Pushing code around seems a little ugly, but doing it that way makes sure the code you're looking at is the correct version.
yeah I will go with this, but the thing that makes it a bit uglier in our case is the fact that repos are private and XCFramework itself is hosted on S3, rather than asset in SPM repository. 😄 The solution is logical and straightforward. Obstacles are quite specific to my use case unfortunately. Thanks for the effort. I appreciate it.
k
Is this for internal dev or are you publishing something for outside users? S3 doesn't have basic auth, so using S3 requires public URLs, unless you add some kind of auth server in front of it. KMMBridge supports pushing to GitHub release assets, which can host the XCFramework zip file, and automatically uses GitHub auth as needed (private, yes, public, no). S3 and similar services from Google Cloud and Azure are all kind of a pain because of the asset auth.
m
This is internal for developers. We share data layers between platforms. For S3 we use asset auth so it makes it more difficult. I think I could just clone the KMP repo in SPM repo, take source code from it and commit. Overall, good opportunity to revisit it all together.
k
For S3 we use asset auth
Any details on what that is and how it works? We haven't done much with S3 because Xcode/SPM need to be able to access resources using basic auth, which S3 doesn't (or didn't at some point) support.
I think I could just clone the KMP repo in SPM repo, take source code from it and commit.
That's effectively what this is doing: https://github.com/touchlab/ga-push-remote-swift-package/blob/ad9b8b94b7e5790be230d7a569da871aac9700ae/src/main.ts#L48 It looks a little complicated, but it's standard git. From the source repo: • Fetch branch from target repo, or fetch default branch if the specific branch doesn't exist • Create a local branch from the fetch, and create a worktree for that branch • Copy everything from the source into the target (skipping
.git
and
.github
, which would be bad) • Push the branch or tag, depending on what you're trying to do
m
Sure, here is overview of how we do it. We have Android project with
shared
module. When we want to publish XCFramework to iOS we use github action from Android repository. In the action, you can choose if you want to release stable or dev version. Stable version will just have semantic versioning number as filename, while dev version will contain lib name + part of checksum (since XCode won't see new file as different if they are named the same). Action runs
assembleMultiplatformLib[Debug|Release]XCFramework
, we configure AWS credentials using predefined Github Action:
Copy code
- name: Configure AWS credentials
  uses: aws-actions/configure-aws-credentials@v2
  with:
    aws-access-key-id: ${{ secrets.YOUR_ID }}
    aws-secret-access-key: ${{ secrets.YOUR_KEY }}
    aws-region: your-region
After that actions finds the XCFramework that needs to be uploaded and syncs it to S3 to its appropriate bucket. Bucket in our case if version of the XCFramework artifact:
Copy code
aws s3 sync "$xcframework_dir" "$s3_bucket/$version/"
The key part after this is notifying iOS SPM repository (which contains path and checksum to XCFramework) via curl that state of library changed. For instance:
Copy code
curl -X POST \
  -H "Authorization: Bearer ${{ secrets.YOUR_TOKEN }}" \
  -H "Accept: application/vnd.github.everest-preview+json" \
  -H "Content-Type: application/json" \
  <https://api.github.com/repos/$SWIFT_PACKAGE_REPO_OWNER/$SWIFT_PACKAGE_REPO/dispatches> \
  --data "{\"event_type\": \"$event_type\", \"client_payload\": {\"release_version\": \"$version\", \"checksum\": \"$checksum\", \"checksum_short\": \"$checksum_short\", \"branch\": \"$GITHUB_REF_NAME\"}}"
This JSON, containing version of library and checksum is then parsed in iOS SPM repository which then modifies Package.swift with new path and checksum to the XCFramework and commits it, creates tag and release. And that is it. iOS devs can choose between main branch which contains stable versions or pick branch on SPM which is specific for some published dev version. Package.swift looks like this in the end:
Copy code
import PackageDescription

let frameworkName = "MultiplatformLib"
let packageUrl = "<https://our-resource-server/app-assets/multiplatform/5.20.0/MultiplatformLib.xcframework.zip>"
let fileChecksum = "generated-checksum-from-json"

let package = Package(
    name: frameworkName,
    products: [
        .library(
            name: frameworkName,
            targets: [frameworkName]),
    ],
    targets: [
        .binaryTarget(
            name: frameworkName,
            url: packageUrl,
            checksum: fileChecksum
        )
    ]
)
So yeah, in order for debugging to work on published SPM XCFramework, I will just clone the source repo, take
shared
and commit it so SPM repository. Basically, as you said, similar to what you suggested. Will let you know how that worked out!
Update: it works. I included
shared
directory in published SPM and debugging now works. Awesome news!
k
How do you access the S3 file from Xcode? I assume
<https://our-resource-server/>
is some kind of S3 front end?
The curl post to GitHub actions starts the workflow and the release data is passed in from that? Interesting.
m
SPM pulls it from S3 via the S3 frontend, correct. Yeah, because repository is hosted in another organization so I don't have sufficient permissions. I just trigger it like this. A bit complicated, but that is the setup
All this could be simplified greatly if it was same project. Or at least same repo.