08. make 作業のレシピ

Photo by Monika Grabkowska on Unsplash

この回で身につける考え方

この回では,make を使って,何段階にも分かれる作業を自動化する方法を学ぶ. この回で最も大事なのは,Makefile の細かな文法を全部暗記することではなく,

作りたいものと材料の依存関係を書き,必要な作業だけを make に判断させる

という考え方である.

具体的には,次のことを学ぶ.

  • makeMakefile の役割
  • ターゲット,依存ファイル,レシピという Makefile の最小文法
  • 変更があったときに,必要な作業だけをやり直す仕組み
  • C プログラムのコンパイル,実行,データ作成,作図を Makefile でつなぐ方法
  • Makefile でよく起こるエラーと,それを調べる方法

  今回のゴール

最後には,C プログラムを編集してから,端末で

1make

と打つだけで,コンパイル,実行,データ作成,グラフ作成まで進められるようにする.

make について

make とは,Makefile と呼ばれるレシピファイルに書かれた内容に沿って,作業を自動的に行うツールである. 作業の対象は,多くの場合,ソースファイル,実行ファイル,データファイル,図のファイル,PDF ファイルなどの「ファイル」である.

make の特徴を,今回必要な範囲に絞って言うと次の通りである.

  • レシピの書き方が比較的単純で,使いまわししやすい.
  • 「作りたいもの」と「それに直接必要なもの」の関係を書けばよい.
  • 作業の順番は make が判断してくれる.
  • やり直す際,必要な作業だけを実行してくれる.
  • 途中でエラーが出て止まっても,原因を直して再度 make すれば,続きから進めやすい.

これまでに学んだシェルスクリプトも,自動的な作業を行うための道具である. しかし,プログラム開発や文書作成のように,

  • ソースを少し修正する
  • コンパイルする
  • 実行する
  • 結果を図にする
  • 図を確認してまた修正する

という作業を何度も繰り返す場合は,make が非常に便利である.

make のインストール状況

今回扱う make は CLI 用のコマンドであり,基本的には Unix 環境の文字端末エミュレータから実行する.

本授業の推奨環境,すなわち Ubuntu で

1sudo apt install build-essential

を行っている環境ならば,make は使えるはずである. 念の為,端末で

1make --version

としてみよう.バージョン情報が表示されればよい. もし make: command not found のように言われる場合は,build-essential が入っていない可能性がある.

Makefile の最小文法

Makefile の基本単位は,次の形である.

つまり,基本形は以下である.

1ターゲット: 依存ファイル1 依存ファイル2
2Tab⭾ コマンド1
3Tab⭾ コマンド2

ここで,

  • ターゲット: 作りたいもの.多くの場合はファイル名.
  • 依存ファイル: そのターゲットを作るために直接必要なもの.
  • コマンド: 実際に行う作業.

である.

とくに重要なのは,コマンド行の先頭である. Makefile では,コマンド行の先頭に 本物のタブ文字 が必要である. スペースを何個並べてもタブの代わりにはならない. この点は,今回もっとも引っかかりやすいところなので強く意識しておこう.

  Makefile を読むときの合言葉

  1. 何を作るのか?
  2. それを作るには何が必要か?
  3. 必要なものが揃ったら,どのコマンドを実行するのか?

make でサラダを作ってみよう

まずは例えから入ろう. make が料理人で,Makefile がレシピに相当すると考える. そして,簡単なサラダを作るレシピを,make の流儀で書くことを考えてみよう.

サラダ作りを,完成品,途中経過品,材料という「モノ」の依存関係として見ると,次のようになる.

最初に,依存関係だけを表にしてみる.

■ サラダ作成時の「モノ」の依存関係
ターゲット品 そのターゲットに必要な品
i. サラダ完成品 1. 適度な大きさに切られた清潔かつ新鮮な野菜
2. ドレッシング
3. 綺麗な器
ii. 適度な大きさに切られた
清潔かつ新鮮な野菜
4. 清潔かつ新鮮な野菜
iii. 清潔かつ新鮮な野菜 5. 新鮮な野菜
iv. ドレッシング 6. 酢
7. サラダ油
8. 醤油
9. 塩
10. コショウ

