so I thought I managed to write "my own" custom de...
# plugin-development
s
so I thought I managed to write "my own" custom dependency setup to support python dependencies. But now I'm facing the problem that I need to use a set of those dependencies as a task input and when resolving them I don't get them if I invoke
Copy code
resolvableConfiguration.incoming.resolutionResult.rootComponent.get().dependencies
Reading around it seems that maybe those dependencies don't match the variant attributes associated to the configuration, but I don't know how I can tweak this so that these self-resolving dependencies "declare" the correct variant. Some code to make this more concrete:
Copy code
// plugin `apply()`
val declarable = project.configurations.dependencyScope("pythonScope")

project.configurations.consumable("pythonConsumable") {
    it.attributes {
        it.attribute(Usage.USAGE_ATTRIBUTE, project.objects.named(Usage::class.java, "python"))
    }
}

val resolvable = project.configurations.resolvable("pythonResolvable") {
    it.extendsFrom(declarable.get())

    it.attributes {
        it.attribute(Usage.USAGE_ATTRIBUTE, project.objects.named(Usage::class.java, "python"))
    }
}

// custom dependency class
interface PipDependency : Dependency {
    fun asPipRequirement(): String
}

data class RemotePipDependency(
    private val name: String,
    private val versionSpec: VersionConstraint,
    private val extras: List<String>,
    private val objectFactory: ObjectFactory,
) : PipDependency, SelfResolvingDependencyInternal, FileCollectionDependency {
    // ... etc etc, not much going on here
}

// plugin extension to encapsulate the custom dependency
internal class DefaultPipDependencyExtension @Inject constructor(
    private val dependencies: DependencySet,
    private val objectFactory: ObjectFactory,
) : PipDependencyExtension {
    override fun invoke(name: String, versionSpec: String?, extras: List<String>): Dependency =
        RemotePipDependency(
            name,
            versionSpec?.let { VersionConstraint(it) } ?: VersionConstraint.EMPTY,
            extras,
            objectFactory,
        ).also { dependencies.add(it) }

    override fun invoke(project: ProjectDependency): Dependency = project.also { dependencies.add(it) }
}
If from a task I call
Copy code
println(resolvable.get().incoming.resolutionResult.rootComponent.get().dependencies)
I get nothing.
v
Maybe because you did not declare any dependency? 😄
Maybe it would help if you show a full MCVE
s
ah eh sorry, I forgot to post that part
v
Besides that you of course should neither use internal classes, nor self-resolving dependency which are deprecated 🙂
s
sorry for the ignorance, but what is a MCVE?
v
s
you were the one suggesting me using self-resolving dependencies in the first place 😅
v
But were we then not told that this is bad by someone?
s
but, aside from the bantering, it's also what Kotlin JS uses (and why the "internal" version exists afaict)
true true 😅 but after a week of trying to find a way it seemed to be the most viable one
v
🤷‍♂️😄
a
I think it's super cool that you're trying to get Python working better in Gradle. Unfortunately Gradle sucks for non-JVM dependencies, there's zero support :( Some plugins (see below) hack-around this by spinning up a localhost server that pretends to be a Maven or Ivy repo, and internal converts the request into an external repo request. It could be a lot of work but unfortunately it's the only Gradle compatible approach. • https://github.com/JetBrains/intellij-platform-gradle-plugin/blob/v2.0.1/src/main/kotlin/org/jetbrains/intellij/platform/gradle/shim/PluginArtifactoryShim.kt#L21https://github.com/gradlets/gradle-typescript/blob/1.4.1/gradle-typescript/src/main/java/com/gradlets/gradle/typescript/shim/NpmArtifactoryShim.java#L45
s
it's a very not ideal situation. For half of what I need tapping into the configuration mechanism seems too much, and yet I need to in order to have the artifact of the plugin usable between subprojects.
v
I think I also suggested the proxy approach @Adam, he just didn't like it too much and preferred to use the internal classes the Kotlin/JS plugin is using 🙂
s
thanks @Adam that's what @Vampire suggested in the first place too. Truth is that I don't want Gradle to fully take over dependency resolution and I still need to put the dependency declarations in a pyproject.toml file, but if that's the only way I can get this done then I'll have to bite this bullet I guess
a
I still need to put the dependency declarations in a pyproject.toml file
Do you mean that the dependencies will be declared in two places, the Python toml and Gradle
dependencies {}
?
s
no, the goal would be to generate the pyproject.toml file from the plugin using the declared dependencies. For this technically I don't need to have the dependencies resolved though
a
Ah gotcha
in that case, what about just avoiding
dependencies {}
altogether and writing your own
pythonDependencies {}
extension?
It makes sense if you want to avoid using Gradle to resolve dependencies, because it would simplify the process. You'd just need a custom PreparePythonDependenciesTask that generates
pyproject.toml
and executes whatever the Python dependency downloading task is.
s
yes that could work for remote dependencies, but the other half of the problem is that I want to be able to do something like
Copy code
dependencies {
  pip(project(grpc.stub.python))
}
and for this it's definitely easier to tap into the variant-aware resolution system
a
true, but you could still do that under the hood with a custom
pythonDependencies {}
block. It would be even easier because the standard
dependencies {}
block is quite old, and is very Groovy based. I'd create a
PythonDependencies
class and add methods with overloads. •
fun pip(coord: String)
would be put straight into the
pyproject.toml
fun pip(project: Project)
would use the Gradle dependency resolution.
s
and to be fair, the plugin in its current state sort of works already, but I'm trying to make the set of dependencies an output of the task that generates the toml file to get the full advantage of Gradle caching and make the code slightly leaner and more readable. But in order to do that, according to the docs at least, I need to use the resolvable configuration
@Adam that seems like a viable alternative, I guess I could use the configurations I define only for project dependencies and keep a simple list for the others
a
yeah, exactly
s
but to be honest, I'm still also curious to understand why my attempt failed. Is it because self-resolving dependencies don't and can't provide metadata and Gradle cannot figure out which variant they belong to?
a
I need to use the resolvable configuration
Yeah, for sharing files between subprojects you need a 'declarable' configuration and a 'resolver' and a 'consumable'. It's a lot of boilerplate. Just to be clear, in case there's some confusion, the docs you linked are about getting the dependency graph. To get the actual files it's simpler:
Copy code
val fooConfResolver by configurations.creating {
  extendsFrom(fooConf)
  isCanBeDeclared = false
  isCanBeConsumed = false
  isCanBeResolved = true
}

