rokoucha/dotfiles の異常な歴史 または私は如何にして高度化を止めて GNU Make を愛するようになったか

created at
updated at
archive
この記事は dotfiles Advent Calendar 2019 - Qiita の21日目の記事です。

この記事は2019-12-21 00:00:00に https://blog.at7s.me/article/13 へ投稿された記事のアーカイブです

どうも。

rokoucha/dotfiles の異常な歴史 または私は如何にして高度化を止めて GNU Make を愛するようになったか

この記事は dotfiles Advent Calendar 2019 - Qiita の21日目の記事です。

タイトルの元ネタは有名な映画である「博士の異常な愛情 または私は如何にして心配するのを止めて水爆を愛するようになったか」です。

私の dotfiles である rokoucha/dotfiles が管理を開始した2017年からどのような遍歴をたどってきたのかについてお話させて頂きます。

歴史

私の dotfiels の歴史は大きく分けて5期に分けられます。

0期: dotfiles 管理前

dotfiles を管理する前はどうしていたのか、その答えは「必要な物だけ Gist などに置いておく」でした。
例えば昔使っていた xmonad というウィンドウマネージャの設定ファイルは Gist に残っています。

1期: とりあえず公開

記念すべき1期はただ dotfile を配置しただけの簡単な物でした。
その時のツリー を見ると、丁度 i3 を使い始めた頃という事が分かります。

2期: インストールの自動化に挑戦 第1弾

自動セットアップに憧れてごにょごにょして心が折れたようで、ツリーにはインストールしてくれないインストールスクリプトが置いてあります。
その後、dotfile を追加したようで凄い量のファイルがツリーに出てきました。
この時は共通な dotfile と環境依存な dotfile を合成して各環境毎に最適な dotfiles を構築しようとしていたようです。

3期: インストールの自動化に挑戦 第2弾

諦めが悪いのでまた自動セットアップに挑戦しました。ツリーの見た目はあまり変わらないですね。
このころから .zsh フォルダに .zshrc.zshenv などの内容を分割して配置し、自動ロードするようにしたようです。
この仕組みはまだ使っていて、ちょっと遅くなる問題がありますがそこそこ便利なのでオススメです。

また、セットアップスクリプトがやっと完成しました。このセットアップシステムは第1世代ですね。

なお、セットアップシステムの仕様については後で解説します。

4期: インストールの自動化に挑戦 第3弾

この頃からまた複数環境に対応するシステムを構築しようと考え、uname を見たりして特定の環境だけ追加ファイルを展開するようにしたりしました。
ツリーを見ると分かるとおり、基本的には共通化し、Arch Linux と WSL でどうしても環境依存になってしまうファイルだけを分けるようにしました。

セットアップシステムとしては第2世代となります。

5期: 悟り←いまここ

他人の dotfiles と自分の dotfiles を見比べた時に、ファイルが散らばっていて見辛いと思うようになりました。
また、メンテナンスがめちゃめちゃダルいしなんかうまく展開出来ないという事が起きるようになりました。

「もうシェルスクリプトで管理するのはよくないのでは?」という悟りを開き、GNU Make に全てを委ねる事にしました。
ツリーを見ると分かる通り、最初期のように dotfiles がルートに配置されるようになり、見通しが良くなりました。

セットアップシステムは今までから刷新した第3世代です。

セットアップシステム

おまたせしました、セットアップシステムのお話です。

第1世代: installer.sh

/dotroot 内のファイルを ~ にリンクしていくだけの簡単なスクリプトです。

おもしろい点としては、ファイルのみリンクしフォルダは mkdir するようになっています。
というのも /.config を直接リンクしてしまうと、dotfiles で管理していないファイルがリポジトリに混入してしまいます。
それを防ぐために、わざわざフォルダを全部掘ってファイルだけリンクするようになっています。

