From a66cde23663dc1752107aa61193abcf801dbef15 Mon Sep 17 00:00:00 2001 From: hatayama Date: Sat, 18 Jul 2026 15:14:21 +0900 Subject: [PATCH 1/2] Capture Unity compiler settings before async compilation Switch to the Unity main thread before reading script defines and unsafe-code settings, then pass an immutable snapshot through the asynchronous compilation pipeline. This prevents UnityEditor APIs from being accessed after ConfigureAwait(false) resumes on a worker thread. --- .../ExternalCompilerPathResolverTests.cs | 1 + .../Compilation/ICompiledAssemblyBuilder.cs | 1 + .../CompiledAssemblyBuilder.cs | 3 ++ .../DynamicCompilation/DynamicCodeCompiler.cs | 11 ++++- .../DynamicCompilationBackend.cs | 2 + .../RoslynCompilerBackend.cs | 26 ++---------- .../RoslynCompilerOptions.cs | 40 +++++++++++++++++++ .../RoslynCompilerOptions.cs.meta | 2 + 8 files changed, 62 insertions(+), 24 deletions(-) create mode 100644 Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/RoslynCompilerOptions.cs create mode 100644 Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/RoslynCompilerOptions.cs.meta diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/ExternalCompilerPathResolverTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/ExternalCompilerPathResolverTests.cs index 6573333fe..e26911308 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/ExternalCompilerPathResolverTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/ExternalCompilerPathResolverTests.cs @@ -273,6 +273,7 @@ public async Task CompileAsync_WhenSharedWorkerBuildFails_ShouldFallbackToOneSho dllPath, references, externalCompilerPaths, + new RoslynCompilerOptions(Array.Empty(), false), compileCancellationTokenSource.Token, () => buildStarted = true, () => buildFinished = true, diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Compilation/ICompiledAssemblyBuilder.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Compilation/ICompiledAssemblyBuilder.cs index 77d51ba6e..2b0fede68 100644 --- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Compilation/ICompiledAssemblyBuilder.cs +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Compilation/ICompiledAssemblyBuilder.cs @@ -10,6 +10,7 @@ public interface ICompiledAssemblyBuilder { Task BuildAsync( DynamicCompilationPlan plan, + RoslynCompilerOptions compilerOptions, CancellationToken ct = default); } } diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/CompiledAssemblyBuilder.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/CompiledAssemblyBuilder.cs index c60309d8b..8b61a2f97 100644 --- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/CompiledAssemblyBuilder.cs +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/CompiledAssemblyBuilder.cs @@ -64,9 +64,11 @@ public CompiledAssemblyBuilder( public async Task BuildAsync( DynamicCompilationPlan plan, + RoslynCompilerOptions compilerOptions, CancellationToken ct = default) { Debug.Assert(plan != null, "plan must not be null"); + Debug.Assert(compilerOptions != null, "compilerOptions must not be null"); ct.ThrowIfCancellationRequested(); @@ -98,6 +100,7 @@ async Task BuildFunc( resolvedDllPath, resolvedReferences, externalCompilerPaths, + compilerOptions, cancellationToken, () => canDeleteTempFiles = false, () => canDeleteTempFiles = true, diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCodeCompiler.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCodeCompiler.cs index b6cdcbb2b..a1c01f483 100644 --- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCodeCompiler.cs +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCodeCompiler.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using UnityEditor; using Debug = UnityEngine.Debug; using Stopwatch = System.Diagnostics.Stopwatch; @@ -61,6 +62,11 @@ public async Task CompileAsync(CompilationRequest request, Ca Debug.Assert(!string.IsNullOrWhiteSpace(request.Code), "request.Code must not be empty"); ct.ThrowIfCancellationRequested(); + await MainThreadSwitcher.SwitchToMainThread(ct); + string[] activeDefineSymbols = EditorUserBuildSettings.activeScriptCompilationDefines ?? Array.Empty(); + RoslynCompilerOptions compilerOptions = new( + activeDefineSymbols, + PlayerSettings.allowUnsafeCode); LastBuildCount = 0; Stopwatch compilerTotalStopwatch = Stopwatch.StartNew(); Stopwatch planStopwatch = Stopwatch.StartNew(); @@ -95,7 +101,10 @@ public async Task CompileAsync(CompilationRequest request, Ca } Stopwatch builderTotalStopwatch = Stopwatch.StartNew(); - CompiledAssemblyBuildResult buildResult = await _assemblyBuilder.BuildAsync(plan, ct).ConfigureAwait(false); + CompiledAssemblyBuildResult buildResult = await _assemblyBuilder.BuildAsync( + plan, + compilerOptions, + ct).ConfigureAwait(false); builderTotalStopwatch.Stop(); LastBuildCount = buildResult.BuildCount; diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCompilationBackend.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCompilationBackend.cs index dbf40e597..cae2b8eab 100644 --- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCompilationBackend.cs +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/DynamicCompilationBackend.cs @@ -34,6 +34,7 @@ public Task CompileAsync( string dllPath, List references, ExternalCompilerPaths externalCompilerPaths, + RoslynCompilerOptions compilerOptions, CancellationToken ct, Action markBuildStarted, Action markBuildFinished, @@ -46,6 +47,7 @@ public Task CompileAsync( dllPath, references, externalCompilerPaths, + compilerOptions, ct, markBuildStarted, markBuildFinished, diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/RoslynCompilerBackend.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/RoslynCompilerBackend.cs index edebe7b8d..050e331f0 100644 --- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/RoslynCompilerBackend.cs +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/RoslynCompilerBackend.cs @@ -4,7 +4,6 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -using UnityEditor; using UnityEditor.Compilation; using UnityEngine; @@ -52,14 +51,15 @@ public static async Task CompileAsync( string dllPath, List references, ExternalCompilerPaths externalCompilerPaths, + RoslynCompilerOptions compilerOptions, CancellationToken ct, Action markBuildStarted, Action markBuildFinished, Action incrementBuildCount) { string workerRequestFilePath = Path.ChangeExtension(sourcePath, ".worker"); - string[] defineSymbols = GetActiveDefineSymbols(); - bool allowUnsafeCode = PlayerSettings.allowUnsafeCode; + IReadOnlyCollection defineSymbols = compilerOptions.DefineSymbols; + bool allowUnsafeCode = compilerOptions.AllowUnsafeCode; try { @@ -271,26 +271,6 @@ internal static void WriteWorkerRequestFile( File.WriteAllLines(Path.GetFullPath(requestFilePath), lines); } - private static string[] GetActiveDefineSymbols() - { - string[] activeDefines = EditorUserBuildSettings.activeScriptCompilationDefines; - if (activeDefines == null || activeDefines.Length == 0) - { - return Array.Empty(); - } - - List filteredDefines = new(activeDefines.Length); - foreach (string define in activeDefines) - { - if (!string.IsNullOrWhiteSpace(define)) - { - filteredDefines.Add(define); - } - } - - return filteredDefines.ToArray(); - } - private static string BuildDefineOption(IReadOnlyCollection defineSymbols) { string serializedDefines = SerializeDefineSymbols(defineSymbols); diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/RoslynCompilerOptions.cs b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/RoslynCompilerOptions.cs new file mode 100644 index 000000000..744800033 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/RoslynCompilerOptions.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; + +namespace io.github.hatayama.UnityCliLoop.FirstPartyTools +{ + /// + /// Captures the Unity compiler settings required by the asynchronous Roslyn pipeline. + /// + public sealed class RoslynCompilerOptions + { + public IReadOnlyList DefineSymbols { get; } + + public bool AllowUnsafeCode { get; } + + /// + /// Creates an immutable compiler-settings snapshot for one compilation request. + /// + public RoslynCompilerOptions( + IReadOnlyCollection defineSymbols, + bool allowUnsafeCode) + { + if (defineSymbols == null) + { + throw new ArgumentNullException(nameof(defineSymbols)); + } + + List filteredDefineSymbols = new(defineSymbols.Count); + foreach (string defineSymbol in defineSymbols) + { + if (!string.IsNullOrWhiteSpace(defineSymbol)) + { + filteredDefineSymbols.Add(defineSymbol); + } + } + + DefineSymbols = filteredDefineSymbols.AsReadOnly(); + AllowUnsafeCode = allowUnsafeCode; + } + } +} diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/RoslynCompilerOptions.cs.meta b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/RoslynCompilerOptions.cs.meta new file mode 100644 index 000000000..83b65fc5b --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/DynamicCompilation/RoslynCompilerOptions.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: d33068d89875483ba33d9dbc2f5df2dd From b733699fb3d494275681645a5d61fd32481ae481 Mon Sep 17 00:00:00 2001 From: hatayama Date: Sat, 18 Jul 2026 15:17:49 +0900 Subject: [PATCH 2/2] Test immutable compiler option snapshots Cover whitespace filtering, empty define sets, null input validation, and input-array isolation so the captured settings preserve the previous compiler behavior without Unity API access. --- .../RoslynCompilerOptionsTests.cs | 61 +++++++++++++++++++ .../RoslynCompilerOptionsTests.cs.meta | 2 + 2 files changed, 63 insertions(+) create mode 100644 Assets/Tests/Editor/DynamicCodeToolTests/RoslynCompilerOptionsTests.cs create mode 100644 Assets/Tests/Editor/DynamicCodeToolTests/RoslynCompilerOptionsTests.cs.meta diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/RoslynCompilerOptionsTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/RoslynCompilerOptionsTests.cs new file mode 100644 index 000000000..5179b0b50 --- /dev/null +++ b/Assets/Tests/Editor/DynamicCodeToolTests/RoslynCompilerOptionsTests.cs @@ -0,0 +1,61 @@ +using System; +using NUnit.Framework; + +using io.github.hatayama.UnityCliLoop.FirstPartyTools; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests +{ + /// + /// Verifies the immutable compiler-settings snapshot used by dynamic compilation. + /// + [TestFixture] + public sealed class RoslynCompilerOptionsTests + { + /// + /// Verifies empty and whitespace-only define symbols are excluded while valid symbols are copied. + /// + [Test] + public void Constructor_FiltersWhitespaceAndCopiesDefineSymbols() + { + string[] defineSymbols = + { + "UNITY_EDITOR", + null, + "", + " ", + "CUSTOM_DEFINE" + }; + + RoslynCompilerOptions options = new(defineSymbols, true); + defineSymbols[0] = "MUTATED_AFTER_CAPTURE"; + + Assert.That( + options.DefineSymbols, + Is.EqualTo(new[] { "UNITY_EDITOR", "CUSTOM_DEFINE" })); + Assert.That(options.AllowUnsafeCode, Is.True); + } + + /// + /// Verifies an empty define-symbol collection produces an empty immutable snapshot. + /// + [Test] + public void Constructor_WithEmptyDefineSymbols_CapturesEmptySnapshot() + { + RoslynCompilerOptions options = new(Array.Empty(), false); + + Assert.That(options.DefineSymbols, Is.Empty); + Assert.That(options.AllowUnsafeCode, Is.False); + } + + /// + /// Verifies a null define-symbol collection fails fast at the snapshot boundary. + /// + [Test] + public void Constructor_WithNullDefineSymbols_ThrowsArgumentNullException() + { + Assert.That( + () => new RoslynCompilerOptions(null, false), + Throws.ArgumentNullException); + } + } +} diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/RoslynCompilerOptionsTests.cs.meta b/Assets/Tests/Editor/DynamicCodeToolTests/RoslynCompilerOptionsTests.cs.meta new file mode 100644 index 000000000..6a552ff7f --- /dev/null +++ b/Assets/Tests/Editor/DynamicCodeToolTests/RoslynCompilerOptionsTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c9ebc13404c3436da8f1de5961992daa