section [[
This file is part of luax.

luax is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

luax is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with luax.  If not, see <https://www.gnu.org/licenses/>.

For further information about luax you can visit
https://codeberg.org/cdsoft/luax
]]

version "9.5.8" "2025-10-06"

local F = require "F"
local fs = require "fs"
local sh = require "sh"

local LUAX = {}
local I = F.I(LUAX)

LUAX.AUTHORS     = "Christophe Delord"
LUAX.URL         = "codeberg.org/cdsoft/luax"
LUAX.VERSION     = vars.version
LUAX.DATE        = vars.date
LUAX.COPYRIGHT   = I"LuaX $(VERSION)  Copyright (C) 2021-$(DATE:split'%-':head()) $(URL), $(AUTHORS)"

local BUILD_CONFIG = {
    ZIG_VERSION  = "0.14.1",
    ZIG_KEY      = "RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U",
    ZIG_PATH     = "~/.local/opt/zig",
    ZIG_PATH_WIN = "~\\zig",
}

help.name "LuaX"
help.description(I[[
$(COPYRIGHT)

luax is a Lua interpreter and REPL based on Lua 5.4
augmented with some useful packages.
luax can also produce standalone scripts from Lua scripts.

luax runs on several platforms with no dependency:

- Linux (x86_64, aarch64)
- MacOS (x86_64, aarch64)
- Windows (x86_64, aarch64)

luax can "compile" scripts executable on all supported platforms
(LuaX must be installed on the target environment).
]])

section [[
WARNING: This file has been generated by bang. DO NOT MODIFY IT.
If you need to update the build system, please modify build.lua
and run bang to regenerate build.ninja.
]]

-- list of targets used for cross compilation (with Zig only)
local targets = dofile "libluax/sys/targets.lua"
local sys = dofile "libluax/sys/sys.lua"

