Skip to content

Commit a309493

Browse files
fix: resolve MSB3491 race condition in WriteLaunchers parallel builds (#199)
## Problem `WriteLaunchers` wrote build variables to a hardcoded global temp path: ``` %TEMP%\IntelliTect.MultiTool.BuildVariables.tmp ``` When a solution has multiple projects referencing this package, `dotnet build` builds them in parallel. All projects resolved to the **same absolute file path**, and `WriteLinesToFile` with `Overwrite="true"` is not atomic (delete-then-create). Two parallel project builds racing on that path caused: ``` error MSB3491: Could not write lines to file "...\IntelliTect.MultiTool.BuildVariables.tmp". Cannot create a file when that file already exists. ``` There was also a **correctness bug**: even when writes didn't collide, the winning writer was non-deterministic — the file could end up containing variables from any project in the solution, not the one that would consume it at runtime. ## Fix Write the build variables file to `$(OutDir)` — each project's output directory — which is already unique per project and per TFM. Update the C# reader to look in `AppContext.BaseDirectory` instead of `Path.GetTempPath()`, since `AppContext.BaseDirectory` resolves to the same output directory at runtime. ### Changes **`IntelliTect.Multitool/Build/IntelliTect.Multitool.targets`** - Remove the `TempStagingPath` property (was the source of the shared path) - Collapse two conditional `WriteLinesToFile` calls into one unconditional write to `$(OutDir)IntelliTect.MultiTool.BuildVariables.tmp` **`IntelliTect.Multitool/RepositoryPaths.cs`** - `Path.GetTempPath()` → `AppContext.BaseDirectory` in `BuildVariables` initializer **`IntelliTect.Multitool.AotTest/Program.cs`** - Update stale comment to reflect the new file location ## Why `$(OutDir)` + `AppContext.BaseDirectory` | Property | Value | |---|---| | Parallel-safe | ✅ Each project has a unique output dir | | LUT-compatible | ✅ VS LUT shadow-copies the entire output dir; `AppContext.BaseDirectory` follows | | AOT-safe | ✅ `AppContext.BaseDirectory` works in single-file/AOT (unlike `Assembly.Location`) | | Cross-platform | ✅ `$(OutDir)` always ends with a path separator; valid on Windows and Linux |
1 parent 6659c99 commit a309493

3 files changed

Lines changed: 3 additions & 5 deletions

File tree

IntelliTect.Multitool.AotTest/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
return 1;
2121
}
2222

23-
// RepositoryPaths is excluded: its static initializer reads a build-time temp file
23+
// RepositoryPaths is excluded: its static initializer reads a build-time file from the output directory
2424
// that doesn't exist at AOT runtime. The class is AOT-compatible (file I/O + LINQ only).
2525

2626
Console.WriteLine("AOT test passed.");

IntelliTect.Multitool/Build/IntelliTect.Multitool.targets

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,13 @@
2424

2525
<Target Name="WriteLaunchers" AfterTargets="CopyFilesToOutputDirectory">
2626
<PropertyGroup>
27-
<TempStagingPath Condition="'$(TempStagingPath)' == ''">$([System.IO.Path]::GetTempPath())</TempStagingPath>
2827
<LUTInformation>
2928
BuildingForLiveUnitTesting::$(BuildingForLiveUnitTesting)
3029
SolutionDir::$(SolutionDir)
3130
ProjectPath::$(ProjectPath)
3231
</LUTInformation>
3332
</PropertyGroup>
3433

35-
<WriteLinesToFile File="$(TempStagingPath)\IntelliTect.MultiTool.BuildVariables.tmp" Overwrite="true" Lines="$(LUTInformation)" Condition="'$(SolutionDir)' != ''" />
36-
<WriteLinesToFile File="$(TempStagingPath)\IntelliTect.MultiTool.BuildVariables.tmp" Overwrite="true" Lines="$(LUTInformation)" Condition="'$(SolutionDir)' == '' AND !Exists('$(TempStagingPath)\IntelliTect.MultiTool.BuildVariables.tmp')" />
34+
<WriteLinesToFile File="$(OutDir)IntelliTect.MultiTool.BuildVariables.tmp" Overwrite="true" Lines="$(LUTInformation)" />
3735
</Target>
3836
</Project>

IntelliTect.Multitool/RepositoryPaths.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public static class RepositoryPaths
1515
/// <summary>
1616
/// Holds the key value pairs of the build variables that this class can use.
1717
/// </summary>
18-
public static ReadOnlyDictionary<string, string?> BuildVariables { get; } = new(File.ReadAllLines(Path.Combine(Path.GetTempPath(), BuildVariableFileName))
18+
public static ReadOnlyDictionary<string, string?> BuildVariables { get; } = new(File.ReadAllLines(Path.Combine(AppContext.BaseDirectory, BuildVariableFileName))
1919
.Select(line => line.Split("::"))
2020
.ToDictionary(split => split[0].Trim(),
2121
split => !string.IsNullOrEmpty(split[1]) ? split[1].Trim() : null));

0 commit comments

Comments
 (0)