次に,この依存関係の左の「ターゲット品」を作るための「作業」を追加で書き入れる.


■ サラダ作成時に作ったり利用したりする「モノ」の 依存関係作業
ターゲット品 or 作業 そのターゲットに必要な品 or 作業内容
i. サラダ完成品 1. 適度な大きさに切られた清潔かつ新鮮な野菜
2. ドレッシング
3. 綺麗な器
(上のターゲット i. を作るための作業) 1 を 3 に入れ,2 をふりかける
ii. 適度な大きさに切られた
清潔かつ新鮮な野菜
4. 清潔かつ新鮮な野菜
(ii. を作る作業) 4 を適当にカットする
iii. 清潔かつ新鮮な野菜 5. 新鮮な野菜
(iii. を作る作業) 5 を洗う
iv. ドレッシング 6. 酢
7. サラダ油
8. 醤油
9. 塩
10. コショウ
(iv. を作る作業) 6-10 を適量,適度に混ぜる

この表に従うと,作業は以下のように進む.

まず,一番最初のターゲット品である「サラダ完成品」について考える. サラダ完成品がまだ無ければ,それに必要な品,つまり「切られた野菜」「ドレッシング」「綺麗な器」があるかを調べる. 必要な品が揃っていれば,直下の作業でサラダを完成させる. 足りないものがあれば,今度はその足りないものをターゲットとして,さらにその材料を調べる.

このように,必要な品をたどっていき,材料まで到達したら,そこから順に作業を戻していけばサラダが完成する. これが Makefile を読んで make が行う基本動作である.

サラダレシピを Makefile 風に書く

上のサラダレシピを Makefile 風に書くと,次のようになる.

 1サラダ完成品: 適切清潔新鮮野菜␣ドレッシング␣綺麗な器
 2Tab⭾ 必要品の最初を最後に入れ,二番目をふりかける
 3
 4適切清潔新鮮野菜: 清潔新鮮野菜
 5Tab⭾ 必要品を適当にカット
 6
 7清潔新鮮野菜: 新鮮野菜
 8Tab⭾ 必要品を洗う
 9
10ドレッシング: 酢␣サラダ油␣醤油␣塩␣コショウ
11Tab⭾ 必要品を適量,適度に混ぜる

ここで,Tab⭾ はタブキーを一回押して入力するタブ文字を意味する. また,␣ はスペースを意味する. 画面表示だけではタブとスペースを区別しにくいので,ここでは明示的に書いている.

さて,このレシピが完成したら,あとはシェルで

1make

と打つだけでよい. 材料が揃っているならば,make がこのレシピに沿って作業を行い,サラダが完成するというわけである.

サラダ作成を途中からやり直す?

例えの続きとして,上のレシピでサラダを作った後に,酢が古かったことに気づき,新しい酢でサラダを作り直したくなったとしよう. ただし,切られた野菜など,途中で作ったものはまだ十分に使えるとする. これはコンピュータ上では,中間ファイルが保存されている状態に相当する.

この場合に必要な作業は,

  1. 新しい酢を使ってドレッシングを作り直す.
  2. すでにある野菜に,新しいドレッシングをかけてサラダを作り直す.

という 2つだけでよい. 野菜を洗う作業や切る作業は,もう一度やる必要がない.

では,この「2つの作業だけでよい」という事実を料理人,すなわち make にどう指示すればよいだろうか?

答えは,具体的には指示しなくてよい である. make は,ファイルの依存関係と更新時刻を調べ,必要な作業だけを自動的に判断してくれる.

つまり,酢を取り替えた後でもう一度

1make

と打つだけで,必要な部分だけを作り直してくれるのである. これが make の大きな利点である.

実習の準備

