1 /* MIT License
2 * Copyright (c) 2025 Matheus C. França
3 * See LICENSE file for details
4 */
5 
6 /// Wraps compiler commands.
7 module builder;
8 
9 import std.stdio;
10 import std.process;
11 import std.array;
12 import std.string;
13 import std.algorithm : canFind, filter, any, startsWith, endsWith;
14 import std.typecons : Nullable;
15 import std.path : extension;
16 import std.exception : enforce;
17 
18 /// Stores build configuration options.
19 struct BuildOptions
20 {
21     /// Target triple (e.g., x86_64-linux-gnu).
22     static Nullable!string triple;
23     /// CPU features (e.g., generic).
24     static Nullable!string cpu;
25 }
26 
27 /// Provides flag filtering and transformation utilities.
28 mixin template FlagChecks()
29 {
30     /// Transforms or skips flags for Zig compatibility.
31     static string[] processFlag(string arg) @safe pure nothrow
32     {
33         static immutable string[] skipExact = [
34             "--exclude-libs", "ALL", "--no-as-needed", "/nologo", "/NOLOGO"
35         ];
36         if (skipExact.canFind(arg))
37             return [];
38         if (arg.endsWith("-group"))
39             return ["-Wl,--start-group", "-Wl,--end-group"];
40         if (arg.endsWith("-dynamic"))
41             return ["-Wl,--export-dynamic"];
42         return [arg];
43     }
44 
45     /// Checks if a flag is Clang-specific for -cflags.
46     static bool isClangFlag(string arg) @safe pure nothrow
47     {
48         return !arg.startsWith("-Wl,") && arg != "-Wl,--start-group" &&
49             arg != "-Wl,--end-group" && arg != "-Wl,--export-dynamic";
50     }
51 }
52 
53 /// Builds and executes Zig subcommands.
54 class Builder
55 {
56     /// Minimum command length for sanitizer flags (zig, cc/c++, +1 arg).
57     private enum MIN_SANITIZE_COMMAND_LENGTH = 3;
58     /// Base command length (zig, cc/c++).
59     private enum BASE_COMMAND_LENGTH = 2;
60     /// Length of "arm64-apple" prefix.
61     private enum ARM64_APPLE_PREFIX_LENGTH = "arm64-apple".length;
62     /// Length of "x86_64-apple" prefix.
63     private enum X86_64_APPLE_PREFIX_LENGTH = "x86_64-apple".length;
64     /// Length of "-unknown-unknown" suffix.
65     private enum UNKNOWN_UNKNOWN_LENGTH = "-unknown-unknown".length;
66     /// Allowed DMD architectures.
67     private static immutable ALLOWED_DMD_ARCHES = ["x86_64", "i386", "i686"];
68     //dfmt off
69     /// Supported Zig triples.
70      private static immutable SUPPORTED_ZIG_TRIPLES = [
71         "arc-linux-gnu", "arm-freebsd-eabihf", "arm-linux-gnueabi", "arm-linux-gnueabihf",
72         "arm-linux-musleabi", "arm-linux-musleabihf", "armeb-linux-gnueabi",
73         "armeb-linux-gnueabihf", "armeb-linux-musleabi", "armeb-linux-musleabihf",
74         "thumb-freebsd-eabihf", "thumb-linux-musleabi", "thumb-linux-musleabihf",
75         "thumb-windows-gnu", "thumbeb-linux-musleabi", "thumbeb-linux-musleabihf",
76         "aarch64-freebsd-none", "aarch64-linux-gnu", "aarch64-linux-musl",
77         "aarch64-macos-none", "aarch64-windows-gnu", "aarch64_be-linux-gnu",
78         "aarch64_be-linux-musl", "csky-linux-gnueabi", "csky-linux-gnueabihf",
79         "hexagon-linux-musl", "loongarch64-linux-gnu", "loongarch64-linux-gnusf",
80         "loongarch64-linux-musl", "loongarch64-linux-muslsf", "m68k-linux-gnu",
81         "m68k-linux-musl", "mips-linux-gnueabi", "mips-linux-gnueabihf",
82         "mips-linux-musleabi", "mips-linux-musleabihf", "mipsel-linux-gnueabi",
83         "mipsel-linux-gnueabihf", "mipsel-linux-musleabi", "mipsel-linux-musleabihf",
84         "mips64-linux-gnuabi64", "mips64-linux-gnuabin32", "mips64-linux-muslabi64",
85         "mips64-linux-muslabin32", "mips64el-linux-gnuabi64", "mips64el-linux-gnuabin32",
86         "mips64el-linux-muslabi64", "mips64el-linux-muslabin32", "powerpc-freebsd-eabihf",
87         "powerpc-linux-gnueabi", "powerpc-linux-gnueabihf", "powerpc-linux-musleabi",
88         "powerpc-linux-musleabihf", "powerpc64-freebsd-none", "powerpc64-linux-gnu",
89         "powerpc64-linux-musl", "powerpc64le-freebsd-none", "powerpc64le-linux-gnu",
90         "powerpc64le-linux-musl", "riscv32-linux-gnu", "riscv32-linux-musl",
91         "riscv64-freebsd-none", "riscv64-linux-gnu", "riscv64-linux-musl",
92         "s390x-linux-gnu", "s390x-linux-musl", "sparc-linux-gnu", "sparc64-linux-gnu",
93         "wasm32-wasi-musl", "wasm32-wasi", "wasm32-emscripten", "x86-freebsd-none",
94         "x86-linux-gnu", "x86-linux-musl", "x86-windows-gnu", "x86_64-freebsd-none",
95         "x86_64-linux-gnu", "x86_64-linux-gnux32", "x86_64-linux-musl",
96         "x86_64-linux-muslx32", "x86_64-macos-none", "x86_64-windows-gnu"
97     ];
98     //dfmt on
99 
100     private Appender!(string[]) cmds;
101     private Appender!(string[]) sourceFiles;
102     private string targetTriple;
103     private string cpu;
104     private bool isCPlusPlus;
105     private string[] warnings;
106 
107     /// Creates a builder for Zig cc or c++.
108     /// Params:
109     ///   useCpp = Use C++ mode if true, C mode if false.
110     this(bool useCpp = false) @safe pure nothrow
111     {
112         cmds = appender!(string[]);
113         sourceFiles = appender!(string[]);
114         cmds.put("zig");
115         cmds.put(useCpp ? "c++" : "cc");
116         isCPlusPlus = useCpp;
117     }
118 
119     /// Adds a compiler flag, ignoring source files and target options.
120     /// Params:
121     ///   arg = Flag to add.
122     /// Returns: This builder for chaining.
123     Builder addArg(string arg) @safe pure
124     {
125         auto ext = extension(arg).toLower;
126         if (ext == ".c" || ext == ".o" || ext == ".obj" || ext == ".s"
127             || ext == ".cpp" || ext == ".cxx" || ext == ".cc" || ext == ".c++")
128             return this;
129         if (arg == "--target" || arg.startsWith("--target="))
130             return this;
131         mixin FlagChecks;
132         cmds.put(processFlag(arg));
133         return this;
134     }
135 
136     /// Adds multiple compiler flags.
137     /// Params:
138     ///   args = Flags to add.
139     /// Returns: This builder for chaining.
140     Builder addArgs(string[] args) @safe pure
141     {
142         foreach (arg; args)
143             addArg(arg);
144         return this;
145     }
146 
147     /// Adds a source file, enabling C++ mode for .cpp/.cxx/.cc/.c++ files.
148     /// Params:
149     ///   file = Source file path (.c, .cpp, .cxx, .cc, .c++, .o, .obj, .s).
150     /// Returns: This builder for chaining.
151     Builder file(string file) @safe pure
152     {
153         auto ext = extension(file).toLower;
154         if (ext == ".cpp" || ext == ".cxx" || ext == ".cc" || ext == ".c++")
155         {
156             if (!targetTriple.endsWith("msvc"))
157             {
158                 isCPlusPlus = true;
159                 cmds.data[1] = "c++";
160             }
161         }
162         else if (ext != ".c" && ext != ".o" && ext != ".obj" && ext != ".s")
163             return this;
164         sourceFiles.put(file);
165         return this;
166     }
167 
168     /// Adds multiple source files.
169     /// Params:
170     ///   files = Source file paths.
171     /// Returns: This builder for chaining.
172     Builder files(string[] files) @safe pure
173     {
174         foreach (f; files)
175             file(f);
176         return this;
177     }
178 
179     /// Sets the target triple, transforming RISC-V, ARM, WebAssembly, Apple, and GNU-style triples.
180     /// Params:
181     ///   triple = Target triple (e.g., x86_64-linux-gnu).
182     /// Returns: This builder for chaining.
183     Builder setTargetTriple(string triple) @safe pure
184     {
185         string transformedTriple = triple;
186 
187         // Rename RISC-V architectures (riscv64*, riscv32*) to riscv64-, riscv32-
188         if (triple.startsWith("riscv"))
189         {
190             auto hyphenIndex = triple.indexOf('-');
191             if (hyphenIndex > 0)
192             {
193                 auto prefix = triple[0 .. hyphenIndex];
194                 if (prefix.startsWith("riscv64"))
195                     transformedTriple = "riscv64" ~ triple[hyphenIndex .. $];
196                 else if (prefix.startsWith("riscv32"))
197                     transformedTriple = "riscv32" ~ triple[hyphenIndex .. $];
198             }
199             else
200                 warnings ~= "Warning: Malformed RISC-V triple " ~ triple;
201         }
202         // Rename ARM architectures (armv5*, armv6*, armv7*, armv8*) to arm-
203         else if (triple.startsWith("armv"))
204         {
205             auto hyphenIndex = triple.indexOf('-');
206             if (hyphenIndex > 0)
207             {
208                 auto prefix = triple[0 .. hyphenIndex];
209                 if (prefix.startsWith("armv5") || prefix.startsWith("armv6") ||
210                     prefix.startsWith("armv7") || prefix.startsWith("armv8"))
211                 {
212                     transformedTriple = "arm" ~ triple[hyphenIndex .. $];
213                 }
214             }
215             else
216                 warnings ~= "Warning: Malformed ARM triple " ~ triple;
217         }
218         // Handle WebAssembly triples
219         else if (triple.startsWith("wasm32-"))
220         {
221             auto parts = triple.split('-');
222             if (parts.length >= 3)
223             {
224                 if (parts[$ - 1] == "wasm" && parts.length >= 4)
225                 {
226                     string abi = parts[$ - 2];
227                     if (abi == "emscripten")
228                         transformedTriple = "wasm32-emscripten";
229                     else if (parts.length == 4 && parts[1] == "unknown" && parts[2] == "unknown")
230                         transformedTriple = "wasm32-freestanding";
231                     else
232                         transformedTriple = "wasm32-" ~ abi;
233                 }
234                 else if (parts[$ - 1] == "musl" && parts.length >= 3)
235                 {
236                     string abi = parts[$ - 2];
237                     transformedTriple = "wasm32-" ~ abi;
238                 }
239                 else
240                 {
241                     string abi = parts[$ - 1];
242                     transformedTriple = "wasm32-" ~ abi;
243                 }
244             }
245             else
246                 warnings ~= "Warning: Malformed WebAssembly triple " ~ triple;
247         }
248 
249         // Apply existing transformations
250         if (transformedTriple.startsWith("arm64-apple"))
251             targetTriple = "aarch64" ~ transformedTriple[ARM64_APPLE_PREFIX_LENGTH .. $];
252         else if (transformedTriple.startsWith("x86_64-apple"))
253             targetTriple = "x86_64" ~ transformedTriple[X86_64_APPLE_PREFIX_LENGTH .. $];
254         else if (transformedTriple.endsWith("-unknown-unknown"))
255             targetTriple = transformedTriple[0 .. $ - UNKNOWN_UNKNOWN_LENGTH] ~ "-freestanding";
256         else if (transformedTriple.canFind("-unknown-"))
257         {
258             auto parts = transformedTriple.split("-unknown-");
259             if (parts.length == 2)
260                 targetTriple = parts[0] ~ "-" ~ parts[1];
261             else
262                 targetTriple = transformedTriple;
263         }
264         else
265             targetTriple = transformedTriple;
266 
267         // Store warning if triple is not supported by Zig
268         if (!SUPPORTED_ZIG_TRIPLES.canFind(targetTriple))
269             warnings ~= "Warning: Target triple " ~ targetTriple ~ " is not in Zig's supported triple list";
270         return this;
271     }
272 
273     /// Sets CPU features.
274     /// Params:
275     ///   cpu = CPU feature string (e.g., generic).
276     /// Returns: This builder for chaining.
277     Builder setCpu(string cpu) @safe pure nothrow
278     {
279         this.cpu = cpu;
280         return this;
281     }
282 
283     /// Builds the Zig command with sanitizer flags if needed.
284     /// Returns: Command array for execution.
285     string[] build() @safe pure
286     {
287         auto result = cmds.data.dup ~ sourceFiles.data;
288         if (!targetTriple.empty)
289             result ~= ["-target", targetTriple];
290         if (!cpu.empty)
291             result ~= ["-mcpu=" ~ cpu];
292         if (result.length > MIN_SANITIZE_COMMAND_LENGTH)
293             result ~= "-fno-sanitize=all";
294         return result;
295     }
296 
297     /// Builds a static or dynamic library with Zig build-lib.
298     /// Params:
299     ///   libpath = Output file path.
300     ///   isShared = Build a dynamic library if true, static if false.
301     /// Returns: Exit status (0 for success).
302     int buildLibrary(string libpath, bool isShared = false) @trusted
303     {
304         if (sourceFiles.data.length == 0)
305         {
306             stderr.writeln("Error: No source files specified for library build");
307             return 1;
308         }
309         auto cmd = ["zig", "build-lib"] ~ sourceFiles.data;
310         cmd ~= ["-femit-bin=" ~ libpath, "-OReleaseFast"]; // disble ubsan
311         if (isShared)
312             cmd ~= ["-dynamic"];
313         if (!targetTriple.empty)
314             cmd ~= ["-target", targetTriple];
315         if (!cpu.empty)
316             cmd ~= ["-mcpu=" ~ cpu];
317 
318         mixin FlagChecks;
319         auto clangFlags = cmds.data.filter!(isClangFlag).array;
320         if (clangFlags.length)
321             cmd ~= ["-cflags"] ~ clangFlags[2 .. $] ~ ["--"];
322         cmd ~= isCPlusPlus && !targetTriple.endsWith("msvc") ? "-lc++" : "-lc";
323 
324         debug
325         {
326             write("[zig build-lib] flags: \"");
327             foreach (c; cmd[2 .. $])
328                 write(c, " ");
329             writeln("\"");
330         }
331 
332         // Log warnings before execution
333         foreach (warning; warnings)
334             stderr.writeln(warning);
335         return executeCommand(cmd, "build-lib");
336     }
337 
338     /// Executes the Zig command, printing flags in debug mode.
339     /// Returns: Exit status (0 for success).
340     int execute() @trusted
341     {
342         auto cmd = build();
343         if (cmd.length > BASE_COMMAND_LENGTH)
344         {
345             debug
346             {
347                 write("[zig ", cmds.data[1], "] flags: \"");
348                 foreach (c; cmd[BASE_COMMAND_LENGTH .. $])
349                     write(c, " ");
350                 writeln("\"");
351             }
352         }
353 
354         // Log warnings before execution
355         foreach (warning; warnings)
356             stderr.writeln(warning);
357         return executeCommand(cmd, cmds.data[1]);
358     }
359 
360     /// Executes a command, enforcing DMD architecture restrictions.
361     /// Params:
362     ///   cmd = Command array to execute.
363     ///   mode = Command mode (e.g., cc, c++, build-lib).
364     /// Returns: Exit status (0 for success).
365     private int executeCommand(string[] cmd, string mode) @trusted
366     {
367         version (DMD)
368         {
369             if (!targetTriple.empty && targetTriple != "native-native" &&
370                 !ALLOWED_DMD_ARCHES.any!(arch => targetTriple.canFind(arch)))
371             {
372                 stderr.writeln("Error: DMD only supports x86/x86_64 or -target native-native");
373                 return 1;
374             }
375         }
376 
377         try
378         {
379             auto result = std.process.execute(cmd);
380             if (result.output.length)
381                 write(result.output);
382             enforce(result.status == 0, format("Zig %s failed with exit code %d: %s",
383                     mode, result.status, result.output));
384             return result.status;
385         }
386         catch (ProcessException e)
387         {
388             stderr.writeln("Error executing zig ", mode, ": ", e.msg);
389             return 1;
390         }
391     }
392 }
393 
394 /// Unit tests for Builder.
395 version (unittest)
396 {
397     import std.exception : assertThrown;
398     import std.algorithm : any;
399 
400     @("Skip excluded flags")
401     unittest
402     {
403         auto builder = new Builder();
404         builder.addArg("--no-as-needed").addArg("--exclude-libs").addArg("/nologo");
405         assert(builder.build() == ["zig", "cc"]);
406     }
407 
408     @("Transform -group and -dynamic flags")
409     unittest
410     {
411         auto builder = new Builder();
412         builder.addArg("-group");
413         assert(builder.build() == [
414             "zig", "cc", "-Wl,--start-group", "-Wl,--end-group",
415             "-fno-sanitize=all"
416         ]);
417     }
418 
419     @("Preserve explicit library flags")
420     unittest
421     {
422         auto builder = new Builder();
423         builder.addArg("-lm");
424         assert(builder.build() == ["zig", "cc", "-lm"]);
425     }
426 
427     @("Set target triple and CPU")
428     unittest
429     {
430         auto builder = new Builder();
431         builder.setTargetTriple("arm64-apple-macos").setCpu("generic");
432         assert(builder.build() == [
433             "zig", "cc", "-target", "aarch64-macos", "-mcpu=generic",
434             "-fno-sanitize=all"
435         ]);
436     }
437 
438     @("Detect C++ mode from file extension")
439     unittest
440     {
441         auto builder = new Builder();
442         builder.file("test.cpp");
443         assert(builder.build() == ["zig", "c++", "test.cpp"]);
444     }
445 
446     @("DMD rejects non-x86/x86_64 targets")
447     unittest
448     {
449         version (DMD)
450         {
451             auto builder = new Builder();
452             builder.setTargetTriple("wasm32-wasi-musl");
453             assert(builder.execute() == 1);
454         }
455     }
456 
457     @("LDC allows all targets")
458     unittest
459     {
460         version (LDC)
461         {
462             auto builder = new Builder();
463             builder.setTargetTriple("riscv64-linux-gnu");
464             assert(builder.build().canFind("riscv64-linux-gnu"));
465         }
466     }
467 
468     @("Pass through Clang flags")
469     unittest
470     {
471         auto builder = new Builder();
472         builder.file("test.c").addArg("-Wall").addArg("-std=c99").addArg("-h");
473         assert(builder.build() == [
474             "zig", "cc", "-Wall", "-std=c99", "-h", "test.c", "-fno-sanitize=all"
475         ]);
476     }
477 
478     @("DMD allows x86_64 target")
479     unittest
480     {
481         version (DMD)
482         {
483             auto builder = new Builder();
484             builder.setTargetTriple("x86_64-linux-gnu");
485             assert(builder.build().canFind("x86_64-linux-gnu"));
486         }
487     }
488 
489     @("Pass --help to zig")
490     unittest
491     {
492         auto builder = new Builder();
493         builder.addArg("--help");
494         assert(builder.build() == ["zig", "cc", "--help"]);
495     }
496 
497     @("Throw on failed execution")
498     unittest
499     {
500         auto builder = new Builder();
501         builder.file("nonexistent.c");
502         assertThrown!Exception(builder.execute());
503     }
504 
505     @("Detect C++ mode with any flag")
506     unittest
507     {
508         auto builder = new Builder();
509         builder.file("test.cpp").addArg("-some-flag");
510         assert(builder.build() == [
511             "zig", "c++", "-some-flag", "test.cpp", "-fno-sanitize=all"
512         ]);
513     }
514 
515     @("DMD allows native-native target")
516     unittest
517     {
518         version (DMD)
519         {
520             auto builder = new Builder();
521             builder.setTargetTriple("native-native");
522             assert(builder.build() == [
523                 "zig", "cc", "-target", "native-native", "-fno-sanitize=all"
524             ]);
525         }
526     }
527 
528     @("Throw on invalid Clang flag")
529     unittest
530     {
531         auto builder = new Builder();
532         builder.file("test.c").addArg("-invalid-flag");
533         assertThrown!Exception(builder.execute());
534     }
535 
536     @("Pass -h to zig")
537     unittest
538     {
539         auto builder = new Builder();
540         builder.addArg("-h");
541         assert(builder.build() == ["zig", "cc", "-h"]);
542     }
543 
544     @("MSVC target avoids zig c++")
545     unittest
546     {
547         auto builder = new Builder();
548         builder.setTargetTriple("x86_64-windows-msvc").file("test.cc");
549         assert(builder.build() == [
550             "zig", "cc", "test.cc", "-target", "x86_64-windows-msvc",
551             "-fno-sanitize=all"
552         ]);
553     }
554 
555     @("Transform arm64-apple-ios to aarch64-ios")
556     unittest
557     {
558         auto builder = new Builder();
559         builder.setTargetTriple("arm64-apple-ios").file("test.c");
560         assert(builder.build() == [
561             "zig", "cc", "test.c", "-target", "aarch64-ios", "-fno-sanitize=all"
562         ]);
563     }
564 
565     @("MSVC target with native-windows-msvc")
566     unittest
567     {
568         auto builder = new Builder();
569         builder.setTargetTriple("native-windows-msvc").file("test.cc");
570         assert(builder.build() == [
571             "zig", "cc", "test.cc", "-target", "native-windows-msvc",
572             "-fno-sanitize=all"
573         ]);
574     }
575 
576     @("Build library with C source")
577     unittest
578     {
579         auto builder = new Builder();
580         builder.file("test.c").addArg("-Wall");
581         assertThrown!Exception(builder.buildLibrary("test.lib"));
582     }
583 
584     @("Build library with C++ source")
585     unittest
586     {
587         auto builder = new Builder();
588         builder.file("test.cpp").addArg("-std=c++11");
589         assertThrown!Exception(builder.buildLibrary("libtest.a"));
590     }
591 
592     @("Build library with extra flags")
593     unittest
594     {
595         auto builder = new Builder();
596         builder.file("test.c").addArg("-Wall").file("rc.s");
597         assertThrown!Exception(builder.buildLibrary("test.dylib"));
598     }
599 
600     @("Add object file")
601     unittest
602     {
603         auto builder = new Builder();
604         builder.file("test.o");
605         assert(builder.build() == ["zig", "cc", "test.o"]);
606     }
607 
608     @("Add assembly file")
609     unittest
610     {
611         auto builder = new Builder();
612         builder.file("test.s");
613         assert(builder.build() == ["zig", "cc", "test.s"]);
614     }
615 
616     @("Transform armv5te-linux-gnu to arm-linux-gnu")
617     unittest
618     {
619         auto builder = new Builder();
620         builder.setTargetTriple("armv5te-linux-gnu");
621         assert(builder.build() == [
622             "zig", "cc", "-target", "arm-linux-gnu", "-fno-sanitize=all"
623         ]);
624     }
625 
626     @("Transform armv6-linux-musl to arm-linux-musl")
627     unittest
628     {
629         auto builder = new Builder();
630         builder.setTargetTriple("armv6-linux-musl");
631         assert(builder.build() == [
632             "zig", "cc", "-target", "arm-linux-musl", "-fno-sanitize=all"
633         ]);
634     }
635 
636     @("Transform armv7-linux-musl to arm-linux-musl")
637     unittest
638     {
639         auto builder = new Builder();
640         builder.setTargetTriple("armv7-linux-musl");
641         assert(builder.build() == [
642             "zig", "cc", "-target", "arm-linux-musl", "-fno-sanitize=all"
643         ]);
644     }
645 
646     @("Transform armv8a-freestanding to arm-freestanding")
647     unittest
648     {
649         auto builder = new Builder();
650         builder.setTargetTriple("armv8a-freestanding");
651         assert(builder.build() == [
652             "zig", "cc", "-target", "arm-freestanding", "-fno-sanitize=all"
653         ]);
654     }
655 
656     @("Transform riscv64gc-linux-gnu to riscv64-linux-gnu")
657     unittest
658     {
659         auto builder = new Builder();
660         builder.setTargetTriple("riscv64gc-linux-gnu");
661         assert(builder.build() == [
662             "zig", "cc", "-target", "riscv64-linux-gnu", "-fno-sanitize=all"
663         ]);
664     }
665 
666     @("Transform riscv32i-linux-musl to riscv32-linux-musl")
667     unittest
668     {
669         auto builder = new Builder();
670         builder.setTargetTriple("riscv32i-linux-musl");
671         assert(builder.build() == [
672             "zig", "cc", "-target", "riscv32-linux-musl", "-fno-sanitize=all"
673         ]);
674     }
675 
676     @("Transform wasm32-unknown-unknown-wasm to wasm32-freestanding")
677     unittest
678     {
679         auto builder = new Builder();
680         builder.setTargetTriple("wasm32-unknown-unknown-wasm");
681         assert(builder.build() == [
682             "zig", "cc", "-target", "wasm32-freestanding", "-fno-sanitize=all"
683         ]);
684     }
685 
686     @("Transform wasm32-unknown-emscripten-wasm to wasm32-emscripten")
687     unittest
688     {
689         auto builder = new Builder();
690         builder.setTargetTriple("wasm32-unknown-emscripten-wasm");
691         assert(builder.build() == [
692             "zig", "cc", "-target", "wasm32-emscripten", "-fno-sanitize=all"
693         ]);
694     }
695 
696     @("Support wasm32-wasi-musl triple")
697     unittest
698     {
699         auto builder = new Builder();
700         builder.setTargetTriple("wasm32-wasi-musl");
701         assert(builder.build() == [
702             "zig", "cc", "-target", "wasm32-wasi", "-fno-sanitize=all"
703         ]);
704     }
705 }