1
1

KMPでKSPをやるって時の現状の制約

Posted at

前提

  • KSP2は未使用

ざっと調査した限りなので間違っている情報があればご指摘ください。

制約

  1. commonMain 内のシンボル処理によって生成するファイルは 各プラットフォームに置かれる = commonMain では使うことができない
  2. KotlinNativeを必要とするターゲットは,その 根っこのターゲットのソースセット空間でのみ コード生成のアウトプットができる

KMPでKSPを使う方法

Processor作成周り

  • Processorの作成は通常通り
class TestProcessor(
    private val codeGenerator: CodeGenerator,
    private val kspLogger: KSPLogger
) : SymbolProcessor {
    override fun process(resolver: Resolver): List<KSAnnotated> {
        resolver.getSymbolsWithAnnotation(SampleAnnotation::class.qualifiedName!!)
            ...
        return emptyList()
    }
}

class TestProcessorProvider : SymbolProcessorProvider {
    override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
        return TestProcessor(environment.codeGenerator, environment.logger)
    }
}
...

// /resources/META-INF/services
TestProcessorProvider
  • build.gradle.kts

ターゲットはjvmを設定する。あとは通常と同じ。

plugins {
    alias(libs.plugins.kotlinMultiplatform)
}

kotlin {
    jvm()
    sourceSets {
        jvmMain.dependencies {
            implementation(libs.ksp.poet)
            implementation(libs.ksp.poet.extension)
            implementation(libs.ksp.symbol.processing.api)
            implementation(projects.annotations)
        }
    }
}
...

これだとProcessorを使う側もマルチプラットフォームなので jvm 以外で使えないのでは? と疑問が出るが、Gradleのconfigurationを マルチプラットフォームの文脈外でやるので 問題ない様子 (後述)

  • ソースセット
    jvmターゲット のため、jvmMain(or alias)となる
├── build.gradle.kts
└── src
    └── jvmMain
        ├── kotlin
        │   └── TestProcessor.kt
        └── resources
            └── META-INF
                └── services
                    └── com.google.devtools.ksp.processing.SymbolProcessorProvider

Processorのconfiguration

Processorを受け入れる側 の設定は..


plugins {
    alias(libs.plugins.kotlinMultiplatform)
    alias(libs.plugins.androidLibrary)
    alias(libs.plugins.ksp)
}
kotlin {
... // ターゲットの設定等

 sourceSets {
        commonMain.dependencies {
            // put your Multiplatform dependencies here
            implementation(projects.annotations)
        }
        ... // その他 依存の追加
    }
}

// ここ
dependencies {
    add("kspCommonMainMetadata", projects.testProcessor)
    add("kspJvm", projects.testProcessor)
    add("kspAndroid", projects.testProcessor)
}

通常、ksp(...)でconfugurationを宣言するのとは違い、add(ksp<Target>) or add(ksp<SourceSet>)を使用する。
理由👇

The universal "ksp" configuration has performance issue and is deprecated on multiplatform since 1.0.1

これでビルド時にシンボル処理 & コード生成 をができるようになる

制約は何か

1. commonMain 内のシンボル処理によって生成するファイルは 各プラットフォームに置かれる = commonMain では使うことができない

例えば 上記のconfigurationで

annotation class SampleAnnotation を 付与したクラスから 同名のファイルを生成する といったサンプルコードを作ってやってみる

// commonMain
@SampleAnnotation
class Greeting {...

// platformMain
@SampleAnnotation
class AndroidPlatform : Platform { ...

// platformMain
@SampleAnnotation
class JVMPlatform : Platform {...

すると生成されるソースは..

  • build/generated/ksp/android/androidDebug/kotlin/AndroidPlatform.kt
  • build/generated/ksp/android/androidDebug/kotlin/Greeting.kt
  • build/generated/ksp/jvm/jvmMain/kotlin/AndroidPlatform.kt
  • build/generated/ksp/jvm/jvmMain/kotlin/Greeting.kt

CommonのMetadataなども作られるが、使う側からは見えないので割愛

commonのシンボル処理のoutput も各プラットフォームへ入る形になる。
CommonMainでは、Greetingが見えないので基本的にはプラットフォーム固有で使うことが前提

2. KotlinNativeを必要とするターゲットは,その 根っこのターゲットのソースセット空間でのみ コード生成のアウトプットができる

KMP Wizard で web desktop android ios を 作ると現状以下のような構成になっている

@OptIn(ExperimentalWasmDsl::class)
    wasmJs {
        browser {
            commonWebpackConfig {
                devServer = (devServer ?: KotlinWebpackConfig.DevServer()).apply {
                    static = (static ?: mutableListOf()).apply {
                        // Serve sources to debug inside browser
                        add(project.projectDir.path)
                    }
                }
            }
        }
    }
    
    androidTarget {
        @OptIn(ExperimentalKotlinGradlePluginApi::class)
        compilerOptions {
            jvmTarget.set(JvmTarget.JVM_11)
        }
    }
    
    jvm()
    
    listOf(
        iosX64(),
        iosArm64(),
        iosSimulatorArm64()
    ).forEach { iosTarget ->
        iosTarget.binaries.framework {
            baseName = "Shared"
            isStatic = true
        }
    }
    
    sourceSets {
        commonMain.dependencies {
            // put your Multiplatform dependencies here
        }
    }

processorのconfigurationを全部プリントしてみると

KSP Configuration: ksp
KSP Configuration: kspAndroid
KSP Configuration: kspAndroidAndroidTest
KSP Configuration: kspAndroidAndroidTestDebug
KSP Configuration: kspAndroidAndroidTestRelease
KSP Configuration: kspAndroidDebug
KSP Configuration: kspAndroidRelease
KSP Configuration: kspAndroidTest
KSP Configuration: kspAndroidTestDebug
KSP Configuration: kspAndroidTestFixtures
KSP Configuration: kspAndroidTestFixturesDebug
KSP Configuration: kspAndroidTestFixturesRelease
KSP Configuration: kspAndroidTestRelease
KSP Configuration: kspAppleMain
KSP Configuration: kspCommonMainKotlinMetadataProcessorClasspath
KSP Configuration: kspCommonMainMetadata
KSP Configuration: kspDebugKotlinAndroidProcessorClasspath
KSP Configuration: kspIosArm64
KSP Configuration: kspIosArm64Test
KSP Configuration: kspIosMain
KSP Configuration: kspIosSimulatorArm64
KSP Configuration: kspIosSimulatorArm64Test
KSP Configuration: kspIosX64
KSP Configuration: kspIosX64Test
KSP Configuration: kspJvm
KSP Configuration: kspJvmTest
KSP Configuration: kspKotlinIosArm64ProcessorClasspath
KSP Configuration: kspKotlinIosSimulatorArm64ProcessorClasspath
KSP Configuration: kspKotlinIosX64ProcessorClasspath
KSP Configuration: kspKotlinJvmProcessorClasspath
KSP Configuration: kspKotlinWasmJsProcessorClasspath
KSP Configuration: kspNativeMain
KSP Configuration: kspPluginClasspath
KSP Configuration: kspPluginClasspathNonEmbeddable
KSP Configuration: kspReleaseKotlinAndroidProcessorClasspath
KSP Configuration: kspWasmJs
KSP Configuration: kspWasmJsTest

なので、kspIosMainを入れれば良さそうだが、それではダメで..

Configuration with name 'kspIosMain' not found.

dependencies {
    add("kspCommonMainMetadata", projects.testProcessor)
    add("kspJvm", projects.testProcessor)
    add("kspAndroid", projects.testProcessor)
    add("kspWasmJs", projects.testProcessor)
    // ここ
    add("kspIosX64", projects.testProcessor)
    add("kspIosArm64", projects.testProcessor)
    add("kspIosSimulatorArm64", projects.testProcessor)
}

のように、内側にあるターゲットを指定する必要がある。

// iosMain
@SampleAnnotation
class IOSPlatform : Platform {...

とiOSMainのシンボルを処理した場合 生成されるコードのソースセットは以下になる。

├── iosArm64
│   └── iosArm64Main
│       └── kotlin
│           ├── Greeting.kt
│           └── IOSPlatform.kt
├── iosSimulatorArm64
│   └── iosSimulatorArm64Main
│       └── kotlin
│           ├── Greeting.kt
│           └── IOSPlatform.kt
├── iosX64
│   └── iosX64Main
│       └── kotlin
│           ├── Greeting.kt
│           └── IOSPlatform.kt

iOSMainからは 上記のターゲットは見えないので 、ios* のソースセットからしか使うことができない。

現状、Androidは その限りではない様子(AndroidもandroidNativeX64androidNativeArm64が根っこにある)なので、おそらくKotlin-Native の制約

コメント

今の仕組みだと、使えるけど使いづらそう という印象。
KSP2 だと、仕組みがだいぶ変わっているので こちらを待つ感じになるのだろうか。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1