最近自分のdotfilesを整備しているのだが、強い人たちのdotfilesを見ると、 make コマンドで実行するようになっているものが多いことに気がついた。 make は難しそうで敬遠していたが、少し調べてみるとdotfilesを作る時にとても便利であることがわかった。
makeとは
本来は、大規模なプログラムのコンパイルを自動化・効率化するための技術。 Makefile にファイル名・コンパイル用のコマンド・その他の設定などを書いて、 make コマンドで実行する。 Makefile の書き方次第では、変更があった部分だけを再コンパイルする等もできる。make は様々なOSにプリインストールされており、UNIX系のOSには GNU make として最初から入っている。また、特定の言語に依存したものでもない。
つまり、端末が変わりほとんど何も入っていない環境になったとしても、 make なら実行できる。dotfilesではポータビリティが非常に重視されるので、 make がよく使われている。
もちろんシェルスクリプトに全部のコマンドを書いても良いのだが、 make の場合、実行したいタスクごとに名前をつけることができ、全タスクの一括実行や、一部だけの実行もできる。そのため、 Makefile を起点としたdotfiles全体の管理がしやすくなる。成長した Makefile は、dotfiles全体のマニュアルのようになっていく。
Makefileの書き方
dotfilesで使う前に、本来の Makefile の書き方を整理しておく。なお、今回はプログラムをコンパイルする用途ではないので、 make 自体の詳細まで立ち入ることはせず、dotfilesに関係ありそうな機能だけをまとめる。 例は公式ドキュメントから抜粋した。
rule
Makefile 内の1つのタスクを rule という。 rule は基本的に以下のような構文で書く。
targets : prerequisites
recipe
…
targets はファイル名で、スペースで区切って複数指定できる。 prerequisites にもファイル名を指定する。 recipe には実行したいコマンドを書く。 recipe の冒頭はタブによるインデントが必須であることに注意。
targets のファイルが存在しないか、あるいは targets のファイルの更新時刻が prerequisites のファイルの更新時刻よりも古くなった場合、 recipe に書いたコマンドが実行される。このため、 targets にはコンパイル後に生成されるファイル、 prerequisites には人間がコードを書いたファイルを指定することで、コードが更新されたファイルのみが再コンパイルされるようにできる。
foo.o : foo.c defs.h
cc -c -g foo.c
実行したいときは、 make ${targets名} の形式で指定する。targets を何も指定しないと、Makefile 内の一番上に書かれた targets が実行される。
phony targets
先程 targets にはファイル名が記載されると書いたが、ファイル削除など、新しいファイルを生成しないコマンドを Makefile 内の recipe として指定したいという場合もある。例えば、以下のような rule を書いたとする。
clean: rm *.o temp
このとき、make clean とすればrecipeが実行される。しかし、万が一 clean という名前のファイルができてしまった場合、 prerequisites が指定されていないので、 recipe は二度と実行されなくなる。
そこで、clean を .PHONY という特別な targets 名の prerequisites にすることで、 clean というファイルの有無に関係なく recipe が実行されるようになる。
.PHONY: clean clean: rm *.o temp
また、以下のように all を使って複数の targets を指定し、それを .PHONY に渡すことで、 make だけですべての rule を実行できる。もちろん、 make prog1 とすれば prog1 のみの実行もできる。
all : prog1 prog2 prog3 .PHONY : all prog1 : prog1.o utils.o cc -o prog1 prog1.o utils.o prog2 : prog2.o cc -o prog2 prog2.o prog3 : prog3.o sort.o utils.o cc -o prog3 prog3.o sort.o utils.o
dotfilesではどのように使うか
例えば、設定したいパッケージごとに Phony targets を作り、それぞれの recipe にコマンドを記載する。それらのパッケージを all で指定すれば、 make だけですべての設定が完了する。
.PHONY: all all: git vim tmux .PHONY: git git: ln -snfv ${PWD}/.gitconfig ${HOME} .PHONY: vim vim: ln -snfv ${PWD}/.vimrc ${HOME} curl -o ${HOME}/.vim/colors/iceberg.vim --create-dirs https://raw.githubusercontent.com/cocopon/iceberg.vim/master/colors/iceberg.vim .PHONY: tmux tmux: ln -snfv ${PWD}/.tmux.conf ${HOME}
上記のやり方はパッケージ単位で targets を作っているが、それぞれの処理を単位として targets を作っても良い。例えば、上記の例で各 recipe の中に出てきている「設定ファイルをホームディレクトリにリンクする」作業を link.sh 等のシェルスクリプトにまとめ、以下のようにすれば make link で全パッケージのリンク作業が完了する。
.PHONY: link link: bash ${PWD}/link.sh
また、 Makefile の良いところは、 all に含めない targets も作れるところにある。いろいろな人の Makefile を見ると、 help や test 等の Phony targets を作っている人もいるようだ。
環境移行はいつ起きるかわからず、移行作業自体にも時間をかけたくはない。今のうちから管理・実行のしやすいdotfilesを少しずつ作っておきたい。