1 /*
2 zlib/libpng license
3 
4 Copyright (c) 2023-2025 Matheus Catarino França <matheus-catarino@hotmail.com>
5 
6 This software is provided 'as-is', without any express or implied warranty.
7 In no event will the authors be held liable for any damages arising from the
8 use of this software.
9 */
10 module build;
11 
12 import std;
13 
14 // Dependency versions
15 enum emsdk_version = "5.0.5";
16 enum imgui_version = "1.92.7";
17 enum nuklear_version = "4.13.2";
18 
19 void main(string[] args) @safe
20 {
21     static if (__VERSION__ < 2111)
22     {
23         static assert(false, "This project requires DMD-frontend 2.111.0 or newer");
24     }
25 
26     // Command-line options
27     struct Options
28     {
29         bool help, verbose, downloadEmsdk, downloadShdc;
30         string compiler, target = defaultTarget(), optimize = "debug", linkExample, runExample, linkage = "static", shellFile;
31         SokolBackend backend;
32         bool useX11 = true, useWayland, useEgl, useLTO, withSokolImgui, withSokolNuklear;
33     }
34 
35     Options opts;
36     immutable sokolRoot = environment.get("SOKOL_ROOTPATH", getcwd);
37     immutable vendorPath = absolutePath(buildPath(sokolRoot, "vendor"));
38     immutable sokolSrcPath = absolutePath(buildPath(sokolRoot, "src", "sokol", "c"));
39 
40     // Parse arguments
41     foreach (arg; args[1 .. $])
42         with (opts) switch (arg)
43     {
44     case "--help":
45         help = true;
46         break;
47     case "--verbose":
48         verbose = true;
49         break;
50     case "--enable-wasm-lto":
51         useLTO = true;
52         break;
53     case "--download-emsdk":
54         downloadEmsdk = true;
55         break;
56     case "--download-sokol-tools":
57         downloadShdc = true;
58         break;
59     case "--with-sokol-imgui":
60         withSokolImgui = true;
61         break;
62     case "--with-sokol-nuklear":
63         withSokolNuklear = true;
64         break;
65     case "--enable-wayland":
66         useWayland = true;
67         break;
68     case "--disable-x11":
69         useX11 = false;
70         break;
71     case "--enable-egl":
72         useEgl = true;
73         break;
74     default:
75         if (arg.startsWith("--backend="))
76             backend = arg[10 .. $].to!SokolBackend;
77         else if (arg.startsWith("--toolchain="))
78             compiler = findProgram(arg[12 .. $]);
79         else if (arg.startsWith("--optimize="))
80             optimize = arg[11 .. $];
81         else if (arg.startsWith("--target="))
82             target = arg[9 .. $];
83         else if (arg.startsWith("--link="))
84             linkExample = arg[7 .. $];
85         else if (arg.startsWith("--run="))
86             runExample = arg[6 .. $];
87         else if (arg.startsWith("--linkage="))
88         {
89             linkage = arg[10 .. $];
90             if (!["static", "dynamic"].canFind(linkage))
91                 throw new Exception("Invalid linkage: use static or dynamic");
92         }
93         else if (arg.startsWith("--shell-file="))
94             shellFile = arg[13 .. $];
95         else
96             throw new Exception("Unknown argument: " ~ arg);
97         break;
98     }
99 
100     if (args.length < 2 || opts.help)
101     {
102         writeln("Usage: build [options]\nOptions:");
103         writeln("  --help                   Show this help message");
104         writeln("  --verbose                Enable verbose output");
105         writeln(
106             "  --backend=<backend>      Select backend (d3d11, metal, glcore, gles3, wgpu, vulkan)");
107         writeln("  --toolchain=<compiler>   Select C toolchain (e.g., gcc, clang, emcc)");
108         writeln("  --optimize=<level>       Select optimization level (debug, release, small)");
109         writeln("  --target=<target>        Select target (native, wasm, android)");
110         writeln("  --linkage=<type>         Library linkage (static or dynamic, default: static)");
111         writeln("  --enable-wayland         Enable Wayland support (Linux only)");
112         writeln("  --disable-x11            Disable X11 support (Linux only)");
113         writeln("  --enable-egl             Force EGL (Linux only)");
114         writeln("  --enable-wasm-lto        Enable Emscripten LTO");
115         writeln("  --download-emsdk         Download Emscripten SDK");
116         writeln("  --download-sokol-tools   Download sokol-tools");
117         writeln("  --link=<example>         Link WASM example (e.g., triangle)");
118         writeln("  --run=<example>          Run WASM example (e.g., triangle)");
119         writeln("  --with-sokol-imgui       Enable sokol_imgui integration");
120         writeln("  --with-sokol-nuklear     Enable sokol_nuklear integration");
121         return;
122     }
123 
124     if (opts.backend == SokolBackend._auto)
125         opts.backend = resolveSokolBackend(opts.backend, opts.target);
126 
127     opts.target = resolveTarget(opts.target);
128 
129     if (!opts.linkExample && !opts.runExample)
130     {
131         if (opts.target.canFind("wasm"))
132             opts.downloadEmsdk = true;
133         writeln("Configuration:");
134         writeln("  Target: ", opts.target, ", Optimize: ", opts.optimize, ", Backend: ", opts
135                 .backend);
136         writeln("  Linkage: ", opts.linkage);
137         writeln("  Download: Emscripten=", opts.downloadEmsdk, ", ImGui=", opts.withSokolImgui,
138             ", Nuklear=", opts.withSokolNuklear, ", Sokol-tools=", opts.downloadShdc);
139         writeln("  Verbose: ", opts.verbose);
140     }
141 
142     // Setup dependencies
143     if (opts.downloadEmsdk || opts.target.canFind("wasm"))
144         getEmSDK(vendorPath);
145     if (opts.withSokolImgui)
146         getIMGUI(vendorPath);
147     if (opts.withSokolNuklear)
148         getNuklear(vendorPath);
149 
150     // Execute build steps
151     if (opts.downloadShdc)
152     {
153         buildShaders(vendorPath, opts.backend);
154     }
155     else if (opts.linkExample)
156     {
157         string shellFile = (opts.shellFile.empty) ? 
158             absolutePath(buildPath(sokolRoot, "src", "sokol", "web", "shell.html")) :
159             absolutePath(opts.shellFile);
160 
161         EmLinkOptions linkOpts = {
162             target: "wasm",
163             optimize: opts.optimize,
164             lib_main: buildPath("build", "lib" ~ opts.linkExample ~ ".a"),
165             vendor: vendorPath,
166             backend: opts.backend,
167             use_emmalloc: true,
168             release_use_lto: opts.useLTO,
169             use_imgui: opts.withSokolImgui,
170             use_nuklear: opts.withSokolNuklear,
171             use_filesystem: false,
172             shell_file_path: shellFile,
173             extra_args: [
174                 "-L" ~ absolutePath(buildPath(sokolRoot, "build")), "-lsokol"
175             ],
176             verbose: opts.verbose
177         };
178         emLinkStep(linkOpts);
179     }
180     else if (opts.runExample)
181     {
182         emRunStep(EmRunOptions(opts.runExample, vendorPath, opts.verbose));
183     }
184     else
185     {
186         LibSokolOptions libOpts = {
187             target: opts.target,
188             optimize: opts.optimize,
189             toolchain: opts.compiler,
190             vendor: vendorPath,
191             sokolSrcPath: sokolSrcPath,
192             backend: opts.backend,
193             use_x11: opts.useX11,
194             use_wayland: opts.useWayland,
195             use_egl: opts.useEgl,
196             with_sokol_imgui: opts.withSokolImgui,
197             with_sokol_nuklear: opts.withSokolNuklear,
198             linkageStatic: opts.target.canFind("wasm") ? true : opts.linkage == "static",
199             verbose: opts.verbose
200         };
201 
202         buildLibSokol(libOpts);
203     }
204 }
205 
206 // Dependency management
207 void getEmSDK(string vendor) @safe
208 {
209     downloadAndExtract("Emscripten SDK", vendor, "emsdk",
210         format("https://github.com/emscripten-core/emsdk/archive/refs/tags/%s.zip", emsdk_version),
211         (path) => emSdkSetupStep(path));
212 }
213 
214 void getIMGUI(string vendor) @safe
215 {
216     string url;
217     enum commitHashRegex = ctRegex!`^[0-9a-fA-F]{7,40}$`;
218     if (matchFirst(imgui_version, commitHashRegex))
219         url = format("https://github.com/floooh/dcimgui/archive/%s.zip", imgui_version);
220     else
221         url = format("https://github.com/floooh/dcimgui/archive/refs/tags/v%s.zip", imgui_version);
222     downloadAndExtract("ImGui", vendor, "imgui", url);
223 }
224 
225 void getNuklear(string vendor) @safe
226 {
227     writeln("Setting up Nuklear");
228     string path = absolutePath(buildPath(vendor, "nuklear"));
229     string file = "nuklear.h";
230 
231     if (!exists(path))
232     {
233         mkdirRecurse(path);
234         download(
235             format("https://raw.githubusercontent.com/Immediate-Mode-UI/Nuklear/refs/tags/%s/nuklear.h",
236                 nuklear_version), file);
237         std.file.write(buildPath(path, "nuklear.h"), read(file));
238     }
239 }
240 
241 void buildShaders(string vendor, ref SokolBackend opts) @safe
242 {
243     immutable shdcPath = getSHDC(vendor);
244     immutable shadersDir = "examples/shaders";
245     immutable shaders = [
246         "triangle", "bufferoffsets", "cube", "instancing", "instancingcompute",
247         "mrt", "noninterleaved", "offscreen", "quad", "shapes", "texcube", "blend",
248         "vertexpull"
249     ];
250 
251     version (OSX)
252         enum glsl = "glsl410";
253     else
254         enum glsl = "glsl430";
255 
256     immutable slangTemplate = opts == SokolBackend.vulkan
257         ? glsl ~ ":metal_macos:hlsl5:%s:wgsl:spirv_vk" : glsl ~ ":metal_macos:hlsl5:%s:wgsl";
258 
259     version (Posix)
260         executeOrFail(["chmod", "+x", shdcPath], "Failed to set shader permissions", true);
261 
262     foreach (shader; shaders)
263     {
264         immutable essl = (shader == "instancingcompute" || shader == "vertexpull")
265             ? "glsl310es" : "glsl300es";
266         immutable slang = slangTemplate.format(essl);
267         executeOrFail([
268             shdcPath, "-i", buildPath(shadersDir, shader ~ ".glsl"),
269             "-o", buildPath(shadersDir, shader ~ ".d"), "-l", slang, "-f",
270             "sokol_d"
271         ], "Shader compilation failed for " ~ shader, true);
272     }
273 }
274 
275 // Download and extract utility
276 void downloadAndExtract(string name, string vendor, string dir, string url,
277     void delegate(string) @safe postExtract = null) @safe
278 {
279     writeln("Setting up ", name);
280     string path = absolutePath(buildPath(vendor, dir));
281     string file = dir ~ ".zip";
282     scope (exit)
283         if (exists(file))
284             remove(file);
285 
286     if (!exists(path))
287     {
288         download(url, file);
289         extractZip(file, path);
290     }
291     if (postExtract)
292         postExtract(path);
293 }
294 
295 // Core build structures
296 enum SokolBackend
297 {
298     _auto,
299     d3d11,
300     metal,
301     glcore,
302     gles3,
303     wgpu,
304     vulkan
305 }
306 
307 struct LibSokolOptions
308 {
309     string target, optimize, toolchain, vendor, sokolSrcPath;
310     SokolBackend backend;
311     bool use_egl, use_x11 = true, use_wayland, with_sokol_imgui, with_sokol_nuklear,
312     linkageStatic, verbose;
313 }
314 
315 struct EmLinkOptions
316 {
317     string target, optimize, lib_main, vendor, shell_file_path;
318     SokolBackend backend;
319     bool release_use_closure = true, release_use_lto, use_emmalloc, use_filesystem,
320     use_imgui, use_nuklear, verbose;
321     string[] extra_args;
322 }
323 
324 struct EmRunOptions
325 {
326     string name, vendor;
327     bool verbose;
328 }
329 
330 struct EmbuilderOptions
331 {
332     string port_name, vendor;
333 }
334 
335 // ---------------------------------------------------------------------------
336 // Platform helpers
337 // ---------------------------------------------------------------------------
338 
339 /// Resolve "native" to the actual compile-time platform string so that all
340 /// subsequent target checks work correctly when DUB passes --target=native.
341 string resolveTarget(string target) @safe pure nothrow
342 {
343     if (target != "native")
344         return target;
345     version (Windows)
346         return "windows";
347     else version (OSX)
348         return "darwin";
349     else version (linux)
350         return "linux";
351     else version (Android)
352         return "android";
353     else version (Emscripten)
354         return "wasm";
355     else
356         return "linux"; // safe fallback
357 }
358 
359 /// Returns true when compiling for (or running on) Windows.
360 bool targetIsWindows(string target) @safe pure nothrow
361 {
362     return resolveTarget(target).canFind("windows");
363 }
364 
365 bool targetIsDarwin(string target) @safe pure nothrow
366 {
367     return resolveTarget(target).canFind("darwin");
368 }
369 
370 bool targetIsWasm(string target) @safe pure nothrow
371 {
372     return resolveTarget(target).canFind("wasm");
373 }
374 
375 /// Object-file extension: ".obj" on Windows, ".o" elsewhere.
376 string objExt(string target) @safe pure nothrow
377 {
378     return targetIsWindows(target) ? ".obj" : ".o";
379 }
380 
381 /// Emit a single compiler flag that defines a C preprocessor macro,
382 /// formatted correctly for the target toolchain.
383 string defineFlag(string macro_, string target) @safe pure nothrow
384 {
385     return (targetIsWindows(target) ? "/D" : "-D") ~ macro_;
386 }
387 
388 /// Emit an include-path flag for the target toolchain.
389 string includeFlag(string path, string target) @safe pure nothrow
390 {
391     return targetIsWindows(target) ? "/I" ~ path : "-I" ~ path;
392 }
393 
394 // ---------------------------------------------------------------------------
395 // Build Sokol (and optionally ImGui / Nuklear) native libraries
396 // ---------------------------------------------------------------------------
397 
398 void buildLibSokol(LibSokolOptions opts) @safe
399 {
400     immutable buildDir = absolutePath("build");
401     mkdirRecurse(buildDir);
402 
403     immutable isWin = targetIsWindows(opts.target);
404     immutable isMac = targetIsDarwin(opts.target);
405     immutable isWasm = targetIsWasm(opts.target);
406 
407     string compiler = opts.toolchain ? opts.toolchain : defaultCompiler(opts.target);
408 
409     // ------------------------------------------------------------------
410     // Assemble compiler flags — kept strictly per-toolchain from the start
411     // ------------------------------------------------------------------
412     string[] cflags;
413     string[] lflags;
414 
415     immutable backendMacro = format("SOKOL_%s",
416         resolveSokolBackend(opts.backend, opts.target).to!string.toUpper);
417 
418     if (isWin)
419     {
420         // MSVC (cl.exe) style
421         cflags = [
422             "/DNDEBUG", "/DIMPL", "/D" ~ backendMacro,
423             "/nologo", "/wd4190",
424             opts.optimize == "debug" ? "/Od": "/O2"
425         ];
426         // For Vulkan backend, add the Vulkan SDK include path
427         if (opts.backend == SokolBackend.vulkan || resolveSokolBackend(opts.backend, opts.target) == SokolBackend
428             .vulkan)
429         {
430             immutable vulkanSdk = environment.get("VULKAN_SDK", "");
431             if (vulkanSdk.length)
432                 cflags ~= "/I" ~ buildPath(vulkanSdk, "Include");
433             else
434                 throw new Exception("VULKAN_SDK environment variable is not set. " ~
435                         "Install the Vulkan SDK from https://vulkan.lunarg.com/");
436         }
437         // Windows libs are handled by DUB; lflags unused here but kept for
438         // completeness if linkLibrary ever needs them.
439     }
440     else
441     {
442         // GCC / Clang / emcc style
443         cflags = [
444             "-DNDEBUG", "-DIMPL", "-D" ~ backendMacro,
445             "-Wall", "-Wextra", "-Wno-unused-function",
446         ];
447 
448         if (isMac)
449             cflags ~= "-Wno-return-type-c-linkage";
450 
451         if (!isWasm)
452         {
453             cflags ~= opts.optimize == "debug" ? "-O0" : "-O2";
454             // Position-independent code: PIE for static, PIC for shared
455             cflags ~= opts.linkageStatic ? "-fPIE" : "-fPIC";
456         }
457 
458         if (!isMac && !isWasm)
459         {
460             // Linux-specific defines
461             if (opts.use_egl)
462                 cflags ~= "-DSOKOL_FORCE_EGL";
463             if (!opts.use_x11)
464                 cflags ~= "-DSOKOL_DISABLE_X11";
465             if (!opts.use_wayland)
466                 cflags ~= "-DSOKOL_DISABLE_WAYLAND";
467 
468             // Linux link flags (informational; actual linking done by DUB)
469             if (opts.use_wayland)
470                 lflags ~= [
471                 "-lwayland-client", "-lwayland-egl",
472                 "-lwayland-cursor", "-lxkbcommon"
473             ];
474             lflags ~= opts.backend == SokolBackend.vulkan ? ["-lvulkan"] : [
475                 "-lGL"
476             ];
477             lflags ~= ["-lX11", "-lXi", "-lXcursor", "-lasound", "-lm", "-ldl"];
478         }
479 
480         if (isMac)
481             lflags ~= [
482             "-framework", "Cocoa",
483             "-framework", "QuartzCore",
484             "-framework", "Foundation",
485             "-framework", "Metal",
486             "-framework", "AudioToolbox",
487         ];
488 
489         if (isWasm)
490         {
491             if (opts.backend == SokolBackend.wgpu)
492             {
493                 EmbuilderOptions embopts = {
494                     port_name: "emdawnwebgpu",
495                     vendor: opts.vendor,
496                 };
497                 embuilderStep(embopts);
498                 cflags ~= format("-I%s",
499                     buildPath(opts.vendor, "emsdk", "upstream", "emscripten",
500                         "cache", "ports", "emdawnwebgpu",
501                         "emdawnwebgpu_pkg", "webgpu", "include"));
502             }
503             compiler = buildPath(opts.vendor, "emsdk", "upstream",
504                 "emscripten", "emcc") ~ (isWindows() ? ".bat" : "");
505         }
506     }
507 
508     // ------------------------------------------------------------------
509     // Optional third-party include paths
510     // ------------------------------------------------------------------
511     if (opts.with_sokol_nuklear)
512         cflags ~= includeFlag(absolutePath(buildPath(opts.vendor, "nuklear")), opts.target);
513 
514     // ------------------------------------------------------------------
515     // Compile & archive libsokol
516     // ------------------------------------------------------------------
517     immutable sokolSources = [
518         "sokol_log.c", "sokol_app.c", "sokol_gfx.c", "sokol_time.c",
519         "sokol_audio.c", "sokol_gl.c", "sokol_debugtext.c", "sokol_shape.c",
520         "sokol_glue.c", "sokol_fetch.c", "sokol_memtrack.c", "sokol_args.c",
521     ];
522 
523     // On macOS the sokol .c headers use ObjC syntax — compile them as ObjC.
524     // This flag must NOT be in the shared cflags because it would force
525     // C++ source files (imgui) to be compiled as ObjC instead of C++.
526     string[] sokolCFlags = isMac ? cflags ~ ["-x", "objective-c"] : cflags;
527 
528     auto sokolObjs = compileSources(
529         sokolSources, buildDir, opts.sokolSrcPath,
530         compiler, sokolCFlags, "sokol_", opts.target, opts.verbose);
531 
532     immutable sokolLib = buildPath(buildDir, sokolLibName(opts.target, opts.linkageStatic));
533     linkLibrary(sokolLib, sokolObjs, opts.target, opts.linkageStatic,
534         opts.vendor, lflags, opts.verbose);
535     sokolObjs.each!(obj => exists(obj) && remove(obj));
536 
537     // ------------------------------------------------------------------
538     // Optionally compile & archive libcimgui
539     // ------------------------------------------------------------------
540     if (opts.with_sokol_imgui)
541     {
542         immutable imguiRoot = absolutePath(buildPath(opts.vendor, "imgui", "src"));
543         enforce(exists(imguiRoot), "ImGui source not found. Run with --with-sokol-imgui after setup.");
544 
545         // ImGui needs its own C++ compiler; also include its headers
546         string imguiCompiler = cppCompiler(compiler, opts.target, opts.vendor);
547         string[] imguiFlags = cflags ~ includeFlag(imguiRoot, opts.target);
548         if (!isWin)
549             imguiFlags ~= "-DNDEBUG"; // already in cflags but harmless duplicate
550 
551         immutable imguiSources = [
552             "cimgui.cpp", "imgui.cpp", "imgui_demo.cpp", "imgui_draw.cpp",
553             "imgui_tables.cpp", "imgui_widgets.cpp", "cimgui_internal.cpp"
554         ];
555         auto imguiObjs = compileSources(
556             imguiSources, buildDir, imguiRoot,
557             imguiCompiler, imguiFlags, "imgui_", opts.target, opts.verbose);
558 
559         // sokol_imgui.c and sokol_gfx_imgui.c are C, compiled with the C compiler
560         // On macOS they also need -x objective-c (same reason as the core sokol sources)
561         foreach (sokolImguiSrc; ["sokol_imgui.c", "sokol_gfx_imgui.c", "sokol_app_imgui.c"])
562         {
563             immutable srcPath = buildPath(opts.sokolSrcPath, sokolImguiSrc);
564             enforce(exists(srcPath), sokolImguiSrc ~ " not found");
565             immutable objPath = buildPath(buildDir,
566                 sokolImguiSrc.stripExtension ~ objExt(opts.target));
567             compileSource(srcPath, objPath, compiler,
568                 sokolCFlags ~ includeFlag(imguiRoot, opts.target),
569                 opts.target, opts.verbose);
570             imguiObjs ~= objPath;
571         }
572 
573         immutable imguiLib = buildPath(buildDir, imguiLibName(opts.target, opts.linkageStatic));
574         linkLibrary(imguiLib, imguiObjs, opts.target, opts.linkageStatic,
575             opts.vendor, lflags, opts.verbose);
576         imguiObjs.each!(obj => exists(obj) && remove(obj));
577     }
578 
579     // ------------------------------------------------------------------
580     // Optionally compile & archive libnuklear
581     // ------------------------------------------------------------------
582     if (opts.with_sokol_nuklear)
583     {
584         immutable nuklearRoot = absolutePath(buildPath(opts.vendor, "nuklear"));
585         enforce(exists(nuklearRoot), "Nuklear source not found. Run after setup.");
586 
587         immutable sokolNuklearPath = buildPath(opts.sokolSrcPath, "sokol_nuklear.c");
588         enforce(exists(sokolNuklearPath), "sokol_nuklear.c not found");
589 
590         // sokol_nuklear.c compiled with SOKOL_NUKLEAR_IMPL (triggered by -DIMPL)
591         // already includes nuklear.h with NK_IMPLEMENTATION internally.
592         // Compiling nuklearc.c separately would redefine every nk_* symbol → link error.
593         // Solution: only compile sokol_nuklear.c, passing the nuklear include path.
594         // On macOS also needs -x objective-c (same as core sokol C sources).
595         string[] nuklearFlags = sokolCFlags ~ includeFlag(nuklearRoot, opts.target);
596 
597         immutable objPath = buildPath(buildDir, "sokol_nuklear" ~ objExt(opts.target));
598         compileSource(sokolNuklearPath, objPath, compiler, nuklearFlags, opts.target, opts.verbose);
599 
600         immutable nuklearLib = buildPath(buildDir, nuklearLibName(opts.target, opts.linkageStatic));
601         linkLibrary(nuklearLib, [objPath], opts.target, opts.linkageStatic,
602             opts.vendor, lflags, opts.verbose);
603         if (exists(objPath))
604             remove(objPath);
605     }
606 }
607 
608 // ---------------------------------------------------------------------------
609 // Library name helpers
610 // ---------------------------------------------------------------------------
611 
612 string sokolLibName(string target, bool static_) @safe pure nothrow
613 {
614     return sharedOrStaticName("sokol", target, static_);
615 }
616 
617 string imguiLibName(string target, bool static_) @safe pure nothrow
618 {
619     return sharedOrStaticName("cimgui", target, static_);
620 }
621 
622 string nuklearLibName(string target, bool static_) @safe pure nothrow
623 {
624     return sharedOrStaticName("nuklear", target, static_);
625 }
626 
627 string sharedOrStaticName(string base, string target, bool static_) @safe pure nothrow
628 {
629     if (targetIsWindows(target))
630         return static_ ? base ~ ".lib" : base ~ ".dll";
631     if (targetIsDarwin(target))
632         return static_ ? "lib" ~ base ~ ".a" : "lib" ~ base ~ ".dylib";
633     if (targetIsWasm(target))
634         return "lib" ~ base ~ ".a"; // wasm is always static
635     return static_ ? "lib" ~ base ~ ".a" : "lib" ~ base ~ ".so"; // linux
636 }
637 
638 // ---------------------------------------------------------------------------
639 // Compile helpers
640 // ---------------------------------------------------------------------------
641 
642 /// Compile one source file to an object file.
643 void compileSource(string srcPath, string objPath, string compiler,
644     string[] cflags, string target, bool verbose) @safe
645 {
646     enforce(exists(srcPath), format("Source file does not exist: %s", srcPath));
647 
648     string[] cmd;
649     if (targetIsWindows(target))
650         cmd = [compiler] ~ cflags ~ ["/c", "/Fo" ~ objPath, srcPath];
651     else
652         cmd = [compiler] ~ cflags ~ ["-c", "-o", objPath, srcPath];
653 
654     executeOrFail(cmd, format("Failed to compile %s", srcPath.baseName), verbose);
655 }
656 
657 /// Compile a list of source files; returns the list of object-file paths.
658 string[] compileSources(const(string[]) sources, string buildDir, string srcRoot,
659     string compiler, string[] cflags, string prefix,
660     string target, bool verbose) @safe
661 {
662     string[] objFiles;
663     foreach (src; sources)
664     {
665         immutable srcPath = buildPath(srcRoot, src);
666         immutable objPath = buildPath(buildDir, prefix ~ src.baseName ~ objExt(target));
667         compileSource(srcPath, objPath, compiler, cflags, target, verbose);
668         objFiles ~= objPath;
669     }
670     return objFiles;
671 }
672 
673 // ---------------------------------------------------------------------------
674 // Link helper
675 // ---------------------------------------------------------------------------
676 
677 void linkLibrary(string libPath, string[] objFiles, string target,
678     bool linkageStatic, string vendor, string[] lflags, bool verbose) @safe
679 {
680     immutable isWin = targetIsWindows(target);
681     immutable isMac = targetIsDarwin(target);
682     immutable isWasm = targetIsWasm(target);
683 
684     string[] cmd;
685 
686     if (linkageStatic || isWasm)
687     {
688         // Static archive
689         string ar;
690         if (isWasm)
691             ar = buildPath(vendor, "emsdk", "upstream", "emscripten", "emar") ~
692                 (isWindows() ? ".bat" : "");
693         else if (isWin)
694             ar = "lib.exe";
695         else
696             ar = "ar";
697 
698         if (isWin)
699             cmd = [ar, "/nologo", "/OUT:" ~ libPath] ~ objFiles;
700         else
701             cmd = [ar, "rcs", libPath] ~ objFiles;
702     }
703     else
704     {
705         // Shared library
706         if (isMac)
707         {
708             cmd = [findProgram("clang"), "-dynamiclib", "-o", libPath] ~ objFiles ~ lflags;
709         }
710         else if (isWin)
711         {
712             // cl.exe /LD
713             cmd = [findProgram("cl"), "/LD", "/nologo", "/Fe:" ~ libPath] ~ objFiles ~ lflags;
714         }
715         else
716         {
717             cmd = [findProgram("gcc"), "-shared", "-o", libPath] ~ objFiles ~ lflags;
718         }
719     }
720 
721     executeOrFail(cmd, format("Failed to create %s", libPath.baseName), verbose);
722 }
723 
724 // ---------------------------------------------------------------------------
725 // Determine the C++ compiler that matches the given C compiler
726 // ---------------------------------------------------------------------------
727 
728 string cppCompiler(string cc, string target, string vendor) @safe
729 {
730     if (targetIsWasm(target))
731         return buildPath(vendor, "emsdk", "upstream", "emscripten", "em++") ~
732             (
733                 isWindows() ? ".bat" : "");
734     if (cc.canFind("clang"))
735         return findProgram(cc.baseName ~ "++");
736     if (cc.canFind("gcc"))
737         return findProgram("g++");
738     // MSVC: same compiler handles both C and C++
739     return cc;
740 }
741 
742 // ---------------------------------------------------------------------------
743 // WASM link / run steps
744 // ---------------------------------------------------------------------------
745 
746 void emLinkStep(EmLinkOptions opts) @safe
747 {
748     string emcc = buildPath(opts.vendor, "emsdk", "upstream", "emscripten",
749         opts.use_imgui ? "em++" : "emcc") ~ (isWindows() ? ".bat" : "");
750     string[] cmd = [emcc];
751 
752     if (opts.use_imgui)
753         cmd ~= "-lcimgui";
754     if (opts.use_nuklear)
755         cmd ~= "-lnuklear";
756 
757     if (opts.optimize == "debug")
758         cmd ~= ["-Og", "-sSAFE_HEAP=1", "-sSTACK_OVERFLOW_CHECK=1"];
759     else
760     {
761         cmd ~= "-sASSERTIONS=0";
762         cmd ~= opts.optimize == "small" ? "-Oz" : "-O3";
763         if (opts.release_use_lto)
764             cmd ~= "-flto";
765         if (opts.release_use_closure)
766             cmd ~= ["--closure", "1"];
767     }
768 
769     if (opts.backend == SokolBackend.wgpu)
770         cmd ~= "--use-port=emdawnwebgpu";
771     if (opts.backend == SokolBackend.gles3)
772         cmd ~= "-sUSE_WEBGL2=1";
773     if (!opts.use_filesystem)
774         cmd ~= "-sNO_FILESYSTEM=1";
775     if (opts.use_emmalloc)
776         cmd ~= "-sMALLOC='emmalloc'";
777     if (opts.shell_file_path)
778         cmd ~= "--shell-file=" ~ opts.shell_file_path;
779 
780     cmd ~= ["-sSTACK_SIZE=512KB"] ~ opts.extra_args ~ opts.lib_main;
781 
782     immutable baseName = opts.lib_main.baseName[3 .. $ - 2]; // strip "lib" and ".a"
783     string outFile = buildPath("build", baseName ~ ".html");
784     cmd ~= ["-o", outFile];
785 
786     executeOrFail(cmd, "emcc link failed for " ~ outFile, opts.verbose);
787 
788     string webDir = "web";
789     mkdirRecurse(webDir);
790     foreach (ext; [".html", ".wasm", ".js"])
791         copy(buildPath("build", baseName ~ ext), buildPath(webDir, baseName ~ ext));
792     rmdirRecurse(buildPath("build"));
793 }
794 
795 void emRunStep(EmRunOptions opts) @safe
796 {
797     string emrun = buildPath(opts.vendor, "emsdk", "upstream", "emscripten", "emrun") ~
798         (
799             isWindows() ? ".bat" : "");
800     executeOrFail([emrun, buildPath("web", opts.name ~ ".html")], "emrun failed", opts.verbose);
801 }
802 
803 // ---------------------------------------------------------------------------
804 // Emscripten SDK setup
805 // ---------------------------------------------------------------------------
806 
807 void emSdkSetupStep(string emsdk) @safe
808 {
809     if (!exists(buildPath(emsdk, ".emscripten")))
810     {
811         immutable cmd = buildPath(emsdk, "emsdk") ~ (isWindows() ? ".bat" : "");
812         executeOrFail(
813             [!isWindows() ? "bash " ~ cmd: cmd, "install", "latest"],
814             "emsdk install failed", true);
815         executeOrFail(
816             [!isWindows() ? "bash " ~ cmd: cmd, "activate", "latest"],
817             "emsdk activate failed", true);
818     }
819 }
820 
821 void embuilderStep(EmbuilderOptions opts) @safe
822 {
823     string embuilder = buildPath(opts.vendor, "emsdk", "upstream", "emscripten", "embuilder") ~
824         (
825             isWindows() ? ".bat" : "");
826     executeOrFail([embuilder, "build", opts.port_name],
827         "embuilder failed to build " ~ opts.port_name, true);
828 }
829 
830 // ---------------------------------------------------------------------------
831 // Utility functions
832 // ---------------------------------------------------------------------------
833 
834 string findProgram(string programName) @safe
835 {
836     foreach (path; environment.get("PATH").split(pathSeparator))
837     {
838         string fullPath = buildPath(path, programName);
839         version (Windows)
840             fullPath ~= ".exe";
841         if (exists(fullPath) && isFile(fullPath))
842             return fullPath;
843     }
844     throw new Exception(format("Program '%s' not found in PATH", programName));
845 }
846 
847 string defaultCompiler(string target) @safe
848 {
849     if (targetIsWasm(target))
850         return ""; // set later from vendor/emsdk
851     version (linux)
852         return findProgram("gcc");
853     version (Windows)
854         return "cl"; // found via PATH / vcvarsall
855     version (OSX)
856         return findProgram("clang");
857     version (Android)
858         return findProgram("clang");
859     throw new Exception("Unsupported platform for defaultCompiler");
860 }
861 
862 SokolBackend resolveSokolBackend(SokolBackend backend, string target) @safe
863 {
864     immutable t = resolveTarget(target);
865     if (t.canFind("linux"))
866         return backend == SokolBackend.vulkan ? SokolBackend.vulkan : SokolBackend.glcore;
867     if (t.canFind("darwin"))
868         return SokolBackend.metal;
869     if (t.canFind("windows"))
870         return backend == SokolBackend.vulkan ? SokolBackend.vulkan : SokolBackend.d3d11;
871     if (t.canFind("wasm"))
872         return backend == SokolBackend.wgpu ? backend : SokolBackend.gles3;
873     if (t.canFind("android"))
874         return SokolBackend.gles3;
875     return backend;
876 }
877 
878 void executeOrFail(string[] cmd, string errorMsg, bool verbose) @safe
879 {
880     if (verbose)
881         writeln("Executing: ", cmd.join(" "));
882     auto result = executeShell(cmd.join(" "));
883     if (verbose && result.output.length)
884         writeln("Output:\n", result.output);
885     enforce(result.status == 0, format("%s: %s", errorMsg, result.output));
886 }
887 
888 bool isWindows() @safe nothrow
889 {
890     version (Windows)
891         return true;
892     return false;
893 }
894 
895 string defaultTarget() @safe
896 {
897     version (linux)
898         return "linux";
899     version (Windows)
900         return "windows";
901     version (OSX)
902         return "darwin";
903     version (Android)
904         return "android";
905     version (Emscripten)
906         return "wasm";
907     throw new Exception("Unsupported platform");
908 }
909 
910 // ---------------------------------------------------------------------------
911 // Download / zip helpers
912 // ---------------------------------------------------------------------------
913 
914 void download(string url, string fileName) @trusted
915 {
916     auto buf = appender!(ubyte[])();
917     size_t contentLength;
918     auto http = HTTP(url);
919     http.onReceiveHeader((in k, in v) {
920         if (k == "content-length")
921             contentLength = to!size_t(v);
922     });
923 
924     int barWidth = 50;
925     http.onReceive((data) {
926         buf.put(data);
927         if (contentLength)
928         {
929             float progress = cast(float) buf.data.length / contentLength;
930             write("\r[",
931                 "=".replicate(cast(int)(barWidth * progress)), ">",
932                 " ".replicate(barWidth - cast(int)(barWidth * progress)),
933                 "] ", format("%d%%", cast(int)(progress * 100)));
934             stdout.flush();
935         }
936         return data.length;
937     });
938 
939     http.perform();
940     enforce(http.statusLine.code / 100 == 2 || http.statusLine.code == 302,
941         format("HTTP request failed: %s", http.statusLine.code));
942     std.file.write(fileName, buf.data);
943     writeln();
944 }
945 
946 void extractZip(string zipFile, string destination) @trusted
947 {
948     ZipArchive archive = new ZipArchive(read(zipFile));
949     string prefix = archive.directory.keys.front[0 .. $
950         - archive.directory.keys.front.find("/")
951             .length + 1];
952 
953     if (exists(destination))
954         rmdirRecurse(destination);
955     mkdirRecurse(destination);
956 
957     foreach (name, am; archive.directory)
958     {
959         if (!am.expandedSize)
960             continue;
961         string path = buildPath(destination, chompPrefix(name, prefix));
962         mkdirRecurse(dirName(path));
963         std.file.write(path, archive.expand(am));
964     }
965 }
966 
967 string getSHDC(string vendor) @safe
968 {
969     string path = absolutePath(buildPath(vendor, "shdc"));
970     string file = "shdc.zip";
971     scope (exit)
972         if (exists(file))
973             remove(file);
974 
975     if (!exists(path))
976     {
977         download("https://github.com/floooh/sokol-tools-bin/archive/refs/heads/master.zip", file);
978         extractZip(file, path);
979     }
980 
981     version (Windows)
982         immutable shdc = buildPath("bin", "win32", "sokol-shdc.exe");
983     else version (linux)
984         immutable shdc = buildPath("bin", isAArch64() ? "linux_arm64" : "linux", "sokol-shdc");
985     else version (OSX)
986         immutable shdc = buildPath("bin", isAArch64() ? "osx_arm64" : "osx", "sokol-shdc");
987     else
988         throw new Exception("Unsupported platform for sokol-tools");
989 
990     return buildPath(path, shdc);
991 }
992 
993 bool isAArch64() @safe nothrow
994 {
995     version (AArch64)
996         return true;
997     return false;
998 }