Emake:你见过最简单的 C/C++ 构建工具

CMake 已经成为 C++ 构建工具事实上的标准了,即便觉得它很难用,但项目发布,跨部门协同,基本都以 cmake 为准。尽管你可能觉得其它构建工具更顺手,没问题,你们平时用就行,但项目发布或者跨团队协同时,你得同时用上 cmake 来标准化。

那么对于内部中小项目,非正式个人练手项目,或者非发布阶段的开发过程,是否也需要上 cmake 呢?还真不一定,一旦不用 cover 整个宇宙的构建需求,我们大可以找一个趁手的二号构建工具,满足平时使用。那么哪个二号构建工具值得推荐呢?

很多流行的构建工具,从 xmake 到 meson,恐怕都不适合,因为他们都试图同 cmake 去竞争试图要 cover 整个宇宙,即便号称精简,也不可能精简到哪里,尽管他们最简单的 demo 看起来好像真的超简单,但再稍微复杂点,比如考虑多平台架构,加个 release/debug 和包管理,一个个都变得丑陋不堪,立马原型毕露,因为他们都是命令式的。

我从 2009 年开发了一个叫做 emake 的构建工具,就是一个 emake.py 的单一脚本,持续使用并陆陆续续迭代了 15 年,今天感觉可以让他出来走两步。

推荐它,因为它有可能是你见过最简单的构建工具了,简单到什么程度呢?

第一:定义式构建工具

简单点例子:main.mak 就三行:

makefile 复制代码
flag: -Wall, -O3
mode: exe
src: foo.c, bar.c, main.c

第一行设定编译参数,第二行指明目标格式,第三行设定源代码,这也是大部分时候写点小玩具,小测试的样子,然后:

bash 复制代码
emake main.mak

就能生成 main.exe 了,定义式的意思就是不像 cmake 一样每次要在 CMakeLists.txt 里写个小程序,而是跟 IDE 一样定义好源文件,设定好 release/debug 的编译参数,然后 emake 帮你做好默认工具链初始化,依赖分析,多核编译等等一堆琐事。

而且还没有 cmake 的 rm -rf build && mkdir build && cmake -B build -G "MinGW Makefiles" . 初始化环节,这个初始化每次都很蛋疼,极大的阻碍了我创建新项目的热情。更不用在 build 目录生成一大堆 shit,写好 emake 工程文件就能直接编译出可执行了,没有任何第二层构建工具的依赖。

也不会每个项目像 cmake 一样单独占用一个目录,我一堆小项目全部放一个目录里都没事情。到这里你可能会说,好像也没比 xmake 简单多少啊?别急我们还能继续简化。

第二:零工程文件

如果你连工程文件都懒得写,没关系,emake 支持以 docstring 的形式将工程配置写到代码中:

cpp 复制代码
#include <stdio.h>

//! flag: -Wall, -O3
//! src: foo.c, bar.c
int main() {
    ...
}

这些以 //! 开头的注释可以用来描述项目配置,然后:

bash 复制代码
emake main.c

就能生成 main.exe 了,连工程文件都不需要,你把上述命令配置到 vscode / vim 里,按 F9 就能编译一个简单的项目,连 "mode: exe" 都不需要写,默认值即可,需要第三方库了就在加一行:

cpp 复制代码
//! link: zlib, m

就能链接对应的 .a 静态库了。

复杂的项目都是由一个个小的想法组成的,在写复杂项目之前,往往有大量的小的想法需要验证,小的模块需要开发,对于 "编码-构建-测试" 这个内循环,属于核心工作流,任何一个环节改进一点,都能带来极大的效率提升,这种 docstring 内嵌的方式,可以极大的简化你验证想法的成本。

还能降低你创建新项目的心理门槛,想建就建,你会发现你更容易开始一个新模块的开发了,等到模块变得足够复杂了,再将这些 docstring 独立出来,放到一个统一的工程文件里。

看到这里也许你会觉得是简化了一些,但 xmake 之类的对简单项目好像也不复杂啊?

别急,我们把需求稍微增加点,支持跨平台编译,支持区别 debug/release 区别就明显了。

第三:条件编译

有个模块需要在 Windows / Linux 下运行,需要区别 debug/release 该怎么写?简单:

makefile 复制代码
src: src/main.c
win32/flag@release: -O3, -Wall
win32/flag@debug: -Og, -g
linux/define: TEST1, TEST2

分别在 Windows 平台的 debug/release 两种配置中定义了不同的 flag ,并在 linux 下面定义了两个宏(不区分 debug/release),编译时:

bash 复制代码
emake --profile=release main.mak    # 按照 release 模式编译
emake --profile=debug main.mak      # 按照 debug 模式编译

只有四行配置,搞定跨平台多模式构建。

横向对比下 xmake:

lua 复制代码
target("main")
    set_kind("binary")
    add_files("src/main.c")
    if is_plat("macosx", "linux") then
        add_defines("TEST1", "TEST2")
    end
    if is_plat("windows") then
        if is_mode("release") then
           add_cxflags("-O3")
        else
           add_cxflags("-Og")
        end
    end

号称简单的 xmake 立马不那么简单了,同样 cmake 也没好到哪里去:

cmake 复制代码
project(main)
add_executable(main 
    src/main.c
)
if (WIN32)
    set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -Og")
    set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -O3")
endif()
if (LINUX)
    target_compile_definitions(main PRIVATE TEST1 TEST2)
endif()

对比下前面 emake 的 4 行工程文件,哪种更简单?你更想写哪种?需求稍微完善点,那些号称简单的构建工具们,一个个都原形毕露了。

第四:第三方包引入

这个更简单:

makefile 复制代码
package: python3, sqlite3, curl

就行了,emake 会使用 pkg-config 将这几个包的 .pc 文件信息提取出来并分析他们的依赖图(比如会进一步补充 lzma, openssl 之类的 python3 依赖),然后添加到 CFLAGSLDFLAGS 上去。

如果你的 /usr/lib/pkgconfig 下面没有对应 .pc ,比如你的第三方库随意乱放,没 install 时,当然也可以手工指定 include 目录,和静态库链接选项,但库多了会略显繁琐。

第五:多套工具链切换

默认工具链配置位于 ~/.config/emake.ini ,默认不提供的话 emake 能自动搜索 $PATH 里的工具,其它工具链位于 ~/.config/emake/{name}.ini 使用时用 --cfg={name} 指明:

bash 复制代码
emake --cfg=mingw32-gcc12 <arguments>

就会读取 ~/.config/emake/mingw32-gcc12.ini 的工具链配置进行初始化了,一般程序员机器上都会不止一套工具链,比开发 Windows 下 32/64 位的程序,或者交叉编译 android 平台等。

配置一个新的工具链很简单,只需要在 ini 文件里写入:

ini 复制代码
[default]

# 工具链的 bin 目录,用于查找 gcc / clang 等工具
home=d:/msys32/mingw32/bin

# 当你有多套工具链时,不可能都加入 $PATH,这个配置可以让 emake 在
# 构建时临时追加到 $PATH 前面,不污染外层父进程的环境变量
path=d:/msys32/mingw32/bin,d:/msys32/usr/bin

# 通用配置,免得每个工程文件写一遍
flag=-Wall
link=stdc++, winmm, wsock32, user32, ws2_32
cflag=-std=c11
cxxflag=-std=c++17

# 针对 debug/release/static 三种 profile 的设置,使用
# emake --profile=<name> xxx 在构建时指明使用啥 profile
define@debug=_DEBUG=1
define@release=_RELEASE=1
define@static=_STATIC=1, _RELEASE=1

flag@debug=-Og, -g, -fno-omit-frame-pointer
flag@release=-O3
flag@static=-O3, -static

# 多核编译
cpu=4

# 目标平台名称,不提供得话默认用 python 的 sys.platform 字符串代替
target=win32

# 条件编译时候的条件变量,在工程文件里可以用 win32/flag: xxx 来使用
name=win32,nt,have_openssl

其实不用写那么多,平时只要 homepath 两项定义好就行,后面的都是一些公共配置的演示,可以进一步帮助少写点工程文件。

写好多个 ini 文件放到 ~/.config/emake 目录下面的话,就能随时切换多套工具链:

bash 复制代码
emake --cfg=mingw32-gcc12  project.mak     # 32 位 Windows 程序,GCC-12
emake --cfg=mingw64-gcc12  project.mak     # 64 位 Windows 程序,GCC-12
emake --cfg=mingw32-gcc53  project.mak     # 用 gcc-5.3 工具链构建 Windows XP 兼容程序。
emake --cfg=clang32  project.mak           # 使用 clang 构建
emake --cfg=android-16-arm  project.mak    # 交叉编译 Android 16 的 arm 程序
emake --cfg=android-16-x86  project.mak    # 交叉编译 Android 16 的 x86 程序

我机器上有一堆工具链,随时随地,想切就切。

这个 emake 我从 2009 年不断迭代使用至今,用它构建过最复杂的项目是 webrtc,是的,我没用 google 那套,并且跨平台支持 android/windows/iOS。

虽然我对外的项目都会写一个 CMakeLists.txt,但同一个项目内也会包含一个 emake 工程文件我自己开发时使用,新项目一开始就用,直到两个月后足够复杂了,项目快完成了,我再写一个 cmake 的工程文件准备发布。

平时我自己验证一些想法,写一些小模块,做些中小项目也都是用它,配置在 vim/vscode 里,一键编译,很趁手,也没一直开发,陆陆续续更新了很多年,感觉可以拿出来见见人了。

项目地址:

限于篇幅,还有很多功能没法说完,比如:

  • 如何同 vcpkg 联动,导入 vcpkg 包?
  • 如何导出 compile_commands.json ,供其它工具使用?
  • 如何使用 clang 等非 gcc 工具?
  • 如何构建动态库和静态库,如何给动态库导出 MSVC 能用的 .lib 文件?
  • 如何位单个源文件指定不同的编译参数?
  • 如何添加汇编语言源文件?
  • 如何在 Windows 项目中加入 .RC 资源文件?
  • 之类的,欢迎参考项目文档。

最后说一句:我觉得 emake 的最大价值在于它放弃了追求全宇宙,在特定的需求范围内寻求最优解,是一个很趁手的二号构建工具。

相关推荐
若亦_Royi1 小时前
C++ 的大括号的用法合集
开发语言·c++
ragnwang4 小时前
C++ Eigen常见的高级用法 [学习笔记]
c++·笔记·学习
lqqjuly7 小时前
特殊的“Undefined Reference xxx“编译错误
c语言·c++
冰红茶兑滴水8 小时前
云备份项目--工具类编写
linux·c++
刘好念8 小时前
[OpenGL]使用 Compute Shader 实现矩阵点乘
c++·计算机图形学·opengl·glsl
酒鬼猿8 小时前
C++进阶(二)--面向对象--继承
java·开发语言·c++
姚先生979 小时前
LeetCode 209. 长度最小的子数组 (C++实现)
c++·算法·leetcode
小王爱吃月亮糖10 小时前
QT开发【常用控件1】-Layouts & Spacers
开发语言·前端·c++·qt·visual studio
aworkholic10 小时前
opencv sdk for java中提示无stiching模块接口的问题
java·c++·opencv·jni·opencv4android·stiching
程序员老冯头10 小时前
第十六章 C++ 字符串
开发语言·c++