Using `make` to compile C programs (for non-C-programmers)

Julia Evans

`make`でCプログラムをコンパイルする(Cプログラマではない人向け)

原文は Julia Evans により に公開されました。 このブログを購読する

私はCプログラマになったことはありませんが、たまにC/C++プログラムをソースからコンパイルする必要が出てきます。ずっとこれがちょっとした悩みの種でした。長い間、私のやり方は基本的に「依存関係をインストールして、makeを実行する。うまくいかなければ、誰かがコンパイルしたバイナリを探すか、あきらめる」というものでした。

「誰かがコンパイルしてくれていることを願う」作戦は、Linuxを使っていた頃はかなりうまくいっていました。でもここ2年ほどMacを使うようになってから、自分で実際にコンパイルしなければならない場面が増えてきました。

では、Cプログラムをコンパイルするために何をする必要があるのか見ていきましょう!私が実際にコンパイルした具体的なCプログラムをいくつか例に、うまくいかないときの話も交えて解説します。今回取り上げるのは、次の3つのプログラムです。

  • paperjam
  • sqlite
  • qfrg -n THING | qfのように実行して、検索結果から素早くファイルを開けるページャー)

ステップ1: Cコンパイラをインストールする

これはとても簡単です。Ubuntuなら、Cコンパイラがまだ入っていなければ次のコマンドでインストールします。

sudo apt-get install build-essential

これでgccg++makeがインストールされます。Macの場合はもう少しややこしいですが、「Xcodeのコマンドラインツールをインストールする」といった感じです。

ステップ2: プログラムの依存関係をインストールする

最近のプログラミング言語とは違って、Cには依存関係を管理してくれるマネージャーがありません。なので、プログラムに依存関係がある場合は、自分で探し出す必要があります。幸いなことに、そのおかげでCプログラマは依存関係をできるだけ最小限に抑えることが多く、たいてい使っているパッケージマネージャーで依存関係を入手できます。

READMEにはたいてい依存関係の入手方法を説明するセクションがあります。たとえばpaperjamのREADMEにはこう書かれています。

PaperJamをコンパイルするには、libqpdfとlibpaperライブラリのヘッダーが必要です(通常はlibqpdf-devとlibpaper-devパッケージとして入手できます)。

マニュアルページをビルドするには、a2xAsciiDocに含まれています)が必要になる場合があります。

なのでDebianベースのシステムなら、次のように依存関係をインストールできます。

sudo apt install -y libqpdf-dev libpaper-dev

READMEにパッケージ名(たとえばlibqpdf-dev)が書かれていたら、基本的には常に「DebianベースのLinuxディストロでの話」と考えていいでしょう。Macでbrew install libqpdf-devとしても動きません。私はまだMacでの開発に100%慣れたわけではないので、ここであまりアドバイスできることはありませんが、この場合ならHomebrewを使っているならbrew install qpdfになるはずです。

ステップ3: ./configureを実行する(必要な場合)

Cプログラムの中にはMakefileが付属しているものもあれば、代わりに./configureというスクリプトが付属しているものもあります。たとえばsqliteのソースコードをダウンロードすると、Makefileの代わりに./configureスクリプトが入っています。

私の理解では、この./configureスクリプトは次のようなものです。

  1. 実行すると、よくわからない出力が大量に表示され、そのあとMakefileが生成されるか、依存関係が足りなくて失敗するかのどちらかになります
  2. ./configureスクリプトはautotoolsというシステムの一部ですが、私は「Makefileを生成するために実行する」以上のことを学ぶ必要を感じたことはありません

