Kotlinマルチプラットフォームライブラリの作り方

# 参考
https://kotlinlang.org/docs/multiplatform-library.html

https://kotlinlang.org/docs/multiplatform-connect-to-apis.html

https://getstream.io/blog/publishing-libraries-to-mavencentral-2021/#generating-a-gpg-key-pair


# はじめに
本記事ではKotlin/JVM,JS,Nativeで使用出来るマルチプラットフォームライブラリの作り方、及びその公開方法(ローカル、セントラル両方)について解説します。
公開方法はいくつか方法がありますが、今回はビルドはGradle、リポジトリはMavenを使用します。

また、自分はKotlinマルチプラットフォームのテンプレートをこの記事を通して作ります。この記事を追って行けば自分自身のKotlinマルチプラットフォームのテンプレートが作成出来ます。また、めんどくさい人は、[私のテンプレート](https://github.com/arashiyama11/kotlin-multiplatform-library-template)を変更しつつ使ってみてください。

# プロジェクトセットアップ
## 作成
IntelliJ Ideaをインストールしていない人はインストールしてください。
IntelliJ Ideaを開いたら左上のファイル→新規→プロジェクトとすると以下のポップアップが出てきます。
![Screenshot (92).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2605477/bad7a512-724a-c3bb-63a8-f4332c4e5e72.png)
    ジェネレータはKotlin マルチプラットフォーム、Project templateはLibraryを選択します。

また、アーティファクトコーディネートでグループ名を設定しておきます。
ローカルだけで良い場合はなんでもいいですが、セントラルのMavenリポジトリを作成するまでセントラルで有効かどうかはわかりません。現在、個人の場合は`io.github.id`が安牌です。私は`io.github.arashiyama11`をグループ名にしました。
現在は`com.github.id`は使用不可なので注意してください。

## Git連携
Mavenリポジトリを作成するときに必要になるのでGit連携をしておきます。
VHS→GitHubでプロジェクトを作成　から行えます。

# コード作成
### commonMain
まず、commonMain直下のkotlinディレクトリを左クリックし、ディレクトリをクリックします。新規ディレクトリ名にはグループ名をそのままぶち込んでください。
例えば`io.github.arashiyama11`と打ち込むとちゃんと`commonMain/kotlin/io/github/arashiyama11`というディレクトリが作成されます。

次にそのディレクトリに任意の名前のKotlinファイルを作成してください。
自分はTemp.ktを作成しました

現在はまだBetaですが便利なのでexpect,actualなるものを使っていきます。
expect,actualはプラットフォーム依存のクラス等を書く時に使います。
```kotlin:commonMain/kotlin/io/github/arashiyama11/Temp.kt
package io.github.arashiyama11

//プラットフォーム依存のクラス
expect open class Plat() {
  //プラットフォーム名を返す
  fun platform():String
}

//共通の関数
class Temp: Plat() {
  fun hello()="Hello!"
}
```
expectはcommonMainにて、actualはその他jvmMain,jsMain,nativeMain等で使います。

commonMain内でexpect宣言されたクラス、関数、あるいはオブジェクトは実装を持ちませんが、存在する全ての~~Mainディレクトリ内でactualを用いて実装しなければなりません。


### その他Main
その他Mainも同じようにディレクトリ、ファイルを作成します。
がIntelij Idea君がサボらせてくれるというのでお言葉に甘えましょう。
現在Platクラスはexpect修飾子の条件を満たしていないため赤線が引っ張られているはずです。そこにカーソルを合わせてAlt+Shift+Enterを押せば自動で適当にディレクトリを作成してくれます。一個づつしか作れないので必要なだけやりましょう。

確認すると各Main下にちゃんとコードが生成されていました。
```kotlin:jsMain/kotlin/io/github/arashiyama11/Plat
package io.github.arashiyama11

actual open class Plat actual constructor() {
  actual fun platform(): String {
    TODO("Not yet implemented")
  }
}
```
それを各プラットフォームごとに変更しましょう。
jsMainならちゃんとjsの`console.log`等が使用出来ます。


```kotlin:jsMain/kotlin/io/github/arashiyama11/Plat
package io.github.arashiyama11

actual open class Plat actual constructor() {
  actual fun platform() = "js"
}
```


## テストを書く

テストも同じようなものです。
### commonTest
まずcommonTestから書きます。
commonTest/kotlinを左クリックして、ファイルを作成から、`io.github.arashiyama11.Test`と入力して以下のようにします。

```kotlin:commonTest/kotlin/io/github/arashiyama11/Test.kt
package io.github.arashiyama11

import kotlin.test.Test
import kotlin.test.assertEquals

class Test {
  @Test
  fun testHello(){
    assertEquals("Hello!",Temp().hello())
  }
}
```

Testアノテーションが付けられたメソッドがテスト対象です。

個別に実行するなら左側にある緑色の三角、全て実行するならターミナルで以下を実行しましょう、
```
./gradlew check
```

テストが失敗し、例外が投げられると`BUILD FAILED`となり、全て成功すると`BUILD SUCCESSFUL`と表示されます。

### その他Test
jsが一番上にあるのでjsを例に取ります。
`jsTest/kotlin/io/github/arashiyama11/JSTest`を作成します。

```kotlin:jsTest/kotlin/io/github/arashiyama11/JSTest
package io.github.arashiyama11

import kotlin.test.Test
import kotlin.test.assertEquals

class JSTest {
  @Test
  fun testPlat(){
    assertEquals("js",Temp().platform())
  }
}
```
commonとやることは同じです。

# ローカルのMavenリポジトリに公開
build.gradle.ktsのpluginsに一行追加するだけです。
```kotlin:build.gradle.kts
plugins {
    kotlin("multiplatform") version "1.7.21"
    //下の一行を追加
    `maven-publish`
}

group = "io.github.arashiyama11"
version = "1.0.0"
```
ターミナルで以下を実行します。
```
./gradlew publishToMavenLocal
```

使用方法は使用したいプロジェクトの`build.gradle.kts`に以下の記述を足すだけです。
```kotlin:build.gradle.kts
repositories {
    mavenCentral()
    //ここを追加
    mavenLocal()
}

dependencies {
    //ここを追加
    implementation("io.github.arashiyama11:kotlin-multiplatform-library-template:1.0.0")
    testImplementation(kotlin("test"))
}

```

implementationの引数は`"${グループ名}:${プロジェクト名}:${バージョン}"`となっています。グループ名、バージョンは`build.gradle.kts`、プロジェクト名は`settings.gradle.kts`のが使用されます。

# セントラルのMavenリポジトリに公開
ネット上のMavenリポジトリに公開し、誰でも使えるようにしていきます。

## リポジトリ作成
Sonatype Jiraを用いて作成します。

https://issues.sonatype.org/secure/Signup!default.jspa

こちらにアクセスしてサインアップを行ってください。
進んで行くと以下のような画面になるはずです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2605477/4f795c0e-8ce6-07d3-e919-baca56acf59f.png)

