diff options
| author | Stefan Boberg <[email protected]> | 2026-03-16 10:56:11 +0100 |
|---|---|---|
| committer | GitHub Enterprise <[email protected]> | 2026-03-16 10:56:11 +0100 |
| commit | 8c3ba4e8c522d119df3cb48966e36c0eaa80aeb9 (patch) | |
| tree | cf51b07e097904044b4bf65bc3fe0ad14134074f /xmake.lua | |
| parent | Merge branch 'sb/no-network' of https://github.ol.epicgames.net/ue-foundation... (diff) | |
| parent | Enable cross compilation of Windows targets on Linux (#839) (diff) | |
| download | zen-sb/no-network.tar.xz zen-sb/no-network.zip | |
Merge branch 'main' into sb/no-networksb/no-network
Diffstat (limited to 'xmake.lua')
| -rw-r--r-- | xmake.lua | 532 |
1 files changed, 99 insertions, 433 deletions
@@ -6,6 +6,12 @@ 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. @@ -48,12 +54,27 @@ set_policy("build.sanitizer.address", use_asan) -- ThreadSanitizer, MemorySanitizer, LeakSanitizer, and UndefinedBehaviorSanitizer -- are supported on Linux and MacOS only. +-- +-- You can enable these by editing the xmake.lua directly, or by passing the +-- appropriate flags on the command line: +-- +-- `xmake --policies=build.sanitizer.thread:y` for ThreadSanitizer, +-- `xmake --policies=build.sanitizer.memory:y` for MemorySanitizer, etc. ---set_policy("build.sanitizer.thread", true) ---set_policy("build.sanitizer.memory", true) +-- When using TSAN you will want to also use the suppression tile to silence +-- known benign races. You do this by ensuring the the TSAN_OPTIONS environment +-- vriable is set to something like `TSAN_OPTIONS="suppressions=$(projectdir)/tsan.supp"` +-- +-- `prompt> TSAN_OPTIONS="detect_deadlocks=0 suppressions=$(projectdir)/tsan.supp" xmake run zenserver` + +--set_policy("build.sanitizer.thread", true) --set_policy("build.sanitizer.leak", true) --set_policy("build.sanitizer.undefined", true) +-- In practice, this does not work because of the difficulty of compiling +-- dependencies with MemorySanitizer. +--set_policy("build.sanitizer.memory", true) + -------------------------------------------------------------------------- -- Dependencies @@ -131,7 +152,9 @@ enable_unity = false 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 - if not is_plat("linux", "macosx") then + -- 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") @@ -154,6 +177,12 @@ 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", @@ -166,33 +195,49 @@ if is_os("windows") then "_WIN32_WINNT=0x0A00", "_WINSOCK_DEPRECATED_NO_WARNINGS" -- let us use the ANSI functions ) - -- Make builds more deterministic and portable - 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:preprocessor") -- Enable preprocessor conformance mode - add_cxxflags("/Zc:u8EscapeEncoding") -- Enable UTF-8 encoding for u8 string literals - add_cxxflags("/Zc:inline") -- Enforce inline semantics + -- 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 -if is_os("linux") or is_os("macosx") then - add_cxxflags("-Wno-implicit-fallthrough") - add_cxxflags("-Wno-missing-field-initializers") - add_cxxflags("-Wno-strict-aliasing") - add_cxxflags("-Wno-switch") - add_cxxflags("-Wno-unused-lambda-capture") - add_cxxflags("-Wno-unused-private-field") - add_cxxflags("-Wno-unused-value") - add_cxxflags("-Wno-unused-variable") - add_cxxflags("-Wno-vla-cxx-extension") +-- 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 -if is_os("macosx") then - -- silence warnings about -Wno-vla-cxx-extension since to my knowledge we can't - -- detect the clang version used in Xcode and only recent versions contain this flag - add_cxxflags("-Wno-unknown-warning-option") +-- 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 @@ -286,6 +331,13 @@ option("zentrace") 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") @@ -328,7 +380,9 @@ end includes("src/zenstore", "src/zenstore-test") includes("src/zentelemetry", "src/zentelemetry-test") includes("src/zenutil", "src/zenutil-test") -includes("src/zenvfs") +if is_plat("windows") then + includes("src/zenvfs") +end includes("src/zenserver", "src/zenserver-test") includes("src/zen") includes("src/zentest-appstub") @@ -349,6 +403,23 @@ task("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", @@ -366,16 +437,6 @@ task("kill") end end) -task("updatefrontend") - set_menu { - usage = "xmake updatefrontend", - description = "Create Zip of the frontend/html folder for bundling with zenserver executable", - } - on_run(function() - import("scripts.updatefrontend") - updatefrontend() - end) - task("precommit") set_menu { usage = "xmake precommit", @@ -430,401 +491,6 @@ task("test") } } on_run(function() - import("core.base.option") - import("core.project.config") - import("core.project.project") - - config.load() - - -- Override table: target name -> short name (for targets that don't follow convention) - local short_name_overrides = { - ["zenserver-test"] = "integration", - } - - -- Build test list from targets in the "tests" group - local available_tests = {} - for name, target in pairs(project.targets()) do - if target:get("group") == "tests" and name:endswith("-test") then - local short = short_name_overrides[name] - if not short then - -- Derive short name: "zencore-test" -> "core" - short = name - if short:startswith("zen") then short = short:sub(4) end - if short:endswith("-test") then short = short:sub(1, -6) end - end - table.insert(available_tests, {short, name}) - end - end - - -- Add non-test-group entries that have a test subcommand - table.insert(available_tests, {"server", "zenserver"}) - - table.sort(available_tests, function(a, b) return a[1] < b[1] end) - - -- Handle --list: print discovered test names and exit - if option.get("list") then - printf("Available tests:\n") - for _, entry in ipairs(available_tests) do - printf(" %-16s -> %s\n", entry[1], entry[2]) - end - return - end - - local testname = option.get("run") - - -- Parse comma-separated test names into a set - local requested = {} - for token in testname:gmatch("[^,]+") do - requested[token:match("^%s*(.-)%s*$")] = true - end - - -- Filter to requested test(s) - local tests = {} - local matched = {} - - for _, entry in ipairs(available_tests) do - local name, target = entry[1], entry[2] - if requested["all"] or requested[name] then - table.insert(tests, {name = name, target = target}) - matched[name] = true - end - end - - -- Check for unknown test names - if not requested["all"] then - for name, _ in pairs(requested) do - if not matched[name] then - raise("no tests match specification: '%s'", name) - end - end - end - - if #tests == 0 then - raise("no tests match specification: '%s'", testname) - end - - local plat, arch - if is_host("windows") then - plat = "windows" - arch = "x64" - elseif is_host("macosx") then - plat = "macosx" - arch = is_arch("arm64") and "arm64" or "x86_64" - else - plat = "linux" - arch = "x86_64" - end - - -- Only reconfigure if current config doesn't already match - if config.get("mode") ~= "debug" or config.get("plat") ~= plat or config.get("arch") ~= arch then - local toolchain_flag = config.get("toolchain") and ("--toolchain=" .. config.get("toolchain")) or "" - local sdk_flag = config.get("sdk") and ("--sdk=" .. config.get("sdk")) or "" - os.exec("xmake config -c -m debug -p %s -a %s %s %s", plat, arch, toolchain_flag, sdk_flag) - end - - -- Build targets we're going to run - if requested["all"] then - os.exec("xmake build -y") - else - for _, entry in ipairs(tests) do - os.exec("xmake build -y %s", entry.target) - end - end - - local use_junit_reporting = option.get("junit") - local use_noskip = option.get("noskip") - local use_verbose = option.get("verbose") - local repeat_count = tonumber(option.get("repeat")) or 1 - local extra_args = option.get("arguments") or {} - local junit_report_files = {} - - local junit_report_dir - if use_junit_reporting then - junit_report_dir = path.join(os.projectdir(), config.get("buildir"), "reports") - os.mkdir(junit_report_dir) - end - - -- Results collection for summary table - local results = {} - local any_failed = false - - -- Format a number with thousands separators (e.g. 31103 -> "31,103") - local function format_number(n) - local s = tostring(n) - local pos = #s % 3 - if pos == 0 then pos = 3 end - local result = s:sub(1, pos) - for i = pos + 1, #s, 3 do - result = result .. "," .. s:sub(i, i + 2) - end - return result - end - - -- Center a string within a given width - local function center_str(s, width) - local pad = width - #s - local lpad = math.floor(pad / 2) - local rpad = pad - lpad - return string.rep(" ", lpad) .. s .. string.rep(" ", rpad) - end - - -- Left-align a string within a given width (with 1-space left margin) - local function left_align_str(s, width) - return " " .. s .. string.rep(" ", width - #s - 1) - end - - -- Right-align a string within a given width (with 1-space right margin) - local function right_align_str(s, width) - return string.rep(" ", width - #s - 1) .. s .. " " - end - - -- Format elapsed seconds as a human-readable string - local function format_time(seconds) - if seconds >= 60 then - local mins = math.floor(seconds / 60) - local secs = seconds - mins * 60 - return string.format("%dm %04.1fs", mins, secs) - else - return string.format("%.1fs", seconds) - end - end - - -- Parse test summary file written by TestListener - local function parse_summary_file(filepath) - if not os.isfile(filepath) then return nil end - local content = io.readfile(filepath) - if not content then return nil end - local ct = content:match("cases_total=(%d+)") - local cp = content:match("cases_passed=(%d+)") - local at = content:match("assertions_total=(%d+)") - local ap = content:match("assertions_passed=(%d+)") - if ct then - local failures = {} - for name, file, line in content:gmatch("failed=([^|\n]+)|([^|\n]+)|(%d+)") do - table.insert(failures, {name = name, file = file, line = tonumber(line)}) - end - local es = content:match("elapsed_seconds=([%d%.]+)") - return { - cases_total = tonumber(ct), - cases_passed = tonumber(cp) or 0, - asserts_total = tonumber(at) or 0, - asserts_passed = tonumber(ap) or 0, - elapsed_seconds = tonumber(es) or 0, - failures = failures - } - end - return nil - end - - -- Temp directory for summary files - local summary_dir = path.join(os.tmpdir(), "zen-test-summary") - os.mkdir(summary_dir) - - -- Run each test suite and collect results - for iteration = 1, repeat_count do - if repeat_count > 1 then - printf("\n*** Iteration %d/%d ***\n", iteration, repeat_count) - end - - for _, entry in ipairs(tests) do - local name, target = entry.name, entry.target - printf("=== %s ===\n", target) - - local suite_name = target - if name == "server" then - suite_name = "zenserver (test)" - end - - local cmd = string.format("xmake run %s", target) - if name == "server" then - cmd = string.format("xmake run %s test", target) - end - cmd = string.format("%s --duration=true", cmd) - - if use_junit_reporting then - local junit_report_file = path.join(junit_report_dir, string.format("junit-%s-%s-%s.xml", config.plat(), arch, target)) - junit_report_files[target] = junit_report_file - cmd = string.format("%s --reporters=junit --out=%s", cmd, junit_report_file) - end - if use_noskip then - cmd = string.format("%s --no-skip", cmd) - end - if use_verbose and name == "integration" then - cmd = string.format("%s --verbose", cmd) - end - for _, arg in ipairs(extra_args) do - cmd = string.format("%s %s", cmd, arg) - end - - -- Tell TestListener where to write the summary - local summary_file = path.join(summary_dir, target .. ".txt") - os.setenv("ZEN_TEST_SUMMARY_FILE", summary_file) - - -- Run test with real-time streaming output - local test_ok = true - try { - function() - os.exec(cmd) - end, - catch { - function(errors) - test_ok = false - end - } - } - - -- Read summary written by TestListener - local summary = parse_summary_file(summary_file) - os.tryrm(summary_file) - - if not test_ok then - any_failed = true - end - - table.insert(results, { - suite = suite_name, - cases_passed = summary and summary.cases_passed or 0, - cases_total = summary and summary.cases_total or 0, - asserts_passed = summary and summary.asserts_passed or 0, - asserts_total = summary and summary.asserts_total or 0, - elapsed_seconds = summary and summary.elapsed_seconds or 0, - failures = summary and summary.failures or {}, - passed = test_ok - }) - end - - if any_failed then - if repeat_count > 1 then - printf("\n*** Failure detected on iteration %d, stopping ***\n", iteration) - end - break - end - end - - -- Clean up - os.setenv("ZEN_TEST_SUMMARY_FILE", "") - os.tryrm(summary_dir) - - -- Print JUnit reports if requested - for test, junit_report_file in pairs(junit_report_files) do - printf("=== report - %s ===\n", test) - if os.isfile(junit_report_file) then - local data = io.readfile(junit_report_file) - if data then - print(data) - end - end - end - - -- Print summary table - if #results > 0 then - -- Calculate column widths based on content - local col_suite = #("Suite") - local col_cases = #("Cases") - local col_asserts = #("Assertions") - local col_time = #("Time") - local col_status = #("Status") - - -- Compute totals - local total_cases_passed = 0 - local total_cases_total = 0 - local total_asserts_passed = 0 - local total_asserts_total = 0 - local total_elapsed = 0 - - for _, r in ipairs(results) do - col_suite = math.max(col_suite, #r.suite) - local cases_str = format_number(r.cases_passed) .. "/" .. format_number(r.cases_total) - col_cases = math.max(col_cases, #cases_str) - local asserts_str = format_number(r.asserts_passed) .. "/" .. format_number(r.asserts_total) - col_asserts = math.max(col_asserts, #asserts_str) - col_time = math.max(col_time, #format_time(r.elapsed_seconds)) - local status_str = r.passed and "SUCCESS" or "FAILED" - col_status = math.max(col_status, #status_str) - - total_cases_passed = total_cases_passed + r.cases_passed - total_cases_total = total_cases_total + r.cases_total - total_asserts_passed = total_asserts_passed + r.asserts_passed - total_asserts_total = total_asserts_total + r.asserts_total - total_elapsed = total_elapsed + r.elapsed_seconds - end - - -- Account for totals row in column widths - col_suite = math.max(col_suite, #("Total")) - col_cases = math.max(col_cases, #(format_number(total_cases_passed) .. "/" .. format_number(total_cases_total))) - col_asserts = math.max(col_asserts, #(format_number(total_asserts_passed) .. "/" .. format_number(total_asserts_total))) - col_time = math.max(col_time, #format_time(total_elapsed)) - - -- Add padding (1 space each side) - col_suite = col_suite + 2 - col_cases = col_cases + 2 - col_asserts = col_asserts + 2 - col_time = col_time + 2 - col_status = col_status + 2 - - -- Build horizontal border segments - local h_suite = string.rep("-", col_suite) - local h_cases = string.rep("-", col_cases) - local h_asserts = string.rep("-", col_asserts) - local h_time = string.rep("-", col_time) - local h_status = string.rep("-", col_status) - - local top = "+" .. h_suite .. "+" .. h_cases .. "+" .. h_asserts .. "+" .. h_time .. "+" .. h_status .. "+" - local mid = "+" .. h_suite .. "+" .. h_cases .. "+" .. h_asserts .. "+" .. h_time .. "+" .. h_status .. "+" - local bottom = "+" .. h_suite .. "+" .. h_cases .. "+" .. h_asserts .. "+" .. h_time .. "+" .. h_status .. "+" - local vbar = "|" - - local header_msg = any_failed and "Some tests failed:" or "All tests passed:" - printf("\n* %s\n", header_msg) - printf(" %s\n", top) - printf(" %s%s%s%s%s%s%s%s%s%s%s\n", vbar, center_str("Suite", col_suite), vbar, center_str("Cases", col_cases), vbar, center_str("Assertions", col_asserts), vbar, center_str("Time", col_time), vbar, center_str("Status", col_status), vbar) - - for _, r in ipairs(results) do - printf(" %s\n", mid) - local cases_str = format_number(r.cases_passed) .. "/" .. format_number(r.cases_total) - local asserts_str = format_number(r.asserts_passed) .. "/" .. format_number(r.asserts_total) - local time_str = format_time(r.elapsed_seconds) - local status_str = r.passed and "SUCCESS" or "FAILED" - printf(" %s%s%s%s%s%s%s%s%s%s%s\n", vbar, left_align_str(r.suite, col_suite), vbar, right_align_str(cases_str, col_cases), vbar, right_align_str(asserts_str, col_asserts), vbar, right_align_str(time_str, col_time), vbar, right_align_str(status_str, col_status), vbar) - end - - -- Totals row - if #results > 1 then - local h_suite_eq = string.rep("=", col_suite) - local h_cases_eq = string.rep("=", col_cases) - local h_asserts_eq = string.rep("=", col_asserts) - local h_time_eq = string.rep("=", col_time) - local h_status_eq = string.rep("=", col_status) - local totals_sep = "+" .. h_suite_eq .. "+" .. h_cases_eq .. "+" .. h_asserts_eq .. "+" .. h_time_eq .. "+" .. h_status_eq .. "+" - printf(" %s\n", totals_sep) - - local total_cases_str = format_number(total_cases_passed) .. "/" .. format_number(total_cases_total) - local total_asserts_str = format_number(total_asserts_passed) .. "/" .. format_number(total_asserts_total) - local total_time_str = format_time(total_elapsed) - local total_status_str = any_failed and "FAILED" or "SUCCESS" - printf(" %s%s%s%s%s%s%s%s%s%s%s\n", vbar, left_align_str("Total", col_suite), vbar, right_align_str(total_cases_str, col_cases), vbar, right_align_str(total_asserts_str, col_asserts), vbar, right_align_str(total_time_str, col_time), vbar, right_align_str(total_status_str, col_status), vbar) - end - - printf(" %s\n", bottom) - end - - -- Print list of individual failing tests - if any_failed then - printf("\n Failures:\n") - for _, r in ipairs(results) do - if #r.failures > 0 then - printf(" -- %s --\n", r.suite) - for _, f in ipairs(r.failures) do - printf(" FAILED: %s (%s:%d)\n", f.name, f.file, f.line) - end - elseif not r.passed then - printf(" -- %s --\n", r.suite) - printf(" (test binary exited with error, no failure details available)\n") - end - end - end - - if any_failed then - raise("one or more test suites failed") - end + import("scripts.test") + test() end) |