Emake
你见过的最简单的 GCC/CLANG 项目构建工具,定义式构建,比命令式更简单
Install / Use
/learn @skywind3000/EmakeREADME
Preface
GNU Make 太麻烦?Makefile 写起来太臃肿?头文件依赖生成搞不定?多核同时编译不好弄?Emake 帮你解决这些问题:
- 使用简单:设定源文件,设定编译参数和输出目标就行了,emake为你打点好一切。
- 依赖分析:快速分析源代码所依赖的头文件,决定是否需要重新编译。
- 输出模式:可执行、静态库(.a)、动态库(.so/.dll)。
- 多核编译:轻松实现并行编译,加速项目构建。
- 精简紧凑:只有唯一的一个 emake.py 文件。
- 干净利索:无需导出 Makefile/.sln 等中间文件,构建一步到位。
- 交叉编译:构建 iOS 项目 ,安卓项目,等等。
- 语言支持
C/C++/ObjC/ObjC++/ASM - 工具支持
gcc/mingw/clang - 运行系统
Windows/Linux/Mac OS X/FreeBSD - 信息导出:项目文件列表,目标文件,以及
compile_commands.json等。 - 包管理支持
pkg-config,vcpkg和手工等三种方式导入第三方包。 - 方便的交叉编译,轻松构建
Android NDK/iOS/asm.js项目。 - 你见过最简单的构建系统,比 Gnu Make / CMake 都简单很多。
只有两三个源代码,那 makefile 随便写,文件一多,搞依赖都可以搞死人。emake 就是简单中的简单,不但比 GNU Make 简单,还要比 cmake 简单很多。
应为 CMake 和 GNU Make 都是命令式构建工具,而 emake 是定义式构建工具,命令式当然可以处理各种复杂情况,本身就是一门编程语言,强大却失之复杂,而定义式类似 IDE 那样,设定文件,编译参数链接参数,就能开始工作了,虽然做不到命令式那么灵活,但能满足大多数中小型项目开发,个人实验项目的日常开发。
Emake 是为快速开发而生的,通过牺牲了部分灵活性,却换来了极大的便利性,最初版本在 2009年发布,多年间团队在不同操作系统下用它构建过:服务端项目、客户端项目、iOS项目、安卓项目 和 Flash项目,这些项目都稳健的跑在生产环境中,为海量用户提供服务。
多年的开发中,emake 提高了各种大小项目的开发效率,自身也随着时间增加不断被完善和稳定。
具体好用在哪里,实际使用比 cmake/xmake 简单在哪里?可以看这个介绍:
Content
Install
Linux / Mac OS X
wget http://skywind3000.github.io/emake/emake.py
sudo python emake.py -i
运行上面两条指令,十秒内完成安装。emake 会拷贝自己到 /usr/local/bin 下面,后面直接使用 emake 指令操作。
Windows
下载 emake.py,放到你的 mingw 根目录下(便于 emake 定位 gcc),并且添加到 PATH 环境变量,同级目录新建立一个 emake.cmd 文件,内容如下:
@echo off
d:\dev\python311\python.exe d:\dev\mingw\emake.py %*
修改一下对应路径即可,建立这个 emake.cmd 的批处理文件是为了方便每次敲 emake 就可以工作,避免敲 "python emake.py" 一长串。
快速开始
假设你有三个文件:foo.c, bar.c, main.c 共同编译成名字为 main(.exe) 的可执行文件,我们创建 “main.mak” 文件:
; 指明目标格式:exe, lib, dll 三选一
mode: exe
; 加入源文件
src: foo.c
src: bar.c
src: main.c
是不是比 makefile, cmake 之类的步骤简单多了?编译项目:
emake main.mak
好了,工程顺利编译成功,每次任何一个文件发生变动,相关对其依赖的源文件都会重新编译,而无依赖的代码则不需要再次编译。
增加编译选项
如果需要增加编译选项的话:
; 指明目标格式:exe, lib, dll 三选一
mode: exe
; 编译选项
flag: -Wall, -O3, -g
; 加入源文件
src: foo.c
src: bar.c
src: main.c
如果项目中使用了数学库 libm.a的话:
link: m
如果还是用了 libstdc++.a 的话:
link: m, stdc++
或者:
link: m
link: stdc++
link 可以直接写 .a 库的文件名:
link: ./lib/libmylib.a
如果需要添加额外的 include 目录 和 lib 目录的话:
inc: /usr/local/opt/jdk/include
lib: /usr/local/opt/jdk/lib
还可以手动指定输出的文件名:
out: main
手动指定临时文件夹,避免临时 .o 文件污染当前目录的话:
int: objs
这样所有的临时文件就会跑到 objs 目录下面了,想要清理的话,删除 objs目录即可。
完整例子
; 指明目标格式:exe, lib, dll 三选一
mode: exe
; 编译选项
flag: -Wall, -O3, -g
; 设定链接
link: m, pthread, stdc++
; 额外头文件路径
inc: /usr/local/opt/jdk/include
inc: /usr/local/opt/jdk/include/linux
; 额外库文件路径
lib: /usr/local/opt/jdk/lib
; 加入源文件
src: foo.c
src: bar.c
src: main.c
零工程文件
写工程文件还是觉得累?没关系,上面所有工程配置都可以用 docstring 的方式写在源文件里,使用 //! 开头的注释中,比如 main.cpp 里:
#include <stdio.h>
//! mode: exe
//! flag: -Wall, -O3, -g
//! link: m, pthread
//! src: foo.c, bar.c
int main(void) {
...
return 0;
}
源文件中 //! 开头的特殊注释会被 emake 提取,作为工程配置的内容。这个例子基本揽括了上面独立工程文件做的事情,当然 src: 部分 main.cpp 是会被自动加入的,不用额外写,然后构建时:
emake main.cpp
即可,简单到了极点,特别适合验证一些小想法,做一些小实验,不用写一大堆乱七八糟的东西。
绝对路径
为了避免有时编译错误输出时,文件名用的相对路径,造成外部编辑器无法正确解析,可以用 --abs 参数:
emake --abs main.cpp
这样错误输出中的文件名,就是绝对路径了。
使用技巧
更多技巧见 “FAQ:常见问题集” 的 wiki 页面。
工程配置说明
Emake 的工程文件里面支持下面几种核心设置:
| 名称 | 含义 |
|-|-|
| src | 指定项目源文件,逗号分割,支持通配符 |
| inc | 指定 include 目录,逗号分割,支持相对路径(相对于工程文件)|
| lib | 指定库文件目录,格式同上 |
| link | 指定库文件,逗号分割,比如 lib: png 就会连接 libpng.a |
| flag | 指定编译通用选项,逗号分割 |
| cflag | 指定 C 语言的编译选项,逗号分割 |
| cxxflag | 指定 C++ 的编译选项,逗号分割 |
| define | 定义宏,格式为 define: USE_UTF8=1,可用逗号间隔多个宏 |
| mode | 设置目标格式:exe,lib, dll, win 几种 |
| out | 设置目标文件名 |
| int | 设置临时目录,放置 .o 文件和中间文件 |
| flnk | 连接时传入的参数,逗号分割 |
| wlnk | 连接时使用 -Wl, 前缀直接透传给 ld 的参数,逗号分割 |
还支持几种辅助配置:
| 名称 | 含义 |
|-|-|
| export | 当 mode 为 dll 时导出符号成 .lib 文件给 MSVC 用 |
| import | 从 emake.ini 配置的非 default 区导入配置 |
| package | 从 pkg-config 导入某个包配置,并设置 inc/lib 目录和 link 选项 |
| pcflag | 调用 pkg-config 时的参数 |
| echo | 输出文字 |
| color | 设置 echo 的颜色 |
| preload | 事件:加载前,后接 shell 命令 |
| prebuild | 事件:编译前,后接 shell 命令 |
| prelink | 事件:连接前,后接 shell 命令 |
| postbuild | 事件:构建后,即连接成功了就调用,后接 shell 命令 |
| environ | 设置环境变量 |
下面对其中几项略作说明。
添加源代码
用于声明项目里面的源文件,格式:
src: file1
src: file2
...
src: filen
或者:
src: file1, file2, file3
src: file4, file5, file6
也可以带通配符:
src: core/*.c
src: source/*.cpp
目录设置
声明项目中的 include 文件夹,相当于 gcc 的 -I 命令:
inc: dir1
inc: dir2
或者:
inc: dir1, dir2
和 src 一样可以使用逗号分隔;而 lib 选项用于设置库文件目录,相当于 gcc 的 -L 命令,格式和 inc 类似。
连接静态库
添加需要链接的库,相当于 gcc 的 -l 指令:
link: m, pthread, stdc++
或者:
link: m
link: pthread
link: stdc++
同时支持单行和多行模式,编译 C++ 项目别忘记链接 stdc++。
目标格式
目标文件的输出格式:
mode: [exe|lib|dll|win]
- exe: 生成可执行文件
- lib: 生成静态链接库
- dll: 生成动态链接库
- win: Windows 下特有,生成无 console 窗口的 Windows 程序。
临时目录
指定中间临时文件目录,一般设置为:
int: objs
或者:
int: objs/$(target)
指定了以后,可以避免项目中间文件污染当前目录。
条件编译
在 emake.ini 中可以指明一系列 name,比如:
[default]
name=android,posix,arm,nossl
每个名字代表一个条件,可以同时定义多个条件,然后在工程文件里使用:
<name>/option: value
来声明,只有 emake.ini 的 name 里包含特定内容时,才会触发后面内容,比如:
win32/link: pdcurses_wincon
linux/link: ncurses, tinfo
arm/src: arm_calculate.c
这两条语句代表不同平台 link 不同的库,name 可以不定义,它的默认值是 target,你可以只定义一个 target 不定义 name:
[default]
target=android
这样手工指明目标平台的名称,默认的话,会使用 python 里的 sys.platform 返回值。同时你又没有定义过 name 项目,那么 name 的默认值就是 target,除非你和上面一样手工定义 name,就能覆盖默认值。
所以 name 只有一个时,一般不定义,用默认值即可,但有多个 name 时需要定义下。
编译配置
在启动 emake 时可以设置一个 --profile 参数来指定 debug/release 等不同的构建配置:
python emake.py --profile=debug main.mak
而在工程文件里,用 @ 符号来指定某个配置是属于什么 profile 的,比如:
flag@debug: -g, -Og
flag@release: -O2
flag@minsize: -Os
flnk@minsize: -s
那么 emake 就会根据命令行传入的 profile 选择对应的配置项目,再比如,给不同配置规定不同的目标文件名:
win32/out: rogue-clone.exe
win32/out@debug: rogue-cloned.exe
linux/out: rogue-clone
linux/out@debug: rogue-cloned
这里分别演示了在不同平台下,不同 profile 可以指定不同的配置。
细粒度参数
Emake 支持为每个源代码文件设置不同的编译参数,格式是就是在 src 定义完源文件后,右边加冒号跟着参数即可:
src: main.c : -O3
src: foo.c, bar.c : -DTEST
上面第一行单独为 main.c 设置了 -O3 的参数,第二行为另外两个源文件定义了一个叫做 TEST 的宏。
事件机制
根据条件运行不同 shell 命令:
# 加载前
preload: echo "preload"
# 编译前
prebuild: echo "prebuild"
# 链接前
prelink: echo "prelink"
# 编译后
postbuild: echo "postbuild"
其中 preload 和 prebuild 之间的区别是,preload 每次都会运行,并且是在解析项目文件和分析依赖之前运行,而 prebuild 是在解析项目文件分析依赖之后运行,如果二进制没更新,prebuild 就不会被调用,而 preload 每次都无条件被执行。
比如某个源代码是使用其他工具生成的,放到 prebuild 时,应为处于依赖分析之后,初次构建尚未触发生成代码前面依赖分析就会报找不到文件了,但 preload 在依赖分析之前运行。
在事件运行时,可以从环境变量中读取一些信息:
$EMAKE_MAIN: 工程文件路径。$EMAKE_OUT: 输出文件路径。$EMAKE_MODE: 模式,可执行还是动态库。$EMAKE_SCRIPT: 脚本(emake.py)的路径。$EMAKE_HOME: 工程文件所在目录,即$EMAKE_MAIN的 dirname。$EMAKE_INT: 中间文件的目录。$EMAKE_TOOLCHAIN: 工具链的目录。$EMAKE_TARGET: 目标平台。
比如 Windows 下面:
postbuild: echo %EMAKE_OUT%
就能每次编译后显示输出文件的路径了。
最后这些事件命令会在工程文件所在的目录被启动。
工具链配置
Emake 支持多个工具链,每个工具链使用一个 ini 进行描述,不显示指定工具链的名字的话,会到下面几个位置寻找默认工具链配置:
1)在 emake.py 同级目录查找 emake.ini 。
2)/etc/emake.ini
3)/usr/local/etc/emake.ini
4)~/.config/emake.ini
默认工具链配置并不强制,没提供的话,Emake 也会到 $PATH 中寻找 gcc 等工具并自动设置。
如果想用别的工具链配置的话,使用 --cfg=name 参数就会加载 ~/.config/emake/{name}.ini :
emake --cfg=mingw64 <parameters>
这样它会去试图加载 ~/.config/emake/mingw64.ini,也可以用 --ini=path 直接给 ini 文件绝对路径:
emake --ini=/absolute/path/to/name.ini <parameters>
通常使用 --cfg=name,并在 ~/.config/emake 目录下统一管理所有配置,比如你有多套工具链,每个工具链一个配置放进去,用起来比较方便,特别是有交叉编译的情况时。
配置格式
工具链配置文件的内容类似:
[default]
# 工具链的 bin 目录,用于查找 gcc / clang 等工具
home=d:/msys32/mingw32/bin
# 当你有多套工具链时,不可能都加入 $PATH,这个配置可以让 emake 在
# 构建时临时追加到 $PATH 前面,不污染外层父进程的环境变量
path=d:/msys32/mingw32/bin,d:/msys32/usr/bin
# 设置传参模式,如果使用 clang,则取消下面注释
# option=x
# 通用配置,免得每个工程文件写一遍
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
def
Related Skills
node-connect
345.9kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
106.4kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
345.9kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
345.9kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
