summaryrefslogtreecommitdiff
path: root/build.zig
blob: d5bba19aeb1e79f73e636677caa0dbb93440920c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
const std = @import("std");

pub fn build(b: *std.Build) void {
    const optimize = b.standardOptimizeOption(.{});
    const target = b.standardTargetOptions(.{});

    // build-time IO, only used to slurp the grammars' highlights.scm queries.
    var threaded: std.Io.Threaded = .init(b.allocator, .{});
    defer threaded.deinit();
    const io = threaded.io();

    // -Dtrace: make the e2e runner print the captured screen state (after each
    //   pump/waitFor and on every assertion) so failures can be inspected live.
    //   Usage: `zig build test -Dtrace`.
    const trace = b.option(bool, "trace", "print captured tty/screen state during e2e tests") orelse false;

    const exe = b.addExecutable(.{
        .name = "pardes",
        .root_module = b.createModule(.{
            .target = target,
            .optimize = optimize,
            .root_source_file = b.path("main.zig"),
        }),
    });
    const tests = b.addExecutable(.{
        .name = "pardes-e2e",
        .root_module = b.createModule(.{
            .target = target,
            .optimize = optimize,
            .root_source_file = b.path("tests.zig"),
            .link_libc = true,
        }),
    });

    // ghostty-vt and vaxis both depend on uucode, but Zig 0.16 forbids the
    // same source file from belonging to two modules, so they can't each have
    // their own uucode instance. We build a single uucode module here (with our
    // own uucode_config.zig) and hand it to both.
    //
    // Our config matches ghostty's exactly except the tables are `.unpacked`;
    // see uucode_config.zig for why that's required on Zig 0.16.
    const uucode_config = b.path("uucode_config.zig");
    const uucode_tables = b.dependency("uucode", .{
        .target = target,
        .optimize = optimize,
        .build_config_path = uucode_config,
    }).namedLazyPath("tables.zig");
    const uucode_mod = b.dependency("uucode", .{
        .target = target,
        .optimize = optimize,
        .build_config_path = uucode_config,
        .tables_path = uucode_tables,
    }).module("uucode");

    // vaxis is built with `external_uucode = true` so it skips its built-in
    // uucode dependency and lets us wire one in.
    const vaxis_dep = b.dependency("vaxis", .{
        .target = target,
        .optimize = optimize,
        .external_uucode = true,
    });
    const vaxis_mod = vaxis_dep.module("vaxis");

    // You'll want to use a lazy dependency here so that ghostty is only
    // downloaded if you actually need it.
    if (b.lazyDependency("ghostty", .{
        // Setting simd to false will force a pure static build that
        // doesn't even require libc, but it has a significant performance
        // penalty. If your embedding app requires libc anyway, you should
        // always keep simd enabled.
        // .simd = false,
    })) |dep| {
        // Accessing ghostty's modules runs its build(), which (because vaxis is
        // a shared dependency, deduplicated to the same module) wires ghostty's
        // own uucode into vaxis. We undo that below by overriding both imports
        // with ours — order matters, so this must happen after this point.
        const ghostty_vt = dep.module("ghostty-vt");
        ghostty_vt.addImport("uucode", uucode_mod);
        exe.root_module.addImport("ghostty-vt", ghostty_vt);
        // The e2e test harness parses the app's pty output with its own ghostty
        // terminal (reads the 2D screen grid, not raw bytes), so it needs the
        // same module.
        tests.root_module.addImport("ghostty-vt", ghostty_vt);
    }

    // Override uucode on vaxis last, so it wins over ghostty's build() above.
    vaxis_mod.addImport("uucode", uucode_mod);
    exe.root_module.addImport("vaxis", vaxis_mod);

    // zstbi: C (stb_image)-backed image decoder for the image-viewer panes. We use
    // it instead of zigimg because zigimg's pure-Zig decoder crashes the Zig 0.16
    // compiler here; stb_image is plain C (extern fns), so it compiles fine, decodes
    // every common format, and offers downscaling. Its module ("root") carries the
    // C source + include path + libc, so importing it is all that's needed.
    const zstbi_mod = b.dependency("zstbi", .{ .target = target, .optimize = optimize }).module("root");
    exe.root_module.addImport("zstbi", zstbi_mod);

    // tree-sitter syntax highlighting for file panes: the Zig bindings + C runtime
    // (the `tree-sitter` module), each grammar's parser.c linked as a static lib,
    // and its highlights.scm read at build time into the `ts_queries` options module.
    const tree_sitter = b.dependency("tree_sitter", .{ .target = target, .optimize = optimize });
    exe.root_module.addImport("tree-sitter", tree_sitter.module("tree_sitter"));

    const grammars = .{
        .{ "c", "ts_c", false },
        .{ "rust", "ts_rust", true },
        .{ "zig", "ts_zig", false },
    };
    const ts_queries = b.addOptions();
    inline for (grammars) |g| {
        const dep = b.dependency(g[1], .{});
        const query = std.Io.Dir.cwd().readFileAlloc(io, dep.path("queries/highlights.scm").getPath(b), b.allocator, .limited(0x100000)) catch @panic("read " ++ g[0] ++ " highlights");
        ts_queries.addOption([]const u8, g[0] ++ "_highlights", query);

        const lib = b.addLibrary(.{
            .name = "tree-sitter-" ++ g[0],
            .root_module = b.createModule(.{ .target = target, .optimize = optimize, .link_libc = true }),
            .linkage = .static,
        });
        lib.root_module.addCSourceFile(.{ .file = dep.path("src/parser.c") });
        if (g[2]) lib.root_module.addCSourceFile(.{ .file = dep.path("src/scanner.c") });
        lib.root_module.addIncludePath(dep.path("src"));
        exe.root_module.linkLibrary(lib);
    }
    exe.root_module.addOptions("ts_queries", ts_queries);

    // Install the exe so `zig build` (and `zig build install`) produces it in
    // zig-out/bin for global install, not just as a hidden dep of `run`/`test`.
    b.installArtifact(exe);

    const run_exe = b.addRunArtifact(exe);

    const run = b.step("run", "descend to the pardes");
    run.dependOn(&run_exe.step);

    // tutor_gen: a separate binary that parses tutor.txt and writes
    // tutor_cases.zig (a file of `test` blocks, one per practice block).
    // `zig build tutor-gen` runs it; the unit-test step depends on the output.
    const tutor_gen = b.addExecutable(.{
        .name = "tutor-gen",
        .root_module = b.createModule(.{
            .target = target,
            .optimize = optimize,
            .root_source_file = b.path("tutor_gen.zig"),
        }),
    });
    const run_tutor_gen = b.addRunArtifact(tutor_gen);
    run_tutor_gen.addArg("tutor.txt");
    const tutor_cases_lazy = run_tutor_gen.addOutputFileArg("tutor_cases.zig");
    const tutor_gen_step = b.step("tutor-gen", "regenerate tutor_cases.zig from tutor.txt");
    tutor_gen_step.dependOn(&run_tutor_gen.step);

    const run_tests = b.addRunArtifact(tests);
    run_tests.addArtifactArg(exe);

    // Expose build-time flags (e.g. -Dtrace) to the test runner as an importable
    // `config` module. tests.zig reads `config.trace` and hands it to the harness.
    const test_opts = b.addOptions();
    test_opts.addOption(bool, "trace", trace);
    tests.root_module.addOptions("config", test_opts);

    const test_step = b.step("test", "run deterministic tests");
    test_step.dependOn(&run_tests.step);

    // Unit tests for the pure modal-editing math (modal.zig has no vaxis/ghostty
    // deps, so `zig test` on it is fast and needs no wired-in modules).
    const unit_tests = b.addTest(.{
        .root_module = b.createModule(.{
            .target = target,
            .optimize = optimize,
            .root_source_file = b.path("modal.zig"),
        }),
    });
    const run_unit = b.addRunArtifact(unit_tests);
    const unit_step = b.step("unit-test", "run modal.zig unit tests");
    unit_step.dependOn(&run_unit.step);
    test_step.dependOn(&run_unit.step);

    // Tutor practice blocks as unit tests: a second `zig test` target over the
    // generated tutor_cases.zig (which imports modal.zig). Depends on tutor-gen
    // so the file is (re)generated first.
    const tutor_tests = b.addTest(.{
        .root_module = b.createModule(.{
            .target = target,
            .optimize = optimize,
            .root_source_file = tutor_cases_lazy,
        }),
    });
    // The generated cases `@import("modal")` (a module name, not a relative
    // path, since the generated file lives in the cache dir, not next to modal.zig).
    const modal_for_tutor = b.createModule(.{
        .target = target,
        .optimize = optimize,
        .root_source_file = b.path("modal.zig"),
    });
    tutor_tests.root_module.addImport("modal", modal_for_tutor);
    const run_tutor = b.addRunArtifact(tutor_tests);
    const tutor_step = b.step("tutor-test", "run the tutor practice-block tests");
    tutor_step.dependOn(&run_tutor.step);
    test_step.dependOn(&run_tutor.step);
}