Lua向C#逻辑迁移 一期 #13

将整个插件代码上传
This commit is contained in:
2025-10-26 21:48:39 +08:00
parent 56994b3927
commit 648386cd73
785 changed files with 53683 additions and 2 deletions

View File

@ -0,0 +1,52 @@
using System.Collections.ObjectModel;
namespace UnrealSharpBuildTool.Actions;
public class BuildSolution : BuildToolAction
{
private readonly BuildConfig _buildConfig;
private readonly string _folder;
private readonly Collection<string>? _extraArguments;
public BuildSolution(string folder, Collection<string>? extraArguments = null, BuildConfig buildConfig = BuildConfig.Debug)
{
_folder = Program.FixPath(folder);
_buildConfig = buildConfig;
_extraArguments = extraArguments;
}
public override bool RunAction()
{
if (!Directory.Exists(_folder))
{
throw new Exception($"Couldn't find the solution file at \"{_folder}\"");
}
using BuildToolProcess buildSolutionProcess = new BuildToolProcess();
if (_buildConfig == BuildConfig.Publish)
{
buildSolutionProcess.StartInfo.ArgumentList.Add("publish");
}
else
{
buildSolutionProcess.StartInfo.ArgumentList.Add("build");
}
buildSolutionProcess.StartInfo.ArgumentList.Add($"{_folder}");
buildSolutionProcess.StartInfo.ArgumentList.Add("--configuration");
buildSolutionProcess.StartInfo.ArgumentList.Add(Program.GetBuildConfiguration(_buildConfig));
buildSolutionProcess.StartInfo.WorkingDirectory = _folder;
if (_extraArguments != null)
{
foreach (var argument in _extraArguments)
{
buildSolutionProcess.StartInfo.ArgumentList.Add(argument);
}
}
return buildSolutionProcess.StartBuildToolProcess();
}
}

View File

@ -0,0 +1,25 @@
namespace UnrealSharpBuildTool.Actions;
public abstract class BuildToolAction
{
public static bool InitializeAction()
{
BuildToolAction buildToolAction = Program.BuildToolOptions.Action switch
{
BuildAction.Build => new BuildUserSolution(),
BuildAction.Clean => new CleanSolution(),
BuildAction.GenerateProject => new GenerateProject(),
BuildAction.UpdateProjectDependencies => new UpdateProjectDependencies(),
BuildAction.Rebuild => new RebuildSolution(),
BuildAction.Weave => new WeaveProject(),
BuildAction.PackageProject => new PackageProject(),
BuildAction.GenerateSolution => new GenerateSolution(),
BuildAction.BuildWeave => new BuildWeave(),
_ => throw new Exception($"Can't find build action with name \"{Program.BuildToolOptions.Action}\"")
};
return buildToolAction.RunAction();
}
public abstract bool RunAction();
}

View File

@ -0,0 +1,12 @@
using System.Collections.ObjectModel;
namespace UnrealSharpBuildTool.Actions;
public class BuildUserSolution : BuildSolution
{
public BuildUserSolution(Collection<string>? extraArguments = null, BuildConfig buildConfig = BuildConfig.Debug)
: base(Program.GetScriptFolder(), extraArguments, buildConfig)
{
}
}

View File

@ -0,0 +1,39 @@
namespace UnrealSharpBuildTool.Actions;
public class BuildWeave : BuildToolAction
{
public override bool RunAction()
{
BuildSolution buildSolution = new BuildUserSolution();
WeaveProject weaveProject = new WeaveProject();
return buildSolution.RunAction() && weaveProject.RunAction() && AddLaunchSettings();
}
bool AddLaunchSettings()
{
List<FileInfo> allProjectFiles = Program.GetAllProjectFiles(new DirectoryInfo(Program.GetProjectDirectory()));
foreach (FileInfo projectFile in allProjectFiles)
{
if (projectFile.Directory!.Name.EndsWith(".Glue"))
{
continue;
}
string csProjectPath = Path.Combine(Program.GetScriptFolder(), projectFile.Directory.Name);
string propertiesDirectoryPath = Path.Combine(csProjectPath, "Properties");
string launchSettingsPath = Path.Combine(propertiesDirectoryPath, "launchSettings.json");
if (!Directory.Exists(propertiesDirectoryPath))
{
Directory.CreateDirectory(propertiesDirectoryPath);
}
if (File.Exists(launchSettingsPath))
{
return true;
}
Program.CreateOrUpdateLaunchSettings(launchSettingsPath);
}
return true;
}
}

View File

