Help us improve
Share bugs, ideas, or general feedback.
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.
npx claudepluginhub xmake-io/xmake-skills --plugin xmake-skillsHow 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).
Guides 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 for C/C++, Python, Go, Java projects with .PHONY targets, GNU standards, standard targets, security hardening, and CI/CD integration.
Generates Makefiles for Python, Rust, and TypeScript projects with standard targets for help, install, lint, format, typecheck, test, build, clean, and automation. Use when projects lack Makefiles or need dev workflow setup.
Share bugs, ideas, or general feedback.
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-toolchains