ビルド作業と向き合う:Makefile、ccache、Autotools、Yocto Projectについて

  • 20
    Like
  • 2
    Comment

前書き

本記事は、Linux Advent Calendar 2016の12/16(金)用として公開しています。
内容としては、Makefile/Autotools(C言語)によるビルドについてがメインとなります。

目次

  1. 動作環境
  2. 使いまわしやすい汎用的なMakefile
  3. makeのログを残すShellScript(bash)
  4. キャッシュ利用でビルドを高速化:ccache
  5. Makefileを自動作成:Autotools(autoconf/automake/libtools)
  6. ビルドツールの活用:Yocto Project
  7. 参考書籍

動作環境

本記事の内容は、ハードウェアに依存する部分は少ないと思います。
Linuxディストリビューション・パッケージバージョンのみ、確認をお願いいたします。

 ・Intel NUC(BOXNUC6I3SYK)
 ・Intel® Core™ i3-6100U Processor(3M Cache, 2.30 GHz)
 ・メモリ32GB(DDR4)
 ・Debian8.6(64bit)
 ・gcc(Debian 4.9.2-10)
 ・make(GNU Make 4.0)
 ・bash(GNU bash 4.3.30)
 ・ccache(3.1.12)
 ・M4(GNU M4 1.4.17)
 ・autoconf(GNU Autoconf 2.69)
 ・automake(GNU automake 1.14.1)
 ・libtool((GNU libtool 2.4.2)
 ・Yocto Project(version2.1、krogoth)

使いまわしやすい汎用的なMakefile

Linux環境でC言語のソースファイルをビルドする場合は、Makefile(make, gcc)の利用が一般的です。
育ちの良いOSS(Open Source Software)ならば、Autotoolsによって自動でMakefileを作成できます。
Autotoolsは非常に強力な仕組みですが、利用するにはそれなりの学習コストが必要となります。

このような背景を踏まえれば、開発の第一歩では、既存のMakefileを流用した方が楽です。
そこで、今回は「C/C++中規模プロジェクトのための超シンプルなMakefile(Job Vranish)」から、
out-of-sourceビルド用Makefileを拝借し、その内容を修正します。

ここでのout-of-sourceビルドとは、以下のディレクトリ構成のように、
「ソースが格納されるディレクトリ」および「生成物が格納されるディレクトリ」を分離する方式です。
(伝統的にはソースファイルは"project/src"以下に配置しますが、本例では"project"以下に配置します)

example_directory.
project              # ソースコードを格納するルートディレクトリ
├── .git
├── LICENSE
├── Makefile
├── README.md
├── build            # 生成物が格納されるディレクトリ
├── daemon           # ビルド対象ディレクトリ
│   ├── daemon.c
│   └── daemon.h
├── io               # ビルド対象ディレクトリ
│   ├── io.c
│   └── io.h
├── main             # ビルド対象ディレクトリ
│   └── keyLogger.c
└── utiles           # ビルド対象ディレクトリ
    ├── utiles.c
    └── utiles.h

 
前述のJob Vranish氏作成のMakefileから、変更を加えた箇所は以下の通りです。
 1. アセンブリ言語、C++用の記述を削除(ただし、存在していても無問題)
 2. .gitディレクトリをソースコードディレクトリとして扱わないように変更
 3. ソースディレクトリを"project/src/"以下から"project"以下に変更
 4. make debugで、バイナリにデバッグ情報を付与できるように変更(gdbが使用可能になる)
 5. 外部ライブラリの受け口(変数ETC_INC)を用意した点
   → 例えば、GTK+ライブラリを加える場合、以下のように変数定義します。
     ETC_INC := $(shell pkg-config --cflags --libs gtk+-2.0)

 
修正したMakefileを以下に示します。

Makefile.
# ターゲット名は自由に変更可能(a.outでなくて良い)
TARGET_EXEC ?= a.out                                                          


# ビルドディレクトリとソースディレクトリは自由に変更可能
BUILD_DIR ?= ./build
SRC_DIRS ?= ./


# findコマンドでビルド対象のソースコードを"./"以下から探します。
SRCS := $(shell find $(SRC_DIRS) -name *.c)
OBJS := $(SRCS:%=$(BUILD_DIR)/%.o)
DEPS := $(OBJS:.o=.d)


# ライブラリやヘッダが格納されているディレクトリを"./"以下から探します。
# なお、gitを利用する場合を想定し、".git"は探索範囲外としています。
# 変数ETC_INCには、外部ライブラリを使用する際に、
# そのライブラリを使用するために必要なオプションを記載します。
INC_DIRS := $(shell find $(SRC_DIRS) -type d \
-name '.git' -prune -o -type d  -print)
ETC_INC := $()
INC_FLAGS := $(addprefix -I,$(INC_DIRS))
CC := gcc 
CPPFLAGS ?= $(INC_FLAGS) -MMD -MP 


$(BUILD_DIR)/$(TARGET_EXEC): $(OBJS)
    $(CC) $(OBJS) -o $@ $(LDFLAGS) $(ETC_INC)


# 元のMakefileではディレクトリ作成部分が原因でErrorが発生したため、
# 現在の形("mkdir -p"を変数に格納せず、そのまま使用する方式)に変更。
$(BUILD_DIR)/%.c.o: %.c 
    mkdir -p $(dir $@)
    $(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@ $(ETC_INC)


# 引数に"debug"が指定された時のみ、gdbが使用できるバイナリを生成します。
# makeに搭載されているMAKEマクロは、makeの"-n"オプション(dry run)を無視して、
# 再帰的にmakeを実行します。
.PHONY: debug
debug:
    $(MAKE) "CFLAGS=-g3 -O0"


# 元のMakefileでは、clean時に"build"ディレクトリごと削除していましたが、残すように変更。
.PHONY: clean
clean:
    rm -rf $(BUILD_DIR)/*

-include $(DEPS)

makeのログを残すShellScript(bash)

プログラム規模が大きくなると、make時にエラーが発生する機会が増えます。
当然、エラーメッセージを確認しながら、ビルドエラー箇所を修正する事になりますが、
エラーの規模によっては「エラーメッセージ部分を検索して探したい」という状態になるかもしれません。

このような場合に備えて、私はmakelogスクリプト(後述)によってビルドログを残しています。
このmakelogスクリプトは、makeコマンドと同じオプションが適用可能です。
使用例は$ makelogで"ビルド+ログ生成"、
$ makelog -dで"ビルド+デバッグ情報付きログ生成"といった形です。
ログは、カレントディレクトリ以下の"log/success"か"log/fail"以下に生成されます。

以下がmakelogスクリプトです。

makelog.sh
#!/bin/bash
# @(#) This script takes log for "make" command.

CURRENT_DIR=$(pwd)
LOG_DIR="${CURRENT_DIR}/log"
SUCCESS_LOG_DIR="${CURRENT_DIR}/log/success"
FAILURE_LOG_DIR="${CURRENT_DIR}/log/fail"
LOG_FILENAME="makeLog`date +%Y%m%d_%H%M%S`"

function makeDir(){
  if [ -d $1 ]; then
    :
  else
    mkdir -p $1
  fi
}

# Connect arguments as character string
ARGS=""
for ARG in "$@"
do
    ARGS="$ARGS $ARG"
done
# Delete the space of the beginning of string.
# If space exists beginning of string, it causes argument error.
ARGS=$(echo "$ARGS" | sed -e s/^\ //g)

# Make a directory for log in lower than current directory
makeDir ${SUCCESS_LOG_DIR}
makeDir ${FAILURE_LOG_DIR}

# Run "make" command and leave log in success directory or failuer one.
make ${ARGS} 2>&1 | tee ${LOG_DIR}/${LOG_FILENAME}
if [ $? -eq 0 ]; then
    mv ${LOG_DIR}/${LOG_FILENAME} ${SUCCESS_LOG_DIR}/.
else
    mv ${LOG_DIR}/${LOG_FILENAME} ${FAILURE_LOG_DIR}/.
fi

 
上記のスクリプトを使用するには、"/usr/local/bin"以下にmakelog.shをリネームして配置します。
スクリプトの準備から配置までの手順を以下に示します(コマンド中の"#"以降は、コメントです)。
面倒な方は私のGitHubにインストーラ付きで同じ物があるので、そちらを利用してください。

$ touch makelog.sh       # ファイル作成
$ vim makelog.sh         # エディタ起動後、上記のスクリプトの内容を転記。
$ chmod a+x makelog.sh   # 実行権の付与
$ sudo mv makelog.sh /usr/local/bin/makelog   # スクリプトの移動 & リネーム

GitHubから入手する場合は、以下の手順です。
ただし、余計なスクリプトが付属するため、念の為READMEを読んでから導入して下さい。

$ sudo apt-get install git  # "apt-get"部分は利用環境に応じたパッケージマネージャに変更して下さい
$ cd ~/Desktop              # gitレポジトリのクローン先。任意のディレクトリでOK
$ git clone https://github.com/nao1215/BashScriptsCompilation.git -b master
$ sudo bash BashScriptsCompilation/installScripts  # installerの実行

キャッシュ利用でビルドを高速化:ccache

小規模なプログラムであればビルド速度が気になる機会は少ないと思いますが、
Linuxカーネルレベルの規模であれば、ビルド待ち時間が発生してしまいます。

そこで、ビルド時間を早めるツールとして、ccacheを利用します。
ccacheはビルド時のキャッシュを残し、二回目以降のビルドを速度を改善するツールです。
導入すれば簡単に使用できるため、以下では導入手順のみを示します。

まず、ccacheのインストール後、ログイン時にccacheの設定が自動で反映されるように、
~/.bash_profileファイルを編集します。

$ sudo apt-get install ccache

$ echo "export USE_CCACHE=1" >> ~/.bash_profile         # ccacheの使用宣言
$ echo "export CCACHE_DIR=~/.ccache" >> ~/.bash_profile # キャッシュの格納先
$ echo "export set CC='ccache gcc'" >> ~/.bash_profile  # gccコマンド時にccacheを使用。
$ echo "export set CXX='ccache g++'" >> ~/.bash_profile # g++コマンド時にccacheを使用。

$ source ~/.bash_profile  # ログインし直す代わりに、手動で変更した設定の反映(初回時のみ)

次に、makeコマンドの使用時にccacheを利用する場合は、以下のいずれかを行う必要があります。
 ・Makefile内のCC、CXX変数を変更(例:CC="ccache gcc")
 ・makeコマンド実行時にオプションを付ける事(例:make CC="ccache gcc")
 
ccacheに必要な設定は、以上です。
キャッシュクリアや設定変更などを行いたい場合は、以下のccacheオプションを参照してください。

Usage:
    ccache [options]
オプション 説明
-c, --cleanup 古いファイルを削除し、サイズカウンタを再計算します。
-C, --clear 完全にキャッシュを削除します。
-F, --max-files=N キャッシュ内に格納できる最大ファイル数を設定します。通常は0(制限なし)。
-M, --max-size=SIZE キャッシュの最大サイズを設定します(◯G、◯M、◯Kという記述方法で指定。例:ccache -M 10G )
-s, --show-stats 統計情報(キャッシュヒット率、キャッシュ内のファイル数など)を表示します。
-z, --zero-stats 統計情報のカウンターをゼロクリア(初期化)します。

 
最後に、参考として、Linuxカーネルのビルド時間(ccache有り、無し)を比較します。
使用したカーネルはVersion4.8.9(stable)、コンフィグはx86_64_defconfigです。
計測時に使用したコマンドは、通常ビルド時がtime make -j8
ccache使用時がtime make CXX="ccache g++" CC="ccache gcc" -j8です。

各実行時間を以下の表に示します。
ccacheを利用した場合、二回目以降のビルドが劇的に早くなります。
ちなみに、ccache公式のパフォーマンス測定では、約37倍の実行速度が示されています。

キャッシュの有無 実行時間 倍率
ccache無し(通常ビルド時) 6分28秒 1.00x
ccache有り(初回ビルド時、キャッシュ無し) 7分20秒 0.88x
ccache有り(二回目ビルド時、キャッシュ有り) 29秒 13.38x

Makefileを自動作成:Autotools(autoconf/automake/libtools)

AutotoolsはOS(ディストリビューション)間の差異を吸収して、
環境に合わせたMakefileを作成するツール群です。Autotoolsにより、パッケージの移植性が高まります。

まず、LinuxはDebian/Ubuntu/CentOS/Fedoraといった多様なOSおよびバージョンが存在し、
それらの中で「ヘッダファイル・マクロ・コマンドなどが存在する・しない」といった環境差が生じています。
これらの環境差を吸収して、Makefileを作成してくれるツール群(Autotools)が下表になります。

ツール名 説明
make プログラムのビルド作業を簡略化するツールであり、Makefileに記載されたルールに従って自動的にビルド作業を実行する。
M4 マクロプロセッサであり、テキスト(ソースコード)中に定義されたマクロ呼び出しをそのマクロ定義で置換する。
autoconf configure.acファイルを読み込み、Makefileを自動生成するためのconfigureファイルを作成する。
automake Makefileの雛形であるMakefile.inを作成する。
libtool 静的ライブラリ・共有ライブラリのコンパイル・リンク・インストール・アンインストールを補助する。

 
上記のツールで重要なのは、「Makefileを自動生成するために必要なファイル(下表)」を把握し、
どのようにそれらファイルを作成すれば良いかを知る事です。
下表が示すように、必須ファイルの大半はAutotoolsが自動生成してくれる事が分かります。

ファイル名 作成方法 説明
configure.ac 自作 configureファイルの雛形であり、autoscanコマンドで
生成されるconfigure.scanをconfigure.acにリネームしてから編集する。
config.guess 自動生成 "automake --add-missing"コマンドで生成されるスクリプトであり、
システム情報(OS、CPUなど)を自動判定する。
Makefile.am中に"AC_CANONICAL_TARGET"マクロを記載する必要がある。
configure 自動生成 autoreconfコマンドによって、configure.acを基に生成される。
config.h.in 自動生成 config.hの雛形であり、autoheaderコマンドで生成される。
config.h 自動生成 システムに依存するマクロを集めたヘッダファイル。
各ソースファイル中でインクルードする。
Makefile.am 自作 Makefileの雛形であるMakefile.inの雛形。
Makefile.in 自動生成 Makefileの雛形であり、"automake"コマンドによって
Makefile.amを基に生成される。
NEWS 自作 改善点やバグフィックスを記載する。最初は空ファイルで良い。
README 自作 パッケージの機能や使用方法を記載したファイル。最初は空ファイルで良い。
AUTHORS 自作 パッケージの製作者を記載する。最初は空ファイルで良い
ChangeLog 自作 パッケージの変更点を記載したファイル。最初は空ファイルで良い。

 
では、実際にAutotoolsを利用して、Makefileを作成します。
全体的な手順は、以下の通りです。
 1. プロジェクトの作成
 2. configure.の雛形(configure.ac)の作成
 3. Makefile.inの雛形(Makefile.am)の作成
 4. AutotoolsによるMakefileの作成

コンソール上に文字列表示だけでは外部ライブラリのリンク方法が説明できないため、
参考プログラムは、GTK+ライブラリを利用して、"Hello, World"を表示する内容(下図)とします。

hello.png

まず、以下のようなディレクトリ構成で、サンプルファイルを用意します。
projectディレクトリ以下にsrcディレクトリを用意した場合(伝統的な方法の場合)、
Makefile.amを余分に用意する必要があるため、今回は簡単なディレクトリ構造にします。

directory.sample
project
├── gui.c  # 内容については後述
├── gui.h # 同上
└── main.c # 同上
gui.c
#include <gtk/gtk.h>
#include "config.h"

void showHelloWorld(int argc, char *argv[]){  // GTKを利用したGUI Hello, World本体。
    GtkWidget *window;
    GtkWidget *button;

    gtk_init (&argc, &argv);
    window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    button = gtk_button_new_with_label ("Hello World");
    gtk_container_add (GTK_CONTAINER (window), button);

    gtk_widget_show (button);
    gtk_widget_show (window);
    gtk_main ();
}
gui.h
#ifndef _GUI_H_
#define _GUI_H_
void showHelloWorld(int argc, char *argv[]);
#endif
main.c
#include "gui.h"
#include "config.h"

int main (int argc, char *argv[]){
    showHelloWorld(&argc, argv);
    return 0;
}

 
次に、Autotoolsをインストール後に、プロジェクト情報およびマクロ情報を
記載するファイル/ディレクトリを作成します。
その後、autoscanコマンドでconfigure.scanファイルを生成し、
生成後のconfigure.scanファイルをconfigure.acにリネームします。

$ sudo apt-get install make m4 autoconf automake libtool
$ cd <project>                                # 上述のサンプルディレクトリのルート(projectディレクトリ)に移動
$ touch NEWS README LICENSE AUTHORS ChangeLog # 本記事では作成のみ
$ mkdir m4                                    # m4マクロの格納先
$ autoscan
$ ls
autoscan.log build configure.scan gui,c gui.h main.c

$ mv configure.scan configure.ac            # リネーム
$ vi configure.ac                           # 具体的な編集内容は後述。 

GTK Hello World向けに変更した後のconfigure.acは、以下の通りです。
なお、configure.acはconfigure.inという名称もサポートされていますが、非推奨です。

configure.in
# AutoconfのVersionチェック。
# 本例では、AutoconfのVersionが"2.69"以下の場合、configureを生成しない。
AC_PREREQ([2.69])

# 引数に"Package Name"、"Package Version"、"バグレポート先アドレス"、
# "tarball(tarで固めたファイル)を作成した時のファイル名"を順に記載する。
AC_INIT([guiHello], [1.0], [aaa@example.com], [gui_hello])

# automake用の初期化。
AM_INIT_AUTOMAKE

# ソースディレクトリのチェックであり、configureが誤ったディレクトリ内で
# 実行されるのを防ぐ。引数には、srcディレクトリ以下のファイルを1個指定する。
AC_CONFIG_SRCDIR([main.c])

# 引数(config.h)の雛形(config.h.in)を作成する。
# 引数は"config.h"に限らず、自由に指定できる。雛形は、"引数.in"という名前になる。
AC_CONFIG_HEADERS([config.h])

# m4マクロの格納先ディレクトリを指定する。
AC_CONFIG_MACRO_DIR([m4])

# 使用するプログラム(例:CC)のチェックであり、
# プログラムが存在しなかった場合、出力変数を代替の内容に変更する。
AC_PROG_CC
AC_PROG_INSTALL
AM_PROG_LIBTOOL

# GTKライブラリのチェック。
AM_PATH_GTK_2_0([2.10.0], ,AC_MSG_ERROR([Gtk+ 2.10.0 or higher required.]))

# 生成ファイル(Makefile)の作成場所を引数で明示する。
AC_CONFIG_FILES([Makefile])
AC_OUTPUT

続いて、Makefile.inの雛形であるMakefile.amを作成します。
記載する内容は、プログラム名・ソースコード名・ライブラリへのリンクについてです。

$ touch Makefile.am
Makefile.am
bin_PROGRAMS = guiHello               # プログラム名
guiHello_SOURCES = main.c gui.c gui.h # 使用するソースコード名

ACLOCAL_AMFLAGS = -I m4               # aclocalを再実行するときのm4ディレクトリの指定
guiHello_CPPFLAGS = @GTK_CFLAGS@      # "pkg-config --cflags"と同等
guiHello_LDADD = @GTK_LIBS@           # "pkg-config --libs"と同等

残りのファイルは、Autotoolsで自動生成されます。
再度、上記のファイルを編集した場合は、以下のコマンドを全て実行すれば、
修正版のMakefileを生成する事ができます。
Autotoolsのより詳しい説明は、来年の早い時期にQiitaで公開する予定です(需要ないでしょうけど)。

$ cd <project>            # 上述のサンプルディレクトリのルート(projectディレクトリ)に移動
$ aclocal -I ./m4         # automake/autoconfで利用されるm4マクロを定義するaclocal.m4を作成
$ autoheader              # ヘッダテンプレートのconfig.h.inを作成
$ automake --add-missing  # Makefileの雛形であるMakefile.inを作成
$ autoreconf --install    # 不足している関連ファイルのコピーおよびconfigureの作成
$ ./configure             # Makefileの作成

$ ls
AUTHORS      Makefile.in     config.h       configure.ac     install-sh
COPYING      NEWS            config.h.in    depcomp          libtool
ChangeLog    README          config.h.in~   gui.c            ltmain.sh
INSTALL      aclocal.m4      config.log     gui.h            m4
LICENSE      autom4te.cache  config.status  guiHello         main.c
Makefile     compile         config.sub     guiHello-gui.o   missing
Makefile.am  config.guess    configure      guiHello-main.o  stamp-h1

$ make              # ビルド
$ ./guiHello        # プログラムの実行

ビルドツールの活用:Yocto Project

Yocto Projectはビルドツール(オープンソースプロジェクト)であり、
ユーザが独自のディストリビューションを作成できるように、様々な機能を提供します。
(機能例:ソースの取得、ビルド、パッケージ化、テスト用パッケージ・スクリプト作成など)

Yoctoでは、機能単位でまとめられたレイヤー(機能例:QTSELinuxなど)の中に、
OSSのビルドに必要なレシピ(メタデータ)が格納されており、このレシピを利用してビルドを実施します。
メジャーなパッケージは、ベンダ・サポート企業によってレイヤー(主にMIT LICENSE)が提供されています。

一般的には、レイヤーの大半をベンダから取得し、独自アプリ用レイヤー(レシピ)のみを自作した後に、
正しく設定を行えば、コマンド一つでディストリビューションを作成できます。
Yocto Projectの使い方は、「Raspberry Pi3 with Yocto Project:環境構築(別記事)」に記載してあります。

参考文献

bashクックブック(O'Reilly)
Inside Linux Software オープンソフトウェアのからくりとしくみ(翔泳社)
Autotoolsについてのメモ
Embedded Linux Systems with the Yocto Project

最後に

Autotoolsはユーザの立場で頻繁に利用しますが、開発者として必須ファイルを作成する経験は乏しいです。
その経験不足によって、今回の記載は内容が浅く、
「もう少し勉強してから記載できれば良かったのにな」、と反省しています。

来年(2017年)は、AutotoolsやYocto Projectを勉強し直して、
「自作パッケージ(man、test、多言語対応)を公開」、「Qiitaにその知見の公開」が出来るようにしたいです。