Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions .github/workflows/publish-shared-core.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -67,6 +67,16 @@ jobs:
echo 'COINBASE_ONRAMP_API_KEY=00000000-0000-0000-0000-000000000000'
} > ./local.properties

# The Swift half of the package lives here, next to the Kotlin it wraps, so the
# two move in one commit; the Swift Package repo is a publish target, not a place
# to edit. `Package.swift` has to be in place before Gradle runs, since KMMBridge
# only rewrites the variables block inside it.
- name: Stage the Swift package sources
run: |
set -euo pipefail
rm -rf spm-repo/Sources spm-repo/Tests
cp -R kmp/shared-core/spm/Package.swift kmp/shared-core/spm/Sources kmp/shared-core/spm/Tests spm-repo/

- name: Build the XCFramework, upload it, and update Package.swift
env:
# Gradle reads ORG_GRADLE_PROJECT_-prefixed vars as project properties,
Expand All@@ -78,16 +88,31 @@ jobs:
-PsharedCoreVersion=${{ inputs.version }} \
-PspmRepoDir=$GITHUB_WORKSPACE/spm-repo

# The release asset is up by now but the tag still points at the old
# Package.swift, so nothing consumes this build until the next step. Compiling
# the Swift glue against the framework we just uploaded is the last moment a
# mismatch between the two is cheap to fix — after the tag moves, it's a
# published-and-broken version.
- name: Verify the package builds against the uploaded framework
working-directory: spm-repo
run: |
set -euo pipefail
xcodebuild -scheme SharedCore \
-destination 'generic/platform=iOS Simulator' \
-clonedSourcePackagesDirPath "$RUNNER_TEMP/spm-verify" \
-quiet \
build

- name: Commit and tag the Swift Package
working-directory: spm-repo
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

git add Package.swift
git add -A Package.swift Sources Tests
if git diff --cached --quiet; then
echo "Package.swift is unchanged — the upload produced the same URL and checksum."
echo "Nothing to commit — the upload produced the same URL and checksum, and the Swift sources are unchanged."
exit 1
fi
git commit -m "SharedCore ${{ inputs.version }}"
Expand Down
14 changes: 9 additions & 5 deletions kmp/shared-core/build.gradle.kts
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,10 +11,10 @@ group = "com.flipcash"
// release version in; the fallback only matters for local builds.
version = findProperty("sharedCoreVersion") as String? ?: "0.1.0"

// Where `Package.swift` is written. CI points this at a checkout of
// `code-payments/flipcash-shared-core-spm`; locally it lands under the root
// build directory so `spmDevBuild` has somewhere to write without dirtying the
// repo. Left unset, KMMBridge would write it to this repo's root.
// Where the published `Package.swift` lives. CI points this at a checkout of
// `code-payments/flipcash-shared-core-spm` that already holds a copy of
// `spm/`; locally it falls back to the root build directory so a publish run
// can't dirty the repo. Left unset, KMMBridge would write it to this repo's root.
val spmPackageDir = findProperty("spmRepoDir") as String?
?: rootProject.layout.buildDirectory.dir("spm").get().asFile.path

