diff --git a/Assets/Tests/Editor/HotReload/HotReloadIntroducedTypeArtifactSweeperTests.cs b/Assets/Tests/Editor/HotReload/HotReloadIntroducedTypeArtifactSweeperTests.cs new file mode 100644 index 000000000..6cf83f89c --- /dev/null +++ b/Assets/Tests/Editor/HotReload/HotReloadIntroducedTypeArtifactSweeperTests.cs @@ -0,0 +1,298 @@ +using System; +using System.IO; + +using NUnit.Framework; + +using io.github.hatayama.UnityCliLoop.FirstPartyTools; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.HotReload +{ + /// + /// Covers which introduced-type artifacts of earlier domains the sweep deletes and which files it + /// keeps, laid out under a temporary project root. + /// + public class HotReloadIntroducedTypeArtifactSweeperTests + { + private string _projectRoot; + private string _currentSessionId; + + private string ArtifactsRoot => + Path.Combine(_projectRoot, HotReloadConstants.IntroducedTypeArtifactsRelativeDirectory); + + private string PublicizedRoot => + Path.Combine(_projectRoot, HotReloadConstants.PublicizedRefsRelativeDirectory); + + private string ExposedRoot => + Path.Combine(_projectRoot, HotReloadConstants.InternalsExposedRefsRelativeDirectory); + + [SetUp] + public void SetUp() + { + _projectRoot = Path.Combine(Path.GetTempPath(), "uloop-artifact-sweep-" + NewId()); + Directory.CreateDirectory(_projectRoot); + _currentSessionId = NewId(); + } + + [TearDown] + public void TearDown() + { + if (Directory.Exists(_projectRoot)) + { + Directory.Delete(_projectRoot, recursive: true); + } + } + + /// + /// What: session directories that earlier domains left are deleted, while the current domain's + /// session directory and its artifact stay. + /// + [Test] + public void Sweep_DeletesEarlierSessionDirectoriesAndKeepsTheCurrentOne() + { + string earlierSession = Path.GetDirectoryName(CreateArtifactDirectory(NewId(), NewId())); + string currentArtifact = CreateArtifactDirectory(_currentSessionId, NewId()); + + CreateSweeper().Sweep(); + + Assert.That(Directory.Exists(earlierSession), Is.False); + Assert.That(Directory.Exists(currentArtifact), Is.True); + } + + /// + /// What: a session directory whose name differs from the current session id only in case is + /// kept, since a case-insensitive file system resolves the current session to it. + /// + [Test] + public void Sweep_KeepsTheCurrentSessionDirectoryWhenItsNameDiffersOnlyInCase() + { + string currentArtifact = CreateArtifactDirectory(_currentSessionId.ToUpperInvariant(), NewId()); + + CreateSweeper().Sweep(); + + Assert.That(Directory.Exists(currentArtifact), Is.True); + } + + /// + /// What: entries under the artifact directory that are not session directories of an earlier + /// domain stay: directories that tests name freely, a GUID written in another format, and a + /// plain file. + /// + [Test] + public void Sweep_KeepsEntriesThatAreNotSessionDirectories() + { + string testSession = CreateArtifactDirectory("compiler-tests", NewId()); + string hyphenatedGuidSession = CreateArtifactDirectory(Guid.NewGuid().ToString("D"), NewId()); + string plainFile = Path.Combine(ArtifactsRoot, NewId()); + File.WriteAllText(plainFile, "file"); + + CreateSweeper().Sweep(); + + Assert.That(Directory.Exists(testSession), Is.True); + Assert.That(Directory.Exists(hyphenatedGuidSession), Is.True); + Assert.That(File.Exists(plainFile), Is.True); + } + + /// + /// What: copies made from artifacts of earlier domains are deleted from both reference caches, + /// including a copy whose artifact directory is already gone and a temporary file a copy was + /// being written through. + /// + [Test] + public void Sweep_DeletesArtifactCopiesOfEarlierSessionsFromBothCaches() + { + string earlierArtifactId = NewId(); + CreateArtifactDirectory(NewId(), earlierArtifactId); + string publicizedCopy = CreateCacheFile(PublicizedRoot, ArtifactCopyName(earlierArtifactId)); + string exposedCopy = CreateCacheFile(ExposedRoot, ArtifactCopyName(earlierArtifactId)); + string temporaryCopy = CreateCacheFile( + PublicizedRoot, + ArtifactCopyName(earlierArtifactId) + ".tmp-" + NewId()); + string orphanCopy = CreateCacheFile(ExposedRoot, ArtifactCopyName(NewId())); + + CreateSweeper().Sweep(); + + Assert.That(File.Exists(publicizedCopy), Is.False); + Assert.That(File.Exists(exposedCopy), Is.False); + Assert.That(File.Exists(temporaryCopy), Is.False); + Assert.That(File.Exists(orphanCopy), Is.False); + } + + /// + /// What: the sweep keeps copies made from artifacts of the current domain, copies of other + /// assemblies, and files whose names only resemble an artifact copy. + /// + [Test] + public void Sweep_KeepsCopiesOfCurrentArtifactsAndOfOtherAssemblies() + { + string currentArtifactId = NewId(); + CreateArtifactDirectory(_currentSessionId, currentArtifactId); + string currentCopy = CreateCacheFile(PublicizedRoot, ArtifactCopyName(currentArtifactId)); + string currentExposedCopy = CreateCacheFile(ExposedRoot, ArtifactCopyName(currentArtifactId)); + string otherAssemblyCopy = CreateCacheFile(PublicizedRoot, "Assembly-CSharp-" + NewId() + ".dll"); + string nonHexIdentifier = CreateCacheFile( + PublicizedRoot, + HotReloadConstants.IntroducedTypeArtifactAssemblyNamePrefix + new string('z', 32) + "-" + NewId() + ".dll"); + string missingSeparator = CreateCacheFile( + ExposedRoot, + HotReloadConstants.IntroducedTypeArtifactAssemblyNamePrefix + NewId() + ".dll"); + + CreateSweeper().Sweep(); + + Assert.That(File.Exists(currentCopy), Is.True); + Assert.That(File.Exists(currentExposedCopy), Is.True); + Assert.That(File.Exists(otherAssemblyCopy), Is.True); + Assert.That(File.Exists(nonHexIdentifier), Is.True); + Assert.That(File.Exists(missingSeparator), Is.True); + } + + /// + /// What: a session directory that cannot be deleted because it is still in use is skipped, + /// and the sweep still deletes the other leftovers. + /// + [Test] + public void Sweep_WhenDeletingADirectoryThrowsIOException_ContinuesWithTheRest() + { + string lockedSessionId = NewId(); + string lockedSession = Path.GetDirectoryName(CreateArtifactDirectory(lockedSessionId, NewId())); + string otherSession = Path.GetDirectoryName(CreateArtifactDirectory(NewId(), NewId())); + string orphanCopy = CreateCacheFile(PublicizedRoot, ArtifactCopyName(NewId())); + HotReloadIntroducedTypeArtifactSweeper sweeper = new HotReloadIntroducedTypeArtifactSweeper( + _projectRoot, + _currentSessionId, + deleteDirectory: path => + { + if (Path.GetFileName(path) == lockedSessionId) + { + throw new IOException("The directory is in use."); + } + + Directory.Delete(path, recursive: true); + }); + + sweeper.Sweep(); + + Assert.That(Directory.Exists(lockedSession), Is.True); + Assert.That(Directory.Exists(otherSession), Is.False); + Assert.That(File.Exists(orphanCopy), Is.False); + } + + /// + /// What: a copy the operating system refuses to delete is skipped, and the sweep still deletes + /// the other copies. + /// + [Test] + public void Sweep_WhenDeletingACopyIsDenied_ContinuesWithTheRest() + { + string deniedCopyName = ArtifactCopyName(NewId()); + string deniedCopy = CreateCacheFile(PublicizedRoot, deniedCopyName); + string otherCopy = CreateCacheFile(ExposedRoot, ArtifactCopyName(NewId())); + HotReloadIntroducedTypeArtifactSweeper sweeper = new HotReloadIntroducedTypeArtifactSweeper( + _projectRoot, + _currentSessionId, + deleteFile: path => + { + if (Path.GetFileName(path) == deniedCopyName) + { + throw new UnauthorizedAccessException("Access to the path is denied."); + } + + File.Delete(path); + }); + + sweeper.Sweep(); + + Assert.That(File.Exists(deniedCopy), Is.True); + Assert.That(File.Exists(otherCopy), Is.False); + } + + /// + /// What: with no artifact directory at all, as before a domain prepares its first batch, the + /// sweep still deletes every artifact copy in both caches and keeps other assemblies' copies. + /// + [Test] + public void Sweep_WhenNoArtifactDirectoryExists_StillDeletesTheArtifactCopies() + { + string publicizedCopy = CreateCacheFile(PublicizedRoot, ArtifactCopyName(NewId())); + string exposedCopy = CreateCacheFile(ExposedRoot, ArtifactCopyName(NewId())); + string otherAssemblyCopy = CreateCacheFile(PublicizedRoot, "Assembly-CSharp-" + NewId() + ".dll"); + + CreateSweeper().Sweep(); + + Assert.That(File.Exists(publicizedCopy), Is.False); + Assert.That(File.Exists(exposedCopy), Is.False); + Assert.That(File.Exists(otherAssemblyCopy), Is.True); + } + + /// + /// What: a project where none of the swept directories exists is left as it is, without the + /// sweep creating any of them. + /// + [Test] + public void Sweep_WhenNoSweptDirectoryExists_CreatesNothing() + { + CreateSweeper().Sweep(); + + Assert.That(Directory.Exists(Path.Combine(_projectRoot, "Library")), Is.False); + } + + /// + /// What: a current session id that is not a GUID is rejected, since the sweep would otherwise + /// take the current domain's session directory for an earlier one. + /// + [Test] + public void Constructor_RejectsASessionIdThatIsNotAGuid() + { + Assert.Throws( + () => new HotReloadIntroducedTypeArtifactSweeper(_projectRoot, string.Empty)); + Assert.Throws( + () => new HotReloadIntroducedTypeArtifactSweeper(_projectRoot, "compiler-tests")); + } + + /// + /// What: a project root that is not an absolute path is rejected, so the sweep never resolves + /// the directories against whatever the working directory happens to be. + /// + [Test] + public void Constructor_RejectsAProjectRootThatIsNotAbsolute() + { + Assert.Throws( + () => new HotReloadIntroducedTypeArtifactSweeper("project", _currentSessionId)); + Assert.Throws( + () => new HotReloadIntroducedTypeArtifactSweeper(string.Empty, _currentSessionId)); + } + + private HotReloadIntroducedTypeArtifactSweeper CreateSweeper() + { + return new HotReloadIntroducedTypeArtifactSweeper(_projectRoot, _currentSessionId); + } + + // Lays an artifact out the way preparation writes one, so the sweep meets a real batch. + private string CreateArtifactDirectory(string sessionId, string artifactId) + { + string directory = Path.Combine(ArtifactsRoot, sessionId, artifactId); + Directory.CreateDirectory(directory); + string assemblyName = HotReloadConstants.IntroducedTypeArtifactAssemblyNamePrefix + artifactId; + File.WriteAllText(Path.Combine(directory, assemblyName + ".dll"), "dll"); + File.WriteAllText(Path.Combine(directory, assemblyName + ".pdb"), "pdb"); + return directory; + } + + private string CreateCacheFile(string cacheRoot, string fileName) + { + Directory.CreateDirectory(cacheRoot); + string path = Path.Combine(cacheRoot, fileName); + File.WriteAllText(path, "copy"); + return path; + } + + private string ArtifactCopyName(string artifactId) + { + return HotReloadConstants.IntroducedTypeArtifactAssemblyNamePrefix + artifactId + "-" + NewId() + ".dll"; + } + + private string NewId() + { + return Guid.NewGuid().ToString("N"); + } + } +} diff --git a/Assets/Tests/Editor/HotReload/HotReloadIntroducedTypeArtifactSweeperTests.cs.meta b/Assets/Tests/Editor/HotReload/HotReloadIntroducedTypeArtifactSweeperTests.cs.meta new file mode 100644 index 000000000..acdf34b95 --- /dev/null +++ b/Assets/Tests/Editor/HotReload/HotReloadIntroducedTypeArtifactSweeperTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a4f89fef21cba4a23950bfdd4c76b96d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadEditorStartup.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadEditorStartup.cs index ac11d7de2..075f9dcf3 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadEditorStartup.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadEditorStartup.cs @@ -1,4 +1,7 @@ +using System.IO; + using UnityEditor; +using UnityEngine; using io.github.hatayama.UnityCliLoop.ToolContracts; @@ -21,6 +24,16 @@ void CaptureOnFirstUpdateTick() HotReloadSourceSnapshotter.CaptureAfterDomainReload(); } + // Why a callback of its own rather than a line in the capture above: an exception in + // one of the two must not keep the other from running. Why unsubscribe first: a sweep + // that throws is then not retried on every later tick. + void SweepArtifactsOnFirstUpdateTick() + { + EditorApplication.update -= SweepArtifactsOnFirstUpdateTick; + HotReloadIntroducedTypePreparation.SweepArtifactsOfEarlierDomains( + Path.GetFullPath(Path.Combine(Application.dataPath, ".."))); + } + // The services are rebuilt here rather than on first use because the introduced type // resolver subscribes to AppDomain.AssemblyResolve when it is built, and that // subscription is lost on every domain reload. @@ -43,6 +56,7 @@ void CaptureOnFirstUpdateTick() HotReloadWiredValueEditorHooks.GetPersistence = () => HotReloadCompositionRoot.Services.WiredValuePersistence; EditorApplication.update += CaptureOnFirstUpdateTick; + EditorApplication.update += SweepArtifactsOnFirstUpdateTick; HotReloadPlayModeEntryDropRecorder.Initialize(); HotReloadAutoRefreshHold.Initialize(); HotReloadUnityMessageForwardingEditorHooks.Initialize(); diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadIntroducedTypePreparation.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadIntroducedTypePreparation.cs index 71060ef2a..926215ba1 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadIntroducedTypePreparation.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadIntroducedTypePreparation.cs @@ -19,6 +19,16 @@ internal static class HotReloadIntroducedTypePreparation // the assembly files a loaded artifact of this domain still maps. private static readonly string SessionId = Guid.NewGuid().ToString("N"); + /// + /// Deletes the artifacts that earlier domains left and the reference-cache copies made from + /// them. Nothing in a later domain loads them, since the registry that knew them died with + /// their domain; this domain's session stays whatever it already holds. + /// + public static void SweepArtifactsOfEarlierDomains(string projectRoot) + { + new HotReloadIntroducedTypeArtifactSweeper(projectRoot, SessionId).Sweep(); + } + public static async Task PrepareAsync( HotReloadGroupStageCollaborators collaborators, IReadOnlyList files, diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/IntroducedType/HotReloadIntroducedTypeArtifactPathFactory.cs b/Packages/src/Editor/FirstPartyTools/HotReload/IntroducedType/HotReloadIntroducedTypeArtifactPathFactory.cs index f7079dd23..8d5ddeba4 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/IntroducedType/HotReloadIntroducedTypeArtifactPathFactory.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/IntroducedType/HotReloadIntroducedTypeArtifactPathFactory.cs @@ -27,7 +27,7 @@ public HotReloadIntroducedTypeArtifactPaths Create() HotReloadConstants.IntroducedTypeArtifactsRelativeDirectory, sessionId, artifactId); - string assemblyName = "UloopIntroducedTypes_" + artifactId; + string assemblyName = HotReloadConstants.IntroducedTypeArtifactAssemblyNamePrefix + artifactId; string dllPath = Path.Combine(directory, assemblyName + ".dll"); return new HotReloadIntroducedTypeArtifactPaths( Path.Combine(directory, assemblyName + ".cs"), diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadConstants.cs b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadConstants.cs index df2a5de73..4c98d8c82 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadConstants.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadConstants.cs @@ -15,6 +15,10 @@ internal static class HotReloadConstants // session and artifact below it. Shared with the publicizer, which accepts an image from // here as well as one from ScriptAssemblies. public const string IntroducedTypeArtifactsRelativeDirectory = "Library/UloopHotReload/IntroducedTypes"; + + // An artifact assembly is named this prefix followed by its artifact id, which is also the + // name of its directory. The reference caches key their copies by the same name. + public const string IntroducedTypeArtifactAssemblyNamePrefix = "UloopIntroducedTypes_"; public const string CompiledAssemblyExtension = ".dll"; // Publicized reference copies are keyed by assembly name + Mvid so a recompiled assembly @@ -572,6 +576,7 @@ public static bool IsPublicizableProjectAssemblyFileName(string fileNameWithoutE public const string VibeLogWorkerHostLifecycleClosed = "hot_reload_worker_lifecycle_closed"; public const string VibeLogWorkerHostBrokenConversation = "hot_reload_worker_broken_conversation"; public const string VibeLogWorkerHostTempCleanupFailed = "hot_reload_worker_temp_cleanup_failed"; + public const string VibeLogIntroducedTypeArtifactSweepFailed = "hot_reload_introduced_type_artifact_sweep_failed"; public const string VibeLogWorkerHostFallbackOneShot = "hot_reload_worker_fallback_one_shot"; public const string VibeLogFileStart = "hot_reload_file_start"; public const string VibeLogWorkerResult = "hot_reload_worker_result"; diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadIntroducedTypeArtifactSweeper.cs b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadIntroducedTypeArtifactSweeper.cs new file mode 100644 index 000000000..da537c780 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadIntroducedTypeArtifactSweeper.cs @@ -0,0 +1,195 @@ +using System; +using System.IO; + +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.FirstPartyTools +{ + /// + /// Deletes the introduced-type artifacts that earlier domains left under the project's Library, + /// together with the reference-cache copies made from them. + /// + internal sealed class HotReloadIntroducedTypeArtifactSweeper + { + private const string GuidFormat = "N"; + private const int ArtifactIdLength = 32; + private const char ArtifactCopyNameSeparator = '-'; + + private readonly string _projectRoot; + private readonly string _currentSessionId; + private readonly Guid _currentSessionGuid; + private readonly Action _deleteDirectory; + private readonly Action _deleteFile; + + public HotReloadIntroducedTypeArtifactSweeper( + string projectRoot, + string currentSessionId, + Action deleteDirectory = null, + Action deleteFile = null) + { + // Why absolute only: a relative root resolves against whatever the working directory + // happens to be, and the sweep deletes directories recursively below it. + if (string.IsNullOrEmpty(projectRoot) || !Path.IsPathRooted(projectRoot)) + { + throw new ArgumentException("The project root must be an absolute path.", nameof(projectRoot)); + } + + // Why a GUID only: session directories are told apart by parsing their names as GUIDs, + // so an id of any other shape would make the current session look like an earlier one. + if (!Guid.TryParseExact(currentSessionId, GuidFormat, out Guid currentSessionGuid)) + { + throw new ArgumentException( + "The current session id must be a GUID in \"N\" format.", + nameof(currentSessionId)); + } + + _projectRoot = projectRoot; + _currentSessionId = currentSessionId; + _currentSessionGuid = currentSessionGuid; + _deleteDirectory = deleteDirectory ?? DeleteDirectoryRecursively; + _deleteFile = deleteFile ?? File.Delete; + } + + /// + /// Deletes every session directory named by a GUID other than the current session's, then + /// every reference-cache copy of an artifact the current session does not hold. + /// + public void Sweep() + { + string artifactsRoot = Path.Combine( + _projectRoot, + HotReloadConstants.IntroducedTypeArtifactsRelativeDirectory); + DeleteEarlierSessionDirectories(artifactsRoot); + + string currentSessionDirectory = Path.Combine(artifactsRoot, _currentSessionId); + DeleteCopiesOfEarlierArtifacts( + Path.Combine(_projectRoot, HotReloadConstants.PublicizedRefsRelativeDirectory), + currentSessionDirectory); + DeleteCopiesOfEarlierArtifacts( + Path.Combine(_projectRoot, HotReloadConstants.InternalsExposedRefsRelativeDirectory), + currentSessionDirectory); + } + + private void DeleteEarlierSessionDirectories(string artifactsRoot) + { + foreach (string sessionDirectory in ListEntries(artifactsRoot, Directory.GetDirectories)) + { + if (!IsEarlierSessionDirectory(Path.GetFileName(sessionDirectory))) + { + continue; + } + + TryDelete(sessionDirectory, _deleteDirectory); + } + } + + // Why compare parsed GUIDs: a name that differs from the current session id only in case + // is the current session's directory on a case-insensitive file system. + private bool IsEarlierSessionDirectory(string directoryName) + { + return Guid.TryParseExact(directoryName, GuidFormat, out Guid sessionGuid) + && sessionGuid != _currentSessionGuid; + } + + private void DeleteCopiesOfEarlierArtifacts(string cacheDirectory, string currentSessionDirectory) + { + foreach (string cachedFile in ListEntries(cacheDirectory, Directory.GetFiles)) + { + if (!TryReadArtifactId(Path.GetFileName(cachedFile), out string artifactId)) + { + continue; + } + + // Why only the current session decides: once the earlier sessions are gone it holds + // every artifact this domain can still load, and a copy deleted too eagerly is + // rebuilt on its next request, while an artifact directory cannot be. + if (Directory.Exists(Path.Combine(currentSessionDirectory, artifactId))) + { + continue; + } + + TryDelete(cachedFile, _deleteFile); + } + } + + // Why read the id at a fixed offset: a copy is named -.dll, a temporary + // file it is written through appends .tmp-, and the assembly name is the prefix + // followed by the 32-digit artifact id. + private static bool TryReadArtifactId(string fileName, out string artifactId) + { + artifactId = null; + string prefix = HotReloadConstants.IntroducedTypeArtifactAssemblyNamePrefix; + int separatorIndex = prefix.Length + ArtifactIdLength; + if (fileName.Length <= separatorIndex + || !fileName.StartsWith(prefix, StringComparison.Ordinal) + || fileName[separatorIndex] != ArtifactCopyNameSeparator) + { + return false; + } + + string candidate = fileName.Substring(prefix.Length, ArtifactIdLength); + if (!Guid.TryParseExact(candidate, GuidFormat, out Guid _)) + { + return false; + } + + artifactId = candidate; + return true; + } + + // Why swallow only these two, here and when deleting: the sweep only reclaims disk space, + // so a directory it cannot read or an entry the OS keeps is left for the next domain to + // retry rather than failing the Editor's first update. + private string[] ListEntries(string directory, Func list) + { + if (!Directory.Exists(directory)) + { + return Array.Empty(); + } + + try + { + return list(directory); + } + catch (IOException ex) + { + LogFailure(directory, ex.Message); + } + catch (UnauthorizedAccessException ex) + { + LogFailure(directory, ex.Message); + } + + return Array.Empty(); + } + + private void TryDelete(string path, Action delete) + { + try + { + delete(path); + } + catch (IOException ex) + { + LogFailure(path, ex.Message); + } + catch (UnauthorizedAccessException ex) + { + LogFailure(path, ex.Message); + } + } + + private static void LogFailure(string path, string reason) + { + VibeLogger.LogWarning( + HotReloadConstants.VibeLogIntroducedTypeArtifactSweepFailed, + "An introduced-type artifact of an earlier domain could not be swept.", + new { path, reason }); + } + + private static void DeleteDirectoryRecursively(string path) + { + Directory.Delete(path, recursive: true); + } + } +} diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadIntroducedTypeArtifactSweeper.cs.meta b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadIntroducedTypeArtifactSweeper.cs.meta new file mode 100644 index 000000000..873963419 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadIntroducedTypeArtifactSweeper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c6266d328acff4cd5a2dc6985dc46aa6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: