`make`を使ってCプログラムをコンパイルする(Cプログラマーではない人向け)
私はCプログラマーではありませんが、ときどきC/C++プログラムをソースからコンパイルする必要があります。これは私にとって、ちょっとした苦行でした。長いあいだ、基本的には「依存関係をインストールして、makeを実行する。うまくいかなければ、誰かがコンパイルしたバイナリを探すか、あきらめる」というやり方でした。
Linuxを使っていたころは「誰かがコンパイルしてくれていることを願う」でかなりうまくいっていました。でもここ数年Macを使うようになってからは、自分でプログラムをコンパイルしなければならない場面が増えています。
そこで、Cプログラムをコンパイルするには何をすればいいのか、話してみましょう。私が実際にコンパイルしたCプログラムをいくつか例にして、うまくいかない原因についても少し説明します。今回コンパイルについて取り上げるのは、次の3つのプログラムです。
ステップ1:Cコンパイラーをインストールする
これはとても簡単です。UbuntuシステムでCコンパイラーがまだ入っていなければ、次のようにインストールします。
sudo apt-get install build-essential
これでgcc、g++、makeがインストールされます。Macの場合はもう少しややこしくて、「Xcodeのコマンドラインツールをインストールする」という感じです。
ステップ2:プログラムの依存関係をインストールする
新しいプログラミング言語の一部とは違い、Cには依存関係マネージャーがありません。そのため、依存関係があるプログラムの場合は、自分で探してこなければなりません。幸い、この事情のおかげでCプログラマーは依存関係を非常に少なくしていることが多く、使っているパッケージマネージャーで入手できる場合もたいていあります。
依存関係の入手方法は、ほぼ必ずREADMEに説明があります。たとえばpaperjamのREADMEには、次のように書かれています。
PaperJamをコンパイルするには、libqpdfライブラリとlibpaperライブラリのヘッダーファイルが必要です(通常はlibqpdf-devおよびlibpaper-devパッケージとして入手できます)。
マニュアルページをビルドするには、
a2x(AsciiDocに含まれています)が必要になる場合があります。
Debianベースのシステムなら、依存関係は次のようにインストールできます。
sudo apt install -y libqpdf-dev libpaper-dev
READMEにパッケージ名(libqpdf-devなど)が書かれていたら、基本的には常に「DebianベースのLinuxディストリビューションでの話」だと思うことにしています。Macではbrew install libqpdf-devを実行しても動きません。私はまだMacでの開発を完全には理解できていないので、今のところあまりアドバイスはできません。Homebrewを使っているなら、この場合はおそらくbrew install qpdfになるのでしょう。
ステップ3:必要なら./configureを実行する
CプログラムにはMakefileが付属しているものもあれば、代わりに./configureというスクリプトが付属しているものもあります。たとえばsqliteのソースコードをダウンロードすると、Makefileではなく./configureスクリプトが入っています。
私の./configureスクリプトに対する理解は、次のようなものです。
- 実行すると、やや不可解な出力が大量に表示され、その後
Makefileが生成されるか、依存関係が足りないために失敗します。 ./configureスクリプトはautotoolsというシステムの一部です。私は「実行してMakefileを生成する」ということ以外、autotoolsについて学ぶ必要がありませんでした。
./configureスクリプトにオプションを渡して、別のMakefileを生成させることもできるのだと思いますが、私は一度もやったことがありません。
ステップ4:makeを実行する
次のステップは、makeを実行してプログラムをビルドすることです。makeについて、いくつかメモしておきます。
- ときどき
make -j8を実行してビルドを並列化し、速くできることがあります。 - プログラムをコンパイルすると、通常はコンパイラーの警告が何百万件も表示されます。私はいつも無視しています。私が書いたソフトウェアではありませんからね!コンパイラーの警告は私の問題ではありません。
コンパイルエラーは依存関係の問題であることが多い
これは、Macでpaperjamをコンパイルしたときに出たエラーです。
/opt/homebrew/Cellar/qpdf/12.0.0/include/qpdf/InputSource.hh:85:19: error: function definition does not declare parameters
85 | qpdf_offset_t last_offset{0};
| ^
長年の経験から、こういう問題はあまり深く考えすぎないのが一番だと学びました。qpdfについて言っているなら、qpdfの依存関係を取り込む方法を私が間違えているだけ、という可能性が高いのです。
では、qpdfの依存関係を正しく取り込む方法をいくつか見ていきましょう。
コンパイラーとリンカーについての、世界一短い入門
依存関係の問題を直す方法の話に入る前に、Cプログラムのビルドは2つのステップに分かれることを説明します。
- コードをオブジェクトファイルにコンパイルする(
gccまたはclangを使います)。 - そのオブジェクトファイルを最終的なバイナリにリンクする(
ldを使います)。
Cプログラムをビルドするときにこれを知っておくことは重要です。コンパイルしようとしているプログラムの依存関係をどこで見つければいいのか、コンパイラーとリンカーに伝えるため、適切なフラグを渡さなければならないことがあるからです。
makeは環境変数を使ってコンパイラーとリンカーを設定する
Macでpaperjamをインストールするためにmakeを実行すると、次のエラーが出ます。
c++ -o paperjam paperjam.o pdf-tools.o parse.o cmds.o pdf.o -lqpdf -lpaper
ld: library 'qpdf' not found
これは、私のシステムにqpdfがインストールされていないからではありません(実際には入っています!)。コンパイラーとリンカーが、qpdfライブラリを見つける方法を知らないのです。これを直すには、次のことが必要です。
- コンパイラーに
"-I/opt/homebrew/include"を渡す(ヘッダーファイルの場所を伝えるため)。 - リンカーに
"-L/opt/homebrew/lib -liconv"を渡す(ライブラリファイルの場所を伝え、iconvをリンクするため)。
そして、環境変数を使えば、makeに追加のパラメーターをコンパイラーとリンカーへ渡させることができます。仕組みを見てみましょう。paperjamのMakefileを見ると、ここにあるLDLIBSのような環境変数がいくつもあります。
paperjam: $(OBJS)
$(LD) -o $@ $^ $(LDLIBS)
LDLIBS環境変数に入れたものはすべて、コマンドライン引数としてリンカー(ld)に渡されます。
秘密の環境変数:CPPFLAGS
Makefileが独自の環境変数を定義し、それをコンパイラーやリンカーに渡すことがあります。しかしmakeには「暗黙の」環境変数も多数あり、Cコンパイラーやリンカーに自動的に渡されます。暗黙の環境変数の完全な一覧はこちらにあります。その一つがCPPFLAGSで、Cコンパイラーに自動的に渡されます。
(厳密には、この用途にはCXXFLAGSを使うほうが普通です。ただし、このMakefileではCXXFLAGSがハードコードされているため、Makefileを編集せずにコンパイラーフラグを設定する方法は、私が見つけられた限りではCPPFLAGSを設定することだけでした。)
makeがC/C++とこれほど密接に結びついていることに気づくまで、私はずいぶん時間がかかりました。以前はmakeを単なる汎用ビルドシステムだと思っていました(もちろん何にでも使えます!)。しかし実際には、ほかの種類のプログラムをビルドする場合にはない、C/C++プログラム向けの機能がたくさんあります。環境変数をmakeに渡す2つの方法
@zwolのおかげで、環境変数をmakeに渡す方法が実は2つあると知りました。
CXXFLAGS=xyz make(通常はこちら)make CXXFLAGS=xyz
この2つの違いは、make CXXFLAGS=xyzの場合はMakefileで設定されたCXXFLAGSの値を上書きするのに対し、CXXFLAGS=xyz makeの場合は上書きしないということです。
どちらが一般的なのかはよくわかりませんが、この投稿では最初の方法を使います。
CPPFLAGSとLDLIBSを使って、このコンパイルエラーを直す方法
CPPFLAGSとLDLIBSがコンパイラーとリンカーに渡される仕組みを説明したところで、最後に、プログラムを無事ビルドできたコマンドを紹介します。
CPPFLAGS="-I/opt/homebrew/include" LDLIBS="-L/opt/homebrew/lib -liconv" make paperjam
これにより、-I/opt/homebrew/includeがコンパイラーに、-L/opt/homebrew/lib -liconvがリンカーに渡されます。
それから、これらが正しい引数だと「魔法のように」わかっていたふりをするつもりはありません。実際には、混乱しながらGoogleでいろいろ検索して突き止めましたが、この記事ではその部分を省略しています。ここで言えるのは次のことです。
-Iコンパイラーフラグは、/opt/homebrew/include/qpdf/QPDF.hhのようなヘッダーファイルを探すディレクトリをコンパイラーに伝えます。-Lリンカーフラグは、/opt/homebrew/lib/libqpdf.aのようなライブラリを探すディレクトリをリンカーに伝えます。-lリンカーフラグは、リンクするライブラリをリンカーに伝えます。たとえば-liconvは「iconvライブラリをリンクする」、-lmは「mathをリンクする」という意味です。
ヒント:特定のファイル1つだけをビルドする方法:make $FILENAME
昨日、qfという便利なツールを見つけました。ripgrepの出力からファイルをすばやく開くためのツールです。
qfはさまざまなツールが入った大きなディレクトリにありますが、コンパイルしたかったのはqfだけでした。そこで、次のようにqfだけをコンパイルしました。
make qf
基本的には、ビルドしたいファイルの出力ファイル名がわかっている(または推測できる)なら、make $FILENAMEを実行して、そのファイルだけをビルドさせることができます。
ヒント:Makefileは必須ではない
私はときどき、依存関係のない5行ほどのCプログラムを書きます。そして最近、blah.cというファイルがあるなら、Makefileを作らなくても次のようにコンパイルできると知りました。
make blah
これは自動的にcc -o blah blah.cへ展開されるので、少し入力を省けます。これを覚えていられるかはわかりません(結局、gcc -o blah blah.cと打ち続けるかもしれません)が、面白い小技のように思えます。
ヒント:ほかのパッケージシステムが同じCプログラムをどうビルドしたかを見る
Cプログラムのビルドで困っているなら、ほかの人もビルドで困ったことがあるのかもしれません!Linuxディストリビューションには、ビルドするすべてのパッケージについてビルド用ファイルがあります。たとえそのディストリビューションから直接パッケージをインストールできなくても、そのLinuxディストリビューションの方法を参考にして、パッケージのビルド方法のヒントを得られるかもしれません。これに気づいたこと(友人のDaveに教えてもらいました)は、私にとって大きな「なるほど!」体験でした。
たとえば、paperjamのNixパッケージにあるこの行には、次のように書かれています。
env.NIX_LDFLAGS = lib.optionalString stdenv.hostPlatform.isDarwin "-liconv";
これは基本的に、「Mac上でこれをビルドするときは、リンカーフラグ-liconvを渡す」という意味です。つまり、ビルドに使える手がかりになります。
同じファイルには、 env.NIX_CFLAGS_COMPILE = "-DPOINTERHOLDER_TRANSITION=1";とも書かれています。これが何を意味するのかはよくわかりません。しかしpaperjamパッケージをビルドしようとすると、PointerHolderというものに関するエラーが出るので、どうやら「PointerHolder transition」と何らかの関係があるようです。
ステップ5:バイナリをインストールする
プログラムをコンパイルできたら、おそらくどこかにインストールしたくなるでしょう!Makefileにはinstallターゲットが用意されていて、make installでシステムにツールをインストールできる場合があります。私はこれを実行するのがいつも少し怖いです(ファイルはどこに置かれるの?あとでアンインストールしたくなったらどうするの?)。そのため、かなり単純なプログラムをコンパイルした場合は、次のようにバイナリを手動でコピーしてインストールすることがよくあります。
cp qf ~/bin
ステップ6:自分でパッケージを作ってみる
ここまでのやり方がわかると、身につけたmakeの知識を使って、Homebrewにpaperjamのパッケージを提供できることに気づきました!そうすれば、今後別のシステムでもbrew install paperjamと実行するだけで済みます。
さまざまなパッケージシステムの細かい仕組みはそれぞれ違っていても、根本的にはどれもCコンパイラーとリンカーを使っている、というのがいいところです。
Cプログラマーでなくても、Cについて少し理解しておくと役に立つ
Cプログラムを書くつもりが一生なくても、「Cプログラムにはヘッダーファイルがある」といった基本的な仕組みを少し理解しておくと役に立つ、という興味深い例だと思います。
自分でC/C++プログラムをコンパイルできるようになったのは気分がいいです。コンパイラーやリンカーのフラグについてはまだ完全に自信がありませんし、autotoolsについても「./configureを実行してMakefileを生成するもの」という以上のことを学ぶつもりはありませんが。
この記事では、次の2つを取り上げませんでした。
LD_LIBRARY_PATH / DYLD_LIBRARY_PATH(実行時に動的リンカーへ、動的リンクされたファイルをどこで見つければいいか伝えるために使います)。最後にLD_LIBRARY_PATHの問題に遭遇して、しかも参考になる例を見つけられなかったのがいつだったか、思い出せないからです。pkg-config。重要なものだとは思っていますが、まだ理解できていません。
記事をランダムに読む