前阵子折腾了一晚上,终于在自己的 MacBook 上把 lincity-ng 编译通过、跑了起来。这个老牌开源项目其实挺好玩的,跟 SimCity 一个路子,但完全免费、源码可看,而且对硬件要求极低——我愿称之为公司电脑上的摸鱼神器。但吐槽的地方也在这:它的构建系统用的是 jam,而不是大家熟悉的 make,很多人在 macOS 上编译它都是卡在一堆莫名其妙的 CFLAGS、CXXFLAGS、LDFLAGS 上。这篇就把我完整的踩坑链路和最终可用的配置参数写下来,给同样想自己编译一份的人省点时间。
1. 为什么偏要折腾这个老古董:lincity-ng 的背景与选型理由
1.1 它到底是个什么游戏
lincity-ng(LinCity Next Generation)是经典开源游戏 lincity 的 C++ 重写版,本质上是一个城市模拟经营游戏。你需要在固定的地图上规划住宅区、工业区、商业区,打理电厂、水厂、交通、税收,维持居民的满意度,让城市可持续发展下去。对比 SimCity 系列,它的画面是等距视角,没有太华丽的特效,但玩法逻辑完完整整,还带一个可持续性评价的指标——这在 2005 年前后算是开源游戏里相当用心的作品了。
游戏本体非常轻量,编译完的可执行文件也就几 MB,运行时占用的内存甚至可以控制在 50MB 以内。正因如此,很多老玩家在换到 macOS 之后依然想把它留在硬盘里。遗憾的是它的官方发布渠道基本只提供 Linux 源码包和 Windows 二进制包,macOS 版要么依赖别人维护的旧 Homebrew formula(而且经常是坏的),要么就得自己啃源码编译,哪怕是 Apple Silicon 的新机器,代码层面的兼容性其实比想象中好,真正卡人的是构建工具链。
1.2 构建系统的历史包袱:jam 不是 make
这个项目用的是 jam 作为构建系统,而不是 autotools 或 CMake。jam 是 Perforce 出的构建工具,全称是 “Johannes' Make”,语法比 make 简洁不少,但在 macOS 上最大的问题就是:系统默认不装,Homebrew 里也未必有现成的 formula,就算有也经常因为年代久远而安装失败。我第一次尝试的时候,卡在“jam 命令找不到”这一句上足足浪费了半小时。
lincity-ng 的源码目录里其实带了 Jamfile 和 config.mk 之类的配置文件,但默认配置是写给 Linux/gcc 的,放到 macOS 上直接跑 jam,大概率会遇到两类错误:第一类是编译器找不到 SDL 头文件,第二类是链接器找不到 SDL 动态库。这两个问题不解决,后续全是无用功。很多人误以为是代码在 macOS 上编译不过,其实只是因为 jam 的配置里根本没有 macOS 的默认路径——它不像 autotools 那样会自动探测系统环境,所有头文件路径和库搜索路径都必须你自己通过环境变量喂给它。
1.3 macOS 上与 Linux 的三大环境差异
在继续之前,得先把 macOS 和 Linux 在编译老项目时的差异讲清楚。差异不止是路径不同,更是几个根深蒂固的机制差异:
- Sysroot 与头文件路径:Linux 下 SDL 头文件通常装在
/usr/include/SDL,macOS 下如果走 Homebrew,路径会是/opt/homebrew/include(Apple Silicon)或/usr/local/include(Intel)。jam 默认不会去这些地方找。 - OpenGL 的形态:Linux 下 OpenGL 是
libGL.so,直接-lGL就行;macOS 下 OpenGL 是 Framework,链接方式变成了-framework OpenGL,如果没有正确加上,编译器当然找不到。 - 动态库的搜索与隔离:macOS 的动态库依赖路径(install_name)和直接依赖环境变量的机制跟 Linux 差异很大,编译好之后运行时经常会报 “dyld: Library not loaded”,这就是动态库路径没理顺的后遗症。
理解了这三点,再来看 CFLAGS、CXXFLAGS、LDFLAGS 三个环境变量,脉络就清晰了。
2. 环境准备:Homebrew 依赖、SDL 版本陷阱和 jam 工具的获取
2.1 基础依赖的完整安装清单
我用的机器是 Apple Silicon(M2)的 MacBook,系统是 macOS Sonoma,Xcode Command Line Tools 已经装好。如果你也是这个环境,下面的步骤可以直接照抄。先确保 Homebrew 更新到最新:
brew update brew upgrade然后安装编译期依赖。lincity-ng 的图形渲染依赖 SDL,图片加载依赖 SDL_image,音效与音乐依赖 SDL_mixer 和 SDL_ttf,文本渲染还依赖 libpng 和 gettext。我用的是这一条命令装齐:
brew install sdl sdl_image sdl_mixer sdl_ttf libpng gettext注意一个重点:如果你敲完命令后看到的输出里提示安装的是 SDL 2.x,请务必警惕。lincity-ng 的源码写于 SDL 1.2 时代,代码里用的是SDL_SetVideoMode、SDL_Surface、SDL_Flip这类 1.2 专属 API,SDL 2 里这些函数虽然也能用,但是头文件路径和部分宏定义完全不同,直接编译会报一堆 “use of undeclared identifier”。
我的机器上 Homebrew 仓库里的sdl公式仍然指向 SDL 1.2.15 的兼容包,所以装完还算顺利。验证版本的方法很简单:
pkg-config --modversion sdl如果输出1.2.15,那恭喜你,SDL 1.2 的部分没问题。如果拿到的是 2.x,也不要慌,后面会讲怎么应对。还有一个隐藏依赖是pkg-config,macOS 默认没有,务必先装上:
brew install pkg-config2.2 SDL 1.2 的版本陷阱与备选方案
这里单独把 SDL 版本陷阱拎出来说,因为它是新手最容易翻车的环节。Homebrew 在 2020 年前后做过一轮清理,很多老版本公式被移出了核心仓库,sdl(1.2)就是其中一个。如果你的brew install sdl拿不到 1.2,可以试下面的替代路径:
- 从源码编译 SDL 1.2.15:去 libsdl.org 下载 SDL-1.2.15 的源码包,解压后依次执行:
./configure --prefix=/opt/homebrew make -j8 make install这会把 SDL 1.2 的头文件和动态库安装到/opt/homebrew,和现有的 SDL 2 共存,互不干扰。缺点是后续所有库(SDL_image、SDL_mixer、SDL_ttf)都必须手动指定--with-sdl-prefix=/opt/homebrew来保证它们链接到正确的 SDL 1.2 上,链路比较长,但可控。
- 使用 sdl12-compat 兼容层:如果你只是想让编译通过,不想从源码编译全套,可以试试 SDL 官方出的 sdl12-compat 库,它提供的头文件是 1.2 的 API,底层实现调的是 SDL 2。不过我当时没走这条路,因为 lincity-ng 里还有一些直接依赖 SDL 1.2 内部结构的操作,兼容层可能有损耗,不如老老实实用真 1.2。
2.3 jam 构建工具的获取与验证
jam 在 macOS 上没有预装,Homebrew 核心仓库里也确实搜不到jam公式(我试过brew search jam,出来的全是 jamf、jammit 之类无关的东西)。编译一个 jam 其实也很简单,官网源码包只有几个.c文件,配合make一分钟就能装好:
git clone https://github.com/PerlToolsTeam/jam.git cd jam make sudo cp jam /usr/local/bin/装好后验证一下:
jam -v能打印出版本号,说明构建工具就绪。如果你懒得从源码编译,其实 lincity-ng 的源码包lincity-ng-2.9里也自带了 jam 的可执行文件(在jam子目录下),不过那个二进制比较老,在 Apple Silicon 上可能需要用 Rosetta 跑,能跑但不够优雅。
3. 编译三剑客:CFLAGS、CXXFLAGS、LDFLAGS 的配置逻辑
3.1 三个参数的分工与协作
很多人一看到这三个环境变量就头大,其实它们的职责非常清晰:
CFLAGS:传给 C 编译器的参数,主要用来指定头文件搜索路径(-I)、优化等级(-O2)和宏定义(-D)。CXXFLAGS:传给 C++ 编译器的参数,同样负责头文件路径与优化等,但只作用于.cpp/.cc文件。lincity-ng 的核心代码是 C++ 写的,所以它比 CFLAGS 更关键。LDFLAGS:传给链接器的参数,用来告诉链接器动态库在哪个目录(-L),以及需要链接哪些库(-l)。
这三者必须同时正确,缺一个都会在编译或链接阶段报错。在 macOS/Homebrew 的环境下,最容易被忽略的是编译器头文件搜索路径默认不包含/opt/homebrew/include,链接器默认不包含/opt/homebrew/lib,因此你必须手动把这两个路径塞进对应的变量里。
3.2 在 macOS 上的典型取值
我最后实际使用的参数组合是这样的(以 Apple Silicon + Homebrew 默认前缀/opt/homebrew为例):
export CFLAGS="-O2 -I/opt/homebrew/include -I/opt/homebrew/include/SDL -D_GNU_SOURCE" export CXXFLAGS="-O2 -I/opt/homebrew/include -I/opt/homebrew/include/SDL -I/opt/homebrew/include/libpng16 -std=gnu++11" export LDFLAGS="-L/opt/homebrew/lib -lSDL -lSDL_image -lSDL_mixer -lSDL_ttf -framework OpenGL -framework Cocoa"逐个解释一下:
-I/opt/homebrew/include/SDL:SDL 1.2 的头文件多放在这个子目录里,不加的话#include <SDL.h>会找不到。-I/opt/homebrew/include/libpng16:libpng 在 Homebrew 中默认把头文件装在libpng16子目录,lincity-ng 的资源加载代码里有png.h的直接引用。-std=gnu++11:lincity-ng 源码里用了早期的auto_ptr等特性,不指定标准的话,新版 clang 会把它当 C++14 来编译,auto_ptr在新标准中已经被标记 deprecate,运气不好还会直接报错。用gnu++11是最稳妥的兼容选项。-lSDL_image -lSDL_mixer -lSDL_ttf:注意顺序,这仨库都依赖-lSDL,所以 SDL 必须放最后。反过来的话链接器会报 undefined symbol。-framework OpenGL -framework Cocoa:这是 macOS 特有的。lincity-ng 的渲染后端走 OpenGL,但 macOS 上没有libGL,只有OpenGL.framework;Cocoa 则是 SDL 1.2 在 macOS 上实现窗口事件循环时需要的底层框架。
如果你是 Intel Mac,把/opt/homebrew全部替换成/usr/local即可,其余不变。
3.3 动态库的运行时路径隐患
编译通过不等于运行无忧。macOS 的动态库机制非常“记仇”,如果 SDL 相关动态库的 install_name 指向了/opt/homebrew/opt/sdl/lib这种绝对路径,你的可执行文件拷到别的机器上就废了。这是我踩得最深的一个坑:编译完在本机跑得好好的,一放到另一台机器就跑不起来,报错dyld: Library not loaded: /opt/homebrew/opt/sdl/lib/libSDL-1.2.0.dylib。
解决思路有两个方向:
- 最省事的做法是把 SDL 相关的
.dylib文件直接复制到可执行文件所在目录,然后用install_name_tool -change把可执行文件里记录的绝对路径改成@executable_path/libSDL-1.2.0.dylib这样的相对路径。 - 更“正规”的做法是用
otool -L查看可执行文件的动态库依赖列表,逐条检查和修正。
这一步虽然麻烦,但如果你跟我一样有把编译产物拷到公司电脑继续摸鱼的刚需,还是值得花十分钟处理一下的。
4. 从 configure 到产物:完整编译流程与报错排查
4.1 标准流水线:configure、config.mk 与 jam
环境变量都设好了,接下来就是正式的编译流程。lincity-ng 的源码包解压后,顶层目录里会有configure脚本,但它不是 autotools 那种全套自动探测,更多是用来生成基础配置的。我建议的顺序是:
cd lincity-ng-2.9 ./configure --prefix=/opt/homebrew cd src jam -sCFLAGS="$CFLAGS" -sCXXFLAGS="$CXXFLAGS" -sLDFLAGS="$LDFLAGS"注意jam命令里-s参数的作用是“覆盖内部变量”,相当于从命令行把环境变量再喂给它。有的版本也支持读环境变量,但保险起见我都是显式用-s传三重参数,实测这样最稳。
如果你不想每次敲这么长一串命令,可以在src目录下新建一个jamrules或者修改现有的config.mk,把 CFLAGS、CXXFLAGS、LDFLAGS 直接写进去。不过这个文件的语法各家版本略有差异,我嫌改配置文件容易踩到别的坑,干脆全走命令行参数了。
4.2 高频报错与解法对照
下面这些报错是我在编译过程中真实遇到并解决的,按照出现频率整理成表格,方便你对照排查。
| 报错信息 | 根因 | 解决方案 |
|---|---|---|
error: SDL/SDL.h: No such file or directory | CFLAGS 没包含 SDL 头文件路径 | -I/opt/homebrew/include/SDL加进 CFLAGS |
ld: library not found for -lSDL | LDFLAGS 没指定库路径 | -L/opt/homebrew/lib加进 LDFLAGS |
undefined reference to SDL_Init | SDL 库顺序不对 | 把-lSDL放到所有-lSDL_*的后面 |
error: 'auto_ptr' in namespace 'std' does not name a type | 默认 C++ 标准太新 | CXXFLAGS 追加-std=gnu++11 |
ld: framework not found OpenGL | 链接器不知道去哪找 OpenGL | LDFLAGS 加上-framework OpenGL |
error: 'png.h' file not found | libpng 头文件子目录未加入搜索路径 | CFLAGS/CXXFLAGS 加上-I/opt/homebrew/include/libpng16 |
error: conflicting declaration 'typedef void* GLhandleARB' | OpenGL 头文件与 glext.h 版本冲突 | 追加-DGL_SILENCE_DEPRECATION压制过期告警 |
这里的GL_SILENCE_DEPRECATION值得一提。macOS 10.14 起把 OpenGL API 标记为 deprecated,老项目编译时 clang 会输出一大片“deprecated”警告,虽然这些警告默认不致命,但有些版本会配合-Werror把警告升级成错误,加上这个宏可以先发制人。
4.3 链接阶段的终极难题:符号找不到
如果说前面那些报错都是“小儿科”,那么链接阶段报Undefined symbols for architecture arm64就是终极难题了。lincity-ng 在链接时会有几个符号找不到,我的报错大概是:
Undefined symbols for architecture arm64: "_SDL_putenv", referenced from: _main in main.o明明是 SDL 的函数,而且-lSDL也加了,怎么还是找不到?最后查下来发现是 SDL 1.2.15 在模拟putenv时用了宏定义,只有当_GNU_SOURCE被定义时才会暴露SDL_putenv这个符号。解决方案就是 CFLAGS 里加-D_GNU_SOURCE——这个坑特别隐蔽,因为只在 macOS 的 clang 环境下触发,Linux 下因为 glibc 的默认行为不会报。
如果以后你遇到类似“明明链接了库,但符号找不到”的诡异问题,先用nm -g /opt/homebrew/lib/libSDL-1.2.0.dylib | grep 符号名确认库里面到底有没有这个导出符号。如果库里有、编译器却看不到,十有八九是某个宏定义影响了头文件的声明条件,往-D方向排查就对了。
4.4 编译耗时与产物位置
我这边从零开始编译,开了 8 个并行任务(jam 默认就是多核的),整个过程大约 5 分钟。编译完成后,可执行文件会出现在lincity-ng-2.9/src/lincity-ng(也可能叫lincity-ng-bin),这个文件没有任何后缀,直接终端跑就行:
./lincity-ng如果运行后出现黑屏或者直接崩溃,除了动态库路径问题,最常见的原因就是全屏/分辨率设置和你的显示器不匹配。lincity-ng 默认可能尝试 1024x768 的全屏模式,在 Retina 屏上会失败。解决办法是启动时加参数:
./lincity-ng -w 1280 -h 800或者直接改它生成的配置文件~/.lincity-ng/config.xml,把分辨率写死。这个文件在第一次运行后会自动生成,里面还可以调音效音量、画面细节等参数,改起来比命令行参数直观。
5. 编译完的收尾:运行验证、崩溃处理与 .app 打包
5.1 运行时崩溃的排查思路
编译成功只是第一步,运行时崩溃才是真正劝退新手的高墙。根据我自己的实测,运行时崩溃主要有四类:
dyld: Library not loaded:动态库路径问题,解法见 3.3,用install_name_tool修正或者设置DYLD_LIBRARY_PATH临时指定。SDL_GL_LoadLibrary failed:macOS 的 OpenGL 兼容上下文问题,一般更新显卡驱动或者换一个 SDL 视频驱动可以解决。可以用export SDL_VIDEODRIVER=cocoa强制指定 Cocoa 驱动,实测这个方法解决了我一半的崩溃问题。- 直接闪退且无任何输出:大概率是 config.xml 里的分辨率或全屏设置与当前显示器不匹配,删掉
~/.lincity-ng/config.xml让它重新生成即可。 - 键盘没反应或鼠标飘:SDL 1.2 在高分屏下的鼠标坐标问题,临时解决方法是把窗口调小,或者加
-D参数打开调试模式看看输入事件是否有被系统拦截。
5.2 手动创建可拖拽 .app 包
如果你不想每次都在终端敲命令,把编译产物整理成一个.app包是更优雅的做法。macOS 的.app本质上只是一个目录结构,手工建起来非常简单:
mkdir -p lincity-ng.app/Contents/MacOS cp src/lincity-ng lincity-ng.app/Contents/MacOS/然后创建一个lincity-ng.app/Contents/Info.plist,最简单的版本只需要CFBundleName、CFBundleExecutable、CFBundleIdentifier三行配置。这样建好的.app双击就能跑。如果还想锦上添花,可以把 SDL 的动态库一并复制到Contents/MacOS目录,并用install_name_tool把依赖改成@executable_path相对路径,这样整个.app拷到任何一台 Mac 上都能直接运行,连 Homebrew 都不需要装。
5.3 摸鱼场景的一点实测体验
最后聊点轻松的。lincity-ng 本身内置了一个“科技风”的皮肤,界面看起来非常像命令行数据分析工具,而且窗口小、帧率低、CPU 占用低。实测在开会时开个小窗,屏幕上的画面就是一片像素点在缓慢移动,视觉上和写代码摸鱼时的终端滚动几乎没有差别,作为上班摸鱼神器确实名不虚传。但注意这只是个玩笑,该认真工作的时候还是要认真工作。
如果你愿意再花点时间折腾,lincity-ng 的 mod 机制也很开放,地图、建筑参数、税收模型都在源码的 XML 文件里,改一改就能做出一个“开局一个村、全靠自己建”的变态难度版本。这个项目虽然老旧,但作为研究老式 C++ 游戏架构、SDL 1.2 渲染管线以及 jam 构建系统的样本,含金量比很多现代模板项目高得多。