この記事では、モジュールやパッケージ関連の操作がわかることを前提として、

公式パッケージ化のために

  • PkgTemplates.jlを用いて雛形作り
  • パッケージ開発の基本中の基本とTravis CIによる自動テスト
  • DocStringとDocumentar.jlを使ったドキュメント作り
  • 登録

の4つを順に解説してパッケージを作っていきます。

(以下の説明のリンク/ファイル構成などは、PixelArt.jlに関するものなので、適宜ユーザー名、パッケージ名etc…は読み替えてください。)

PkgTemplates.jlを用いた雛形づくり

さて、パッケージを作ると言っても具体的にどこから手をつければ良いかわからないと思います。

そこで役に立つのがPkgTemplates.jlです。

このパッケージはJuliaでPackageを作るための雛形を自動で生成してくれます。


(@v1.5) pkg> add PkgTemplates

julia> using PkgTemplates

julia> t = Template(;
         user="abap34",  # GitHubのuser名
         license="MIT",  
         authors=["Yuchi Yamaguhci"], 
         dir=".",      # 生成するディレクトリ
         julia_version=v"1.5",
         plugins=[     # 使うplugin. 公式登録にはtravisとdocumentが必須らしいです
         TravisCI(),
         Documenter(),
         GitHubPages()
       ],


julia> generate("PixelArt.jl", t)
[ Info: Running prehooks
[ Info: Running hooks
 Activating environment at `~/Desktop/PixelArt/Project.toml`
<中略>
[ Info: Running posthooks
[ Info: New package is at /Users/yuchi_ymgc/Desktop/PixelArt

とすることで、雛形を生成することができます。

確認してみます。

shell> cd PixelArt/
/Users/yuchi_ymgc/Desktop/PixelArt

shell> tree
.
├── LICENSE
├── Manifest.toml
├── Project.toml
├── README.md
├── docs
│   ├── Manifest.toml
│   ├── Project.toml
│   ├── make.jl
│   └── src
│       └── index.md
├── src
│   └── PixelArt.jl
└── test
    └── runtests.jl

こうして出来たファイル群を更新/追加 することでパッケージを作成していきます。

今回は例として、前回作った 画像をdot絵にするやつ をパッケージにしていきます。

まず、これからコードを動かすためにPixelArt.jlをJuliaに認識してもらいます。

dev {filepath} で開発中のローカルのパッケージを認識させることができます。

shell> pwd
/Users/yuchi_ymgc/Desktop/PixelArt

(@v1.5) pkg> dev .
[ Info: Resolving package identifier `.` as a directory at `~/Desktop/PixelArt`.
Path `.` exists and looks like the correct package. Using existing path.
  Resolving package versions...
Updating `~/.julia/environments/v1.5/Project.toml`
  [0ba03b52] + PixelArt v0.1.0 `../../../Desktop/PixelArt`
Updating `~/.julia/environments/v1.5/Manifest.toml`
  [0ba03b52] + PixelArt v0.1.0 `../../../Desktop/PixelArt`

また、PixelArt.jlはImages.jlに依存しているので追加しておきます。

このようにactivateした環境でパッケージを追加することで、自動的にProject.tomlが更新されます。(そのほかにも適宜必要なパッケージを追加しておきます。)

(@v1.5) pkg> activate .
 Activating environment at `~/Desktop/PixelArt/Project.toml`

(PixelArt) pkg> add Images

パッケージ開発の基本中の基本

さて、パッケージの設計技法などに深く言及するのではなく、一般的なパッケージ開発のやり方について少しだけ言及します。

ファイル構成

まず, src/PixelArt.jlは主に、

  • 他のfileをincludeなどで取り込んで、exportする

という用途で使われ、基本的にその関数は src/ 以下のファイル群に書かれていることが多いと思います。

なので具体的には以下のようになっており、本質的なコード量は少なめという印象があります。

# src/PixelArt.jl

module PixelArt

include("functions.jl")

export pixel

end
# src/functions.jl

function img_to_arr(img)
    return convert(Array{Float64}, channelview(img))
end

function pixel(img; n_color=5, w=64, h=64)
    img = imresize(img, (w, h))
    img = reshape(img_to_arr(img), (3, :))
    color_class = kmeans(img, n_color)
    img = hcat((x -> color_class.centers[:, x]).(color_class.assignments)...)
    return convert(Array{RGB{Float64}}, colorview(RGB, reshape(img, (3, w, h))))
end

このくらいの規模感では恩恵はほとんどないですが、ある程度以上の規模のパッケージではこのような構成を取らないと大変なことになります….

次に、test/runtest.jlです。

using PixelArt
using Test

@testset "PixelArt.jl" begin
    # Write your tests here.
end

デフォルトでは次のような形になっていて、それぞれの@testsetでテストを書くことができます。

そしてtravis ciなどを用いてテストおこないます。

もちろんローカルでも実行できるので、試してみます。

#test/runtests.jl
using PixelArt
using Test

@testset "Sugoi-Test" begin
    @test 1 + 1 == 2
    @test 2 + 2 == 4
end


@testset "Nice-Test" begin
    @test 1 + 1 == "田んぼの田"
end
julia> include("test/runtests.jl")
Test Summary: | Pass  Total
Sugoi-Test    |    2      2
Nice-Test: Test Failed at /Users/yuchi_ymgc/Desktop/PixelArt/test/runtests.jl:11
  Expression: 1 + 1 == "田んぼの田"
   Evaluated: 2 == "田んぼの田"
Stacktrace:
 [1] top-level scope at /Users/yuchi_ymgc/Desktop/PixelArt/test/runtests.jl:11
 [2] top-level scope at /Users/julia/buildbot/worker/package_macos64/build/usr/share/julia/stdlib/v1.5/Test/src/Test.jl:1115
 [3] top-level scope at /Users/yuchi_ymgc/Desktop/PixelArt/test/runtests.jl:11
Test Summary: | Fail  Total
Nice-Test     |    1      1
ERROR: LoadError: Some tests did not pass: 0 passed, 1 failed, 0 errored, 0 broken.
in expression starting at /Users/yuchi_ymgc/Desktop/PixelArt/test/runtests.jl:10

1 + 1は、田んぼの田ではないようです。(かなしい)

このように、@testsetを一つの単位としてテストを実行することができます。

また、PkgTemplates.jlでgenerateするときにTravis CIをpluginとして導入していたので、自動で.travis.ymlが生成されて、travisを有効化することで、test/runtests.jl は自動で実行されます。

一応今回は

  • pixel()がちゃんと配列を返すのか
  • nothingが来た場合ちゃんとエラーを吐くか

をチェックしておきます。(本当はもっとちゃんと書こうね!)

(追記: @test_throwns HogeError f(x) は、f(x)がHogeErrorを投げるかどうかをテストします。)

# test/runtests.jl
using PixelArt
using Test
using TestImages

@testset "PixelArt.jl" begin
    img = testimage("l")
    @test typeof(pixel(img)) <: AbstractArray 
    # A wrong path often causes passing nothing.
    @test_throws MethodError pixel(nothing) 
end

Revise.jl

さて、ここまでファイル構成に関して解説していましたが、ここでJuliaのパッケージ開発において、もはや必須と言ってもいいパッケージ、Revise.jlを紹介します。

Revise.jlは、 REPLでusing Reviseした状態でパッケージを読み込むと自動で更新を検知してくれるパッケージです。

パッと言われても「..?」という感じだと思うので、

実際に確認してみましょう。

PixelArt.jlに次のような関数, hello を追加してexportしておきます。

function hello()
    println("Hello!")
end

この状態で、REPLを起動し、ReviseとPixelArt.jlを読み込みます。

julia> using Revise

julia> using PixelArt

先ほど追加したhello()を使ってみます。

julia> hello()
Hello!

ここで、このhello()を更新してみます。

通常ならばこの変更はusingされた後ですから反映されません。

function hello()
    println("Hi!")
end

しかし、Revise.jlを用いることで、

julia> hello()
Hi!

なんと、REPL上で何も操作をせずに更新された関数を使うことができました。

このようにして逐次的に結果を確認しながら開発をすることができます。

いちいちREPLを再起動したり julia hoge.jl するのは非効率なので、Revise.jlの強力さがわかると思います。

DocStringとDocumentar.jlを使ったドキュメント作り

docstring

さて、パッケージの中身ができても他の人に使ってもらうには, ドキュメントを作る必要があります。

が、一般に、これはとてもめんどくさいです。

しかし、Juliaでは面倒なドキュメントの構築をとても簡単に行える方法が用意されています。

まず最初は、docstringと呼ばれる機能です。

これは、関数や構造体などにmarkdown形式で書かれたコメントをつけることで、ドキュメントをつけることができ、ユーザーはREPLなどの対話的環境で、

?foo などとすることで閲覧できます。

(例:?sort の結果)

help?> sort
search: sort sort! sortperm sortperm! sortslices Cshort issorted QuickSort MergeSort Cushort partialsort partialsort!

  sort(v; alg::Algorithm=defalg(v), lt=isless, by=identity, rev::Bool=false, order::Ordering=Forward)

  Variant of sort! that returns a sorted copy of v leaving v itself unmodified.

  Examples
  ≡≡≡≡≡≡≡≡≡≡

  julia> v = [3, 1, 2];
  
  julia> sort(v)
  3-element Array{Int64,1}:
   1
   2
   3
  
  julia> v
  3-element Array{Int64,1}:
   3
   1
   2

  ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

  sort(A; dims::Integer, alg::Algorithm=DEFAULT_UNSTABLE, lt=isless, by=identity, rev::Bool=false, order::Ordering=Forward)

  Sort a multidimensional array A along the given dimension. See sort! for a description of possible keyword arguments.

  To sort slices of an array, refer to sortslices.

  Examples
  ≡≡≡≡≡≡≡≡≡≡

  julia> A = [4 3; 1 2]
  2×2 Array{Int64,2}:
   4  3
   1  2
  
  julia> sort(A, dims = 1)
  2×2 Array{Int64,2}:
   1  2
   4  3
  
  julia> sort(A, dims = 2)
  2×2 Array{Int64,2}:
   3  4
   1  2

さて、このdocstringは厳密に定義されているわけではないですが、大体次のようなルールで書かれるようです。

  • 最初に関数名と引数、帰り値を書く(このとき、省略可能な引数は[]で囲う。)
    f(hoge, [, huga, huge])
    
  • 引数が多いときはその説明を追記する。
# Arguments
- `hoge`: the hoge param.
- `huga`: the huga param.
- `huge`: the huge param.
  • 実行例を書く。
# Examples

​```jldoctest

julia> f("huga")
"hugahuga"
```

ここまでを連結して書くと以下のような感じ。

"""
    f(hoge, [, huga, huge])

# Arguments
- `hoge`: the hoge param.
- `huga`: the huga param.
- `huge`: the huge param.
# Examples

​```jldoctest

julia> f("huga")
"hugahuga"
```
"""
function f(hoge; huga=1, huga=2)
    # なんかの処理
end

ここで実行例の先頭にjldoctestと書いてありますが、

これを追記することでのちにドキュメントを生成するときに自動でテストを行ってくれます。

ちなみにpixel()には次のようなdocstringをつけてみました。

"""
    pixel(img, [, n_color, w, h]) -> Array{RGB{Float64}}

# Examples

```
julia> using PixelArt

julia> using Images

julia> img = load("img.jpg");

julia> img_pixel = pixel(img);

julia> save("img_pixel.jpg", img_pixel)
```
"""
function pixel(img::AbstractArray; n_color=5, w=64, h=64) 
    img = imresize(img, (w, h))
    img = reshape(img_to_arr(img), (3, :))
    color_class = kmeans(img, n_color)
    img = hcat((x -> color_class.centers[:, x]).(color_class.assignments)...)
    return convert(Array{RGB{Float64}}, colorview(RGB, reshape(img, (3, w, h))))
end

Documenter.jl

Documenter.jlは、docstringから自動でドキュメントを生成してくれるパッケージです。

PkgTemplates.jlで生成した場合、自動でdocs/make.jlが生成されるので、それを実行しましょう。

# docs/make.jl

using PixelArt
using Documenter

makedocs(;
    modules=[PixelArt],
    authors="Yuchi Yamaguchi",
    repo="https://github.com/abap34/PixelArt.jl/blob/{commit}{path}#L{line}",
    sitename="PixelArt.jl",
    format=Documenter.HTML(;
        prettyurls=get(ENV, "CI", "false") == "true",
        canonical="https://abap34.github.io/PixelArt.jl",
        assets=String[],
    ),
    pages=[
        "Home" => "index.md",
    ],
)

deploydocs(;
    repo="github.com/abap34/PixelArt.jl",
)

result

すると、このように自動で構築されます!便利。

ローカルではこのようにしてドキュメントが構築されましたが、誰もが閲覧できるようにインターネット上に公開しましょう。

ここでは、既に設定されている、Travis CIの定期実行とGitHubPagesを用いて, 自動でドキュメントを生成/公開する方法を紹介します。

TravisCIとGitHubPagesを用いたドキュメントの公開。

まずは、DocumenterTools.jlを用いて、SSH DeployKeysを作成します。

pkg> add DocumenterTools
julia> using DocumenterTools
Then call the DocumenterTools.genkeys function as follows:

julia> using PixelArt
julia> DocumenterTools.genkeys(user="abap34", repo="git@github.com:abap34/PixelArt.jl.git")

このように自分のGithubのユーザ名、レポジトリを指定します。

すると、

[ Info: add the public key below to https://github.com/USER/REPO/settings/keys
      with read/write access:

[SSH PUBLIC KEY HERE]

[ Info: add a secure environment variable named 'DOCUMENTER_KEY' to
  https://travis-ci.com/USER/REPO/settings with value:

[LONG BASE64 ENCODED PRIVATE KEY]

というフォーマットの文字列がたくさん出てきます。

これを登録することでドキュメントをデプロイできるようにします。 まず前者、 ssh-rsa 以下の公開鍵をレポジトリに登録します。

レポジトリの, settings > Deploy keysから、文字列を登録してください。

注意点として、このときwrite sccessにチェックを入れるのを忘れないでください。

次にTravis CIの設定です。

https://travis-ci.org/github/abap34/PixelArt.jl にアクセスして、

settings > Environment Variablesから、

DOCUMENTER_KEYと言う名前で、秘密鍵を登録しましょう。

また、安全性の観点から、この時DISPLAY VALUE IN BUILD LOGはオフになっていることを確認しましょう。

これが漏れてしまうと、パッケージがやりたい放題されてしまいます。

結果

さて, このように設定を完了した結果、documentを自動生成し、公開することができました。

登録

さて、ここまでで構築、テスト、documentとやってきましたが最後にこれを公式のパッケージとして登録します。

※ 一応、ここまでの結果をGitHubなどにアップロードすることで非公式(いわゆる野良パッケージ)として使えます。

もしも公式パッケージにする場合は、まずProject.tomlを編集する必要があります。

Project.tomlの[compat]は、PkgTemplateでは julia="1" のみ生成されていますが、追加されたパッケージについても書く必要があります。

([compat]は動作保証をするパッケージのバージョンを列挙する項目です。)

今回は、以下のようにしました。

[compat]
julia = "1.5"
Clustering = "0.14"
Documenter = "0.24"
ImageMagick = "1.1"
Images  = "0.22"
QuartzImageIO  = "0.6"
TestImages  = "1.0"

さて、Project.tomlを更新したら早速登録しましょう。

公式パッケージにするためには https://github.com/JuliaRegistries/General にPRを出す必要がありますが、

自分で書くわけではなく基本的に自動で生成します。

このとき、

  • JuliaHubから申請
  • Registrator.jlを使用

という二つの選択肢があります。

おそらく最も敷居が低いのは前者のJuliaHubから申請する方法です。

https://juliahub.com 

にログインして、Contribute > Register Packagesを選択して、

  • パッケージのGitHubレポジトリのURL
  • パッケージの名前
  • 開発者(もしくはグループ)の名前 etc…

を入力します。

そしてSubmitを押すと、自動的にプルリクエストが作られます。

)

また、Registrator.jlを利用する方法もあります。

README.mdのInstall部分 からインストールして、登録したいパッケージのレポジトリにissueを立て,

@JuliaRegistrator register とコメントすることで自動でPRを立ててくれます。

実は最初のPRでProject.tomlに[compat]を追加するのを忘れてしまって更新する必要があったのですが、この方法だと何度も更新して一発で申請できるので不安な場合はこちらの方がいいかもしれないです。

)

全てのCIを通過して3日たつと自動でマージされます!仮に何かしら引っかかった場合は修正して再度@JuliaRegistrator register をしましょう。

通過しなかったPRを自動で修正してくれます。便利。(つまりPRを取り下げる必要はありません)

kekka

無事にマージされ、

add PixelArt

のみでパッケージを追加できるようになりました!

(@v1.5) pkg> activate .
 Activating new environment at `~/Desktop/_workspace/Project.toml`

(_workspace) pkg> status
Status `~/Desktop/_workspace/Project.toml` (empty project)

(_workspace) pkg> add PixelArt
  Resolving package versions...
  Installed Images ───────── v0.22.5
  Installed DataStructures ─ v0.18.9
Updating `~/Desktop/_workspace/Project.toml`
  [0ba03b52] + PixelArt v0.1.0
Updating `~/Desktop/_workspace/Manifest.toml`
  [621f4979] + AbstractFFTs v1.0.1
  [79e6a3ab] + Adapt v3.2.0
  [4fba245c] + ArrayInterface v3.1.5
  [56f22d72] + Artifacts v1.3.0
<中略>
  [4ec0a83e] + Unicode

julia> using PixelArt
[ Info: Precompiling PixelArt [0ba03b52-44ea-4ae8-ae76-4c95056a1ba8]

help?> pixel
search: pixel PixelArt CompositeException ProcessFailedException

  pixel(img, [, n_color, w, h]) -> Array{RGB{Float64}}

  Examples
  ≡≡≡≡≡≡≡≡≡≡

  julia> using PixelArt

  julia> using Images

  julia> img = load("img.jpg");

  julia> img_pixel = pixel(img);

  julia> save("img_pixel.jpg", img_pixel)

無事にdocstringとdocumentも確認できました。

どんどん公式パッケージを開発して、Juliaの発展に寄与していきましょう!