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

「誰かがコンパイルしてくれていることを期待する」という方法は、Linuxを使っていた頃はかなりうまくいきましたが、ここ数年Macを使うようになってからは、自分でプログラムをコンパイルしなければならない状況に遭遇することが増えました。

それでは、Cプログラムをコンパイルするために何をしなければならないかについて話しましょう。コンパイルした具体的なCプログラムの例をいくつか挙げて、起こり得る問題についてお話しします。ここでは、コンパイルについて話す3つのプログラムを紹介します。

  • paperjam
  • sqlite
  • qfrg -n THING | qfで検索した結果からファイルを素早く開くために実行できるページャー)

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

これはとても簡単です。UbuntuシステムでCコンパイラを持っていない場合、以下のようにしてインストールします。

sudo apt-get install build-essential

これにより、gccg++makeがインストールされます。Macの場合、状況は少し複雑ですが、「xcode command line toolsをインストールする」といった感じです。

ステップ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. gccまたはclangを使ってコードをオブジェクトファイルコンパイルする
  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

Makefileはコンパイラ/リンカーに渡す独自の環境変数を定義することがありますが、makeにはCコンパイラとリンカーに自動的に渡される「暗黙の」環境変数が多数あります。暗黙の環境変数の完全なリストはこちらにありますが、そのうちの1つがCPPFLAGSで、Cコンパイラに自動的に渡されます。

(技術的にはCXXFLAGSを使うのがより一般的ですが、この特定のMakefileCXXFLAGSをハードコードしているため、Makefileを編集せずにコンパイラフラグを設定する方法としてCPPFLAGSを設定するしかありませんでした)

C/C++にmakeがどれだけ密接に関連しているかを理解するのに時間がかかりました。私は以前、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がリンカーに渡されます。

また、これらが正しい引数であることを「魔法のように」知っていたわけではないことをお伝えしておきます。これらを理解するには、この記事で省略した混乱したGoogle検索がたくさん必要でした。以下についてお伝えしておきます。

  • -Iコンパイラフラグは、/opt/homebrew/include/qpdf/QPDF.hhのようなヘッダーファイルを見つけるディレクトリをコンパイラに伝えます
  • -Lリンカーフラグは、/opt/homebrew/lib/libqpdf.aのようなライブラリを見つけるディレクトリをリンカーに伝えます
  • -lリンカーフラグは、-liconvが「iconvライブラリをリンクする」という意味、または-lmが「mathをリンクする」という意味のように、リンクするライブラリをリンカーに伝えます

ヒント: 1つの特定のファイルだけをビルドする方法: make $FILENAME

昨日、ripgrepの出力からファイルを素早く開くために使用できるqfという便利なツールを発見しました。

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ディストリビューションには、ビルドするすべてのパッケージのビルドファイルがあるので、そのディストリビューションからパッケージを直接インストールできなくても、その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プログラムを書く予定がなくても役に立つことの面白い例だと思います。

コンパイラとリンカーのフラグについてまだ完全に自信がなく、autotoolsの仕組みについても「./configureを実行してMakefileを生成する」ということ以外は学ぶ予定はありませんが、自分でC/C++プログラムをコンパイルできる能力を持てたことは良いことです。

この記事で省略した2つのこと:

  • LD_LIBRARY_PATH / DYLD_LIBRARY_PATH(実行時に動的リンカーに動的にリンクされたファイルの場所を伝えるために使用します)。LD_LIBRARY_PATHの問題に遭遇した最後の記憶がなく、例を見つけることができなかったため。
  • pkg-config。重要だとは思いますが、まだ理解していません。