🤔

Juliaパッケージ開発のアレってどうやんだっけ?となった時に見る記事

に公開

はじめに

久々にJuliaのパッケージメンテナンスを再開した時にアレどうなってたっけ?となることが多かったので、雑多ですが列挙していきます。

  • 共有パッケージ環境を使用する方法
  • リリース時にのみドキュメントが生成できない問題を解決する方法
  • ローカル環境でのテストのcoverage取得
  • Documenter.jlで生成したドキュメントをローカルで確認
  • jldoctestの更新
  • テストの実行環境を用意
  • コード検索

Julia Advent Calendar 2025の17日目の記事のはじまりでした。一日遅れですみません。
よろしくおねがいします。

共有パッケージ環境を使用する方法

先に結論を提示

juliaのCLI引数で--project=@pkgdevのように@<shared env name>を指定すれば共有パッケージ環境でJuliaが起動します。

julia --project=@pkgdev

(重要) 今回の記事で使用するパッケージは以下のコマンドでインストールできます。

julia --project=@pkgdev --startup-file=no -e 'using Pkg; Pkg.add(["Documenter", "DocumenterTools", "Coverage", "LiveServer"])'

詳細をもう少し解説

Juliaのパッケージ環境には大雑把に言って「グローバル環境のv1.x」「ディレクトリごとのプロジェクト環境」「共有プロジェクト環境」「スクリプト環境」の4種類があります。

  • グローバル環境のv1.x
    • --projectを指定しなかった場合に起動するデフォルトの環境
    • ~/.julia/environments/v1.x/Project.toml
  • ディレクトリごとのプロジェクト環境
    • Project.tomlの配置されたディレクトリを直接指定して起動する環境
    • path/to/Project.toml
  • 共有プロジェクト環境
    • ディレクトリを指定せずに実行できるようなプロジェクト環境
    • ~/.julia/environments/<shared env name>/Project.toml
  • スクリプト環境

この共有プロジェクト環境というのが便利で、以下のような要望を満たしてくれるのですよね。

  • デフォルトのグローバル環境のようにどのディレクトリに居たとしても使いたい
  • しかしパッケージ間の依存関係を解決するために複数のグローバル環境を切り替えたい

この機能はv1.7辺りから存在してたようなのですが、私は最近になってRunic.jlのインストール方法を見て知りました。もしかして知らなかったの私だけか????????[1]

リリース時にのみドキュメントが生成できない問題を解決する方法

先に結論を提示

以下のコードをREPLとかで実行して出力に従ってキーをGitHubリポジトリに登録すれば良い。

using DocumenterTools
DocumenterTools.genkeys(; user="GitHubUserName", repo="MyPkg.jl")

詳細をもう少し解説

JuliaではDocumenter.jlでドキュメント生成する慣習があります。

通常の開発時のワークフローはこんな感じ:

  1. git pushかPRをmergeしてデフォルトブランチを更新
  2. デフォルトブランチの更新を検知して.github/workflows/Docs.ymlを起動
  3. GitHub Actions上でドキュメントビルドしてgh-pagesブランチにpush
  4. デプロイされたドキュメントが閲覧できるようになる

リリース時のワークフローはこんな感じ:

  1. @JuliaRegistrator registerのように依頼してパッケージリリース申請
  2. Generalに登録されたことを検知してTagBotがリリースを打つ
  3. 新規タグの更新を検知して.github/workflows/Docs.ymlを起動
  4. GitHub Actions上でドキュメントビルドしてgh-pagesブランチにpush
  5. デプロイされたドキュメントが閲覧できるようになる

ここの「新規タグの更新を検知して.github/workflows/Docs.ymlを起動」に失敗しているケースが多いんですよね。
.github/workflows/Docs.ymlを用意してタグ追加でトリガーされると記載されていても、botが作ったタグには反応してくれないケースがあるんですよね。
これを解決するには、冒頭で述べたようなコマンドを実行して案内にしたがってキーを追加すればOKです。
私(hyrodium)の作っているDesmos.jlというパッケージの場合だとこんな感じ:

