跳转到内容

Xmake 软件包描述规范文档

0. 软件包生命周期概览 (Package Lifecycle Overview)

Xmake 包描述脚本各钩子的执行顺序如下,理解此顺序是正确编写包的前提:

阶段钩子前置条件用途
预检on_check无(最早校验阶段)判断平台/工具链是否受支持;失败时提前终止,便于跳过不支持的 CI 环境
加载on_load无(元数据阶段)动态添加 deps/patches/defines,修改包属性
探测on_fetch自定义系统库探测,返回 nil 则回退到安装流程
下载add_urls + add_versionson_load 完成源码下载与完整性校验
补丁add_patches源码已解压构建前自动应用补丁
安装on_install源码已解压、所有依赖已安装调用构建系统,将产物安装至 installdir
测试on_teston_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:必须指定许可证类型(如 MITApache-2.0BSD-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")
end

2.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:声明构建所需工具(如 cmakeninja)或链接所需库(如 zlib)。

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 编译工具隔离:cmakeninja 等工具依赖的 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.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 中动态追加 extsourcespackage:add("extsources", ...)),按启用组件精细映射发行版包名。


4. 构建配置与环境预处理 (Configuration and Pre-processing)

4.1 用户配置项 (User Options)

4.1.1 add_configs:提供自定义编译开关。内置保留配置项有 sharedstaticpicltovs_runtimedebug。通常不需要重复定义;仅在需要设置 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 动态修改 kindpackage:set("kind", ...)on_load 中虽可用,但当前存在已知行为问题(可能导致 headeronly/非 headeronly 形态处理异常)。除非必要不建议使用;如必须使用,请在注释中说明原因并参考:

https://github.com/xmake-io/xmake/issues/5807#issuecomment-2467654245

4.1.5 配置联动与约束:可在 on_loadpackage: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") 过滤内置配置(如 debugshared);这是常用语法糖,可避免误把内置项当成业务开关传给上游构建系统:

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
end

4.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

definessyslinks 等属性,仍推荐优先放在 on_load,以保持元数据与安装逻辑分离、提高可读性。

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
CMakeimport("package.tools.cmake").install(package, configs)
Mesonimport("package.tools.meson").install(package, configs)
Autoconfimport("package.tools.autoconf").install(package, configs)
Xmakeimport("package.tools.xmake").install(package, configs)
Makeimport("package.tools.make").install(package, configs)
Nmakeimport("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_STATICZSTD_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")
end

5.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.8 使用 package.tools.* 安装/配置时,若需要将依赖包的构建信息显式注入上游构建系统,可通过选项表传入 packagedeps(支持字符串或数组)。其本质是通过 cxflags/shflags 等参数把依赖信息直接塞入构建过程。

该方式应作为最后手段:优先尝试修补上游构建脚本(如 CMakeLists.txtmeson.buildconfigure.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 支持的字段:includedirslinkdirslinkslibfilesdefines

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_installon_test 等其他钩子,不是 on_fetch 专属能力。

6.1.6 on_fetchfind_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)
end

7. 特殊软件包类型处理 (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")
end

7.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() 区分逻辑,仅在源码构建路径添加必需的构建期依赖(如 perlgperf),避免对纯预编译路径引入无效依赖。

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 宿主工具构建:若包在构建过程中需要先编译运行在宿主平台的工具(如 protocflatc),须将该工具拆分为独立工具包并通过 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
    -- ...
end

8.1.8 编译器差异分支可用 package:has_tool("cc"/"cxx", ...) 判断当前工具链实现(如 clclang_clclangxx),但因工具链缓存原因建议仅在 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.dllstatic.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 交叉测试:必须覆盖至少两个平台(如 linuxmingw):

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):

  • closereopen PR
  • 推送一个空提交: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 等中间产物到安装目录;优先用 builddirbuildir 视为旧写法package:builddir(), os.trycp(...)
运行时环境变量binlib 路径注入 PATHpackage:addenv("PATH", "bin")
Git 子模块URL 配置禁用 submodulesadd_urls(..., {submodules = false})
Git-only 源直接用版本号绑定 commit/tag(无需 git: 前缀)add_versions("2025.03.02", "<hash>")
动态来源切换按平台/形态在 on_source 动态设置 urls/versionsif 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_urlsset_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_loadconfig_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.x2.xadd_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)
目标信息 getteris_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 中校验版本后返回 nilsemver.satisfies(ver, ">=x.y")
非标准版本 tagURL 传入 version 映射函数add_urls(..., {version = fn})
无 Release 软件包使用日期版本号 + 完整 commit hashadd_versions("2024.01.01", "hash")
CI/平台预检在最早阶段快速失败,跳过不支持环境on_check(..., function (package) assert(...) end)
on_check 兼容写法兼容旧版时先判断 if on_check thenif 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 按配置动态追加 extsourcespackage:add("extsources", "apt::libxcb-foo-dev")
on_test 豁免场景on_fetchset_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_loadpackage:add("linkorders", ...);分组可用 group:: 前缀add_linkorders("mingw32", "SDL2main")
补丁文件后缀结构性修复可用 .patch.diffadd_patches("x", "patches/x/fix.diff", "<sha256>")
补丁应用顺序同版本多个 add_patches 不保证顺序;禁止顺序依赖,必要时合并补丁或改用 io.replaceadd_patches(...), io.replace(...)
工具链版本条件补丁on_load 读取工具链配置后动态追加补丁,仅对命中环境生效package:toolchain("ndk"):config("ndkver"), package:add("patches", ...)
补丁文件编码规范补丁统一 UTF-8 无 BOM + LF,变更后需重算 SHA256add_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/ruleadd_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/.pcset_configvar("FOO", 1), add_configfiles("config.h.in")