Xmake从入门到精通12:通过自定义脚本实现更灵活地配置

作者:waruqi(本文来自 xmake 开发者的投稿)

xmake 是一个基于 Lua 的轻量级现代化 c/c++ 的项目构建工具,主要特点是:语法简单易上手,提供更加可读的项目维护,实现跨平台行为一致的构建体验。

本文主要详细讲解下,如何通过添加自定义的脚本,在脚本域实现更加复杂灵活的定制。

  • 项目源码

  • 官方文档

配置分离

xmake.lua 采用二八原则实现了描述域、脚本域两层分离式配置。

什么是二八原则呢,简单来说,大部分项目的配置,80% 的情况下,都是些基础的常规配置,比如:add_cxflagsadd_links 等,
只有剩下不到 20% 的地方才需要额外做些复杂来满足一些特殊的配置需求。

而这剩余的 20% 的配置通常比较复杂,如果直接充斥在整个 xmake.lua 里面,会把整个项目的配置整个很混乱,非常不可读。

因此,xmake 通过描述域、脚本域两种不同的配置方式,来隔离 80% 的简单配置以及 20% 的复杂配置,使得整个 xmake.lua 看起来非常的清晰直观,可读性和可维护性都达到最佳。

描述域

对于刚入门的新手用户,或者仅仅是维护一些简单的小项目,通过完全在描述配置就已经完全满足需求了,那什么是描述域呢?它长这样:

target("test")
    set_kind("binary")
    add_files("src/*.c")
    add_defines("DEBUG")
    add_syslinks("pthread")

一眼望去,其实就是个 set_xxx/add_xxx 的配置集,对于新手,完全可以不把它当做 lua 脚本,仅仅作为普通的,但有一些基础规则的配置文件就行了。

这是不是看着更像配置文件了?其实描述域就是配置文件,类似像 json 等 key/values 的配置而已,所以即使完全不会 lua 的新手,也是能很快上手的。

而且,对于通常的项目,仅通过 set_xxx/add_xxx 去配置各种项目设置,已经完全满足需求了。

这也就是开头说的:80% 的情况下,可以用最简单的配置规则去简化项目的配置,提高可读性和可维护性,这样对用户和开发者都会非常的友好,也更加直观。

如果我们要针对不同平台,架构做一些条件判断怎么办?没关系,描述域除了基础配置,也是支持条件判断,以及 for 循环的:

target("test")
    set_kind("binary")
    add_files("src/*.c")
    add_defines("DEBUG")
    if is_plat("linux", "macosx") then
        add_links("pthread", "m", "dl")
    end
target("test")
    set_kind("binary")
    add_files("src/*.c")
    add_defines("DEBUG")
    for _, name in ipairs({"pthread", "m", "dl"}) do
        add_links(name)
    end

这是不是看着有点像 lua 了?虽说,平常可以把它当做普通配置问题,但是 xmake 毕竟基于 lua,所以描述域还是支持 lua 的基础语言特性的。

!> 不过需要注意的是,描述域虽然支持 lua 的脚本语法,但在描述域尽量不要写太复杂的 lua 脚本,比如一些耗时的函数调用和 for 循环

并且在描述域,主要目的是为了设置配置项,因此 xmake 并没有完全开放所有的模块接口,很多接口在描述域是被禁止调用的,
即使开放出来的一些可调用接口,也是完全只读的,不耗时的安全接口,比如:os.getenv() 等读取一些常规的系统信息,用于配置逻辑的控制。

!> 另外需要注意一点,xmake.lua 是会被多次解析的,用于在不同阶段解析不同的配置域:比如:option()target() 等域。

因此,不要想着在 xmake.lua 的描述域,写复杂的 lua 脚本,也不要在描述域调用 print 去显示信息,因为会被执行多遍,记住:会被执行多遍!!!

脚本域

限制描述域写复杂的 lua,各种 lua 模块和接口都用不了?怎么办?这个时候就是脚本域出场的时候了。

如果用户已经完全熟悉了 xmake 的描述域配置,并且感觉有些满足不了项目上的一些特殊配置维护了,那么我们可以在脚本域做更加复杂的配置逻辑:

