Makefileで書く誰得スクリプト集 + ハマりポイント対処

# はじめに

基本的な Makefile の書き方はこの記事では紹介してません。
Makefile 初学者の方には以下の記事を先に読むことをおすすめします。
（私の記事ではないですが紹介させていただきます。）

- [Makefileの基本](https://zenn.dev/keitean/articles/aaef913b433677)

# 便利スクリプト集

## 色付き出力で結果を変わりやすく

こういった使い回したいスクリプトは `Makefile.common` などに記載し、 `Makefile` で `include Makefile.common` すると便利になります。

| 文字列        | 色   |
| ------------- | ---- |
| `\033[0;31m` | 赤色 |
| `\033[0;32m` | 緑色 |
| `\033[0;33m` | 黄色 |
| `\033[0m`      | 無色 |

`Makefile.common`
```makefile
.PHONY: echo_green
TEXT	?=	Success
echo_green:
	@echo "\n\033[0;32m${TEXT}\033[0m\n"

#-----------------------------------------------------------------------------------------------

.PHONY: echo_red
echo_red:
	@echo "\n\033[0;31m${TEXT}\033[0m\n"

#-----------------------------------------------------------------------------------------------

.PHONY: echo_yellow
echo_yellow:
	@echo "\n\033[0;33m${TEXT}\033[0m\n"
```

`Makefile`
```makefile
include Makefile.common

#-----------------------------------------------------------------------------------------------

.PHONY: test
test:
	@make echo_green
	@make echo_red TEXT="Error: something wrongs"
	@make echo_yellow TEXT="Warning: something is warning"
```

```bash
$ make test
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/908316/8f881eae-83ca-ea23-5d08-8db65439bbdb.png" width="400px" />


## 依存パッケージなどのインストールチェック

「`configure` でやることじゃね？」と思われた方もいるのでは、、、

`Makefile`
```makefile
include Makefile.common

#-----------------------------------------------------------------------------------------------

.PHONY: check_deps
check_deps:
	# Check node
	@if ! which node > /dev/null ; then \
		make echo_red TEXT="Error: node not installed" ; false ; \
	fi
	@make echo_green
	# Check yarn
	@if ! which yarn > /dev/null ; then \
		make echo_red TEXT="Error: yarn not installed" ; false ; \
	fi
	@make echo_green

#-----------------------------------------------------------------------------------------------

.PHONY: install
installl: check_deps
```

実行例
```
$ make check_deps
# Check node

Success

# Check yarn

Success

```

## 環境変数が指定されているかチェックする

```makefile
.PHONY: check_env
SAMPLE_ENV ?= NOT_SPECIFIED
check_env:
	@if [ ${SAMPLE_ENV} = "NOT_SPECIFIED" ] ; then \
		make echo_red TEXT="Error: SAMPLE_ENV not specified"; false ; \
	fi
```

実行例

```bash
$ make check_env SAMPLE_ENV=aaa # 正常終了
$ make check_env # エラー

Error: SAMPLE_ENV not specified

```

## アプリケーションを `systemd` のユニットサービスとしてインストールする

`systemd` 初学者向けの記事

- [これからSystemd入門する](https://qiita.com/bluesDD/items/eaf14408d635ffd55a18)

バイナリなどの必須ファイルのインストールやユニットファイルの配置をします。

```makefile
include Makefile.common

#-----------------------------------------------------------------------------------------------

## systemd unit file
define unit_file
[Unit]
Description=Sample
Documentation=Sample
After=network-online.target

[Service]
Type=simple
EnvironmentFile=${ENV_FILE_PATH}
ExecStart=${DIR_BIN}/sample $$ARGS

[Install]
WantedBy=multi-user.target
endef
export unit_file

## application env file
define env_file
ARGS="--port ${PORT}"
endef
export env_file

DIR_BIN			?= /usr/local/bin
UNIT_FILE_PATH	?= /usr/local/lib/systemd/system/sample.service
ENV_FILE_PATH	?= /usr/local/etc/default/sample
PORT			?= 80
.PHONY: install
install:
	# Install
	#@cp -b something ${DIR_BIN}/
	@echo "-> ${DIR_BIN}/something"
	@make echo_green

	# Install as systemd unit
	@echo "$$unit_file" > ${UNIT_FILE_PATH}
	@echo "-> ${UNIT_FILE_PATH}"
	@echo "$$env_file" > ${ENV_FILE_PATH}
	@echo "-> ${ENV_FILE_PATH}"
	@make echo_green
```

```bash
$ make install
# Install
#@cp -b something /usr/local/bin/
-> /usr/local/bin/something

Success

# Install as systemd unit
-> /usr/local/lib/systemd/system/sample.service
-> /usr/local/etc/default/sample

Success

$ systemctl daemon-reload # 配置したユニットファイルのロード
$ systemctl start sample.service # ユニットサービスの起動
```

## ヘルプの表示

変数についてまでヘルプに含む場合は可読性が低い記述になってしまうためおすすめはしませんが、
個人で使う分には細かく記載しておきたいことが多いので僕はよくこんな感じで書いてます。

```makefile
.DEFAULT_GOAL := help
.PHONY: help
TARGET	?=	''
help: ## Display this help screen.\n    VARIABLES:\n      - TARGET\n          Specify make target
	@grep '^[a-zA-Z]' Makefile | \
    awk -F ':.*?## ' 'NF==2 {printf "\033[36m  %-25s\033[0m %s\n", $$1, $$2}' | \
		grep -E ${TARGET} | \
		sed 's/^/\n/g' | sed 's/\\n/\n/g'
	@echo ""

.PHONY: check_installed
check_installed: ## Check installed (node, yarn)
# ...

.PHONY: Install
install: check_installed ## Install as systemd unit service.\n    VARIABLES:\n      - DIR_BIN\n          Specify bin dir (default: "/usr/local/bin")\n      - UNIT_FILE_PATH\n          Path to create systemd unit file. (default: "/usr/local/bin")\n      - ENV_FILE_PATH\n          Path to create env file. (default: "/usr/local/etc/default/sample")
# ...
```

```bash
$ make help

help                     Display this help screen.
    VARIABLES:
      - TARGET
          Specify make target

check_installed          Check installed (node, yarn)

install                  Install as systemd unit service.
    VARIABLES:
      - DIR_BIN
          Specify bin dir (default: "/usr/local/bin")
      - UNIT_FILE_PATH
          Path to create systemd unit file. (default: "/usr/local/bin")
      - ENV_FILE_PATH
          Path to create env file. (default: "/usr/local/etc/default/sample")

```

# Makefile ハマりポイント集

## `echo -n` の `-n` が出力されてしまう

問題のスクリプト

```makefile
.PHONY: test
test:
	@echo -n "改行したくない!"
	@echo "改行されてるじゃん!!!"
```

```bash
$ make test
-n 改行したくない!
改行されてるじゃん!!!
```

修正後

```makefile
.PHONY: test
test:
	@/bin/echo -n "改行したくない!"
	@echo "できた!"
```

or

```makefile
.PHONY: test
test:
	@bash -c 'echo -n "改行したくない!"'
	@echo "できた!"
```

```bash
$ make test
改行したくない!できた!
```

### 原因

`make` には組み込みの `echo` が存在し、この `echo` は `-n` を解読できないため


## make 変数とシェル実行時の環境変数の扱い

### make 変数ではなくシェル実行時の環境変数を参照したい

make 変数

```makefile
VAR		?= make変数
.PHONY: test
test:
	@echo ${VAR}
```

環境変数

```makefile
VAR		?= make変数
.PHONY: test
test:
	@VAR=環境変数 ; \
		echo $$VAR
```

### for ループの中で環境変数を変えながら処理をしたい

分かり易い例が思いつかなかったので僕が使っているものを抜粋して紹介します。

パイプで渡された出力やファイルの内容の1行ずつを for ループで繰り返し処理する際に解析処理などによって変数の切り出しが複雑になる場合、一度変数保持用のファイルを作成すると記述が簡単になります。

```makefile
.PHONY: build_cross
build_cross: get
	go tool dist list | grep 'aix\|darwin\|freebsd\|illumos\|linux\|netbsd\|openbsd\|windows' | while read line ; \
	do \
# .build.env ファイルに環境変数を一時的に保存する
		printf GOOS= > ./.build.env ; \
		echo $$line | cut -f 1 -d "/" >> ./.build.env ; \
		printf GOARCH= >> ./.build.env ; \
		echo $$line | cut -f 2 -d "/" >> ./.build.env ; \
# .build.env ファイルの環境変数を読み込んでコマンドを実行する
		. ./.build.env ; \
		go build GOOS=$$GOOS GOARCH=$$GOARCH -o ./${MODULE}-$$GOOS-$$GOARCH ;
	done
	rm ./.build.env
```