Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
663f7e5
Add resource validation tests to prevent missing resource string erro…
Copilot Jan 26, 2026
d6db114
[automated] Merge branch 'vs18.3' => 'main' (#13105)
github-actions[bot] Jan 26, 2026
b795233
[automated] Merge branch 'vs18.3' => 'main' (#13116)
github-actions[bot] Jan 26, 2026
bef17a5
Localized file check-in by OneLocBuild Task: Build definition ID 9434…
dotnet-bot Jan 26, 2026
15593e9
Reduce allocations in ProjectItemInstance.CommonConstructor by settin…
nareshjo Jan 27, 2026
25422ea
Change version check from != to < to allow loading newer assembly ver…
huulinhnguyen-dev Jan 28, 2026
708dd79
absolutizing "" should be an error (#13120)
JanProvaznik Jan 28, 2026
6b7a28c
Refactor Microsoft.IO usage (#13062)
AR-May Jan 28, 2026
9fe4e71
Localized file check-in by OneLocBuild Task: Build definition ID 9434…
dotnet-bot Jan 28, 2026
211a2e1
remove dead code (#13125)
SimaTian Jan 28, 2026
80d7328
Update VersionPrefix to 18.5.0 + insertion flow (#13134)
YuliiaKovalova Jan 29, 2026
27296fe
add multithreaded task migration agent skill (#13131)
JanProvaznik Jan 29, 2026
caef8e6
Update MicrosoftBuildVersion in analyzer template (#13139)
github-actions[bot] Jan 29, 2026
1d035a3
Localize path validation strings in AbsolutePath
JanProvaznik Jan 29, 2026
5f7ae5d
Add fast minimal build scripts for faster development iteration
JanProvaznik Jan 29, 2026
bfe3074
Fix minimal build: disable IBC optimization and remove broken TFM opt…
JanProvaznik Jan 30, 2026
bd35c89
Add -core and -netfx flags for fast single-TFM builds
JanProvaznik Jan 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .config/git-merge-flow-config.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,12 @@
"vs18.0": {
"MergeToBranch": "vs18.3"
},
// Automate opening PRs to merge msbuild's vs18.3 (SDK 10.0.2xx) into main
// Automate opening PRs to merge msbuild's vs18.3 (SDK 10.0.2xx) into vs18.4 (VS)
"vs18.3": {
"MergeToBranch": "vs18.4"
},
// Automate opening PRs to merge msbuild's vs18.4 (VS) into main (VS canary, SDK main & next-feature-band)
"vs18.4": {
"MergeToBranch": "main"
}
}
Expand Down
244 changes: 244 additions & 0 deletions .github/skills/multithreaded-task-migration/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
---
name: multithreaded-task-migration
description: Guide for migrating MSBuild tasks to the multithreaded mode support. Use this when asked to convert tasks to thread-safe versions, implement IMultiThreadableTask, or add TaskEnvironment support to tasks.
---

# Migrating MSBuild Tasks to Multithreaded API

This skill guides you through migrating MSBuild tasks to support multithreaded execution by implementing `IMultiThreadableTask` and using `TaskEnvironment`.

## Overview

MSBuild's multithreaded execution model requires tasks to avoid global process state (working directory, environment variables). Thread-safe tasks declare this capability by annotating with `MSBuildMultiThreadableTask` and use `TaskEnvironment` provided by `IMultiThreadableTask` for safe alternatives.

## Migration Steps

### Step 1: Update Task Class Declaration

a. add the attribute
b. AND implement the interface if it's necessary to use TaskEnvironment APIs.

```csharp
[MSBuildMultiThreadableTask]
public class MyTask : Task, IMultiThreadableTask
{
public TaskEnvironment TaskEnvironment { get; set; }
...
}
```

### Step 2: Absolutize Paths Before File Operations

**Critical**: All path strings must be absolutized with `TaskEnvironment.GetAbsolutePath()` before use in file system APIs. This ensures paths resolve relative to the project directory, not the process working directory.

```csharp
// BEFORE - File.Exists uses process working directory for relative paths (UNSAFE)
if (File.Exists(inputPath))
{
string content = File.ReadAllText(inputPath);
}

// AFTER - Absolutize first, then use in file operations (SAFE)
AbsolutePath absolutePath = TaskEnvironment.GetAbsolutePath(inputPath);
if (File.Exists(absolutePath))
{
string content = File.ReadAllText(absolutePath);
}
```

`GetAbsolutePath()` throws for null/empty inputs. See [Exception Handling in Batch Operations](#exception-handling-in-batch-operations) for handling strategies.

The [`AbsolutePath`](https://github.com/dotnet/msbuild/blob/main/src/Framework/PathHelpers/AbsolutePath.cs) struct:
- Has `Value` property returning the absolute path string
- Has `OriginalValue` property preserving the input path
- Is implicitly convertible to `string` for File/Directory API compatibility

**CAUTION**: `FileInfo` can be created from relative paths - only use `FileInfo.FullName` if constructed with an absolute path.

#### Note:
If code previously used `Path.GetFullPath()` for canonicalization (resolving `..` segments, normalizing separators), call `AbsolutePath.GetCanonicalForm()` after absolutization to preserve that behavior. Do not simply replace `Path.GetFullPath` with `GetAbsolutePath` if canonicalization was the intent. You can replace `Path.GetFullPath` behavior by combining both:

```csharp
AbsolutePath absolutePath = TaskEnvironment.GetAbsolutePath(inputPath).GetCanonicalForm();
```
The goal is MAXIMUM compatibility so think about these edge cases so it behaves the same as before.

### Step 3: Replace Environment Variable APIs

```csharp
// BEFORE (UNSAFE)
string value = Environment.GetEnvironmentVariable("VAR");
Environment.SetEnvironmentVariable("VAR", "value");

// AFTER (SAFE)
string value = TaskEnvironment.GetEnvironmentVariable("VAR");
TaskEnvironment.SetEnvironmentVariable("VAR", "value");
```

### Step 4: Replace Process Start APIs

```csharp
// BEFORE (UNSAFE - inherits process state)
var psi = new ProcessStartInfo("tool.exe");

// AFTER (SAFE - uses task's isolated environment)
var psi = TaskEnvironment.GetProcessStartInfo();
psi.FileName = "tool.exe";
```

## Updating Unit Tests

**Every test creating a task instance must set TaskEnvironment.** Use `TaskEnvironmentHelper.CreateForTest()`:

```csharp
// BEFORE
var task = new Copy
{
BuildEngine = new MockEngine(true),
SourceFiles = sourceFiles,
DestinationFolder = new TaskItem(destFolder),
};

// AFTER
var task = new Copy
{
TaskEnvironment = TaskEnvironmentHelper.CreateForTest(),
BuildEngine = new MockEngine(true),
SourceFiles = sourceFiles,
DestinationFolder = new TaskItem(destFolder),
};
```

### Testing Exception Cases

Tasks must handle null/empty path inputs properly.

```csharp
[Fact]
public void Task_WithNullPath_Throws()
{
var task = CreateTask();

Should.Throw<ArgumentNullException>(() => task.ProcessPath(null!));
}
```

## APIs to Avoid

### Critical Errors (No Alternative)
- `Environment.Exit()`, `Environment.FailFast()` - Return false or throw instead
- `Process.GetCurrentProcess().Kill()` - Never terminate process
- `ThreadPool.SetMinThreads/MaxThreads` - Process-wide settings
- `CultureInfo.DefaultThreadCurrentCulture` (setter) - Affects all threads
- `Console.*` - Interferes with logging

### Requires TaskEnvironment
- `Environment.CurrentDirectory` → `TaskEnvironment.ProjectDirectory`
- `Environment.GetEnvironmentVariable` → `TaskEnvironment.GetEnvironmentVariable`
- `Environment.SetEnvironmentVariable` → `TaskEnvironment.SetEnvironmentVariable`
- `Path.GetFullPath` → `TaskEnvironment.GetAbsolutePath`
- `Process.Start`, `ProcessStartInfo` → `TaskEnvironment.GetProcessStartInfo`

### File APIs Need Absolute Paths
- `File.*`, `Directory.*`, `FileInfo`, `DirectoryInfo`, `FileStream`, `StreamReader`, `StreamWriter`
- All path parameters must be absolute

### Potential Issues (Review Required)
- `Assembly.Load*`, `LoadFrom`, `LoadFile` - Version conflicts
- `Activator.CreateInstance*` - Version conflicts

## Practical Notes

### CRITICAL: Trace All Path String Usage

**You MUST trace every path string variable through the entire codebase** to find all places where it flows into file system operations - including helper methods, utility classes, and third-party code that may internally use File APIs.

Steps:
1. Find every path string (e.g., `item.ItemSpec`, function parameters)
2. **Trace downstream**: Follow the variable through all method calls and assignments
3. Absolutize BEFORE any code path that touches the file system
4. Use `OriginalValue` for user-facing output (logs, errors)

```csharp
// WRONG - LockCheck internally uses File APIs with non-absolutized path
string sourceSpec = item.ItemSpec; // sourceSpec is string
string lockedMsg = LockCheck.GetLockedFileMessage(sourceSpec); // BUG! Trace the call!

// CORRECT - absolutized path passed to helper
AbsolutePath sourceFile = TaskEnvironment.GetAbsolutePath(item.ItemSpec);
string lockedMsg = LockCheck.GetLockedFileMessage(sourceFile);

// For error messages, preserve original user input
Log.LogError("...", sourceFile.OriginalValue, ...);
```

### Exception Handling in Batch Operations

**Important**: `GetAbsolutePath()` throws on null/empty inputs. In batch processing scenarios (e.g., iterating over multiple files), an unhandled exception will abort the entire batch. Tasks must catch and handle these exceptions appropriately to avoid cutting short processing of valid items:

```csharp
// WRONG - one bad path aborts entire batch
foreach (ITaskItem item in SourceFiles)
{
AbsolutePath path = TaskEnvironment.GetAbsolutePath(item.ItemSpec); // throws, batch stops!
ProcessFile(path);
}

// CORRECT - handle exceptions, continue processing valid items
bool success = true;
foreach (ITaskItem item in SourceFiles)
{
try
{
AbsolutePath path = TaskEnvironment.GetAbsolutePath(item.ItemSpec);
ProcessFile(path);
}
catch (ArgumentException ex)
{
Log.LogError($"Invalid path '{item.ItemSpec}': {ex.Message}");
success = false;
// Continue processing remaining items
}
}
return success;
```

Consider the task's error semantics: should one invalid path fail the entire task immediately, or should all items be processed with errors collected? Match the original task's behavior.

### Prefer AbsolutePath Over String

When working with paths, stay in the `AbsolutePath` world as much as possible rather than converting back and forth to `string`. This reduces unnecessary conversions and maintains type safety:

```csharp
// AVOID - unnecessary conversions
string path = TaskEnvironment.GetAbsolutePath(input).Value;
AbsolutePath again = TaskEnvironment.GetAbsolutePath(path); // redundant!

// PREFER - stay in AbsolutePath
AbsolutePath path = TaskEnvironment.GetAbsolutePath(input);
// Use path directly - it's implicitly convertible to string where needed
File.ReadAllText(path);
```

### TaskEnvironment is Not Thread-Safe

If your task spawns multiple threads internally, you must synchronize access to `TaskEnvironment`. However, each task instance gets its own environment, so no synchronization with other tasks is needed.

## Checklist

- [ ] Task is annotated with `MSBuildMultiThreadableTask` attribute and implements `IMultiThreadableTask` if TaskEnvironment APIs are required
- [ ] All environment variable access uses `TaskEnvironment` APIs
- [ ] All process spawning uses `TaskEnvironment.GetProcessStartInfo()`
- [ ] All file system APIs receive absolute paths
- [ ] All helper methods receiving path strings are traced to verify they don't internally use File APIs with non-absolutized paths
- [ ] No use of `Environment.CurrentDirectory`
- [ ] All tests set `TaskEnvironment = TaskEnvironmentHelper.CreateForTest()`
- [ ] Tests verify exception behavior for null/empty paths
- [ ] No use of forbidden APIs (Environment.Exit, etc.)

## References

- [Thread-Safe Tasks Spec](https://github.com/dotnet/msbuild/blob/main/documentation/specs/multithreading/thread-safe-tasks.md) - Full specification for multithreaded task support
- [`AbsolutePath`](https://github.com/dotnet/msbuild/blob/main/src/Framework/PathHelpers/AbsolutePath.cs) - Struct for representing absolute paths
- [`TaskEnvironment`](https://github.com/dotnet/msbuild/blob/main/src/Framework/TaskEnvironment.cs) - Thread-safe environment APIs for tasks
- [`IMultiThreadableTask`](https://github.com/dotnet/msbuild/blob/main/src/Framework/IMultiThreadableTask.cs) - Interface for multithreaded task support
14 changes: 14 additions & 0 deletions MSBuild.Minimal.slnf
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"solution": {
"path": "MSBuild.sln",
"projects": [
"src\\Build\\Microsoft.Build.csproj",
"src\\Framework\\Microsoft.Build.Framework.csproj",
"src\\MSBuild\\MSBuild.csproj",
"src\\StringTools\\StringTools.csproj",
"src\\Tasks\\Microsoft.Build.Tasks.csproj",
"src\\Utilities\\Microsoft.Build.Utilities.csproj",
"src\\MSBuild.Bootstrap\\MSBuild.Bootstrap.csproj"
]
}
}
1 change: 1 addition & 0 deletions azure-pipelines/vs-insertion-experimental.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ parameters:
displayName: 'Insertion Target Branch (select for manual insertion)'
values:
- main
- rel/d18.5
- rel/d18.4
- rel/d18.3
- rel/d18.0
Expand Down
5 changes: 4 additions & 1 deletion azure-pipelines/vs-insertion.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ parameters:
values:
- auto
- main
- rel/d18.5
- rel/d18.4
- rel/d18.3
- rel/d18.0
Expand All @@ -68,7 +69,9 @@ parameters:
variables:
# `auto` should work every time and selecting a branch in parameters is likely to fail due to incompatible versions in MSBuild and VS
- name: AutoInsertTargetBranch
${{ if eq(variables['Build.SourceBranchName'], 'vs18.4') }}:
${{ if eq(variables['Build.SourceBranchName'], 'vs18.5') }}:
value: 'rel/d18.5'
${{ elseif eq(variables['Build.SourceBranchName'], 'vs18.4') }}:
value: 'rel/d18.4'
${{ elseif eq(variables['Build.SourceBranchName'], 'vs18.3') }}:
value: 'rel/d18.3'
Expand Down
Loading