Files
ft/Client/Packages/io.github.hatayama.uloopmcp/Editor/Compilation/Templates/SharedRoslynCompilerWorkerProgram.cs.template

323 lines
11 KiB
Plaintext

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;
using Microsoft.CodeAnalysis.Text;
public static class Program
{
private const string ResultPrefix = "{{SHARED_COMPILER_WORKER_RESULT_PREFIX}}";
private const string EndMarker = "{{SHARED_COMPILER_WORKER_END_MARKER}}";
private const string QuitCommand = "{{SHARED_COMPILER_WORKER_QUIT_COMMAND}}";
private const string RequestPathPrefix = "{{COMPILE_REQUEST_PATH_PREFIX}}";
private const string UnsafePrefix = "unsafe:";
private const string DefinePrefix = "define:";
private const string ReferencePrefix = "ref:";
private static readonly object SyncRoot = new object();
private static readonly Dictionary<string, CachedReference> Cache = new Dictionary<string, CachedReference>(StringComparer.OrdinalIgnoreCase);
public static int Main()
{
string requestPath;
while ((requestPath = Console.ReadLine()) != null)
{
if (requestPath == QuitCommand)
{
return 0;
}
CompileResponse response = Compile(DecodeRequestPath(requestPath));
Console.WriteLine(ResultPrefix + " " + response.ExitCode);
foreach (string diagnosticLine in response.DiagnosticLines)
{
Console.WriteLine(diagnosticLine);
}
Console.WriteLine(EndMarker);
Console.Out.Flush();
}
return 0;
}
private static string DecodeRequestPath(string requestPath)
{
int encodedPathIndex = FindRequestPathPrefixIndex(requestPath);
if (encodedPathIndex >= 0)
{
string encodedPath = requestPath.Substring(encodedPathIndex + RequestPathPrefix.Length);
if (!IsBase64Payload(encodedPath))
{
return RecoverRawRequestPath(requestPath);
}
byte[] requestPathBytes = Convert.FromBase64String(encodedPath);
return Encoding.UTF8.GetString(requestPathBytes);
}
return RecoverRawRequestPath(requestPath);
}
private static bool IsBase64Payload(string value)
{
if (string.IsNullOrEmpty(value) || value.Length % 4 != 0 || !HasValidBase64Padding(value))
{
return false;
}
for (int i = 0; i < value.Length; i++)
{
char character = value[i];
bool isBase64Character = character >= 'A' && character <= 'Z'
|| character >= 'a' && character <= 'z'
|| character >= '0' && character <= '9'
|| character == '+'
|| character == '/'
|| character == '=';
if (!isBase64Character)
{
return false;
}
}
return true;
}
private static bool HasValidBase64Padding(string value)
{
int firstPaddingIndex = value.IndexOf('=');
if (firstPaddingIndex < 0)
{
return true;
}
int paddingLength = value.Length - firstPaddingIndex;
if (paddingLength > 2)
{
return false;
}
for (int i = firstPaddingIndex; i < value.Length; i++)
{
if (value[i] != '=')
{
return false;
}
}
return true;
}
private static int FindRequestPathPrefixIndex(string requestPath)
{
int encodedPathIndex = requestPath.IndexOf(RequestPathPrefix, StringComparison.Ordinal);
if (encodedPathIndex <= 0)
{
return encodedPathIndex;
}
return HasDirectorySeparatorBeforePrefix(requestPath, encodedPathIndex) ? -1 : encodedPathIndex;
}
private static bool HasDirectorySeparatorBeforePrefix(string requestPath, int prefixIndex)
{
for (int i = 0; i < prefixIndex; i++)
{
if (requestPath[i] == '\\' || requestPath[i] == '/')
{
return true;
}
}
return false;
}
private static string RecoverRawRequestPath(string requestPath)
{
string trimmedPath = requestPath.TrimStart('\uFEFF', '\uFFFD');
int drivePathIndex = FindWindowsDrivePathIndex(trimmedPath);
return drivePathIndex > 0 ? trimmedPath.Substring(drivePathIndex) : trimmedPath;
}
private static int FindWindowsDrivePathIndex(string value)
{
for (int i = 0; i <= value.Length - 3; i++)
{
bool isDriveLetter = (value[i] >= 'A' && value[i] <= 'Z') || (value[i] >= 'a' && value[i] <= 'z');
if (isDriveLetter && value[i + 1] == ':' && (value[i + 2] == '\\' || value[i + 2] == '/'))
{
return i;
}
}
return -1;
}
private static CompileResponse Compile(string requestPath)
{
string[] requestLines = File.ReadAllLines(requestPath);
string sourcePath = requestLines[0];
string dllPath = requestLines[1];
bool allowUnsafe = ParseAllowUnsafe(requestLines);
string[] defineSymbols = ParseDefineSymbols(requestLines);
string source = File.ReadAllText(sourcePath, Encoding.UTF8);
SourceText sourceText = SourceText.From(source, Encoding.UTF8);
SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(sourceText, CreateParseOptions(defineSymbols), sourcePath);
List<MetadataReference> references = BuildReferences(requestLines);
CSharpCompilation compilation = CSharpCompilation.Create(Path.GetFileNameWithoutExtension(dllPath), new[] { syntaxTree }, references, CreateCompilationOptions(allowUnsafe));
using (FileStream peStream = new FileStream(dllPath, FileMode.Create, FileAccess.Write, FileShare.Read))
{
EmitResult emitResult = compilation.Emit(peStream);
int exitCode = 0;
List<string> diagnosticLines = new List<string>();
foreach (Diagnostic diagnostic in emitResult.Diagnostics)
{
if (diagnostic.Severity != DiagnosticSeverity.Error && diagnostic.Severity != DiagnosticSeverity.Warning)
{
continue;
}
if (diagnostic.Severity == DiagnosticSeverity.Error)
{
exitCode = 1;
}
FileLinePositionSpan lineSpan = diagnostic.Location != Location.None ? diagnostic.Location.GetMappedLineSpan() : default(FileLinePositionSpan);
int line = lineSpan.StartLinePosition.Line + 1;
int column = lineSpan.StartLinePosition.Character + 1;
string file = string.IsNullOrEmpty(lineSpan.Path) ? sourcePath : lineSpan.Path;
string severity = diagnostic.Severity == DiagnosticSeverity.Warning ? "warning" : "error";
diagnosticLines.Add(file + "(" + line + "," + column + "): " + severity + " " + diagnostic.Id + ": " + diagnostic.GetMessage());
}
return new CompileResponse(exitCode, diagnosticLines);
}
}
private static CSharpParseOptions CreateParseOptions(string[] defineSymbols)
{
return defineSymbols == null || defineSymbols.Length == 0
? CSharpParseOptions.Default
: CSharpParseOptions.Default.WithPreprocessorSymbols(defineSymbols);
}
private static CSharpCompilationOptions CreateCompilationOptions(bool allowUnsafe)
{
return new CSharpCompilationOptions(
OutputKind.DynamicallyLinkedLibrary,
optimizationLevel: OptimizationLevel.Release,
allowUnsafe: allowUnsafe);
}
private static bool ParseAllowUnsafe(string[] requestLines)
{
for (int i = 2; i < requestLines.Length; i++)
{
string line = requestLines[i];
if (!line.StartsWith(UnsafePrefix, StringComparison.Ordinal))
{
continue;
}
return string.Equals(line.Substring(UnsafePrefix.Length), "1", StringComparison.Ordinal);
}
return false;
}
private static string[] ParseDefineSymbols(string[] requestLines)
{
for (int i = 2; i < requestLines.Length; i++)
{
string line = requestLines[i];
if (!line.StartsWith(DefinePrefix, StringComparison.Ordinal))
{
continue;
}
string serializedSymbols = line.Substring(DefinePrefix.Length);
if (string.IsNullOrWhiteSpace(serializedSymbols))
{
return new string[0];
}
return serializedSymbols.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
}
return new string[0];
}
private static List<MetadataReference> BuildReferences(string[] requestLines)
{
List<MetadataReference> references = new List<MetadataReference>(Math.Max(0, requestLines.Length - 2));
lock (SyncRoot)
{
for (int i = 2; i < requestLines.Length; i++)
{
string referencePath = requestLines[i];
if (referencePath.StartsWith(UnsafePrefix, StringComparison.Ordinal)
|| referencePath.StartsWith(DefinePrefix, StringComparison.Ordinal))
{
continue;
}
if (referencePath.StartsWith(ReferencePrefix, StringComparison.Ordinal))
{
referencePath = referencePath.Substring(ReferencePrefix.Length);
}
if (string.IsNullOrWhiteSpace(referencePath) || !File.Exists(referencePath))
{
continue;
}
FileInfo info = new FileInfo(referencePath);
long writeTicks = info.LastWriteTimeUtc.Ticks;
long fileLength = info.Length;
CachedReference cachedReference;
if (Cache.TryGetValue(referencePath, out cachedReference)
&& cachedReference.WriteTicks == writeTicks
&& cachedReference.FileLength == fileLength)
{
references.Add(cachedReference.Reference);
continue;
}
PortableExecutableReference createdReference = MetadataReference.CreateFromFile(referencePath);
Cache[referencePath] = new CachedReference(createdReference, writeTicks, fileLength);
references.Add(createdReference);
}
}
return references;
}
private sealed class CachedReference
{
public CachedReference(PortableExecutableReference reference, long writeTicks, long fileLength)
{
Reference = reference;
WriteTicks = writeTicks;
FileLength = fileLength;
}
public PortableExecutableReference Reference { get; }
public long WriteTicks { get; }
public long FileLength { get; }
}
private sealed class CompileResponse
{
public CompileResponse(int exitCode, List<string> diagnosticLines)
{
ExitCode = exitCode;
DiagnosticLines = diagnosticLines;
}
public int ExitCode { get; }
public List<string> DiagnosticLines { get; }
}
}