📂

Juliaパッケージの標準的なディレクトリ構成

2023/12/01に公開
1

これはJulia Advent Calendar 2023の1日目の記事です。

はじめに

Julia言語には標準でPkg.jlというパッケージマネージャが備わっており、パッケージ管理しやすい言語として知られています。
他の言語、例えばPythonではrequirements.txtのようなレガシーや、Poetry / Pipenv / Rye のようなパッケージマネージャーの乱立があったりしますが、Juliaでは標準的な方法が広く浸透しているため、このようなレガシーや乱立を見る機会はありません。[1]

この一貫性があるため、Juliaパッケージのディレクトリ構成に関する議論もスムーズに行うことができます。本記事では、最初に最小限のパッケージ構成を示し、その後で更により詳細な全部盛りのパッケージディレクトリ構成について紹介していきます。

最小限の構成

JuliaのREPLのパッケージモードでgenerateコマンドを実行すれば最小限の構成でパッケージが生成できます。

(@v1.9) pkg> generate MyPkg

これらのファイル構成は以下のようになっています。

MyPkg
├── Project.toml
└── src
    └── MyPkg.jl
Project.toml
name = "MyPkg"
uuid = "eb62ad0d-c07c-4140-a83b-b6ff560c525f"
authors = ["hyrodium <hyrodium@gmail.com>"]
version = "0.1.0"
src/MyPkg.jl
module MyPkg

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

end # module MyPkg

このパッケージを実行するには、以下のようにパッケージモードでdevコマンドを実行してインストールします。
MyPkg.greet()で挨拶します。

(@v1.9) pkg> dev /home/hyrodium/Git/zenn-content/MyPkg
   Resolving package versions...
    Updating `~/.julia/environments/v1.9/Project.toml`
  [24b0717a] + MyPkg v0.1.0 `~/Git/zenn-content/MyPkg`
    Updating `~/.julia/environments/v1.9/Manifest.toml`
  [24b0717a] + MyPkg v0.1.0 `~/Git/zenn-content/MyPkg`

