neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

build.zig (32732B)


      1 const std = @import("std");
      2 const LazyPath = std.Build.LazyPath;
      3 const Compile = std.Build.Step.Compile;
      4 const build_lua = @import("src/build_lua.zig");
      5 const gen = @import("src/gen/gen_steps.zig");
      6 const runtime = @import("runtime/gen_runtime.zig");
      7 const tests = @import("test/run_tests.zig");
      8 
      9 const version = struct {
     10    const major = 0;
     11    const minor = 12;
     12    const patch = 0;
     13    const prerelease = "-dev";
     14 
     15    const api_level = 14;
     16    const api_level_compat = 0;
     17    const api_prerelease = true;
     18 };
     19 
     20 pub const SystemIntegrationOptions = packed struct {
     21    lpeg: bool,
     22    lua: bool,
     23    tree_sitter: bool,
     24    unibilium: bool,
     25    utf8proc: bool,
     26    uv: bool,
     27 };
     28 
     29 // TODO(bfredl): this is for an upstream issue
     30 pub fn lazyArtifact(d: *std.Build.Dependency, name: []const u8) ?*std.Build.Step.Compile {
     31    var found: ?*std.Build.Step.Compile = null;
     32    for (d.builder.install_tls.step.dependencies.items) |dep_step| {
     33        const inst = dep_step.cast(std.Build.Step.InstallArtifact) orelse continue;
     34        if (std.mem.eql(u8, inst.artifact.name, name)) {
     35            if (found != null) std.debug.panic("artifact name '{s}' is ambiguous", .{name});
     36            found = inst.artifact;
     37        }
     38    }
     39    return found;
     40 }
     41 
     42 pub fn build(b: *std.Build) !void {
     43    const target = b.standardTargetOptions(.{});
     44    const optimize = b.standardOptimizeOption(.{});
     45 
     46    const t = target.result;
     47    const os_tag = t.os.tag;
     48    const is_windows = (os_tag == .windows);
     49    const is_linux = (os_tag == .linux);
     50    const is_darwin = os_tag.isDarwin();
     51    const modern_unix = is_darwin or os_tag.isBSD() or is_linux;
     52 
     53    const cross_compiling = b.option(bool, "cross", "cross compile") orelse false;
     54    // TODO(bfredl): option to set nlua0 target explicitly when cross compiling?
     55    const target_host = if (cross_compiling) b.graph.host else target;
     56    // without cross_compiling we like to reuse libluv etc at the same optimize level
     57    const optimize_host = if (cross_compiling) .ReleaseSafe else optimize;
     58 
     59    const use_unibilium = b.option(bool, "unibilium", "use unibilium") orelse true;
     60 
     61    // puc lua 5.1 is not ReleaseSafe "safe"
     62    const optimize_lua = if (optimize == .Debug or optimize == .ReleaseSafe) .ReleaseSmall else optimize;
     63 
     64    const use_luajit = b.option(bool, "luajit", "use luajit") orelse true;
     65    const lualib_name = if (use_luajit) "luajit" else "lua5.1";
     66    const host_use_luajit = if (cross_compiling) false else use_luajit;
     67    const E = enum { luajit, lua51 };
     68 
     69    const system_integration_options = SystemIntegrationOptions{
     70        .lpeg = b.systemIntegrationOption("lpeg", .{}),
     71        .lua = b.systemIntegrationOption("lua", .{}),
     72        .tree_sitter = b.systemIntegrationOption("tree-sitter", .{}),
     73        .unibilium = b.systemIntegrationOption("unibilium", .{}),
     74        .utf8proc = b.systemIntegrationOption("utf8proc", .{}),
     75        .uv = b.systemIntegrationOption("uv", .{}),
     76    };
     77 
     78    const ziglua = b.dependency("zlua", .{
     79        .target = target,
     80        .optimize = optimize_lua,
     81        .lang = if (use_luajit) E.luajit else E.lua51,
     82        .shared = false,
     83        .system_lua = system_integration_options.lua,
     84    });
     85    const ziglua_host = if (cross_compiling) b.dependency("zlua", .{
     86        .target = target_host,
     87        .optimize = .ReleaseSmall,
     88        .lang = if (host_use_luajit) E.luajit else E.lua51,
     89        .system_lua = system_integration_options.lua,
     90        .shared = false,
     91    }) else ziglua;
     92    var lua: ?*Compile = null;
     93    var libuv: ?*Compile = null;
     94    var libluv: ?*Compile = null;
     95    var libluv_host: ?*Compile = null;
     96    if (!system_integration_options.lua) {
     97        // this is currently not necessary, as ziglua currently doesn't use lazy dependencies
     98        // to circumvent ziglua.artifact() failing in a bad way.
     99        lua = lazyArtifact(ziglua, "lua") orelse return;
    100        if (cross_compiling) {
    101            _ = lazyArtifact(ziglua_host, "lua") orelse return;
    102        }
    103    }
    104    if (!system_integration_options.uv) {
    105        if (b.lazyDependency("libuv", .{ .target = target, .optimize = optimize })) |dep| {
    106            libuv = dep.artifact("uv");
    107            libluv = try build_lua.build_libluv(b, target, optimize, lua, libuv.?, use_luajit);
    108 
    109            libluv_host = if (cross_compiling) libluv_host: {
    110                const libuv_dep_host = b.lazyDependency("libuv", .{
    111                    .target = target_host,
    112                    .optimize = optimize_host,
    113                });
    114                const libuv_host = libuv_dep_host.?.artifact("uv");
    115                break :libluv_host try build_lua.build_libluv(
    116                    b,
    117                    target_host,
    118                    optimize_host,
    119                    ziglua_host.artifact("lua"),
    120                    libuv_host,
    121                    host_use_luajit,
    122                );
    123            } else libluv;
    124        }
    125    }
    126 
    127    const lpeg = if (system_integration_options.lpeg) null else b.lazyDependency("lpeg", .{});
    128 
    129    const iconv = if (is_windows or is_darwin) b.lazyDependency("libiconv", .{
    130        .target = target,
    131        .optimize = optimize,
    132    }) else null;
    133 
    134    const utf8proc = if (system_integration_options.utf8proc) null else b.lazyDependency("utf8proc", .{
    135        .target = target,
    136        .optimize = optimize,
    137    });
    138    const unibilium = if (use_unibilium and !system_integration_options.unibilium) b.lazyDependency("unibilium", .{
    139        .target = target,
    140        .optimize = optimize,
    141    }) else null;
    142 
    143    // TODO(bfredl): fix upstream bugs with UBSAN
    144    const optimize_ts = .ReleaseFast;
    145    const treesitter = if (system_integration_options.tree_sitter) null else b.lazyDependency("treesitter", .{
    146        .target = target,
    147        .optimize = optimize_ts,
    148    });
    149 
    150    const nlua0 = try build_lua.build_nlua0(
    151        b,
    152        target_host,
    153        optimize_host,
    154        host_use_luajit,
    155        ziglua_host,
    156        lpeg,
    157        libluv_host,
    158        system_integration_options,
    159    );
    160 
    161    // usual caveat emptor: might need to force a rebuild if the only change is
    162    // addition of new .c files, as those are not seen by any hash
    163    const subdirs = [_][]const u8{
    164        "", // src/nvim itself
    165        "os/",
    166        "api/",
    167        "api/private/",
    168        "msgpack_rpc/",
    169        "tui/",
    170        "tui/termkey/",
    171        "event/",
    172        "eval/",
    173        "lib/",
    174        "lua/",
    175        "viml/",
    176        "viml/parser/",
    177        "vterm/",
    178    };
    179 
    180    // source names _relative_ src/nvim/, not including other src/ subdircs
    181    var nvim_sources = try std.ArrayList(gen.SourceItem).initCapacity(b.allocator, 100);
    182    var nvim_headers = try std.ArrayList([]u8).initCapacity(b.allocator, 100);
    183 
    184    // both source headers and the {module}.h.generated.h files
    185    var api_headers = try std.ArrayList(std.Build.LazyPath).initCapacity(b.allocator, 10);
    186 
    187    // TODO(bfredl): these should just become subdirs..
    188    const windows_only = [_][]const u8{
    189        "pty_proc_win.c",
    190        "pty_proc_win.h",
    191        "pty_conpty_win.c",
    192        "pty_conpty_win.h",
    193        "os_win_console.c",
    194        "win_defs.h",
    195    };
    196    const unix_only = [_][]const u8{ "unix_defs.h", "pty_proc_unix.c", "pty_proc_unix.h" };
    197    const exclude_list = if (is_windows) &unix_only else &windows_only;
    198 
    199    const src_dir = b.build_root.handle;
    200    for (subdirs) |s| {
    201        var dir = try src_dir.openDir(b.fmt("src/nvim/{s}", .{s}), .{ .iterate = true });
    202        defer dir.close();
    203        var it = dir.iterateAssumeFirstIteration();
    204        const api_export = std.mem.eql(u8, s, "api/");
    205        const os_check = std.mem.eql(u8, s, "os/");
    206        entries: while (try it.next()) |entry| {
    207            if (entry.name.len < 3) continue;
    208            if (entry.name[0] < 'a' or entry.name[0] > 'z') continue;
    209            if (os_check) {
    210                for (exclude_list) |name| {
    211                    if (std.mem.eql(u8, name, entry.name)) {
    212                        continue :entries;
    213                    }
    214                }
    215            }
    216            if (std.mem.eql(u8, ".c", entry.name[entry.name.len - 2 ..])) {
    217                try nvim_sources.append(b.allocator, .{
    218                    .name = b.fmt("{s}{s}", .{ s, entry.name }),
    219                    .api_export = api_export,
    220                });
    221            }
    222            if (std.mem.eql(u8, ".h", entry.name[entry.name.len - 2 ..])) {
    223                try nvim_headers.append(b.allocator, b.fmt("{s}{s}", .{ s, entry.name }));
    224                if (api_export and !std.mem.eql(u8, "ui_events.in.h", entry.name)) {
    225                    try api_headers.append(b.allocator, b.path(b.fmt("src/nvim/{s}{s}", .{ s, entry.name })));
    226                }
    227            }
    228        }
    229    }
    230 
    231    const support_unittests = use_luajit;
    232 
    233    const gen_config = b.addWriteFiles();
    234 
    235    const version_lua = gen_config.add("nvim_version.lua", lua_version_info(b));
    236 
    237    var config_str = b.fmt("zig build -Doptimize={s}", .{@tagName(optimize)});
    238    if (cross_compiling) {
    239        config_str = b.fmt("{s} -Dcross -Dtarget={s} (host: {s})", .{
    240            config_str,
    241            try t.linuxTriple(b.allocator),
    242            try b.graph.host.result.linuxTriple(b.allocator),
    243        });
    244    }
    245 
    246    const versiondef_step = b.addConfigHeader(.{
    247        .style = .{ .cmake = b.path("cmake.config/versiondef.h.in") },
    248    }, .{
    249        .NVIM_VERSION_MAJOR = version.major,
    250        .NVIM_VERSION_MINOR = version.minor,
    251        .NVIM_VERSION_PATCH = version.patch,
    252        .NVIM_VERSION_PRERELEASE = version.prerelease,
    253        .NVIM_VERSION_MEDIUM = "",
    254        .VERSION_STRING = "TODO", // TODO(bfredl): not sure what to put here. summary already in "config_str"
    255        .CONFIG = config_str,
    256    });
    257    _ = gen_config.addCopyFile(versiondef_step.getOutput(), "auto/versiondef.h"); // run_preprocessor() workaronnd
    258 
    259    const ptrwidth = t.ptrBitWidth() / 8;
    260    const sysconfig_step = b.addConfigHeader(.{
    261        .style = .{ .cmake = b.path("cmake.config/config.h.in") },
    262    }, .{
    263        .SIZEOF_INT = t.cTypeByteSize(.int),
    264        .SIZEOF_INTMAX_T = t.cTypeByteSize(.longlong), // TODO
    265        .SIZEOF_LONG = t.cTypeByteSize(.long),
    266        .SIZEOF_SIZE_T = ptrwidth,
    267        .SIZEOF_VOID_PTR = ptrwidth,
    268 
    269        .PROJECT_NAME = "nvim",
    270 
    271        .HAVE__NSGETENVIRON = is_darwin,
    272        .HAVE_FD_CLOEXEC = modern_unix,
    273        .HAVE_FSEEKO = modern_unix,
    274        .HAVE_LANGINFO_H = modern_unix,
    275        .HAVE_NL_LANGINFO_CODESET = modern_unix,
    276        .HAVE_NL_MSG_CAT_CNTR = t.isGnuLibC(),
    277        .HAVE_PWD_FUNCS = modern_unix,
    278        .HAVE_READLINK = modern_unix,
    279        .HAVE_STRNLEN = modern_unix,
    280        .HAVE_STRCASECMP = modern_unix,
    281        .HAVE_STRINGS_H = modern_unix,
    282        .HAVE_STRNCASECMP = modern_unix,
    283        .HAVE_STRPTIME = modern_unix,
    284        .HAVE_XATTR = is_linux,
    285        .HAVE_SYS_SDT_H = false,
    286        .HAVE_SYS_UTSNAME_H = modern_unix,
    287        .HAVE_SYS_WAIT_H = false, // unused
    288        .HAVE_TERMIOS_H = modern_unix,
    289        .HAVE_WORKING_LIBINTL = t.isGnuLibC(),
    290        .UNIX = modern_unix,
    291        .CASE_INSENSITIVE_FILENAME = is_darwin or is_windows,
    292        .HAVE_SYS_UIO_H = modern_unix,
    293        .HAVE_READV = modern_unix,
    294        .HAVE_DIRFD_AND_FLOCK = modern_unix,
    295        .HAVE_FORKPTY = modern_unix and !is_darwin, // also on Darwin but we lack the headers :(
    296        .HAVE_BE64TOH = modern_unix and !is_darwin,
    297        .ORDER_BIG_ENDIAN = t.cpu.arch.endian() == .big,
    298        .ENDIAN_INCLUDE_FILE = "endian.h",
    299        .HAVE_EXECINFO_BACKTRACE = modern_unix and !t.isMuslLibC(),
    300        .HAVE_BUILTIN_ADD_OVERFLOW = true,
    301        .HAVE_WIMPLICIT_FALLTHROUGH_FLAG = true,
    302        .HAVE_BITSCANFORWARD64 = null,
    303 
    304        .VTERM_TEST_FILE = "test/vterm_test_output", // TODO(bfredl): revisit when porting libvterm tests
    305    });
    306 
    307    const system_install_path = b.option([]const u8, "install-path", "Install path (for packagers)");
    308    const install_path = system_install_path orelse b.install_path;
    309    const lib_dir = if (system_install_path) |path| b.fmt("{s}/lib", .{path}) else b.lib_dir;
    310    _ = gen_config.addCopyFile(sysconfig_step.getOutput(), "auto/config.h"); // run_preprocessor() workaronnd
    311 
    312    _ = gen_config.add("auto/pathdef.h", b.fmt(
    313        \\char *default_vim_dir = "{s}/share/nvim";
    314        \\char *default_vimruntime_dir = "";
    315        \\char *default_lib_dir = "{s}/nvim";
    316        // b.lib_dir is typically b.install_path + "/lib" but may be overridden
    317    , .{ try replace_backslashes(b, install_path), try replace_backslashes(b, lib_dir) }));
    318 
    319    const opt_version_string = b.option(
    320        []const u8,
    321        "version-string",
    322        "Override Neovim version string. Default is to find out with git.",
    323    );
    324    const version_medium = if (opt_version_string) |version_string| version_string else v: {
    325        var code: u8 = undefined;
    326        const version_string = b.fmt("v{d}.{d}.{d}", .{
    327            version.major,
    328            version.minor,
    329            version.patch,
    330        });
    331        const git_describe_untrimmed = b.runAllowFail(&[_][]const u8{
    332            "git",
    333            "-C", b.build_root.path orelse ".", // affects the --git-dir argument
    334            "--git-dir", ".git", // affected by the -C argument
    335            "describe", "--dirty", "--match", "v*.*.*", //
    336        }, &code, .Ignore) catch {
    337            break :v version_string;
    338        };
    339        const git_describe = std.mem.trim(u8, git_describe_untrimmed, " \n\r");
    340 
    341        const num_parts = std.mem.count(u8, git_describe, "-") + 1;
    342        if (num_parts < 3) {
    343            break :v version_string; // achtung: unrecognized format
    344        }
    345 
    346        var it = std.mem.splitScalar(u8, git_describe, '-');
    347        const tagged_ancestor = it.first();
    348        _ = tagged_ancestor;
    349        const commit_height = it.next().?;
    350        const commit_id = it.next().?;
    351        const maybe_dirty = it.next();
    352 
    353        // Check that the commit hash is prefixed with a 'g' (a Git convention).
    354        if (commit_id.len < 1 or commit_id[0] != 'g') {
    355            std.debug.print("Unexpected `git describe` output: {s}\n", .{git_describe});
    356            break :v version_string;
    357        }
    358 
    359        const dirty_tag = if (maybe_dirty) |dirty| b.fmt("-{s}", .{dirty}) else "";
    360 
    361        break :v b.fmt("{s}-dev-{s}+{s}{s}", .{ version_string, commit_height, commit_id, dirty_tag });
    362    };
    363 
    364    const versiondef_git = gen_config.add("auto/versiondef_git.h", b.fmt(
    365        \\#define NVIM_VERSION_MEDIUM "{s}"
    366        \\#define NVIM_VERSION_BUILD "zig"
    367        \\
    368    , .{version_medium}));
    369 
    370    // TODO(zig): using getEmittedIncludeTree() is ugly af. we want unittests
    371    // to reuse the std.build.Module include_path thing
    372    var unittest_include_path: std.ArrayList(LazyPath) = try .initCapacity(b.allocator, 2);
    373    try unittest_include_path.append(b.allocator, b.path("src/"));
    374    try unittest_include_path.append(b.allocator, gen_config.getDirectory());
    375    if (system_integration_options.lua) {
    376        try appendSystemIncludePath(b, &unittest_include_path, lualib_name);
    377    } else if (lua) |compile| {
    378        try unittest_include_path.append(b.allocator, compile.getEmittedIncludeTree());
    379    }
    380    if (system_integration_options.uv) {
    381        try appendSystemIncludePath(b, &unittest_include_path, "libuv");
    382        try appendSystemIncludePath(b, &unittest_include_path, "libluv");
    383    } else {
    384        if (libuv) |compile| try unittest_include_path.append(b.allocator, compile.getEmittedIncludeTree());
    385        if (libluv) |compile| try unittest_include_path.append(b.allocator, compile.getEmittedIncludeTree());
    386    }
    387    if (system_integration_options.utf8proc) {
    388        try appendSystemIncludePath(b, &unittest_include_path, "libutf8proc");
    389    } else if (utf8proc) |dep| {
    390        try unittest_include_path.append(b.allocator, dep.artifact("utf8proc").getEmittedIncludeTree());
    391    }
    392    if (use_unibilium) {
    393        if (system_integration_options.unibilium) {
    394            try appendSystemIncludePath(b, &unittest_include_path, "unibilium");
    395        } else if (unibilium) |dep| {
    396            try unittest_include_path.append(b.allocator, dep.artifact("unibilium").getEmittedIncludeTree());
    397        }
    398    }
    399    if (system_integration_options.tree_sitter) {
    400        try appendSystemIncludePath(b, &unittest_include_path, "tree-sitter");
    401    } else if (treesitter) |dep| {
    402        try unittest_include_path.append(b.allocator, dep.artifact("tree-sitter").getEmittedIncludeTree());
    403    }
    404    if (iconv) |dep| {
    405        try unittest_include_path.append(b.allocator, dep.artifact("iconv").getEmittedIncludeTree());
    406    }
    407 
    408    const gen_headers, const funcs_data = try gen.nvim_gen_sources(
    409        b,
    410        nlua0,
    411        &nvim_sources,
    412        &nvim_headers,
    413        &api_headers,
    414        versiondef_git,
    415        version_lua,
    416    );
    417 
    418    const test_config_step = b.addWriteFiles();
    419    _ = test_config_step.add("test/cmakeconfig/paths.lua", try test_config(b));
    420 
    421    const test_gen_step = b.step("gen_headers", "debug: output generated headers");
    422    const config_install = b.addInstallDirectory(.{
    423        .source_dir = gen_config.getDirectory(),
    424        .install_dir = .prefix,
    425        .install_subdir = "config/",
    426    });
    427    test_gen_step.dependOn(&config_install.step);
    428    test_gen_step.dependOn(&b.addInstallDirectory(.{
    429        .source_dir = gen_headers.getDirectory(),
    430        .install_dir = .prefix,
    431        .install_subdir = "headers/",
    432    }).step);
    433 
    434    const nvim_exe = b.addExecutable(.{
    435        .name = "nvim",
    436        .root_module = b.createModule(.{
    437            .target = target,
    438            .optimize = optimize,
    439            .link_libc = true,
    440        }),
    441    });
    442    nvim_exe.rdynamic = true; // -E
    443 
    444    if (system_integration_options.lua) {
    445        nvim_exe.root_module.linkSystemLibrary(lualib_name, .{});
    446    } else if (lua) |compile| {
    447        nvim_exe.root_module.linkLibrary(compile);
    448    }
    449    if (system_integration_options.uv) {
    450        nvim_exe.root_module.linkSystemLibrary("libuv", .{});
    451        nvim_exe.root_module.linkSystemLibrary("libluv", .{});
    452    } else {
    453        if (libuv) |compile| nvim_exe.root_module.linkLibrary(compile);
    454        if (libluv) |compile| nvim_exe.root_module.linkLibrary(compile);
    455    }
    456    if (iconv) |dep| nvim_exe.linkLibrary(dep.artifact("iconv"));
    457    if (system_integration_options.utf8proc) {
    458        nvim_exe.root_module.linkSystemLibrary("utf8proc", .{});
    459    } else if (utf8proc) |dep| {
    460        nvim_exe.root_module.linkLibrary(dep.artifact("utf8proc"));
    461    }
    462    if (use_unibilium) {
    463        if (system_integration_options.unibilium) {
    464            nvim_exe.root_module.linkSystemLibrary("unibilium", .{});
    465        } else if (unibilium) |dep| {
    466            nvim_exe.root_module.linkLibrary(dep.artifact("unibilium"));
    467        }
    468    }
    469    if (system_integration_options.tree_sitter) {
    470        nvim_exe.root_module.linkSystemLibrary("tree-sitter", .{});
    471    } else if (treesitter) |dep| {
    472        nvim_exe.root_module.linkLibrary(dep.artifact("tree-sitter"));
    473    }
    474    if (is_windows) {
    475        nvim_exe.linkSystemLibrary("netapi32");
    476    }
    477    nvim_exe.addIncludePath(b.path("src"));
    478    nvim_exe.addIncludePath(gen_config.getDirectory());
    479    nvim_exe.addIncludePath(gen_headers.getDirectory());
    480    try build_lua.add_lua_modules(
    481        b,
    482        t,
    483        nvim_exe.root_module,
    484        lpeg,
    485        use_luajit,
    486        false,
    487        system_integration_options,
    488    );
    489 
    490    var unit_test_sources = try std.ArrayList([]u8).initCapacity(b.allocator, 10);
    491    if (support_unittests) {
    492        var unit_test_fixtures = try src_dir.openDir("test/unit/fixtures/", .{ .iterate = true });
    493        defer unit_test_fixtures.close();
    494        var it = unit_test_fixtures.iterateAssumeFirstIteration();
    495        while (try it.next()) |entry| {
    496            if (entry.name.len < 3) continue;
    497            if (std.mem.eql(u8, ".c", entry.name[entry.name.len - 2 ..])) {
    498                try unit_test_sources.append(b.allocator, b.fmt("test/unit/fixtures/{s}", .{entry.name}));
    499            }
    500        }
    501    }
    502 
    503    const src_paths = try b.allocator.alloc([]u8, nvim_sources.items.len + unit_test_sources.items.len);
    504    for (nvim_sources.items, 0..) |s, i| {
    505        src_paths[i] = b.fmt("src/nvim/{s}", .{s.name});
    506    }
    507    @memcpy(src_paths[nvim_sources.items.len..], unit_test_sources.items);
    508 
    509    const flags = [_][]const u8{
    510        "-std=gnu99",
    511        "-DZIG_BUILD",
    512        "-D_GNU_SOURCE",
    513        if (support_unittests) "-DUNIT_TESTING" else "",
    514        if (use_luajit) "" else "-DNVIM_VENDOR_BIT",
    515        if (is_windows) "-DMSWIN" else "",
    516        if (is_windows) "-DWIN32_LEAN_AND_MEAN" else "",
    517        if (is_windows) "-DUTF8PROC_STATIC" else "",
    518        if (use_unibilium) "-DHAVE_UNIBILIUM" else "",
    519    };
    520    nvim_exe.addCSourceFiles(.{ .files = src_paths, .flags = &flags });
    521 
    522    nvim_exe.addCSourceFiles(.{ .files = &.{
    523        "src/xdiff/xdiffi.c",
    524        "src/xdiff/xemit.c",
    525        "src/xdiff/xhistogram.c",
    526        "src/xdiff/xpatience.c",
    527        "src/xdiff/xprepare.c",
    528        "src/xdiff/xutils.c",
    529        "src/cjson/lua_cjson.c",
    530        "src/cjson/fpconv.c",
    531        "src/cjson/strbuf.c",
    532    }, .flags = &flags });
    533 
    534    if (is_windows) {
    535        nvim_exe.addWin32ResourceFile(.{ .file = b.path("src/nvim/os/nvim.rc") });
    536    }
    537 
    538    const nvim_exe_step = b.step("nvim_bin", "only the binary (not a fully working install!)");
    539    const nvim_exe_install = b.addInstallArtifact(nvim_exe, .{});
    540 
    541    nvim_exe_step.dependOn(&nvim_exe_install.step);
    542 
    543    const gen_runtime = try runtime.nvim_gen_runtime(b, nlua0, funcs_data);
    544 
    545    const lua_dev_deps = b.dependency("lua_dev_deps", .{});
    546 
    547    const test_deps = b.step("test_deps", "test prerequisites");
    548    test_deps.dependOn(&nvim_exe_install.step);
    549    // running tests doesn't require copying the static runtime, only the generated stuff
    550    const test_runtime_install = b.addInstallDirectory(.{
    551        .source_dir = gen_runtime.getDirectory(),
    552        .install_dir = .prefix,
    553        .install_subdir = "runtime/",
    554    });
    555    test_deps.dependOn(&test_runtime_install.step);
    556 
    557    const nvim_dev = b.step("nvim_dev", "build the editor for development");
    558    b.default_step = nvim_dev;
    559 
    560    nvim_dev.dependOn(&nvim_exe_install.step);
    561    nvim_dev.dependOn(&test_runtime_install.step);
    562 
    563    // run from dev environment
    564    const run_cmd = b.addRunArtifact(nvim_exe);
    565    run_cmd.setEnvironmentVariable("VIMRUNTIME", try b.build_root.join(b.graph.arena, &.{"runtime"}));
    566    run_cmd.setEnvironmentVariable("NVIM_ZIG_INSTALL_DIR", b.getInstallPath(.prefix, "runtime"));
    567    run_cmd.step.dependOn(nvim_dev);
    568    run_cmd.addArgs(&.{ "--cmd", "let &rtp = &rtp.','.$NVIM_ZIG_INSTALL_DIR" });
    569    if (b.args) |args| {
    570        run_cmd.addArgs(args);
    571    }
    572    const run_step = b.step("run_dev", "run the editor (for development)");
    573    run_step.dependOn(&run_cmd.step);
    574 
    575    // installation
    576    const install = b.getInstallStep();
    577    install.dependOn(&nvim_exe_install.step);
    578    b.installDirectory(.{
    579        .source_dir = b.path("runtime/"),
    580        .install_dir = .prefix,
    581        .install_subdir = "share/nvim/runtime/",
    582    });
    583    b.installDirectory(.{
    584        .source_dir = gen_runtime.getDirectory(),
    585        .install_dir = .prefix,
    586        .install_subdir = "share/nvim/runtime/",
    587    });
    588 
    589    test_deps.dependOn(test_fixture(b, "shell-test", false, false, null, target, optimize, &flags));
    590    test_deps.dependOn(test_fixture(
    591        b,
    592        "tty-test",
    593        true,
    594        system_integration_options.uv,
    595        libuv,
    596        target,
    597        optimize,
    598        &flags,
    599    ));
    600    test_deps.dependOn(test_fixture(b, "pwsh-test", false, false, null, target, optimize, &flags));
    601    test_deps.dependOn(test_fixture(b, "printargs-test", false, false, null, target, optimize, &flags));
    602    test_deps.dependOn(test_fixture(b, "printenv-test", false, false, null, target, optimize, &flags));
    603    test_deps.dependOn(test_fixture(
    604        b,
    605        "streams-test",
    606        true,
    607        system_integration_options.uv,
    608        libuv,
    609        target,
    610        optimize,
    611        &flags,
    612    ));
    613 
    614    // tee: vendored in src/tee/
    615    const tee_exe = b.addExecutable(.{
    616        .name = "tee",
    617        .root_module = b.createModule(.{
    618            .target = target,
    619            .optimize = optimize,
    620        }),
    621    });
    622    tee_exe.addCSourceFile(.{ .file = b.path("src/tee/tee.c") });
    623    tee_exe.linkLibC();
    624    test_deps.dependOn(&b.addInstallArtifact(tee_exe, .{}).step);
    625 
    626    // xxd - hex dump utility (vendored from Vim)
    627    const xxd_exe = b.addExecutable(.{
    628        .name = "xxd",
    629        .root_module = b.createModule(.{
    630            .target = target,
    631            .optimize = optimize,
    632        }),
    633    });
    634    xxd_exe.addCSourceFile(.{ .file = b.path("src/xxd/xxd.c") });
    635    xxd_exe.linkLibC();
    636    test_deps.dependOn(&b.addInstallArtifact(xxd_exe, .{}).step);
    637 
    638    const parser_c = b.dependency("treesitter_c", .{ .target = target, .optimize = optimize_ts });
    639    test_deps.dependOn(add_ts_parser(b, "c", parser_c.path("."), false, target, optimize_ts, .test_));
    640    install.dependOn(add_ts_parser(b, "c", parser_c.path("."), false, target, optimize_ts, .install));
    641 
    642    const parser_markdown = b.dependency("treesitter_markdown", .{ .target = target, .optimize = optimize_ts });
    643    test_deps.dependOn(add_ts_parser(b, "markdown", parser_markdown.path("tree-sitter-markdown/"), true, target, optimize_ts, .test_));
    644    install.dependOn(add_ts_parser(b, "markdown", parser_markdown.path("tree-sitter-markdown/"), true, target, optimize_ts, .install));
    645    test_deps.dependOn(add_ts_parser(b, "markdown_inline", parser_markdown.path("tree-sitter-markdown-inline/"), true, target, optimize_ts, .test_));
    646    install.dependOn(add_ts_parser(b, "markdown_inline", parser_markdown.path("tree-sitter-markdown-inline/"), true, target, optimize_ts, .install));
    647 
    648    const parser_vim = b.dependency("treesitter_vim", .{ .target = target, .optimize = optimize_ts });
    649    test_deps.dependOn(add_ts_parser(b, "vim", parser_vim.path("."), true, target, optimize_ts, .test_));
    650    install.dependOn(add_ts_parser(b, "vim", parser_vim.path("."), true, target, optimize_ts, .install));
    651 
    652    const parser_vimdoc = b.dependency("treesitter_vimdoc", .{ .target = target, .optimize = optimize_ts });
    653    test_deps.dependOn(add_ts_parser(b, "vimdoc", parser_vimdoc.path("."), false, target, optimize_ts, .test_));
    654    install.dependOn(add_ts_parser(b, "vimdoc", parser_vimdoc.path("."), false, target, optimize_ts, .install));
    655 
    656    const parser_lua = b.dependency("treesitter_lua", .{ .target = target, .optimize = optimize_ts });
    657    test_deps.dependOn(add_ts_parser(b, "lua", parser_lua.path("."), true, target, optimize_ts, .test_));
    658    install.dependOn(add_ts_parser(b, "lua", parser_lua.path("."), true, target, optimize_ts, .install));
    659 
    660    const parser_query = b.dependency("treesitter_query", .{ .target = target, .optimize = optimize_ts });
    661    test_deps.dependOn(add_ts_parser(b, "query", parser_query.path("."), false, target, optimize_ts, .test_));
    662    install.dependOn(add_ts_parser(b, "query", parser_query.path("."), false, target, optimize_ts, .install));
    663 
    664    var unit_headers: ?[]const LazyPath = null;
    665    if (support_unittests) {
    666        try unittest_include_path.append(b.allocator, gen_headers.getDirectory());
    667        unit_headers = unittest_include_path.items;
    668    }
    669    try tests.test_steps(
    670        b,
    671        nvim_exe,
    672        test_deps,
    673        lua_dev_deps.path("."),
    674        test_config_step.getDirectory(),
    675        unit_headers,
    676    );
    677 }
    678 
    679 pub fn test_fixture(
    680    b: *std.Build,
    681    name: []const u8,
    682    use_libuv: bool,
    683    use_system_libuv: bool,
    684    libuv: ?*std.Build.Step.Compile,
    685    target: std.Build.ResolvedTarget,
    686    optimize: std.builtin.OptimizeMode,
    687    flags: []const []const u8,
    688 ) *std.Build.Step {
    689    const fixture = b.addExecutable(.{
    690        .name = name,
    691        .root_module = b.createModule(.{
    692            .target = target,
    693            .optimize = optimize,
    694        }),
    695    });
    696    const source = if (std.mem.eql(u8, name, "pwsh-test")) "shell-test" else name;
    697    if (std.mem.eql(u8, name, "printenv-test")) {
    698        fixture.mingw_unicode_entry_point = true; // uses UNICODE on WINDOWS :scream:
    699    }
    700 
    701    fixture.addCSourceFile(.{
    702        .file = b.path(b.fmt("./test/functional/fixtures/{s}.c", .{source})),
    703        .flags = flags,
    704    });
    705    fixture.linkLibC();
    706    if (use_libuv) {
    707        if (use_system_libuv) {
    708            fixture.root_module.linkSystemLibrary("libuv", .{});
    709        } else if (libuv) |uv| {
    710            fixture.linkLibrary(uv);
    711        }
    712    }
    713    return &b.addInstallArtifact(fixture, .{}).step;
    714 }
    715 
    716 pub fn add_ts_parser(
    717    b: *std.Build,
    718    name: []const u8,
    719    parser_dir: LazyPath,
    720    scanner: bool,
    721    target: std.Build.ResolvedTarget,
    722    optimize: std.builtin.OptimizeMode,
    723    path: enum { test_, install },
    724 ) *std.Build.Step {
    725    const parser = b.addLibrary(.{
    726        .name = name,
    727        .root_module = b.createModule(.{
    728            .target = target,
    729            .optimize = optimize,
    730        }),
    731        .linkage = .dynamic,
    732    });
    733    parser.addCSourceFile(.{ .file = parser_dir.path(b, "src/parser.c") });
    734    if (scanner) parser.addCSourceFile(.{ .file = parser_dir.path(b, "src/scanner.c") });
    735    parser.addIncludePath(parser_dir.path(b, "src"));
    736    parser.linkLibC();
    737 
    738    switch (path) {
    739        .install => {
    740            const parser_install = b.addInstallArtifact(parser, .{
    741                .dest_dir = .{ .override = .{ .custom = "share/nvim/runtime/parser" } },
    742                .dest_sub_path = b.fmt("{s}.so", .{name}),
    743            });
    744            return &parser_install.step;
    745        },
    746        .test_ => {
    747            const parser_install = b.addInstallArtifact(parser, .{
    748                .dest_sub_path = b.fmt("parser/{s}.so", .{name}),
    749            });
    750            return &parser_install.step;
    751        },
    752    }
    753 }
    754 
    755 pub fn lua_version_info(b: *std.Build) []u8 {
    756    const v = version;
    757    return b.fmt(
    758        \\return {{
    759        \\  {{"major", {}}},
    760        \\  {{"minor", {}}},
    761        \\  {{"patch", {}}},
    762        \\  {{"prerelease", {}}},
    763        \\  {{"api_level", {}}},
    764        \\  {{"api_compatible", {}}},
    765        \\  {{"api_prerelease", {}}},
    766        \\}}
    767    , .{
    768        v.major,
    769        v.minor,
    770        v.patch,
    771        v.prerelease.len > 0,
    772        v.api_level,
    773        v.api_level_compat,
    774        v.api_prerelease,
    775    });
    776 }
    777 
    778 /// Replace all backslashes in `input` with with forward slashes when the target is Windows.
    779 /// Returned memory is stored in `b.graph.arena`.
    780 fn replace_backslashes(b: *std.Build, input: []const u8) ![]const u8 {
    781    return if (b.graph.host.result.os.tag == .windows)
    782        std.mem.replaceOwned(u8, b.graph.arena, input, "\\", "/")
    783    else
    784        input;
    785 }
    786 
    787 pub fn test_config(b: *std.Build) ![]u8 {
    788    var buf: [std.fs.max_path_bytes]u8 = undefined;
    789    const src_path = try b.build_root.handle.realpath(".", &buf);
    790 
    791    // we don't use test/cmakeconfig/paths.lua.in because it contains cmake specific logic
    792    return b.fmt(
    793        \\local M = {{}}
    794        \\
    795        \\M.apple_sysroot = ""
    796        \\M.translations_enabled = "$ENABLE_TRANSLATIONS" == "ON"
    797        \\M.is_asan = "$ENABLE_ASAN_UBSAN" == "ON"
    798        \\M.is_zig_build = true
    799        \\M.vterm_test_file = "test/vterm_test_output"
    800        \\M.test_build_dir = "{[bin_dir]s}" -- bull
    801        \\M.test_source_path = "{[src_path]s}"
    802        \\M.test_lua_prg = ""
    803        \\M.test_luajit_prg = ""
    804        \\ -- include path passed on the cmdline, see test/lua_runner.lua
    805        \\M.include_paths = _G.c_include_path or {{}}
    806        \\
    807        \\return M
    808    , .{ .bin_dir = try replace_backslashes(b, b.install_path), .src_path = try replace_backslashes(b, src_path) });
    809 }
    810 
    811 fn appendSystemIncludePath(
    812    b: *std.Build,
    813    path: *std.ArrayList(LazyPath),
    814    system_name: []const u8,
    815 ) !void {
    816    var code: u8 = 0;
    817    const stdout = try b.runAllowFail(
    818        &[_][]const u8{ "pkg-config", system_name, "--cflags-only-I", "--keep-system-cflags" },
    819        &code,
    820        .Ignore,
    821    );
    822    if (code != 0) return std.Build.PkgConfigError.PkgConfigFailed;
    823    var arg_it = std.mem.tokenizeAny(u8, stdout, " \r\n\t");
    824    while (arg_it.next()) |arg| {
    825        if (std.mem.eql(u8, arg, "-I")) {
    826            // -I /foo/bar
    827            const dir = arg_it.next() orelse return std.Build.PkgConfigError.PkgConfigInvalidOutput;
    828            try path.append(b.allocator, .{ .cwd_relative = dir });
    829        } else if (std.mem.startsWith(u8, arg, "-I")) {
    830            // -I/foo/bar
    831            const dir = arg[("-I".len)..];
    832            try path.append(b.allocator, .{ .cwd_relative = dir });
    833        }
    834    }
    835 }