# Xmake

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

- Skill: `luisagroup/xmake` (Agent Skill)
- Install (CLI): `npx skillmds@latest add luisagroup/xmake`
- Raw SKILL.md: https://api.skillmd.com/api/skills/luisagroup/xmake/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: luisagroup (https://skillmd.com/u/luisagroup)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/luisagroup/xmake

---


# XMake Build System

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

## Quick Start

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

## Configuration

| Platform | Command |
|---|---|
| Linux GCC | `xmake f -p linux -a x86_64 --toolchain=gcc -m release -c` |
| Linux Clang | `xmake f -p linux -a x86_64 --toolchain=clang -m release -c` |
| Windows MSVC | `xmake f -p windows -a x64 --toolchain=msvc -m release -c` |
| Windows Clang-CL | `xmake f -p windows -a x64 --toolchain=clang-cl -m release -c` |
| Windows LLVM | `xmake f -p windows -a x64 --toolchain=llvm -m release -c` |
| macOS Clang | `xmake 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:

```bash
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`:

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

Or from the command line:

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

Available policies:

| Policy | Sanitizer |
|---|---|
| `build.sanitizer.address` | AddressSanitizer |
| `build.sanitizer.thread` | ThreadSanitizer |
| `build.sanitizer.memory` | MemorySanitizer |
| `build.sanitizer.leak` | LeakSanitizer |
| `build.sanitizer.undefined` | UndefinedBehaviorSanitizer |

Multiple sanitizers can be combined, e.g.:

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

## Commands

| Command | Description |
|---|---|
| `xmake clean` | Clean |
| `xmake -r` | Rebuild |
| `xmake build <target>` | Build target |
| `xmake run <target>` | Run target |
| `xmake run <target> <args>` | Run target with arguments |
| `xmake -l` | List targets |
| `xmake install -o <dir>` | Install binaries to `<dir>` |
| `xmake -y` | Auto-accept all prompts (downloads, overwrites, etc.), skip interaction |
| `xmake project -k compile_commands --lsp=clangd .vscode` | Generate `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

```lua
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:

| Category | Fields |
|---|---|
| Identity | `kind`, `basename`, `filename`, `prefixname`, `suffixname`, `extension`, `group` |
| Output | `targetdir`, `objectdir`, `dependir`, `rundir`, `installdir`, `prefixdir` |
| Sources | `files`, `headerfiles`, `extrafiles`, `remove_files`, `configfiles`, `installfiles` |
| Includes | `includedirs`, `sysincludedirs` |
| Defines | `defines`, `undefines`, `configvar` |
| Links | `links`, `syslinks`, `linkdirs`, `rpathdirs`, `linkorders`, `linkgroups`, `frameworks`, `frameworkdirs` |
| Compilation | `languages`, `optimize`, `warnings`, `symbols`, `runtimes`, `exceptions`, `fpmodels`, `encodings`, `strip`, `vectorexts`, `forceincludes`, `pcheader`, `pcxxheader` |
| Dependencies | `deps`, `packages`, `options`, `rules` |
| Misc | `values.*`, `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 field | Runs when | Typical use |
|---|---|---|
| `on_load(function(target) ... end)` | Target is loaded (early) | Dynamic config, conditional deps/files |
| `on_config(function(target) ... end)` | After `xmake config`, before build | Validate toolchain/options |
| `before_build(function(target) ... end)` | Before compilation starts | Pre-build checks/code generation |
| `on_build(function(target) ... end)` | Build phase | Override entire build |
| `after_build(function(target) ... end)` | After build finishes | Copy outputs, print reports |
| `before_link(function(target) ... end)` | Before linking | Inject link args |
| `after_link(function(target) ... end)` | After linking | Sign/post-process binary |
| `on_install(function(target) ... end)` | Install phase | Custom install logic |
| `on_run(function(target) ... end)` | `xmake run` | Override 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

```lua
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:

| API | Description |
|---|---|
| `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

| API | Description |
|---|---|
| `add_ldflags(...)` | Static library/exe link flags |
| `add_arflags(...)` | Archive (static library) flags |
| `add_shflags(...)` | Dynamic library link flags |

