Welcome to Mashykom WebSite



C/C++言語 :Makefile、CMake、及び Bazel によるビルド


 コンピュータが実行できる言語はマシン(機械)語と呼ばれるプログラミング言語で書かれたプログラムだけです。それ以外の言語で書かれたプログラムはすべてマシン語に翻訳する必要があります。この翻訳をする方法にコンパイラを使う方法と、インタープリタを使用する方法があります。C/C++で書かれたプログラムはコンパイラを用いてマシン語に翻訳して、コンピュータに実行されます。後者のインタープリタを用いて翻訳をするプログラミング言語として、PythonやJavaScriptなどがあります。

 C++のプログラムを作成するためには、プログラムをコンパイルするコンパイラと、プログラムを書くエディターが必要です。よく知られているコンパイラは以下の通りです。

 独自のエディターを使用することもできますが、統合型開発環境(IDE)を利用することは最も便利は方法です。主要なIDEを以下に挙げます。

 NetBeans IDE は無償のオープンソースソフトウェアで全世界のコミュニティーユーザーと開発者に支援されています。 ユーザーインタフェースは日本語に翻訳されているのでどなたでも簡単に使えます。ダウンロードサイトは こちらです。

 日本語 Eclipse の ディストリビューションサイト、つまり、 Pleiades – Eclipse 日本語化プラグインのサイトは こちらです。 OSDN(オーエスディーエヌ)は、日本の OSS(オープンソースソフトウェア)プロジェクト向けのホスティングサイトです。

 C/C++を書いた時に複数ファイルから実行ファイルを生成するときやライブラリをIncludeする場合、コンパイルのオプションが複雑になります。複雑なオプションを毎回コマンドラインで入力するのではなく、Makefileというコンパイルのオプションルールを記載してmakeコマンドにて実行ファイルを生成すると便利です。まず始めに、Makefileの簡単なルールについて紹介します。次に、クロスプラットフォームで活用されているビルドの方法、CMakeとCMakeList.txtを用いてコンパイルする方法を説明します。

 makeはプラットフォームに依存せず、ほとんどすべてのプログラミング言語やツールに対応できる。また、記述ルールがシンプルであり学習しやすいというメリットもある。その半面、複雑なルールを記述しにくかったり、並列処理が苦手といったデメリットもある。一見しただけでは記述ルールが分かりにくくなってしまうことも少なくない。こういった背景から、makeを置き換えるべくさまざまなビルドツールが登場している。Googleが社内で使用しているものをベースとして開発されたビルドツール、Bazelが登場しました。このページの最後に、Bazel のインストール手順、Bazelを利用するための基本的なルールを解説し、実際のルール記述例やコマンド実行例を紹介します。

Last updated: 2022.2.16



Makefileを用いたビルドの方法



 ターミナルのシェルプロンプトを用いて、ソースプログラムをコンパイルする方法を説明します。以下のC++プログラムをテキストエディターで作成して、hello.cppとして保存して下さい。


#include <iostream>

int main() {
  std::cout << "Hello, World!\n";
}

ターミナルを開いて、hello.cppファイルと同じディレクトリに移動して、

$g++ -o hello.o hello.cpp

とすると、実行ファイルがhello.oとして生成される。コンパイル後の実行は以下のコマンドを入力します。


$./hello.o

結果は

Hello, World!

と表示されます。

 GNU makeコマンドを使えば、Makefileにあらかじめ記述しておいた手順にしたがって、C/C++などのソースファイルから実行ファイルを自動で生成できます。例えば、単一のC++ソースのhello.cppというファイルをコンパイルする場合は


$ g++ hello.cpp -o hello

と実行するとhelloという実行ファイルが生成されます。これを最も単純なMakefileを使って書くと次のようになります。


hello: hello.cpp  
	g++ -Wall hello.cpp -o hello  #実行コマンド
clean:
	rm -f *.o hello

 Makefileの書式を簡単に説明すると、Ruleとは、上記の hello: や clean: から始まる行からその下のインデントされた行までのブロックのことを指します。各Ruleは ターゲット (target) 、 必須項目 (prereq) 、 実行コマンド (commands) の3つの構成要素で成り立っています。


