Files
UnrealEngine/Engine/Plugins/Marketplace/PCGExt/Source/PCGExtendedToolkit/PCGExtendedToolkit.Build.cs
Brandyn / Techy fcc1b09210 init
2026-04-04 15:40:51 -05:00

318 lines
9.7 KiB
C#
Executable File

// Copyright 2026 Timothé Lapetite and contributors
// Released under the MIT license https://opensource.org/license/MIT/
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using EpicGames.Core;
using Microsoft.Extensions.Logging;
using UnrealBuildTool;
public class PCGExtendedToolkit : ModuleRules
{
private const string PluginName = "PCGExtendedToolkit";
private const string ModulePrefix = "PCGEx";
private const string EditorSuffix = "Editor";
private static readonly string[] BaseDependencies = { "PCGExCore", "PCGExBlending" };
private static readonly string[] BaseEditorDependencies = { "PCGExCoreEditor" };
private readonly Dictionary<string, List<string>> _moduleDependencies = new();
public PCGExtendedToolkit(ReadOnlyTargetRules Target) : base(Target)
{
bool bNoPCH = File.Exists(Path.Combine(ModuleDirectory, "..", "..", "Config", ".noPCH"));
PCHUsage = bNoPCH ? PCHUsageMode.NoPCHs : PCHUsageMode.UseExplicitOrSharedPCHs;
bUseUnity = true;
MinSourceFilesForUnityBuildOverride = 4;
PrecompileForTargets = PrecompileTargetsType.Any;
ConfigureBaseDependencies();
FileReference upluginFile = new FileReference(Path.Combine(PluginDirectory, $"{PluginName}.uplugin"));
ExternalDependencies.Add(upluginFile.FullName);
string pluginsDepsPath = Path.Combine(PluginDirectory, "Config", "PluginsDeps.ini");
if (File.Exists(pluginsDepsPath))
{
ExternalDependencies.Add(pluginsDepsPath);
}
PluginDescriptor descriptor = PluginDescriptor.FromFile(upluginFile);
var declaredModules = new HashSet<string>(descriptor.Modules.Select(m => m.Name));
var enabledPlugins = new HashSet<string>(
descriptor.Plugins
.Where(p => p.bEnabled)
.Select(p => p.Name)
);
var modulePluginRequirements = LoadPluginsDeps(pluginsDepsPath);
ValidatePluginRequirements(declaredModules, enabledPlugins, modulePluginRequirements);
foreach (ModuleDescriptor module in descriptor.Modules)
{
RegisterModule(module.Name, declaredModules);
}
ValidateConfiguration(declaredModules);
GenerateSubModulesHeader();
PublicIncludePaths.AddRange(new string[] { Path.Combine(ModuleDirectory, "../../Intermediate/Generated") });
}
private Dictionary<string, List<string>> LoadPluginsDeps(string filePath)
{
var deps = new Dictionary<string, List<string>>();
if (!File.Exists(filePath))
{
Logger.LogInformation("[{Plugin}] PluginsDeps.ini not found - skipping plugin requirement validation", PluginName);
return deps;
}
string currentModule = null;
foreach (string line in File.ReadAllLines(filePath))
{
string trimmed = line.Trim();
if (string.IsNullOrEmpty(trimmed) || trimmed.StartsWith("#") || trimmed.StartsWith(";"))
continue;
// Section header: [ModuleName]
if (trimmed.StartsWith("[") && trimmed.EndsWith("]"))
{
currentModule = trimmed.Substring(1, trimmed.Length - 2);
if (!deps.ContainsKey(currentModule))
{
deps[currentModule] = new List<string>();
}
continue;
}
// Plugin name under current section
if (currentModule != null && !string.IsNullOrEmpty(trimmed))
{
deps[currentModule].Add(trimmed);
}
}
return deps;
}
private void ValidatePluginRequirements(
HashSet<string> declaredModules,
HashSet<string> enabledPlugins,
Dictionary<string, List<string>> modulePluginRequirements)
{
var errors = new List<string>();
foreach (var (moduleName, requiredPlugins) in modulePluginRequirements)
{
if (!declaredModules.Contains(moduleName)) continue;
foreach (string plugin in requiredPlugins)
{
if (!enabledPlugins.Contains(plugin))
{
errors.Add($"Module '{moduleName}' requires plugin '{plugin}' to be listed and enabled in .uplugin");
}
}
}
if (errors.Count > 0)
{
string message = $"[{PluginName}] Plugin requirements not met:\n" + string.Join("\n", errors.Select(e => $" - {e}"));
throw new BuildException(message);
}
}
private void ConfigureBaseDependencies()
{
PublicDependencyModuleNames.AddRange(new[] { "Core", "CoreUObject", "Engine", "PCG" });
PublicDependencyModuleNames.AddRange(BaseDependencies);
PrivateDependencyModuleNames.Add("DeveloperSettings");
if (Target.bBuildEditor)
{
PrivateDependencyModuleNames.AddRange(new[] { "UnrealEd", "Settings" });
PrivateDependencyModuleNames.AddRange(BaseEditorDependencies);
}
}
private void RegisterModule(string moduleName, HashSet<string> declaredModules)
{
if (IsUmbrellaModule(moduleName)) return;
if (!IsPCGExModule(moduleName)) return;
bool isEditor = moduleName.EndsWith(EditorSuffix);
if (isEditor && !Target.bBuildEditor) return;
if (isEditor)
{
PrivateDependencyModuleNames.Add(moduleName);
}
else
{
PublicDependencyModuleNames.Add(moduleName);
}
string buildFile = GetBuildFilePath(moduleName);
if (File.Exists(buildFile))
{
ExternalDependencies.Add(buildFile);
_moduleDependencies[moduleName] = ScanDependencies(buildFile, moduleName);
}
}
private List<string> ScanDependencies(string buildFilePath, string selfName)
{
var deps = new List<string>();
string content = File.ReadAllText(buildFilePath);
var blockPattern = new Regex(
@"(?:Public|Private)DependencyModuleNames\s*\.\s*AddRange\s*\(\s*new\s*(?:string)?\s*\[\s*\]\s*\{([^}]*)\}",
RegexOptions.Singleline
);
foreach (Match block in blockPattern.Matches(content))
{
var namePattern = new Regex(@"""(PCGEx\w+)""");
foreach (Match name in namePattern.Matches(block.Groups[1].Value))
{
string dep = name.Groups[1].Value;
if (dep != selfName && !deps.Contains(dep))
{
deps.Add(dep);
}
}
}
return deps;
}
private void ValidateConfiguration(HashSet<string> declaredModules)
{
var missingDeps = new Dictionary<string, List<string>>();
var missingEditors = new List<string>();
foreach (var (module, deps) in _moduleDependencies)
{
foreach (string dep in deps)
{
if (!declaredModules.Contains(dep) && !IsBaseDependency(dep))
{
if (!missingDeps.ContainsKey(dep))
missingDeps[dep] = new List<string>();
missingDeps[dep].Add(module);
}
}
}
foreach (string moduleName in declaredModules)
{
if (moduleName.EndsWith(EditorSuffix) || IsUmbrellaModule(moduleName)) continue;
string editorName = moduleName + EditorSuffix;
if (Directory.Exists(Path.Combine(ModuleDirectory, "..", editorName)) && !declaredModules.Contains(editorName))
{
missingEditors.Add(editorName);
}
}
foreach (var (dep, referencedBy) in missingDeps.OrderBy(kv => kv.Key))
{
Logger.LogWarning(
"[{Plugin}] Dependency '{Dep}' required by [{Refs}] is not declared in .uplugin. Add:\n{Entry}",
PluginName, dep, string.Join(", ", referencedBy), FormatModuleEntry(dep)
);
}
foreach (string editor in missingEditors.OrderBy(e => e))
{
Logger.LogWarning(
"[{Plugin}] Editor module '{Editor}' exists but is not declared in .uplugin. Add:\n{Entry}",
PluginName, editor, FormatModuleEntry(editor)
);
}
}
private void GenerateSubModulesHeader()
{
string headerPath = Path.Combine(ModuleDirectory, "..", "..", "Intermediate", "Generated", "PCGExSubModules.generated.h");
Directory.CreateDirectory(Path.GetDirectoryName(headerPath)!);
var modules = _moduleDependencies.Keys.OrderBy(m => m).ToList();
var sb = new StringBuilder();
sb.AppendLine("// Auto-generated by PCGExtendedToolkit.Build.cs - DO NOT EDIT");
sb.AppendLine("#pragma once");
sb.AppendLine("#include \"CoreMinimal.h\"");
sb.AppendLine();
sb.AppendLine("namespace PCGExSubModules");
sb.AppendLine("{");
sb.AppendLine("\tinline const TArray<FString>& GetEnabledModules()");
sb.AppendLine("\t{");
sb.AppendLine("\t\tstatic TArray<FString> Modules = {");
for (int i = 0; i < modules.Count; i++)
{
sb.AppendLine($"\t\t\tTEXT(\"{modules[i]}\"){(i < modules.Count - 1 ? "," : "")}");
}
sb.AppendLine("\t\t};");
sb.AppendLine("\t\treturn Modules;");
sb.AppendLine("\t}");
sb.AppendLine();
sb.AppendLine("\tinline const TMap<FString, TArray<FString>>& GetModuleDependencies()");
sb.AppendLine("\t{");
sb.AppendLine("\t\tstatic TMap<FString, TArray<FString>> Dependencies = {");
var sortedDeps = _moduleDependencies.OrderBy(kv => kv.Key).ToList();
for (int i = 0; i < sortedDeps.Count; i++)
{
var (mod, deps) = sortedDeps[i];
string depsStr = deps.Count > 0
? string.Join(", ", deps.Select(d => $"TEXT(\"{d}\")"))
: "";
sb.AppendLine($"\t\t\t{{ TEXT(\"{mod}\"), {{ {depsStr} }} }}{(i < sortedDeps.Count - 1 ? "," : "")}");
}
sb.AppendLine("\t\t};");
sb.AppendLine("\t\treturn Dependencies;");
sb.AppendLine("\t}");
sb.AppendLine("}");
string content = sb.ToString();
if (!File.Exists(headerPath) || File.ReadAllText(headerPath) != content)
{
File.WriteAllText(headerPath, content);
}
}
private string GetBuildFilePath(string moduleName) =>
Path.Combine(ModuleDirectory, "..", moduleName, $"{moduleName}.Build.cs");
private string FormatModuleEntry(string name)
{
bool isEditor = name.EndsWith(EditorSuffix);
string platforms = isEditor
? "\"Win64\", \"Mac\", \"Linux\""
: "\"Win64\", \"Mac\", \"IOS\", \"Android\", \"Linux\", \"LinuxArm64\"";
return $@" {{
""Name"": ""{name}"",
""Type"": ""{(isEditor ? "Editor" : "Runtime")}"",
""LoadingPhase"": ""Default"",
""PlatformAllowList"": [ {platforms} ]
}}";
}
private bool IsBaseDependency(string name) =>
BaseDependencies.Contains(name) || BaseEditorDependencies.Contains(name);
private static bool IsPCGExModule(string name) => name.StartsWith(ModulePrefix);
private static bool IsUmbrellaModule(string name) =>
name == PluginName || name == PluginName + EditorSuffix;
}