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 }