target: prereq1 prereq2
    commands

 1行目に生成したいターゲットファイル名(例えば、hello): 依存ファイル(hello.cpp)、2行目に生成するための実行コマンド(g++ -Wall hello.cpp -o hello)を記載します。実行コマンドの先頭にはTabを入力する必要があります。-Wallオプションは、全ての警告オプションを結合してくれるもので、一番厳密に文法をチェックします。

 ソースファイルと Makefileを同じディレクトリに配置します。プログラムを構築するには、ターミナルのディレクトリをMakefileと同じディレクトリに移動させて、コマンドプロンプトに次のように入力し make を実行します。


 $ make

 これにより make は Makefile を読み込み、次のように最初のターゲットを構築 します。


 $ make
  g++ -Wall hello.c -o hello

 helloという実行ファイルが生成されます。生成された実行ファイルを消去したい場合は「make clean」と実行すると、rm -f *.o helloのコマンドが実行されます。

 プログラム全体が、相互依存した複数のソースファイルから構成されているケースが一般的です。こうしたソースファイルを効率的にビルドする方法の一つがMakefileを活用することです。以下の例は、2つのソースプログラムからなるコードのコンパイルです。ソースはmain.cpp、および、function.cppから構成されているとします。mainの中で利用したい関数をfunction.cppのなかに記述しています。この時、Makefileは以下のようになります。


main: main.o function.o
	g++ -Wall -o main main.o function.o 
function.o: function.cpp
	g++ -Wall -c function.cpp 
main.o: main.cpp
	g++ -Wall -c main.cpp
clean:
	rm -f *.o main

 最初のルールのコマンド行に「 g++ -Wall main.cpp print.cpp -o hello」 と記述すると、コンパイルの実行が毎回両方のファイルに適用されてしまいます。片方のファイルだけを修正したい時もあります。この理由から、上記の例のように各生成ファイル毎に記述を分離するのが一般的です。

 シンボルを使ったMakefileの例を以下に示します。


CC = g++
CFLAGS = -g -Wall

ALL: main.o function.o
	$(CC) $(CFLAGS) -o main main.o function.o

main.o: main.cpp
	$(CC) $(CFLAGS) -o main.o -c main.cpp

function.o: function.cpp sub.h
	$(CC) $(CFLAGS) -o function.o -c function.cpp

 function.cppのなかでヘッダファイル sub.h を使用しています。オブジェクトファイル main.oとfunction.oを-cフラグで別々に生成してから、ALLの処理でリンクしています。make コマンドを実行すると


cpp $ make
g++ -g -Wall -o main.o -c main.cpp
g++ -g -Wall -o function.o -c function.cpp
g++ -g -Wall -o main main.o function.o

となります。mainという実行ファイルが作成されます。./main とコマンドを打つと実行ファイルが結果を表示します。

 Makefile の使用法についてよく知りたい人は、GNU make の公式マニュアルを参照ください。また、少し古いですが、 GNU make の参考書(Robert Mecklenburg 著、矢吹 道郎 監訳、菊池 彰 訳)の pdf が https://www.oreilly.co.jp/library/4873112699/からダウンロードできます。


CMakeを用いたビルドの方法



 C++のコンパイラはOSに依存して複数のコンパイラが存在しています。標準C++ライブラリだけを使ったソースコードであればどのコンパイラでもビルドすることができますが、コンパイラのオプションなどの仕様が異なります。CMakeではCMakeLists.txtという設定ファイルを作成しておけば、そこからCMakeがサポートする任意の実行ファイルを作成することができます。 つまりOSに依存せず、どの開発環境でもプロジェクトをビルドできるようになります。CMakeはクロスプラットフォームなC++プロジェクトを開発するために欠かせないツールとなっています。

CMakeは、公式サイトのダウンロードページからインストーラーをダウンロードできます。コマンドラインから、 cmake --version と入力すると、インストールされているバージョンがわかります。

CMakeを用いてコンパイルする時の設定ファイルが、CMakeLists.txtです。cmakeコマンドを実行すると、この設定ファイルに従って、各OSに対応したMakefileが生成される仕組みになっています。ビルドしたいソースコードのあるディレクトリに、CMakeLists.txtファイルを作成します。例えば、上記のhello.cppというソースファイルから、実行ファイルmainを生成するためのプロジェクトは、以下のように書きます。


