XMake Build System

SkillDev tools

XMake build configuration, options, commands, and patterns for LuisaCompute.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the XMake Build System skill

What this skill tells your AI

The instructions your AI receives, as published by luisagroup/luisacompute in .agents/skills/xmake/SKILL.md and read by ahel’s review.

Primary build system. Requires XMake 3.0.6+. Optional: CUDA Toolkit, Vulkan SDK, LLVM 20, Rust.

Quick Start

xmake f -m debug -c -y
xmake build
# Update compile_commands.json:
xmake project -k compile_commands --lsp=clangd .vscode

Configuration

PlatformCommand
Linux GCCxmake f -p linux -a x86_64 --toolchain=gcc -m release -c
Linux Clangxmake f -p linux -a x86_64 --toolchain=clang -m release -c
Windows MSVCxmake f -p windows -a x64 --toolchain=msvc -m release -c
Windows Clang-CLxmake f -p windows -a x64 --toolchain=clang-cl -m release -c
Windows LLVMxmake f -p windows -a x64 --toolchain=llvm -m release -c
macOS Clangxmake f -p macosx -a arm64 --toolchain=clang -m release -c

Flags

-c clean cache, -m <mode> (release/debug/releasedbg/check/profile/coverage), -p <plat> (linux/windows/macosx), -a <arch> (x86_64/x64/arm64), --check check before building, -y auto-accept all prompts and skip interaction (useful in scripts/CI).

In this project debug mode automatically enables AddressSanitizer (ASan). To enable ASan for other modes, use --policies=build.sanitizer.address.

Sanitizer Modes

XMake supports sanitizer builds through sanitizer policies. Policies propagate the sanitizer configuration to dependent packages and avoid the deprecation warnings produced by the legacy mode.asan/mode.tsan/mode.lsan/mode.ubsan rules.

ASan in debug mode

Configure and build with debug mode as usual:

xmake f -m debug -c -y
xmake build
xmake run <target>

Enable via policy manually

To enable a sanitizer for a different mode, use the corresponding policy. In xmake.lua:

set_policy("build.sanitizer.address", true)

Or from the command line:

xmake f --policies=build.sanitizer.address -c -y
xmake build
xmake run <target>

Available policies:

PolicySanitizer
build.sanitizer.addressAddressSanitizer
build.sanitizer.threadThreadSanitizer
build.sanitizer.memoryMemorySanitizer
build.sanitizer.leakLeakSanitizer
build.sanitizer.undefinedUndefinedBehaviorSanitizer

Multiple sanitizers can be combined, e.g.:

xmake f --policies=build.sanitizer.address,build.sanitizer.undefined -c -y

Commands

CommandDescription
xmake cleanClean
xmake -rRebuild
xmake build <target>Build target
xmake run <target>Run target
xmake run <target> <args>Run target with arguments
xmake -lList targets
xmake install -o <dir>Install binaries to <dir>
xmake -yAuto-accept all prompts (downloads, overwrites, etc.), skip interaction
xmake project -k compile_commands --lsp=clangd .vscodeGenerate compile_commands.json

Common Issues

  • -v, -D, --diagnosis invalid; use --verbose
  • Boolean options: --lc_option=true/=false
  • Use -c to clean cache when reconfiguring with different options
  • Use -y to auto-accept all prompts and skip interaction — essential in automated scripts and CI pipelines
  • lc_fallback_backend requires both lc_llvm_path and lc_embree_path
  • lc_dx_backend is silently disabled on non-Windows platforms
  • lc_metal_backend is silently disabled on non-macOS platforms
  • lc_cuda_backend is silently disabled outside Windows/Linux
  • PCH (precompiled header) error like has been modified since the precompiled header / redefinition of ... means the target's PCH is stale — use xmake build -r <target> to force a clean rebuild of that target.

Xmake Target Writing Tutorial

Overview

This tutorial covers how to write xmake targets using the standard xmake API, with examples drawn from real projects like LuisaCompute. The recommended style uses on_load callbacks for dynamic configuration, with static declarations outside.


1. Basic Target Structure

target("<name>", {kind = "static"})   -- Optional: pass kind inline
-- or
target("<name>")
set_kind("static")  -- "static", "shared", "binary", "object", "phony", "headeronly", "moduleonly"

-- Static settings (outside on_load)
set_basename("my-lib")        -- Override output filename
add_deps("dep1", "dep2")      -- Target dependencies
add_rules("my-rule")          -- Custom build rules (MUST be outside on_load)
add_files("src/*.cpp")        -- Source files (simple globs outside)
add_headerfiles("include/**.h") -- Header files

on_load(function(target)
    -- Dynamic settings (preferred for conditional config)
    target:add("includedirs", "include", {public = true})
    target:add("defines", "MY_DEFINE", {public = true})
    target:add("deps", "another-dep")   -- Same as add_deps() outside
    target:set("kind", "shared")
    target:add("links", "pthread")
    target:add("syslinks", "dl")
    target:add("packages", "spdlog")    -- For xrepo packages
end)

after_build(function(target)
    -- Post-build steps (e.g., copy DLLs)
end)
target_end()

Key rules:

  • add_rules() must be outside on_load — they are target-level; cannot be set from inside on_load.
  • add_deps() outside = target:add("deps", ...) inside — they are equivalent.
  • Simple globs (add_files, add_headerfiles) can go outside; conditional additions go inside on_load.
  • Visibility — pass {public = true}, {interface = true}, or {private = true} (default) to control inheritance.

2. Config Fields and Script Fields

Every xmake target has two kinds of declarations:

  • Config fields — static target properties (what to build, how to build it).
  • Script fields — lifecycle callbacks (when to run custom Lua code).

Config Fields

Config fields are the key/value pairs that describe a target. They are set with set_* / add_* outside on_load, or equivalently with target:set() / target:add() inside a script field.

Common config field categories:

CategoryFields
Identitykind, basename, filename, prefixname, suffixname, extension, group
Outputtargetdir, objectdir, dependir, rundir, installdir, prefixdir
Sourcesfiles, headerfiles, extrafiles, remove_files, configfiles, installfiles
Includesincludedirs, sysincludedirs
Definesdefines, undefines, configvar
Linkslinks, syslinks, linkdirs, rpathdirs, linkorders, linkgroups, frameworks, frameworkdirs
Compilationlanguages, optimize, warnings, symbols, runtimes, exceptions, fpmodels, encodings, strip, vectorexts, forceincludes, pcheader, pcxxheader
Dependenciesdeps, packages, options, rules
Miscvalues.*, runenv, runargs, enabled, default, toolchains, toolset, plat, arch, policy

Rules for config fields:

  1. Most config fields can be set either outside or inside on_load using the equivalent target:set("field", value) / target:add("field", value) form.
  2. add_rules() must be outside on_load — rules are target-level metadata and cannot be added from inside a script field.
  3. Static config goes outside for readability; dynamic/conditional config goes inside on_load.
  4. Use {public = true} / {interface = true} / {private = true} with target:add() to control inheritance of includedirs, defines, links, etc.
  5. target:get("field") reads a config field inside a script field; has_config("opt") reads project-level options.

Script Fields

Script fields are the lifecycle hooks where you write imperative Lua code. They receive the target object (and sometimes other arguments) and run at specific build phases.

Common script fields:

Script fieldRuns whenTypical use
on_load(function(target) ... end)Target is loaded (early)Dynamic config, conditional deps/files
on_config(function(target) ... end)After xmake config, before buildValidate toolchain/options
before_build(function(target) ... end)Before compilation startsPre-build checks/code generation
on_build(function(target) ... end)Build phaseOverride entire build
after_build(function(target) ... end)After build finishesCopy outputs, print reports
before_link(function(target) ... end)Before linkingInject link args
after_link(function(target) ... end)After linkingSign/post-process binary
on_install(function(target) ... end)Install phaseCustom install logic
on_run(function(target) ... end)xmake runOverride run behavior

Rules for writing code in script fields:

  1. Always operate on the target argument for target-local config: target:add("field", value), target:set("field", value), target:get("field").
  2. Project-scope helpers are still available: is_plat(), is_arch(), is_mode(), has_config(), get_config(), os.*, io.*, path.*, etc.
  3. You can import() extension modules at the top of the script field callback (or at file scope).
  4. on_load is for configuration — it should set/add target config fields. It runs very early, so dependencies may not be fully resolved yet.
  5. before_build / after_build are for actions — they run around compilation and are the right place to generate files, copy DLLs, run validators, or emit summaries.
  6. Returning false from some hooks (e.g. on_test) signals failure; most hooks ignore return values.

Example: Writing Code Inside Script Fields

target("my-scripted-target")
set_kind("binary")
add_files("src/*.cpp")
add_includedirs("include")

-- Static config field outside
set_basename("myapp")
set_warnings("all")

