開発効率化3(自動コンパイル)

前回,プログラミング作業の効率化のためソースファイルを分割したが, かえってコンパイル作業が面倒なことになっていた. そこで今回は,分割コンパイルを自動化・効率化しよう.

Unix では,複雑なコンパイル作業(cc ...)を 自動化するためのツール make が用意されている. Makefile という名前のファイルにコンパイル手順を記録しておけば, どんなに複雑なコンパイル作業であっても 単純に make コマンド1発だけで完了できるようになる.

教科書の該当範囲:なし

超単純な make

Step 0. ソースの作成

まずは,ソースファイルが1個だけの単純な開発プロジェクトを例として, make を使ってみたい. このプロジェクトのためのディレクトリを作成し,その中で作業を進めよう.

$ mkdir ~/c-1211	# 本日のディレクトリ
$ cd ~/c-1211/

$ mkdir proj-0	# このプロジェクトのディレクトリ
$ cd proj-0/

$ vim hello.c
#include <stdio.h>

int main(void)
{
	printf("こんにちworld\n");
	return (0);
}

今回は,まだコンパイルしませんよ.

Step 1. Makefile の作成

コンパイル方法をファイル Makefile に記述します.

$ vim Makefile
hello:	hello.c
	cc hello.c -o hello
なお,2行目の先頭の空白は [Tab] でなければならない. [Space] は NG.

一般に,Makefile は, 次の書式のようなエントリ(要素)から構成される:

ターゲット:	依存ターゲット ...
	ターゲット生成のためのコマンド
Step 2. make の利用

では,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

Step 0. ソースの作成

複数個のプログラムから構成されるシステム開発のプロジェクトでも make を利用してみよう. 数列生成プログラムと総和計算プログラムとを作成する.

ディレクトリの準備:

$ cd ../
$ mkdir proj-1
$ cd proj-1/

ソースファイルの作成:

$ 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);
}
Step 1. 自動的にコンパイル

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

再コンパイルの前にいちいちプログラムを削除したり, すでに完成済みのプログラムを何度もコンパイルするのは無駄...

Step 2. スマートにコンパイル

無駄な作業の発生を抑止できるように 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 の依存ターゲットは, 疑似ターゲット であり, それら依存ターゲットがファイルではないことを示す. 疑似ターゲットと同名のファイルの存在・更新の有無はチェックされない.

seqsum はファイルだが, 疑似ターゲット all はファイルではない. もしファイル all があっても,無視され, seqsum だけが make される.
Step 3. コンパイル以外も自動化

コンパイル以外の作業も 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
これでオープンソース化も捗る. なお,dist は配布(distribution)用ファイルを意味する.
補足:圧縮ファイルの取扱方法

分割ソースのmake

Step 0. ソースの作成

今度は,数列の総和を計算する単独のプログラムを 複数のソースファイルにより作成してみる.

ディレクトリの準備:

$ cd ../
$ mkdir proj-2
$ cd proj-2/

ソースファイルの作成:

$ 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
Step 1. 自動的に分割コンパイル

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
Step 2. スマートな Makefile

変数を利用して 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		# デバッグ出力は抑止しました
Step 3. さらにスマートな Makefile

$ 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:
...
$ make clean
$ make
...
補足:依存関係の自動生成

ファイルの分割数が多い場合, 依存関係の記述がとても面倒になる. Makefile の作成の準備として, 自動生成してしまうとよい.

$ cc -MM *.c
main.o: main.c sub.h
sub.o: sub.c sub.h

$ cc -MM *.c >> Makefile

$ vim Makefile
...	# 依存関係以外の記述を手動で追加

なお,当然ですが,ソース内のインクルードを適切に記述していないと, 依存関係も正しく生成されませんよ.


本日の課題

レポートを提出せよ

質問 Q1〜Q4 に回答し,電子メールで提出せよ.

  • 注意事項:

  • (c) 2023, yanagawa@kushiro-ct.ac.jp