ここからは,実際にプログラムを作って make を使ってみる. まず,実習用ディレクトリを作ってそこに移動しておこう.

1cd ~
2mkdir -p work/make08
3cd work/make08
4pwd

以降の作業は,この make08 ディレクトリで行うものとする. 過去のファイルと混ざると分かりにくいので,別のディレクトリで作業することを勧める.

  改行コードは Unix に合わせよう

Makefile や C のソースファイルは,Unix の改行コードで保存するのが無難である. Windows 側のエディタで作る場合は,LF / Unix 形式で保存する設定にしておこう.

改行コードの合わせ方は,例えば以下の通りである.

ツール How to
Emacs ファイルを編集しているときに C-x Return f とすると,その文書の漢字コード・改行コードを尋ねられるので,unix と入力すればよい.
Notepad++ 「設定 > 環境設定 > 新規文書」でフォーマットを Unix にする.これから新しく作るファイルは Unix の改行コードになるはずである.
VS Code 画面右下の CRLF または LF の表示をクリックし,LF を選ぶ.

単純な例: コンパイルして実行する

最初はごく簡単に,C プログラムをコンパイルし,それを実行してデータファイルを作る,という二段階の作業をさせてみよう.

  実習

以下の手順に従い,make を動かしてみよう. ただし,ただタイプするだけではなく,

  • 何を作ろうとしているのか
  • それを作るには何が必要なのか
  • どのコマンドが実行されるのか

を確認しながら進めよう.

1. C プログラムを作る

まず,f-sin.c という名前のファイルを作り,次の中身を書き込んでおく.

 1#include <stdio.h>
 2#include <math.h>
 3int main () {
 4    double x, dx;
 5    dx = 1.0/1000;
 6    x = 0.0;
 7    for ( x = 0.0; x <= 0.5+dx; x += dx) {
 8        printf("%6.3f %6.3f\n", x, sin(100*x)+sin(110*x));
 9    }
10    return 0;
11}

内容は,$x \in [0,0.5]$ の範囲で $0.001$ おきに $\sin(100x)+\sin(110x)$ を計算して出力するプログラムである.

2. Makefile を作る

同じディレクトリに Makefile という名前のファイルを作り,次の中身を書き込んでおく.

1f-sin.dat: f-sin.exe
2Tab⭾ \rm -f f-sin.dat
3Tab⭾ ./f-sin.exe > f-sin.dat
4
5f-sin.exe: f-sin.c
6Tab⭾ gcc f-sin.c -lm -o f-sin.exe

上で書いているように,2, 3, 6行目の Tab⭾ は「タブ」キーを押して入力するタブ文字を意味している. 実際の Makefile には Tab⭾ という文字列を書くのではなく,タブ文字を入力すること.

この Makefile は,次の意味である.

  • f-sin.dat を作るには f-sin.exe が必要である.
  • f-sin.exe が用意できたら,それを実行して f-sin.dat を作る.
  • f-sin.exe を作るには f-sin.c が必要である.
  • f-sin.c が用意できたら,gcc でコンパイルして f-sin.exe を作る.

  \rm の先頭のバックスラッシュは,rm に alias を設定している場合でも,素の rm コマンドを使うためのものである.

3. make してみる

Makefile と f-sin.c があることを確認してから,次を実行する.

1make

問題がなければ,f-sin.c がコンパイルされて f-sin.exe が作られ,さらにそれが実行されて f-sin.dat が作られるはずである.

確認してみよう.

1ls -l
2less f-sin.dat

4. もう一度 make してみる

もう一度,何も変更せずに

1make

としてみよう. このとき,何が表示されるだろうか. すでに必要なファイルが新しい状態で揃っていれば,make は「やるべき作業がない」と判断するはずである.

5. データファイルだけ消して make してみる

今度は,結果のデータファイルだけを消してみよう.

1rm f-sin.dat
2make

