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 = "3b98e0a57fc17cc72fdda6934bd932426778a16e"; 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", "mrt", 227 "noninterleaved", "offscreen", "quad", "shapes", "texcube", "blend" 228 ]; 229 230 version (OSX) 231 enum glsl = "glsl410"; 232 else 233 enum glsl = "glsl430"; 234 immutable slang = glsl ~ ":metal_macos:hlsl5:glsl300es:wgsl"; 235 236 version (Posix) 237 executeOrFail(["chmod", "+x", shdcPath], "Failed to set shader permissions", true); 238 239 foreach (shader; shaders) 240 executeOrFail([ 241 shdcPath, "-i", buildPath(shadersDir, shader ~ ".glsl"), 242 "-o", buildPath(shadersDir, shader ~ ".d"), "-l", slang, "-f", "sokol_d" 243 ], "Shader compilation failed for " ~ shader, true); 244 } 245 246 // Download and extract utility 247 void downloadAndExtract(string name, string vendor, string dir, string url, void delegate(string) @safe postExtract = null) @safe 248 { 249 writeln("Setting up ", name); 250 string path = absolutePath(buildPath(vendor, dir)); 251 string file = dir ~ ".zip"; 252 scope (exit) 253 if (exists(file)) 254 remove(file); 255 256 if (!exists(path)) 257 { 258 download(url, file); 259 extractZip(file, path); 260 } 261 if (postExtract) 262 postExtract(path); 263 } 264 265 // Core build structures 266 enum SokolBackend 267 { 268 _auto, 269 d3d11, 270 metal, 271 glcore, 272 gles3, 273 wgpu 274 } 275 276 struct LibSokolOptions 277 { 278 string target, optimize, toolchain, vendor, sokolSrcPath; 279 SokolBackend backend; 280 bool use_egl, use_x11 = true, use_wayland, with_sokol_imgui, with_sokol_nuklear, linkageStatic, verbose; 281 } 282 283 struct EmLinkOptions 284 { 285 string target, optimize, lib_main, vendor, shell_file_path; 286 SokolBackend backend; 287 bool release_use_closure = true, release_use_lto, use_emmalloc, use_filesystem, use_imgui, use_nuklear, verbose; 288 string[] extra_args; 289 } 290 291 struct EmRunOptions 292 { 293 string name, vendor; 294 bool verbose; 295 } 296 297 struct EmbuilderOptions 298 { 299 string port_name, vendor; 300 } 301 302 // Build Sokol, ImGui, and Nuklear libraries 303 void buildLibSokol(LibSokolOptions opts) @safe 304 { 305 immutable buildDir = absolutePath("build"); 306 mkdirRecurse(buildDir); 307 308 // Compiler setup 309 string compiler = opts.toolchain ? opts.toolchain : defaultCompiler(opts.target); 310 string[] cflags = [ 311 "-DNDEBUG", "-DIMPL", 312 format("-DSOKOL_%s", resolveSokolBackend(opts.backend, opts.target).to!string.toUpper) 313 ]; 314 string[] lflags; 315 316 // Platform-specific flags 317 switch (opts.target) 318 { 319 case "darwin": 320 cflags ~= [ 321 "-ObjC", "-Wall", "-Wextra", "-Wno-unused-function", 322 "-Wno-return-type-c-linkage" 323 ]; 324 lflags ~= [ 325 "-framework", "Cocoa", "-framework", "QuartzCore", "-framework", 326 "Foundation", 327 "-framework", "MetalKit", "-framework", "Metal", "-framework", 328 "AudioToolbox" 329 ]; 330 break; 331 case "linux": 332 cflags ~= ["-Wall", "-Wextra", "-Wno-unused-function"]; 333 if (opts.use_egl) 334 cflags ~= "-DSOKOL_FORCE_EGL"; 335 if (!opts.use_x11) 336 cflags ~= "-DSOKOL_DISABLE_X11"; 337 if (!opts.use_wayland) 338 cflags ~= "-DSOKOL_DISABLE_WAYLAND"; 339 lflags ~= opts.use_wayland ? [ 340 "-lwayland-client", "-lwayland-egl", "-lwayland-cursor", "-lxkbcommon" 341 ] : []; 342 lflags ~= ["-lX11", "-lGL", "-lXi", "-lXcursor", "-lasound"]; 343 break; 344 case "windows": 345 cflags ~= ["/DNDEBUG", "/DIMPL", "/wd4190", "/O2"]; 346 lflags ~= ["dxgi.lib", "d3d11.lib"]; 347 break; 348 case "wasm": 349 cflags ~= ["-fPIE"]; 350 if (opts.backend == SokolBackend.wgpu) 351 { 352 //dfmt off 353 EmbuilderOptions embopts = { 354 port_name: "emdawnwebgpu", 355 vendor: opts.vendor, 356 }; 357 //dfmt on 358 embuilderStep(embopts); 359 cflags ~= format("-I%s", buildPath(opts.vendor, "emsdk", "upstream", "emscripten", "cache", "ports", "emdawnwebgpu", "emdawnwebgpu_pkg", "webgpu", "include")); 360 } 361 compiler = buildPath(opts.vendor, "emsdk", "upstream", "emscripten", "emcc") ~ (isWindows ? ".bat" 362 : ""); 363 break; 364 default: 365 break; 366 } 367 368 // Optimization and dynamic library flags 369 cflags ~= opts.optimize == "debug" && !opts.target.canFind("windows") ? "-O0" : "-O2"; 370 if (!opts.linkageStatic && !opts.target.canFind("wasm")) 371 cflags ~= "-fPIC"; 372 373 // Add Nuklear include path if enabled 374 if (opts.with_sokol_nuklear) 375 { 376 immutable nuklearRoot = absolutePath(buildPath(opts.vendor, "nuklear")); 377 cflags ~= format("-I%s", nuklearRoot); 378 } 379 380 // Compile Sokol sources 381 immutable sokolSources = [ 382 "sokol_log.c", "sokol_app.c", "sokol_gfx.c", "sokol_time.c", 383 "sokol_audio.c", "sokol_gl.c", "sokol_debugtext.c", "sokol_shape.c", 384 "sokol_glue.c", "sokol_fetch.c", "sokol_memtrack.c", "sokol_args.c", 385 ]; 386 auto sokolObjs = compileSources(sokolSources, buildDir, opts.sokolSrcPath, compiler, cflags, "sokol_", opts 387 .verbose); 388 389 // Create Sokol library 390 immutable sokolLib = buildPath(buildDir, opts.linkageStatic ? "libsokol.a" : (opts.target.canFind("darwin") ? "libsokol.dylib" : opts 391 .target.canFind("windows") ? "sokol.dll" : "libsokol.so")); 392 linkLibrary(sokolLib, sokolObjs, opts.target, opts.linkageStatic, opts.vendor, lflags, opts 393 .verbose); 394 sokolObjs.each!(obj => exists(obj) && remove(obj)); 395 396 // Handle ImGui 397 if (opts.with_sokol_imgui) 398 { 399 immutable imguiRoot = absolutePath(buildPath(opts.vendor, "imgui", "src")); 400 enforce(exists(imguiRoot), "ImGui source not found. Use --download-imgui."); 401 402 immutable imguiSources = [ 403 "cimgui.cpp", "imgui.cpp", "imgui_demo.cpp", "imgui_draw.cpp", 404 "imgui_tables.cpp", "imgui_widgets.cpp", "cimgui_internal.cpp" 405 ]; 406 cflags ~= format("-I%s", imguiRoot); 407 408 string imguiCompiler = opts.target.canFind("wasm") ? buildPath(opts.vendor, "emsdk", "upstream", "emscripten", "em++") ~ ( 409 isWindows ? ".bat" : "") : compiler.canFind("clang") ? findProgram(compiler ~ "++") : compiler.canFind( 410 "gcc") ? findProgram("g++") : compiler; 411 412 // Compile ImGui sources 413 auto imguiObjs = compileSources(imguiSources, buildDir, imguiRoot, imguiCompiler, cflags ~ "-DNDEBUG", "imgui_", opts 414 .verbose); 415 416 // Compile sokol_imgui.c 417 immutable sokolImguiPath = buildPath(opts.sokolSrcPath, "sokol_imgui.c"); 418 enforce(exists(sokolImguiPath), "sokol_imgui.c not found"); 419 immutable sokolImguiObj = buildPath(buildDir, "sokol_imgui.o"); 420 compileSource(sokolImguiPath, sokolImguiObj, compiler, cflags, opts.verbose); 421 imguiObjs ~= sokolImguiObj; 422 423 // Create ImGui library 424 immutable imguiLib = buildPath(buildDir, opts.linkageStatic ? "libcimgui.a" : (opts.target.canFind("darwin") ? "libcimgui.dylib" : opts 425 .target.canFind("windows") ? "cimgui.dll" : "libcimgui.so")); 426 linkLibrary(imguiLib, imguiObjs, opts.target, opts.linkageStatic, opts.vendor, lflags, opts 427 .verbose); 428 imguiObjs.each!(obj => exists(obj) && remove(obj)); 429 } 430 431 // Handle Nuklear 432 if (opts.with_sokol_nuklear) 433 { 434 immutable nuklearRoot = absolutePath(buildPath(opts.vendor, "nuklear")); 435 enforce(exists(nuklearRoot), "Nuklear source not found. Ensure it is downloaded."); 436 437 // Define Nuklear sources 438 string[] nuklearObjs; 439 440 // Compile sokol_nuklear.c 441 immutable sokolNuklearPath = buildPath(opts.sokolSrcPath, "sokol_nuklear.c"); 442 enforce(exists(sokolNuklearPath), "sokol_nuklear.c not found"); 443 immutable sokolNuklearObj = buildPath(buildDir, "sokol_nuklear.o"); 444 compileSource(sokolNuklearPath, sokolNuklearObj, compiler, cflags, opts 445 .verbose); 446 nuklearObjs ~= sokolNuklearObj; 447 448 // Compile nuklearc.c 449 immutable nuklearcPath = absolutePath(buildPath("src", "nuklear", "c", "nuklearc.c")); 450 enforce(exists(nuklearcPath), "nuklearc.c not found in src/nuklear/c"); 451 immutable nuklearcObj = buildPath(buildDir, "nuklearc.o"); 452 compileSource(nuklearcPath, nuklearcObj, compiler, cflags, opts 453 .verbose); 454 nuklearObjs ~= nuklearcObj; 455 456 // Create Nuklear library 457 immutable nuklearLib = buildPath(buildDir, opts.linkageStatic ? "libnuklear.a" : (opts.target.canFind("darwin") ? "libnuklear.dylib" : opts 458 .target.canFind("windows") ? "nuklear.dll" : "libnuklear.so")); 459 linkLibrary(nuklearLib, nuklearObjs, opts.target, opts.linkageStatic, opts.vendor, lflags, opts 460 .verbose); 461 nuklearObjs.each!(obj => exists(obj) && remove(obj)); 462 } 463 } 464 465 // Compile a single source file 466 void compileSource(string srcPath, string objPath, string compiler, string[] cflags, bool verbose) @safe 467 { 468 enforce(exists(srcPath), format("Source file %s does not exist", srcPath)); 469 string[] cmd = [compiler] ~ cflags ~ ["-c", "-o", objPath, srcPath]; 470 if (verbose) 471 writeln("Executing: ", cmd.join(" ")); 472 auto result = executeShell(cmd.join(" ")); 473 if (verbose && result.output.length) 474 writeln("Output:\n", result.output); 475 enforce(result.status == 0, format("Failed to compile %s: %s", srcPath, result.output)); 476 } 477 478 // Compile multiple sources 479 string[] compileSources(const(string[]) sources, string buildDir, string srcRoot, string compiler, string[] cflags, string prefix, bool verbose) @safe 480 { 481 string[] objFiles; 482 foreach (src; sources) 483 { 484 immutable srcPath = buildPath(srcRoot, src); 485 immutable objPath = buildPath(buildDir, prefix ~ src.baseName ~ ".o"); 486 compileSource(srcPath, objPath, compiler, cflags, verbose); 487 objFiles ~= objPath; 488 } 489 return objFiles; 490 } 491 492 // Link objects into a static or dynamic library 493 void linkLibrary(string libPath, string[] objFiles, string target, bool linkageStatic, string vendor, string[] lflags, bool verbose) @safe 494 { 495 string arCmd = target.canFind("wasm") ? buildPath(vendor, "emsdk", "upstream", "emscripten", "emar") ~ ( 496 isWindows ? ".bat" : "") : isWindows ? "lib.exe" : "ar"; 497 string[] cmd; 498 499 if (!linkageStatic && !target.canFind("wasm")) 500 { 501 if (target.canFind("darwin")) 502 { 503 string linker = findProgram("clang"); 504 cmd = [linker, "-dynamiclib", "-o", libPath] ~ objFiles ~ lflags; 505 } 506 else if (target.canFind("windows")) 507 { 508 string linker = findProgram("cl"); 509 cmd = [linker, "/LD", format("/Fe:%s", libPath)] ~ objFiles ~ lflags; 510 } 511 else // Linux 512 { 513 string linker = findProgram("gcc"); 514 cmd = [linker, "-shared", "-o", libPath] ~ objFiles ~ lflags; 515 } 516 } 517 else if (isWindows && !target.canFind("wasm")) 518 { 519 cmd = [arCmd, "/nologo", format("/OUT:%s", libPath)] ~ objFiles; 520 } 521 else 522 { 523 cmd = [arCmd, "rcs", libPath] ~ objFiles; 524 } 525 526 if (verbose) 527 writeln("Executing: ", cmd.join(" ")); 528 auto result = executeShell(cmd.join(" ")); 529 if (verbose && result.output.length) 530 writeln("Output:\n", result.output); 531 enforce(result.status == 0, format("Failed to create %s: %s", libPath, result.output)); 532 } 533 534 // Link WASM executable 535 void emLinkStep(EmLinkOptions opts) @safe 536 { 537 string emcc = buildPath(opts.vendor, "emsdk", "upstream", "emscripten", opts.use_imgui ? "em++" 538 : "emcc") ~ ( 539 isWindows ? ".bat" : ""); 540 string[] cmd = [emcc]; 541 542 if (opts.use_imgui) 543 cmd ~= "-lcimgui"; 544 if (opts.use_nuklear) 545 cmd ~= "-lnuklear"; 546 if (opts.optimize == "debug") 547 cmd ~= ["-gsource-map", "-sSAFE_HEAP=1", "-sSTACK_OVERFLOW_CHECK=1"]; 548 else 549 { 550 cmd ~= "-sASSERTIONS=0"; 551 cmd ~= opts.optimize == "small" ? "-Oz" : "-O3"; 552 if (opts.release_use_lto) 553 cmd ~= "-flto"; 554 if (opts.release_use_closure) 555 cmd ~= ["--closure", "1"]; 556 } 557 558 if (opts.backend == SokolBackend.wgpu) 559 cmd ~= "--use-port=emdawnwebgpu"; 560 if (opts.backend == SokolBackend.gles3) 561 cmd ~= "-sUSE_WEBGL2=1"; 562 if (!opts.use_filesystem) 563 cmd ~= "-sNO_FILESYSTEM=1"; 564 if (opts.use_emmalloc) 565 cmd ~= "-sMALLOC='emmalloc'"; 566 if (opts.shell_file_path) 567 cmd ~= "--shell-file=" ~ opts.shell_file_path; 568 569 cmd ~= ["-sSTACK_SIZE=512KB"] ~ opts.extra_args ~ opts.lib_main; 570 immutable baseName = opts.lib_main.baseName[3 .. $ - 2]; // Strip "lib" and ".a" 571 string outFile = buildPath("build", baseName ~ ".html"); 572 cmd ~= ["-o", outFile]; 573 574 if (opts.verbose) 575 writeln("Executing: ", cmd.join(" ")); 576 auto result = executeShell(cmd.join(" ")); 577 if (opts.verbose && result.output.length) 578 writeln("Output:\n", result.output); 579 enforce(result.status == 0, format("emcc failed: %s: %s", outFile, result.output)); 580 581 string webDir = "web"; 582 mkdirRecurse(webDir); 583 foreach (ext; [".html", ".wasm", ".js"]) 584 copy(buildPath("build", baseName ~ ext), buildPath(webDir, baseName ~ ext)); 585 rmdirRecurse(buildPath("build")); 586 } 587 588 // Run WASM executable 589 void emRunStep(EmRunOptions opts) @safe 590 { 591 string emrun = buildPath(opts.vendor, "emsdk", "upstream", "emscripten", "emrun") ~ ( 592 isWindows ? ".bat" : ""); 593 executeOrFail([emrun, buildPath("web", opts.name ~ ".html")], "emrun failed", opts.verbose); 594 } 595 596 // Setup Emscripten SDK 597 void emSdkSetupStep(string emsdk) @safe 598 { 599 if (!exists(buildPath(emsdk, ".emscripten"))) 600 { 601 immutable cmd = buildPath(emsdk, "emsdk") ~ (isWindows ? ".bat" : ""); 602 executeOrFail([!isWindows ? "bash " ~ cmd: cmd, "install", "latest"], "emsdk install failed", true); 603 executeOrFail([!isWindows ? "bash " ~ cmd: cmd, "activate", "latest"], "emsdk activate failed", true); 604 } 605 } 606 607 void embuilderStep(EmbuilderOptions opts) @safe 608 { 609 string embuilder = buildPath(opts.vendor, "emsdk", "upstream", "emscripten", "embuilder") ~ ( 610 isWindows ? ".bat" : ""); 611 string[] bFlags = ["build", opts.port_name]; 612 executeOrFail(embuilder ~ bFlags, "embuilder failed to build " ~ opts.port_name, true); 613 } 614 615 // Utility functions 616 string findProgram(string programName) @safe 617 { 618 foreach (path; environment.get("PATH").split(pathSeparator)) 619 { 620 string fullPath = buildPath(path, programName); 621 version (Windows) 622 fullPath ~= ".exe"; 623 if (exists(fullPath) && isFile(fullPath)) 624 return fullPath; 625 } 626 throw new Exception(format("Program '%s' not found in PATH", programName)); 627 } 628 629 string defaultCompiler(string target) @safe 630 { 631 if (target.canFind("wasm")) 632 return ""; 633 version (linux) 634 return findProgram("gcc"); 635 version (Windows) 636 return findProgram("cl"); 637 version (OSX) 638 return findProgram("clang"); 639 version (Android) 640 return findProgram("clang"); 641 throw new Exception("Unsupported platform"); 642 } 643 644 SokolBackend resolveSokolBackend(SokolBackend backend, string target) @safe 645 { 646 if (target.canFind("linux")) 647 return SokolBackend.glcore; 648 if (target.canFind("darwin")) 649 return SokolBackend.metal; 650 if (target.canFind("windows")) 651 return SokolBackend.d3d11; 652 if (target.canFind("wasm")) 653 return backend == SokolBackend.wgpu ? backend : SokolBackend.gles3; 654 if (target.canFind("android")) 655 return SokolBackend.gles3; 656 version (linux) 657 return SokolBackend.glcore; 658 version (Windows) 659 return SokolBackend.d3d11; 660 version (OSX) 661 return SokolBackend.metal; 662 return backend; 663 } 664 665 void executeOrFail(string[] cmd, string errorMsg, bool verbose) @safe 666 { 667 if (verbose) 668 writeln("Executing: ", cmd.join(" ")); 669 auto result = executeShell(cmd.join(" ")); 670 if (verbose && result.output.length) 671 writeln("Output:\n", result.output); 672 enforce(result.status == 0, format("%s: %s", errorMsg, result.output)); 673 } 674 675 bool isWindows() @safe 676 { 677 version (Windows) 678 return true; 679 return false; 680 } 681 682 // Download and extract functions 683 void download(string url, string fileName) @trusted 684 { 685 auto buf = appender!(ubyte[])(); 686 size_t contentLength; 687 auto http = HTTP(url); 688 http.onReceiveHeader((in k, in v) { 689 if (k == "content-length") 690 contentLength = to!size_t(v); 691 }); 692 693 int barWidth = 50; 694 http.onReceive((data) { 695 buf.put(data); 696 if (contentLength) 697 { 698 float progress = cast(float) buf.data.length / contentLength; 699 write("\r[", "=".replicate(cast(int)(barWidth * progress)), ">", " ".replicate( 700 barWidth - cast(int)(barWidth * progress)), "] ", 701 format("%d%%", cast(int)(progress * 100))); 702 stdout.flush(); 703 } 704 return data.length; 705 }); 706 707 http.perform(); 708 enforce(http.statusLine.code / 100 == 2 || http.statusLine.code == 302, format( 709 "HTTP request failed: %s", http.statusLine.code)); 710 std.file.write(fileName, buf.data); 711 writeln(); 712 } 713 714 void extractZip(string zipFile, string destination) @trusted 715 { 716 ZipArchive archive = new ZipArchive(read(zipFile)); 717 string prefix = archive.directory.keys.front[0 .. $ - archive.directory.keys.front.find("/") 718 .length + 1]; 719 720 if (exists(destination)) 721 rmdirRecurse(destination); 722 mkdirRecurse(destination); 723 724 foreach (name, am; archive.directory) 725 { 726 if (!am.expandedSize) 727 continue; 728 string path = buildPath(destination, chompPrefix(name, prefix)); 729 mkdirRecurse(dirName(path)); 730 std.file.write(path, archive.expand(am)); 731 } 732 } 733 734 string getSHDC(string vendor) @safe 735 { 736 string path = absolutePath(buildPath(vendor, "shdc")); 737 string file = "shdc.zip"; 738 scope (exit) 739 if (exists(file)) 740 remove(file); 741 742 if (!exists(path)) 743 { 744 download("https://github.com/floooh/sokol-tools-bin/archive/refs/heads/master.zip", file); 745 extractZip(file, path); 746 } 747 748 version (Windows) 749 immutable shdc = buildPath("bin", "win32", "sokol-shdc.exe"); 750 else version (linux) 751 immutable shdc = buildPath("bin", isAArch64 ? "linux_arm64" : "linux", "sokol-shdc"); 752 else version (OSX) 753 immutable shdc = buildPath("bin", isAArch64 ? "osx_arm64" : "osx", "sokol-shdc"); 754 else 755 throw new Exception("Unsupported platform for sokol-tools"); 756 757 return buildPath(path, shdc); 758 } 759 760 bool isAArch64() @safe 761 { 762 version (AArch64) 763 return true; 764 return false; 765 } 766 767 string defaultTarget() @safe 768 { 769 version (linux) 770 return "linux"; 771 version (Windows) 772 return "windows"; 773 version (OSX) 774 return "darwin"; 775 version (Android) 776 return "android"; 777 version (Emscripten) 778 return "wasm"; 779 throw new Exception("Unsupported platform"); 780 }