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