.. _buildproc: ビルド処理 ======================================================================== 実行ファイル、ライブラリとソースコードファイルとの関係を :ref:`sourcecode` にて説明しました。ただし説明は概念的なものでした。 ここではビルド処理の中心を担うgccのコマンドラインを具体的に示します。 またビルドツールの一つであるmakeについて説明します。 gccのコマンドラインを示すのは、2つ目的があります。 一つはビルド処理を解析して、読むべきソースコードを割り出せるようになる ための予備知識となるためです。 規模の大きいソフトウェアを調査する場合、ソースコードファイルの数は増え、 実行ファイル、ライブラリとソースコードの関係が複雑化します。開発者にとっ ても複雑なビルド処理を手動で実施するのは困難です。そこでビルドツールを 用いて、一連のビルド処理を自動化しています。開発者はビルドスクリプトと 呼ばれるファイルにビルド手順を記述します。ビルドツールはビルドスクリプ トを入力として実行ファイルやライブラリをビルドします。 そのため、読むべきソースコードを割り出すのにビルドスクリプトを読む必要が 出てきます。具体的なコマンドラインを知っていればビルドスクリプトを読むとき の礎となります。 もう一つは、実験用のプログラム(以下ミニプログラム、独自用語)を書いて動 かすためです。ソースコードを読んでいると、読み辛い箇所が出てきます。そ のとき、その部分だけ切り出した、動作確認だけを目的とするプログラムを作 成し、実行したくなることがります。その結果を見て読解の補助とします。こ のときミニプログラムに(読解対象のソフトウェアと同様に)ライブラリをリンク したり、特別なビルド条件(後述)を与える具体的な方法を知っておく必要があり ます。 gccのコマンドライン ------------------------------------------------------------------------ gccのコマンドラインオプションには大変な種類があります。ここでは主要なものを 紹介します。詳細についてはinfoドキュメントを読んで下さい。 プリプロセス ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, :ref:`runcpp` を参照して下さい。 コンパイル処理 ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, foo.cというソースコードをコンパイルしてfoo.oを得るための典型的なコマンドライン の例を示します。厳密には-Iと-Dはプリプロセッサへの指示になります。 :: $ gcc -g -O2 -c -I. -I../include -DDEBUG=1 foo.c オプションの意味を説明します。 ``-c`` は、リンク処理は実施せずコンパイル処理までを進めよ、という意味に なります。gccはソースコードファイルの拡張子 ``.c`` を ``.o`` で置き換え た名前のオブジェクトファイルにコンパイル結果が出力します。 ``-IPATH`` は 指定したパスをインクルードパスへ追加します。 gccはソースコード中にインクルードディレクティブ(``#include``)があると、 ディレクティブで指定されたファイル(インクルードファイル)をインクルード パスから探します。gccにはデフォルトのインクルードパスが内蔵されています。 ``-I`` で指定したパスは、デフォルトのインクルードパスに優先してインクルード ファイルの検索の対象となります。 例にある ``-I. -I../include`` というのは、カレントディレクトリ(``.``)と 親ディレクトリ(``..``)の直下にある include という名前のディレクトリ から優先して、foo.c中でインクルードしているファイルを探せという意味に なります。 gccに内蔵されたデフォルトのインクルードパスを確認するには空の.cファイルを 用意して、そのファイル名とともに-vオプションをつけてgccを呼び出します:: $ touch baz.c $ gcc -v baz.c #include <...> search starts here: /usr/lib/gcc/x86_64-redhat-linux/4.7.2/include /usr/local/include /usr/include End of search list. .. code-block:: c #include とある場合、gccはインクルードパスからbar.hを探します。 .. code-block:: c #include "bar.h" とある場合、gccはインクルードパスに優先してカレントディレクトリからbar.hを 探します。みつからなければインクルードパスから探します。 ``-D`` はマクロを定義します。 ``#define`` と同じ意味です。 ``-DDEBUG=1`` というのは以下の記述と同じ意味になります。 .. code-block:: c #define DEBUG 1 という意味になります。このようなマクロ定義は、次に示すような 条件付きコンパイルの条件として参照されます。 .. code-block:: c #ifdef DEBUG #include #define LOG(X) fprintf(stderr, "LOG: %s\n", X) #else #define LOG(X) #endif ... LOG("something debug log"); .cファイルをいくら調べても、コンパイル時にどのような条件が与えられたのか がわからないと、 .. code-block:: c LOG("something debug log"); の処理を説明できません。 他に良くみかけるオプションに ``-g`` と ``-On`` があります。 ``-g`` はデバッ ガの使うデバッグ情報をコンパイル結果に埋め込め、という指示となります。 一方 ``-On`` はレベルnの最適化を実施せよ、という指示となります。 リンク処理 ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, (この説明では共有ライブラリを想定しています。) コンパイル処理が完了して複数のオブジェクトファイルが用意できているとします。 複数のオブジェクトファイルをリンクして実行ファイルを得るための典型的な コマンドラインの例を示します。 :: $ gcc -o my_app -L . -L ../libs -lmy_cypto -lmy_algorithm foo.o bar.o baz.o ``-o FILE`` で生成する実行ファイルの名前を指定します。 ``-lNAME`` でリンクするライブラリの名前を指定します。``-lNAME``と指定す るとgccはlibNAME.so という名前のライブラリファイルを後述するライブラリ サーチパスから探し実行ファイルにリンクします。例では ``-lmy_cypto -lmy_algorithm`` としているので gccはlibmy_cryto.so と libmy_algorithm.so を my_app にリンクします。 ライブラリパスは gcc がリンク処理時にライブラリを指すパスのことです。 ``-print-search-dirs`` オプションをつけてgccを呼び出すと、gccに内蔵された デフォルトのライブラリパスを確認できます。 :: $ gcc -print-search-dirs ... libraries: =/usr/lib/gcc/x86_64-redhat-linux/4.7.2/:... ``-LPATH`` は 指定したパスをライブラリパスに追加します。 なお実行ファイルの生成にあたり、実行ファイル中から参照されている全ての 変数、関数について定義を発見できない場合、リンク処理は失敗します。 ライブラリ ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ここで、ビルドの観点からライブラリを使おうとすると.hファイルと.soファイルの 2つがかかわってきます。 .hファイル ........................................................................ ライブラリに関連する変数や関数の宣言および型の定義が記載されています。これは コンパイル処理、すなわち.cから.oへ変換するのに必要となります。 プログラムの実行時には必要ありません。 .soファイル ........................................................................ (この説明では共有ライブラリを想定しています。) ライブラリに関連する変数や関数の定義が記載されています。これはリンク処理 及び実行時に必要となります。 ライブラリのソースコード中で「プログラム全域」のスコープを持つ変数と関 数だけが「エクスポート」されます。そのライブラリにリンクするアプリケー ションやライブラリは「エクスポート」された変数と関数だけを利用すること ができます。 エクスポートされた変数や関数は通常ライブラリのヘッダファイルに宣言が 記載されています。しかし稀にライブラリの開発者がエクスポートしていても 利用してもらいたくない場合と考える変数や関数について宣言を記載しない場合が あります。 nmコマンドを使うとライブラリからエクスポートされている変数や関数の名前を 知ることができます。 :: $ nm -D /usr/lib/libz.so.1 ... U close 42681e30 T compress 42681d40 T compress2 U free ... Tはエクスポートされている関数を意味します。 Uは未定義の名前を意味します。 例にはありませんが、エクスポートされた変数を意味するB, Dの記号が あります。 :: $ nm -D /usr/lib/libc.so.6 | grep ' free$' 421ee090 T free とあるのでzlibで未定義だったfreeはlibcで関数として定義されていることが わかります。 パッケージング ........................................................................ Fedoraでは目的によって構成を柔軟に変更できるよう、ライブラリについて .soファイルを含むバイナリパッケージから独立した.hだけを含むバイナリパッケージを 用意しています。 .hだけを含むバイナリパッケージはライブラリを自身のアプリケーションに組込もう としている開発者にだけ必要であろう、という理由からパッケージ名に -devel という サフィックスが付けられています。 なおソースパッケージは一つで共通です。 例:: $ rpm -qi zlib Name : zlib Version : 1.2.5 Release : 7.fc17 ... Summary : The zlib compression and decompression library $ rpm -ql zlib /lib64/libz.so.1 /lib64/libz.so.1.2.5 ... $ rpm -qi zlib-devel Name : zlib-devel Version : 1.2.5 Release : 7.fc17 ... Summary : Header files and libraries for Zlib development $ rpm -ql zlib-devel /usr/include/zconf.h /usr/include/zlib.h ... ビルドツールとビルドスクリプト ------------------------------------------------------------------------ ビルドツールはビルド手順を記述したファイル(ビルドスクリプト)を入力とし てビルド処理を実行します。すなわちリンカやコンパイラを呼び出して実行ファ イルやライブラリを生成します。またなんらかのツールを呼び出してソースコー ドを生成する場合もあります。 ビルドツールの引数やビルドスクリプトを変更することでビルド結果を調整で きるようになっているものもあります。例えばビルド全体で使うコンパイラの 最適化のオプションを与えたり、実行時に開発者向けの情報がより多く出力さ れるようデバッグモードを有効にしたり、選択的な機能を組込むかどうかを指 定できるようになっています。このようなビルド時の調整内容をビルド コンフィギュレーションと呼びます。 ソースコードの記述に使われている言語がC言語で同じであったとしても、ビル ドツールの選択とビルドスクリプト、すなわちビルドツールで何をやらせるか というのはソフトウェア毎に大きく異なります。ここではFedora上で動作する ソフトウェアのビルドに広く使われているmakeコマンドの概要を説明します。 .. 絵が必要 FedoraなどGNU/Linuxディストリビューションは、様々なソフトウェアをビルド してバイナリパッケージの形で提供します。ビルドツールやその呼び出し手順 が異なる可能性のある様々なソフトウェアをビルドするのは簡単ではありませ ん。 Fedoraの開発ではソフトウェア毎にspecと呼ばれるファイルを用意して、 個別のソフトウェアのビルド手順からFedoraのバイナリパッケージを作成する 仕掛け(rpmbuild)を分離しています。ソフトウェア毎のパッケージ保守担当者 がspecファイルを作成します。specファイルに、そのソフトウェアで採用してい るビルドツール(を含むパッケージの名前)とビルドツールの呼び出し手順を記 述します。 ソースコードを読む立場からすると、関心のあるソフトウェアについてパッケー ジ化されているのであれば、specファイルを読むことでどのようなビルドツー ルを使っていて、それをどのような手順で呼び出すのかがわかります。あるか どうかわからないビルド手順を説明した文章をソースコードツリー中から探す よりも効率が良いでしょう。ただしspecファイルもまた独自の文法と意味を持 つので、それを知っておく必要があります。 makeとMakefile ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, makeコマンドは特に指定しないと実行したディレクトリにあるMakefileという名前の ファイルを探しそれを入力、すなわちビルドスクリプトとしてビルド処理を実行します。 Makefileには、ビルドの各ステップ(ルール)を以下の書式で記述します。 .. code-block:: makefile 成果物: 入力 ... 入力から成果物を得るコマンドライン ... 成果物(ターゲット)は一つとは限りません。あるターゲットが他のターゲット の入力となるように記述することで巨大なビルド処理をルールの集合として記 述できます。 あるターゲットをビルドするにはmakeの引数にターゲットを指定して呼び出します:: make [ターゲット] 特にターゲットを指定しない場合、Makefile中にならぶルールのうち、先頭の ルールに記載されたターゲットがビルドの対象となります。 makeコマンドは読み込んだMakefileを調べてターゲットをビルドするのに必要な入 力が何か特定します。すでに入力(として記載されたファイル)が存在すれば、 ルールの記述通り入力からターゲットを得るコマンドラインを実行します。もし入 力が存在しなければ、まずその入力をターゲットとするルールを探し、入力にあた るファイルをビルドしようとします。これを再帰的に繰り返し、引数で 指定されたターゲットに必要な入力をビルドした上でターゲットをビルドします。 ターゲットがすでに存在する場合でも、ターゲットをビルドし直すことがあり ます。ファイルシステムから得られる入力の最終更新日時と、ターゲットの最 終更新日時を比較して、入力の方が新しい場合にはターゲットをビルドし直し ます。 Makefileの例 ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, 今foo.c、bar.cをコンパイルしてfoo.o、bar.oを生成し、その二つをリンクし て、myappという実行ファイルを作成したいとします。foo.c、bar.cが Makefileと同じディレクトリにあるとするとMakefileは次のように書けます。 .. code-block:: makefile myapp: foo.o bar.o gcc -o myapp foo.o bar.o foo.o: foo.c gcc -c foo.c bar.o: bar.c gcc -c bar.c 変数 ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, Makefile中で変数を定義、参照することができます。繰り返し使う文字列や、 ビルド時に変更可能なオプションを明示するのに使うことができます。 変数を定義するには、 .. code-block:: makefile VAR = VALUE とします。参照するときは、 .. code-block:: makefile $(VAR) とします。初期化されていない変数を参照すると空文字列となります。 次に例を示します. .. code-block:: makefile DEBUG_FLAGS = -g -DDEBUG=1 OFLAGS = -O2 CFLAGS = -I. -I../include $(OFLAGS) $(DEBUG_FLAGS) myapp: foo.o bar.o gcc -o myapp foo.o bar.o foo.o: foo.c gcc -c $(CFLAGS) foo.c bar.o: bar.c gcc -c $(CFLAGS) bar.c 変数の値はmakeコマンドを呼び出すコマンドラインから指定することもできます:: $ make VAR="VALUE" ターゲット コマンドラインで指定した変数がMakefile中にも定義されている場合、その値は コマンドラインで指定したものが優先します。上の紹介したMakefileの例であれば、 次のようにしてDEBUG_FLAGSの値をコマンドラインからクリアできます:: $ make DEBUG_FLAGS= myapp foo.cやbar.cで条件付きコンパイルの条件にDEBUGを参照している場合、コンパイル 結果が変ってきます。それはmakeファイルの呼び出し引数によって、読むソース コード箇所も変ってくる、ということを意味します。 内蔵ルール ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, makeコマンド自体がcコンパイラを呼び出して.cから.oを得るといった、典型 的な利用ケースに対するルールを内蔵しているので、実際には内蔵ルールを利 用することでMakefileの内容を短くすることができます。 内蔵ルールを利用した例 .. code-block:: makefile DEBUG_FLAGS = -g -DDEBUG=1 OFLAGS = -O2 CFLAGS = -I. -I../include $(OFLAGS) $(DEBUG_FLAGS) myapp: foo.o bar.o gcc -o myapp foo.o bar.o 内蔵ルールの閲覧 ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, :: make -p -f /dev/null とすると全ての内蔵ルールが出力されます。 以下は.cファイルから.oファイルの生成に関連する部分だを抜粋したものです。 .. code-block:: makefile CC = cc OUTPUT_OPTION = -o $@ COMPILE.c = $(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c .c.o: $(COMPILE.c) $(OUTPUT_OPTION) $< rpmbuildとspecファイル ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, :ref:`srcrpm` にて延べた手順に従い、ソースコードツリーを合成する方法を 延べました。specファイルはsrc.rpmをインストール(rpm -ivh \*.src.rpm) すると ~/rpmbuild/SPECS以下に配置されます。 rpmbuild のオプションとして -bp を指定してソースコードツリーを合成しま した。バイナリパッケージを作成するには、-bp のかわりに -bb を指定します:: $ rpmbuild -bb ~/rpmbuild/SPECS/coreutils.spec ... バイナリパッケージは、~/rpmbuild/RPMS/x86_64/coreutils-8.15-7.fc17.x86_64.rpm といった名前で生成されているはずです。出力を見てバイナリパッケージ作成の過程で makeやgccが呼び出されていることを確認して下さい。 specファイルにはソースコードを合成する手順やビルドするのに必要なビルド ツールを含むパッケージ(ビルド時依存パッケージ)とビルド手順が記載されて います。 coreutilsパッケージのspecファイル~/rpmbuild/SPECS/coreutils.specを見てみ ましょう。 ビルド時依存パッケージは:: BuildRequires: パッケージ... という文法で記載されています。 .. code-block:: spec BuildRequires: libselinux-devel BuildRequires: libacl-devel BuildRequires: gettext bison BuildRequires: texinfo BuildRequires: autoconf BuildRequires: automake %{?!nopam:BuildRequires: pam-devel} BuildRequires: libcap-devel BuildRequires: libattr-devel BuildRequires: gmp-devel BuildRequires: attr BuildRequires: strace coreutils.specからBuildRequiresを指定した行を抜粋しました。 :ref:`srcrpm` にて延べた通り、yum-builddepを使うと、引数 で指定したパッケージを作成するのに必要となるパッケージ、す なわち、そのパッケージのspecファイル中でBuildRequiresとして 指定されているパッケージをインストールします。 行頭%ではじまる行で、specファイルは理論的に分割されています。分割された 各範囲をセクションと呼びます。ソースコードツリーを合成するのに使った-bpオプション を指定してrpmbuildを実行すると %prepセクション に記載された内容までを実行します。 バイナリパッケージの作成に使った -bb オプションを指定してrpmbuildを実行すると %prepセクションに加えて%buildセクションに記載された内容までが実行されます。 以下にcoreutils.specの%prepセクションと%buildセクションを抜粋します。 .. code-block:: spec %prep %setup -q # From upstream %patch1 -p1 -b .trunc %patch2 -p1 -b .xnondir # Our patches %patch100 -p1 -b .configure %patch101 -p1 -b .manpages %patch102 -p1 -b .tcsadrain %patch103 -p1 -b .sysinfo %patch104 -p1 -b .dfdirect %patch107 -p1 -b .mkdirmode ... chmod a+x tests/misc/sort-mb-tests tests/df/direct tests/cp/attr-existing || : #fix typos/mistakes in localized documentation(#439410, #440056) find ./po/ -name "*.p*" | xargs \ sed -i \ -e 's/-dpR/-cdpR/' %build export CFLAGS="$RPM_OPT_FLAGS -fno-strict-aliasing -fpic" %{expand:%%global optflags %{optflags} -D_GNU_SOURCE=1} #autoreconf -i -v touch aclocal.m4 configure config.hin Makefile.in */Makefile.in aclocal -I m4 autoconf --force automake --copy --add-missing %configure --enable-largefile %{?!nopam:--enable-pam} \ %{?!noselinux:--enable-selinux} \ --enable-install-program=su,hostname,arch \ --with-tty-group \ DEFAULT_POSIX2_VERSION=200112 alternative=199209 || : # Regenerate manpages touch man/*.x make all %{?_smp_mflags} \ %{?!nopam:CPPFLAGS="-DUSE_PAM"} # XXX docs should say /var/run/[uw]tmp not /etc/[uw]tmp sed -i -e 's,/etc/utmp,/var/run/utmp,g;s,/etc/wtmp,/var/run/wtmp,g' doc/coreutils.texi %check ... %prepセクションではアップストリームに由来するソースコードを変更します。 %prepセクションの大部分が%patchで始まる行です。%patchで始まる行は、 パッチファイルを適用してソースコードを変更することを指示しています。 :ref:`srcrpm` の箇所で ソースコードを「合成する」と書きました。 合成とは具体的には%prepに記載されたソースコード変更処理、特に パッチファイルを適用することだったのです。 %buildセクションを見てみると最終的にmake allを実行することがわかります。 .. _diff: 変更履歴とパッチファイル ------------------------------------------------------------------------ ソースコードを読んでいるうちに、あるソースコード行について変更の経緯を調べ たくなることがあります。ソースコードがバージョン管理システムに格納されてい れば、変更履歴を調べることになります。 対象のソフトウェアがフリー/オープンソースソフトウェアであれば、開発者が メーリングリストに投稿するパッチファイルを調べることになります。 変更履歴もパッチファイルも同じ形式(diff形式)で具体的な変更が説明されて います。ここではdiff形式の読み方を説明します。 diffコマンドを使うと2つのファイルの差異をdiff形式で表示できます。 :: $ cat x.c #include int main(void) { printf("hello, world\n"); return 0; } $ cat y.c #include int main(void) { const char* msg = "hello, world"; printf("%s\n", msg); return 0; } $ diff -u x.c y.c --- x.c 2013-01-28 06:20:41.271385668 +0900 +++ y.c 2013-01-28 06:21:11.575162951 +0900 @@ -2,6 +2,7 @@ int main(void) { - printf("hello, world\n"); + const char* msg = "hello, world"; + printf("%s\n", msg); return 0; } x.c(オリジナルファイル)を編集更新してy.c(更新ファイル)を得たとして、そ の差分をdiffコマンドで出力しています。 `---` で始まる行はオリジナルファイルを `+++` で始まる行で更新ファイルを表します。 :: --- x.c 2013-01-28 06:20:41.271385668 +0900 +++ y.c 2013-01-28 06:21:11.575162951 +0900 続く `@@` で始まる行は、 オリジナルファイルと変更ファイル中で差異のあった行の番号と範囲を表します。 :: @@ -2,6 +2,7 @@ オリジナルファイルx.cの2行目から6行、更新ファイルの2行目から7行に差異が あったことがわかります。 行頭の-と+が具体的は差異を表します。-は削除された行、+は追加された行です。 :: int main(void) { - printf("hello, world\n"); + const char* msg = "hello, world"; + printf("%s\n", msg); return 0; } .. .. .. figure:: buildtool-gen.svg .. この図版では、 .. ツール名(入力ファイル名) .. を要素として、要素間を矢印で接続しています。矢印の根本にツール名を記載 .. し、矢印の先にそのツールが生成するファイルを記載しています。点線で囲ん .. でいるのは、そのツールの利用が選択的であることを意味します。 .. autoconfとconfigure .. ........................................................................ .. TBW .. automake .. ........................................................................ .. TBW .. .. figure:: buildtool-run.svg .. .. figure:: buildtool-gen-rpm.svg