1//! To learn more about the Zig build system:
2//! https://ziglang.org/learn/build-system/
3//!
4//! See `architecture.md` for a codebase overview, and `zig-out/doc/README.html`
5//! for information on the project.
6
7const BuildConfig = @import("src/BuildConfig.zig");
8
9pub const Options = struct {
10 target: Build.ResolvedTarget,
11 optimize: std.builtin.OptimizeMode,
12
13 /// Experiment with the Zig self-hosted backends. Build option '-Dno_llvm'
14 no_llvm: bool,
15
16 /// BuildConfig contains most of the configurable aspects of the compiler,
17 /// and can be accessed in most modules with `@import("config")`
18 config: BuildConfig,
19};
20
21/// `createArtifactGroup` is called multiple times in order to have different
22/// targets and configurations of Ix at the same time.
23const ArtifactGroup = struct {
24 build: *Build,
25 opts: Options,
26
27 cli: *Step.Compile,
28 compiler_cli: *Step.Compile,
29 format_cli: *Step.Compile,
30 transform_cli: *Step.Compile,
31 loader_cli: *Step.Compile,
32
33 cli_mod: *Module,
34 core_mod: *Module,
35 loader_mod: *Module,
36 config_mod: *Module,
37
38 core_mod_tests: *Step.Compile,
39 loader_mod_tests: *Step.Compile,
40 compiler_tests: *Step.Compile,
41};
42
43fn createArtifactGroup(b: *Build, options: Options) ArtifactGroup {
44 const dep_opts = .{ .target = options.target, .optimize = options.optimize };
45 const use_llvm = if (options.no_llvm) false else null;
46 const use_lld = if (options.no_llvm) false else null;
47
48 // Dependencies
49 const zg = b.dependency("zg", dep_opts);
50 const tracer = b.dependency("tracer", dep_opts);
51 const quickjs = b.dependency("quickjs", dep_opts);
52
53 // Access build configuration via @import("config")
54 const config_mod = config_mod: {
55 const config = b.addOptions();
56 inline for (std.meta.fields(BuildConfig)) |field| {
57 config.addOption(field.type, field.name, @field(options.config, field.name));
58 }
59 config.addOption([]const u8, "version_string", b.fmt("{}", .{options.config.version}));
60 break :config_mod config.createModule();
61 };
62
63 // Core
64 const core_mod = b.createModule(.{
65 .root_source_file = b.path("src/core/mod.zig"),
66 .optimize = options.optimize,
67 .target = options.target,
68 .strip = false,
69 });
70 core_mod.addImport("config", config_mod);
71 core_mod.addImport("tracer", tracer.module("tracer"));
72 core_mod.addImport("zg:PropsData", zg.module("PropsData"));
73
74 // Format
75 const format_mod = b.createModule(.{
76 .root_source_file = b.path("src/format/Format.zig"),
77 .optimize = options.optimize,
78 .target = options.target,
79 .strip = false,
80 });
81 format_mod.addImport("core", core_mod);
82 format_mod.addImport("config", config_mod);
83 format_mod.addImport("tracer", tracer.module("tracer"));
84
85 // Loader
86 const loader_mod = b.createModule(.{
87 .root_source_file = b.path("src/loader/Loader.zig"),
88 .optimize = options.optimize,
89 .target = options.target,
90 .strip = false,
91 });
92 loader_mod.addImport("core", core_mod);
93 loader_mod.addImport("config", config_mod);
94
95 // Core Unit Tests
96 const core_mod_tests = createModuleTests(core_mod, options);
97 const loader_mod_tests = createModuleTests(loader_mod, options);
98
99 // Cli helper module
100 const cli_mod = b.createModule(.{
101 .root_source_file = b.path("src/Cli.zig"),
102 .optimize = options.optimize,
103 .target = options.target,
104 .strip = false,
105 });
106
107 // Compiler test CLI
108 const compiler_tests = b.addExecutable(.{
109 .name = "compiler_tests",
110 .root_source_file = b.path("test/compiler_tests.zig"),
111 .target = options.target,
112 .optimize = options.optimize,
113 .use_lld = use_lld,
114 .use_llvm = use_llvm,
115 });
116 compiler_tests.root_module.addImport("core", core_mod);
117 compiler_tests.root_module.addImport("config", config_mod);
118 compiler_tests.root_module.addImport("tracer", tracer.module("tracer"));
119 compiler_tests.root_module.addImport("quickjs", quickjs.module("quickjs"));
120 compiler_tests.root_module.addImport("Cli", cli_mod);
121
122 // Main CLI
123 const cli = b.addExecutable(.{
124 .name = "ix",
125 .root_source_file = b.path("src/main.zig"),
126 .target = options.target,
127 .optimize = options.optimize,
128 .use_lld = use_lld,
129 .use_llvm = use_llvm,
130 .single_threaded = true,
131 });
132 cli.root_module.addImport("core", core_mod);
133 cli.root_module.addImport("config", config_mod);
134 cli.root_module.addImport("tracer", tracer.module("tracer"));
135 if (options.config.have_embedded_quickjs) {
136 cli.root_module.addImport("quickjs", quickjs.module("quickjs-libc"));
137 }
138 cli.root_module.addImport("Cli", cli_mod);
139
140 // Compiler Dev CLI
141 const compiler_cli = b.addExecutable(.{
142 .name = "compiler",
143 .root_source_file = b.path("src/cli_compiler.zig"),
144 .target = options.target,
145 .optimize = options.optimize,
146 .use_lld = use_lld,
147 .use_llvm = use_llvm,
148 .single_threaded = true,
149 });
150 compiler_cli.root_module.addImport("core", core_mod);
151 compiler_cli.root_module.addImport("config", config_mod);
152 compiler_cli.root_module.addImport("tracer", tracer.module("tracer"));
153 if (options.config.have_embedded_quickjs) {
154 cli.root_module.addImport("quickjs", quickjs.module("quickjs-libc"));
155 }
156 compiler_cli.root_module.addImport("Cli", cli_mod);
157
158 const format_cli = b.addExecutable(.{
159 .name = "format",
160 .root_source_file = b.path("src/format/format_cli.zig"),
161 .target = options.target,
162 .optimize = options.optimize,
163 .use_lld = use_lld,
164 .use_llvm = use_llvm,
165 .single_threaded = true,
166 });
167 format_cli.root_module.addImport("core", core_mod);
168 format_cli.root_module.addImport("format", format_mod);
169 format_cli.root_module.addImport("config", config_mod);
170 format_cli.root_module.addImport("tracer", tracer.module("tracer"));
171 format_cli.root_module.addImport("Cli", cli_mod);
172
173 const loader_cli = b.addExecutable(.{
174 .name = "loader",
175 .root_source_file = b.path("src/loader/main.zig"),
176 .target = options.target,
177 .optimize = options.optimize,
178 .use_lld = use_lld,
179 .use_llvm = use_llvm,
180 .single_threaded = true,
181 });
182 loader_cli.root_module.addImport("loader", loader_mod);
183 loader_cli.root_module.addImport("config", config_mod);
184 loader_cli.root_module.addImport("tracer", tracer.module("tracer"));
185 loader_cli.root_module.addImport("Cli", cli_mod);
186
187 const transform_cli = b.addExecutable(.{
188 .name = "transform",
189 .root_source_file = b.path("src/cli_transform.zig"),
190 .target = options.target,
191 .optimize = options.optimize,
192 .use_lld = use_lld,
193 .use_llvm = use_llvm,
194 .single_threaded = true,
195 });
196 transform_cli.root_module.addImport("core", core_mod);
197 transform_cli.root_module.addImport("config", config_mod);
198 transform_cli.root_module.addImport("tracer", tracer.module("tracer"));
199 transform_cli.root_module.addImport("Cli", cli_mod);
200
201 return .{
202 .build = b,
203 .opts = options,
204
205 .cli = cli,
206 .cli_mod = cli_mod,
207 .compiler_cli = compiler_cli,
208 .format_cli = format_cli,
209 .loader_cli = loader_cli,
210 .transform_cli = transform_cli,
211
212 .core_mod = core_mod,
213 .loader_mod = loader_mod,
214 .config_mod = config_mod,
215
216 .core_mod_tests = core_mod_tests,
217 .loader_mod_tests = loader_mod_tests,
218 .compiler_tests = compiler_tests,
219 };
220}
221
222fn createModuleTests(module: *Build.Module, options: Options) *Build.Step.Compile {
223 const tests = module.owner.addTest(.{
224 .root_source_file = module.root_source_file.?,
225 .target = options.target,
226 .optimize = options.optimize,
227 .use_lld = if (options.no_llvm) false else null,
228 .use_llvm = if (options.no_llvm) false else null,
229 .strip = false,
230 });
231 var it = module.import_table.iterator();
232 while (it.next()) |entry| {
233 tests.root_module.addImport(entry.key_ptr.*, entry.value_ptr.*);
234 }
235 return tests;
236}
237
238pub fn build(b: *Build) !void {
239 var target = b.standardTargetOptions(.{});
240 var host = b.graph.host;
241
242 // I disagree with how the standard optimize flag works
243 const optimize_fallback = switch (b.release_mode) {
244 .off => b.standardOptimizeOption(.{}),
245 else => b.option(
246 std.builtin.OptimizeMode,
247 "optimize",
248 "Prioritize performance, safety, or binary size",
249 ),
250 };
251 const optimize: std.builtin.OptimizeMode = switch (b.release_mode) {
252 .off => optimize_fallback.?,
253 .safe => .ReleaseSafe,
254 .fast => .ReleaseFast,
255 .small => .ReleaseSmall,
256 .any => brk: {
257 b.release_mode = .safe;
258 break :brk .ReleaseSafe;
259 },
260 };
261
262 const tracing = b.option(
263 BuildConfig.TracingMode,
264 "trace",
265 "Enable tracing",
266 ) orelse .none;
267
268 const no_llvm = b.option(
269 bool,
270 "no_llvm",
271 "Experiment with the Zig self-hosted backends",
272 ) orelse false;
273
274 if (no_llvm) {
275 if (b.user_input_options.getPtr("target") == null)
276 target = mapTargetForSelfHosted(target);
277 host = mapTargetForSelfHosted(host);
278 }
279
280 const have_embedded_quickjs = b.option(
281 bool,
282 "quickjs",
283 "Build with QuickJS features",
284 ) orelse true;
285
286 const js_runtime: PathOrString = if (b.option(
287 []const u8,
288 "js_runtime",
289 "JavaScript runtime to use during behavior tests and manual generation",
290 )) |path|
291 .{ .string = path }
292 else
293 // Default to building QuickJS
294 .{ .path = b.dependency("quickjs", .{
295 .target = b.graph.host,
296 .optimize = .ReleaseSafe,
297 }).artifact("qjs").getEmittedBin() };
298
299 // This code is taken from Zig
300 const version_override = b.option(
301 []const u8,
302 "version",
303 "Override version string. Default is to find out with CHANGELOG.md and Git.",
304 );
305 const version = if (version_override) |version|
306 std.SemanticVersion.parse(version) catch {
307 std.debug.panic("Invalid -Dversion value \"{s}\"", .{version});
308 }
309 else version: {
310 const text = try b.build_root.handle.readFileAlloc(
311 b.graph.arena,
312 "CHANGELOG.md",
313 std.math.maxInt(usize),
314 );
315 const first_header = std.mem.indexOf(u8, text, "## ") orelse @panic("Invalid changelog");
316 var tokenizer = std.mem.tokenizeAny(u8, text[first_header + 2 ..], " \t\r\n");
317 const version_text = tokenizer.next() orelse @panic("Invalid changelog");
318 var version = std.SemanticVersion.parse(version_text) catch
319 std.debug.panic("Invalid version: {s}", .{version_text});
320
321 if (!std.process.can_spawn) {
322 std.debug.print("error: version info cannot be retrieved from git. Version must be provided using -Dversion\n", .{});
323 std.process.exit(1);
324 }
325
326 var code: u8 = undefined;
327 const git_describe_untrimmed = b.runAllowFail(&[_][]const u8{
328 "git",
329 "-C",
330 b.build_root.path orelse ".",
331 "describe",
332 "--match",
333 "*.*.*",
334 "--tags",
335 "--abbrev=9",
336 }, &code, .Ignore) catch {
337 break :version version;
338 };
339 const git_describe = std.mem.trim(u8, git_describe_untrimmed, " \n\r");
340
341 switch (std.mem.count(u8, git_describe, "-")) {
342 0 => {
343 // Tagged release version (e.g. 0.10.0).
344 if (!std.mem.eql(u8, git_describe, b.fmt("{}", .{version}))) {
345 std.debug.print("Version '{}' does not match Git tag '{s}'\n", .{ version, git_describe });
346 std.process.exit(1);
347 }
348 break :version version;
349 },
350 2 => {
351 // Untagged development build (e.g. 0.10.0-dev.2025+ecf0050a9).
352 var it = std.mem.splitScalar(u8, git_describe, '-');
353 const tagged_ancestor = it.first();
354 const commit_height = it.next().?;
355 const commit_id = it.next().?;
356
357 const ancestor_ver = try std.SemanticVersion.parse(tagged_ancestor);
358 if (version.order(ancestor_ver) != .gt) {
359 std.debug.print("Version '{}' must be greater than tagged ancestor '{}'\n", .{ version, ancestor_ver });
360 std.process.exit(1);
361 }
362
363 // Check that the commit hash is prefixed with a 'g' (a Git convention).
364 if (commit_id.len < 1 or commit_id[0] != 'g') {
365 std.debug.print("Unexpected `git describe` output: {s}\n", .{git_describe});
366 break :version version;
367 }
368
369 // The version is reformatted in accordance with the https://semver.org specification.
370 version.pre = b.fmt("dev.{s}", .{commit_height});
371 version.build = commit_id[1..];
372
373 break :version version;
374 },
375 else => {
376 std.debug.print("Unexpected `git describe` output: {s}\n", .{git_describe});
377 break :version version;
378 },
379 }
380 };
381
382 // This artifact group is configured for the host system,
383 // for use in code generators + tests.
384 const native_debug = createArtifactGroup(b, .{
385 .config = .{ .version = version },
386 .optimize = .Debug,
387 .target = b.graph.host,
388 .no_llvm = no_llvm,
389 });
390
391 // This artifact group is used for the separated run steps,
392 // each with a minimal configuration applied.
393 const minimal = createArtifactGroup(b, .{
394 .config = .{ .version = version },
395 .optimize = optimize,
396 .target = b.graph.host,
397 .no_llvm = no_llvm,
398 });
399
400 // This artifact group is configured for the build target.
401 const main = createArtifactGroup(b, .{
402 .config = .{
403 .version = version,
404 .tracing_mode = tracing,
405 .have_embedded_quickjs = have_embedded_quickjs,
406 },
407 .target = target,
408 .optimize = optimize,
409 .no_llvm = no_llvm,
410 });
411
412 b.installArtifact(main.cli);
413
414 const run_templates = .{
415 .{ main.cli, "run" },
416 .{ minimal.compiler_cli, "compiler" },
417 .{ minimal.format_cli, "format" },
418 .{ minimal.loader_cli, "loader" },
419 .{ minimal.transform_cli, "transform" },
420 };
421 inline for (run_templates) |entry| {
422 const artifact, const name = entry;
423
424 const run_cmd = b.addRunArtifact(artifact);
425 if (b.args) |args| {
426 run_cmd.addArgs(args);
427 }
428 const run_step = b.step(name, "Run " ++ name ++ " binary");
429 run_step.dependOn(&run_cmd.step);
430 run_step.dependOn(&b.addInstallArtifact(artifact, .{}).step);
431 }
432
433 const test_all = b.step("test", "Run all tests");
434 inline for (.{
435 .{ main.core_mod_tests, "core", "core module" },
436 .{ main.loader_mod_tests, "loader", "loader module" },
437 .{ main.compiler_tests, "compiler", "compiler behavior" },
438 }) |entry| {
439 const artifact, const name, const description = entry;
440 const run_cmd = b.addRunArtifact(artifact);
441 if (artifact == main.compiler_tests) {
442 if (b.args) |args| {
443 run_cmd.addArgs(args);
444 } else {
445 run_cmd.enableTestRunnerMode();
446 }
447 }
448 const run_step = b.step("test-" ++ name, "Test " ++ description);
449 run_step.dependOn(&run_cmd.step);
450 run_step.dependOn(&b.addInstallArtifact(artifact, .{}).step);
451 test_all.dependOn(run_step);
452 }
453
454 // Language Manual Generator
455 const manual_generator = b.addExecutable(.{
456 .name = "generate_manual",
457 .root_source_file = b.path("src/manual/generate_manual.zig"),
458 .target = b.graph.host,
459 .optimize = .Debug,
460 .link_libc = true,
461 });
462 manual_generator.root_module.addImport("config", native_debug.config_mod);
463 manual_generator.root_module.addImport("core", native_debug.core_mod);
464
465 const manual_run = b.addRunArtifact(manual_generator);
466 manual_run.addFileArg(native_debug.cli.getEmittedBin());
467 switch (js_runtime) {
468 .path => |path| manual_run.addFileArg(path),
469 .string => |str| manual_run.addArg(str),
470 }
471 manual_run.addFileArg(b.path("doc/language-manual.md"));
472 manual_run.addFileArg(b.path("src/manual/template.html"));
473 manual_run.addFileArg(b.path("src/manual/style.css"));
474 const manual_out = manual_run.addOutputFileArg("language-reference.html");
475 const install_manual = b.addInstallFile(manual_out, "doc/README.html");
476
477 b.step("manual", "build language reference").dependOn(&install_manual.step);
478
479 {
480 const wasm_target = b.resolveTargetQuery(.{
481 .cpu_arch = .wasm32,
482 .os_tag = .freestanding,
483 });
484 const wasm_artifacts = createArtifactGroup(b, .{
485 .config = .{
486 .version = version,
487 .have_embedded_quickjs = false,
488 },
489 .target = wasm_target,
490 .optimize = optimize,
491 .no_llvm = no_llvm,
492 });
493 const wasm_api = b.addExecutable(.{
494 .name = "sdk",
495 .root_source_file = b.path("src/wasm/wasm_api.zig"),
496 .target = wasm_target,
497 .optimize = optimize,
498 });
499 wasm_api.root_module.addImport("config", wasm_artifacts.config_mod);
500 wasm_api.root_module.addImport("core", wasm_artifacts.core_mod);
501 wasm_api.root_module.export_symbol_names = &.{
502 "iterateTokens",
503 };
504 wasm_api.entry = .disabled;
505 const wasm_step = b.step("wasm", "[WIP] Build wasm api");
506 b.installArtifact(wasm_api);
507 wasm_step.dependOn(&wasm_api.step);
508
509 const wasm_codegen = b.step("wasm-codegen", "[WIP] Build wasm codegen");
510 const wasm_codegen_bin = b.addExecutable(.{
511 .name = "sdk",
512 .root_source_file = b.path("src/wasm/enum_codegen.zig"),
513 .target = b.graph.host,
514 .optimize = .Debug,
515 });
516 wasm_codegen_bin.root_module.addImport("core", native_debug.core_mod);
517 const run_wasm_codegen = b.addRunArtifact(wasm_codegen_bin);
518 wasm_codegen.dependOn(&run_wasm_codegen.step);
519 }
520
521 // Packaging
522 const package = b.step("package", "Package a release build");
523
524 for ([_]Build.ResolvedTarget{
525 crossTarget(.x86_64, .linux),
526 crossTarget(.aarch64, .linux),
527 crossTarget(.x86_64, .macos),
528 crossTarget(.aarch64, .macos),
529 crossTarget(.x86_64, .windows),
530 crossTarget(.aarch64, .windows),
531 }) |cross_target| {
532 const name = b.fmt("{s}-{s}", .{
533 @tagName(cross_target.result.cpu.arch),
534 @tagName(cross_target.result.os.tag),
535 });
536
537 const artifacts = createArtifactGroup(b, .{
538 .config = .{
539 .version = version,
540 .have_embedded_quickjs = have_embedded_quickjs,
541 },
542 .optimize = .ReleaseSafe,
543 .target = cross_target,
544 .no_llvm = no_llvm,
545 });
546
547 package.dependOn(&b.addInstallFile(
548 artifacts.cli.getEmittedBin(),
549 b.pathJoin(&.{
550 "package",
551 name,
552 b.fmt("ix{s}", .{cross_target.result.exeFileExt()}),
553 }),
554 ).step);
555 package.dependOn(&b.addInstallFile(b.path("CHANGELOG.md"), "package/CHANGELOG.md").step);
556 package.dependOn(&b.addInstallFile(manual_out, "package/ix.html").step);
557 }
558
559 {
560 _ = b.top_level_steps.swapRemove("uninstall"); // this step does not work
561
562 const all_step = b.step("all", "Build everything (used by CI)");
563 all_step.dependOn(&b.top_level_steps.get("install").?.step);
564 all_step.dependOn(&b.top_level_steps.get("package").?.step);
565 all_step.dependOn(&b.top_level_steps.get("test").?.step);
566 all_step.dependOn(&b.top_level_steps.get("manual").?.step);
567 inline for (run_templates) |run| {
568 all_step.dependOn(&run.@"0".step);
569 }
570 }
571}
572
573fn crossTarget(cpu_arch: std.Target.Cpu.Arch, os_tag: std.Target.Os.Tag) Build.ResolvedTarget {
574 const query: std.Target.Query = .{
575 .cpu_arch = cpu_arch,
576 .os_tag = os_tag,
577
578 .cpu_model = switch (cpu_arch) {
579 .aarch64 => switch (os_tag) {
580 .macos => .{ .explicit = &std.Target.aarch64.cpu.apple_m1 },
581 else => .baseline,
582 },
583 else => .baseline,
584 },
585
586 .os_version_min = switch (os_tag) {
587 .linux => .{ .semver = .{ .major = 4, .minor = 19, .patch = 0 } },
588 .macos => .{ .semver = .{ .major = 11, .minor = 0, .patch = 0 } },
589 .windows => .{ .windows = .win7 },
590 else => @panic("unhandled"),
591 },
592
593 .abi = switch (os_tag) {
594 .linux => .musl,
595 .macos => .none,
596 .windows => .gnu, // cannot cross compile .msvc
597 else => @panic("unhandled"),
598 },
599 };
600 return .{
601 .query = query,
602 .result = std.zig.system.resolveTargetQuery(query) catch
603 @panic("unable to resolve target query"),
604 };
605}
606
607fn mapTargetForSelfHosted(target: Build.ResolvedTarget) Build.ResolvedTarget {
608 if (target.result.os.tag == .macos and target.result.cpu.arch == .aarch64) {
609 var mut = target;
610 mut.result.cpu = std.Target.Cpu.baseline(.x86_64, target.result.os);
611 }
612 return target;
613}
614
615pub const PathOrString = union(enum) {
616 path: LazyPath,
617 string: []const u8,
618};
619
620const std = @import("std");
621const Build = std.Build;
622const Step = Build.Step;
623const Module = Build.Module;
624const LazyPath = Build.LazyPath;
625const Target = std.Target;
626const OsVersion = Target.Query.OsVersion;