_
shfor file in `\find $dotfiles_path/dotroot -type f`; do
	dotfile=${file#$dotfiles_path/dotroot/}
	dotpath=$(dirname "$dotfile")

	echo "Install $dotfile to $install_path/$dotfile"
	mkdir -p "$install_path/$dotpath"

	if [ -L $install_path/$dotfile ] ;then
		# もし、dotfileがシンボリックリンクなら消す
		rm "$install_path/$dotfile"
	elif [ -f $install_path/$dotfile ] ;then
		# もし、dotfileが既に存在するならバックアップ送り
		mkdir -p $old_dotfiles_path
		mv "$install_path/$dotfile" "$old_dotfiles_path/$dotfile"
	fi

	# 実際にリンクを張る
	ln -s "$file" "$install_path/$dotfile"
done

なお実際には既に存在している場合の処理などもあるためかなり複雑なロジックになっています。
よく自分で書いたなと思います、もう書ける自信がないです…

またフックが用意されているので、ファイルの展開後にプラグインマネージャをインストールしたりといった事も自動化できます。

第2世代: dotctl.sh

第1世代をベースに、uname による環境の判定や環境ごとに展開する内容を変更するようにしました。

環境の判定は普通に正規表現にマッチするかチェックしてるだけです。

_
sh# Environment detector
if expr "$(uname -r)" : ".*Microsoft$" > /dev/null; then
    # WSL
    _ENV="WSL"
elif expr "$(uname -r)" : ".*ARCH$" > /dev/null; then
    # ArchLinux
    _ENV="Arch"
else
    # Some linux
    _ENV=""
fi

ファイルを展開する部分は環境毎にフォルダを分けたのでより複雑になりました。

共通なファイルは common に、環境依存ファイルは適当な名前のフォルダに配置するようになっています。
そのため、どの環境でも common だけは必ず展開するようになっています。

_
shfor env in "common" $_ENV; do
    echo "# $env"
    # shellcheck disable=SC2044
    for app in $(find "$_DOTFILES/$env" -maxdepth 1 ! -path "$_DOTFILES/$env" -type d -exec basename {} \;); do
        echo "Deploy $app:"
        # shellcheck disable=SC2044
        for file in $(find "$_DOTFILES/$env/$app" -type f); do
            dotfile="${file#$_DOTFILES/$env/$app}"
            dotpath=$(dirname "$dotfile")

            echo " - $dotfile to $_INSTALL$dotfile"
            mkdir -p "$_INSTALL/$dotpath"

            if [ -L "$_INSTALL$dotfile" ] ;then
                # Delete when target is symbolic link
                rm "$_INSTALL$dotfile"
            elif [ -f "$_INSTALL$dotfile" ] ;then
                # Move when target is file
                mkdir -p "$_OLD_DOTFILES/$dotpath"
                mv "$_INSTALL$dotfile" "$_OLD_DOTFILES$dotfile"
            fi

            # Link file
            ln -s "$file" "$_INSTALL$dotfile"
        done
    done
done

でかいですね…

第3世代: Makefile

GNU Make に全てを委ねるようにしました。

GNU Make はとても便利で内部コマンドだけでも dotfiles をリストアップできちゃうのですが、コマンドを駆使してより便利にしました。

_
Makefile# 除外するファイル/フォルダ
EXCLUSION := .git/\* docker-compose.yml Dockerfile LICENSE Makefile README.md

# dotfiles をリストアップ
DOTFILES := $(shell printf " ! -path $(DOTFILES_PATH)/%s" $(EXCLUSION) | xargs find $(DOTFILES_PATH) -type f | sed 's|^$(DOTFILES_PATH)/||')

普通に EXCLUSION に指定されたファイル以外をリストアップしてるだけです。

展開はリストアップされたファイル一覧を内部コマンドの foreach で回してフォルダを掘ってリンクするだけです。
なんと1行で出来ちゃうんです!

_
Makefile$(foreach dotfile,$(DOTFILES),mkdir -p "$(INSTALL_PATH)/$(dir $(dotfile))"; $(LN) "$(abspath $(dotfile))" "$(INSTALL_PATH)/$(dotfile)";)

便利な時代になりましたね…

この世代からはただファイルを展開するだけではなく、ソフトウェアのインストールも出来るようにしました。
タスクとして用意する事で、従来のフックよりも柔軟に指定する事が可能になりました。

例えば Arch Linux ユーザー御用達の yay は次のタスクでインストールできます。

_
Makefileyay: git ## Install Yay
	@if ! type yay >/dev/null 2>&1; then \
		$(eval YAY_TEMP := $(shell mktemp -d)) \
		git clone https://aur.archlinux.org/yay.git "$(YAY_TEMP)"; \
		sh -c "cd \"$(YAY_TEMP)\"; makepkg -sri --noconfirm"; \
		rm -rf "$(YAY_TEMP)"; \
	fi
	yay -Syu --noconfirm

普通にコマンドをぺたぺた書くだけでタスクが作れちゃうんです!

グループ的な物も作れちゃいます。

_
Makefile##@ Group tasks
.PHONY: arch cli

arch-cli: yay docker git gnupg openssh vim xdg-user-dirs zsh ## Install Arch Linux CLI applications

cli: dircolos vundle zplugin zprezto asdf ## Install CLI applications

もうシェルスクリプトには帰れないですね…

また Docker でデバッグできるようにしたので、手元の環境を破壊しながらデバッグする必要もなくなりました。

dotfiles 盆栽を続けてきて思ったこと

最初の頃は何でもこの dotfiles で管理してやる!という気概があったのですが、今はほどほどに…と大分丸くなってしまいました。
まあそれだけ何でも管理するというのは難しいという事なんですよね、どんどん複雑化してしまうので…

GNU Make に委ねてからはある程度シンプルになって管理しやすくなったので、KISS の原則のありがたみを全身で味わってます。
皆さんも GNU Make に全てを委ねませんか?

将来の展望

冪等性を担保したいなーと思ってます。
冪等性を担保すればアンインストールも出来るようになりますし、他のタスクへの依存を明示できるようになるのでめちゃ便利になります。
あとは一回実行したタスクを何回も走らせなくて済むようにしたいですね、タスクを実行したかどうかをファイルがあるかで判別するのが一番楽なのですがファイル散蒔かれるのはちょっと…

Docker 化したので CI 回したりテスト実装したりもしたいところ。

おわりに

やっぱり dotfiles は便利です、たった1つのコマンドで環境を構築してくれるので手軽に環境を破壊できます。
皆さんもご自慢の dotfiles を公開してみませんか?