SkillAgentSearch skills...

Emake

你见过的最简单的 GCC/CLANG 项目构建工具,定义式构建,比命令式更简单

Install / Use

/learn @skywind3000/Emake
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

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-configvcpkg 和手工等三种方式导入第三方包。
  • 方便的交叉编译,轻松构建 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 简单在哪里?可以看这个介绍:

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

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 | 设置目标格式:exelib, 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"

其中 preloadprebuild 之间的区别是,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

View on GitHub
GitHub Stars914
CategoryDevelopment
Updated7h ago
Forks120

Languages

Python

Security Score

100/100

Audited on Apr 2, 2026

No findings