ファイルが存在しない「偽ターゲット」

Makefileの本質は依存関係(ルール)の記述にある。その依存関係を書く際のポイントがターゲットだ。ターゲットは基本的には「生成されるファイル」であり、生成元のファイルと合わせて次のように記述する。

生成されるファイル: 生成元のファイル
    「生成元ファイル」から「生成されるファイル」を作るコマンド

ただし、Makefileでは実際には生成されないファイル名がターゲットに記載されていることがある。次のような書き方になる。

存在しないし生成もされないファイルの名前:
    なんらかのコマンド

このように、実際には生成されるファイルが存在しないターゲットを「偽ターゲット(Phony Target)」と呼んでいる。本連載で扱うMakefileの使い方は、どちらかと言うとこの偽ターゲットを使うものになる。

偽ターゲットには、通常のターゲットのように依存関係を書くことができる。“偽”とは言えターゲットなので、ほかのターゲットと同じように依存関係を記述できるのだ。

存在しないし生成もされないファイルの名前: 依存するファイルやターゲット
    なんらかのコマンド

偽ターゲットとして使いたいのに、処理の途中でターゲット名と同じ名前のファイルやディレクトリが生成されてしまうことがあるかもしれない。その場合、偽ターゲットはユーザーが想定しているようには動かなくなってしまう。

しかしながら、Makefileには偽ターゲットを指定する特別なターゲット「.PHONY」が用意されており、「.PHONY」に指定したターゲットは明示的に偽ターゲットとして扱われるようになる。この指定は、次のように使う。

.PHONY: 存在しないし生成もされないファイルの名前

存在しないし生成もされないファイルの名前:
    なんらかのコマンド

「.PHONY」は必ずしも使う必要はなく、実際のファイルやディレクトリが存在しなければ偽ターゲットになる。明示的に書きたい場合には逆に「.PHONY」を書くようにすればよい。

よく使われる偽ターゲット「clean」で使い方を学ぶ

これはプログラミングでの利用となるが、よく使われる偽ターゲットに「clean」がある。ビルドで生成された中間ファイルやバイナリファイルなどを削除して、ソースコードだけの状態に戻すために使われるターゲットだ。依存に関係なく常に動作して欲しい処理なので、偽ターゲットとして作成することが多い。

cleanは、例えばMakefileに次のように記述する。

clean:
    @echo "entered: target: clean"
    rm -f *.o

実行すると次のようになる。

% make clean
entered: target: clean
rm -f *.o
% make clean
entered: target: clean
rm -f *.o
% make clean
entered: target: clean
rm -f *.o
% 

では、明示的に「clean」というファイルを作った場合、このMakefileがどうなるか見てみよう。使うMakefileは、先ほどと全く同じだ。

clean:
    @echo "entered: target: clean"
    rm -f *.o

これを次のように実行する。実行の途中で「clean」というファイルを手動で作成して動作がどのように変わるかを比べてみる。

% make clean
entered: target: clean
rm -f *.o
% touch clean
% ls -l
合計 1
-rw-r--r-- 1 daichi daichi  0  4月 16 17:10 clean
-rw-r--r-- 1 daichi daichi 53  4月 16 16:52 Makefile
% make clean
make: 'clean' は更新済みです.
% make clean
make: 'clean' は更新済みです.
% 

「clean」というファイルを作成した後は、cleanターゲットが実行されなくなったことがわかる。ファイルが存在しているので常に条件を満たさなくなり、実行されなくなるのだ。

では、今度はcleanを明示的に「.PHONY」ターゲットに指定してみる。次のような感じだ。

.PHONY: clean

clean:
    @echo "entered: target: clean"
    rm -f *.o

実行すると次のようになる。

% make clean
entered: target: clean
rm -f *.o
% make clean
entered: target: clean
rm -f *.o
% touch clean
% ls -l
合計 1
-rw-r--r-- 1 daichi daichi  0  4月 16 17:12 clean
-rw-r--r-- 1 daichi daichi 70  4月 16 17:11 Makefile
% make clean
entered: target: clean
rm -f *.o
% make clean
entered: target: clean
rm -f *.o
% 
  • 実行結果

    実行結果

途中で明示的にcleanというファイルを作成しているが、作成後もcleanターゲットが実行されていることがわかる。「.PHONY」に明示的に指定されたことで、常に偽ターゲットとして機能するようになったためだ。

本連載では、日々の作業をまとめる方法としてMakefileを使おうとしている。つまり、典型的な使い方は次のような感じになる。

タスク1:
    コマンドやコマンド
    コマンドやコマンド
    コマンドやコマンド
    コマンドやコマンド
    コマンドやコマンド
    コマンドやコマンド

タスク2:
    コマンドやコマンド
    コマンドやコマンド
    コマンドやコマンド
    コマンドやコマンド
    コマンドやコマンド
    コマンドやコマンド

タスク3:
    コマンドやコマンド
    コマンドやコマンド
    コマンドやコマンド
    コマンドやコマンド
    コマンドやコマンド
    コマンドやコマンド

タスク4:
    コマンドやコマンド
    コマンドやコマンド
    コマンドやコマンド
    コマンドやコマンド
    コマンドやコマンド
    コマンドやコマンド

.PHONY: タスク1 タスク2 タスク3 タスク4

「.PHONY」で偽ターゲットの指定となることは覚えておこう。

偽ターゲットでありデフォルトターゲット「all」

