Tag Archives: build

Makefile 自动生成头文件的依赖关系

最近在看一本书《Windows游戏编程大师技巧》 (Tricks of Windows Game Programming Gurus). 第一章给出了一个打砖块小游戏的示例程序. 包括三个文件: blackbox.h, blackbox.cpp和freakout.cpp (600行代码, 对于Windows C++程序来说还好, 没有让我freak out…). blackbox.cpp封装了部分DirectDraw, 提供了一些更傻瓜化的初始化DirectDraw, 画点, 画方框的工具函数. blackbox.h包括了这些函数的声明. freakout.cpp引用了blackbox.h文件, 包括WinMain主函数和主要的游戏逻辑.

How to build?

和我一样还在使用N年前的垃圾电脑的看官们, 多半也不愿意用Visual Studio来挑战自己的耐心. 但是所以让我们选择小米加步枪: GCC (MinGW) + Makefile. 写个简单的makefile例如:

all:    freakout.exe
 
freakout.exe: freakout.o blackbox.o
    g++ freakout.o blackbox.o -o stupid.exe -L D:gamedevdx81sdkDXFDXSDKlib -lddraw -mwindows
 
freakout.o: freakout.cpp <strong><span style="color: #ff0000;">blackbox.h</span></strong>
    g++ -c freakout.cpp -o freakout.o -I D:gamedevdx81sdkDXFDXSDKinclude
 
blackbox.o: blackbox.cpp <strong><span style="color: #ff0000;">blackbox.h</span></strong>
    g++ -c blackbox.cpp -o blackbox.o -I D:gamedevdx81sdkDXFDXSDKinclude

Problem?

上面的代码当然有很多问题, 比如”-L <DX lib path>”, “-I <DX inc path>”可以被定义在类似$(LIB), $(INC)的变量里面, 比如我没有按照makefile的习惯写一个clean目标清理生成的东东…不过我的重点是在红色高亮的blackbox.h: 两个.o文件都需要依赖于blackbox.h.

假设我们修改freakout.cpp引用更多的头文件, 每加一条#include “somefile.h”指令, 就需要相应地在makefile里为freakout.o增加一个依赖项somefile.h.

假设我们修改stupid.h文件, 让stupid.h也引用一个头文件someotherfile.h, 那么我们也需要相应地在makefile里为所有依赖stupid.h的.o文件 (在这个例子中是freakout.o和blackbox.o) 增加依赖项someotherfile.h.

所以问题是: 在.cpp和.h被修改的情况下, 怎么维护.o目标文件和.h头文件的依赖关系.

Solution – 自动生成依赖关系

Google一下”makefile 头文件 依赖”会发现大多数编译器都提供了一个选项生成.o目标文件所依赖的文件列表. 比如GCC的”-MM”选项. 运行GCC –MM freakout.cpp blackbox.cpp <库文件和头文件选项>得到输出:

freakout.o: freakout.cpp blackbox.h D:/gamedev/dx81sdk/DXF/DXSDK/include/ddraw.h
blackbox.o: blackbox.cpp blackbox.h D:/gamedev/dx81sdk/DXF/DXSDK/include/ddraw.h

所以一个简单的处理依赖关系的办法是把这些编译器生成的依赖关系写入一个文件里, 然后在makefile中用include指令包含这个文件:

CPP = g++
OBJ = freakout.o blackbox.o
LIB = -L D:gamedevdx81sdkDXFDXSDKlib -mwindows -l ddraw
INC = -I D:gamedevdx81sdkDXFDXSDKinclude
 
all: freakout.exe
 
freakout.exe: ${OBJ}
    ${CPP} ${OBJ} -o freakout.exe ${LIB}
 
<span style="color: #ff0000;"><strong>include depend</strong></span>
 
# note the symbols: $&lt; and '$@
%.o: %.cpp
    ${CPP} -c $&lt; -o $@ ${INC}
 
# generate depend file
<span style="color: #ff0000;"><strong>depend:</strong></span>
    <span style="color: #ff0000;"><strong>${CPP} -MM ${OBJ:.o=.cpp} ${INC} &gt; depend</strong></span>