この時,どの作業が行われるかを注意深く見ておこう. f-sin.exe が残っていれば,C プログラムのコンパイルはやり直さず,実行だけで f-sin.dat を作り直すはずである.

6. C プログラムを修正して make してみる

次に,f-sin.c の for の行を次のように修正してみる.

1for ( x = 0.0; x <= 1.0+dx; x += dx) {

つまり,0.5 だったところを 1.0 に修正する. その後で,再び

1make

としてみよう. 今度は,ソースファイルが変更されたので,コンパイルからやり直されるはずである.

結果を確認してみよう.

1less f-sin.dat

このように,どのファイルを変更しても,基本的には make と打てば必要な作業だけが実行される.

ちょっと複雑な例: グラフ PDF まで作る

次に,今の例を少しだけ拡張しよう. C プログラムをコンパイルして,実行して,データファイルを作り,さらにそのデータファイルからグラフ PDF を作る,という三段階の作業をさせる.

  実習

先ほどの続きとして,以下のようにやってみよう.

1. gnuplot を準備する

この例では,gnuplot というグラフ描画ソフトウェアを使う. まだインストールされていない場合,本授業の推奨環境の Ubuntu では次のようにすればよい.

1sudo apt install gnuplot

2. gnuplot 用のファイルを作る

先と同じディレクトリに sin2pdf.gpl という名前のファイルを作り,次の中身を書き込んでおく.

1set output "f-sin.pdf"
2set term pdf
3plot "f-sin.dat" with lines

最後の行のあとにも改行を入れておくのが無難である.

3. Makefile を修正する

Makefile の中身を以下のように修正する. 先頭に f-sin.pdf のルールが加わる.

1f-sin.pdf: f-sin.dat
2Tab⭾ gnuplot "sin2pdf.gpl"
3
4f-sin.dat: f-sin.exe
5Tab⭾ \rm -f f-sin.dat
6Tab⭾ ./f-sin.exe > f-sin.dat
7    
8f-sin.exe: f-sin.c
9Tab⭾ gcc f-sin.c -lm -o f-sin.exe

この Makefile では,最初のターゲットが f-sin.pdf である. そのため,単に

1make

と打つと,makef-sin.pdf を作ろうとする. そのために f-sin.dat が必要で,さらにそのために f-sin.exe が必要なので,結局,必要な作業を順にたどって実行する.

4. make して PDF を見る

1make

問題がなければ,f-sin.pdf という画像ファイルができているので,見てみよう. Windows ならば,エクスプローラーでそのファイルをダブルクリックすればよいはずである. 他の OS でも似たような方法で開けるだろう.

すると,下記のようになっているはずである.

このように,作業が何段階かになっていても make は問題なく使える.

もうちょっとだけ続くんじゃ...

今度は,上の作業をさらに拡張する. 次のような作業を行うことにしよう.

  1. 2つのプログラムをコンパイルする.
  2. 2つの実行ファイルを実行して,2つのデータファイルを作る.
  3. 片方のデータファイルを処理して,もう1つデータファイルを作る.
  4. それら 3つのデータを同時にプロットしたグラフ PDF を 1つ作る.

依存関係を図にすると,次のようになる.

理屈がわかっていれば,やることは単純である. Makefile では,局所的なルールを並べていけばよい.

  実習

先の続きとして,以下のようにやってみよう.

1. f-cos.c を作る

f-cos.c という名前のファイルを作り,次の中身を書き込んでおく.

 1#include <stdio.h>
 2#include <math.h>
 3int main () {
 4    double x, dx;
 5    dx = 1.0/1000;
 6    x = 0.0;
 7    for ( x = 0.0; x <= 1.0+dx; x += dx) {
 8        printf("%6.3f %6.3f\n", x, 1.0+cos(10*x));
 9    }
10    return 0;
11}

何をするプログラムか,読んで確認しておこう.

2. Makefile を修正する

Makefile の中身を以下のように修正する. 結構変わるので,注意深く作業しよう.

 1all.pdf: f-sin.dat f-cos.dat f-cosm.dat
 2Tab⭾ gnuplot "all2pdf.gpl"
 3
 4f-sin.dat: f-sin.exe
 5Tab⭾ \rm -f f-sin.dat
 6Tab⭾ ./f-sin.exe > f-sin.dat
 7
 8f-cos.dat: f-cos.exe
 9Tab⭾ \rm -f f-cos.dat
10Tab⭾ ./f-cos.exe > f-cos.dat
11
12f-cosm.dat: f-cos.dat
13Tab⭾ \rm -f f-cosm.dat
14Tab⭾ cat f-cos.dat | awk '{print $$1, -$$2}' > f-cosm.dat
15
16f-sin.exe: f-sin.c
17Tab⭾ gcc f-sin.c -lm -o f-sin.exe
18
19f-cos.exe: f-cos.c
20Tab⭾ gcc f-cos.c -lm -o f-cos.exe

  Makefile 内の awk と $

awk では第1列,第2列を $1$2 と書く. しかし,Makefile の中で単に $1$2 と書くと,make の変数と解釈されてしまう. そのため,Makefile の中では $$1$$2 と書いている.

3. all2pdf.gpl を作る

先と同じディレクトリに all2pdf.gpl という名前のファイルを作り,次の中身を書き込んでおく.

1set output "all.pdf"
2set term pdf
3plot "f-sin.dat" w l, "f-cos.dat" w l, "f-cosm.dat" w l

やはり,最後の行のあとに改行を入れておくのが無難である.

4. make してみる

1make

問題がなければ,all.pdf という画像ファイルができているので,上と同じ方法で見てみよう. 下記のような図のはずである.

5. 修正して,再び make してみる

プログラムのいずれかを適当に修正してみよう. ただし,グラフがおかしくならないような修正にしておくこと. 例えば,f-cos.c1.0+cos(10*x) の係数や定数を少し変えてみる,などが考えられる.

その後,再び

1make

としてみよう. どこから作業がやり直されるかを確認し,最後に all.pdf を見てみよう.

よくあるエラーと確認方法

Makefile は便利だが,慣れるまでは同じところでよく引っかかる. まずは次の点を疑うとよい.

1. missing separator と言われる

例えば,次のようなエラーが出ることがある.

1Makefile:2: *** missing separator.  Stop.

これは,多くの場合,コマンド行の先頭がタブ文字ではなくスペースになっていることが原因である. Makefile の中の見えない文字を確認するには,次のようにするとよい.

1cat -A Makefile

タブ文字は ^I のように見える. コマンド行の先頭が ^I になっていれば,そこはタブ文字である.

2. No rule to make target と言われる

例えば,次のようなエラーが出ることがある.

1make: *** No rule to make target 'f-sin.c', needed by 'f-sin.exe'.  Stop.

これは,必要なファイルが無いか,ファイル名の綴りが Makefile と実際のファイルで食い違っている場合によく起こる. まず,

1ls

でファイル名を確認しよう. 大文字・小文字の違いや,ハイフン - とアンダースコア _ の違いにも注意すること.

3. make しても何も起こらない

すでにターゲットが最新ならば,make は何もしない. これはエラーではない.

何が起こるかを実行前に確認したい場合は,

1make -n

を使うとよい. これは「実際には実行せず,実行する予定のコマンドだけを表示する」機能である.

4. とにかく最初からやり直したい

授業中の実習では,途中でよく分からなくなったら,作られたファイルを消してからやり直す方が早い場合もある. 今回の例なら,例えば

1rm -f f-sin.exe f-cos.exe f-sin.dat f-cos.dat f-cosm.dat f-sin.pdf all.pdf
2make

のようにする. ただし,C のソースファイルや Makefile まで消さないように注意 すること.

発展・補足編

基本編の内容に対する、発展・補足的な内容を以下に述べる.
学習初期においては無視しても良い内容として、授業時も解説を省略する予定である.

もう少しだけ詳しく: 便利なターゲット

Makefile では,実際のファイル名ではない「作業名」のようなターゲットを書くことも多い. 典型例が clean である.

1clean:
2Tab⭾ rm -f f-sin.exe f-cos.exe f-sin.dat f-cos.dat f-cosm.dat f-sin.pdf all.pdf

こう書いておくと,

1make clean

で作業途中にできたファイルをまとめて消せる. ただし,もし clean という名前のファイルが存在すると意味が変わってしまうので,実際には次のように書くことが多い.

1.PHONY: clean
2clean:
3Tab⭾ rm -f f-sin.exe f-cos.exe f-sin.dat f-cos.dat f-cosm.dat f-sin.pdf all.pdf

.PHONY は「これは実際のファイル名ではなく,作業名としてのターゲットである」という意味である.

もう少しだけ詳しく: TeX 文書作成に make を使う

TeX を使っている人は,TeX のコンパイル作業にも make を使える. 例えば,paper.tex から paper.pdf を作るだけなら,概念的には次のような Makefile が考えられる.

1paper.pdf: paper.tex
2Tab⭾ platex paper.tex
3Tab⭾ dvipdfmx paper.dvi

ただし,実際の TeX 文書では,参考文献,索引,図の作成などが絡むこともあり,もう少し複雑になる. その場合でも,

  • 最終的に作りたいものは何か
  • そのために直接必要なファイルは何か
  • 必要なものが揃ったら,どのコマンドを実行するのか

という考え方は同じである.

もう少しだけ詳しく: make の文法を使うと楽できるが...

これまでの Makefile を見ると,似たようなことが何回も書かれていて,面倒な上に間違えやすい. もちろん,make の文法は,このような重複を減らせるように作られている. 例えば,先の Makefile とほぼ同じ動作,ただし f-cosm.dat は無し,をするようなシンプルな Makefile は次のように書ける.

 1.SUFFIXES: .dat .exe
 2DATA = f-sin.dat f-cos.dat
 3
 4.c.exe:
 5Tab⭾ gcc $< -lm -o $@
 6
 7.exe.dat:
 8Tab⭾ rm -f $@
 9Tab⭾ ./$< > $@
10
11all.pdf: $(DATA)
12Tab⭾ gnuplot "all2pdf.gpl"
13
14$(DATA):

上の Makefile 中には繰り返しの記述が少ないことに注意しよう. ただし,$<$@ などの自動変数,サフィックスルール,変数 $(DATA) などが出てくるので,最初からここまで覚える必要はない. まずは,今回の授業で扱った素朴な書き方を理解しよう.

  追加実習

余裕があるならば make の文法を調べ,上の Makefile を理解してみよう.

参考資料

レポート No.8

  注意

近年はセキュリティ上の懸念から,実行形式のプログラムなどをメールに添付すると,そのメール自体の受信を受信側サーバが拒否することがある. そういうことを避けるため,レポートをファイルで提出するときは,そのような懸念のあるファイル形式を避けるようにしよう.

要するに,レポートは PDF ファイルにして送るのが良い と思っておけばよい.

以下の課題について,自らの将来のスキルアップに繋がるように調査と考察を行い,
     学籍番号-氏名-08.pdf
というファイルとしてレポートを作成し, webフォーム から教官宛に提出しよう.

なお,レポートを $\TeX$ 等で作成したものを印刷した紙媒体として教官に直接手渡す形で提出してもよいが,物質によるレポート提出は常に破損や紛失の可能性があるので,あまりお勧めはしない.

課題

  1. Makefile にさらに追加して,できたグラフファイルを圧縮する機能をつけてみよ.

  2. Makefile を変更して,make を実行すると,以前作ったグラフファイルをどこかへバックアップしてから新しいグラフを作るようにしてみよ.

  3. TeX が使える者は,今回の授業例を修正して,TeX の編集作業に make を使えないか考えてみよ.