Expand DownExpand Up@@ -59,7 +59,11 @@ kmmbridge {
// `Package.swift` that references it is committed there. iOS then depends on
// a small public repo instead of the whole Android app.
gitHubReleaseArtifacts(repository = "code-payments/flipcash-shared-core-spm")
spm(spmDirectory = spmPackageDir, swiftToolVersion = "5.9") {
// `useCustomPackageFile` keeps `spm/Package.swift` — which adds the `SharedCoreKit`
// Swift target over the framework — and rewrites only the variables block inside it.
// The platform and tools version below are what KMMBridge would generate on its own;
// with a custom file it's `spm/Package.swift` that decides, so keep the two in step.
spm(spmDirectory = spmPackageDir, useCustomPackageFile = true, swiftToolVersion = "5.9") {
iOS { v("15") }
}
}
42 changes: 42 additions & 0 deletions kmp/shared-core/spm/Package.swift
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
// swift-tools-version:5.9
import PackageDescription

// 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
// they've drifted.
// BEGIN KMMBRIDGE VARIABLES BLOCK (do not edit)
let remoteKotlinUrl = "https://api.github.com/repos/code-payments/flipcash-shared-core-spm/releases/assets/524049263.zip"
let remoteKotlinChecksum = "b86241d770fa186e44eec4c9ff0ff092d54cf66329828485d243cb7b6cf588b5"
let packageName = "SharedCore"
// END KMMBRIDGE BLOCK

let package = Package(
name: packageName,
platforms: [
.iOS(.v15)
],
products: [
// The only product on purpose. Callers get Swift types; the Kotlin framework's
// own surface — `KotlinByteArray`, `.shared` singletons, no default arguments —
// stays behind this target.
.library(
name: "SharedCoreKit",
targets: ["SharedCoreKit"]
),
],
targets: [
.binaryTarget(
name: packageName,
url: remoteKotlinUrl,
checksum: remoteKotlinChecksum
),
.target(
name: "SharedCoreKit",
dependencies: [.target(name: packageName)]
),
.testTarget(
name: "SharedCoreKitTests",
dependencies: ["SharedCoreKit"]
),
]
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
import Foundation
import SharedCore

extension Data {

/// Kotlin's `ByteArray` has no `Data` bridge of its own, so every exported function taking
/// bytes needs this copy.
var kotlinByteArray: KotlinByteArray {
let array = KotlinByteArray(size: Int32(count))
for (offset, byte) in enumerated() {
array.set(index: Int32(offset), value: Int8(bitPattern: byte))
}
return array
}
}
32 changes: 32 additions & 0 deletions kmp/shared-core/spm/Sources/SharedCoreKit/KikCode.swift
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
import Foundation
import SharedCore

/// Renders scannable codes from the shared Kotlin implementation.
public enum KikCode {

/// The export size used when none is given; SVG scales losslessly, so this only sets the
/// numbers in the document.
public static var defaultDimension: Double { KikCodeSvg.shared.DEFAULT_DIMENSION }

/// Renders `payload` as a standalone SVG document, byte-for-byte identical to Android's.
///
/// - Parameters:
/// - background: the surface color the code sits on, or `nil` for a transparent document.
/// Codes are light-on-dark, so a transparent export is invisible on light surfaces.
/// - includeBadge: whether to embed the logo in the middle well.
public static func svg(
payload: Data,
dimension: Double = KikCode.defaultDimension,
foreground: String = "#FFFFFF",
background: String? = nil,
includeBadge: Bool = true
) -> String {
KikCodeSvg.shared.render(
payload: payload.kotlinByteArray,
dimension: dimension,
foreground: foreground,
background: background,
includeBadge: includeBadge
)
}
}
10 changes: 10 additions & 0 deletions kmp/shared-core/spm/Sources/SharedCoreKit/SharedCoreInfo.swift
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
import SharedCore

/// Identifies the Kotlin framework this package was built from.
public enum SharedCoreInfo {

/// The `:kmp:shared-core` version the linked XCFramework was published at.
// Unqualified on purpose: inside this module the name `SharedCore` resolves to the
// Kotlin object, not the framework it lives in.
public static var version: String { SharedCore.shared.version }
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
import Foundation
import Testing
@testable import SharedCoreKit

@Suite struct SharedCoreKitTests {

@Test func reachesTheKotlinFramework() {
#expect(!SharedCoreInfo.version.isEmpty)
}

/// Kotlin bytes are signed, so a naive copy mangles anything above 0x7F.
@Test func highBitBytesSurviveTheByteArrayCopy() {
let payload = Data([0x00, 0x7F, 0x80, 0xFF] + Array(repeating: UInt8(0xAB), count: 31))

let array = payload.kotlinByteArray

#expect(array.size == Int32(payload.count))
for (offset, byte) in payload.enumerated() {
#expect(UInt8(bitPattern: array.get(index: Int32(offset))) == byte)
}
}

@Test func rendersAnSvgDocument() {
let svg = KikCode.svg(payload: Data(repeating: 0xAB, count: 35), dimension: 512, background: "#000000")

#expect(svg.hasPrefix("<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"512\""))
#expect(svg.contains("<circle"))
#expect(svg.contains("#000000"))
#expect(svg.hasSuffix("</svg>\n"))
}
}
13 changes: 9 additions & 4 deletions libs/encryption/ed25519/build.gradle.kts
Original file line numberDiff line numberDiff line change
Expand Up@@ -85,6 +85,15 @@ kotlin {
compilations["main"].cinterops.create("ed25519") {
definitionFile = file("cinterop/ed25519.def")
includeDirs(ed25519SrcDir)
// Embed the archive in the klib rather than leaving it to each binary's
// linker flags: a static framework built from this module ships the C
// objects inside it, so consumers link one artifact and nothing else.
// Without this the framework exports `ed25519_*` as undefined symbols and
// only links inside an app that happens to compile the same C itself.
extraOpts(
"-staticLibrary", "libored25519.a",
"-libraryPath", libDir.get().asFile.absolutePath,
)
}

// Both the cinterop binding task and the Kotlin compile task need the
Expand All@@ -95,10 +104,6 @@ kotlin {
tasks.matching { it.name == "compileKotlin${name.replaceFirstChar { it.uppercaseChar() }}" }.configureEach {
dependsOn(compileTaskName)
}

binaries.configureEach {
linkerOpts("-L${libDir.get().asFile.absolutePath}", "-lored25519")
}
}

sourceSets {
Expand Down
Loading