前回,プログラミング作業の効率化のためソースファイルを分割したが, かえってコンパイル作業が面倒なことになっていた. そこで今回は,分割コンパイルを自動化・効率化しよう.
Unix では,複雑なコンパイル作業(cc ...)を 自動化するためのツール make が用意されている. Makefile という名前のファイルにコンパイル手順を記録しておけば, どんなに複雑なコンパイル作業であっても 単純に make コマンド1発だけで完了できるようになる.
まずは,ソースファイルが1個だけの単純な開発プロジェクトを例として, make を使ってみたい. このプロジェクトのためのディレクトリを作成し,その中で作業を進めよう.
$ mkdir ~/c-1216 # 本日のディレクトリ $ cd ~/c-1216/ # ...の中で更に... $ mkdir proj-0 # このプロジェクト用のディレクトリに... $ cd proj-0/ $ cp ~/tmpl.c hello.c # 適当なソースファイルを作成しておく $ vim hello.c
#include <stdio.h> int main(void) { printf("こんにちworld\n"); return (0); }
今回は,まだコンパイルしませんよ.
コンパイル方法をファイル Makefile に記述しよう.
$ vim Makefile
hello: hello.c
cc hello.c -o hello
一般に,Makefile は, 次の書式のようなエントリ(entry;記入項目,要素)から構成される:
ターゲット: 依存ターゲット ... ターゲット生成のためのコマンド ...
では,make でコンパイルしてみよう.
コンパイルと実行:
$ make cc hello.c -o hello $ ./hello こんにちworld
はい,長たらしく入力が面倒だったコンパイルコマンド cc ... が, たったの4文字だけの短いコマンド make で実行された.
ソース更新と再コンパイル:
$ make make: 'hello' は更新済みです. # 編集していなければ,再コンパイルの必要なし $ rm hello # プログラムが無ければ... $ make cc ... # ...コンパイルされる $ vim hello.c # ソースを変更したら... $ make cc ... # ...コンパイルされる $ make make: 'hello' は更新済みです.
プログラム開発では,何度もソースを修正し, その度にコンパイルも必要となっている. そしてコンパイルコマンドの入力ミスも発生する. したがって,コマンドラインが短縮化されただけでも, 開発効率のかなりの向上となっていることだろう.
make の利点:
複数個のプログラムから構成されるシステム開発のプロジェクトでも make を利用してみよう. 数列生成プログラムと総和計算プログラムとを作成する.
ディレクトリの準備:
$ cd ../ $ mkdir proj-1 $ cd proj-1/
ソースファイルの作成: (ディレクトリ proj-1/ からDL可能)
$ vim seq.c
// 等差数列を標準出力するプログラム // $ ./seq 項数 初項 公差 // コマンドライン引数は省略可 #include <stdio.h> #include <stdlib.h> int main(int argc, char *argv[]) { int n = 10; // 項の個数 int from = 1; // 初項の値 int step = 1; // 公差 int x; // 各項の値 int i; // カウンタ if (argc > 1) n = atoi(argv[1]); if (argc > 2) from = atoi(argv[2]); if (argc > 3) step = atoi(argv[3]); x = from; for (i = 0; i < n; i++) { printf("%d\n", x); x += step; } return (0); }
$ vim sum.c
// 標準入力の数列を合計し,標準出力するプログラム #include <stdio.h> int main(void) { int x; // 各項の値 int sum = 0; // 総和 while (1) { if (scanf("%d", &x) == EOF) break; sum += x; } printf("%d\n", sum); return (0); }
Makefile の作成:
$ vim Makefile
# 複数プログラムの Makefile # ver.1 とにかくコンパイルを自動化 seq: cc seq.c -o seq sum: cc sum.c -o sum
コンパイル:
$ make seq # seq だけをコンパイル cc seq.c -o seq $ make sum # sum だけをコンパイル cc sum.c -o sum
実行:
$ ./seq 1 2 ... 10 $ ./seq | ./sum 55 $ ./seq 3 0 2 0 2 4 $ ./seq 3 0 2 | ./sum 6
再度のコンパイル:
$ make # ターゲット省略だと最初のターゲットだけをコンパイル make: 'seq' は更新済みです. # 再コンパイルできない? $ rm seq # プログラムを削除してから... $ make # ...再コンパイル cc seq.c -o seq
再コンパイルの前にいちいちプログラムを削除したり, すでに完成済みのプログラムを何度もコンパイルするのは無駄...
無駄な作業の発生を抑止できるように Makefile を改良してみよう. 依存ターゲットの記述が重要ポイントとなる.
Makefile の編集:
$ vim Makefile
... # ver.2 必要な作業だけを自動判断してコンパイル all: seq sum # make all で連鎖的に seq と sum を make するよ seq: seq.c cc ... sum: sum.c cc ... .PHONY: all # all は疑似ターゲット,ファイルじゃないよ
開発作業(のフリ):ソースの修正(のフリ)とコンパイル
$ vim seq.c # 変更せず再保存 $ vim sum.c # 同上 # または... $ touch seq.c sum.c # 更新時刻だけ変更 $ make # または make all cc seq.c -o seq # seq が生成された cc sum.c -o sum # sum が生成された # make seq とかしていないのに? # all の依存ターゲット seq と sum のエントリが連鎖的に実行されたんだ $ make make: 'all' に対して行うべき事はありません. # seq も sum も完成済みなので,また作る必要ないべさ $ make seq make: 'seq' は更新済みです. # 同上 $ make sum make: 'sum' は更新済みです. # 同上 $ touch sum.c $ make cc sum.c -o sum # 変更したソースだけを自動選択してコンパイル $ touch seq.c $ make cc seq.c -o seq # 同上
このように,ユーザが再コンパイルを指示しても, もしすでにコンパイル済みであれば, その指示は無視される. 必要なものだけが自動的に選択され再帰的・連鎖的にコンパイルされる
なお,特殊なターゲット名 .PHONY の依存ターゲットは, 疑似ターゲット であり, それら依存ターゲットがファイルではないことを示す. 疑似ターゲットと同名のファイルの存在・更新の有無はチェックされない.
コンパイル以外の作業も Makefile で自動化できる.
... # ver.3 コンパイル以外の作業も自動化 all: ... ... clean: -rm sum seq # 「-」はエラー無視.rm がエラーでも make を続行するよ dist: clean ( cd ..; tar zcvf proj-1.tgz proj-1/ ) # ディレクトリの圧縮ファイル proj-1.tgz を生成するよ .PHONY: all clean dist
$ make dist rm sum seq ( cd ..; tar zcvf proj-1.tgz proj-1/ ) proj-1/ proj-1/Makefile proj-1/sum.c proj-1/seq.c $ ls ../ proj-0/ proj-1/ proj-1.tgz
$ tar zcvf ファイル.tgz ディレクトリ
$ tar ztvf ファイル.tgz
$ tar zxvf ファイル.tgz
今度は,数列の総和を計算する単独のプログラムを 複数のソースファイルにより作成してみる.
ディレクトリの準備:
$ cd ../ $ mkdir proj-2 $ cd proj-2/
ソースファイルの作成: (ディレクトリ proj-2/ からDL可能)
$ vim main.c
#include <stdio.h> #include <stdlib.h> #include "sub.h" int main(int argc, char *argv[]) { Array a; int n = 10, from = 1, step = 1; if (argc > 1) n = atoi(argv[1]); if (n > MAXLEN) return (EXIT_FAILURE); if (argc > 2) from = atoi(argv[2]); if (argc > 3) step = atoi(argv[3]); GenSeq(&a, n, from, step); printf("%d\n", Sum(&a)); return (EXIT_SUCCESS); }
$ vim sub.c
#ifdef DEBUG #include <stdio.h> #endif #include "sub.h" // 等差数列を生成する関数(generate sequence) // n:項数,from:初項,step:公差 void GenSeq(Array *a, int n, int from, int step) { int i; int x = from; a->len = n; for (i = 0; i < n; i++) { #ifdef DEBUG // デバッグ出力.動作をわかり易くするよ printf("DEBUG: %d\n", x); #endif a->data[i] = x; x += step; } } // 総和を計算する関数 int Sum(Array *a) { int s = 0; int i; for (i = 0; i < a->len; i++) { s += a->data[i]; } return (s); }
$ vim sub.h
#ifndef SUB_H #define SUB_H // 長さ付き配列の構造体 #define MAXLEN 1024 // 最大長 typedef struct { int data[MAXLEN]; // 配列要素 int len; // 配列長 } Array; // 手抜き...本来なら data[] は MAXLEN なしの動的配列とすべき. // 等差数列を生成する関数(generate sequence) // n:項数,from:初項,step:公差 extern void GenSeq(Array *a, int n, int from, int step); // 総和を計算する関数 extern int Sum(Array *a); #endif
Makefile の作成:
$ vim Makefile
# Makefile # ver.1 とにかく分割コンパイルを自動化 all: sumseq sumseq: main.o sub.o cc main.o sub.o -o sumseq # すべてのオブジェクトをリンク main.o: main.c sub.h cc -c main.c -Wall -DDEBUG # 個別のソースをコンパイル,オブジェクトを生成 sub.o: sub.c sub.h cc -c sub.c -Wall -DDEBUG # 個別のソースをコンパイル,オブジェクトを生成 clean: -rm *.o # 生成したオブジェクトを削除 distclean: clean -rm sumseq # 生成したプログラムも削除 dist: distclean (cd ..; tar zcvf proj-2.tgz proj-2/) # 配布用圧縮ファイルを生成 .PHONY: clean distclean dist
開発作業(のフリ):
$ make cc -c main.c -Wall -DDEBUG cc -c sub.c -Wall -DDEBUG cc main.o sub.o -o sumseq $ make make: 'all' に対して行うべき事はありません. $ touch main.c $ make cc -c main.c -Wall -DDEBUG cc main.o sub.o -o sumseq $ touch sub.c ... $ touch sub.h ...
実行:
$ ./sumseq DEBUG: 1 DEBUG: 2 ... DEBUG: 10 55
変数を利用して Makefile の記述も効率化してみよう.
$ vim Makefile
... # ver.2 変数を利用して記述の効率化 #CFLAGS = -DDEBUG -Wall # コンパイルオプションを変数化(デバッグ用) CFLAGS = -Wall # (デバッグ完了後はこちらを有効化) all: ... ... main.o: ... cc -c main.c $(CFLAGS) # 変数を利用 sub.o: ... cc -c sub.c $(CFLAGS) # 変数を利用 ...
再コンパイルと実行:
$ make clean $ make ... $ ./sumseq 55 # デバッグ出力は抑止しました
$ vim Makefile
... # ver.3 さらに効率化 ... main.o: main.c sub.h # main.c を依存リストの最初に書くことcc -c ...# この行を完全に削除せよ.タブやコメントもNG. sub.o: sub.c sub.h # sub.c を 〃cc -c ...# この行を完全に削除せよ.タブやコメントもNG. .c.o: # *.c から *.o へのコンパイル方法は共通なので統合したよ (デフォルトで,このルールを適用しますよ) cc -c $< $(CFLAGS) # $< は最初の依存ターゲットに置き換わるよ .SUFFIXES: .c .o # .c とか .o は拡張子だよ.ファイル名じゃないよーん clean: ...
オブジェクト.o: ソース.c ヘッダ.h
# cc -c ...
$ make clean $ make ...
ファイルの分割数が多い場合, 依存関係の記述がとても面倒になる. Makefile の作成の準備として, 自動生成してしまうとよい.
$ cc -MM *.c # ソース.c から依存関係を生成 main.o: main.c sub.h sub.o: sub.c sub.h $ cc -MM *.c >> Makefile # 依存関係を Makefile に追記 $ vim Makefile ... # 依存関係以外の記述を手動で編集
なお,当然ですが,ソース内のインクルードを適切に記述していないと, 依存関係も正しく生成されませんよ.
質問 Q1〜Q4 に回答し,電子メールで提出せよ.
メールの送信形式を必ず「テキスト形式 or プレーンテキスト」に変更せよ.