Example with per-tool flags:

```lua
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)

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

Enable conditionally with:

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

---

## 6. Conditional Configuration with Conditions

```lua
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)

```lua
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

```lua
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

```lua
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:

```lua
-- 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:

```lua
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

```lua
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

```lua
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

```lua
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)

```lua
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)

```lua
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

```lua
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)

```lua
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)

```lua
target("my-objects")
set_kind("object")  -- Compiles sources but does not link
add_files("src/*.cpp")
target_end()
```

---

## 11. Custom Rules

`add_rules()` must be **outside** `on_load`:

```lua
target("my-target")
add_rules("c.unity_build", {batchsize = 8})    -- Unity build
add_rules("c++.unity_build", {batchsize = 8})
add_rules("utils.bin2obj", {extensions = {".cu", ".h"}})  -- Binary embedding
add_rules("build_cargo")   -- Rust/Cargo build
add_rules("lc_llvm")       -- LLVM integration
target_end()
```

### Rules with Custom Values

```lua
target("my-target")
add_rules("my-rule")
set_values("mykey", "value1", "value2")
add_values("mykey", "value3")
target_end()
```

---

## 12. The `on_load` / `on_config` Target Object

Inside lifecycle hooks, the `target` object provides these methods:

| Method | Description |
|---|---|
| `target:name()` | Get target name |
| `target:fullname()` | Get full name (with namespace) |
| `target:targetdir()` | Get output directory |
| `target:targetfile()` | Get target file path |
| `target:scriptdir()` | Get directory of the xmake.lua file |
| `target:arch()` | Get target architecture |
| `target:plat()` | Get target platform |
| `target:is_plat("windows")` | Check platform |
| `target:is_arch("x64")` | Check architecture |
| `target:is_arch64()` | Is 64-bit architecture? |
| `target:is_mode("debug")` | Check build mode (alias for `is_mode()`) |
| `target:is_cross()` | Is cross-compilation? |
| `target:has_tool("cxx", "clang")` | Check if using specific tool |
| `target:get("kind")` | Get any target property |
| `target:get_from("links", "*")` | Get values from all sources (self, deps, options, packages) |
| `target:add("key", "value", {public=true})` | Add configuration |
| `target:set("key", "value")` | Override configuration |
| `target:deps()` | Get all dependent targets (after_load only) |
| `target:dep("name")` | Get a specific dependency (after_load only) |
| `target:orderdeps({inherit=true})` | Get ordered deps |
| `target:toolchain("msvc")` | Get toolchain instance |
| `target:compiler("cxx")` | Get compiler instance |
| `target:linker()` | Get linker instance |
| `target:sourcebatches()` | Get source file batches |
| `target:objectdir()` | Get object directory |
| `target:dependir()` | Get dependency directory |
| `target:autogendir()` | Get auto-generated files directory |
| `target:data("key")` | Get user private data |
| `target:data_set("key", value)` | Set user private data |
| `target:values("name")` | Get custom values |
| `target:values_set("name", ...)` | Set custom values |
| `target:rule("name")` | Get a rule instance |
| `target:rule_enable("name", bool)` | Enable/disable a rule |
| `target:extraconf("name", "item", "key")` | Get extra configuration |
| `target:extraconf_from("name", "source")` | Get extra config from source |
| `target:pkgs()` | Get all packages |
| `target:pkg("name")` | Get a package instance |
| `target:is_kind("kind")` | Check target kind |
| `target:kind()` | Get target kind |
| `target:basename()` | Get output base name |
| `target:filename()` | Get output filename |
| `target:version()` | Get target version |
| `target:clone()` | Clone the target (after_load only) |
| `target:is_phony()` | Is phony target? |
| `target:is_binary()` | Is binary target? |
| `target:is_shared()` | Is shared library? |
| `target:is_static()` | Is static library? |
| `target:is_library()` | Is any library type? |
| `target:is_enabled()` | Is target enabled? |
| `target:is_default()` | Is default build target? |
| `target:is_rebuilt()` | Was target rebuilt? |

---

## 13. Dependencies: Options & Packages