on_load(function(target)
    -- Dynamic config field inside script field
    target:add("defines", "VERSION=\"1.0.0\"", {public = true})

    if target:is_plat("windows") then
        target:add("syslinks", "Advapi32", "Ole32")
    elseif target:is_plat("linux") then
        target:add("syslinks", "pthread", "dl")
    end

    if is_mode("debug") then
        target:set("symbols", "debug")
        target:set("optimize", "none")
    end

    -- Record data for later script fields
    target:data_set("build_start", os.mclock())
end)

before_build(function(target)
    -- Script field code: validate before compiling
    local main = path.join(target:scriptdir(), "src/main.cpp")
    if not os.isfile(main) then
        raise("missing entry point: " .. main)
    end

    -- Generate a version header
    local out = path.join(target:autogendir(), "version.h")
    os.mkdir(path.directory(out))
    io.writefile(out, string.format("#define BUILD_TIME %d\n", os.time()))
    target:add("includedirs", path.directory(out))
end)

after_build(function(target)
    -- Script field code: post-build action
    local exe = target:targetfile()
    if os.isfile(exe) then
        local dest = path.join("$(buildir)", "publish")
        os.mkdir(dest)
        os.cp(exe, dest)
        print("published:", exe)
    end

    local start = target:data("build_start")
    if start then
        print("elapsed:", os.mclock() - start, "ms")
    end
end)

target_end()

3. API Equivalence: Inside on_load

Use target:add() and target:set() inside on_load to dynamically configure targets:

target:add() — cumulative (equivalent to add_*)

Inside on_load(target)
target:add("deps", "foo")
target:add("files", "*.cpp")
target:add("headerfiles", "*.h")
target:add("includedirs", "inc")
target:add("sysincludedirs", "inc")
target:add("defines", "FOO")
target:add("undefines", "BAR")
target:add("links", "foo")
target:add("syslinks", "dl")
target:add("linkorders", ...)
target:add("linkgroups", {group = true})
target:add("linkdirs", "lib")
target:add("rpathdirs", "lib")
target:add("frameworks", "Foundation")
target:add("frameworkdirs", "dir")
target:add("embeddirs", "dir")
target:add("packages", "spdlog")
target:add("options", "myopt")
target:add("vectorexts", "avx2")
target:add("languages", "cxx20")
target:add("imports", "module")
target:add("runenvs", "PATH", "/usr/bin")
target:add("forceincludes", "inc.h")
target:add("configfiles", "config.h.in")
target:add("installfiles", "data/*")
target:add("extrafiles", "readme.md")
target:add("filegroups", "src", files)

target:set() — singular (equivalent to set_*)

Inside on_load(target)
target:set("kind", "static")
target:set("basename", "foo")
target:set("filename", "foo.dll")
target:set("prefixname", "lib")
target:set("suffixname", "-d")
target:set("extension", ".dll")
target:set("targetdir", "lib")
target:set("objectdir", "obj")
target:set("dependir", "deps")
target:set("rundir", "bin")
target:set("runargs", "--verbose")
target:set("installdir", "/usr")
target:set("prefixdir", "subdir")
target:set("configdir", "out")
target:set("group", "mygroup")
target:set("languages", "cxx20")
target:set("optimize", "fastest")
target:set("warnings", "all")
target:set("symbols", "debug")
target:set("exceptions", "cxx")
target:set("runtimes", "MD")
target:set("fpmodels", "fast")
target:set("encodings", "utf-8")
target:set("strip", "all")
target:set("enabled", true)
target:set("default", false)
target:set("toolchains", "clang")
target:set("toolset", "cc", "/usr/bin/gcc")
target:set("plat", "linux")
target:set("arch", "x64")
target:set("policy", "build.optimization.lto", true)
target:set("options", "opt1")
target:set("values.mykey", "val")
target:set("configvar", "VAR", "value")
target:set("runenv", "PATH", "/usr/bin")
target:set("pcheader", "header.h")
target:set("pcxxheader", "header.hpp")
target:set("pmheader", "header.m")
target:set("pmxxheader", "header.mm")

Note: For the target:add("name", ...) / target:set("name", ...) pattern, any key name works through xmake's generic values mechanism. Only explicitly defined APIs (like files, deps, kind) have special handling.


4. Compilation Flags (by Language)

These APIs pass compiler-specific flags:

APIDescription
add_cflags(...)C compilation flags
add_cxflags(...)C/C++ compilation flags
add_cxxflags(...)C++ compilation flags
add_mflags(...)ObjC compilation flags
add_mxflags(...)ObjC/ObjC++ compilation flags
add_mxxflags(...)ObjC++ compilation flags
add_scflags(...)Swift compilation flags
add_asflags(...)Assembly compilation flags
add_gcflags(...)Go compilation flags
add_dcflags(...)D language compilation flags
add_rcflags(...)Rust compilation flags
add_fcflags(...)Fortran compilation flags
add_zcflags(...)Zig compilation flags
add_cuflags(...)CUDA compilation flags
add_culdflags(...)CUDA device link flags
add_cugencodes(...)CUDA gencode settings (e.g., "sm_30", "native")

Linker Flags

APIDescription
add_ldflags(...)Static library/exe link flags
add_arflags(...)Archive (static library) flags
add_shflags(...)Dynamic library link flags

Example with per-tool flags:

on_load(function(target)
    target:add("cxflags", "-fPIC", {tools = {"clang", "gcc"}, public = true})
    target:add("cxflags", "/Zc:preprocessor", {tools = "cl"})
    target:add("ldflags", "-Wl,-rpath,.", {force = true, expand = false})
end)

5. Precompiled Headers (PCH)

target("my-target")
set_pcheader("precompiled.h")     -- C PCH
set_pcxxheader("precompiled.hpp") -- C++ PCH

Enable conditionally with:

if has_config("enable_pch") then
    set_pcxxheader("mypch.hpp")
end

6. Conditional Configuration with Conditions

on_load(function(target)
    -- Platform checks
    if target:is_plat("windows") then
        target:add("defines", "NOMINMAX", "PLATFORM_WINDOWS")
        target:add("syslinks", "Advapi32", "Ole32")
    elseif target:is_plat("linux") then
        target:add("syslinks", "dl", "uuid", "pthread")
        target:add("cxflags", "-fPIC")
    elseif target:is_plat("macosx") then
        target:add("frameworks", "CoreFoundation", "Metal")
    end

    -- Architecture checks
    if target:is_arch("x64", "x86_64") then
        target:add("vectorexts", "avx2")
    elseif target:is_arch("arm64", "aarch64") then
        target:add("defines", "PLATFORM_ARM")
    end

    -- Build mode
    if is_mode("debug") then
        target:set("symbols", "debug")
        target:set("optimize", "none")
        target:set("runtimes", "MDd")
    elseif is_mode("release") then
        target:set("optimize", "aggressive")
        target:set("symbols", "hidden")
        target:set("runtimes", "MD")
    end

    -- Config option checks
    if has_config("my_feature") then
        target:add("defines", "MY_FEATURE_ENABLED")
        target:add("deps", "my-feature-dep")
    end

    if has_package("spdlog") then
        target:add("packages", "spdlog")
    end

    -- Target kind checks
    if target:get("kind") == "static" then
        target:add("defines", "MYLIB_STATIC", {public = true})
    elseif target:get("kind") == "shared" then
        target:add("defines", "MYLIB_EXPORT", {public = true})
    end
end)

Standalone Condition Functions (usable in any scope)

if is_plat("windows") then ... end    -- Current target platform
if is_arch("x64") then ... end        -- Current target architecture
if is_mode("debug") then ... end      -- Current build mode
if is_os("windows") then ... end      -- Target OS (e.g., "ios", "android")
if is_host("windows") then ... end    -- Host OS running xmake
if is_subhost("msys") then ... end    -- Subsystem (e.g., "msys", "cygwin")
if is_subarch(...) then ... end       -- Subsystem architecture
if is_cross() then ... end            -- Cross-compilation check
if is_kind("static") then ... end     -- Target kind check
if is_config("var", "value") then ... end  -- Config option value check
if has_config("feature") then ... end     -- Config option exists/enabled?
if has_package("pkg") then ... end        -- Package exists/enabled?

7. Lifecycle Hooks

target("my-target")

-- Loading phase
on_load(function(target)     -- When target is loaded (early)
end)
on_config(function(target)   -- After 'xmake config', before build
end)

-- Build preparation
on_prepare(function(target)     -- Source preprocessing/code generation
end)
on_prepare_file(func)           -- Single file preprocessing
on_prepare_files(func)          -- Batch file preprocessing

-- Build phase
on_build(function(target)       -- Override entire build
end)
on_build_file(func)             -- Replace single file compilation
on_build_files(func)            -- Replace batch file compilation
on_link(function(target)        -- Custom link process
end)

-- Clean / Package / Install / Run
on_clean(function(target)
end)
on_package(function(target)
end)
on_install(function(target)
end)
on_uninstall(function(target)
end)
on_run(function(target)         -- Override 'xmake run'
end)

