Home About Contact
Kotlin , Kotlin Native , Gradle

kotlinx datetime を Gradle を使って実行ファイルを生成する

前回のエントリー「kotlinx datetime と Spring Boot , 2つの期間の重なりを判定」の追伸で、 Spring Boot ではなく、kotlinc で実行できないか試した。 結果、 import kotlinx.datetime.* は kotlinc で実行できなくて、 import java.time.* は kotlinc で実行できた。

でも、今頃気づいたのだが、kotlinc コマンドが JavaVM 用のものだったから、当然そのようになっただけで、 kotlin native 用の kotlinc-native コマンドを使えば、実行できるのではないのか?

そこで、 https://github.com/JetBrains/kotlin/releases/tag/v1.8.10 から kotlin-compiler-1.8.10.zip ではなく、kotlin-native-linux-x86_64-1.8.10.tar.gz を ダウンロードして使ってみた。 でも、 結局 kotlinc-native コマンドを使っても動かなかった。 バージョンが上がればそのうち動くようになるのかも知れない。

それで思い出したのだが、以前のエントリーで kotlin native は既に試していた。 それに沿って Gradle プロジェクトとしてこのコードをビルドしたところ、 そちらは難なく実行できたので、それを備忘録として書き残します。

kotlinc-native コマンドはうまくいかなかった

kotlinc-native でうまく実行できなかった datetime.main.kts (コードが悪いわけではない!):

// datetime.main.kts

@file:Repository("https://repo1.maven.org/maven2/")
@file:DependsOn("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")

import kotlinx.datetime.*

data class Period(val start: String, val stop: String)

fun isOverlapped(a: Period, b: Period): Boolean {

    val toLocalDateTime: (String)->LocalDateTime = {
        val v = LocalDate.parse(it)
        val h = 0
        val m = 0
        LocalDateTime(v.year, v.month, v.dayOfMonth, h, m)
    }

    // (t0 < t1) かを検査:
    val isBefore: (LocalDateTime, LocalDateTime)-> Boolean = { t0,t1->
        (t0.compareTo(t1) < 0)
    }

    val isNotOverlapped: (LocalDateTime, LocalDateTime, LocalDateTime, LocalDateTime)->Boolean = { aStart, aStop, bStart, bStop->
        //
        // 重なりがないケースを考える:
        //

        // case 0)
        // aStop が bStart より古い場合
        // ---------------------------
        // Period a: <--->
        // Period b:       <--->
        // ---------------------------
        val notOverlapped0 = isBefore(aStop, bStart)

        // case 1)
        // bStop が aStart より古い場合
        // ---------------------------
        // Period a:        <--->
        // Period b:  <--->
        // ---------------------------
        val notOverlapped1 = isBefore(bStop, aStart)

        // case 0 or 1 が true の場合
        (notOverlapped0 || notOverlapped1)
    }

    return !isNotOverlapped(
        toLocalDateTime(a.start),
        toLocalDateTime(a.stop),
        toLocalDateTime(b.start),
        toLocalDateTime(b.stop))
}


val test1: ()->Unit = {
    val a = Period("2022-01-01", "2022-12-31")
    val b = Period("2021-12-01", "2021-12-31")
    val result = isOverlapped(a,b)
    println("- test1 assert false : ${result}")
}

val test2: ()->Unit = {
    val a = Period("2022-01-01", "2022-12-31")
    val b = Period("2022-01-10", "2022-01-31")
    val result = isOverlapped(a,b)
    println("- test1 assert true : ${result}")
}


test1()
test2()

では実行してみます。

まず kotlinc-native コマンドのバージョン確認:

$ kotlinc-native -version
info: kotlinc-native 1.8.10 (JRE 11.0.17+8-post-Ubuntu-1ubuntu220.04)
Kotlin/Native: 1.8.10

実行:

/kotlinc-native -script datetime.main.kts
error: compilation failed: Front-end Internal error: Failed to analyze declaration Datetime_main
File being compiled: (6,26) in /path/to/datetime.main.kts
The root cause org.jetbrains.kotlin.resolve.lazy.NoDescriptorForDeclarationException was thrown at: org.jetbrains.kotlin.resolve.lazy.BasicAbsentDescriptorHandler.diagnoseDescriptorNotFound(AbsentDescriptorHandler.kt:18)

 * Source files: datetime.main.kts
  * Compiler version info: Konan: 1.8.10 / Kotlin: 1.8.10
   * Output kind: PROGRAM

exception: org.jetbrains.kotlin.util.KotlinFrontEndException: Front-end Internal error: Failed to analyze declaration Datetime_main
File being compiled: (6,26) in /path/to/datetime.main.kts
The root cause org.jetbrains.kotlin.resolve.lazy.NoDescriptorForDeclarationException was thrown at: org.jetbrains.kotlin.resolve.lazy.BasicAbsentDescriptorHandler.diagnoseDescriptorNotFound(AbsentDescriptorHandler.kt:18)