cmake_minimum_required(VERSION 3.1)
project(hello_world CXX)
add_executable(main hello.cpp)

実は最後の行だけ書いておけば一応動くのですが、最低限やっておきたいという設定がこの3行です。1行目のcmake_minimum_required(VERSION 3.1)はCMakeのバージョンを指定するコマンドです。バージョンの確認に加えて暗黙的にcmake_policyコマンドを呼び出し、このCMakeLists.txtをCMake 3.1の仕様で解析するように設定が行われます。

2行目のproject(hello_world CXX)はプロジェクト名(hello_world)とプロジェクトで使用するプログラム言語(CXX)を指定しています。この言語の指定がなくても正常に作動します。ここで定義したプロジェクト名は、Xcodeのプロジェクトファイル名に使われます。

3行目のadd_executable(main hello.cpp)はビルドする実行ファイル名とそれを構成するソースファイルを指定しています。 「hello.cppを使ってmainという名前の実行ファイルを作成する」という設定になります。

 cmakeは、カレントディレクトリに生成物やキャッシュ等を色々と生成するため、ディレクトリを汚します。この理由から、「build」のようなディレクトリを作成し、そこへ移動してから、一つ上のディレクトリを渡してcmakeを実行します。


$ mkdir build 
$ cd build 
build $ cmake .. 

 ..の部分はCMakeLists.txtがあるディレクトリを指定しています。以下のようにコンソールに表示されて、Cmake が終わります。


-- The CXX compiler identification is AppleClang 11.0.0.11000033
-- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++
-- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /Users/koichi/cpp/cpp-cmake/build

buildディレクトリの中にMakefileなどのプロジェクトファイルとその他の必要なファイルが生成されているはずです。

 最後に、


(build)$ make .

とコマンドを打てば実行ファイル main.o が作成されます。.の部分はConfigureとGenerateを実行したフォルダつまりbuildフォルダを指定しています。このコマンドの入力後

Scanning dependencies of target main
[ 50%] Building CXX object CMakeFiles/main.dir/hello.cpp.o
[100%] Linking CXX executable main
[100%] Built target main

となって、ビルドが完成します。生成されたファイルを見てみると


build $ ls
CMakeCache.txt		Makefile		main
CMakeFiles		cmake_install.cmake

 となっています。実行ファイルのmain、CMakeFilesディレクトリ、および、Makefileファイルなどが作成されています。


build $ ./main


と入力すれば、mainファイルが実行できます。「Hello, World!」と表示されます。

 ソースファイルが複数になるケースでは、add_executableの記述内で、hello.cppの後にそのファイル名を追加することになります。例えば、main.cpp, func1.cpp, func2.cpp, sub.hという複数のソースプログラムの時、


cmake_minimum_required(VERSION 3.1)
project(Myproject CXX)
add_executable(Main main.cpp func1.cpp func2.cpp)

と記述します。ヘッダ sub.h の依存関係はcmakeが勝手に解決してくれます。

 cmakeは広範に普及しているツールですが、日本語マニュアルの情報は乏しいです。cmakeの英文マニュアルはcmake.orgのサイトから入手できます。

 CMakeにIDE(XcodeやVisual Studioなど)のプロジェクトファイルを生成させる方が便利なことが多いです。CMakeにはbuild system generatorを指定するオプション -G があります。Xcodeを活用するときは、以下のようにオプションを設定します。


$ mkdir build 
$ cd build 
build $ cmake .. -G Xcode 

 以下のような経緯をたどります。


-- The CXX compiler identification is AppleClang 11.0.0.11000033
-- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang++
-- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /Users/koichi/cpp/cpp-xcode/cpp-cmake/build

build $ ls
CMakeCache.txt		CMakeScripts		hello_world.xcodeproj
CMakeFiles		cmake_install.cmake

 Xcodeのプロジェクトが作成されています。しかし、Xcode 12.1 以降では、この方法ではエラーが出ます。Xcode を起動して、プロジェクトを新規に作成する方がベターです。この方法を簡単に説明します。

 Xcode を起動し、「Create a new Xcode project」をクリックして開きます。「macOS」->「Command Line Tool」を選び、「next」に行きます。「product name」に適当なファイル名を書き込んで、「Language」で C++を選択します。新しい画面が登場して、エディターに、開始用のHello World コードが書かれています。この main.cpp コードの内容を消去して、自らのプログラムをコピペして、書き入れます。

 左上にある三角形の実行ボタンをクリックします。すると、成功すれば、「Build Succeeded」と出て、結果が表示されます。

 複数のソースコードがあるケースでは、[File] -> [Add Files to "..."] とクリックして、[options] で追加したいファイルを選択して、[add]をクリックします。


