Skip to content

Improve support for isolated projects#196

Open
OndraBasler wants to merge 1 commit into
touchlab:mainfrom
OndraBasler:bugfix/isolated-project-task-deps
Open

Improve support for isolated projects#196
OndraBasler wants to merge 1 commit into
touchlab:mainfrom
OndraBasler:bugfix/isolated-project-task-deps

Conversation

@OndraBasler

Copy link
Copy Markdown

Summary

The SKIE Gradle plugin reads task dependencies across the configuration graph while
configuring a project that applies co.touchlab.skie. This is forbidden under Gradle's
Configuration Cache / Isolated Projects, so enabling
org.gradle.unsafe.isolated-projects=true fails any
project that builds an XCFramework.

getFrameworksUsedInXCFrameworks() in ActualKgpShim calls
XCFrameworkTask.taskDependencies.getDependencies(...). This PR replaces that with a
configuration-cache-safe lookup based on the task's input files.

The bug

Configuration cache problems found in this build.
- Plugin 'co.touchlab.skie': Project ':shared' cannot access task dependencies directly
> Project ':shared' cannot access task dependencies directly

:shared here is any KMP module that applies SKIE and assembles an XCFramework
(XCFramework("Shared") with iosX64 / iosArm64 / iosSimulatorArm64). The
failure occurs while storing the configuration cache.

Root cause

co/touchlab/skie/plugin/shim/impl/ActualKgpShim.kt:

private fun getFrameworksUsedInXCFrameworks(): Set<Framework> =
    project.tasks.withType<XCFrameworkTask>()
        .toList()
        .flatMap { xcFrameworkTask ->
            xcFrameworkTask.taskDependencies.getDependencies(xcFrameworkTask)  // forbidden under isolated projects
                .filterIsInstance<KotlinNativeLink>()
                .map { it.binary }
        }
        .filterIsInstance<Framework>()
        .toSet()

taskDependencies.getDependencies() walks the task graph, which Gradle disallows during
configuration under the configuration cache / isolated projects. The result is only used
to set the isForXCFramework flag on each ActualSkieBinaryTarget, which gates
SkieConfigurationFlag.Build_SwiftLibraryEvolution. So the function only needs to know
which Framework binaries end up inside an XCFrameworkTask.

The fix

KGP's XCFrameworkTask.from(framework) registers the framework's link-task output as an
input file of the XCFramework task:

// org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFrameworkTask (KGP 2.4.0)
private fun processFramework(framework: Framework) {
    require(framework.konanTarget.family.isAppleFamily) { ... }
    inputs.files(framework.linkTaskProvider.map { it.outputFile.get() })   // link output is an INPUT FILE
}

So the same framework↔XCFramework relationship is reachable via task.inputs.files
(config-cache safe) instead of walking task dependencies. The replacement matches each
KotlinNativeLink output file against the XCFramework tasks' input files:

private fun getFrameworksUsedInXCFrameworks(): Set<Framework> {
    val xcFrameworkTasks = project.tasks.withType<XCFrameworkTask>()
        // Prevents concurrent modification exception.
        .toList()
    if (xcFrameworkTasks.isEmpty()) return emptySet()

    // KGP's XCFrameworkTask.from(framework) registers linkTask.outputFile as an input file, so we match on
    // input files instead of taskDependencies.getDependencies(), which Gradle forbids under configuration
    // cache / isolated projects ("cannot access task dependencies directly").
    val xcFrameworkInputPaths = xcFrameworkTasks
        .flatMap { it.inputs.files.files }
        .mapTo(HashSet()) { it.canonicalPath }

    return project.tasks.withType<KotlinNativeLink>()
        .toList()
        .filter { linkTask ->
            linkTask.binary is Framework &&
                linkTask.outputFile.get().canonicalPath in xcFrameworkInputPaths
        }
        .map { it.binary }
        .filterIsInstance<Framework>()
        .toSet()
}

How to reproduce

  1. A KMP module applies co.touchlab.skie and builds an XCFramework.
  2. Set in gradle.properties:
    org.gradle.configuration-cache=true
    org.gradle.unsafe.isolated-projects=true
    
  3. Sync Gradle.
  4. Before the fix: build fails with Project ':shared' cannot access task dependencies directly
    and the cache entry is discarded.
  5. After the fix: configuration succeeds and the cache stores.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant