前几天在玩CMake的时候想让CMake来生成pkgconfig的配置文件.pc文件,尝试了很久都没成功,借助DeepSeek也没搞出来。于是研究起了国产构建工具xmake,不用不知道,一用真觉得xmake比CMake好用多了,最主要是的语法简洁,没有CMake怪异语法和晦涩的生成表达式,xmake完全使用的Lua的语言规则。还有一个非常重要的工具就是VSCode有xmake插件,可以像CMake插件一样顺畅的工作。
一、xmake简介
1、简单示例

举一下简单的示例,创建一个xmake工程,xmake create hello:
$ xmake create hello
create hello ...
[+]: .gitignore
[+]: src\main.cpp
[+]: xmake.lua
create ok!
xmake.lua:
add_rules("mode.debug", "mode.release") -- 添加debug和release模式
target("hello") -- 目标为hello
set_kind("binary") -- 生成可执行文件,如果是static为静态库,shared为动态库
add_files("src/*.cpp") -- 添加要编译的源文件
日常项目中可能会有多目录的情况,比如一个目录为库,一个目录为依赖这个库的可执行文件:
顶层xmake.lua:
includes("mylib")
includes("app")
mylib的xmake.lua:
target("mylib")
set_kind("static")
add_files("*.c")
add_includedirs(".", {public = true})
app的xmake.lua:
target("mylib")
set_kind("binary")
add_files("*.c")
add_deps("mylib")
感觉入门非常容易。但是要想要在实战项目中得心应手,还需要深入学习很多东西。
2、xmake的目标时机回调函数简介
xmake的target有一系列的时机回调函数,比如on_XXX,before_XXX,after_XXX类的函数。它们会在不同的时机执行,比如大致有以下一些时机:
- 加载目标(on_load)
- 配置目标(on_config)
- 准备前(before_prepare, before_prepare_file, before_prepare_files)
- 准备(on_prepare, on_prepare_file, on_prepare_files)
- 准备后(after_prepare, after_prepare_file, after_prepare_files)
- 编译前(before_build, before_build_file, before_build_files)
- 编译时(on_build, on_build_file, on_build_files)
- 编译后(after_build, after_build_file, after_build_files)
- 链接前(before_link)
- 链接时(on_link)
- 链接后(after_link)
- 运行前(before_run)
- 运行(on_run)
- 运行后(after_run)
- 清理前(before_clean)
- 清理(on_clean)
- 清理后(after_clean)
- 打包前(before_package)
- 打包(on_package)
- 打包后(after_package)
- 安装前(before_install)
- 安装(on_install)
- 安装后(after_install)
- 卸载前(before_uninstall)
- 卸载(on_uninstall)
- 卸载后(after_uninstall)
其中以_file结尾的回调函数是处理单个文件的,_files结尾的回调函数是处理多个文件的,从命名上就可以看出来。
3、各阶段的时机顺序
- 配置期(xmake f)的顺序为1到2
- 构建期(xmake)的顺序为1到11
- 运行(xmake r)的顺序为1到14
- 清理(xmake c)的顺序为配置(1,2)+清理(15,16,17)
其它阶段类推,打包就是配置+打包,安装就配置+安装,卸载就是配置+卸载。
注意:这个顺序是没有设置预编译头时的一般顺序,如果设置了预编译头,则其优先级会非常高,会在编译前(before_build, before_build_file, before_build_files)编译预编译头。
4、xmake源文件列表的处理
在xmake中所有需要编译的源文件列表,一定要在编译(build)之前给出,如果是还没有生成的文件,可以使用always_added来告诉xmake暂时不管文件是否存在,比如描述域可以如下添加:
add_files("$(buildir)/test.pb.c", {always_added = true})
脚本域可以如下添加:
target:add("files", "$(buildir)/test.pb.c", {always_added = true})
如果在编译期的回调函数中添加源文件列表,是不会生效的。
5、自定义规则
如果有多个目标(target)都需要某个功能,比如处理proto文件,就可以使用
自定义规则(rule),让不同的目标(target)使用同样的规则来处理。自定义规则(rule)不仅有目标(target)各时机的所有回调函数,还额外添加了几个函数用于批处理:
- before_config, after_config
- before_buildcmd_file,before_buildcmd_files
- on_buildcmd_file,on_buildcmd_files
- after_buildcmd_file,after_buildcmd_files
二、使用内置规则"protobuf.c"集成C语言版protobuf
1、 使用官方的protoc-gen-c来生成
如果在项目中使用了protobuf,则可以使用内置的protobuf规则,有“protobuf.c”规则,也有“protobuf.cpp”规则。
app/xmake.lua:
add_requires("libprotobuf-c") -- 查找PBC的依赖库
target("app")
set_kind("binary")
add_rules("protobuf.c")
add_files("pb/*.proto", {proto_public = true}) -- 这里需要导出proto 的头文件搜索目录
add_packages("libprotobuf-c") -- 将PBC的依赖库添加到目标
但是“protobuf.c”是查找的是protoc-c程序,可以查看xmake/rules/protobuf/proto.lua源码:
-- get protoc
function _get_protoc(target, sourcekind)
-- find protoc
-- @see https://github.com/xmake-io/xmake/issues/4659
local envs = os.joinenvs(target:pkgenvs(), os.getenvs())
local protoc = target:data("protobuf.protoc")
if not protoc and sourcekind == "cxx" then
protoc = find_tool("protoc", {envs = envs})
if protoc and protoc.program then
target:data_set("protobuf.protoc", protoc.program)
end
end
-- find protoc-c
local protoc_c = target:data("protobuf.protoc-c")
if not protoc_c and sourcekind == "cc" then
protoc_c = find_tool("protoc-c", {envs = envs}) or protoc
if protoc_c and protoc_c.program then
target:data_set("protobuf.protoc-c", protoc_c.program)
end
end
-- get protoc
return assert(target:data(sourcekind == "cxx" and "protobuf.protoc" or "protobuf.protoc-c"), "protoc not found!")
end
在MinGW下会报错:
$ xmake
error: protoc not found!
protoc-c已经被弃用了,官方建议使用protoc程序,它会调用protoc-gen-c来生成C代码。从上面的源码可以看到在会先使用target:data(“protobuf.protoc-c”)获取生成C语言文件的protoc程序,如果没有设置则查找"protoc-c",找到了再使用target:data_set(“protobuf.protoc-c”, protoc_c.program)设置数据。我们可以手动设置这个数据,避免查找"protoc-c"。
app/xmake.lua:
add_requires("libprotobuf-c") -- 查找PBC的依赖库
target("app") -- 目标app
set_kind("binary") -- 可执行文件
add_rules("protobuf.c") -- 添加规则
add_files("pb/*.proto", {proto_public = true}) -- 添加proto文件,这里需要导出proto 的头文件搜索目录
add_files("*.c") -- 添加C源文件
add_packages("libprotobuf-c") -- 将PBC的依赖库添加到目标
on_load(function (target)
target:data_set("protobuf.protoc-c","protoc") -- 手动设置PB的C生成器
end)
这样就可以正常使用了,生成C++代码不用手动设置。xmake会先使用protobuf规则将proto文件生成C/C++源文件和头文件,再编译C/C++源文件。
2、使用自己编译的protoc-gen-c来生成
笔者曾经写过一篇博文:C语言使用Protobuf进行网络通信,里面介绍了如何定义宏来方便快捷的处理Protobuf的网络消息,最终为了方便宏处理,对protobuf-c库进行了一点小调整,也就是需要使用自己修改过源码生成的protoc-gen-c程序。假如自己编译生成的protoc-gen-c程序在app/pb目录中,则需要在处理proto文件前将app/pb目录的路径添加到PATH环境变量中去,才能优先使用自己的程序:
app/xmake.lua:
add_requires("libprotobuf-c") -- 查找PBC的依赖库
target("app") -- 目标app
set_kind("binary") -- 可执行文件
add_rules("protobuf.c") -- 添加规则
add_files("pb/*.proto", {proto_public = true}) -- 添加proto文件,这里需要导出proto 的头文件搜索目录
add_files("*.c") -- 添加C源文件
add_packages("libprotobuf-c") -- 将PBC的依赖库添加到目标
on_load(function (target)
target:data_set("protobuf.protoc-c","protoc") -- 手动设置PB的C生成器
-- 需要使用vformat手动展开变量$(scriptdir),构成绝对路径
local custom_dir = vformat("$(scriptdir)/pb")
-- 获取当前 PATH
local current_path = os.getenv("PATH") or ""
-- 根据操作系统选择路径分隔符
local sep = (os.host() == "windows") and ";" or ":"
-- 将自定义目录添加到 PATH 最前面(确保优先级最高)
local new_path = custom_dir .. sep .. current_path
-- 设置PATH变量
os.setenv("PATH", new_path)
end)
三、使用自定义规则来集成C语言版protobuf
1、新思路
C语言使用Protobuf进行网络通信中使用自己编译的protoc-gen-c程序来编译,需要自己不断维护源码,比如官方修改了源码,需要自己同步,最近两个版本的protobuf API发生了变化,protoc-gen-c官方都还没同步更新,自己维护的源码编译就会变得困难。其实需求非常简单,就是使用typedef添加一个原类型的别名,比如protoc-gen-c生成的结构为:
typedef struct Netmsg__PingPong Netmsg__PingPong;
我们根据生成的头文件,重新创建一个头文件,添加一个别名:
typedef Netmsg__PingPong netmsg__ping_pong;
只要有了这个别名,就可以使用简化的宏了。在xmake下完全能做到。
2、使用自定义规则处理proto文件
现在理一下有需要自己处理proto文件时,整个目标的构建过程及顺序:
- 配置目标
- 处理proto,生成C文件
- 根据生成的C头文件,按规则创建别名,并生成别名文件
- 编译链接所有源文件
由于项目可能会使用set_pcheader设置预编译头,预编译头中可能会包含proto生成的头文件,而生成预编译头会在编译所有源文件之前执行,所以所有on_build及后的时机都不行。只有on_build之前的时机可用。
xmake官方建议使用准备阶段的函数,比如:on_prepare(function (target, opt) end),它们是xmake 3.0 新增的函数,专门用于处理各种源码级的预处理、代码生成以及源码依赖分析,后续才会进入 build 阶段。对于有使用set_pcheader设置预编译头的目标,这个时机是非常好的,它在预编译头编译之前执行。
如果使用on_load(function (target)end)或者on_config(function (target)end)来处理,也可以,只是不能像on_prepare(function (target, opt) end)那样有整体的进度信息。但使用编译前(before_build, before_build_file, before_build_files)函数在有设置预编译头时不行,预编译头会先于这些函数执行。
下面给出完整的自定义规则代码:
rule("pbc")
set_extensions(".proto")
on_prepare_file(function(target, file, opt)
import("core.project.depend")
import("utils.progress")
local rel_dir = path.directory(file) -- app/pb
local output_dir = path.join(target:autogendir(), rel_dir) -- build/.gens/app/<plat>/<arch>/<mode>/app/pb
os.mkdir(output_dir) -- 先构建输出目录
local basename = path.basename(file) -- login
local prefix = path.join(output_dir,basename) -- -- build/.gens/app/<plat>/<arch>/<mode>/app/pb/login
local sourcefile = path.join(prefix .. ".pb-c.c") -- build/.gens/app/<plat>/<arch>/<mode>/app/pb/login.pb-c.c
-- 将生成的源文件添加到需要编译的文件列表
target:add("files", sourcefile)
-- 需要把生成的目录添加进目标的搜索目录,否则会找不到生成的头文件
target:add("includedirs", output_dir, {public = true})
-- 检查文件是否存在,如果不存在则需要生成;如果是重编也需要生成
local need_gen = target:is_rebuilt() or (not os.isfile(sourcefile))
-- 检查proto文件是否有修改,修改了也需要生成
depend.on_changed(function ()
need_gen = true
end, {files = file})
if need_gen then -- 需要生成
progress.show(opt.progress, "compiling.proto.c %s", file)
os.vrunv("protoc", {"-I".. rel_dir, "--c_out=" .. output_dir, path.filename(file)})
local headfile = path.join(prefix .. ".pb-c.h")
local alias_header = path.join(prefix .. ".pb-alias.h")
local content = io.readfile(headfile)
local aliases = {}
-- 驼峰写法变成蛇形写法
local function camel_to_snake(name)
name = name:gsub("(%u+)(%u%l)", "%1_%2")
name = name:gsub("(%l%d)(%u)", "%1_%2")
name = name:gsub("(%l)(%u)", "%1_%2")
return name:lower()
end
-- 匹配 protobuf-c:
-- typedef struct Netmsg__PingPong Netmsg__PingPong;
-- package 名称不固定
for type_name in content:gmatch("typedef%s+struct%s+([%w_]+__[%w_]+)%s+%1%s*;") do
local package, message = type_name:match("^(.+)__([%w_]+)$")
if package and message then
local alias = camel_to_snake(package) .. "__" .. camel_to_snake(message) -- 生成别名
aliases[#aliases + 1] = string.format("typedef %s %s;", type_name, alias) -- 添加到别名表
end
end
table.sort(aliases) -- 对所有别名排序
local output = string.format("#pragma once\n\n#include \"%s.pb-c.h\" // IWYU pragma: keep\n\n%s\n",
basename, table.concat(aliases, "\n")) -- 生成内容
io.writefile(alias_header, output) -- 写入文件
end
end)
使用很简单:
add_requires("libprotobuf-c")
target("app")
set_kind("binary")
add_rules("pbc")
add_files("pb/*.proto")
add_files("*.c")
add_packages("libprotobuf-c")
如本文对你有帮助,欢迎点赞收藏!

2万+

被折叠的 条评论
为什么被折叠?



