Skip to content

Commit

Permalink
Implement editor integration
Browse files Browse the repository at this point in the history
- Projects that implement the integration of Godot .NET with the Godot editor:
  - `Godot.EditorIntegration` to implement the editor and export plugins, the exporter platforms, etc.
  - `Godot.EditorIntegration.MSBuildLogger` to collect diagnostics and log the build output.
  - `Godot.EditorIntegration.OpenVisualStudio` to open the Visual Studio IDE as an external code editor.
- `Godot.PluginLoader` to load .NET assemblies when in the editor using ALCs.
  • Loading branch information
raulsntos committed Sep 30, 2024
1 parent 6bd7581 commit ce3bb41
Show file tree
Hide file tree
Showing 78 changed files with 6,602 additions and 0 deletions.
28 changes: 28 additions & 0 deletions Godot.sln
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Godot.Bindings", "src\Godot
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Godot.SourceGenerators", "src\Godot.SourceGenerators\Godot.SourceGenerators.csproj", "{E90FA2F5-90B5-4A70-AB9D-18ED4CD08E81}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Godot.PluginLoader", "src\Godot.PluginLoader\Godot.PluginLoader.csproj", "{09F7C19C-169A-4C04-A667-FE51B3372859}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Godot.EditorIntegration", "src\Godot.EditorIntegration\Godot.EditorIntegration.csproj", "{479EC232-AD97-4A00-876E-00A409E5065F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Godot.EditorIntegration.MSBuildLogger", "src\Godot.EditorIntegration.MSBuildLogger\Godot.EditorIntegration.MSBuildLogger.csproj", "{026F4A94-1D82-4029-B135-665488B7BEDD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Godot.EditorIntegration.OpenVisualStudio", "src\Godot.EditorIntegration.OpenVisualStudio\Godot.EditorIntegration.OpenVisualStudio.csproj", "{F96DED1A-A50E-4044-8C9C-E9F906C9D2B2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Godot.BindingsGenerator.Tests", "tests\Godot.BindingsGenerator.Tests\Godot.BindingsGenerator.Tests.csproj", "{565D82E3-9327-4065-BE7D-533D84EB6036}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Godot.Bindings.Tests", "tests\Godot.Bindings.Tests\Godot.Bindings.Tests.csproj", "{9E69D61E-3392-4432-BADA-B7B15CF86017}"
Expand Down Expand Up @@ -59,6 +67,22 @@ Global
{E90FA2F5-90B5-4A70-AB9D-18ED4CD08E81}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E90FA2F5-90B5-4A70-AB9D-18ED4CD08E81}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E90FA2F5-90B5-4A70-AB9D-18ED4CD08E81}.Release|Any CPU.Build.0 = Release|Any CPU
{09F7C19C-169A-4C04-A667-FE51B3372859}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{09F7C19C-169A-4C04-A667-FE51B3372859}.Debug|Any CPU.Build.0 = Debug|Any CPU
{09F7C19C-169A-4C04-A667-FE51B3372859}.Release|Any CPU.ActiveCfg = Release|Any CPU
{09F7C19C-169A-4C04-A667-FE51B3372859}.Release|Any CPU.Build.0 = Release|Any CPU
{479EC232-AD97-4A00-876E-00A409E5065F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{479EC232-AD97-4A00-876E-00A409E5065F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{479EC232-AD97-4A00-876E-00A409E5065F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{479EC232-AD97-4A00-876E-00A409E5065F}.Release|Any CPU.Build.0 = Release|Any CPU
{026F4A94-1D82-4029-B135-665488B7BEDD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{026F4A94-1D82-4029-B135-665488B7BEDD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{026F4A94-1D82-4029-B135-665488B7BEDD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{026F4A94-1D82-4029-B135-665488B7BEDD}.Release|Any CPU.Build.0 = Release|Any CPU
{F96DED1A-A50E-4044-8C9C-E9F906C9D2B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F96DED1A-A50E-4044-8C9C-E9F906C9D2B2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F96DED1A-A50E-4044-8C9C-E9F906C9D2B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F96DED1A-A50E-4044-8C9C-E9F906C9D2B2}.Release|Any CPU.Build.0 = Release|Any CPU
{565D82E3-9327-4065-BE7D-533D84EB6036}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{565D82E3-9327-4065-BE7D-533D84EB6036}.Debug|Any CPU.Build.0 = Debug|Any CPU
{565D82E3-9327-4065-BE7D-533D84EB6036}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand Down Expand Up @@ -86,6 +110,10 @@ Global
{5500C1BD-81B2-4CD9-A530-67AE8FCDC6A2} = {9C93C9B0-F173-456D-AD30-751731192FF0}
{3916AEB4-BE8A-413C-8369-440ADA87E119} = {9C93C9B0-F173-456D-AD30-751731192FF0}
{E90FA2F5-90B5-4A70-AB9D-18ED4CD08E81} = {9C93C9B0-F173-456D-AD30-751731192FF0}
{09F7C19C-169A-4C04-A667-FE51B3372859} = {9C93C9B0-F173-456D-AD30-751731192FF0}
{479EC232-AD97-4A00-876E-00A409E5065F} = {9C93C9B0-F173-456D-AD30-751731192FF0}
{026F4A94-1D82-4029-B135-665488B7BEDD} = {9C93C9B0-F173-456D-AD30-751731192FF0}
{F96DED1A-A50E-4044-8C9C-E9F906C9D2B2} = {9C93C9B0-F173-456D-AD30-751731192FF0}
{565D82E3-9327-4065-BE7D-533D84EB6036} = {409FDDB1-07E1-40F7-AB83-2CDF7837AD4C}
{9E69D61E-3392-4432-BADA-B7B15CF86017} = {409FDDB1-07E1-40F7-AB83-2CDF7837AD4C}
{B63C79A4-5FBA-4E7B-A2E5-69868CCC2DE0} = {409FDDB1-07E1-40F7-AB83-2CDF7837AD4C}
Expand Down
4 changes: 4 additions & 0 deletions eng/Versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,9 @@
<CoverletCollectorVersion>6.0.2</CoverletCollectorVersion>
<!-- ClangSharp -->
<ClangSharpVersion>18.1.0</ClangSharpVersion>
<!-- EnvDTE -->
<EnvDTEVersion>17.8.37221</EnvDTEVersion>
<!-- JetBrains -->
<JetBrainsRiderPathLocatorVersion>1.0.11</JetBrainsRiderPathLocatorVersion>
</PropertyGroup>
</Project>
1 change: 1 addition & 0 deletions src/Godot.Bindings/Godot.Bindings.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
</PropertyGroup> -->

<ItemGroup>
<InternalsVisibleTo Include="Godot.EditorIntegration" />
<InternalsVisibleTo Include="Godot.Bindings.Tests" />
</ItemGroup>

Expand Down
1 change: 1 addition & 0 deletions src/Godot.BindingsGenerator/Utils/NamingUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ internal static class NamingUtils
{ "HBOX", "HBox" }, // Horizontal Box
{ "ID", "Id" },
{ "IO", "IO" }, // Input/Output
{ "IOS", "IOS" }, // iOS
{ "IP", "IP" }, // Internet Protocol
{ "IV", "IV" }, // Initialization Vector
{ "MACOS", "MacOS" },
Expand Down
223 changes: 223 additions & 0 deletions src/Godot.EditorIntegration.MSBuildLogger/BuildLogger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
using System;
using System.CodeDom.Compiler;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Security;
using System.Text;
using Microsoft.Build.Framework;

namespace Godot.EditorIntegration.MSBuildLogger;

// IMPORTANT: This class must be public so MSBuild can find it.
/// <summary>
/// Implementation of <see cref="ILogger"/> used by the editor integration when building .NET projects
/// to collect diagnostics and display them in Godot.
/// </summary>
public sealed class BuildLogger : ILogger, IDisposable
{
// Keep in sync with the constants in 'Godot.EditorIntegration.Build.BuildManager'.
private const string MSBuildIssuesFileName = "msbuild_issues.csv";
private const string MSBuildLogFileName = "msbuild_log.txt";
private const string MSBuildBinaryLogFileName = "msbuild.binlog";

/// <inheritdoc/>
public string? Parameters { get; set; }

/// <inheritdoc/>
public LoggerVerbosity Verbosity { get; set; }

private IndentedTextWriter? _logWriter;
private TextWriter? _issuesWriter;

/// <inheritdoc/>
public void Initialize(IEventSource eventSource)
{
if (string.IsNullOrEmpty(Parameters))
{
throw new LoggerException(SR.Logger_LogDirectoryParameterNotSpecified);
}

string[] parameters = Parameters.Split(';');

string logDir = parameters[0];

if (string.IsNullOrEmpty(logDir))
{
throw new LoggerException(SR.Logger_LogDirectoryParameterIsEmpty);
}

if (parameters.Length > 1)
{
throw new LoggerException(SR.Logger_TooManyParametersPassed);
}

string logFile = Path.Join(logDir, MSBuildLogFileName);
string issuesFile = Path.Join(logDir, MSBuildIssuesFileName);

try
{
if (!Directory.Exists(logDir))
{
Directory.CreateDirectory(logDir);
}

_logWriter = new IndentedTextWriter(new StreamWriter(logFile), "\t");
_issuesWriter = new StreamWriter(issuesFile);
}
catch (Exception ex)
{
if (ex is UnauthorizedAccessException
|| ex is ArgumentNullException
|| ex is PathTooLongException
|| ex is DirectoryNotFoundException
|| ex is NotSupportedException
|| ex is ArgumentException
|| ex is SecurityException
|| ex is IOException)
{
throw new LoggerException(SR.FormatLogger_CreateLogFileFailed(ex.Message));
}

// Unexpected failure.
throw;
}

eventSource.ProjectStarted += EventSource_ProjectStarted;
eventSource.ProjectFinished += EventSource_ProjectFinished;
eventSource.MessageRaised += EventSource_MessageRaised;
eventSource.WarningRaised += EventSource_WarningRaised;
eventSource.ErrorRaised += EventSource_ErrorRaised;
}

private void EventSource_ProjectStarted(object sender, ProjectStartedEventArgs e)
{
WriteLine(e.Message);
if (_logWriter is not null)
{
_logWriter.Indent++;
}
}

private void EventSource_ProjectFinished(object sender, ProjectFinishedEventArgs e)
{
if (_logWriter is not null)
{
_logWriter.Indent--;
}
WriteLine(e.Message);
}

private void LogBuildEvent(string file, int lineNumber, int columnNumber, string severity, string diagnosticId, string? message, string? projectFile)
{
var sb = new StringBuilder();

// Log event to the log file.
sb.Append(file);
sb.Append(CultureInfo.InvariantCulture, $"({lineNumber},{columnNumber})");
sb.Append(CultureInfo.InvariantCulture, $": {severity} {diagnosticId}: {message}");
if (string.IsNullOrEmpty(projectFile))
{
sb.Append(CultureInfo.InvariantCulture, $" [{projectFile}]");
}
WriteLine(sb.ToString());

sb.Clear();

// Log event to the CSV file.
sb.Append(severity);
sb.Append(',');
sb.Append(CsvEscape(file));
sb.Append(',');
sb.Append(lineNumber);
sb.Append(',');
sb.Append(columnNumber);
sb.Append(',');
sb.Append(CsvEscape(diagnosticId));
sb.Append(',');
sb.Append(CsvEscape(message));
sb.Append(',');
sb.Append(CsvEscape(projectFile));
_issuesWriter?.WriteLine(sb.ToString());
}

private void EventSource_ErrorRaised(object sender, BuildErrorEventArgs e)
{
LogBuildEvent(e.File, e.LineNumber, e.ColumnNumber, "error", e.Code, e.Message, e.ProjectFile);
}

private void EventSource_WarningRaised(object sender, BuildWarningEventArgs e)
{
LogBuildEvent(e.File, e.LineNumber, e.ColumnNumber, "warning", e.Code, e.Message, e.ProjectFile);
}

private void EventSource_MessageRaised(object sender, BuildMessageEventArgs e)
{
// BuildMessageEventArgs adds Importance to BuildEventArgs.
// Let's take account of the verbosity setting we've been passed in deciding whether to log the message.
if (e.Importance == MessageImportance.High && IsVerbosityAtLeast(LoggerVerbosity.Minimal)
|| e.Importance == MessageImportance.Normal && IsVerbosityAtLeast(LoggerVerbosity.Normal)
|| e.Importance == MessageImportance.Low && IsVerbosityAtLeast(LoggerVerbosity.Detailed))
{
WriteLineWithSenderAndMessage(string.Empty, e);
}

bool IsVerbosityAtLeast(LoggerVerbosity checkVerbosity)
{
return Verbosity >= checkVerbosity;
}
}

/// <summary>
/// Write a line to the log, adding the SenderName and Message
/// (these parameters are on all MSBuild event argument objects)
/// </summary>
private void WriteLineWithSenderAndMessage(string line, BuildEventArgs e)
{
if (string.Equals(e.SenderName, "MSBuild", StringComparison.OrdinalIgnoreCase))
{
// Well, if the sender name is MSBuild, let's leave it out for prettiness.
WriteLine($"{line}{e.Message}");
}
else
{
WriteLine($"{e.SenderName}: {line}{e.Message}");
}
}

private void WriteLine(string line)
{
_logWriter?.WriteLine(line);
}

/// <inheritdoc/>
public void Shutdown()
{
Dispose();
}

/// <inheritdoc/>
public void Dispose()
{
_logWriter?.Dispose();
_issuesWriter?.Dispose();
}

[return: NotNullIfNotNull(nameof(value))]
private static string? CsvEscape(string? value, char delimiter = ',')
{
if (string.IsNullOrEmpty(value))
{
return value;
}

bool hasSpecialChar = value.IndexOfAny(['\"', '\n', '\r', delimiter]) != -1;

if (hasSpecialChar)
{
return $"\"{value.Replace("\"", "\"\"")}\"";
}

return value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<IsTrimmable>true</IsTrimmable>
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<EnablePackageValidation>true</EnablePackageValidation>
</PropertyGroup>

<PropertyGroup>
<IsPackable>true</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Build" Version="$(MicrosoftBuildVersion)" ExcludeAssets="runtime" />
</ItemGroup>

<ItemGroup>
<EmbeddedResource Update="Resources\Strings.resx">
<ClassName>Godot.EditorIntegration.MSBuildLogger.SR</ClassName>
<EmitFormatMethods>true</EmitFormatMethods>
</EmbeddedResource>
</ItemGroup>

</Project>
5 changes: 5 additions & 0 deletions src/Godot.EditorIntegration.MSBuildLogger/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Godot.EditorIntegration.MSBuildLogger

Build logger used by the Godot editor integration when building .NET projects to collect diagnostics and log the build output.

This package should not be referenced directly by users, it will be retrieved on-demand by the Godot editor.
Loading

0 comments on commit ce3bb41

Please sign in to comment.