From b02c2304baf5bfb8c73ab622dbe3526996bb52fb Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Sat, 5 Sep 2026 18:08:34 -0400 Subject: [PATCH] fix(shared-core): preserve macOS framework symlinks in the published zip KMMBridge's zipXCFramework task is a bare Gradle Zip task, and Gradle's Zip/CopySpec machinery dereferences symlinks instead of storing them. A macOS framework bundle is built entirely from symlinks (Versions/Current -> A, and Headers/Modules/Resources/binary each pointing into Versions/Current/*), so the archive KMMBridge produced for the macOS slice shipped three duplicate copies of the same content instead of the real bundle layout. iOS bundles are flat, which is why this went unnoticed until the macOS targets landed (#1402). Confirmed against the published 0.5.0 release: unzipping it and listing the macOS slice shows Versions/Current, Headers, Modules, Resources, and the binary as full duplicated files rather than symlinks, which fails codesign on anything that builds against that slice ("code object is not signed at all" on the nested framework). KMMBridge 1.2.1 is still the newest release and its tracker has nothing open about this, so there's no upstream fix to pick up. Once KMMBridge's own zipXCFramework runs, doLast deletes the result and re-zips the same source directory with `zip -y`, which stores symlinks as symlinks. The reconfiguration only touches the task when KMMBridge registers it, so it's a no-op on any build where publishing isn't enabled. --- kmp/shared-core/build.gradle.kts | 33 ++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/kmp/shared-core/build.gradle.kts b/kmp/shared-core/build.gradle.kts index b260f6abd4..fa9490a922 100644 --- a/kmp/shared-core/build.gradle.kts +++ b/kmp/shared-core/build.gradle.kts @@ -1,3 +1,6 @@ +import co.touchlab.kmmbridge.KmmBridgeExtension +import org.gradle.api.tasks.bundling.Zip + plugins { kotlin("multiplatform") id("com.android.kotlin.multiplatform.library") @@ -70,3 +73,33 @@ kmmbridge { macOS { v("14") } } } + +// KMMBridge 1.2.1's `zipXCFramework` task (a bare Gradle `Zip` task, only registered when +// `ENABLE_PUBLISHING=true`) dereferences symlinks instead of preserving them. A macOS framework +// bundle depends entirely on symlinks — `Versions/Current -> A`, and the top-level +// Headers/Modules/Resources/binary each symlink into `Versions/Current/*` — so the archive it +// produces for the macOS slice ships three literal duplicate copies of the same content instead of +// a real `Versions/A` plus symlinks. That breaks codesign for anything that consumes the macOS +// slice (nested-bundle signing needs the real bundle layout). `matching {}.configureEach {}` is a +// no-op when the task doesn't exist (i.e. every non-publishing build), and applies once KMMBridge +// registers it otherwise, regardless of task-registration order. +tasks.withType().matching { it.name == "zipXCFramework" }.configureEach { + // Resolved at configuration time — `project` can't be touched inside `doLast` under the + // configuration cache, so only plain values and the `ProviderFactory` itself cross into it. + val buildType = project.extensions.getByType().buildType.get().getName() + val sourceDir = project.layout.buildDirectory.dir("XCFrameworks/$buildType").get().asFile + val execOperations = project.providers + doLast { + val xcframeworkDir = sourceDir.listFiles { file -> file.isDirectory && file.name.endsWith(".xcframework") } + ?.singleOrNull() + ?: error("Expected exactly one .xcframework under $sourceDir") + val zipFile = archiveFile.get().asFile + zipFile.delete() + // `-y` stores symlinks as symlinks instead of following them (the default `zip` behavior, + // and Gradle's `Zip` task, both dereference). + execOperations.exec { + workingDir = sourceDir + commandLine("zip", "-y", "-r", zipFile.absolutePath, xcframeworkDir.name) + }.result.get() + } +}