Xmake 软件包描述规范文档
0. 软件包生命周期概览 (Package Lifecycle Overview)
Xmake 包描述脚本各钩子的执行顺序如下,理解此顺序是正确编写包的前提:
| 阶段 | 钩子 | 前置条件 | 用途 |
|---|---|---|---|
| 预检 | on_check | 无(最早校验阶段) | 判断平台/工具链是否受支持;失败时提前终止,便于跳过不支持的 CI 环境 |
| 加载 | on_load | 无(元数据阶段) | 动态添加 deps/patches/defines,修改包属性 |
| 探测 | on_fetch | 无 | 自定义系统库探测,返回 nil 则回退到安装流程 |
| 下载 | add_urls + add_versions | on_load 完成 | 源码下载与完整性校验 |
| 补丁 | add_patches | 源码已解压 | 构建前自动应用补丁 |
| 安装 | on_install | 源码已解压、所有依赖已安装 | 调用构建系统,将产物安装至 installdir |
| 测试 | on_test | on_install 完成 | 编译小段代码验证安装结果是否可用 |
关键区别:
on_load在下载之前执行,可以动态决定"需要什么";on_install执行时源码已解压、依赖已就绪,只负责"如何构建"。
1. 软件包标识与元数据 (Package Identification and Metadata)
1.1 命名规范 (Naming Conventions)
1.1.1 软件包名称应统一使用小写,可包含数字、中划线(-)和下划线(_);严禁使用大写或驼峰命名法。
1.1.2 若上游项目名已带 - 或 _,建议沿用;若无明确约定,可任选其一,或参考主流包管理器命名保持一致。
1.1.3 API:
lua
package("name")1.2 set/add 接口语义 (set/add Semantics)
1.2.1 一般语义上,set_xxx 表示覆盖(重置)该字段,add_xxx 表示追加。
1.2.2 维护已有规则时通常优先使用 add_xxx,避免误覆盖已有条目;只有确实需要重置全量字段时再使用 set_xxx。
1.3 描述与属性 (Description and Attributes)
1.3.1 set_homepage:必须提供有效的项目官网或 GitHub 主页。
1.3.2 set_description:简短描述软件包功能。
1.3.3 set_license:必须指定许可证类型(如 MIT、Apache-2.0、BSD-3-Clause),如果实在找不到可以不填。
1.3.4 set_kind:默认为 library。纯头文件库必须显式声明:
lua
set_kind("library", {headeronly = true})1.3.5 非库类型包可显式声明:
lua
set_kind("binary") -- 可执行工具包
set_kind("toolchain") -- 工具链包1.3.6 包重命名兼容:若历史包名需平滑迁移到新包名,可通过 set_base("newpkg") 复用新包脚本,并在 on_load 打印迁移提示。该方式建议仅用于兼容过渡,不宜长期保留多个同义包。
1.3.7 需要按包类型分支时,建议使用 package:is_library()、package:is_binary()、package:is_toolchain() 读取当前 kind,比手写字符串判断更直观:
lua
on_load(function(package)
if package:is_binary() then
package:config_set("tools", true)
elseif package:is_library() then
package:add("defines", "FOO_STATIC")
end
end)1.3.8 工具链包或二进制分发包可通过 set_installtips(...) 给出许可证确认、手动下载步骤或环境前置条件提示,减少用户安装阶段误用:
lua
set_installtips("This package requires manual EULA acceptance before first use.")1.3.9 除 is_binary/is_library/is_toolchain 外,也可用 package:kind() 直接读取当前包类型字符串;新增脚本优先使用语义化布尔接口,kind() 适合需要做字符串拼接或透传上游参数的场景。
2. 源码获取与版本控制 (Source Acquisition and Versioning)
2.1 源码 URL 定义 (Source URLs)
2.1.1 必须提供至少一个稳定的源码下载地址,优先使用官方 Release 压缩包(tar.gz/tar.xz/tar.bz2/zip)。
2.1.2 建议提供 Git 仓库作为备选源,以便在压缩包下载失败或需要特定 commit 时自动回退:
lua
add_urls("https://github.com/user/repo/archive/refs/tags/$(version).tar.gz",
"https://github.com/user/repo.git")2.1.3 默认拉取 Git 子模块,如果不需要,在 URL 配置中关闭:
lua
add_urls("https://github.com/user/repo.git", {submodules = false})2.1.4 当同时提供多个来源(如 release 压缩包、github:/bitbucket: 简写源、git 仓库)时,建议为需要独立版本映射的来源设置 alias,并在 add_versions 中用 <alias>:<version> 绑定该来源(alias 不限于 git 源):
lua
add_urls("https://github.com/user/repo/archive/refs/tags/$(version).tar.gz")
add_urls("github:user/repo.git", {alias = "github"})
add_urls("bitbucket:user/repo.git", {alias = "bitbucket"})
add_versions("1.1.9", "sha256...")
add_versions("github:1.1.9", "ver.1.1.9")
add_versions("bitbucket:1.1.9", "ver.1.1.9")2.1.5 URL 字段中,set_urls 为覆盖(重置)整个 URL 列表,add_urls 为追加。通常优先使用 add_urls,确需重置时再用 set_urls:
lua
set_urls("https://github.com/user/repo.git") -- 覆盖 URL 列表
add_urls("https://mirror.example.com/repo.git") -- 追加镜像2.1.6 若需按平台或构建形态动态切换源码来源(如 Windows 使用预编译压缩包、其他平台走源码构建),可在 on_source 中动态设置 urls/versions。为兼容旧版 Xmake,建议使用 if on_source then ... else ... 保护:
lua
if on_source then
on_source(function (package)
if package:is_plat("windows") then
package:set("urls", "https://example.com/prebuilt-$(version).zip")
else
package:set("urls", "https://example.com/source-$(version).tar.xz")
end
end)
else
set_urls("https://example.com/source-$(version).tar.xz")
end2.1.7 若源码压缩包包含明显无关的大体积目录(如网页文档、示例站点资源),或包含当前平台不支持/不应解压的文件,可在 add_urls 里用 excludes 过滤,减少解压体积与 CI I/O 开销并避免无效文件进入源码树:
lua
add_urls("https://github.com/user/repo/archive/refs/tags/$(version).zip", {
excludes = {"*/html/*", "*/docs/site/*"}
})2.1.8 若同一包存在多种安装方案(如预编译与源码构建),可通过 add_schemes(...) 声明方案,并在 on_source/on_install 中读取 package:current_scheme() 切换逻辑;兼容旧版时可回退到 package:data("scheme"):
lua
add_schemes("binary", "source")
on_install(function (package)
local scheme = package:current_scheme() or package:data("scheme")
if scheme == "binary" then
-- install prebuilt artifacts
else
-- build from source
end
end)2.2 版本校验与映射 (Versions and Hashing)
2.2.1 每个压缩包版本必须对应一个 SHA-256 校验码;Git 源版本可绑定完整 40 位 Commit Hash 或 tag 名称(两者都依赖上游仓库可用性):
lua
add_versions("v1.0.0", "abc123...sha256-64chars")
add_versions("git:v1.0.0", "full-40-char-commit-hash")
add_versions("git:1.1.9", "ver.1.1.9")2.2.2 非标准版本号映射:若上游 tag 格式与语义化版本号不符(如 jun2023 对应 2023.06),须在 add_urls 中传入映射函数:
lua
local tag = {["2023.06"] = "jun2023"}
add_urls("https://.../$(version).tar.gz", {
version = function(version) return tag[tostring(version)] end
})2.2.3 无 Release 软件包:使用日期作为版本号(如 2024.01.01),绑定对应的完整 commit hash:
lua
add_versions("2024.01.01", "full-40-char-commit-hash")2.2.4 若只提供 git 源(没有压缩包源),add_versions 可直接写版本号与 commit/tag 绑定,无需使用 git: 前缀:
lua
add_urls("https://github.com/user/repo.git")
add_versions("2025.03.02", "full-40-char-commit-hash")2.2.5 若版本列表较长,可拆到独立文件维护(如 versions.txt/versions.lua),在包脚本中通过 add_versionfiles(...) 引入;兼容旧版时可回退到 add_versions_list() 等旧接口。
2.2.6 如需区分“按 release 版本下载”与“按 git 引用(branch/tag/commit)下载”的逻辑分支,可用 package:gitref() 做条件判断(常用于上游目录结构或 CMake 逻辑在 git 版本与 release 包之间不一致的场景)。
2.2.7 在 on_source 中可通过 package:requireinfo().version 读取用户请求版本(必要时可重写),用于处理“复合版本字符串”拆分、来源映射或版本别名归一化。
2.2.8 package:get("versions") / package:set("versions", ...) 更偏历史用法;新增脚本通常不建议动态改写整张版本表。读取当前选中版本建议优先使用 package:version()(或字符串化的 package:version_str()),必要时再配合 2.2.7 的 requireinfo().version 做来源映射。
lua
on_load(function (package)
local ver = package:version()
if ver then
-- branch by selected version
end
end)2.3 本地源码目录 (Local Source Directory)
2.3.1 若软件包源码来自本地路径(调试或私有包),使用 set_sourcedir 替代 add_urls:
lua
set_sourcedir(path.join(os.scriptdir(), "src"))2.3.2 使用 set_sourcedir 时无需 add_versions,Xmake 不会执行下载流程,如果使用了 package:version() 就会报错。
2.4 附加资源 (Extra Resources)
2.4.1 当上游构建缺少必要辅助文件(如额外 CMake 脚本、config.guess/config.sub、第三方子仓库)时,建议使用 add_resources 单独拉取附加资源,避免把这类文件混入主源码补丁:
lua
add_resources(">=1.0.26", "libusb-cmake",
"https://github.com/libusb/libusb-cmake.git",
"8f0b4a38fc3eefa2b26a99dff89e1c12bf37afd4")2.4.2 在 on_install 中通过 package:resourcefile(name) 或 package:resourcedir(name) 访问附加资源;资源版本表达式可与 add_patches 一样使用单版本/范围/通配(如 *),也可使用 2.x 这类主版本通配写法。
2.4.3 附加资源除顶层 add_resources(...) 外,也可在 on_load 按版本/配置动态追加 package:add("resources", ...),用于延迟决定资源来源。
3. 依赖管理 (Dependency Management)
3.1 构建依赖与运行依赖 (Build and Runtime Dependencies)
3.1.1 add_deps:声明构建所需工具(如 cmake、ninja)或链接所需库(如 zlib)。
3.1.2 依赖的 includedirs、links 等属性默认向下游传递。若不希望传递(如仅构建期使用的工具库),设置 private = true:
lua
add_deps("zlib") -- 传递给下游
add_deps("libcodegen", {private = true}) -- 不传递,仅构建期使用3.1.3 依赖可携带版本约束与配置约束(版本表达式常见写法如 >=、<=、^、x 通配):
lua
add_deps("nasm >=2.13", {kind = "binary"})
add_deps("xtl ^0.8.0")
add_deps("python 3.x", {kind = "binary"})
add_deps("lcms 2.x")
add_deps("zlib", {configs = {shared = false}})3.1.4 编译工具隔离:cmake、ninja 等工具依赖的 bin 目录仅在 on_install 阶段可见,不会污染用户系统 PATH。
3.1.5 依赖版本可与当前包版本联动(如同仓库子包保持同版本下限),可在 on_load 中用 package:version_str() 组合约束字符串再 package:add("deps", ...)。
3.1.6 在 on_load 动态追加依赖时,也可用参数表传入版本约束;例如:
lua
on_load(function (package)
package:add("deps", "zlib", {version = ">1.0.0"})
end)3.1.7 可将依赖声明为可选(optional = true)用于“有则启用、无则降级”的软依赖场景;常用于大型包对压缩/加速后端的可选接入。
lua
add_deps("zlib", "zstd", {optional = true})3.1.8 需要遍历“库依赖”做 include/link 注入时,可使用 package:librarydeps()(支持 {private = true})与 package:orderdeps() 分工:前者偏“库依赖集合”,后者偏“有序依赖链”。
3.1.9 仅需遍历直接依赖(不展开完整依赖图)时,可使用 package:plaindeps();常用于模板/聚合包在 on_fetch 做轻量探测。
3.2 外部源联动 (External Sources)
3.2.1 若主流发行版包管理器中已有该包,建议通过 add_extsources 关联系统包管理器;若无可用系统包可省略。探测成功时将跳过下载与安装流程。extsources 不仅支持 apt/pacman/brew,也支持 pkgconfig::foo 这类系统探测入口。
lua
add_extsources("pkgconfig::libxml-2.0", "apt::libfoo-dev", "pacman::foo", "brew::foo")3.2.2 当系统包名依赖平台/组件配置时,可在 on_load 中动态追加 extsources(package:add("extsources", ...)),按启用组件精细映射发行版包名。
4. 构建配置与环境预处理 (Configuration and Pre-processing)
4.1 用户配置项 (User Options)
4.1.1 add_configs:提供自定义编译开关。内置保留配置项有 shared、static、pic、lto、vs_runtime、debug。通常不需要重复定义;仅在需要设置 readonly 或重写描述时再显式定义。
支持的 type 值及示例:
lua
-- boolean
add_configs("tools", {description = "Build tools.", default = false, type = "boolean"})
add_configs("minimal", {description = "Build a minimal version.", default = true, type = "boolean"})
-- string(可选 values 限定枚举)
add_configs("endian", {
description = [[Byte order: "little" or "big". Leave nil for arch default.]],
default = nil, type = "string", values = {"little", "big"}
})
-- table(多选列表)
add_configs("modules", {
description = [[Enable modules, e.g. {configs = {modules = {"zlib", "lzma"}}}]],
type = "table"
})4.1.2 只读选项:若软件包不支持某种模式(如不支持静态编译),必须将该选项标记为 readonly:
lua
add_configs("shared", {description = "Build shared library.", default = true, readonly = true})4.1.3 MSVC 运行时:Xmake 默认会向 CMake 传递 CMAKE_MSVC_RUNTIME_LIBRARY,通常无需手动拼接该参数。若需要按运行时做条件分支,使用 package:has_runtime("MD", "MT") 判断即可;若上游 CMake 显式硬编码了运行时选项,建议用 io.replace 删除上游强制设置,避免覆盖 Xmake 默认传递值:
lua
if package:has_runtime("MD", "MT") then
-- runtime-related branching when needed
end
io.replace("CMakeLists.txt", "set(CMAKE_MSVC_RUNTIME_LIBRARY \"MultiThreaded\")", "", {plain = true})
io.replace("CMakeLists.txt", "set(CMAKE_MSVC_RUNTIME_LIBRARY \"MultiThreadedDLL\")", "", {plain = true})4.1.4 动态修改 kind:package:set("kind", ...) 在 on_load 中虽可用,但当前存在已知行为问题(可能导致 headeronly/非 headeronly 形态处理异常)。除非必要不建议使用;如必须使用,请在注释中说明原因并参考:
https://github.com/xmake-io/xmake/issues/5807#issuecomment-2467654245
4.1.5 配置联动与约束:可在 on_load 用 package:config_set(...) 推导默认配置或对上游限制做强制收敛(例如某版本仅支持静态库)。若会覆盖用户传入配置,建议同步 wprint 给出原因提示。
4.1.6 后端选择类配置可使用 values 做枚举约束,且可包含 false 与字符串混合值;该写法可不显式声明 type,用于“关闭/后端 A/后端 B”三态切换:
lua
add_configs("openssl", {
description = "Enable PKCS7 signatures support",
default = "openssl3",
values = {false, "openssl", "openssl3"}
})4.1.7 批量映射配置到上游构建参数时,可遍历 package:configs(),并用 package:extraconf("configs", name, "builtin") 过滤内置配置(如 debug、shared);这是常用语法糖,可避免误把内置项当成业务开关传给上游构建系统:
lua
for name, enabled in pairs(package:configs()) do
if not package:extraconf("configs", name, "builtin") then
table.insert(configs, "-D" .. name:upper() .. "=" .. (enabled and "ON" or "OFF"))
end
end4.1.8 处理 MSVC 运行时时,除 has_runtime(...) 外,也可直接读取 package:runtimes()(如 MT/MD)并透传上游参数;新脚本建议保持写法一致,避免同包混用多套 runtime 分支风格。
4.2 环境导出 (Environment Export)
4.2.1 on_load:在下载源码之前执行,用于根据配置动态决定包的依赖、补丁和属性。典型用途:
- 条件性
add_deps(如按配置决定是否依赖 openssl) - 导出宏定义供下游 target 引用
- 按平台注入不同的系统链接库
lua
on_load(function(package)
if package:config("with_ssl") then
package:add("defines", "FOO_WITH_SSL=1")
package:add("deps", "openssl")
end
end)4.2.2 deps 具有阶段约束:只能通过顶层 add_deps(...) 或 on_load 阶段的 package:add("deps", ...) 添加;不要在 on_install 中添加 deps。
对 defines、syslinks 等属性,仍推荐优先放在 on_load,以保持元数据与安装逻辑分离、提高可读性。
4.2.3 平台差异链接:Linux 下的 pthread 或 Windows 下的系统库,必须根据 is_plat 动态注入 syslinks:
lua
on_load(function(package)
if package:is_plat("linux") then
package:add("syslinks", "pthread", "dl")
elseif package:is_plat("windows") then
package:add("syslinks", "ws2_32", "advapi32")
end
end)4.2.4 运行时环境变量扩展:需要导出 PYTHONPATH 等路径型变量时,建议配合 package:mark_as_pathenv("PYTHONPATH")。mark_as_pathenv 仅应在 on_load 阶段调用。若安装期计算结果(如最终安装路径)需在后续阶段复用,可用 package:data_set("k", v) 与 package:data("k") 在包生命周期内传递数据。
4.2.5 工具链/二进制包若需固定导出 *_ROOT 等环境变量,可在 on_load 使用 package:setenv("KEY", value);路径型变量仍建议配合 mark_as_pathenv。
5. 构建与安装生命周期 (Installation Lifecycle)
5.1 构建系统抽象 (Build System Abstraction)
5.1.1 严禁在脚本中硬编码执行编译命令(如 os.run("make"))。必须使用 Xmake 提供的工具模块:
| 上游构建系统 | 推荐 API |
|---|---|
| CMake | import("package.tools.cmake").install(package, configs) |
| Meson | import("package.tools.meson").install(package, configs) |
| Autoconf | import("package.tools.autoconf").install(package, configs) |
| Xmake | import("package.tools.xmake").install(package, configs) |
| Make | import("package.tools.make").install(package, configs) |
| Nmake | import("package.tools.nmake").install(package, configs) |
5.1.2 现存少量历史包仍直接使用 os.vrun(v) 调上游构建命令,属于遗留问题,不作为放宽规范的依据。后续维护触及时应优先迁移到 package.tools.*。
5.1.3 对暂未迁移完成的遗留脚本(例如手动调用 configure/make),至少应通过 package:build_getenv(...) 透传编译器与 flags,避免硬编码工具链导致交叉编译/宿主环境污染。
5.1.4 使用 Port 脚本或安装阶段需要拷贝包内辅助文件(如 port/xmake.lua、.def、模板文件)时,建议通过 package:scriptdir() 定位包脚本目录,避免依赖当前工作目录:
lua
os.cp(path.join(package:scriptdir(), "port", "xmake.lua"), "xmake.lua")5.1.5 若需从临时构建目录拷贝中间产物(如 .pdb)到安装目录,建议使用 package:builddir() 获取构建目录根路径;历史脚本中的 package:buildir() 归为旧接口写法:
lua
os.trycp(path.join(package:builddir(), "foo/**.pdb"), package:installdir("bin"))5.1.6 单文件下载包(如仅下载一个 .h/.exe)可在安装阶段通过 package:originfile() 获取原始下载文件路径,再手动拷贝到目标目录。
5.1.7 如需在安装阶段生成或覆盖临时构建文件,建议使用 package:cachedir() 定位解包缓存目录,避免污染脚本目录或仓库文件。
5.1.8 平台分流转发包(如 macOS 走系统库、其他平台转发到第三方依赖)可保留最小 on_install 做分流与安装期适配,不应简单删空安装钩子。
5.2 构建参数优化 (Optimization of Build Parameters)
5.2.1 对需要实际编译产物的包(非纯头文件、非纯预编译搬运),必须显式映射 Debug/Release 模式:
lua
table.insert(configs, "-DCMAKE_BUILD_TYPE=" .. (package:is_debug() and "Debug" or "Release"))5.2.2 对需要实际编译产物的包(非纯头文件、非纯预编译搬运),必须显式映射 shared/static。若上游使用标准开关可直接映射 BUILD_SHARED_LIBS;若上游使用自定义变量(如 BUILD_STATIC、ZSTD_BUILD_SHARED),需按其接口主动适配:
lua
table.insert(configs, "-DBUILD_SHARED_LIBS=" .. (package:config("shared") and "ON" or "OFF"))5.2.3 Windows 符号全导出:构建动态库且上游 CMake 未处理 __declspec(dllexport),须注入:
lua
if package:is_plat("windows") and package:config("shared") then
table.insert(configs, "-DCMAKE_WINDOWS_EXPORT_ALL_SYMBOLS=ON")
end5.2.4 禁用 Test/Examples 编译: 若上游无关闭开关,使用 io.replace 注释掉相关 add_subdirectory:
lua
io.replace("CMakeLists.txt", "add_subdirectory(tests)", "", {plain = true})
io.replace("CMakeLists.txt", "add_subdirectory(examples)", "", {plain = true})5.2.5 若上游构建系统强制启用 /WX 或 -Werror(将告警视为错误),必须使用 io.replace 删除该强制选项,避免因编译器差异导致构建失败:
lua
io.replace("CMakeLists.txt", "/WX", "", {plain = true})
io.replace("CMakeLists.txt", "-Werror", "", {plain = true})5.2.6 Windows 产物一致性:适配上游 shared/static 选项时,应确保产物形态与配置一致——shared=true 时生成 .dll(通常伴随导入 .lib),shared=false 时生成静态 .lib(或 .a)。
5.2.7 若上游构建脚本未正确处理依赖的级联链接(常见于部分 autoconf/meson 项目),可遍历 package:orderdeps() 并通过 dep:fetch() 组装 cflags/cppflags/ldflags(或 c_link_args)显式注入,避免缺符号/缺头文件。
5.2.8 使用 package.tools.* 安装/配置时,若需要将依赖包的构建信息显式注入上游构建系统,可通过选项表传入 packagedeps(支持字符串或数组)。其本质是通过 cxflags/shflags 等参数把依赖信息直接塞入构建过程。
该方式应作为最后手段:优先尝试修补上游构建脚本(如 CMakeLists.txt、meson.build、configure.ac);仅在修补无果或维护成本过高时再使用 packagedeps。
lua
import("package.tools.cmake").install(package, configs, {packagedeps = {"libogg", "xxhash"}})
import("package.tools.autoconf").install(package, configs, {packagedeps = "libiconv"})5.2.9 package:debug() 属历史接口,新增或重构脚本建议统一使用 package:is_debug();维护遗留包时可逐步迁移:
lua
table.insert(configs, "-DCMAKE_BUILD_TYPE=" .. (package:is_debug() and "Debug" or "Release"))5.3 补丁管理 (Patch Management)
5.3.1 通过 add_patches 在源码解压后、构建前自动应用补丁。版本选择支持单版本、通配(*)、单侧范围和区间表达式(可写 &&,也可直接空格并列):
lua
add_patches("1.0.0", "patches/1.0.0/fix-windows.patch", "sha256-of-patch-file")
add_patches("*", "patches/common/fix-clang.patch", "sha256-of-patch-file")
add_patches(">=3.2.4", "patches/common/fix-cxx17.patch", "sha256-of-patch-file")
add_patches(">=5.3.0 <=5.8.0", "patches/common/fix-cmake.patch", "sha256-of-patch-file")
add_patches(">=2.57.3 <2.57.6", "patches/common/fix-headers.patch", "sha256-of-patch-file")5.3.2 补丁文件建议优先存放于包目录的 patches/<version>/ 子目录并纳入版本控制;若同一补丁需跨多个版本复用,可放在公共路径并由多个 add_patches 复用。
5.3.3 策略建议:
- 结构性 bug 修复(改动边界稳定、希望保留可审计 diff)→ 建议使用
.patch/.diff文件(便于向上游提 PR) - 临时侵入性修改(删除测试、修改安装路径)→ 建议使用
io.replace(更直观,无需维护 diff 上下文)
5.3.4 若修改逻辑关键且通常可跨多个上游版本复用,可优先采用 io.replace(或等价文本修补脚本)。原因是 add_patches 依赖版本号范围匹配,在自动更新 CI 场景下维护成本更高。
5.3.5 add_patches 支持本地补丁文件与远程补丁 URL(含较大补丁文件场景)。关键修复建议尽量在仓库内保留本地补丁副本,降低上游链接失效风险。
5.3.6 同一版本若声明多个 add_patches,应用顺序不作保证;脚本逻辑不得依赖“先打 A 再打 B”。若两个修改存在顺序耦合,建议合并为单个补丁,或改用 io.replace/脚本化修补消除顺序依赖。
5.3.7 需要按工具链版本条件启用补丁时,建议在 on_load 动态追加 patches,并将版本约束与平台/工具链条件拆分表达(例如“包版本是 v2.1.0 且 Android NDK 为 r27”):
lua
on_load(function (package)
if package:is_plat("android") then
local ndk = package:toolchain("ndk")
local ndkver = ndk and ndk:config("ndkver")
if ndkver and tonumber(ndkver) == 27 then
package:add("patches", "v2.1.0", "patches/v2.1.0/fix-r27.diff", "sha256...")
end
end
end)5.3.8 补丁文件编码与行尾统一要求:UTF-8(无 BOM)+ LF。修改补丁内容或行尾后必须重新计算并更新 add_patches(..., sha256),避免跨平台哈希漂移。
6. 系统库探测 (System Library Detection)
6.1 on_fetch 自定义探测 (Custom Fetch Logic)
6.1.1 当 add_extsources 的自动探测不足以覆盖复杂场景时,使用 on_fetch 实现自定义探测逻辑。返回 nil 则自动回退到 on_install 流程:
lua
on_fetch(function(package, opt)
if opt.system then
local result = {}
result.includedirs = {"/usr/include/foo"}
result.libfiles = {"/usr/lib/libfoo.a"}
return result
end
end)6.1.2 on_fetch 返回 table 支持的字段:includedirs、linkdirs、links、libfiles、defines。
6.1.3 pkg-config 联动:可使用内置 find_package 辅助函数简化探测。Xmake 已预先配置好 find_package 和 pkg-config 的搜索路径,通常无需手动传递路径参数:
lua
on_fetch(function(package, opt)
if opt.system then
return package:find_package("pkgconfig::foo", opt)
end
end)6.1.4 版本约束探测:系统库版本不满足要求时返回 nil 强制走安装流程:
lua
on_fetch(function(package, opt)
if opt.system then
local result = package:find_package("pkgconfig::foo", opt)
if result and result.version and semver.satisfies(result.version, ">=1.2.0") then
return result
end
end
end)6.1.5 复杂探测逻辑可拆到独立脚本文件复用(例如 on_fetch("fetch"));同样的“脚本拆分”也适用于 on_install、on_test 等其他钩子,不是 on_fetch 专属能力。
6.1.6 on_fetch 除 find_package(...) 外,也可用 package:find_tool(...) 探测系统工具并返回探测结果。
6.1.7 on_fetch 返回语义建议区分:nil 表示继续 fallback 到安装流程,false 可用于显式阻止 fallback(例如已知某些探测副作用/卡死场景时提前终止)。
lua
on_fetch(function (package, opt)
if opt.system then
if should_abort_fetch() then
return false -- stop fallback install
end
return nil -- continue fallback install
end
end)6.2 on_check 早期约束校验 (Early Constraint Checks)
6.2.1 on_check 是最早执行的校验阶段之一,适合做“是否允许继续构建”的前置判断(如 CI 不支持的平台、工具链版本不满足)。校验失败应尽早 assert 终止,避免浪费后续下载与构建时间。
lua
on_check("android", function(package)
local ndkver = package:toolchain("ndk"):config("ndkver")
assert(ndkver and tonumber(ndkver) > 22, "need ndk > 22")
end)6.2.2 推荐在 on_check 中只做环境可用性判定,不做源码修改与安装动作。
6.2.3 为兼容旧版 Xmake,可在调用前做存在性判断:
lua
if on_check then
on_check("android", function(package)
-- ...
end)
end7. 特殊软件包类型处理 (Special Package Handling)
7.1 Xmake Port (原生重写构建)
7.1.1 当上游构建系统失效或过于复杂时,推荐使用 Xmake Port。常见方式包括:
- 包目录维护固定
port/xmake.lua,在on_install中拷贝到源码根目录后安装; - 在
on_install中按版本/平台动态io.writefile("xmake.lua", ...)生成构建脚本; - 直接在包脚本中组织最小构建逻辑并调用 Xmake 工具模块安装。
7.1.2 动态库的符号导出处理策略:
- 推荐(库源码较小时): 修改源码添加平台符号修饰(
__declspec(dllexport)/__attribute__((visibility("default")))),行为明确可控。 - 备选(库源码较大,修改成本高): 使用
utils.symbols.export_all,其底层依赖objdump/dumpbin等工具扫描目标文件导出符号,结果不稳定,仅在无更好选择时使用:
lua
if is_plat("windows") and is_kind("shared") then
add_rules("utils.symbols.export_all")
end7.1.3 若上游依赖 config.h.in、.pc.in 等模板文件,可在 Port 脚本中用 set_configvar + add_configfiles 生成配置头/元数据文件;这类生成逻辑应与版本号、平台特性保持同步,避免写死常量。
7.2 预编译二进制 (Precompiled Binaries)
7.2.1 on_install 阶段必须将产物严格分类移动至 package:installdir() 的标准子目录:
lua
os.cp("include/*", package:installdir("include"))
os.cp("lib/*.a", package:installdir("lib"))
os.cp("bin/*", package:installdir("bin"))7.2.2 使用 os.trycp 处理非跨平台文件(如 .dll 仅在 Windows 存在):
lua
os.trycp("bin/*.dll", package:installdir("bin"))7.2.3 当前预编译包的自动化构建实践多依赖 github action,且可稳定复用的平台主要是 Windows。跨平台预编译覆盖不足时,优先保证源码构建路径可用。
7.2.4 若上游仅提供预编译产物(如 yy-thunks),可直接打包上游二进制/目标文件,并在包脚本配置 set_policy("package.precompiled", false)。
7.2.5 同一包同时支持“预编译下载”和“源码构建”时,建议用 package:is_precompiled() 区分逻辑,仅在源码构建路径添加必需的构建期依赖(如 perl、gperf),避免对纯预编译路径引入无效依赖。
7.2.6 仅在“源码构建路径”追加逻辑时,也可使用 package:is_built() 判断;兼容旧版常见写法为 if not package.is_built or package:is_built() then ... end。
7.3 组件包 (Component Packages)
7.3.1 对于提供多个独立子库的大型包(如 Boost、Qt),使用组件机制让用户按需依赖,避免强制链接全部子库:
lua
add_components("core", "net", "ssl")
on_component("core", function(package, component)
component:add("links", "foo_core")
end)
on_component("net", function(package, component)
component:add("links", "foo_net")
component:add("deps", "core") -- 组件间依赖
end)7.3.2 组件可在 on_load 动态注册(package:add("components", ...)),并通过 {default = true} 标记默认组件、{deps = "base"} 声明组件依赖,适合“按版本/配置启用不同组件集合”的包。
7.3.3 个别平台(尤其 MinGW)对链接顺序敏感时,优先使用顶层 add_linkorders(...) 固定顺序;若需按条件动态追加,再在 on_load 使用 package:add("linkorders", ...)。同一链接分组可用 group::name 前缀声明顺序。
lua
add_linkorders("mingw32", "SDL2main")
add_linkorders("group::foo", "group::bar")
on_load(function (package)
if package:is_plat("mingw") then
package:add("linkorders", "mingw32", "SDL2main")
end
end)7.4 包规则导出 (Exporting Package Rules)
7.4.1 包可通过 rules/*.lua 导出下游可复用规则;用户侧通过 add_rules("@<pkg>/<rule>") 引用。
lua
-- package side
-- rules/link.lua -> rule("xp")
-- user side
add_requires("yy-thunks")
add_rules("@yy-thunks/xp")8. 交叉编译支持 (Cross-Compilation Support)
8.1 平台与架构检测 (Platform and Architecture Detection)
8.1.1 在 on_install 中使用以下 API 进行条件分支:
lua
package:is_plat("windows", "mingw") -- 目标平台
package:is_arch("x86_64", "arm64") -- 目标架构
package:is_cross() -- 是否交叉编译(host != target)8.1.2 交叉编译时,工具链由 Xmake 自动注入至下游构建系统(CMake toolchain file 等),无需在脚本中手动指定编译器路径。
8.1.3 宿主工具构建:若包在构建过程中需要先编译运行在宿主平台的工具(如 protoc、flatc),须将该工具拆分为独立工具包并通过 add_deps 引用,严禁在同一个 on_install 中混合编译宿主与目标产物。
8.1.4 同名包多配置并存:可通过 pkg~xxx 形式引用同名包的变体配置;配合 {host = true} 表示该依赖使用 host toolchain 构建,从而确保产物可在当前构建机直接运行(常用于构建期代码生成工具)。
lua
add_deps("opencc~host", {kind = "binary", host = true})
-- 使用时仍通过包名访问依赖对象
local host_opencc = package:dep("opencc")8.1.5 on_load/on_install/on_check 等钩子支持目标平台过滤与宿主平台过滤(@host 语法),并支持更丰富的条件表达式:and/or/!、plat|arch、通配符(如 arm*)及 target@host1,host2 组合。适合二进制工具包或“按宿主分发预编译产物”的场景:
lua
on_install("@windows", "@linux", function(package)
-- 根据宿主系统执行安装逻辑
end)
on_install("windows|x64", "windows|x86", function(package)
-- 目标平台+架构过滤
end)
on_install("!cross and !wasm and mingw|!i386", function(package)
-- 复合布尔表达式
end)
on_install("windows|!arm*", function(package)
-- 通配符与取反
end)
on_install("mingw@windows", function(package)
-- 目标 mingw,宿主 windows
end)
on_install("android@linux,macosx", function(package)
-- 指定目标平台 + 多宿主平台
end)
on_install("@linux|x86_64", "@linux|arm64", function(package)
-- 宿主平台 + 宿主架构过滤
end)
on_load("windows", function(package)
-- 仅在目标 windows 生效
end)8.1.6 若需区分宿主平台的子环境(如 Windows 下区分原生终端与 MSYS),可用 is_subhost(...)。该写法常用于选择系统包源(如 pacman::)或做 MSYS 专属安装分支。
8.1.7 目标信息访问除 is_arch(...) / is_plat(...) 外,还可使用 package:is_arch64()、package:arch()、package:plat() 作为补充(常用于路径拼接或与 arch_set/plat_set 配对保存/恢复):
lua
local oldarch = package:arch()
if package:is_arch64() then
-- ...
end8.1.8 编译器差异分支可用 package:has_tool("cc"/"cxx", ...) 判断当前工具链实现(如 cl、clang_cl、clangxx),但因工具链缓存原因建议仅在 on_install 阶段使用:
lua
on_install(function (package)
if package:has_tool("cxx", "cl", "clang_cl") then
-- msvc-like branch
end
end)8.1.9 宿主子环境检测应使用全局 is_subhost(...);package:is_subhost(...) 不是可用 API。
8.1.10 针对目标平台字符串拼接或条件分支,可使用 package:targetarch() 与 package:is_targetos(...)(与 is_arch/is_plat 互补):
lua
local triplet = package:is_targetos("windows") and ("win-" .. package:targetarch()) or "unix"8.1.11 需要读取当前工具链实际工具路径(或工具名)做三元组推断/参数拼装时,可使用 package:tool("cc"/"cxx"/...)。
8.1.12 个别迁移/兼容场景可在安装阶段临时切换目标三元组(package:plat_set(...) / package:arch_set(...))复用构建逻辑,但应在安装后恢复原值,避免污染后续流程。
8.2 Android / iOS 注意事项
8.2.1 Android NDK 目标下,package:is_plat("android") 为 true,C++ STL 类型由 Xmake 统一管理,无需手动向 CMake 传递 -DANDROID_STL。
8.2.2 若上游 CMake 脚本在检测到 Android 时有特殊逻辑,须验证其与 Xmake 生成的 toolchain file 的兼容性,必要时通过 add_patches 修正。
9. 验证与测试 (Validation and Testing)
9.1 测试逻辑 (Test Logic)
9.1.1 原则上每个包都应包含 on_test 段落。以下场景可豁免:
- 仅做系统探测、只有
on_fetch且无安装流程的包; - 仅做重命名/兼容转发的继承包(如
set_base(...)),且父包已覆盖测试; - 仅做依赖聚合/转发的
set_kind("template")元包; - 仅做依赖聚合/语法糖转发、无独立产物的工具包(如
autotools); - 上游拆分子包但由父包统一验证的场景(如
libc++归属libllvm生态)。
9.1.2 on_test 的核心目标是验证“头文件可见 + 符号可链接”(即最终可用)。优先使用轻量的符号/类型检测;check_*snippets 作为补充用于覆盖更完整调用路径。C 接口库常见写法:
lua
on_test(function(package)
assert(package:has_cfuncs("foo_init", {includes = "foo/foo.h"}))
end)同类可用接口(按需要选其一,不必全部使用):
package:has_ctypes(...)package:has_cxxfuncs(...)package:has_cxxtypes(...)package:has_cincludes(...)/package:has_cxxincludes(...)package:check_importfiles(...)(用于导入目标可见性校验)
9.1.3 C++ 类/模板测试:使用 check_cxxsnippets 编写最小实例化代码:
lua
on_test(function(package)
assert(package:check_cxxsnippets({test = [[
#include <foo/bar.hpp>
void test() { foo::Bar b; b.run(); }
]]}, {configs = {languages = "c++17"}}))
end)9.1.4 C 代码片段测试:对 C 库使用 check_csnippets 验证完整调用路径:
lua
on_test(function(package)
assert(package:check_csnippets({test = [[
#include <foo.h>
void test() { foo_ctx_t* ctx = foo_create(); foo_destroy(ctx); }
]]}, {configs = {languages = "c11"}}))
end)9.1.5 Objective-C / Objective-C++ 场景可使用 check_msnippets 做最小可编译/可链接校验,作为 check_csnippets/check_cxxsnippets 的语言特化补充。
9.1.6 语言标准依赖:测试代码若依赖特定标准(如 C++17 结构化绑定、C11 原子操作),必须在 configs 中显式声明 languages(见 9.1.3、9.1.4),否则低版本编译器会误报构建失败。
9.1.7 shared/static 产物形态(如 shared 有 .dll、static 有 .lib/.a)原则上应由包管理框架统一校验;当前尚无通用自动检查机制,也不适合要求每个包都编写额外检测脚本。
因此这里采用人工辅助检查:在新增/修改包时,维护者结合构建日志与安装目录产物进行抽查确认,并保留基础符号/代码片段测试(见 9.1.2~9.1.6)。
10. 维护与 CI 操作标准 (Maintenance and CI Standards)
10.1 本地验证命令
10.1.1 生成包模板:
bash
xmake l scripts/new.lua github:<owner>/<repo>10.1.2 完整测试(含详细构建日志):
bash
xmake l scripts/test.lua -vD --shallow <package>10.1.3 测试指定版本:
bash
xmake l scripts/test.lua -vD --shallow <package> <version>10.1.4 交叉测试:必须覆盖至少两个平台(如 linux 和 mingw):
bash
xmake l scripts/test.lua -vD --shallow --plat=mingw <package>10.2 PR 提交规范
10.2.1 必须提交至 dev 分支,禁止直接向 master 提交。
10.2.2 以下重试方式仅用于少见异常场景(如 GitHub Actions 自身异常、tarball 下载偶发失败等),不应替代正常修复提交流程。若需触发重新检测,可通过以下两种方式(普通贡献者通常无权直接 rerun 具体 CI job):
close后reopenPR- 推送一个空提交:
git commit --allow-empty -m "ci: retrigger"
10.2.3 包描述文件末尾不能以 package_end() 结束。
10.2.4 单次 PR 原则上只新增或修改一个包,多包变更须拆分为独立 PR。
11. 包管理边界情况处理总结 (Package-Management Corner Cases)
| 场景 | 处理策略 | 推荐 API |
|---|---|---|
| 构建系统缺少安装逻辑 | 手动将产物从 build 目录拷贝至 install 目录 | os.cp(...), package:installdir() |
| Port 辅助文件定位 | 拷贝 port/* 到源码目录时,用包脚本目录定位源文件,避免相对路径歧义 | package:scriptdir() |
| 构建目录产物回收 | 从临时构建目录拷贝 .pdb 等中间产物到安装目录;优先用 builddir,buildir 视为旧写法 | package:builddir(), os.trycp(...) |
| 运行时环境变量 | 将 bin 或 lib 路径注入 PATH | package:addenv("PATH", "bin") |
| Git 子模块 | URL 配置禁用 submodules | add_urls(..., {submodules = false}) |
| Git-only 源 | 直接用版本号绑定 commit/tag(无需 git: 前缀) | add_versions("2025.03.02", "<hash>") |
| 动态来源切换 | 按平台/形态在 on_source 动态设置 urls/versions | if on_source then on_source(function (package) ... end) end |
| 版本清单外置 | 大量版本映射拆分到外部文件,减少主脚本噪声 | add_versionfiles("versions.txt") |
| Git 引用分支逻辑 | release 包与 git 引用目录结构不一致时分支处理 | if package:gitref() then ... end |
| URL 覆盖与追加 | 重置用 set_urls,增量维护优先 add_urls | set_urls(...), add_urls(...) |
| 压缩包内容裁剪 | 通过 excludes 过滤无关目录或当前平台不支持文件,降低解压开销并避免无效文件干扰 | add_urls("...zip", {excludes = {"*/html/*"}}) |
| 包重命名兼容 | 用 set_base 继承新包脚本,并在 on_load 给出迁移提示 | set_base("libsdl2"), package:base():script("load")(package) |
| 附加资源下载 | 缺失构建辅助文件时单独拉取资源并在安装期读取 | add_resources(...), package:resourcefile(...) |
| CMake 生成器策略 | 按上游兼容性显式启用/关闭 Ninja 生成器策略 | set_policy("package.cmake_generator.ninja", true/false) |
| Windows 长路径 | Git 子模块路径过深时启用 longpaths 策略 | set_policy("platform.longpaths", true) |
| 动态修改 kind | 尽量避免在 on_load 动态 package:set("kind", ...)(见 issue #5807) | package:set("kind", "library", {headeronly = true}) |
| 交叉编译宿主工具 | 拆分为独立工具包,通过 deps 引用 | add_deps("protoc") |
| 同名包多配置 | 使用 pkg~xxx + {host = true} 获取宿主可执行工具变体 | add_deps("foo~host", {host = true}) |
| 配置强制收敛 | 在 on_load 用 config_set 约束不受支持组合,并提示原因 | package:config_set("shared", false) |
| 按包 kind 分支 | 根据当前包类型切换依赖/测试/行为,优先用 kind 访问函数提高可读性 | package:is_library(), package:is_binary(), package:is_toolchain() |
| 可选依赖降级 | 依赖非强制时可标记 optional = true,按可用性启用能力 | add_deps("zstd", {optional = true}) |
| 依赖版本联动 | 依赖版本按当前包版本动态拼接约束 | package:add("deps", "libselinux >=" .. package:version_str()) |
| 依赖主版本锁定 | 可用 x 通配锁定主/次版本线(如 3.x、2.x) | add_deps("python 3.x") |
| 三态后端配置 | add_configs(..., {values = {false, "a", "b"}}) 表达关闭/多后端二选一 | add_configs("ssl", {values = {false, "openssl", "mbedtls"}}) |
| 过滤内置配置项 | 批量遍历配置时过滤 debug/shared 等内置项,仅向上游传递业务配置 | package:configs(), package:extraconf("configs", name, "builtin") |
| Debug 判定 | 新脚本统一使用 is_debug();debug() 归为历史接口 | package:is_debug() |
| 平台 + 架构过滤 | 用 plat|arch 精确限制钩子生效范围 | on_install("windows|x64", fn) |
| 目标信息 getter | 除 is_plat/is_arch 外,可用 plat/arch/is_arch64 获取原始目标信息 | package:plat(), package:arch(), package:is_arch64() |
| 编译器差异分支 | 按当前工具链实现分流(如 cl/clang_cl/clangxx),建议仅在 on_install 使用 | package:has_tool("cc"/"cxx", ...) |
| 宿主子环境区分 | 需要区分 MSYS/原生终端时使用全局函数 | is_subhost("msys") |
| 系统库版本不满足要求 | on_fetch 中校验版本后返回 nil | semver.satisfies(ver, ">=x.y") |
| 非标准版本 tag | URL 传入 version 映射函数 | add_urls(..., {version = fn}) |
| 无 Release 软件包 | 使用日期版本号 + 完整 commit hash | add_versions("2024.01.01", "hash") |
| CI/平台预检 | 在最早阶段快速失败,跳过不支持环境 | on_check(..., function (package) assert(...) end) |
| on_check 兼容写法 | 兼容旧版时先判断 if on_check then | if on_check then on_check(...) end |
| 宿主平台过滤安装 | 用 @host 语法限制安装脚本执行环境 | on_install("@windows", "@linux", fn) |
| 宿主平台 + 架构过滤 | @host|arch 组合精确限制预编译/安装逻辑 | on_install("@linux|x86_64", fn) |
| 路径型环境变量导出 | 对 PYTHONPATH 等变量标记为 pathenv,避免路径拼接异常;mark_as_pathenv 仅在 on_load 调用 | package:addenv("PYTHONPATH", "python"), package:mark_as_pathenv("PYTHONPATH") |
| 系统源探测入口 | 除发行版名外也可接入 pkg-config 探测 | add_extsources("pkgconfig::libxml-2.0") |
| 组件化系统源映射 | 在 on_load 按配置动态追加 extsources | package:add("extsources", "apt::libxcb-foo-dev") |
| on_test 豁免场景 | 仅 on_fetch、set_base 转发包、template 元包、依赖聚合语法糖包、父生态统一验证子包可豁免 | on_fetch(...), set_base("..."), set_kind("template") |
| 平台分流转发包 | 平台间混合“系统库/第三方依赖”时保留 on_install 做分流适配 | on_install(...), is_plat(...) |
| 构建工具依赖透传 | packagedeps 通过 cxflags/shflags 直传依赖信息;作为 patch 构建脚本无果后的最后手段 | import("package.tools.cmake").install(..., {packagedeps = {"libogg"}}) |
| 依赖级联链接缺失 | 遍历 orderdeps + fetch 手动注入编译/链接参数 | for _, dep in ipairs(package:orderdeps()) do ... end |
| 动态组件注册 | 在 on_load 动态添加组件并声明默认/依赖关系 | package:add("components", "base", {default = true}) |
| 链接顺序敏感平台 | 顶层优先用 add_linkorders 固定顺序;动态场景在 on_load 用 package:add("linkorders", ...);分组可用 group:: 前缀 | add_linkorders("mingw32", "SDL2main") |
| 补丁文件后缀 | 结构性修复可用 .patch 或 .diff | add_patches("x", "patches/x/fix.diff", "<sha256>") |
| 补丁应用顺序 | 同版本多个 add_patches 不保证顺序;禁止顺序依赖,必要时合并补丁或改用 io.replace | add_patches(...), io.replace(...) |
| 工具链版本条件补丁 | 在 on_load 读取工具链配置后动态追加补丁,仅对命中环境生效 | package:toolchain("ndk"):config("ndkver"), package:add("patches", ...) |
| 补丁文件编码规范 | 补丁统一 UTF-8 无 BOM + LF,变更后需重算 SHA256 | add_patches(..., "<sha256>") |
| 上游仅预编译发布 | 显式记录来源与平台架构限制后直接打包上游产物 | os.cp("objs/x64/*.obj", package:installdir("lib")) |
| 预编译与源码双路径 | 通过 is_precompiled 仅在源码路径添加构建依赖 | if not package:is_precompiled() then package:add("deps", "perl") end |
| 包规则导出 | 通过 rules/*.lua 向下游导出 @pkg/rule | add_rules("@yy-thunks/xp") |
| 依赖不向下游传递 | 声明为私有依赖 | add_deps("lib", {private = true}) |
12. Xmake 脚本编写边界情况 (Xmake-Script Corner Cases)
| 场景 | 处理策略 | 常用 API |
|---|---|---|
| 头文件重名冲突 | 使用 prefixdir 将头文件安装到子目录 | add_headerfiles("...", {prefixdir = "foo"}) |
| 模板配置文件生成 | Port 中用 set_configvar + add_configfiles 生成 config.h/.pc | set_configvar("FOO", 1), add_configfiles("config.h.in") |