偽ターゲットとしてcleanとともによく使われているのが「all」だ。このターゲットは、Makefileの一番最初のターゲットとして記述されることが多い。次のようなイメージだ。

all: ターゲット1 ターゲット2 ターゲット3

ターゲット1:
    コマンド...

ターゲット2:
    コマンド...

ターゲット3:
    コマンド...

最初に記述されるので、allはデフォルトのターゲットにもなる。デフォルトであり、かつ、偽ターゲットというわけだ。

allはプログラミングで使われることが多い。通常はフルビルドを実行するためのターゲットを指定する目的で使われる。allとcleanは偽ターゲットの最もよく使われるユースケースの一つだ。偽ターゲットの使い方としてもわかりやすいので覚えておこう。

偽ターゲットの使い方はサブディレクトリでの処理なども

偽ターゲットはそのほかの用途でも使われる。よく使われるのは、サブディレクトリだ。

Makefileはディレクトリごとに配置されることが多い。これもプログラミングでよく使われる方法だ。ツリー構造に整理されたディレクトリに、それぞれMakefileを配置する。Makefileはそのディレクトリにあるファイルをビルドする方法を記述しているほか、サブディレクトリがある場合にはそちらのビルドを走らせるトリガー処理が記述される。最も上の階層のMakefileでmakeすると、その要領で全てのサブディレクトリへ処理がトリガーしていくといった構造だ。

こういったトリガー構造を作るときにも偽ターゲットは使われる。そして、この場合には明示的に「.PHONY」に指定する必要がある。

まず、正しい書き方から見ていこう。次のような感じでMakefileを用意する。

SUBDIRS:=   subdir1 subdir2 subdir3

.PHONY: subdirs $(SUBDIRS)

subdirs: $(SUBDIRS)

subdir1:
    $(MAKE) -C subdir1

subdir2:
    $(MAKE) -C subdir2

subdir3:
    $(MAKE) -C subdir3

実行すると次のようになる。

% tree
Folder PATH listing
Volume serial number is 20DE-B039
C:.
│   clean
│   Makefile
│
├───subdir1
│       Makefile
│
├───subdir2
│       Makefile
│
└───subdir3
        Makefile

% cat Makefile
SUBDIRS:=       subdir1 subdir2 subdir3

.PHONY: subdirs $(SUBDIRS)

subdirs: $(SUBDIRS)

subdir1:
        $(MAKE) -C subdir1

subdir2:
        $(MAKE) -C subdir2

subdir3:
        $(MAKE) -C subdir3
% cat subdir1/Makefile
all:
        @echo "enterd in subdir1"
% cat subdir2/Makefile
all:
        @echo "enterd in subdir2"
% cat subdir3/Makefile
all:
        @echo "enterd in subdir3"
% make
/usr/bin/make -C subdir1
make[1]: ディレクトリ '/Users/daichi/Documents/lwt/20220416/stuffs/subdir1' に入ります
enterd in subdir1
make[1]: ディレクトリ '/Users/daichi/Documents/lwt/20220416/stuffs/subdir1' から出ます
/usr/bin/make -C subdir2
make[1]: ディレクトリ '/Users/daichi/Documents/lwt/20220416/stuffs/subdir2' に入ります
enterd in subdir2
make[1]: ディレクトリ '/Users/daichi/Documents/lwt/20220416/stuffs/subdir2' から出ます
/usr/bin/make -C subdir3
make[1]: ディレクトリ '/Users/daichi/Documents/lwt/20220416/stuffs/subdir3' に入ります
enterd in subdir3
make[1]: ディレクトリ '/Users/daichi/Documents/lwt/20220416/stuffs/subdir3' から出ます
% 

サブディレクトリとしてsubdir1、subdir2、subdir3というディレクトリがあり、それぞれにMakefileが用意してある。カレントディレクトリのMakefileには、これらサブディレクトリのMakefileをmakeで処理するように記述している。そして想定しているように、それぞれサブディレクトリのMakefileへ処理がトリガーしていっていることがわかる。

次に、ダメな書き方を見てみよう。先ほどのMakefileとほとんど同じだが、「.PHONY」からサブディレクトリを削除してある。

 SUBDIRS:=  subdir1 subdir2 subdir3

.PHONY: subdirs

subdirs: $(SUBDIRS)

subdir1:
    $(MAKE) -C subdir1

subdir2:
    $(MAKE) -C subdir2

subdir3:
    $(MAKE) -C subdir3

実行すると次のようになる。

% make
make: 'subdirs' に対して行うべき事はありません.
% 

サブディレクトリはすでに存在している。そのため、偽ターゲットではなく通常のターゲットとして扱われてしまい、条件を満たさなくなるので実行されなくなる。サブディレクトリに対して処理を行わせていきたい場合には、明示的に「.PHONY」で偽ターゲットを指定する必要がある。

偽ターゲットについて、よく知っておこう

偽ターゲットはMakefileでは通常のターゲットとともによく使われるターゲットだ。その機能はよく知らなくても、すぐに使えるくらいには直観的でわかりやすいので、あえてマニュアルで調べようとは思わないかもしれない。

しなしながら「.PHONY」などは調べておかないと、意味がわからないだろう。特にサブディレクトリに処理をトリガーしていく場合には、この書き方が必須になる。偽ターゲットについては、最初に一通りマニュアルを読んでおきたいところだ。

参考