target("test")
    set_kind("binary")
    add_files("src/*.c")
    on_load(function (target)
        if is_plat("linux", "macosx") then
            target:add("links", "pthread", "m", "dl")
        end
    end)
    after_build(function (target)
        import("core.project.config")
        local targetfile = target:targetfile()
        os.cp(targetfile, path.join(config.buildir(), path.filename(targetfile)))
        print("build %s", targetfile)
    end)

只要是类似:on_xxxafter_xxxbefore_xxx 等字样的 function body 内部的脚本,都属于脚本域。

在脚本域中,用户可以干任何事,xmake 提供了 import 接口可以导入 xmake 内置的各种 lua 模块,也可以导入用户提供的 lua 脚本。

我们可以在脚本域实现你想实现的任意功能,甚至写个独立项目出来都是可以的。

对于一些脚本片段,不是很臃肿的话,像上面这么内置写写就足够了,如果需要实现更加复杂的脚本,不想充斥在一个 xmake.lua 里面,可以把脚本分离到独立的 lua 文件中去维护。

例如:

target("test")
    set_kind("binary")
    add_files("src/*.c")
    on_load("modules.test.load")
    on_install("modules.test.install")

我们可以把自定义的脚本放置到 xmake.lua 对应目录下,modules/test/load.lua 和 modules/test/install.lua 中独立维护。

单独的 lua 脚本文件以 main 作为主入口,例如:

-- 我们也可以在此处导入一些内置模块或者自己的扩展模块来使用
import("core.project.config")
import("mymodule")

function main(target)
    if is_plat("linux", "macosx") then
        target:add("links", "pthread", "m", "dl")
    end
end

这些独立的 lua 脚本里面,我们还可以通过 import 导入各种内置模块和自定义模块进来使用,就跟平常写 lua, java 没啥区别。

而对于脚本的域的不同阶段,on_load 主要用于 target 加载时候,做一些动态化的配置,这里不像描述域,只会执行一遍哦!!!

其他阶段,还有很多,比如:on/after/before_build/install/package/run 等,我们下面会详细描述。

import

导入扩展模块

在讲解各个脚本域之前,我们先来简单介绍下 xmake 的模块导入和使用方式,xmake 采用 import 来引入其他的扩展模块,以及用户自己定义的模块,它可以在下面一些地方使用:

  • 自定义脚本 (on_build, on_run ..)

  • 插件开发

  • 模板开发

  • 平台扩展

  • 自定义任务 task

导入机制如下:

  1. 优先从当前脚本目录下导入

  2. 再从扩展类库中导入

导入的语法规则:

基于. 的类库路径规则,例如:

import("core.base.option")
import("core.base.task")

function main()

    -- 获取参数选项
    print(option.get("version"))

    -- 运行任务和插件
    task.run("hello")
end

导入当前目录下的自定义模块:

目录结构:

plugin
  - xmake.lua
  - main.lua
  - modules
    - hello1.lua
    - hello2.lua

在 main.lua 中导入 modules

import("modules.hello1")
import("modules.hello2")

导入后就可以直接使用里面的所有公有接口,私有接口用_前缀标示,表明不会被导出,不会被外部调用到。。

除了当前目录,我们还可以导入其他指定目录里面的类库,例如:

import("hello3", {rootdir = "/home/xxx/modules"})

为了防止命名冲突,导入后还可以指定的别名:

import("core.platform.platform", {alias = "p"})

function main()
    -- 这样我们就可以使用p来调用platform模块的plats接口,获取所有xmake支持的平台列表了
    print(p.plats())
end

2.1.5 版本新增两个新属性:import("xxx.xxx", {try = true, anonymous = true})

try 为 true,则导入的模块不存在的话,仅仅返回 nil,并不会抛异常后中断 xmake.
anonymous 为 true,则导入的模块不会引入当前作用域,仅仅在 import 接口返回导入的对象引用。

测试扩展模块

一种方式我们可以在 on_load 等脚本中,直接调用 print 去打印模块的调用结果信息,来测试和验证。

