From fcda0660a3a437c97534916f41a1a1a3caa1b384 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Tue, 1 Sep 2026 16:22:33 -0400 Subject: [PATCH] build(shared-core): resolve a local XCFramework when FLIPCASH_SHARED_CORE_LOCAL is set MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Android compiles :kmp:shared-core from source, so a Kotlin change shows up in the next Android build. iOS consumes it as a published XCFramework pinned by version, so the same change needed the publish workflow, a moved tag, and a bumped pin before it could be tried at all. That cost is about to be paid per iteration: the derivation and bonding-curve moves are far more iterative than the beachhead was. The switch goes in the package manifest rather than in the app, because the app reaches SharedCoreKit twice — FlipcashUI depends on it, and Code.xcodeproj carries its own remote package reference for the app target's direct import. Overriding one leaves the other on the published package and resolution fails on duplicate SharedCore/SharedCoreKit targets. The publish job copies this manifest verbatim into the SPM repo, so the tagged copy the app resolves carries the same conditional and both consumers follow it. The path is rewritten relative to the package root because SwiftPM rejects an absolute binary-target path, and the published manifest is checked out into DerivedData with no Kotlin next to it. Loop and verification: docs/shared-core-local-development.md in the orchestrator. --- kmp/shared-core/spm/.gitignore | 3 ++ kmp/shared-core/spm/Package.swift | 47 +++++++++++++++++++++++++++---- 2 files changed, 45 insertions(+), 5 deletions(-) create mode 100644 kmp/shared-core/spm/.gitignore diff --git a/kmp/shared-core/spm/.gitignore b/kmp/shared-core/spm/.gitignore new file mode 100644 index 000000000..b53258420 --- /dev/null +++ b/kmp/shared-core/spm/.gitignore @@ -0,0 +1,3 @@ +# Left behind by `swift package` / `xcodebuild` runs against this package. +.build/ +.swiftpm/ diff --git a/kmp/shared-core/spm/Package.swift b/kmp/shared-core/spm/Package.swift index bb4746055..7ab786038 100644 --- a/kmp/shared-core/spm/Package.swift +++ b/kmp/shared-core/spm/Package.swift @@ -1,5 +1,6 @@ // swift-tools-version:5.9 import PackageDescription +import Foundation // The publish job rewrites this block — everything else in this file is ours. Note the // tags are load-bearing: KMMBridge looks for them verbatim and fails the publish if @@ -10,6 +11,46 @@ let remoteKotlinChecksum = "b86241d770fa186e44eec4c9ff0ff092d54cf66329828485d243 let packageName = "SharedCore" // END KMMBRIDGE BLOCK +// FLIPCASH_SHARED_CORE_LOCAL points at a code-android-app checkout and swaps the published +// XCFramework for one assembled from that checkout's Kotlin, so a Kotlin change can be tried +// from an iOS build without cutting a release and moving a tag: +// +// ./gradlew :kmp:shared-core:assembleSharedCoreReleaseXCFramework +// +// This manifest is what gets published, so the override also works from the tagged copy the +// iOS app resolves — nothing on the iOS side has to change. That copy sits in DerivedData, and +// SwiftPM only accepts a binary target path relative to the package root, so the absolute path +// the variable gives us is rewritten as a relative one from wherever the manifest was checked +// out to. +// +// The publish workflow never sets the variable, so CI always resolves the binary target +// through the URL and checksum above. +func relativePath(from base: String, to target: String) -> String { + let from = (base as NSString).resolvingSymlinksInPath.split(separator: "/").map(String.init) + let to = (target as NSString).resolvingSymlinksInPath.split(separator: "/").map(String.init) + var shared = 0 + while shared < from.count, shared < to.count, from[shared] == to[shared] { shared += 1 } + return (Array(repeating: "..", count: from.count - shared) + to[shared...]).joined(separator: "/") +} + +let sharedCoreLocalRoot = ProcessInfo.processInfo.environment["FLIPCASH_SHARED_CORE_LOCAL"] + .map { ($0 as NSString).expandingTildeInPath } + .flatMap { $0.isEmpty ? nil : $0 } + +let binaryTarget: Target = sharedCoreLocalRoot.map { + .binaryTarget( + name: packageName, + path: relativePath( + from: Context.packageDirectory, + to: "\($0)/kmp/shared-core/build/XCFrameworks/release/\(packageName).xcframework" + ) + ) +} ?? .binaryTarget( + name: packageName, + url: remoteKotlinUrl, + checksum: remoteKotlinChecksum +) + let package = Package( name: packageName, platforms: [ @@ -25,11 +66,7 @@ let package = Package( ), ], targets: [ - .binaryTarget( - name: packageName, - url: remoteKotlinUrl, - checksum: remoteKotlinChecksum - ), + binaryTarget, .target( name: "SharedCoreKit", dependencies: [.target(name: packageName)]