```lua
target("my-target")
-- Option dependencies
add_options("my_option")
set_options("my_option")

-- Package dependencies (requires add_requires in root scope)
add_requires("spdlog", "fmt")
target("my-target")
add_packages("spdlog", "fmt")

-- With component selection
add_packages("sfml", {components = {"graphics", "window"}})

-- Internal target dependencies with fine-grained control
add_deps("lib-a", "lib-b", {inherit = true})   -- Full inheritance
add_deps("lib-c", {inherit = false})             -- No inheritance
add_deps("lib-d", {links = false})               -- Don't inherit links
```

---

## 14. Run Environment

```lua
target("my-target")
set_runenv("PATH", "/custom/path")       -- Override environment variable
add_runenvs("PATH", "/extra/path")       -- Append to environment variable
```

---

## 15. File Management

```lua
target("my-target")
add_files("src/*.cpp")
add_files("src/*.cpp", {sourcekind = "cxx"})  -- With per-file options
add_files("src/*.m", {sourcekind = "mxx"})    -- ObjC++ files
remove_files("src/old.cpp")                   -- Remove previously added files
add_headerfiles("include/**.h")
remove_headerfiles("include/deprecated.h")
add_installfiles("config/*.ini")
add_configfiles("config.h.in")    -- Template config files with @var@ substitution
add_extrafiles("README.md")       -- Extra files for IDE listing
add_forceincludes("precompiled.h") -- Force-include header
```

---

## 16. Complete Example

```lua
-- Root xmake.lua
set_xmakever("3.0.6")
add_rules("mode.release", "mode.debug")
add_requires("spdlog")

-- Library target
target("mylib")
set_kind("shared")
set_basename("mylib")
add_deps("core")
add_headerfiles("include/**.h")
add_rules("c++.unity_build", {batchsize = 8})

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

on_load(function(target)
    -- Source files
    target:add("files", "src/*.cpp")
    if has_config("enable_extra") then
        target:add("files", "src/extra/*.cpp")
        target:add("defines", "ENABLE_EXTRA")
    end

    -- Public include dirs and defines
    target:add("includedirs", "include", {public = true})

    -- Platform config
    if target:is_plat("windows") then
        target:add("defines", "NOMINMAX", "PLATFORM_WIN", "MYLIB_EXPORT_DLL")
        target:add("syslinks", "Advapi32", "Ole32")
        target:add("cxflags", "/Zc:preprocessor", {tools = "cl"})
    elseif target:is_plat("macosx") then
        target:add("frameworks", "Foundation", "Metal")
        target:add("cxflags", "-fobjc-arc")
    elseif target:is_plat("linux") then
        target:add("syslinks", "dl", "pthread", "uuid")
        target:add("cxflags", "-fPIC")
    end

    -- Arch config
    if target:is_arch("x64", "x86_64") then
        target:add("vectorexts", "avx2")
    end

    -- Package dependencies
    if has_config("use_xrepo_spdlog") then
        target:add("packages", "spdlog")
    else
        target:add("deps", "spdlog-bundled")
    end
end)

after_build(function(target)
    if is_plat("windows") then
        os.cp("$(buildir)/mydep.dll", target:targetdir())
    end
end)
target_end()

-- Test executable
target("test-mylib")
set_kind("binary")
add_deps("mylib")
add_files("tests/*.cpp")

add_tests("test_basic", {
    runargs = {"--verbose"},
    group = "unit",
})

on_load(function(target)
    target:add("includedirs", "tests")
end)
target_end()
```

---

## Summary

1. **Use `on_load` for conditional logic** — platform checks, feature flags, dynamic file lists.
2. **`add_rules()` stays outside** — cannot be set from inside `on_load`.
3. **Simple globs outside, conditional additions inside** — keep `add_files`/`add_headerfiles` outside for simple cases.
4. **Prefer `target:set()` / `target:add()` inside `on_load`** for most configuration — it's equivalent to outside calls.
5. **Visibility** — `{public = true}` propagates to dependents, `{interface = true}` propagates only to dependents, `{private = true}` (default) is local-only.
6. **`add_deps()` outside = `target:add("deps", ...)` inside** — choose whichever fits your style.
7. **All APIs listed here work at the target scope level** — use them outside `on_load` as `set_kind(...)` or inside as `target:set("kind", ...)`.---