val fetchFiles by tasks.registering(Sync::class) {
  from(fooConfResolver.incoming.files)
  into("fetched-files")
}
possibly because
resolvableConfiguration.incoming.resolutionResult.rootComponent.get().dependencies
has
.get()
it resolved the graph too early, so it didn't get the resolved dependencies? But I'm just guessing.
it could also be that there are just no resolved dependencies. Maybe nothing was found in the repo, or there was a dependency but it wasn't passed through correctly?
s
yeah the thing I don't need the files there, I need the name and version specifications I can find in the
declarable
. I'm also not sure if it's resolved too early, it's inside the
doLast
of the task that otherwise reads from the
declarable
configuration
also, the dependencies we're talking about here are of a custom class that extends SelfResolvingDependency
a
was it registered as a task input correctly? (Or you're running with
--no-build-cache
&
--no-configuration-cache
)?
s
umm, no I don't think so and definitely not. I was just "testing out" the approach suggested by the documentation
a
okay cool, well it sounds like you've got the basics done correctly
although if you just want to get the declared dependencies, not the resolved dependencies, maybe
incoming.resolutionResult
isn't the best fit
I think there's an API for fetching declared dependencies, but you'd probably want to call that on the declarable configuration
s
yup, before trying this I was just passing
declared.get().dependencies
as an input in a property. This works but breaks the build the moment I try to define an output for the task, I guess because something in the tree of classes making up the set of
RemotePipDependency
is not serializable and thus Gradle cannot cache it. The sample you pointed at is precisely where I got the line about
incoming.resolutionResult.rootComponent
.
a
ahh okay - and was there a task in the sample for logging the declared dependencies? I remember seeing that somewhere...
if a Gradle class isn't serializable you can always try mapping the key properties to a custom 'data holder' class, that only contains serializable values (i.e. primitives!)
ahh yes here's where I saw the info about getting declared dependencies haha https://mastodon.jakewharton.com/@jw/112171457869714385
s
That’s literally me for the last 3 weeks 😂
I honestly thought of giving up and resign to use Bazel, but the help I received here from you guys convinced me that even though Gradle has its quirks it’s still the better choice
Anyway, I think I have at least a couple of directions I can explore, I feel more confident that I can pull this off. Thanks!
Regarding the sample project, yes there’s a task that walks the dependency graph using the dependencies in the resolvable’s root component. It’s only in the downloadable zip file though not in the docs page