Bazel を用いたビルドの方法



 Bazelのインストール方法について説明します。Bazel の公式サイトのInstalling Bazel のページの説明に従います。

 MacOS の場合、brew でインストールすると、bazelの最新版5.0.0がダウンロードされます。


$ brew install bazel

 とすると、実行ファイルは /usr/local/Cellar/bazel/5.0.0 にインストールされます。

 なお、bazel 5.0.0はTensorflowのC++APIをサポートしていません、Tensorflow C++APIに適合させるためには、例えば、bazel-4.0以下のバージョンをインストールすることが必要です。そのときは以下のようにします。最新のMacOSでbazel--installer-darwin-x86_64.shをダウンロードすると、正常にインストールできません。なので、以下の手順を踏みます。Linux でも bazel--installer-linux-x86_64.sh をダウンロードして、同様の手続きとなります。


$ export BAZEL_VERSION=3.2.0
$ curl -fLO "https://github.com/bazelbuild/bazel/releases/download/${BAZEL_VERSION}/bazel-${BAZEL_VERSION}-installer-darwin-x86_64.sh"

$ chmod +x "bazel-${BAZEL_VERSION}-installer-darwin-x86_64.sh"
$ ./bazel-${BAZEL_VERSION}-installer-darwin-x86_64.sh --user

 「--user」 flagは Bazel を $HOME/bin ディレクトリにインストールします。なので、パスの設定をbashrc あるいは bash_profile に
PATH="$HOME/bin:$PATH"
を追加するか、実行ファイル bazel をパスが通っているディレクトリ、例えば、/usr/local/lib/ にコピーします。

 上記のどちらかの手順を実行した後、bazelのバージョンチェックをします。


$ bazel --version

  bazel 3.2.0 or bazel 5.0.0-homebrew

 Bazel’s GitHub repository をダウンロードして、bazelの学習をしてみましょう。Bazel Tutorial のサイトを参照ください。


$ git clone https://github.com/bazelbuild/examples

 examples/cpp-tutorialは以下のファイル構造になっています。


├── README.md
├── stage1
│   ├── README.md
│   ├── WORKSPACE
│   └── main
│       ├── BUILD
│       └── hello-world.cc
├── stage2
│   ├── README.md
│   ├── WORKSPACE
│   └── main
│       ├── BUILD
│       ├── hello-greet.cc
│       ├── hello-greet.h
│       └── hello-world.cc
└── stage3
    ├── README.md
    ├── WORKSPACE
    ├── lib
    │   ├── BUILD
    │   ├── hello-time.cc
    │   └── hello-time.h
    └── main
        ├── BUILD
        ├── hello-greet.cc
        ├── hello-greet.h
        └── hello-world.cc

 各stage1,2,3 にあるC++ソースファイルをコンパイルします。コンパイルする対象の各stageフォルダはWORKSPACEファイルとMainフォルダから構成されます。mainフォルダの中にソースコードとBUILDファイルがあります。stage1 のBUILDファイルの中身は、


load("@rules_cc//cc:defs.bzl", "cc_binary")

cc_binary(
    name = "hello-world",
    srcs = ["hello-world.cc"],
)

となっています。この構造が基本です。このBUILDファイルはsrcsで指定されたソースコードをコンパイルして、"hello-world"というターゲットを生成するというルールを定めています。name でのターゲットの指定は必須ですが、ソースファイルの指定は任意です。BUILD ファイルにある "@rules_cc//cc:defs.bzl" は「Starlark rules for building C++ projects」と呼ばれるものらしいです。内容がよく見えないので、C++コードをビルドするときにはこのように記述すると理解するしかないです。WORKSPACE ファイルは中身は空でも、存在することが必須です。とりあえず、ここでは WORKSPACE ファイルは空のファイルとして作成されています。

 BUILDファイルおよびコマンドラインで、ターゲットを指定するためにBazelはラベルを使用します。たとえば、// main:hello-worldまたは// lib:hello-time のように。 それらの構文は次のとおりです。


