Ccmmutty logo
Commutty IT
15 min read

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

https://cdn.magicode.io/media/notebox/blob_qTkHRke

参考

はじめに

本記事ではKotlin/JVM,JS,Nativeで使用出来るマルチプラットフォームライブラリの作り方、及びその公開方法(ローカル、セントラル両方)について解説します。 公開方法はいくつか方法がありますが、今回はビルドはGradle、リポジトリはMavenを使用します。
また、自分はKotlinマルチプラットフォームのテンプレートをこの記事を通して作ります。この記事を追って行けば自分自身のKotlinマルチプラットフォームのテンプレートが作成出来ます。また、めんどくさい人は、私のテンプレートを変更しつつ使ってみてください。

プロジェクトセットアップ

作成

IntelliJ Ideaをインストールしていない人はインストールしてください。 IntelliJ Ideaを開いたら左上のファイル→新規→プロジェクトとすると以下のポップアップが出てきます。 Screenshot (92).png ジェネレータはKotlin マルチプラットフォーム、Project templateはLibraryを選択します。
また、アーティファクトコーディネートでグループ名を設定しておきます。 ローカルだけで良い場合はなんでもいいですが、セントラルのMavenリポジトリを作成するまでセントラルで有効かどうかはわかりません。現在、個人の場合はio.github.nameが安牌です。私はio.github.arashiyama11をグループ名にしました。 現在はcom.github.nameは使用不可なので注意してください。

Git連携

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

コード作成

commonMain

まず、commonMain直下のkotlinディレクトリを左クリックし、ディレクトリをクリックします。新規ディレクトリ名にはグループ名をそのままぶち込んでください。 例えばio.github.arashiyama11と打ち込むとちゃんとcommonMain/kotlin/io/github/arashiyama11というディレクトリが作成されます。
次にそのディレクトリに任意の名前のKotlinファイルを作成してください。 自分はTemp.ktを作成しました
現在はまだBetaですが便利なのでexpect,actualなるものを使っていきます。 expect,actualはプラットフォーム依存のクラス等を書く時に使います。
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下にちゃんとコードが生成されていました。
package io.github.arashiyama11

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

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

テストを書く

テストも同じようなものです。

commonTest

まずcommonTestから書きます。 commonTest/kotlinを左クリックして、ファイルを作成から、io.github.arashiyama11.Testと入力して以下のようにします。
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を作成します。
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のplaginsに一行追加するだけです。
plugins {
    kotlin("multiplatform") version "1.7.21"
    //下の一行を追加
    `maven-publish`
}

group = "io.github.arashiyama11"
version = "1.0.0"
ターミナルで以下を実行します。
./gradlew publishToMavenLocal
使用方法は使用したいプロジェクトの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を用いて作成します。
こちらにアクセスしてサインアップを行ってください。 進んで行くと以下のような画面になるはずです。 image.png
真ん中のCreate an issueを選択し、プロジェクトはCommunity Support - Open、課題タイプはNew Projectを選択します。
image.png
次の画面では課題の詳細を入力します。
私は次のように入力しました。 基本的にBotが対応するのであまり畏まる必要はありません。
課題を作成してから少し経つと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からダウンロードしてください。
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を作成して以下の内容を書き込みます。
plugins {
  `kotlin-dsl`
}

repositories {
  gradlePluginPortal()
}
また、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に一行書き足します。
rootProject.name = "kotlin-multiplatform-library-template"
//下一行を追加
includeBuild("convention-plugins")
build.gradle.ktsmaven-publishid("convention.publication")に変えます。
plugins {
    kotlin("multiplatform") version "1.7.21"
    id("convention.publication")
}
また、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
を実行してください。
成功したら以下にアクセスしてください。
ログインしたら左側の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()なしでもライブラリが使用出来るはずです。
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.ktsrootProject.nameconvention.publication.gradle.ktsのURL類ぐらいです。(コードは言うまでもなく)

Discussion

コメントにはログインが必要です。