Makefile だけで何とかする、ビルド環境


はじめに

コンパイラと、テキストエディター(最近は VSCode に移ってきている)で開発を回すとなると、どうしても「make」の助けを借りなければなりません。

現在の流行で言えば、「CMake」を使って、環境毎の違いを吸収して、Makefile を作成するのが一般的なのかと思います。

しかし、これの欠点は、まず最初に CMake を動かす事が必要で、CMake の設定ファイルを作る必要があり二度手間です。
もし直接 Makefile を修正したら、それは、CMake の設定ファイルに反映されません。
そもそも、何故 CMake のようなツールが必要なのでしょうか?
それは、Makefile の文法が簡単ではない事があると思いますが、既に多くの場面で使われており、今更、別のシステムに乗り換える事も難しくなっていると思います。

gcc の make では、configure で環境の状態を収集して、Makefile に反映させる事もしていますが、それは大掛かり過ぎると思います。

また、make は非常に高機能で複雑なので、全てを簡潔に説明する事は難しいと思います。
※よく、簡単に「Makefile の書き方」を説明した初心者向けの情報などもありますが、本来は自動で出来る事を設定させたり、柔軟ではない方法論が使われているとか、冗長だったり、特定の場合にしか機能しない事が多いように思います。

Makefile のテンプレート化

通常の開発では、以下の事さえ満足すれば良さそうです。

  • ソースコードの指定
  • フレームワークや、ライブラリのヘッダーパス
  • 生成オブジェクトの管理フォルダ
  • コンパイルオプション
  • リンクするライブラリなど

そこで、自分は、これらを踏まえて、Makefile をテンプレート化し、必要な部分を書き換えて、色々なプロジェクトで使えるようにしています。
※基本、gcc、clang の場合を想定しているので、VC や、他のコンパイラでは、別の工夫が必要でしょう~

開発環境毎の場合分け

開発環境が変わると、ライブラリのパスが異なったり、システムのリンクライブラリ名が微妙に違ったりしますので、それらを何とか吸収しなければなりません。

# platform switcher (Windows, Linux, OS-X)
ifeq ($(OS),Windows_NT)
    FEXT    = .exe
    ICON_RC = icon.rc
    SYSTEM := WIN
    CPP_VER := -std=c++17
    LOCAL_INC_PATH := /mingw64/include
    LOCAL_LIB_PATH := /mingw64/lib
    OPTLIBS = opengl32 glew32 openal
    CPMM    =   g++
    CCMM    =   gcc
    CFLAGS  = -DWIN32
    PFLAGS  = -DWIN32 -DBOOST_USE_WINDOWS_H
    LFLAGS =
else
  FEXT  =
  ICON_RC =
  UNAME := $(shell uname -s)
  ifeq ($(UNAME),Linux)
    SYSTEM := LINUX
    CPP_VER := -std=c++17
    CFLAGS =
    PFLAGS =
    LFLAGS =
  endif
  ifeq ($(UNAME),Darwin)
    SYSTEM := OSX
    OSX_VER := $(shell sw_vers -productVersion | sed 's/^\([0-9]*.[0-9]*\).[0-9]*/\1/')
    CPP_VER := -std=c++14
    CFLAGS =
    PFLAGS =
    LFLAGS = -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Develop\
er/SDKs/MacOSX.sdk \
        -Wl,-search_paths_first -Wl,-headerpad_max_install_names \
        -framework AGL -framework Cocoa -framework OpenGL -framework IOKit \
        -framework CoreFoundation -framework CoreVideo -framework OpenAL
  endif
  LOCAL_INC_PATH := /usr/local/include
  LOCAL_LIB_PATH := /usr/local/lib
  OPTLIBS = GLEW
  CPMM  =   clang++
  CCMM  =   clang
endif

これは、Windows(MSYS2)、Linux、OS-X の違いを検出して、細かい違いを吸収しています。

従属規則の自動生成

「従属規則」は、ソースコードがどのヘッダーをインクルードしているかをリストしたもので、make の根幹の機能です。
make は、それらのリストを使い、タイムスタンプを比較して、コンパイルが必要なソースコードを判別します。

gcc には、「-MM」オプションがあり、これを使うと、インクルードしているパスを出力します。
ただ、出力されるパスは不完全なので、sed を使って整形します。
この時、内部の「#ifdef」のような制御文が正しく評価されます。

$(BUILD)/%.d : %.cpp
    mkdir -p $(dir $@); \
    $(CPMM) -MM -DDEPEND_ESCAPE $(POPT) $(PFLAGS) $(PINCS) $< \
    | sed 's/$(notdir $*)\.o:/$(subst /,\/,$(patsubst %.d,%.o,$@) $@):/' > $@ ; \
    [ -s $@ ] || rm -f $@
  • この例では、C++ ソースコードをコンパイルして、「.o」オブジェクトを生成します。
  • 「-DDEPEND_ESCAPE」は特定のソースで、従属規則の生成に支障がある場合に備えています。
  • 従属規則は、「*.d」ファイルにセーブされます。
