diff --git a/buildTransitive/EmptyFileTargets.md b/buildTransitive/EmptyFileTargets.md index 1bc15c7..85e4fdf 100644 --- a/buildTransitive/EmptyFileTargets.md +++ b/buildTransitive/EmptyFileTargets.md @@ -1,18 +1,21 @@ How it works: 1. Inputs="$(MSBuildThisFileFile)" - Tracks the package file timestamp - 2. Outputs="$(EmptyFilesMarker)" - Tracks a marker file in the output directory + 2. Outputs="$(EmptyFilesMarker)" - Tracks a marker file in the intermediate output directory 3. MSBuild automatically skips the target if all outputs are newer than all inputs 4. SkipUnchangedFiles="true" - Additional optimization to skip individual unchanged files 5. Touch - Updates the marker file timestamp after successful copy + 6. DeleteEmptyFilesMarkerIfTargetMissing - If the EmptyFiles directory is missing from the output, deletes the marker file so the copy re-runs This will only copy files when: 1. The targets is newer than the marker file, OR - 2. The marker file doesn't exist (first build/clean build) + 2. The marker file doesn't exist (first build/clean build), OR + 3. The EmptyFiles directory doesn't exist in the output (marker is deleted to force re-copy) Benefits: * Dramatically reduces IO on incremental builds * Works with dotnet clean (removes marker file) + * Recovers when the output directory is cleaned without cleaning the intermediate directory * Still respects individual file changes via SkipUnchangedFiles diff --git a/buildTransitive/EmptyFiles.targets b/buildTransitive/EmptyFiles.targets index 427bde5..fcefd44 100644 --- a/buildTransitive/EmptyFiles.targets +++ b/buildTransitive/EmptyFiles.targets @@ -4,6 +4,12 @@ $(MSBuildThisFileDirectory)..\files + + + + - \ No newline at end of file + diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index d3c3561..0b65cdd 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -10,10 +10,12 @@ + + \ No newline at end of file diff --git a/src/Tests/BuildTargetsTests.cs b/src/Tests/BuildTargetsTests.cs new file mode 100644 index 0000000..eeec61e --- /dev/null +++ b/src/Tests/BuildTargetsTests.cs @@ -0,0 +1,130 @@ +public class BuildTargetsTests +{ + [Test] + public async Task BuildCopiesEmptyFiles() + { + using var temp = new TempDirectory(); + + var (nugetSource, packageVersion) = FindPackageInfo(); + WriteCsproj(temp, packageVersion); + WriteNugetConfig(temp, nugetSource); + + var exitCode = await DotnetBuild(temp); + That(exitCode, Is.EqualTo(0)); + + var emptyFilesDir = Path.Combine(temp.Path, "bin", "Release", "net10.0", "EmptyFiles"); + That(Directory.Exists(emptyFilesDir), Is.True, "EmptyFiles directory should exist after build"); + That(Directory.GetFiles(emptyFilesDir, "*", SearchOption.AllDirectories), Is.Not.Empty, "EmptyFiles directory should contain files"); + } + + [Test] + public async Task RecoverFromDeletedEmptyFilesDirectory() + { + using var temp = new TempDirectory(); + + var (nugetSource, packageVersion) = FindPackageInfo(); + WriteCsproj(temp, packageVersion); + WriteNugetConfig(temp, nugetSource); + + // First build + var exitCode = await DotnetBuild(temp); + That(exitCode, Is.EqualTo(0)); + + var emptyFilesDir = Path.Combine(temp.Path, "bin", "Release", "net10.0", "EmptyFiles"); + That(Directory.Exists(emptyFilesDir), Is.True, "EmptyFiles directory should exist after first build"); + + // Delete EmptyFiles directory from output (leave obj/ intact so marker file survives) + Directory.Delete(emptyFilesDir, true); + That(Directory.Exists(emptyFilesDir), Is.False); + + // Second build — should recover + exitCode = await DotnetBuild(temp); + That(exitCode, Is.EqualTo(0)); + + That(Directory.Exists(emptyFilesDir), Is.True, "EmptyFiles directory should exist after second build"); + That(Directory.GetFiles(emptyFilesDir, "*", SearchOption.AllDirectories), Is.Not.Empty, "EmptyFiles directory should contain files after recovery"); + } + + static void WriteCsproj(TempDirectory temp, string version) + { + var csproj = $""" + + + net10.0 + packages + + + + + + """; + File.WriteAllText(Path.Combine(temp.Path, "Project.csproj"), csproj); + } + + static void WriteNugetConfig(TempDirectory temp, string nugetSource) + { + var config = $""" + + + + + + + + """; + File.WriteAllText(Path.Combine(temp.Path, "nuget.config"), config); + } + + static async Task DotnetBuild(TempDirectory temp) + { + var startInfo = new ProcessStartInfo("dotnet", "build --configuration Release --disable-build-servers -nodeReuse:false /p:UseSharedCompilation=false") + { + WorkingDirectory = temp.Path, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }; + + using var process = Process.Start(startInfo)!; + var stdout = await process.StandardOutput.ReadToEndAsync(); + var stderr = await process.StandardError.ReadToEndAsync(); + await process.WaitForExitAsync(); + + await TestContext.Out.WriteLineAsync("STDOUT:"); + await TestContext.Out.WriteLineAsync(stdout); + await TestContext.Out.WriteLineAsync("STDERR:"); + await TestContext.Out.WriteLineAsync(stderr); + + return process.ExitCode; + } + + static (string nugetSource, string packageVersion) FindPackageInfo() + { + var nugetSource = FindNugetSource(); + var packageVersion = FindPackageVersion(nugetSource); + return (nugetSource, packageVersion); + } + + static string FindNugetSource() + { + var nugetsDir = Path.GetFullPath(Path.Combine(ProjectFiles.SolutionDirectory, "..", "nugets")); + if (Directory.Exists(nugetsDir)) + { + return nugetsDir; + } + + throw new InvalidOperationException($"Cannot find nugets directory at {nugetsDir}"); + } + + static string FindPackageVersion(string nugetSource) + { + var nupkg = Directory + .GetFiles(nugetSource, "EmptyFiles.*.nupkg") + .FirstOrDefault(_ => !Path.GetFileName(_).StartsWith("EmptyFiles.Tool")) ?? + throw new InvalidOperationException($"Cannot find EmptyFiles nupkg in {nugetSource}"); + + var fileName = Path.GetFileNameWithoutExtension(nupkg); + return fileName["EmptyFiles.".Length..]; + } +} \ No newline at end of file diff --git a/src/Tests/GlobalUsings.cs b/src/Tests/GlobalUsings.cs index f5a79e6..ad8e2a1 100644 --- a/src/Tests/GlobalUsings.cs +++ b/src/Tests/GlobalUsings.cs @@ -1,3 +1,5 @@ global using EmptyFiles; global using NUnit.Framework; -global using System.Collections.Immutable; \ No newline at end of file +global using System.Collections.Immutable; +global using System.Diagnostics; +global using System.Runtime.InteropServices; \ No newline at end of file diff --git a/src/Tests/SolutionDirectoryFinder.cs b/src/Tests/SolutionDirectoryFinder.cs deleted file mode 100644 index a55bcbc..0000000 --- a/src/Tests/SolutionDirectoryFinder.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -static class SolutionDirectoryFinder -{ - public static string Find([CallerFilePath] string testFile = "") - { - var testDirectory = Path.GetDirectoryName(testFile)!; - if (!TryFind(testDirectory, out var solutionDirectory)) - { - throw new("Could not find solution directory"); - } - - return solutionDirectory; - } - - public static bool TryFind(string testDirectory, [NotNullWhen(true)] out string? path) - { - var currentDirectory = testDirectory; - do - { - if (Directory - .GetFiles(currentDirectory, "*.slnx").Length != 0) - { - path = currentDirectory; - return true; - } - - var parent = Directory.GetParent(currentDirectory); - if (parent == null) - { - path = null; - return false; - } - - currentDirectory = parent.FullName; - } while (true); - } -} \ No newline at end of file diff --git a/src/Tests/Tests.cs b/src/Tests/Tests.cs index 2bb7958..f409451 100644 --- a/src/Tests/Tests.cs +++ b/src/Tests/Tests.cs @@ -213,7 +213,7 @@ public void UseFile() [Test] public async Task WriteExtensions() { - var md = Path.Combine(SolutionDirectoryFinder.Find(), "extensions.include.md"); + var md = Path.Combine(ProjectFiles.SolutionDirectory, "extensions.include.md"); File.Delete(md); await using var writer = File.CreateText(md); await WriteCategory(writer, "Archive", AllFiles.Archives); diff --git a/src/Tests/Tests.csproj b/src/Tests/Tests.csproj index 19625b1..2438137 100644 --- a/src/Tests/Tests.csproj +++ b/src/Tests/Tests.csproj @@ -12,6 +12,8 @@ + +