OSSに依存するプロジェクトでは依存しているライブラリのライセンス表記が必要になることがある。(体感的にはほとんどのライブラリはライセンス表記を求めているように思う)

そのため、依存ライブラリが増えた場合にはライセンス表記を更新する必要があるが、依存ライブラリの依存ライブラリに対しても再帰的にライセンス表記を見に行って自リポジトリの依存ライセンス表記ファイルを更新して…という作業を行うのは現実的ではない。

他にもうっかりコピーレフトなライブラリに依存してしまうとプロジェクトのライセンスを変更しなければならなかったりと案外注意すべきポイントが多い。

cargoプロジェクトにおいてこれらの労力を削減してくれるツールがいくつか存在している。

筆者は趣味プロジェクトとしてfzf-makeというmake targetやpnpm script, yarn script, just recipeをfuzzy finder形式で選択、実行できるCLIツール1をRustで開発しており、このプロジェクトで最近それらのツールを導入したので使い方とともに紹介する。

tl;dr#

以下のチェックをCIで実行することでcargoプロジェクトの依存関係を継続的に省力で管理できる。

  • cargo-machete : 不要な依存関係がないか
  • cargo-deny : 依存ライブラリに許容しないライセンスが含まれていないか
  • cargo-about : リポジトリのライセンス表記ファイルに更新漏れがないか

それぞれ紹介する。なお、コード例はそれぞれ以下のランナーを想定している。

環境 ランナー
ローカル make
CI GitHub Actions

cargo-macheteで不要な依存関係を検出する#

why#

ある程度の期間開発を続けていると以前追加した依存関係が不要になったことに気付かずにcargo.tomldependenciesdev-dependenciesに残ったままになることがある。 不要な依存関係が残ったままになっているとrenovateやdependabotによるバージョン更新の手間が不必要に増えてしまうのでPRの時点で気付けるようにしたい。

what#

cargo-machete を使ってcargo.tomlに記述されている依存関係のうち、プロジェクトが実際には依存していない依存関係を検出する。

how(ローカル)#

以下のようなmake targetを定義しておくとmake detect-unused-dependenciesで不要な依存関係を検出できる。

.PHONY: tool-detect-unused-dependencies
tool-detect-unused-dependencies:
	@if ! which cargo-machete > /dev/null; then \
		cargo install --locked cargo-machete; \
	fi

.PHONY: detect-unused-dependencies
detect-unused-dependencies: tool-detect-unused-dependencies
	cargo machete

以下は不必要な依存関係としてtempfileが検出されている様子。

cargo-machete

how(CI)#

以下のようなyamlを定義しておくと不要な依存関係がある場合にCIが落ちてくれる。

name: Detect unused dependencies
on:
  push:
    branches: [ "main" ]
  pull_request:

jobs:
  detect-unused-dependencies:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Machete
        uses: bnjbvr/cargo-machete@v0.8.0

cargo-denyで依存ライブラリに許容しないライセンスが含まれていないか検証する#

why#

筆者はfzf-makeをより自由に利用してほしいと考えているためMITライセンスでプロジェクトを公開したい。 コピーレフトのライセンスに依存してしまうとライセンス変更を余儀なくされるため、コピーレフトのライブラリに依存することを避けたい。

what#

cargo-deny は依存関係のlint機能を提供する。fzf-makeでは、cargo deny check licensesを用いてホワイトリストに登録したライセンス以外のライセンスが依存ライブラリに含まれていないか検証している。

# deny.toml
[licenses]
allow = [
    "MIT",
    "ISC",
    "Apache-2.0",
    "MPL-2.0",
    "BSD-2-Clause",
    "BSD-3-Clause",
    "Unicode-DFS-2016",
    "OpenSSL",
]
confidence-threshold = 0.8
exceptions = []

[[licenses.clarify]]
name = "ring"
expression = "MIT AND ISC AND OpenSSL"
license-files = [
    { path = "LICENSE", hash = 0xbd0eed23 }
]

how(ローカル)#

以下のようなmake targetを定義しておくとmake check-licensesで依存ライブラリに許容しないライセンスが含まれていないかを検証できる。

.PHONY: tool-check-licenses
tool-check-licenses:
	@if ! which cargo-deny > /dev/null; then \
		cargo install --locked cargo-deny; \
	fi

.PHONY: check-licenses
check-licenses: tool-check-licenses
	cargo deny check licenses

許可していないライセンスのライブラリに依存していることがわかると以下のようなエラーが出力される。

cargo-deny.webp

how(CI)#

以下のようなyamlを定義しておくと不要な依存関係があるとCIが落ちてくれる。

name: Check licenses
on:
  push:
    branches: [ "main" ]
  pull_request:

jobs:
  check-licenses:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: EmbarkStudios/cargo-deny-action@v2
      with:
        command: check licenses

cargo-aboutでリポジトリのライセンス表記が最新か検証する#

why#

冒頭でも触れた通り、多くのOSSでは依存ライブラリのライセンス表記を求めている。

fzf-makeではCREDITS.htmlというファイルを用意しておりそこに依存ライブラリのライセンス表記を記載している。

これまでは依存ライブラリを追加/削除した際に手作業でライセンス表記ファイルを更新していたが、漏れがちになっていた。

また、本来は依存ライブラリの依存ライブラリを再帰的にチェックしてすべてのライセンス表記をすべきなのだと思うが、手作業でそれをする気にはなれなかったので直接依存しているライブラリのライセンスしか表記できていなかった。

これらの課題をcargo-aboutを用いることで解決できそうだったので導入してみた。

what#

cargo-about は依存ライブラリのライセンス表記ファイルを自動生成する機能を提供する。

以下のようなテンプレートを用意し、cargo about generate about.hbs > CREDITS.htmlを実行するとCREDITS.htmlにライセンス表記を出力してくれる。

<html>
    <head>
        <style>
            @media (prefers-color-scheme: dark) {
                body {
                    background: #333;
                    color: white;
                }
                a {
                    color: skyblue;
                }
            }
            .container {
                font-family: sans-serif;
                max-width: 800px;
                margin: 0 auto;
            }
            .intro {
                text-align: center;
            }
            .licenses-list {
                list-style-type: none;
                margin: 0;
                padding: 0;
            }
            .license-used-by {
                margin-top: -10px;
            }
            .license-text {
                max-height: 200px;
                overflow-y: scroll;
                white-space: pre-wrap;
            }
        </style>
    </head>
    <body>
        <main class="container">
            <div class="intro">
                <h1>Third Party Licenses</h1>
                <p>This page lists the licenses of the projects used in fzf-make.</p>
            </div>
        
            <h2>Overview of licenses:</h2>
            <ul class="licenses-overview">
                {{#each overview}}
                <li><a href="#{{id}}">{{name}}</a> ({{count}})</li>
                {{/each}}
            </ul>

            <h2>All license text:</h2>
            <ul class="licenses-list">
                {{#each licenses}}
                <li class="license">
                    <h3 id="{{id}}">{{name}}</h3>
                    <h4>Used by:</h4>
                    <ul class="license-used-by">
                        {{#each used_by}}
                        <li><a href="{{#if crate.repository}} {{crate.repository}} {{else}} https://crates.io/crates/{{crate.name}} {{/if}}">{{crate.name}} {{crate.version}}</a></li>
                        {{/each}}
                    </ul>
                    <pre class="license-text">{{text}}</pre>
                </li>
                {{/each}}
            </ul>
        </main>
    </body>
</html>

how(ローカル)#

以下のようなmake targetを定義しておくとmake update-license-fileで依存ライブラリのライセンス表記が最新か検証できる。

.PHONY: tool-update-license-file
tool-update-license-file:
	@if ! which cargo-about > /dev/null; then \
		cargo install --locked cargo-about; \
	fi

.PHONY: update-license-file
update-license-file: tool-update-license-file
	cargo about generate about.hbs > CREDITS.html

how(CI)#

以下のようなyamlを定義しておくと依存ライブラリのライセンス表記が最新でないとCIが落ちてくれる。

name: Check license file
on:
  push:
    branches: [ "main" ]
  pull_request:

jobs:
  check-license-file:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Check
        run: make update-license-file
      - name: Check diff
        run: |
          git add .
          git diff --cached --exit-code          

なお、デフォルトだと以下の部分のThis page...fzf-makeの部分がThis page lists the licenses of the projects used in cargo-about.として出力される。そのため、そのまま利用すると「cargo-aboutプロジェクトでは以下のライセンスに依存しています」という意味の表示になってしまうためユーザーが忘れずにcargo-aboutの部分をプロジェクト名に変更する必要があるという問題がある。2

<div class="intro">
    <h1>Third Party Licenses</h1>
    <p>This page lists the licenses of the projects used in fzf-make.</p>
</div>

これに関しては以下のissueを立ててみたのでcargo-about側と意見が一致したら修正するPRを送ってみようと思う。

[Feature request] change cargo about init to generate the statement including user’s project name.

おわりに#

繰り返しの定型タスクをCIに任せることができたので開発により集中できるようになった。(趣味開発だと数ヶ月ぶりにコミットするとかもザラにあり、開発に必要なタスクを忘れがちなのでそういった意味でも自動化の範囲を増やすことができてよかった)

便利ツールをOSSとして公開してくれている人/企業に感謝しつつ利用するだけでなく自分にできる貢献はやっていきたい。