真ん中の`Create an issue`を選択し、プロジェクトは`Community Support - Open`、課題タイプは`New Project`を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2605477/a4927c88-993b-41b0-9949-c36a95809b9b.png)

次の画面では課題の詳細を入力します。

私は次のように入力しました。
基本的にBotが対応するのであまり畏まる必要はありません。
- 要約:Create repository for io.github.arashiyama11:kotlin-multiplatform-library-template
- 説明:空欄
- Group Id:io.github.arashiyama11
- Project URL:https://github.com/arashiyama11/kotlin-multiplatform-library-template
- SCM url:https://github.com/arashiyama11/kotlin-multiplatform-library-template.git
- Username(s):arashiyama
- Already Synced to Central:No

課題を作成してから少し経つとBotからコメントが付きます。
私は以下のようなコメントが付きました。
グループ名が無効だったりするとここで直すように言われます。

> To continue the registration process, please follow these steps:
1.Create a temporary, public repository called https://github.com/arashiyama11/OSSRH-87327 to verify github account ownership.
2.Edit this ticket and set Status to Open.
If you do not own this github account, you must define a new groupId.



言われた通りに`OSSRH-87327`リポジトリを作成し上のほうにある`Respond`をクリックします。

10分ぐらい待つとお祝いがきました。(長いので最初の一部のみ)
> Congratulations! Welcome to the Central Repository!
io.github.arashiyama11 has been prepared, now user(s) arashiyama can:
Publish snapshot and release artifacts to s01.oss.sonatype.org

