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 = "5.0.5"; 16 enum imgui_version = "1.92.7"; 17 enum nuklear_version = "4.13.2"; 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", shellFile; 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 case "--enable-wayland": 66 useWayland = true; 67 break; 68 case "--disable-x11": 69 useX11 = false; 70 break; 71 case "--enable-egl": 72 useEgl = true; 73 break; 74 default: 75 if (arg.startsWith("--backend=")) 76 backend = arg[10 .. $].to!SokolBackend; 77 else if (arg.startsWith("--toolchain=")) 78 compiler = findProgram(arg[12 .. $]); 79 else if (arg.startsWith("--optimize=")) 80 optimize = arg[11 .. $]; 81 else if (arg.startsWith("--target=")) 82 target = arg[9 .. $]; 83 else if (arg.startsWith("--link=")) 84 linkExample = arg[7 .. $]; 85 else if (arg.startsWith("--run=")) 86 runExample = arg[6 .. $]; 87 else if (arg.startsWith("--linkage=")) 88 { 89 linkage = arg[10 .. $]; 90 if (!["static", "dynamic"].canFind(linkage)) 91 throw new Exception("Invalid linkage: use static or dynamic"); 92 } 93 else if (arg.startsWith("--shell-file=")) 94 shellFile = arg[13 .. $]; 95 else 96 throw new Exception("Unknown argument: " ~ arg); 97 break; 98 } 99 100 if (args.length < 2 || opts.help) 101 { 102 writeln("Usage: build [options]\nOptions:"); 103 writeln(" --help Show this help message"); 104 writeln(" --verbose Enable verbose output"); 105 writeln( 106 " --backend=<backend> Select backend (d3d11, metal, glcore, gles3, wgpu, vulkan)"); 107 writeln(" --toolchain=<compiler> Select C toolchain (e.g., gcc, clang, emcc)"); 108 writeln(" --optimize=<level> Select optimization level (debug, release, small)"); 109 writeln(" --target=<target> Select target (native, wasm, android)"); 110 writeln(" --linkage=<type> Library linkage (static or dynamic, default: static)"); 111 writeln(" --enable-wayland Enable Wayland support (Linux only)"); 112 writeln(" --disable-x11 Disable X11 support (Linux only)"); 113 writeln(" --enable-egl Force EGL (Linux only)"); 114 writeln(" --enable-wasm-lto Enable Emscripten LTO"); 115 writeln(" --download-emsdk Download Emscripten SDK"); 116 writeln(" --download-sokol-tools Download sokol-tools"); 117 writeln(" --link=<example> Link WASM example (e.g., triangle)"); 118 writeln(" --run=<example> Run WASM example (e.g., triangle)"); 119 writeln(" --with-sokol-imgui Enable sokol_imgui integration"); 120 writeln(" --with-sokol-nuklear Enable sokol_nuklear integration"); 121 return; 122 } 123 124 if (opts.backend == SokolBackend._auto) 125 opts.backend = resolveSokolBackend(opts.backend, opts.target); 126 127 opts.target = resolveTarget(opts.target); 128 129 if (!opts.linkExample && !opts.runExample) 130 { 131 if (opts.target.canFind("wasm")) 132 opts.downloadEmsdk = true; 133 writeln("Configuration:"); 134 writeln(" Target: ", opts.target, ", Optimize: ", opts.optimize, ", Backend: ", opts 135 .backend); 136 writeln(" Linkage: ", opts.linkage); 137 writeln(" Download: Emscripten=", opts.downloadEmsdk, ", ImGui=", opts.withSokolImgui, 138 ", Nuklear=", opts.withSokolNuklear, ", Sokol-tools=", opts.downloadShdc); 139 writeln(" Verbose: ", opts.verbose); 140 } 141 142 // Setup dependencies 143 if (opts.downloadEmsdk || opts.target.canFind("wasm")) 144 getEmSDK(vendorPath); 145 if (opts.withSokolImgui) 146 getIMGUI(vendorPath); 147 if (opts.withSokolNuklear) 148 getNuklear(vendorPath); 149 150 // Execute build steps 151 if (opts.downloadShdc) 152 { 153 buildShaders(vendorPath, opts.backend); 154 } 155 else if (opts.linkExample) 156 { 157 string shellFile = (opts.shellFile.empty) ? 158 absolutePath(buildPath(sokolRoot, "src", "sokol", "web", "shell.html")) : 159 absolutePath(opts.shellFile); 160 161 EmLinkOptions linkOpts = { 162 target: "wasm", 163 optimize: opts.optimize, 164 lib_main: buildPath("build", "lib" ~ opts.linkExample ~ ".a"), 165 vendor: vendorPath, 166 backend: opts.backend, 167 use_emmalloc: true, 168 release_use_lto: opts.useLTO, 169 use_imgui: opts.withSokolImgui, 170 use_nuklear: opts.withSokolNuklear, 171 use_filesystem: false, 172 shell_file_path: shellFile, 173 extra_args: [ 174 "-L" ~ absolutePath(buildPath(sokolRoot, "build")), "-lsokol" 175 ], 176 verbose: opts.verbose 177 }; 178 emLinkStep(linkOpts); 179 } 180 else if (opts.runExample) 181 { 182 emRunStep(EmRunOptions(opts.runExample, vendorPath, opts.verbose)); 183 } 184 else 185 { 186 LibSokolOptions libOpts = { 187 target: opts.target, 188 optimize: opts.optimize, 189 toolchain: opts.compiler, 190 vendor: vendorPath, 191 sokolSrcPath: sokolSrcPath, 192 backend: opts.backend, 193 use_x11: opts.useX11, 194 use_wayland: opts.useWayland, 195 use_egl: opts.useEgl, 196 with_sokol_imgui: opts.withSokolImgui, 197 with_sokol_nuklear: opts.withSokolNuklear, 198 linkageStatic: opts.target.canFind("wasm") ? true : opts.linkage == "static", 199 verbose: opts.verbose 200 }; 201 202 buildLibSokol(libOpts); 203 } 204 } 205 206 // Dependency management 207 void getEmSDK(string vendor) @safe 208 { 209 downloadAndExtract("Emscripten SDK", vendor, "emsdk", 210 format("https://github.com/emscripten-core/emsdk/archive/refs/tags/%s.zip", emsdk_version), 211 (path) => emSdkSetupStep(path)); 212 } 213 214 void getIMGUI(string vendor) @safe 215 { 216 string url; 217 enum commitHashRegex = ctRegex!`^[0-9a-fA-F]{7,40}$`; 218 if (matchFirst(imgui_version, commitHashRegex)) 219 url = format("https://github.com/floooh/dcimgui/archive/%s.zip", imgui_version); 220 else 221 url = format("https://github.com/floooh/dcimgui/archive/refs/tags/v%s.zip", imgui_version); 222 downloadAndExtract("ImGui", vendor, "imgui", url); 223 } 224 225 void getNuklear(string vendor) @safe 226 { 227 writeln("Setting up Nuklear"); 228 string path = absolutePath(buildPath(vendor, "nuklear")); 229 string file = "nuklear.h"; 230 231 if (!exists(path)) 232 { 233 mkdirRecurse(path); 234 download( 235 format("https://raw.githubusercontent.com/Immediate-Mode-UI/Nuklear/refs/tags/%s/nuklear.h", 236 nuklear_version), file); 237 std.file.write(buildPath(path, "nuklear.h"), read(file)); 238 } 239 } 240 241 void buildShaders(string vendor, ref SokolBackend opts) @safe 242 { 243 immutable shdcPath = getSHDC(vendor); 244 immutable shadersDir = "examples/shaders"; 245 immutable shaders = [ 246 "triangle", "bufferoffsets", "cube", "instancing", "instancingcompute", 247 "mrt", "noninterleaved", "offscreen", "quad", "shapes", "texcube", "blend", 248 "vertexpull" 249 ]; 250 251 version (OSX) 252 enum glsl = "glsl410"; 253 else 254 enum glsl = "glsl430"; 255 256 immutable slangTemplate = opts == SokolBackend.vulkan 257 ? glsl ~ ":metal_macos:hlsl5:%s:wgsl:spirv_vk" : glsl ~ ":metal_macos:hlsl5:%s:wgsl"; 258 259 version (Posix) 260 executeOrFail(["chmod", "+x", shdcPath], "Failed to set shader permissions", true); 261 262 foreach (shader; shaders) 263 { 264 immutable essl = (shader == "instancingcompute" || shader == "vertexpull") 265 ? "glsl310es" : "glsl300es"; 266 immutable slang = slangTemplate.format(essl); 267 executeOrFail([ 268 shdcPath, "-i", buildPath(shadersDir, shader ~ ".glsl"), 269 "-o", buildPath(shadersDir, shader ~ ".d"), "-l", slang, "-f", 270 "sokol_d" 271 ], "Shader compilation failed for " ~ shader, true); 272 } 273 } 274 275 // Download and extract utility 276 void downloadAndExtract(string name, string vendor, string dir, string url, 277 void delegate(string) @safe postExtract = null) @safe 278 { 279 writeln("Setting up ", name); 280 string path = absolutePath(buildPath(vendor, dir)); 281 string file = dir ~ ".zip"; 282 scope (exit) 283 if (exists(file)) 284 remove(file); 285 286 if (!exists(path)) 287 { 288 download(url, file); 289 extractZip(file, path); 290 } 291 if (postExtract) 292 postExtract(path); 293 } 294 295 // Core build structures 296 enum SokolBackend 297 { 298 _auto, 299 d3d11, 300 metal, 301 glcore, 302 gles3, 303 wgpu, 304 vulkan 305 } 306 307 struct LibSokolOptions 308 { 309 string target, optimize, toolchain, vendor, sokolSrcPath; 310 SokolBackend backend; 311 bool use_egl, use_x11 = true, use_wayland, with_sokol_imgui, with_sokol_nuklear, 312 linkageStatic, verbose; 313 } 314 315 struct EmLinkOptions 316 { 317 string target, optimize, lib_main, vendor, shell_file_path; 318 SokolBackend backend; 319 bool release_use_closure = true, release_use_lto, use_emmalloc, use_filesystem, 320 use_imgui, use_nuklear, verbose; 321 string[] extra_args; 322 } 323 324 struct EmRunOptions 325 { 326 string name, vendor; 327 bool verbose; 328 } 329 330 struct EmbuilderOptions 331 { 332 string port_name, vendor; 333 } 334 335 // --------------------------------------------------------------------------- 336 // Platform helpers 337 // --------------------------------------------------------------------------- 338 339 /// Resolve "native" to the actual compile-time platform string so that all 340 /// subsequent target checks work correctly when DUB passes --target=native. 341 string resolveTarget(string target) @safe pure nothrow 342 { 343 if (target != "native") 344 return target; 345 version (Windows) 346 return "windows"; 347 else version (OSX) 348 return "darwin"; 349 else version (linux) 350 return "linux"; 351 else version (Android) 352 return "android"; 353 else version (Emscripten) 354 return "wasm"; 355 else 356 return "linux"; // safe fallback 357 } 358 359 /// Returns true when compiling for (or running on) Windows. 360 bool targetIsWindows(string target) @safe pure nothrow 361 { 362 return resolveTarget(target).canFind("windows"); 363 } 364 365 bool targetIsDarwin(string target) @safe pure nothrow 366 { 367 return resolveTarget(target).canFind("darwin"); 368 } 369 370 bool targetIsWasm(string target) @safe pure nothrow 371 { 372 return resolveTarget(target).canFind("wasm"); 373 } 374 375 /// Object-file extension: ".obj" on Windows, ".o" elsewhere. 376 string objExt(string target) @safe pure nothrow 377 { 378 return targetIsWindows(target) ? ".obj" : ".o"; 379 } 380 381 /// Emit a single compiler flag that defines a C preprocessor macro, 382 /// formatted correctly for the target toolchain. 383 string defineFlag(string macro_, string target) @safe pure nothrow 384 { 385 return (targetIsWindows(target) ? "/D" : "-D") ~ macro_; 386 } 387 388 /// Emit an include-path flag for the target toolchain. 389 string includeFlag(string path, string target) @safe pure nothrow 390 { 391 return targetIsWindows(target) ? "/I" ~ path : "-I" ~ path; 392 } 393 394 // --------------------------------------------------------------------------- 395 // Build Sokol (and optionally ImGui / Nuklear) native libraries 396 // --------------------------------------------------------------------------- 397 398 void buildLibSokol(LibSokolOptions opts) @safe 399 { 400 immutable buildDir = absolutePath("build"); 401 mkdirRecurse(buildDir); 402 403 immutable isWin = targetIsWindows(opts.target); 404 immutable isMac = targetIsDarwin(opts.target); 405 immutable isWasm = targetIsWasm(opts.target); 406 407 string compiler = opts.toolchain ? opts.toolchain : defaultCompiler(opts.target); 408 409 // ------------------------------------------------------------------ 410 // Assemble compiler flags — kept strictly per-toolchain from the start 411 // ------------------------------------------------------------------ 412 string[] cflags; 413 string[] lflags; 414 415 immutable backendMacro = format("SOKOL_%s", 416 resolveSokolBackend(opts.backend, opts.target).to!string.toUpper); 417 418 if (isWin) 419 { 420 // MSVC (cl.exe) style 421 cflags = [ 422 "/DNDEBUG", "/DIMPL", "/D" ~ backendMacro, 423 "/nologo", "/wd4190", 424 opts.optimize == "debug" ? "/Od": "/O2" 425 ]; 426 // For Vulkan backend, add the Vulkan SDK include path 427 if (opts.backend == SokolBackend.vulkan || resolveSokolBackend(opts.backend, opts.target) == SokolBackend 428 .vulkan) 429 { 430 immutable vulkanSdk = environment.get("VULKAN_SDK", ""); 431 if (vulkanSdk.length) 432 cflags ~= "/I" ~ buildPath(vulkanSdk, "Include"); 433 else 434 throw new Exception("VULKAN_SDK environment variable is not set. " ~ 435 "Install the Vulkan SDK from https://vulkan.lunarg.com/"); 436 } 437 // Windows libs are handled by DUB; lflags unused here but kept for 438 // completeness if linkLibrary ever needs them. 439 } 440 else 441 { 442 // GCC / Clang / emcc style 443 cflags = [ 444 "-DNDEBUG", "-DIMPL", "-D" ~ backendMacro, 445 "-Wall", "-Wextra", "-Wno-unused-function", 446 ]; 447 448 if (isMac) 449 cflags ~= "-Wno-return-type-c-linkage"; 450 451 if (!isWasm) 452 { 453 cflags ~= opts.optimize == "debug" ? "-O0" : "-O2"; 454 // Position-independent code: PIE for static, PIC for shared 455 cflags ~= opts.linkageStatic ? "-fPIE" : "-fPIC"; 456 } 457 458 if (!isMac && !isWasm) 459 { 460 // Linux-specific defines 461 if (opts.use_egl) 462 cflags ~= "-DSOKOL_FORCE_EGL"; 463 if (!opts.use_x11) 464 cflags ~= "-DSOKOL_DISABLE_X11"; 465 if (!opts.use_wayland) 466 cflags ~= "-DSOKOL_DISABLE_WAYLAND"; 467 468 // Linux link flags (informational; actual linking done by DUB) 469 if (opts.use_wayland) 470 lflags ~= [ 471 "-lwayland-client", "-lwayland-egl", 472 "-lwayland-cursor", "-lxkbcommon" 473 ]; 474 lflags ~= opts.backend == SokolBackend.vulkan ? ["-lvulkan"] : [ 475 "-lGL" 476 ]; 477 lflags ~= ["-lX11", "-lXi", "-lXcursor", "-lasound", "-lm", "-ldl"]; 478 } 479 480 if (isMac) 481 lflags ~= [ 482 "-framework", "Cocoa", 483 "-framework", "QuartzCore", 484 "-framework", "Foundation", 485 "-framework", "Metal", 486 "-framework", "AudioToolbox", 487 ]; 488 489 if (isWasm) 490 { 491 if (opts.backend == SokolBackend.wgpu) 492 { 493 EmbuilderOptions embopts = { 494 port_name: "emdawnwebgpu", 495 vendor: opts.vendor, 496 }; 497 embuilderStep(embopts); 498 cflags ~= format("-I%s", 499 buildPath(opts.vendor, "emsdk", "upstream", "emscripten", 500 "cache", "ports", "emdawnwebgpu", 501 "emdawnwebgpu_pkg", "webgpu", "include")); 502 } 503 compiler = buildPath(opts.vendor, "emsdk", "upstream", 504 "emscripten", "emcc") ~ (isWindows() ? ".bat" : ""); 505 } 506 } 507 508 // ------------------------------------------------------------------ 509 // Optional third-party include paths 510 // ------------------------------------------------------------------ 511 if (opts.with_sokol_nuklear) 512 cflags ~= includeFlag(absolutePath(buildPath(opts.vendor, "nuklear")), opts.target); 513 514 // ------------------------------------------------------------------ 515 // Compile & archive libsokol 516 // ------------------------------------------------------------------ 517 immutable sokolSources = [ 518 "sokol_log.c", "sokol_app.c", "sokol_gfx.c", "sokol_time.c", 519 "sokol_audio.c", "sokol_gl.c", "sokol_debugtext.c", "sokol_shape.c", 520 "sokol_glue.c", "sokol_fetch.c", "sokol_memtrack.c", "sokol_args.c", 521 ]; 522 523 // On macOS the sokol .c headers use ObjC syntax — compile them as ObjC. 524 // This flag must NOT be in the shared cflags because it would force 525 // C++ source files (imgui) to be compiled as ObjC instead of C++. 526 string[] sokolCFlags = isMac ? cflags ~ ["-x", "objective-c"] : cflags; 527 528 auto sokolObjs = compileSources( 529 sokolSources, buildDir, opts.sokolSrcPath, 530 compiler, sokolCFlags, "sokol_", opts.target, opts.verbose); 531 532 immutable sokolLib = buildPath(buildDir, sokolLibName(opts.target, opts.linkageStatic)); 533 linkLibrary(sokolLib, sokolObjs, opts.target, opts.linkageStatic, 534 opts.vendor, lflags, opts.verbose); 535 sokolObjs.each!(obj => exists(obj) && remove(obj)); 536 537 // ------------------------------------------------------------------ 538 // Optionally compile & archive libcimgui 539 // ------------------------------------------------------------------ 540 if (opts.with_sokol_imgui) 541 { 542 immutable imguiRoot = absolutePath(buildPath(opts.vendor, "imgui", "src")); 543 enforce(exists(imguiRoot), "ImGui source not found. Run with --with-sokol-imgui after setup."); 544 545 // ImGui needs its own C++ compiler; also include its headers 546 string imguiCompiler = cppCompiler(compiler, opts.target, opts.vendor); 547 string[] imguiFlags = cflags ~ includeFlag(imguiRoot, opts.target); 548 if (!isWin) 549 imguiFlags ~= "-DNDEBUG"; // already in cflags but harmless duplicate 550 551 immutable imguiSources = [ 552 "cimgui.cpp", "imgui.cpp", "imgui_demo.cpp", "imgui_draw.cpp", 553 "imgui_tables.cpp", "imgui_widgets.cpp", "cimgui_internal.cpp" 554 ]; 555 auto imguiObjs = compileSources( 556 imguiSources, buildDir, imguiRoot, 557 imguiCompiler, imguiFlags, "imgui_", opts.target, opts.verbose); 558 559 // sokol_imgui.c and sokol_gfx_imgui.c are C, compiled with the C compiler 560 // On macOS they also need -x objective-c (same reason as the core sokol sources) 561 foreach (sokolImguiSrc; ["sokol_imgui.c", "sokol_gfx_imgui.c", "sokol_app_imgui.c"]) 562 { 563 immutable srcPath = buildPath(opts.sokolSrcPath, sokolImguiSrc); 564 enforce(exists(srcPath), sokolImguiSrc ~ " not found"); 565 immutable objPath = buildPath(buildDir, 566 sokolImguiSrc.stripExtension ~ objExt(opts.target)); 567 compileSource(srcPath, objPath, compiler, 568 sokolCFlags ~ includeFlag(imguiRoot, opts.target), 569 opts.target, opts.verbose); 570 imguiObjs ~= objPath; 571 } 572 573 immutable imguiLib = buildPath(buildDir, imguiLibName(opts.target, opts.linkageStatic)); 574 linkLibrary(imguiLib, imguiObjs, opts.target, opts.linkageStatic, 575 opts.vendor, lflags, opts.verbose); 576 imguiObjs.each!(obj => exists(obj) && remove(obj)); 577 } 578 579 // ------------------------------------------------------------------ 580 // Optionally compile & archive libnuklear 581 // ------------------------------------------------------------------ 582 if (opts.with_sokol_nuklear) 583 { 584 immutable nuklearRoot = absolutePath(buildPath(opts.vendor, "nuklear")); 585 enforce(exists(nuklearRoot), "Nuklear source not found. Run after setup."); 586 587 immutable sokolNuklearPath = buildPath(opts.sokolSrcPath, "sokol_nuklear.c"); 588 enforce(exists(sokolNuklearPath), "sokol_nuklear.c not found"); 589 590 // sokol_nuklear.c compiled with SOKOL_NUKLEAR_IMPL (triggered by -DIMPL) 591 // already includes nuklear.h with NK_IMPLEMENTATION internally. 592 // Compiling nuklearc.c separately would redefine every nk_* symbol → link error. 593 // Solution: only compile sokol_nuklear.c, passing the nuklear include path. 594 // On macOS also needs -x objective-c (same as core sokol C sources). 595 string[] nuklearFlags = sokolCFlags ~ includeFlag(nuklearRoot, opts.target); 596 597 immutable objPath = buildPath(buildDir, "sokol_nuklear" ~ objExt(opts.target)); 598 compileSource(sokolNuklearPath, objPath, compiler, nuklearFlags, opts.target, opts.verbose); 599 600 immutable nuklearLib = buildPath(buildDir, nuklearLibName(opts.target, opts.linkageStatic)); 601 linkLibrary(nuklearLib, [objPath], opts.target, opts.linkageStatic, 602 opts.vendor, lflags, opts.verbose); 603 if (exists(objPath)) 604 remove(objPath); 605 } 606 } 607 608 // --------------------------------------------------------------------------- 609 // Library name helpers 610 // --------------------------------------------------------------------------- 611 612 string sokolLibName(string target, bool static_) @safe pure nothrow 613 { 614 return sharedOrStaticName("sokol", target, static_); 615 } 616 617 string imguiLibName(string target, bool static_) @safe pure nothrow 618 { 619 return sharedOrStaticName("cimgui", target, static_); 620 } 621 622 string nuklearLibName(string target, bool static_) @safe pure nothrow 623 { 624 return sharedOrStaticName("nuklear", target, static_); 625 } 626 627 string sharedOrStaticName(string base, string target, bool static_) @safe pure nothrow 628 { 629 if (targetIsWindows(target)) 630 return static_ ? base ~ ".lib" : base ~ ".dll"; 631 if (targetIsDarwin(target)) 632 return static_ ? "lib" ~ base ~ ".a" : "lib" ~ base ~ ".dylib"; 633 if (targetIsWasm(target)) 634 return "lib" ~ base ~ ".a"; // wasm is always static 635 return static_ ? "lib" ~ base ~ ".a" : "lib" ~ base ~ ".so"; // linux 636 } 637 638 // --------------------------------------------------------------------------- 639 // Compile helpers 640 // --------------------------------------------------------------------------- 641 642 /// Compile one source file to an object file. 643 void compileSource(string srcPath, string objPath, string compiler, 644 string[] cflags, string target, bool verbose) @safe 645 { 646 enforce(exists(srcPath), format("Source file does not exist: %s", srcPath)); 647 648 string[] cmd; 649 if (targetIsWindows(target)) 650 cmd = [compiler] ~ cflags ~ ["/c", "/Fo" ~ objPath, srcPath]; 651 else 652 cmd = [compiler] ~ cflags ~ ["-c", "-o", objPath, srcPath]; 653 654 executeOrFail(cmd, format("Failed to compile %s", srcPath.baseName), verbose); 655 } 656 657 /// Compile a list of source files; returns the list of object-file paths. 658 string[] compileSources(const(string[]) sources, string buildDir, string srcRoot, 659 string compiler, string[] cflags, string prefix, 660 string target, bool verbose) @safe 661 { 662 string[] objFiles; 663 foreach (src; sources) 664 { 665 immutable srcPath = buildPath(srcRoot, src); 666 immutable objPath = buildPath(buildDir, prefix ~ src.baseName ~ objExt(target)); 667 compileSource(srcPath, objPath, compiler, cflags, target, verbose); 668 objFiles ~= objPath; 669 } 670 return objFiles; 671 } 672 673 // --------------------------------------------------------------------------- 674 // Link helper 675 // --------------------------------------------------------------------------- 676 677 void linkLibrary(string libPath, string[] objFiles, string target, 678 bool linkageStatic, string vendor, string[] lflags, bool verbose) @safe 679 { 680 immutable isWin = targetIsWindows(target); 681 immutable isMac = targetIsDarwin(target); 682 immutable isWasm = targetIsWasm(target); 683 684 string[] cmd; 685 686 if (linkageStatic || isWasm) 687 { 688 // Static archive 689 string ar; 690 if (isWasm) 691 ar = buildPath(vendor, "emsdk", "upstream", "emscripten", "emar") ~ 692 (isWindows() ? ".bat" : ""); 693 else if (isWin) 694 ar = "lib.exe"; 695 else 696 ar = "ar"; 697 698 if (isWin) 699 cmd = [ar, "/nologo", "/OUT:" ~ libPath] ~ objFiles; 700 else 701 cmd = [ar, "rcs", libPath] ~ objFiles; 702 } 703 else 704 { 705 // Shared library 706 if (isMac) 707 { 708 cmd = [findProgram("clang"), "-dynamiclib", "-o", libPath] ~ objFiles ~ lflags; 709 } 710 else if (isWin) 711 { 712 // cl.exe /LD 713 cmd = [findProgram("cl"), "/LD", "/nologo", "/Fe:" ~ libPath] ~ objFiles ~ lflags; 714 } 715 else 716 { 717 cmd = [findProgram("gcc"), "-shared", "-o", libPath] ~ objFiles ~ lflags; 718 } 719 } 720 721 executeOrFail(cmd, format("Failed to create %s", libPath.baseName), verbose); 722 } 723 724 // --------------------------------------------------------------------------- 725 // Determine the C++ compiler that matches the given C compiler 726 // --------------------------------------------------------------------------- 727 728 string cppCompiler(string cc, string target, string vendor) @safe 729 { 730 if (targetIsWasm(target)) 731 return buildPath(vendor, "emsdk", "upstream", "emscripten", "em++") ~ 732 ( 733 isWindows() ? ".bat" : ""); 734 if (cc.canFind("clang")) 735 return findProgram(cc.baseName ~ "++"); 736 if (cc.canFind("gcc")) 737 return findProgram("g++"); 738 // MSVC: same compiler handles both C and C++ 739 return cc; 740 } 741 742 // --------------------------------------------------------------------------- 743 // WASM link / run steps 744 // --------------------------------------------------------------------------- 745 746 void emLinkStep(EmLinkOptions opts) @safe 747 { 748 string emcc = buildPath(opts.vendor, "emsdk", "upstream", "emscripten", 749 opts.use_imgui ? "em++" : "emcc") ~ (isWindows() ? ".bat" : ""); 750 string[] cmd = [emcc]; 751 752 if (opts.use_imgui) 753 cmd ~= "-lcimgui"; 754 if (opts.use_nuklear) 755 cmd ~= "-lnuklear"; 756 757 if (opts.optimize == "debug") 758 cmd ~= ["-Og", "-sSAFE_HEAP=1", "-sSTACK_OVERFLOW_CHECK=1"]; 759 else 760 { 761 cmd ~= "-sASSERTIONS=0"; 762 cmd ~= opts.optimize == "small" ? "-Oz" : "-O3"; 763 if (opts.release_use_lto) 764 cmd ~= "-flto"; 765 if (opts.release_use_closure) 766 cmd ~= ["--closure", "1"]; 767 } 768 769 if (opts.backend == SokolBackend.wgpu) 770 cmd ~= "--use-port=emdawnwebgpu"; 771 if (opts.backend == SokolBackend.gles3) 772 cmd ~= "-sUSE_WEBGL2=1"; 773 if (!opts.use_filesystem) 774 cmd ~= "-sNO_FILESYSTEM=1"; 775 if (opts.use_emmalloc) 776 cmd ~= "-sMALLOC='emmalloc'"; 777 if (opts.shell_file_path) 778 cmd ~= "--shell-file=" ~ opts.shell_file_path; 779 780 cmd ~= ["-sSTACK_SIZE=512KB"] ~ opts.extra_args ~ opts.lib_main; 781 782 immutable baseName = opts.lib_main.baseName[3 .. $ - 2]; // strip "lib" and ".a" 783 string outFile = buildPath("build", baseName ~ ".html"); 784 cmd ~= ["-o", outFile]; 785 786 executeOrFail(cmd, "emcc link failed for " ~ outFile, opts.verbose); 787 788 string webDir = "web"; 789 mkdirRecurse(webDir); 790 foreach (ext; [".html", ".wasm", ".js"]) 791 copy(buildPath("build", baseName ~ ext), buildPath(webDir, baseName ~ ext)); 792 rmdirRecurse(buildPath("build")); 793 } 794 795 void emRunStep(EmRunOptions opts) @safe 796 { 797 string emrun = buildPath(opts.vendor, "emsdk", "upstream", "emscripten", "emrun") ~ 798 ( 799 isWindows() ? ".bat" : ""); 800 executeOrFail([emrun, buildPath("web", opts.name ~ ".html")], "emrun failed", opts.verbose); 801 } 802 803 // --------------------------------------------------------------------------- 804 // Emscripten SDK setup 805 // --------------------------------------------------------------------------- 806 807 void emSdkSetupStep(string emsdk) @safe 808 { 809 if (!exists(buildPath(emsdk, ".emscripten"))) 810 { 811 immutable cmd = buildPath(emsdk, "emsdk") ~ (isWindows() ? ".bat" : ""); 812 executeOrFail( 813 [!isWindows() ? "bash " ~ cmd: cmd, "install", "latest"], 814 "emsdk install failed", true); 815 executeOrFail( 816 [!isWindows() ? "bash " ~ cmd: cmd, "activate", "latest"], 817 "emsdk activate failed", true); 818 } 819 } 820 821 void embuilderStep(EmbuilderOptions opts) @safe 822 { 823 string embuilder = buildPath(opts.vendor, "emsdk", "upstream", "emscripten", "embuilder") ~ 824 ( 825 isWindows() ? ".bat" : ""); 826 executeOrFail([embuilder, "build", opts.port_name], 827 "embuilder failed to build " ~ opts.port_name, true); 828 } 829 830 // --------------------------------------------------------------------------- 831 // Utility functions 832 // --------------------------------------------------------------------------- 833 834 string findProgram(string programName) @safe 835 { 836 foreach (path; environment.get("PATH").split(pathSeparator)) 837 { 838 string fullPath = buildPath(path, programName); 839 version (Windows) 840 fullPath ~= ".exe"; 841 if (exists(fullPath) && isFile(fullPath)) 842 return fullPath; 843 } 844 throw new Exception(format("Program '%s' not found in PATH", programName)); 845 } 846 847 string defaultCompiler(string target) @safe 848 { 849 if (targetIsWasm(target)) 850 return ""; // set later from vendor/emsdk 851 version (linux) 852 return findProgram("gcc"); 853 version (Windows) 854 return "cl"; // found via PATH / vcvarsall 855 version (OSX) 856 return findProgram("clang"); 857 version (Android) 858 return findProgram("clang"); 859 throw new Exception("Unsupported platform for defaultCompiler"); 860 } 861 862 SokolBackend resolveSokolBackend(SokolBackend backend, string target) @safe 863 { 864 immutable t = resolveTarget(target); 865 if (t.canFind("linux")) 866 return backend == SokolBackend.vulkan ? SokolBackend.vulkan : SokolBackend.glcore; 867 if (t.canFind("darwin")) 868 return SokolBackend.metal; 869 if (t.canFind("windows")) 870 return backend == SokolBackend.vulkan ? SokolBackend.vulkan : SokolBackend.d3d11; 871 if (t.canFind("wasm")) 872 return backend == SokolBackend.wgpu ? backend : SokolBackend.gles3; 873 if (t.canFind("android")) 874 return SokolBackend.gles3; 875 return backend; 876 } 877 878 void executeOrFail(string[] cmd, string errorMsg, bool verbose) @safe 879 { 880 if (verbose) 881 writeln("Executing: ", cmd.join(" ")); 882 auto result = executeShell(cmd.join(" ")); 883 if (verbose && result.output.length) 884 writeln("Output:\n", result.output); 885 enforce(result.status == 0, format("%s: %s", errorMsg, result.output)); 886 } 887 888 bool isWindows() @safe nothrow 889 { 890 version (Windows) 891 return true; 892 return false; 893 } 894 895 string defaultTarget() @safe 896 { 897 version (linux) 898 return "linux"; 899 version (Windows) 900 return "windows"; 901 version (OSX) 902 return "darwin"; 903 version (Android) 904 return "android"; 905 version (Emscripten) 906 return "wasm"; 907 throw new Exception("Unsupported platform"); 908 } 909 910 // --------------------------------------------------------------------------- 911 // Download / zip helpers 912 // --------------------------------------------------------------------------- 913 914 void download(string url, string fileName) @trusted 915 { 916 auto buf = appender!(ubyte[])(); 917 size_t contentLength; 918 auto http = HTTP(url); 919 http.onReceiveHeader((in k, in v) { 920 if (k == "content-length") 921 contentLength = to!size_t(v); 922 }); 923 924 int barWidth = 50; 925 http.onReceive((data) { 926 buf.put(data); 927 if (contentLength) 928 { 929 float progress = cast(float) buf.data.length / contentLength; 930 write("\r[", 931 "=".replicate(cast(int)(barWidth * progress)), ">", 932 " ".replicate(barWidth - cast(int)(barWidth * progress)), 933 "] ", format("%d%%", cast(int)(progress * 100))); 934 stdout.flush(); 935 } 936 return data.length; 937 }); 938 939 http.perform(); 940 enforce(http.statusLine.code / 100 == 2 || http.statusLine.code == 302, 941 format("HTTP request failed: %s", http.statusLine.code)); 942 std.file.write(fileName, buf.data); 943 writeln(); 944 } 945 946 void extractZip(string zipFile, string destination) @trusted 947 { 948 ZipArchive archive = new ZipArchive(read(zipFile)); 949 string prefix = archive.directory.keys.front[0 .. $ 950 - archive.directory.keys.front.find("/") 951 .length + 1]; 952 953 if (exists(destination)) 954 rmdirRecurse(destination); 955 mkdirRecurse(destination); 956 957 foreach (name, am; archive.directory) 958 { 959 if (!am.expandedSize) 960 continue; 961 string path = buildPath(destination, chompPrefix(name, prefix)); 962 mkdirRecurse(dirName(path)); 963 std.file.write(path, archive.expand(am)); 964 } 965 } 966 967 string getSHDC(string vendor) @safe 968 { 969 string path = absolutePath(buildPath(vendor, "shdc")); 970 string file = "shdc.zip"; 971 scope (exit) 972 if (exists(file)) 973 remove(file); 974 975 if (!exists(path)) 976 { 977 download("https://github.com/floooh/sokol-tools-bin/archive/refs/heads/master.zip", file); 978 extractZip(file, path); 979 } 980 981 version (Windows) 982 immutable shdc = buildPath("bin", "win32", "sokol-shdc.exe"); 983 else version (linux) 984 immutable shdc = buildPath("bin", isAArch64() ? "linux_arm64" : "linux", "sokol-shdc"); 985 else version (OSX) 986 immutable shdc = buildPath("bin", isAArch64() ? "osx_arm64" : "osx", "sokol-shdc"); 987 else 988 throw new Exception("Unsupported platform for sokol-tools"); 989 990 return buildPath(path, shdc); 991 } 992 993 bool isAArch64() @safe nothrow 994 { 995 version (AArch64) 996 return true; 997 return false; 998 }