using DocumenterTools
DocumenterTools.genkeys(; user="hyrodium", repo="Desmos.jl")

出力は以下のようになります。Infoの内側にURLが2つ記載されているので、ここにアクセスして登録するだけでOKです。

julia> DocumenterTools.genkeys(; user="hyrodium", repo="Desmos.jl")
┌ Info: Add the key below as a new 'Deploy key' on GitHub (https://github.com/hyrodium/Desmos.jl/settings/keys) with read and write access.
└ The 'Title' field can be left empty as GitHub can infer it from the key comment.

ssh-rsa AAAAB3Nz(中略)hmcQISM= Documenter

[ Info: Add a secure 'Repository secret' named 'DOCUMENTER_KEY' (to https://github.com/hyrodium/Desmos.jl/settings/secrets if you deploy using GitHub Actions) with value:

LS0tLS1C(中略)LS0tLQo=

すいません、書いてから気づきましたが、ごまふあざらしさんが同じ内容の記事を書いてくれてましたね。
https://zenn.dev/terasakisatoshi/articles/87e730a50915f9

以降のセクションでは、(たぶん)他の日本語記事と内容が被ってない気がするので許してください!!!

ローカル環境でのテストのカバレッジ取得

先に結論を提示

以下のコマンドを~/.julia/dev/MyPkg以下で実行すればカバレッジが~/.julia/dev/MyPkg/coverage/index.htmlに出力されます。(注意: genhtmlコマンドの事前インストールが必要)

julia --project=. --startup-file=no -e 'using Pkg; Pkg.test(basename(pwd()); coverage=true)' && julia --project=@pkgdev --startup-file=no -e 'using Coverage; coverage=process_folder(); LCOV.writefile("coverage-lcov.info", coverage)' && genhtml coverage-lcov.info --output-directory coverage

詳細をもう少し解説

GitHub上でパブリックリポジトリとしてパッケージ開発を進めている場合には.github/workflows/Test.yml(Desmos.jlでの例)を設定すればcodecov.ioとかでカバレッジが閲覧できますよね。
具体的には以下のような手順です。

  1. パッケージ本体(src/)とテスト(test/)の実装を進める
  2. 定期的にcommitしてGitHubにpushする
  3. CIが走ってcodecov.ioにカバレッジがアップロードされるので目視確認する

https://app.codecov.io/gh/hyrodium/Desmos.jl/tree/main/src

しかし以下のようなケースではローカル環境でのカバレッジ取得が欲しくなります。

  • プライベートリポジトリで作業しているので、codecov.ioを使えない
  • CIを待つ時間がもったいないので、ローカル環境でカバレッジ確認したい
  • Claude Code等のツールからカバレッジ取得するにはローカルファイルにカバレッジが出力される方が好ましい

そこで実行するのが前述のコマンドですね。
あれを実行すればカバレッジの測定結果が人間に見やすい形式で~/.julia/dev/MyPkg/coverage/index.htmlに出力されます。

Codecovのようなイケてる見た目ではないですが、ローカルで現在のテストコードでのカバレッジを確認する分には十分でしょう。

冒頭のコードの解説に移ります。
&&で繋いでましたが、実質的に以下の3つのコマンドを実行していました。

# 現在のパッケージディレクトリでテストを実行。カバレッジ取得モードをtrueに設定。
julia --project=. --startup-file=no -e 'using Pkg; Pkg.test(basename(pwd()); coverage=true)'

# Coverage.jlを利用して`coverage-lcov.info`ファイルを出力
julia --project=@pkgdev --startup-file=no -e 'using Coverage; coverage=process_folder(); LCOV.writefile("coverage-lcov.info", coverage)'

# `coverage-lcov.info`をベースに`coverage`ディレクトリに人間可読性の高いHTMLファイルを出力
genhtml coverage-lcov.info --output-directory coverage

最初のテストではsrc/*.jl.*.covのようなJulia言語特有のカバレッジ記録ファイルが出力されています。
これを、lcov形式という他の言語でも共通のカバレッジ記録用フォーマットに変換するのがCoverage.jlの役割ですね。
lcov形式から人間の読みやすいフォーマットに変換するコマンドが最後のgenhtmlコマンドです。[2]

Documenter.jlで生成したドキュメントをローカルで確認

先に結論を提示

以下のコマンドをパッケージのルートディレクトリで実行すれば localhost:8000 などのアドレスからドキュメントがプレビューできます。

julia --project=docs --startup-file=no -e 'using Pkg;Pkg.develop(PackageSpec(path=pwd()));Pkg.instantiate();include("docs/make.jl");' && julia --project=@pkgdev --startup-file=no -e 'using LiveServer; serve(dir="docs/build")'

詳細をもう少し解説

GitHubでパッケージを管理している場合は、GitHub Pagesにデプロイされたドキュメントを直接確認することもできます。
しかしやっぱりローカル環境でドキュメントをプレビューできる方が使い勝手が良いですよね。
そういうときに前述のコマンドが役立ちます。

# ドキュメントを生成
julia --project=docs --startup-file=no -e 'using Pkg;Pkg.develop(PackageSpec(path=pwd()));Pkg.instantiate();include("docs/make.jl");'

# ビルドされたドキュメントを表示するサーバーを立ち上げる
julia --project=@pkgdev --startup-file=no -e 'using LiveServer; serve(dir="docs/build")'

jldoctestの更新

先に結論を提示

以下のコマンドを~/.julia/dev/<MyPkg>以下で実行すればjldoctestブロックが更新されます。
忙しい人向け。長過ぎるワンライナーなので、本来ならちゃんとシェルスクリプトとして整備するのが良いでしょう。

[ -f docs/make.jl ] && julia --project=docs --startup-file=no -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate(); using Documenter; Documenter.deploydocs(kwargs...) = nothing; try include("docs/make.jl"); catch end; thispkg = getfield(Main, Symbol(basename(pwd()))); doctest(thispkg; fix=true)' || julia --project=@pkgdev --startup-file=no -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate(); mkpath("docs/src"); using Documenter; pkg_sym = Symbol(basename(pwd())); Core.eval(Main, :(using $pkg_sym)); thispkg = getfield(Main, pkg_sym); Documenter.DocMeta.setdocmeta!(thispkg, :DocTestSetup, :(using $pkg_sym); recursive=true); doctest(thispkg; fix=true)'

詳細をもう少し解説

jldoctestというのはJulia言語で使用されるdoctestのことですね。
以下のように書くものです。

module MyPkg

"""
```jldoctest
julia> using MyPkg  # jldoctestに毎回書きたくはない。省略可能(後述)。

julia> myfunc(2)
3

julia> myfunc(-3)
8
```
"""
myfunc(x) = x^2 - 1

end

jldoctestは通常のtestディレクトリ内のテストとは別のもので、docstring内にREPLの出力結果として記述できることが特徴です。
しかしこのテスト、ドキュメントの更新がコード更新に間に合っていなかったり、出力フォーマットが微妙に変わったり[3]とかで、最新の状態に保たれていないことが多いんですよね。

この壊れがちなdoctestを更新するにはDocumenter.jlの力を借りて以下のようなコマンドを実行します。

using Documenter, MyPkg
doctest(MyPkg, fix=true)

もっとも短いケースではこれで十分なのですが、冒頭のusing MyPkgを省略したいときには以下のようにDocumenter.DocMeta.setdocmeta!を宣言する必要があります。

using Documenter, MyPkg
Documenter.DocMeta.setdocmeta!(MyPkg, :DocTestSetup, :(using MyPkg); recursive=true)
doctest(MyPkg, fix=true)

JuliaのREPLは優秀で便利ではあるのですが、上記のコードをパッケージごとに毎回入力するのは面倒ではあります。
しかもsetdocmeta!の引数はパッケージごとに単純に切り替えられるとは限らず、状況によってはusing MyOtherPkgを読み込む必要があったり、Random.seed!(42)を呼び出したりすることがあるのですよね。

この問題を解決するコマンドが前述の結論で提示したJuliaコマンドです。
あれの中身を展開してインデントを揃えると以下のようになります。

docs/make.jlが存在する場合
using Pkg
Pkg.develop(PackageSpec(path=pwd()))
Pkg.instantiate()
using Documenter
Documenter.deploydocs(kwargs...) = nothing
try
    include("docs/make.jl")
catch
end
thispkg = getfield(Main, Symbol(basename(pwd())))
doctest(thispkg; fix=true)
docs/make.jlが存在しない場合
using Pkg
Pkg.develop(PackageSpec(path=pwd()))
Pkg.instantiate()
mkpath("docs/src")
using Documenter
pkg_sym = Symbol(basename(pwd()))
Core.eval(Main, :(using $pkg_sym))
thispkg = getfield(Main, pkg_sym)
Documenter.DocMeta.setdocmeta!(thispkg, :DocTestSetup, :(using $pkg_sym); recursive=true)
doctest(thispkg; fix=true)

まだ少し分かりにくいですね。
Desmos.jlが対象パッケージだとして変数を展開して、コメントも追加してみましょう。

docs/make.jlが存在する場合
# Desmos.jlをdevとして依存関係に追加
using Pkg
Pkg.develop(PackageSpec(path=pwd()))
Pkg.instantiate()
using Documenter
# 以降の`make.jl`の実行時に不要なデプロイ処理を避けるためにメソッドを上書きして無効化しておく
Documenter.deploydocs(kwargs...) = nothing
try
    # `setdocmeta!`の設定を読み込むためにincludeする
    # このファイルの中で実行される`Documenter.makedocs`は不要に思えるが、後段の`doctest`で`makedocs`が呼ばれるので無効化してはいけない
    include("docs/make.jl")
catch
end
# ここでdoctestを更新する
doctest(Desmos; fix=true)
docs/make.jlが存在しない場合
# Desmos.jlをdevとして依存関係に追加
using Pkg
Pkg.develop(PackageSpec(path=pwd()))
Pkg.instantiate()
# doctestの実行には`docs/src`ディレクトリが必要らしいので作っておく (空でOK)
mkpath("docs/src")
using Documenter
using Desmos
# doctest実行時に`using MyPkg`を省略するのは標準的な作法だとして`setdocmeta!`しておく
Documenter.DocMeta.setdocmeta!(Desmos, :DocTestSetup, :(using Desmos); recursive=true)
# ここでdoctestを更新する
doctest(Desmos; fix=true)

このようなちょっと面倒なコードを実行してくれるのが前述のワンライナーでした。[4]

テストの実行環境を用意

先に結論を提示

TestEnv.jlを使えば良い。
ただしインストール先は「グローバル環境のv1.x」であって、「ディレクトリごとのプロジェクト環境」や「共有プロジェクト環境」ではない。[5]

~/.julia/dev/Desmosをプロジェクト環境としてJuliaを起動した場合
julia> using TestEnv;

julia> TestEnv.activate();

julia> using Desmos, Aqua  # 通常のパッケージ環境だとAquaはusingできない

詳細をもう少し解説

パッケージのために定義される環境は「ディレクトリごとのプロジェクト環境」に該当します。
例えばDesmos.jlの場合は~/.julia/dev/Desmos/Project.tomlに依存関係などが記載されています。
このプロジェクトファイルを指定した場合には「パッケージが依存しているパッケージ」のみが使用可能な状態でJuliaが起動します。

~/.julia/dev/Desmosをプロジェクト環境としてJuliaを起動した場合
julia> using Desmos  # もちろんDesmos.jlはインポートできる

julia> using Aqua  # しかし[extras]内のAqua.jlは使用できない
ERROR: ArgumentError: Package Aqua not found in current path.
- Run `import Pkg; Pkg.add("Aqua")` to install the Aqua package.
Stacktrace:
 [...]

しかし、このプロジェクトファイルにはテスト用のパッケージ依存関係も[extras]として記載されています。[6]
このようなテスト用パッケージまで使用可能な状態でJuliaを起動するときに便利なのが、前述のTestEnv.jlです。
この関数を読み込んでTestEnv.activate()すればテスト用のパッケージ環境が揃った状態になります。

~/.julia/dev/Desmosをプロジェクト環境としてJuliaを起動した場合
julia> using TestEnv  # ~/.julia/dev/Desmos/Project.tomlの依存関係には記載されていないが、グローバル環境(v1.x)にインストール済みなので使用可能

julia> TestEnv.activate();

julia> using Desmos  # もちろんDesmos.jlはインポートできる

julia> using Aqua  # [extras]内のAqua.jlも使えるようになった!

便利で良いですね。

コード検索

先に結論を提示

以下の2種類の方法でコード検索ができるので便利

  • GitHub上のコード検索
  • JuliaHubによるコード検索

詳細をもう少し解説

パッケージ開発中に「この関数は他のパッケージでどう使われているか」を知りたくなることがあります。
そんなときに便利なのがコード検索です。

GitHub上のコード検索

多くのJuliaパッケージがGitHubにホストされているので、GitHubのコード検索が使えます。
GitHub Code Searchにアクセスして、言語をJuliaに限定して検索すれば効率的です。

例えばDocMeta.setdocmeta!の使用例を探したい場合は以下のように入力します。

DocMeta.setdocmeta! language:Julia

他にも以下のような検索オプションが便利です:

検索条件 検索コマンド
特定のディレクトリ内に限定 using Documenter path:docs/ language:Julia
特定のファイル名を指定 makedocs path:**/make.jl language:Julia
特定の組織/ユーザーのリポジトリに限定 @assert org:JuliaLang language:Julia
ファイル拡張子で絞り込み struct path:*.jl

JuliaHubによるコード検索

JuliaHubのコード検索も便利です。

こちらにはGitHubのコード検索に比べて以下のようなメリットがあります:

  • Generalに登録済みパッケージに限定されるのでコード品質が多少は期待できる
  • Julia言語に特化した検索機能が利用可能
  • GitHubだけでなくGitLabなどの他のホスティングサービスも検索対象になる

特に開発者の立場からは以下のような場面で便利です。

  • 新しくexportする識別子が他のパッケージと重複していないか確認したい
  • 自分のメンテナンス中のパッケージの機能を廃止する際に与える、他のパッケージへの影響を確認したい

おわりに

  • 他にTipsあればコメントで教えてください![7]
  • 本文では触れませんでしたが、Modern Julia WorkflowsにはJulia言語のtipsが体系的に整理されているのでオススメです。
  • 質問あれば、本記事へのコメントでもJuliaLangJaのDiscordでの投稿でもOKです!
脚注
  1. 現在(Julia v1.12.3)のjulia --helpのメッセージには少なくともこの機能の説明は無いんですよね。あとで余裕のあるときにPRつくります。 ↩︎

  2. genhtmlコマンドはManjaroなどではpacman -S lcovでインストール可能です。ところで、このコマンド名はImageMagickのconvertくらい酷い命名じゃないですか? ↩︎

  3. 通常、Base.showで出力される文字列の変更は破壊的変更として扱われません。そのため、出力文字列の比較として実行されるjldoctestは依存パッケージでのBase.showが更新されたりするだけで容易に壊れてしまいます。 ↩︎

  4. このような処理を自動でGitHub Actionsが定期的に実行してくれるのが理想的だと思いますよね?実はactions/julia-fix-doctestsというものがあるんですが、数年間メンテナンスされておらず、本記事で示したようにdocs/make.jlを読み込んだりはしてくれません。気が向いた時にPR送ってみようと思います。 ↩︎

  5. もしかすると私が知らないだけで「共有プロジェクト環境」からTestEnv.jlを使う方法はあるかも知れないです。 ↩︎

  6. [extras]の代わりにtest/Project.tomlファイルを用意してテスト用のパッケージ環境を構築することも可能です。別ファイルで管理する方法の方が新しく、(どちらかと言えば)推奨されているようです。詳細はPkg.jlの公式ドキュメントを確認してください。 ↩︎

  7. 私はプロファイラとかデバッガの周辺には明るくなく、その辺りのことは本記事で書いてませんでした。 ↩︎

GitHubで編集を提案

Discussion