Edited at

GNU Make は include 先が見つからなくてもルールさえあれば生成して include してくれる

More than 1 year has passed since last update.


おさらい

GNU Make には別の Makefile を読み込むための include 機能がある1


Makefile

.PHONY: all

all:
@echo $(ANSWER)

include answer.mk



answer.mk

ANSWER := 42


上記の 2 ファイルを用意して make すると、めでたく 42 と表示される。


include 先を生成するルールを書く

answer.mk を削除して、代わりに以下のように「answer.mk を生成する answer.sh」を用意してみる。


Makefile

.PHONY: all

all:
@echo $(ANSWER)

include answer.mk

answer.mk: answer.sh
./$< > $@



answer.sh

#!/bin/sh

echo "ANSWER := 42"


make すると出力はこうなる。

Makefile:6: answer.mk: No such file or directory

./answer.sh > answer.mk
42

処理の流れとしては以下のようになる。


  1. answer.mk を include しようとしたがファイルが存在していなかったので警告を出力する


    • エラーとして停止することはなく処理は続行される



  2. Makefile を読み込み終わると、GNU Make は関連する Makefile 自体をターゲットとしてその生成・更新を試みる



  3. answer.mk を生成するルールがあるので、GNU Make はそのルールを利用して answer.mk を生成する

  4. Makefile に更新があった場合、GNU Make は全ての Makefile を再度読み込み直す

  5. 本来のターゲット (all) についての処理を行う

  6. 最終的にめでたく 42 と表示される

2 と 4 がポイントで、GNU Make は「Makefile を生成する Makefile」のようなメタメタしい使い方をされることをちゃんと考慮している。そのため、今回のような例でもエラーとせず動いてくれるようだ。


応用例

これを応用すると、複雑なロジックは *.mk を生成するスクリプトの方に書き、GNU Make には依存関係の解決とファイルの生成・更新のみに専念してもらう、といった使い方ができる。

以下は、「頭文字が _(アンダーバー)でないディレクトリ・ファイルについてのみ Stylus → CSS に変換する」という実際に自分が使っている Makefile の例である。npm install -D stylus glob している状況を想定。


Makefile

.PHONY: all reload css

all: css

include vars.mk

vars.mk: vars.js
node $< > $@

reload: vars.mk
$(MAKE) -B vars.mk

BIN := $(shell npm bin)

css: $(DST_CSS)

$(DST)/%.css: $(SRC)/%.styl
@mkdir -p $(dir $@)
$(BIN)/stylus $< -o $@

$(DST)/%.css: $(SRC)/%.css
@mkdir -p $(dir $@)
cp $< $@


SRC, DST, DST_CSS といった変数は以下のスクリプトによって生成され include される。


vars.js

const glob = require("glob");

const src = "src";
const dst = "dst";

const rejectUnderscore = (pathstr) => {
for (let part of pathstr.split("/")) {
if (part.length && part[0] === "_") {
return false;
}
}
return true;
};

const src_styl = glob.sync(src + "/**/*.styl");
const src_css = glob.sync(src + "/**/*.css");
const styl_re = new RegExp(`^${src}/(.+?)\.(?:styl|css)$`);
const dst_css = [...src_styl, ...src_css].filter(rejectUnderscore).map(s => s.replace(styl_re, `${dst}/$1.css`));

const vars = {
src: [src],
dst: [dst],
src_styl,
src_css,
dst_css,
};

if (require.main === module) {
for (let key in vars) {
console.log(`${key.toUpperCase()} := ${vars[key].join(" ")}`);
}
}

module.exports = vars;


vars.mk を再生成する際には make の -B オプション(変更先が新しい場合でも強制的に再生成する)を用い make -B vars.mk とする。上の例ではそれを reload ターゲットとして定義している。


おわりに

簡単な処理であれば GNU Make の wildcard とか patsubst とかでも十分やっていけるけれど、ちょっと複雑なことをしようとすると途端にしんどくなる。それをどうにかしようとした結果 include 先を make 中に生成するという上記のような形に落ち着いた。Makefile の自動生成は古来からよくやられていると思うけど(makedepend とか)、手書きの適当なスクリプトでもそれなりに実用的になる。

GNU Make 便利なのでうまく使っていきましょう。


参考リンク





  1. POSIX の make のページに見当たらないので多分 GNU Make 実装の拡張機能。