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