ちゃんとリポジトリが作成できたようですね。

## GPGキーの取得
Mavenセントラルにプロジェクトを公開するにはGPG署名する必要があります。まだGPGのツールをダウンロードしていない方は[gnupg.org](https://gnupg.org/download/#:~:text=GnuPG%20binary%20releases)からダウンロードしてください。

Mac,WinのものにはGUIツールがついていますが、後々ファイル出力する必要があるのでIntellij Idea上のコマンドラインで行います。

以下のコマンドで作成できます。
```
> gpg --full-gen-key
gpg (GnuPG) 2.3.8; Copyright (C) 2021 g10 Code GmbH
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

ご希望の鍵の種類を選択してください:
   (1) RSA と RSA
   (2) DSA と Elgamal
   (3) DSA (署名のみ)
   (4) RSA (署名のみ)
   (9) ECC (署名と暗号化) *デフォルト
  (10) ECC (署名のみ)
  (14) カードに存在する鍵
あなたの選択は? 1
RSA 鍵は 1024 から 4096 ビットの長さで可能です。
鍵長は? (3072) 4096
要求された鍵長は4096ビット
鍵の有効期限を指定してください。
         0 = 鍵は無期限
      <n>  = 鍵は n 日間で期限切れ
      <n>w = 鍵は n 週間で期限切れ
      <n>m = 鍵は n か月間で期限切れ
      <n>y = 鍵は n 年間で期限切れ
鍵の有効期間は? (0)0
鍵は無期限です
これで正しいですか? (y/N) y
                   
GnuPGはあなたの鍵を識別するためにユーザIDを構成する必要があります。

```
最初の質問にはRSAとRSAの1を選択
鍵長は4096
有効期限は任意です。
また、この後名前、メールアドレス、パスワード等が聞かれるので適宜入力してください。
完了すると最後に公開鍵が出力されて終了します。

## GPG公開鍵のアップロード
例のため`7A5D73CFEDDDBC915986998A36271B955BEF072A`という公開鍵が出来たとします。

公開鍵の最後の8文字、この場合は`5BEF072A`をサーバーにアップロードします。
コマンドは以下の通りです。
```
gpg --keyserver keyserver.ubuntu.com --send-keys 5BEF072A
```

次に秘密鍵の出力をします。

```
gpg --export-secret-keys --output private-key 5BEF072A
```
これでprivate-keyフォルダに秘密鍵が出力されます

## 公開ロジックの作成

公開ロジックは長いので別ディレクトリに作成します。
`convention-plugins/build.gradle.kts`を作成して以下の内容を書き込みます。
```kotlin:convention-plugins/build.gradle.kts
plugins {
  `kotlin-dsl`
}

repositories {
  gradlePluginPortal()
}
```

また、`convention-plugins/src/main/kotlin/convention.publication.gradle.kts`を作成し、以下を適切に変更して書き込みます。

```kotlin:convention-plugins/src/main/kotlin/convention.publication.gradle.kts
import org.gradle.api.publish.maven.MavenPublication
import org.gradle.api.tasks.bundling.Jar
import org.gradle.kotlin.dsl.`maven-publish`
import org.gradle.kotlin.dsl.signing
import java.util.*

plugins {
  `maven-publish`
  signing
}

ext["signing.keyId"] = null
ext["signing.password"] = null
ext["signing.secretKeyRingFile"] = null
ext["ossrhUsername"] = null
ext["ossrhPassword"] = null

val secretPropsFile = project.rootProject.file("local.properties")
if (secretPropsFile.exists()) {
  secretPropsFile.reader().use {
    Properties().apply {
      load(it)
    }
  }.onEach { (name, value) ->
    ext[name.toString()] = value
  }
} else {
  ext["signing.keyId"] = System.getenv("SIGNING_KEY_ID")
  ext["signing.password"] = System.getenv("SIGNING_PASSWORD")
  ext["signing.secretKeyRingFile"] = System.getenv("SIGNING_SECRET_KEY_RING_FILE")
  ext["ossrhUsername"] = System.getenv("OSSRH_USERNAME")
  ext["ossrhPassword"] = System.getenv("OSSRH_PASSWORD")
}

val javadocJar by tasks.registering(Jar::class) {
  archiveClassifier.set("javadoc")
}

fun getExtraString(name: String) = ext[name]?.toString()

publishing {
  repositories {
    maven {
      name = "sonatype"
      setUrl("https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/")
      credentials {
        username = getExtraString("ossrhUsername")
        password = getExtraString("ossrhPassword")
      }
    }
  }

  publications.withType<MavenPublication> {
    artifact(javadocJar.get())

    pom {
      //下の3行変更
      name.set("kotlin-multiplatform-library-template")
      description.set("kotlin-multiplatform-library-template")
      url.set("https://github.com/arashiyama11/kotlin-multiplatform-library-template")

      licenses {
        license {
          name.set("MIT")
          url.set("https://opensource.org/licenses/MIT")
        }
      }
      developers {
        developer {
          //下2行変更
          id.set("arashiyama11")
          name.set("arashiyama")
        }
      }
      scm {
        //下1行変更
        url.set("https://github.com/arashiyama11/kotlin-multiplatform-library-template")
      }
    }
  }
}


signing {
  sign(publishing.publications)
}
```

`settings.gradle.kts`に一行書き足します。
```kotlin:settings.gradle.kts
rootProject.name = "kotlin-multiplatform-library-template"
//下一行を追加
includeBuild("convention-plugins")
```

`build.gradle.kts`の`maven-publish`を`id("convention.publication")`に変えます。
```kotlin
plugins {
    kotlin("multiplatform") version "1.7.21"
    id("convention.publication")
}
```

また、`local.properties`ファイルを作成し、以下を書き込みます。
```:local.properties
# The GPG key pair ID (last 8 digits of its fingerprint)
signing.keyId=...
# The passphrase of the key pair
signing.password=...
# Private key you exported earlier
signing.secretKeyRingFile=...
# Your credentials for the Jira account
ossrhUsername=...
ossrhPassword=...
```

signing.keyIdはキーサーバーにアップロードしたものと同じ、公開鍵の最後の8文字です。(digitsと書いてあるのはそれを16進数の数字と捉えているからだと思います。)今回の場合は`5BEF072A`です。

signing.passwordはGPG鍵作成の時に聞かれたパスワードです。

signing.secretKeyRingFileは先程エクスポートしたファイルです。特に変えてなければ`private-key`です

ossrhUsername,ossrhPasswordはJiraアカウントのユーザー名とパスワードです。


## 公開
やっと公開作業です

```
./gradlew publishAllPublicationsToSonatypeRepository
```

を実行してください。

成功したら以下にアクセスしてください。

https://s01.oss.sonatype.org/

ログインしたら左側の`Staging Repositories`を選択すると真ん中に自分が作成したリポジトリがあるはずです。

それを選択して上にあるCloseボタンを押して一旦クローズしてください。
Closeが完了するとReleaseボタンが押せるようになるので、押してリリースします。

最後に先程のJiraの課題に戻り、`The first version has been released`と報告し
> Central sync is activated for io.github.arashiyama11. After you successfully release, your component will be available to the public on Central https://repo1.maven.org/maven2/, typically within 30 minutes, though updates to https://search.maven.org can take up to four hours. 

みたいなのが帰ってくればもう完了です。あとは反映されるまで待つだけです。

反映されれば、`mavenLocal()`なしでもライブラリが使用出来るはずです。
```kotlin:build.gradle.kts
repositories {
    mavenCentral()
}

dependencies {
    //ここを追加
    implementation("io.github.arashiyama11:kotlin-multiplatform-library-template:1.0.0")
    testImplementation(kotlin("test"))
}

```

# 終わりに
バージョンアップする時は`build.gradle`のバージョンを上げてから公開作業をもう一度行ってください。

また、2回目以降のライブラリ作成で、グループ名を変えない場合はSonatype Jiraの課題制作やGPGの鍵出力は必要ありません。2回目以降で変える(変わる)のは`settings.gradle.kts`の`rootProject.name`や`convention.publication.gradle.kts`のURL類ぐらいです。(コードは言うまでもなく)