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
5 changes: 5 additions & 0 deletions docs/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,11 @@ All in `PM/commonTest`, so they run wherever the interpreter loads.
README naming the Compose version and the commit, and `kotlin-stubs.zip` on every `v*` tag's release.
`Status: partial` — the workflow could not be run where it was written; its YAML parses and its
assemble step was executed locally.
- **B-9** When a configuration named `typedpythonStubs` exists (toolchain's plugin creates it), every
stubs task's output directory is added to it, with the task dependency travelling with the files, in
either plugin application order. Without that configuration nothing happens and none is created.
`Status: implemented` — `GP/TypedPythonStubsWiringTest.kt` (a `ProjectBuilder` project; the toolchain
side that consumes the configuration is toolchain#23 and is not exercised here).

## 7. Threading and builds

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ private const val OPTION_EXCLUDE_PACKAGES = "python.multiplatform.excludePackage
private const val ROLE_LIBRARY = "library"
private const val ROLE_APP = "app"

/** Created by toolchain's plugin (toolchain#23); referenced by name only. */
private const val TYPEDPYTHON_STUBS_CONFIGURATION = "typedpythonStubs"

/**
* Which KSP configurations the processor belongs on.
*
Expand Down Expand Up @@ -495,6 +498,20 @@ class PythonBindingsPlugin : Plugin<Project> {
outputDirectory.set(project.layout.buildDirectory.dir("generated/pythonStubs/$sourceSetName"))
}
project.tasks.matching { it.name == "prepareKotlinIdeaImport" }.configureEach { dependsOn(task) }
feedTypedPythonStubs(project, task)
}

/**
* Adds [task]'s output to toolchain's `typedpythonStubs` configuration, by name only -- there is
* no compile-time dependency on toolchain. `matching` + `configureEach` covers a configuration
* that already exists and one created later, so plugin application order does not matter; if it
* never appears, nothing happens. The provider is `flatMap`ped from the task, so resolving the
* configuration runs the task.
*/
private fun feedTypedPythonStubs(project: Project, task: org.gradle.api.tasks.TaskProvider<PythonStubsTask>) {
project.configurations.matching { it.name == TYPEDPYTHON_STUBS_CONFIGURATION }.configureEach {
project.dependencies.add(name, project.files(task.flatMap { it.outputDirectory }))
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package python.multiplatform.gradle

import org.gradle.api.Project
import org.gradle.testfixtures.ProjectBuilder
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNull
import kotlin.test.assertTrue

/**
* SPEC B-9 (python-multiplatform#27): the stubs task output reaches toolchain's `typedpythonStubs`
* configuration by name, with the task dependency attached, whichever side appears first.
*/
class TypedPythonStubsWiringTest {

private val configurationName = "typedpythonStubs"

private fun projectWith(sourceSets: List<String>): Project {
val project = ProjectBuilder.builder().build()
val container = project.container(DummySourceSet::class.java) { name -> DummySourceSet(name, project) }
sourceSets.forEach { container.create(it) }
project.extensions.add("kotlin", DummyKotlinExt(container))
sourceSets.forEach { project.configurations.create("${it}Classpath") }
return project
}

private fun wire(project: Project, sourceSets: List<String>) {
val extension = project.extensions.create("pythonBindingsUnderTest", PythonBindingsExtension::class.java)
extension.artifactIncludePackages.set(listOf("junit.runner"))
extension.artifactTargets.set(sourceSets.associate { "${it}Classpath" to it })
PythonBindingsPlugin().configureArtifactBindings(project, extension)
}

private fun stubsDir(project: Project, sourceSet: String) =
project.layout.buildDirectory.dir("generated/pythonStubs/$sourceSet").get().asFile

@Test
fun aConfigurationThatExistsBeforeTheWiringReceivesEverySourceSetsStubs() {
val sets = listOf("desktopMain", "androidNativeArm64Main")
val project = projectWith(sets)
project.configurations.create(configurationName)
wire(project, sets)

val files = project.configurations.getByName(configurationName).files
sets.forEach { assertTrue(stubsDir(project, it) in files, "$it missing from $files") }
}

@Test
fun aConfigurationCreatedAfterTheWiringReceivesTheStubsToo() {
val sets = listOf("desktopMain", "androidNativeArm64Main")
val project = projectWith(sets)
wire(project, sets)
project.configurations.create(configurationName)

val files = project.configurations.getByName(configurationName).files
sets.forEach { assertTrue(stubsDir(project, it) in files, "$it missing from $files") }
}

@Test
fun resolvingTheConfigurationDependsOnTheStubsTask() {
val project = projectWith(listOf("desktopMain"))
val configuration = project.configurations.create(configurationName)
wire(project, listOf("desktopMain"))

val deps = configuration.buildDependencies.getDependencies(null)
val names = deps.map { it.name }
assertTrue(
names.any { it.startsWith("generatePythonStubs") },
"the task must travel with the files; dependencies were $names",
)
}

@Test
fun withoutTheConfigurationNothingIsCreatedAndNothingFails() {
val project = projectWith(listOf("desktopMain"))
wire(project, listOf("desktopMain"))

assertNull(project.configurations.findByName(configurationName))
assertFalse(project.tasks.names.isEmpty())
}
}
Loading