前回は,単一のプログラムを複数のソースファイルから作成してみた. プログラムの規模が大きいほど, ソースファイル分割のメリットも大きくなる. その反面,コンパイル方法が複雑になってしまっていた. 今回は,プログラミングそのものではなく, コンパイル作業について改善してみよう.
Unix では,複雑なコンパイル作業を効率的に実行するためのツール make が用意されている. Makefile という名前のファイルにコンパイル手順を記録しておけば, どんなに複雑なコンパイル作業であっても 単純に make コマンド1発だけで完了できるようになる.
とにかく,make を使ってみよう. まず準備として,前回作成した統計処理プログラムの分割ソース版 calcstat-2 のディレクトリへ移動しておこう. そして,もしオブジェクトファイル(拡張子 .o) とプログラムファイル(実行形式)があれば, それらを削除しておこう. なお,誤って ソースファイル .c を消してしまわない よう,注意すること!
$ cd .../calcstat-2 # 前回のディレクトリへ $ rm *.o # オブジェクトファイルの削除.*.c と間違わないこと! $ rm calcstat # プログラムファイルの削除
最も単純な Makefile の例が List 1 である.
calcstat:
cc main.c input.c func.c -lm -o calcstat -Wall # コンパイルとリンク
このような Makefile を用意しておけば, そのディレクトリ内では, 次のコマンドを実行するだけでコンパイルできるようになる:
$ make または $ make calcstat
ただし,List 1 のような単純すぎる Makefile では, ソース分割のメリットをまったく活かしていない. そこで,より実用的な形式の Makefile を作成してみよう. List 1 の Makefile を元にして, List 2 のように書き換えよう.
calcstat: main.o input.o func.o cc main.o input.o func.o -lm -o calcstat # 全オブジェクトのリンク(プログラムの生成) main.o: main.c input.h func.h cc -c main.c -Wall # 個別のコンパイル(オブジェクトの生成) input.o: input.c input.h cc -c input.c -Wall # 〃 func.o: func.c func.h cc -c func.c -Wall # 〃 .PHONY: clean distclean clean: -rm *.o distclean: clean -rm calcstat
一般に,Makefile は, 次の書式のような複数のエントリ(要素)から構成されている:
ターゲット: 依存ファイル1 依存ファイル2 ... ターゲット作成などのコマンド
ターゲット(目標)には,生成したいファイルの名前などを指定しておく. 依存ファイルには, そのターゲットを生成するために必要となるファイルやターゲットを指定しておく.
たとえば,List.2 のターゲット main.o に関するエントリ:
main.o: main.c input.h func.h cc -c main.c -Wall
は,次のことを表わしている:
もし,その依存ファイルと同じ名前のターゲットが この Makefile 内に記述されていれば, そのエントリについても芋蔓式(イモヅル式;連鎖的)に実行されることになる. たとえば,最初のエントリ:
calcstat: main.o input.o func.o cc main.o input.o func.o -lm -o calcstat
は,オブジェクトファイル main.o,input.o,func.o を材料にしているので, このエントリ calcstat(複数オブジェクトの連結)を実行しようとすると, その前に,各オブジェクトファイルのエントリ(各ソースファイルの個別コンパイル)が自動的に実行されることになる.
なお,ターゲット名は,ファイル名だけに限らず,何でも構わない. とにかく,次のように書けば, そのエントリのコマンドが実行されることになっている:
$ make ターゲット名
ただし,ターゲット(オブジェクトファイルなど)が 依存ファイル(ソースファイルなど)よりも新しい場合には, 「すでに実行済みなので,再実行は不要」ということになるので, そのエントリのコマンドは実行されない. 依存ファイルを更新した場合(材料が変わった場合)だけ実行される.
つまり,make は, 書き換えられたソース(再コンパイルが必要なソース)だけを選別してコンパイルする. ソースを書き換えていない場合には, そのソースのコンパイルを自動的に省略してくれる. 前回説明した通り, 更新していないのに再コンパイルするのは無駄なので.
ところで,List 2 では, ターゲット clean と distclean のエントリだけ, 他とは違う形式になっている:
.PHONY: clean distclean clean: -rm *.o distclean: clean -rm calcstat
オブジェクトファイル .o は, プログラムファイル calcstat の完成後には不要になるので, これらのエントリでは, これらの不要ファイルを簡単に削除できるようにしている. しかし,これらでは,他のエントリとは異なり, clean というファイルを生成しているわけではないので, 更新の有無は判断できないことになる. そこで,ターゲット名 clean と distclean を 擬似ターゲット .PHONY に指定している. 通常のターゲットとは異なり,擬似ターゲットのエントリは, 更新有無に関係なく常に実行されることになっている:
$ make clean または $ make distclean
なお,コマンド rm の前の記号「-」は, そのコマンドのエラーを無視するための指定である. すでにファイルを削除済みの場合でも, make を中断しないようにしている.
すでに理解しているように,プログラム開発では, ソースの変更とコンパイルとを何度も繰り返す必要がある. make を使えば,コンパイル作業を効率化できる. 主なメリットは次の通り:
では,make による分割コンパイルのメリットを理解するため, いくつか実験してみよう.
$ make distclean # 初期状態へ $ make cc ... # 全ソースファイルをコンパイル cc ... ...
$ make make: 'calcstat' は更新済みです
たとえば:
$ touch main.c # 更新時間の変更コマンド.ファイルの再保存と同じ. $ make cc -c main.c -Wall # 更新されたソース main.c だけがコンパイルされる. cc main.o input.o func.o -lm -o calcstat
たとえば:
$ touch func.h $ make cc -c main.c -Wall # 更新されたヘッダ func.h を材料とするソース cc -c func.c -Wall # だけがコンパイルされる. cc main.o input.o func.o -lm -o calcstat
実行結果と List 2 とを比べて見れば, 更新されたファイルに関連する処理だけが 選択的かつイモヅル式に実行されることがわかるだろう.
前回の課題で作成した 分割ソース版 cg プログラムに対して, Makefile を作成せよ. そして,すべてのエントリが正しく動作することを確かめよ.
レポートには,次の内容を記述すること:
ソース分割(関数分類)の妥当性を検討したり, Makefile を効率よく作るために, すべてのソースファイルおよびヘッダファイルについて, 構成情報を抽出(ちゅうしゅつ)しよう.
分割ソースファイルの構成情報の書式の例:
ヘッダファイル名.h: 依存:ヘッダファイル名.h, ... # include した自作のヘッダすべて(標準ライブラリのヘッダは不要) 宣言:関数名(), ... # extern 宣言した関数すべて(もしあれば,グローバル変数も) 定義:型名, ... # typedef した型すべて ソースファイル名.c: 依存:ヘッダファイル名.h, ... # include した自作のヘッダすべて(標準ライブラリのヘッダは不要) 定義:関数名(), ... # 定義した関数すべて(もしあれば,グローバル変数も) ...
List 2 の Makefile では, 同じようなコマンド(cc -c ソース.c )を3回も羅列していて冗長だった. List 3 では,コンパクトに書き直してみた.
... main.o: main.c input.h func.h input.o: input.c input.h func.o: func.c func.h .c.o: cc -c $< -Wall ...