using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Text; using System.Threading; using System.Threading.Tasks; using UnityEditor.Compilation; using UnityEngine; using Debug = UnityEngine.Debug; namespace io.github.hatayama.uLoopMCP { internal sealed class CompiledAssemblyBuilder : ICompiledAssemblyBuilder { private static int _compileCounter; private static readonly string[] LiteralHoistingFallbackErrorCodes = { "CS0133", "CS0150", "CS0182", "CS1736" }; private readonly ExternalCompilerPathResolutionService _externalCompilerPathResolver; private readonly DynamicReferenceSetBuilderService _referenceSetBuilder; private readonly DynamicCompilationBackend _compilationBackend; private sealed class BuildAttemptResult { public string UpdatedSource { get; } public CompilerDiagnostics Diagnostics { get; } public Dictionary> AmbiguousTypeCandidates { get; } public List AutoInjectedNamespaces { get; } public byte[] AssemblyBytes { get; } public BuildAttemptResult( string updatedSource, CompilerDiagnostics diagnostics, Dictionary> ambiguousTypeCandidates, List autoInjectedNamespaces, byte[] assemblyBytes) { UpdatedSource = updatedSource; Diagnostics = diagnostics; AmbiguousTypeCandidates = ambiguousTypeCandidates; AutoInjectedNamespaces = autoInjectedNamespaces; AssemblyBytes = assemblyBytes; } } public CompiledAssemblyBuilder( ExternalCompilerPathResolutionService externalCompilerPathResolver, DynamicReferenceSetBuilderService referenceSetBuilder, DynamicCompilationBackend compilationBackend) { _externalCompilerPathResolver = externalCompilerPathResolver; _referenceSetBuilder = referenceSetBuilder; _compilationBackend = compilationBackend; } public bool SupportsAutoPrewarm() { return SupportsAutoPrewarm(_externalCompilerPathResolver.Resolve(), Application.platform); } internal static bool SupportsAutoPrewarm( ExternalCompilerPaths externalCompilerPaths, RuntimePlatform platform) { return externalCompilerPaths != null; } public async Task BuildAsync( DynamicCompilationPlan plan, CancellationToken ct = default) { Debug.Assert(plan != null, "plan must not be null"); ct.ThrowIfCancellationRequested(); ExternalCompilerPaths externalCompilerPaths = _externalCompilerPathResolver.Resolve(); string tempDirectoryPath = Path.Combine("Temp", "uLoopMCPCompilation"); int compileCounter = Interlocked.Increment(ref _compileCounter); string uniqueName = CreateUniqueCompilationName(plan.ClassName, compileCounter); string sourcePath = Path.Combine(tempDirectoryPath, $"{uniqueName}.cs"); string dllPath = Path.Combine(tempDirectoryPath, $"{uniqueName}.dll"); bool canDeleteTempFiles = true; double referenceResolutionMilliseconds = 0; double buildMilliseconds = 0; int buildCount = 0; DynamicCompilationBackendKind compilationBackendKind = DynamicCompilationBackendKind.Unknown; Directory.CreateDirectory(tempDirectoryPath); try { async Task BuildFunc( string resolvedSourcePath, string resolvedDllPath, List resolvedReferences, CancellationToken cancellationToken) { Stopwatch buildStopwatch = Stopwatch.StartNew(); DynamicCompilationBackendResult backendResult = await _compilationBackend.CompileAsync( resolvedSourcePath, resolvedDllPath, resolvedReferences, externalCompilerPaths, cancellationToken, () => canDeleteTempFiles = false, () => canDeleteTempFiles = true, () => buildCount++); compilationBackendKind = backendResult.BackendKind; buildStopwatch.Stop(); buildMilliseconds += buildStopwatch.Elapsed.TotalMilliseconds; return backendResult.CompilerMessages; } BuildAttemptResult attemptResult = await BuildPreparedCodeAsync(plan.PreparedCode, ct); bool shouldCacheResult = true; if (ShouldRetryWithoutLiteralHoisting(plan.PreparedCode, attemptResult.Diagnostics)) { PreparedDynamicCode fallbackPreparedCode = DynamicCodeSourcePreparer.PrepareWithoutLiteralHoisting( plan.OriginalRequest.Code, plan.NamespaceName, plan.ClassName); attemptResult = await BuildPreparedCodeAsync(fallbackPreparedCode, ct); shouldCacheResult = false; } return new CompiledAssemblyBuildResult( attemptResult.UpdatedSource, attemptResult.Diagnostics, attemptResult.AmbiguousTypeCandidates, attemptResult.AutoInjectedNamespaces, attemptResult.AssemblyBytes, referenceResolutionMilliseconds, buildMilliseconds, buildCount, shouldCacheResult, compilationBackendKind); async Task BuildPreparedCodeAsync( PreparedDynamicCode preparedCode, CancellationToken cancellationToken) { string wrappedCode = preparedCode.PreparedSource; string originalWrappedCode = wrappedCode; bool preUsingAdded = false; PreUsingResult preUsingResult = null; Stopwatch initialReferenceResolutionStopwatch = Stopwatch.StartNew(); List initialReferences = BuildInitialReferences( plan, externalCompilerPaths, preparedCode, ref wrappedCode, ref preUsingAdded, ref preUsingResult); initialReferenceResolutionStopwatch.Stop(); referenceResolutionMilliseconds += initialReferenceResolutionStopwatch.Elapsed.TotalMilliseconds; AutoUsingResolver resolver = new AutoUsingResolver(); AutoUsingResult autoResult = await resolver.ResolveAsync( sourcePath, dllPath, wrappedCode, initialReferences, BuildFunc, cancellationToken); referenceResolutionMilliseconds += autoResult.ReferenceResolutionMilliseconds; wrappedCode = autoResult.UpdatedSource; CompilerDiagnostics diagnostics = CompilerDiagnostics.FromMessages(autoResult.Messages); bool preUsingRolledBack = false; if (diagnostics.Errors.Count > 0 && preUsingAdded && diagnostics.HasAmbiguityErrors) { Stopwatch rollbackReferenceResolutionStopwatch = Stopwatch.StartNew(); List rollbackReferences = _referenceSetBuilder.BuildReferenceSet( plan.OriginalRequest.AdditionalReferences, null, externalCompilerPaths); rollbackReferenceResolutionStopwatch.Stop(); referenceResolutionMilliseconds += rollbackReferenceResolutionStopwatch.Elapsed.TotalMilliseconds; AutoUsingResult rollbackResult = await resolver.ResolveAsync( sourcePath, dllPath, originalWrappedCode, rollbackReferences, BuildFunc, cancellationToken); referenceResolutionMilliseconds += rollbackResult.ReferenceResolutionMilliseconds; CompilerDiagnostics rollbackDiagnostics = CompilerDiagnostics.FromMessages(rollbackResult.Messages); if (rollbackDiagnostics.Errors.Count < diagnostics.Errors.Count) { wrappedCode = rollbackResult.UpdatedSource; diagnostics = rollbackDiagnostics; autoResult = rollbackResult; preUsingRolledBack = true; } } List autoInjectedNamespaces = MergeAutoInjectedNamespaces( preUsingRolledBack, preUsingResult, autoResult); byte[] assemblyBytes = null; if (diagnostics.Errors.Count == 0) { assemblyBytes = File.ReadAllBytes(dllPath); } return new BuildAttemptResult( wrappedCode, diagnostics, autoResult.AmbiguousTypeCandidates, autoInjectedNamespaces, assemblyBytes); } } finally { if (canDeleteTempFiles) { File.Delete(sourcePath); File.Delete(dllPath); File.Delete(Path.ChangeExtension(dllPath, ".pdb")); } } } internal static string CreateUniqueCompilationName(string className, int compileCounter) { string safeClassName = SanitizeCompilationFileNameSegment(className); return $"{safeClassName}_{compileCounter}"; } internal static string SanitizeCompilationFileNameSegment(string value) { if (string.IsNullOrWhiteSpace(value)) { return DynamicCodeConstants.DEFAULT_CLASS_NAME; } StringBuilder builder = new StringBuilder(value.Length); foreach (char ch in value) { builder.Append(IsUnsafeFileNameCharacter(ch) ? '_' : ch); } string sanitized = builder.ToString().Trim('.'); if (string.IsNullOrWhiteSpace(sanitized)) { return DynamicCodeConstants.DEFAULT_CLASS_NAME; } return sanitized; } private static bool IsUnsafeFileNameCharacter(char value) { if (value == Path.DirectorySeparatorChar || value == Path.AltDirectorySeparatorChar) { return true; } switch (value) { case '<': case '>': case ':': case '"': case '/': case '\\': case '|': case '?': case '*': return true; } foreach (char invalidChar in Path.GetInvalidFileNameChars()) { if (value == invalidChar) { return true; } } return false; } private List BuildInitialReferences( DynamicCompilationPlan plan, ExternalCompilerPaths externalCompilerPaths, PreparedDynamicCode preparedCode, ref string wrappedCode, ref bool preUsingAdded, ref PreUsingResult preUsingResult) { if (!preparedCode.IsScriptMode) { return _referenceSetBuilder.BuildReferenceSet( plan.OriginalRequest.AdditionalReferences, null, externalCompilerPaths); } preUsingResult = PreUsingResolver.Resolve(wrappedCode, AssemblyTypeIndex.Instance); preUsingAdded = !ReferenceEquals(preUsingResult.UpdatedSource, wrappedCode); wrappedCode = preUsingResult.UpdatedSource; return _referenceSetBuilder.BuildReferenceSet( plan.OriginalRequest.AdditionalReferences, preUsingResult.AddedAssemblyReferences, externalCompilerPaths); } private static bool ShouldRetryWithoutLiteralHoisting( PreparedDynamicCode preparedCode, CompilerDiagnostics diagnostics) { if (preparedCode == null || diagnostics == null) { return false; } if (!preparedCode.IsScriptMode || preparedCode.HoistedLiteralBindings.Count == 0) { return false; } foreach (CompilationError error in diagnostics.Errors) { foreach (string errorCode in LiteralHoistingFallbackErrorCodes) { if (error.ErrorCode == errorCode) { return true; } } } return false; } private static List MergeAutoInjectedNamespaces( bool preUsingRolledBack, PreUsingResult preUsingResult, AutoUsingResult autoResult) { List mergedNamespaces = new List(); if (!preUsingRolledBack && preUsingResult != null && preUsingResult.AddedNamespaces.Count > 0) { foreach (string namespaceName in preUsingResult.AddedNamespaces) { mergedNamespaces.Add(namespaceName); } } foreach (string namespaceName in autoResult.AddedNamespaces) { if (!mergedNamespaces.Contains(namespaceName)) { mergedNamespaces.Add(namespaceName); } } return mergedNamespaces; } } }