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