release/main.o release/main.d: main.cpp main.hpp ../common/snd_io/sound.hpp \
 ../common/snd_io/audio_io.hpp ../common/utils/file_io.hpp \
 ../common/utils/string_utils.hpp ../common/utils/mtx.hpp \
 ../common/utils/vtx.hpp ../common/snd_io/i_audio.hpp \
 ../common/snd_io/audio.hpp ../common/snd_io/snd_files.hpp \
 ../common/snd_io/i_snd_io.hpp ../common/snd_io/tag.hpp \
 ../common/utils/array.hpp ../common/img_io/img_files.hpp \
 ../common/img_io/i_img_io.hpp ../common/img_io/i_img.hpp \
 ../common/img_io/img.hpp ../common/snd_io/pcm.hpp \
 ../common/utils/fifo.hpp ../common/widgets/widget_director.hpp \
 ../common/widgets/widget.hpp ../common/img_io/paint.hpp \
 ../common/img_io/img_rgba8.hpp ../common/img_io/img_idx8.hpp \
 ../common/img_io/img_clut.hpp ../common/img_io/img_gray8.hpp \
 ../common/img_io/img_utils.hpp ../common/img_io/perlin_noise.hpp \
 ../common/utils/vmath.hpp ../common/utils/preference.hpp \
 ../common/widgets/common_parts.hpp ../common/gl_fw/glmobj.hpp \
 ../common/gl_fw/gl_info.hpp ../common/utils/keyboard.hpp \
 ../common/core/glcore.hpp ../common/utils/singleton_policy.hpp \
 ../common/core/device.hpp ../common/gl_fw/glfonts.hpp \
 ../common/core/ftimg.hpp ../common/utils/director.hpp \

※これは、あるプロジェクトの、main.cpp に関する従属規則の一部です。

生成オブジェクトの一括管理フォルダの導入

通常、コンパイラは、ソースコードと同じ場所にオブジェクトファイルを生成します。
しかし、それだと、共有されているソースコードなどで問題がありますし、管理上の問題もあります。
そこで、特定のディレクトリにまとめて、出力するように工夫しています。
この工夫は、「従属規則生成」に含まれています。

Makefile の先頭で、

# 'debug' or 'release'
BUILD       =   release

上記のようにフォルダーを指定しており、従属規則生成の時、このパスが有効になるように工夫しています。

各プロジェクト毎のソースコードと実行バイナリー、リンクライブラリ

TARGET      =   player

# 'debug' or 'release'
BUILD       =   release

VPATH       =   ../common

CSOURCES    =   minizip/ioapi.c \
                minizip/unzip.c

PSOURCES    =   main.cpp \
                player.cpp \
                core/glcore.cpp \
                core/device.cpp \
                core/ftimg.cpp \
                gl_fw/glfonts.cpp \
                gl_fw/glmobj.cpp \
                gl_fw/glutils.cpp \
                gl_fw/glterminal.cpp \
                utils/vtx.cpp \
                utils/vmath.cpp \
                utils/sjis_utf16.cpp \
                utils/string_utils.cpp \
                utils/file_io.cpp \
                utils/file_info.cpp \
                utils/files.cpp \
                utils/keyboard.cpp \
                img_io/paint.cpp \
                img_io/bmp_io.cpp \
                img_io/tga_io.cpp \
                img_io/dds_io.cpp \
                img_io/png_io.cpp \
                img_io/jpeg_io.cpp \
                img_io/openjpeg_io.cpp \
                img_io/pvr_io.cpp \
                img_io/img_files.cpp \
                img_io/img_utils.cpp \
                snd_io/audio_io.cpp \
                snd_io/pcm.cpp \
                snd_io/wav_io.cpp \
                snd_io/mp3_io.cpp \
                snd_io/snd_files.cpp \
                snd_io/sound.cpp \
                widgets/common_parts.cpp \
                widgets/widget_director.cpp \
                widgets/widget_utils.cpp \
                widgets/widget_filer.cpp

上記のように、「TARGET」、「VPATH」、「CSOURCES」、「PSOURCES」を設定します。
※「VPATH」は make の機能で、サーチするパスを指定します。

また、リンクライブラリを設定します。

OPTLIBS +=  glfw3 pthread \
            png jpeg openjp2 \
            freetype \
            mad tag \
            z

その他の設定

システムライブラリのヘッダーは、少しだけ工夫が必要です。

※「openjpeg」は、バージョンアップに伴って、バージョンが変化します、それを吸収して、異なったバージョンの適切なパスを導入します。

# build openjpeg any version path
OPENJPEG_PATH := $(shell ls -d $(LOCAL_INC_PATH)/openjpeg*)

INC_SYS     =   $(LOCAL_INC_PATH) \
                $(LOCAL_INC_PATH)/freetype2 \
                $(OPENJPEG_PATH) \
                $(LOCAL_INC_PATH)/taglib

また、「INC_SYS」でインクルードされるパスは従属規則生成で含まれないような工夫をしています。

INC_S   =   $(addprefix -isystem , $(INC_SYS))
INC_L   =   $(addprefix -isystem , $(INC_LIB))

オブジェクト名は、ソースコードを元に生成しています。

OBJECTS =   $(addprefix $(BUILD)/,$(patsubst %.cpp,%.o,$(PSOURCES))) \
            $(addprefix $(BUILD)/,$(patsubst %.c,%.o,$(CSOURCES)))
DEPENDS =   $(patsubst %.o,%.d, $(OBJECTS))

最後に

結局、プロジェクト毎に微妙に異なる設定も必要ですが、基本的に、「TARGET」、「VPATH」、「CSOURCES」、「PSOURCES」の部分だけ記述すれば良く、使いまわしが出来ると思います。

以下のリンクを参照下さい。
RX マイコン用 Makefile
https://github.com/hirakuni45/RX/blob/master/RTK5_AUDIO_sample/Makefile?ts=4

GLFW3 アプリケーション用 Makefile
https://github.com/hirakuni45/glfw3_app/blob/master/glfw3_app/player/Makefile?ts=4