Juliaのパッケージ開発に便利なPkgTemplates, Revise, developコマンド


この記事では，パッケージのひな形を生成するところからGithubでの公開までのステップをひとつずつ見ていきながら，`develop`（`dev`）コマンドなど，REPLでのパッケージ開発に有用な機能を紹介します。まずは簡単なプロジェクトを作成し，それをパッケージとしてまとめてみます。

# パッケージ開発の手順

[前回の記事](https://qiita.com/mametank/items/9fc1c9227303d6ca304b)で説明したプロジェクトの作成方法は（pkgモードで`generate`コマンド実行），パッケージのひな形を作るのに有効です。しかし，パッケージの開発を助けるためには，もう少し多くの手順を，順を追って実行することになるでしょう。それらの手順を簡素化するために，外部パッケージやJuliaコマンドを利用します。

1. まずパッケージのひな形を用意します。必要なファイルを自動生成する`PkgTemplates`パッケージが便利です。
2. 開発途中のパッケージを，Juliaに認識させます。これはpkgモードの`develop`コマンドで実現できます。
3. 開発途中のパッケージをJuliaに認識させたあとで，そこに含まれるファイルを修正したとき，その変更をJuliaに反映するには，Juliaを再起動しなければなりません。この問題は`Revise`パッケージで解消できます。
4. テストは`test/runtests.jl`に記述します。pkgモードの`test`コマンドで，このスクリプトを実行できます。
5. 必要ならば，パッケージをGithubで公開します。公式レポジトリに登録するには，さらにもうひと手間かかります。この手順を丁寧に説明したWebサイトがあります。

上から順に見てゆきましょう。なお，この記事の執筆に当たり，以下のサイトを参考にしました。

- [Julia - パッケージ作り](https://goropikari.hatenablog.com/entry/julia_make_package)
- [Julia - 野良パッケージを公式パッケージにする方法](https://goropikari.hatenablog.com/entry/julia_make_official_package)
- [Juliaでのパッケージ作り](https://qiita.com/cometscome_phys/items/989389db3540ebd9e026)
- [Julia v1.0 でユニットテスト](https://qiita.com/antimon2/items/5222f4f773bf1944b745)
- [Docker, GitHub Actions を組み合わせた Julia パッケージ開発環境を作るよ．](https://qiita.com/SatoshiTerasaki/items/88ff6a1e9351cec6aafe)
- [Julia パッケージをいわゆる公式パッケージとして使ってもらう方法 2021年 12 月版](https://qiita.com/SatoshiTerasaki/items/7dbb809c962e794a9df6)
- [Pkg](https://docs.julialang.org/en/v1.0/stdlib/Pkg/); パッケージマネージャ`Pkg`の，Standard Libraryに含まれる公式ドキュメントです。
- [PkgTemplates](https://github.com/invenia/PkgTemplates.jl); パッケージのひな形を作るための関数群を定義したパッケージです。

2023年9月26日追記：`PkgTemplates`は積極的に更新されており，その使い方も以前と比べてかなり変わっています。上記のリストも，随時入れ替えていますが，一部には古い内容が含まれているかもしれません。この記事では，記事更新時点での最新版である`PkgTemplates` v0.7.45を使用します。

# パッケージのひな形を作る

## PkgTemplatesの使い方

標準の`generate`は最小限のプロジェクトを作成しました。自分自身または身内だけで使う場合，この構成で十分です。

もしこのプロジェクトを標準的なパッケージとして公開するなら，このコマンドで生成されないファイルやディレクトリを用意する必要があります。このような「パッケージのひな形を作る」には，[PkgTemplatesパッケージ](https://github.com/invenia/PkgTemplates.jl)が利用できます。

具体的な手順は，[cometscome_phys さんの記事](https://qiita.com/SatoshiTerasaki/items/88ff6a1e9351cec6aafe)で説明されています。[Hiroshi Shinaokaさん](https://zenn.dev/h_shinaoka/articles/fd302ce7d41a33)も簡潔に説明しています。ここでは，今後の説明のために，小さなパッケージ`MyPackage`を作ります。これはGithub上に置かれ，最終的には公式パッケージとして登録する可能性も考慮します。あらかじめデフォルト環境に`PkgTemplates`をインストールしておきます。

### PkgTemplatesのデフォルト挙動（Gitのグローバル設定を引き継ぐ）

Juliaのパッケージは（たとえ公開する予定がなくても）gitレポジトリとして作成される機会が多くなっています。これを反映して，`PkgTemplates`が作成するひな形はgitレポジトリそのものであり，その作成の際にはgitのユーザ名を必要とします。システムにgitが存在するならば，`PkgTemplates`は`git config --global`のuser.nameとuser.emailを読み取り，新規作成するパッケージに引き継ぎます。もし，個別のユーザ名を使いたい場合，あるいは，gitがうまく呼び出せなくてエラーになる場合（さらにはgitが不要な場合）には，テンプレート内にそれらの情報を記述することで回避できます。このための設定は後述します。

ひとまずここでは，gitのグローバル設定を引き継いで，パッケージのひな形を作成します。REPLで以下のコマンドを入力します。なお，各項目の説明は，`?Template`で表示できます。

```
# パッケージをロードします
julia> using PkgTemplates

# gitのグローバル設定に基づいてテンプレートを作ります
julia> tmpl = Template(;
      user="mametank",
      authors=["Mametank <mametank@example.com>"],
      dir=".",
      julia=v"1.9",
      plugins=[
        License(; name="MIT"),
        Git(; ssh=true),
        GitHubActions(),
      ],
  )

# テンプレートに基づいてファイルとディレクトリを作成します
julia> tmpl("MyPackage")
```

なお，`plugins=[]`内には，さまざまな追加機能や設定を記述します。上記の例では，`License()`がライセンスを設定し，`Git()`がgitの詳細な設定を行います。`GitHubActions()`は，Github ActionsによるCIをサポートします。AppVeyorやTravisCIを使いたい場合，それらを適切な書式で記述します（詳しくは`?AppVeyor`または`?TravisCI`を参照してください）。このほかにも，多数の機能がサポートされています。

さて，上記のコマンドを実行すると，以下のディレクトリとファイルが生成されます。

```
C:.
└───MyPackage
    │   .gitignore
    │   LICENSE
    │   Manifest.toml
    │   Project.toml
    │   README.md
    │
    ├───.git
    │   （Gitリポジトリの情報）
    │
    ├───.github
    │   └───workflows
    │           CI.yml
    │           CompatHelper.yml
    │           TagBot.yml
    │
    ├───src
    │       MyPackage.jl
    │
    └───test
            runtests.jl

```

このパッケージは，カレントディレクトリ直下にgitレポジトリとして作られます。もしgitやGithubを使う予定がなければ，不要なファイルは削除するか，単に無視してかまいません。

### ユーザ名とemailの個別指定

グローバルなgit設定とは異なるユーザ名を使いたい場合，あるいは，gitがうまく呼び出せなくてエラーになる場合には，テンプレートの`Git`内にそれらの情報を記述することで回避できます。また，gitを使う予定がない場合にも，適当なユーザ名とメールドレスを記入しておけば，以下の方法でひな形が作成できます。上記のコマンドを以下のように変更します。

```
# テンプレートの作成時に，gitのuserとemailを指定してしまう
julia> tmpl = Template(;
      user="mametank",
      authors=["Mametank <mametank@example.com>"],
      dir=".",
      julia=v"1.9",
      plugins=[
        License(; name="MIT"),
        Git(; name="mametank", email="mametank@example.com", ssh=true),
        GitHubActions(),
      ],
  )

# テンプレートに基づいてファイルとディレクトリを作成します
julia> tmpl("MyPackage")
```

### ひな形作成後の設定変更

ファイルを生成したあとで設定を変えたいときは，`Project.toml`をエディタで開いて修正し，さらにgitレポジトリの設定を変える必要があります。これらの修正は，Julia上のshellモードで行うか，Juliaコマンドを実行することで実現できます。

以下，コマンドを使って修正する場合のサンプルを示します。パス表記がWindowsのものですので，適宜読み替えてください。

```
# エディタでファイルを開く
julia> edit("MyPackage\\Project.toml")

# gitリポジトリのユーザ情報を変更する
julia> run(`git -C MyPackage config --local user.name mametank`)

julia> run(`git -C MyPackage config --local user.email mametank@example.com`)
```

ここまで作成できれば，あとは必要に応じてファイルを追加・編集できます。また，gitによるcommitやpushも実行できます。

## 依存するパッケージを追加

新規作成したばかりのパッケージは，独立した環境を持ちます。この環境で使える依存パッケージを登録するには，pkgモードで`activate`した上で，`add`コマンドを実行します。詳しくは[前回の記事](https://qiita.com/mametank/items/9fc1c9227303d6ca304b)を参照してください

たとえば，新規作成したパッケージ内で`LinearAlgebra`を使うとしましょう。この場合，Pkgモードで以下のように入力します。

```
# 開発中のパッケージのパスを指定し，その環境に切り替える
(v1.1) pkg> activate MyPackage

# 依存パッケージを明示する
(MyPackage) pkg> add LinearAlgebra
 Resolving package versions...
  Updating `c:\Users\home\local\src\julia\MyPackage\Project.toml`
  [37e2e46d] + LinearAlgebra
  Updating `c:\Users\home\local\src\julia\MyPackage\Manifest.toml`
  [8f399da3] + Libdl
  [37e2e46d] + LinearAlgebra

# 追加が終わったらグローバル環境に戻る
(MyPackage) pkg> activate

# 表示がもとに戻ったことを確認する
(v1.1) pkg>
```

このプロジェクトが直接依存するパッケージ名は`Project.toml`に，すべてのパッケージの依存関係は`Manifest.toml`に保存されます。これらはパッケージマネージャがうまく処理してくれますので，ユーザが気にする必要はありません。


# developコマンドで開発をスタートする

まずはじめに，パッケージを新規開発し，それをローカルに保存するケースを考えます。続けて，Githubに配置したパッケージの開発も紹介します。

## なぜdevelopコマンドか

通常，Juliaでパッケージを利用可能にするには，Pkgモードで`add`コマンドを実行し，Juliaにそれを認識させる必要があります。正確に言えば，パッケージを使うには，その情報を`Project.toml`と`Manifest.toml`に追加しなければなりません。たとえそれが開発途中のパッケージで，その内容がすぐに変更されるにしても，です。

しかし，開発中のパッケージを`add`で追加することは，実用的ではありません。`add`を使うべきでない（使おうにも使えない）理由は，以下のとおりです。

1. `add`は，1つの環境にパッケージの複数のバージョンをインストールできない。すでに完成してインストールされているパッケージ修正したい場合，開発版を一時的に`add`しようとすれば，既存のバージョンをアンインストールする（あるいは開発版で上書きする）必要が生じる。
2. `add`が読み込めるローカルなパッケージは，gitレポジトリだけである。パッケージを`PkgTemplates`で作成した場合には問題ないが，`generate`で生成した生のパッケージは読み込めない。

パッケージを開発するために，Juliaには`develop`（短縮して`dev`）コマンドが用意されています。これは`add`と同様に，開発中のパッケージをJuliaにインストールしますが，上記の欠点を克服し，より柔軟に動作します。

## ローカルで新規パッケージを開発する

さて，上の例で作った`MyPackage`ディレクトリを，`dev`コマンドでJuliaに認識させましょう。まずは，自分がデフォルト環境にいることを確認します。続けて，`dev`コマンドに，このプロジェクトのフルパスを与えて実行します（プロジェクト名だけ与えると，JuliaはGithubを読みに行き，エラーを返すかもしれません）。

```julia
(v1.1) pkg> dev c:\\Users\\home\\local\\src\\julia\\MyPackage
 Resolving package versions...
  Updating `C:\Users\home\.julia\environments\v1.1\Project.toml`
 [no changes]
  Updating `C:\Users\home\.julia\environments\v1.1\Manifest.toml`
 [no changes]
```

これで，現在の環境（デフォルト環境）で，自作パッケージを認識させることができました。もし，自作パッケージをactivateしたまま，この`dev`を実行すると，以下のエラーが出ます。

```
(v1.1) pkg> activate MyPackage

(MyPackage) pkg> dev C:\Users\home\local\src\julia\MyPackage
ERROR: Cannot develop package with the same name or uuid as the project
```

あとは，REPLのコマンドモードに戻り，`using`でパッケージを読み込みます。これで，開発中のパッケージの機能が使えるようになります。

```julia
julia> using MyPackage
```

このように追加したパッケージを削除するには，通常と同じく`rm`を使います。[^1]

[^1]: パッケージ開発を紹介するブログやマニュアルでは`free`を使う例が多く出てきます。これは，後述するような，以前のバージョンと開発版のバージョンが両方存在する場合に，開発版だけを除去するコマンドです。この例では，開発中のバージョンだけが環境に存在するので，`rm`で除去できます。

```
(v1.1) pkg> rm MyPackage
  Updating `C:\Users\home\.julia\environments\v1.1\Project.toml`
  [a1239a20] - MyPackage v0.1.0 [`c:\\Users\\home\\local\\src\\julia\\MyPackage`]
  Updating `C:\Users\home\.julia\environments\v1.1\Manifest.toml`
  [a1239a20] - MyPackage v0.1.0 [`c:\\Users\\home\\local\\src\\julia\\MyPackage`]
```

## Github上の既存パッケージに手を入れる

ここまで説明した方法は，Github上のパッケージの開発でも共通です。ここからは，すでにパッケージがGithub上にある場合に，これを修正するケースを考えます。そして，その自作パッケージがすでにインストールされている環境で，このパッケージを修正していきます。Julia公式のテスト用のパッケージ`Example`を用いて，実際の手順を見てみましょう。

まず，デフォルト環境に`Example`をインストールします。

```julia
(v1.1) pkg> add Example
 Resolving package versions...
  Updating `C:\Users\home\.julia\environments\v1.1\Project.toml`
  [7876af07] + Example v0.5.1
  Updating `C:\Users\home\.julia\environments\v1.1\Manifest.toml`
  [7876af07] + Example v0.5.1
  [2a0f44e3] + Base64
  [8ba89e20] + Distributed
  [b77e0a4c] + InteractiveUtils
  [56ddb016] + Logging
  [d6f4376e] + Markdown
  [9a3f8284] + Random
  [9e88b42a] + Serialization
  [6462fe0b] + Sockets
  [8dfed614] + Test

(v1.1) pkg> status
    Status `C:\Users\home\.julia\environments\v1.1\Project.toml`
  [7876af07] Example v0.5.1
```

このパッケージを修正するには，以下の`dev`コマンドを使います。これもデフォルト環境で実行します。

```julia
(v1.1) pkg> dev Example
[ Info: Path `C:\Users\home\.julia\dev\Example` exists and looks like the correct package, using existing path
 Resolving package versions...
  Updating `C:\Users\home\.julia\environments\v1.1\Project.toml`
  [7876af07] ↑ Example v0.5.1 ⇒ v0.5.1+ [`C:\Users\home\.julia\dev\Example`]
  Updating `C:\Users\home\.julia\environments\v1.1\Manifest.toml`
  [7876af07] ↑ Example v0.5.1 ⇒ v0.5.1+ [`C:\Users\home\.julia\dev\Example`]

(v1.1) pkg> status
    Status `C:\Users\home\.julia\environments\v1.1\Project.toml`
  [7876af07] Example v0.5.1+ [`C:\Users\home\.julia\dev\Example`]
```

このパッケージがcloneされ，規定の`.julia/dev/Example`ディレクトリに保存されました。`Example`のバージョン番号に`+`が表示され，これが開発中であることを意味しています。つまり，この環境には，既存のバージョンと開発版のバージョンの両方がインストールされていますが，開発中のバージョンが優先して使われています。そのパッケージを使うには，REPLで`using Example`とします。

```julia
julia> using Example
[ Info: Recompiling stale cache file C:\Users\home\.julia\compiled\v1.1\Example\lLvWP.ji for Example [7876af07-990d-54b4-ab0e-23690620f79a]
```

これ以降，パッケージの中身を変更してゆき，修正を適時`git commit`します。この開発版のパッケージは，ずっと`.julia/dev`以下に居座ります。パッケージマネージャ自身がはその中身を変更することはありません。また，修正をgitレポジトリにpushするかどうかは，開発者の判断です。開発を終えて，これを削除するには，pkgモードの（`rm`ではなく）`free`コマンドを使います。

```julia
(v1.1) pkg> free Example
 Resolving package versions...
  Updating `C:\Users\home\.julia\environments\v1.1\Project.toml`
  [7876af07] ↓ Example v0.5.1+ [`C:\Users\home\.julia\dev\Example`] ⇒ v0.5.1
  Updating `C:\Users\home\.julia\environments\v1.1\Manifest.toml`
  [7876af07] ↓ Example v0.5.1+ [`C:\Users\home\.julia\dev\Example`] ⇒ v0.5.1

(v1.1) pkg> status
    Status `C:\Users\home\.julia\environments\v1.1\Project.toml`
  [7876af07] Example v0.5.1
```

開発版を解放したことで，以前のように，開発版ではない（`+`のない）オリジナルのパッケージが利用できるようになりました。


# ReviseでJuliaの再起動を避ける

パッケージ開発で最大の障害は，`using`で読み込んだパッケージは，REPLを再起動するまで変更できないことです。いちど`using`してしまうと，パッケージ内のファイルを修正して再び`using`しても，その変更は現在のセッションには反映されません。[Reviseパッケージ](https://github.com/timholy/Revise.jl)は，この制限を廃し，ファイルの変更をすぐに現在のセッションに反映させるものです。なお，開発環境であるJunoは，これとは別の仕組みで，これと同等の機能を提供しています。

使い方は簡単で，開発中のパッケージを`using`する前に，`using Revise`を実行するだけです。あとは何もする必要はありません。これをスタートアップスクリプトに書いておくと，実行忘れを防げます。

実際にさきほどの`MyPackage`を`Revise`で更新してみましょう。`dev`で開発を始めたあと，`Revise`，`MyPackage`の順にパッケージを読み込みます。

```
# これを最初に実行する
julia> using Revise

# 続けてパッケージを読み込む
julia> using MyPackage
```

さて，ここで`src/MyPackage.jl`に含まれる`greet()`関数を実行してみましょう。これはexportされていないので，パッケージ名をつけて明示的に実行する必要があります。

```
# exportされていないのでエラーになる
julia> greet()
ERROR: UndefVarError: greet not defined
Stacktrace:
 [1] top-level scope at none:0

# パッケージ名を含めると実行できる
julia> MyPackage.greet()
Hello World!
```

ここで`src/MyProject.jl`をエディタで開き，`greet()`をexportするように修正しましょう。

```julia
module MyPackage

greet() = print("Hello World!")

# この行を新たに追加する
export greet

end # module
```

REPLに戻り，今度は単に`greet()`を実行してみましょう。

```
# 少し待ちます
julia> greet()
Hello World!
```

コマンド入力後，更新ファイルを検知してプログラムを再コンパイルするので，少し待たされます。その後，この関数が問題なく実行されるでしょう。


# テストを書く

パッケージにはテストのためのスクリプトを含めることができます。このスクリプト名は決め打ちで，`test/runtests.jl`です。この中で別のファイルをincludeできますので，ファイルを分割することができます。

テストの書き方は，いくつかのサイト（たとえば，[Julia - パッケージ作り](https://goropikari.hatenablog.com/entry/julia_make_package)や[Julia v1.0 でユニットテスト](https://qiita.com/antimon2/items/5222f4f773bf1944b745)）でわかりやすく紹介されています。ここでは，さきほどの続きで，Reviseを利用した状態で `test/runtests.jl` を編集し，これが正しく動くことを確認します。

テストファイル`test/runtests.jl`を開き，その内容を以下のように編集して保存します。

```julia
using MyPackage
using Test

@testset "Function tests" begin
    @test_nowarn greet()
end
```

このテストは完全に独立な環境下で実施されるので，必要なパッケージをすべて`using`で読み込む必要があります。`@testset "テストグループ名" begin ... end` は，テストグループを定義するマクロで，その中に複数のテストを記述します。異なる種類のテストを行う場合には，また別の`@testset`を定義します。

テストの鍵である`@test_nowarn`は，引数の命令文がwarningを出さないかをテストし，出さない場合にtrue（テスト成功）を返すマクロです。もし命令文がbooleanを返すなら，単に`@test`マクロが利用できます（真ならばテスト成功）。

さて，このファイルはJuliaスクリプトですので，REPLからそのまま実行できます。ですが，これはすでにパッケージの一部ですので，pkgモードで実行するほうが統一的です。`test`コマンドのあとにパッケージ名をつけて実行します。

```
# パッケージ内の test/runtests.jl を実行する
(v1.1) pkg> test MyPackage
   Testing MyPackage
 Resolving package versions...
    Status `C:\Users\home\AppData\Local\Temp\jl_2C17.tmp\Manifest.toml`
  [a1239a20] MyPackage v0.1.0 [`c:\Users\home\local\src\julia\MyPackage`]
  [2a0f44e3] Base64  [`@stdlib/Base64`]
  [8ba89e20] Distributed  [`@stdlib/Distributed`]
  [b77e0a4c] InteractiveUtils  [`@stdlib/InteractiveUtils`]
  [8f399da3] Libdl  [`@stdlib/Libdl`]
  [37e2e46d] LinearAlgebra  [`@stdlib/LinearAlgebra`]
  [56ddb016] Logging  [`@stdlib/Logging`]
  [d6f4376e] Markdown  [`@stdlib/Markdown`]
  [9a3f8284] Random  [`@stdlib/Random`]
  [9e88b42a] Serialization  [`@stdlib/Serialization`]
  [6462fe0b] Sockets  [`@stdlib/Sockets`]
  [8dfed614] Test  [`@stdlib/Test`]
Hello World!Test Summary: | Pass  Total
Function tests |    1      1
   Testing MyPackage tests passed
```

もちろん警告は出ませんので，テストは成功します。このスクリプトは，テストグループを1つだけ（`Function tests`）含むので，その結果だけが表示されています。`Revise`を実行していれば，このテストスクリプトの変更も，即座に`test`コマンドに反映されます。


# パッケージを公開する

以上の手順を繰り返し，パッケージを開発します。開発者の判断で，git commitとgit pushを行います。ここには示しませんでしたが，依存ライブラリは`deps`に，ドキュメントは`doc`にそれぞれ保存します。そのほかの公開に必要なファイルは，すでに`PkgTemplates`が用意してくれました。このgitレポジトリをGithubに配置すれば，非公式パッケージの完成です。

これを公式パッケージとして登録するには，CIを実行してビルドをパスしている必要があります。その後，JuliaRegistratorでパッケージを登録します。以下の記事がその方法を丁寧に説明してくれています。

- [Julia - 野良パッケージを公式パッケージにする方法](https://goropikari.hatenablog.com/entry/julia_make_official_package)
- [Julia パッケージをいわゆる公式パッケージとして使ってもらう方法 2021年 12 月版](https://qiita.com/SatoshiTerasaki/items/7dbb809c962e794a9df6)

# まとめ

- 最初にパッケージのひな形をつくる。`PkgTemplates`はパッケージをgitレポジトリとして準備する。
- パッケージ開発は，pkgモードの`develop`コマンドで開始する。
- 開発中のパッケージを読み込む前に`using Revise`を実行する。
- テストは`test/runtests.jl`に書き，pkgモードで`test`コマンドを実行する。
- 以上を繰り返して，パッケージを仕上げる。
- これを公式パッケージにするには，CIをパスし，JuliaRegistratorを呼び出す。