aboutsummaryrefslogtreecommitdiff
path: root/xmake.lua
blob: 3b22169e5cbc8e0390dcf5556aa0c26ef2fdc554 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
-- Copyright Epic Games, Inc. All Rights Reserved.

set_configvar("ZEN_SCHEMA_VERSION", 5) -- force state wipe after 0.2.31 causing bad data (dan.engelbrecht)
set_configvar("ZEN_DATA_FORCE_SCRUB_VERSION", 0)

set_allowedplats("windows", "linux", "macosx")
set_allowedarchs("windows|x64", "linux|x86_64", "macosx|x86_64", "macosx|arm64")

-- Returns true when building for Windows with native MSVC (not clang-cl cross-compilation)
function is_native_msvc()
    local tc = get_config("toolchain") or ""
    return is_plat("windows") and tc ~= "clang-cl"
end

--------------------------------------------------------------------------
-- We support debug and release modes. On Windows we use static CRT to
-- minimize dependencies.

set_allowedmodes("debug", "release")
add_rules("mode.debug", "mode.release")

-- Disable xmake's run.autobuild (on by default in xmake 3.x). Auto-rebuild
-- before run is dangerous: if the current config differs from the build that
-- produced the binary (e.g. ASAN was toggled), xmake can rebuild with wrong
-- flags, causing linker errors (e.g. MSVC annotate_string mismatch). Users
-- are expected to run `xmake build` explicitly before `xmake run`.
set_policy("run.autobuild", false)

if is_plat("windows") then
    if false then
        -- DLL runtime
        if is_mode("debug") then
            set_runtimes("MDd") 
        else
            set_runtimes("MD")
        end
    else
        -- static runtime
        if is_mode("debug") then
            set_runtimes("MTd") 
        else
            set_runtimes("MT")
        end
    end
end

