Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kotlin dsl support #262

Merged
merged 36 commits into from
Oct 24, 2018
Merged

Kotlin dsl support #262

merged 36 commits into from
Oct 24, 2018

Conversation

marcoferrer
Copy link
Contributor

@marcoferrer marcoferrer commented Aug 23, 2018

This is not ready to merge and is still a work in progress but I wanted to open the pr a little early to get some feedback on the proposed approach to resolving #219.

@googlebot
Copy link

Thanks for your pull request. It looks like this may be your first contribution to a Google open source project (if not, look below for help). Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

📝 Please visit https://cla.developers.google.com/ to sign.

Once you've signed (or fixed any issues), please reply here (e.g. I signed it!) and we'll verify it.


What to do if you already signed the CLA

Individual signers
Corporate signers

@marcoferrer
Copy link
Contributor Author

I signed it!

@zpencer
Copy link
Contributor

zpencer commented Aug 23, 2018

dummy comment to try and unwedge CLA bot

@zpencer
Copy link
Contributor

zpencer commented Aug 23, 2018

I like this idea of adding extensions functions, and then changing the groovy API only if extensions can not solve the problem. Taking a compile only dependency on kotlin is fine.

Would it be possible to add a test case that shows how a kotlin DSL build file looks? https://github.com/google/protobuf-gradle-plugin/pull/205/files may be a possible starting point.

// Extra proto source files besides the ones residing under
// "src/main".
"protobuf"(files("lib/protos.tar.gz"))
"protobuf"(fileTree("ext/"))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using string configuration access but i should probably look into adding an extension scoped to the dependency handler

build.gradle Outdated
// Temporary work around to get compilation working during development
// Considering moving to a separate module
// https://discuss.gradle.org/t/kotlin-groovy-and-java-compilation/14903/10
compileGroovy.dependsOn = compileGroovy.taskDependencies.mutableValues - 'compileJava'
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really ugly so Im hoping to move it to a seperate module. Im just not sure where it should live in the project.

@marcoferrer
Copy link
Contributor Author

@zpencer I added an example of a kotlin dsl script based on the original example. It is still in the example directory while I try to figure out how to wire up a test project for this that would work with the build process. Im open to suggestions on how to approach it.

@marcoferrer
Copy link
Contributor Author

marcoferrer commented Aug 24, 2018

Tested the current impl in a new idea project and everything seems to be working so far.
Type resolution and code generation are working as expected. Here’s a screenshot with type hints enabled.

screen shot 2018-08-23 at 11 38 15 pm

@gmale
Copy link

gmale commented Oct 10, 2018

This is a huge improvement over the currently required syntax!

I'm not sure if this question is out of scope (i.e. too complex of a change to the underlying plugin). I only bring it up since the PR requested feedback:

Is there a way to reduce the amount of boilerplate? All the developer is really trying to convey here is, "I want to use protobuf with grpc and javalite" which is probably an extremely common usecase for kotlin-dsl users. Any logic that could be moved out of the extension object and into the plugin would make life easier for many users. For example:

protobuf {
    plugins {
        id("grpc") option "lite"
    }
}

or maybe:

protobuf {
    plugins {
        id("javalite") version "3.0.0"
        id("grpc") version "1.8.0"
    }
}

@zhangkun83 zhangkun83 requested a review from zpencer October 10, 2018 16:38
@marcoferrer
Copy link
Contributor Author

Performed some clean up and pushed the updated example project.

I think there might be a way to implement the shorter syntax for options in the task.plugin config block. ie

ofSourceSet("main").forEach{
            it.plugins {
                id("grpc") option "testing"
            }
}

Im just not sure having options configurable in the protobuf.plugins block is a good idea since the assumed config would be implicitly applied to all source sets.

@googlebot
Copy link

CLAs look good, thanks!

@marcoferrer
Copy link
Contributor Author

marcoferrer commented Oct 11, 2018

@zpencer Im not sure if my build is failing in travis to due master failing?

Copy link
Contributor