@ -0,0 +1,18 @@
namespace UnrealSharpBuildTool.Actions;
public class CleanSolution : BuildToolAction
{
public override bool RunAction()
{
using BuildToolProcess cleanProcess = new BuildToolProcess();
string unrealSharpBinaries = Program.GetOutputPath();
if (Directory.Exists(unrealSharpBinaries))
{
Directory.Delete(unrealSharpBinaries, true);
}
return cleanProcess.StartBuildToolProcess();
}
}

View File

@ -0,0 +1,342 @@
using System.Xml;
using Newtonsoft.Json;
namespace UnrealSharpBuildTool.Actions;
public class GenerateProject : BuildToolAction
{
private string _projectPath = string.Empty;
private string _projectFolder = string.Empty;
private string _projectRoot = string.Empty;
bool ContainsUPluginOrUProjectFile(string folder)
{
string[] files = Directory.GetFiles(folder, "*.*", SearchOption.AllDirectories);
foreach (string file in files)
{
if (file.EndsWith(".uplugin", StringComparison.OrdinalIgnoreCase) || file.EndsWith(".uproject", StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
public override bool RunAction()
{
string folder = Program.TryGetArgument("NewProjectFolder");
_projectRoot = Program.TryGetArgument("ProjectRoot");
if (!ContainsUPluginOrUProjectFile(_projectRoot))
{
throw new InvalidOperationException("Project folder must contain a .uplugin or .uproject file.");
}
if (folder == _projectRoot)
{
folder = Path.Combine(folder, "Script");
}
string projectName = Program.TryGetArgument("NewProjectName");
string csProjFileName = $"{projectName}.csproj";
if (!Directory.Exists(folder))
{
Directory.CreateDirectory(folder);
}
_projectFolder = Path.Combine(folder, projectName);
_projectPath = Path.Combine(_projectFolder, csProjFileName);
string version = Program.GetVersion();
using BuildToolProcess generateProjectProcess = new BuildToolProcess();
// Create a class library.
generateProjectProcess.StartInfo.ArgumentList.Add("new");
generateProjectProcess.StartInfo.ArgumentList.Add("classlib");
// Assign project name to the class library.
generateProjectProcess.StartInfo.ArgumentList.Add("-n");
generateProjectProcess.StartInfo.ArgumentList.Add(projectName);
// Set the target framework to the current version.
generateProjectProcess.StartInfo.ArgumentList.Add("-f");
generateProjectProcess.StartInfo.ArgumentList.Add(version);
generateProjectProcess.StartInfo.WorkingDirectory = folder;
if (!generateProjectProcess.StartBuildToolProcess())
{
return false;
}
// dotnet new class lib generates a file named Class1, remove it.
string myClassFile = Path.Combine(_projectFolder, "Class1.cs");
if (File.Exists(myClassFile))
{
File.Delete(myClassFile);
}
string slnPath = Program.GetSolutionFile();
if (!File.Exists(slnPath))
{
GenerateSolution generateSolution = new GenerateSolution();
generateSolution.RunAction();
}
if (Program.HasArgument("SkipUSharpProjSetup"))
{
return true;
}
AddLaunchSettings();
ModifyCSProjFile();
string relativePath = Path.GetRelativePath(Program.GetScriptFolder(), _projectPath);
AddProjectToSln(relativePath);
return true;
}
public static void AddProjectToSln(string relativePath)
{
AddProjectToSln([relativePath]);
}
public static void AddProjectToSln(List<string> relativePaths)
{
foreach (IGrouping<string, string> projects in GroupPathsBySolutionFolder(relativePaths))
{
using BuildToolProcess addProjectToSln = new BuildToolProcess();
addProjectToSln.StartInfo.ArgumentList.Add("sln");
addProjectToSln.StartInfo.ArgumentList.Add("add");
foreach (string relativePath in projects)
{
addProjectToSln.StartInfo.ArgumentList.Add(relativePath);
}
addProjectToSln.StartInfo.ArgumentList.Add("-s");
addProjectToSln.StartInfo.ArgumentList.Add(projects.Key);
addProjectToSln.StartInfo.WorkingDirectory = Program.GetScriptFolder();
addProjectToSln.StartBuildToolProcess();
}
}
private static IEnumerable<IGrouping<string, string>> GroupPathsBySolutionFolder(List<string> relativePaths)
{
return relativePaths.GroupBy(GetPathRelativeToProject)!;
}
private static string GetPathRelativeToProject(string path)
{
var fullPath = Path.GetFullPath(path, Program.GetScriptFolder());
var relativePath = Path.GetRelativePath(Program.GetProjectDirectory(), fullPath);
var projectDirName = Path.GetDirectoryName(relativePath)!;
// If we're in the script folder we want these to be in the Script solution folder, otherwise we want these to
// be in the directory for the plugin itself.
var containingDirName = Path.GetDirectoryName(projectDirName)!;
return containingDirName == "Script" ? containingDirName : Path.GetDirectoryName(containingDirName)!;
}
private void ModifyCSProjFile()
{
try
{
XmlDocument csprojDocument = new XmlDocument();
csprojDocument.Load(_projectPath);
if (csprojDocument.SelectSingleNode("//ItemGroup") is not XmlElement newItemGroup)
{
newItemGroup = csprojDocument.CreateElement("ItemGroup");
csprojDocument.DocumentElement!.AppendChild(newItemGroup);
}
AppendProperties(csprojDocument);
AppendConstantDefines(csprojDocument);
AppendReference(csprojDocument, newItemGroup, "UnrealSharp", GetPathToBinaries());
AppendReference(csprojDocument, newItemGroup, "UnrealSharp.Core", GetPathToBinaries());
AppendSourceGeneratorReference(csprojDocument, newItemGroup);
if (!Program.HasArgument("SkipIncludeProjectGlue"))
{
AppendGeneratedCode(csprojDocument, newItemGroup);
}
foreach (string dependency in Program.GetArguments("Dependency"))
{
AddDependency(csprojDocument, newItemGroup, dependency);
}
csprojDocument.Save(_projectPath);
}
catch (Exception ex)
{
throw new InvalidOperationException($"An error occurred while updating the .csproj file: {ex.Message}", ex);
}
}
private void AddProperty(string name, string value, XmlDocument doc, XmlNode propertyGroup)
{
XmlNode? newProperty = propertyGroup.SelectSingleNode(name);
if (newProperty == null)
{
newProperty = doc.CreateElement(name);
propertyGroup.AppendChild(newProperty);
}
newProperty.InnerText = value;
}
private void AppendProperties(XmlDocument doc)
{
XmlNode? propertyGroup = doc.SelectSingleNode("//PropertyGroup");
if (propertyGroup == null)
{
propertyGroup = doc.CreateElement("PropertyGroup");
}
AddProperty("CopyLocalLockFileAssembliesName", "true", doc, propertyGroup);
AddProperty("AllowUnsafeBlocks", "true", doc, propertyGroup);
AddProperty("EnableDynamicLoading", "true", doc, propertyGroup);
}
private void AddConstDefine(string value, XmlDocument doc, XmlNode propertyGroup, string? condition = null)
{
var newProperty = doc.CreateElement("DefineConstants");
propertyGroup.AppendChild(newProperty);
newProperty.InnerText = value;
if (condition is not null)
{
newProperty.SetAttribute("Condition", condition);
}
}
private void AppendConstantDefines(XmlDocument doc)
{
var propertyGroup = doc.CreateElement("PropertyGroup");
AddConstDefine("WITH_EDITOR", doc, propertyGroup);
AddConstDefine("$(DefineConstants.Replace('WITH_EDITOR;', '').Replace('WITH_EDITOR', ''))", doc, propertyGroup,
"'$(DisableWithEditor)' == 'true'");
AddConstDefine("$(DefineConstants);$(DefineAdditionalConstants)", doc, propertyGroup,
"'$(DefineAdditionalConstants)' != ''");
}
private string GetPathToBinaries()
{
string directoryPath = Path.GetDirectoryName(_projectPath)!;
string unrealSharpPath = GetRelativePathToUnrealSharp(directoryPath);
return Path.Combine(unrealSharpPath, "Binaries", "Managed");
}
private void AppendReference(XmlDocument doc, XmlElement itemGroup, string referenceName, string binPath)
{
XmlElement referenceElement = doc.CreateElement("Reference");
referenceElement.SetAttribute("Include", referenceName);
XmlElement hintPath = doc.CreateElement("HintPath");
hintPath.InnerText = Path.Combine(binPath, Program.GetVersion(), referenceName + ".dll");
referenceElement.AppendChild(hintPath);
itemGroup.AppendChild(referenceElement);
}
private void AppendSourceGeneratorReference(XmlDocument doc, XmlElement itemGroup)
{
string sourceGeneratorPath = Path.Combine(GetPathToBinaries(), "UnrealSharp.SourceGenerators.dll");
XmlElement sourceGeneratorReference = doc.CreateElement("Analyzer");
sourceGeneratorReference.SetAttribute("Include", sourceGeneratorPath);
itemGroup.AppendChild(sourceGeneratorReference);
}
private void AppendPackageReference(XmlDocument doc, XmlElement itemGroup, string packageName, string packageVersion)
{
XmlElement packageReference = doc.CreateElement("PackageReference");
packageReference.SetAttribute("Include", packageName);
packageReference.SetAttribute("Version", packageVersion);
itemGroup.AppendChild(packageReference);
}
private void AppendGeneratedCode(XmlDocument doc, XmlElement itemGroup)
{
string providedGlueName = Program.TryGetArgument("GlueProjectName");
string scriptFolder = string.IsNullOrEmpty(_projectRoot) ? Program.GetScriptFolder() : Path.Combine(_projectRoot, "Script");
string generatedGluePath = Path.Combine(scriptFolder, providedGlueName, $"{providedGlueName}.csproj");
AddDependency(doc, itemGroup, generatedGluePath);
}
private void AddDependency(XmlDocument doc, XmlElement itemGroup, string dependency)
{
string relativePath = GetRelativePath(_projectFolder, dependency);
XmlElement generatedCode = doc.CreateElement("ProjectReference");
generatedCode.SetAttribute("Include", relativePath);
itemGroup.AppendChild(generatedCode);
}
private string GetRelativePathToUnrealSharp(string basePath)
{
string targetPath = Path.Combine(basePath, Program.BuildToolOptions.PluginDirectory);
return GetRelativePath(basePath, targetPath);
}
public static string GetRelativePath(string basePath, string targetPath)
{
Uri baseUri = new Uri(basePath.EndsWith(Path.DirectorySeparatorChar.ToString())
? basePath
: basePath + Path.DirectorySeparatorChar);
Uri targetUri = new Uri(targetPath);
Uri relativeUri = baseUri.MakeRelativeUri(targetUri);
return OperatingSystem.IsWindows() ? Uri.UnescapeDataString(relativeUri.ToString()).Replace('/', '\\') : Uri.UnescapeDataString(relativeUri.ToString());
}
void AddLaunchSettings()
{
string csProjectPath = Path.Combine(Program.GetScriptFolder(), _projectFolder);
string propertiesDirectoryPath = Path.Combine(csProjectPath, "Properties");
string launchSettingsPath = Path.Combine(propertiesDirectoryPath, "launchSettings.json");
if (!Directory.Exists(propertiesDirectoryPath))
{
Directory.CreateDirectory(propertiesDirectoryPath);
}
if (File.Exists(launchSettingsPath))
{
return;
}
Program.CreateOrUpdateLaunchSettings(launchSettingsPath);
}
}
public class Root
{
[JsonProperty("profiles")]
public Profiles Profiles { get; set; } = new Profiles();
}
public class Profiles
{
[JsonProperty("UnrealSharp")]
public Profile ProfileName { get; set; } = new Profile();
}
public class Profile
{
[JsonProperty("commandName")]
public string CommandName { get; set; } = string.Empty;
[JsonProperty("executablePath")]
public string ExecutablePath { get; set; } = string.Empty;
[JsonProperty("commandLineArgs")]
public string CommandLineArgs { get; set; } = string.Empty;
}

View File

@ -0,0 +1,47 @@
namespace UnrealSharpBuildTool.Actions;
public class GenerateSolution : BuildToolAction
{
public override bool RunAction()
{
using BuildToolProcess generateSln = new BuildToolProcess();
// Create a solution.
generateSln.StartInfo.ArgumentList.Add("new");
generateSln.StartInfo.ArgumentList.Add("sln");
// Assign project name to the solution.
generateSln.StartInfo.ArgumentList.Add("-n");
generateSln.StartInfo.ArgumentList.Add(Program.GetProjectNameAsManaged());
generateSln.StartInfo.WorkingDirectory = Program.GetScriptFolder();
// Force the creation of the solution.
generateSln.StartInfo.ArgumentList.Add("--force");
generateSln.StartBuildToolProcess();
List<string> existingProjectsList = GetExistingProjects()
.Select(x => Path.GetRelativePath(Program.GetScriptFolder(), x))
.ToList();
GenerateProject.AddProjectToSln(existingProjectsList);
return true;
}
private static IEnumerable<string> GetExistingProjects()
{
var scriptsDirectory = new DirectoryInfo(Program.GetScriptFolder());
var pluginsDirectory = new DirectoryInfo(Program.GetPluginsFolder());
return FindCSharpProjects(scriptsDirectory)
.Concat(pluginsDirectory.EnumerateFiles("*.uplugin", SearchOption.AllDirectories)
.Select(x => x.Directory)
.SelectMany(x => x!.EnumerateDirectories("Script"))
.SelectMany(FindCSharpProjects))
.Select(x => x.FullName);
}
private static IEnumerable<FileInfo> FindCSharpProjects(DirectoryInfo directoryInfo)
{
IEnumerable<FileInfo> files = directoryInfo.EnumerateFiles("*.csproj", SearchOption.AllDirectories);
return files;
}
}

View File

@ -0,0 +1,42 @@
using System.Collections.ObjectModel;
namespace UnrealSharpBuildTool.Actions;
public class PackageProject : BuildToolAction
{
public override bool RunAction()
{
string archiveDirectoryPath = Program.TryGetArgument("ArchiveDirectory");
if (string.IsNullOrEmpty(archiveDirectoryPath))
{
throw new Exception("ArchiveDirectory argument is required for the Publish action.");
}
string rootProjectPath = Path.Combine(archiveDirectoryPath, Program.BuildToolOptions.ProjectName);
string binariesPath = Program.GetOutputPath(rootProjectPath);
string bindingsPath = Path.Combine(Program.BuildToolOptions.PluginDirectory, "Managed", "UnrealSharp");
string bindingsOutputPath = Path.Combine(Program.BuildToolOptions.PluginDirectory, "Intermediate", "Build", "Managed");
Collection<string> extraArguments =
[
"--self-contained",
"--runtime",
"win-x64",
"-p:DisableWithEditor=true",
$"-p:PublishDir=\"{binariesPath}\"",
$"-p:OutputPath=\"{bindingsOutputPath}\"",
];
BuildSolution buildBindings = new BuildSolution(bindingsPath, extraArguments, BuildConfig.Publish);
buildBindings.RunAction();
BuildUserSolution buildUserSolution = new BuildUserSolution(null, BuildConfig.Publish);
buildUserSolution.RunAction();
WeaveProject weaveProject = new WeaveProject(binariesPath);
weaveProject.RunAction();
return true;
}
}

View File

@ -0,0 +1,25 @@
using System.Collections.ObjectModel;
namespace UnrealSharpBuildTool.Actions;
public class RebuildSolution : BuildToolAction
{
public override bool RunAction()
{
CleanSolution cleanSolutionProcess = new CleanSolution();
if (!cleanSolutionProcess.RunAction())
{
return false;
}
BuildSolution buildSolution = new BuildUserSolution();
if (!buildSolution.RunAction())
{
return false;
}
return true;
}
}

View File

@ -0,0 +1,77 @@
using System.Collections.Immutable;
using System.Xml;
namespace UnrealSharpBuildTool.Actions;
public class UpdateProjectDependencies : BuildToolAction
{
private string _projectPath = string.Empty;
private string _projectFolder = string.Empty;
private ImmutableList<string> _dependencies = ImmutableList<string>.Empty;
private HashSet<string> _existingDependencies = new HashSet<string>();
public override bool RunAction()
{
_projectPath = Program.TryGetArgument("ProjectPath");
_projectFolder = Directory.GetParent(_projectPath)!.FullName;
_dependencies = Program.GetArguments("Dependency").ToImmutableList();
Console.WriteLine($"Project Path: {_projectPath}");
Console.WriteLine($"Project Folder: {_projectFolder}");
UpdateProject();
return true;
}
private void UpdateProject()
{
try
{
XmlDocument csprojDocument = new XmlDocument();
csprojDocument.Load(_projectPath);
XmlNodeList itemGroups = csprojDocument.SelectNodes("//ItemGroup")!;
_existingDependencies = itemGroups
.OfType<XmlElement>()
.Where(x => x.Name == "ItemGroup")
.SelectMany(x => x.ChildNodes.OfType<XmlElement>())
.Where(x => x.Name == "ProjectReference")
.Select(x => x.GetAttribute("Include"))
.ToHashSet();
XmlElement? newItemGroup = itemGroups.OfType<XmlElement>().FirstOrDefault();
if (newItemGroup is null)
{
newItemGroup = csprojDocument.CreateElement("ItemGroup");
csprojDocument.DocumentElement!.AppendChild(newItemGroup);
}
foreach (string dependency in _dependencies)
{
AddDependency(csprojDocument, newItemGroup, dependency);
}
csprojDocument.Save(_projectPath);
}
catch (Exception ex)
{
throw new InvalidOperationException($"An error occurred while updating the .csproj file: {ex.Message}", ex);
}
}
private void AddDependency(XmlDocument doc, XmlElement itemGroup, string dependency)
{
string relativePath = GenerateProject.GetRelativePath(_projectFolder, dependency);
if (_existingDependencies.Contains(relativePath))
{
return;
}
XmlElement generatedCode = doc.CreateElement("ProjectReference");
generatedCode.SetAttribute("Include", relativePath);
itemGroup.AppendChild(generatedCode);
}
}

View File

@ -0,0 +1,64 @@
namespace UnrealSharpBuildTool.Actions;
public class WeaveProject : BuildToolAction
{
readonly string _outputDirectory;
public WeaveProject(string outputDirectory = "")
{
_outputDirectory = string.IsNullOrEmpty(outputDirectory) ? Program.GetOutputPath() : outputDirectory;
}
public override bool RunAction()
{
string weaverPath = Program.GetWeaver();
if (!File.Exists(weaverPath))
{
throw new Exception("Couldn't find the weaver");
}
DirectoryInfo scriptRootDirInfo = new DirectoryInfo(Program.GetProjectDirectory());
return Weave(scriptRootDirInfo, weaverPath);
}
private bool Weave(DirectoryInfo scriptFolder, string weaverPath)
{
Dictionary<string, List<FileInfo>> projectFiles = Program.GetProjectFilesByDirectory(scriptFolder);
List<FileInfo> allProjectFiles = projectFiles.Values.SelectMany(x => x).ToList();
if (allProjectFiles.Count == 0)
{
Console.WriteLine("No project files found. Skipping weaving...");
return true;
}
using BuildToolProcess weaveProcess = new BuildToolProcess();
weaveProcess.StartInfo.ArgumentList.Add(weaverPath);
weaveProcess.StartInfo.WorkingDirectory = Directory.GetCurrentDirectory();
bool foundValidProject = false;
foreach (FileInfo projectFile in allProjectFiles)
{
weaveProcess.StartInfo.ArgumentList.Add("-p");
string csProjName = Path.GetFileNameWithoutExtension(projectFile.Name);
string assemblyPath = Path.Combine(projectFile.DirectoryName!, "bin",
Program.GetBuildConfiguration(), Program.GetVersion(), csProjName + ".dll");
weaveProcess.StartInfo.ArgumentList.Add(assemblyPath);
foundValidProject = true;
}
if (!foundValidProject)
{
Console.WriteLine("No valid project found to weave. Skipping weaving...");
return true;
}
// Add path to the output folder for the weaver.
weaveProcess.StartInfo.ArgumentList.Add("-o");
weaveProcess.StartInfo.ArgumentList.Add($"{Program.FixPath(_outputDirectory)}");
return weaveProcess.StartBuildToolProcess();
}
}

View File

@ -0,0 +1,83 @@
using System.Reflection;
using CommandLine;
using CommandLine.Text;
namespace UnrealSharpBuildTool;
public enum BuildAction : int
{
Build,
Clean,
GenerateProject,
UpdateProjectDependencies,
Rebuild,
Weave,
PackageProject,
GenerateSolution,
BuildWeave,
}
public enum BuildConfig : int
{
Debug,
Release,
Publish,
}
public class BuildToolOptions
{
[Option("Action", Required = true, HelpText = "The action the build tool should process. Possible values: Build, Clean, GenerateProject, Rebuild, Weave, PackageProject, GenerateSolution, BuildWeave.")]
public BuildAction Action { get; set; }
[Option("DotNetPath", Required = false, HelpText = "The path to the dotnet.exe")]
public string DotNetPath { get; set; } = string.Empty;
[Option("ProjectDirectory", Required = true, HelpText = "The directory where the .uproject file resides.")]
public string ProjectDirectory { get; set; } = string.Empty;
[Option("PluginDirectory", Required = false, HelpText = "The UnrealSharp plugin directory.")]
public string PluginDirectory { get; set; } = string.Empty;
[Option("EngineDirectory", Required = false, HelpText = "The Unreal Engine directory.")]
public string EngineDirectory { get; set; } = string.Empty;
[Option("ProjectName", Required = true, HelpText = "The name of the Unreal Engine project.")]
public string ProjectName { get; set; } = string.Empty;
[Option("AdditionalArgs", Required = false, HelpText = "Additional key-value arguments for the build tool.")]
public IEnumerable<string> AdditionalArgs { get; set; } = new List<string>();
public string TryGetArgument(string argument)
{
return GetArguments(argument).FirstOrDefault() ?? string.Empty;
}
public IEnumerable<string> GetArguments(string argument)
{
return AdditionalArgs.Where(arg => arg.StartsWith(argument))
.Select(arg => arg[(argument.Length + 1)..]);
}
public bool HasArgument(string argument)
{
return AdditionalArgs.Any(arg => arg.StartsWith(argument));
}
public static void PrintHelp(ParserResult<BuildToolOptions> result)
{
string name = Path.GetFileNameWithoutExtension(Assembly.GetEntryAssembly()!.Location);
Console.Error.WriteLine($"Usage: {name} [options]");
Console.Error.WriteLine("Options:");
var helpText = HelpText.AutoBuild(result, h => h, e => e);
Console.WriteLine(helpText);
}
public void NormalizePaths()
{
ProjectDirectory = Path.GetFullPath(ProjectDirectory);
PluginDirectory = Path.GetFullPath(PluginDirectory);
EngineDirectory = Path.GetFullPath(EngineDirectory);
DotNetPath = Path.GetFullPath(DotNetPath);
}
}

View File

@ -0,0 +1,74 @@
using System.Collections;
using System.Diagnostics;
using System.Text;
namespace UnrealSharpBuildTool;
public class BuildToolProcess : Process
{
public BuildToolProcess(string? fileName = null)
{
if (fileName == null)
{
if (string.IsNullOrEmpty(Program.BuildToolOptions.DotNetPath))
{
fileName = "dotnet";
}
else
{
fileName = Program.BuildToolOptions.DotNetPath;
}
}
StartInfo.FileName = fileName;
StartInfo.CreateNoWindow = true;
StartInfo.ErrorDialog = false;
StartInfo.UseShellExecute = false;
StartInfo.RedirectStandardError = true;
StartInfo.RedirectStandardInput = true;
StartInfo.RedirectStandardOutput = true;
EnableRaisingEvents = true;
}
public bool StartBuildToolProcess()
{
StringBuilder output = new StringBuilder();
OutputDataReceived += (sender, e) =>
{
if (e.Data != null)
{
output.AppendLine(e.Data);
}
};
ErrorDataReceived += (sender, e) =>
{
if (e.Data != null)
{
output.AppendLine(e.Data);
}
};
if (!Start())
{
throw new Exception("Failed to start process");
}
BeginErrorReadLine();
BeginOutputReadLine();
WaitForExit();
if (ExitCode != 0)
{
string errorMessage = output.ToString();
if (string.IsNullOrEmpty(errorMessage))
{
errorMessage = "BuildTool process exited with non-zero exit code, but no output was captured.";
}
throw new Exception($"BuildTool process failed with exit code {ExitCode}:\n{errorMessage}");
}
return true;
}
}

View File

@ -0,0 +1,258 @@
using System.Xml.Linq;
using CommandLine;
using Newtonsoft.Json;
using UnrealSharpBuildTool.Actions;
namespace UnrealSharpBuildTool;
public static class Program
{
public static BuildToolOptions BuildToolOptions = null!;
public static int Main(string[] args)
{
try
{
Console.WriteLine(">>> UnrealSharpBuildTool");
Parser parser = new Parser(with => with.HelpWriter = null);
ParserResult<BuildToolOptions> result = parser.ParseArguments<BuildToolOptions>(args);
if (result.Tag == ParserResultType.NotParsed)
{
BuildToolOptions.PrintHelp(result);
string errors = string.Empty;
foreach (Error error in result.Errors)
{
if (error is TokenError tokenError)
{
errors += $"{tokenError.Tag}: {tokenError.Token} \n";
}
}
throw new Exception($"Invalid arguments. Errors: {errors}");
}
BuildToolOptions = result.Value;
if (!BuildToolAction.InitializeAction())
{
return 1;
}
Console.WriteLine($"UnrealSharpBuildTool executed {BuildToolOptions.Action.ToString()} action successfully.");
}
catch (Exception exception)
{
Console.WriteLine("An error occurred: " + exception.Message + Environment.NewLine + exception.StackTrace);
return 1;
}
return 0;
}
public static string TryGetArgument(string argument)
{
return BuildToolOptions.TryGetArgument(argument);
}
public static IEnumerable<string> GetArguments(string argument)
{
return BuildToolOptions.GetArguments(argument);
}
public static bool HasArgument(string argument)
{
return BuildToolOptions.HasArgument(argument);
}
public static string GetSolutionFile()
{
return Path.Combine(GetScriptFolder(), BuildToolOptions.ProjectName + ".sln");
}
public static string GetUProjectFilePath()
{
return Path.Combine(BuildToolOptions.ProjectDirectory, BuildToolOptions.ProjectName + ".uproject");
}
public static string GetBuildConfiguration()
{
string buildConfig = TryGetArgument("BuildConfig");
if (string.IsNullOrEmpty(buildConfig))
{
buildConfig = "Debug";
}
return buildConfig;
}
public static BuildConfig GetBuildConfig()
{
string buildConfig = GetBuildConfiguration();
Enum.TryParse(buildConfig, out BuildConfig config);
return config;
}
public static string GetBuildConfiguration(BuildConfig buildConfig)
{
return buildConfig switch
{
BuildConfig.Debug => "Debug",
BuildConfig.Release => "Release",
BuildConfig.Publish => "Release",
_ => "Release"
};
}
public static string GetScriptFolder()
{
return Path.Combine(BuildToolOptions.ProjectDirectory, "Script");
}
public static string GetPluginsFolder()
{
return Path.Combine(BuildToolOptions.ProjectDirectory, "Plugins");
}
public static string GetProjectDirectory()
{
return BuildToolOptions.ProjectDirectory;
}
public static string FixPath(string path)
{
if (OperatingSystem.IsWindows())
{
return path.Replace('/', '\\');
}
return path;
}
public static string GetProjectNameAsManaged()
{
return "Managed" + BuildToolOptions.ProjectName;
}
public static string GetOutputPath(string rootDir = "")
{
if (string.IsNullOrEmpty(rootDir))
{
rootDir = BuildToolOptions.ProjectDirectory;
}
return Path.Combine(rootDir, "Binaries", "Managed");
}
public static string GetWeaver()
{
return Path.Combine(GetManagedBinariesDirectory(), "UnrealSharpWeaver.dll");
}
public static string GetManagedBinariesDirectory()
{
return Path.Combine(BuildToolOptions.PluginDirectory, "Binaries", "Managed");
}
public static string GetVersion()
{
Version currentVersion = Environment.Version;
string currentVersionStr = $"{currentVersion.Major}.{currentVersion.Minor}";
return "net" + currentVersionStr;
}
public static void CreateOrUpdateLaunchSettings(string launchSettingsPath)
{
Root root = new Root();
string executablePath = string.Empty;
if (OperatingSystem.IsWindows())
{
executablePath = Path.Combine(BuildToolOptions.EngineDirectory, "Binaries", "Win64", "UnrealEditor.exe");
}
else if (OperatingSystem.IsMacOS())
{
executablePath = Path.Combine(BuildToolOptions.EngineDirectory, "Binaries", "Mac", "UnrealEditor");
}
string commandLineArgs = FixPath(GetUProjectFilePath());
// Create a new profile if it doesn't exist
if (root.Profiles == null)
{
root.Profiles = new Profiles();
}
root.Profiles.ProfileName = new Profile
{
CommandName = "Executable",
ExecutablePath = executablePath,
CommandLineArgs = $"\"{commandLineArgs}\"",
};
string newJsonString = JsonConvert.SerializeObject(root, Formatting.Indented);
StreamWriter writer = File.CreateText(launchSettingsPath);
writer.Write(newJsonString);
writer.Close();
}
public static List<FileInfo> GetAllProjectFiles(DirectoryInfo folder)
{
return folder.GetDirectories("Script")
.SelectMany(GetProjectsInDirectory)
.Concat(folder.GetDirectories("Plugins")
.SelectMany(x => x.EnumerateFiles("*.uplugin", SearchOption.AllDirectories))
.Select(x => x.Directory)
.Select(x => x!.GetDirectories("Script").FirstOrDefault())
.Where(x => x is not null)
.SelectMany(GetProjectsInDirectory!))
.ToList();
}
public static Dictionary<string, List<FileInfo>> GetProjectFilesByDirectory(DirectoryInfo folder)
{
Dictionary<string, List<FileInfo>> result = new Dictionary<string, List<FileInfo>>();
DirectoryInfo? scriptsFolder = folder.GetDirectories("Script").FirstOrDefault();
if (scriptsFolder is not null)
{
result.Add(GetOutputPathForDirectory(scriptsFolder), GetProjectsInDirectory(scriptsFolder).ToList());
}
foreach (DirectoryInfo? pluginFolder in folder.GetDirectories("Plugins")
.SelectMany(x => x.EnumerateFiles("*.uplugin", SearchOption.AllDirectories))
.Select(x => x.Directory)
.Select(x => x!.GetDirectories("Script").FirstOrDefault())
.Where(x => x is not null))
{
result.Add(GetOutputPathForDirectory(pluginFolder!), GetProjectsInDirectory(pluginFolder!).ToList());
}
return result;
}
private static string GetOutputPathForDirectory(DirectoryInfo directory)
{
return Path.Combine(directory.Parent!.FullName, "Binaries", "Managed");
}
private static IEnumerable<FileInfo> GetProjectsInDirectory(DirectoryInfo folder)
{
var csprojFiles = folder.EnumerateFiles("*.csproj", SearchOption.AllDirectories);
var fsprojFiles = folder.EnumerateFiles("*.fsproj", SearchOption.AllDirectories);
return csprojFiles.Concat(fsprojFiles).Where(IsWeavableProject);
}
private static bool IsWeavableProject(FileInfo projectFile)
{
// We need to be able to filter out certain non-production projects.
// The main target of this is source generators and analyzers which users
// may want to leverage as part of their solution and can't be weaved because
// they have to use netstandard2.0.
XDocument doc = XDocument.Load(projectFile.FullName);
return !doc.Descendants()
.Where(element => element.Name.LocalName == "PropertyGroup")
.SelectMany(element => element.Elements())
.Any(element => element.Name.LocalName == "ExcludeFromWeaver" &&
element.Value.Equals("true", StringComparison.OrdinalIgnoreCase));
}
}

View File

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CommandLineParser" />
<PackageReference Include="Newtonsoft.Json" />
</ItemGroup>
</Project>