不过 xmake 还提供了 xmake lua 插件可以更加灵活方便的测试脚本。

运行指定的脚本文件

比如,我们可以直接指定 lua 脚本来加载运行,这对于想要快速测试一些接口模块,验证自己的某些思路,都是一个不错的方式。

我们先写个简单的 lua 脚本:

function main()
    print("hello xmake!")
end

然后直接运行它就行了:

$ xmake lua /tmp/test.lua
直接调用扩展模块

所有内置模块和扩展模块的接口,我们都可以通过 xmake lua 直接调用,例如:

$ xmake lua lib.detect.find_tool gcc

上面的命令,我们直接调用了 import("lib.detect.find_tool") 模块接口来快速执行。

运行交互命令 (REPL)

有时候在交互模式下,运行命令更加的方便测试和验证一些模块和 api,也更加的灵活,不需要再去额外写一个脚本文件来加载。

我们先看下,如何进入交互模式:

# 不带任何参数执行,就可以进入
$ xmake lua
>

# 进行表达式计算
> 1 + 2
3

# 赋值和打印变量值
> a = 1
> a
1

# 多行输入和执行
> for _, v in pairs({1, 2, 3}) do
>> print(v)
>> end
1
2
3

我们也能够通过 import 来导入扩展模块:

> task = import("core.project.task")
> task.run("hello")
hello xmake!

如果要中途取消多行输入,只需要输入字符:q 就行了

> for _, v in ipairs({1, 2}) do
>> print(v)
>> q             <--  取消多行输入,清空先前的输入数据
> 1 + 2
3

target:on_load

自定义目标加载脚本

在 target 初始化加载的时候,将会执行此脚本,在里面可以做一些动态的目标配置,实现更灵活的目标描述定义,例如:

target("test")
    on_load(function (target)
        target:add("defines", "DEBUG", "TEST=\"hello\"")
        target:add("linkdirs", "/usr/lib", "/usr/local/lib")
        target:add({includedirs = "/usr/include", "links" = "pthread"})
    end)

可以在 on_load 里面,通过 target:settarget:add 来动态添加各种 target 属性,所有描述域的 set_add_配置都可以通过这种方式动态配置。

另外,我们可以调用 target 的一些接口,获取和设置一些基础信息,比如:

target:on_link

自定义链接脚本

这个是在 v2.2.7 之后新加的接口,用于定制化处理 target 的链接过程。

target("test")
    on_link(function (target) 
        print("link it")
    end)

target:on_build

自定义编译脚本

覆盖 target 目标默认的构建行为,实现自定义的编译过程,一般情况下,并不需要这么做,除非确实需要做一些 xmake 默认没有提供的编译操作。

你可以通过下面的方式覆盖它,来自定义编译操作:

target("test")

    -- 设置自定义编译脚本
    on_build(function (target) 
        print("build it")
    end)

注:2.1.5 版本之后,所有 target 的自定义脚本都可以针对不同平台和架构,分别处理,例如:

target("test")
    on_build("iphoneos|arm*", function (target)
        print("build for iphoneos and arm")
    end)

其中如果第一个参数为字符串,那么就是指定这个脚本需要在哪个平台|架构下,才会被执行,并且支持模式匹配,例如 arm* 匹配所有 arm 架构。

当然也可以只设置平台,不设置架构,这样就是匹配指定平台下,执行脚本:

target("test")
    on_build("windows", function (target)
        print("build for windows")
    end)

注:一旦对这个 target 目标设置了自己的 build 过程,那么 xmake 默认的构建过程将不再被执行。

target:on_build_file

自定义编译脚本,实现单文件构建

通过此接口,可以用来 hook 指定 target 内置的构建过程,自己重新实现每个源文件编译过程:

target("test")
    set_kind("binary")
    add_files("src/*.c")
    on_build_file(function (target, sourcefile, opt)
    end)

target:on_build_files

自定义编译脚本,实现多文件构建

通过此接口,可以用来 hook 指定 target 内置的构建过程,替换一批同类型源文件编译过程:

target("test")
    set_kind("binary")
    add_files("src/*.c")
    on_build_files(function (target, sourcebatch, opt)
    end)

设置此接口后,对应源文件列表中文件,就不会出现在自定义的 target.on_build_file 了,因为这个是包含关系。

其中 sourcebatch 描述了这批同类型源文件:

  • sourcebatch.sourcekind: 获取这批源文件的类型,比如:cc, as, ..

  • sourcebatch.sourcefiles(): 获取源文件列表

  • sourcebatch.objectfiles(): 获取对象文件列表

  • sourcebatch.dependfiles(): 获取对应依赖文件列表,存有源文件中编译依赖信息,例如:xxx.d

target:on_clean

自定义清理脚本

覆盖 target 目标的 xmake [c|clean} 的清理操作,实现自定义清理过程。

target("test")

    -- 设置自定义清理脚本
    on_clean(function (target) 

        -- 仅删掉目标文件
        os.rm(target:targetfile())
    end)

target:on_package

自定义打包脚本

覆盖 target 目标的 xmake [p|package} 的打包操作,实现自定义打包过程,如果你想对指定 target 打包成自己想要的格式,可以通过这个接口自定义它。

target("demo")
    set_kind("shared")
    add_files("jni/*.c")
    on_package(function (target) 
        os.exec("./gradlew app:assembleDebug") 
    end)

当然这个例子有点老了,这里只是举例说明下用法而已,现在 xmake 提供了专门的 xmake-gradle 插件,来与 gradle 更好的集成。

target:on_install

自定义安装脚本

覆盖 target 目标的 xmake [i|install} 的安装操作,实现自定义安装过程。

例如,将生成的 apk 包,进行安装。

target("test")

    -- 设置自定义安装脚本,自动安装apk文件
    on_install(function (target) 

        -- 使用adb安装打包生成的apk文件
        os.run("adb install -r ./bin/Demo-debug.apk")
    end)

target:on_uninstall

自定义卸载脚本

覆盖 target 目标的 xmake [u|uninstall} 的卸载操作,实现自定义卸载过程。

target("test")
    on_uninstall(function (target) 
        ...
    end)

target:on_run

自定义运行脚本

覆盖 target 目标的 xmake [r|run} 的运行操作,实现自定义运行过程。

例如,运行安装好的 apk 程序:

target("test")

    -- 设置自定义运行脚本,自动运行安装好的app程序,并且自动获取设备输出信息
    on_run(function (target) 
        os.run("adb shell am start -n com.demo/com.demo.DemoTest")
        os.run("adb logcat")
    end)

before_xxx 和 after_xxx

需要注意的是,target:on_xxx 的所有接口都覆盖内部默认实现,通常我们并不需要完全复写,只是额外挂接自己的一些逻辑,那么可以使用 target:before_xxx 和 target:after_xxx 系列脚本就行了。

所有的 on_xxx 都有对应的 before_和 after_xx 版本,参数也完全一致,例如:

target("test")
    before_build(function (target)
        print("")
    end)

内置模块

在自定义脚本中,除了使用 import 接口导入各种扩展模块使用,xmake 还提供了很多基础的内置模块,比如:os,io 等基础操作,实现更加跨平台的处理系统接口。

os.cp

os.cp 的行为和 shell 中的 cp 命令类似,不过更加强大,不仅支持模式匹配(使用的是 lua 模式匹配),而且还确保目的路径递归目录创建、以及支持 xmake 的内置变量。

例如:

os.cp("$(scriptdir)/*.h", "$(buildir)/inc")
os.cp("$(projectdir)/src/test/**.h", "$(buildir)/inc")

上面的代码将:当前 xmake.lua 目录下的所有头文件、工程源码 test 目录下的头文件全部复制到 $(buildir) 输出目录中。

其中 $(scriptdir)$(projectdir) 这些变量是 xmake 的内置变量,具体详情见:内置变量的相关文档。

而 *.h 和 **.h 中的匹配模式,跟 add_files 中的类似,前者是单级目录匹配,后者是递归多级目录匹配。

