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