:loudspeaker: Hi folks! <Amper 0.9.0> is out! This...
# kotlin-toolchain
j
📢 Hi folks! Amper 0.9.0 is out! This release brings a bunch of features that we're excited about: • First preview of extensibility - you can finally try Amper plugins and let us know what you think! • Dependency resolution graph caching - faster model reading when your dependencies don't change • Incremental compilation for Java • Maven-like layout support (for easier transitions) • Auto-sync in the IDE (use IJ 2025.3 Beta) • ...and plenty of new diagnostics and quality of life improvements! Check out our public blog post and our GitHub release notes for more info. For the occasion, we also revamped our documentation and published it as a proper website! We hope you'll like it 🙂
amper intensifies 8
🎉 14
❤️ 2
r
Hii amper intensifies Can we already use "kotlin compiler plugins" with this plugin thing, or not yet? I mean able to pass arguments to builds. Are the plans to keep the layouts permanent (Gradle or maven like), or is this only recommended for migration? Thanks ✨
o
Skimming the docs, it looks very promising, in particular the combination of declarative YAML with imperative Kotlin code, and the path-based dependencies between tasks. I wonder if you had considered a more explicit API than the magic behind the scenes associated with
@TaskAction
,
@Input
and
@Output
? I guess with testballoon TestBalloon, I have to wait for a couple of days 😉 until KMP and compiler plugin support evolves, but that's fine. kodee loving
➕ 1
j
> Are the plans to keep the layouts permanent (Gradle or maven like), or is this only recommended for migration? Hi @Rey (Kingg22)! Well technically you're free to keep the
layout: maven-like
forever, but initially we introduced it to ease transitions from Maven/Gradle. Do you generally prefer this layout? Could you please share why you would stick to it? That would be quite helpful for us to understand the users' expectations.
Can we already use "kotlin compiler plugins" with this plugin thing, or not yet? I mean able to pass arguments to builds.
There are several things to unpack here, sorry for the delay in my response. 1. In the current state, plugins are only local to the project, and only consist of custom tasks. 2. Custom tasks are separate from built-in tasks like the Kotlin compilation task. They can generate sources and thus provide more inputs to the compilation task, but they can't customize the compilation task itself (e.g., add compiler arguments). 3. The way to configure built-in things (like compiler arguments) in Amper is via the module settings, which can be set directly in
module.yaml
, or via templates if you want to share configuration between module. Plugins don't provide anything in this regard for now – since they are only local to the project, that would be redundant with templates. That said, we will soon provide a way to publish/consume plugins to/from Maven. With this feature will come the need to provide some kind of template-like data together with the plugin, so the consumers of the plugin will get some module settings by default when enabling the plugin. Now, if something is not doable in
module.yaml
, it is not doable in templates either (nor in future bundled plugin templates). So if we want to support 3rd party compiler plugins, this needs to be implemented first as a feature on its own. Then, templates (and later plugins) will be able to leverage this. We're aware that 3rd party compiler plugins are important to many users. _ TL;DR: not yet. Once we implement support for the 3rd party compiler plugins + support for plugin publication and plugin bundled templates, you'll be able to write Amper plugins that wrap a custom Kotlin compiler plugin.
kodee loving 1
> I wonder if you had considered a more explicit API than the magic behind the scenes associated with
@TaskAction
,
@Input
and
@Output
? Hi @Oliver.O! Could you please clarify what you're referring to here? Do you mean the inferred task dependencies from input/output paths? Or maybe the automatic task avoidance based on changed inputs/outputs? By "more explicit API", do you mean for example having to declare task dependencies by hand in
plugin.yaml
? We considered this, but it seems like the majority of the time, having an input that's the same path as another task's output really should mean you depend on that task, so we tried to optimize for the common case. There is an obvious need for an escape hatch though. A very common special case is linter-like plugins that save a baseline/reference file, and provide a task to update it and a task to check against it. These 2 tasks have this output/input link, but obviously the one that checks shouldn't depend on the task that generates it, otherwise you'll never see any failure.
o
My more explicit I was not referring to anything YAML. I also think that the automatic input/output dependency wiring is a good thing (I have occasionally stumbled across Gradle not capturing this automatically), though I understand the exceptional linter case. So I wonder about explicitness in contrast to annotated parameters. For example, if I understand things correctly, an
@Input
annotation can only be present in a task's parameter. In that case I would lose the ability to use plain Kotlin to construct inputs, like it would be possible via an explicit invocation of something like
inputs.add(myFirstInput)
, then followed by
inputs.add(mySecondInput)
.
j
Ah, so you mean dynamically registering inputs/outputs via Kotlin code, right? We considered things like this, but we refrained from doing so - at least for now. The main idea is that we want to get the task list (and inputs/outputs, and maybe even the task graph) without executing any third-party code. This gives us some desirable outcomes: • the import/sync in the IDE stays super fast (no bad-behaved plugin ruining the party) • we can import/sync a project without security risks (no "do you trust this project?" pop-up) • we can provide tooling around the tasks and task graph (detect cycles, invalid input/outputs, or things like this statically)
So our plan is to try to keep things like this until we see some cases where this approach is too problematic. This is partly why we're eager to get feedback. So if you're stuck when writing some task/plugin because of the lack of flexibility, please tell us about your case, it will be very useful!
o
Sure, and I appreciate your motivations. Naturally, I'm comparing your approach to my experiences with Gradle, where we've got lots of annotations, lots of magic behind the scenes, and – on top of that – an explicit way to replicate the annotation-based functionality. With multiple techniques to do the same thing, building up a sound mental model is hard. Even more so, because things happening behind the scenes can get messy if there is no clear execution model, or an inadequate one. Think of all the issues with the Gradle lifecycle where every plugin is expected to do everything lazily, and then plugins depend on the results from other plugins but there is no clear execution model that enables you to rely on such results being ready at a specific point in time. So in your case, if it's just these inputs and outputs and it works and makes fast IDE imports a reality, I'm totally fine with it. If it would evolve into a convoluted model of lots of annotations and lots of magic behind the scenes, we'd all be in trouble. So for now, it's mainly an abundance of caution. Unless I actually try to write a third-party Amper plugin (once that becomes possible), I can only guess what might become complicated. So for now, the best hint on possible use cases that I have is probably what's inside TestBalloon's gradlePlugin/shared/configureWithTestBalloon.kt, including • adding a source file (compilation input), • registering the source with with either ◦ all top-level source sets (lifecycle issue: when do other plugins finish setting them up?), or ◦ all target source sets (how do I know their names if users could configure them?). • passing environment variables to test task invocations without invalidating the configuration cache, depending on ◦ input environment variables, ◦ command line settings (currently, Gradle properties), ◦ detecting a "run from the IDE" situation, • capturing command line parameters for test selection (Gradle's
--tests
) without invalidating the configuration cache to ◦ set up environment variables, ◦ create a custom Karma configuration (simulating environment variables), ◦ removing or patching command line parameters which legacy test frameworks wouldn't understand. Based on that experience, a clean execution model which would alleviate the lifecycles issues we are currently experiencing with Gradle (so many plugins resort to
afterEvaluate
) would be the most important contributing factor. Does that help at this stage?
👍 2
🙏 1
🙏 1
r
When thinking about the execution model of plugins, please also consider multi-round code generation. Example: a KSP plugin (https://kotlinlang.org/docs/ksp-multi-round.html). I recently worked on a Gradle code generator plugin using KSP that operated in the presence of another KSP-based plugin, and found the mental model very hard to understand. I even posted about it on #C013BA8EQSE (https://kotlinlang.slack.com/archives/C013BA8EQSE/p1762363136196159) but nobody really replied, indicating to me that maybe not many others have a clear mental model of it either. I managed to (seemingly) make it work with what feels like a hack to me, but who knows if it is the "right way"? Amper will hopefully make this sort of thing much more understandable.
j
@Oliver.O thanks a ton for sharing all this. We'll definitely use this input (no pun intended)!
😆 2
o
Cool! In my case, it seems like the source set issues could evaporate as we're progressing towards 2.3.20: If that works as expected, I can stop generating a source file via Gradle and add it directly in the compiler plugin via IR: https://kotlinlang.slack.com/archives/C7L3JB43G/p1764161013977889?thread_ts=1763638455.273279&amp;cid=C7L3JB43G
j
@rocketraman thanks for sharing this.
When thinking about the execution model of plugins, please also consider multi-round code generation. Example: a KSP plugin (https://kotlinlang.org/docs/ksp-multi-round.html).
Just to be clear, Amper supports KSP2 out-of-the-box, and with it, its multi-round processor calls. We don't control how KSP works, we just pass it all the processors, and then KSP does its own multi-round operations and ends up producing some outputs. We use these final outputs for our compilation. KSP2 is not a compiler plugin integrated into the compiler anymore (unlike KSP1), it's now a separate process that runs before Kotlin compilation. On the other hand, Amper doesn't support multi-round codegen in general Amper plugins. If your plugin generates sources, they are currently not seen by other plugins (they are just registered as inputs for the compilation). That said, the idea is that, if your plugin's goal is to read Kotlin sources to generate stuff, it should probably be a KSP processor instead, and this way you get the multi-round lifecycle "for free". If your plugin doesn't read source files, then it doesn't need multi-round execution because its inputs will not change. This is why we didn't consider introducing multi-round codegen for the time being. But we'll keep an eye out for more use cases, for sure. cc @Fedor Ihnatkevich
f
This is why we didn't consider introducing multi-round codegen for the time being.
If you need the multi-round code generation (based on annotation processing) - then KSP is the way to go - it has all the necessary infrastructure. And Amper integrates KSP out of the box. In our new Amper plugin tasks you can read user sources (from
src
) and generate more sources - that's pefectly fine. However you can't depend on the generated sources and then produce more (this requires multi-round or some other workarounds). We are not currently looking into implementing multi-round source generation using custom tasks in Amper, because KSP covers this, at least somehow.
r
That makes sense. One thing that wasn't clear to me was how KSP's API integrated with Gradle's (and now Amper) task input/output declarations. When I was using the KSP API to declare generated code, Gradle didn't seem to recognize that as an output. I had to explicitly declare it as an output in the Gradle plugin. Maybe Amper can do a better job at reading KSP's input/output information. I don't know if this can be made compatible with the model discussed above https://kotlinlang.slack.com/archives/C062WG3A7T8/p1764096694971039?thread_ts=1763998565.289599&cid=C062WG3A7T8.
f
That makes sense. One thing that wasn't clear to me was how KSP's API integrated with Gradle's (and now Amper) task input/output declarations.
It doesn't have to, because KSP is a built-in thing in Amper and doesn't have to use public plugin APIs, like `@Input`/`@Output`, to interface with Amper build engine. So for KSP it just works. And KSP outputs (after all the rounds) are available to the external plugin tasks using
ModuleSources
configurable with
includeGenerated
property enabled. See here.
❤️ 1
But let's assume that Amper doesn't support KSP out of the box. So, If you wanted to implement KSP as an Amper plugin right now, then you'd just have to declare a
ksp
task with something along the lines:
Copy code
@TaskAction fun doKsp(
  @Output kotlinOutputDir: Path,
  @Output javaOutputDir: Path,
  @Output resourcesOutputDir: Path,
  @Input userSources: ModuleSources,
  @Input userProcessors: Classpath,
  @Input kspRuntimeClasspath: Classpath,
  ...
) { ... }
And all the multi-round processing would happen inside this task and it would only produce single well-defined output directories per output type. Which is perfectly fine with Amper's public plugins API. Note: this would only work for JVM-only, because current plugins preview doesn't play well with KMP, but it may be implemented in the future.
👍 1
r
Do you generally prefer this layout? Could you please share why you would stick to it? That would be quite helpful for us to understand the users' expectations.
Hi @joffrey! I think I prefer keeping the same file layout for two main reasons. First, it makes it easier to test Amper and, if anything is missing, quickly switch back to Gradle or Maven without friction. Second, it helps avoid large commits or PRs that move many files around, which makes it harder to track changes in Git (and also affects
git blame
readability). Finally, I appreciate Amper’s philosophy of letting me focus on my application rather than the build tool itself — and that includes not having to reorganize my project structure just to satisfy the build system.
kodee loving 1
j
Ok thank you for sharing!