./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つのステップに分かれていることを知っておきましょう。

  1. コンパイルしてコードをオブジェクトファイルにする(gccclangで)
  2. それらのオブジェクトファイルを最終的なバイナリにリンクする(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

Makefilesではコンパイラやリンカに渡す独自の環境変数を定義していることもありますが、makeにはCコンパイラやリンカに自動的に渡される「暗黙の」環境変数もたくさんあります。暗黙の環境変数の一覧はこちらにありますが、そのうちのひとつがCPPFLAGSで、これは自動的にCコンパイラに渡されます。

(厳密にはここではCXXFLAGSを使うのが普通なのですが、この特定のMakefileではCXXFLAGSがハードコードされているので、Makefileを編集せずにコンパイラフラグを設定する唯一の方法がCPPFLAGSを設定することでした)

余談ですが、makeがどれだけC/C++と密接に結びついているかに気づくまで、かなり時間がかかりました。以前はmakeは単なる汎用的なビルドシステムだと思っていました(もちろん何にでも使えます!)が、他の種類のプログラムのビルドにはない、C/C++プログラムのビルドのための多くの便宜が用意されているのです。

makeに環境変数を渡す2つの方法

@zwolさんのおかげで、makeに環境変数を渡す方法が実は2つあることを知りました。

  1. CXXFLAGS=xyz make(通常のやり方)
  2. make CXXFLAGS=xyz

両者の違いは、make CXXFLAGS=xyzMakefile内で設定されたCXXFLAGSの値を上書きしますが、CXXFLAGS=xyz makeは上書きしないということです。

どちらが一般的かはよくわかりませんが、この記事では前者の方法を使います。

CPPFLAGSLDLIBSを使ってこのコンパイラエラーを直す方法

CPPFLAGSLDLIBSがどのようにコンパイラとリンカに渡されるかについて話したので、プログラムを無事にビルドするために私が使った最終的なおまじないを紹介します!

CPPFLAGS="-I/opt/homebrew/include" LDLIBS="-L/opt/homebrew/lib -liconv" make paperjam

これはコンパイラに-I/opt/homebrew/includeを、リンカに-L/opt/homebrew/lib -liconvを渡します。

あと、正しい引数が何かを私が「魔法のように」知っていたふりをするつもりはありません。それらを突き止めるまでには、この記事では省略した混乱しながらのググりがたくさんありました。ただ、補足しておくと、

  • -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を実行することでmakeにそのファイルだけをビルドさせることができます。

ヒント: Makefileは不要

私はときどき依存関係のない5行程度のCプログラムを書くのですが、blah.cというファイルがあれば、Makefileを作らずに次のようにコンパイルできることを最近知りました。

make blah

これは自動的にcc -o blah blah.cに展開されるので、少しタイプする手間が省けます。これを覚えていられるかはわかりませんが(結局gcc -o blah blah.cと打ち続けそうですが)、ちょっと面白い小技だと思います。

ヒント: 他のパッケージシステムが同じCプログラムをどうビルドしたかを見る

Cプログラムのビルドで困っているなら、他の人も同じように苦労したかもしれません!どの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の移行」に関係しているのでしょう。

ステップ5: バイナリをインストールする

プログラムのコンパイルに成功したら、たぶんどこかにインストールしたくなりますよね!Makefileによってはinstallターゲットが用意されていて、make installでシステムにツールをインストールできます。私はこれがいつもちょっと怖いです(どこにファイルが置かれるの?あとでアンインストールしたくなったらどうするの?)。なので、かなりシンプルなプログラムをコンパイルする場合は、代わりに次のようにバイナリを手動でコピーしてインストールすることがよくあります。

cp qf ~/bin

ステップ6: ついでに自分でパッケージを作ってみる!

これらすべてのやり方を理解したとき、新しく得たmakeの知識を使ってHomebrewにpaperjamパッケージをコントリビュートできることに気づきました!そうすれば、今後はbrew install paperjamで済むようになります。

いいところは、さまざまなパッケージシステムの詳細がどうであれ、根本的にはどれもCコンパイラとリンカを使っているということです。

Cプログラマでなくても、Cについて少し知っておくと役に立つ

これは、たとえ人生で一度も本格的なCプログラムを書くつもりがなくても、Cプログラムがどう動くかの基本(たとえば「ヘッダーファイルがある」といったこと)を少し理解しておくことがどれだけ役に立つかを示す面白い例だと思います。

コンパイラやリンカのフラグについてまだ完全に自信があるわけではなく、autotoolsについても「./configureを実行してMakefileを生成する」以上のことはこれからも学ぶつもりはありませんが、それでも自分でC/C++プログラムをコンパイルできる能力が少しでもあるのは気持ちがいいものです。

この記事で省いたことが2つあります。

  • LD_LIBRARY_PATH / DYLD_LIBRARY_PATH(実行時に動的リンカに動的リンクされたファイルをどこで探すかを教えるために使うもの)。最後にLD_LIBRARY_PATHの問題に遭遇したのがいつだったか思い出せず、例も見つからなかったからです
  • pkg-config。重要だとは思うのですが、まだ理解できていません

この記事は「muse-spark-1.2-contributor」を使用して翻訳されました。

コメント