macOS上编译lincity-ng:从jam到CFLAGS的完整踩坑指南

macOS上编译lincity-ng:从jam到CFLAGS的完整踩坑指南 前阵子折腾了一晚上终于在自己的 MacBook 上把 lincity-ng 编译通过、跑了起来。这个老牌开源项目其实挺好玩的跟 SimCity 一个路子但完全免费、源码可看而且对硬件要求极低——我愿称之为公司电脑上的摸鱼神器。但吐槽的地方也在这它的构建系统用的是 jam而不是大家熟悉的 make很多人在 macOS 上编译它都是卡在一堆莫名其妙的 CFLAGS、CXXFLAGS、LDFLAGS 上。这篇就把我完整的踩坑链路和最终可用的配置参数写下来给同样想自己编译一份的人省点时间。1. 为什么偏要折腾这个老古董lincity-ng 的背景与选型理由1.1 它到底是个什么游戏lincity-ngLinCity 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/SDLmacOS 下如果走 Homebrew路径会是/opt/homebrew/includeApple Silicon或/usr/local/includeIntel。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 SiliconM2的 MacBook系统是 macOS SonomaXcode 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 专属 APISDL 2 里这些函数虽然也能用但是头文件路径和部分宏定义完全不同直接编译会报一堆 “use of undeclared identifier”。我的机器上 Homebrew 仓库里的sdl公式仍然指向 SDL 1.2.15 的兼容包所以装完还算顺利。验证版本的方法很简单pkg-config --modversion sdl如果输出1.2.15那恭喜你SDL 1.2 的部分没问题。如果拿到的是 2.x也不要慌后面会讲怎么应对。还有一个隐藏依赖是pkg-configmacOS 默认没有务必先装上brew install pkg-config2.2 SDL 1.2 的版本陷阱与备选方案这里单独把 SDL 版本陷阱拎出来说因为它是新手最容易翻车的环节。Homebrew 在 2020 年前后做过一轮清理很多老版本公式被移出了核心仓库sdl1.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 -stdgnu11 export LDFLAGS-L/opt/homebrew/lib -lSDL -lSDL_image -lSDL_mixer -lSDL_ttf -framework OpenGL -framework Cocoa逐个解释一下-I/opt/homebrew/include/SDLSDL 1.2 的头文件多放在这个子目录里不加的话#include SDL.h会找不到。-I/opt/homebrew/include/libpng16libpng 在 Homebrew 中默认把头文件装在libpng16子目录lincity-ng 的资源加载代码里有png.h的直接引用。-stdgnu11lincity-ng 源码里用了早期的auto_ptr等特性不指定标准的话新版 clang 会把它当 C14 来编译auto_ptr在新标准中已经被标记 deprecate运气不好还会直接报错。用gnu11是最稳妥的兼容选项。-lSDL_image -lSDL_mixer -lSDL_ttf注意顺序这仨库都依赖-lSDL所以 SDL 必须放最后。反过来的话链接器会报 undefined symbol。-framework OpenGL -framework Cocoa这是 macOS 特有的。lincity-ng 的渲染后端走 OpenGL但 macOS 上没有libGL只有OpenGL.frameworkCocoa 则是 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 directoryCFLAGS 没包含 SDL 头文件路径-I/opt/homebrew/include/SDL加进 CFLAGSld: library not found for -lSDLLDFLAGS 没指定库路径-L/opt/homebrew/lib加进 LDFLAGSundefined reference to SDL_InitSDL 库顺序不对把-lSDL放到所有-lSDL_*的后面error: auto_ptr in namespace std does not name a type默认 C 标准太新CXXFLAGS 追加-stdgnu11ld: framework not found OpenGL链接器不知道去哪找 OpenGLLDFLAGS 加上-framework OpenGLerror: png.h file not foundlibpng 头文件子目录未加入搜索路径CFLAGS/CXXFLAGS 加上-I/opt/homebrew/include/libpng16error: conflicting declaration typedef void* GLhandleARBOpenGL 头文件与 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 failedmacOS 的 OpenGL 兼容上下文问题一般更新显卡驱动或者换一个 SDL 视频驱动可以解决。可以用export SDL_VIDEODRIVERcocoa强制指定 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 构建系统的样本含金量比很多现代模板项目高得多。