-- Test hooks
on_test(function(target)        -- Custom test (return true=pass)
end)

-- Before/After variants exist for all of the above:
before_build(function(target) ... end)
after_build(function(target) ... end)
before_link(function(target) ... end)
after_link(function(target) ... end)
before_install(function(target) ... end)
after_install(function(target) ... end)
-- ... etc.

Common Use of after_build — Copy DLLs

after_build(function(target)
    if is_plat("windows") then
        os.cp("path/to/mylib.dll", target:targetdir())
    elseif is_plat("linux") then
        os.cp("path/to/libmylib.so", target:targetdir())
    end
end)

8. Visibility and Inheritance

Many target:add() / target:set() calls accept a visibility table to control propagation:

-- Public: propagated to dependent targets + current target
target:add("includedirs", "include", {public = true})
target:add("defines", "PUBLIC_DEF", {public = true})
target:add("links", "mylib", {public = true})

-- Interface: only propagated to dependents (not current target)
target:add("includedirs", "include", {interface = true})

-- Private: only for current target (default)
target:add("defines", "PRIVATE_DEF", {private = true})

Dependency inheritance can be controlled per-target:

add_deps("foo", {inherit = false})     -- No inheritance from this dep
add_deps("bar", {inherit = true})      -- Default: inherit
add_deps("baz", {links = false})       -- Don't inherit links from this dep

9. Tests

target("my-test")
set_kind("binary")
add_files("test_*.cpp")

add_tests("test_foo", {
    runargs = {"--arg1", "--arg2"},
    runenvs = {PATH = "/usr/bin"},
    timeout = 30,
    group = "unit",
    pass_outputs = {"PASSED"},
    fail_outputs = {"FAILED"},
    should_fail = false,
    build_should_pass = true,
})

-- Or with custom test script
on_test(function(target)
    -- Return true for pass, false + error for fail
    local ok = os.execv("./my_test")
    if not ok then
        return false, "test failed"
    end
    return true
end)

10. Common Target Patterns

10.1 Shared Library

target("mylib")
set_kind("shared")
set_basename("mylib")
add_deps("core")
add_headerfiles("include/**.h")

on_load(function(target)
    target:add("defines", "MYLIB_EXPORT_DLL")
    target:add("includedirs", "include", {public = true})
    target:add("files", "src/*.cpp")

    if target:is_plat("windows") then
        target:add("defines", "NOMINMAX")
        target:add("syslinks", "Advapi32")
    elseif target:is_plat("macosx") then
        target:add("frameworks", "Foundation")
    end

    if has_config("enable_extra") then
        target:add("defines", "EXTRA_FEATURE")
        target:add("files", "src/extra/*.cpp")
    end
end)

if has_config("enable_pch") then
    set_pcxxheader("src/mylib_pch.h")
end
target_end()

10.2 Static Library

target("mystatic")
set_kind("static")
set_basename("mystatic")
add_deps("core")
add_headerfiles("include/**.h")
add_files("src/*.cpp")
add_defines("MYSTATIC_STATIC_LIB", {public = true})
target_end()

10.3 Executable (Binary)

target("my-tool")
set_kind("binary")
add_deps("runtime", "dsl")
add_files("main.cpp")
add_includedirs("include")

on_load(function(target)
    if has_config("enable_gui") then
        target:add("deps", "gui")
        target:add("defines", "ENABLE_GUI")
    end
end)
target_end()

10.4 Phony Target (Meta / Validation)

target("my-validator")
set_kind("phony")  -- No build output
add_deps("runtime")

on_config(function(target)
    if target:is_plat("windows") then
        local toolchain = target:toolchain("msvc")
        -- Validate SDK version, toolchain, etc.
    end
end)
target_end()

10.5 Header-only Target

target("my-headers")
set_kind("headeronly")
add_headerfiles("include/**.h")
add_includedirs("include", {public = true})
target_end()

10.6 Test Target (using a helper function)

local function test_proj(name, source, extra)
    target(name)
    set_kind("binary")
    add_deps("runtime", "dsl")
    add_files(source)
    add_includedirs("common")
    if extra then extra() end
    target_end()
end

test_proj("test_foo", "tests/test_foo.cpp")
test_proj("test_bar", "tests/test_bar.cpp", function()
    add_defines("EXTRA")
    add_deps("extra-dep")
end)

10.7 Object Target (Intermediate objects only)

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
1k
Forks
108
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
xmake
Source
github.com/luisagroup/luisacompute