注意红色的部分, depend是GCC生成的包含了依赖关系的文件 (也注意上面的makefile中有”$<”, “$@”这种perl风格的bt匹配字符…) . 有了这样的makefile, 我们就能用两个步骤来build项目:

1) 在需要更新依赖关系的时候 (比如在某个文件里多加了一条#include指令) 运行make depend.

2) 运行make.

Makefile Auto Dependency – 只运行一次make

懒惰是程序员的一大美德, 所以GNU make的手册里提供了一个运行一次make就能更新所有依赖关系并且按照依赖关系build的办法 (http://www.gnu.org/software/make/manual/make.html#Automatic-Prerequisites): 与整体引入一个depend目标不同, 我们为每一个.o/.cpp文件引入一个.d依赖关系文件. 依赖关系文件由GCC的”-MM”选项生成, 并且在GCC输出的基础上把自己本身加入到目标列表中. 比如:

# freakout.d
freakout.o <strong><span style="color: #ff0000;">freakout.d</span></strong>: freakout.cpp D:/gamedev/dx81sdk/DXF/DXSDK/include/ddraw.h blackbox.h
 
# blackbox.d
blackbox.o <strong><span style="color: #ff0000;">blackbox.d</span></strong>: blackbox.cpp D:/gamedev/dx81sdk/DXF/DXSDK/include/ddraw.h blackbox.h

注意红色的部份: .d文件被加入到了目标列表, 依赖相关的.h和.cpp文件. 那么怎样自动生成这些.d文件呢? 类似的, makefile:

CPP = g++
OBJ = freakout.o blackbox.o
LIB = -L D:gamedevdx81sdkDXFDXSDKlib -mwindows -l ddraw
INC = -I D:gamedevdx81sdkDXFDXSDKinclude
 
all: freakout.exe
 
freakout.exe: ${OBJ}
    ${CPP} ${OBJ} -o freakout.exe ${LIB}
 
<span style="color: #ff0000;"><strong>include ${OBJ:.o=.d}</strong></span>
 
%.o: %.cpp
    ${CPP} -c $&lt; -o $@ ${INC}
 
<strong><span style="color: #ff0000;">%.d: %.cpp</span></strong>
    rm -f $@ &amp; 
    ${CPP} -MM $&lt; ${INC} &gt; $@.$$ &amp; 
    insertdfile.exe $&lt; $@.$$ &gt; $@ &amp; 
    rm -f $@.$$

这个makefile和上一节给出的makefile大致差不多, 不同的是两个红色的部分: 第一部分我们用include指令include相关的所有.d文件; 第二部分定义了生成.d文件的规则 (又是$@, $<符号…): 第一行首先删除原有的.d文件; 第二行运行g++ -MM生成依赖关系写入一个临时文件($@.$$)里; 第三行把.d文件加入到第二行生成的依赖关系中并把最终结果写到.d文件中; 第四行删除临时文件.

P.S. insertdfile.exe是我自己写的一个小程序, 用来把.d文件加入到第二行生成的依赖关系中, 例如把”blackbox.o: blackbox.cpp blackbox.h”转换为”blackbox.o blackbox.d: blackbox.cpp blackbox.h”. 我为什么要自己写一个字符串替换的程序? 因为我没有装sed工具…如果有, 当然用官方推荐的办法, 把第三行换成神奇的符咒: sed ‘s,($*).o[ :]*,1.o $@ : ,g’ < $@.$$ > $@ .

好了, 运行make:

1) make尝试去包含blackbox.d和freakout.d文件, 没有发现, 所以检查有没有规则可以生成这些.d文件, 发现我们%.d: %.cpp的这条规则, 于是运行这条规则生成所有的.d文件并且包含进来.
2) 按照正常规则生成.o文件, 然后生成.exe.

然后我们做一些修改, 比如增加一个stupid.h文件, 然后修改blackbox.h文件以包含stupid.h. 运行make:

1) make把第一个目标all加入到需要生成的目标列表中.
2) make包含blackbox.d和freakout.d文件. 注意在这个过程中这两个.d文件也会被加入到make的需要生成的目标列表中 – 因为可能有规则能更新这两个.d文件.
3) 根据blackbox.d和freakout.d包含的规则: .d文件 (已经被入到了make的需要生成目标列表中) 需要被更新, 于是相应地运行%.d: %.cpp规则更新.d文件: 新加入的 stupid.h文件被加入到两个.d文件的依赖项中.
4) make发现包含的两个.d文件在第2)步中都被更新, 于是重新包含这两个.d文件.
5) make根据在第2)步生成在第3)步被包含进来的规则, 发现stupid.h的日期比两个.o文件的日期都要新, 于是重新生成.o文件.
6) 根据规则生成.exe.

刺猬战争 Hedgewars

又大半年没有更新blog了…
昨天发现一个类似”百战天虫”的开源游戏Hedgewars, 支持很多操作系统 (windows/linux/BSD/…), 可以在http://www.hedgewars.org下载安装程序和源码 (Windows/Linux). 游戏里面的主角从虫子变成了刺猬 (单人模式里面貌似npc是水果人…), 也是一样的游戏套路, 满天乱扔东西…游戏是用C++和Pascal写成的, 用了Qt库和SDL (Simple DirectMedia Layer)系列库, 都是跨平台的东西.

下了一个0.9.9版, 在windows下面安装完之后, 运行没多久就出现一个错误说某个zh-CN.txt文件找不到…google一下, 删掉了locale目录下面所有的*zh-cn*文件, 游戏变成英文版, 不过能正常运行了. 总体来说感觉不错, 在我的破电脑上也能比较流畅的运行. 画面和百战天虫相比, 总体风格差不多, 一些细节还需要改进, 比如把炸弹扔到别人头上就看不见了…音乐还是有一定差距的, 不论是音质还是背景音乐的选择. 音效貌似是照搬百战天虫的音效. 操作方式和原来一样, 省的再去适应. 然后就是不仅支持网络对战, 竟然还有官方服务器…进去过后发现也有20多个人. orz…

作为一个爱岗敬业的IT民工, 体验一下安装版的游戏过后, 应该要自己build一个出来玩玩. 查了一下怎么build, 就只有在官网的FAQ里面有几句说明: 先下Qt, FreePascal, SDL系列库, 然后去源代码目录cmake + make搞定. 当然, 这是理想情况, 实际操作起来, 不会那么河蟹…我在windows下的步骤:

1) 先找到官网上列的库, 下载下来. 总共有200多MB的东西. 因为Qt很大 (100多MB), FreePascal也有35MB. 然后把安装程序都装上, 压缩包都解开. 安装Qt的时候会提示下载MinGW (GCC的windows移植版), 如果有, 就不必下了.

2) 设置PATH环境变量. 包括:
<CMake路径>bin – 能在cmd line直接下运行CMake
<MinGW路径>bin – 使CMake的选项-G “MinGW Makefiles”能工作
<Qt路径>bin – CMake Hedgewars的过程中需要用qmake等工具
<FreePascal路径>bini386-win32 – CMake Hedgewars的过程中需要用到FreePascal. 注意这个目录下面也有gcc的一系列工具, 所以最好把这个目录放到<MinGW路径>bin之后
<SDL路径>includeSDL – CMake Hedgewars的过程中需要SDL的头文件和库文件
<SDL_mixer路径>include – CMake Hedgewars的过程中需要SDL的头文件和库文件
<SDL路径>bin – (build生成的) Hedgewars.exe运行需要SDL的DLL
<SDL_mixer, SDL_net, SDL_ttf的路径>lib – 共3个路径, (build生成的) Hedgewars.exe运行需要的DLL

3) 进入cmd line到Hedgewars的源代码目录, 运行CMake . -G “MinGW Makefiles”, DONE. 选项-G指定生成makefile的generator. 这里用的MinGW.

第二步里加了很多路径到PATH中, 有三种类型的路径: 第一是为了使cmd line能找到并直接运行程序比如<CMake路径>bin; 第二是使CMake能找到需要的工具程序, 头文件和库文件比如<SDL_mixer路径>include (因为Hedgewars源代码里的的CMakeLists.txt文件包含一些搜索相应工具/库的”find_program”, “find_package”指令, 我不确定除了修改PATH以外还有其他什么更简单的方式让CMake能找到这些工具/库); 第三是使生成的Hedgewars.exe程序运行时能找到DLL比如<SDL路径>bin.