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 = "4.0.13";
16 enum imgui_version = "1.92.3";
17 enum nuklear_version = "4.12.7";
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     default:
66         if (arg.startsWith("--backend="))
67             backend = arg[10 .. $].to!SokolBackend;
68         else if (arg.startsWith("--toolchain="))
69             compiler = findProgram(arg[12 .. $]);
70         else if (arg.startsWith("--optimize="))
71             optimize = arg[11 .. $];
72         else if (arg.startsWith("--target="))
73             target = arg[9 .. $];
74         else if (arg.startsWith("--link="))
75             linkExample = arg[7 .. $];
76         else if (arg.startsWith("--run="))
77             runExample = arg[6 .. $];
78         else if (arg.startsWith("--linkage="))
79         {
80             linkage = arg[10 .. $];
81             if (!["static", "dynamic"].canFind(linkage))
82                 throw new Exception("Invalid linkage: use static or dynamic");
83         }
84         else
85             throw new Exception("Unknown argument: " ~ arg);
86         break;
87     }
88 
89     if (args.length < 2 || opts.help)
90     {
91         writeln("Usage: build [options]\nOptions:");
92         writeln("  --help                Show this help message");
93         writeln("  --verbose             Enable verbose output");
94         writeln("  --backend=<backend>   Select backend (d3d11, metal, glcore, gles3, wgpu)");
95         writeln("  --toolchain=<compiler> Select C toolchain (e.g., gcc, clang, emcc)");
96         writeln("  --optimize=<level>    Select optimization level (debug, release, small)");
97         writeln("  --target=<target>     Select target (native, wasm, android)");
98         writeln("  --enable-wasm-lto     Enable Emscripten LTO");
99         writeln(
100             "  --linkage=<type>      Specify library linkage (static or dynamic, default: static)");
101         writeln("  --download-emsdk      Download Emscripten SDK");
102         writeln("  --download-sokol-tools Download sokol-tools");
103         writeln("  --link=<example>      Link WASM example (e.g., triangle)");
104         writeln("  --run=<example>       Run WASM example (e.g., triangle)");
105         writeln("  --with-sokol-imgui    Enable sokol_imgui integration");
106         writeln("  --with-sokol-nuklear  Enable sokol_nuklear integration");
107         return;
108     }
109 
110     if (opts.backend == SokolBackend._auto)
111         opts.backend = resolveSokolBackend(opts.backend, opts.target);
112 
113     if (!opts.linkExample && !opts.runExample)
114     {
115         if (opts.target.canFind("wasm"))
116             opts.downloadEmsdk = true;
117         writeln("Configuration:");
118         writeln("  Target: ", opts.target, ", Optimize: ", opts.optimize, ", Backend: ", opts
119                 .backend);
120         writeln("  Linkage: ", opts.linkage);
121         writeln("  Download: Emscripten=", opts.downloadEmsdk, ", ImGui=", opts.withSokolImgui,
122             ", Nuklear=", opts.withSokolNuklear, ", Sokol-tools=", opts.downloadShdc);
123         writeln("  Verbose: ", opts.verbose);
124     }
125 
126     // Setup dependencies
127     if (opts.downloadEmsdk || opts.target.canFind("wasm"))
128         getEmSDK(vendorPath);
129     if (opts.withSokolImgui)
130         getIMGUI(vendorPath);
131     if (opts.withSokolNuklear)
132         getNuklear(vendorPath);
133 
134     // Execute build steps
135     if (opts.downloadShdc)
136         buildShaders(vendorPath);
137     else if (opts.linkExample)
138     {
139         EmLinkOptions linkOpts = {
140             target: "wasm",
141             optimize: opts.optimize,
142             lib_main: buildPath("build", "lib" ~ opts.linkExample ~ ".a"),
143             vendor: vendorPath,
144             backend: opts.backend,
145             use_emmalloc: true,
146             release_use_lto: opts.useLTO,
147             use_imgui: opts.withSokolImgui,
148             use_nuklear: opts.withSokolNuklear,
149             use_filesystem: false,
150             shell_file_path: absolutePath(buildPath(sokolRoot, "src", "sokol", "web", "shell.html")),
151             extra_args: [
152                 "-L" ~ absolutePath(buildPath(sokolRoot, "build")), "-lsokol"
153             ],
154             verbose: opts.verbose
155         };
156         emLinkStep(linkOpts);
157     }
158     else if (opts.runExample)
159     {
160         emRunStep(EmRunOptions(opts.runExample, vendorPath, opts.verbose));
161     }
162     else
163     {
164         LibSokolOptions libOpts = {
165             target: opts.target,
166             optimize: opts.optimize,
167             toolchain: opts.compiler,
168             vendor: vendorPath,
169             sokolSrcPath: sokolSrcPath,
170             backend: opts.backend,
171             use_x11: opts.useX11,
172             use_wayland: opts.useWayland,
173             use_egl: opts.useEgl,
174             with_sokol_imgui: opts.withSokolImgui,
175             with_sokol_nuklear: opts.withSokolNuklear,
176             linkageStatic: opts.target.canFind("wasm") ? true : opts.linkage == "static",
177             verbose: opts.verbose
178         };
179         if (opts.target.canFind("wasm"))
180             buildLibSokol(libOpts);
181     }
182 }
183 
184 // Dependency management
185 void getEmSDK(string vendor) @safe
186 {
187     downloadAndExtract("Emscripten SDK", vendor, "emsdk",
188         format("https://github.com/emscripten-core/emsdk/archive/refs/tags/%s.zip", emsdk_version),
189         (path) => emSdkSetupStep(path));
190 }
191 
192 void getIMGUI(string vendor) @safe
193 {
194     string url;
195     enum commitHashRegex = ctRegex!`^[0-9a-fA-F]{7,40}$`;
196     if (matchFirst(imgui_version, commitHashRegex))
197     {
198         url = format("https://github.com/floooh/dcimgui/archive/%s.zip", imgui_version);
199     }
200     else
201     {
202         url = format("https://github.com/floooh/dcimgui/archive/refs/tags/v%s.zip", imgui_version);
203     }
204     downloadAndExtract("ImGui", vendor, "imgui", url);
205 }
206 
207 void getNuklear(string vendor) @safe
208 {
209     writeln("Setting up Nuklear");
210     string path = absolutePath(buildPath(vendor, "nuklear"));
211     string file = "nuklear.h";
212 
213     if (!exists(path))
214     {
215         mkdirRecurse(path);
216         download(format("https://raw.githubusercontent.com/Immediate-Mode-UI/Nuklear/refs/tags/%s/nuklear.h", nuklear_version), file);
217         std.file.write(buildPath(path, "nuklear.h"), read(file));
218     }
219 }
220 
221 void buildShaders(string vendor) @safe
222 {
223     immutable shdcPath = getSHDC(vendor);
224     immutable shadersDir = "examples/shaders";
225     immutable shaders = [
226         "triangle", "bufferoffsets", "cube", "instancing", "instancingcompute",
227         "mrt", "noninterleaved", "offscreen", "quad", "shapes", "texcube", "blend",
228         "vertexpull"
229     ];
230 
231     version (OSX)
232         enum glsl = "glsl410";
233     else
234         enum glsl = "glsl430";
235 
236     immutable slangTemplate = glsl ~ ":metal_macos:hlsl5:%s:wgsl";
237 
238     version (Posix)
239         executeOrFail(["chmod", "+x", shdcPath], "Failed to set shader permissions", true);
240 
241     foreach (shader; shaders)
242     {
243         immutable essl = (shader == "instancingcompute" || shader == "vertexpull")
244             ? "glsl310es" : "glsl300es";
245         immutable slang = slangTemplate.format(essl);
246         executeOrFail([
247             shdcPath, "-i", buildPath(shadersDir, shader ~ ".glsl"),
248             "-o", buildPath(shadersDir, shader ~ ".d"), "-l", slang, "-f",
249             "sokol_d"
250         ], "Shader compilation failed for " ~ shader, true);
251     }
252 }
253 
254 // Download and extract utility
255 void downloadAndExtract(string name, string vendor, string dir, string url, void delegate(string) @safe postExtract = null) @safe
256 {
257     writeln("Setting up ", name);
258     string path = absolutePath(buildPath(vendor, dir));
259     string file = dir ~ ".zip";
260     scope (exit)
261         if (exists(file))
262             remove(file);
263 
264     if (!exists(path))
265     {
266         download(url, file);
267         extractZip(file, path);
268     }
269     if (postExtract)
270         postExtract(path);
271 }
272 
273 // Core build structures
274 enum SokolBackend
275 {
276     _auto,
277     d3d11,
278     metal,
279     glcore,
280     gles3,
281     wgpu
282 }
283 
284 struct LibSokolOptions
285 {
286     string target, optimize, toolchain, vendor, sokolSrcPath;
287     SokolBackend backend;
288     bool use_egl, use_x11 = true, use_wayland, with_sokol_imgui, with_sokol_nuklear, linkageStatic, verbose;
289 }
290 
291 struct EmLinkOptions
292 {
293     string target, optimize, lib_main, vendor, shell_file_path;
294     SokolBackend backend;
295     bool release_use_closure = true, release_use_lto, use_emmalloc, use_filesystem, use_imgui, use_nuklear, verbose;
296     string[] extra_args;
297 }
298 
299 struct EmRunOptions
300 {
301     string name, vendor;
302     bool verbose;
303 }
304 
305 struct EmbuilderOptions
306 {
307     string port_name, vendor;
308 }
309 
310 // Build Sokol, ImGui, and Nuklear libraries
311 void buildLibSokol(LibSokolOptions opts) @safe
312 {
313     immutable buildDir = absolutePath("build");
314     mkdirRecurse(buildDir);
315 
316     // Compiler setup
317     string compiler = opts.toolchain ? opts.toolchain : defaultCompiler(opts.target);
318     string[] cflags = [
319         "-DNDEBUG", "-DIMPL",
320         format("-DSOKOL_%s", resolveSokolBackend(opts.backend, opts.target).to!string.toUpper)
321     ];
322     string[] lflags;
323 
324     // Platform-specific flags
325     switch (opts.target)
326     {
327     case "darwin":
328         cflags ~= [
329             "-ObjC", "-Wall", "-Wextra", "-Wno-unused-function",
330             "-Wno-return-type-c-linkage"
331         ];
332         lflags ~= [
333             "-framework", "Cocoa", "-framework", "QuartzCore", "-framework",
334             "Foundation",
335             "-framework", "MetalKit", "-framework", "Metal", "-framework",
336             "AudioToolbox"
337         ];
338         break;
339     case "linux":
340         cflags ~= ["-Wall", "-Wextra", "-Wno-unused-function"];
341         if (opts.use_egl)
342             cflags ~= "-DSOKOL_FORCE_EGL";
343         if (!opts.use_x11)
344             cflags ~= "-DSOKOL_DISABLE_X11";
345         if (!opts.use_wayland)
346             cflags ~= "-DSOKOL_DISABLE_WAYLAND";
347         lflags ~= opts.use_wayland ? [
348             "-lwayland-client", "-lwayland-egl", "-lwayland-cursor", "-lxkbcommon"
349         ] : [];
350         lflags ~= ["-lX11", "-lGL", "-lXi", "-lXcursor", "-lasound"];
351         break;
352     case "windows":
353         cflags ~= ["/DNDEBUG", "/DIMPL", "/wd4190", "/O2"];
354         lflags ~= ["dxgi.lib", "d3d11.lib"];
355         break;
356     case "wasm":
357         cflags ~= ["-fPIE"];
358         if (opts.backend == SokolBackend.wgpu)
359         {
360             //dfmt off
361             EmbuilderOptions embopts = {
362                 port_name: "emdawnwebgpu",
363                 vendor: opts.vendor,
364             };
365             //dfmt on
366             embuilderStep(embopts);
367             cflags ~= format("-I%s", buildPath(opts.vendor, "emsdk", "upstream", "emscripten", "cache", "ports", "emdawnwebgpu", "emdawnwebgpu_pkg", "webgpu", "include"));
368         }
369         compiler = buildPath(opts.vendor, "emsdk", "upstream", "emscripten", "emcc") ~ (isWindows ? ".bat"
370                 : "");
371         break;
372     default:
373         break;
374     }
375 
376     // Optimization and dynamic library flags
377     cflags ~= opts.optimize == "debug" && !opts.target.canFind("windows") ? "-O0" : "-O2";
378     if (!opts.linkageStatic && !opts.target.canFind("wasm"))
379         cflags ~= "-fPIC";
380 
381     // Add Nuklear include path if enabled
382     if (opts.with_sokol_nuklear)
383     {
384         immutable nuklearRoot = absolutePath(buildPath(opts.vendor, "nuklear"));
385         cflags ~= format("-I%s", nuklearRoot);
386     }
387 
388     // Compile Sokol sources
389     immutable sokolSources = [
390         "sokol_log.c", "sokol_app.c", "sokol_gfx.c", "sokol_time.c",
391         "sokol_audio.c", "sokol_gl.c", "sokol_debugtext.c", "sokol_shape.c",
392         "sokol_glue.c", "sokol_fetch.c", "sokol_memtrack.c", "sokol_args.c",
393     ];
394     auto sokolObjs = compileSources(sokolSources, buildDir, opts.sokolSrcPath, compiler, cflags, "sokol_", opts
395             .verbose);
396 
397     // Create Sokol library
398     immutable sokolLib = buildPath(buildDir, opts.linkageStatic ? "libsokol.a" : (opts.target.canFind("darwin") ? "libsokol.dylib" : opts
399             .target.canFind("windows") ? "sokol.dll" : "libsokol.so"));
400     linkLibrary(sokolLib, sokolObjs, opts.target, opts.linkageStatic, opts.vendor, lflags, opts
401             .verbose);
402     sokolObjs.each!(obj => exists(obj) && remove(obj));
403 
404     // Handle ImGui
405     if (opts.with_sokol_imgui)
406     {
407         immutable imguiRoot = absolutePath(buildPath(opts.vendor, "imgui", "src"));
408         enforce(exists(imguiRoot), "ImGui source not found. Use --download-imgui.");
409 
410         immutable imguiSources = [
411             "cimgui.cpp", "imgui.cpp", "imgui_demo.cpp", "imgui_draw.cpp",
412             "imgui_tables.cpp", "imgui_widgets.cpp", "cimgui_internal.cpp"
413         ];
414         cflags ~= format("-I%s", imguiRoot);
415 
416         string imguiCompiler = opts.target.canFind("wasm") ? buildPath(opts.vendor, "emsdk", "upstream", "emscripten", "em++") ~ (
417             isWindows ? ".bat" : "") : compiler.canFind("clang") ? findProgram(compiler ~ "++") : compiler.canFind(
418             "gcc") ? findProgram("g++") : compiler;
419 
420         // Compile ImGui sources
421         auto imguiObjs = compileSources(imguiSources, buildDir, imguiRoot, imguiCompiler, cflags ~ "-DNDEBUG", "imgui_", opts
422                 .verbose);
423 
424         // Compile sokol_imgui.c
425         immutable sokolImguiPath = buildPath(opts.sokolSrcPath, "sokol_imgui.c");
426         enforce(exists(sokolImguiPath), "sokol_imgui.c not found");
427         immutable sokolImguiObj = buildPath(buildDir, "sokol_imgui.o");
428         compileSource(sokolImguiPath, sokolImguiObj, compiler, cflags, opts.verbose);
429         imguiObjs ~= sokolImguiObj;
430         // Compile sokol_gfx_imgui.c
431         immutable sokolGfxImguiPath = buildPath(opts.sokolSrcPath, "sokol_gfx_imgui.c");
432         enforce(exists(sokolGfxImguiPath), "sokol_gfx_imgui.c not found");
433         immutable sokolGfxImguiObj = buildPath(buildDir, "sokol_gfx_imgui.o");
434         compileSource(sokolGfxImguiPath, sokolGfxImguiObj, compiler, cflags, opts.verbose);
435         imguiObjs ~= sokolGfxImguiObj;
436 
437         // Create ImGui library
438         immutable imguiLib = buildPath(buildDir, opts.linkageStatic ? "libcimgui.a" : (opts.target.canFind("darwin") ? "libcimgui.dylib" : opts
439                 .target.canFind("windows") ? "cimgui.dll" : "libcimgui.so"));
440         linkLibrary(imguiLib, imguiObjs, opts.target, opts.linkageStatic, opts.vendor, lflags, opts
441                 .verbose);
442         imguiObjs.each!(obj => exists(obj) && remove(obj));
443     }
444 
445     // Handle Nuklear
446     if (opts.with_sokol_nuklear)
447     {
448         immutable nuklearRoot = absolutePath(buildPath(opts.vendor, "nuklear"));
449         enforce(exists(nuklearRoot), "Nuklear source not found. Ensure it is downloaded.");
450 
451         // Define Nuklear sources
452         string[] nuklearObjs;
453 
454         // Compile sokol_nuklear.c
455         immutable sokolNuklearPath = buildPath(opts.sokolSrcPath, "sokol_nuklear.c");
456         enforce(exists(sokolNuklearPath), "sokol_nuklear.c not found");
457         immutable sokolNuklearObj = buildPath(buildDir, "sokol_nuklear.o");
458         compileSource(sokolNuklearPath, sokolNuklearObj, compiler, cflags, opts
459                 .verbose);
460         nuklearObjs ~= sokolNuklearObj;
461 
462         // Compile nuklearc.c
463         immutable nuklearcPath = absolutePath(buildPath("src", "nuklear", "c", "nuklearc.c"));
464         enforce(exists(nuklearcPath), "nuklearc.c not found in src/nuklear/c");
465         immutable nuklearcObj = buildPath(buildDir, "nuklearc.o");
466         compileSource(nuklearcPath, nuklearcObj, compiler, cflags, opts
467                 .verbose);
468         nuklearObjs ~= nuklearcObj;
469 
470         // Create Nuklear library
471         immutable nuklearLib = buildPath(buildDir, opts.linkageStatic ? "libnuklear.a" : (opts.target.canFind("darwin") ? "libnuklear.dylib" : opts
472                 .target.canFind("windows") ? "nuklear.dll" : "libnuklear.so"));
473         linkLibrary(nuklearLib, nuklearObjs, opts.target, opts.linkageStatic, opts.vendor, lflags, opts
474                 .verbose);
475         nuklearObjs.each!(obj => exists(obj) && remove(obj));
476     }
477 }
478 
479 // Compile a single source file
480 void compileSource(string srcPath, string objPath, string compiler, string[] cflags, bool verbose) @safe
481 {
482     enforce(exists(srcPath), format("Source file %s does not exist", srcPath));
483     string[] cmd = [compiler] ~ cflags ~ ["-c", "-o", objPath, srcPath];
484     if (verbose)
485         writeln("Executing: ", cmd.join(" "));
486     auto result = executeShell(cmd.join(" "));
487     if (verbose && result.output.length)
488         writeln("Output:\n", result.output);
489     enforce(result.status == 0, format("Failed to compile %s: %s", srcPath, result.output));
490 }
491 
492 // Compile multiple sources
493 string[] compileSources(const(string[]) sources, string buildDir, string srcRoot, string compiler, string[] cflags, string prefix, bool verbose) @safe
494 {
495     string[] objFiles;
496     foreach (src; sources)
497     {
498         immutable srcPath = buildPath(srcRoot, src);
499         immutable objPath = buildPath(buildDir, prefix ~ src.baseName ~ ".o");
500         compileSource(srcPath, objPath, compiler, cflags, verbose);
501         objFiles ~= objPath;
502     }
503     return objFiles;
504 }
505 
506 // Link objects into a static or dynamic library
507 void linkLibrary(string libPath, string[] objFiles, string target, bool linkageStatic, string vendor, string[] lflags, bool verbose) @safe
508 {
509     string arCmd = target.canFind("wasm") ? buildPath(vendor, "emsdk", "upstream", "emscripten", "emar") ~ (
510         isWindows ? ".bat" : "") : isWindows ? "lib.exe" : "ar";
511     string[] cmd;
512 
513     if (!linkageStatic && !target.canFind("wasm"))
514     {
515         if (target.canFind("darwin"))
516         {
517             string linker = findProgram("clang");
518             cmd = [linker, "-dynamiclib", "-o", libPath] ~ objFiles ~ lflags;
519         }
520         else if (target.canFind("windows"))
521         {
522             string linker = findProgram("cl");
523             cmd = [linker, "/LD", format("/Fe:%s", libPath)] ~ objFiles ~ lflags;
524         }
525         else // Linux
526         {
527             string linker = findProgram("gcc");
528             cmd = [linker, "-shared", "-o", libPath] ~ objFiles ~ lflags;
529         }
530     }
531     else if (isWindows && !target.canFind("wasm"))
532     {
533         cmd = [arCmd, "/nologo", format("/OUT:%s", libPath)] ~ objFiles;
534     }
535     else
536     {
537         cmd = [arCmd, "rcs", libPath] ~ objFiles;
538     }
539 
540     if (verbose)
541         writeln("Executing: ", cmd.join(" "));
542     auto result = executeShell(cmd.join(" "));
543     if (verbose && result.output.length)
544         writeln("Output:\n", result.output);
545     enforce(result.status == 0, format("Failed to create %s: %s", libPath, result.output));
546 }
547 
548 // Link WASM executable
549 void emLinkStep(EmLinkOptions opts) @safe
550 {
551     string emcc = buildPath(opts.vendor, "emsdk", "upstream", "emscripten", opts.use_imgui ? "em++"
552             : "emcc") ~ (
553         isWindows ? ".bat" : "");
554     string[] cmd = [emcc];
555 
556     if (opts.use_imgui)
557         cmd ~= "-lcimgui";
558     if (opts.use_nuklear)
559         cmd ~= "-lnuklear";
560     if (opts.optimize == "debug")
561         cmd ~= ["-gsource-map", "-sSAFE_HEAP=1", "-sSTACK_OVERFLOW_CHECK=1"];
562     else
563     {
564         cmd ~= "-sASSERTIONS=0";
565         cmd ~= opts.optimize == "small" ? "-Oz" : "-O3";
566         if (opts.release_use_lto)
567             cmd ~= "-flto";
568         if (opts.release_use_closure)
569             cmd ~= ["--closure", "1"];
570     }
571 
572     if (opts.backend == SokolBackend.wgpu)
573         cmd ~= "--use-port=emdawnwebgpu";
574     if (opts.backend == SokolBackend.gles3)
575         cmd ~= "-sUSE_WEBGL2=1";
576     if (!opts.use_filesystem)
577         cmd ~= "-sNO_FILESYSTEM=1";
578     if (opts.use_emmalloc)
579         cmd ~= "-sMALLOC='emmalloc'";
580     if (opts.shell_file_path)
581         cmd ~= "--shell-file=" ~ opts.shell_file_path;
582 
583     cmd ~= ["-sSTACK_SIZE=512KB"] ~ opts.extra_args ~ opts.lib_main;
584     immutable baseName = opts.lib_main.baseName[3 .. $ - 2]; // Strip "lib" and ".a"
585     string outFile = buildPath("build", baseName ~ ".html");
586     cmd ~= ["-o", outFile];
587 
588     if (opts.verbose)
589         writeln("Executing: ", cmd.join(" "));
590     auto result = executeShell(cmd.join(" "));
591     if (opts.verbose && result.output.length)
592         writeln("Output:\n", result.output);
593     enforce(result.status == 0, format("emcc failed: %s: %s", outFile, result.output));
594 
595     string webDir = "web";
596     mkdirRecurse(webDir);
597     foreach (ext; [".html", ".wasm", ".js"])
598         copy(buildPath("build", baseName ~ ext), buildPath(webDir, baseName ~ ext));
599     rmdirRecurse(buildPath("build"));
600 }
601 
602 // Run WASM executable
603 void emRunStep(EmRunOptions opts) @safe
604 {
605     string emrun = buildPath(opts.vendor, "emsdk", "upstream", "emscripten", "emrun") ~ (
606         isWindows ? ".bat" : "");
607     executeOrFail([emrun, buildPath("web", opts.name ~ ".html")], "emrun failed", opts.verbose);
608 }
609 
610 // Setup Emscripten SDK
611 void emSdkSetupStep(string emsdk) @safe
612 {
613     if (!exists(buildPath(emsdk, ".emscripten")))
614     {
615         immutable cmd = buildPath(emsdk, "emsdk") ~ (isWindows ? ".bat" : "");
616         executeOrFail([!isWindows ? "bash " ~ cmd: cmd, "install", "latest"], "emsdk install failed", true);
617         executeOrFail([!isWindows ? "bash " ~ cmd: cmd, "activate", "latest"], "emsdk activate failed", true);
618     }
619 }
620 
621 void embuilderStep(EmbuilderOptions opts) @safe
622 {
623     string embuilder = buildPath(opts.vendor, "emsdk", "upstream", "emscripten", "embuilder") ~ (
624         isWindows ? ".bat" : "");
625     string[] bFlags = ["build", opts.port_name];
626     executeOrFail(embuilder ~ bFlags, "embuilder failed to build " ~ opts.port_name, true);
627 }
628 
629 // Utility functions
630 string findProgram(string programName) @safe
631 {
632     foreach (path; environment.get("PATH").split(pathSeparator))
633     {
634         string fullPath = buildPath(path, programName);
635         version (Windows)
636             fullPath ~= ".exe";
637         if (exists(fullPath) && isFile(fullPath))
638             return fullPath;
639     }
640     throw new Exception(format("Program '%s' not found in PATH", programName));
641 }
642 
643 string defaultCompiler(string target) @safe
644 {
645     if (target.canFind("wasm"))
646         return "";
647     version (linux)
648         return findProgram("gcc");
649     version (Windows)
650         return findProgram("cl");
651     version (OSX)
652         return findProgram("clang");
653     version (Android)
654         return findProgram("clang");
655     throw new Exception("Unsupported platform");
656 }
657 
658 SokolBackend resolveSokolBackend(SokolBackend backend, string target) @safe
659 {
660     if (target.canFind("linux"))
661         return SokolBackend.glcore;
662     if (target.canFind("darwin"))
663         return SokolBackend.metal;
664     if (target.canFind("windows"))
665         return SokolBackend.d3d11;
666     if (target.canFind("wasm"))
667         return backend == SokolBackend.wgpu ? backend : SokolBackend.gles3;
668     if (target.canFind("android"))
669         return SokolBackend.gles3;
670     version (linux)
671         return SokolBackend.glcore;
672     version (Windows)
673         return SokolBackend.d3d11;
674     version (OSX)
675         return SokolBackend.metal;
676     return backend;
677 }
678 
679 void executeOrFail(string[] cmd, string errorMsg, bool verbose) @safe
680 {
681     if (verbose)
682         writeln("Executing: ", cmd.join(" "));
683     auto result = executeShell(cmd.join(" "));
684     if (verbose && result.output.length)
685         writeln("Output:\n", result.output);
686     enforce(result.status == 0, format("%s: %s", errorMsg, result.output));
687 }
688 
689 bool isWindows() @safe
690 {
691     version (Windows)
692         return true;
693     return false;
694 }
695 
696 // Download and extract functions
697 void download(string url, string fileName) @trusted
698 {
699     auto buf = appender!(ubyte[])();
700     size_t contentLength;
701     auto http = HTTP(url);
702     http.onReceiveHeader((in k, in v) {
703         if (k == "content-length")
704             contentLength = to!size_t(v);
705     });
706 
707     int barWidth = 50;
708     http.onReceive((data) {
709         buf.put(data);
710         if (contentLength)
711         {
712             float progress = cast(float) buf.data.length / contentLength;
713             write("\r[", "=".replicate(cast(int)(barWidth * progress)), ">", " ".replicate(
714                 barWidth - cast(int)(barWidth * progress)), "] ",
715                 format("%d%%", cast(int)(progress * 100)));
716             stdout.flush();
717         }
718         return data.length;
719     });
720 
721     http.perform();
722     enforce(http.statusLine.code / 100 == 2 || http.statusLine.code == 302, format(
723             "HTTP request failed: %s", http.statusLine.code));
724     std.file.write(fileName, buf.data);
725     writeln();
726 }
727 
728 void extractZip(string zipFile, string destination) @trusted
729 {
730     ZipArchive archive = new ZipArchive(read(zipFile));
731     string prefix = archive.directory.keys.front[0 .. $ - archive.directory.keys.front.find("/")
732             .length + 1];
733 
734     if (exists(destination))
735         rmdirRecurse(destination);
736     mkdirRecurse(destination);
737 
738     foreach (name, am; archive.directory)
739     {
740         if (!am.expandedSize)
741             continue;
742         string path = buildPath(destination, chompPrefix(name, prefix));
743         mkdirRecurse(dirName(path));
744         std.file.write(path, archive.expand(am));
745     }
746 }
747 
748 string getSHDC(string vendor) @safe
749 {
750     string path = absolutePath(buildPath(vendor, "shdc"));
751     string file = "shdc.zip";
752     scope (exit)
753         if (exists(file))
754             remove(file);
755 
756     if (!exists(path))
757     {
758         download("https://github.com/floooh/sokol-tools-bin/archive/refs/heads/master.zip", file);
759         extractZip(file, path);
760     }
761 
762     version (Windows)
763         immutable shdc = buildPath("bin", "win32", "sokol-shdc.exe");
764     else version (linux)
765         immutable shdc = buildPath("bin", isAArch64 ? "linux_arm64" : "linux", "sokol-shdc");
766     else version (OSX)
767         immutable shdc = buildPath("bin", isAArch64 ? "osx_arm64" : "osx", "sokol-shdc");
768     else
769         throw new Exception("Unsupported platform for sokol-tools");
770 
771     return buildPath(path, shdc);
772 }
773 
774 bool isAArch64() @safe
775 {
776     version (AArch64)
777         return true;
778     return false;
779 }
780 
781 string defaultTarget() @safe
782 {
783     version (linux)
784         return "linux";
785     version (Windows)
786         return "windows";
787     version (OSX)
788         return "darwin";
789     version (Android)
790         return "android";
791     version (Emscripten)
792         return "wasm";
793     throw new Exception("Unsupported platform");
794 }