From 4c855819c99c23184329c867fe6070dbac0f48d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20L=C3=BCbe=C3=9F?= Date: Sun, 12 Apr 2026 12:05:55 +0200 Subject: [PATCH] Added FreeType to BuildTool --- GlitchyEngine/vendor/freetype | 2 +- README.md | 27 ++ bin/BuildTool/BuildTool.cs | 2 +- bin/BuildTool/Modules/BeefWorkspaceModule.cs | 2 +- bin/BuildTool/Modules/FreetypeStackModule.cs | 287 +++++++++++++++++++ 5 files changed, 317 insertions(+), 3 deletions(-) create mode 100644 bin/BuildTool/Modules/FreetypeStackModule.cs diff --git a/GlitchyEngine/vendor/freetype b/GlitchyEngine/vendor/freetype index 9168ee0..311ccb4 160000 --- a/GlitchyEngine/vendor/freetype +++ b/GlitchyEngine/vendor/freetype @@ -1 +1 @@ -Subproject commit 9168ee0f2cb0e7708a10b9dd2171a2a917dbdce0 +Subproject commit 311ccb4514d234cdda945e4a684dc519db524762 diff --git a/README.md b/README.md index 7677287..037eaec 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,29 @@ # Glitchy Engine A complete refocusation, reimagination and reimplementation of the world-renowned "Glitchy Engine" + +## Prerequisites + +To build the engine and its vendored dependencies, install the following tools on your developer machine. On Windows the commands below assume [Scoop](https://scoop.sh/) — substitute `winget`, `choco` or a manual install if you prefer. + +| Tool | Purpose | Install (Windows) | +|-----------------------------|----------------------------------------------|---------------------------------------| +| Visual Studio 2022 | MSVC toolchain, MSBuild, vswhere | Installer — with the *Desktop development with C++* workload | +| .NET SDK 10 | Builds the `bin/BuildTool` C# orchestrator, also required for scripting | https://dotnet.microsoft.com/download | +| CMake | Builds `GlitchyEngineHelper` and some vendor libs | `scoop install cmake` | +| [Python 3](https://www.python.org/) + pip | Required by Meson and some vendor build scripts | `winget install Python.Python.3.13` (or use [uv](https://github.com/astral-sh/uv)) | +| [Meson](https://mesonbuild.com/) | Configures Freetype, Harfbuzz, Fribidi builds | `uv tool install meson` *or* `pip install --user meson` | +| [Ninja](https://ninja-build.org/) | Backend build system used by Meson | `scoop install ninja` | +| [pkg-config](https://www.freedesktop.org/wiki/Software/pkg-config/) | Resolves library metadata during Meson builds | `scoop install pkg-config` | +| Beef compiler (`beefbuild`) | Compiles the Beef workspace | https://www.beeflang.org/ | + +The `BuildTool` (`bin/BuildTool`) orchestrates the vendor-library builds and the Beef workspace compile. To verify your environment is ready, run: + +``` +./bin/BuildTool.exe build list +``` + +You should see a list of modules (`Box2D`, `FreetypeStack`, `BeefWorkspace`, …) and the topologically-sorted build order. Running individual modules takes the form: + +``` +./bin/BuildTool.exe build --release FreetypeStack +``` diff --git a/bin/BuildTool/BuildTool.cs b/bin/BuildTool/BuildTool.cs index c473cf3..e1c815b 100644 --- a/bin/BuildTool/BuildTool.cs +++ b/bin/BuildTool/BuildTool.cs @@ -78,7 +78,7 @@ class Program static async Task Main(string[] args) { - List modules = [new BeefWorkspaceModule(), new GlitchyEngineHelperModule(), new ScriptCoreModule(), new SetupNethostModule(), new Box2DModule(), new AllModule()]; + List modules = [new BeefWorkspaceModule(), new GlitchyEngineHelperModule(), new ScriptCoreModule(), new SetupNethostModule(), new Box2DModule(), new FreetypeStackModule(), new AllModule()]; // Find dependencies foreach (Module module in modules) diff --git a/bin/BuildTool/Modules/BeefWorkspaceModule.cs b/bin/BuildTool/Modules/BeefWorkspaceModule.cs index c4739e8..50dcc2a 100644 --- a/bin/BuildTool/Modules/BeefWorkspaceModule.cs +++ b/bin/BuildTool/Modules/BeefWorkspaceModule.cs @@ -8,7 +8,7 @@ class BeefWorkspaceModule : Module public override string Name => ModuleName; - public override List DependencyTypes => [typeof(GlitchyEngineHelperModule), typeof(Box2DModule)]; + public override List DependencyTypes => [typeof(GlitchyEngineHelperModule), typeof(Box2DModule), typeof(FreetypeStackModule)]; public override async Task Run(BuildInfo buildInfo) { diff --git a/bin/BuildTool/Modules/FreetypeStackModule.cs b/bin/BuildTool/Modules/FreetypeStackModule.cs new file mode 100644 index 0000000..3e24875 --- /dev/null +++ b/bin/BuildTool/Modules/FreetypeStackModule.cs @@ -0,0 +1,287 @@ +using SimpleExec; +using static SimpleExec.Command; + +namespace BuildTool.Modules; + +class FreetypeStackModule : Module +{ + public static readonly string ModuleName = "FreetypeStack"; + + public override string Name => ModuleName; + + public override List DependencyTypes => []; + + /// + /// Per-flavor build configuration. Each active flavor produces its own set of static + /// libs under dist/win64/{DistSubdir}, so a Beef Debug engine links against the + /// /MTd set and a Beef Release engine against the /MT set — matching Box2D's convention + /// and avoiding the _ITERATOR_DEBUG_LEVEL / RuntimeLibrary link errors that + /// a shared output directory would cause. + /// + private record Flavor(string Name, string Buildtype, string Vscrt, string DistSubdir); + + private static readonly Flavor DebugFlavor = new("debug", "debug", "mtd", "debug"); + private static readonly Flavor ReleaseFlavor = new("release", "release", "mt", "release"); + + public override async Task Run(BuildInfo buildInfo) + { + if (!buildInfo.BuildDebug && !buildInfo.BuildRelease) + return true; + + await EnsureToolsAvailableAsync(); + + Dictionary vsEnv = await CaptureVsDevEnvAsync(); + + string workspaceRoot = buildInfo.WorkingDirectory.WorkspaceRoot; + string vendorRoot = Path.Combine(workspaceRoot, "GlitchyEngine", "vendor", "freetype"); + string distRoot = Path.Combine(vendorRoot, "dist", "win64"); + string hbSrcDir = Path.Combine(vendorRoot, "harfbuzz"); + string fribidiSrcDir = Path.Combine(vendorRoot, "fribidi"); + + // Build directories live outside the submodules so Meson output never dirties them. + string buildRoot = Path.Combine(workspaceRoot, "build", "freetype-stack"); + Directory.CreateDirectory(buildRoot); + + if (buildInfo.BuildDebug) + await BuildFlavorAsync(DebugFlavor, hbSrcDir, fribidiSrcDir, buildRoot, distRoot, vsEnv, buildInfo.ForceRebuild); + + if (buildInfo.BuildRelease) + await BuildFlavorAsync(ReleaseFlavor, hbSrcDir, fribidiSrcDir, buildRoot, distRoot, vsEnv, buildInfo.ForceRebuild); + + return true; + } + + private static async Task BuildFlavorAsync( + Flavor flavor, + string hbSrcDir, string fribidiSrcDir, + string buildRoot, string distRoot, + Dictionary vsEnv, + bool forceRebuild) + { + string hbBuildDir = Path.Combine(buildRoot, $"harfbuzz-{flavor.Name}"); + string fribidiBuildDir = Path.Combine(buildRoot, $"fribidi-{flavor.Name}"); + string distDir = Path.Combine(distRoot, flavor.DistSubdir); + + if (forceRebuild) + { + if (Directory.Exists(hbBuildDir)) Directory.Delete(hbBuildDir, recursive: true); + if (Directory.Exists(fribidiBuildDir)) Directory.Delete(fribidiBuildDir, recursive: true); + } + + await BuildHarfbuzzAsync(flavor, hbSrcDir, hbBuildDir, vsEnv); + await BuildFribidiAsync(flavor, fribidiSrcDir, fribidiBuildDir, vsEnv); + + Directory.CreateDirectory(distDir); + CopyArtefacts(hbBuildDir, fribidiBuildDir, distDir); + } + + /// + /// Verifies that meson, ninja and pkg-config are available. These are external + /// prerequisites that the user must install once — we don't try to auto-install them + /// because each has a different canonical install path and package manager on Windows. + /// + private static async Task EnsureToolsAvailableAsync() + { + async Task Check(string tool, string args, string hint) + { + try + { + await ReadAsync(tool, args); + } + catch (Exception ex) + { + throw new Exception( + $"Required tool '{tool}' not found on PATH. {hint}", ex); + } + } + + await Check("meson", "--version", + "Install with 'uv tool install meson' (if you use uv) or 'pip install --user meson'."); + await Check("ninja", "--version", + "Install with 'scoop install ninja' or 'winget install Ninja-build.Ninja'."); + await Check("pkg-config", "--version", + "Install with 'scoop install pkg-config' or 'choco install pkgconfiglite'."); + } + + /// + /// Runs vcvarsall.bat x64 and captures the resulting environment block so that Meson's + /// subprocesses find cl.exe, link.exe and the MSVC headers/libs without needing the + /// developer to open a Developer Command Prompt manually. + /// + private static async Task> CaptureVsDevEnvAsync() + { + string programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); + string vswherePath = Path.Combine(programFilesX86, "Microsoft Visual Studio", "Installer", "vswhere.exe"); + + if (!File.Exists(vswherePath)) + { + throw new Exception( + $"Could not find vswhere.exe at '{vswherePath}'. Is Visual Studio 2017 or newer installed?"); + } + + (string stdout, _) = await ReadAsync(vswherePath, + "-latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 " + + "-find VC/Auxiliary/Build/vcvarsall.bat"); + + string vcvarsallPath = stdout + .Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .FirstOrDefault() ?? ""; + + if (string.IsNullOrWhiteSpace(vcvarsallPath) || !File.Exists(vcvarsallPath)) + { + throw new Exception( + "vswhere could not locate vcvarsall.bat. Make sure the 'Desktop development with C++' workload " + + "(component 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64') is installed."); + } + + // Quadruple-quoted cmd /c trick: outer quotes wrap the whole payload, inner quotes + // protect the vcvarsall path because it typically contains a space. + (string envDump, _) = await ReadAsync("cmd", + $"/d /c \"\"{vcvarsallPath}\" x64 >nul && set\""); + + var env = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (string line in envDump.Split('\n', StringSplitOptions.RemoveEmptyEntries)) + { + int idx = line.IndexOf('='); + if (idx <= 0) continue; + string key = line.Substring(0, idx).Trim(); + string value = line.Substring(idx + 1).TrimEnd('\r', '\n'); + env[key] = value; + } + + if (!env.ContainsKey("INCLUDE") || !env.ContainsKey("LIB")) + { + throw new Exception( + "vcvarsall.bat did not produce the expected MSVC environment variables (INCLUDE, LIB). " + + "Something went wrong while capturing the VS Developer environment."); + } + + return env; + } + + private static async Task BuildHarfbuzzAsync( + Flavor flavor, string srcDir, string buildDir, Dictionary vsEnv) + { + Console.WriteLine($"[FreetypeStack] Building Harfbuzz + Freetype ({flavor.Name}, /{flavor.Vscrt.ToUpper()}) -> {buildDir}"); + + void ApplyEnv(IDictionary target) + { + foreach (var kv in vsEnv) target[kv.Key] = kv.Value; + } + + if (!Directory.Exists(buildDir)) + { + // Harfbuzz' meson.build pulls Freetype in as a subproject via + // harfbuzz/subprojects/freetype2.wrap, which fetches VER-2-11-0 from GitLab the + // first time (and caches it thereafter). That gives us a separately linkable + // libfreetype.a as a side-effect of the Harfbuzz build. + await RunAsync("meson", + $"setup \"{buildDir}\" " + + $"--buildtype={flavor.Buildtype} " + + "--default-library=static " + + $"-Db_vscrt={flavor.Vscrt} " + + "--wrap-mode=default " + + "-Dfreetype=enabled " + + "-Dglib=disabled " + + "-Dgobject=disabled " + + "-Dcairo=disabled " + + "-Dchafa=disabled " + + "-Dicu=disabled " + + "-Dtests=disabled " + + "-Ddocs=disabled", + workingDirectory: srcDir, + configureEnvironment: ApplyEnv); + } + + await RunAsync("meson", $"compile -C \"{buildDir}\"", + workingDirectory: srcDir, + configureEnvironment: ApplyEnv); + } + + private static async Task BuildFribidiAsync( + Flavor flavor, string srcDir, string buildDir, Dictionary vsEnv) + { + Console.WriteLine($"[FreetypeStack] Building Fribidi ({flavor.Name}, /{flavor.Vscrt.ToUpper()}) -> {buildDir}"); + + void ApplyEnv(IDictionary target) + { + foreach (var kv in vsEnv) target[kv.Key] = kv.Value; + } + + if (!Directory.Exists(buildDir)) + { + // Fribidi is LGPL 2.1+, so we ship it as a DLL + import-lib to stay on the right + // side of the LGPL's dynamic-linking exception. + await RunAsync("meson", + $"setup \"{buildDir}\" " + + $"--buildtype={flavor.Buildtype} " + + "--default-library=shared " + + $"-Db_vscrt={flavor.Vscrt} " + + "-Ddeprecated=false " + + "-Dtests=false " + + "-Ddocs=false", + workingDirectory: srcDir, + configureEnvironment: ApplyEnv); + } + + await RunAsync("meson", $"compile -C \"{buildDir}\"", + workingDirectory: srcDir, + configureEnvironment: ApplyEnv); + } + + private static void CopyArtefacts(string hbBuildDir, string fribidiBuildDir, string distDir) + { + Console.WriteLine($"[FreetypeStack] Copying artefacts to {distDir}"); + + // Static libs coming out of the Harfbuzz build tree. Meson names them .a on Windows + // when default_library=static; the file format is MSVC-compatible AR so renaming + // to .lib is enough for Beef's linker. + Copy(Path.Combine(hbBuildDir, "src", "libharfbuzz.a"), + Path.Combine(distDir, "harfbuzz.lib")); + Copy(Path.Combine(hbBuildDir, "subprojects", "freetype", "libfreetype.a"), + Path.Combine(distDir, "freetype.lib")); + Copy(FindVersionedSubprojectLib(hbBuildDir, "libpng", "libpng16.a"), + Path.Combine(distDir, "png16.lib")); + Copy(FindVersionedSubprojectLib(hbBuildDir, "zlib", "libz.a"), + Path.Combine(distDir, "z.lib")); + + // Fribidi: shared lib + import lib. + Copy(Path.Combine(fribidiBuildDir, "lib", "fribidi.lib"), + Path.Combine(distDir, "fribidi.lib")); + Copy(Path.Combine(fribidiBuildDir, "lib", "fribidi-0.dll"), + Path.Combine(distDir, "fribidi-0.dll")); + } + + private static void Copy(string src, string dst) + { + if (!File.Exists(src)) + throw new FileNotFoundException($"Expected build artefact not found: {src}"); + + File.Copy(src, dst, overwrite: true); + } + + /// + /// Subproject directories under a Meson build tree are named with an embedded version + /// (e.g. libpng-1.6.37) that drifts whenever the wrap files are updated upstream, + /// so we locate by prefix instead of hard-coding the version. + /// + private static string FindVersionedSubprojectLib(string buildDir, string dirPrefix, string fileName) + { + string subprojectsDir = Path.Combine(buildDir, "subprojects"); + if (!Directory.Exists(subprojectsDir)) + throw new DirectoryNotFoundException($"Expected Meson subprojects dir missing: {subprojectsDir}"); + + foreach (string dir in Directory.EnumerateDirectories(subprojectsDir)) + { + string name = Path.GetFileName(dir); + if (!name.StartsWith(dirPrefix, StringComparison.OrdinalIgnoreCase)) continue; + + string candidate = Path.Combine(dir, fileName); + if (File.Exists(candidate)) + return candidate; + } + + throw new FileNotFoundException( + $"Could not find {fileName} under any subprojects/{dirPrefix}* directory in {buildDir}"); + } +}