上面的复制,会把所有文件全部展开复制到指定目录,丢失源目录层级,如果要按保持原有的目录结构复制,可以设置 rootdir 参数:

os.cp("src/**.h", "/tmp/", {rootdir = "src"})

上面的脚本可以按 src 根目录,将 src 下的所有子文件保持目录结构复制过去。

注:尽量使用 os.cp 接口,而不是 os.run("cp .."),这样更能保证平台一致性,实现跨平台构建描述。

os.run

此接口会安静运行原生 shell 命令,用于执行第三方的 shell 命令,但不会回显输出,仅仅在出错后,高亮输出错误信息。

此接口支持参数格式化、内置变量,例如:

-- 格式化参数传入
os.run("echo hello %s!", "xmake")

-- 列举构建目录文件
os.run("ls -l $(buildir)")
os.execv

此接口相比 os.run,在执行过程中还会回显输出,并且参数是通过列表方式传入,更加的灵活。

os.execv("echo", {"hello", "xmake!"})

另外,此接口还支持一个可选的参数,用于传递设置:重定向输出,执行环境变量设置,例如:

os.execv("echo", {"hello", "xmake!"}, {stdout = outfile, stderr = errfile, envs = {PATH = "xxx;xx", CFLAGS = "xx", curdir = "/tmp"}}

其中,stdout 和 stderr 参数用于传递重定向输出和错误输出,可以直接传入文件路径,也可以传入 io.open 打开的文件对象。

另外,如果想在这次执行中临时设置和改写一些环境变量,可以传递 envs 参数,里面的环境变量设置会替换已有的设置,但是不影响外层的执行环境,只影响当前命令。

我们也可以通过 os.getenvs() 接口获取当前所有的环境变量,然后改写部分后传入 envs 参数。

另外,还能通过 curdir 参数设置,在执行过程中修改子进程的工作目录。

其相关类似接口还有,os.runv, os.exec, os.execv, os.iorun, os.iorunv 等等,比如 os.iorun 可以获取运行的输出内容。

这块的具体详情和差异,还有更多 os 接口,都可以到:os 接口文档 查看。

io.readfile

此接口,从指定路径文件读取所有内容,我们可在不打开文件的情况下,直接读取整个文件的内容,更加的方便,例如:

local data = io.readfile("xxx.txt")
io.writefile

此接口写入所有内容到指定路径文件,我们可在不打开文件的情况下,直接写入整个文件的内容,更加的方便,例如:

io.writefile("xxx.txt", "all data")
path.join

此接口实现跨平台地路径拼接操作,将多个路径项进行追加拼接,由于 windows/unix 风格的路径差异,使用 api 来追加路径更加跨平台,例如:

print(path.join("$(tmpdir)", "dir1", "dir2", "file.txt"))

上述拼接在 unix 上相当于:$(tmpdir)/dir1/dir2/file.txt,而在 windows 上相当于:$(tmpdir)\\dir1\\dir2\\file.txt

更多内置模块详情见:内置模块文档

本文作者是 xmake 的开发者 waruqi,目前 xmake 已经在 Github 收获了 2.9K star、300+ fork,并且处理了 900+ issues、5400+ Commits。同时, xmake 在易用性上完全赶超 cmake,对新手非常友好,短时间内就能入门。

如果你想学习更多 xmake 和 C/C++ 的内容,可以扫码关注作者的公众号,学习更多内容~

入门教程

作者还在实验楼发布了一门 Xmake 入门和进阶课程,以边学边练习的方式快速学习 xmake 的使用,带你轻松构建 C/C++ 项目。

课程地址:https://www.lanqiao.cn/courses/2764

课程以循序渐进的方式,带你入门和进阶 xmake,从最基础的编译配置,到复杂项目的定制化组织和维护,在课程最后几节,我们还通过实战的方式,一步步带你体验第三方 C/C++ 项目的移植编译,以及 vscode/xmake 集成环境的可视化编译操作流程。最后一节实验中,我们还会讲解如何使用 xmake 和 vscode 去编译开发基于 Qt 的应用程序。

你将学到


???????????? 点击阅读原文,了解课程更多信息~

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值