Julia - パッケージ作り

目次

TL;DR

  • 野良パッケージはどんどん作ろう!
  • 新規の METADATA デビューは今は待ったほうが良いかなぁ。。。

環境

Julia Version 1.0.0
Commit 5d4eaca0c9 (2018-08-08 20:58 UTC)
Platform Info:
  OS: Linux (x86_64-pc-linux-gnu)
  CPU: Intel(R) Core(TM) i5-4460T CPU @ 1.90GHz
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-6.0.0 (ORCJIT, haswell)
Environment:
  JULIA_SHELL = /bin/bash
  JULIA_EDITOR = nvim

自作のパッケージ

オリジナルパッケージの雛形を作る

2019/1/29 追記: 以下の方法でも雛形作れますが、現在は PkgTemplates.jl を使えばライセンスファイルや TravisCI の設定ファイルを一度に作ることができるので最終的に GitHub に公開するならばこちらを使ったほうが後々の手間が省けます。

github.com

(v1.0) pkg> generate MyPackage # 自作のパッケージの雛形を作成
Generating project MyPackage:
    MyPackage/Project.toml
    MyPackage/src/MyPackage.jl

shell> cd MyPackage/
/home/arch/tmp/MyPackage

(v1.0) pkg> activate .

(MyPackage) pkg> 

自動的に出来るファイルの中身
src/MyPackage.jl

module MyPackage

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

end # module

この module の間にずらずらプログラムを書いていくと晴れて自分のパッケージが完成します。 詳しくは公式ドキュメント https://docs.julialang.org/en/stable/manual/modules/ を読んでください。

試しに作ったパッケージを読み込んでみます。