bazel build  //path/to/package:target-name

 ターゲットがルールターゲットの場合、path/to/packageは、WORKSPACEルート(WORKSPACEファイルを含むディレクトリ)からBUILDファイルを含むディレクトリへのパスであり、target-nameはBUILDファイル内でターゲットとして名前を付けたものです。ターゲットがファイルターゲットの場合、path/to/packageはパッケージのルートへのパスであり、target-nameはターゲットファイルの名前です。ビルド対象のディレクトリルートでターゲットを参照する場合、パッケージパスは必要なくて、//:target-nameを使用してください。

 早速くこのstage1をビルドしてみましょう。カレントディレクトリは stage1 のルートにあるので、ビルドのためのコマンドは以下のようになります。


$ bazel build //main:hello-world

-- その結果 --

Extracting Bazel installation...
Starting local Bazel server and connecting to it...
INFO: Analyzed target //main:hello-world (36 packages loaded, 240 targets configured).
INFO: Found 1 target...
INFO: Deleting stale sandbox base /private/var/tmp/_bazel_koichi/cfc5033cf078b62b2592dc9aa58fcbe7/sandbox
Target //main:hello-world up-to-date:
  bazel-bin/main/hello-world
INFO: Elapsed time: 42.917s, Critical Path: 2.42s
INFO: 6 processes: 4 internal, 2 darwin-sandbox.
INFO: Build completed successfully, 6 total actions

 以下のようにbazel関連ファイルが生成されます。


├── README.md
├── WORKSPACE
├── bazel-bin 
├── bazel-out 
├── bazel-stage1 
├── bazel-testlogs 
└── main
    ├── BUILD
    └── hello-world.cc

 bazel-binフォルダにビルドされた実行ファイルが配置されます。このファイルを実行します。


$ bazel-bin/main/hello-world

-- 結果の表示 --
Hello world
Tue Feb 15 10:08:25 2022

 BUILDファイルの中に複数のターゲットを指定することができます。stage2 に移動して下さい。BUILDファイルの中身は


load("@rules_cc//cc:defs.bzl", "cc_binary")

cc_library(
    name = "hello-greet",
    srcs = ["hello-greet.cc"],
    hdrs = ["hello-greet.h"],
)

cc_binary(
    name = "hello-world",
    srcs = ["hello-world.cc"],
    deps = [
        ":hello-greet",
    ],
)


 となっています。ビルドします。


$ bazel build //main:hello-world

INFO: Found 1 target...
Target //main:hello-world up-to-date:
  bazel-bin/main/hello-world
INFO: Elapsed time: 20.603s, Critical Path: 2.85s
INFO: 9 processes: 5 internal, 4 darwin-sandbox.
INFO: Build completed successfully, 9 total actions

 実行ファイルを起動すると


$ bazel-bin/main/hello-world

Hello world
Tue Feb 15 10:20:38 2022

という表示が出ます。

 上記の通り、Bazel の使用は cmake を利用したビルドに比較してシンプルです。



Visual Studio Code を用いたビルドの方法


 Visual Studio Code のインストール方法について説明します。公式サイトのVisual Studio.microsoftのページからダウンロードします。

 Windows では、Visual Studio Code のインストールに従って、インストールします。Macでは、Visual Studio Code の説明に沿って、ダウンロードすると、最新版1.71がインストールされます。

 簡単なソースコードをビルドする方法について説明します。Visual Studio Code を起動して、cppソースコードのファイルを新規作成、または開きます。「実行」「デバッグの開始」をクリックし、「c++」を選択して「ビルドとデバッグ」を実行します。デバッグの結果が下端にポップアップします。実行ファイルが作成されるので、「ターミナル」でファイルのあるディレクトリに移動して


$ ./cpp 

 と打って結果を見ます。

 複数のソースコードから構成されるプロジェクトをデバッグする方法は少し複雑です。



このページの先頭に戻る

C++ 言語入門のページに戻る

トップ・ページに行く