この記事では,パッケージのひな形を生成するところからGithubでの公開までのステップをひとつずつ見ていきながら,develop
(dev
)コマンドなど,REPLでのパッケージ開発に有用な機能を紹介します。まずは簡単なプロジェクトを作成し,それをパッケージとしてまとめてみます。
パッケージ開発の手順
前回の記事で説明したプロジェクトの作成方法は(pkgモードでgenerate
コマンド実行),パッケージのひな形を作るのに有効です。しかし,パッケージの開発を助けるためには,もう少し多くの手順を,順を追って実行することになるでしょう。それらの手順を簡素化するために,外部パッケージやJuliaコマンドを利用します。
- まずパッケージのひな形を用意します。必要なファイルを自動生成する
PkgTemplates
パッケージが便利です。 - 開発途中のパッケージを,Juliaに認識させます。これはpkgモードの
develop
コマンドで実現できます。 - 開発途中のパッケージをJuliaに認識させたあとで,そこに含まれるファイルを修正したとき,その変更をJuliaに反映するには,Juliaを再起動しなければなりません。この問題は
Revise
パッケージで解消できます。 - テストは
test/runtests.jl
に記述します。pkgモードのtest
コマンドで,このスクリプトを実行できます。 - 必要ならば,パッケージをGithubで公開します。公式レポジトリに登録するには,さらにもうひと手間かかります。この手順を丁寧に説明したWebサイトがあります。
上から順に見てゆきましょう。なお,この記事の執筆に当たり,以下のサイトを参考にしました。
- Julia - パッケージ作り
- Julia - 野良パッケージを公式パッケージにする方法
- Juliaでのパッケージ作り
- Julia v1.0 でユニットテスト
- Docker, GitHub Actions を組み合わせた Julia パッケージ開発環境を作るよ.
- Julia パッケージをいわゆる公式パッケージとして使ってもらう方法 2021年 12 月版
-
Pkg; パッケージマネージャ
Pkg
の,Standard Libraryに含まれる公式ドキュメントです。 - PkgTemplates; パッケージのひな形を作るための関数群を定義したパッケージです。
2023年9月26日追記:PkgTemplates
は積極的に更新されており,その使い方も以前と比べてかなり変わっています。上記のリストも,随時入れ替えていますが,一部には古い内容が含まれているかもしれません。この記事では,記事更新時点での最新版であるPkgTemplates
v0.7.45を使用します。
パッケージのひな形を作る
PkgTemplatesの使い方
標準のgenerate
は最小限のプロジェクトを作成しました。自分自身または身内だけで使う場合,この構成で十分です。
もしこのプロジェクトを標準的なパッケージとして公開するなら,このコマンドで生成されないファイルやディレクトリを用意する必要があります。このような「パッケージのひな形を作る」には,PkgTemplatesパッケージが利用できます。
具体的な手順は,cometscome_phys さんの記事で説明されています。Hiroshi Shinaokaさんも簡潔に説明しています。ここでは,今後の説明のために,小さなパッケージ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
コマンドを実行します。詳しくは前回の記事を参照してください
たとえば,新規作成したパッケージ内で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
を使うべきでない(使おうにも使えない)理由は,以下のとおりです。
-
add
は,1つの環境にパッケージの複数のバージョンをインストールできない。すでに完成してインストールされているパッケージ修正したい場合,開発版を一時的にadd
しようとすれば,既存のバージョンをアンインストールする(あるいは開発版で上書きする)必要が生じる。 -
add
が読み込めるローカルなパッケージは,gitレポジトリだけである。パッケージをPkgTemplates
で作成した場合には問題ないが,generate
で生成した生のパッケージは読み込めない。
パッケージを開発するために,Juliaにはdevelop
(短縮してdev
)コマンドが用意されています。これはadd
と同様に,開発中のパッケージをJuliaにインストールしますが,上記の欠点を克服し,より柔軟に動作します。
ローカルで新規パッケージを開発する
さて,上の例で作ったMyPackage
ディレクトリを,dev
コマンドでJuliaに認識させましょう。まずは,自分がデフォルト環境にいることを確認します。続けて,dev
コマンドに,このプロジェクトのフルパスを与えて実行します(プロジェクト名だけ与えると,JuliaはGithubを読みに行き,エラーを返すかもしれません)。
(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> using MyPackage
このように追加したパッケージを削除するには,通常と同じくrm
を使います。1
(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
をインストールします。
(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
コマンドを使います。これもデフォルト環境で実行します。
(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> 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
コマンドを使います。
(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パッケージは,この制限を廃し,ファイルの変更をすぐに現在のセッションに反映させるものです。なお,開発環境である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するように修正しましょう。
module MyPackage
greet() = print("Hello World!")
# この行を新たに追加する
export greet
end # module
REPLに戻り,今度は単にgreet()
を実行してみましょう。
# 少し待ちます
julia> greet()
Hello World!
コマンド入力後,更新ファイルを検知してプログラムを再コンパイルするので,少し待たされます。その後,この関数が問題なく実行されるでしょう。
テストを書く
パッケージにはテストのためのスクリプトを含めることができます。このスクリプト名は決め打ちで,test/runtests.jl
です。この中で別のファイルをincludeできますので,ファイルを分割することができます。
テストの書き方は,いくつかのサイト(たとえば,Julia - パッケージ作りやJulia v1.0 でユニットテスト)でわかりやすく紹介されています。ここでは,さきほどの続きで,Reviseを利用した状態で test/runtests.jl
を編集し,これが正しく動くことを確認します。
テストファイルtest/runtests.jl
を開き,その内容を以下のように編集して保存します。
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でパッケージを登録します。以下の記事がその方法を丁寧に説明してくれています。
まとめ
- 最初にパッケージのひな形をつくる。
PkgTemplates
はパッケージをgitレポジトリとして準備する。 - パッケージ開発は,pkgモードの
develop
コマンドで開始する。 - 開発中のパッケージを読み込む前に
using Revise
を実行する。 - テストは
test/runtests.jl
に書き,pkgモードでtest
コマンドを実行する。 - 以上を繰り返して,パッケージを仕上げる。
- これを公式パッケージにするには,CIをパスし,JuliaRegistratorを呼び出す。
-
パッケージ開発を紹介するブログやマニュアルでは
free
を使う例が多く出てきます。これは,後述するような,以前のバージョンと開発版のバージョンが両方存在する場合に,開発版だけを除去するコマンドです。この例では,開発中のバージョンだけが環境に存在するので,rm
で除去できます。 ↩