# Lua Scripting in xmake

> Reference: `D:/xmake/core/sandbox/modules/`, `D:/xmake/modules/`, `D:/xmake/core/base/`

xmake scripts (in `on_load`, `on_build`, `after_install`, etc.) run in a **sandboxed Lua environment**. This section documents all available built-in modules and APIs.

---

## 1. Built-in Sandbox Modules

### 1.1 `print` / `printf` — Output

```lua
print("hello", "world")       -- Print with newline
printf("hello %s", "world")   -- Print without newline
vprint("verbose msg")         -- Only printed with -v/--verbose
dprint("diagnosis msg")       -- Only printed with --diagnosis
```

### 1.2 `cprint` / `cprintf` — Colored Output

```lua
cprint("${bright}hello${reset}")           -- Bright text
cprint("${red}error${reset}")               -- Red text
cprint("${color.dump.string}hello")         -- Dump color
cprint("${dim}%s${reset}", "world")         -- Dim text
```

Available color tags: `${red}`, `${green}`, `${blue}`, `${yellow}`, `${magenta}`, `${cyan}`, `${bright}`, `${dim}`, `${reset}`, `${underline}`, etc.

### 1.3 `utils` — Utilities

```lua
utils.dump(obj)                    -- Dump object for debugging
utils.assert(value, "msg", ...)    -- Assert with error message
utils.error("err %s", arg)         -- Error message
utils.warning("warn %s", arg)      -- Warning message
utils.trycall(func)                -- Call function safely (returns ok, ...)
```

### 1.4 Path Operations

```lua
path.join("a", "b", "c")          -- "a/b/c" (OS-aware)
path.join("a", "..", "b")         -- "b"
path.absolute("rel/path")         -- Full absolute path
path.relative("/abs/path", "/base") -- Relative path from base
path.basename("foo/bar.cpp")      -- "bar.cpp"
path.filename("foo/bar.cpp")      -- "bar"
path.extension("foo/bar.cpp")     -- ".cpp"
path.directory("foo/bar.cpp")     -- "foo"
path.normalize("a/./b/../c")     -- "a/c"
```

### 1.5 `string` — String Operations

All standard Lua string functions are available. Extended functions:

```lua
string.vformat("$(var) hello", ...) -- Format with built-in variables
string.format("hello %s", "world")  -- Standard Lua format
-- All standard: sub, gsub, find, match, gmatch, upper, lower, rep, reverse, char, byte, len, split
```

**Built-in variables** (resolved in strings via `$()` or vformat):

| Variable | Description |
|---|---|
| `$(host)` | Host OS (windows, linux, macosx) |
| `$(tmpdir)` | Temp directory |
| `$(curdir)` | Current directory |
| `$(scriptdir)` | Directory of the current xmake.lua |
| `$(projectdir)` | Project root directory |
| `$(buildir)` | Build output directory |
| `$(globaldir)` | Global xmake directory |
| `$(programdir)` | xmake installation directory |

Example:
```lua
path.join("$(projectdir)", "build")  -- Resolves to /path/to/project/build
print("$(scriptdir)")                -- Prints script directory
```

### 1.6 `table` — Table Operations

```lua
table.join(t1, t2)                    -- Merge tables (new table)
table.join2(t1, t2)                   -- Merge into t1 (in-place)
table.clone(t)                        -- Deep clone
table.wrap(v)                         -- Wrap single value as table {v}
table.unwrap({v})                     -- Unwrap table to single value
table.contains(t, value)               -- Check if value exists
table.unique(t)                        -- Remove duplicates
table.reverse(t)                       -- Reverse array
table.slice(t, first, last)            -- Slice array
table.is_array(t)                      -- Is array-like?
table.is_dictionary(t)                 -- Is dict-like?
table.keys(t)                          -- Get keys array
table.values(t)                        -- Get values array
table.pack(...)                        -- Pack arguments (like {...} but with .n)
table.map(t, mapper)                   -- Map values
table.imap(t, mapper)                  -- In-place map
table.find(t, value)                   -- Find index of value
table.find_if(t, pred)                 -- Find if predicate matches
table.remove_if(t, pred)               -- Remove if predicate matches
table.empty(t)                         -- Is empty?
table.orderkeys(t, callback)           -- Ordered keys
table.orderpairs(t, callback)          -- Ordered pairs iterator
table.inherit(...)                     -- Prototype-based inheritance
```

