Skip to content
Draft
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

### Changed

- Upgrade Kotlin to `2.4.20-Beta2` and generate App Platform's Metro binding containers in IR, retaining FIR only for cross-module contribution hints.
- Upgrade the blueprint projects to App Platform `0.1.1`.
- Upgrade Metro to `1.4.0`.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package software.ralf.app.platform.gradle.buildsrc;

import java.util.List;
import kotlin.Unit;
import org.gradle.api.Project;
import org.jetbrains.kotlin.gradle.plugin.CompilerPluginConfig;
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle;
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycleKt;
import org.jetbrains.kotlin.gradle.plugin.SubpluginOption;
import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile;
import org.jetbrains.kotlin.gradle.tasks.CompilerPluginOptions;
import org.jetbrains.kotlin.gradle.tasks.KotlinNativeCompile;

final class MetroCompilerOptions {
private static final String METRO_COMPILER_PLUGIN_ID = "dev.zacsweers.metro.compiler";
private static final String GENERATE_CONTRIBUTION_HINTS_IN_FIR =
"generate-contribution-hints-in-fir";

private MetroCompilerOptions() {}

/**
* Keeps Metro's package-discovery hints in FIR while all generated classes remain in IR.
*
* <p>Metro 1.4.0 couples these two settings in its Gradle plugin even though Kotlin cannot
* discover package-level callables generated in IR from a downstream KLIB compilation. This
* replaces only the hint option after Metro has configured each task, leaving
* {@code generate-classes-in-ir} enabled.
*/
@SuppressWarnings({"rawtypes", "unchecked"})
static void enable(Project project) {
KotlinPluginLifecycleKt.launchInStage(
project,
KotlinPluginLifecycle.Stage.AfterFinaliseCompilations,
(lifecycle, continuation) -> {
project
.getTasks()
.withType(AbstractKotlinCompile.class)
.configureEach(MetroCompilerOptions::replaceMetroFirHints);
project
.getTasks()
.withType(KotlinNativeCompile.class)
.configureEach(MetroCompilerOptions::replaceMetroFirHints);
return Unit.INSTANCE;
});
}

private static void replaceMetroFirHints(AbstractKotlinCompile<?> task) {
List<CompilerPluginOptions> updatedOptions =
task.getPluginOptions().get().stream()
.map(MetroCompilerOptions::withMetroFirHints)
.toList();
task.getPluginOptions().set(updatedOptions);
}

private static void replaceMetroFirHints(KotlinNativeCompile task) {
CompilerPluginOptions updatedOptions = withMetroFirHints(task.getCompilerPluginOptions());
task.getCompilerPluginOptions().allOptions().clear();
updatedOptions
.allOptions()
.forEach(
(pluginId, options) ->
options.forEach(
option ->
task.getCompilerPluginOptions().addPluginArgument(pluginId, option)));
}

private static CompilerPluginOptions withMetroFirHints(CompilerPluginConfig pluginOptions) {
CompilerPluginOptions updatedOptions = new CompilerPluginOptions();
pluginOptions
.allOptions()
.forEach(
(pluginId, options) ->
options.forEach(
option ->
updatedOptions.addPluginArgument(
pluginId, withMetroFirHints(pluginId, option))));
return updatedOptions;
}

private static SubpluginOption withMetroFirHints(String pluginId, SubpluginOption option) {
if (METRO_COMPILER_PLUGIN_ID.equals(pluginId)
&& GENERATE_CONTRIBUTION_HINTS_IN_FIR.equals(option.getKey())) {
return new SubpluginOption(option.getKey(), "true");
}
return option;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import org.jetbrains.compose.ComposeExtension
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.NATIVE_COMPILER_PLUGIN_CLASSPATH_CONFIGURATION_NAME
import org.jetbrains.kotlin.gradle.plugin.PLUGIN_CLASSPATH_CONFIGURATION_NAME
import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask
import software.ralf.app.platform.gradle.buildsrc.AppPlatformExtension.Companion.appPlatformBuildSrc
import software.ralf.app.platform.gradle.buildsrc.Platform.Companion.allPlatforms

Expand Down Expand Up @@ -264,8 +265,6 @@ public open class KmpPlugin : Plugin<Project> {
}

fun Project.enableMetro() {
plugins.apply(Plugins.METRO)

val useMetroKsp =
providers
.gradleProperty("app.platform.metro.ksp")
Expand All @@ -274,9 +273,13 @@ public open class KmpPlugin : Plugin<Project> {
.get()

if (useMetroKsp) {
plugins.apply(Plugins.METRO)
enableMetroKsp()
} else {
// Add App Platform's declaration generator before Metro's IR graph transformer.
enableMetroCompilerPlugin()
plugins.apply(Plugins.METRO)
MetroCompilerOptions.enable(this)
}
}

Expand Down Expand Up @@ -309,6 +312,12 @@ public open class KmpPlugin : Plugin<Project> {
}

private fun Project.enableMetroCompilerPlugin() {
tasks.withType(KotlinCompilationTask::class.java).configureEach { task ->
task.compilerOptions.freeCompilerArgs.add(
"-Xcompiler-plugin-order=software.ralf.app.platform.metro.compiler>dev.zacsweers.metro.compiler"
)
}

if (isKmpModule) {
kmpExtension.sourceSets.getByName("commonMain").dependencies {
implementation(project(":di-common:public"))
Expand Down
4 changes: 4 additions & 0 deletions gradle-plugin/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ tasks.withType(ValidatePlugins).configureEach {
it.enableStricterValidation = true
}

tasks.named('javadoc', Javadoc).configure {
exclude '**/MetroCompilerOptions.java'
}

tasks.withType(dev.detekt.gradle.Detekt).configureEach {
it.jvmTarget.set(libs.versions.jvm.gradle.plugin.get())
it.setSource(layout.files("src"))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package software.ralf.app.platform.gradle;

import java.util.List;
import kotlin.Unit;
import org.gradle.api.Project;
import org.jetbrains.kotlin.gradle.plugin.CompilerPluginConfig;
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycle;
import org.jetbrains.kotlin.gradle.plugin.KotlinPluginLifecycleKt;
import org.jetbrains.kotlin.gradle.plugin.SubpluginOption;
import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile;
import org.jetbrains.kotlin.gradle.tasks.CompilerPluginOptions;
import org.jetbrains.kotlin.gradle.tasks.KotlinNativeCompile;

final class MetroCompilerOptions {
private static final String METRO_COMPILER_PLUGIN_ID = "dev.zacsweers.metro.compiler";
private static final String GENERATE_CONTRIBUTION_HINTS_IN_FIR =
"generate-contribution-hints-in-fir";

private MetroCompilerOptions() {}

/**
* Keeps Metro's package-discovery hints in FIR while all generated classes remain in IR.
*
* <p>Metro 1.4.0 couples these two settings in its Gradle plugin even though Kotlin cannot
* discover package-level callables generated in IR from a downstream KLIB compilation. This
* replaces only the hint option after Metro has configured each task, leaving
* {@code generate-classes-in-ir} enabled.
*/
@SuppressWarnings({"rawtypes", "unchecked"})
static void enable(Project project) {
KotlinPluginLifecycleKt.launchInStage(
project,
KotlinPluginLifecycle.Stage.AfterFinaliseCompilations,
(lifecycle, continuation) -> {
project
.getTasks()
.withType(AbstractKotlinCompile.class)
.configureEach(MetroCompilerOptions::replaceMetroFirHints);
project
.getTasks()
.withType(KotlinNativeCompile.class)
.configureEach(MetroCompilerOptions::replaceMetroFirHints);
return Unit.INSTANCE;
});
}

private static void replaceMetroFirHints(AbstractKotlinCompile<?> task) {
List<CompilerPluginOptions> updatedOptions =
task.getPluginOptions().get().stream()
.map(MetroCompilerOptions::withMetroFirHints)
.toList();
task.getPluginOptions().set(updatedOptions);
}

private static void replaceMetroFirHints(KotlinNativeCompile task) {
CompilerPluginOptions updatedOptions = withMetroFirHints(task.getCompilerPluginOptions());
task.getCompilerPluginOptions().allOptions().clear();
updatedOptions
.allOptions()
.forEach(
(pluginId, options) ->
options.forEach(
option ->
task.getCompilerPluginOptions().addPluginArgument(pluginId, option)));
}

private static CompilerPluginOptions withMetroFirHints(CompilerPluginConfig pluginOptions) {
CompilerPluginOptions updatedOptions = new CompilerPluginOptions();
pluginOptions
.allOptions()
.forEach(
(pluginId, options) ->
options.forEach(
option ->
updatedOptions.addPluginArgument(
pluginId, withMetroFirHints(pluginId, option))));
return updatedOptions;
}

private static SubpluginOption withMetroFirHints(String pluginId, SubpluginOption option) {
if (METRO_COMPILER_PLUGIN_ID.equals(pluginId)
&& GENERATE_CONTRIBUTION_HINTS_IN_FIR.equals(option.getKey())) {
return new SubpluginOption(option.getKey(), "true");
}
return option;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
import org.jetbrains.kotlin.gradle.plugin.NATIVE_COMPILER_PLUGIN_CLASSPATH_CONFIGURATION_NAME
import org.jetbrains.kotlin.gradle.plugin.PLUGIN_CLASSPATH_CONFIGURATION_NAME
import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask
import software.ralf.app.platform.gradle.ModuleStructurePlugin.Companion.testingSourceSets

/**
Expand Down Expand Up @@ -280,15 +281,17 @@ private fun Project.enableKotlinInject() {
}

private fun Project.enableMetro() {
plugins.apply(PluginIds.METRO)

val useMetroKsp =
providers.gradleProperty("app.platform.metro.ksp").map(String::toBoolean).orElse(false).get()

if (useMetroKsp) {
plugins.apply(PluginIds.METRO)
enableMetroKsp()
} else {
// Add App Platform's declaration generator before Metro's IR graph transformer.
enableMetroCompilerPlugin()
plugins.apply(PluginIds.METRO)
MetroCompilerOptions.enable(this)
}
}

Expand Down Expand Up @@ -323,6 +326,12 @@ private fun Project.enableMetroKsp() {
}

private fun Project.enableMetroCompilerPlugin() {
tasks.withType(KotlinCompilationTask::class.java).configureEach { task ->
task.compilerOptions.freeCompilerArgs.add(
"-Xcompiler-plugin-order=software.ralf.app.platform.metro.compiler>dev.zacsweers.metro.compiler"
)
}

val compilerPluginDependency =
"$APP_PLATFORM_GROUP:metro-contribute-impl-compiler-plugin:$APP_PLATFORM_VERSION"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ class AppPlatformPluginDependencyTest {
val project = createProject(name = "impl")
project.plugins.apply(PluginIds.KOTLIN_MULTIPLATFORM)
project.plugins.apply(AppPlatformPlugin::class.java)

project.appPlatform.enableMetro(true)
project.appPlatform.addImplModuleDependencies(true)

Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jvm-compatibility = "11"
# with 25.
jvm-buildsrc = "25"
jvm-gradle-plugin = "17"
kotlin = "2.4.10"
kotlin = "2.4.20-Beta2"
kotlin-atomicfu = "0.33.0"
kotlin-compile-testing = "0.13.0"
kotlin-hierarchy = "1.1"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package software.ralf.app.platform.metro.compiler

import com.google.auto.service.AutoService
import dev.zacsweers.metro.compiler.MetroOptions
import dev.zacsweers.metro.compiler.api.fir.MetroContributionHintExtension
import dev.zacsweers.metro.compiler.api.fir.MetroContributionHintExtension.ContributionHint
import dev.zacsweers.metro.compiler.compat.CompatContext
import org.jetbrains.kotlin.fir.FirSession
import org.jetbrains.kotlin.fir.extensions.FirDeclarationPredicateRegistrar
import org.jetbrains.kotlin.fir.extensions.predicateBasedProvider
import org.jetbrains.kotlin.fir.symbols.impl.FirRegularClassSymbol
import software.ralf.app.platform.metro.compiler.fir.extractScopeClassId
import software.ralf.app.platform.metro.compiler.renderer.ContributesRendererIds
import software.ralf.app.platform.metro.compiler.renderer.rendererContributionMetadata
import software.ralf.app.platform.metro.compiler.robot.ContributesRobotIds
import software.ralf.app.platform.metro.compiler.scoped.ContributesScopedIds
import software.ralf.app.platform.metro.compiler.scoped.contributesScopedMetadata

/** Supplies source-class hints for binding containers that App Platform generates in IR. */
internal class AppPlatformContributionHintExtension(private val session: FirSession) :
MetroContributionHintExtension {

override fun FirDeclarationPredicateRegistrar.registerPredicates() {
register(ContributesRendererIds.PREDICATE)
register(ContributesRobotIds.PREDICATE)
register(ContributesScopedIds.PREDICATE)
register(ContributesScopedIds.SINGLE_IN_PREDICATE)
}

override fun getContributionHints(): List<ContributionHint> {
return buildList {
annotatedClasses(ContributesRendererIds.PREDICATE).forEach { sourceClass ->
if (rendererContributionMetadata(sourceClass, session) != null) {
add(ContributionHint(sourceClass.classId, ClassIds.RENDERER_SCOPE))
}
}
annotatedClasses(ContributesRobotIds.PREDICATE).forEach { sourceClass ->
extractScopeClassId(
sourceClass,
ContributesRobotIds.CONTRIBUTES_ROBOT_CLASS_ID,
session,
)
?.let { scope -> add(ContributionHint(sourceClass.classId, scope)) }
}
annotatedClasses(ContributesScopedIds.PREDICATE).forEach { sourceClass ->
if (contributesScopedMetadata(sourceClass, session) == null) return@forEach
extractScopeClassId(
sourceClass,
ContributesScopedIds.CONTRIBUTES_SCOPED_CLASS_ID,
session,
)
?.let { scope -> add(ContributionHint(sourceClass.classId, scope)) }
}
}
.distinct()
}

private fun annotatedClasses(
predicate: org.jetbrains.kotlin.fir.extensions.predicate.LookupPredicate
): List<FirRegularClassSymbol> {
return session.predicateBasedProvider
.getSymbolsByPredicate(predicate)
.filterIsInstance<FirRegularClassSymbol>()
}
}

@AutoService(MetroContributionHintExtension.Factory::class)
public class AppPlatformContributionHintExtensionFactory : MetroContributionHintExtension.Factory {
override fun create(
session: FirSession,
options: MetroOptions,
compatContext: CompatContext,
): MetroContributionHintExtension? {
if (!options.generateClassesInIr) return null
return AppPlatformContributionHintExtension(session)
}
}
Loading
Loading