julia> using MyPkg
[ Info: Precompiling MyPkg [24b0717a-998c-4ceb-8e3e-6314e0e60432]

julia> MyPkg.greet()
Hello World!

全部盛りの構成

諸々全部盛りで構成すると以下のようなディレクトリ構成になります。[2]
以下ではこのパッケージをMyGreatPkg.jlと呼ぶこととしましょう。

MyGreatPkg
├── .git
├── .gitignore
├── LICENSE.md
├── Project.toml
├── (Manifest.toml)
├── src
│   ├── MyGreatPkg.jl
│   └── (他のソースコード)
├── ext
│   ├── MyGreatPkgOtherPkgExt.jl
│   └── (他の拡張メソッドの定義)
├── test
│   ├── runtests.jl
│   └── (テスト用に分割したファイル)
├── README.md
├── docs
│   ├── make.jl
│   ├── Manifest.toml
│   ├── Project.toml
│   ├── src
│   │   ├── assets
│   │   │   ├── custom.css
│   │   │   ├── favicon.ico
│   │   │   ├── logo-dark.svg
│   │   │   └── logo.svg
│   │   ├── img
│   │   │   └── (画像ファイルはここにまとめて置く)
│   │   ├── index.md
│   │   └── (他のMarkdownドキュメント)
│   └── examples
│       └── (DemoCards.jl用のドキュメント)
├── .JuliaFormatter.toml
├── .pre-commit-config.yaml
└── .github
    ├── dependabot.yml
    └── workflows
        ├── CI.yml
        ├── Docs.yml
        ├── CompatHelper.yml
        ├── TagBot.yml
        ├── Format.yml
        ├── SpellCheck.yml
        ├── LabelCheck.yml
        └── Invalidations.yml

これら全部のファイルを自分で毎回用意するは大変なので、PkgTemplates.jlで自動生成する仕組みもあります。[3]
しかし

  • PkgTemplates.jlでまだ対応していない設定がある
  • 古いバージョンのPkgTemplates.jlで生成したリポジトリに新しい設定を加えたい
  • PkgTemplates.jlで生成したファイルの役割が知りたい

などの要望もあるかと思います。
そういう訳で,これらのファイルの意味やカスタマイズを解説するのが本記事の趣旨になります。

Git関連

.git

JuliaのパッケージはGitでバージョン管理されることが求められます。
generateコマンドで作ったMyPkgではGit管理していませんでしたが、Generalに登録されているパッケージではGit管理が求められます。[4]

.gitignore

リポジトリ直下のManifest.tomlやビルド済みドキュメントはignoreしましょう。

.gitignore
/Manifest.toml
/docs/build/

ライセンス

LICENSE.md

JuliaコミュニティではMITライセンスが好まれます。
これは以下のような理由からです。

  • Julia本体がMITライセンスを採用しているため[5]
  • GPL系ライセンスの制約の難しさと互換性低下を避けたいため[6]

また、「BSDライセンスをベースとして商用利用禁止を追加したもの」のようなライセンスもJuliaのパッケージライセンスとして不的確とされることがあります。[7]

ファイル名はLICENSEよりもLICENSE.mdの方が自由度が高くて良いと思います。例えば、Julia本体でのLICENSE.mdのようにURLを使用できるのは便利ですね。

Copyright (c) 2009-2023: Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors: https://github.com/JuliaLang/julia/contributors

プロジェクトファイル

Project.toml

Project.tomlはパッケージのメタデータを管理するような設定ファイルです。[8]

  • パッケージの名前 name
  • パッケージのUUID uuid
  • パッケージのバージョン version
  • パッケージの依存しているパッケージ [deps]
  • パッケージの互換性 [compat]
  • テスト環境で依存するパッケージ [extras] [targets]

Manifest.toml

Manifest.tomlも生成されますが、これは通常は.gitignoreで無視します。

ソースコード

src/MyGreatPkg.jl

通常のソースコードをsrc以下に記載します。
ファイルを分けたい場合はsrc/func1.jlのようにファイルを作ってinclude("func1.jl")のようにsrc/MyGreatPkg.jl内で読み込みます。

src/MyGreatPkg.jl
module MyGreatPkg
function myadd(a,b)
    return a+b
end
include("func1.jl")
end
src/func1.jl
myprod(a,b) = a*b

ext/MyGreatPkgOtherPkgExt.jl

extディレクトリにはPackage extension用のコードを置きます。
Package extentionsについては以下の記事などをご覧ください。

テスト

リポジトリ直下のtestディレクトリがテストとして使われます。[9]

test/runtests.jl

以下のように@testset@testなど使ってテストコードを書きます。
ここでも適当にincludeを使ってファイル分割できます。

test/runtests.jl
using MyGreatPkg
using Test

@testset "MyGreatPkg" begin
    @test MyGreatPkg.myadd(1,2) == 3
    include("func1.jl")
end
test/func1.jl
@test MyGreatPkg.myprod(3,4) == 12

doctest

Juliaには上記のようなTest.jlによるテスト以外にも、doctestによるテストもあります。
doctestはdocstring内に書くテストで、REPLの実行結果がそのままテストになるものです。

"""
    myadd(a, b)

Add two numbers.

# Examples
```jldoctest
julia> myadd(1,2)
3

julia> myadd(8,-4)
4
```
"""
function myadd(a,b)
    return a+b
end

Documenter.jlでdoctestが実行できますが、以下の理由からtest/runtests.jlにdoctest実行を含めないことが一般的です。

  • doctestは実行例の明示が目的であり、coverageでカウントしたくない場合がある。
  • REPLへの出力の変更は通常は破壊的変更とは見なされず、Juliaのバージョンに依存して変わるかも知れない。その場合に特定のバージョンでしかdoctestが通らないことになる。

ではいつdoctestを実行するのか。後述のDocs.ymlでの実行が有力です。

ドキュメント (README)

README.md

リポジトリを参照した人が最初に見るファイルがREADME.mdです。
以下の3つは揃える方が好ましいです。

  • ドキュメントへのリンク
  • インストール方法
  • 最小限の実行例

私がメンテナンスしているIntervalSets.jlのREADME.mdはシンプルで気に入っています。

READMEに色々なバッジが並んでいると格好いいですよね!付けましょう。

ドキュメント (最新リリース)

最新のリリースに対応したバージョンのドキュメントへのリンク

Stable

<!-- BasicBSpline.jlでの例 -->
[![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://hyrodium.github.io/BasicBSpline.jl/stable)

ドキュメント (開発版)

最新のmainブランチに対応したバージョンのドキュメントへのリンク

Dev

<!-- BasicBSpline.jlでの例 -->
[![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://hyrodium.github.io/BasicBSpline.jl/dev)

Build Status

GitHub Actionsのテストが成功しているかを表すバッジ

Build Status

<!-- BasicBSpline.jlでの例 -->
[![Build Status](https://github.com/hyrodium/BasicBSpline.jl/workflows/CI/badge.svg)](https://github.com/hyrodium/BasicBSpline.jl/actions)

コードのカバレッジ

Codecovへのリンクとcoverageを表すバッジ

Coverage

<!-- BasicBSpline.jlでの例 -->
[![Coverage](https://codecov.io/gh/hyrodium/BasicBSpline.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/hyrodium/BasicBSpline.jl)

Aqua.jl

Aqua.jlでテストしていることを表すバッジ

Aqua QA

[![Aqua QA](https://raw.githubusercontent.com/JuliaTesting/Aqua.jl/master/badge.svg)](https://github.com/JuliaTesting/Aqua.jl)

arXiv

リポジトリに対応するarXivの論文があることを表すバッジ

arXiv

<!-- ElasticSurfaceEmbedding.jlでの例 -->
[![arXiv](https://img.shields.io/badge/math.DG-arXiv%3A2211.06372-B31B1B.svg)](https://arxiv.org/abs/2211.06372)

DOI

リポジトリに対応するDOIがあることを表すバッジ

DOI

<!-- BasicBSpline.jlでの例 -->
[![DOI](https://zenodo.org/badge/258791290.svg)](https://zenodo.org/badge/latestdoi/258791290)

Package downloads

パッケージのダウンロード数を表すバッジ

BasicBSpline Downloads.

<!-- BasicBSpline.jlでの例 -->
[![BasicBSpline Downloads](https://shields.io/endpoint?url=https://pkgs.genieframework.com/api/v1/badge/BasicBSpline)](https://pkgs.genieframework.com?packages=BasicBSpline).

code style

採用しているコードスタイルを表すバッジ

Code Style: Blue

<!-- BlueStyleでの例 -->
[![Code Style: Blue](https://img.shields.io/badge/code%20style-blue-4495d1.svg)](https://github.com/invenia/BlueStyle)

ColPrac

ColPracをリポジトリの規約として採用していることを明示するためのバッジ

ColPrac: Contributor's Guide on Collaborative Practices for Community Packages

[![ColPrac: Contributor's Guide on Collaborative Practices for Community Packages](https://img.shields.io/badge/ColPrac-Contributor's%20Guide-blueviolet)](https://github.com/SciML/ColPrac)

ドキュメント (Documenter.jl)

この節ではDocumenter.jlを使ったドキュメント生成について解説します。

docs/Project.toml

ドキュメント生成環境での仮想環境を管理するファイルです。
[deps]にDocumenter.jlを含めましょう。

docs/make.jl

ドキュメントを生成するためのJuliaスクリプトです。
基本的にはmakedocsでドキュメント生成してdeploydocsでドキュメントをデプロイします。

docs/make.jl
using Documenter
using MyGreatPkg

makedocs(;
    # オプション色々指定する
)

deploydocs(
    # オプション色々指定する
)

docs/Manifest.toml

このファイルもgitignoreして良いですが、手元の環境とドキュメント生成環境を一致させたい場合にはgit管理に含めることもあります。

docs/src/assets/logo.svg

ドキュメントのロゴ画像です。SVGだけでなく、PNGなども可能です。
明々後日の記事ではロゴについて書く予定です。お楽しみに。

docs/src/assets/logo-dark.svg

ドキュメントのロゴ画像です。ダークモードで使われる画像を別途配置可能です。

docs/src/assets/custom.css

基本的に不要ですが、ドキュメントの見た目を変更したい場合に使用します。
私はドキュメントのロゴの大きさを変更するために以下のように記述することが多いです。()

#documenter .docs-sidebar .docs-logo > img,
html.theme--documenter-dark #documenter .docs-sidebar .docs-logo > img {
    max-height: 8rem;
}

docs/src/assets/favicon.ico

ブラウザで表示した時に表示されるアイコンです。

docs/src/index.md

ドキュメントのトップページに関するmarkdownファイルです。

docs/src/hoge.md

他のページのmarkdownファイルはdocs/src以下に置きます。
ページの順番や構造はmakedocs内のキーワード引数で指定します。

ドキュメント (DemoCards.jl)

DemoCards.jlはデモを作りやすくするためのJuliaパッケージで、Documenter.jlのドキュメントに組み込むことができます。

詳細は記載しませんが、docs/src以下にデモページを置かないことに注意してください。
docs/srcの代わりに、docs/examplesのようなディレクトリをdocs以下に作成します。

examples
├── part1
│   ├── assets
│   ├── demo_1.md
│   ├── demo_2.md
│   └── demo_3.md
└── part2
    ├── demo_4.jl
    └── demo_5.jl

のようにファイルを配置すれば

# Examples
  ## Part1
    demo_1.md
    demo_2.md
    demo_3.md
  ## Part2
    demo_4.jl
    demo_5.jl

のようなmarkdownファイルが対応するようなデモページを作成することができます。

フォーマッタ

.JuliaFormatter.toml

JuliaFormatter.jlで使う設定を記述するファイルです。

.JuliaFormatter.toml
style = "blue"

などが最小限の構成です。

.pre-commit-config.yaml

pre-commitの設定もできます。

https://domluna.github.io/JuliaFormatter.jl/v1.0.39/integrations/#Integrations

GitHub Actions

.github/dependabot.yml

後述の.github/workfows以下のactionsに記載されたバージョンを自動的にアップデートするためのbot。[10]

https://github.com/JuliaArrays/StaticArrays.jl/blob/master/.github/dependabot.yml

.github/workflows/CI.yml

GitHub Actionsでtest/runtests.jlを実行するための設定。
julia-runtestを参考にしてください。
Quaternions.jlでの例も参考になると思います:

https://github.com/JuliaGeometry/Quaternions.jl/blob/main/.github/workflows/ci.yml

.github/workflows/Docs.yml

GitHub Actionsでdocs/makedocs.jlを実行するための設定。
julia-docdeployを参考にしてください。
PkgTemplates.jlではCI.ymlにドキュメント生成を含めていますが、分割した方が便利な場合もあります。[11]
こちらもQuaternions.jlでの例が参考になると思います:

https://github.com/JuliaGeometry/Quaternions.jl/blob/main/.github/workflows/docs.yml

.github/workflows/CompatHelper.yml

GitHub ActionsでCompatHelper.jlを実行するための設定。

https://github.com/JuliaRegistries/CompatHelper.jl/blob/master/.github/workflows/CompatHelper.yml

をコピペすればOK。

.github/workflows/TagBot.yml

パッケージをGeneralに登録したときに自動的にリリースを打つための設定。
GitHub ActionsでTagBotを実行するための設定。

https://github.com/JuliaRegistries/TagBot#setup に書かれてあるコードをコピペすればOK。

.github/workflows/Format.yml

GitHub ActionsでJuliaFormatter.jlを実行するための設定。
実行方法に関しては以下の2つの流儀があります。

  • PRごとに適切にフォーマッティングされているかチェックする
  • フォーマッティングのPRをbotが定期的に作成する

後者だと不要なcommitが増えてconflictを起こしがちなので、基本的には前者が良いと思います。
julia-formatのREADME.mdに記載されている設定を参照してください。[12]

.github/workflows/SpellCheck.yml

スペルチェックしてくれます。
Documenter.jlなどで導入されています。

https://github.com/JuliaDocs/Documenter.jl/blob/master/.github/workflows/SpellCheck.yml

.github/workflows/LabelCheck.yml

特定の種類のラベルが付いている時に間違ってmergeしないようにブロックしてくれます。

https://github.com/JuliaLang/julia/blob/master/.github/workflows/LabelCheck.yml

.github/workflows/Invalidations.yml

https://github.com/JuliaArrays/StaticArrays.jl/pull/1087 参照。

https://github.com/JuliaArrays/StaticArrays.jl/blob/master/.github/workflows/Invalidations.yml

おわりに

本記事で列挙したもの以外にも色々なパッケージ構成があると思います。
たとえば、以下の項目には意図的に触れませんでした。

  • 一つのリポジトリで複数のパッケージを管理する方法
  • Documenter.jl以外でのドキュメント生成
  • GitLabを使った場合のディレクトリ構成

「このファイルも便利だから用意した方が良いよ!」などあればコメントください!

脚注
  1. よっぽどメンテナンスされていないJuliaパッケージでは古い形式のREQUIREが存在してたりしますが、ほぼ見ないでしょう。参考: https://discourse.julialang.org/t/convert-require-to-project-toml-and-manifest-toml/17775/2 ↩︎

  2. アルファベット順ではなく、説明順にsortしてます。 ↩︎

  3. 他にもPkgSkeleton.jlもあったりしますが、PkgTemplates.jlが最有力だと思います。 ↩︎

  4. バージョンごとにgitのcommitがGeneralに登録されているためです。例えばBasicBSpline.jlでは https://github.com/JuliaRegistries/General/blob/master/B/BasicBSpline/Versions.toml のように登録されています。 ↩︎

  5. "Why We Created Julia"では"liberal license"としか言及されていなくて、MITライセンスとは書かれていませんでした。 ↩︎

  6. 例えば、PkgA.jlPkgB.jl(GPLライセンス)に依存していた場合に、PkgA.jlPkgB.jlの派生物とみなされるか(コピーレフトが適用されるか)が問題になります。Juliaのパッケージ依存はリンクではないので派生物でないと判断されることが通常ですが、一方で派生物に関する議論は紛らわしく、GPLではなくMITの方が好ましいと考える人も多いです。例えばDiscourseの https://discourse.julialang.org/t/gpl-and-virality/7715/11 を参照して下さい。 ↩︎

  7. https://github.com/JuliaRegistries/General/pull/31549#issuecomment-804196208 を参照してください。 ↩︎

  8. 公式ドキュメントJuliaCon2019の動画も参照してください。 ↩︎

  9. python、少なくともpytestでは複数形のtestsディレクトリが推奨されているようですね。https://docs.pytest.org/en/7.1.x/explanation/goodpractices.html#tests-outside-application-code 後述のdocsフォルダは複数形だし、英語は謎です。 ↩︎

  10. StaticArrays.jlにdependabotを追加するPRdependabotが送ったPRも参照して下さい。 ↩︎

  11. CI.yml側で定期実行させる場合など。 ↩︎

  12. 最近私がメンテナンスして使えるようになりました。 https://github.com/julia-actions/julia-format/pull/24 をご覧ください。 ↩︎

GitHubで編集を提案

Discussion