### 1.7 `os` — Operating System

#### File/Directory Operations

```lua
os.cp("src/file", "dst/file")          -- Copy file/dir
os.mv("src/file", "dst/file")          -- Move file/dir
os.rm("file_or_dir")                    -- Remove file/dir
os.ln("target", "symlink")             -- Create symlink
os.mkdir("dir")                        -- Create directory
os.rmdir("dir")                        -- Remove directory
os.cd("dir")                           -- Change directory (returns old cwd)
os.touch("file")                       -- Touch file
os.isfile("path")                      -- Is file?
os.isdir("path")                       -- Is directory?
os.islink("path")                      -- Is symlink?
os.isexec("path")                      -- Is executable?
os.exists("path")                      -- Exists?
os.readlink("symlink")                 -- Read symlink target
os.filesize("file")                    -- File size
os.mtime("file")                       -- Modification time
```

#### File Matching (Globbing)

```lua
os.files("src/*.cpp")                  -- Match .cpp files
os.dirs("src/*")                       -- Match directories
os.filedirs("src/*")                   -- Match files and dirs
os.match("src/*.c", "file")            -- Match with mode ("file", "dir", "alldir")
```

#### Running Commands

```lua
-- Run command, raise on failure
os.run("gcc -c %s -o %s", "file.c", "file.o")
os.runv("gcc", {"-c", "file.c", "-o", "file.o"})

-- Run command, return output
local out, err = os.iorun("echo hello")
local out, err = os.iorunv("python", {"--version"})

-- Run command, capture exit code
local exitok, errors = os.exec("ls")
local exitok, errors = os.execv("python", {"script.py"})

-- Verbose variants (print command if -v enabled)
os.vrun("gcc %s", "file.c")
os.vrunv("gcc", {"-c", "file.c"})
os.vexec("echo hello")
os.vexecv("echo", {"hello"})

-- Try variants (no raise on failure)
os.trycp("src", "dst")
os.trymv("src", "dst")
os.tryrm("file")
```

#### Environment Variables

```lua
os.getenv("PATH")                      -- Get env var
os.setenv("MY_VAR", "value")           -- Set env var (override)
os.addenv("PATH", "/new/path")         -- Append to env var
os.getenvs()                           -- Get all env vars
os.setenvs({PATH = "/usr/bin"})        -- Set multiple env vars
os.addenvs({PATH = "/new/path"})       -- Append multiple env vars
os.joinenvs({PATH = "/a:/b"})          -- Join env values
```

#### Directory/System Info

```lua
os.curdir()                            -- Current directory
os.scriptdir()                         -- Directory of current xmake.lua
os.projectdir()                        -- Project root directory
os.tmpdir()                            -- System temp directory
os.tmpfile("key")                      -- Generate temp file path
os.host()                              -- Host OS name
os.arch()                              -- Host architecture
os.subhost()                           -- Subsystem host
os.subarch()                           -- Subsystem arch
os.is_host("windows")                  -- Check host OS
os.is_arch("x64")                      -- Check host arch
os.is_subhost("msys")                  -- Check subsystem
os.isroot()                            -- Is running as root?
os.fscase()                            -- Is filesystem case-sensitive?
os.mclock()                            -- CPU clock (ms)
os.sleep(1000)                         -- Sleep ms (coroutine-safe)
os.nuldev()                            -- Null device path
os.xmakever()                          -- xmake version (semver)
os.args({"-a", "-b"})                  -- Format args array to string
os.getpid()                            -- Current process ID
os.cpuinfo()                           -- CPU info table
os.meminfo()                           -- Memory info table
```

### 1.8 `io` — File I/O

```lua
-- Read/write entire files
local data = io.read

…(truncated)