-- Sanitizers
--
-- https://xmake.io/api/description/builtin-policies.html#build-sanitizer-address
--
-- Each sanitizer has a .supp file (asan.supp, tsan.supp, msan.supp). A single
-- sanitizer.options rule handles all three in one before_run callback.
--
-- asan.supp and msan.supp contain key=value runtime options (no native
-- "options from file" mechanism exists for either sanitizer on any platform).
--
-- tsan.supp is a mixed-format file: key=value runtime options (parsed by xmake
-- and set in TSAN_OPTIONS) plus native TSAN suppression patterns (race:...,
-- mutex:...) that are passed via TSAN_OPTIONS=suppressions=<path> on Linux/Mac.
-- The TSAN runtime silently skips key=value lines when parsing the suppression
-- file (they don't match type:pattern; TSAN emits a verbose-level warning).
--
-- IMPORTANT: The env vars are set by xmake's before_run hook. If you run a
-- binary directly (not via xmake run / xmake test) you must set them manually.

--------------------------------------------------------------------------

-- AddressSanitizer is supported on Windows (MSVC 2019+), Linux, and MacOS.
-- Enable with: `xmake config --asan=y` or `xmake test --asan`
-- (automatically disables mimalloc and sentry, which are incompatible with ASAN)
--
-- Options live in asan.supp. For manual runs:
--   Linux/Mac:  ASAN_OPTIONS="new_delete_type_mismatch=0" ./zenserver
--   Windows:    set ASAN_OPTIONS=new_delete_type_mismatch=0

option("asan")
    set_default(false)
    set_showmenu(true)
    set_description("Enable AddressSanitizer (disables mimalloc and sentry)")
option_end()

-- Global flag: read by sub-target xmake.lua files (like enable_unity)
use_asan = has_config("asan")

if use_asan then
    set_policy("build.sanitizer.address", true)
    -- On Windows/MSVC, explicitly define __SANITIZE_ADDRESS__ so that
    -- IntelliSense in VS sees it (XmakeDefines -> PreprocessorDefinitions).
    -- MSVC sets it implicitly at compile time via /fsanitize=address, but
    -- vsxmake IntelliSense needs it in the explicit defines list.
    -- Not needed on Clang/GCC: they set it implicitly and adding it via -D
    -- would cause a macro-redefinition warning (error with -Werror).
    if is_os("windows") then
        add_defines("__SANITIZE_ADDRESS__=1")
    end
end

--------------------------------------------------------------------------

-- ThreadSanitizer is supported on Linux and MacOS only.
-- Enable with: `xmake config --tsan=y` or `xmake test --tsan`
--
-- Key=value options and suppression patterns both live in tsan.supp. For
-- manual runs: TSAN_OPTIONS="detect_deadlocks=0:suppressions=$(pwd)/tsan.supp"

option("tsan")
    set_default(false)
    set_showmenu(true)
    set_description("Enable ThreadSanitizer (Linux/Mac only)")
option_end()

use_tsan = has_config("tsan")

if use_tsan then
    if is_os("windows") then
        raise("TSAN is not supported on Windows. Use Linux or macOS.")
    end
    set_policy("build.sanitizer.thread", true)
end

--------------------------------------------------------------------------

-- MemorySanitizer is supported on Linux only. In practice it rarely works
-- because all dependencies must also be compiled with MSan instrumentation.
-- Enable with: `xmake config --msan=y` or `xmake test --msan`
--
-- Options live in msan.supp. For manual runs: set MSAN_OPTIONS manually.

option("msan")
    set_default(false)
    set_showmenu(true)
    set_description("Enable MemorySanitizer (Linux only, requires all deps instrumented)")
option_end()

use_msan = has_config("msan")

if use_msan then
    set_policy("build.sanitizer.memory", true)
end

--------------------------------------------------------------------------

-- Single rule handles all three sanitizer env vars. parse_supp_opts is defined
-- once inside before_run (scripting scope) where io is a valid global.

rule("sanitizer.options")
    before_run(function(target)
        -- Parses key=value lines from a .supp file. Blank lines and lines whose
        -- first non-whitespace character is '#' are ignored. Suppression-pattern
        -- lines (race:..., mutex:...) don't match ^[%w_]+= and are skipped.
        local function parse_supp_opts(filepath)
            local opts = {}
            local content = os.isfile(filepath) and io.readfile(filepath) or ""
            for line in content:gmatch("[^\r\n]+") do
                line = line:match("^%s*(.-)%s*$")
                if line ~= "" and not line:match("^#") and line:match("^[%w_]+=") then
                    table.insert(opts, line)
                end
            end
            return opts
        end

        if target:policy("build.sanitizer.address") and not os.getenv("ASAN_OPTIONS") then
            local opts = parse_supp_opts(path.join(os.projectdir(), "asan.supp"))
            if #opts > 0 then
                os.setenv("ASAN_OPTIONS", table.concat(opts, ":"))
            end
        end

        if target:policy("build.sanitizer.thread") and not os.getenv("TSAN_OPTIONS") then
            local suppfile = path.join(os.projectdir(), "tsan.supp")
            local opts = parse_supp_opts(suppfile)
            -- Also pass the suppression patterns via the native file mechanism when
            -- both conditions hold:
            --   1. Not clang-cl: file path support requires the LLVM/GCC TSAN runtime.
            --   2. Platform is Linux or macOS: TSAN is unavailable on Windows
            --      (config-time raise() above prevents reaching here on Windows).
            local tc_name = get_config("toolchain") or ""
            local is_not_clang_cl = tc_name ~= "clang-cl"
            if os.isfile(suppfile) and is_not_clang_cl
                    and (target:is_plat("linux") or target:is_plat("macosx")) then
                table.insert(opts, "suppressions=" .. suppfile)
            end
            if #opts > 0 then
                os.setenv("TSAN_OPTIONS", table.concat(opts, ":"))
            end
        end

        if target:policy("build.sanitizer.memory") and not os.getenv("MSAN_OPTIONS") then
            local opts = parse_supp_opts(path.join(os.projectdir(), "msan.supp"))
            if #opts > 0 then
                os.setenv("MSAN_OPTIONS", table.concat(opts, ":"))
            end
        end
    end)
rule_end()
add_rules("sanitizer.options")

--set_policy("build.sanitizer.leak", true)
--set_policy("build.sanitizer.undefined", true)

--------------------------------------------------------------------------
-- Dependencies

add_repositories("zen-repo repo")
set_policy("build.ccache", false)
set_policy("package.precompiled", false)

add_defines("gsl_FEATURE_GSL_COMPATIBILITY_MODE=1") 
add_requires("gsl-lite", {system = false})
add_requires("http_parser", {system = false})
add_requires("json11", {system = false})
add_requires("lua", {system = false})
add_requires("lz4", {system = false})
add_requires("xxhash", {system = false})
add_requires("zlib", {system = false})

add_defines("EASTL_STD_ITERATOR_CATEGORY_ENABLED", "EASTL_DEPRECATIONS_FOR_2024_APRIL=EA_DISABLED")
add_requires("eastl", {system = false})

add_requires("consul", {system = false}) -- for hub tests
add_requires("minio RELEASE.2025-07-23T15-54-02Z", {system = false}) -- for S3 storage tests
add_requires("nomad", {system = false}) -- for nomad provisioner tests
add_requires("oidctoken", {system = false})

if has_config("zenmimalloc") and not use_asan then
    add_requires("mimalloc", {system = false})
end

--------------------------------------------------------------------------
-- Crypto configuration. For reasons unknown each platform needs a 
-- different package

if is_plat("windows") then
    -- we use schannel on Windows
    add_defines("ZEN_USE_OPENSSL=0")
    add_requires("libcurl", {system = false})
elseif is_plat("linux", "macosx") then
    add_requires("openssl3", {system = false})
    add_defines("ZEN_USE_OPENSSL=1")
    add_requires("libcurl", {system = false, configs = {openssl3 = true}})
end

--------------------------------------------------------------------------

if is_plat("windows") then
    -- for bundling, Linux tries to compile from source which fails with UE toolchain, 
    -- fallback is regular zip
    add_requires("7z")
end

-- When using the UE clang toolchain, statically link the toolchain's libc++ and
-- libc++abi to avoid ABI mismatches with system libraries at runtime.
-- These are project-level flags (not in the toolchain definition) so they don't
-- propagate into cmake package builds.
if is_plat("linux") and get_config("toolchain") == "ue-clang" then
    add_ldflags("-static-libstdc++", {force = true})
    add_ldflags("$(projectdir)/thirdparty/ue-libcxx/lib64/libc++.a", {force = true})
    add_ldflags("$(projectdir)/thirdparty/ue-libcxx/lib64/libc++abi.a", {force = true})
    add_ldflags("-lpthread", {force = true})
end

if has_config("zensentry") and not use_asan then
    if is_plat("linux") then
        add_requires("sentry-native 0.12.1", {configs = {backend = "crashpad"}})
    elseif is_plat("windows") then
        add_requires("sentry-native 0.12.1", {debug = is_mode("debug"), configs = {backend = "crashpad"}})
    else
        add_requires("sentry-native 0.12.1", {configs = {backend = "crashpad"}})
    end
end

enable_unity = false

--add_rules("c++.unity_build")

if is_mode("release") then
    -- LTO does not appear to work with the current Linux UE toolchain
    -- Also, disabled LTO on Mac to reduce time spent building openssl tests
    -- Disabled for cross-compilation (clang-cl on Linux) due to cmake package compat issues
    local is_cross_win = is_plat("windows") and is_host("linux")
    if not is_plat("linux", "macosx") and not is_cross_win then
        set_policy("build.optimization.lto", true)
    end
    set_optimize("fastest")
end

if is_mode("debug") then
    add_defines("DEBUG")
end

if is_mode("debug") then
    add_defines("ZEN_WITH_TESTS=1")
else
    add_defines("ZEN_WITH_TESTS=0")
end

-- fmt 11+ requires utf-8 when using unicode
if is_os("windows") then
    set_encodings("utf-8")
else
    set_encodings("source:utf-8", "target:utf-8")
end

-- When cross-compiling with clang-cl, the xwin SDK may ship a newer MSVC STL
-- than the host clang version supports. Bypass the version gate.
if is_plat("windows") and not is_native_msvc() then
    add_defines("_ALLOW_COMPILER_AND_STL_VERSION_MISMATCH")
end

if is_os("windows") then
    add_defines(
        "_CRT_SECURE_NO_WARNINGS",
        "_UNICODE",
        "UNICODE",
        "_CONSOLE",
        "NOMINMAX",             -- stop Windows SDK defining 'min' and 'max'
        "NOGDI",                -- otherwise Windows.h defines 'GetObject'
        "WIN32_LEAN_AND_MEAN",  -- cut down Windows.h
        "_WIN32_WINNT=0x0A00",
        "_WINSOCK_DEPRECATED_NO_WARNINGS" -- let us use the ANSI functions
    )

    -- Make builds more deterministic and portable (MSVC-only flags)
    if is_native_msvc() then
        add_cxxflags("/d1trimfile:$(curdir)\\")     -- eliminates the base path from __FILE__ paths
        add_cxxflags("/experimental:deterministic") -- (more) deterministic compiler output
        add_ldflags("/PDBALTPATH:%_PDB%")           -- deterministic pdb reference in exe
        add_cxxflags("/Zc:u8EscapeEncoding")        -- Enable UTF-8 encoding for u8 string literals (clang does this by default)
        add_cxxflags("/Zc:preprocessor")            -- Enable preprocessor conformance mode
        add_cxxflags("/Zc:inline")                  -- Enforce inline semantics
    end

    -- add_ldflags("/MAP")
end

-- Clang warning suppressions (native clang on Linux/Mac, or clang-cl cross-compile)
if is_os("linux") or is_os("macosx") or not is_native_msvc() then
    -- Silence warnings about unrecognized -Wno-* flags on older clang versions
    add_cxxflags("-Wno-unknown-warning-option", {force = true})
    add_cxxflags("-Wno-delete-non-abstract-non-virtual-dtor", {force = true})
    add_cxxflags("-Wno-format", {force = true})
    add_cxxflags("-Wno-implicit-fallthrough", {force = true})
    add_cxxflags("-Wno-inconsistent-missing-override", {force = true})
    add_cxxflags("-Wno-missing-field-initializers", {force = true})
    add_cxxflags("-Wno-nonportable-include-path", {force = true})
    add_cxxflags("-Wno-sign-compare", {force = true})
    add_cxxflags("-Wno-strict-aliasing", {force = true})
    add_cxxflags("-Wno-switch", {force = true})
    add_cxxflags("-Wno-unused-lambda-capture", {force = true})
    add_cxxflags("-Wno-unused-private-field", {force = true})
    add_cxxflags("-Wno-unused-value", {force = true})
    add_cxxflags("-Wno-unused-variable", {force = true})
    add_cxxflags("-Wno-vla-cxx-extension", {force = true})
    -- GCC false positive: constinit static locals used by reference are reported as unused-but-set
    add_cxxflags("-Wno-unused-but-set-variable", {tools = "gcc"})
end

-- Additional suppressions specific to clang-cl cross-compilation
if get_config("toolchain") == "clang-cl" then
    add_cxxflags("-Wno-cast-function-type-mismatch", {force = true})
    add_cxxflags("-Wno-parentheses-equality", {force = true})
    add_cxxflags("-Wno-reorder-ctor", {force = true})
    add_cxxflags("-Wno-unused-but-set-variable", {force = true})
    add_cxxflags("-Wno-unused-parameter", {force = true})
    add_cflags("-Wno-unknown-warning-option", {force = true})
    add_cflags("-Wno-unused-command-line-argument", {force = true})
end

if is_os("linux") then
    add_defines("_GNU_SOURCE")
end

-- Turn use of undefined cpp macros into errors
if is_os("windows") then
    add_cxxflags("/we4668")
else
    add_cxxflags("-Wundef")
end

function add_define_by_config(define, config_name)
    local value = has_config(config_name) and 1 or 0
    add_defines(define.."="..value)
end

option("zensentry")
    set_default(true)
    set_showmenu(true)
    set_description("Enables Sentry support (incompatible with ASAN)")
option_end()
add_defines("ZEN_USE_SENTRY=" .. ((has_config("zensentry") and not use_asan) and "1" or "0"))

option("zenmimalloc")
    set_default(true)
    set_showmenu(true)
    set_description("Use MiMalloc for faster memory management (incompatible with ASAN)")
option_end()
add_defines("ZEN_USE_MIMALLOC=" .. ((has_config("zenmimalloc") and not use_asan) and "1" or "0"))

option("zenrpmalloc")
    set_default(true)
    set_showmenu(true)
    set_description("Use rpmalloc for faster memory management")
option_end()
add_define_by_config("ZEN_USE_RPMALLOC", "zenrpmalloc")

if is_os("windows") then
    option("httpsys")
        set_default(true)
        set_showmenu(true)
        set_description("Enable http.sys server")
    option_end()
    add_define_by_config("ZEN_WITH_HTTPSYS", "httpsys")
else
    add_defines("ZEN_WITH_HTTPSYS=0")
end

-- Note that this controls the defaults here but currently
-- releases explicitly disable compute services since they are 
-- WIP and should not be enabled widely yet.

local compute_default = true

option("zencompute")
    set_default(compute_default)
    set_showmenu(true)
    set_description("Enable compute services endpoint")
option_end()
add_define_by_config("ZEN_WITH_COMPUTE_SERVICES", "zencompute")

option("zenhorde")
    set_default(compute_default)
    set_showmenu(true)
    set_description("Enable Horde worker provisioning")
option_end()
add_define_by_config("ZEN_WITH_HORDE", "zenhorde")

option("zennomad")
    set_default(compute_default)
    set_showmenu(true)
    set_description("Enable Nomad worker provisioning")
option_end()
add_define_by_config("ZEN_WITH_NOMAD", "zennomad")


if is_os("windows") then
    add_defines("UE_MEMORY_TRACE_AVAILABLE=1")
    option("zenmemtrack")
        set_default(true)
        set_showmenu(true)
        set_description("Enable UE's Memory Trace support")
    option_end()
    add_define_by_config("ZEN_WITH_MEMTRACK", "zenmemtrack")
else
    add_defines("ZEN_WITH_MEMTRACK=0")
end

option("zentrace")
    set_default(true)
    set_showmenu(true)
    set_description("Enable UE's Trace support")
option_end()
add_define_by_config("ZEN_WITH_TRACE", "zentrace")

option("zencpr")
    set_default(true)
    set_showmenu(true)
    set_description("Enable CPR HTTP client backend")
option_end()
add_define_by_config("ZEN_WITH_CPR", "zencpr")

set_warnings("allextra", "error")
set_languages("cxx20")

-- always generate debug information
set_symbols("debug")

includes("toolchains")

-- Auto-select the UE clang toolchain on Linux when the SDK is available
if is_plat("linux") and not get_config("toolchain") then
    local ue_sdk = os.getenv("UE_TOOLCHAIN_DIR")
    if not ue_sdk or ue_sdk == "" then
        local home = os.getenv("HOME")
        if home then
            local default_path = path.join(home, ".ue-toolchain")
            if os.isdir(default_path) then
                ue_sdk = default_path
            end
        end
    end
    if ue_sdk and ue_sdk ~= "" and os.isdir(ue_sdk) then
        set_toolchains("ue-clang")
    end
end

includes("thirdparty")
includes("src/transports")
includes("src/zenbase")
includes("src/zencore",             "src/zencore-test")
includes("src/zenhttp",             "src/zenhttp-test")
includes("src/zennet",              "src/zennet-test")
includes("src/zenremotestore",      "src/zenremotestore-test")
includes("src/zencompute",          "src/zencompute-test")
if has_config("zenhorde") then
    includes("src/zenhorde")
end
if has_config("zennomad") then
    includes("src/zennomad")
end
includes("src/zens3-testbed")
includes("src/zenstore",            "src/zenstore-test")
includes("src/zentelemetry",        "src/zentelemetry-test")
includes("src/zenutil",             "src/zenutil-test")
if is_plat("windows") then
    includes("src/zenvfs")
end
includes("src/zenserver",           "src/zenserver-test")
includes("src/zen")
includes("src/zentest-appstub")

--------------------------------------------------------------------------

task("bundle")
    set_menu {
        usage = "xmake bundle",
        description = "Create Zip bundle from binaries",
        options = {
            {nil, "withtrace", "k", nil, "Compiles with trace support"},
            {nil, "codesignidentity", "v", nil, "Code signing identity"},
        }
    }
    on_run(function ()
        import("scripts.bundle")
        bundle()
    end)

task("docker")
    set_menu {
        usage = "xmake docker [--push] [--no-wine] [--win-binary PATH] [--tag TAG] [--registry REGISTRY]",
        description = "Build Docker image for zenserver compute workers",
        options = {
            {nil, "push", "k", nil, "Push the image after building"},
            {nil, "no-wine", "k", nil, "Build without Wine (smaller image, Linux-only workers)"},
            {nil, "win-binary", "v", nil, "Path to Windows zenserver.exe to include in image"},
            {nil, "tag", "v", nil, "Override image tag (default: version from VERSION.txt)"},
            {nil, "registry", "v", nil, "Registry prefix (e.g. ghcr.io/epicgames)"},
        }
    }
    on_run(function ()
        import("scripts.docker")
        docker()
    end)

task("kill")
    set_menu {
        usage = "xmake kill",
        description = "Terminate any running zenserver instances",
    }
    on_run(function ()
        local ok = try { function() os.execv("xmake", {"run", "zen", "down", "--all"}) end }
        if ok then return end
        if is_host("windows") then
            ok = try { function() os.runv("taskkill", {"/F", "/IM", "zenserver.exe"}) end }
            if ok then print("zenserver terminated") end
        else
            ok = try { function() os.runv("pkill", {"-f", "zenserver"}) end }
            if ok then print("zenserver terminated") end
        end
    end)

task("precommit")
    set_menu {
        usage = "xmake precommit",
        description = "Run required pre-commit steps (clang-format, etc)",
    }
    on_run(function ()
        print(os.exec("python -m pre_commit run --all-files"))
    end)

task("sln")
    set_menu {
        usage = "xmake sln [vsXXXX|xcode|vscode] [--open]",
        description = "Generate IDE project files (default: vs2022 on Windows, xcode on macOS, vscode on Linux)",
        options = {
            {nil, "arguments", "vs", nil, "IDE kind (vsXXXX, xcode, vscode) and flags (--open)"},
        }
    }
    on_run(function ()
        import("core.base.option")
        local open = false
        local kind = nil
        for _, v in ipairs(option.get("arguments") or {}) do
            if v == "--open" or v == "-o" then
                open = true
            elseif not v:startswith("-") then
                kind = v
            end
        end
        if not kind then
            if os.is_host("windows") then
                kind = "vs2022"
            elseif os.is_host("macosx") then
                kind = "xcode"
            else
                kind = "vscode"
            end
        end

        if kind == "vscode" then
            os.exec("xmake project --yes --kind=compile_commands --lsp=clangd")
            printf("generated compile_commands.json\n")
        elseif kind == "xcode" then
            os.exec("xmake project --yes --kind=xcode -m release,debug -a x64,arm64")
            if open then
                local xcproj = path.join(os.projectdir(), path.filename(os.projectdir()) .. ".xcodeproj")
                printf("opening %s\n", xcproj)
                os.exec("open \"%s\"", xcproj)
            end
        elseif kind:startswith("vs") then
            local vs_ver = kind:sub(3)
            if vs_ver == "" then
                raise("'vs' requires a version, e.g. vs2022")
            end
            local xmake_kind = "vsxmake" .. vs_ver
            os.exec("xmake project --yes --kind=" .. xmake_kind .. " -m release,debug -a x64")
            if open then
                local sln = path.join(os.projectdir(), xmake_kind, path.filename(os.projectdir()) .. ".sln")
                printf("opening %s\n", sln)
                try { function() os.execv("explorer", {sln}) end, catch { function() end } }
            end
        else
            raise("Unknown kind '%s'. Expected vsXXXX (e.g. vs2022), xcode, or vscode.", kind)
        end
    end)

task("test")
    set_menu {
        usage = "xmake test --run=[name|all] [-- extra-args...] (use --list to see available tests)",
        description = "Run Zen tests",
        options = {
            {'r', "run", "kv", "all", "Run test(s) - comma-separated"},
            {'l', "list", "k", nil, "List available test names"},
            {'j', "junit", "k", nil, "Enable junit report output"},
            {'n', "noskip", "k", nil, "Run skipped tests (passes --no-skip to doctest)"},
            {nil, "repeat", "kv", nil, "Repeat tests N times (stops on first failure)"},
            {'V', "output", "k", nil, "Route child process output to stdout (zenserver-test)"},
            {nil, "asan", "k", nil, "Enable AddressSanitizer (disables mimalloc and sentry)"},
            {nil, "tsan", "k", nil, "Enable ThreadSanitizer (Linux/Mac only)"},
            {nil, "msan", "k", nil, "Enable MemorySanitizer (Linux only, requires all deps instrumented)"},
            {nil, "arguments", "vs", nil, "Extra arguments passed to test runners (after --)"}
        }
    }
    on_run(function()
        import("scripts.test")
        test()
    end)