Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Compose Multiplatform 製アプリの OSS ライセンス表示

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
Avatar for Ryo WATANABE Ryo WATANABE
March 24, 2025
400

Compose Multiplatform 製アプリの OSS ライセンス表示

https://hey.connpass.com/event/347637/

アプリを公開・配布する場合、利用しているライブラリのライセンス情報をアプリ内で表示することが求められます。本発表では、Compose Multiplatform を用いて UI 層まで共通化した Android / iOS アプリにおいて、両プラットフォームで利用しているOSS ライセンス情報をどのように管理・表示するかを紹介します。

Avatar for Ryo WATANABE

Ryo WATANABE

March 24, 2025
Tweet

Transcript

  1. • 渡邊 亮 / naberyo (@error96num) • STORES 株式会社 •

    Android エンジニア • KMP / Compose Multiplatform すきです 2 自己紹介
  2. アプリを配布するためには、利用している OSS ライセンスの表記が欠かせない 3 • アプリで利用しているオープンソースライブラリの多くは、 ライセンス条項・著作権を人の目に触れる場所に表記することを義務づけている 例)Apache 2.0 /

    MIT / BSD … • Google / Apple の開発者向けドキュメントにも、 OSSライセンスの遵守義務について 明記されている ◦ Include open source notices | Google Play services | Google for Developers ◦ Apple Developer Program License Agreement - Agreements and Guidelines - Support - Apple Developer
  3. Compose Multiplatform でのライセンス表示は難しい 5 理由2: ライセンスを簡単に表示する UI 側の仕組みが確立されていない https://github.com/mono0926/LicensePlist 参考:

    ネイティブであれば仕組みが確立されている • Android: OSS Licenses Gradle Plugin, … • iOS: LicensePlist, LicenseList, … https://developers.google.com/android/gui des/opensource
  4. 利用したツール 8 Licensee: • KMP対応のGradle プラグイン • 依存ライブラリのライセンス情報 を解析・検証し、期待するライセンス以外が 含まれていたら

    ビルドを失敗 させる • JSON 形式のライセンス一覧 をレポートとして出力 LicensePlist: • iOS アプリ の依存ライブラリをスキャンし、ライセンス一覧を自動生成するコマンドラインツール • 実行すると、iOS の設定アプリに対応した形式 (plist形式) でライセンス一覧を出力
  5. Android - ライセンス情報の更新フロー( buildType: release の例) 10 afterEvaluate { tasks.named("preReleaseBuild").configure

    { // リリースビルド前の前処理をする Grdleタスク dependsOn("updateReleaseLicensee") // カスタムのGrdleタスク } } tasks.register("updateReleaseLicensee") { dependsOn("licenseeAndroidRelease") // Licenseeによるライセンス収集の Gradleタスク doLast { val srcFile = File("androidApp/build/reports/licensee/androidRelease/artifacts.json") val dstFile = File("core/data/src/commonMain/composeResources/files/licensee/android/artifacts.json") // JSONを読み込み、必要な加工(不要なフィールド削除・ライセンス本文追加など)を行う val json = processJson(srcFile.readText()) dstFile.writeText(json) } } fun processJson(raw: String): String {(省略)} androidApp/build.gradle.kt
  6. Android - ライセンス情報の更新フロー( buildType: release の例) 11 afterEvaluate { tasks.named("preReleaseBuild").configure

    { // リリースビルド前の前処理をする Grdleタスク dependsOn("updateReleaseLicensee") // カスタムのGrdleタスク } } tasks.register("updateReleaseLicensee") { dependsOn("licenseeAndroidRelease") // Licenseeによるライセンス収集の Gradleタスク doLast { val srcFile = File("androidApp/build/reports/licensee/androidRelease/artifacts.json") val dstFile = File("core/data/src/commonMain/composeResources/files/licensee/android/artifacts.json") // JSONを読み込み、必要な加工(不要なフィールド削除・ライセンス本文追加など)を行う val json = processJson(srcFile.readText()) dstFile.writeText(json) } } fun processJson(raw: String): String {(省略)} androidApp/build.gradle.kt
  7. Android - ライセンス情報の更新フロー( buildType: release の例) 12 afterEvaluate { tasks.named("preReleaseBuild").configure

    { // リリースビルド前の前処理をする Grdleタスク dependsOn("updateReleaseLicensee") // カスタムのGrdleタスク } } tasks.register("updateReleaseLicensee") { dependsOn("licenseeAndroidRelease") // Licenseeによるライセンス収集の Gradleタスク doLast { val srcFile = File("androidApp/build/reports/licensee/androidRelease/artifacts.json") val dstFile = File("core/data/src/commonMain/composeResources/files/licensee/android/artifacts.json") // JSONを読み込み、必要な加工(不要なフィールド削除・ライセンス本文追加など)を行う val json = processJson(srcFile.readText()) dstFile.writeText(json) } } fun processJson(raw: String): String {(省略)} androidApp/build.gradle.kt
  8. iOS - ライセンス情報の更新フロー( target: iosArm64 の例) 14 iosApp/iosApp.xcodeproj/project.pbxproj iosKtEntryPoint/build.gradle.kts Xcodeプロジェクト設定の

    Build Phasesにスクリプトを定義 KMP側のライセンス情報の収集や 複雑なファイル操作は Gradleタスクに任せる
  9. iOS - ライセンス情報の更新フロー( target: iosArm64 の例) 15 cd "${PROJECT_DIR}" mint

    run mono0926/LicensePlist /-output-path licenseplist /-prefix artifacts iosApp/iosApp.xcodeproj/project.pbxproj
  10. iOS - ライセンス情報の更新フロー( target: iosArm64 の例) 16 iosApp/iosApp.xcodeproj/project.pbxproj cd "${PROJECT_DIR}//./"

    ./gradlew :iosEntryPoint:convertLicensePlistToJson iosKtEntryPoint/build.gradle.kts tasks.register("convertLicensePlistToJson") { doLast { // iOSで生成された *.plist 一覧を取得 val srcDir = file("iosApp/licenseplist") val dstDir = file("core/data/src/commonMain/composeResources/files/license plist") // 既存の *.json を整理しつつ、plutil で .plist → .json 変換 srcDir.walkTopDown().filter { it.extension /= "plist" }.forEach { plistFile /> val jsonFile = dstDir.resolve(plistFile.name.replace(".plist", ".json")) exec { commandLine("plutil", "-convert", "json", "-o", jsonFile, plistFile) } } } }
  11. iOS - ライセンス情報の更新フロー( target: iosArm64 の例) 17 iosApp/iosApp.xcodeproj/project.pbxproj ./gradlew :iosEntryPoint:updateLicensee

    tasks.register("updateLicensee") { dependsOn("licenseeIosArm64") doLast { val srcFile = File("iosKtEntryPoint/build/reports/licensee/iosArm64/artifa cts.json") val dstFile = File("core/data/src/commonMain/composeResources/files/licens ee/ios/artifacts.json") // JSONを読み込み、必要な加工(不要なフィールド削除・ライセンス本文 追加など)を行う val json = processJson(srcFile.readText()) dstFile.writeText(json) } } fun processJson(raw: String): String {(省略)} iosKtEntryPoint/build.gradle.kts
  12. アプリ内でのライセンス情報データの扱い 19 // ひとつのライブラリを表現したモデル data class Artifact(val name: String, val

    licenses: List<License>) // ひとつのライセンスを表現したモデル sealed interface License { val name: String // ライセンスの全文を取得できる場合 data class Text(override val name: String, val text: String) : License // ライセンスの全文取得が難しく URLから参照する必要がある場合 data class Url(override val name: String, val url: String) : License }
  13. Compose UI でライセンス情報を表示 20 @Composable fun ArtifactListItem( artifact: Artifact, onClick:

    (License) /> Unit, ) { Column((省略)) { Text(artifact.name) artifact.licenses.forEach { license /> TextButton(onClick = { onClick(license) }) { Text(license.name) // MIT, Apache−2.0 など } } } } @Composable fun ArtifactList( artifacts: List<Artifact>, onClick: (License) /> Unit, ) { LazyColumn((省略)) { items(artifacts) { artifact /> ArtifactListItem(artifact, onClick) } } }
  14. まとめ 21 Compose Multiplatform 製のアプリで OSS ライセンス情報を 管理・表示する方法を紹介した • Android

    アプリのライセンス情報は、 Licensee で収集した • iOS アプリのライセンス情報は、 Swift 側で依存しているものを LicensePlist で、 KMP側で依存しているものを Licensee で収集した • 収集したライセンス情報は JSON ファイルとして Compose Resources に配置した • Compose UI で各プラットフォームのライセンス情報を表示した
  15. Appendix - デバッグビルド時のビルド時間短縮 22 afterEvaluate { tasks.named("preDebugBuild").configure { dependsOn("updateDebugLicensee") }

    tasks.named("preReleaseBuild").configure { dependsOn("updateReleaseLicensee") } } tasks.register("updateDebugLicensee") { (省略)// ダミーのJSONをCompose Resourcesに配置する } if [ "$CONFIGURATION" = "Debug" ]; then ./gradlew :iosKtEntryPoint:updateDebugLicensee else ./gradlew :iosKtEntryPoint:updateReleaseLicensee fi iosApp/iosApp.xcodeproj/project.pbxproj androidApp/build.gradle.kt iosKtEntryPoint/build.gradle.kts tasks.register("updateDebugLicensee") { (省略)// ダミーのJSONをCompose Resourcesに配置する } デバッグビルドではダミーのライセンス情報を参照することでビルド時間を短縮