julia> using MyPackage
[ Info: Precompiling MyPackage [f41f8282-a16f-11e8-2120-1f4619698387]

julia> MyPackage.greet()
Hello World!

関数を追加する

src/MyPackage.jl

module MyPackage

greet() = print("Hello World!")
hello(io::IO, name) = print(io, "Hello ", name)
hello(name) = hello(stdout, name)

end # module

Julia を立ち上げ直して、もう一度パッケージを読みこみ直す。

(v1.0) pkg> activate .

(MyPackage) pkg> 

julia> using MyPackage
[ Info: Recompiling stale cache file /home/arch/.julia/compiled/v1.0/MyPackage/o5PoH.ji for MyPackage [f41f8282-a16f-11e8-2120-1f4619698387]

julia> MyPackage.greet()
Hello World!
julia> MyPackage.hello("Mike")
Hello Mike

テストを書く

テストケースは test/runtests.jl に書いていきます。@test 後が false になるとテストを通すのに失敗します。

using Test, MyPackage

@test sprint(MyPackage.hello, "Mike") == "Hello Mike"

Project.toml を編集。テストだけに使うパッケージは [extras][targets] に書きます。

name = "MyPackage"
uuid = "f41f8282-a16f-11e8-2120-1f4619698387"
authors = ["goropikari <goropikari56@gmail.com>"]
version = "0.1.0"


[extras]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Test"]

現在のディレクトリ構成はこんな感じ

shell> tree
.
├── Project.toml
├── src
│   └── MyPackage.jl
└── test
    └── runtests.jl

2 directories, 3 files

テストする。 tests passed と出れば OK。

(MyPackage) pkg> test
   Testing MyPackage
 Resolving package versions...
   Testing MyPackage tests passed

自分のパッケージの中で他のパッケージを使う場合

自分のパッケージの中で Random モジュールを使う例を通して v0.6 までとの違いを紹介します。 例えば、以下のようにプログラムを書いたとします。

MyPackage.jl

module MyPackage

using Random

greet() = print("Hello World!")
hello(io::IO, name) = print(io, "Hello ", name)
hello(name) = hello(stdout, name)
randstr() = randstring()

end # module

Julia v0.6 までは、今回の Random module のように Julia に標準実装された module に関しては REQUIRE *1 に書かずとも自分のパッケージ内で using してもその module を普通に利用することができました。

しかし、Julia v1.0 からは例え標準モジュールであろうとも using または import で読み込む module は Project.toml に書かなければならなくなりました。なかなか面倒な仕様になったのですが、幸いこれらを自分で Project.toml に書く必要はありません。add でこれらの module を追加すると自動的に Project.toml が更新されます。

(MyPackage) pkg> add Random
  Updating registry at `~/.julia/registries/General`
  Updating git-repo `https://github.com/JuliaRegistries/General.git`
 Resolving package versions...
  Updating `Project.toml`
  [9a3f8284] + Random 
  Updating `Manifest.toml`
 [no changes]

Project.toml が自動的に更新される。

name = "MyPackage"
uuid = "f41f8282-a16f-11e8-2120-1f4619698387"
authors = ["goropikari <goropikari56@gmail.com>"]
version = "0.1.0"

[deps]
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"

[extras]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Test"]

ついでに Manifest.toml というのが出来きますが、このファイルにはテストしたときの全てのパッケージ情報が書かれていています。Project.tomlManifest.tomlという2つの軽量なテキストファイルを人に渡すだけで、自分がテスト通したときの環境がそっくりそのまま他の人のマシン上でも再現できるので、再現性が重要になってくる分野では便利でしょとのこと*2

関数を追加したのでテストケースも追加します。

using Test, MyPackage, Random

Random.seed!(0)

@test sprint(MyPackage.hello, "Mike") == "Hello Mike"
@test MyPackage.randstr() == "0IPrGg0J"

もう一度テスト

(MyPackage) pkg> test
   Testing MyPackage
 Resolving package versions...
   Testing MyPackage tests passed 

Julia 以外のライブラリに依存する場合

既存のC*3のライブラリを使いたい、Julia だと処理が遅い部分を C で書いたコードを使いたいといった場合、deps/build.jl を作って build の仕方を書いていきます。 一番手っ取り早い方法だと Makefile 作って build.jl 内に run(`make`) とか書けばいいのですけど、C のコンパイラ入れていない人もいるでしょうし、そもそも make も使えるのか等々あるのでそんなに話は単純ではありません。

この辺のことについて私は詳しくないので以下を参考にしてください。 おそらく、公式ドキュメントには載っていません。

julialang.github.io

github.com

github.com

github.com

UUID

今までスルーしてきましたが Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" などの謎の文字列は各パッケージのUUIDです。 Generalを見れば調べられますが*4、探すの面倒なので上記の ]add Package 使って入れていくのが吉。ただ、add で入れると全部 [deps] のほうに入ってしまうのでテストだけにしか使わないものは適宜 [extras] の方に自分で移し [targets] も編集します。

GitHub に公開する

作ったパッケージを野良パッケージ*5として GitHub に公開してみましょう!

まずは GitHub に新しくリポジトリを作ります。Julia のパッケージの慣例としてパッケージ名の最後に .jl をつけるので今回は MyPackage.jl とします。(慣例で .jl と名前をつけますが、これをつけておくと確か野良パッケージでも Julia Observer に見つけてもらえます。)

作ったら以下を実行。もちろん GitHub のアカウントはご自身のものに直してください。

$ cd /path/to/MyPackage
$ git init
$ git add -A
$ git commit -m 'first commit'
$ git remote add origin https://github.com/goropikari/MyPackage.jl
$ git push -u origin master

これで野良パッケージの公開終了。今後は以下のようにして誰でもインストールできるようになります。

(v1.0) pkg> add https://github.com/goropikari/MyPackage.jl

適宜ライセンスやREADMEも加えましょう。

MyPackage に PATH を通す

作ったパッケージはディレクトリを activate していると using などで読み込めますが、そうでない一般の時は PATH が通っていないので読み込むことが出来ません。

julia> using MyPackage
ERROR: ArgumentError: Package MyPackage not found in current path:
- Run `Pkg.add("MyPackage")` to install the MyPackage package.

Stacktrace:
 [1] require(::Module, ::Symbol) at ./loading.jl:817

バリバリ開発しているときは activate すればよいですが、一段落付いて普通のパッケージとして使いたい時は下記のようにして PATH を通します。

julia> push!(LOAD_PATH, "/path/to/MyPackage/src")

こうすると他のパッケージ同様、どこのディレクトリにいても読み込むことが出来ます。

julia> using MyPackage

julia> MyPackage.greet()
Hello World!

ただこのままだと 毎度、毎度 PATH を通さなければならないので ~/.julia/config/startup.jl に追記しておきましょう。

push!(LOAD_PATH, "/path/to/MyPackage/src")

METADATA.jl に登録する

作ったパッケージを Julia の公式パッケージとして登録*6したい場合は Travis CI を通すことが必須です。 PythonPyPI ではコードレビューが無いらしい*7ですが Julia の場合はあるので、print("Hello World") しかないようなパッケージを登録しようとすると却下されます。

README またはドキュメントもちゃんと書きましょう。当たり前ですが英語で*8。 ドキュメントが書かれていないパッケージはいかに優れていたものだったとしても使う気になりません。

Julia v0.6 まではパッケージの命名に関する決まりごと*9等もドキュメントに書いてありましたが、現時点 (2018/8/17) の Julia v1.0 のドキュメントではパッケージの登録方法の項目が見当たらないので、どういうルールになったのかはよくわかりません。概ね一緒だと思いますが。

登録をするときは AttoBot を使うことが推奨されていますが、現状これを使う場合は v0.6 の作法の REQUIRE というファイルを作らなければなりません。Pkg3用に attobot3 というのもあるようですが、従来の attobot と同様にして使えるのかは不明。

パッケージの雛形を作ってくれる PkgDev.jl*10 がまだ Julia v1.0 に対応しておらず、また、Julia v1.0 が急にリリースされたせいで以前から Julia のパッケージを作っていた古参 Julian も困惑している感じが否めないので新規の METADATA デビュー *11 は当面控えたほうがよさそうといった印象です。

それでも「いますぐ私の素晴らしいパッケージを世に広めたいんだ!」という方は Julia Discourse や Julia の Slack であたりで質問してください。私は従来の方式でしかパッケージ公開をしたことないのでアドバイスできるようなノウハウがありません。

discourse.julialang.org

続編書きました。 goropikari.hatenablog.com

参考

LICENSE

Copyright (c) 2018: goropikari

License: MIT https://opensource.org/licenses/MIT

Copyright (c) 2009-2018: Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors:

https://julialang.org/license

*1:現在のProject.tomlに相当

*2:https://youtu.be/GBi__3nF-rM

*3:C言語でなくても構いませんが

*4:標準モジュールについてはありませんが

*5:METADATA.jl に登録されていないパッケージ

*6: add PackageName でインストールできるようになる

*7:PyPI 使ったことがないので確証はありませんが

*8:今Julia使っている人の英語力なら特に問題ないはず

*9:https://docs.julialang.org/en/v0.6.4/manual/packages/#Creating-a-new-Package-1

*10:Travis とかのテンプレも作ってくれる

*11:今後は General デビューというべきなのかもしれない