From xmake-skills
Applies idiomatic xmake style to `xmake.lua` files: description vs script domain separation, naming, indentation, and `set_` vs `add_` conventions. Use alongside `xmake-targets` or `xmake-packages` as a stylistic overlay.
How this skill is triggered — by the user, by Claude, or both
Slash command
/xmake-skills:xmake-styleThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
`xmake.lua` is Lua, but idiomatic xmake code looks less like Lua and more like a declarative configuration. Following the conventions below keeps files readable, fast to parse, and consistent with the style used throughout [`xmake`](https://github.com/xmake-io/xmake) and [`xmake-repo`](https://github.com/xmake-io/xmake-repo).
xmake.lua is Lua, but idiomatic xmake code looks less like Lua and more like a declarative configuration. Following the conventions below keeps files readable, fast to parse, and consistent with the style used throughout xmake and xmake-repo.
xmake.lua has two domains and they have very different rules.
This is the top-level body of target(), option(), package(), task(), rule(), and their set_xxx/add_xxx calls.
target("app")
set_kind("binary")
add_files("src/*.cpp")
add_defines("DEBUG")
add_syslinks("pthread")
Rules for the description domain:
if is_plat(...) and for _, x in ipairs({...}) are fine; complex logic is not.set_/add_ call may run more than once as xmake re-enters the file at different configuration stages. Never print() here (you will see it twice), and never do expensive work (I/O, git, network, shell).os.getenv is read-only; most mutating os.* calls are blocked.if or for, move it to the script domain.Anything inside on_load, on_config, before_build, after_build, on_install, on_test, etc., is the script domain. It runs once per lifecycle hook and has the full xmake Lua environment.
target("app")
set_kind("binary")
add_files("src/*.cpp")
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")
os.cp(target:targetfile(), path.join(config.buildir(), "dist"))
end)
Use on_load for dynamic per-target configuration that would be ugly as nested if blocks at description level.
Once a hook body exceeds ~15 lines, move it out:
target("app")
on_load("modules.app.load")
on_install("modules.app.install")
With files at modules/app/load.lua and modules/app/install.lua, each exporting a main function. Keeps the main xmake.lua scannable.
The conventions used throughout the xmake project itself:
-- 4 spaces, never tabs
add_rules("mode.debug", "mode.release")
-- blank line between top-level blocks
add_requires("fmt 10.x", "spdlog")
-- everything inside a target() is indented one level
target("mylib")
set_kind("static")
add_files("src/lib/*.cpp")
add_includedirs("include", {public = true})
add_packages("fmt")
target("app")
set_kind("binary")
add_files("src/app/*.cpp")
add_deps("mylib")
target_end() unless you have to. A new top-level call (target, option, package, rule, task) implicitly closes the previous one.add_* calls stay grouped. Put add_files / add_headerfiles together; put add_includedirs / add_defines together; put add_packages / add_deps together.--. Use -- for single-line comments. Avoid --[[ ]] blocks unless you are commenting out several lines temporarily.Both of these are valid:
target("test")
set_kind("binary")
add_files("src/*.c")
target "test"
set_kind "binary"
add_files "src/*.c"
Pick one and stick with it per file. The xmake and xmake-repo codebases predominantly use parentheses — match that style unless you have a reason.
- or _ for word separators. mylib, my-lib, my_lib. Match the produced binary name when it makes sense._. Prefix with enable_ / with_ / has_ to signal intent (enable_foo, with_openssl, has_avx2).mycompany.codegen, mycompany.protobuf). Match how built-in rules are named (mode.debug, c++.unity_build, plugin.compile_commands.autoupdate).myclang, arm-muslgcc.xmake-repo recipes): exactly the upstream name, lowercase, --separated. Match what add_requires users will type.set_ vs add_: pick the right oneset_xxx — replaces. Use for single-valued properties: set_kind, set_version, set_languages, set_optimize, set_symbols, set_default, set_toolchains.add_xxx — appends. Use for list-valued properties: add_files, add_includedirs, add_defines, add_links, add_deps, add_packages, add_cxflags.If you call set_xxx twice for the same key, the second wins. If that is what you want, fine — but it is almost always a mistake.
-- ✗ wrong: second set_languages replaces the first
set_languages("c++17")
set_languages("c++20")
-- ✓ just set it once, at the level you want
set_languages("c++20")
{public = true})Use {public = true} for anything a dependent target needs to see: public headers, public defines, public link libraries.
target("mylib")
set_kind("static")
add_files("src/*.cpp")
add_includedirs("include", {public = true}) -- dependents get -Iinclude
add_defines("MYLIB_STATIC", {public = true}) -- and this define
src/ include dir or your internal defines.{force = true} bypasses xmake's automatic flag-detection filter. Use sparingly and only when you know the flag is supported.is_plat / is_arch / is_modeIdiomatic:
if is_plat("windows") then
add_defines("WIN32_LEAN_AND_MEAN")
elseif is_plat("linux", "macosx") then
add_syslinks("pthread")
end
if is_mode("debug") then
add_defines("DEBUG")
set_symbols("debug")
end
Avoid reaching into os.host() / os.arch() at description level for platform gating — that is the host, not the target. is_plat/is_arch reflect the configured target and are the right answer 99% of the time.
option("enable_foo")
set_default(false)
set_showmenu(true)
set_description("Enable the foo subsystem")
set_category("feature") -- optional grouping in --menu
option_end()
Conventions:
set_showmenu(true) if the option is meant for end users — otherwise it's invisible to xmake f --menu.set_description(...) — the description is what shows up in the configure menu.set_default(...) to pin a default; do not rely on nil.set_values(...) to constrain to an enum when applicable.has_config("name") over get_config("name") in the target body unless you need the actual value.In a project xmake.lua:
-- all add_requires at the top of the file
add_requires("fmt 10.x")
add_requires("spdlog", {configs = {header_only = false}})
add_requires("openssl", {system = false})
target("app")
add_packages("fmt", "spdlog", "openssl")
add_requires near the top, before any target(). Keeps the dependency surface visible in one place."fmt 10.x", "boost 1.84.x").add_packages(...) over manually setting add_links/add_includedirs — the package integration does the right thing automatically.In a package recipe (packages/<a>/<name>/xmake.lua in xmake-repo):
package("mylib")
set_homepage("https://example.com/mylib")
set_description("A short one-line description")
set_license("MIT")
add_urls("https://github.com/example/mylib/archive/v$(version).tar.gz",
"https://github.com/example/mylib.git")
add_versions("1.2.3", "abcdef...sha256...")
add_configs("shared", {description = "Build shared library", default = false, type = "boolean"})
add_configs("with_ssl", {description = "Enable SSL support", default = true, type = "boolean"})
add_deps("cmake")
if is_plat("linux") then
add_syslinks("pthread")
end
on_install(function (package)
local configs = {"-DBUILD_TESTS=OFF"}
table.insert(configs, "-DBUILD_SHARED_LIBS=" .. (package:config("shared") and "ON" or "OFF"))
import("package.tools.cmake").install(package, configs)
end)
on_test(function (package)
assert(package:has_cfuncs("mylib_init", {includes = "mylib.h"}))
end)
package_end()
Recipe conventions:
set_homepage / set_description / set_license in that order, at the top.add_urls for mirror fallback; the .git URL last.add_configs names lowercased with underscores: shared, with_ssl, header_only, enable_foo. Match what users already type in other recipes.add_versions kept in descending order (newest at top) in xmake-repo.on_install uses import("package.tools.cmake") (or autoconf, meson, make, msbuild, xmake) — almost always one of these. Don't hand-roll.on_test asserts a real symbol with has_cfuncs / has_cxxtypes / check_csnippets. Empty on_test is a red flag.add_rules("mode.debug", "mode.release"). Don't hand-roll debug/release defines.compile_commands.json → add_rules("plugin.compile_commands.autoupdate", {outputdir = "."}).add_rules("c++.unity_build"), not hand-crafted aggregating files.add_rules("qt.widgetapp" / "qt.quickapp"), not manual uic/moc/rcc invocations.add_configfiles("config.h.in") + set_configvar, not a before_build Lua shell script.If the built-in exists, use it. It handles edge cases (cross-compilation, multiple platforms, caching) that your hand-rolled version will not.
| Anti-pattern | Why | Do this instead |
|---|---|---|
print(...) at description level | Runs multiple times | Use cprint in on_load / on_config, or utils.vprint gated by -v |
Complex for / nested if at description level | Parsed multiple times; harder to reason about | Move into on_load(function(target) ... end) |
os.iorun / git calls at description level | Runs during every parse | Move into on_load or a task |
add_cxflags("-std=c++20", {force = true}) | Bypasses xmake's language handling | set_languages("c++20") |
add_links("pthread") on cross-platform code | Not a library on Windows | add_syslinks("pthread") |
set_languages called per-target when every target uses the same standard | Duplication | Call once at the top of xmake.lua |
| Using absolute paths | Not portable | Paths relative to xmake.lua; xmake resolves them |
Re-exporting every header with add_includedirs | Pollutes dependents | {public = true} only on the public include dir |
Empty on_test in a package recipe | Passes trivially | Use has_cfuncs / check_csnippets to actually exercise the library |
From CONTRIBUTING.md of both xmake and xmake-repo:
add ..., fix ..., improve ..., update ....xmake.lua rule.target / add_files / add_deps → xmake-targetsoption / has_config → xmake-optionsadd_requires / add_packages → xmake-packagesxmake-repo-testingxmake-rules / xmake-toolchainsnpx claudepluginhub xmake-io/xmake-skills --plugin xmake-skillsGuides users through starting a new Xmake project: installation (brew, curl, scoop), creating projects from templates, and writing the minimal xmake.lua. Helpful for first-time Xmake users or scaffolding a new project.
Generates Makefiles with testing, linting, formatting, and automation targets for Python, Rust, or TypeScript projects. Use when starting a project or standardizing build automation.
Generates Makefiles for C/C++, Python, Go, Java projects with .PHONY targets, GNU standards, standard targets, security hardening, and CI/CD integration.