PkgTemplates.jl をベースに VSCode と Docker を組み合わせる自作テンプレートを作ります

# 本日は

Julia アドベントカレンダーネタです．
本日は [MyTemplate.jl](https://github.com/terasakisatoshi/MyTemplate.jl) というPkgTemplates.jl をベースに自分好みのテンプレートを作成する方法を紹介します．

# 背景

[MyWorkflow.jl](https://github.com/terasakisatoshi/MyWorkflow.jl) というリポジトリで Julia + Docker + Jupyter Notebook の組み合わせをする心地いいワークフローを開発していました．MyWorkflow.jl 自身は 別の Qiita でも書いています.

個人の自作パッケージを作る際や Julia に限らず Python で（個人的な）開発するときにもこのリポジトリで用いていたファイルをよく愛用していました．

とりあえず `make` コマンドを叩けば環境が構築でき，`docker-compose up lab` で Jupyter 環境を立ち上げることができるのはとても便利です（と思っています）

さて，問題だったのは新しい Julia のプロジェクトを行う際に， MyWorkflow.jl から `Dockerfile` や `Makefile`, `docker-compose.yml`, `.devcontainer/devcontainer.json` をいちいちコピペするという作業が必要でした．そしてイメージ名を変更したりとか．．．置換作業が必要でした．

## なければテンプレートを作ればいいじゃない！

そもそも MyWorkflow.jl 自身が PkgTemplates.jl から作られたのでした.

[PkgTemplates.jl](https://github.com/invenia/PkgTemplates.jl) は Julia プロジェクトを開発する時のテンプレートを作ってくれる Julia のパッケージです．

# さて本題（MyTemplate.jl の使い方）

例えば `YourPkg.jl` を作りたいとします. ひとまず下記のようにするとテンプレートができます．

```console
$ git config --global github.user "あなたのGitHubアカウント名を入力" # これは初回のみ
$ git clone https://github.com/terasakisatoshi/MyTemplate.jl.git # コードをクローン
$ cd MyTemplate.jl
$ julia --project=@. -e 'using Pkg; Pkg.instantiate()' # `generate.jl` を使うための依存関係を追加
$ julia --project=@. generate.jl YourPkg --with-jupyter # YourPkg という名前のパッケージ名を作る
```

このようにすると `YourPkg.jl` というディレクトリができます. `tree` コマンドでディレクトリ構造を眺めてみましょう

```console
$ cd YourPkg.jl
$ tree
.
├── Dockerfile
├── LICENSE
├── Makefile
├── Manifest.toml
├── Project.toml
├── README.md
├── docker-compose.yml
├── docs
│   ├── Manifest.toml
│   ├── Project.toml
│   ├── make.jl
│   └── src
│       └── index.md
├── jupytext.toml
├── playground
│   ├── notebook
│   └── pluto
├── src
│   └── YourPkg.jl
└── test
    └── runtests.jl
```

`jupytext.toml` や `Dockerfile`, `docker-compose.yml`, `Makefile`, `playground/` などが含まれています．これがオレオレテンプレートで，それ以外は PkgTemplates.jl がよしなに作成したものです.

## テンプレートから作成できたら

作業する場所を `YourPkg.jl` に移動して `make` コマンドを叩きます

```console
$ cd YourPkg.jl
$ make
...
ちょっと時間がかかる
```

`make` コマンドで Jupyter 環境をコンテナ内で立ち上げるためのDockerイメージをビルドします(M1 Mac 環境だと JupyterLab 関連の extension のビルドが失敗するかもしれないです. 実機持ってないので GitHub Sponsor とかで経済的支援していただけると対応します)

イメージがビルドできたら `docker-compose up lab` とすると `docker-compose.yml` で定義したサービスを立ち上げることができます.

```
$ docker-compose up lab
...
... (色々ログが出力される)
...
       |     To access the server, open this file in a browser:
       |         file:///root/.local/share/jupyter/runtime/jpserver-1-open.html
       |     Or copy and paste one of these URLs:
       |         http://320812028b46:8888/lab?token=xxxxxxxx
       |      or http://127.0.0.1:8888/lab?token=xxxxxxxx
```

こういうログが出てきたらブラウザを開いて `http://127.0.0.1:8888/lab?token=xxxxxxxx` にアクセスするとカスタマイズされた JupyterLab 環境が得られます.

# Jupyter が不要な場合

とりあえず VSCode と組み合わせるだけに興味があって Jupyter 環境が不要な場合は上記で使用した `generate.jl` のオプションでつけていた `with-jupyter` を使わずに `generate.jl` を実行してください:

```console
$ https://github.com/terasakisatoshi/MyTemplate.jl.git # コードをクローン
$ cd MyTemplate.jl
$ julia --project=@. -e 'using Pkg; Pkg.instantiate()' # `generate.jl` を使うための依存関係を追加
$ julia --project=@. generate.jl YourPkg # YourPkg という名前のパッケージ名を作る
$ cd YourPkg
$ docker-compose run --rm julia
               _
   _       _ _(_)_     |  Documentation: https://docs.julialang.org
  (_)     | (_) (_)    |
   _ _   _| |_  __ _   |  Type "?" for help, "]?" for Pkg help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 1.6.4 (2021-11-19)
 _/ |\__'_|_|_|\__'_|  |  Official https://julialang.org/ release
|__/                   |

julia> # あとはお好きにどうぞ
```

こんな感じで楽にできるので使ってみてください.

## VSCode との連携

VSCode の Remote Containers の機能を利用するだけです. いろんな設定は `.devcontainer/devcontainer.json` で設定されているのでよしなにDockerによるコンテナ環境の中で作業できるようになります.

https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.vscode-remote-extensionpack

をインストールすると使えるようになります. そのあとは作成した `YourPkg.jl` ディレクトリで `code .` で開いたディレクトリをプロジェクトとしてみなしながら VSCode を立ち上げます. 画面左下に `><` というマークがあるのでそれをクリックして `ReOpen in Container` をクリックするとコンテナ内部で作業することができます.　具体的な操作方法は Julia と関係ないので VSCode Docker などでググって調べてください．例えば
外部のブログ [VS Code + Remote Containers で快適な Dev ライフを](https://www.sun-m.co.jp/blog/tips/414.html) がとてもわかりやすいです.

# 自作テンプレートを追加する方法

例えば自作パッケージを作るときに自分が使う定型ファイル `oreorefile.txt` も入れ込んでおきたい！とします．そのテンプレートはパッケージ名に依存しているとしましょう. なんでもいいので例えば下記のようなテンプレートを作っておきます:

```templates/oreorefile.txt
Hi, I'm {{name}}. {{{PKG}}} is my julia package.
```

`{{name}}` や `{{{PKG}}}` の部分が動的に変わってほしい部分です. 例えば David さんが `YourPkg` を作るのであれば `Hi, I'm David. YourPkg is my julia package` というファイルができて欲しいとしましょう．

## `FilePlugin` のサブタイプを作る

ここでは Julia コードを持ってきて説明します. `PkgTemplates.jl` ではファイルのテンプレートをハンドリングするために `FilePlugin` という抽象型を定義しています. この型のサブタイプとして `oreorefile.txt` を操作するための構造体(ここでは `OreOreFile` という自作型)と対応する構造体に対して幾つかのメソッドを定義することになります. あまりJuliaに慣れてない，でもオブジェクト指向なら知っているよ！って読者なら，`FilePlugin` というのは基底クラスで `OreOreFile` は `FilePlugin` を継承するというノリで読んでください. 幾つかのメソッドを定義するのは `FilePlugin` を継承するクラスが持っているべきメソッドをオーバライドするものと解釈してください.

メソッドは `source`, `destination`, `view` というメソッド（関数の具体的な手続き）を `OreOreFile` 構造体に対して実装することになります.

- `source` はテンプレートファイルのパスを返す
- `destination` はテンプレートから作られたファイルを配置するパス(作成されるパッケージのディレクトリからの相対パス)を返す
- `view` `{{name}}` や `{{{PKG}}}` となっているところに何を代入するかを指定するために必要な対応表(Juliaの辞書オブジェクトして使う)

`PKG` はPkgTemplates.jl の中ではパッケージ名を表す特別なキーワードになっています. ですので `{{{PKG}}}` と３重カッコが必要ですが，自作の変数であればカッコは2つで良いみたいです.

基本的に下記をそのままコピペすれば良いでしょう:

```julia:oreorefile.jl
Base.@kwdef struct OreOreFile <: FilePlugin
    file::String = "templates/oreorefile.txt"
    destination::String = "oreorefile.txt"
    name::String = "David"
end

source(p::OreOreFile) = p.file
destination(p::OreOreFile) = p.destination
view(of::OreOreFile, ::Template, pkg::AbstractString) = Dict("PKG" => pkg, "name"=>of.name)
```

## `OreOreFile` 構造体のインスタンスを `plugins` 変数に渡す

あとは PkgTemplates.jl のお作法に則ればOKです. `PkgTemplates.Template` のインスタンスに渡す `plugins` 引数に先ほど定義した `OreOreFile` 構造体のインスタンスを含めればOKです. 下記のように `generate.jl` というスクリプト（名前はJuliaのスクリプトであればなんでも良い）を作ります．

```julia:generate.jl
using PkgTemplates
t = Template(;
    dir = pwd(),
    julia = v"1",
    plugins = [
        License(; name = "MIT"),
        Git(; manifest = false, ssh = true),
        OreOreFile(;name="あなたの名前を入れてね♪"),
    ]
)

t("YourPkg")
```

 `julia generate.jl` とすると無事にテンプレートから必要な情報が穴埋めされた `oreorefile.txt` が完成します.

# まとめ

こんな感じで Julia の型システムを使うと外部パッケージの機能と同等の機能を実現するオレオレ機能を実装し組み込むことができるようになります.
テンプレート作成だけでなく，Julia の型システムの一部も学ぶことができました．