tl;dr
- Multiple Targets 使うと、複数ターゲットを1つのターゲットで表現できて便利
- 複数ターゲットがほぼ同じコマンドで構成されるときに使えて便利
- Makefileは奥が深い
はじめに
ほぼ同じコマンドで、パスだけ異なるターゲットを作成することがあり、そのときに知ったMakefileの機能を紹介します。
コードは以下においています。
Makefileで複数ターゲットを1ターゲットで表現する方法
Golangでは、cmd/
ディレクトリ配下に実行エンドポイントを複数置くことがあります。例えば、Web APIサーバーを開発する場合、APIサーバーの実行エンドポイント(cmd/api/main.go
)と、APIサーバーが参照するDBのマイグレーションツール (cmd/tools/main.go
) を置くことがあります。
.
├── Makefile
├── cmd/
│ ├── api/ # API サーバー実行エンドポイント
│ └── tools/ # CLI用の実行エンドポイント
└── go.mod
このとき、それぞれのコマンドのビルドコマンドを素直にMakefileに書くと、以下のようになります。見ての通り、ターゲット api
とtools
それぞれはほぼ同じコマンドであり、パス名のみ異なります。今後、コマンドの種類が増えると同じようなコマンドを持つターゲットを繰り返すことになり大変です。
ifndef GOARCH
GOARCH=$(shell go env GOARCH)
endif
ifndef GOOS
GOOS := $(shell go env GOOS)
endif
.PHONY: build-api
build-api:
CGO_ENABLED=0 GO111MODULE=on GOOS=$(GOOS) GOARCH=$(GOARCH) go build -o ./bin/api cmd/api/main.go
.PHONY: build-tools
build-tools:
CGO_ENABLED=0 GO111MODULE=on GOOS=$(GOOS) GOARCH=$(GOARCH) go build -o ./bin/tools cmd/tools/main.go
引数を受け取る_build
ターゲットを作り、そこに環境変数や引数でパス名を渡すのもよいのですが、このような場合に使えるMakefileの機能があります。それが複数ターゲット機能です。
こちらの機能を使い、汎用的なターゲットを作ると以下のようなMakefileとなります。こちらのMakefileによりcmd
配下のコマンドが増えても1つのターゲットで対応できます。
ifndef GOARCH
GOARCH=$(shell go env GOARCH)
endif
ifndef GOOS
GOOS := $(shell go env GOOS)
endif
COMMAND_DIRS := $(wildcard cmd/*)
BUILD_TARGETS := $(addprefix build-,$(notdir $(COMMAND_DIRS)))
.PHONY: $(BUILD_TARGETS)
$(BUILD_TARGETS): build-%:
CGO_ENABLED=0 GO111MODULE=on GOOS=$(GOOS) GOARCH=$(GOARCH) go build -o ./bin/$* -mod=vendor cmd/$*/main.go
上記Makefileで使用している機能も補足説明しておきます。
項目 | 説明内容 | 公式ページ |
---|---|---|
wildcard |
(wildcard pattern)という書き方で、パターンを展開したリストを返します。例えば、上記では $(wildcard cmd/*)= cmd/api cmd/tools`となります |
Wildcard Function (GNU make) |
notdir |
$(notdir paths…) という書き方でpathの親ディレクトリ部分は取り除いた結果を返します。例えば、$(notdir /hoge/foo hoge.c) は foo hoge.c を返します。 |
File Name Functions (GNU make) |
addprefix |
$(addprefix prefix,names...) という書き方で、namesには空白区切りのリストが与えられます。与えられたリストの各要素に、prefix という文字列が連結されます。 例えば、$(addprefix src/,foo bar) は src/foo src/bar となります。 |
File Name Functions (GNU make) |
$(BUILD_TARGETS): build-%: |
これは静的ルールと呼ばれるもので、$(BUILD_TARGETS) というターゲットリストにのみ、 build-% というルールを適用します。つまり、$(BUILD_TARGETS) にないような build-hoge などはターゲットになりません。 |
Static Pattern Rules |
% |
pattern ruleと呼ばれるもので、任意の一文字以上の文字にマッチするもの(stem)を表しています。例えば、上記では build-api やbuild-tools がマッチします。 |
Pattern Intro (GNU make) |
$* |
pattern ruleでマッチした部分を表す文字列(stem)に置換されます。例えば、build-api であれば、$* = api となります。 |
Automatic Variables (GNU make) |
小話
上記は、動的にターゲットが決まるため、どういうターゲットが存在するのかがパット見では分かりづらいのが悪い点です。
ただ、fish shell や CodeWhisperer(旧 fig) ではターゲットを解析してくれてちゃんと補完してくれるようです。
まとめ
ほぼ同じコマンドで、パスだけ異なるターゲットを作成するときに使える機能を紹介しました。
他にもMakefileが内蔵している機能は多くあるようなので、一度Makefileの公式ドキュメントを読んでみるのも面白いかもしれません。