local usage = I{
    title = function(s) return F.unlines {s, ("="):rep(#s)}:rtrim() end,
    list = function(t) return t:map(F.prefix"    - "):unlines():rtrim() end,
    targets = targets:map(F.partial(F.nth, "name")),
}[[
$(title "LuaX bang file usage")

The LuaX bang file can be given options to customize the LuaX compilation.

Without any options, LuaX:
    - is compiled with gcc
    - is optimized for size
    - is not a cross-compiler
    - does not use optional modules (lz4, luasocket, luasec)
    - does not use OpenSSL

$(title "Compiler")

bang -- gcc         Compile LuaX with gcc (default)
bang -- clang       Compile LuaX with clang
bang -- zig         Compile LuaX with Zig

$(title "Compilation mode")

bang -- fast        Code optimized for speed
bang -- small       Code optimized for size (default)
bang -- debug       Compiled with debug informations and no optimization
bang -- san         Compiled with ASan and UBSan (implies clang)
bang -- lax         Disable strict compilation options
bang -- strip       Remove debug information from precompiled bytecode
bang -- lto         Enable LTO optimizations
bang -- nolto       Disable LTO optimizations (default)

$(title "Optional features")

bang -- lz4         Add LZ4 support
bang -- nolz4       No LZ4 support (default)
bang -- socket      Add socket support via luasocket
bang -- nosocket    No socket support via luasocket (default)
bang -- ssl         Add SSL support via LuaSec and OpenSSL
bang -- nossl       No SSL support via LuaSec and OpenSSL (default)
bang -- cross       Generate cross-compilers (implies compilation with zig)
bang -- nocross     Do not generate cross-compilers (default)

Zig is downloaded by the ninja file.
gcc and clang must be already installed.

Supported targets:
$(list(targets))
]]

if F.elem("help", arg) then
    print(usage)
    os.exit(0)
end

local mode = "small" -- fast, small, debug
local compiler = "gcc" -- zig, gcc, clang
local san = false
local strict = true -- strict compilation options and checks
local use_lto = false
local lz4 = false
local socket = false
local ssl = false
local release = false
local cross = false

local bytecode = "-b"

F.foreach(arg, function(a)
    local function set_mode() mode = a end
    local function set_compiler() compiler = a end

    case(a) {
        fast  = set_mode,
        small = set_mode,
        debug = set_mode,
        zig   = set_compiler,
        gcc   = set_compiler,
        clang = set_compiler,
        san   = function() san = true end,
        lax   = function() strict = false end,
        strip = function() bytecode = "-s" end,
        lto   = function() use_lto = true end,
        nolto = function() use_lto = false end,
        lz4   = function() lz4 = true end,
        nolz4 = function() lz4 = false end,
        socket   = function() socket = true end,
        nosocket = function() socket = false end,
        ssl      = function() ssl = true end,
        nossl    = function() ssl = false end,
        release = function() release = true end,
        cross   = function() cross = true end,
        nocross = function() cross = false end,
        [Nil] = function()
            F.error_without_stack_trace(a..": unknown parameter\n\n"..usage, 1)
        end,
    } ()
end)

if san then compiler = "clang" end
if release or cross then compiler = "zig"; san = false end

if ssl then socket = true end

local host_targets = F{targets:find(function(t) return t.os==sys.os and t.arch==sys.arch end)}
assert(#host_targets == 1)

local targets_to_compile = (release or cross) and targets or host_targets

BUILD_CONFIG.COMPILER_NAME = compiler
BUILD_CONFIG.COMPILER_FULL_VERSION = case(compiler) {
    zig = function() return compiler.." version "..BUILD_CONFIG.ZIG_VERSION end,
    gcc = function() return assert(sh "gcc --version"):lines():head() end,
    clang = function() return assert(sh "clang --version"):lines():head() end,
}()
BUILD_CONFIG.COMPILER_VERSION = case(compiler) {
    zig = function() return BUILD_CONFIG.ZIG_VERSION end,
    gcc = function() return BUILD_CONFIG.COMPILER_FULL_VERSION:match "[%d.]+" end,
    clang = function() return BUILD_CONFIG.COMPILER_FULL_VERSION:match "[%d.]+" end,
}()
BUILD_CONFIG.MODE = mode
BUILD_CONFIG.LTO = use_lto
BUILD_CONFIG.SOCKET = socket
BUILD_CONFIG.SSL = ssl

-- check for "modern" compiler
local compiler_version = tonumber(BUILD_CONFIG.COMPILER_VERSION:match"%d+")
local modern_compiler = case(compiler) {
    gcc   = compiler_version >= 13,
    clang = compiler_version >= 16,
    zig   = true,
}

-- disable strict mode with old compilers to avoid unknown options
strict = strict and modern_compiler

section("Compilation options")
comment(("Compilation mode  : %s"):format(F{mode, use_lto and "+ LTO" or {}}:flatten():unwords()))
comment(("Compiler          : %s"):format(compiler))
comment(("Sanitizers        : %s"):format(san and "ASan and UBSan" or "none"))
comment(("Compilation checks: %s"):format(strict and "strict" or "lax"))
comment(("Lua code          : %s"):format(case(bytecode) { ["-b"] = "bytecode",
                                                           ["-s"] = "stripped bytecode",
                                                           [Nil]  = "source code" }))
comment(("Compression       : %s"):format(F.flatten{"Lzip", lz4 and "LZ4" or {}} : str " + "))
comment(("Socket support    : %s"):format(F.flatten{socket and "LuaSocket" or "none", ssl and {"LuaSec", "OpenSSL"} or {}} : str " + "))

local function is_dynamic(target) return target.libc~="musl" and not san end

local function optional(cond)
    return cond and F.id or F.const{}
end

local function zig_target(t)
    return {"-target", F{t.arch, t.os, t.libc}:str"-"}
end

--===================================================================
section "Build environment"
---------------------------------------------------------------------

var "bin" "$builddir/bin"
var "lib" "$builddir/lib"
var "doc" "$builddir/doc"
var "tmp" "$builddir/tmp"
var "test" "$builddir/test"
var "dist" "$builddir/dist"

local compile = {}
local test = {}
local doc = {}

local function once(f)
    local cache = {}
    return F.curry(function(output, input)
        local hash = F.show{output=output, input=input}
        local val = cache[hash]
        if val ~= nil then return val end
        val = f(output)(input)
        cache[hash] = val
        return val
    end)
end

local build_once = once(build)

local openssl_libs = targets : map2t(function(target) return target.name, {} end)

local function add_openssl_rules()

if not ssl then return end

--===================================================================
section "OpenSSL"
---------------------------------------------------------------------

var "openssl_version" "3.5.3"
var "openssl_archive" "openssl-${openssl_version}.tar.gz"
var "openssl_url" "https://github.com/openssl/openssl/releases/download/openssl-${openssl_version}/${openssl_archive}"

var "openssl" "$builddir/openssl"
var "openssl_src" "$builddir/src/openssl-${openssl_version}"

var "root" { fs.getcwd() }

local openssl_downloaded_archive = build "$builddir/src/$openssl_archive" {
    description = "download $out",
    command = "curl -sSLf $openssl_url -o $out && touch $out",
}

local openssl_configure_script = build "$openssl_src/Configure" { openssl_downloaded_archive,
    description = "extract $in",
    command = "tar xzf $in -C $openssl_src --strip-components 1 && touch $out",
}

local openssl_options = {
    case(mode) {
        fast  = "--release",
        small = "--release",
        debug = "--debug",
    },
    "--static",

    "no-apps",
    "no-asm",
    "no-async",
    "no-atexit",    -- atexit crashes tests with luax shared libraries
    "no-docs",
    "no-dso",
    "no-tests",
    "no-threads",
    "no-shared",

    -- https://github.com/openssl/openssl/discussions/23542
    "no-afalgeng",
    "no-autoalginit",
    "no-autoerrinit",
    "no-cached-fetch",
    "no-dgram",
    "no-dynamic-engine",
    "no-filenames",
    "no-fips-securitychecks",
    "no-gost",
    "no-module",
    "no-nextprotoneg",
    "no-pinshared",
    "no-srtp",
    "no-sse2",
    "no-ssl-trace",
    "no-static-engine",
    "no-quic",
    "no-ui-console",
    "no-uplink",
    "no-argon2",
    "no-scrypt",

    "-DOPENSSL_NO_APPLE_CRYPTO_RANDOM",
    "-DOPENSSL_SMALL_FOOTPRINT",
}

local nproc = (sh "getconf _NPROCESSORS_ONLN" or "8"):trim()

rule "make_openssl" {
    description = "compile OpenSSL for $zig_target",
    command = {
        "set -e;",
        "mkdir -p $openssl/$target;",
        "cd $openssl/$target;",
        case(compiler) {
            zig = {
                'export AR="$zig ar";',
                'export CC="$zig cc $zig_target";',
                'export CXX="$zig c++ $zig_target";',
                'export LD="$zig ld $zig_target";',
                'export RANLIB="$zig ranlib";',
                'export RC="$zig rc";',
            },
            gcc = {
                'export CC="gcc";',
                'export CXX="g++";',
            },
            clang = {
                'export CC="clang";',
                'export CXX="clang++";',
            },
        },
        case(mode) {
            fast  = 'export CFLAGS="-pipe -Os";',
            small = 'export CFLAGS="-pipe -Os";',
            debug = 'export CFLAGS="-pipe -Og -g";',
        },
        "$lto",
        "$root"/(vars%openssl_configure_script), "$openssl_target", openssl_options, ";",
        "make", "-j", nproc, ";",
        "touch *.a",
    },
    implicit_in = openssl_configure_script,
    pool = "console",
}

targets_to_compile:foreach(function(target)

    local lto_opt = case(compiler) {
        zig   = 'export CFLAGS="$$CFLAGS -flto=thin";',
        gcc   = 'export CFLAGS="$$CFLAGS -flto=auto";',
        clang = 'export CFLAGS="$$CFLAGS -flto=thin";',
    }

    openssl_libs[target.name] = build { "$openssl"/target.name/"libssl.a", "$openssl"/target.name/"libcrypto.a" } { "make_openssl",
        target = target.name,
        zig_target = zig_target(target),
        openssl_target = case(target.name) {
            ["linux-x86_64"]        = F.const"linux-x86_64",
            ["linux-x86_64-musl"]   = F.const"linux-x86_64",
            ["linux-aarch64"]       = F.const"linux-aarch64",
            ["linux-aarch64-musl"]  = F.const"linux-aarch64",
            ["macos-x86_64"]        = F.const"darwin64-x86_64",
            ["macos-aarch64"]       = F.const"darwin64-arm64",
            ["windows-x86_64"]      = F.const"mingw64",
            ["windows-aarch64"]     = F.const"mingw64", -- assume the x86_64 configuration compiles for aarch64
            [Nil]                   = function() error(target.name..": unsupported OpenSSL target") end,
        } (),
        lto = optional(use_lto) {
            case(target.os) {
                linux   = lto_opt,
                macos   = {},
                windows = lto_opt,
            },
        },
    }

end)

end -- add_openssl_rules

--===================================================================
section "Compiler"
---------------------------------------------------------------------

local compiler_deps = {}

case(compiler) {

    zig = function()
        var "zig_version" (BUILD_CONFIG.ZIG_VERSION)
        local zig_path = case(sys.os) {
            windows = BUILD_CONFIG.ZIG_PATH_WIN:gsub("^~", os.getenv"LOCALAPPDATA" or "~"),
            [Nil]   = BUILD_CONFIG.ZIG_PATH:gsub("^~", os.getenv"HOME" or "~"),
        }
        var "zig" { zig_path/"$zig_version"/"zig" }
        var "zig_key" { BUILD_CONFIG.ZIG_KEY }

        build "$zig" {
            description = "install zig $zig_version",
            command = "tools/install_zig.sh $zig_version $out $zig_key",
            pool = "console",
        }

        compiler_deps = { "$zig" }
        local function zig_rules(target)
            local target_opt = zig_target(target)

            var("cc-"..target.name) { "$zig cc", target_opt }
            var("ar-"..target.name) { "$zig ar" }
            var("ld-"..target.name) { "$zig cc", target_opt }
        end
        targets_to_compile:foreach(zig_rules)
    end,

    gcc = function()
        var("cc-"..sys.name) "gcc"
        var("ar-"..sys.name) "ar"
        var("ld-"..sys.name) "gcc"
    end,

    clang = function()
        var("cc-"..sys.name) "clang"
        var("ar-"..sys.name) "ar"
        var("ld-"..sys.name) "clang"
    end,

}()

local include_path = F.flatten {
    ".",
    "$tmp",
    "lua",
    "ext/c/lzlib/lib",
    "ext/c/lzlib/lib/inc",
    "libluax",
    optional(lz4) "ext/opt/lz4/lib",
}

local lto_opt = optional(use_lto) {
    case(compiler) {
        zig   = "-flto=thin",
        gcc   = "-flto=auto",
        clang = "-flto=thin",
    }
}

local sanitizer_cflags = optional(san) {
    "-g", "-fno-omit-frame-pointer", "-fno-optimize-sibling-calls",
    "-fsanitize=address",
    "-fsanitize=undefined",
    "-fsanitize=float-divide-by-zero",
    --"-fsanitize=unsigned-integer-overflow",
    "-fsanitize=implicit-conversion",
    "-fsanitize=local-bounds",
    "-fsanitize=float-cast-overflow",
    "-fsanitize=nullability-arg",
    "-fsanitize=nullability-assign",
    "-fsanitize=nullability-return",
}

local sanitizer_ext_cflags = optional(san) {
    "-g", "-fno-omit-frame-pointer", "-fno-optimize-sibling-calls",
    "-fsanitize=address",
    --"-fsanitize=undefined",
    --"-fsanitize=float-divide-by-zero",
    --"-fsanitize=unsigned-integer-overflow",
    --"-fsanitize=implicit-conversion",
    "-fsanitize=local-bounds",
    "-fsanitize=float-cast-overflow",
    "-fsanitize=nullability-arg",
    "-fsanitize=nullability-assign",
    "-fsanitize=nullability-return",
}

local sanitizer_ldflags = optional(san) {
    "-fsanitize=address",
    "-fsanitize=undefined",
}

local sanitizer_options = optional(san) {
    "export ASAN_OPTIONS=check_initialization_order=1:detect_stack_use_after_return=1:detect_leaks=1;",
    "export UBSAN_OPTIONS=print_stacktrace=1;",
}

local host_cflags = {
    "-O2",
    "-pipe",
    "-fPIC",
    include_path:map(F.prefix"-I"),
    case(sys.os) {
        linux = "-DLUA_USE_LINUX",
        macos = "-DLUA_USE_MACOSX",
        windows = {},
    },
}

local host_ldflags = {
    "-pipe",
    "-s",
    optional(is_dynamic(sys)) "-rdynamic",
}
local host_ldlibs = {
    "-lm",
}

local cflags = {
    "-std=gnu2x",
    case(mode) {
        fast  = "-O3",
        small = "-Os",
        debug = { "-g", "-Og" },
    },
    "-pipe",
    "-fPIC",
    include_path:map(F.prefix"-I"),
    optional(lz4)    "-DLUAX_USE_LZ4",
    optional(socket) "-DLUAX_USE_SOCKET",
    optional(ssl)    "-DLUAX_USE_SSL",
    optional(san) {
        "-DLUAI_ASSERT",
        "-DLUA_USE_APICHECK",
    },
}

local luax_cflags = build.compile_flags {
    cflags,
    "-DFNV1A_BIT128",
    optional(strict) {
        "-Werror",
        "-Wall",
        "-Wextra",
        "-pedantic",

        "-Wstrict-prototypes",
        "-Wmissing-field-initializers",
        "-Wmissing-prototypes",
        "-Wmissing-declarations",
        "-Werror=switch-enum",
        "-Werror=implicit-fallthrough",
        "-Werror=missing-prototypes",

        "-Wno-unused-macros",

        case(compiler) {
            zig = {
                "-Weverything",
                "-Wno-padded",
                "-Wno-reserved-identifier",
                "-Wno-disabled-macro-expansion",
                "-Wno-used-but-marked-unused",
                "-Wno-documentation-unknown-command",
                "-Wno-declaration-after-statement",
                "-Wno-unsafe-buffer-usage",
                "-Wno-pre-c2x-compat",
                optional(ssl) "-Wno-missing-include-dirs",
            },
            gcc = {},
            clang = {
                "-Weverything",
                "-Wno-padded",
                "-Wno-reserved-identifier",
                "-Wno-disabled-macro-expansion",
                "-Wno-used-but-marked-unused",
                "-Wno-documentation-unknown-command",
                "-Wno-declaration-after-statement",
                "-Wno-unsafe-buffer-usage",
                "-Wno-pre-c2x-compat",
                optional(ssl) "-Wno-missing-include-dirs",
            },
        },
    },
    sanitizer_cflags,
}

local ext_cflags = {
    cflags,
    case(compiler) {
        zig = {
            "-Wno-constant-logical-operand",
        },
        gcc = {},
        clang = {
            "-Wno-constant-logical-operand",
        },
    },
    sanitizer_ext_cflags,
}

local ldflags = {
    "-pipe",
    case(mode) {
        fast  = "-s",
        small = "-s",
        debug = {},
    },
    case(compiler) {
        zig = {},
        gcc = "-Wstringop-overflow=0",
        clang = {},
    },
    sanitizer_ldflags,
}
local ldlibs = {
    "-lm",
}

local cc = {}
local cc_ext = {}

cc.host = build.cc : new "cc-host"
    : set "cc" { "$cc-"..sys.name }
    : set "ar" { "$ar-"..sys.name }
    : set "so" { "$ld-"..sys.name }
    : set "ld" { "$ld-"..sys.name }
    : add "implicit_in" { compiler_deps }
    : add "cflags"  { host_cflags }
    : add "ldflags" { host_ldflags }
    : add "ldlibs"  { host_ldlibs }

targets_to_compile:foreach(function(target)

    local lto = case(target.os) {
        linux   = lto_opt,
        macos   = {},
        windows = lto_opt,
    }
    local target_flags = {
        "-DLUAX_ARCH='\""..target.arch.."\"'",
        "-DLUAX_OS='\""..target.os.."\"'",
        "-DLUAX_LIBC='\""..target.libc.."\"'",
        "-DLUAX_EXE='\""..target.exe.."\"'",
        "-DLUAX_SO='\""..target.so.."\"'",
        "-DLUAX_NAME='\""..target.name.."\"'",
    }
    local lua_flags = {
        case(target.os) {
            linux   = "-DLUA_USE_LINUX",
            macos   = "-DLUA_USE_MACOSX",
            windows = {},
        },
    }
    local opt_include_path = F.flatten {
        optional(lz4) "ext/opt/lz4/lib",
        optional(ssl) {
            "$openssl"/target.name/"include",
            "$openssl_src/include",
        },
    }
    local opt_flags = {
        F.map(F.prefix"-I", opt_include_path),
    }
    if target.name == sys.name then
        build.compile_flags {
            F{target_flags, lua_flags}
                : flatten()
                : map(function(s) return s:gsub("'", "") end)
        }
    end
    local target_ld_flags = {
        optional(is_dynamic(target)) "-rdynamic",
        case(target.os) {
            linux   = {},
            macos   = {},
            windows = {
                "-lshlwapi",
                optional(socket) "-lws2_32",
                optional(ssl) "-lcrypt32",
            },
        },
    }
    cc[target.name] = build.zigcc[target.name] : new("cc-"..target.name)
        : set "cc" { "$cc-"..target.name }
        : set "ar" { "$ar-"..target.name }
        : set "so" { "$ld-"..target.name }
        : set "ld" { "$ld-"..target.name }
        : add "implicit_in" { compiler_deps }
        : add "cflags" {
            lto, luax_cflags, lua_flags, target_flags, opt_flags, "$additional_flags",
            case(target.os) {
                linux   = {},
                macos   = {},
                windows = "$build_as_dll",
            },
        }
        : add "ldflags" { lto, ldflags, target_ld_flags }
        : add "ldlibs"  { ldlibs }
        : add "soflags" { lto, ldflags, target_ld_flags }
        : add "solibs"  { ldlibs }
    cc_ext[target.name] = build.zigcc[target.name] : new("cc_ext-"..target.name)
        : set "cc" { "$cc-"..target.name }
        : set "ar" { "$ar-"..target.name }
        : set "so" { "$ld-"..target.name }
        : set "ld" { "$ld-"..target.name }
        : add "implicit_in" { compiler_deps }
        : add "cflags"  { lto, ext_cflags, lua_flags, opt_flags, "$additional_flags" }
        : add "ldflags" { lto, ldflags, target_ld_flags }
        : add "ldlibs"  { ldlibs }
        : add "soflags" { lto, ldflags, target_ld_flags }
        : add "solibs"  { ldlibs }

end)

add_openssl_rules()

--===================================================================
section "Third-party modules update"
---------------------------------------------------------------------

build "update_modules" {
    description = "update third-party modules",
    command = "tools/update-third-party-modules.sh $builddir/update",
    pool = "console",
}

--===================================================================
section "LuaX sources"
---------------------------------------------------------------------

local linux_only = F.flatten {
    "ext/c/linenoise/linenoise.c",
    "ext/c/linenoise/utf8.c",
    optional(socket) {
        "ext/opt/luasocket/serial.c",
        "ext/opt/luasocket/unixdgram.c",
        "ext/opt/luasocket/unixstream.c",
        "ext/opt/luasocket/usocket.c",
        "ext/opt/luasocket/unix.c",
    },
}
local windows_only = F.flatten {
    optional(socket) {
        "ext/opt/luasocket/wsocket.c",
    },
}
local ignored_sources = {
    "ext/c/lqmath/src/imath.c",
}

local sources = F{
    lua_c_files = ls "lua/*.c"
        : difference { "lua/lua.c", "lua/luac.c" },
    lua_main_c_files = F{ "lua/lua.c" },
    luax_main_c_files = F{ "luax/luax.c" },
    loader_main_c_files = F{ "luax/luax-loader.c" },
    libluax_main_c_files = F{ "luax/libluax.c" },
    luax_c_files = ls "libluax/**.c"
        : difference(lz4 and {} or ls "libluax/lz4/**.c")
        : difference(socket and {} or ls "libluax/socket/**.c")
        : difference(ssl and {} or ls "libluax/sec/**.c"),
    third_party_c_files = F.flatten {
        ls "ext/c/**.c"
            : difference(ls "ext/c/lzlib/lib/inc/*.c")
            : difference(ls "ext/c/lzlib/programs/*.c"),
        optional(lz4)    { ls "ext/opt/lz4/lib/*.c" },
        optional(socket) { ls "ext/opt/luasocket/*.c" },
        optional(ssl)    { ls "ext/opt/luasec/**.c" },
    }
    : difference(linux_only)
    : difference(windows_only)
    : difference(ignored_sources),
    linux_third_party_c_files = linux_only,
    windows_third_party_c_files = windows_only,
}

--===================================================================
section "Native Lua interpreter"
---------------------------------------------------------------------

var "lua" "$bin/lua"

var "lua_path" (
    F{
        "$tmp",
        ls "libluax/*" : filter(fs.is_dir),
        "ext/lua/argparse",
        "ext/lua/cbor",
        "luax",
    }
    : flatten()
    : map(function(path) return path / "?.lua" end)
    : str ";"
)

cc.host "$lua" {
    sources.lua_c_files,
    sources.lua_main_c_files,
}

--===================================================================
section "lzip cli"
---------------------------------------------------------------------

var "lzip" "$bin/lzip"

cc.host "$lzip" {
    ls "ext/c/lzlib/programs/*.c",
    ls "ext/c/lzlib/lib/*.c",
}

--===================================================================
section "LuaX configuration"
---------------------------------------------------------------------

comment [[
The LuaX configuration files (luax_config.h and luax_config.lua)
are created in `$tmp`
]]

var "luax_config_h"   "$tmp/luax_config.h"
var "luax_config_lua" "$tmp/luax_config.lua"

local ypp = build.ypp : new "ypp-luax"
    : set "cmd" "$lua tools/luax.lua tools/ypp.luax"
    : add "implicit_in" "$lua tools/luax.lua tools/ypp.luax"

local ypp_config = ypp : new "ypp-config"
    : add "flags" { build.ypp_vars(LUAX) }

ypp_config "$luax_config_h"   { "libluax/luax_config.h.in" }
ypp_config "$luax_config_lua" { "libluax/luax_config.lua.in" }

comment [[
The LuaX build configuration file (luax_build_config.lua)
is created in `$tmp`
]]

var "luax_build_config_lua" "$tmp/luax_build_config.lua"

local ypp_build_config = ypp : new "ypp-build-config"
    : add "flags" { build.ypp_vars(BUILD_CONFIG) }

ypp_build_config "$luax_build_config_lua" { "luax/luax_build_config.lua.in" }

if bang.output:dirname() == bang.input:dirname() then
    local function dirs(path)
        return fs.ls(path/"**.lua")
            : map(F.compose{F.prefix"    \"", F.suffix"\",", fs.dirname})
            : nub() : unlines() : rtrim() : gsub(",$", "")
    end
    file(bang.input:dirname()/".luarc.json")(F.unlines(F.flatten{
        "{",
        [=[  "$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json",]=],
        [=[  "runtime.version": "Lua 5.4",]=],
        [=[  "workspace.library": []=],
        dirs "libluax" .. ",",
        dirs "ext/lua",
        [=[  ],]=],
        [=[  "workspace.checkThirdParty": false]=],
        "}",
    }))
end

--===================================================================
section "Lua runtime"
---------------------------------------------------------------------

rule "bundle" {
    description = "bundle $out",
    command = {
        "PATH=$bin:$$PATH",
        "LUA_PATH=\"$lua_path\"",
        "LUAX_LIB=$lib",
        "$lua tools/bundle.lua $args $in -o $out",
    },
    implicit_in = {
        "$lua tools/bundle.lua",
        "$lzip",
        "luax/luax_bundle.lua",
        "$luax_config_lua",
        "$luax_build_config_lua",
    },
}

local luax_runtime, lua_runtime = {}, {}

local function rt(t)
    acc(luax_runtime) { t.luax }
    acc(lua_runtime)  { t.lua  }
end

rt { luax="libluax/F/F.lua",                        lua="libluax/F/F.lua"                                       }
rt {                                                lua="libluax/complex/complex.lua"                           }
rt { luax="libluax/crypt/crypt.lua",                lua={"libluax/crypt/crypt.lua", "libluax/crypt/_crypt.lua"} }
rt { luax="libluax/curl/curl.lua",                  lua="libluax/curl/curl.lua"                                 }
rt { luax="libluax/fs/fs.lua",                      lua={"libluax/fs/fs.lua", "libluax/fs/_fs.lua"}             }
rt {                                                lua="libluax/imath/imath.lua"                               }
rt { luax="libluax/import/import.lua",              lua="libluax/import/import.lua"                             }
rt {                                                lua="libluax/linenoise/linenoise.lua"                       }
rt { luax="libluax/lar/lar.lua",                    lua="libluax/lar/lar.lua"                                   }
rt { luax="libluax/lzip/lzip.lua",                  lua={"libluax/lzip/lzip.lua", "libluax/lzip/_lzip.lua"}     }
rt {                                                lua="libluax/mathx/mathx.lua"                               }
rt {                                                lua="libluax/ps/ps.lua"                                     }
rt { luax="libluax/qmath/qmath.lua",                lua={"libluax/qmath/qmath.lua", "libluax/qmath/_qmath.lua"} }
rt { luax="libluax/sh/sh.lua",                      lua="libluax/sh/sh.lua"                                     }
rt { luax="libluax/sys/targets.lua",                lua={"libluax/sys/sys.lua", "libluax/sys/targets.lua"}      }
rt { luax="libluax/tar/tar.lua",                    lua="libluax/tar/tar.lua"                                   }
rt { luax="libluax/term/term.lua",                  lua={"libluax/term/term.lua", "libluax/term/_term.lua"}     }
rt { luax="libluax/wget/wget.lua",                  lua="libluax/wget/wget.lua"                                 }

rt { luax="libluax/package/package_hook.lua",       lua="libluax/package/package_hook.lua"                      }
rt { luax="libluax/debug/debug_hook.lua",           lua="libluax/debug/debug_hook.lua"                          }

rt { luax={ls "ext/c/**.lua", ls "ext/lua/**.lua"}, lua={ls "ext/lua/**.lua"}                                   }

if lz4 then
rt { luax="libluax/lz4/lz4.lua",                    lua={"libluax/lz4/lz4.lua", "libluax/lz4/_lz4.lua"}         }
end
if socket then
rt { luax={ls "ext/opt/luasocket/**.lua"},          lua={}                                                      }
end
if ssl then
rt { luax={ls "ext/opt/luasec/**.lua"},             lua={}                                                      }
end

-- Ensures all Lua scripts are in the runtime
local used_scripts = F.flatten{luax_runtime, lua_runtime} : nub()
local expected_scripts = F.flatten{
    ls "libluax/**.lua"
        : difference(ls "libluax/lz4/**.lua"),
    ls "ext/c/**.lua",
    ls "ext/lua/**.lua",
    optional(lz4)    { ls "libluax/lz4/**.lua" },
    optional(socket) { ls "ext/opt/luasocket/**.lua" },
    optional(ssl)    { ls "ext/opt/luasec/**.lua" },
} : nub()
local unused_scripts = expected_scripts : difference(used_scripts)
if not unused_scripts:null()
then
    error("Some Lua scripts are not in the runtime:\n"..unused_scripts:sort():map(F.prefix"    "):unlines())
end

local luax_runtime_bundle = build "$tmp/lua_runtime_bundle.c" {
    "bundle", luax_runtime,
    args = {
        "-e lib",
        "-t c",
        bytecode,
        "-n luax",
    },
}

local luax_app = {
    "$luax_config_lua",
    "$luax_build_config_lua",
    ls "luax/**.lua",
}

local luax_app_bundle = build "$tmp/lua_app_bundle.c" {
    "bundle", luax_app,
    args = {
        "-e app",
        "-t c",
        bytecode,
        "-n luax",
    },
}

--===================================================================
section "Binaries and shared libraries"
---------------------------------------------------------------------

local binaries = F{}
local libraries = F{}

local liblua = {}
local libluax = {}
local main_luax = {}
local main_loader = {}
local main_libluax = {}
local binary = {}
local loader = {}
local shared_library = {}

-- imath is also provided by qmath, both versions shall be compatible
rule "diff" {
    description = "diff $in",
    command = "diff $in > $out",
}
phony "check_limath_version" {
    build "$tmp/check_limath_header_version" { "diff", "ext/c/lqmath/src/imath.h", "ext/c/limath/src/imath.h" },
    build "$tmp/check_limath_source_version" { "diff", "ext/c/lqmath/src/imath.c", "ext/c/limath/src/imath.c" },
}

local function additional_flags(name)
    return case(name:basename():splitext()) {
        usocket = "-Wno-#warnings",
        ec      = "-Wno-deprecated-declarations",
        context = "-Wno-deprecated-declarations",
        ssl     = {
            "-Wno-#warnings",
            "-Wno-deprecated-declarations",
        },
        luasec  = {
            "-Wno-cast-function-type-strict",
            case(compiler) {
                zig   = {"-Wno-pre-c11-compat"},
                gcc   = {},
                clang = {"-Wno-pre-c11-compat"},
            },
        },
        [Nil] = {},
    }
end

local function implicit_in(target, name)
    return {
        case(name:basename():splitext()) {
            version = "$luax_config_h",
            limath  = "check_limath_version",
            imath   = "check_limath_version",
            luasec  = openssl_libs[target.name],
            [Nil]   = {},
        },
        case(name:dirname():basename()) {
            luasec = openssl_libs[target.name],
        },
    }
end

targets_to_compile:foreach(function(target)

    liblua[target.name] = cc_ext[target.name]:static_lib("$tmp"/target.name/"lib/liblua.a") {
        sources.lua_c_files,
    }

    libluax[target.name] = cc[target.name]:static_lib("$tmp"/target.name/"lib/libluax.a") {
        F.flatten {
            sources.luax_c_files,
            luax_runtime_bundle,
        } : map(function(src)
            return cc[target.name]:compile("$tmp"/target.name/"obj"/src:chext".o") {
                src,
                additional_flags = additional_flags(src),
                implicit_in = implicit_in(target, src),
            }
        end),
        F.flatten {
            sources.third_party_c_files,
            case(target.os) {
                linux   = sources.linux_third_party_c_files,
                macos   = sources.linux_third_party_c_files,
                windows = sources.windows_third_party_c_files,
            },
        } : map(function(src)
            return cc_ext[target.name]:compile("$tmp"/target.name/"obj"/src:chext".o") {
                src,
                additional_flags = additional_flags(src),
                implicit_in = implicit_in(target, src),
            }
        end),
    }

    main_luax[target.name] = F.flatten { sources.luax_main_c_files }
        : map(function(src)
            return cc[target.name]:compile("$tmp"/target.name/"obj"/src:chext".o") {
                src,
                implicit_in = "$luax_config_h",
            }
        end)

    main_loader[target.name] = F.flatten { sources.loader_main_c_files }
        : map(function(src)
            return cc[target.name]:compile("$tmp"/target.name/"obj"/src:chext".o") {
                src,
                implicit_in = "$luax_config_h",
            }
        end)

    main_libluax[target.name] = F.flatten { sources.libluax_main_c_files }
        : map(function(src)
            return cc[target.name]:compile("$tmp"/target.name/"obj"/src:chext".o") {
                src,
                build_as_dll = case(target.os) {
                    windows = "-DLUA_BUILD_AS_DLL -DLUA_LIB",
                },
            }
        end)

    binary[target.name] = cc[target.name]("$tmp"/target.name/"bin"/"luax") {
        main_luax[target.name],
        main_libluax[target.name],
        liblua[target.name],
        libluax[target.name],
        openssl_libs[target.name],
        luax_app_bundle,
    }

    loader[target.name] = cc[target.name]("$tmp"/target.name/"lib"/"luax-loader") {
        main_loader[target.name],
        main_libluax[target.name],
        liblua[target.name],
        libluax[target.name],
        openssl_libs[target.name],
    }

    shared_library[target.name] = is_dynamic(target) and
        cc[target.name]:dynamic_lib("$tmp"/target.name/"lib/libluax") {
            main_libluax[target.name],
            case(target.os) {
                linux   = {},
                macos   = liblua[target.name],
                windows = liblua[target.name],
            },
            libluax[target.name],
            openssl_libs[target.name],
        }

end)

var "luax" ("$bin"/binary[sys.name]:basename())

acc(binaries) {
    build.cp "$luax" { binary[sys.name] }
}

if shared_library[sys.name] then
    acc(libraries) {
        build.cp("$lib"/shared_library[sys.name]:basename()) { shared_library[sys.name] }
    }
end

--===================================================================
section "LuaX archives"
---------------------------------------------------------------------

rule "lzip" {
    description = "lzip $in",
    command = { "$lzip", ssl and "-6" or "-0", "$in --output=- > $out" },
    implicit_in = "$lzip",
}

local compress = F.curry(function(dest, source)
    return build_once(dest/source:basename()..".lz") { "lzip", source }
end)

rule "pack" {
    description = "pack $out",
    command = "$luax tools/pack.lua $in -o $out $flags";
    implicit_in = "$luax tools/pack.lua",
}

local function luax_archive(archive, compilation_targets)

    return build(archive) { "pack",
        flags = {
            "-z none",
            "-s $tmp/lib/",
        },

        -- Lua runtime
        build_once "$tmp/lib/lua_runtime/libluax.lar" {
            "bundle", lua_runtime,
            args = {
                "-e lib",
                "-t lib",
                "-n luax",
                "-z none", -- do not compress the Lua runtime => no external lzip required by luax.lua
            },
        },

        compilation_targets and {

            -- Lua headers
            F.map(compress "$tmp/lib/headers", {
                "lua/lua.h",
                "lua/luaconf.h",
                "lua/lauxlib.h",
            }),

            -- Binary runtimes
            compilation_targets : map(function(target)
                local libs = F.flatten {
                    main_luax[target.name],
                    main_libluax[target.name],
                    libluax[target.name],
                    liblua[target.name],
                    openssl_libs[target.name],
                }
                return F.map(compress("$tmp/lib/targets"/target.name), libs)
            end),

            -- Loaders
            compilation_targets : map(function(target)
                return compress("$tmp/lib/loaders"/target.name) (loader[target.name])
            end),

        } or {},

    }

end

acc(libraries) {
    luax_archive("$lib/libluax.lar", cross and targets or host_targets),
}

local luax_lar = targets : map2t(function(target)
    return target.name, release and luax_archive("$tmp/dist"/target.name/"lib/libluax.lar", cross and targets or F{target})
end)
luax_lar.lua = luax_archive("$tmp/dist/lua/lib/libluax.lar")

--===================================================================
section "LuaX Lua implementation"
---------------------------------------------------------------------

--===================================================================
section "$lib/libluax.lua"
---------------------------------------------------------------------

acc(libraries) {
    build "$lib/libluax.lua" {
        "bundle", "$luax_config_lua", "$luax_build_config_lua", lua_runtime,
        args = {
            "-e lib",
            "-t lua",
            "-n luax",
        },
    }
}

--===================================================================
section "$bin/luax.lua"
---------------------------------------------------------------------

rule "luax-bundle" {
    description = "bundle $out",
    command = {
        "PATH=$bin:$$PATH",
        "LUA_PATH=\"$lua_path\"",
        "LUAX_LIB=$lib",
        "$lua luax/luax.lua compile -c -q $args -o $out $in",
    },
    implicit_in = {
        "$lua luax/luax.lua",
        "$lzip",
        "$lib/libluax.lua",
        "$luax_config_lua",
        "$luax_build_config_lua",
        libraries,
    },
}

acc(binaries) {
    build "$bin/luax.lua" {
        "luax-bundle", luax_app,
        args = "-t lua",
    },
}

--===================================================================
section "$bin/luax-pandoc.lua"
---------------------------------------------------------------------

acc(binaries) {
    build "$bin/luax-pandoc.lua" {
        "luax-bundle", luax_app,
        args = "-t pandoc",
    },
}

--===================================================================
section "Tests"
---------------------------------------------------------------------

local imported_test_sources = ls "tests/luax-tests/to_be_imported-*.lua"
local test_sources = {
    ls "tests/luax-tests/*.*" : difference(imported_test_sources),
    build "$test/resource.txt.lz" { "lzip", "tests/luax-tests/resource.txt", level=6 },
}
local test_main = "tests/luax-tests/main.lua"

local libc = case(sys.os) {
    linux   = "gnu",
    macos   = "none",
    windows = "gnu",
}

local test_options = {
    "BUILD=$builddir",
    optional(lz4)    { "USE_LZ4=1" },
    optional(socket) { "USE_SOCKET=1" },
    optional(ssl)    { "USE_SSL=1" },
}

local native_targets = (cross and targets or F{})
    : filter(function(t) return t.os==sys.os and t.arch==sys.arch end)
    : map(function(t) return t.name end)

local _port = 8000
local function new_httpd_port_range()
    _port = _port + 10
    return _port
end

var "httpd" "$test/httpd"
cc.host "$httpd" { "tests/luax-tests/httpd.c" }

acc(test) {

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

    build "$test/test-1-luax_executable.ok" { test_sources,
        description = "test $out",
        command = {
            sanitizer_options,
            "$luax compile -q -b -k test-1-key -o $test/test-luax $in",
            "&&",
            "PATH=$bin:$$PATH",
            "LUA_PATH='tests/luax-tests/?.lua;luax/?.lua'",
            "LUA_CPATH='foo/?.so'",
            "TEST_NUM=1",
            test_options,
            optional(sys.os=="linux") {
                "HTTP_SERVER=$httpd",
                "HTTP_PORT_RANGE="..new_httpd_port_range(),
            },
            "LUAX=$luax",
            "ARCH="..sys.arch, "OS="..sys.os, "LIBC="..libc, "EXE="..sys.exe, "SO="..sys.so, "NAME="..sys.name,
            "$test/test-luax Lua is great",
            "&&",
            "touch $out",
        },
        implicit_in = {
            "$luax",
            "$lib/libluax.lar",
            imported_test_sources,
            "$httpd",
        },
    },

    not san and {
        ({"native"} .. native_targets) : mapi(function(i, target_name)
        return F{ false, true } : map(function(use_cc)
            local test_libc = ("-musl"):is_suffix_of(target_name) and "musl" or libc
            local test_name = target_name=="native" and sys.name or target_name
            local exe_name = "$test/test-compiled"..(use_cc and "-c" or "").."-"..i
            return build("$test/test-1-"..i.."-compiled_executable"..(use_cc and "-c" or "")..".ok") { test_sources,
                description = "test $out",
                command = {
                    sanitizer_options,
                    "$luax compile -q", "-t", target_name, "-b -k test-1-key",
                        use_cc and "-c" or {},
                        "-o", exe_name,
                        "$in",
                    "&&",
                    "PATH=$bin:$$PATH",
                    "LUA_PATH='tests/luax-tests/?.lua;luax/?.lua'",
                    "LUA_CPATH='foo/?.so'",
                    "TEST_NUM=1", "TEST_CASE="..i,
                    test_options,
                    "LUAX=$luax",
                    "IS_COMPILED=true", "EXE_NAME="..exe_name,
                    "ARCH="..sys.arch, "OS="..sys.os, "LIBC="..test_libc, "EXE="..sys.exe, "SO="..sys.so, "NAME="..test_name,
                    exe_name, "Lua is great",
                    "&&",
                    "touch $out",
                },
                implicit_in = {
                    "$luax",
                    "$lib/libluax.lar",
                    libraries,
                    imported_test_sources,
                },
            }
        end)
        end),
    } or {},

}

local pandoc_version = (sh"pandoc --version" or "0") : match"[%d%.]+" : split"%." : map(tonumber)
local has_pandoc = F.op.uge(pandoc_version, {3, 1, 2})

if not san then
acc(test) {

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

    build "$test/test-2-lib.ok" { test_main,
        description = "test $out",
        command = {
            sanitizer_options,
            "export LUA_CPATH=;",
            "eval \"$$($luax env)\";",
            "PATH=$bin:$$PATH",
            "LUA_PATH='tests/luax-tests/?.lua'",
            "TEST_NUM=2",
            test_options,
            "ARCH="..sys.arch, "OS="..sys.os, "LIBC="..libc, "EXE="..sys.exe, "SO="..sys.so, "NAME="..sys.name,
            "$lua -l libluax $in Lua is great",
            "&&",
            "touch $out",
        },
        implicit_in = {
            "$lua",
            "$luax",
            "$lib/libluax.lar",
            libraries,
            test_sources,
            imported_test_sources,
        },
    },

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

    build "$test/test-3-lua.ok" { test_main,
        description = "test $out",
        command = {
            sanitizer_options,
            "PATH=$bin:$$PATH",
            "LIBC=lua LUA_PATH='$lib/?.lua;tests/luax-tests/?.lua'",
            "TEST_NUM=3",
            test_options,
            "ARCH="..sys.arch, "OS="..sys.os, "LIBC=lua", "EXE="..sys.exe, "SO="..sys.so, "NAME="..sys.name,
            "$lua -l libluax $in Lua is great",
            "&&",
            "touch $out",
        },
        implicit_in = {
            "$lua",
            "$lib/libluax.lua",
            "$lib/libluax.lar",
            "$lzip",
            libraries,
            test_sources,
            imported_test_sources,
        },
    },

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

    build "$test/test-4-lua-luax-lua.ok" { test_main,
        description = "test $out",
        command = {
            sanitizer_options,
            "PATH=$bin:$$PATH",
            "LIBC=lua LUA_PATH='$lib/?.lua;tests/luax-tests/?.lua'",
            "TEST_NUM=4",
            test_options,
            "ARCH="..sys.arch, "OS="..sys.os, "LIBC=lua", "EXE="..sys.exe, "SO="..sys.so, "NAME="..sys.name,
            "$bin/luax.lua $in Lua is great",
            "&&",
            "touch $out",
        },
        implicit_in = {
            "$lua",
            "$bin/luax.lua",
            "$lib/libluax.lar",
            "$lzip",
            test_sources,
            imported_test_sources,
        },
    },

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

    has_pandoc and build "$test/test-5-pandoc-luax-lua.ok" { test_main,
        description = "test $out",
        command = {
            sanitizer_options,
            "PATH=$bin:$$PATH",
            "LIBC=lua LUA_PATH='$lib/?.lua;tests/luax-tests/?.lua'",
            "TEST_NUM=5",
            test_options,
            "ARCH="..sys.arch, "OS="..sys.os, "LIBC=lua", "EXE="..sys.exe, "SO="..sys.so, "NAME="..sys.name,
            "pandoc lua -l libluax $in Lua is great",
            "&&",
            "touch $out",
        },
        implicit_in = {
            "$lua",
            "$lib/libluax.lua",
            "$lib/libluax.lar",
            "$lzip",
            libraries,
            test_sources,
            imported_test_sources,
        },
    } or {},

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

    build "$test/test-ext-1-lua.ok" { "tests/external_interpreter_tests/external_interpreters.lua",
        description = "test $out",
        command = {
            sanitizer_options,
            "eval \"$$($luax env)\";",
            "$luax compile -q -b -k test-ext-1-key -t lua -o $test/ext-lua $in",
            "&&",
            "PATH=$bin:$$PATH",
            "TARGET=lua",
            test_options,
            "$test/ext-lua Lua is great",
            "&&",
            "touch $out",
        },
        implicit_in = {
            "$lib/libluax.lua",
            "$luax",
            "$lib/libluax.lar",
            binaries,
        },
    },

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

    build "$test/test-ext-2-luax.ok" { "tests/external_interpreter_tests/external_interpreters.lua",
        description = "test $out",
        command = {
            sanitizer_options,
            "eval \"$$($luax env)\";",
            "$luax compile -q -b -k test-ext-2-key -t luax -o $test/ext-luax-key $in",
            "&&",
            "PATH=$bin:$$PATH",
            "TARGET=luax-key",
            test_options,
            "$test/ext-luax-key Lua is great",
            "&&",
            "touch $out",
        },
        implicit_in = {
            "$lib/libluax.lua",
            "$luax",
            "$lib/libluax.lar",
        },
    },

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

    build "$test/test-ext-3-luax.ok" { "tests/external_interpreter_tests/external_interpreters.lua",
        description = "test $out",
        command = {
            sanitizer_options,
            "eval \"$$($luax env)\";",
            "$luax compile -q -b -z -t luax -o $test/ext-luax-z $in",
            "&&",
            "PATH=$bin:$$PATH",
            "TARGET=luax-z",
            test_options,
            "$test/ext-luax-z Lua is great",
            "&&",
            "touch $out",
        },
        implicit_in = {
            "$lib/libluax.lua",
            "$luax",
            "$lib/libluax.lar",
        },
    },

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

    has_pandoc and build "$test/test-ext-4-pandoc.ok" { "tests/external_interpreter_tests/external_interpreters.lua",
        description = "test $out",
        command = {
            sanitizer_options,
            "eval \"$$($luax env)\";",
            "$luax compile -q -t pandoc -o $test/ext-pandoc $in", -- no bytecode to remain compatible with pandoc
            "&&",
            "PATH=$bin:$$PATH",
            "TARGET=pandoc",
            test_options,
            "$test/ext-pandoc Lua is great",
            "&&",
            "touch $out",
        },
        implicit_in = {
            "$lib/libluax.lua",
            "$luax",
            "$lib/libluax.lar",
            binaries,
        },
    } or {},

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

}
end

--===================================================================
section "Documentation"
---------------------------------------------------------------------

build.lsvg.svg
    : set "cmd" "$luax tools/lsvg.luax"
    : add "implicit_in" "$luax tools/lsvg.luax"
    : set "depfile" "$tmp/$out.d"

if has_pandoc then

local markdown_sources = ls "doc/src/*.md"

local images = {
    build.lsvg.svg "doc/luax-banner.svg" {"doc/src/luax-logo.lua", args={1024,  192}},
    build.lsvg.svg "doc/luax-logo.svg"   {"doc/src/luax-logo.lua", args={ 256,  256}},
}

acc(doc)(images)

local ypp_config_params = {
    build.ypp_vars {
        BYTECODE = bytecode,
        LUAX = "$luax",
    },
    build.ypp_vars(LUAX),
}

local gfm = pipe {
    ypp : new "ypp.md"
        : add "implicit_in" "$lib/libluax.lar"
        : add "flags" { ypp_config_params },
    build.pandoc_gfm : new "pandoc-gfm-luax"
        : add "flags" "--lua-filter doc/src/fix_links.lua"
        : add "implicit_in" { "doc/src/fix_links.lua", images }
}

acc(doc) {

    gfm "README.md" { "doc/src/luax.md" },

    markdown_sources : map(function(src)
        return gfm("doc"/src:basename()) { src }
    end)

}

end

--===================================================================
section "Update dist for all targets"
---------------------------------------------------------------------

local function pure_lua(name)
    local ext = name:ext()
    return ext==".lua"
end

local dist = (function()
    if not release then return {} end

    var "release" ("$builddir/release"/LUAX.VERSION)

    local cp_to = F.curry(function(dest, files)
            return F.flatten{files} : map(function(file)
                return build.cp("$dist"/dest/file:basename()) { file }
            end)
    end)

    return {
        targets : map(function(target)
            local name = F.flatten { "luax", LUAX.VERSION, target.name } : str "-"
            cp_to(name/"bin") {
                binary[target.name],
                binaries:filter(pure_lua),
            }
            cp_to(name/"lib") {
                shared_library[target.name] or {},
                libraries:filter(pure_lua),
                luax_lar[target.name],
            }
            return build.tar("$release"/name..".tar.gz") {
                base = "$dist",
                name = name,
            }
        end),
        (function()
            local name = F.flatten { "luax", LUAX.VERSION, "lua" } : str "-"
            cp_to(name/"bin") {
                binaries:filter(pure_lua),
            }
            cp_to(name/"lib") {
                libraries:filter(pure_lua),
                luax_lar.lua
            }
            return build.tar("$release"/name..".tar.gz") {
                base = "$dist",
                name = name,
            }
        end)(),
        (function()
            local releasenote = ypp : new "ypp-releasenote"
                : add "implicit_in" "$lib/libluax.lar"
                : set "depfile" "$tmp/$out.d"
                : add "flags" {
                    build.ypp_vars {
                        VERSION = LUAX.VERSION,
                        URL = "https://"..LUAX.URL,
                        COMPILER = BUILD_CONFIG.COMPILER_FULL_VERSION,
                        OPTIONS = F.flatten {
                            mode,
                            use_lto and "lto" or {},
                        } : str ", ",
                        COMPRESSION = F.flatten{"Lzip", optional(lz4)"LZ4"} : str " + ",
                        SOCKETS = F.flatten{socket and "LuaSocket" or "no", optional(ssl){"LuaSec", "OpenSSL"}} : str " + ",
                        CROSS = cross and "yes" or "no",
                    },
                }
            return releasenote "$release/ReleaseNote.md" { "tools/ReleaseNote.md.in" }
        end)(),
    }

end)()

--===================================================================
section "Shorcuts"
---------------------------------------------------------------------

acc(compile) {binaries, libraries, dist}

install "bin" {binaries}
install "lib" {libraries}

clean "$builddir"

phony "compile" (compile)
default "compile"
help "compile" "compile LuaX"

if #test > 0 then

    phony "test-fast" (test[1])
    help "test-fast" "run LuaX tests (fast, host tests only)"

    phony "test" (test)
    help "test" "run all LuaX tests"

end

if #doc > 0 then

    phony "doc" (doc)
    help "doc" "update LuaX documentation"

end

phony "all" {
    "compile",
    "test",
    optional(#doc > 0) "doc",
}
help "all" "alias for compile, test and doc"

phony "update" "update_modules"
help "update" "update third-party modules"