Front-end Internal error というエラーが出ます。長いので省略しています。 原因はわかりません。

Gradle プロジェクトにする

それでは、datetime.main.kts を Gradle プロジェクトにして native 実行できるファイルを生成します。 基本的に、このエントリー「Kotlin Native で テキストファイルを読み込む Hello, World!」の通りです。

$ mkdir mydatetime
$ cd mydatetime/
$ touch build.gradle
$ mkdir -p src/nativeMain/kotlin
$ touch src/nativeMain/kotlin/main.kt

build.gradle

plugins {
    id 'org.jetbrains.kotlin.multiplatform' version '1.8.10'
}

repositories {
    mavenCentral()
}

kotlin {
    linuxX64('native'){
        binaries {
            executable()
        }
    }
    sourceSets {
        commonMain {
            dependencies {
                implementation "org.jetbrains.kotlinx:kotlinx-datetime:0.4.0"
            }
        }
    }
}

src/nativeMain/kotlin/main.kt

import kotlinx.datetime.*

data class Period(val start: String, val stop: String)

fun isOverlapped(a: Period, b: Period): Boolean {

    val toLocalDateTime: (String)->LocalDateTime = {
        val v = LocalDate.parse(it)
        val h = 0
        val m = 0
        LocalDateTime(v.year, v.month, v.dayOfMonth, h, m)
    }

    // (t0 < t1) かを検査:
    val isBefore: (LocalDateTime, LocalDateTime)-> Boolean = { t0,t1->
        (t0.compareTo(t1) < 0)
    }

    val isNotOverlapped: (LocalDateTime, LocalDateTime, LocalDateTime, LocalDateTime)->Boolean = { aStart, aStop, bStart, bStop->
        //
        // 重なりがないケースを考える:
        //

        // case 0)
        // aStop が bStart より古い場合
        // ---------------------------
        // Period a: <--->
        // Period b:       <--->
        // ---------------------------
        val notOverlapped0 = isBefore(aStop, bStart)

        // case 1)
        // bStop が aStart より古い場合
        // ---------------------------
        // Period a:        <--->
        // Period b:  <--->
        // ---------------------------
        val notOverlapped1 = isBefore(bStop, aStart)

        // case 0 or 1 が true の場合
        (notOverlapped0 || notOverlapped1)
    }

    return !isNotOverlapped(
        toLocalDateTime(a.start),
        toLocalDateTime(a.stop),
        toLocalDateTime(b.start),
        toLocalDateTime(b.stop))
}

fun main() {
    val test1: ()->Unit = {
        val a = Period("2022-01-01", "2022-12-31")
        val b = Period("2021-12-01", "2021-12-31")
        val result = isOverlapped(a,b)
        println("- test1 assert false : ${result}")
    }
    
    val test2: ()->Unit = {
        val a = Period("2022-01-01", "2022-12-31")
        val b = Period("2022-01-10", "2022-01-31")
        val result = isOverlapped(a,b)
        println("- test1 assert true : ${result}")
    }
    
    
    test1()
    test2()
}

以前のコードと基本的には同じです。 ただし、トップレベルに val test1 のように変数を宣言できないようなので、 そこのみ fun main() 関数でくくりました。

あとはビルドして native 実行ファイルをつくるだけです。

Spring Boot方式 よりは負担が少ない!

まず環境の確認:

$ gradle -version

------------------------------------------------------------
Gradle 7.4
------------------------------------------------------------

Build time:   2022-02-08 09:58:38 UTC
Revision:     f0d9291c04b90b59445041eaa75b2ee744162586

Kotlin:       1.5.31
Groovy:       3.0.9
Ant:          Apache Ant(TM) version 1.10.11 compiled on July 10 2021
JVM:          11.0.17 (Ubuntu 11.0.17+8-post-Ubuntu-1ubuntu220.04)
OS:           Linux 5.4.0-137-generic amd64

続いてデバッグ実行:

$ gradle runDebugExecutableNative

> Task :runDebugExecutableNative
- test1 assert false : false
- test1 assert true : true

作動しています。

続いて native ファイルを生成:

$ gradle nativeBinaries

build/bin/native/releaseExecutable/mydatetime.kexe が生成されるので、これを実行します。

$ build/bin/native/releaseExecutable/mydatetime.kexe
- test1 assert false : false
- test1 assert true : true

こちらも問題なく作動しました。

まとめ

このような実験コードは kotlinc-native コマンドで開発できると楽なのですが、それはまだ?できないようです。 (ここで試した範囲では、という意味ですが) しかし、Gralde プロジェクトにすれば、実行できました。 これなら、Spring Boot を使うよりはだいぶ楽ですね。