@zpencer zpencer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RE failing CI: it looks like this test is failing, but in master it is passing: ./gradlew test --tests com.google.protobuf.gradle.ProtobufAndroidPluginTest --stacktrace"

I'm still learning Kotlin, so please excuse my basic questions about syntax that is hard to learn about by Googling :)


fun ProtobufConfigurator.plugins(closure: NamedDomainObjectContainerScope<ExecutableLocator>.() -> Unit) {
plugins(delegateClosureOf<NamedDomainObjectContainer<ExecutableLocator>> {
this{ this.closure() }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this { ... } mean in Kotlin?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah looks gross and after doing some additional digging I updated with a cleaner usage. This implementation was a port from the existing example provided here #219 (comment)

Back to your original question. There are a couple of Kotlin features at work here that make up this statement.

  • Lambda with Receiver
    • The closureOf method takes a lambda and the this property in that lambda is the argument passed in.
  • Invoke Operator Overloading
    • The gradle kotlin dsl provides and extension function overloading the invoke operator for objects with type NamedDomainObjectContainer
    • This function allows us to apply our configuration to the receiver
  • Lambda as Last Param
    • If the last argument to a function is a lambda then the parenthesis can be omitted.

In the latest commit, this code has been refactored to something thats a little more readable, after looking into what exactly the method needed to achieve.

fun GenerateProtoTask.plugins(closure: NamedDomainObjectContainerScope<GenerateProtoTask.PluginOptions>.()-> Unit) {
    plugins(closureOf<NamedDomainObjectContainer<GenerateProtoTask.PluginOptions>> {
        this(closure)
    })
}

/**
* The 'testProtobuf' configuration.
*/
val ConfigurationContainer.testProtobuf: Configuration
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be more general? It looks like this gives special treatment to the main sourceset (protobuf) and the test sourceset (testProtobuf) but won't handle user created sourcesets like this:

sourceSets {
    main {
    }
    foo {
        java {
            srcDirs 'src/foo/'
        }
    }
}


dependencies {
  fooProtobuf files("/tmp/protos-test.tar.gz")
}

createConfiguration is where the plugin creates the xxxxProtobuf configurations in the project:
https://github.com/google/protobuf-gradle-plugin/blob/master/src/main/groovy/com/google/protobuf/gradle/ProtobufPlugin.groovy#L154

Maybe there is some way this extension can plug into them in a general way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I cant dynamically provide the dependency configuration methods, but I can try providing a nicer mechanism.

You can try it out in the latest commit.

dependencies{
    protobuf["sample"]("com.google.protobuf:protobuf-java:3.6.1")
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This solution has some pros and cons so I wanted to discuss them with you when possible. Ill add a full explanation in a few.

}

fun ProtobufConfigurator.protoc(closure: ExecutableLocator.() -> Unit) {
protoc(delegateClosureOf(closure))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am still trying to wrap my head around delegateClosureOf vs closureOf (https://github.com/gradle/kotlin-dsl/blob/master/doc/getting-started/Closures.md). Is delegateClosureOf meant for closures that make use of the closure delegate from groovy ? As far as I can tell this plugin does not use this feature.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So is a by-product of my ignorance of groovy. I updated to use closureOf

})
}

fun ProtobufConfigurator.GenerateProtoTaskCollection.ofSourceSet(sourceSet: String): Collection<GenerateProtoTask> =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why can't the groovy version of these functions be called directly? Maybe a comment would be helpful.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a comment to the source but Ill post it here as well.

The method generatorProtoTasks applies the supplied closure to an
instance of GenerateProtoTaskCollection.

Since JavaGenerateProtoTaskCollection and AndroidGenerateProtoTaskCollection
each have unique methods, and only one instance in allocated per project, we need a way to statically resolve the available methods. This is a necessity since Kotlin does not have any dynamic method resolution capabilities.

Copy link
Contributor

@zpencer zpencer Oct 15, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, so the problem is the dynamic typing of GenerateProtoTaskCollection, since we don't know whether it is JavaGenerateProtoTaskCollection or AndroidGenerateProtoTaskCollection, kotlin's static typing means we can call neither's methods.

What do you think about instead adding two extension methods to GenerateProtoTaskCollection like asJavaTasks returning the same object casted as JavaGenerateProtoTaskCollection (or an empty collection if not applicable), and asAndroidTasks casting as AndroidGenerateProtoTaskCollection or an emtpy collection. That way, we minimize the amount of extension code that must be kept in sync with the groovy DSL. Since we expect kotlin DSL users to be in an IDE, this should be relatively discoverable code auto completion.

@marcoferrer
Copy link
Contributor Author

Awesome Ill clean up ProtobufDependencyConfiguration.kt.

In regards to writing up tests, Im still trying to get a test project working with the existing test classes. There isnt much available in terms of resources for testing kotlin dsl projects using the gradle runner so its been a slow process. Just wanted to give a heads up.

@marcoferrer
Copy link
Contributor Author

Also there are extensions built into the kotlin gradle dsl that clean up the custom source set syntax a little further

dependencies {
    "fooProtobuf"(files("/tmp/protos-test.tar.gz"))
}


// This extension is not auto generated when we apply the plugin using
// apply(plugin = "com.google.protobuf")
val Project.protobuf: ProtobufConvention get() =
Copy link
Contributor Author

@marcoferrer marcoferrer Oct 16, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is another extension that relies on the plugin being applied in the plugins block, should we add it to ProtobufConfiguratorExts.kt as well? It would normalize different plugin usages.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you clarify what this block is doing? Also it looks like HasConvention is not a part of the public gradle API

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This extension is used to get the plugin convention from the project, which was needed for Line 106. I didnt realize that HasConvention isnt part of the public api. The code itself I copied from the dynamically generated code file that gradle creates when you use the plugins block. I can see if there's a way around casting to HasConvention

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cleaned up the usage to the following

val Project.protobuf: ProtobufConvention get() =
    this.convention.getPlugin(ProtobufConvention::class)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Can this workaround be moved nearer to apply(plugin = "com.google.protobuf") ?

@marcoferrer
Copy link
Contributor Author

Finally got tests added. PR is ready for a full review whenever you get a chance.


// This extension is not auto generated when we apply the plugin using
// apply(plugin = "com.google.protobuf")
val Project.protobuf: ProtobufConvention get() =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you clarify what this block is doing? Also it looks like HasConvention is not a part of the public gradle API

Copy link
Contributor

@zpencer zpencer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for contributing this PR! I added a few small remaining concerns.

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.8-bin.zip
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

undo this change


// This extension is not auto generated when we apply the plugin using
// apply(plugin = "com.google.protobuf")
val Project.protobuf: ProtobufConvention get() =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Can this workaround be moved nearer to apply(plugin = "com.google.protobuf") ?

@zpencer
Copy link
Contributor

zpencer commented Oct 24, 2018

@zhangkun83 would you like to take a look before I merge?

@zhangkun83
Copy link
Collaborator

I took a glance and the DSL looks pretty good. I trust @zpencer's judgement. Good job guys.

@zpencer
Copy link
Contributor

zpencer commented Oct 24, 2018

Great! Let's merge it so our users can start using this experimental feature.

@zpencer zpencer merged commit 548171d into google:master Oct 24, 2018
@marcoferrer
Copy link
Contributor Author

Really happy to see this get merged. @zpencer Thanks again for all your help. When I get some free time Im hoping to add more documentation to the new code and possibly some additional tests.

@marcoferrer marcoferrer deleted the kotlin-dsl-support branch October 24, 2018 16:14
zhangkun83 pushed a commit to zhangkun83/protobuf-gradle-plugin-1 that referenced this pull request Nov 7, 2018
This is an experimental feature.
See `examples/exampleKotlinDslProject` for an example.
@zhangkun83
Copy link
Collaborator

zhangkun83 commented Nov 8, 2018

0.8.7 is released! It's already live on plugins.gradle.org. It may take a few hours for it to show up in Maven Central.

@ejona86 ejona86 mentioned this pull request Oct 8, 2022
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.

5 participants